Start UI redesign (#172)
This commit is contained in:
parent
b0e5344560
commit
563c6175f7
20 changed files with 1219 additions and 525 deletions
|
@ -97,4 +97,6 @@ dependencies {
|
|||
implementation(projects.kugou)
|
||||
|
||||
coreLibraryDesugaring(libs.desugaring)
|
||||
|
||||
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
|
||||
}
|
||||
|
|
|
@ -386,7 +386,7 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
|
|||
lateinit var Instance: DatabaseInitializer
|
||||
|
||||
context(Context)
|
||||
operator fun invoke() {
|
||||
operator fun invoke() {
|
||||
if (!::Instance.isInitialized) {
|
||||
Instance = Room
|
||||
.databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db")
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components
|
||||
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.layout.layout
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun TabColumn(
|
||||
tabIndex: Int,
|
||||
onTabIndexChanged: (Int) -> Unit,
|
||||
selectedTextColor: Color,
|
||||
disabledTextColor: Color,
|
||||
textStyle: TextStyle,
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
val transition = updateTransition(targetState = tabIndex, label = null)
|
||||
|
||||
content { index, text, icon ->
|
||||
val dothAlpha by transition.animateFloat(label = "") {
|
||||
if (it == index) 1f else 0f
|
||||
}
|
||||
|
||||
val textColor by transition.animateColor(label = "") {
|
||||
if (it == index) selectedTextColor else disabledTextColor
|
||||
}
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onTabIndexChanged(index) }
|
||||
)
|
||||
.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(icon),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(selectedTextColor),
|
||||
modifier = Modifier
|
||||
.vertical()
|
||||
.graphicsLayer {
|
||||
alpha = dothAlpha
|
||||
translationX = (1f - dothAlpha) * -48.dp.toPx()
|
||||
rotationZ = -90f
|
||||
}
|
||||
.size(12.dp)
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = text,
|
||||
style = textStyle.copy(color = textColor),
|
||||
modifier = Modifier
|
||||
.vertical()
|
||||
.rotate(-90f)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Modifier.vertical() =
|
||||
layout { measurable, constraints ->
|
||||
val placeable = measurable.measure(constraints)
|
||||
layout(placeable.height, placeable.width) {
|
||||
placeable.place(
|
||||
x = -(placeable.width / 2 - placeable.height / 2),
|
||||
y = -(placeable.height / 2 - placeable.width / 2)
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,148 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedContentScope
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.Spring
|
||||
import androidx.compose.animation.core.VisibilityThreshold
|
||||
import androidx.compose.animation.core.spring
|
||||
import androidx.compose.animation.with
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
|
||||
@SuppressLint("ModifierParameter")
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun Scaffold(
|
||||
topIconButtonId: Int,
|
||||
onTopIconButtonClick: () -> Unit,
|
||||
tabIndex: Int,
|
||||
onTabChanged: (Int) -> Unit,
|
||||
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||
primaryIconButtonId: Int? = null,
|
||||
onPrimaryIconButtonClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable AnimatedVisibilityScope.(Int) -> Unit
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
) {
|
||||
VerticalBar(
|
||||
topIconButtonId = topIconButtonId,
|
||||
onTopIconButtonClick = onTopIconButtonClick,
|
||||
tabIndex = tabIndex,
|
||||
onTabChanged = onTabChanged,
|
||||
tabColumnContent = tabColumnContent,
|
||||
// primaryIconButtonId = primaryIconButtonId,
|
||||
// onPrimaryIconButtonClick = onPrimaryIconButtonClick,
|
||||
modifier = Modifier
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
)
|
||||
|
||||
AnimatedContent(
|
||||
targetState = tabIndex,
|
||||
transitionSpec = {
|
||||
val slideDirection = when (targetState > initialState) {
|
||||
true -> AnimatedContentScope.SlideDirection.Up
|
||||
false -> AnimatedContentScope.SlideDirection.Down
|
||||
}
|
||||
|
||||
val animationSpec = spring(
|
||||
dampingRatio = 0.9f,
|
||||
stiffness = Spring.StiffnessLow,
|
||||
visibilityThreshold = IntOffset.VisibilityThreshold
|
||||
)
|
||||
|
||||
slideIntoContainer(slideDirection, animationSpec) with
|
||||
slideOutOfContainer(slideDirection, animationSpec)
|
||||
},
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
primaryIconButtonId?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomEnd)
|
||||
.padding(all = 16.dp)
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(onClick = onPrimaryIconButtonClick)
|
||||
.background(colorPalette.background2)
|
||||
.size(62.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(primaryIconButtonId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ModifierParameter")
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SimpleScaffold(
|
||||
topIconButtonId: Int,
|
||||
onTopIconButtonClick: () -> Unit,
|
||||
title: String = "",
|
||||
primaryIconButtonId: Int? = null,
|
||||
primaryIconButtonEnabled: Boolean = true,
|
||||
onPrimaryIconButtonClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
VerticalTitleBar(
|
||||
topIconButtonId = topIconButtonId,
|
||||
onTopIconButtonClick = onTopIconButtonClick,
|
||||
title = title,
|
||||
primaryIconButtonId = primaryIconButtonId,
|
||||
primaryIconButtonEnabled = primaryIconButtonEnabled,
|
||||
onPrimaryIconButtonClick = onPrimaryIconButtonClick,
|
||||
modifier = Modifier
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
)
|
||||
|
||||
content()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import it.vfsfitvnm.vimusic.ui.components.TabColumn
|
||||
import it.vfsfitvnm.vimusic.ui.components.vertical
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
|
||||
@SuppressLint("ModifierParameter")
|
||||
@Composable
|
||||
fun VerticalBar(
|
||||
topIconButtonId: Int,
|
||||
onTopIconButtonClick: () -> Unit,
|
||||
tabIndex: Int,
|
||||
onTabChanged: (Int) -> Unit,
|
||||
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||
// primaryIconButtonId: Int? = null,
|
||||
// onPrimaryIconButtonClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .clip(RoundedCornerShape(16.dp))
|
||||
// .clickable(onClick = onTopIconButtonClick)
|
||||
// .background(color = colorPalette.background1)
|
||||
// .size(48.dp)
|
||||
// ) {
|
||||
Image(
|
||||
painter = painterResource(topIconButtonId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(onClick = onTopIconButtonClick)
|
||||
.padding(all = 12.dp)
|
||||
// .align(Alignment.Center)
|
||||
.size(22.dp)
|
||||
)
|
||||
// }
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(64.dp)
|
||||
.height(32.dp)
|
||||
)
|
||||
|
||||
TabColumn(
|
||||
tabIndex = tabIndex,
|
||||
onTabIndexChanged = onTabChanged,
|
||||
selectedTextColor = colorPalette.text,
|
||||
disabledTextColor = colorPalette.textDisabled,
|
||||
textStyle = typography.xs.semiBold,
|
||||
content = tabColumnContent,
|
||||
)
|
||||
|
||||
// Spacer(
|
||||
// modifier = Modifier
|
||||
// .weight(1f)
|
||||
// )
|
||||
|
||||
// primaryIconButtonId?.let {
|
||||
// Box(
|
||||
// modifier = Modifier
|
||||
// .offset(x = 8.dp)
|
||||
// .clip(RoundedCornerShape(16.dp))
|
||||
// .clickable(onClick = onPrimaryIconButtonClick)
|
||||
// .background(colorPalette.background1)
|
||||
// .size(62.dp)
|
||||
// ) {
|
||||
// Image(
|
||||
// painter = painterResource(primaryIconButtonId),
|
||||
// contentDescription = null,
|
||||
// colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
// modifier = Modifier
|
||||
// .align(Alignment.Center)
|
||||
// .size(20.dp)
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ModifierParameter")
|
||||
@Composable
|
||||
fun VerticalTitleBar(
|
||||
topIconButtonId: Int,
|
||||
onTopIconButtonClick: () -> Unit,
|
||||
title: String,
|
||||
primaryIconButtonId: Int? = null,
|
||||
primaryIconButtonEnabled: Boolean = true,
|
||||
onPrimaryIconButtonClick: () -> Unit = {},
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
modifier = modifier
|
||||
.padding(vertical = 16.dp)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(onClick = onTopIconButtonClick)
|
||||
.background(color = colorPalette.background1)
|
||||
.size(48.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(topIconButtonId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(22.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(78.dp)
|
||||
.height(32.dp)
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = title,
|
||||
style = typography.m.semiBold,
|
||||
modifier = Modifier
|
||||
.vertical()
|
||||
.rotate(-90f)
|
||||
.padding(horizontal = 16.dp)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
primaryIconButtonId?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.offset(x = 8.dp)
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable(enabled = primaryIconButtonEnabled, onClick = onPrimaryIconButtonClick)
|
||||
.background(colorPalette.background1)
|
||||
.size(62.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(primaryIconButtonId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,115 +1,26 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.zIndex
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.SearchQuery
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
||||
import it.vfsfitvnm.vimusic.ui.components.badge
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownSection
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownSectionSpacer
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownTextItem
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropdownMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem
|
||||
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||
import it.vfsfitvnm.vimusic.utils.isFirstLaunchKey
|
||||
import it.vfsfitvnm.vimusic.utils.playlistGridExpandedKey
|
||||
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
||||
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||
import it.vfsfitvnm.vimusic.ui.views.PlaylistsTab
|
||||
import it.vfsfitvnm.vimusic.ui.views.SongsTab
|
||||
import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey
|
||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
||||
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
val lazyHorizontalGridState = rememberLazyGridState()
|
||||
|
||||
var playlistSortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
|
||||
var playlistSortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
|
||||
var playlistGridExpanded by rememberPreference(playlistGridExpandedKey, false)
|
||||
|
||||
val playlistPreviews by remember(playlistSortBy, playlistSortOrder) {
|
||||
Database.playlistPreviews(playlistSortBy, playlistSortOrder)
|
||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
||||
|
||||
var songSortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
|
||||
var songSortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
|
||||
|
||||
val songCollection by remember(songSortBy, songSortOrder) {
|
||||
Database.songs(songSortBy, songSortOrder)
|
||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
||||
RouteHandler(listenToGlobalEmitter = true) {
|
||||
settingsRoute {
|
||||
|
@ -153,15 +64,7 @@ fun HomeScreen() {
|
|||
)
|
||||
}
|
||||
|
||||
albumRoute { browseId ->
|
||||
AlbumScreen(browseId = browseId ?: error("browseId cannot be null"))
|
||||
}
|
||||
|
||||
artistRoute { browseId ->
|
||||
ArtistScreen(
|
||||
browseId = browseId ?: error("browseId cannot be null")
|
||||
)
|
||||
}
|
||||
globalRoutes()
|
||||
|
||||
intentUriRoute { uri ->
|
||||
IntentUriScreen(
|
||||
|
@ -170,392 +73,38 @@ fun HomeScreen() {
|
|||
}
|
||||
|
||||
host {
|
||||
// This somehow prevents items to not be displayed sometimes...
|
||||
@Suppress("UNUSED_EXPRESSION") playlistPreviews
|
||||
@Suppress("UNUSED_EXPRESSION") songCollection
|
||||
val (tabIndex, onTabChanged) = rememberPreference(homeScreenTabIndexKey, defaultValue = 0)
|
||||
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
|
||||
val isFirstLaunch by rememberPreference(isFirstLaunchKey, true)
|
||||
|
||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||
|
||||
var isCreatingANewPlaylist by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (isCreatingANewPlaylist) {
|
||||
TextFieldDialog(
|
||||
hintText = "Enter the playlist name",
|
||||
onDismiss = {
|
||||
isCreatingANewPlaylist = false
|
||||
},
|
||||
onDone = { text ->
|
||||
query {
|
||||
Database.insert(Playlist(name = text))
|
||||
}
|
||||
Scaffold(
|
||||
topIconButtonId = R.drawable.equalizer,
|
||||
onTopIconButtonClick = { settingsRoute() },
|
||||
tabIndex = tabIndex,
|
||||
onTabChanged = onTabChanged,
|
||||
tabColumnContent = { Item ->
|
||||
Item(0, "Songs", R.drawable.musical_notes)
|
||||
Item(1, "Playlists", R.drawable.playlist)
|
||||
Item(2, "Artists", R.drawable.person)
|
||||
Item(3, "Albums", R.drawable.disc)
|
||||
},
|
||||
primaryIconButtonId = R.drawable.search,
|
||||
onPrimaryIconButtonClick = { searchRoute("") }
|
||||
) { currentTabIndex ->
|
||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||
when (currentTabIndex) {
|
||||
0 -> SongsTab()
|
||||
1 -> PlaylistsTab(
|
||||
onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) },
|
||||
onPlaylistClicked = { localPlaylistRoute(it.id) }
|
||||
)
|
||||
// 2 -> ArtistsTab(
|
||||
// lazyListState = lazyListStates[currentTabIndex],
|
||||
// onArtistClicked = { artistRoute(it.id) }
|
||||
// )
|
||||
// 3 -> AlbumsTab(
|
||||
// lazyListState = lazyListStates[currentTabIndex],
|
||||
// onAlbumClicked = { albumRoute(it.id) }
|
||||
// )
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
state = lazyListState,
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item("topAppBar") {
|
||||
TopAppBar(
|
||||
modifier = Modifier
|
||||
.height(52.dp)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.equalizer),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable { settingsRoute() }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.badge(color = colorPalette.red, isDisplayed = isFirstLaunch)
|
||||
.size(24.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.search),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable { searchRoute("") }
|
||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||
.size(24.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
item("playlistsHeader") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.zIndex(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(top = 16.dp)
|
||||
) {
|
||||
BasicText(
|
||||
text = "Your playlists",
|
||||
style = typography.m.semiBold,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.add),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable { isCreatingANewPlaylist = true }
|
||||
.padding(all = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
Box {
|
||||
var isSortMenuDisplayed by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.sort),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable { isSortMenuDisplayed = true }
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
isDisplayed = isSortMenuDisplayed,
|
||||
onDismissRequest = { isSortMenuDisplayed = false }
|
||||
) {
|
||||
DropDownSection {
|
||||
DropDownTextItem(
|
||||
text = "NAME",
|
||||
isSelected = playlistSortBy == PlaylistSortBy.Name,
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
playlistSortBy = PlaylistSortBy.Name
|
||||
}
|
||||
)
|
||||
|
||||
DropDownTextItem(
|
||||
text = "DATE ADDED",
|
||||
isSelected = playlistSortBy == PlaylistSortBy.DateAdded,
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
playlistSortBy = PlaylistSortBy.DateAdded
|
||||
}
|
||||
)
|
||||
|
||||
DropDownTextItem(
|
||||
text = "SONG COUNT",
|
||||
isSelected = playlistSortBy == PlaylistSortBy.SongCount,
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
playlistSortBy = PlaylistSortBy.SongCount
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
DropDownSectionSpacer()
|
||||
|
||||
DropDownSection {
|
||||
DropDownTextItem(
|
||||
text = when (playlistSortOrder) {
|
||||
SortOrder.Ascending -> "ASCENDING"
|
||||
SortOrder.Descending -> "DESCENDING"
|
||||
},
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
playlistSortOrder = !playlistSortOrder
|
||||
}
|
||||
)
|
||||
}
|
||||
DropDownSectionSpacer()
|
||||
|
||||
DropDownSection {
|
||||
DropDownTextItem(
|
||||
text = when (playlistGridExpanded) {
|
||||
true -> "COLLAPSE"
|
||||
false -> "EXPAND"
|
||||
},
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
playlistGridExpanded = !playlistGridExpanded
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item("playlists") {
|
||||
LazyHorizontalGrid(
|
||||
state = lazyHorizontalGridState,
|
||||
rows = GridCells.Fixed(if (playlistGridExpanded) 3 else 1),
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
modifier = Modifier
|
||||
.animateContentSize()
|
||||
.fillMaxWidth()
|
||||
.height(124.dp * (if (playlistGridExpanded) 3 else 1))
|
||||
) {
|
||||
item(key = "favorites") {
|
||||
BuiltInPlaylistItem(
|
||||
icon = R.drawable.heart,
|
||||
colorTint = colorPalette.red,
|
||||
name = "Favorites",
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(all = 8.dp)
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { builtInPlaylistRoute(BuiltInPlaylist.Favorites) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "offline") {
|
||||
BuiltInPlaylistItem(
|
||||
icon = R.drawable.airplane,
|
||||
colorTint = colorPalette.blue,
|
||||
name = "Offline",
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(all = 8.dp)
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { builtInPlaylistRoute(BuiltInPlaylist.Offline) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = playlistPreviews,
|
||||
key = { it.playlist.id },
|
||||
contentType = { it }
|
||||
) { playlistPreview ->
|
||||
PlaylistPreviewItem(
|
||||
playlistPreview = playlistPreview,
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
.padding(all = 8.dp)
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { localPlaylistRoute(playlistPreview.playlist.id) }
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item("songs") {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.zIndex(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
.padding(top = 32.dp)
|
||||
) {
|
||||
BasicText(
|
||||
text = "Songs",
|
||||
style = typography.m.semiBold,
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.shuffle),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable(enabled = songCollection.isNotEmpty()) {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songCollection
|
||||
.shuffled()
|
||||
.map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
Box {
|
||||
var isSortMenuDisplayed by remember {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.sort),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
isSortMenuDisplayed = true
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
||||
.size(20.dp)
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
isDisplayed = isSortMenuDisplayed,
|
||||
onDismissRequest = {
|
||||
isSortMenuDisplayed = false
|
||||
}
|
||||
) {
|
||||
DropDownSection {
|
||||
DropDownTextItem(
|
||||
text = "PLAY TIME",
|
||||
isSelected = songSortBy == SongSortBy.PlayTime,
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
songSortBy = SongSortBy.PlayTime
|
||||
}
|
||||
)
|
||||
|
||||
DropDownTextItem(
|
||||
text = "TITLE",
|
||||
isSelected = songSortBy == SongSortBy.Title,
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
songSortBy = SongSortBy.Title
|
||||
}
|
||||
)
|
||||
|
||||
DropDownTextItem(
|
||||
text = "DATE ADDED",
|
||||
isSelected = songSortBy == SongSortBy.DateAdded,
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
songSortBy = SongSortBy.DateAdded
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
DropDownSectionSpacer()
|
||||
|
||||
DropDownSection {
|
||||
DropDownTextItem(
|
||||
text = when (songSortOrder) {
|
||||
SortOrder.Ascending -> "ASCENDING"
|
||||
SortOrder.Descending -> "DESCENDING"
|
||||
},
|
||||
onClick = {
|
||||
isSortMenuDisplayed = false
|
||||
songSortOrder = !songSortOrder
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = songCollection,
|
||||
key = { _, song -> song.id },
|
||||
contentType = { _, song -> song }
|
||||
) { index, song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
songCollection.map(DetailedSong::asMediaItem),
|
||||
index
|
||||
)
|
||||
},
|
||||
menuContent = {
|
||||
InHistoryMediaItemMenu(song = song)
|
||||
},
|
||||
onThumbnailContent = {
|
||||
AnimatedVisibility(
|
||||
visible = songSortBy == SongSortBy.PlayTime,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
) {
|
||||
BasicText(
|
||||
text = song.formattedTotalPlayTime,
|
||||
style = typography.xxs.semiBold.center.color(Color.White),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.75f)
|
||||
)
|
||||
),
|
||||
shape = ThumbnailRoundness.shape
|
||||
)
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@ data class Typography(
|
|||
val s: TextStyle,
|
||||
val m: TextStyle,
|
||||
val l: TextStyle,
|
||||
val xxl: TextStyle,
|
||||
)
|
||||
|
||||
fun typographyOf(color: Color): Typography {
|
||||
|
@ -54,5 +55,6 @@ fun typographyOf(color: Color): Typography {
|
|||
s = textStyle.copy(fontSize = 16.sp),
|
||||
m = textStyle.copy(fontSize = 18.sp),
|
||||
l = textStyle.copy(fontSize = 20.sp),
|
||||
xxl = textStyle.copy(fontSize = 32.sp),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,11 +3,13 @@ package it.vfsfitvnm.vimusic.ui.views
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.requiredSize
|
||||
import androidx.compose.foundation.layout.requiredWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
|
@ -17,7 +19,6 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
|
@ -31,7 +32,7 @@ import it.vfsfitvnm.vimusic.Database
|
|||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
|
@ -61,7 +62,6 @@ fun PlaylistPreviewItem(
|
|||
|
||||
PlaylistItem(
|
||||
name = playlistPreview.playlist.name,
|
||||
textColor = Color.White,
|
||||
thumbnailSize = thumbnailSize,
|
||||
imageContent = {
|
||||
if (thumbnails.toSet().size == 1) {
|
||||
|
@ -112,7 +112,6 @@ fun BuiltInPlaylistItem(
|
|||
PlaylistItem(
|
||||
name = name,
|
||||
thumbnailSize = thumbnailSize,
|
||||
withGradient = false,
|
||||
imageContent = {
|
||||
Image(
|
||||
painter = painterResource(icon),
|
||||
|
@ -131,48 +130,31 @@ fun BuiltInPlaylistItem(
|
|||
fun PlaylistItem(
|
||||
name: String,
|
||||
modifier: Modifier = Modifier,
|
||||
textColor: Color? = null,
|
||||
thumbnailSize: Dp = Dimensions.thumbnails.song,
|
||||
withGradient: Boolean = true,
|
||||
imageContent: @Composable BoxScope.() -> Unit
|
||||
) {
|
||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Box(
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = modifier
|
||||
.clip(thumbnailShape)
|
||||
.background(colorPalette.background1)
|
||||
.size(thumbnailSize * 2)
|
||||
.requiredWidth(thumbnailSize * 2)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(thumbnailSize * 2),
|
||||
.clip(thumbnailShape)
|
||||
.background(colorPalette.background1)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.requiredSize(thumbnailSize * 2),
|
||||
content = imageContent
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = name,
|
||||
style = typography.xxs.semiBold.color(textColor ?: colorPalette.text),
|
||||
style = typography.xxs.semiBold.center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.align(Alignment.BottomStart)
|
||||
.run {
|
||||
if (withGradient) {
|
||||
background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.75f)
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,297 @@
|
|||
package it.vfsfitvnm.vimusic.ui.views
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||
import it.vfsfitvnm.vimusic.models.Playlist
|
||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||
import it.vfsfitvnm.vimusic.utils.medium
|
||||
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
||||
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
||||
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
||||
import it.vfsfitvnm.vimusic.utils.preferences
|
||||
import it.vfsfitvnm.vimusic.utils.putEnum
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlaylistsTabViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var items by mutableStateOf(emptyList<PlaylistPreview>())
|
||||
private set
|
||||
|
||||
var sortBy by mutableStatePreferenceOf(
|
||||
preferences.getEnum(
|
||||
playlistSortByKey,
|
||||
PlaylistSortBy.DateAdded
|
||||
)
|
||||
) {
|
||||
preferences.edit { putEnum(playlistSortByKey, it) }
|
||||
collectItems(sortBy = it)
|
||||
}
|
||||
|
||||
var sortOrder by mutableStatePreferenceOf(
|
||||
preferences.getEnum(
|
||||
playlistSortOrderKey,
|
||||
SortOrder.Ascending
|
||||
)
|
||||
) {
|
||||
preferences.edit { putEnum(playlistSortOrderKey, it) }
|
||||
collectItems(sortOrder = it)
|
||||
}
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private val preferences: SharedPreferences
|
||||
get() = getApplication<Application>().preferences
|
||||
|
||||
init {
|
||||
collectItems()
|
||||
}
|
||||
|
||||
private fun collectItems(
|
||||
sortBy: PlaylistSortBy = this.sortBy,
|
||||
sortOrder: SortOrder = this.sortOrder
|
||||
) {
|
||||
job?.cancel()
|
||||
job = viewModelScope.launch {
|
||||
Database.playlistPreviews(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
||||
items = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun PlaylistsTab(
|
||||
viewModel: PlaylistsTabViewModel = viewModel(),
|
||||
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
|
||||
onPlaylistClicked: (Playlist) -> Unit,
|
||||
) {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
|
||||
var isCreatingANewPlaylist by rememberSaveable {
|
||||
mutableStateOf(false)
|
||||
}
|
||||
|
||||
if (isCreatingANewPlaylist) {
|
||||
TextFieldDialog(
|
||||
hintText = "Enter the playlist name",
|
||||
onDismiss = {
|
||||
isCreatingANewPlaylist = false
|
||||
},
|
||||
onDone = { text ->
|
||||
query {
|
||||
Database.insert(Playlist(name = text))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
val sortOrderIconRotation by animateFloatAsState(
|
||||
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||
)
|
||||
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = Dimensions.itemsVerticalPadding * 2,
|
||||
alignment = Alignment.CenterHorizontally
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.background(colorPalette.background0)
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0,
|
||||
span = { GridItemSpan(maxLineSpan) }
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(128.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
BasicText(
|
||||
text = "Playlists",
|
||||
style = typography.xxl.medium
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
@Composable
|
||||
fun Item(
|
||||
@DrawableRes iconId: Int,
|
||||
sortBy: PlaylistSortBy
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(iconId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
||||
modifier = Modifier
|
||||
.clickable { viewModel.sortBy = sortBy }
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
BasicText(
|
||||
text = "New playlist",
|
||||
style = typography.xxs.medium,
|
||||
modifier = Modifier
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
.clickable { isCreatingANewPlaylist = true }
|
||||
.background(colorPalette.background2)
|
||||
.padding(all = 8.dp)
|
||||
.padding(horizontal = 8.dp)
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Item(
|
||||
iconId = R.drawable.medical,
|
||||
sortBy = PlaylistSortBy.SongCount
|
||||
)
|
||||
|
||||
Item(
|
||||
iconId = R.drawable.text,
|
||||
sortBy = PlaylistSortBy.Name
|
||||
)
|
||||
|
||||
Item(
|
||||
iconId = R.drawable.calendar,
|
||||
sortBy = PlaylistSortBy.DateAdded
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.arrow_up),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item(key = "favorites") {
|
||||
BuiltInPlaylistItem(
|
||||
icon = R.drawable.heart,
|
||||
colorTint = colorPalette.red,
|
||||
name = "Favorites",
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
item(key = "offline") {
|
||||
BuiltInPlaylistItem(
|
||||
icon = R.drawable.airplane,
|
||||
colorTint = colorPalette.blue,
|
||||
name = "Offline",
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) }
|
||||
)
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
|
||||
items(
|
||||
items = viewModel.items,
|
||||
key = { it.playlist.id }
|
||||
) { playlistPreview ->
|
||||
PlaylistPreviewItem(
|
||||
playlistPreview = playlistPreview,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { onPlaylistClicked(playlistPreview.playlist) }
|
||||
)
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
261
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt
Normal file
261
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt
Normal file
|
@ -0,0 +1,261 @@
|
|||
package it.vfsfitvnm.vimusic.ui.views
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||
import it.vfsfitvnm.vimusic.utils.medium
|
||||
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
||||
import it.vfsfitvnm.vimusic.utils.preferences
|
||||
import it.vfsfitvnm.vimusic.utils.putEnum
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
||||
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SongsTabViewModel(application: Application) : AndroidViewModel(application) {
|
||||
var items by mutableStateOf(emptyList<DetailedSong>())
|
||||
private set
|
||||
|
||||
var sortBy by mutableStatePreferenceOf(preferences.getEnum(songSortByKey, SongSortBy.DateAdded)) {
|
||||
preferences.edit { putEnum(songSortByKey, it) }
|
||||
collectItems(sortBy = it)
|
||||
}
|
||||
|
||||
var sortOrder by mutableStatePreferenceOf(preferences.getEnum(songSortOrderKey, SortOrder.Ascending)) {
|
||||
preferences.edit { putEnum(songSortOrderKey, it) }
|
||||
collectItems(sortOrder = it)
|
||||
}
|
||||
|
||||
private var job: Job? = null
|
||||
|
||||
private val preferences: SharedPreferences
|
||||
get() = getApplication<Application>().preferences
|
||||
|
||||
init {
|
||||
collectItems()
|
||||
}
|
||||
|
||||
private fun collectItems(sortBy: SongSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
|
||||
job?.cancel()
|
||||
job = viewModelScope.launch {
|
||||
Database.songs(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
||||
items = it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SongsTab(
|
||||
viewModel: SongsTabViewModel = viewModel()
|
||||
) {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
|
||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||
|
||||
val sortOrderIconRotation by animateFloatAsState(
|
||||
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.Bottom,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 16.dp)
|
||||
.height(128.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
BasicText(
|
||||
text = "Songs",
|
||||
style = typography.xxl.medium
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
@Composable
|
||||
fun Item(
|
||||
@DrawableRes iconId: Int,
|
||||
sortBy: SongSortBy
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(iconId),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
||||
modifier = Modifier
|
||||
.clickable { viewModel.sortBy = sortBy }
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Item(
|
||||
iconId = R.drawable.trending,
|
||||
sortBy = SongSortBy.PlayTime
|
||||
)
|
||||
|
||||
Item(
|
||||
iconId = R.drawable.text,
|
||||
sortBy = SongSortBy.Title
|
||||
)
|
||||
|
||||
Item(
|
||||
iconId = R.drawable.calendar,
|
||||
sortBy = SongSortBy.DateAdded
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.width(2.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.arrow_up),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = viewModel.items,
|
||||
key = { _, song -> song.id }
|
||||
) { index, song ->
|
||||
SongItem(
|
||||
song = song,
|
||||
thumbnailSize = thumbnailSize,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
viewModel.items.map(DetailedSong::asMediaItem),
|
||||
index
|
||||
)
|
||||
},
|
||||
menuContent = {
|
||||
InHistoryMediaItemMenu(song = song)
|
||||
},
|
||||
onThumbnailContent = {
|
||||
AnimatedVisibility(
|
||||
visible = viewModel.sortBy == SongSortBy.PlayTime,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.BottomCenter)
|
||||
) {
|
||||
BasicText(
|
||||
text = song.formattedTotalPlayTime,
|
||||
style = typography.xxs.semiBold.center.color(
|
||||
Color.White
|
||||
),
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
brush = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Transparent,
|
||||
Color.Black.copy(alpha = 0.75f)
|
||||
)
|
||||
),
|
||||
shape = ThumbnailRoundness.shape
|
||||
)
|
||||
.padding(
|
||||
horizontal = 8.dp,
|
||||
vertical = 4.dp
|
||||
)
|
||||
)
|
||||
}
|
||||
},
|
||||
modifier = Modifier
|
||||
.animateItemPlacement()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -21,7 +21,6 @@ const val songSortOrderKey = "songSortOrder"
|
|||
const val songSortByKey = "songSortBy"
|
||||
const val playlistSortOrderKey = "playlistSortOrder"
|
||||
const val playlistSortByKey = "playlistSortBy"
|
||||
const val playlistGridExpandedKey = "playlistGridExpanded"
|
||||
const val searchFilterKey = "searchFilter"
|
||||
const val repeatModeKey = "repeatMode"
|
||||
const val skipSilenceKey = "skipSilence"
|
||||
|
@ -29,6 +28,7 @@ const val volumeNormalizationKey = "volumeNormalization"
|
|||
const val persistentQueueKey = "persistentQueue"
|
||||
const val isShowingSynchronizedLyricsKey = "isShowingSynchronizedLyrics"
|
||||
const val isShowingThumbnailInLockscreenKey = "isShowingThumbnailInLockscreen"
|
||||
const val homeScreenTabIndexKey = "homeScreenTabIndex"
|
||||
|
||||
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
||||
key: String,
|
||||
|
@ -61,6 +61,16 @@ fun rememberPreference(key: String, defaultValue: Boolean): MutableState<Boolean
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberPreference(key: String, defaultValue: Int): MutableState<Int> {
|
||||
val context = LocalContext.current
|
||||
return remember {
|
||||
mutableStatePreferenceOf(context.preferences.getInt(key, defaultValue)) {
|
||||
context.preferences.edit { putInt(key, it) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberPreference(key: String, defaultValue: String): MutableState<String> {
|
||||
val context = LocalContext.current
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
package it.vfsfitvnm.vimusic.utils
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.saveable.listSaver
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
|
||||
@Composable
|
||||
fun rememberLazyListStates(count: Int): List<LazyListState> {
|
||||
return rememberSaveable(
|
||||
saver = listSaver(
|
||||
save = { states: List<LazyListState> ->
|
||||
List(states.size * 2) {
|
||||
when (it % 2) {
|
||||
0 -> states[it / 2].firstVisibleItemIndex
|
||||
1 -> states[it / 2].firstVisibleItemScrollOffset
|
||||
else -> error("unreachable")
|
||||
}
|
||||
}
|
||||
},
|
||||
restore = { states ->
|
||||
List(states.size / 2) {
|
||||
LazyListState(
|
||||
firstVisibleItemIndex = states[it * 2],
|
||||
firstVisibleItemScrollOffset = states[it * 2 + 1]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
List(count) { LazyListState(0, 0) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberLazyGridStates(count: Int): List<LazyGridState> {
|
||||
return rememberSaveable(
|
||||
saver = listSaver(
|
||||
save = { states: List<LazyGridState> ->
|
||||
List(states.size * 2) {
|
||||
when (it % 2) {
|
||||
0 -> states[it / 2].firstVisibleItemIndex
|
||||
1 -> states[it / 2].firstVisibleItemScrollOffset
|
||||
else -> error("unreachable")
|
||||
}
|
||||
}
|
||||
},
|
||||
restore = { states ->
|
||||
List(states.size / 2) {
|
||||
LazyGridState(
|
||||
firstVisibleItemIndex = states[it * 2],
|
||||
firstVisibleItemScrollOffset = states[it * 2 + 1]
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
) {
|
||||
List(count) { LazyGridState(0, 0) }
|
||||
}
|
||||
}
|
20
app/src/main/res/drawable/arrow_down.xml
Normal file
20
app/src/main/res/drawable/arrow_down.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M112,268l144,144l144,-144"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="48"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M256,392L256,100"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="48"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
20
app/src/main/res/drawable/arrow_up.xml
Normal file
20
app/src/main/res/drawable/arrow_up.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M112,244l144,-144l144,144"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="48"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M256,120L256,412"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="48"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
12
app/src/main/res/drawable/calendar.xml
Normal file
12
app/src/main/res/drawable/calendar.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M480,128a64,64 0,0 0,-64 -64H400V48.45c0,-8.61 -6.62,-16 -15.23,-16.43A16,16 0,0 0,368 48V64H144V48.45c0,-8.61 -6.62,-16 -15.23,-16.43A16,16 0,0 0,112 48V64H96a64,64 0,0 0,-64 64v12a4,4 0,0 0,4 4H476a4,4 0,0 0,4 -4Z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M32,416a64,64 0,0 0,64 64L416,480a64,64 0,0 0,64 -64L480,179a3,3 0,0 0,-3 -3L35,176a3,3 0,0 0,-3 3ZM376,208a24,24 0,1 1,-24 24A24,24 0,0 1,376 208ZM376,288a24,24 0,1 1,-24 24A24,24 0,0 1,376 288ZM296,208a24,24 0,1 1,-24 24A24,24 0,0 1,296 208ZM296,288a24,24 0,1 1,-24 24A24,24 0,0 1,296 288ZM296,368a24,24 0,1 1,-24 24A24,24 0,0 1,296 368ZM216,288a24,24 0,1 1,-24 24A24,24 0,0 1,216 288ZM216,368a24,24 0,1 1,-24 24A24,24 0,0 1,216 368ZM136,288a24,24 0,1 1,-24 24A24,24 0,0 1,136 288ZM136,368a24,24 0,1 1,-24 24A24,24 0,0 1,136 368Z"/>
|
||||
</vector>
|
|
@ -1,6 +1,6 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="512dp"
|
||||
android:height="512dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
|
|
9
app/src/main/res/drawable/medical.xml
Normal file
9
app/src/main/res/drawable/medical.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M272,464L240,464a32,32 0,0 1,-32 -32l0.05,-85.82a4,4 0,0 0,-6 -3.47l-74.34,43.06a31.48,31.48 0,0 1,-43 -11.52L68.21,345.61l-0.06,-0.1a31.65,31.65 0,0 1,11.56 -42.8l74.61,-43.25a4,4 0,0 0,0 -6.92L79.78,209.33a31.41,31.41 0,0 1,-11.55 -43l16.44,-28.55a31.48,31.48 0,0 1,19.27 -14.74,31.14 31.14,0 0,1 23.8,3.2l74.31,43a4,4 0,0 0,6 -3.47L208,80a32,32 0,0 1,32 -32h32a32,32 0,0 1,32 32L304,165.72a4,4 0,0 0,6 3.47l74.34,-43.06a31.51,31.51 0,0 1,43 11.52l16.49,28.64 0.06,0.09a31.52,31.52 0,0 1,-11.64 42.86l-74.53,43.2a4,4 0,0 0,0 6.92l74.53,43.2a31.42,31.42 0,0 1,11.56 43l-16.44,28.55a31.48,31.48 0,0 1,-19.27 14.74,31.14 31.14,0 0,1 -23.8,-3.2l-74.31,-43a4,4 0,0 0,-6 3.46L304,432A32,32 0,0 1,272 464ZM178.44,266.52h0ZM178.44,245.52h0ZM333.54,245.44ZM333.54,245.44h0Z"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/musical_notes.xml
Normal file
9
app/src/main/res/drawable/musical_notes.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M421.84,37.37a25.86,25.86 0,0 0,-22.6 -4.46L199.92,86.49A32.3,32.3 0,0 0,176 118v226c0,6.74 -4.36,12.56 -11.11,14.83l-0.12,0.05 -52,18C92.88,383.53 80,402 80,423.91a55.54,55.54 0,0 0,23.23 45.63A54.78,54.78 0,0 0,135.34 480a55.82,55.82 0,0 0,17.75 -2.93l0.38,-0.13L175.31,469A47.84,47.84 0,0 0,208 423.91v-212c0,-7.29 4.77,-13.21 12.16,-15.07l0.21,-0.06L395,150.14a4,4 0,0 1,5 3.86V295.93c0,6.75 -4.25,12.38 -11.11,14.68l-0.25,0.09 -50.89,18.11A49.09,49.09 0,0 0,304 375.92a55.67,55.67 0,0 0,23.23 45.8,54.63 54.63,0 0,0 49.88,7.35l0.36,-0.12L399.31,421A47.83,47.83 0,0 0,432 375.92V58A25.74,25.74 0,0 0,421.84 37.37Z"/>
|
||||
</vector>
|
20
app/src/main/res/drawable/trending.xml
Normal file
20
app/src/main/res/drawable/trending.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="512"
|
||||
android:viewportHeight="512">
|
||||
<path
|
||||
android:pathData="M352,144l112,0l0,112"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="32"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M48,368 L169.37,246.63a32,32 0,0 1,45.26 0l50.74,50.74a32,32 0,0 0,45.26 0L448,160"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="32"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -6,6 +6,7 @@ dependencyResolutionManagement {
|
|||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven { setUrl("https://jitpack.io") }
|
||||
}
|
||||
|
||||
versionCatalogs {
|
||||
|
|
Loading…
Reference in a new issue