From 563c6175f70565c8b33253889ce33d9019825776 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Thu, 22 Sep 2022 19:08:01 +0200 Subject: [PATCH 001/100] Start UI redesign (#172) --- app/build.gradle.kts | 2 + .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 2 +- .../vimusic/ui/components/TabColumn.kt | 105 ++++ .../vimusic/ui/components/themed/Scaffold.kt | 148 +++++ .../ui/components/themed/VerticalBar.kt | 186 +++++++ .../vimusic/ui/screens/HomeScreen.kt | 525 ++---------------- .../vimusic/ui/styling/Typography.kt | 2 + .../vimusic/ui/views/PlaylistPreviewItem.kt | 48 +- .../vimusic/ui/views/PlaylistsTab.kt | 297 ++++++++++ .../it/vfsfitvnm/vimusic/ui/views/SongsTab.kt | 261 +++++++++ .../it/vfsfitvnm/vimusic/utils/Preferences.kt | 12 +- .../vimusic/utils/RememberLazyListStates.kt | 61 ++ app/src/main/res/drawable/arrow_down.xml | 20 + app/src/main/res/drawable/arrow_up.xml | 20 + app/src/main/res/drawable/calendar.xml | 12 + app/src/main/res/drawable/chevron_down.xml | 4 +- app/src/main/res/drawable/medical.xml | 9 + app/src/main/res/drawable/musical_notes.xml | 9 + app/src/main/res/drawable/trending.xml | 20 + settings.gradle.kts | 1 + 20 files changed, 1219 insertions(+), 525 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt create mode 100644 app/src/main/res/drawable/arrow_down.xml create mode 100644 app/src/main/res/drawable/arrow_up.xml create mode 100644 app/src/main/res/drawable/calendar.xml create mode 100644 app/src/main/res/drawable/medical.xml create mode 100644 app/src/main/res/drawable/musical_notes.xml create mode 100644 app/src/main/res/drawable/trending.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 5687011..58bf36d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -97,4 +97,6 @@ dependencies { implementation(projects.kugou) coreLibraryDesugaring(libs.desugaring) + + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1") } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 6d12504..2f07d66 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -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") diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt new file mode 100644 index 0000000..60170e8 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt @@ -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) + ) + } + } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt new file mode 100644 index 0000000..ed4a95a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -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() + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt new file mode 100644 index 0000000..d37a256 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt @@ -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) + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index 43c9bff..89a3a6e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -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() - ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Typography.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Typography.kt index 6b57ecb..d17b065 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Typography.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Typography.kt @@ -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), ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt index 13785d5..ddc99c9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt @@ -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 ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt new file mode 100644 index 0000000..095c7d1 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt @@ -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()) + 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().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() + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt new file mode 100644 index 0000000..ccfe8c1 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt @@ -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()) + 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().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() + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt index de2c103..945bc3f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt @@ -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 > SharedPreferences.getEnum( key: String, @@ -61,6 +61,16 @@ fun rememberPreference(key: String, defaultValue: Boolean): MutableState { + 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 { val context = LocalContext.current diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt new file mode 100644 index 0000000..c914a7f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt @@ -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 { + return rememberSaveable( + saver = listSaver( + save = { states: List -> + 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 { + return rememberSaveable( + saver = listSaver( + save = { states: List -> + 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) } + } +} diff --git a/app/src/main/res/drawable/arrow_down.xml b/app/src/main/res/drawable/arrow_down.xml new file mode 100644 index 0000000..9d45330 --- /dev/null +++ b/app/src/main/res/drawable/arrow_down.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/arrow_up.xml b/app/src/main/res/drawable/arrow_up.xml new file mode 100644 index 0000000..9de10de --- /dev/null +++ b/app/src/main/res/drawable/arrow_up.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/calendar.xml b/app/src/main/res/drawable/calendar.xml new file mode 100644 index 0000000..3eb788b --- /dev/null +++ b/app/src/main/res/drawable/calendar.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/chevron_down.xml b/app/src/main/res/drawable/chevron_down.xml index 4504c11..41cdd90 100644 --- a/app/src/main/res/drawable/chevron_down.xml +++ b/app/src/main/res/drawable/chevron_down.xml @@ -1,6 +1,6 @@ + + diff --git a/app/src/main/res/drawable/musical_notes.xml b/app/src/main/res/drawable/musical_notes.xml new file mode 100644 index 0000000..ac341fb --- /dev/null +++ b/app/src/main/res/drawable/musical_notes.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/trending.xml b/app/src/main/res/drawable/trending.xml new file mode 100644 index 0000000..ed34e01 --- /dev/null +++ b/app/src/main/res/drawable/trending.xml @@ -0,0 +1,20 @@ + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index 6c9737d..176589f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,6 +6,7 @@ dependencyResolutionManagement { repositories { google() mavenCentral() + maven { setUrl("https://jitpack.io") } } versionCatalogs { From 6a3b41ca28d81353f4a99003d7f5dda9ad684915 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Fri, 23 Sep 2022 15:35:31 +0200 Subject: [PATCH 002/100] Redesign SettingsScreen (#172) --- app/build.gradle.kts | 3 +- .../ui/components/themed/DropDownSection.kt | 106 ------- .../ui/components/themed/DropdownMenu.kt | 205 ------------- .../vimusic/ui/components/themed/Header.kt | 68 +++++ .../vimusic/ui/screens/SettingsScreen.kt | 274 +++--------------- .../ui/screens/settings/AboutScreen.kt | 100 ------- .../vimusic/ui/screens/settings/AboutTab.kt | 73 +++++ .../settings/AppearanceSettingsScreen.kt | 126 -------- .../screens/settings/AppearanceSettingsTab.kt | 91 ++++++ .../settings/BackupAndRestoreScreen.kt | 173 ----------- .../screens/settings/CacheSettingsScreen.kt | 144 --------- .../ui/screens/settings/CacheSettingsTab.kt | 113 ++++++++ .../screens/settings/OtherSettingsScreen.kt | 183 ------------ .../ui/screens/settings/OtherSettingsTab.kt | 241 +++++++++++++++ .../screens/settings/PlayerSettingsScreen.kt | 148 ---------- .../ui/screens/settings/PlayerSettingsTab.kt | 116 ++++++++ .../vimusic/ui/views/PlaylistsTab.kt | 134 ++++----- .../it/vfsfitvnm/vimusic/ui/views/SongsTab.kt | 105 +++---- settings.gradle.kts | 1 + 19 files changed, 847 insertions(+), 1557 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58bf36d..165754e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -84,6 +84,7 @@ dependencies { implementation(libs.compose.ripple) implementation(libs.compose.shimmer) implementation(libs.compose.coil) + implementation(libs.compose.viewmodel) implementation(libs.palette) @@ -97,6 +98,4 @@ dependencies { implementation(projects.kugou) coreLibraryDesugaring(libs.desugaring) - - implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1") } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt deleted file mode 100644 index 6ce771e..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt +++ /dev/null @@ -1,106 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components.themed - -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn -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.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.medium - -@Composable -fun DropDownSection(content: @Composable ColumnScope.() -> Unit) { - val (colorPalette) = LocalAppearance.current - Column( - modifier = Modifier - .shadow( - elevation = 2.dp, - shape = RoundedCornerShape(16.dp) - ) - .background(colorPalette.background1) - .width(IntrinsicSize.Max), - content = content - ) -} - -@Composable -fun DropDownSectionSpacer() { - Spacer( - modifier = Modifier - .height(4.dp) - ) -} - -@Composable -fun DropDownTextItem( - text: String, - isSelected: Boolean, - onClick: () -> Unit -) { - val (colorPalette) = LocalAppearance.current - - DropDownTextItem( - text = text, - textColor = if (isSelected) { - colorPalette.onAccent - } else { - colorPalette.textSecondary - }, - backgroundColor = if (isSelected) { - colorPalette.accent - } else { - colorPalette.background1 - }, - onClick = onClick - ) -} - -@Composable -fun DropDownTextItem( - text: String, - backgroundColor: Color? = null, - textColor: Color? = null, - onClick: () -> Unit -) { - val (colorPalette, typography) = LocalAppearance.current - - BasicText( - text = text, - style = typography.xxs.medium.copy( - color = textColor ?: colorPalette.text, - letterSpacing = 1.sp - ), - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onClick - ) - .background(backgroundColor ?: colorPalette.background1) - .fillMaxWidth() - .widthIn(min = 124.dp, max = 248.dp) - .padding( - horizontal = 16.dp, - vertical = 8.dp - ) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt deleted file mode 100644 index bd86c6d..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt +++ /dev/null @@ -1,205 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components.themed - -import androidx.compose.animation.core.LinearOutSlowInEasing -import androidx.compose.animation.core.MutableTransitionState -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.tween -import androidx.compose.animation.core.updateTransition -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.TransformOrigin -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.IntOffset -import androidx.compose.ui.unit.IntRect -import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupPositionProvider -import androidx.compose.ui.window.PopupProperties -import kotlin.math.max -import kotlin.math.min - -@Composable -fun DropdownMenu( - isDisplayed: Boolean, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, - offset: DpOffset = DpOffset(0.dp, 0.dp), - properties: PopupProperties = PopupProperties(focusable = true), - content: @Composable ColumnScope.() -> Unit -) { - val expandedStates = remember { - MutableTransitionState(false) - }.apply { targetState = isDisplayed } - - if (expandedStates.currentState || expandedStates.targetState) { - val density = LocalDensity.current - - var transformOrigin by remember { - mutableStateOf(TransformOrigin.Center) - } - - val popupPositionProvider = - DropdownMenuPositionProvider(offset, density) { parentBounds, menuBounds -> - transformOrigin = calculateTransformOrigin(parentBounds, menuBounds) - } - - Popup( - onDismissRequest = onDismissRequest, - popupPositionProvider = popupPositionProvider, - properties = properties - ) { - DropdownMenuContent( - expandedStates = expandedStates, - transformOrigin = transformOrigin, - modifier = modifier, - content = content - ) - } - } -} - -@Composable -internal fun DropdownMenuContent( - expandedStates: MutableTransitionState, - transformOrigin: TransformOrigin, - modifier: Modifier = Modifier, - content: @Composable ColumnScope.() -> Unit -) { - val transition = updateTransition(expandedStates, "DropDownMenu") - - val scale by transition.animateFloat( - transitionSpec = { - if (false isTransitioningTo true) { - // Dismissed to expanded - tween( - durationMillis = 128, - easing = LinearOutSlowInEasing - ) - } else { - // Expanded to dismissed. - tween( - durationMillis = 64, - delayMillis = 64 - ) - } - }, label = "" - ) { isDisplayed -> - if (isDisplayed) 1f else 0.9f - } - - Column( - modifier = modifier - .graphicsLayer { - scaleX = scale - scaleY = scale - this.transformOrigin = transformOrigin - }, - content = content, - ) -} - -@Immutable -private data class DropdownMenuPositionProvider( - val contentOffset: DpOffset, - val density: Density, - val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> } -) : PopupPositionProvider { - override fun calculatePosition( - anchorBounds: IntRect, - windowSize: IntSize, - layoutDirection: LayoutDirection, - popupContentSize: IntSize - ): IntOffset { - // The min margin above and below the menu, relative to the screen. - val verticalMargin = with(density) { 48.dp.roundToPx() } - // The content offset specified using the dropdown offset parameter. - val contentOffsetX = with(density) { contentOffset.x.roundToPx() } - val contentOffsetY = with(density) { contentOffset.y.roundToPx() } - - // Compute horizontal position. - val toRight = anchorBounds.left + contentOffsetX - val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width - val toDisplayRight = windowSize.width - popupContentSize.width - val toDisplayLeft = 0 - val x = if (layoutDirection == LayoutDirection.Ltr) { - sequenceOf( - toRight, - toLeft, - // If the anchor gets outside of the window on the left, we want to position - // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight. - if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft - ) - } else { - sequenceOf( - toLeft, - toRight, - // If the anchor gets outside of the window on the right, we want to position - // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft. - if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight - ) - }.firstOrNull { - it >= 0 && it + popupContentSize.width <= windowSize.width - } ?: toLeft - - // Compute vertical position. - val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) - val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height - val toCenter = anchorBounds.top - popupContentSize.height / 2 - val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin - val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { - it >= verticalMargin && - it + popupContentSize.height <= windowSize.height - verticalMargin - } ?: toTop - - onPositionCalculated( - anchorBounds, - IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height) - ) - return IntOffset(x, y) - } -} - -fun calculateTransformOrigin( - parentBounds: IntRect, - menuBounds: IntRect -): TransformOrigin { - val pivotX = when { - menuBounds.left >= parentBounds.right -> 0f - menuBounds.right <= parentBounds.left -> 1f - menuBounds.width == 0 -> 0f - else -> { - val intersectionCenter = - ( - max(parentBounds.left, menuBounds.left) + - min(parentBounds.right, menuBounds.right) - ) / 2 - (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width - } - } - val pivotY = when { - menuBounds.top >= parentBounds.bottom -> 0f - menuBounds.bottom <= parentBounds.top -> 1f - menuBounds.height == 0 -> 0f - else -> { - val intersectionCenter = - ( - max(parentBounds.top, menuBounds.top) + - min(parentBounds.bottom, menuBounds.bottom) - ) / 2 - (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height - } - } - return TransformOrigin(pivotX, pivotY) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt new file mode 100644 index 0000000..4d308a0 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt @@ -0,0 +1,68 @@ +package it.vfsfitvnm.vimusic.ui.components.themed + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +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.unit.dp +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.medium + +@Composable +fun Header( + title: String, + modifier: Modifier = Modifier, + actionsContent: @Composable RowScope.() -> Unit = {}, +) { + val typography = LocalAppearance.current.typography + + Header( + modifier = modifier, + titleContent = { + BasicText( + text = title, + style = typography.xxl.medium + ) + }, + actionsContent = actionsContent + ) +} + +@Composable +fun Header( + modifier: Modifier = Modifier, + titleContent: @Composable ColumnScope.() -> Unit, + actionsContent: @Composable RowScope.() -> Unit, +) { + Column( + horizontalAlignment = Alignment.End, + modifier = modifier + .padding(horizontal = 16.dp) + .height(128.dp) + .fillMaxWidth() + ) { + Spacer( + modifier = Modifier + .height(48.dp), + ) + + titleContent() + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .height(48.dp), + content = actionsContent, + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt index f26b944..0e0755d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt @@ -1,25 +1,22 @@ package it.vfsfitvnm.vimusic.ui.screens -import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.text.BasicText import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.* -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import it.vfsfitvnm.route.* -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.badge +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.Switch import it.vfsfitvnm.vimusic.ui.components.themed.ValueSelectorDialog import it.vfsfitvnm.vimusic.ui.screens.settings.* @@ -29,192 +26,38 @@ import it.vfsfitvnm.vimusic.utils.* @ExperimentalAnimationApi @Composable fun SettingsScreen() { - val scrollState = rememberScrollState() + val saveableStateHolder = rememberSaveableStateHolder() - RouteHandler( - listenToGlobalEmitter = true, - transitionSpec = { - when (targetState.route) { - albumRoute, artistRoute -> fastFade - else -> when (initialState.route) { - albumRoute, artistRoute -> fastFade - null -> leftSlide - else -> rightSlide - } - } - } - ) { + val (tabIndex, onTabChanged) = rememberSaveable { + mutableStateOf(0) + } + + RouteHandler(listenToGlobalEmitter = true) { globalRoutes() - appearanceSettingsRoute { - AppearanceSettingsScreen() - } - - playerSettingsRoute { - PlayerSettingsScreen() - } - - backupAndRestoreRoute { - BackupAndRestoreScreen() - } - - cacheSettingsRoute { - CacheSettingsScreen() - } - - otherSettingsRoute { - OtherSettingsScreen() - } - - aboutRoute { - AboutScreen() - } - host { - val (colorPalette, typography) = LocalAppearance.current - - var isFirstLaunch by rememberPreference(isFirstLaunchKey, true) - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = tabIndex, + onTabChanged = onTabChanged, + tabColumnContent = { Item -> + Item(0, "Appearance", R.drawable.color_palette) + Item(1, "Player", R.drawable.play) + Item(2, "Cache", R.drawable.server) + Item(3, "Other", R.drawable.shapes) + Item(4, "About", R.drawable.information) } - - BasicText( - text = "Settings", - style = typography.l.semiBold, - modifier = Modifier - .padding(start = 48.dp) - .padding(all = 16.dp) - ) - - @Composable - fun Entry( - @DrawableRes icon: Int, - color: Color, - title: String, - description: String, - route: Route0, - withAlert: Boolean = false, - onClick: (() -> Unit)? = null - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { - route() - onClick?.invoke() - } - ) - .padding(horizontal = 16.dp, vertical = 12.dp) - .fillMaxWidth() - ) { - Box( - modifier = Modifier - .background(color = color, shape = CircleShape) - .size(36.dp) - .badge(color = colorPalette.red, isDisplayed = withAlert) - ) { - Image( - painter = painterResource(icon), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .align(Alignment.Center) - .size(16.dp) - ) - } - - Column( - modifier = Modifier - .weight(1f) - ) { - BasicText( - text = title, - style = typography.s.semiBold, - ) - - BasicText( - text = description, - style = typography.xs.secondary.medium, - maxLines = 2 - ) - } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(currentTabIndex) { + when (currentTabIndex) { + 0 -> AppearanceSettingsTab() + 1 -> PlayerSettingsTab() + 2 -> CacheSettingsTab() + 3 -> OtherSettingsTab() + 4 -> AboutTab() } } - - Entry( - color = colorPalette.background2, - icon = R.drawable.color_palette, - title = "Appearance", - description = "Change the colors and shapes", - route = appearanceSettingsRoute, - ) - - Entry( - color = colorPalette.background2, - icon = R.drawable.play, - title = "Player & Audio", - description = "Player and audio settings", - route = playerSettingsRoute, - ) - - Entry( - color = colorPalette.background2, - icon = R.drawable.server, - title = "Cache", - description = "Manage the used space", - route = cacheSettingsRoute - ) - - Entry( - color = colorPalette.background2, - icon = R.drawable.save, - title = "Backup & Restore", - description = "Backup and restore the database", - route = backupAndRestoreRoute - ) - - Entry( - color = colorPalette.background2, - icon = R.drawable.shapes, - title = "Other", - description = "Advanced settings", - route = otherSettingsRoute, - withAlert = isFirstLaunch, - onClick = { - isFirstLaunch = false - } - ) - - Entry( - color = colorPalette.background2, - icon = R.drawable.information, - title = "About", - description = "App version and social links", - route = aboutRoute - ) } } } @@ -300,8 +143,8 @@ fun SwitchSettingEntry( enabled = isEnabled ) .alpha(if (isEnabled) 1f else 0.5f) - .padding(start = 24.dp) - .padding(horizontal = 32.dp, vertical = 16.dp) + .padding(start = 16.dp) + .padding(all = 16.dp) .fillMaxWidth() ) { @@ -332,8 +175,7 @@ fun SettingsEntry( onClick: () -> Unit, isEnabled: Boolean = true ) { - val (_, typography) = LocalAppearance.current - val (colorPalette) = LocalAppearance.current + val (colorPalette, typography) = LocalAppearance.current Column( modifier = modifier @@ -344,8 +186,8 @@ fun SettingsEntry( enabled = isEnabled ) .alpha(if (isEnabled) 1f else 0.5f) - .padding(start = 24.dp) - .padding(horizontal = 32.dp, vertical = 16.dp) + .padding(start = 16.dp) + .padding(all = 16.dp) .fillMaxWidth() ) { BasicText( @@ -360,22 +202,6 @@ fun SettingsEntry( } } -@Composable -fun SettingsTitle( - text: String, - modifier: Modifier = Modifier, -) { - val (_, typography) = LocalAppearance.current - - BasicText( - text = text, - style = typography.m.semiBold, - modifier = modifier - .padding(start = 40.dp) - .padding(all = 16.dp) - ) -} - @Composable fun SettingsDescription( text: String, @@ -387,24 +213,8 @@ fun SettingsDescription( text = text, style = typography.xxs.secondary, modifier = modifier - .padding(start = 56.dp, end = 24.dp) - .padding(bottom = 16.dp) - ) -} - -@Composable -fun SettingsGroupDescription( - text: String, - modifier: Modifier = Modifier, -) { - val (_, typography) = LocalAppearance.current - - BasicText( - text = text, - style = typography.xxs.secondary, - modifier = modifier - .padding(start = 56.dp, end = 24.dp) - .padding(vertical = 8.dp) + .padding(start = 16.dp) + .padding(horizontal = 16.dp, vertical = 8.dp) ) } @@ -419,7 +229,17 @@ fun SettingsEntryGroupText( text = title.uppercase(), style = typography.xxs.semiBold.copy(colorPalette.accent), modifier = modifier - .padding(start = 24.dp, top = 24.dp) - .padding(horizontal = 32.dp) + .padding(start = 16.dp) + .padding(horizontal = 16.dp) + ) +} + +@Composable +fun SettingsGroupSpacer( + modifier: Modifier = Modifier, +) { + Spacer( + modifier = modifier + .height(24.dp) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt deleted file mode 100644 index b6a39e1..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt +++ /dev/null @@ -1,100 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.settings - -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.BuildConfig -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle -import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance - -@ExperimentalAnimationApi -@Composable -fun AboutScreen() { - val scrollState = rememberScrollState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val (colorPalette) = LocalAppearance.current - val uriHandler = LocalUriHandler.current - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - - SettingsTitle(text = "About") - - SettingsDescription(text = "v${BuildConfig.VERSION_NAME}\nby vfsfitvnm") - - SettingsEntryGroupText(title = "SOCIAL") - - SettingsEntry( - title = "GitHub", - text = "View the source code", - onClick = { - uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic") - } - ) - - SettingsEntryGroupText(title = "TROUBLESHOOTING") - - SettingsEntry( - title = "Report an issue", - text = "You will be redirected to GitHub", - onClick = { - uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=bug&template=bug_report.yaml") - } - ) - - SettingsEntry( - title = "Request a feature or suggest an idea", - text = "You will be redirected to GitHub", - onClick = { - uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=enhancement&template=feature_request.yaml") - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt new file mode 100644 index 0000000..c5ed846 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt @@ -0,0 +1,73 @@ +package it.vfsfitvnm.vimusic.ui.screens.settings + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import it.vfsfitvnm.vimusic.BuildConfig +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText +import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.secondary + +@ExperimentalAnimationApi +@Composable +fun AboutTab() { + val (colorPalette, typography) = LocalAppearance.current + val uriHandler = LocalUriHandler.current + + Column( + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(LocalPlayerAwarePaddingValues.current) + ) { + Header(title = "About") { + BasicText( + text = "v${BuildConfig.VERSION_NAME} by vfsfitvnm", + style = typography.s.secondary + ) + } + + SettingsEntryGroupText(title = "SOCIAL") + + SettingsEntry( + title = "GitHub", + text = "View the source code", + onClick = { + uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic") + } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "TROUBLESHOOTING") + + SettingsEntry( + title = "Report an issue", + text = "You will be redirected to GitHub", + onClick = { + uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=bug&template=bug_report.yaml") + } + ) + + SettingsEntry( + title = "Request a feature or suggest an idea", + text = "You will be redirected to GitHub", + onClick = { + uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=enhancement&template=feature_request.yaml") + } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt deleted file mode 100644 index 6dc7abf..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt +++ /dev/null @@ -1,126 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.settings - -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.enums.ColorPaletteMode -import it.vfsfitvnm.vimusic.enums.ColorPaletteName -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle -import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry -import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey -import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey -import it.vfsfitvnm.vimusic.utils.isShowingThumbnailInLockscreenKey -import it.vfsfitvnm.vimusic.utils.rememberPreference -import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey - -@ExperimentalAnimationApi -@Composable -fun AppearanceSettingsScreen() { - val scrollState = rememberScrollState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val (colorPalette) = LocalAppearance.current - - var colorPaletteName by rememberPreference(colorPaletteNameKey, ColorPaletteName.Dynamic) - var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System) - var thumbnailRoundness by rememberPreference( - thumbnailRoundnessKey, - ThumbnailRoundness.Light - ) - var isShowingThumbnailInLockscreen by rememberPreference( - isShowingThumbnailInLockscreenKey, - false - ) - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - - SettingsTitle(text = "Appearance") - - SettingsEntryGroupText(title = "COLORS") - - EnumValueSelectorSettingsEntry( - title = "Theme", - selectedValue = colorPaletteName, - onValueSelected = { - colorPaletteName = it - } - ) - - EnumValueSelectorSettingsEntry( - title = "Theme mode", - selectedValue = colorPaletteMode, - isEnabled = colorPaletteName != ColorPaletteName.PureBlack, - onValueSelected = { - colorPaletteMode = it - } - ) - - SettingsEntryGroupText(title = "SHAPES") - - EnumValueSelectorSettingsEntry( - title = "Thumbnail roundness", - selectedValue = thumbnailRoundness, - onValueSelected = { - thumbnailRoundness = it - } - ) - - SettingsEntryGroupText(title = "LOCKSCREEN") - - SwitchSettingEntry( - title = "Show song cover", - text = "Use the playing song cover as the lockscreen wallpaper", - isChecked = isShowingThumbnailInLockscreen, - onCheckedChange = { isShowingThumbnailInLockscreen = it } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt new file mode 100644 index 0000000..352ef21 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt @@ -0,0 +1,91 @@ +package it.vfsfitvnm.vimusic.ui.screens.settings + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.enums.ColorPaletteMode +import it.vfsfitvnm.vimusic.enums.ColorPaletteName +import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText +import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer +import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey +import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey +import it.vfsfitvnm.vimusic.utils.isShowingThumbnailInLockscreenKey +import it.vfsfitvnm.vimusic.utils.rememberPreference +import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey + +@ExperimentalAnimationApi +@Composable +fun AppearanceSettingsTab() { + val (colorPalette) = LocalAppearance.current + + var colorPaletteName by rememberPreference(colorPaletteNameKey, ColorPaletteName.Dynamic) + var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System) + var thumbnailRoundness by rememberPreference( + thumbnailRoundnessKey, + ThumbnailRoundness.Light + ) + var isShowingThumbnailInLockscreen by rememberPreference( + isShowingThumbnailInLockscreenKey, + false + ) + + Column( + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(LocalPlayerAwarePaddingValues.current) + ) { + Header(title = "Appearance") + + SettingsEntryGroupText(title = "COLORS") + + EnumValueSelectorSettingsEntry( + title = "Theme", + selectedValue = colorPaletteName, + onValueSelected = { colorPaletteName = it } + ) + + EnumValueSelectorSettingsEntry( + title = "Theme mode", + selectedValue = colorPaletteMode, + isEnabled = colorPaletteName != ColorPaletteName.PureBlack, + onValueSelected = { colorPaletteMode = it } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "SHAPES") + + EnumValueSelectorSettingsEntry( + title = "Thumbnail roundness", + selectedValue = thumbnailRoundness, + onValueSelected = { thumbnailRoundness = it } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "LOCKSCREEN") + + SwitchSettingEntry( + title = "Show song cover", + text = "Use the playing song cover as the lockscreen wallpaper", + isChecked = isShowingThumbnailInLockscreen, + onCheckedChange = { isShowingThumbnailInLockscreen = it } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt deleted file mode 100644 index ed9938d..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt +++ /dev/null @@ -1,173 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.settings - -import android.annotation.SuppressLint -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.checkpoint -import it.vfsfitvnm.vimusic.internal -import it.vfsfitvnm.vimusic.path -import it.vfsfitvnm.vimusic.query -import it.vfsfitvnm.vimusic.service.PlayerService -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle -import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.intent -import java.io.FileInputStream -import java.io.FileOutputStream -import java.text.SimpleDateFormat -import java.util.Date -import kotlin.system.exitProcess - -@ExperimentalAnimationApi -@Composable -fun BackupAndRestoreScreen() { - val scrollState = rememberScrollState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val (colorPalette) = LocalAppearance.current - val context = LocalContext.current - - val backupLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/vnd.sqlite3")) { uri -> - if (uri == null) return@rememberLauncherForActivityResult - - query { - Database.internal.checkpoint() - context.applicationContext.contentResolver.openOutputStream(uri) - ?.use { outputStream -> - FileInputStream(Database.internal.path).use { inputStream -> - inputStream.copyTo(outputStream) - } - } - } - } - - val restoreLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> - if (uri == null) return@rememberLauncherForActivityResult - - query { - Database.internal.checkpoint() - Database.internal.close() - - FileOutputStream(Database.internal.path).use { outputStream -> - context.applicationContext.contentResolver.openInputStream(uri) - ?.use { inputStream -> - inputStream.copyTo(outputStream) - } - } - - context.stopService(context.intent()) - exitProcess(0) - } - } - - var isShowingRestoreDialog by rememberSaveable { - mutableStateOf(false) - } - - if (isShowingRestoreDialog) { - ConfirmationDialog( - text = "The application will automatically close itself to avoid problems after restoring the database.", - onDismiss = { - isShowingRestoreDialog = false - }, - onConfirm = { - restoreLauncher.launch( - arrayOf( - "application/x-sqlite3", - "application/vnd.sqlite3", - "application/octet-stream" - ) - ) - }, - confirmText = "Ok" - ) - } - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - - SettingsTitle(text = "Backup & Restore") - - SettingsEntryGroupText(title = "BACKUP") - - SettingsGroupDescription(text = "Personal preferences (i.e. the theme mode) and the cache are excluded.") - - SettingsEntry( - title = "Backup", - text = "Export the database to the external storage", - onClick = { - @SuppressLint("SimpleDateFormat") - val dateFormat = SimpleDateFormat("yyyyMMddHHmmss") - backupLauncher.launch("vimusic_${dateFormat.format(Date())}.db") - } - ) - - SettingsEntryGroupText(title = "RESTORE") - - SettingsGroupDescription(text = "Existing data will be overwritten.") - - SettingsEntry( - title = "Restore", - text = "Import the database from the external storage", - onClick = { - isShowingRestoreDialog = true - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt deleted file mode 100644 index 0e511e4..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt +++ /dev/null @@ -1,144 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.settings - -import android.text.format.Formatter -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import coil.Coil -import coil.annotation.ExperimentalCoilApi -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.enums.CoilDiskCacheMaxSize -import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle -import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.coilDiskCacheMaxSizeKey -import it.vfsfitvnm.vimusic.utils.exoPlayerDiskCacheMaxSizeKey -import it.vfsfitvnm.vimusic.utils.rememberPreference - -@OptIn(ExperimentalCoilApi::class) -@ExperimentalAnimationApi -@Composable -fun CacheSettingsScreen() { - - val scrollState = rememberScrollState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val context = LocalContext.current - val (colorPalette, _) = LocalAppearance.current - val binder = LocalPlayerServiceBinder.current - - var coilDiskCacheMaxSize by rememberPreference( - coilDiskCacheMaxSizeKey, - CoilDiskCacheMaxSize.`128MB` - ) - var exoPlayerDiskCacheMaxSize by rememberPreference( - exoPlayerDiskCacheMaxSizeKey, - ExoPlayerDiskCacheMaxSize.`2GB` - ) - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - - SettingsTitle(text = "Cache") - - SettingsDescription(text = "When the cache runs out of space, the resources that haven't been accessed for the longest time are cleared.") - - Coil.imageLoader(context).diskCache?.let { diskCache -> - val diskCacheSize = remember(diskCache) { - diskCache.size - } - - SettingsEntryGroupText(title = "IMAGE CACHE") - - SettingsGroupDescription(text = "${Formatter.formatShortFileSize(context, diskCacheSize)} used (${diskCacheSize * 100 / coilDiskCacheMaxSize.bytes.coerceAtLeast(1)}%)") - - EnumValueSelectorSettingsEntry( - title = "Max size", - selectedValue = coilDiskCacheMaxSize, - onValueSelected = { - coilDiskCacheMaxSize = it - } - ) - } - - binder?.cache?.let { cache -> - val diskCacheSize by remember { - derivedStateOf { - cache.cacheSpace - } - } - - SettingsEntryGroupText(title = "SONG CACHE") - - SettingsGroupDescription( - text = buildString { - append(Formatter.formatShortFileSize(context, diskCacheSize)) - append(" used") - when (val size = exoPlayerDiskCacheMaxSize) { - ExoPlayerDiskCacheMaxSize.Unlimited -> {} - else -> append(" (${diskCacheSize * 100 / size.bytes}%)") - } - } - ) - - EnumValueSelectorSettingsEntry( - title = "Max size", - selectedValue = exoPlayerDiskCacheMaxSize, - onValueSelected = { - exoPlayerDiskCacheMaxSize = it - } - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt new file mode 100644 index 0000000..0ced60e --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt @@ -0,0 +1,113 @@ +package it.vfsfitvnm.vimusic.ui.screens.settings + +import android.text.format.Formatter +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import coil.Coil +import coil.annotation.ExperimentalCoilApi +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.enums.CoilDiskCacheMaxSize +import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry +import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText +import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.coilDiskCacheMaxSizeKey +import it.vfsfitvnm.vimusic.utils.exoPlayerDiskCacheMaxSizeKey +import it.vfsfitvnm.vimusic.utils.rememberPreference + +@OptIn(ExperimentalCoilApi::class) +@ExperimentalAnimationApi +@Composable +fun CacheSettingsTab() { + val context = LocalContext.current + val (colorPalette) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + + var coilDiskCacheMaxSize by rememberPreference( + coilDiskCacheMaxSizeKey, + CoilDiskCacheMaxSize.`128MB` + ) + var exoPlayerDiskCacheMaxSize by rememberPreference( + exoPlayerDiskCacheMaxSizeKey, + ExoPlayerDiskCacheMaxSize.`2GB` + ) + + Column( + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(LocalPlayerAwarePaddingValues.current) + ) { + Header(title = "Cache") + + SettingsDescription(text = "When the cache runs out of space, the resources that haven't been accessed for the longest time are cleared") + + Coil.imageLoader(context).diskCache?.let { diskCache -> + val diskCacheSize = remember(diskCache) { + diskCache.size + } + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "IMAGE CACHE") + + SettingsDescription(text = "${Formatter.formatShortFileSize(context, diskCacheSize)} used (${diskCacheSize * 100 / coilDiskCacheMaxSize.bytes.coerceAtLeast(1)}%)") + + EnumValueSelectorSettingsEntry( + title = "Max size", + selectedValue = coilDiskCacheMaxSize, + onValueSelected = { + coilDiskCacheMaxSize = it + } + ) + } + + binder?.cache?.let { cache -> + val diskCacheSize by remember { + derivedStateOf { + cache.cacheSpace + } + } + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "SONG CACHE") + + SettingsDescription( + text = buildString { + append(Formatter.formatShortFileSize(context, diskCacheSize)) + append(" used") + when (val size = exoPlayerDiskCacheMaxSize) { + ExoPlayerDiskCacheMaxSize.Unlimited -> {} + else -> append(" (${diskCacheSize * 100 / size.bytes}%)") + } + } + ) + + EnumValueSelectorSettingsEntry( + title = "Max size", + selectedValue = exoPlayerDiskCacheMaxSize, + onValueSelected = { + exoPlayerDiskCacheMaxSize = it + } + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt deleted file mode 100644 index 0729292..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt +++ /dev/null @@ -1,183 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.settings - -import android.annotation.SuppressLint -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.provider.Settings -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.verticalScroll -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.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.query -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupDescription -import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry -import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.isIgnoringBatteryOptimizations -import it.vfsfitvnm.vimusic.utils.isInvincibilityEnabledKey -import it.vfsfitvnm.vimusic.utils.rememberPreference -import it.vfsfitvnm.vimusic.utils.semiBold -import kotlinx.coroutines.Dispatchers - -@ExperimentalAnimationApi -@Composable -fun OtherSettingsScreen() { - - val scrollState = rememberScrollState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val context = LocalContext.current - val (colorPalette, typography) = LocalAppearance.current - - val queriesCount by remember { - Database.queriesCount() - }.collectAsState(initial = 0, context = Dispatchers.IO) - - var isInvincibilityEnabled by rememberPreference(isInvincibilityEnabledKey, false) - - var isIgnoringBatteryOptimizations by remember { - mutableStateOf(context.isIgnoringBatteryOptimizations) - } - - val activityResultLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - isIgnoringBatteryOptimizations = context.isIgnoringBatteryOptimizations - } - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - - BasicText( - text = "Other", - style = typography.m.semiBold, - modifier = Modifier - .padding(start = 40.dp) - .padding(all = 16.dp) - ) - - SettingsEntryGroupText(title = "SEARCH HISTORY") - - SettingsEntry( - title = "Clear search history", - text = if (queriesCount > 0) { - "Delete $queriesCount search queries" - } else { - "History is empty" - }, - isEnabled = queriesCount > 0, - onClick = { - query { - Database.clearQueries() - } - } - ) - - SettingsEntryGroupText(title = "SERVICE LIFETIME") - - SettingsGroupDescription(text = "If battery optimizations are applied, the playback notification can suddenly disappear when paused.") - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - SettingsDescription(text = "Since Android 12, disabling battery optimizations is required for the \"Invincible service\" option to take effect.") - } - - SettingsEntry( - title = "Ignore battery optimizations", - isEnabled = !isIgnoringBatteryOptimizations, - text = if (isIgnoringBatteryOptimizations) { - "Already unrestricted" - } else { - "Disable background restrictions" - }, - onClick = { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return@SettingsEntry - - @SuppressLint("BatteryLife") - val intent = - Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { - data = Uri.parse("package:${context.packageName}") - } - - if (intent.resolveActivity(context.packageManager) != null) { - activityResultLauncher.launch(intent) - } else { - val fallbackIntent = - Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) - - if (fallbackIntent.resolveActivity(context.packageManager) != null) { - activityResultLauncher.launch(fallbackIntent) - } else { - Toast.makeText( - context, - "Couldn't find battery optimization settings, please whitelist ViMusic manually", - Toast.LENGTH_SHORT - ).show() - } - } - } - ) - - SwitchSettingEntry( - title = "Invincible service", - text = "When turning off battery optimizations is not enough", - isChecked = isInvincibilityEnabled, - onCheckedChange = { - isInvincibilityEnabled = it - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt new file mode 100644 index 0000000..9ec669a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt @@ -0,0 +1,241 @@ +package it.vfsfitvnm.vimusic.ui.screens.settings + +import android.annotation.SuppressLint +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.provider.Settings +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +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.Modifier +import androidx.compose.ui.platform.LocalContext +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.checkpoint +import it.vfsfitvnm.vimusic.internal +import it.vfsfitvnm.vimusic.path +import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.service.PlayerService +import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText +import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer +import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.intent +import it.vfsfitvnm.vimusic.utils.isIgnoringBatteryOptimizations +import it.vfsfitvnm.vimusic.utils.isInvincibilityEnabledKey +import it.vfsfitvnm.vimusic.utils.rememberPreference +import java.io.FileInputStream +import java.io.FileOutputStream +import java.text.SimpleDateFormat +import java.util.Date +import kotlin.system.exitProcess +import kotlinx.coroutines.Dispatchers + +@ExperimentalAnimationApi +@Composable +fun OtherSettingsTab() { + val context = LocalContext.current + val (colorPalette) = LocalAppearance.current + + val queriesCount by remember { + Database.queriesCount() + }.collectAsState(initial = 0, context = Dispatchers.IO) + + var isInvincibilityEnabled by rememberPreference(isInvincibilityEnabledKey, false) + + var isIgnoringBatteryOptimizations by remember { + mutableStateOf(context.isIgnoringBatteryOptimizations) + } + + var isShowingRestoreDialog by rememberSaveable { + mutableStateOf(false) + } + + val activityResultLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + isIgnoringBatteryOptimizations = context.isIgnoringBatteryOptimizations + } + + val backupLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/vnd.sqlite3")) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + + query { + Database.internal.checkpoint() + context.applicationContext.contentResolver.openOutputStream(uri) + ?.use { outputStream -> + FileInputStream(Database.internal.path).use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + } + + val restoreLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> + if (uri == null) return@rememberLauncherForActivityResult + + query { + Database.internal.checkpoint() + Database.internal.close() + + FileOutputStream(Database.internal.path).use { outputStream -> + context.applicationContext.contentResolver.openInputStream(uri) + ?.use { inputStream -> + inputStream.copyTo(outputStream) + } + } + + context.stopService(context.intent()) + exitProcess(0) + } + } + + if (isShowingRestoreDialog) { + ConfirmationDialog( + text = "The application will automatically close itself to avoid problems after restoring the database.", + onDismiss = { + isShowingRestoreDialog = false + }, + onConfirm = { + restoreLauncher.launch( + arrayOf( + "application/x-sqlite3", + "application/vnd.sqlite3", + "application/octet-stream" + ) + ) + }, + confirmText = "Ok" + ) + } + + Column( + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(LocalPlayerAwarePaddingValues.current) + ) { + Header(title = "Other") + + SettingsEntryGroupText(title = "SEARCH HISTORY") + + SettingsEntry( + title = "Clear search history", + text = if (queriesCount > 0) { + "Delete $queriesCount search queries" + } else { + "History is empty" + }, + isEnabled = queriesCount > 0, + onClick = { + query { + Database.clearQueries() + } + } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "SERVICE LIFETIME") + + SettingsDescription(text = "If battery optimizations are applied, the playback notification can suddenly disappear when paused.") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + SettingsDescription(text = "Since Android 12, disabling battery optimizations is required for the \"Invincible service\" option to take effect.") + } + + SettingsEntry( + title = "Ignore battery optimizations", + isEnabled = !isIgnoringBatteryOptimizations, + text = if (isIgnoringBatteryOptimizations) { + "Already unrestricted" + } else { + "Disable background restrictions" + }, + onClick = { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return@SettingsEntry + + @SuppressLint("BatteryLife") + val intent = + Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply { + data = Uri.parse("package:${context.packageName}") + } + + if (intent.resolveActivity(context.packageManager) != null) { + activityResultLauncher.launch(intent) + } else { + val fallbackIntent = + Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS) + + if (fallbackIntent.resolveActivity(context.packageManager) != null) { + activityResultLauncher.launch(fallbackIntent) + } else { + Toast.makeText( + context, + "Couldn't find battery optimization settings, please whitelist ViMusic manually", + Toast.LENGTH_SHORT + ).show() + } + } + } + ) + + SwitchSettingEntry( + title = "Invincible service", + text = "When turning off battery optimizations is not enough", + isChecked = isInvincibilityEnabled, + onCheckedChange = { isInvincibilityEnabled = it } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "BACKUP") + + SettingsDescription(text = "Personal preferences (i.e. the theme mode) and the cache are excluded.") + + SettingsEntry( + title = "Backup", + text = "Export the database to the external storage", + onClick = { + @SuppressLint("SimpleDateFormat") + val dateFormat = SimpleDateFormat("yyyyMMddHHmmss") + backupLauncher.launch("vimusic_${dateFormat.format(Date())}.db") + } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "RESTORE") + + SettingsDescription(text = "Existing data will be overwritten.") + + SettingsEntry( + title = "Restore", + text = "Import the database from the external storage", + onClick = { + isShowingRestoreDialog = true + } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt deleted file mode 100644 index 17cd475..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt +++ /dev/null @@ -1,148 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.settings - -import android.content.Intent -import android.media.audiofx.AudioEffect -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle -import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry -import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.persistentQueueKey -import it.vfsfitvnm.vimusic.utils.rememberPreference -import it.vfsfitvnm.vimusic.utils.skipSilenceKey -import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey - -@ExperimentalAnimationApi -@Composable -fun PlayerSettingsScreen() { - - val scrollState = rememberScrollState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val context = LocalContext.current - val (colorPalette) = LocalAppearance.current - val binder = LocalPlayerServiceBinder.current - - var persistentQueue by rememberPreference(persistentQueueKey, false) - var skipSilence by rememberPreference(skipSilenceKey, false) - var volumeNormalization by rememberPreference(volumeNormalizationKey, false) - - val activityResultLauncher = - rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { - } - - Column( - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - .verticalScroll(scrollState) - .padding(LocalPlayerAwarePaddingValues.current) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - - SettingsTitle(text = "Player & Audio") - - SettingsEntryGroupText(title = "PLAYER") - - SwitchSettingEntry( - title = "Persistent queue", - text = "Save and restore playing songs", - isChecked = persistentQueue, - onCheckedChange = { - persistentQueue = it - } - ) - - SettingsEntryGroupText(title = "AUDIO") - - SwitchSettingEntry( - title = "Skip silence", - text = "Skip silent parts during playback", - isChecked = skipSilence, - onCheckedChange = { - skipSilence = it - } - ) - - SwitchSettingEntry( - title = "Loudness normalization", - text = "Lower the volume to a standard level", - isChecked = volumeNormalization, - onCheckedChange = { - volumeNormalization = it - } - ) - - SettingsEntry( - title = "Equalizer", - text = "Interact with the system equalizer", - onClick = { - val intent = - Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { - putExtra( - AudioEffect.EXTRA_AUDIO_SESSION, - binder?.player?.audioSessionId - ) - putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) - putExtra( - AudioEffect.EXTRA_CONTENT_TYPE, - AudioEffect.CONTENT_TYPE_MUSIC - ) - } - - if (intent.resolveActivity(context.packageManager) != null) { - activityResultLauncher.launch(intent) - } else { - Toast.makeText(context, "No equalizer app found!", Toast.LENGTH_SHORT) - .show() - } - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt new file mode 100644 index 0000000..8be656f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt @@ -0,0 +1,116 @@ +package it.vfsfitvnm.vimusic.ui.screens.settings + +import android.content.Intent +import android.media.audiofx.AudioEffect +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry +import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText +import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer +import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.persistentQueueKey +import it.vfsfitvnm.vimusic.utils.rememberPreference +import it.vfsfitvnm.vimusic.utils.skipSilenceKey +import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey + +@ExperimentalAnimationApi +@Composable +fun PlayerSettingsTab() { + val context = LocalContext.current + val (colorPalette) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + + var persistentQueue by rememberPreference(persistentQueueKey, false) + var skipSilence by rememberPreference(skipSilenceKey, false) + var volumeNormalization by rememberPreference(volumeNormalizationKey, false) + + val activityResultLauncher = + rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { + } + + Column( + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(LocalPlayerAwarePaddingValues.current) + ) { + Header(title = "Player & Audio") + + SettingsEntryGroupText(title = "PLAYER") + + SwitchSettingEntry( + title = "Persistent queue", + text = "Save and restore playing songs", + isChecked = persistentQueue, + onCheckedChange = { + persistentQueue = it + } + ) + + SettingsGroupSpacer() + + SettingsEntryGroupText(title = "AUDIO") + + SwitchSettingEntry( + title = "Skip silence", + text = "Skip silent parts during playback", + isChecked = skipSilence, + onCheckedChange = { + skipSilence = it + } + ) + + SwitchSettingEntry( + title = "Loudness normalization", + text = "Lower the volume to a standard level", + isChecked = volumeNormalization, + onCheckedChange = { + volumeNormalization = it + } + ) + + SettingsEntry( + title = "Equalizer", + text = "Interact with the system equalizer", + onClick = { + val intent = + Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply { + putExtra( + AudioEffect.EXTRA_AUDIO_SESSION, + binder?.player?.audioSessionId + ) + putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName) + putExtra( + AudioEffect.EXTRA_CONTENT_TYPE, + AudioEffect.CONTENT_TYPE_MUSIC + ) + } + + if (intent.resolveActivity(context.packageManager) != null) { + activityResultLauncher.launch(intent) + } else { + Toast.makeText(context, "No equalizer app found!", Toast.LENGTH_SHORT) + .show() + } + } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt index 095c7d1..6fa6a15 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt @@ -12,12 +12,8 @@ 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 @@ -54,6 +50,7 @@ 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.Header import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -164,88 +161,69 @@ fun PlaylistsTab( 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) + Header(title = "Playlists") { + @Composable + fun Item( + @DrawableRes iconId: Int, + sortBy: PlaylistSortBy ) { - @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), + painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), + colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { viewModel.sortBy = sortBy } .padding(all = 4.dp) .size(18.dp) - .graphicsLayer { rotationZ = sortOrderIconRotation } ) } + + 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 } + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt index ccfe8c1..76f9f0c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt @@ -14,19 +14,14 @@ 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 @@ -34,7 +29,6 @@ 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 @@ -54,6 +48,7 @@ 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.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -63,7 +58,6 @@ 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 @@ -134,72 +128,53 @@ fun SongsTab( 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) + Header(title = "Songs") { + @Composable + fun Item( + @DrawableRes iconId: Int, + sortBy: SongSortBy ) { - @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), + painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), + colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { viewModel.sortBy = sortBy } .padding(all = 4.dp) .size(18.dp) - .graphicsLayer { rotationZ = sortOrderIconRotation } ) } + + 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 } + ) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 176589f..ececf2e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,6 +26,7 @@ dependencyResolutionManagement { library("compose-shimmer", "com.valentinilk.shimmer", "compose-shimmer").version("1.0.3") + library("compose-viewmodel", "androidx.lifecycle", "lifecycle-viewmodel-compose").version("2.6.0-alpha02") library("compose-activity", "androidx.activity", "activity-compose").version("1.5.1") library("compose-coil", "io.coil-kt", "coil-compose").version("2.2.1") From 35e0070bdacf47f1144358309a36056b6f229045 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Fri, 23 Sep 2022 17:00:12 +0200 Subject: [PATCH 003/100] Redesign SearchScreen (#172) --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 3 + .../vimusic/ui/screens/HomeScreen.kt | 2 + .../vimusic/ui/screens/SearchScreen.kt | 400 ------------------ .../ui/screens/search/LibrarySearchTab.kt | 145 +++++++ .../search/LibrarySearchTabViewModel.kt | 25 ++ .../ui/screens/search/OnlineSearchTab.kt | 298 +++++++++++++ .../search/OnlineSearchTabViewModel.kt | 36 ++ .../vimusic/ui/screens/search/SearchScreen.kt | 89 ++++ .../vimusic/ui/screens/settings/AboutTab.kt | 3 - .../screens/settings/AppearanceSettingsTab.kt | 4 - .../ui/screens/settings/CacheSettingsTab.kt | 4 - .../ui/screens/settings/OtherSettingsTab.kt | 5 - .../ui/screens/settings/PlayerSettingsTab.kt | 4 - .../screens/{ => settings}/SettingsScreen.kt | 3 +- app/src/main/res/drawable/globe.xml | 33 ++ app/src/main/res/drawable/library.xml | 24 ++ 16 files changed, 657 insertions(+), 421 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/{ => settings}/SettingsScreen.kt (98%) create mode 100644 app/src/main/res/drawable/globe.xml create mode 100644 app/src/main/res/drawable/library.xml diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 2f07d66..3352650 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -220,6 +220,9 @@ interface Database { @Query("SELECT loudnessDb FROM Format WHERE songId = :songId") fun loudnessDb(songId: String): Flow + @Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query") + fun search(query: String): Flow> + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(format: Format) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index 89a3a6e..be01e99 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -11,6 +11,8 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen +import it.vfsfitvnm.vimusic.ui.screens.settings.SettingsScreen import it.vfsfitvnm.vimusic.ui.views.PlaylistsTab import it.vfsfitvnm.vimusic.ui.views.SongsTab import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt deleted file mode 100644 index 790044f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt +++ /dev/null @@ -1,400 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import android.net.Uri -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut -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.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.calculateStartPadding -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.imePadding -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.text.BasicText -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.produceState -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.paint -import androidx.compose.ui.draw.rotate -import androidx.compose.ui.focus.FocusRequester -import androidx.compose.ui.focus.focusRequester -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.core.net.toUri -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.models.SearchQuery -import it.vfsfitvnm.vimusic.query -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.styling.Dimensions -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.medium -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.withContext - -@ExperimentalAnimationApi -@Composable -fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) { - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val (colorPalette, typography) = LocalAppearance.current - val layoutDirection = LocalLayoutDirection.current - val paddingValues = WindowInsets.systemBars.asPaddingValues() - - val timeIconPainter = painterResource(R.drawable.time) - val closeIconPainter = painterResource(R.drawable.close) - val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) - val rippleIndication = rememberRipple(bounded = true) - - var textFieldValue by rememberSaveable( - initialTextInput, - stateSaver = TextFieldValue.Saver - ) { - mutableStateOf( - TextFieldValue( - text = initialTextInput, - selection = TextRange(initialTextInput.length) - ) - ) - } - - val focusRequester = remember { - FocusRequester() - } - - val searchSuggestionsResult by produceState?>?>( - initialValue = null, - key1 = textFieldValue - ) { - value = if (textFieldValue.text.isNotEmpty()) { - withContext(Dispatchers.IO) { - YouTube.getSearchSuggestions(textFieldValue.text) - } - } else { - null - } - } - - val history by remember(textFieldValue.text) { - Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new -> - old.size == new.size - } - }.collectAsState(initial = emptyList(), context = Dispatchers.IO) - - val isOpenableUrl = remember(textFieldValue.text) { - listOf( - "https://www.youtube.com/watch?", - "https://music.youtube.com/watch?", - "https://m.youtube.com/watch?", - "https://www.youtube.com/playlist?", - "https://music.youtube.com/playlist?", - "https://m.youtube.com/playlist?", - "https://youtu.be/", - ).any(textFieldValue.text::startsWith) - } - - LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() - } - - Column( - modifier = Modifier - .fillMaxSize() - .imePadding() - .padding( - start = paddingValues.calculateStartPadding(layoutDirection), - end = paddingValues.calculateEndPadding(layoutDirection), - top = paddingValues.calculateTopPadding(), - ) - ) { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - BasicTextField( - value = textFieldValue, - onValueChange = { - textFieldValue = it - }, - textStyle = typography.m.medium, - singleLine = true, - maxLines = 1, - keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), - keyboardActions = KeyboardActions( - onSearch = { - if (textFieldValue.text.isNotEmpty()) { - onSearch(textFieldValue.text) - } - } - ), - cursorBrush = SolidColor(colorPalette.text), - decorationBox = { innerTextField -> - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - pop() - focusRequester.freeFocus() - } - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - - Box( - modifier = Modifier - .weight(1f) - ) { - androidx.compose.animation.AnimatedVisibility( - visible = textFieldValue.text.isEmpty(), - enter = fadeIn(tween(100)), - exit = fadeOut(tween(100)), - ) { - BasicText( - text = "Enter a song, an album, an artist name...", - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = typography.m.secondary, - ) - } - - innerTextField() - } - - Box( - modifier = Modifier - .clickable { - textFieldValue = TextFieldValue() - } - .padding(horizontal = 14.dp, vertical = 6.dp) - .background( - color = colorPalette.background1, - shape = CircleShape - ) - .size(28.dp) - ) { - Image( - painter = painterResource(R.drawable.close), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.textSecondary), - modifier = Modifier - .align(Alignment.Center) - .size(14.dp) - ) - } - } - }, - modifier = Modifier - .weight(1f) - .focusRequester(focusRequester) - ) - } - - if (isOpenableUrl) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable( - indication = rippleIndication, - interactionSource = remember { MutableInteractionSource() }, - onClick = { onUri(textFieldValue.text.toUri()) } - ) - .fillMaxWidth() - .background(colorPalette.background1) - .padding(vertical = 16.dp, horizontal = 8.dp) - ) { - Image( - painter = painterResource(R.drawable.link), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.textDisabled), - modifier = Modifier - .padding(horizontal = 8.dp) - .size(20.dp) - ) - - BasicText( - text = "Open URL", - style = typography.s.secondary, - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1f) - ) - } - } - - LazyColumn( - contentPadding = PaddingValues( - bottom = Dimensions.collapsedPlayer + paddingValues.calculateBottomPadding() - ) - ) { - items( - items = history, - key = SearchQuery::id - ) { searchQuery -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable( - indication = rippleIndication, - interactionSource = remember { MutableInteractionSource() }, - onClick = { onSearch(searchQuery.query) } - ) - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 8.dp) - ) { - Spacer( - modifier = Modifier - .padding(horizontal = 8.dp) - .size(20.dp) - .paint( - painter = timeIconPainter, - colorFilter = ColorFilter.tint(colorPalette.textDisabled) - ) - ) - - BasicText( - text = searchQuery.query, - style = typography.s.secondary, - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1f) - ) - - Spacer( - modifier = Modifier - .clickable { - query { - Database.delete(searchQuery) - } - } - .padding(horizontal = 8.dp) - .size(20.dp) - .paint( - painter = closeIconPainter, - colorFilter = ColorFilter.tint(colorPalette.textDisabled) - ) - ) - - Spacer( - modifier = Modifier - .clickable { - textFieldValue = TextFieldValue( - text = searchQuery.query, - selection = TextRange(searchQuery.query.length) - ) - } - .rotate(225f) - .padding(horizontal = 8.dp) - .size(20.dp) - .paint( - painter = arrowForwardIconPainter, - colorFilter = ColorFilter.tint(colorPalette.textDisabled) - ) - ) - } - } - - searchSuggestionsResult?.getOrNull()?.let { suggestions -> - items(items = suggestions) { suggestion -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable( - indication = rippleIndication, - interactionSource = remember { MutableInteractionSource() }, - onClick = { onSearch(suggestion) } - ) - .fillMaxWidth() - .padding(vertical = 16.dp, horizontal = 8.dp) - ) { - Spacer( - modifier = Modifier - .padding(horizontal = 8.dp) - .size(20.dp) - ) - - BasicText( - text = suggestion, - style = typography.s.secondary, - modifier = Modifier - .padding(horizontal = 8.dp) - .weight(1f) - ) - - Spacer( - modifier = Modifier - .clickable { - textFieldValue = TextFieldValue( - text = suggestion, - selection = TextRange(suggestion.length) - ) - } - .rotate(225f) - .padding(horizontal = 8.dp) - .size(22.dp) - .paint( - painter = arrowForwardIconPainter, - colorFilter = ColorFilter.tint(colorPalette.textDisabled) - ) - ) - } - } - } ?: searchSuggestionsResult?.exceptionOrNull()?.let { throwable -> - item { - LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {} - } - } - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt new file mode 100644 index 0000000..5ffcf39 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt @@ -0,0 +1,145 @@ +package it.vfsfitvnm.vimusic.ui.screens.search + +import androidx.compose.animation.ExperimentalAnimationApi +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.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +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.SolidColor +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.ui.components.themed.Header +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.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.align +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.forcePlay +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun LibrarySearchTab( + textFieldValue: TextFieldValue, + onTextFieldValueChanged: (TextFieldValue) -> Unit, + viewModel: LibrarySearchTabViewModel = viewModel( + key = textFieldValue.text, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return LibrarySearchTabViewModel(textFieldValue.text) as T + } + } + ) +) { + val (colorPalette, typography) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + val thumbnailSize = Dimensions.thumbnails.song.px + + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header( + titleContent = { + BasicTextField( + value = textFieldValue, + onValueChange = onTextFieldValueChanged, + textStyle = typography.xxl.medium.align(TextAlign.End), + singleLine = true, + maxLines = 1, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), + cursorBrush = SolidColor(colorPalette.text), + decorationBox = { innerTextField -> + Box { + androidx.compose.animation.AnimatedVisibility( + visible = textFieldValue.text.isEmpty(), + enter = fadeIn(tween(200)), + exit = fadeOut(tween(200)), + modifier = Modifier + .align(Alignment.CenterEnd) + ) { + BasicText( + text = "Enter a name", + maxLines = 1, + style = typography.xxl.secondary + ) + } + + innerTextField() + } + } + ) + }, + actionsContent = { + BasicText( + text = "Clear", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = textFieldValue.text.isNotEmpty()) { + onTextFieldValueChanged(TextFieldValue()) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } + ) + } + + items( + items = viewModel.items, + key = DetailedSong::id, + ) { song -> + SongItem( + song = song, + thumbnailSize = thumbnailSize, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + menuContent = { InHistoryMediaItemMenu(song = song) }, + modifier = Modifier + .animateItemPlacement() + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt new file mode 100644 index 0000000..4572e75 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt @@ -0,0 +1,25 @@ +package it.vfsfitvnm.vimusic.ui.screens.search + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.models.DetailedSong +import kotlinx.coroutines.launch + +class LibrarySearchTabViewModel(text: String) : ViewModel() { + var items by mutableStateOf(emptyList()) + private set + + init { + if (text.isNotEmpty()) { + viewModelScope.launch { + Database.search("%$text%").collect { + items = it + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt new file mode 100644 index 0000000..9e24905 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt @@ -0,0 +1,298 @@ +package it.vfsfitvnm.vimusic.ui.screens.search + +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +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.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +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.paint +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +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.models.SearchQuery +import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.align +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.secondary +import kotlinx.coroutines.delay + +@Composable +fun OnlineSearchTab( + textFieldValue: TextFieldValue, + onTextFieldValueChanged: (TextFieldValue) -> Unit, + isOpenableUrl: Boolean, + onSearch: (String) -> Unit, + onUri: () -> Unit, + viewModel: OnlineSearchTabViewModel = viewModel( + key = textFieldValue.text, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return OnlineSearchTabViewModel(textFieldValue.text) as T + } + } + ) +) { + val (colorPalette, typography) = LocalAppearance.current + + val timeIconPainter = painterResource(R.drawable.time) + val closeIconPainter = painterResource(R.drawable.close) + val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) + val rippleIndication = rememberRipple(bounded = true) + + val focusRequester = remember { + FocusRequester() + } + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } + + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header( + titleContent = { + BasicTextField( + value = textFieldValue, + onValueChange = onTextFieldValueChanged, + textStyle = typography.xxl.medium.align(TextAlign.End), + singleLine = true, + maxLines = 1, + keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search), + keyboardActions = KeyboardActions( + onSearch = { + if (textFieldValue.text.isNotEmpty()) { + onSearch(textFieldValue.text) + } + } + ), + cursorBrush = SolidColor(colorPalette.text), + decorationBox = { innerTextField -> + Box { + androidx.compose.animation.AnimatedVisibility( + visible = textFieldValue.text.isEmpty(), + enter = fadeIn(tween(200)), + exit = fadeOut(tween(200)), + modifier = Modifier + .align(Alignment.CenterEnd) + ) { + BasicText( + text = "Enter a name", + maxLines = 1, + style = typography.xxl.secondary + ) + } + + innerTextField() + } + }, + modifier = Modifier + .focusRequester(focusRequester) + ) + }, + actionsContent = { + BasicText( + text = if (isOpenableUrl) "Open url" else "Search", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = textFieldValue.text.isNotEmpty()) { + if (isOpenableUrl) onUri() else onSearch(textFieldValue.text) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + + Spacer( + modifier = Modifier + .weight(1f) + ) + + BasicText( + text = "Clear", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = textFieldValue.text.isNotEmpty()) { + onTextFieldValueChanged(TextFieldValue()) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } + ) + } + + items( + items = viewModel.history, + key = SearchQuery::id + ) { searchQuery -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + indication = rippleIndication, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onSearch(searchQuery.query) } + ) + .fillMaxWidth() + .padding(all = 16.dp) + ) { + Spacer( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(20.dp) + .paint( + painter = timeIconPainter, + colorFilter = ColorFilter.tint(colorPalette.textDisabled) + ) + ) + + BasicText( + text = searchQuery.query, + style = typography.s.secondary, + modifier = Modifier + .padding(horizontal = 8.dp) + .weight(1f) + ) + + Spacer( + modifier = Modifier + .clickable { + query { + Database.delete(searchQuery) + } + } + .padding(horizontal = 8.dp) + .size(20.dp) + .paint( + painter = closeIconPainter, + colorFilter = ColorFilter.tint(colorPalette.textDisabled) + ) + ) + + Spacer( + modifier = Modifier + .clickable { + onTextFieldValueChanged( + TextFieldValue( + text = searchQuery.query, + selection = TextRange(searchQuery.query.length) + ) + ) + } + .rotate(225f) + .padding(horizontal = 8.dp) + .size(20.dp) + .paint( + painter = arrowForwardIconPainter, + colorFilter = ColorFilter.tint(colorPalette.textDisabled) + ) + ) + } + } + + viewModel.suggestionsResult?.getOrNull()?.let { suggestions -> + items(items = suggestions) { suggestion -> + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + indication = rippleIndication, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onSearch(suggestion) } + ) + .fillMaxWidth() + .padding(all = 16.dp) + ) { + Spacer( + modifier = Modifier + .padding(horizontal = 8.dp) + .size(20.dp) + ) + + BasicText( + text = suggestion, + style = typography.s.secondary, + modifier = Modifier + .padding(horizontal = 8.dp) + .weight(1f) + ) + + Spacer( + modifier = Modifier + .clickable { + onTextFieldValueChanged( + TextFieldValue( + text = suggestion, + selection = TextRange(suggestion.length) + ) + ) + } + .rotate(225f) + .padding(horizontal = 8.dp) + .size(22.dp) + .paint( + painter = arrowForwardIconPainter, + colorFilter = ColorFilter.tint(colorPalette.textDisabled) + ) + ) + } + } + } ?: viewModel.suggestionsResult?.exceptionOrNull()?.let { throwable -> + item { + LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {} + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt new file mode 100644 index 0000000..c334cc7 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt @@ -0,0 +1,36 @@ +package it.vfsfitvnm.vimusic.ui.screens.search + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.models.SearchQuery +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +class OnlineSearchTabViewModel(text: String) : ViewModel() { + var history by mutableStateOf(emptyList()) + private set + + var suggestionsResult by mutableStateOf?>?>(null) + private set + + init { + viewModelScope.launch { + Database.queries("%$text%").distinctUntilChanged { old, new -> + old.size == new.size + }.collect { + history = it + } + } + + if (text.isNotEmpty()) { + viewModelScope.launch { + suggestionsResult = YouTube.getSearchSuggestions(text) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt new file mode 100644 index 0000000..13d8b31 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt @@ -0,0 +1,89 @@ +package it.vfsfitvnm.vimusic.ui.screens.search + +import android.net.Uri +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.core.net.toUri +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) { + val saveableStateHolder = rememberSaveableStateHolder() + + val (tabIndex, onTabChanged) = rememberSaveable { + mutableStateOf(0) + } + + val (textFieldValue, onTextFieldValueChanged) = rememberSaveable( + initialTextInput, + stateSaver = TextFieldValue.Saver + ) { + mutableStateOf( + TextFieldValue( + text = initialTextInput, + selection = TextRange(initialTextInput.length) + ) + ) + } + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + val isOpenableUrl = remember(textFieldValue.text) { + listOf( + "https://www.youtube.com/watch?", + "https://music.youtube.com/watch?", + "https://m.youtube.com/watch?", + "https://www.youtube.com/playlist?", + "https://music.youtube.com/playlist?", + "https://m.youtube.com/playlist?", + "https://youtu.be/", + ).any(textFieldValue.text::startsWith) + } + + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = tabIndex, + onTabChanged = onTabChanged, + tabColumnContent = { Item -> + Item(0, "Online", R.drawable.globe) + Item(1, "Library", R.drawable.library) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(currentTabIndex) { + when (currentTabIndex) { + 0 -> OnlineSearchTab( + textFieldValue = textFieldValue, + onTextFieldValueChanged = onTextFieldValueChanged, + isOpenableUrl = isOpenableUrl, + onSearch = onSearch, + onUri = { + if (isOpenableUrl) { + onUri(textFieldValue.text.toUri()) + } + } + ) + 1 -> LibrarySearchTab( + textFieldValue = textFieldValue, + onTextFieldValueChanged = onTextFieldValueChanged + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt index c5ed846..27951c1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt @@ -14,9 +14,6 @@ import androidx.compose.ui.platform.LocalUriHandler import it.vfsfitvnm.vimusic.BuildConfig import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.secondary diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt index 352ef21..842f644 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt @@ -16,10 +16,6 @@ import it.vfsfitvnm.vimusic.enums.ColorPaletteMode import it.vfsfitvnm.vimusic.enums.ColorPaletteName import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer -import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt index 0ced60e..6694760 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt @@ -22,10 +22,6 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.enums.CoilDiskCacheMaxSize import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.coilDiskCacheMaxSizeKey import it.vfsfitvnm.vimusic.utils.exoPlayerDiskCacheMaxSizeKey diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt index 9ec669a..716f69f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt @@ -33,11 +33,6 @@ import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.service.PlayerService import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer -import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.isIgnoringBatteryOptimizations diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt index 8be656f..fcf907c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt @@ -20,10 +20,6 @@ import androidx.compose.ui.platform.LocalContext import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry -import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText -import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer -import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.persistentQueueKey import it.vfsfitvnm.vimusic.utils.rememberPreference diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt similarity index 98% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt index 0e0755d..a6e4037 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.screens +package it.vfsfitvnm.vimusic.ui.screens.settings import androidx.compose.animation.* import androidx.compose.foundation.* @@ -19,6 +19,7 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.Switch import it.vfsfitvnm.vimusic.ui.components.themed.ValueSelectorDialog +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.settings.* import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.* diff --git a/app/src/main/res/drawable/globe.xml b/app/src/main/res/drawable/globe.xml new file mode 100644 index 0000000..10a3b37 --- /dev/null +++ b/app/src/main/res/drawable/globe.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + diff --git a/app/src/main/res/drawable/library.xml b/app/src/main/res/drawable/library.xml new file mode 100644 index 0000000..1105723 --- /dev/null +++ b/app/src/main/res/drawable/library.xml @@ -0,0 +1,24 @@ + + + + + + + + From ef0567650ce5a99114d6db13cca5c8d0517556c9 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Fri, 23 Sep 2022 17:22:26 +0200 Subject: [PATCH 004/100] Reorganize UI packages --- .../it/vfsfitvnm/vimusic/MainActivity.kt | 4 +- .../it/vfsfitvnm/vimusic/ui/screens/Routes.kt | 6 -- .../ui/screens/{ => home}/HomeScreen.kt | 15 +++- .../{views => screens/home}/PlaylistsTab.kt | 66 +---------------- .../ui/screens/home/PlaylistsTabViewModel.kt | 70 +++++++++++++++++++ .../ui/{views => screens/home}/SongsTab.kt | 54 +------------- .../ui/screens/home/SongsTabViewModel.kt | 67 ++++++++++++++++++ .../ui/{views => screens}/player/Controls.kt | 2 +- .../ui/{views => screens}/player/Lyrics.kt | 2 +- .../player/PlaybackError.kt | 2 +- .../player}/PlayerBottomSheet.kt | 3 +- .../{views => screens/player}/PlayerView.kt | 4 +- .../player/StatsForNerds.kt | 2 +- .../ui/{views => screens}/player/Thumbnail.kt | 2 +- 14 files changed, 164 insertions(+), 135 deletions(-) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/{ => home}/HomeScreen.kt (85%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens/home}/PlaylistsTab.kt (80%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens/home}/SongsTab.kt (79%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens}/player/Controls.kt (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens}/player/Lyrics.kt (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens}/player/PlaybackError.kt (98%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens/player}/PlayerBottomSheet.kt (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens/player}/PlayerView.kt (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens}/player/StatsForNerds.kt (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/{views => screens}/player/Thumbnail.kt (99%) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index df38c92..43792b7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -63,7 +63,7 @@ import it.vfsfitvnm.vimusic.ui.components.collapsedAnchor import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState -import it.vfsfitvnm.vimusic.ui.screens.HomeScreen +import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.styling.Appearance import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -71,7 +71,7 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.typographyOf -import it.vfsfitvnm.vimusic.ui.views.PlayerView +import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey import it.vfsfitvnm.vimusic.utils.getEnum diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index 338bba5..593b9cd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -9,17 +9,11 @@ import it.vfsfitvnm.route.Route1 import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist -val aboutRoute = Route0("aboutRoute") val albumRoute = Route1("albumRoute") -val appearanceSettingsRoute = Route0("appearanceSettingsRoute") val artistRoute = Route1("artistRoute") -val backupAndRestoreRoute = Route0("backupAndRestoreRoute") val builtInPlaylistRoute = Route1("builtInPlaylistRoute") -val cacheSettingsRoute = Route0("cacheSettingsRoute") val intentUriRoute = Route1("intentUriRoute") val localPlaylistRoute = Route1("localPlaylistRoute") -val otherSettingsRoute = Route0("otherSettingsRoute") -val playerSettingsRoute = Route0("playerSettingsRoute") val playlistRoute = Route1("playlistRoute") val searchResultRoute = Route1("searchResultRoute") val searchRoute = Route1("searchRoute") diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt similarity index 85% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index be01e99..13fe42b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.screens +package it.vfsfitvnm.vimusic.ui.screens.home import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi @@ -11,10 +11,19 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen +import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen +import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen +import it.vfsfitvnm.vimusic.ui.screens.SearchResultScreen +import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute +import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen +import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute +import it.vfsfitvnm.vimusic.ui.screens.searchRoute import it.vfsfitvnm.vimusic.ui.screens.settings.SettingsScreen -import it.vfsfitvnm.vimusic.ui.views.PlaylistsTab -import it.vfsfitvnm.vimusic.ui.views.SongsTab +import it.vfsfitvnm.vimusic.ui.screens.settingsRoute import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey import it.vfsfitvnm.vimusic.utils.rememberPreference diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTab.kt similarity index 80% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTab.kt index 6fa6a15..e1714b2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTab.kt @@ -1,7 +1,5 @@ -package it.vfsfitvnm.vimusic.ui.views +package it.vfsfitvnm.vimusic.ui.screens.home -import android.app.Application -import android.content.SharedPreferences import androidx.annotation.DrawableRes import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.animateFloatAsState @@ -37,9 +35,6 @@ 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 @@ -48,69 +43,14 @@ 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.Header 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.ui.views.BuiltInPlaylistItem +import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem 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()) - 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().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 diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt new file mode 100644 index 0000000..a524665 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt @@ -0,0 +1,70 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import android.app.Application +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.enums.PlaylistSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.PlaylistPreview +import it.vfsfitvnm.vimusic.utils.getEnum +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()) + 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().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 + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTab.kt similarity index 79% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTab.kt index 76f9f0c..dcb98ee 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTab.kt @@ -1,7 +1,5 @@ -package it.vfsfitvnm.vimusic.ui.views +package it.vfsfitvnm.vimusic.ui.screens.home -import android.app.Application -import android.content.SharedPreferences import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi @@ -25,8 +23,6 @@ import androidx.compose.foundation.lazy.itemsIndexed 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.graphics.Brush @@ -36,11 +32,7 @@ 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 @@ -53,54 +45,12 @@ 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.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.getEnum -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()) - 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().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 diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt new file mode 100644 index 0000000..e6cdd0f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt @@ -0,0 +1,67 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import android.app.Application +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.enums.SongSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.utils.getEnum +import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf +import it.vfsfitvnm.vimusic.utils.preferences +import it.vfsfitvnm.vimusic.utils.putEnum +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()) + 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().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 + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Controls.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Controls.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt index efd1c89..fd7cef8 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Controls.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views.player +package it.vfsfitvnm.vimusic.ui.screens.player import android.text.format.DateUtils import androidx.compose.animation.core.LinearEasing diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Lyrics.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt index aeb15e4..2196223 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Lyrics.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views.player +package it.vfsfitvnm.vimusic.ui.screens.player import android.app.SearchManager import android.content.Intent diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/PlaybackError.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlaybackError.kt similarity index 98% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/PlaybackError.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlaybackError.kt index df9b2a7..a654e67 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/PlaybackError.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlaybackError.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views.player +package it.vfsfitvnm.vimusic.ui.screens.player import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.fadeIn diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index 9cd5103..e3c9da0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views +package it.vfsfitvnm.vimusic.ui.screens.player import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.fadeIn @@ -45,6 +45,7 @@ import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.onOverlay import it.vfsfitvnm.vimusic.ui.styling.px +import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt index 6461bec..7c73bff 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views +package it.vfsfitvnm.vimusic.ui.screens.player import android.content.Intent import android.content.res.Configuration @@ -61,8 +61,6 @@ import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.collapsedPlayerProgressBar import it.vfsfitvnm.vimusic.ui.styling.px -import it.vfsfitvnm.vimusic.ui.views.player.Controls -import it.vfsfitvnm.vimusic.ui.views.player.Thumbnail import it.vfsfitvnm.vimusic.utils.rememberMediaItem import it.vfsfitvnm.vimusic.utils.rememberPositionAndDuration import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/StatsForNerds.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/StatsForNerds.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt index 2aea50b..3984ac9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/StatsForNerds.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/StatsForNerds.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views.player +package it.vfsfitvnm.vimusic.ui.screens.player import android.text.format.Formatter import androidx.compose.animation.AnimatedVisibility diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Thumbnail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Thumbnail.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt index 4577241..70375e6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/player/Thumbnail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt @@ -1,4 +1,4 @@ -package it.vfsfitvnm.vimusic.ui.views.player +package it.vfsfitvnm.vimusic.ui.screens.player import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope From 20de24bfb3a9ea2f902088e4b0cc6a62ae305e2f Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Fri, 23 Sep 2022 18:24:18 +0200 Subject: [PATCH 005/100] Redesign SearchResultScreen (#172) --- .../vimusic/ui/components/themed/Header.kt | 5 +- .../vimusic/ui/screens/IntentUriScreen.kt | 2 + .../vimusic/ui/screens/SearchResultScreen.kt | 601 ------------------ .../vimusic/ui/screens/home/HomeScreen.kt | 2 +- .../ui/screens/player/PlayerBottomSheet.kt | 2 +- .../ui/screens/search/OnlineSearchTab.kt | 1 - .../searchresult/ItemSearchResultTab.kt | 95 +++ .../searchresult/ItemSearchResultViewModel.kt | 43 ++ .../searchresult/SearchResultScreen.kt | 204 ++++++ .../vimusic/ui/views/YouTubeItems.kt | 297 +++++++++ .../it/vfsfitvnm/vimusic/utils/Preferences.kt | 1 + app/src/main/res/drawable/film.xml | 9 + 12 files changed, 657 insertions(+), 605 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt create mode 100644 app/src/main/res/drawable/film.xml diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt index 4d308a0..bdcc125 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt @@ -13,6 +13,7 @@ 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.text.style.TextOverflow import androidx.compose.ui.unit.dp import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.medium @@ -30,7 +31,9 @@ fun Header( titleContent = { BasicText( text = title, - style = typography.xxl.medium + style = typography.xxl.medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) }, actionsContent = actionsContent diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt index 675a8e0..c475ea2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt @@ -43,6 +43,8 @@ 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.SmallSongItem +import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt deleted file mode 100644 index a0f5973..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchResultScreen.kt +++ /dev/null @@ -1,601 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import androidx.compose.animation.ExperimentalAnimationApi -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.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -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.SideEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import coil.compose.AsyncImage -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness -import it.vfsfitvnm.vimusic.ui.components.ChipGroup -import it.vfsfitvnm.vimusic.ui.components.ChipItem -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError -import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.TextCard -import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder -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.styling.shimmer -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.forcePlay -import it.vfsfitvnm.vimusic.utils.medium -import it.vfsfitvnm.vimusic.utils.relaunchableEffect -import it.vfsfitvnm.vimusic.utils.rememberPreference -import it.vfsfitvnm.vimusic.utils.searchFilterKey -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@ExperimentalAnimationApi -@Composable -fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { - val (colorPalette, typography) = LocalAppearance.current - val binder = LocalPlayerServiceBinder.current - - var searchFilter by rememberPreference(searchFilterKey, YouTube.Item.Song.Filter.value) - - val lazyListState = rememberLazyListState() - - val items = remember(searchFilter) { - mutableStateListOf() - } - - var continuationResult by remember(searchFilter) { - mutableStateOf?>(null) - } - - val onLoad = relaunchableEffect(searchFilter) { - withContext(Dispatchers.Main) { - val token = continuationResult?.getOrNull() - - continuationResult = null - - continuationResult = withContext(Dispatchers.IO) { - YouTube.search(query, searchFilter, token) - }?.map { searchResult -> - items.addAll(searchResult.items) - searchResult.continuation - } - } - } - - val thumbnailSizePx = Dimensions.thumbnails.song.px - - RouteHandler(listenToGlobalEmitter = true) { - albumRoute { browseId -> - AlbumScreen( - browseId = browseId ?: "browseId cannot be null" - ) - } - - artistRoute { browseId -> - ArtistScreen( - browseId = browseId ?: "browseId cannot be null" - ) - } - - playlistRoute { browseId -> - PlaylistScreen( - browseId = browseId ?: "browseId cannot be null" - ) - } - - host { - LazyColumn( - state = lazyListState, - horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - - BasicText( - text = query, - style = typography.m.semiBold.center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = onSearchAgain - ) - .weight(1f) - ) - - Spacer( - modifier = Modifier - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - } - } - - item { - ChipGroup( - items = listOf( - ChipItem( - text = "Songs", - value = YouTube.Item.Song.Filter.value - ), - ChipItem( - text = "Albums", - value = YouTube.Item.Album.Filter.value - ), - ChipItem( - text = "Artists", - value = YouTube.Item.Artist.Filter.value - ), - ChipItem( - text = "Videos", - value = YouTube.Item.Video.Filter.value - ), - ChipItem( - text = "Playlists", - value = YouTube.Item.CommunityPlaylist.Filter.value - ), - ChipItem( - text = "Featured playlists", - value = YouTube.Item.FeaturedPlaylist.Filter.value - ), - ), - value = searchFilter, - selectedBackgroundColor = colorPalette.accent, - unselectedBackgroundColor = colorPalette.background1, - selectedTextStyle = typography.xs.medium.color(colorPalette.onAccent), - unselectedTextStyle = typography.xs.medium, - shape = RoundedCornerShape(36.dp), - onValueChanged = { - searchFilter = it - }, - modifier = Modifier - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .padding(bottom = 8.dp) - ) - } - - items( - items = items, - contentType = { it } - ) { item -> - SmallItem( - item = item, - thumbnailSizeDp = Dimensions.thumbnails.song, - thumbnailSizePx = thumbnailSizePx, - onClick = { - when (item) { - is YouTube.Item.Album -> albumRoute(item.info.endpoint!!.browseId) - is YouTube.Item.Artist -> artistRoute(item.info.endpoint!!.browseId) - is YouTube.Item.Playlist -> playlistRoute(item.info.endpoint!!.browseId) - is YouTube.Item.Song -> { - binder?.stopRadio() - binder?.player?.forcePlay(item.asMediaItem) - binder?.setupRadio(item.info.endpoint) - } - is YouTube.Item.Video -> { - binder?.stopRadio() - binder?.player?.forcePlay(item.asMediaItem) - binder?.setupRadio(item.info.endpoint) - } - } - } - ) - } - - continuationResult?.getOrNull()?.let { - if (items.isNotEmpty()) { - item { - SideEffect(onLoad) - } - } - } ?: continuationResult?.exceptionOrNull()?.let { throwable -> - item { - LoadingOrError( - errorMessage = throwable.javaClass.canonicalName, - onRetry = onLoad - ) - } - } ?: continuationResult?.let { - if (items.isEmpty()) { - item { - TextCard(icon = R.drawable.sad) { - Title(text = "No results found") - Text(text = "Please try a different query or category.") - } - } - } - } ?: item(key = "loading") { - LoadingOrError( - itemCount = if (items.isEmpty()) 8 else 3, - isLoadingArtists = searchFilter == YouTube.Item.Artist.Filter.value - ) - } - } - } - } -} - -@Composable -fun SmallSongItemShimmer( - thumbnailSizeDp: Dp, - modifier: Modifier = Modifier -) { - val (colorPalette, _, thumbnailShape) = LocalAppearance.current - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = thumbnailShape) - .size(thumbnailSizeDp) - ) - - Column { - TextPlaceholder() - TextPlaceholder() - } - } -} - -@Composable -fun SmallArtistItemShimmer( - thumbnailSizeDp: Dp, - modifier: Modifier = Modifier -) { - val (colorPalette) = LocalAppearance.current - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = CircleShape) - .size(thumbnailSizeDp) - ) - - TextPlaceholder() - } -} - -@ExperimentalAnimationApi -@Composable -fun SmallItem( - item: YouTube.Item, - thumbnailSizeDp: Dp, - thumbnailSizePx: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - when (item) { - is YouTube.Item.Artist -> SmallArtistItem( - artist = item, - thumbnailSizeDp = thumbnailSizeDp, - thumbnailSizePx = thumbnailSizePx, - modifier = modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onClick - ) - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - is YouTube.Item.Song -> SmallSongItem( - song = item, - thumbnailSizePx = thumbnailSizePx, - onClick = onClick, - modifier = modifier - ) - is YouTube.Item.Album -> SmallAlbumItem( - album = item, - thumbnailSizeDp = thumbnailSizeDp, - thumbnailSizePx = thumbnailSizePx, - modifier = modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onClick - ) - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - is YouTube.Item.Video -> SmallVideoItem( - video = item, - thumbnailSizePx = thumbnailSizePx, - onClick = onClick, - modifier = modifier - ) - is YouTube.Item.Playlist -> SmallPlaylistItem( - playlist = item, - thumbnailSizeDp = thumbnailSizeDp, - thumbnailSizePx = thumbnailSizePx, - modifier = modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = onClick - ) - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - } -} - -@ExperimentalAnimationApi -@Composable -fun SmallSongItem( - song: YouTube.Item.Song, - thumbnailSizePx: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - SongItem( - thumbnailModel = song.thumbnail?.size(thumbnailSizePx), - title = song.info.name, - authors = song.authors.joinToString("") { it.name }, - durationText = song.durationText, - onClick = onClick, - menuContent = { - NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) - }, - modifier = modifier - ) -} - -@ExperimentalAnimationApi -@Composable -fun SmallVideoItem( - video: YouTube.Item.Video, - thumbnailSizePx: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier -) { - SongItem( - thumbnailModel = video.thumbnail?.size(thumbnailSizePx), - title = video.info.name, - authors = (if (video.isOfficialMusicVideo) video.authors else video.views) - .joinToString("") { it.name }, - durationText = video.durationText, - onClick = onClick, - menuContent = { - NonQueuedMediaItemMenu(mediaItem = video.asMediaItem) - }, - modifier = modifier - ) -} - -@ExperimentalAnimationApi -@Composable -fun SmallPlaylistItem( - playlist: YouTube.Item.Playlist, - thumbnailSizeDp: Dp, - thumbnailSizePx: Int, - modifier: Modifier = Modifier -) { - val (_, typography) = LocalAppearance.current - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier - ) { - AsyncImage( - model = playlist.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(thumbnailSizeDp) - ) - - Column( - modifier = Modifier - .weight(1f) - ) { - BasicText( - text = playlist.info.name, - style = typography.xs.semiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - BasicText( - text = playlist.channel?.name ?: "", - style = typography.xs.semiBold.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - - playlist.songCount?.let { songCount -> - BasicText( - text = "$songCount songs", - style = typography.xxs.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -fun SmallAlbumItem( - album: YouTube.Item.Album, - thumbnailSizeDp: Dp, - thumbnailSizePx: Int, - modifier: Modifier = Modifier, -) { - val (_, typography) = LocalAppearance.current - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier - ) { - AsyncImage( - model = album.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(thumbnailSizeDp) - ) - - Column( - modifier = Modifier - .weight(1f) - ) { - BasicText( - text = album.info.name, - style = typography.xs.semiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - BasicText( - text = album.authors?.joinToString("") { it.name } ?: "", - style = typography.xs.semiBold.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - - album.year?.let { year -> - BasicText( - text = year, - style = typography.xxs.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } - } -} - -@Composable -fun SmallArtistItem( - artist: YouTube.Item.Artist, - thumbnailSizeDp: Dp, - thumbnailSizePx: Int, - modifier: Modifier = Modifier, -) { - val (_, typography) = LocalAppearance.current - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = modifier - ) { - AsyncImage( - model = artist.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - modifier = Modifier - .clip(CircleShape) - .size(thumbnailSizeDp) - ) - - BasicText( - text = artist.info.name, - style = typography.xs.semiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .weight(1f) - ) - } -} - -@Composable -private fun LoadingOrError( - itemCount: Int = 0, - isLoadingArtists: Boolean = false, - errorMessage: String? = null, - onRetry: (() -> Unit)? = null -) { - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry, - horizontalAlignment = Alignment.CenterHorizontally - ) { - repeat(itemCount) { index -> - if (isLoadingArtists) { - SmallArtistItemShimmer( - thumbnailSizeDp = Dimensions.thumbnails.song, - modifier = Modifier - .alpha(1f - index * 0.125f) - .fillMaxWidth() - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - } else { - SmallSongItemShimmer( - thumbnailSizeDp = Dimensions.thumbnails.song, - modifier = Modifier - .alpha(1f - index * 0.125f) - .fillMaxWidth() - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 13fe42b..8d4e28a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -14,7 +14,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen -import it.vfsfitvnm.vimusic.ui.screens.SearchResultScreen +import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResultScreen import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index e3c9da0..11d51a5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -40,7 +40,7 @@ import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.components.MusicBars import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu -import it.vfsfitvnm.vimusic.ui.screens.SmallSongItemShimmer +import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.onOverlay diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt index 9e24905..de99708 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt @@ -3,7 +3,6 @@ package it.vfsfitvnm.vimusic.ui.screens.search import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt new file mode 100644 index 0000000..4971fe8 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt @@ -0,0 +1,95 @@ +package it.vfsfitvnm.vimusic.ui.screens.searchresult + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyItemScope +import androidx.compose.foundation.lazy.items +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInput +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.TextCard +import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError +import it.vfsfitvnm.youtubemusic.YouTube + +@ExperimentalAnimationApi +@Composable +inline fun ItemSearchResultTab( + query: String, + filter: String, + crossinline onSearchAgain: () -> Unit, + isArtists: Boolean = false, + viewModel: ItemSearchResultViewModel = viewModel( + key = query + filter, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return ItemSearchResultViewModel(query, filter) as T + } + } + ), + crossinline itemContent: @Composable (LazyItemScope.(I) -> Unit) +) { + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header( + title = query, + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + onSearchAgain() + } + } + ) + } + + items( + items = viewModel.items, + itemContent = itemContent + ) + + viewModel.continuationResult?.getOrNull()?.let { + if (viewModel.items.isNotEmpty()) { + item { + SideEffect(viewModel::fetch) + } + } + } ?: viewModel.continuationResult?.exceptionOrNull()?.let { throwable -> + item { + SearchResultLoadingOrError( + errorMessage = throwable.javaClass.canonicalName, + onRetry = viewModel::fetch + ) + } + } ?: viewModel.continuationResult?.let { + if (viewModel.items.isEmpty()) { + item { + TextCard(icon = R.drawable.sad) { + Title(text = "No results found") + Text(text = "Please try a different query or category.") + } + } + } + } ?: item(key = "loading") { + SearchResultLoadingOrError( + itemCount = if (viewModel.items.isEmpty()) 8 else 3, + isLoadingArtists = isArtists + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt new file mode 100644 index 0000000..cf7057c --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt @@ -0,0 +1,43 @@ +package it.vfsfitvnm.vimusic.ui.screens.searchresult + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ItemSearchResultViewModel(private val query: String, private val filter: String) : ViewModel() { + val items = mutableStateListOf() + + var continuationResult by mutableStateOf?>(null) + + private var job: Job? = null + + init { + fetch() + } + + fun fetch() { + job?.cancel() + + viewModelScope.launch { + val token = continuationResult?.getOrNull() + + continuationResult = null + + continuationResult = withContext(Dispatchers.IO) { + YouTube.search(query, filter, token) + }?.map { searchResult -> + @Suppress("UNCHECKED_CAST") + items.addAll(searchResult.items as List) + searchResult.continuation + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt new file mode 100644 index 0000000..7cbb0b5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -0,0 +1,204 @@ +package it.vfsfitvnm.vimusic.ui.screens.searchresult + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.padding +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen +import it.vfsfitvnm.vimusic.ui.screens.albumRoute +import it.vfsfitvnm.vimusic.ui.screens.artistRoute +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.ui.screens.playlistRoute +import it.vfsfitvnm.vimusic.ui.styling.Dimensions +import it.vfsfitvnm.vimusic.ui.styling.px +import it.vfsfitvnm.vimusic.ui.views.SmallAlbumItem +import it.vfsfitvnm.vimusic.ui.views.SmallArtistItem +import it.vfsfitvnm.vimusic.ui.views.SmallPlaylistItem +import it.vfsfitvnm.vimusic.ui.views.SmallSongItem +import it.vfsfitvnm.vimusic.ui.views.SmallVideoItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.forcePlay +import it.vfsfitvnm.vimusic.utils.rememberPreference +import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey +import it.vfsfitvnm.youtubemusic.YouTube + +@ExperimentalAnimationApi +@Composable +fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { + val saveableStateHolder = rememberSaveableStateHolder() + val (tabIndex, onTabIndexChanges) = rememberPreference(searchResultScreenTabIndexKey, 0) + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + playlistRoute { browseId -> + PlaylistScreen( + browseId = browseId ?: "browseId cannot be null" + ) + } + + host { + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = tabIndex, + onTabChanged = onTabIndexChanges, + tabColumnContent = { Item -> + Item(0, "Songs", R.drawable.musical_notes) + Item(1, "Albums", R.drawable.disc) + Item(2, "Artists", R.drawable.person) + Item(3, "Videos", R.drawable.film) + Item(4, "Playlists", R.drawable.playlist) + Item(5, "Featured", R.drawable.playlist) + } + ) { tabIndex -> + val searchFilter = when (tabIndex) { + 0 -> YouTube.Item.Song.Filter + 1 -> YouTube.Item.Album.Filter + 2 -> YouTube.Item.Artist.Filter + 3 -> YouTube.Item.Video.Filter + 4 -> YouTube.Item.CommunityPlaylist.Filter + 5 -> YouTube.Item.FeaturedPlaylist.Filter + else -> error("unreachable") + }.value + + saveableStateHolder.SaveableStateProvider(tabIndex) { + when (tabIndex) { + 0 -> { + val binder = LocalPlayerServiceBinder.current + val thumbnailSizePx = Dimensions.thumbnails.song.px + + ItemSearchResultTab( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain + ) { song -> + SmallSongItem( + song = song, + thumbnailSizePx = thumbnailSizePx, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlay(song.asMediaItem) + binder?.setupRadio(song.info.endpoint) + } + ) + } + } + + 1 -> { + val thumbnailSizeDp = Dimensions.thumbnails.song + val thumbnailSizePx = thumbnailSizeDp.px + + ItemSearchResultTab( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain + ) { album -> + SmallAlbumItem( + album = album, + thumbnailSizePx = thumbnailSizePx, + thumbnailSizeDp = thumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { albumRoute(album.info.endpoint?.browseId) } + ) + .padding( + vertical = Dimensions.itemsVerticalPadding, + horizontal = 16.dp + ) + ) + } + } + + 2 -> { + val thumbnailSizeDp = Dimensions.thumbnails.song + val thumbnailSizePx = thumbnailSizeDp.px + + ItemSearchResultTab( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain, + isArtists = true + ) { artist -> + SmallArtistItem( + artist = artist, + thumbnailSizePx = thumbnailSizePx, + thumbnailSizeDp = thumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { artistRoute(artist.info.endpoint?.browseId) } + ) + .padding( + vertical = Dimensions.itemsVerticalPadding, + horizontal = 16.dp + ) + ) + } + } + 3 -> { + val binder = LocalPlayerServiceBinder.current + val thumbnailSizePx = Dimensions.thumbnails.song.px + + ItemSearchResultTab( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain + ) { video -> + SmallVideoItem( + video = video, + thumbnailSizePx = thumbnailSizePx, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlay(video.asMediaItem) + binder?.setupRadio(video.info.endpoint) + } + ) + } + } + + 4, 5 -> { + val thumbnailSizeDp = Dimensions.thumbnails.song + val thumbnailSizePx = thumbnailSizeDp.px + + ItemSearchResultTab( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain + ) { playlist -> + SmallPlaylistItem( + playlist = playlist, + thumbnailSizePx = thumbnailSizePx, + thumbnailSizeDp = thumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { playlistRoute(playlist.info.endpoint?.browseId) } + ) + .padding( + vertical = Dimensions.itemsVerticalPadding, + horizontal = 16.dp + ) + ) + } + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt new file mode 100644 index 0000000..93fb40a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -0,0 +1,297 @@ +package it.vfsfitvnm.vimusic.ui.views + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.background +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +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.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness +import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +import it.vfsfitvnm.vimusic.ui.styling.Dimensions +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.ui.styling.shimmer +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.youtubemusic.YouTube + +@Composable +fun SmallSongItemShimmer( + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier +) { + val (colorPalette, _, thumbnailShape) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(thumbnailSizeDp) + ) + + Column { + TextPlaceholder() + TextPlaceholder() + } + } +} + +@Composable +fun SmallArtistItemShimmer( + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier +) { + val (colorPalette) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = CircleShape) + .size(thumbnailSizeDp) + ) + + TextPlaceholder() + } +} + + +@ExperimentalAnimationApi +@Composable +fun SmallSongItem( + song: YouTube.Item.Song, + thumbnailSizePx: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + SongItem( + thumbnailModel = song.thumbnail?.size(thumbnailSizePx), + title = song.info.name, + authors = song.authors.joinToString("") { it.name }, + durationText = song.durationText, + onClick = onClick, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + }, + modifier = modifier + ) +} + +@ExperimentalAnimationApi +@Composable +fun SmallVideoItem( + video: YouTube.Item.Video, + thumbnailSizePx: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + SongItem( + thumbnailModel = video.thumbnail?.size(thumbnailSizePx), + title = video.info.name, + authors = (if (video.isOfficialMusicVideo) video.authors else video.views) + .joinToString("") { it.name }, + durationText = video.durationText, + onClick = onClick, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = video.asMediaItem) + }, + modifier = modifier + ) +} + +@ExperimentalAnimationApi +@Composable +fun SmallPlaylistItem( + playlist: YouTube.Item.Playlist, + thumbnailSizePx: Int, + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier +) { + val (_, typography) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + AsyncImage( + model = playlist.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(ThumbnailRoundness.shape) + .size(thumbnailSizeDp) + ) + + Column( + modifier = Modifier + .weight(1f) + ) { + BasicText( + text = playlist.info.name, + style = typography.xs.semiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + BasicText( + text = playlist.channel?.name ?: "", + style = typography.xs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + playlist.songCount?.let { songCount -> + BasicText( + text = "$songCount songs", + style = typography.xxs.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +fun SmallAlbumItem( + album: YouTube.Item.Album, + thumbnailSizePx: Int, + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier, +) { + val (_, typography) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + AsyncImage( + model = album.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(ThumbnailRoundness.shape) + .size(thumbnailSizeDp) + ) + + Column( + modifier = Modifier + .weight(1f) + ) { + BasicText( + text = album.info.name, + style = typography.xs.semiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + BasicText( + text = album.authors?.joinToString("") { it.name } ?: "", + style = typography.xs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + + album.year?.let { year -> + BasicText( + text = year, + style = typography.xxs.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +fun SmallArtistItem( + artist: YouTube.Item.Artist, + thumbnailSizePx: Int, + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier, +) { + val (_, typography) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + ) { + AsyncImage( + model = artist.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .size(thumbnailSizeDp) + ) + + BasicText( + text = artist.info.name, + style = typography.xs.semiBold, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f) + ) + } +} + +@Composable +fun SearchResultLoadingOrError( + itemCount: Int = 0, + isLoadingArtists: Boolean = false, + errorMessage: String? = null, + onRetry: (() -> Unit)? = null +) { + LoadingOrError( + errorMessage = errorMessage, + onRetry = onRetry, + horizontalAlignment = Alignment.CenterHorizontally + ) { + repeat(itemCount) { index -> + if (isLoadingArtists) { + SmallArtistItemShimmer( + thumbnailSizeDp = Dimensions.thumbnails.song, + modifier = Modifier + .alpha(1f - index * 0.125f) + .fillMaxWidth() + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + ) + } else { + SmallSongItemShimmer( + thumbnailSizeDp = Dimensions.thumbnails.song, + modifier = Modifier + .alpha(1f - index * 0.125f) + .fillMaxWidth() + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt index 945bc3f..aafcb71 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt @@ -29,6 +29,7 @@ const val persistentQueueKey = "persistentQueue" const val isShowingSynchronizedLyricsKey = "isShowingSynchronizedLyrics" const val isShowingThumbnailInLockscreenKey = "isShowingThumbnailInLockscreen" const val homeScreenTabIndexKey = "homeScreenTabIndex" +const val searchResultScreenTabIndexKey = "searchResultScreenTabIndex" inline fun > SharedPreferences.getEnum( key: String, diff --git a/app/src/main/res/drawable/film.xml b/app/src/main/res/drawable/film.xml new file mode 100644 index 0000000..5e334a8 --- /dev/null +++ b/app/src/main/res/drawable/film.xml @@ -0,0 +1,9 @@ + + + From 8db6f7a13e930a69af6af46c555927a295381edb Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 24 Sep 2022 10:15:25 +0200 Subject: [PATCH 006/100] Do not show "Search" button in OnlineSearchTab --- .../ui/screens/search/OnlineSearchTab.kt | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt index de99708..015a32b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt @@ -138,18 +138,18 @@ fun OnlineSearchTab( ) }, actionsContent = { - BasicText( - text = if (isOpenableUrl) "Open url" else "Search", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = textFieldValue.text.isNotEmpty()) { - if (isOpenableUrl) onUri() else onSearch(textFieldValue.text) - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) - ) + if (isOpenableUrl) { + BasicText( + text = "Open url", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onUri) + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } Spacer( modifier = Modifier From e71e34c0d74c29fa21728020419d31d6261d7bab Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 24 Sep 2022 13:02:52 +0200 Subject: [PATCH 007/100] Improve SearchResultScreen UI --- .../it/vfsfitvnm/vimusic/MainActivity.kt | 4 +- ...SearchResultTab.kt => ItemSearchResult.kt} | 15 +- .../searchresult/ItemSearchResultViewModel.kt | 10 +- .../searchresult/SearchResultScreen.kt | 230 +++++----- .../vimusic/ui/views/YouTubeItems.kt | 406 +++++++++++++----- .../it/vfsfitvnm/youtubemusic/YouTube.kt | 16 + 6 files changed, 450 insertions(+), 231 deletions(-) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/{ItemSearchResultTab.kt => ItemSearchResult.kt} (87%) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index 43792b7..67c712b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -63,15 +63,15 @@ import it.vfsfitvnm.vimusic.ui.components.collapsedAnchor import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState -import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen +import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen +import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView import it.vfsfitvnm.vimusic.ui.styling.Appearance import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.typographyOf -import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey import it.vfsfitvnm.vimusic.utils.getEnum diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResult.kt similarity index 87% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResult.kt index 4971fe8..a78686c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResult.kt @@ -2,6 +2,7 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope @@ -22,11 +23,10 @@ import it.vfsfitvnm.youtubemusic.YouTube @ExperimentalAnimationApi @Composable -inline fun ItemSearchResultTab( +inline fun ItemSearchResult( query: String, filter: String, crossinline onSearchAgain: () -> Unit, - isArtists: Boolean = false, viewModel: ItemSearchResultViewModel = viewModel( key = query + filter, factory = object : ViewModelProvider.Factory { @@ -36,7 +36,8 @@ inline fun ItemSearchResultTab( } } ), - crossinline itemContent: @Composable (LazyItemScope.(I) -> Unit) + crossinline itemContent: @Composable LazyItemScope.(I) -> Unit, + noinline itemShimmer: @Composable BoxScope.() -> Unit, ) { LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, @@ -45,7 +46,7 @@ inline fun ItemSearchResultTab( ) { item( key = "header", - contentType = 0 + contentType = 0, ) { Header( title = query, @@ -60,6 +61,7 @@ inline fun ItemSearchResultTab( items( items = viewModel.items, + key = { it.key!! }, itemContent = itemContent ) @@ -73,7 +75,8 @@ inline fun ItemSearchResultTab( item { SearchResultLoadingOrError( errorMessage = throwable.javaClass.canonicalName, - onRetry = viewModel::fetch + onRetry = viewModel::fetch, + shimmerContent = {} ) } } ?: viewModel.continuationResult?.let { @@ -88,7 +91,7 @@ inline fun ItemSearchResultTab( } ?: item(key = "loading") { SearchResultLoadingOrError( itemCount = if (viewModel.items.isEmpty()) 8 else 3, - isLoadingArtists = isArtists + shimmerContent = itemShimmer ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt index cf7057c..e754cf5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt @@ -1,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel @@ -12,8 +11,11 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ItemSearchResultViewModel(private val query: String, private val filter: String) : ViewModel() { - val items = mutableStateListOf() +class ItemSearchResultViewModel( + private val query: String, + private val filter: String +) : ViewModel() { + var items by mutableStateOf(listOf()) var continuationResult by mutableStateOf?>(null) @@ -35,7 +37,7 @@ class ItemSearchResultViewModel(private val query: String, pri YouTube.search(query, filter, token) }?.map { searchResult -> @Suppress("UNCHECKED_CAST") - items.addAll(searchResult.items as List) + items = items.plus(searchResult.items as List).distinctBy(YouTube.Item::key) searchResult.continuation } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index 7cbb0b5..3252377 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -1,9 +1,9 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.padding import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -21,17 +21,23 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.playlistRoute import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.px -import it.vfsfitvnm.vimusic.ui.views.SmallAlbumItem -import it.vfsfitvnm.vimusic.ui.views.SmallArtistItem -import it.vfsfitvnm.vimusic.ui.views.SmallPlaylistItem +import it.vfsfitvnm.vimusic.ui.views.AlbumItem +import it.vfsfitvnm.vimusic.ui.views.AlbumItemShimmer +import it.vfsfitvnm.vimusic.ui.views.ArtistItem +import it.vfsfitvnm.vimusic.ui.views.ArtistItemShimmer +import it.vfsfitvnm.vimusic.ui.views.PlaylistItem +import it.vfsfitvnm.vimusic.ui.views.PlaylistItemShimmer import it.vfsfitvnm.vimusic.ui.views.SmallSongItem -import it.vfsfitvnm.vimusic.ui.views.SmallVideoItem +import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer +import it.vfsfitvnm.vimusic.ui.views.VideoItem +import it.vfsfitvnm.vimusic.ui.views.VideoItemShimmer import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey import it.vfsfitvnm.youtubemusic.YouTube +@ExperimentalFoundationApi @ExperimentalAnimationApi @Composable fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { @@ -76,125 +82,139 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { when (tabIndex) { 0 -> { val binder = LocalPlayerServiceBinder.current - val thumbnailSizePx = Dimensions.thumbnails.song.px - - ItemSearchResultTab( - query = query, - filter = searchFilter, - onSearchAgain = onSearchAgain - ) { song -> - SmallSongItem( - song = song, - thumbnailSizePx = thumbnailSizePx, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlay(song.asMediaItem) - binder?.setupRadio(song.info.endpoint) - } - ) - } - } - - 1 -> { val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResultTab( - query = query, - filter = searchFilter, - onSearchAgain = onSearchAgain - ) { album -> - SmallAlbumItem( - album = album, - thumbnailSizePx = thumbnailSizePx, - thumbnailSizeDp = thumbnailSizeDp, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { albumRoute(album.info.endpoint?.browseId) } - ) - .padding( - vertical = Dimensions.itemsVerticalPadding, - horizontal = 16.dp - ) - ) - } - } - - 2 -> { - val thumbnailSizeDp = Dimensions.thumbnails.song - val thumbnailSizePx = thumbnailSizeDp.px - - ItemSearchResultTab( + ItemSearchResult( query = query, filter = searchFilter, onSearchAgain = onSearchAgain, - isArtists = true - ) { artist -> - SmallArtistItem( - artist = artist, - thumbnailSizePx = thumbnailSizePx, - thumbnailSizeDp = thumbnailSizeDp, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { artistRoute(artist.info.endpoint?.browseId) } - ) - .padding( - vertical = Dimensions.itemsVerticalPadding, - horizontal = 16.dp - ) - ) - } + itemContent = { song -> + SmallSongItem( + song = song, + thumbnailSizePx = thumbnailSizePx, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlay(song.asMediaItem) + binder?.setupRadio(song.info.endpoint) + } + ) + }, + itemShimmer = { + SmallSongItemShimmer(thumbnailSizeDp = thumbnailSizeDp) + } + ) + } + + 1 -> { + val thumbnailSizeDp = 108.dp + val thumbnailSizePx = thumbnailSizeDp.px + + ItemSearchResult( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain, + itemContent = { album -> + AlbumItem( + album = album, + thumbnailSizePx = thumbnailSizePx, + thumbnailSizeDp = thumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { albumRoute(album.info.endpoint?.browseId) } + ) + ) + + }, + itemShimmer = { + AlbumItemShimmer(thumbnailSizeDp = thumbnailSizeDp) + } + ) + } + + 2 -> { + val thumbnailSizeDp = 64.dp + val thumbnailSizePx = thumbnailSizeDp.px + + ItemSearchResult( + query = query, + filter = searchFilter, + onSearchAgain = onSearchAgain, + itemContent = { artist -> + ArtistItem( + artist = artist, + thumbnailSizePx = thumbnailSizePx, + thumbnailSizeDp = thumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { artistRoute(artist.info.endpoint?.browseId) } + ) + ) + }, + itemShimmer = { + ArtistItemShimmer(thumbnailSizeDp = thumbnailSizeDp) + } + ) } 3 -> { val binder = LocalPlayerServiceBinder.current - val thumbnailSizePx = Dimensions.thumbnails.song.px + val thumbnailHeightDp = 72.dp + val thumbnailWidthDp = 128.dp - ItemSearchResultTab( + ItemSearchResult( query = query, filter = searchFilter, - onSearchAgain = onSearchAgain - ) { video -> - SmallVideoItem( - video = video, - thumbnailSizePx = thumbnailSizePx, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlay(video.asMediaItem) - binder?.setupRadio(video.info.endpoint) - } - ) - } + onSearchAgain = onSearchAgain, + itemContent = { video -> + VideoItem( + video = video, + thumbnailWidthDp = thumbnailWidthDp, + thumbnailHeightDp = thumbnailHeightDp, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlay(video.asMediaItem) + binder?.setupRadio(video.info.endpoint) + } + ) + }, + itemShimmer = { + VideoItemShimmer( + thumbnailHeightDp = thumbnailHeightDp, + thumbnailWidthDp = thumbnailWidthDp + ) + } + ) } 4, 5 -> { - val thumbnailSizeDp = Dimensions.thumbnails.song + val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResultTab( + ItemSearchResult( query = query, filter = searchFilter, - onSearchAgain = onSearchAgain - ) { playlist -> - SmallPlaylistItem( - playlist = playlist, - thumbnailSizePx = thumbnailSizePx, - thumbnailSizeDp = thumbnailSizeDp, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { playlistRoute(playlist.info.endpoint?.browseId) } - ) - .padding( - vertical = Dimensions.itemsVerticalPadding, - horizontal = 16.dp - ) - ) - } + onSearchAgain = onSearchAgain, + itemContent = { playlist -> + PlaylistItem( + playlist = playlist, + thumbnailSizePx = thumbnailSizePx, + thumbnailSizeDp = thumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { playlistRoute(playlist.info.endpoint?.browseId) } + ) + ) + }, + itemShimmer = { + PlaylistItemShimmer(thumbnailSizeDp = thumbnailSizeDp) + } + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index 93fb40a..fa59313 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -1,8 +1,13 @@ package it.vfsfitvnm.vimusic.ui.views import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource 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.Row import androidx.compose.foundation.layout.Spacer @@ -10,8 +15,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape +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.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -21,14 +29,18 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness +import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.ui.styling.onOverlay +import it.vfsfitvnm.vimusic.ui.styling.overlay import it.vfsfitvnm.vimusic.ui.styling.shimmer import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.color +import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.youtubemusic.YouTube @@ -44,6 +56,8 @@ fun SmallSongItemShimmer( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) ) { Spacer( modifier = Modifier @@ -58,29 +72,6 @@ fun SmallSongItemShimmer( } } -@Composable -fun SmallArtistItemShimmer( - thumbnailSizeDp: Dp, - modifier: Modifier = Modifier -) { - val (colorPalette) = LocalAppearance.current - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = modifier - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = CircleShape) - .size(thumbnailSizeDp) - ) - - TextPlaceholder() - } -} - - @ExperimentalAnimationApi @Composable fun SmallSongItem( @@ -102,74 +93,80 @@ fun SmallSongItem( ) } +@ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun SmallVideoItem( +fun VideoItem( video: YouTube.Item.Video, - thumbnailSizePx: Int, + thumbnailHeightDp: Dp, + thumbnailWidthDp: Dp, onClick: () -> Unit, modifier: Modifier = Modifier ) { - SongItem( - thumbnailModel = video.thumbnail?.size(thumbnailSizePx), - title = video.info.name, - authors = (if (video.isOfficialMusicVideo) video.authors else video.views) - .joinToString("") { it.name }, - durationText = video.durationText, - onClick = onClick, - menuContent = { - NonQueuedMediaItemMenu(mediaItem = video.asMediaItem) - }, - modifier = modifier - ) -} - -@ExperimentalAnimationApi -@Composable -fun SmallPlaylistItem( - playlist: YouTube.Item.Playlist, - thumbnailSizePx: Int, - thumbnailSizeDp: Dp, - modifier: Modifier = Modifier -) { - val (_, typography) = LocalAppearance.current + val menuState = LocalMenuState.current + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier + .combinedClickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onLongClick = { + menuState.display { + NonQueuedMediaItemMenu(mediaItem = video.asMediaItem) + } + }, + onClick = onClick + ) + .fillMaxWidth() + .padding(vertical = Dimensions.itemsVerticalPadding) + .padding(horizontal = 16.dp) ) { - AsyncImage( - model = playlist.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(thumbnailSizeDp) - ) + Box { + AsyncImage( + model = video.thumbnail?.url, + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(thumbnailShape) + .size(width = thumbnailWidthDp, height = thumbnailHeightDp) + ) - Column( - modifier = Modifier - .weight(1f) - ) { + video.durationText?.let { durationText -> + BasicText( + text = durationText, + style = typography.xxs.medium.color(colorPalette.onOverlay), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(all = 4.dp) + .background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp)) + .padding(horizontal = 4.dp, vertical = 2.dp) + .align(Alignment.BottomEnd) + ) + } + } + + Column { BasicText( - text = playlist.info.name, + text = video.info.name, style = typography.xs.semiBold, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) + BasicText( - text = playlist.channel?.name ?: "", + text = video.authors.joinToString("") { it.name }, style = typography.xs.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - } - playlist.songCount?.let { songCount -> BasicText( - text = "$songCount songs", - style = typography.xxs.secondary, + text = video.views.firstOrNull()?.name ?: "", + style = typography.xxs.medium.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) @@ -177,60 +174,208 @@ fun SmallPlaylistItem( } } +@ExperimentalFoundationApi +@ExperimentalAnimationApi @Composable -fun SmallAlbumItem( +fun VideoItemShimmer( + thumbnailHeightDp: Dp, + thumbnailWidthDp: Dp, + modifier: Modifier = Modifier +) { + val (colorPalette, _, thumbnailShape) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(width = thumbnailWidthDp, height = thumbnailHeightDp) + ) + + Column { + TextPlaceholder() + TextPlaceholder() + TextPlaceholder() + } + } +} + +@Composable +fun PlaylistItem( + playlist: YouTube.Item.Playlist, + thumbnailSizePx: Int, + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier, +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() + ) { + Box { + AsyncImage( + model = playlist.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + + playlist.songCount?.let { songCount -> + BasicText( + text = "$songCount", + style = typography.xxs.medium.color(colorPalette.onOverlay), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(all = 4.dp) + .background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp)) + .padding(horizontal = 4.dp, vertical = 2.dp) + .align(Alignment.BottomEnd) + ) + } + } + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + BasicText( + text = playlist.info.name, + style = typography.xs.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + + BasicText( + text = playlist.channel?.name ?: "", + style = typography.xs.semiBold.secondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } + } +} + +@Composable +fun PlaylistItemShimmer( + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier, +) { + val (colorPalette, _, thumbnailShape) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(thumbnailSizeDp) + ) + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + TextPlaceholder() + TextPlaceholder() + TextPlaceholder() + } + } +} + +@Composable +fun AlbumItem( album: YouTube.Item.Album, thumbnailSizePx: Int, thumbnailSizeDp: Dp, modifier: Modifier = Modifier, ) { - val (_, typography) = LocalAppearance.current + val (_, typography, thumbnailShape) = LocalAppearance.current Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() ) { AsyncImage( model = album.thumbnail?.size(thumbnailSizePx), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier - .clip(ThumbnailRoundness.shape) + .clip(thumbnailShape) .size(thumbnailSizeDp) ) - Column( - modifier = Modifier - .weight(1f) - ) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( text = album.info.name, style = typography.xs.semiBold, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) + BasicText( text = album.authors?.joinToString("") { it.name } ?: "", style = typography.xs.semiBold.secondary, - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis, ) - } - album.year?.let { year -> - BasicText( - text = year, - style = typography.xxs.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + album.year?.let { year -> + BasicText( + text = year, + style = typography.xxs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(top = 8.dp) + ) + } } } } @Composable -fun SmallArtistItem( +fun AlbumItemShimmer( + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier, +) { + val (colorPalette, _, thumbnailShape) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(thumbnailSizeDp) + ) + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + TextPlaceholder() + TextPlaceholder() + TextPlaceholder() + } + } +} + +@Composable +fun ArtistItem( artist: YouTube.Item.Artist, thumbnailSizePx: Int, thumbnailSizeDp: Dp, @@ -240,8 +385,10 @@ fun SmallArtistItem( Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = modifier + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() ) { AsyncImage( model = artist.thumbnail?.size(thumbnailSizePx), @@ -251,23 +398,49 @@ fun SmallArtistItem( .size(thumbnailSizeDp) ) - BasicText( - text = artist.info.name, - style = typography.xs.semiBold, - maxLines = 1, - overflow = TextOverflow.Ellipsis, + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + BasicText( + text = artist.info.name, + style = typography.xs.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } +} + +@Composable +fun ArtistItemShimmer( + thumbnailSizeDp: Dp, + modifier: Modifier = Modifier, +) { + val (colorPalette) = LocalAppearance.current + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() + ) { + Spacer( modifier = Modifier - .weight(1f) + .background(color = colorPalette.shimmer, shape = CircleShape) + .size(thumbnailSizeDp) ) + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + TextPlaceholder() + } } } @Composable fun SearchResultLoadingOrError( itemCount: Int = 0, - isLoadingArtists: Boolean = false, errorMessage: String? = null, - onRetry: (() -> Unit)? = null + onRetry: (() -> Unit)? = null, + shimmerContent: @Composable BoxScope.() -> Unit, ) { LoadingOrError( errorMessage = errorMessage, @@ -275,23 +448,28 @@ fun SearchResultLoadingOrError( horizontalAlignment = Alignment.CenterHorizontally ) { repeat(itemCount) { index -> - if (isLoadingArtists) { - SmallArtistItemShimmer( - thumbnailSizeDp = Dimensions.thumbnails.song, - modifier = Modifier - .alpha(1f - index * 0.125f) - .fillMaxWidth() - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - } else { - SmallSongItemShimmer( - thumbnailSizeDp = Dimensions.thumbnails.song, - modifier = Modifier - .alpha(1f - index * 0.125f) - .fillMaxWidth() - .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) - ) - } + Box( + modifier = Modifier + .alpha(1f - index * 0.125f), + content = shimmerContent + ) +// if (isLoadingArtists) { +// SmallArtistItemShimmer( +// thumbnailSizeDp = Dimensions.thumbnails.song, +// modifier = Modifier +// .alpha(1f - index * 0.125f) +// .fillMaxWidth() +// .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) +// ) +// } else { +// SmallSongItemShimmer( +// thumbnailSizeDp = Dimensions.thumbnails.song, +// modifier = Modifier +// .alpha(1f - index * 0.125f) +// .fillMaxWidth() +// .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) +// ) +// } } } } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index c24f77b..5407590 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -177,6 +177,7 @@ object YouTube { sealed class Item { abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? + abstract val key: String? data class Song( val info: Info, @@ -185,6 +186,9 @@ object YouTube { val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { + override val key: String? + get() = info.endpoint?.videoId + companion object : FromMusicShelfRendererContent { val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D") @@ -232,6 +236,9 @@ object YouTube { val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { + override val key: String? + get() = info.endpoint?.videoId + val isOfficialMusicVideo: Boolean get() = info .endpoint @@ -278,6 +285,9 @@ object YouTube { val year: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { + override val key: String? + get() = info.endpoint?.browseId + companion object : FromMusicShelfRendererContent { val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D") @@ -312,6 +322,9 @@ object YouTube { val info: Info, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { + override val key: String? + get() = info.endpoint?.browseId + companion object : FromMusicShelfRendererContent { val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D") @@ -341,6 +354,9 @@ object YouTube { val songCount: Int?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { + override val key: String? + get() = info.endpoint?.browseId + companion object : FromMusicShelfRendererContent { override fun from(content: MusicShelfRenderer.Content): Playlist { val (mainRuns, otherRuns) = content.runs From 4362456995a3e528ee9eb390614db89d99001494 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 24 Sep 2022 13:12:54 +0200 Subject: [PATCH 008/100] Retrieve YouTube artists subscribers count --- .../vimusic/ui/views/YouTubeItems.kt | 44 ++++++++++++++----- .../it/vfsfitvnm/youtubemusic/YouTube.kt | 7 ++- 2 files changed, 40 insertions(+), 11 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index fa59313..467eb79 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -164,12 +164,16 @@ fun VideoItem( overflow = TextOverflow.Ellipsis, ) - BasicText( - text = video.views.firstOrNull()?.name ?: "", - style = typography.xxs.medium.secondary, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + video.views.firstOrNull()?.name?.let { viewsText -> + BasicText( + text = viewsText, + style = typography.xxs.medium.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(top = 8.dp) + ) + } } } } @@ -200,7 +204,10 @@ fun VideoItemShimmer( Column { TextPlaceholder() TextPlaceholder() - TextPlaceholder() + TextPlaceholder( + modifier = Modifier + .padding(top = 8.dp) + ) } } } @@ -287,7 +294,6 @@ fun PlaylistItemShimmer( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { TextPlaceholder() TextPlaceholder() - TextPlaceholder() } } } @@ -369,7 +375,10 @@ fun AlbumItemShimmer( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { TextPlaceholder() TextPlaceholder() - TextPlaceholder() + TextPlaceholder( + modifier = Modifier + .padding(top = 8.dp) + ) } } } @@ -405,6 +414,17 @@ fun ArtistItem( maxLines = 2, overflow = TextOverflow.Ellipsis ) + + artist.subscribersCountText?.let { subscribersCountText -> + BasicText( + text = subscribersCountText, + style = typography.xxs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(top = 4.dp) + ) + } } } } @@ -430,7 +450,11 @@ fun ArtistItemShimmer( ) Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - TextPlaceholder() + TextPlaceholder() + TextPlaceholder( + modifier = Modifier + .padding(top = 4.dp) + ) } } } diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index 5407590..7eec479 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -320,6 +320,7 @@ object YouTube { data class Artist( val info: Info, + val subscribersCountText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { override val key: String? @@ -329,7 +330,7 @@ object YouTube { val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D") override fun from(content: MusicShelfRenderer.Content): Artist { - val (mainRuns) = content.runs + val (mainRuns, otherRuns) = content.runs return Artist( info = Info( @@ -341,6 +342,10 @@ object YouTube { .navigationEndpoint ?.browseEndpoint ), + subscribersCountText = otherRuns + .lastOrNull() + ?.last() + ?.text, thumbnail = content .thumbnail ) From a54b795666107eb70eaec386257fc74e0d63b4fc Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 24 Sep 2022 13:15:51 +0200 Subject: [PATCH 009/100] Do not show "Clear" text button in SearchScreen if the text input is already empty --- .../ui/screens/search/LibrarySearchTab.kt | 24 +++++++++---------- .../ui/screens/search/OnlineSearchTab.kt | 24 +++++++++---------- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt index 5ffcf39..1579b83 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt @@ -105,18 +105,18 @@ fun LibrarySearchTab( ) }, actionsContent = { - BasicText( - text = "Clear", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = textFieldValue.text.isNotEmpty()) { - onTextFieldValueChanged(TextFieldValue()) - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) - ) + if (textFieldValue.text.isNotEmpty()) { + BasicText( + text = "Clear", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { onTextFieldValueChanged(TextFieldValue()) } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt index 015a32b..9a8407f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt @@ -156,18 +156,18 @@ fun OnlineSearchTab( .weight(1f) ) - BasicText( - text = "Clear", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = textFieldValue.text.isNotEmpty()) { - onTextFieldValueChanged(TextFieldValue()) - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) - ) + if (textFieldValue.text.isNotEmpty()) { + BasicText( + text = "Clear", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { onTextFieldValueChanged(TextFieldValue()) } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } } ) } From f463f82f7297591a4254d747d7119497f33fc879 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 24 Sep 2022 20:18:43 +0200 Subject: [PATCH 010/100] Redesign AlbumScreen (#172) --- .../18.json | 602 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 16 +- .../vimusic/models/AlbumWithSongs.kt | 22 + .../vimusic/models/SortedSongAlbumMap.kt | 13 + .../vimusic/ui/components/themed/Header.kt | 34 + .../vimusic/ui/components/themed/Scaffold.kt | 11 +- .../ui/components/themed/VerticalBar.kt | 73 +-- .../vimusic/ui/screens/AlbumScreen.kt | 421 ------------ .../it/vfsfitvnm/vimusic/ui/screens/Routes.kt | 1 + .../vimusic/ui/screens/album/AlbumScreen.kt | 27 + .../vimusic/ui/screens/album/AlbumSongList.kt | 276 ++++++++ .../ui/screens/album/AlbumViewModel.kt | 66 ++ .../vimusic/ui/styling/Dimensions.kt | 2 + .../it/vfsfitvnm/vimusic/ui/views/SongItem.kt | 2 + 14 files changed, 1066 insertions(+), 500 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json new file mode 100644 index 0000000..00613b9 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json @@ -0,0 +1,602 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "dec162db7ec49f4324481d54c49a793d", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronizedLyrics", + "columnName": "synchronizedLyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongPlaylistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongPlaylistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongPlaylistMap_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shuffleVideoId", + "columnName": "shuffleVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shufflePlaylistId", + "columnName": "shufflePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioVideoId", + "columnName": "radioVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioPlaylistId", + "columnName": "radioPlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongArtistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_SongArtistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongArtistMap_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrl", + "columnName": "shareUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongAlbumMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_SongAlbumMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongAlbumMap_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "QueuedMediaItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaItem", + "columnName": "mediaItem", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "SortedSongPlaylistMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" + }, + { + "viewName": "SortedSongAlbumMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dec162db7ec49f4324481d54c49a793d')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 3352650..e83fbfd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -32,6 +32,7 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.AlbumWithSongs import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength @@ -45,6 +46,7 @@ import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.SongAlbumMap import it.vfsfitvnm.vimusic.models.SongArtistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap +import it.vfsfitvnm.vimusic.models.SortedSongAlbumMap import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -144,8 +146,9 @@ interface Database { @Query("SELECT * FROM Artist WHERE id = :id") fun artist(id: String): Flow + @Transaction @Query("SELECT * FROM Album WHERE id = :id") - fun album(id: String): Flow + fun albumWithSongs(id: String): Flow @Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id") fun incrementTotalPlayTimeMs(id: String, addition: Long) @@ -190,11 +193,6 @@ interface Database { @RewriteQueriesToDropUnusedColumns fun artistSongs(artistId: String): Flow> - @Transaction - @Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position") - @RewriteQueriesToDropUnusedColumns - fun albumSongs(albumId: String): Flow> - @Query("SELECT * FROM Format WHERE songId = :songId") fun format(songId: String): Flow @@ -361,9 +359,10 @@ interface Database { Format::class, ], views = [ - SortedSongPlaylistMap::class + SortedSongPlaylistMap::class, + SortedSongAlbumMap::class ], - version = 17, + version = 18, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -379,6 +378,7 @@ interface Database { AutoMigration(from = 13, to = 14), AutoMigration(from = 15, to = 16), AutoMigration(from = 16, to = 17), + AutoMigration(from = 17, to = 18), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt new file mode 100644 index 0000000..0ca36e5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt @@ -0,0 +1,22 @@ +package it.vfsfitvnm.vimusic.models + +import androidx.compose.runtime.Immutable +import androidx.room.Embedded +import androidx.room.Junction +import androidx.room.Relation + +@Immutable +data class AlbumWithSongs( + @Embedded val album: Album, + @Relation( + entity = Song::class, + parentColumn = "id", + entityColumn = "id", + associateBy = Junction( + value = SortedSongAlbumMap::class, + parentColumn = "albumId", + entityColumn = "songId" + ) + ) + val songs: List +) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt new file mode 100644 index 0000000..b41b451 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt @@ -0,0 +1,13 @@ +package it.vfsfitvnm.vimusic.models + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.DatabaseView + +@Immutable +@DatabaseView("SELECT * FROM SongAlbumMap ORDER BY position") +data class SortedSongAlbumMap( + @ColumnInfo(index = true) val songId: String, + @ColumnInfo(index = true) val albumId: String, + val position: Int +) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt index bdcc125..c174126 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt @@ -1,6 +1,8 @@ package it.vfsfitvnm.vimusic.ui.components.themed +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row @@ -11,12 +13,15 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.ui.styling.shimmer import it.vfsfitvnm.vimusic.utils.medium +import kotlin.random.Random @Composable fun Header( @@ -69,3 +74,32 @@ fun Header( ) } } + +@Composable +fun HeaderPlaceholder( + modifier: Modifier = Modifier, +) { + val (colorPalette, typography) = LocalAppearance.current + + Column( + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.Center, + modifier = modifier + .padding(horizontal = 16.dp) + .height(128.dp) + .fillMaxWidth() + ) { + Box( + modifier = Modifier + .background(colorPalette.shimmer) + .fillMaxWidth(remember { 0.25f + Random.nextFloat() * 0.5f }) + ) { + BasicText( + text = "", + style = typography.xxl.medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt index ed4a95a..1a951fa 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -112,16 +112,11 @@ fun Scaffold( } } -@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 ) { @@ -132,13 +127,9 @@ fun SimpleScaffold( .background(colorPalette.background0) .fillMaxSize() ) { - VerticalTitleBar( + VerticalBar( topIconButtonId = topIconButtonId, onTopIconButtonClick = onTopIconButtonClick, - title = title, - primaryIconButtonId = primaryIconButtonId, - primaryIconButtonEnabled = primaryIconButtonEnabled, - onPrimaryIconButtonClick = onPrimaryIconButtonClick, modifier = Modifier .padding(LocalPlayerAwarePaddingValues.current) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt index d37a256..c21cf51 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt @@ -24,6 +24,7 @@ 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.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.semiBold @@ -35,8 +36,6 @@ fun VerticalBar( 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 @@ -44,6 +43,7 @@ fun VerticalBar( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier +// .width(Dimensions.verticalBarWidth) .padding(vertical = 16.dp) ) { // Box( @@ -68,7 +68,7 @@ fun VerticalBar( Spacer( modifier = Modifier - .width(64.dp) + .width(Dimensions.verticalBarWidth) .height(32.dp) ) @@ -110,77 +110,28 @@ fun VerticalBar( @SuppressLint("ModifierParameter") @Composable -fun VerticalTitleBar( +fun VerticalBar( topIconButtonId: Int, onTopIconButtonClick: () -> Unit, - title: String, - primaryIconButtonId: Int? = null, - primaryIconButtonEnabled: Boolean = true, - onPrimaryIconButtonClick: () -> Unit = {}, modifier: Modifier = Modifier ) { - val (colorPalette, typography) = LocalAppearance.current + val (colorPalette) = LocalAppearance.current Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier + .width(Dimensions.verticalBarWidth) .padding(vertical = 16.dp) ) { - Box( + Image( + painter = painterResource(topIconButtonId), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.textSecondary), 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) + .padding(all = 12.dp) + .size(22.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) - ) - } - } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt deleted file mode 100644 index b36df80..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/AlbumScreen.kt +++ /dev/null @@ -1,421 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import android.content.Intent -import androidx.compose.animation.ExperimentalAnimationApi -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.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -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.lazy.rememberLazyListState -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -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.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -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 coil.compose.AsyncImage -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.ThumbnailRoundness -import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.models.DetailedSong -import it.vfsfitvnm.vimusic.models.Playlist -import it.vfsfitvnm.vimusic.models.SongAlbumMap -import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.query -import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError -import it.vfsfitvnm.vimusic.ui.components.themed.Menu -import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry -import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder -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.styling.shimmer -import it.vfsfitvnm.vimusic.ui.views.SongItem -import it.vfsfitvnm.vimusic.utils.asMediaItem -import it.vfsfitvnm.vimusic.utils.bold -import it.vfsfitvnm.vimusic.utils.center -import it.vfsfitvnm.vimusic.utils.enqueue -import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex -import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.vimusic.utils.thumbnail -import it.vfsfitvnm.vimusic.utils.toMediaItem -import it.vfsfitvnm.youtubemusic.YouTube -import java.text.DateFormat -import java.util.Date -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.map - -@ExperimentalAnimationApi -@Composable -fun AlbumScreen(browseId: String) { - val lazyListState = rememberLazyListState() - - val albumResult by remember(browseId) { - Database.album(browseId).map { album -> - album - ?.takeIf { album.timestamp != null } - ?.let(Result.Companion::success) - ?: YouTube.album(browseId)?.map { youtubeAlbum -> - Database.upsert( - Album( - id = browseId, - title = youtubeAlbum.title, - thumbnailUrl = youtubeAlbum.thumbnail?.url, - year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, - shareUrl = youtubeAlbum.url, - timestamp = System.currentTimeMillis() - ), - youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> - albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> - Database.insert(mediaItem) - SongAlbumMap( - songId = mediaItem.mediaId, - albumId = browseId, - position = position - ) - } - } ?: emptyList() - ) - - null - } - }.distinctUntilChanged() - }.collectAsState(initial = null, context = Dispatchers.IO) - - val songs by remember(browseId) { - Database.albumSongs(browseId) - }.collectAsState(initial = emptyList(), context = Dispatchers.IO) - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val context = LocalContext.current - val binder = LocalPlayerServiceBinder.current - - val (colorPalette, typography) = LocalAppearance.current - val menuState = LocalMenuState.current - - LazyColumn( - state = lazyListState, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - } - } - - item { - albumResult?.getOrNull()?.let { album -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 8.dp) - ) { - AsyncImage( - model = album.thumbnailUrl?.thumbnail(Dimensions.thumbnails.album.px), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(Dimensions.thumbnails.album) - ) - - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .weight(1f) - ) { - BasicText( - text = album.title ?: "Unknown", - style = typography.m.semiBold - ) - - BasicText( - text = album.authorsText ?: "", - style = typography.xs.secondary.semiBold, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - - album.year?.let { year -> - BasicText( - text = year, - style = typography.xs.secondary, - maxLines = 1, - modifier = Modifier - .padding(top = 8.dp) - ) - } - } - } - } ?: albumResult?.exceptionOrNull()?.let { throwable -> - LoadingOrError(errorMessage = throwable.javaClass.canonicalName) - } ?: LoadingOrError() - } - - item { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .zIndex(1f) - .padding(horizontal = 8.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(enabled = songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs - .shuffled() - .map(DetailedSong::asMediaItem) - ) - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - onClick = { - menuState.hide() - binder?.player?.enqueue( - songs.map(DetailedSong::asMediaItem) - ) - } - ) - - MenuEntry( - icon = R.drawable.playlist, - text = "Import as playlist", - onClick = { - menuState.hide() - - albumResult - ?.getOrNull() - ?.let { album -> - query { - val playlistId = - Database.insert( - Playlist( - name = album.title - ?: "Unknown" - ) - ) - - songs.forEachIndexed { index, song -> - Database.insert( - SongPlaylistMap( - songId = song.id, - playlistId = playlistId, - position = index - ) - ) - } - } - } - } - ) - - MenuEntry( - icon = R.drawable.share_social, - text = "Share", - onClick = { - menuState.hide() - - albumResult?.getOrNull()?.shareUrl?.let { url -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - - context.startActivity( - Intent.createChooser( - sendIntent, - null - ) - ) - } - } - ) - - MenuEntry( - icon = R.drawable.download, - text = "Refetch", - secondaryText = albumResult?.getOrNull()?.timestamp?.let { timestamp -> - "Last updated on ${ - DateFormat - .getDateTimeInstance() - .format(Date(timestamp)) - }" - }, - isEnabled = albumResult?.getOrNull() != null, - onClick = { - menuState.hide() - - query { - albumResult - ?.getOrNull() - ?.let(Database::delete) - } - } - ) - } - } - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - } - } - - itemsIndexed( - items = songs, - key = { _, song -> song.id }, - contentType = { _, song -> song } - ) { index, song -> - SongItem( - title = song.title, - authors = song.artistsText ?: albumResult?.getOrNull()?.authorsText, - durationText = song.durationText, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - songs.map(DetailedSong::asMediaItem), - index - ) - }, - startContent = { - BasicText( - text = "${index + 1}", - style = typography.xs.secondary.bold.center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .width(36.dp) - ) - }, - menuContent = { - NonQueuedMediaItemMenu( - mediaItem = song.asMediaItem, - onDismiss = menuState::hide, - ) - } - ) - } - } - } - } -} - -@Composable -private fun LoadingOrError( - errorMessage: String? = null, - onRetry: (() -> Unit)? = null -) { - val (colorPalette) = LocalAppearance.current - - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 8.dp) - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = ThumbnailRoundness.shape) - .size(Dimensions.thumbnails.album) - ) - - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxHeight() - ) { - Column { - TextPlaceholder() - - TextPlaceholder( - modifier = Modifier - .alpha(0.7f) - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index 593b9cd..d41a827 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -8,6 +8,7 @@ import it.vfsfitvnm.route.Route0 import it.vfsfitvnm.route.Route1 import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist +import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen val albumRoute = Route1("albumRoute") val artistRoute = Route1("artistRoute") diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt new file mode 100644 index 0000000..a10d6b9 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -0,0 +1,27 @@ +package it.vfsfitvnm.vimusic.ui.screens.album + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.runtime.Composable +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.themed.SimpleScaffold +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes + +@OptIn(ExperimentalFoundationApi::class) +@ExperimentalAnimationApi +@Composable +fun AlbumScreen(browseId: String) { + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + SimpleScaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + ) { + AlbumSongList(browseId = browseId) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt new file mode 100644 index 0000000..08e442e --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt @@ -0,0 +1,276 @@ +package it.vfsfitvnm.vimusic.ui.screens.album + +import android.content.Intent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import com.valentinilk.shimmer.shimmer +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +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.styling.shimmer +import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.enqueue +import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalAnimationApi +@ExperimentalFoundationApi +@Composable +fun AlbumSongList( + browseId: String, + viewModel: AlbumViewModel = viewModel( + key = browseId, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return AlbumViewModel(browseId) as T + } + } + ) +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + val context = LocalContext.current + + BoxWithConstraints { + val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth + val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px + + viewModel.result?.getOrNull()?.let { albumWithSongs -> + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Column { + Header(title = albumWithSongs.album.title ?: "Unknown") { + if (albumWithSongs.songs.isNotEmpty()) { + BasicText( + text = "Enqueue", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + binder?.player?.enqueue( + albumWithSongs.songs.map(DetailedSong::asMediaItem) + ) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } + + Spacer( + modifier = Modifier + .weight(1f) + ) + + Image( + painter = painterResource(R.drawable.share_social), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + albumWithSongs.album.shareUrl?.let { url -> + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + + context.startActivity( + Intent.createChooser( + sendIntent, + null + ) + ) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + AsyncImage( + model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + } + } + + itemsIndexed( + items = albumWithSongs.songs, + key = { _, song -> song.id } + ) { index, song -> + SongItem( + title = song.title, + authors = song.artistsText ?: albumWithSongs.album.authorsText, + durationText = song.durationText, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + albumWithSongs.songs.map(DetailedSong::asMediaItem), + index + ) + }, + startContent = { + BasicText( + text = "${index + 1}", + style = typography.m.secondary.secondary.center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .width(Dimensions.thumbnails.song) + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + albumWithSongs.songs + .shuffled() + .map(DetailedSong::asMediaItem) + ) + } + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } + } ?: viewModel.result?.exceptionOrNull()?.let { _ -> + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + viewModel.fetch(browseId) + } + } + .align(Alignment.Center) + .fillMaxSize() + ) { + BasicText( + text = "An error has occurred.\nTap to retry", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) + } + } ?: Column( + modifier = Modifier + .padding(LocalPlayerAwarePaddingValues.current) + .shimmer() + ) { + HeaderPlaceholder() + + Spacer( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + .background(colorPalette.shimmer) + ) + + repeat(3) { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .alpha(1f - index * 0.25f) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) + .height(Dimensions.thumbnails.song) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(Dimensions.thumbnails.song) + ) + + Column { + TextPlaceholder() + TextPlaceholder() + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt new file mode 100644 index 0000000..3e6b3fe --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt @@ -0,0 +1,66 @@ +package it.vfsfitvnm.vimusic.ui.screens.album + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.AlbumWithSongs +import it.vfsfitvnm.vimusic.models.SongAlbumMap +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class AlbumViewModel(browseId: String) : ViewModel() { + var result by mutableStateOf?>(null) + private set + + private var job: Job? = null + + init { + fetch(browseId) + } + + fun fetch(browseId: String) { + job?.cancel() + result = null + + job = viewModelScope.launch(Dispatchers.IO) { + Database.albumWithSongs(browseId).collect { albumWithSongs -> + result = if (albumWithSongs?.album?.timestamp == null) { + YouTube.album(browseId)?.map { youtubeAlbum -> + Database.upsert( + Album( + id = browseId, + title = youtubeAlbum.title, + thumbnailUrl = youtubeAlbum.thumbnail?.url, + year = youtubeAlbum.year, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + shareUrl = youtubeAlbum.url, + timestamp = System.currentTimeMillis() + ), + youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> + albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> + Database.insert(mediaItem) + SongAlbumMap( + songId = mediaItem.mediaId, + albumId = browseId, + position = position + ) + } + } ?: emptyList() + ) + + null + } + } else { + Result.success(albumWithSongs) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt index 37eae8b..f503f5b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt @@ -10,6 +10,8 @@ import androidx.compose.ui.unit.dp object Dimensions { val itemsVerticalPadding = 8.dp + val verticalBarWidth = 64.dp + object thumbnails { val album = 128.dp val artist = 192.dp diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt index 835d2e4..121d7d9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Column 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.text.BasicText @@ -158,6 +159,7 @@ fun SongItem( .fillMaxWidth() .padding(vertical = Dimensions.itemsVerticalPadding) .padding(start = 16.dp, end = if (trailingContent == null) 16.dp else 8.dp) + .height(Dimensions.thumbnails.song) ) { startContent() From 9efe4f95799ce56b69456fec1e0fe1276ba1bfca Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sun, 25 Sep 2022 13:30:11 +0200 Subject: [PATCH 011/100] Rename files --- .../vimusic/ui/components/themed/Scaffold.kt | 29 ------ .../ui/components/themed/VerticalBar.kt | 91 ++----------------- .../vimusic/ui/screens/album/AlbumScreen.kt | 18 +++- .../vimusic/ui/screens/album/AlbumSongList.kt | 10 +- ...ViewModel.kt => AlbumSongListViewModel.kt} | 2 +- .../{PlaylistsTab.kt => HomePlaylistList.kt} | 4 +- ...wModel.kt => HomePlaylistListViewModel.kt} | 2 +- .../vimusic/ui/screens/home/HomeScreen.kt | 4 +- .../home/{SongsTab.kt => HomeSongList.kt} | 4 +- ...bViewModel.kt => HomeSongListViewModel.kt} | 2 +- ...LibrarySearchTab.kt => LocalSongSearch.kt} | 6 +- ...ewModel.kt => LocalSongSearchViewModel.kt} | 2 +- .../{OnlineSearchTab.kt => OnlineSearch.kt} | 6 +- ...bViewModel.kt => OnlineSearchViewModel.kt} | 2 +- .../vimusic/ui/screens/search/SearchScreen.kt | 4 +- .../{ItemSearchResult.kt => SearchResult.kt} | 4 +- ...tViewModel.kt => SearchResultViewModel.kt} | 2 +- .../settings/{AboutTab.kt => About.kt} | 2 +- ...ceSettingsTab.kt => AppearanceSettings.kt} | 2 +- .../{CacheSettingsTab.kt => CacheSettings.kt} | 2 +- .../{OtherSettingsTab.kt => OtherSettings.kt} | 2 +- ...PlayerSettingsTab.kt => PlayerSettings.kt} | 2 +- .../ui/screens/settings/SettingsScreen.kt | 10 +- app/src/main/res/drawable/sparkles.xml | 15 +++ 24 files changed, 77 insertions(+), 150 deletions(-) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/{AlbumViewModel.kt => AlbumSongListViewModel.kt} (97%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/{PlaylistsTab.kt => HomePlaylistList.kt} (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/{PlaylistsTabViewModel.kt => HomePlaylistListViewModel.kt} (95%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/{SongsTab.kt => HomeSongList.kt} (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/{SongsTabViewModel.kt => HomeSongListViewModel.kt} (95%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/{LibrarySearchTab.kt => LocalSongSearch.kt} (97%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/{LibrarySearchTabViewModel.kt => LocalSongSearchViewModel.kt} (91%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/{OnlineSearchTab.kt => OnlineSearch.kt} (98%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/{OnlineSearchTabViewModel.kt => OnlineSearchViewModel.kt} (94%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/{ItemSearchResult.kt => SearchResult.kt} (96%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/{ItemSearchResultViewModel.kt => SearchResultViewModel.kt} (96%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/{AboutTab.kt => About.kt} (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/{AppearanceSettingsTab.kt => AppearanceSettings.kt} (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/{CacheSettingsTab.kt => CacheSettings.kt} (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/{OtherSettingsTab.kt => OtherSettings.kt} (99%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/{PlayerSettingsTab.kt => PlayerSettings.kt} (99%) create mode 100644 app/src/main/res/drawable/sparkles.xml diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt index 1a951fa..8ef5ea0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -15,7 +15,6 @@ 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 @@ -61,8 +60,6 @@ fun Scaffold( tabIndex = tabIndex, onTabChanged = onTabChanged, tabColumnContent = tabColumnContent, -// primaryIconButtonId = primaryIconButtonId, -// onPrimaryIconButtonClick = onPrimaryIconButtonClick, modifier = Modifier .padding(LocalPlayerAwarePaddingValues.current) ) @@ -111,29 +108,3 @@ fun Scaffold( } } } - -@ExperimentalAnimationApi -@Composable -fun SimpleScaffold( - topIconButtonId: Int, - onTopIconButtonClick: () -> Unit, - modifier: Modifier = Modifier, - content: @Composable () -> Unit -) { - val (colorPalette) = LocalAppearance.current - - Row( - modifier = modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - VerticalBar( - topIconButtonId = topIconButtonId, - onTopIconButtonClick = onTopIconButtonClick, - modifier = Modifier - .padding(LocalPlayerAwarePaddingValues.current) - ) - - content() - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt index c21cf51..1eb24b0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt @@ -1,34 +1,26 @@ 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.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.semiBold -@SuppressLint("ModifierParameter") @Composable fun VerticalBar( topIconButtonId: Int, @@ -43,28 +35,18 @@ fun VerticalBar( Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier -// .width(Dimensions.verticalBarWidth) .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) - ) -// } + Image( + painter = painterResource(topIconButtonId), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.textSecondary), + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onTopIconButtonClick) + .padding(all = 12.dp) + .size(22.dp) + ) Spacer( modifier = Modifier @@ -80,58 +62,5 @@ fun VerticalBar( 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 VerticalBar( - topIconButtonId: Int, - onTopIconButtonClick: () -> Unit, - modifier: Modifier = Modifier -) { - val (colorPalette) = LocalAppearance.current - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .width(Dimensions.verticalBarWidth) - .padding(vertical = 16.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) - .size(22.dp) - ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index a10d6b9..ac71200 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -3,24 +3,34 @@ package it.vfsfitvnm.vimusic.ui.screens.album import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.ui.components.themed.SimpleScaffold +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes @OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @Composable fun AlbumScreen(browseId: String) { + val saveableStateHolder = rememberSaveableStateHolder() + RouteHandler(listenToGlobalEmitter = true) { globalRoutes() host { - SimpleScaffold( + Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, - ) { - AlbumSongList(browseId = browseId) + tabIndex = 0, + onTabChanged = {}, + tabColumnContent = { Item -> + Item(0, "Overview", R.drawable.sparkles) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { + AlbumSongList(browseId = browseId) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt index 08e442e..da56efc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt @@ -54,11 +54,13 @@ import it.vfsfitvnm.vimusic.ui.styling.shimmer 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.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @@ -66,12 +68,12 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @Composable fun AlbumSongList( browseId: String, - viewModel: AlbumViewModel = viewModel( + viewModel: AlbumSongListViewModel = viewModel( key = browseId, factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return AlbumViewModel(browseId) as T + return AlbumSongListViewModel(browseId) as T } } ) @@ -175,7 +177,7 @@ fun AlbumSongList( startContent = { BasicText( text = "${index + 1}", - style = typography.m.secondary.secondary.center, + style = typography.s.semiBold.center.color(colorPalette.textDisabled), maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier @@ -215,7 +217,7 @@ fun AlbumSongList( .size(20.dp) ) } - } ?: viewModel.result?.exceptionOrNull()?.let { _ -> + } ?: viewModel.result?.exceptionOrNull()?.let { Box( modifier = Modifier .pointerInput(Unit) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt similarity index 97% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt index 3e6b3fe..ae14d16 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class AlbumViewModel(browseId: String) : ViewModel() { +class AlbumSongListViewModel(browseId: String) : ViewModel() { var result by mutableStateOf?>(null) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index e1714b2..464b229 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -54,8 +54,8 @@ import it.vfsfitvnm.vimusic.utils.medium @ExperimentalFoundationApi @Composable -fun PlaylistsTab( - viewModel: PlaylistsTabViewModel = viewModel(), +fun HomePlaylistList( + viewModel: HomePlaylistListViewModel = viewModel(), onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit, onPlaylistClicked: (Playlist) -> Unit, ) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt similarity index 95% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt index a524665..257ef63 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/PlaylistsTabViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch -class PlaylistsTabViewModel(application: Application) : AndroidViewModel(application) { +class HomePlaylistListViewModel(application: Application) : AndroidViewModel(application) { var items by mutableStateOf(emptyList()) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 8d4e28a..63c7046 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -102,8 +102,8 @@ fun HomeScreen() { ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { - 0 -> SongsTab() - 1 -> PlaylistsTab( + 0 -> HomeSongList() + 1 -> HomePlaylistList( onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, onPlaylistClicked = { localPlaylistRoute(it.id) } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index dcb98ee..2cc16d6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -55,8 +55,8 @@ import it.vfsfitvnm.vimusic.utils.semiBold @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun SongsTab( - viewModel: SongsTabViewModel = viewModel() +fun HomeSongList( + viewModel: HomeSongListViewModel = viewModel() ) { val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt similarity index 95% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt index e6cdd0f..596038f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/SongsTabViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt @@ -23,7 +23,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.launch -class SongsTabViewModel(application: Application) : AndroidViewModel(application) { +class HomeSongListViewModel(application: Application) : AndroidViewModel(application) { var items by mutableStateOf(emptyList()) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt similarity index 97% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 1579b83..ec87fdb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -47,15 +47,15 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun LibrarySearchTab( +fun LocalSongSearch( textFieldValue: TextFieldValue, onTextFieldValueChanged: (TextFieldValue) -> Unit, - viewModel: LibrarySearchTabViewModel = viewModel( + viewModel: LocalSongSearchViewModel = viewModel( key = textFieldValue.text, factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return LibrarySearchTabViewModel(textFieldValue.text) as T + return LocalSongSearchViewModel(textFieldValue.text) as T } } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt similarity index 91% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt index 4572e75..7735846 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LibrarySearchTabViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt @@ -9,7 +9,7 @@ import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.models.DetailedSong import kotlinx.coroutines.launch -class LibrarySearchTabViewModel(text: String) : ViewModel() { +class LocalSongSearchViewModel(text: String) : ViewModel() { var items by mutableStateOf(emptyList()) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt similarity index 98% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 9a8407f..63db5f4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -56,18 +56,18 @@ import it.vfsfitvnm.vimusic.utils.secondary import kotlinx.coroutines.delay @Composable -fun OnlineSearchTab( +fun OnlineSearch( textFieldValue: TextFieldValue, onTextFieldValueChanged: (TextFieldValue) -> Unit, isOpenableUrl: Boolean, onSearch: (String) -> Unit, onUri: () -> Unit, - viewModel: OnlineSearchTabViewModel = viewModel( + viewModel: OnlineSearchViewModel = viewModel( key = textFieldValue.text, factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return OnlineSearchTabViewModel(textFieldValue.text) as T + return OnlineSearchViewModel(textFieldValue.text) as T } } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt similarity index 94% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt index c334cc7..d96eb7f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchTabViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt @@ -11,7 +11,7 @@ import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch -class OnlineSearchTabViewModel(text: String) : ViewModel() { +class OnlineSearchViewModel(text: String) : ViewModel() { var history by mutableStateOf(emptyList()) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt index 13d8b31..c5b025b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt @@ -66,7 +66,7 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(currentTabIndex) { when (currentTabIndex) { - 0 -> OnlineSearchTab( + 0 -> OnlineSearch( textFieldValue = textFieldValue, onTextFieldValueChanged = onTextFieldValueChanged, isOpenableUrl = isOpenableUrl, @@ -77,7 +77,7 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U } } ) - 1 -> LibrarySearchTab( + 1 -> LocalSongSearch( textFieldValue = textFieldValue, onTextFieldValueChanged = onTextFieldValueChanged ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt similarity index 96% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResult.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index a78686c..d718e25 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -27,12 +27,12 @@ inline fun ItemSearchResult( query: String, filter: String, crossinline onSearchAgain: () -> Unit, - viewModel: ItemSearchResultViewModel = viewModel( + viewModel: SearchResultViewModel = viewModel( key = query + filter, factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return ItemSearchResultViewModel(query, filter) as T + return SearchResultViewModel(query, filter) as T } } ), diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt similarity index 96% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt index e754cf5..bfbfaab 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/ItemSearchResultViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt @@ -11,7 +11,7 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ItemSearchResultViewModel( +class SearchResultViewModel( private val query: String, private val filter: String ) : ViewModel() { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/About.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/About.kt index 27951c1..5da3f39 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/About.kt @@ -19,7 +19,7 @@ import it.vfsfitvnm.vimusic.utils.secondary @ExperimentalAnimationApi @Composable -fun AboutTab() { +fun About() { val (colorPalette, typography) = LocalAppearance.current val uriHandler = LocalUriHandler.current diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettings.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettings.kt index 842f644..a4c7d15 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettings.kt @@ -25,7 +25,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey @ExperimentalAnimationApi @Composable -fun AppearanceSettingsTab() { +fun AppearanceSettings() { val (colorPalette) = LocalAppearance.current var colorPaletteName by rememberPreference(colorPaletteNameKey, ColorPaletteName.Dynamic) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettings.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettings.kt index 6694760..aaee5a0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettings.kt @@ -30,7 +30,7 @@ import it.vfsfitvnm.vimusic.utils.rememberPreference @OptIn(ExperimentalCoilApi::class) @ExperimentalAnimationApi @Composable -fun CacheSettingsTab() { +fun CacheSettings() { val context = LocalContext.current val (colorPalette) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettings.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettings.kt index 716f69f..75868d1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettings.kt @@ -47,7 +47,7 @@ import kotlinx.coroutines.Dispatchers @ExperimentalAnimationApi @Composable -fun OtherSettingsTab() { +fun OtherSettings() { val context = LocalContext.current val (colorPalette) = LocalAppearance.current diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettings.kt similarity index 99% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettings.kt index fcf907c..d82701a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettings.kt @@ -28,7 +28,7 @@ import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey @ExperimentalAnimationApi @Composable -fun PlayerSettingsTab() { +fun PlayerSettings() { val context = LocalContext.current val (colorPalette) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt index a6e4037..9c57f7b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/SettingsScreen.kt @@ -52,11 +52,11 @@ fun SettingsScreen() { ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(currentTabIndex) { when (currentTabIndex) { - 0 -> AppearanceSettingsTab() - 1 -> PlayerSettingsTab() - 2 -> CacheSettingsTab() - 3 -> OtherSettingsTab() - 4 -> AboutTab() + 0 -> AppearanceSettings() + 1 -> PlayerSettings() + 2 -> CacheSettings() + 3 -> OtherSettings() + 4 -> About() } } } diff --git a/app/src/main/res/drawable/sparkles.xml b/app/src/main/res/drawable/sparkles.xml new file mode 100644 index 0000000..e0c6622 --- /dev/null +++ b/app/src/main/res/drawable/sparkles.xml @@ -0,0 +1,15 @@ + + + + + From 19fa11672da3bfb635bef02645e47f0cfe3143b8 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sun, 25 Sep 2022 15:06:03 +0200 Subject: [PATCH 012/100] Add HomeAlbumList --- .../19.json | 608 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 39 +- .../it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt | 7 + .../it/vfsfitvnm/vimusic/models/Album.kt | 3 +- .../{AlbumSongList.kt => AlbumOverview.kt} | 34 +- ...ViewModel.kt => AlbumOverviewViewModel.kt} | 2 +- .../vimusic/ui/screens/home/HomeAlbumList.kt | 187 ++++++ .../ui/screens/home/HomeAlbumListViewModel.kt | 67 ++ .../ui/screens/home/HomePlaylistList.kt | 2 +- .../vimusic/ui/screens/home/HomeScreen.kt | 20 +- .../vimusic/ui/screens/home/HomeSongList.kt | 2 +- .../it/vfsfitvnm/vimusic/utils/Preferences.kt | 3 +- app/src/main/res/drawable/bookmark.xml | 9 + .../main/res/drawable/bookmark_outline.xml | 13 + 14 files changed, 979 insertions(+), 17 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/{AlbumSongList.kt => AlbumOverview.kt} (87%) rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/{AlbumSongListViewModel.kt => AlbumOverviewViewModel.kt} (97%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt create mode 100644 app/src/main/res/drawable/bookmark.xml create mode 100644 app/src/main/res/drawable/bookmark_outline.xml diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json new file mode 100644 index 0000000..240b6e0 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json @@ -0,0 +1,608 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "41479c8284963d3533c4baa46d7464a6", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronizedLyrics", + "columnName": "synchronizedLyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongPlaylistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongPlaylistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongPlaylistMap_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shuffleVideoId", + "columnName": "shuffleVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shufflePlaylistId", + "columnName": "shufflePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioVideoId", + "columnName": "radioVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioPlaylistId", + "columnName": "radioPlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongArtistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_SongArtistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongArtistMap_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrl", + "columnName": "shareUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongAlbumMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_SongAlbumMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongAlbumMap_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "QueuedMediaItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaItem", + "columnName": "mediaItem", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "SortedSongPlaylistMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" + }, + { + "viewName": "SortedSongAlbumMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '41479c8284963d3533c4baa46d7464a6')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index e83fbfd..42e2e32 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -28,6 +28,7 @@ import androidx.room.migration.AutoMigrationSpec import androidx.room.migration.Migration import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteDatabase +import it.vfsfitvnm.vimusic.enums.AlbumSortBy import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder @@ -150,6 +151,41 @@ interface Database { @Query("SELECT * FROM Album WHERE id = :id") fun albumWithSongs(id: String): Flow + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC") + fun albumsByTitleAsc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY year ASC") + fun albumsByYearAsc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID ASC") + fun albumsByRowIdAsc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title DESC") + fun albumsByTitleDesc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY year DESC") + fun albumsByYearDesc(): Flow> + + @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY ROWID DESC") + fun albumsByRowIdDesc(): Flow> + + fun albums(sortBy: AlbumSortBy, sortOrder: SortOrder): Flow> { + return when (sortBy) { + AlbumSortBy.Title -> when (sortOrder) { + SortOrder.Ascending -> albumsByTitleAsc() + SortOrder.Descending -> albumsByTitleDesc() + } + AlbumSortBy.Year -> when (sortOrder) { + SortOrder.Ascending -> albumsByYearAsc() + SortOrder.Descending -> albumsByYearDesc() + } + AlbumSortBy.DateAdded -> when (sortOrder) { + SortOrder.Ascending -> albumsByRowIdAsc() + SortOrder.Descending -> albumsByRowIdDesc() + } + } + } + @Query("UPDATE Song SET totalPlayTimeMs = totalPlayTimeMs + :addition WHERE id = :id") fun incrementTotalPlayTimeMs(id: String, addition: Long) @@ -362,7 +398,7 @@ interface Database { SortedSongPlaylistMap::class, SortedSongAlbumMap::class ], - version = 18, + version = 19, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -379,6 +415,7 @@ interface Database { AutoMigration(from = 15, to = 16), AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), + AutoMigration(from = 18, to = 19), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt new file mode 100644 index 0000000..4d99975 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/AlbumSortBy.kt @@ -0,0 +1,7 @@ +package it.vfsfitvnm.vimusic.enums + +enum class AlbumSortBy { + Title, + Year, + DateAdded +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt index e07b81f..75cc575 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt @@ -13,5 +13,6 @@ data class Album( val year: String? = null, val authorsText: String? = null, val shareUrl: String? = null, - val timestamp: Long? + val timestamp: Long?, + val bookmarkedAt: Long? = null ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt similarity index 87% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index da56efc..e8fd7bd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -39,10 +39,12 @@ import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.valentinilk.shimmer.shimmer +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu @@ -68,12 +70,12 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @Composable fun AlbumSongList( browseId: String, - viewModel: AlbumSongListViewModel = viewModel( + viewModel: AlbumOverviewViewModel = viewModel( key = browseId, factory = object : ViewModelProvider.Factory { override fun create(modelClass: Class): T { @Suppress("UNCHECKED_CAST") - return AlbumSongListViewModel(browseId) as T + return AlbumOverviewViewModel(browseId) as T } } ) @@ -121,6 +123,34 @@ fun AlbumSongList( .weight(1f) ) + Image( + painter = painterResource( + if (albumWithSongs.album.bookmarkedAt == null) { + R.drawable.bookmark_outline + } else { + R.drawable.bookmark + } + ), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.accent), + modifier = Modifier + .clickable { + query { + Database.update( + albumWithSongs.album.copy( + bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { + System.currentTimeMillis() + } else { + null + } + ) + ) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + Image( painter = painterResource(R.drawable.share_social), contentDescription = null, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt similarity index 97% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt index ae14d16..7174d59 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongListViewModel.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt @@ -15,7 +15,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch -class AlbumSongListViewModel(browseId: String) : ViewModel() { +class AlbumOverviewViewModel(browseId: String) : ViewModel() { var result by mutableStateOf?>(null) private set diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt new file mode 100644 index 0000000..aa77279 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt @@ -0,0 +1,187 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import androidx.annotation.DrawableRes +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.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.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +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.remember +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.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.AlbumSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.ui.components.themed.Header +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.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun HomeAlbumList( + onAlbumClick: (Album) -> Unit, + viewModel: HomeAlbumListViewModel = viewModel() +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 + val thumbnailSizePx = thumbnailSizeDp.px + + val sortOrderIconRotation by animateFloatAsState( + targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + animationSpec = tween(durationMillis = 400, easing = LinearEasing) + ) + + val rippleIndication = rememberRipple(bounded = true) + + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header(title = "Albums") { + @Composable + fun Item( + @DrawableRes iconId: Int, + sortBy: AlbumSortBy + ) { + 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.calendar, + sortBy = AlbumSortBy.Year + ) + + Item( + iconId = R.drawable.text, + sortBy = AlbumSortBy.Title + ) + + Item( + iconId = R.drawable.time, + sortBy = AlbumSortBy.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 } + ) + } + } + + items( + items = viewModel.items, + key = Album::id + ) { album -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .clickable( + indication = rippleIndication, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onAlbumClick(album) } + ) + .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) + .fillMaxWidth() + .animateItemPlacement() + ) { + AsyncImage( + model = album.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + BasicText( + text = album.title ?: "", + style = typography.xs.semiBold, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + + BasicText( + text = album.authorsText ?: "", + style = typography.xs.semiBold.secondary, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + + album.year?.let { year -> + BasicText( + text = year, + style = typography.xxs.semiBold.secondary, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .padding(top = 8.dp) + ) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt new file mode 100644 index 0000000..172bf3c --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt @@ -0,0 +1,67 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import android.app.Application +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.enums.AlbumSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.utils.albumSortByKey +import it.vfsfitvnm.vimusic.utils.albumSortOrderKey +import it.vfsfitvnm.vimusic.utils.getEnum +import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf +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 HomeAlbumListViewModel(application: Application) : AndroidViewModel(application) { + var items by mutableStateOf(emptyList()) + private set + + var sortBy by mutableStatePreferenceOf( + preferences.getEnum( + albumSortByKey, + AlbumSortBy.DateAdded + ) + ) { + preferences.edit { putEnum(albumSortByKey, it) } + collectItems(sortBy = it) + } + + var sortOrder by mutableStatePreferenceOf( + preferences.getEnum( + albumSortOrderKey, + SortOrder.Ascending + ) + ) { + preferences.edit { putEnum(albumSortOrderKey, it) } + collectItems(sortOrder = it) + } + + private var job: Job? = null + + private val preferences: SharedPreferences + get() = getApplication().preferences + + init { + collectItems() + } + + private fun collectItems(sortBy: AlbumSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { + job?.cancel() + job = viewModelScope.launch { + Database.albums(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { + items = it + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 464b229..3003383 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -145,7 +145,7 @@ fun HomePlaylistList( ) Item( - iconId = R.drawable.calendar, + iconId = R.drawable.time, sortBy = PlaylistSortBy.DateAdded ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 63c7046..f064c1f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -14,7 +14,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen -import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResultScreen +import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute @@ -22,6 +22,7 @@ import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute import it.vfsfitvnm.vimusic.ui.screens.searchRoute +import it.vfsfitvnm.vimusic.ui.screens.searchresult.SearchResultScreen import it.vfsfitvnm.vimusic.ui.screens.settings.SettingsScreen import it.vfsfitvnm.vimusic.ui.screens.settingsRoute import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey @@ -84,7 +85,10 @@ fun HomeScreen() { } host { - val (tabIndex, onTabChanged) = rememberPreference(homeScreenTabIndexKey, defaultValue = 0) + val (tabIndex, onTabChanged) = rememberPreference( + homeScreenTabIndexKey, + defaultValue = 0 + ) Scaffold( topIconButtonId = R.drawable.equalizer, @@ -102,19 +106,17 @@ fun HomeScreen() { ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { - 0 -> HomeSongList() 1 -> HomePlaylistList( onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, onPlaylistClicked = { localPlaylistRoute(it.id) } ) -// 2 -> ArtistsTab( -// lazyListState = lazyListStates[currentTabIndex], +// 2 -> HomeArtistList( // onArtistClicked = { artistRoute(it.id) } // ) -// 3 -> AlbumsTab( -// lazyListState = lazyListStates[currentTabIndex], -// onAlbumClicked = { albumRoute(it.id) } -// ) + 3 -> HomeAlbumList( + onAlbumClick = { albumRoute(it.id) } + ) + else -> HomeSongList() } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index 2cc16d6..cb64513 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -106,7 +106,7 @@ fun HomeSongList( ) Item( - iconId = R.drawable.calendar, + iconId = R.drawable.time, sortBy = SongSortBy.DateAdded ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt index aafcb71..def091b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt @@ -21,7 +21,8 @@ const val songSortOrderKey = "songSortOrder" const val songSortByKey = "songSortBy" const val playlistSortOrderKey = "playlistSortOrder" const val playlistSortByKey = "playlistSortBy" -const val searchFilterKey = "searchFilter" +const val albumSortOrderKey = "albumSortOrder" +const val albumSortByKey = "albumSortBy" const val repeatModeKey = "repeatMode" const val skipSilenceKey = "skipSilence" const val volumeNormalizationKey = "volumeNormalization" diff --git a/app/src/main/res/drawable/bookmark.xml b/app/src/main/res/drawable/bookmark.xml new file mode 100644 index 0000000..416e06c --- /dev/null +++ b/app/src/main/res/drawable/bookmark.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/bookmark_outline.xml b/app/src/main/res/drawable/bookmark_outline.xml new file mode 100644 index 0000000..1544145 --- /dev/null +++ b/app/src/main/res/drawable/bookmark_outline.xml @@ -0,0 +1,13 @@ + + + From 29b4a8f5da034f97ec9d9a21f883f29c413ee332 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sun, 25 Sep 2022 20:29:58 +0200 Subject: [PATCH 013/100] Prepare ArtistScreen redesign --- .../20.json | 614 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 29 +- .../vfsfitvnm/vimusic/enums/ArtistSortBy.kt | 6 + .../it/vfsfitvnm/vimusic/models/Artist.kt | 3 +- .../vimusic/ui/screens/album/AlbumOverview.kt | 2 +- .../vimusic/ui/screens/album/AlbumScreen.kt | 2 +- .../ui/screens/artist/ArtistOverview.kt | 308 +++++++++ .../screens/artist/ArtistOverviewViewModel.kt | 66 ++ .../ui/screens/{ => artist}/ArtistScreen.kt | 41 +- .../vimusic/ui/screens/home/HomeArtistList.kt | 171 +++++ .../screens/home/HomeArtistListViewModel.kt | 67 ++ .../vimusic/ui/screens/home/HomeScreen.kt | 10 +- .../it/vfsfitvnm/vimusic/utils/Preferences.kt | 2 + 13 files changed, 1312 insertions(+), 9 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt rename app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/{ => artist}/ArtistScreen.kt (92%) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json new file mode 100644 index 0000000..ed16244 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json @@ -0,0 +1,614 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "821aa30ff7d14b31e839b2f3b2312f78", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronizedLyrics", + "columnName": "synchronizedLyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongPlaylistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongPlaylistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongPlaylistMap_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shuffleVideoId", + "columnName": "shuffleVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shufflePlaylistId", + "columnName": "shufflePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioVideoId", + "columnName": "radioVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioPlaylistId", + "columnName": "radioPlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongArtistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_SongArtistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongArtistMap_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrl", + "columnName": "shareUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongAlbumMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_SongAlbumMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongAlbumMap_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "QueuedMediaItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaItem", + "columnName": "mediaItem", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "SortedSongPlaylistMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" + }, + { + "viewName": "SortedSongAlbumMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '821aa30ff7d14b31e839b2f3b2312f78')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 42e2e32..34b5d65 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -29,6 +29,7 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SimpleSQLiteQuery import androidx.sqlite.db.SupportSQLiteDatabase import it.vfsfitvnm.vimusic.enums.AlbumSortBy +import it.vfsfitvnm.vimusic.enums.ArtistSortBy import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder @@ -147,6 +148,31 @@ interface Database { @Query("SELECT * FROM Artist WHERE id = :id") fun artist(id: String): Flow + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY name DESC") + fun artistsByNameDesc(): Flow> + + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY name ASC") + fun artistsByNameAsc(): Flow> + + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY ROWID DESC") + fun artistsByRowIdDesc(): Flow> + + @Query("SELECT * FROM Artist WHERE timestamp IS NOT NULL ORDER BY ROWID ASC") + fun artistsByRowIdAsc(): Flow> + + fun artists(sortBy: ArtistSortBy, sortOrder: SortOrder): Flow> { + return when (sortBy) { + ArtistSortBy.Name -> when (sortOrder) { + SortOrder.Ascending -> artistsByNameAsc() + SortOrder.Descending -> artistsByNameDesc() + } + ArtistSortBy.DateAdded -> when (sortOrder) { + SortOrder.Ascending -> artistsByRowIdAsc() + SortOrder.Descending -> artistsByRowIdDesc() + } + } + } + @Transaction @Query("SELECT * FROM Album WHERE id = :id") fun albumWithSongs(id: String): Flow @@ -398,7 +424,7 @@ interface Database { SortedSongPlaylistMap::class, SortedSongAlbumMap::class ], - version = 19, + version = 20, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -416,6 +442,7 @@ interface Database { AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), AutoMigration(from = 18, to = 19), + AutoMigration(from = 19, to = 20), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt new file mode 100644 index 0000000..2df4053 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ArtistSortBy.kt @@ -0,0 +1,6 @@ +package it.vfsfitvnm.vimusic.enums + +enum class ArtistSortBy { + Name, + DateAdded +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt index bca49ff..9f826f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Artist.kt @@ -15,5 +15,6 @@ data class Artist( val shufflePlaylistId: String? = null, val radioVideoId: String? = null, val radioPlaylistId: String? = null, - val timestamp: Long? + val timestamp: Long?, + val bookmarkedAt: Long? = null, ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index e8fd7bd..4ae81d2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -68,7 +68,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @ExperimentalFoundationApi @Composable -fun AlbumSongList( +fun AlbumOverview( browseId: String, viewModel: AlbumOverviewViewModel = viewModel( key = browseId, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index ac71200..6a77fdc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -29,7 +29,7 @@ fun AlbumScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - AlbumSongList(browseId = browseId) + AlbumOverview(browseId = browseId) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt new file mode 100644 index 0000000..e943ccb --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -0,0 +1,308 @@ +package it.vfsfitvnm.vimusic.ui.screens.artist + +import android.content.Intent +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import com.valentinilk.shimmer.shimmer +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +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.styling.shimmer +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.enqueue +import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalAnimationApi +@ExperimentalFoundationApi +@Composable +fun ArtistOverview( + browseId: String, + viewModel: ArtistOverviewViewModel = viewModel( + key = browseId, + factory = object : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + @Suppress("UNCHECKED_CAST") + return ArtistOverviewViewModel(browseId) as T + } + } + ) +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + val context = LocalContext.current + + BoxWithConstraints { + val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth + val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px + + viewModel.result?.getOrNull()?.let { albumWithSongs -> + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Column { + Header(title = albumWithSongs.album.title ?: "Unknown") { + if (albumWithSongs.songs.isNotEmpty()) { + BasicText( + text = "Enqueue", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + binder?.player?.enqueue( + albumWithSongs.songs.map(DetailedSong::asMediaItem) + ) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } + + Spacer( + modifier = Modifier + .weight(1f) + ) + + Image( + painter = painterResource( + if (albumWithSongs.album.bookmarkedAt == null) { + R.drawable.bookmark_outline + } else { + R.drawable.bookmark + } + ), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.accent), + modifier = Modifier + .clickable { + query { + Database.update( + albumWithSongs.album.copy( + bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { + System.currentTimeMillis() + } else { + null + } + ) + ) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + + Image( + painter = painterResource(R.drawable.share_social), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + albumWithSongs.album.shareUrl?.let { url -> + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + + context.startActivity( + Intent.createChooser( + sendIntent, + null + ) + ) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + AsyncImage( + model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + } + } + + itemsIndexed( + items = albumWithSongs.songs, + key = { _, song -> song.id } + ) { index, song -> + SongItem( + title = song.title, + authors = song.artistsText ?: albumWithSongs.album.authorsText, + durationText = song.durationText, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + albumWithSongs.songs.map(DetailedSong::asMediaItem), + index + ) + }, + startContent = { + BasicText( + text = "${index + 1}", + style = typography.s.semiBold.center.color(colorPalette.textDisabled), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .width(Dimensions.thumbnails.song) + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + albumWithSongs.songs + .shuffled() + .map(DetailedSong::asMediaItem) + ) + } + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } + } ?: viewModel.result?.exceptionOrNull()?.let { + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + viewModel.fetch(browseId) + } + } + .align(Alignment.Center) + .fillMaxSize() + ) { + BasicText( + text = "An error has occurred.\nTap to retry", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) + } + } ?: Column( + modifier = Modifier + .padding(LocalPlayerAwarePaddingValues.current) + .shimmer() + ) { + HeaderPlaceholder() + + Spacer( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + .background(colorPalette.shimmer) + ) + + repeat(3) { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .alpha(1f - index * 0.25f) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) + .height(Dimensions.thumbnails.song) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(Dimensions.thumbnails.song) + ) + + Column { + TextPlaceholder() + TextPlaceholder() + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt new file mode 100644 index 0000000..ccf0efb --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt @@ -0,0 +1,66 @@ +package it.vfsfitvnm.vimusic.ui.screens.artist + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.AlbumWithSongs +import it.vfsfitvnm.vimusic.models.SongAlbumMap +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +class ArtistOverviewViewModel(browseId: String) : ViewModel() { + var result by mutableStateOf?>(null) + private set + + private var job: Job? = null + + init { + fetch(browseId) + } + + fun fetch(browseId: String) { + job?.cancel() + result = null + + job = viewModelScope.launch(Dispatchers.IO) { + Database.albumWithSongs(browseId).collect { albumWithSongs -> + result = if (albumWithSongs?.album?.timestamp == null) { + YouTube.album(browseId)?.map { youtubeAlbum -> + Database.upsert( + Album( + id = browseId, + title = youtubeAlbum.title, + thumbnailUrl = youtubeAlbum.thumbnail?.url, + year = youtubeAlbum.year, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + shareUrl = youtubeAlbum.url, + timestamp = System.currentTimeMillis() + ), + youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> + albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> + Database.insert(mediaItem) + SongAlbumMap( + songId = mediaItem.mediaId, + albumId = browseId, + position = position + ) + } + } ?: emptyList() + ) + + null + } + } else { + Result.success(albumWithSongs) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt similarity index 92% rename from app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt rename to app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index 26638b7..b47a102 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -1,7 +1,8 @@ -package it.vfsfitvnm.vimusic.ui.screens +package it.vfsfitvnm.vimusic.ui.screens.artist import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.Canvas +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -25,7 +26,10 @@ import androidx.compose.foundation.text.BasicText 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.saveable.rememberSaveableStateHolder import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -49,7 +53,10 @@ import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +import it.vfsfitvnm.vimusic.ui.screens.album.AlbumOverview +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -70,9 +77,39 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking +@OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @Composable -fun ArtistScreen(browseId: String) { +fun AlbumScreen(browseId: String) { + val saveableStateHolder = rememberSaveableStateHolder() + val (tabIndex, onTabIndexChanged) = rememberSaveable { + mutableStateOf(0) + } + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = tabIndex, + onTabChanged = onTabIndexChanged, + tabColumnContent = { Item -> + Item(0, "Overview", R.drawable.sparkles) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { + ArtistOverview(browseId = browseId) + } + } + } + } +} + +@ExperimentalAnimationApi +@Composable +fun ArtistScreen2(browseId: String) { val lazyListState = rememberLazyListState() RouteHandler(listenToGlobalEmitter = true) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt new file mode 100644 index 0000000..110231b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt @@ -0,0 +1,171 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import androidx.annotation.DrawableRes +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.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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +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.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.CircleShape +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.remember +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.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.ArtistSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Artist +import it.vfsfitvnm.vimusic.ui.components.themed.Header +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.center +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.thumbnail + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun HomeArtistList( + onArtistClick: (Artist) -> Unit, + viewModel: HomeArtistListViewModel = viewModel() +) { + val (colorPalette, typography) = LocalAppearance.current + + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 + val thumbnailSizePx = thumbnailSizeDp.px + + val sortOrderIconRotation by animateFloatAsState( + targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + animationSpec = tween(durationMillis = 400, easing = LinearEasing) + ) + + val rippleIndication = rememberRipple(bounded = true) + + 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 + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0, + span = { GridItemSpan(maxLineSpan) } + ) { + Header(title = "Artists") { + @Composable + fun Item( + @DrawableRes iconId: Int, + sortBy: ArtistSortBy + ) { + 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.text, + sortBy = ArtistSortBy.Name + ) + + Item( + iconId = R.drawable.time, + sortBy = ArtistSortBy.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 } + ) + } + } + + items( + items = viewModel.items, + key = Artist::id + ) { artist -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .requiredWidth(thumbnailSizeDp) + .animateItemPlacement() + ) { + AsyncImage( + model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .clip(CircleShape) + .clickable( + indication = rippleIndication, + interactionSource = remember { MutableInteractionSource() }, + onClick = { onArtistClick(artist) } + ) + .background(colorPalette.background1) + .align(Alignment.CenterHorizontally) + .requiredSize(thumbnailSizeDp), + ) + + BasicText( + text = artist.name, + style = typography.xxs.semiBold.center, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt new file mode 100644 index 0000000..e733957 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt @@ -0,0 +1,67 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import android.app.Application +import android.content.SharedPreferences +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.core.content.edit +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.enums.ArtistSortBy +import it.vfsfitvnm.vimusic.enums.SortOrder +import it.vfsfitvnm.vimusic.models.Artist +import it.vfsfitvnm.vimusic.utils.artistSortByKey +import it.vfsfitvnm.vimusic.utils.artistSortOrderKey +import it.vfsfitvnm.vimusic.utils.getEnum +import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf +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 HomeArtistListViewModel(application: Application) : AndroidViewModel(application) { + var items by mutableStateOf(emptyList()) + private set + + var sortBy by mutableStatePreferenceOf( + preferences.getEnum( + artistSortByKey, + ArtistSortBy.DateAdded + ) + ) { + preferences.edit { putEnum(artistSortByKey, it) } + collectItems(sortBy = it) + } + + var sortOrder by mutableStatePreferenceOf( + preferences.getEnum( + artistSortOrderKey, + SortOrder.Ascending + ) + ) { + preferences.edit { putEnum(artistSortOrderKey, it) } + collectItems(sortOrder = it) + } + + private var job: Job? = null + + private val preferences: SharedPreferences + get() = getApplication().preferences + + init { + collectItems() + } + + private fun collectItems(sortBy: ArtistSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { + job?.cancel() + job = viewModelScope.launch { + Database.artists(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { + items = it + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index f064c1f..1b7192a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -15,6 +15,7 @@ import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute +import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute @@ -110,12 +111,15 @@ fun HomeScreen() { onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, onPlaylistClicked = { localPlaylistRoute(it.id) } ) -// 2 -> HomeArtistList( -// onArtistClicked = { artistRoute(it.id) } -// ) + + 2 -> HomeArtistList( + onArtistClick = { artistRoute(it.id) } + ) + 3 -> HomeAlbumList( onAlbumClick = { albumRoute(it.id) } ) + else -> HomeSongList() } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt index def091b..2cd3eb2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Preferences.kt @@ -23,6 +23,8 @@ const val playlistSortOrderKey = "playlistSortOrder" const val playlistSortByKey = "playlistSortBy" const val albumSortOrderKey = "albumSortOrder" const val albumSortByKey = "albumSortBy" +const val artistSortOrderKey = "artistSortOrder" +const val artistSortByKey = "artistSortBy" const val repeatModeKey = "repeatMode" const val skipSilenceKey = "skipSilence" const val volumeNormalizationKey = "volumeNormalization" From f98172506228ffb8d3f0c331af210ab3026d4b7b Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Mon, 26 Sep 2022 14:52:39 +0200 Subject: [PATCH 014/100] Drop ViewModel --- app/build.gradle.kts | 1 - .../18.json | 24 +- .../19.json | 608 ----------------- .../20.json | 614 ------------------ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 17 +- .../vimusic/models/AlbumWithSongs.kt | 22 - .../vimusic/models/SortedSongAlbumMap.kt | 13 - .../vimusic/savers/AlbumListSaver.kt | 3 + .../vimusic/savers/AlbumResultSaver.kt | 3 + .../it/vfsfitvnm/vimusic/savers/AlbumSaver.kt | 29 + .../vimusic/savers/ArtistListSaver.kt | 3 + .../vfsfitvnm/vimusic/savers/ArtistSaver.kt | 34 + .../vimusic/savers/DetailedSongListSaver.kt | 3 + .../vimusic/savers/DetailedSongSaver.kt | 33 + .../vfsfitvnm/vimusic/savers/InfoListSaver.kt | 3 + .../it/vfsfitvnm/vimusic/savers/InfoSaver.kt | 16 + .../it/vfsfitvnm/vimusic/savers/ListSaver.kt | 20 + .../savers/PlaylistPreviewListSaver.kt | 3 + .../vimusic/savers/PlaylistPreviewSaver.kt | 21 + .../vfsfitvnm/vimusic/savers/PlaylistSaver.kt | 19 + .../vfsfitvnm/vimusic/savers/ResultSaver.kt | 18 + .../vimusic/savers/SearchQueryListSaver.kt | 3 + .../vimusic/savers/SearchQuerySaver.kt | 17 + .../vimusic/savers/StringListResultSaver.kt | 5 + .../vimusic/savers/StringResultSaver.kt | 5 + .../vimusic/savers/YouTubeAlbumListSaver.kt | 3 + .../vimusic/savers/YouTubeAlbumSaver.kt | 22 + .../vimusic/savers/YouTubeArtistListSaver.kt | 3 + .../vimusic/savers/YouTubeArtistSaver.kt | 19 + .../savers/YouTubeBrowseEndpointSaver.kt | 18 + .../savers/YouTubeBrowseInfoListSaver.kt | 3 + .../vimusic/savers/YouTubeBrowseInfoSaver.kt | 18 + .../savers/YouTubePlaylistListSaver.kt | 3 + .../vimusic/savers/YouTubePlaylistSaver.kt | 21 + .../vimusic/savers/YouTubeSongListSaver.kt | 3 + .../vimusic/savers/YouTubeSongSaver.kt | 24 + .../vimusic/savers/YouTubeThumbnailSaver.kt | 19 + .../vimusic/savers/YouTubeVideoListSaver.kt | 3 + .../vimusic/savers/YouTubeVideoSaver.kt | 24 + .../savers/YouTubeWatchEndpointSaver.kt | 24 + .../vimusic/savers/YouTubeWatchInfoSaver.kt | 18 + .../it/vfsfitvnm/vimusic/ui/screens/Routes.kt | 1 + .../vimusic/ui/screens/album/AlbumOverview.kt | 57 +- .../screens/album/AlbumOverviewViewModel.kt | 66 -- .../vimusic/ui/screens/album/AlbumScreen.kt | 54 +- .../ui/screens/artist/ArtistOverview.kt | 452 +++++++------ .../screens/artist/ArtistOverviewViewModel.kt | 66 -- .../vimusic/ui/screens/artist/ArtistScreen.kt | 2 +- .../vimusic/ui/screens/home/HomeAlbumList.kt | 39 +- .../ui/screens/home/HomeAlbumListViewModel.kt | 67 -- .../vimusic/ui/screens/home/HomeArtistList.kt | 37 +- .../screens/home/HomeArtistListViewModel.kt | 67 -- .../ui/screens/home/HomePlaylistList.kt | 35 +- .../screens/home/HomePlaylistListViewModel.kt | 70 -- .../vimusic/ui/screens/home/HomeSongList.kt | 75 ++- .../ui/screens/home/HomeSongListViewModel.kt | 67 -- .../ui/screens/search/LocalSongSearch.kt | 29 +- .../search/LocalSongSearchViewModel.kt | 25 - .../vimusic/ui/screens/search/OnlineSearch.kt | 47 +- .../screens/search/OnlineSearchViewModel.kt | 36 - .../ui/screens/searchresult/SearchResult.kt | 69 +- .../searchresult/SearchResultScreen.kt | 20 +- .../searchresult/SearchResultViewModel.kt | 45 -- .../vimusic/ui/views/YouTubeItems.kt | 6 +- .../vimusic/utils/ProduceSaveableListState.kt | 102 +++ .../vimusic/utils/ProduceSaveableState.kt | 118 ++++ .../it/vfsfitvnm/vimusic/utils/Utils.kt | 12 +- settings.gradle.kts | 1 - .../it/vfsfitvnm/youtubemusic/YouTube.kt | 16 +- 69 files changed, 1269 insertions(+), 2174 deletions(-) delete mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json delete mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 165754e..5687011 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -84,7 +84,6 @@ dependencies { implementation(libs.compose.ripple) implementation(libs.compose.shimmer) implementation(libs.compose.coil) - implementation(libs.compose.viewmodel) implementation(libs.palette) diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json index 00613b9..e0c912d 100644 --- a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/18.json @@ -2,7 +2,7 @@ "formatVersion": 1, "database": { "version": 18, - "identityHash": "dec162db7ec49f4324481d54c49a793d", + "identityHash": "c8f776e899b181081f0230bffec99ac5", "entities": [ { "tableName": "Song", @@ -181,7 +181,7 @@ }, { "tableName": "Artist", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -236,6 +236,12 @@ "columnName": "timestamp", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -318,7 +324,7 @@ }, { "tableName": "Album", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", "fields": [ { "fieldPath": "id", @@ -361,6 +367,12 @@ "columnName": "timestamp", "affinity": "INTEGER", "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false } ], "primaryKey": { @@ -588,15 +600,11 @@ { "viewName": "SortedSongPlaylistMap", "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" - }, - { - "viewName": "SortedSongAlbumMap", - "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" } ], "setupQueries": [ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dec162db7ec49f4324481d54c49a793d')" + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c8f776e899b181081f0230bffec99ac5')" ] } } \ No newline at end of file diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json deleted file mode 100644 index 240b6e0..0000000 --- a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json +++ /dev/null @@ -1,608 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 19, - "identityHash": "41479c8284963d3533c4baa46d7464a6", - "entities": [ - { - "tableName": "Song", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "artistsText", - "columnName": "artistsText", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "durationText", - "columnName": "durationText", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnailUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lyrics", - "columnName": "lyrics", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "synchronizedLyrics", - "columnName": "synchronizedLyrics", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "likedAt", - "columnName": "likedAt", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "totalPlayTimeMs", - "columnName": "totalPlayTimeMs", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SongPlaylistMap", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "playlistId", - "columnName": "playlistId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId", - "playlistId" - ] - }, - "indices": [ - { - "name": "index_SongPlaylistMap_songId", - "unique": false, - "columnNames": [ - "songId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" - }, - { - "name": "index_SongPlaylistMap_playlistId", - "unique": false, - "columnNames": [ - "playlistId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" - } - ], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Playlist", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "playlistId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Playlist", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "browseId", - "columnName": "browseId", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Artist", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnailUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "info", - "columnName": "info", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "shuffleVideoId", - "columnName": "shuffleVideoId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "shufflePlaylistId", - "columnName": "shufflePlaylistId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "radioVideoId", - "columnName": "radioVideoId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "radioPlaylistId", - "columnName": "radioPlaylistId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SongArtistMap", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "artistId", - "columnName": "artistId", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId", - "artistId" - ] - }, - "indices": [ - { - "name": "index_SongArtistMap_songId", - "unique": false, - "columnNames": [ - "songId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" - }, - { - "name": "index_SongArtistMap_artistId", - "unique": false, - "columnNames": [ - "artistId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" - } - ], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Artist", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "artistId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Album", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnailUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "year", - "columnName": "year", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "authorsText", - "columnName": "authorsText", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "shareUrl", - "columnName": "shareUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "bookmarkedAt", - "columnName": "bookmarkedAt", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SongAlbumMap", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "albumId", - "columnName": "albumId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId", - "albumId" - ] - }, - "indices": [ - { - "name": "index_SongAlbumMap_songId", - "unique": false, - "columnNames": [ - "songId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" - }, - { - "name": "index_SongAlbumMap_albumId", - "unique": false, - "columnNames": [ - "albumId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" - } - ], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Album", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "albumId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "SearchQuery", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "query", - "columnName": "query", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_SearchQuery_query", - "unique": true, - "columnNames": [ - "query" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "QueuedMediaItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "mediaItem", - "columnName": "mediaItem", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Format", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "itag", - "columnName": "itag", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "mimeType", - "columnName": "mimeType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "bitrate", - "columnName": "bitrate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "contentLength", - "columnName": "contentLength", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "lastModified", - "columnName": "lastModified", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "loudnessDb", - "columnName": "loudnessDb", - "affinity": "REAL", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId" - ] - }, - "indices": [], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - } - ] - } - ], - "views": [ - { - "viewName": "SortedSongPlaylistMap", - "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" - }, - { - "viewName": "SortedSongAlbumMap", - "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '41479c8284963d3533c4baa46d7464a6')" - ] - } -} \ No newline at end of file diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json deleted file mode 100644 index ed16244..0000000 --- a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/20.json +++ /dev/null @@ -1,614 +0,0 @@ -{ - "formatVersion": 1, - "database": { - "version": 20, - "identityHash": "821aa30ff7d14b31e839b2f3b2312f78", - "entities": [ - { - "tableName": "Song", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "artistsText", - "columnName": "artistsText", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "durationText", - "columnName": "durationText", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnailUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "lyrics", - "columnName": "lyrics", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "synchronizedLyrics", - "columnName": "synchronizedLyrics", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "likedAt", - "columnName": "likedAt", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "totalPlayTimeMs", - "columnName": "totalPlayTimeMs", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SongPlaylistMap", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "playlistId", - "columnName": "playlistId", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId", - "playlistId" - ] - }, - "indices": [ - { - "name": "index_SongPlaylistMap_songId", - "unique": false, - "columnNames": [ - "songId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" - }, - { - "name": "index_SongPlaylistMap_playlistId", - "unique": false, - "columnNames": [ - "playlistId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" - } - ], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Playlist", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "playlistId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Playlist", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "browseId", - "columnName": "browseId", - "affinity": "TEXT", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Artist", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "name", - "columnName": "name", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnailUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "info", - "columnName": "info", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "shuffleVideoId", - "columnName": "shuffleVideoId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "shufflePlaylistId", - "columnName": "shufflePlaylistId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "radioVideoId", - "columnName": "radioVideoId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "radioPlaylistId", - "columnName": "radioPlaylistId", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "bookmarkedAt", - "columnName": "bookmarkedAt", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SongArtistMap", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "artistId", - "columnName": "artistId", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId", - "artistId" - ] - }, - "indices": [ - { - "name": "index_SongArtistMap_songId", - "unique": false, - "columnNames": [ - "songId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" - }, - { - "name": "index_SongArtistMap_artistId", - "unique": false, - "columnNames": [ - "artistId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" - } - ], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Artist", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "artistId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "Album", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "title", - "columnName": "title", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "thumbnailUrl", - "columnName": "thumbnailUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "year", - "columnName": "year", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "authorsText", - "columnName": "authorsText", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "shareUrl", - "columnName": "shareUrl", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "timestamp", - "columnName": "timestamp", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "bookmarkedAt", - "columnName": "bookmarkedAt", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "SongAlbumMap", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "albumId", - "columnName": "albumId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId", - "albumId" - ] - }, - "indices": [ - { - "name": "index_SongAlbumMap_songId", - "unique": false, - "columnNames": [ - "songId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" - }, - { - "name": "index_SongAlbumMap_albumId", - "unique": false, - "columnNames": [ - "albumId" - ], - "orders": [], - "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" - } - ], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - }, - { - "table": "Album", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "albumId" - ], - "referencedColumns": [ - "id" - ] - } - ] - }, - { - "tableName": "SearchQuery", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "query", - "columnName": "query", - "affinity": "TEXT", - "notNull": true - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [ - { - "name": "index_SearchQuery_query", - "unique": true, - "columnNames": [ - "query" - ], - "orders": [], - "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" - } - ], - "foreignKeys": [] - }, - { - "tableName": "QueuedMediaItem", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", - "fields": [ - { - "fieldPath": "id", - "columnName": "id", - "affinity": "INTEGER", - "notNull": true - }, - { - "fieldPath": "mediaItem", - "columnName": "mediaItem", - "affinity": "BLOB", - "notNull": true - }, - { - "fieldPath": "position", - "columnName": "position", - "affinity": "INTEGER", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": true, - "columnNames": [ - "id" - ] - }, - "indices": [], - "foreignKeys": [] - }, - { - "tableName": "Format", - "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", - "fields": [ - { - "fieldPath": "songId", - "columnName": "songId", - "affinity": "TEXT", - "notNull": true - }, - { - "fieldPath": "itag", - "columnName": "itag", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "mimeType", - "columnName": "mimeType", - "affinity": "TEXT", - "notNull": false - }, - { - "fieldPath": "bitrate", - "columnName": "bitrate", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "contentLength", - "columnName": "contentLength", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "lastModified", - "columnName": "lastModified", - "affinity": "INTEGER", - "notNull": false - }, - { - "fieldPath": "loudnessDb", - "columnName": "loudnessDb", - "affinity": "REAL", - "notNull": false - } - ], - "primaryKey": { - "autoGenerate": false, - "columnNames": [ - "songId" - ] - }, - "indices": [], - "foreignKeys": [ - { - "table": "Song", - "onDelete": "CASCADE", - "onUpdate": "NO ACTION", - "columns": [ - "songId" - ], - "referencedColumns": [ - "id" - ] - } - ] - } - ], - "views": [ - { - "viewName": "SortedSongPlaylistMap", - "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" - }, - { - "viewName": "SortedSongAlbumMap", - "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongAlbumMap ORDER BY position" - } - ], - "setupQueries": [ - "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", - "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '821aa30ff7d14b31e839b2f3b2312f78')" - ] - } -} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index 34b5d65..ea007bb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -34,7 +34,6 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SongSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.models.AlbumWithSongs import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength @@ -48,7 +47,6 @@ import it.vfsfitvnm.vimusic.models.Song import it.vfsfitvnm.vimusic.models.SongAlbumMap import it.vfsfitvnm.vimusic.models.SongArtistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.models.SortedSongAlbumMap import it.vfsfitvnm.vimusic.models.SortedSongPlaylistMap import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -173,9 +171,13 @@ interface Database { } } - @Transaction @Query("SELECT * FROM Album WHERE id = :id") - fun albumWithSongs(id: String): Flow + fun album(id: String): Flow + + @Transaction + @Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position") + @RewriteQueriesToDropUnusedColumns + fun albumSongs(albumId: String): Flow> @Query("SELECT * FROM Album WHERE bookmarkedAt IS NOT NULL ORDER BY title ASC") fun albumsByTitleAsc(): Flow> @@ -421,10 +423,9 @@ interface Database { Format::class, ], views = [ - SortedSongPlaylistMap::class, - SortedSongAlbumMap::class + SortedSongPlaylistMap::class ], - version = 20, + version = 18, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -441,8 +442,6 @@ interface Database { AutoMigration(from = 15, to = 16), AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), - AutoMigration(from = 18, to = 19), - AutoMigration(from = 19, to = 20), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt deleted file mode 100644 index 0ca36e5..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/AlbumWithSongs.kt +++ /dev/null @@ -1,22 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.compose.runtime.Immutable -import androidx.room.Embedded -import androidx.room.Junction -import androidx.room.Relation - -@Immutable -data class AlbumWithSongs( - @Embedded val album: Album, - @Relation( - entity = Song::class, - parentColumn = "id", - entityColumn = "id", - associateBy = Junction( - value = SortedSongAlbumMap::class, - parentColumn = "albumId", - entityColumn = "songId" - ) - ) - val songs: List -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt deleted file mode 100644 index b41b451..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/SortedSongAlbumMap.kt +++ /dev/null @@ -1,13 +0,0 @@ -package it.vfsfitvnm.vimusic.models - -import androidx.compose.runtime.Immutable -import androidx.room.ColumnInfo -import androidx.room.DatabaseView - -@Immutable -@DatabaseView("SELECT * FROM SongAlbumMap ORDER BY position") -data class SortedSongAlbumMap( - @ColumnInfo(index = true) val songId: String, - @ColumnInfo(index = true) val albumId: String, - val position: Int -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt new file mode 100644 index 0000000..b0c86d0 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val AlbumListSaver = ListSaver.of(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt new file mode 100644 index 0000000..4b9eea3 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val AlbumResultSaver = ResultSaver.of(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt new file mode 100644 index 0000000..2f88b36 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumSaver.kt @@ -0,0 +1,29 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Album + +object AlbumSaver : Saver> { + override fun SaverScope.save(value: Album): List = listOf( + value.id, + value.title, + value.thumbnailUrl, + value.year, + value.authorsText, + value.shareUrl, + value.timestamp, + value.bookmarkedAt, + ) + + override fun restore(value: List): Album = Album( + id = value[0] as String, + title = value[1] as String, + thumbnailUrl = value[2] as String?, + year = value[3] as String?, + authorsText = value[4] as String?, + shareUrl = value[5] as String?, + timestamp = value[6] as Long?, + bookmarkedAt = value[7] as Long?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt new file mode 100644 index 0000000..125d725 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val ArtistListSaver = ListSaver.of(ArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt new file mode 100644 index 0000000..a609450 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ArtistSaver.kt @@ -0,0 +1,34 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Artist +import it.vfsfitvnm.vimusic.models.Playlist + +object ArtistSaver : Saver> { + override fun SaverScope.save(value: Artist): List = listOf( + value.id, + value.name, + value.thumbnailUrl, + value.info, + value.shuffleVideoId, + value.shufflePlaylistId, + value.radioVideoId, + value.radioPlaylistId, + value.timestamp, + value.bookmarkedAt, + ) + + override fun restore(value: List): Artist = Artist( + id = value[0] as String, + name = value[1] as String, + thumbnailUrl = value[2] as String?, + info = value[3] as String?, + shuffleVideoId = value[4] as String?, + shufflePlaylistId = value[5] as String?, + radioVideoId = value[6] as String?, + radioPlaylistId = value[7] as String?, + timestamp = value[8] as Long?, + bookmarkedAt = value[9] as Long?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt new file mode 100644 index 0000000..cdca8e8 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val DetailedSongListSaver = ListSaver.of(DetailedSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt new file mode 100644 index 0000000..cfede7b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt @@ -0,0 +1,33 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.DetailedSong + +object DetailedSongSaver : Saver> { + override fun SaverScope.save(value: DetailedSong): List = + listOf( + value.id, + value.title, + value.artistsText, + value.durationText, + value.thumbnailUrl, + value.totalPlayTimeMs, + value.albumId, + value.artists?.let { with(InfoListSaver) { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List): DetailedSong? { + return if (value.size == 8) DetailedSong( + id = value[0] as String, + title = value[1] as String, + artistsText = value[2] as String?, + durationText = value[3] as String, + thumbnailUrl = value[4] as String?, + totalPlayTimeMs = value[5] as Long, + albumId = value[6] as String?, + artists = InfoListSaver.restore(value[7] as List>) + ) else null + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt new file mode 100644 index 0000000..8c3347f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val InfoListSaver = ListSaver.of(InfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt new file mode 100644 index 0000000..3f59e7b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt @@ -0,0 +1,16 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Info + +object InfoSaver : Saver> { + override fun SaverScope.save(value: Info): List = listOf(value.id, value.name) + + override fun restore(value: List): Info? { + return if (value.size == 2) Info( + id = value[0], + name = value[1], + ) else null + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt new file mode 100644 index 0000000..3b7f617 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt @@ -0,0 +1,20 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +interface ListSaver : Saver, List> { + companion object { + fun of(saver: Saver): ListSaver { + return object : ListSaver { + override fun restore(value: List): List { + return value.mapNotNull(saver::restore) + } + + override fun SaverScope.save(value: List): List { + return with(saver) { value.mapNotNull { save(it) } } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt new file mode 100644 index 0000000..6d726e3 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val PlaylistPreviewListSaver = ListSaver.of(PlaylistPreviewSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt new file mode 100644 index 0000000..5641d4f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistPreviewSaver.kt @@ -0,0 +1,21 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.PlaylistPreview + +object PlaylistPreviewSaver : Saver> { + override fun SaverScope.save(value: PlaylistPreview): List { + return listOf( + with(PlaylistSaver) { save(value.playlist) }, + value.songCount, + ) + } + + override fun restore(value: List): PlaylistPreview? { + return if (value.size == 2) PlaylistPreview( + playlist = PlaylistSaver.restore(value[0] as List), + songCount = value[1] as Int, + ) else null + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt new file mode 100644 index 0000000..c660f57 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistSaver.kt @@ -0,0 +1,19 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.Playlist + +object PlaylistSaver : Saver> { + override fun SaverScope.save(value: Playlist): List = listOf( + value.id, + value.name, + value.browseId, + ) + + override fun restore(value: List): Playlist = Playlist( + id = value[0] as Long, + name = value[1] as String, + browseId = value[2] as String?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt new file mode 100644 index 0000000..e750098 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +interface ResultSaver : Saver?, Pair> { + companion object { + fun of(saver: Saver) = + object : Saver?, Pair> { + override fun restore(value: Pair) = + value.first?.let(saver::restore)?.let(Result.Companion::success) + ?: value.second?.let(Result.Companion::failure) + + override fun SaverScope.save(value: Result?) = + with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull() + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt new file mode 100644 index 0000000..9e580f5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQueryListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val SearchQueryListSaver = ListSaver.of(SearchQuerySaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt new file mode 100644 index 0000000..57df500 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/SearchQuerySaver.kt @@ -0,0 +1,17 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.SearchQuery + +object SearchQuerySaver : Saver> { + override fun SaverScope.save(value: SearchQuery): List = listOf( + value.id, + value.query, + ) + + override fun restore(value: List) = SearchQuery( + id = value[0] as Long, + query = value[1] as String + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt new file mode 100644 index 0000000..f7da5d5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt @@ -0,0 +1,5 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.autoSaver + +val StringListResultSaver = ResultSaver.of(autoSaver?>()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt new file mode 100644 index 0000000..a5b35aa --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt @@ -0,0 +1,5 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.autoSaver + +val StringResultSaver = ResultSaver.of(autoSaver()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt new file mode 100644 index 0000000..b7c6f0a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeAlbumListSaver = ListSaver.of(YouTubeAlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt new file mode 100644 index 0000000..c4a5f8d --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt @@ -0,0 +1,22 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeAlbumSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Album): List = listOf( + with(YouTubeBrowseInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.year, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Item.Album( + info = YouTubeBrowseInfoSaver.restore(value[0] as List), + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), + year = value[2] as String?, + thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt new file mode 100644 index 0000000..07d70fc --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeArtistListSaver = ListSaver.of(YouTubeArtistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt new file mode 100644 index 0000000..98a1965 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt @@ -0,0 +1,19 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeArtistSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Artist): List = listOf( + with(YouTubeBrowseInfoSaver) { save(value.info) }, + value.subscribersCountText, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Item.Artist( + info = YouTubeBrowseInfoSaver.restore(value[0] as List), + subscribersCountText = value[1] as String?, + thumbnail = (value[2] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt new file mode 100644 index 0000000..30aa186 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseEndpointSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeBrowseEndpointSaver : Saver> { + override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Browse) = listOf( + value.browseId, + value.params + ) + + override fun restore(value: List) = NavigationEndpoint.Endpoint.Browse( + browseId = value[0] as String, + params = value[1] as String?, + browseEndpointContextSupportedConfigs = null + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt new file mode 100644 index 0000000..6d700e9 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeBrowseInfoListSaver = ListSaver.of(YouTubeBrowseInfoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt new file mode 100644 index 0000000..a421e53 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeBrowseInfoSaver : Saver, List> { + override fun SaverScope.save(value: YouTube.Info) = listOf( + value.name, + with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Info( + name = value[0] as String, + endpoint = (value[1] as List?)?.let(YouTubeBrowseEndpointSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt new file mode 100644 index 0000000..7fe7d81 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubePlaylistListSaver = ListSaver.of(YouTubePlaylistSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt new file mode 100644 index 0000000..9767efe --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt @@ -0,0 +1,21 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubePlaylistSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Playlist): List = listOf( + with(YouTubeBrowseInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoSaver) { value.channel?.let { save(it) } }, + value.songCount, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Item.Playlist( + info = YouTubeBrowseInfoSaver.restore(value[0] as List), + channel = (value[1] as List?)?.let(YouTubeBrowseInfoSaver::restore), + songCount = value[2] as Int?, + thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt new file mode 100644 index 0000000..8dade3e --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeSongListSaver = ListSaver.of(YouTubeSongSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt new file mode 100644 index 0000000..4b8cdf8 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt @@ -0,0 +1,24 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeSongSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Song): List = listOf( + with(YouTubeWatchInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + with(YouTubeBrowseInfoSaver) { value.album?.let { save(it) } }, + value.durationText, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Item.Song( + info = YouTubeWatchInfoSaver.restore(value[0] as List), + authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + album = (value[2] as List?)?.let(YouTubeBrowseInfoSaver::restore), + durationText = value[3] as String?, + thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt new file mode 100644 index 0000000..d5664a4 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeThumbnailSaver.kt @@ -0,0 +1,19 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer + +object YouTubeThumbnailSaver : Saver> { + override fun SaverScope.save(value: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail) = listOf( + value.url, + value.width, + value.height + ) + + override fun restore(value: List) = ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail( + url = value[0] as String, + width = value[1] as Int, + height = value[2] as Int?, + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt new file mode 100644 index 0000000..2e05f70 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoListSaver.kt @@ -0,0 +1,3 @@ +package it.vfsfitvnm.vimusic.savers + +val YouTubeVideoListSaver = ListSaver.of(YouTubeVideoSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt new file mode 100644 index 0000000..b745939 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt @@ -0,0 +1,24 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeVideoSaver : Saver> { + override fun SaverScope.save(value: YouTube.Item.Video): List = listOf( + with(YouTubeWatchInfoSaver) { save(value.info) }, + with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.viewsText, + value.durationText, + with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Item.Video( + info = YouTubeWatchInfoSaver.restore(value[0] as List), + authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + viewsText = value[2] as String?, + durationText = value[3] as String?, + thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt new file mode 100644 index 0000000..5548bcf --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchEndpointSaver.kt @@ -0,0 +1,24 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeWatchEndpointSaver : Saver> { + override fun SaverScope.save(value: NavigationEndpoint.Endpoint.Watch) = listOf( + value.params, + value.playlistId, + value.videoId, + value.index, + value.playlistSetVideoId, + ) + + override fun restore(value: List) = NavigationEndpoint.Endpoint.Watch( + params = value[0] as String?, + playlistId = value[1] as String?, + videoId = value[2] as String?, + index = value[3] as Int?, + playlistSetVideoId = value[4] as String?, + watchEndpointMusicSupportedConfigs = null + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt new file mode 100644 index 0000000..a563724 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt @@ -0,0 +1,18 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint + +object YouTubeWatchInfoSaver : Saver, List> { + override fun SaverScope.save(value: YouTube.Info) = listOf( + value.name, + with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } } + ) + + override fun restore(value: List) = YouTube.Info( + name = value[0] as String, + endpoint = (value[1] as List?)?.let(YouTubeWatchEndpointSaver::restore) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index d41a827..8f8e16f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -9,6 +9,7 @@ import it.vfsfitvnm.route.Route1 import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen +import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen val albumRoute = Route1("albumRoute") val artistRoute = Route1("artistRoute") diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index 4ae81d2..fdffff9 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -24,6 +24,7 @@ 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.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -34,17 +35,16 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu @@ -61,6 +61,7 @@ import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableListState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @@ -69,26 +70,26 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalFoundationApi @Composable fun AlbumOverview( + albumResult: Result?, browseId: String, - viewModel: AlbumOverviewViewModel = viewModel( - key = browseId, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return AlbumOverviewViewModel(browseId) as T - } - } - ) ) { val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val context = LocalContext.current + + val songs by produceSaveableListState( + flowProvider = { + Database.albumSongs(browseId) + }, + stateSaver = DetailedSongListSaver + + ) BoxWithConstraints { val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px - viewModel.result?.getOrNull()?.let { albumWithSongs -> + albumResult?.getOrNull()?.let { album -> LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, modifier = Modifier @@ -100,8 +101,8 @@ fun AlbumOverview( contentType = 0 ) { Column { - Header(title = albumWithSongs.album.title ?: "Unknown") { - if (albumWithSongs.songs.isNotEmpty()) { + Header(title = album.title ?: "Unknown") { + if (songs.isNotEmpty()) { BasicText( text = "Enqueue", style = typography.xxs.medium, @@ -109,7 +110,7 @@ fun AlbumOverview( .clip(RoundedCornerShape(16.dp)) .clickable { binder?.player?.enqueue( - albumWithSongs.songs.map(DetailedSong::asMediaItem) + songs.map(DetailedSong::asMediaItem) ) } .background(colorPalette.background2) @@ -125,7 +126,7 @@ fun AlbumOverview( Image( painter = painterResource( - if (albumWithSongs.album.bookmarkedAt == null) { + if (album.bookmarkedAt == null) { R.drawable.bookmark_outline } else { R.drawable.bookmark @@ -137,8 +138,8 @@ fun AlbumOverview( .clickable { query { Database.update( - albumWithSongs.album.copy( - bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { + album.copy( + bookmarkedAt = if (album.bookmarkedAt == null) { System.currentTimeMillis() } else { null @@ -157,7 +158,7 @@ fun AlbumOverview( colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier .clickable { - albumWithSongs.album.shareUrl?.let { url -> + album.shareUrl?.let { url -> val sendIntent = Intent().apply { action = Intent.ACTION_SEND type = "text/plain" @@ -178,7 +179,7 @@ fun AlbumOverview( } AsyncImage( - model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), + model = album.thumbnailUrl?.thumbnail(thumbnailSizePx), contentDescription = null, modifier = Modifier .align(Alignment.CenterHorizontally) @@ -190,17 +191,17 @@ fun AlbumOverview( } itemsIndexed( - items = albumWithSongs.songs, + items = songs, key = { _, song -> song.id } ) { index, song -> SongItem( title = song.title, - authors = song.artistsText ?: albumWithSongs.album.authorsText, + authors = song.artistsText ?: album.authorsText, durationText = song.durationText, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex( - albumWithSongs.songs.map(DetailedSong::asMediaItem), + songs.map(DetailedSong::asMediaItem), index ) }, @@ -227,10 +228,10 @@ fun AlbumOverview( .padding(all = 16.dp) .padding(LocalPlayerAwarePaddingValues.current) .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { + .clickable(enabled = songs.isNotEmpty()) { binder?.stopRadio() binder?.player?.forcePlayFromBeginning( - albumWithSongs.songs + songs .shuffled() .map(DetailedSong::asMediaItem) ) @@ -247,12 +248,12 @@ fun AlbumOverview( .size(20.dp) ) } - } ?: viewModel.result?.exceptionOrNull()?.let { + } ?: albumResult?.exceptionOrNull()?.let { Box( modifier = Modifier .pointerInput(Unit) { detectTapGestures { - viewModel.fetch(browseId) +// viewModel.fetch(browseId) } } .align(Alignment.Center) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt deleted file mode 100644 index 7174d59..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverviewViewModel.kt +++ /dev/null @@ -1,66 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.album - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.models.AlbumWithSongs -import it.vfsfitvnm.vimusic.models.SongAlbumMap -import it.vfsfitvnm.vimusic.utils.toMediaItem -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - -class AlbumOverviewViewModel(browseId: String) : ViewModel() { - var result by mutableStateOf?>(null) - private set - - private var job: Job? = null - - init { - fetch(browseId) - } - - fun fetch(browseId: String) { - job?.cancel() - result = null - - job = viewModelScope.launch(Dispatchers.IO) { - Database.albumWithSongs(browseId).collect { albumWithSongs -> - result = if (albumWithSongs?.album?.timestamp == null) { - YouTube.album(browseId)?.map { youtubeAlbum -> - Database.upsert( - Album( - id = browseId, - title = youtubeAlbum.title, - thumbnailUrl = youtubeAlbum.thumbnail?.url, - year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, - shareUrl = youtubeAlbum.url, - timestamp = System.currentTimeMillis() - ), - youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> - albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> - Database.insert(mediaItem) - SongAlbumMap( - songId = mediaItem.mediaId, - albumId = browseId, - position = position - ) - } - } ?: emptyList() - ) - - null - } - } else { - Result.success(albumWithSongs) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index 6a77fdc..530a211 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -3,11 +3,21 @@ package it.vfsfitvnm.vimusic.ui.screens.album import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.runtime.saveable.rememberSaveableStateHolder import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.models.SongAlbumMap +import it.vfsfitvnm.vimusic.savers.AlbumResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @@ -19,6 +29,45 @@ fun AlbumScreen(browseId: String) { globalRoutes() host { + val albumResult by produceSaveableState( + initialValue = null, + stateSaver = AlbumResultSaver, + ) { + withContext(Dispatchers.IO) { + Database.album(browseId).collect { album -> + if (album?.timestamp == null) { + YouTube.album(browseId)?.map { youtubeAlbum -> + Database.upsert( + Album( + id = browseId, + title = youtubeAlbum.title, + thumbnailUrl = youtubeAlbum.thumbnail?.url, + year = youtubeAlbum.year, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + shareUrl = youtubeAlbum.url, + timestamp = System.currentTimeMillis() + ), + youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> + albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> + Database.insert(mediaItem) + SongAlbumMap( + songId = mediaItem.mediaId, + albumId = browseId, + position = position + ) + } + } ?: emptyList() + ) + + null + } + } else { + value = Result.success(album) + } + } + } + } + Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, @@ -29,7 +78,10 @@ fun AlbumScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - AlbumOverview(browseId = browseId) + AlbumOverview( + albumResult = albumResult, + browseId = browseId, + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt index e943ccb..ba04073 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -36,7 +36,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.vimusic.Database @@ -70,239 +69,230 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @Composable fun ArtistOverview( browseId: String, - viewModel: ArtistOverviewViewModel = viewModel( - key = browseId, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return ArtistOverviewViewModel(browseId) as T - } - } - ) ) { val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val context = LocalContext.current - BoxWithConstraints { - val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth - val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px - - viewModel.result?.getOrNull()?.let { albumWithSongs -> - LazyColumn( - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item( - key = "header", - contentType = 0 - ) { - Column { - Header(title = albumWithSongs.album.title ?: "Unknown") { - if (albumWithSongs.songs.isNotEmpty()) { - BasicText( - text = "Enqueue", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable { - binder?.player?.enqueue( - albumWithSongs.songs.map(DetailedSong::asMediaItem) - ) - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) - ) - } - - Spacer( - modifier = Modifier - .weight(1f) - ) - - Image( - painter = painterResource( - if (albumWithSongs.album.bookmarkedAt == null) { - R.drawable.bookmark_outline - } else { - R.drawable.bookmark - } - ), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.accent), - modifier = Modifier - .clickable { - query { - Database.update( - albumWithSongs.album.copy( - bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { - System.currentTimeMillis() - } else { - null - } - ) - ) - } - } - .padding(all = 4.dp) - .size(18.dp) - ) - - Image( - painter = painterResource(R.drawable.share_social), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - albumWithSongs.album.shareUrl?.let { url -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - - context.startActivity( - Intent.createChooser( - sendIntent, - null - ) - ) - } - } - .padding(all = 4.dp) - .size(18.dp) - ) - } - - AsyncImage( - model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), - contentDescription = null, - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - .clip(thumbnailShape) - .size(thumbnailSizeDp) - ) - } - } - - itemsIndexed( - items = albumWithSongs.songs, - key = { _, song -> song.id } - ) { index, song -> - SongItem( - title = song.title, - authors = song.artistsText ?: albumWithSongs.album.authorsText, - durationText = song.durationText, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - albumWithSongs.songs.map(DetailedSong::asMediaItem), - index - ) - }, - startContent = { - BasicText( - text = "${index + 1}", - style = typography.s.semiBold.center.color(colorPalette.textDisabled), - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .width(Dimensions.thumbnails.song) - ) - }, - menuContent = { - NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) - } - ) - } - } - - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(all = 16.dp) - .padding(LocalPlayerAwarePaddingValues.current) - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - albumWithSongs.songs - .shuffled() - .map(DetailedSong::asMediaItem) - ) - } - .background(colorPalette.background2) - .size(62.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .align(Alignment.Center) - .size(20.dp) - ) - } - } ?: viewModel.result?.exceptionOrNull()?.let { - Box( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures { - viewModel.fetch(browseId) - } - } - .align(Alignment.Center) - .fillMaxSize() - ) { - BasicText( - text = "An error has occurred.\nTap to retry", - style = typography.s.medium.secondary.center, - modifier = Modifier - .align(Alignment.Center) - ) - } - } ?: Column( - modifier = Modifier - .padding(LocalPlayerAwarePaddingValues.current) - .shimmer() - ) { - HeaderPlaceholder() - - Spacer( - modifier = Modifier - .align(Alignment.CenterHorizontally) - .padding(all = 16.dp) - .clip(thumbnailShape) - .size(thumbnailSizeDp) - .background(colorPalette.shimmer) - ) - - repeat(3) { index -> - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - modifier = Modifier - .alpha(1f - index * 0.25f) - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) - .height(Dimensions.thumbnails.song) - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = thumbnailShape) - .size(Dimensions.thumbnails.song) - ) - - Column { - TextPlaceholder() - TextPlaceholder() - } - } - } - } - } +// BoxWithConstraints { +// val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth +// val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px +// +// viewModel.result?.getOrNull()?.let { albumWithSongs -> +// LazyColumn( +// contentPadding = LocalPlayerAwarePaddingValues.current, +// modifier = Modifier +// .background(colorPalette.background0) +// .fillMaxSize() +// ) { +// item( +// key = "header", +// contentType = 0 +// ) { +// Column { +// Header(title = albumWithSongs.album.title ?: "Unknown") { +// if (albumWithSongs.songs.isNotEmpty()) { +// BasicText( +// text = "Enqueue", +// style = typography.xxs.medium, +// modifier = Modifier +// .clip(RoundedCornerShape(16.dp)) +// .clickable { +// binder?.player?.enqueue( +// albumWithSongs.songs.map(DetailedSong::asMediaItem) +// ) +// } +// .background(colorPalette.background2) +// .padding(all = 8.dp) +// .padding(horizontal = 8.dp) +// ) +// } +// +// Spacer( +// modifier = Modifier +// .weight(1f) +// ) +// +// Image( +// painter = painterResource( +// if (albumWithSongs.album.bookmarkedAt == null) { +// R.drawable.bookmark_outline +// } else { +// R.drawable.bookmark +// } +// ), +// contentDescription = null, +// colorFilter = ColorFilter.tint(colorPalette.accent), +// modifier = Modifier +// .clickable { +// query { +// Database.update( +// albumWithSongs.album.copy( +// bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { +// System.currentTimeMillis() +// } else { +// null +// } +// ) +// ) +// } +// } +// .padding(all = 4.dp) +// .size(18.dp) +// ) +// +// Image( +// painter = painterResource(R.drawable.share_social), +// contentDescription = null, +// colorFilter = ColorFilter.tint(colorPalette.text), +// modifier = Modifier +// .clickable { +// albumWithSongs.album.shareUrl?.let { url -> +// val sendIntent = Intent().apply { +// action = Intent.ACTION_SEND +// type = "text/plain" +// putExtra(Intent.EXTRA_TEXT, url) +// } +// +// context.startActivity( +// Intent.createChooser( +// sendIntent, +// null +// ) +// ) +// } +// } +// .padding(all = 4.dp) +// .size(18.dp) +// ) +// } +// +// AsyncImage( +// model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), +// contentDescription = null, +// modifier = Modifier +// .align(Alignment.CenterHorizontally) +// .padding(all = 16.dp) +// .clip(thumbnailShape) +// .size(thumbnailSizeDp) +// ) +// } +// } +// +// itemsIndexed( +// items = albumWithSongs.songs, +// key = { _, song -> song.id } +// ) { index, song -> +// SongItem( +// title = song.title, +// authors = song.artistsText ?: albumWithSongs.album.authorsText, +// durationText = song.durationText, +// onClick = { +// binder?.stopRadio() +// binder?.player?.forcePlayAtIndex( +// albumWithSongs.songs.map(DetailedSong::asMediaItem), +// index +// ) +// }, +// startContent = { +// BasicText( +// text = "${index + 1}", +// style = typography.s.semiBold.center.color(colorPalette.textDisabled), +// maxLines = 1, +// overflow = TextOverflow.Ellipsis, +// modifier = Modifier +// .width(Dimensions.thumbnails.song) +// ) +// }, +// menuContent = { +// NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) +// } +// ) +// } +// } +// +// Box( +// modifier = Modifier +// .align(Alignment.BottomEnd) +// .padding(all = 16.dp) +// .padding(LocalPlayerAwarePaddingValues.current) +// .clip(RoundedCornerShape(16.dp)) +// .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { +// binder?.stopRadio() +// binder?.player?.forcePlayFromBeginning( +// albumWithSongs.songs +// .shuffled() +// .map(DetailedSong::asMediaItem) +// ) +// } +// .background(colorPalette.background2) +// .size(62.dp) +// ) { +// Image( +// painter = painterResource(R.drawable.shuffle), +// contentDescription = null, +// colorFilter = ColorFilter.tint(colorPalette.text), +// modifier = Modifier +// .align(Alignment.Center) +// .size(20.dp) +// ) +// } +// } ?: viewModel.result?.exceptionOrNull()?.let { +// Box( +// modifier = Modifier +// .pointerInput(Unit) { +// detectTapGestures { +// viewModel.fetch(browseId) +// } +// } +// .align(Alignment.Center) +// .fillMaxSize() +// ) { +// BasicText( +// text = "An error has occurred.\nTap to retry", +// style = typography.s.medium.secondary.center, +// modifier = Modifier +// .align(Alignment.Center) +// ) +// } +// } ?: Column( +// modifier = Modifier +// .padding(LocalPlayerAwarePaddingValues.current) +// .shimmer() +// ) { +// HeaderPlaceholder() +// +// Spacer( +// modifier = Modifier +// .align(Alignment.CenterHorizontally) +// .padding(all = 16.dp) +// .clip(thumbnailShape) +// .size(thumbnailSizeDp) +// .background(colorPalette.shimmer) +// ) +// +// repeat(3) { index -> +// Row( +// verticalAlignment = Alignment.CenterVertically, +// horizontalArrangement = Arrangement.spacedBy(12.dp), +// modifier = Modifier +// .alpha(1f - index * 0.25f) +// .fillMaxWidth() +// .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) +// .height(Dimensions.thumbnails.song) +// ) { +// Spacer( +// modifier = Modifier +// .background(color = colorPalette.shimmer, shape = thumbnailShape) +// .size(Dimensions.thumbnails.song) +// ) +// +// Column { +// TextPlaceholder() +// TextPlaceholder() +// } +// } +// } +// } +// } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt deleted file mode 100644 index ccf0efb..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverviewViewModel.kt +++ /dev/null @@ -1,66 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.artist - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.models.AlbumWithSongs -import it.vfsfitvnm.vimusic.models.SongAlbumMap -import it.vfsfitvnm.vimusic.utils.toMediaItem -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch - -class ArtistOverviewViewModel(browseId: String) : ViewModel() { - var result by mutableStateOf?>(null) - private set - - private var job: Job? = null - - init { - fetch(browseId) - } - - fun fetch(browseId: String) { - job?.cancel() - result = null - - job = viewModelScope.launch(Dispatchers.IO) { - Database.albumWithSongs(browseId).collect { albumWithSongs -> - result = if (albumWithSongs?.album?.timestamp == null) { - YouTube.album(browseId)?.map { youtubeAlbum -> - Database.upsert( - Album( - id = browseId, - title = youtubeAlbum.title, - thumbnailUrl = youtubeAlbum.thumbnail?.url, - year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, - shareUrl = youtubeAlbum.url, - timestamp = System.currentTimeMillis() - ), - youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> - albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> - Database.insert(mediaItem) - SongAlbumMap( - songId = mediaItem.mediaId, - albumId = browseId, - position = position - ) - } - } ?: emptyList() - ) - - null - } - } else { - Result.success(albumWithSongs) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index b47a102..e281687 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -80,7 +80,7 @@ import kotlinx.coroutines.runBlocking @OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @Composable -fun AlbumScreen(browseId: String) { +fun ArtistScreen(browseId: String) { val saveableStateHolder = rememberSaveableStateHolder() val (tabIndex, onTabIndexChanged) = rememberSaveable { mutableStateOf(0) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt index aa77279..b025763 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt @@ -26,6 +26,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -35,17 +36,22 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.AlbumSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Album +import it.vfsfitvnm.vimusic.savers.AlbumListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header 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.albumSortByKey +import it.vfsfitvnm.vimusic.utils.albumSortOrderKey +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @@ -54,16 +60,25 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @Composable fun HomeAlbumList( - onAlbumClick: (Album) -> Unit, - viewModel: HomeAlbumListViewModel = viewModel() + onAlbumClick: (Album) -> Unit ) { val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded) + var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.albums(sortBy, sortOrder) }, + stateSaver = AlbumListSaver, + key1 = sortBy, + key2 = sortOrder + ) + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizePx = thumbnailSizeDp.px val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -83,14 +98,14 @@ fun HomeAlbumList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: AlbumSortBy + targetSortBy: AlbumSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -98,17 +113,17 @@ fun HomeAlbumList( Item( iconId = R.drawable.calendar, - sortBy = AlbumSortBy.Year + targetSortBy = AlbumSortBy.Year ) Item( iconId = R.drawable.text, - sortBy = AlbumSortBy.Title + targetSortBy = AlbumSortBy.Title ) Item( iconId = R.drawable.time, - sortBy = AlbumSortBy.DateAdded + targetSortBy = AlbumSortBy.DateAdded ) Spacer( @@ -121,7 +136,7 @@ fun HomeAlbumList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -130,7 +145,7 @@ fun HomeAlbumList( } items( - items = viewModel.items, + items = items, key = Album::id ) { album -> Row( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt deleted file mode 100644 index 172bf3c..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumListViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.home - -import android.app.Application -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.content.edit -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.enums.AlbumSortBy -import it.vfsfitvnm.vimusic.enums.SortOrder -import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.utils.albumSortByKey -import it.vfsfitvnm.vimusic.utils.albumSortOrderKey -import it.vfsfitvnm.vimusic.utils.getEnum -import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf -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 HomeAlbumListViewModel(application: Application) : AndroidViewModel(application) { - var items by mutableStateOf(emptyList()) - private set - - var sortBy by mutableStatePreferenceOf( - preferences.getEnum( - albumSortByKey, - AlbumSortBy.DateAdded - ) - ) { - preferences.edit { putEnum(albumSortByKey, it) } - collectItems(sortBy = it) - } - - var sortOrder by mutableStatePreferenceOf( - preferences.getEnum( - albumSortOrderKey, - SortOrder.Ascending - ) - ) { - preferences.edit { putEnum(albumSortOrderKey, it) } - collectItems(sortOrder = it) - } - - private var job: Job? = null - - private val preferences: SharedPreferences - get() = getApplication().preferences - - init { - collectItems() - } - - private fun collectItems(sortBy: AlbumSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { - job?.cancel() - job = viewModelScope.launch { - Database.albums(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { - items = it - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt index 110231b..9730504 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt @@ -29,6 +29,7 @@ import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,18 +38,23 @@ 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.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ArtistSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Artist +import it.vfsfitvnm.vimusic.savers.ArtistListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header 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.artistSortByKey +import it.vfsfitvnm.vimusic.utils.artistSortOrderKey import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail @@ -56,16 +62,25 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @ExperimentalAnimationApi @Composable fun HomeArtistList( - onArtistClick: (Artist) -> Unit, - viewModel: HomeArtistListViewModel = viewModel() + onArtistClick: (Artist) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current + var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded) + var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.artists(sortBy, sortOrder) }, + stateSaver = ArtistListSaver, + key1 = sortBy, + key2 = sortOrder + ) + val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizePx = thumbnailSizeDp.px val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -92,14 +107,14 @@ fun HomeArtistList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: ArtistSortBy + targetSortBy: ArtistSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -107,12 +122,12 @@ fun HomeArtistList( Item( iconId = R.drawable.text, - sortBy = ArtistSortBy.Name + targetSortBy = ArtistSortBy.Name ) Item( iconId = R.drawable.time, - sortBy = ArtistSortBy.DateAdded + targetSortBy = ArtistSortBy.DateAdded ) Spacer( @@ -125,7 +140,7 @@ fun HomeArtistList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -134,7 +149,7 @@ fun HomeArtistList( } items( - items = viewModel.items, + items = items, key = Artist::id ) { artist -> Column( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt deleted file mode 100644 index e733957..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistListViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.home - -import android.app.Application -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.content.edit -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.enums.ArtistSortBy -import it.vfsfitvnm.vimusic.enums.SortOrder -import it.vfsfitvnm.vimusic.models.Artist -import it.vfsfitvnm.vimusic.utils.artistSortByKey -import it.vfsfitvnm.vimusic.utils.artistSortOrderKey -import it.vfsfitvnm.vimusic.utils.getEnum -import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf -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 HomeArtistListViewModel(application: Application) : AndroidViewModel(application) { - var items by mutableStateOf(emptyList()) - private set - - var sortBy by mutableStatePreferenceOf( - preferences.getEnum( - artistSortByKey, - ArtistSortBy.DateAdded - ) - ) { - preferences.edit { putEnum(artistSortByKey, it) } - collectItems(sortBy = it) - } - - var sortOrder by mutableStatePreferenceOf( - preferences.getEnum( - artistSortOrderKey, - SortOrder.Ascending - ) - ) { - preferences.edit { putEnum(artistSortOrderKey, it) } - collectItems(sortOrder = it) - } - - private var job: Job? = null - - private val preferences: SharedPreferences - get() = getApplication().preferences - - init { - collectItems() - } - - private fun collectItems(sortBy: ArtistSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) { - job?.cancel() - job = viewModelScope.launch { - Database.artists(sortBy, sortOrder).flowOn(Dispatchers.IO).collect { - items = it - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 3003383..936ed92 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -35,7 +35,6 @@ 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.lifecycle.viewmodel.compose.viewModel import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R @@ -44,6 +43,7 @@ import it.vfsfitvnm.vimusic.enums.PlaylistSortBy import it.vfsfitvnm.vimusic.enums.SortOrder import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -51,11 +51,14 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.playlistSortByKey +import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference @ExperimentalFoundationApi @Composable fun HomePlaylistList( - viewModel: HomePlaylistListViewModel = viewModel(), onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit, onPlaylistClicked: (Playlist) -> Unit, ) { @@ -79,8 +82,18 @@ fun HomePlaylistList( ) } + var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded) + var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.playlistPreviews(sortBy, sortOrder) }, + stateSaver = PlaylistPreviewListSaver, + key1 = sortBy, + key2 = sortOrder + ) + val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -105,14 +118,14 @@ fun HomePlaylistList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: PlaylistSortBy + targetSortBy: PlaylistSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -136,17 +149,17 @@ fun HomePlaylistList( Item( iconId = R.drawable.medical, - sortBy = PlaylistSortBy.SongCount + targetSortBy = PlaylistSortBy.SongCount ) Item( iconId = R.drawable.text, - sortBy = PlaylistSortBy.Name + targetSortBy = PlaylistSortBy.Name ) Item( iconId = R.drawable.time, - sortBy = PlaylistSortBy.DateAdded + targetSortBy = PlaylistSortBy.DateAdded ) Spacer( @@ -159,7 +172,7 @@ fun HomePlaylistList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -197,7 +210,7 @@ fun HomePlaylistList( } items( - items = viewModel.items, + items = items, key = { it.playlist.id } ) { playlistPreview -> PlaylistPreviewItem( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt deleted file mode 100644 index 257ef63..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistListViewModel.kt +++ /dev/null @@ -1,70 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.home - -import android.app.Application -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.content.edit -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.enums.PlaylistSortBy -import it.vfsfitvnm.vimusic.enums.SortOrder -import it.vfsfitvnm.vimusic.models.PlaylistPreview -import it.vfsfitvnm.vimusic.utils.getEnum -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 HomePlaylistListViewModel(application: Application) : AndroidViewModel(application) { - var items by mutableStateOf(emptyList()) - 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().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 - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index cb64513..f77f25a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Brush @@ -32,7 +33,7 @@ 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.lifecycle.viewmodel.compose.viewModel +import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R @@ -40,6 +41,7 @@ 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.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -50,21 +52,55 @@ 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.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.vimusic.utils.songSortByKey +import it.vfsfitvnm.vimusic.utils.songSortOrderKey @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun HomeSongList( - viewModel: HomeSongListViewModel = viewModel() -) { +fun HomeSongList() { + println("[${System.currentTimeMillis()}] HomeSongList") val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val thumbnailSize = Dimensions.thumbnails.song.px + var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded) + var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending) + + val items by produceSaveableListState( + flowProvider = { Database.songs(sortBy, sortOrder) }, + stateSaver = DetailedSongListSaver, + key1 = sortBy, + key2 = sortOrder + ) + +// var items by rememberSaveable(stateSaver = DetailedSongListSaver) { +// mutableStateOf(emptyList()) +// } +// +// var hasToRecollect by rememberSaveable(sortBy, sortOrder) { +// println("hasToRecollect: $sortBy, $sortOrder") +// mutableStateOf(true) +// } +// +// LaunchedEffect(sortBy, sortOrder) { +// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") +// Database.songs(sortBy, sortOrder) +// .flowOn(Dispatchers.IO) +// .drop(if (hasToRecollect) 0 else 1) +// .collect { +// hasToRecollect = false +// println("[${System.currentTimeMillis()}] collecting... ") +// items = it +// } +// } + val sortOrderIconRotation by animateFloatAsState( - targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f, + targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) @@ -74,6 +110,8 @@ fun HomeSongList( .background(colorPalette.background0) .fillMaxSize() ) { +// println("[${System.currentTimeMillis()}] LazyColumn") + item( key = "header", contentType = 0 @@ -82,14 +120,14 @@ fun HomeSongList( @Composable fun Item( @DrawableRes iconId: Int, - sortBy: SongSortBy + targetSortBy: SongSortBy ) { Image( painter = painterResource(iconId), contentDescription = null, - colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled), + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), modifier = Modifier - .clickable { viewModel.sortBy = sortBy } + .clickable { sortBy = targetSortBy } .padding(all = 4.dp) .size(18.dp) ) @@ -97,17 +135,17 @@ fun HomeSongList( Item( iconId = R.drawable.trending, - sortBy = SongSortBy.PlayTime + targetSortBy = SongSortBy.PlayTime ) Item( iconId = R.drawable.text, - sortBy = SongSortBy.Title + targetSortBy = SongSortBy.Title ) Item( iconId = R.drawable.time, - sortBy = SongSortBy.DateAdded + targetSortBy = SongSortBy.DateAdded ) Spacer( @@ -120,7 +158,7 @@ fun HomeSongList( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .clickable { viewModel.sortOrder = !viewModel.sortOrder } + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) .graphicsLayer { rotationZ = sortOrderIconRotation } @@ -129,25 +167,24 @@ fun HomeSongList( } itemsIndexed( - items = viewModel.items, + items = items, key = { _, song -> song.id } ) { index, song -> SongItem( song = song, thumbnailSize = thumbnailSize, onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - viewModel.items.map(DetailedSong::asMediaItem), - index - ) + items.map(DetailedSong::asMediaItem)?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(mediaItems, index) + } }, menuContent = { InHistoryMediaItemMenu(song = song) }, onThumbnailContent = { AnimatedVisibility( - visible = viewModel.sortBy == SongSortBy.PlayTime, + visible = sortBy == SongSortBy.PlayTime, enter = fadeIn(), exit = fadeOut(), modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt deleted file mode 100644 index 596038f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongListViewModel.kt +++ /dev/null @@ -1,67 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.home - -import android.app.Application -import android.content.SharedPreferences -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.core.content.edit -import androidx.lifecycle.AndroidViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.enums.SongSortBy -import it.vfsfitvnm.vimusic.enums.SortOrder -import it.vfsfitvnm.vimusic.models.DetailedSong -import it.vfsfitvnm.vimusic.utils.getEnum -import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf -import it.vfsfitvnm.vimusic.utils.preferences -import it.vfsfitvnm.vimusic.utils.putEnum -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 HomeSongListViewModel(application: Application) : AndroidViewModel(application) { - var items by mutableStateOf(emptyList()) - 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().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 - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index ec87fdb..243d5b2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -17,6 +17,7 @@ import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -25,12 +26,11 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -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.models.DetailedSong +import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -41,6 +41,7 @@ import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableListState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @@ -49,19 +50,19 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint @Composable fun LocalSongSearch( textFieldValue: TextFieldValue, - onTextFieldValueChanged: (TextFieldValue) -> Unit, - viewModel: LocalSongSearchViewModel = viewModel( - key = textFieldValue.text, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return LocalSongSearchViewModel(textFieldValue.text) as T - } - } - ) + onTextFieldValueChanged: (TextFieldValue) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current + + val items by produceSaveableListState( + flowProvider = { + Database.search("%${textFieldValue.text}%") + }, + stateSaver = DetailedSongListSaver, + key1 = textFieldValue.text + ) + val thumbnailSize = Dimensions.thumbnails.song.px LazyColumn( @@ -122,7 +123,7 @@ fun LocalSongSearch( } items( - items = viewModel.items, + items = items, key = DetailedSong::id, ) { song -> SongItem( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt deleted file mode 100644 index 7735846..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearchViewModel.kt +++ /dev/null @@ -1,25 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.search - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.models.DetailedSong -import kotlinx.coroutines.launch - -class LocalSongSearchViewModel(text: String) : ViewModel() { - var items by mutableStateOf(emptyList()) - private set - - init { - if (text.isNotEmpty()) { - viewModelScope.launch { - Database.search("%$text%").collect { - items = it - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 63db5f4..b690e50 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -23,6 +23,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -39,21 +40,24 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -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.models.SearchQuery import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver +import it.vfsfitvnm.vimusic.savers.StringListResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.distinctUntilChanged @Composable fun OnlineSearch( @@ -61,19 +65,30 @@ fun OnlineSearch( onTextFieldValueChanged: (TextFieldValue) -> Unit, isOpenableUrl: Boolean, onSearch: (String) -> Unit, - onUri: () -> Unit, - viewModel: OnlineSearchViewModel = viewModel( - key = textFieldValue.text, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return OnlineSearchViewModel(textFieldValue.text) as T - } - } - ) + onUri: () -> Unit ) { val (colorPalette, typography) = LocalAppearance.current + val history by produceSaveableListState( + flowProvider = { + Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new -> + old.size == new.size + } + }, + stateSaver = SearchQueryListSaver, + key1 = textFieldValue.text + ) + + val suggestionsResult by produceSaveableState( + initialValue = null, + stateSaver = StringListResultSaver, + key1 = textFieldValue.text + ) { + if (textFieldValue.text.isNotEmpty()) { + value = YouTube.getSearchSuggestions(textFieldValue.text) + } + } + val timeIconPainter = painterResource(R.drawable.time) val closeIconPainter = painterResource(R.drawable.close) val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) @@ -173,7 +188,7 @@ fun OnlineSearch( } items( - items = viewModel.history, + items = history, key = SearchQuery::id ) { searchQuery -> Row( @@ -241,7 +256,7 @@ fun OnlineSearch( } } - viewModel.suggestionsResult?.getOrNull()?.let { suggestions -> + suggestionsResult?.getOrNull()?.let { suggestions -> items(items = suggestions) { suggestion -> Row( verticalAlignment = Alignment.CenterVertically, @@ -288,7 +303,7 @@ fun OnlineSearch( ) } } - } ?: viewModel.suggestionsResult?.exceptionOrNull()?.let { throwable -> + } ?: suggestionsResult?.exceptionOrNull()?.let { throwable -> item { LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {} } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt deleted file mode 100644 index d96eb7f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearchViewModel.kt +++ /dev/null @@ -1,36 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.search - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.models.SearchQuery -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.launch - -class OnlineSearchViewModel(text: String) : ViewModel() { - var history by mutableStateOf(emptyList()) - private set - - var suggestionsResult by mutableStateOf?>?>(null) - private set - - init { - viewModelScope.launch { - Database.queries("%$text%").distinctUntilChanged { old, new -> - old.size == new.size - }.collect { - history = it - } - } - - if (text.isNotEmpty()) { - viewModelScope.launch { - suggestionsResult = YouTube.getSearchSuggestions(text) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index d718e25..0750a88 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -9,36 +9,59 @@ import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.compose.viewModel import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.savers.ListSaver +import it.vfsfitvnm.vimusic.savers.StringResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError +import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableState import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext @ExperimentalAnimationApi @Composable -inline fun ItemSearchResult( +inline fun SearchResult( query: String, filter: String, + stateSaver: ListSaver>, crossinline onSearchAgain: () -> Unit, - viewModel: SearchResultViewModel = viewModel( - key = query + filter, - factory = object : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - @Suppress("UNCHECKED_CAST") - return SearchResultViewModel(query, filter) as T - } - } - ), - crossinline itemContent: @Composable LazyItemScope.(I) -> Unit, + crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, noinline itemShimmer: @Composable BoxScope.() -> Unit, ) { + var items by rememberSaveable(query, filter, stateSaver = stateSaver) { + mutableStateOf(listOf()) + } + + val (continuationResultState, fetch) = produceSaveableRelaunchableState( + initialValue = null, + stateSaver = StringResultSaver, + key1 = query, + key2 = filter + ) { + val token = value?.getOrNull() + + value = null + + value = withContext(Dispatchers.IO) { + YouTube.search(query, filter, token) + }?.map { searchResult -> + @Suppress("UNCHECKED_CAST") + items = items.plus(searchResult.items as List).distinctBy(YouTube.Item::key) + searchResult.continuation + } + } + + val continuationResult by continuationResultState + LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, modifier = Modifier @@ -60,27 +83,27 @@ inline fun ItemSearchResult( } items( - items = viewModel.items, + items = items, key = { it.key!! }, itemContent = itemContent ) - viewModel.continuationResult?.getOrNull()?.let { - if (viewModel.items.isNotEmpty()) { + continuationResult?.getOrNull()?.let { + if (items.isNotEmpty()) { item { - SideEffect(viewModel::fetch) + SideEffect(fetch) } } - } ?: viewModel.continuationResult?.exceptionOrNull()?.let { throwable -> + } ?: continuationResult?.exceptionOrNull()?.let { throwable -> item { SearchResultLoadingOrError( errorMessage = throwable.javaClass.canonicalName, - onRetry = viewModel::fetch, + onRetry = fetch, shimmerContent = {} ) } - } ?: viewModel.continuationResult?.let { - if (viewModel.items.isEmpty()) { + } ?: continuationResult?.let { + if (items.isEmpty()) { item { TextCard(icon = R.drawable.sad) { Title(text = "No results found") @@ -90,7 +113,7 @@ inline fun ItemSearchResult( } } ?: item(key = "loading") { SearchResultLoadingOrError( - itemCount = if (viewModel.items.isEmpty()) 8 else 3, + itemCount = if (items.isEmpty()) 8 else 3, shimmerContent = itemShimmer ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index 3252377..ec8f8ab 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -13,6 +13,11 @@ import androidx.compose.ui.unit.dp import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.savers.YouTubeAlbumListSaver +import it.vfsfitvnm.vimusic.savers.YouTubeArtistListSaver +import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver +import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver +import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute @@ -85,10 +90,11 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, onSearchAgain = onSearchAgain, + stateSaver = YouTubeSongListSaver, itemContent = { song -> SmallSongItem( song = song, @@ -110,9 +116,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubeAlbumListSaver, onSearchAgain = onSearchAgain, itemContent = { album -> AlbumItem( @@ -138,9 +145,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 64.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubeArtistListSaver, onSearchAgain = onSearchAgain, itemContent = { artist -> ArtistItem( @@ -165,9 +173,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailHeightDp = 72.dp val thumbnailWidthDp = 128.dp - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubeVideoListSaver, onSearchAgain = onSearchAgain, itemContent = { video -> VideoItem( @@ -194,9 +203,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - ItemSearchResult( + SearchResult( query = query, filter = searchFilter, + stateSaver = YouTubePlaylistListSaver, onSearchAgain = onSearchAgain, itemContent = { playlist -> PlaylistItem( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt deleted file mode 100644 index bfbfaab..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultViewModel.kt +++ /dev/null @@ -1,45 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens.searchresult - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue -import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext - -class SearchResultViewModel( - private val query: String, - private val filter: String -) : ViewModel() { - var items by mutableStateOf(listOf()) - - var continuationResult by mutableStateOf?>(null) - - private var job: Job? = null - - init { - fetch() - } - - fun fetch() { - job?.cancel() - - viewModelScope.launch { - val token = continuationResult?.getOrNull() - - continuationResult = null - - continuationResult = withContext(Dispatchers.IO) { - YouTube.search(query, filter, token) - }?.map { searchResult -> - @Suppress("UNCHECKED_CAST") - items = items.plus(searchResult.items as List).distinctBy(YouTube.Item::key) - searchResult.continuation - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index 467eb79..f6f2126 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -83,7 +83,7 @@ fun SmallSongItem( SongItem( thumbnailModel = song.thumbnail?.size(thumbnailSizePx), title = song.info.name, - authors = song.authors.joinToString("") { it.name }, + authors = song.authors?.joinToString("") { it.name } ?: "", durationText = song.durationText, onClick = onClick, menuContent = { @@ -158,13 +158,13 @@ fun VideoItem( ) BasicText( - text = video.authors.joinToString("") { it.name }, + text = video.authors?.joinToString("") { it.name } ?: "", style = typography.xs.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, ) - video.views.firstOrNull()?.name?.let { viewsText -> + video.viewsText?.let { viewsText -> BasicText( text = viewsText, style = typography.xxs.medium.secondary, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt new file mode 100644 index 0000000..f765c76 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt @@ -0,0 +1,102 @@ +package it.vfsfitvnm.vimusic.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.setValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import it.vfsfitvnm.vimusic.savers.ListSaver +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.flow.flowOn + + +@Composable +fun produceSaveableListState( + flowProvider: () -> Flow>, + stateSaver: ListSaver>, +): State> { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(emptyList()) + } + + var hasToRecollect by rememberSaveable { + mutableStateOf(true) + } + + LaunchedEffect(Unit) { + flowProvider() + .flowOn(Dispatchers.IO) + .drop(if (hasToRecollect) 0 else 1) + .collect { + hasToRecollect = false + state.value = it + } + } + + return state +} + +@Composable +fun produceSaveableListState( + flowProvider: () -> Flow>, + stateSaver: ListSaver>, + key1: Any?, +): State> { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(emptyList()) + } + + var hasToRecollect by rememberSaveable(key1) { +// println("hasToRecollect: $sortBy, $sortOrder") + mutableStateOf(true) + } + + LaunchedEffect(key1) { +// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") + flowProvider() + .flowOn(Dispatchers.IO) + .drop(if (hasToRecollect) 0 else 1) + .collect { + hasToRecollect = false +// println("[${System.currentTimeMillis()}] collecting... ") + state.value = it + } + } + + return state +} + +@Composable +fun produceSaveableListState( + flowProvider: () -> Flow>, + stateSaver: ListSaver>, + key1: Any?, + key2: Any?, +): State> { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(emptyList()) + } + +// var hasToRecollect by rememberSaveable(key1, key2) { +//// println("hasToRecollect: $sortBy, $sortOrder") +// mutableStateOf(true) +// } + + LaunchedEffect(key1, key2) { +// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") + flowProvider() + .flowOn(Dispatchers.IO) +// .drop(if (hasToRecollect) 0 else 1) + .collect { +// hasToRecollect = false +// println("[${System.currentTimeMillis()}] collecting... ") + state.value = it + } + } + + return state +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt new file mode 100644 index 0000000..720ddac --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt @@ -0,0 +1,118 @@ +package it.vfsfitvnm.vimusic.utils + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.ProduceStateScope +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import kotlin.coroutines.CoroutineContext +import kotlin.experimental.ExperimentalTypeInference +import kotlinx.coroutines.suspendCancellableCoroutine + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableState( + initialValue: T, + stateSaver: Saver, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable { mutableStateOf(true) } + + LaunchedEffect(Unit) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + return result +} + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable(key1) { mutableStateOf(true) } + + LaunchedEffect(key1) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + return result +} + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + key2: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } + + LaunchedEffect(Unit) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + + return result +} + +@OptIn(ExperimentalTypeInference::class) +@Composable +fun produceSaveableRelaunchableState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + key2: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): Pair, () -> Unit> { + val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + + var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } + + val relaunchableEffect = relaunchableEffect(key1, key2) { + if (hasToFetch) { + ProduceSaveableStateScope(result, coroutineContext).producer() + hasToFetch = false + } + } + + return result to { + hasToFetch = true + relaunchableEffect() + } +} + +private class ProduceSaveableStateScope( + state: MutableState, + override val coroutineContext: CoroutineContext +) : ProduceStateScope, MutableState by state { + override suspend fun awaitDispose(onDispose: () -> Unit): Nothing { + try { + suspendCancellableCoroutine { } + } finally { + onDispose() + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 269668c..82ea165 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -28,7 +28,7 @@ val YouTube.Item.Song.asMediaItem: MediaItem .setMediaMetadata( MediaMetadata.Builder() .setTitle(info.name) - .setArtist(authors.joinToString("") { it.name }) + .setArtist(authors?.joinToString("") { it.name }) .setAlbumTitle(album?.name) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( @@ -36,8 +36,8 @@ val YouTube.Item.Song.asMediaItem: MediaItem "videoId" to info.endpoint!!.videoId, "albumId" to album?.endpoint?.browseId, "durationText" to durationText, - "artistNames" to authors.filter { it.endpoint != null }.map { it.name }, - "artistIds" to authors.mapNotNull { it.endpoint?.browseId }, + "artistNames" to authors?.filter { it.endpoint != null }?.map { it.name }, + "artistIds" to authors?.mapNotNull { it.endpoint?.browseId }, ) ) .build() @@ -52,14 +52,14 @@ val YouTube.Item.Video.asMediaItem: MediaItem .setMediaMetadata( MediaMetadata.Builder() .setTitle(info.name) - .setArtist(authors.joinToString("") { it.name }) + .setArtist(authors?.joinToString("") { it.name }) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( bundleOf( "videoId" to info.endpoint!!.videoId, "durationText" to durationText, - "artistNames" to if (isOfficialMusicVideo) authors.filter { it.endpoint != null }.map { it.name } else null, - "artistIds" to if (isOfficialMusicVideo) authors.mapNotNull { it.endpoint?.browseId } else null, + "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.map { it.name } else null, + "artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null, ) ) .build() diff --git a/settings.gradle.kts b/settings.gradle.kts index ececf2e..176589f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,7 +26,6 @@ dependencyResolutionManagement { library("compose-shimmer", "com.valentinilk.shimmer", "compose-shimmer").version("1.0.3") - library("compose-viewmodel", "androidx.lifecycle", "lifecycle-viewmodel-compose").version("2.6.0-alpha02") library("compose-activity", "androidx.activity", "activity-compose").version("1.5.1") library("compose-coil", "io.coil-kt", "coil-compose").version("2.2.1") diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index 7eec479..40ec4bf 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -181,7 +181,7 @@ object YouTube { data class Song( val info: Info, - val authors: List>, + val authors: List>?, val album: Info?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? @@ -231,8 +231,8 @@ object YouTube { data class Video( val info: Info, - val authors: List>, - val views: List>, + val authors: List>?, + val viewsText: String?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { @@ -263,14 +263,14 @@ object YouTube { info = Info.from(mainRuns.first()), authors = otherRuns .getOrNull(otherRuns.lastIndex - 2) - ?.map(Info.Companion::from) - ?: emptyList(), - views = otherRuns + ?.map(Info.Companion::from), + viewsText = otherRuns .getOrNull(otherRuns.lastIndex - 1) - ?.map(Info.Companion::from) ?: emptyList(), + ?.firstOrNull() + ?.text, durationText = otherRuns .getOrNull(otherRuns.lastIndex) - ?.first() + ?.firstOrNull() ?.text, thumbnail = content .thumbnail From 82c2a952aa0080c5e5d2de39283b260d38f61e2b Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Mon, 26 Sep 2022 21:36:05 +0200 Subject: [PATCH 015/100] Code tweaks --- .../it/vfsfitvnm/vimusic/models/Album.kt | 4 +- .../vimusic/ui/screens/album/AlbumOverview.kt | 66 ++++++-- .../vimusic/ui/screens/album/AlbumScreen.kt | 142 +++++++++++------- .../vimusic/ui/screens/artist/ArtistScreen.kt | 3 + .../vimusic/ui/screens/home/HomeAlbumList.kt | 18 ++- .../vimusic/ui/screens/home/HomeArtistList.kt | 18 ++- .../ui/screens/home/HomePlaylistList.kt | 18 ++- .../vimusic/ui/screens/home/HomeSongList.kt | 52 ++----- .../ui/screens/search/LocalSongSearch.kt | 21 ++- .../vimusic/ui/screens/search/OnlineSearch.kt | 21 +-- .../ui/screens/searchresult/SearchResult.kt | 7 +- .../searchresult/SearchResultScreen.kt | 2 +- .../vimusic/utils/ProduceSaveableListState.kt | 102 ------------- .../vimusic/utils/ProduceSaveableState.kt | 86 +++++++---- .../vimusic/utils/RememberLazyListStates.kt | 61 -------- 15 files changed, 284 insertions(+), 337 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt index 75cc575..57f6827 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Album.kt @@ -8,11 +8,11 @@ import androidx.room.PrimaryKey @Entity data class Album( @PrimaryKey val id: String, - val title: String?, + val title: String? = null, val thumbnailUrl: String? = null, val year: String? = null, val authorsText: String? = null, val shareUrl: String? = null, - val timestamp: Long?, + val timestamp: Long? = null, val bookmarkedAt: Long? = null ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index fdffff9..65520d5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -43,7 +43,9 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.models.SongAlbumMap import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.AlbumResultSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder @@ -61,29 +63,74 @@ import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.medium -import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext @ExperimentalAnimationApi @ExperimentalFoundationApi @Composable fun AlbumOverview( - albumResult: Result?, browseId: String, ) { val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val context = LocalContext.current - - val songs by produceSaveableListState( - flowProvider = { - Database.albumSongs(browseId) - }, + + val albumResult by produceSaveableState( + initialValue = null, + stateSaver = AlbumResultSaver, + ) { + withContext(Dispatchers.IO) { + Database.album(browseId).collect { album -> + if (album?.timestamp == null) { + YouTube.album(browseId)?.map { youtubeAlbum -> + Database.upsert( + Album( + id = browseId, + title = youtubeAlbum.title, + thumbnailUrl = youtubeAlbum.thumbnail?.url, + year = youtubeAlbum.year, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + shareUrl = youtubeAlbum.url, + timestamp = System.currentTimeMillis() + ), + youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> + albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> + Database.insert(mediaItem) + SongAlbumMap( + songId = mediaItem.mediaId, + albumId = browseId, + position = position + ) + } + } ?: emptyList() + ) + + null + } + } else { + value = Result.success(album) + } + } + } + } + + val songs by produceSaveableState( + initialValue = emptyList(), stateSaver = DetailedSongListSaver - - ) + ) { + Database + .albumSongs(browseId) + .flowOn(Dispatchers.IO) + .collect { value = it } + } BoxWithConstraints { val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth @@ -270,6 +317,7 @@ fun AlbumOverview( modifier = Modifier .padding(LocalPlayerAwarePaddingValues.current) .shimmer() + .fillMaxSize() ) { HeaderPlaceholder() diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index 530a211..dad80ec 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -3,21 +3,95 @@ package it.vfsfitvnm.vimusic.ui.screens.album import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.runtime.saveable.rememberSaveableStateHolder import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.models.Album -import it.vfsfitvnm.vimusic.models.SongAlbumMap -import it.vfsfitvnm.vimusic.savers.AlbumResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.utils.produceSaveableState -import it.vfsfitvnm.vimusic.utils.toMediaItem -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext + +//@Stable +//class AlbumScreenState( +// initialIsLoading: Boolean = false, +// initialError: Throwable? = null, +// initialAlbum: Album? = null, +// initialYouTubeAlbum: YouTube.PlaylistOrAlbum? = null, +//) { +// var isLoading by mutableStateOf(initialIsLoading) +// var error by mutableStateOf(initialError) +// var album by mutableStateOf(initialAlbum) +// var youtubeAlbum by mutableStateOf(initialYouTubeAlbum) +// +// suspend fun loadAlbum(browseId: String) { +// println("loadAlbum $browseId") +// Database.album(browseId).flowOn(Dispatchers.IO).collect { +// if (it == null) { +// loadYouTubeAlbum(browseId) +// } else { +// album = it +// } +// } +// } +// +// suspend fun loadYouTubeAlbum(browseId: String) { +// println("loadYouTubeAlbum $browseId") +// if (youtubeAlbum == null) { +// isLoading = true +// withContext(Dispatchers.IO) { +// YouTube.album(browseId) +// }?.onSuccess { +// youtubeAlbum = it +// isLoading = false +// +// query { +// Database.upsert( +// Album( +// id = browseId, +// title = it.title, +// thumbnailUrl = it.thumbnail?.url, +// year = it.year, +// authorsText = it.authors?.joinToString( +// "", +// transform = YouTube.Info::name +// ), +// shareUrl = it.url, +// timestamp = System.currentTimeMillis() +// ), +// it.items?.mapIndexedNotNull { position, albumItem -> +// albumItem.toMediaItem(browseId, it)?.let { mediaItem -> +// Database.insert(mediaItem) +// SongAlbumMap( +// songId = mediaItem.mediaId, +// albumId = browseId, +// position = position +// ) +// } +// } ?: emptyList() +// ) +// } +// +// }?.onFailure { +// error = it +// isLoading = false +// } +// } +// } +//} +// +//object AlbumScreenStateSaver : Saver> { +// override fun restore(value: List) = AlbumScreenState( +// initialIsLoading = value[0] as Boolean, +// initialError = value[1] as Throwable?, +// initialAlbum = (value[1] as List?)?.let(AlbumSaver::restore), +// ) +// +// override fun SaverScope.save(value: AlbumScreenState): List = +// listOf( +// value.isLoading, +// value.error, +// value.album?.let { with(AlbumSaver) { save(it) } }, +//// value.youtubeAlbum?.let { with(YouTubeAlbumSaver) { save(it) } }, +// ) +//} @OptIn(ExperimentalFoundationApi::class) @ExperimentalAnimationApi @@ -29,59 +103,17 @@ fun AlbumScreen(browseId: String) { globalRoutes() host { - val albumResult by produceSaveableState( - initialValue = null, - stateSaver = AlbumResultSaver, - ) { - withContext(Dispatchers.IO) { - Database.album(browseId).collect { album -> - if (album?.timestamp == null) { - YouTube.album(browseId)?.map { youtubeAlbum -> - Database.upsert( - Album( - id = browseId, - title = youtubeAlbum.title, - thumbnailUrl = youtubeAlbum.thumbnail?.url, - year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, - shareUrl = youtubeAlbum.url, - timestamp = System.currentTimeMillis() - ), - youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> - albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> - Database.insert(mediaItem) - SongAlbumMap( - songId = mediaItem.mediaId, - albumId = browseId, - position = position - ) - } - } ?: emptyList() - ) - - null - } - } else { - value = Result.success(album) - } - } - } - } - Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, tabIndex = 0, - onTabChanged = {}, + onTabChanged = { }, tabColumnContent = { Item -> Item(0, "Overview", R.drawable.sparkles) } - ) { currentTabIndex -> + ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - AlbumOverview( - albumResult = albumResult, - browseId = browseId, - ) + AlbumOverview(browseId = browseId) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index e281687..ca61383 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -97,6 +97,9 @@ fun ArtistScreen(browseId: String) { onTabChanged = onTabIndexChanged, tabColumnContent = { Item -> Item(0, "Overview", R.drawable.sparkles) + Item(1, "Songs", R.drawable.musical_notes) + Item(2, "Albums", R.drawable.disc) + Item(3, "Singles", R.drawable.disc) } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt index b025763..8903be3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbumList.kt @@ -50,11 +50,13 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.utils.albumSortByKey import it.vfsfitvnm.vimusic.utils.albumSortOrderKey -import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn @ExperimentalFoundationApi @ExperimentalAnimationApi @@ -67,12 +69,16 @@ fun HomeAlbumList( var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded) var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending) - val items by produceSaveableListState( - flowProvider = { Database.albums(sortBy, sortOrder) }, + val items by produceSaveableState( + initialValue = emptyList(), stateSaver = AlbumListSaver, - key1 = sortBy, - key2 = sortOrder - ) + sortBy, sortOrder, + ) { + Database + .albums(sortBy, sortOrder) + .flowOn(Dispatchers.IO) + .collect { value = it } + } val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizePx = thumbnailSizeDp.px diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt index 9730504..0668205 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtistList.kt @@ -53,10 +53,12 @@ import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.utils.artistSortByKey import it.vfsfitvnm.vimusic.utils.artistSortOrderKey import it.vfsfitvnm.vimusic.utils.center -import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn @ExperimentalFoundationApi @ExperimentalAnimationApi @@ -69,12 +71,16 @@ fun HomeArtistList( var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded) var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending) - val items by produceSaveableListState( - flowProvider = { Database.artists(sortBy, sortOrder) }, + val items by produceSaveableState( + initialValue = emptyList(), stateSaver = ArtistListSaver, - key1 = sortBy, - key2 = sortOrder - ) + sortBy, sortOrder, + ) { + Database + .artists(sortBy, sortOrder) + .flowOn(Dispatchers.IO) + .collect { value = it } + } val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizePx = thumbnailSizeDp.px diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 936ed92..7d49b05 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -53,8 +53,10 @@ import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.playlistSortByKey import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey -import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberPreference +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn @ExperimentalFoundationApi @Composable @@ -85,12 +87,16 @@ fun HomePlaylistList( var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded) var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending) - val items by produceSaveableListState( - flowProvider = { Database.playlistPreviews(sortBy, sortOrder) }, + val items by produceSaveableState( + initialValue = emptyList(), stateSaver = PlaylistPreviewListSaver, - key1 = sortBy, - key2 = sortOrder - ) + sortBy, sortOrder, + ) { + Database + .playlistPreviews(sortBy, sortOrder) + .flowOn(Dispatchers.IO) + .collect { value = it } + } val sortOrderIconRotation by animateFloatAsState( targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index f77f25a..2e46bd5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -52,17 +52,18 @@ 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.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState 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 +import kotlinx.coroutines.flow.flowOn @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable fun HomeSongList() { - println("[${System.currentTimeMillis()}] HomeSongList") val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current @@ -71,33 +72,16 @@ fun HomeSongList() { var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded) var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending) - val items by produceSaveableListState( - flowProvider = { Database.songs(sortBy, sortOrder) }, + val items by produceSaveableState( + initialValue = emptyList(), stateSaver = DetailedSongListSaver, - key1 = sortBy, - key2 = sortOrder - ) - -// var items by rememberSaveable(stateSaver = DetailedSongListSaver) { -// mutableStateOf(emptyList()) -// } -// -// var hasToRecollect by rememberSaveable(sortBy, sortOrder) { -// println("hasToRecollect: $sortBy, $sortOrder") -// mutableStateOf(true) -// } -// -// LaunchedEffect(sortBy, sortOrder) { -// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") -// Database.songs(sortBy, sortOrder) -// .flowOn(Dispatchers.IO) -// .drop(if (hasToRecollect) 0 else 1) -// .collect { -// hasToRecollect = false -// println("[${System.currentTimeMillis()}] collecting... ") -// items = it -// } -// } + sortBy, sortOrder, + ) { + Database + .songs(sortBy, sortOrder) + .flowOn(Dispatchers.IO) + .collect { value = it } + } val sortOrderIconRotation by animateFloatAsState( targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, @@ -110,8 +94,6 @@ fun HomeSongList() { .background(colorPalette.background0) .fillMaxSize() ) { -// println("[${System.currentTimeMillis()}] LazyColumn") - item( key = "header", contentType = 0 @@ -174,10 +156,8 @@ fun HomeSongList() { song = song, thumbnailSize = thumbnailSize, onClick = { - items.map(DetailedSong::asMediaItem)?.let { mediaItems -> - binder?.stopRadio() - binder?.player?.forcePlayAtIndex(mediaItems, index) - } + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index) }, menuContent = { InHistoryMediaItemMenu(song = song) @@ -192,9 +172,7 @@ fun HomeSongList() { ) { BasicText( text = song.formattedTotalPlayTime, - style = typography.xxs.semiBold.center.color( - Color.White - ), + style = typography.xxs.semiBold.center.color(Color.White), maxLines = 2, overflow = TextOverflow.Ellipsis, modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 243d5b2..200c7ec 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -41,9 +41,15 @@ import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.medium -import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn + +//context(ProduceStateScope) +//fun Flow.distinctUntilChangedWithProducedState() = +// distinctUntilChanged { old, new -> new != old && new != value } @ExperimentalFoundationApi @ExperimentalAnimationApi @@ -55,13 +61,16 @@ fun LocalSongSearch( val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current - val items by produceSaveableListState( - flowProvider = { - Database.search("%${textFieldValue.text}%") - }, + val items by produceSaveableState( + initialValue = emptyList(), stateSaver = DetailedSongListSaver, key1 = textFieldValue.text - ) + ) { + Database + .search("%${textFieldValue.text}%") + .flowOn(Dispatchers.IO) + .collect { value = it } + } val thumbnailSize = Dimensions.thumbnails.song.px diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index b690e50..35b091a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -52,12 +52,14 @@ import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.medium -import it.vfsfitvnm.vimusic.utils.produceSaveableListState +import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn @Composable fun OnlineSearch( @@ -69,17 +71,18 @@ fun OnlineSearch( ) { val (colorPalette, typography) = LocalAppearance.current - val history by produceSaveableListState( - flowProvider = { - Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new -> - old.size == new.size - } - }, + val history by produceSaveableState( + initialValue = emptyList(), stateSaver = SearchQueryListSaver, key1 = textFieldValue.text - ) + ) { + Database.queries("%${textFieldValue.text}%") + .flowOn(Dispatchers.IO) + .distinctUntilChanged { old, new -> old.size == new.size } + .collect { value = it } + } - val suggestionsResult by produceSaveableState( + val suggestionsResult by produceSaveableOneShotState( initialValue = null, stateSaver = StringListResultSaver, key1 = textFieldValue.text diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index 0750a88..9c039c7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -22,7 +22,7 @@ import it.vfsfitvnm.vimusic.savers.StringResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError -import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableState +import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -41,11 +41,10 @@ inline fun SearchResult( mutableStateOf(listOf()) } - val (continuationResultState, fetch) = produceSaveableRelaunchableState( + val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState( initialValue = null, stateSaver = StringResultSaver, - key1 = query, - key2 = filter + query, filter ) { val token = value?.getOrNull() diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index ec8f8ab..4b3ce98 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -90,7 +90,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizePx = thumbnailSizeDp.px - SearchResult( + SearchResult( query = query, filter = searchFilter, onSearchAgain = onSearchAgain, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt deleted file mode 100644 index f765c76..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableListState.kt +++ /dev/null @@ -1,102 +0,0 @@ -package it.vfsfitvnm.vimusic.utils - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.State -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import it.vfsfitvnm.vimusic.savers.ListSaver -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.drop -import kotlinx.coroutines.flow.flowOn - - -@Composable -fun produceSaveableListState( - flowProvider: () -> Flow>, - stateSaver: ListSaver>, -): State> { - val state = rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(emptyList()) - } - - var hasToRecollect by rememberSaveable { - mutableStateOf(true) - } - - LaunchedEffect(Unit) { - flowProvider() - .flowOn(Dispatchers.IO) - .drop(if (hasToRecollect) 0 else 1) - .collect { - hasToRecollect = false - state.value = it - } - } - - return state -} - -@Composable -fun produceSaveableListState( - flowProvider: () -> Flow>, - stateSaver: ListSaver>, - key1: Any?, -): State> { - val state = rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(emptyList()) - } - - var hasToRecollect by rememberSaveable(key1) { -// println("hasToRecollect: $sortBy, $sortOrder") - mutableStateOf(true) - } - - LaunchedEffect(key1) { -// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") - flowProvider() - .flowOn(Dispatchers.IO) - .drop(if (hasToRecollect) 0 else 1) - .collect { - hasToRecollect = false -// println("[${System.currentTimeMillis()}] collecting... ") - state.value = it - } - } - - return state -} - -@Composable -fun produceSaveableListState( - flowProvider: () -> Flow>, - stateSaver: ListSaver>, - key1: Any?, - key2: Any?, -): State> { - val state = rememberSaveable(stateSaver = stateSaver) { - mutableStateOf(emptyList()) - } - -// var hasToRecollect by rememberSaveable(key1, key2) { -//// println("hasToRecollect: $sortBy, $sortOrder") -// mutableStateOf(true) -// } - - LaunchedEffect(key1, key2) { -// println("[${System.currentTimeMillis()}] LaunchedEffect, $hasToRecollect, $sortBy, $sortOrder") - flowProvider() - .flowOn(Dispatchers.IO) -// .drop(if (hasToRecollect) 0 else 1) - .collect { -// hasToRecollect = false -// println("[${System.currentTimeMillis()}] collecting... ") - state.value = it - } - } - - return state -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt index 720ddac..0b73804 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalTypeInference::class) + package it.vfsfitvnm.vimusic.utils import androidx.compose.runtime.Composable @@ -14,27 +16,23 @@ import kotlin.coroutines.CoroutineContext import kotlin.experimental.ExperimentalTypeInference import kotlinx.coroutines.suspendCancellableCoroutine -@OptIn(ExperimentalTypeInference::class) @Composable fun produceSaveableState( initialValue: T, stateSaver: Saver, @BuilderInference producer: suspend ProduceStateScope.() -> Unit ): State { - val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } - - var hasToFetch by rememberSaveable { mutableStateOf(true) } + val result = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(initialValue) + } LaunchedEffect(Unit) { - if (hasToFetch) { - ProduceSaveableStateScope(result, coroutineContext).producer() - hasToFetch = false - } + ProduceSaveableStateScope(result, coroutineContext).producer() } + return result } -@OptIn(ExperimentalTypeInference::class) @Composable fun produceSaveableState( initialValue: T, @@ -42,20 +40,42 @@ fun produceSaveableState( key1: Any?, @BuilderInference producer: suspend ProduceStateScope.() -> Unit ): State { - val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } - - var hasToFetch by rememberSaveable(key1) { mutableStateOf(true) } + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(initialValue) + } LaunchedEffect(key1) { - if (hasToFetch) { - ProduceSaveableStateScope(result, coroutineContext).producer() - hasToFetch = false - } + ProduceSaveableStateScope(state, coroutineContext).producer() } - return result + + return state +} + +@Composable +fun produceSaveableOneShotState( + initialValue: T, + stateSaver: Saver, + key1: Any?, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(initialValue) + } + + var produced by rememberSaveable(key1) { + mutableStateOf(false) + } + + LaunchedEffect(key1) { + if (!produced) { + ProduceSaveableStateScope(state, coroutineContext).producer() + produced = true + } + } + + return state } -@OptIn(ExperimentalTypeInference::class) @Composable fun produceSaveableState( initialValue: T, @@ -64,42 +84,42 @@ fun produceSaveableState( key2: Any?, @BuilderInference producer: suspend ProduceStateScope.() -> Unit ): State { - val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + val result = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(initialValue) + } - var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } - - LaunchedEffect(Unit) { - if (hasToFetch) { - ProduceSaveableStateScope(result, coroutineContext).producer() - hasToFetch = false - } + LaunchedEffect(key1, key2) { + ProduceSaveableStateScope(result, coroutineContext).producer() } return result } -@OptIn(ExperimentalTypeInference::class) @Composable -fun produceSaveableRelaunchableState( +fun produceSaveableRelaunchableOneShotState( initialValue: T, stateSaver: Saver, key1: Any?, key2: Any?, @BuilderInference producer: suspend ProduceStateScope.() -> Unit ): Pair, () -> Unit> { - val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } + val result = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(initialValue) + } - var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } + var produced by rememberSaveable(key1, key2) { + mutableStateOf(false) + } val relaunchableEffect = relaunchableEffect(key1, key2) { - if (hasToFetch) { + if (!produced) { ProduceSaveableStateScope(result, coroutineContext).producer() - hasToFetch = false + produced = true } } return result to { - hasToFetch = true + produced = false relaunchableEffect() } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt deleted file mode 100644 index c914a7f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/RememberLazyListStates.kt +++ /dev/null @@ -1,61 +0,0 @@ -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 { - return rememberSaveable( - saver = listSaver( - save = { states: List -> - 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 { - return rememberSaveable( - saver = listSaver( - save = { states: List -> - 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) } - } -} From eeec55c3693ce4f0d143c5a46b51023734485766 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Mon, 26 Sep 2022 21:37:48 +0200 Subject: [PATCH 016/100] Change VerticalBar top button shape to circle --- .../it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt index 1eb24b0..ef9e254 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt @@ -8,7 +8,7 @@ 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.shape.RoundedCornerShape +import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -42,7 +42,7 @@ fun VerticalBar( contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.textSecondary), modifier = Modifier - .clip(RoundedCornerShape(16.dp)) + .clip(CircleShape) .clickable(onClick = onTopIconButtonClick) .padding(all = 12.dp) .size(22.dp) From 9d1ed51d616ada90634cadac1465c7405afc8244 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 27 Sep 2022 12:07:56 +0200 Subject: [PATCH 017/100] Add scroll to top button in HomeSongList --- .../ui/components/themed/ScrollToTop.kt | 101 +++++++++ .../vimusic/ui/screens/home/HomeSongList.kt | 203 ++++++++++-------- 2 files changed, 211 insertions(+), 93 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt new file mode 100644 index 0000000..a2dd671 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/ScrollToTop.kt @@ -0,0 +1,101 @@ +package it.vfsfitvnm.vimusic.ui.components.themed + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import kotlinx.coroutines.launch + +@Composable +fun ScrollToTop( + lazyListState: LazyListState, + modifier: Modifier = Modifier, +) { + val showScrollTopButton by remember { + derivedStateOf { + lazyListState.firstVisibleItemIndex > lazyListState.layoutInfo.visibleItemsInfo.size + } + } + + ScrollToTop( + isVisible = showScrollTopButton, + onClick = { lazyListState.animateScrollToItem(0) }, + modifier = modifier + ) +} + +@Composable +fun ScrollToTop( + lazyGridState: LazyGridState, + modifier: Modifier = Modifier, +) { + val showScrollTopButton by remember { + derivedStateOf { + lazyGridState.firstVisibleItemIndex > lazyGridState.layoutInfo.visibleItemsInfo.size + } + } + + ScrollToTop( + isVisible = showScrollTopButton, + onClick = { lazyGridState.animateScrollToItem(0) }, + modifier = modifier + ) +} + +@Composable +private fun ScrollToTop( + isVisible: Boolean, + onClick: suspend () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isVisible, + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + modifier = modifier + ) { + val coroutineScope = rememberCoroutineScope() + + Box( + modifier = Modifier + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clickable { + coroutineScope.launch { + onClick() + } + } + .size(32.dp) + ) { + Image( + painter = painterResource(R.drawable.chevron_down), + contentDescription = null, + colorFilter = ColorFilter.tint(LocalAppearance.current.colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .rotate(180f) + .size(20.dp) + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index 2e46bd5..bc2a12d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -12,14 +12,17 @@ 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.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +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.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -44,6 +47,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.ScrollToTop import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -88,114 +92,127 @@ fun HomeSongList() { animationSpec = tween(durationMillis = 400, easing = LinearEasing) ) - LazyColumn( - contentPadding = LocalPlayerAwarePaddingValues.current, + val lazyListState = rememberLazyListState() + + Box( modifier = Modifier .background(colorPalette.background0) .fillMaxSize() ) { - item( - key = "header", - contentType = 0 + LazyColumn( + state = lazyListState, + contentPadding = LocalPlayerAwarePaddingValues.current, ) { - Header(title = "Songs") { - @Composable - fun Item( - @DrawableRes iconId: Int, - targetSortBy: SongSortBy - ) { - Image( - painter = painterResource(iconId), - contentDescription = null, - colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), + item( + key = "header", + contentType = 0 + ) { + Header(title = "Songs") { + @Composable + fun Item( + @DrawableRes iconId: Int, + targetSortBy: SongSortBy + ) { + Image( + painter = painterResource(iconId), + contentDescription = null, + colorFilter = ColorFilter.tint(if (sortBy == targetSortBy) colorPalette.text else colorPalette.textDisabled), + modifier = Modifier + .clickable { sortBy = targetSortBy } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + Item( + iconId = R.drawable.trending, + targetSortBy = SongSortBy.PlayTime + ) + + Item( + iconId = R.drawable.text, + targetSortBy = SongSortBy.Title + ) + + Item( + iconId = R.drawable.time, + targetSortBy = SongSortBy.DateAdded + ) + + Spacer( modifier = Modifier - .clickable { sortBy = targetSortBy } + .width(2.dp) + ) + + Image( + painter = painterResource(R.drawable.arrow_up), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { sortOrder = !sortOrder } .padding(all = 4.dp) .size(18.dp) + .graphicsLayer { rotationZ = sortOrderIconRotation } ) } + } - Item( - iconId = R.drawable.trending, - targetSortBy = SongSortBy.PlayTime - ) - - Item( - iconId = R.drawable.text, - targetSortBy = SongSortBy.Title - ) - - Item( - iconId = R.drawable.time, - targetSortBy = SongSortBy.DateAdded - ) - - Spacer( + itemsIndexed( + items = items, + key = { _, song -> song.id } + ) { index, song -> + SongItem( + song = song, + thumbnailSize = thumbnailSize, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index) + }, + menuContent = { + InHistoryMediaItemMenu(song = song) + }, + onThumbnailContent = { + AnimatedVisibility( + visible = 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 - .width(2.dp) - ) - - Image( - painter = painterResource(R.drawable.arrow_up), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { sortOrder = !sortOrder } - .padding(all = 4.dp) - .size(18.dp) - .graphicsLayer { rotationZ = sortOrderIconRotation } + .animateItemPlacement() ) } } - itemsIndexed( - items = items, - key = { _, song -> song.id } - ) { index, song -> - SongItem( - song = song, - thumbnailSize = thumbnailSize, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index) - }, - menuContent = { - InHistoryMediaItemMenu(song = song) - }, - onThumbnailContent = { - AnimatedVisibility( - visible = 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() - ) - } + ScrollToTop( + lazyListState = lazyListState, + modifier = Modifier + .offset(x = -Dimensions.verticalBarWidth) + .align(Alignment.BottomStart) + ) } } From 2e3d437c1536513ccbf3dd2ba56bbfb056f3e9f1 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 27 Sep 2022 15:17:27 +0200 Subject: [PATCH 018/100] Redesign LocalPlaylistScreen (#172) --- .../vimusic/models/PlaylistWithSongs.kt | 7 +- .../vimusic/savers/DetailedSongSaver.kt | 24 +- .../it/vfsfitvnm/vimusic/savers/ListSaver.kt | 3 + .../vimusic/savers/PlaylistWithSongsSaver.kt | 20 ++ .../vimusic/ui/screens/LocalPlaylistScreen.kt | 333 ------------------ .../vimusic/ui/screens/home/HomeScreen.kt | 2 +- .../localplaylist/LocalPlaylistScreen.kt | 40 +++ .../localplaylist/LocalPlaylistSongList.kt | 302 ++++++++++++++++ 8 files changed, 378 insertions(+), 353 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt index cb3f47e..ac26fda 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/PlaylistWithSongs.kt @@ -19,9 +19,4 @@ data class PlaylistWithSongs( ) ) val songs: List -) { - companion object { - val Empty = PlaylistWithSongs(Playlist(-1, ""), emptyList()) - val NotFound = PlaylistWithSongs(Playlist(-2, "Not found"), emptyList()) - } -} +) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt index cfede7b..db11ff5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt @@ -5,7 +5,7 @@ import androidx.compose.runtime.saveable.SaverScope import it.vfsfitvnm.vimusic.models.DetailedSong object DetailedSongSaver : Saver> { - override fun SaverScope.save(value: DetailedSong): List = + override fun SaverScope.save(value: DetailedSong) = listOf( value.id, value.title, @@ -18,16 +18,14 @@ object DetailedSongSaver : Saver> { ) @Suppress("UNCHECKED_CAST") - override fun restore(value: List): DetailedSong? { - return if (value.size == 8) DetailedSong( - id = value[0] as String, - title = value[1] as String, - artistsText = value[2] as String?, - durationText = value[3] as String, - thumbnailUrl = value[4] as String?, - totalPlayTimeMs = value[5] as Long, - albumId = value[6] as String?, - artists = InfoListSaver.restore(value[7] as List>) - ) else null - } + override fun restore(value: List) = DetailedSong( + id = value[0] as String, + title = value[1] as String, + artistsText = value[2] as String?, + durationText = value[3] as String, + thumbnailUrl = value[4] as String?, + totalPlayTimeMs = value[5] as Long, + albumId = value[6] as String?, + artists = InfoListSaver.restore(value[7] as List>) + ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt index 3b7f617..62e8621 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ListSaver.kt @@ -4,6 +4,9 @@ import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope interface ListSaver : Saver, List> { + override fun SaverScope.save(value: List): List + override fun restore(value: List): List + companion object { fun of(saver: Saver): ListSaver { return object : ListSaver { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt new file mode 100644 index 0000000..c6b4a9d --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/PlaylistWithSongsSaver.kt @@ -0,0 +1,20 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.vimusic.models.PlaylistWithSongs + +object PlaylistWithSongsSaver : Saver> { + override fun SaverScope.save(value: PlaylistWithSongs?) = value?.let { + listOf( + with(PlaylistSaver) { save(value.playlist) }, + with(DetailedSongListSaver) { save(value.songs) }, + ) + } + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List): PlaylistWithSongs = PlaylistWithSongs( + playlist = PlaylistSaver.restore(value[0] as List), + songs = DetailedSongListSaver.restore(value[1] as List>) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt deleted file mode 100644 index 66f4754..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/LocalPlaylistScreen.kt +++ /dev/null @@ -1,333 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import androidx.compose.animation.ExperimentalAnimationApi -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.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.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.BasicText -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.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -import it.vfsfitvnm.reordering.ReorderingLazyColumn -import it.vfsfitvnm.reordering.animateItemPlacement -import it.vfsfitvnm.reordering.draggedItem -import it.vfsfitvnm.reordering.rememberReorderingState -import it.vfsfitvnm.reordering.reorder -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.models.DetailedSong -import it.vfsfitvnm.vimusic.models.PlaylistWithSongs -import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.query -import it.vfsfitvnm.vimusic.transaction -import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog -import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.Menu -import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry -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.SongItem -import it.vfsfitvnm.vimusic.utils.asMediaItem -import it.vfsfitvnm.vimusic.utils.enqueue -import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex -import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.vimusic.utils.toMediaItem -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.map -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext - -@ExperimentalFoundationApi -@ExperimentalAnimationApi -@Composable -fun LocalPlaylistScreen(playlistId: Long) { - val playlistWithSongs by remember(playlistId) { - Database.playlistWithSongs(playlistId).map { it ?: PlaylistWithSongs.NotFound } - }.collectAsState(initial = PlaylistWithSongs.Empty, context = Dispatchers.IO) - - val lazyListState = rememberLazyListState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val (colorPalette, typography) = LocalAppearance.current - val menuState = LocalMenuState.current - val binder = LocalPlayerServiceBinder.current - - val thumbnailSize = Dimensions.thumbnails.song.px - - val reorderingState = rememberReorderingState( - lazyListState = lazyListState, - key = playlistWithSongs.songs, - onDragEnd = { fromIndex, toIndex -> - query { - Database.move(playlistWithSongs.playlist.id, fromIndex, toIndex) - } - }, - extraItemCount = 1 - ) - - var isRenaming by rememberSaveable { - mutableStateOf(false) - } - - if (isRenaming) { - TextFieldDialog( - hintText = "Enter the playlist name", - initialTextInput = playlistWithSongs.playlist.name, - onDismiss = { isRenaming = false }, - onDone = { text -> - query { - Database.update(playlistWithSongs.playlist.copy(name = text)) - } - } - ) - } - - var isDeleting by rememberSaveable { - mutableStateOf(false) - } - - if (isDeleting) { - ConfirmationDialog( - text = "Do you really want to delete this playlist?", - onDismiss = { isDeleting = false }, - onConfirm = { - query { - Database.delete(playlistWithSongs.playlist) - } - pop() - } - ) - } - - ReorderingLazyColumn( - reorderingState = reorderingState, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - Column { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp, horizontal = 16.dp) - .size(24.dp) - ) - } - - Column( - modifier = Modifier - .padding(top = 16.dp, bottom = 8.dp) - .padding(horizontal = 16.dp) - ) { - BasicText( - text = playlistWithSongs.playlist.name, - style = typography.m.semiBold - ) - - BasicText( - text = "${playlistWithSongs.songs.size} songs", - style = typography.xxs.semiBold.secondary - ) - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .zIndex(1f) - .padding(horizontal = 8.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(enabled = playlistWithSongs.songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - playlistWithSongs.songs - .shuffled() - .map(DetailedSong::asMediaItem) - ) - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - isEnabled = playlistWithSongs.songs.isNotEmpty(), - onClick = { - menuState.hide() - binder?.player?.enqueue( - playlistWithSongs.songs.map( - DetailedSong::asMediaItem - ) - ) - } - ) - - MenuEntry( - icon = R.drawable.pencil, - text = "Rename", - onClick = { - menuState.hide() - isRenaming = true - } - ) - - playlistWithSongs.playlist.browseId?.let { browseId -> - MenuEntry( - icon = R.drawable.sync, - text = "Sync", - onClick = { - menuState.hide() - transaction { - runBlocking(Dispatchers.IO) { - withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { - it.next() - }?.map { playlist -> - playlist.copy(items = playlist.items?.filter { it.info.endpoint != null }) - } - } - }?.getOrNull()?.let { remotePlaylist -> - Database.clearPlaylist(playlistWithSongs.playlist.id) - - remotePlaylist.items?.forEachIndexed { index, song -> - song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem -> - Database.insert(mediaItem) - - Database.insert( - SongPlaylistMap( - songId = mediaItem.mediaId, - playlistId = playlistId, - position = index - ) - ) - } - } - } - } - } - ) - } - - MenuEntry( - icon = R.drawable.trash, - text = "Delete", - onClick = { - menuState.hide() - isDeleting = true - } - ) - } - } - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - } - } - } - - itemsIndexed( - items = playlistWithSongs.songs, - key = { _, song -> song.id }, - contentType = { _, song -> song }, - ) { index, song -> - SongItem( - song = song, - thumbnailSize = thumbnailSize, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - playlistWithSongs.songs.map( - DetailedSong::asMediaItem - ), index - ) - }, - menuContent = { - InPlaylistMediaItemMenu( - playlistId = playlistId, - positionInPlaylist = index, - song = song - ) - }, - trailingContent = { - Image( - painter = painterResource(R.drawable.reorder), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.textSecondary), - modifier = Modifier - .clickable { } - .reorder( - reorderingState = reorderingState, - index = index - ) - .padding(horizontal = 8.dp, vertical = 4.dp) - .size(20.dp) - ) - }, - modifier = Modifier - .animateItemPlacement(reorderingState = reorderingState) - .draggedItem(reorderingState = reorderingState, index = index) - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 1b7192a..8140ab5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -13,7 +13,7 @@ import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen -import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen +import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistScreen.kt new file mode 100644 index 0000000..a1c4581 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistScreen.kt @@ -0,0 +1,40 @@ +package it.vfsfitvnm.vimusic.ui.screens.localplaylist + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun LocalPlaylistScreen(playlistId: Long) { + val saveableStateHolder = rememberSaveableStateHolder() + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = 0, + onTabChanged = { }, + tabColumnContent = { Item -> + Item(0, "Songs", R.drawable.musical_notes) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { + LocalPlaylistSongList( + playlistId = playlistId, + onDelete = pop + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt new file mode 100644 index 0000000..a8c17be --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -0,0 +1,302 @@ +package it.vfsfitvnm.vimusic.ui.screens.localplaylist + +import androidx.compose.animation.ExperimentalAnimationApi +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.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.lazy.rememberLazyListState +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.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.res.painterResource +import androidx.compose.ui.unit.dp +import it.vfsfitvnm.reordering.ReorderingLazyColumn +import it.vfsfitvnm.reordering.animateItemPlacement +import it.vfsfitvnm.reordering.draggedItem +import it.vfsfitvnm.reordering.rememberReorderingState +import it.vfsfitvnm.reordering.reorder +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.DetailedSong +import it.vfsfitvnm.vimusic.models.SongPlaylistMap +import it.vfsfitvnm.vimusic.query +import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver +import it.vfsfitvnm.vimusic.transaction +import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu +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.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.enqueue +import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.vimusic.utils.toMediaItem +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +@ExperimentalAnimationApi +@ExperimentalFoundationApi +@Composable +fun LocalPlaylistSongList( + playlistId: Long, + onDelete: () -> Unit, +) { + val (colorPalette, typography) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + + val playlistWithSongs by produceSaveableState( + initialValue = null, + stateSaver = PlaylistWithSongsSaver + ) { + Database + .playlistWithSongs(playlistId) + .flowOn(Dispatchers.IO) + .collect { value = it } + } + + val lazyListState = rememberLazyListState() + + val reorderingState = rememberReorderingState( + lazyListState = lazyListState, + key = playlistWithSongs?.songs ?: emptyList(), + onDragEnd = { fromIndex, toIndex -> + query { + Database.move(playlistId, fromIndex, toIndex) + } + }, + extraItemCount = 1 + ) + + var isRenaming by rememberSaveable { + mutableStateOf(false) + } + + if (isRenaming) { + TextFieldDialog( + hintText = "Enter the playlist name", + initialTextInput = playlistWithSongs?.playlist?.name ?: "", + onDismiss = { isRenaming = false }, + onDone = { text -> + query { + playlistWithSongs?.playlist?.copy(name = text)?.let(Database::update) + } + } + ) + } + + var isDeleting by rememberSaveable { + mutableStateOf(false) + } + + if (isDeleting) { + ConfirmationDialog( + text = "Do you really want to delete this playlist?", + onDismiss = { isDeleting = false }, + onConfirm = { + query { + playlistWithSongs?.playlist?.let(Database::delete) + } + onDelete() + } + ) + } + + val thumbnailSize = Dimensions.thumbnails.song.px + + Box { + ReorderingLazyColumn( + reorderingState = reorderingState, + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header(title = playlistWithSongs?.playlist?.name ?: "Unknown") { + BasicText( + text = "Enqueue", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = playlistWithSongs?.songs?.isNotEmpty() == true) { + playlistWithSongs?.songs + ?.map(DetailedSong::asMediaItem) + ?.let { mediaItems -> + binder?.player?.enqueue(mediaItems) + } + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + + Spacer( + modifier = Modifier + .weight(1f) + ) + + playlistWithSongs?.playlist?.browseId?.let { browseId -> + Image( + painter = painterResource(R.drawable.sync), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + transaction { + runBlocking(Dispatchers.IO) { + withContext(Dispatchers.IO) { + YouTube.playlist(browseId)?.map { + it.next() + }?.map { playlist -> + playlist.copy(items = playlist.items?.filter { it.info.endpoint != null }) + } + } + }?.getOrNull()?.let { remotePlaylist -> + Database.clearPlaylist(playlistId) + + remotePlaylist.items?.forEachIndexed { index, song -> + song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem -> + Database.insert(mediaItem) + + Database.insert( + SongPlaylistMap( + songId = mediaItem.mediaId, + playlistId = playlistId, + position = index + ) + ) + } + } + } + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + Image( + painter = painterResource(R.drawable.pencil), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { isRenaming = true } + .padding(all = 4.dp) + .size(18.dp) + ) + + + Image( + painter = painterResource(R.drawable.trash), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { isDeleting = true } + .padding(all = 4.dp) + .size(18.dp) + ) + } + } + + itemsIndexed( + items = playlistWithSongs?.songs ?: emptyList(), + key = { _, song -> song.id }, + contentType = { _, song -> song }, + ) { index, song -> + SongItem( + song = song, + thumbnailSize = thumbnailSize, + onClick = { + playlistWithSongs?.songs?.map(DetailedSong::asMediaItem) + ?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(mediaItems, index) + } + }, + menuContent = { + InPlaylistMediaItemMenu( + playlistId = playlistId, + positionInPlaylist = index, + song = song + ) + }, + trailingContent = { + Image( + painter = painterResource(R.drawable.reorder), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.textSecondary), + modifier = Modifier + .clickable { } + .reorder( + reorderingState = reorderingState, + index = index + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + .size(20.dp) + ) + }, + modifier = Modifier + .animateItemPlacement(reorderingState = reorderingState) + .draggedItem(reorderingState = reorderingState, index = index) + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = playlistWithSongs?.songs?.isNotEmpty() == true) { + playlistWithSongs?.songs + ?.shuffled() + ?.map(DetailedSong::asMediaItem) + ?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(mediaItems) + } + } + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } + } +} From 83230e3817ef6ae34aea912a7e26bddbf4154535 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 27 Sep 2022 16:43:59 +0200 Subject: [PATCH 019/100] Redesign PlaylistScreen (#172) --- .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 6 + .../vimusic/enums/ThumbnailRoundness.kt | 7 - .../vimusic/savers/AlbumResultSaver.kt | 2 +- .../vfsfitvnm/vimusic/savers/ResultSaver.kt | 20 +- .../vimusic/savers/StringListResultSaver.kt | 2 +- .../vimusic/savers/StringResultSaver.kt | 2 +- .../savers/YouTubePlaylistOrAlbumSaver.kt | 27 ++ .../vimusic/ui/screens/IntentUriScreen.kt | 3 + .../vimusic/ui/screens/PlaylistScreen.kt | 457 ------------------ .../vimusic/ui/screens/album/AlbumOverview.kt | 18 +- .../vimusic/ui/screens/home/HomeSongList.kt | 5 +- .../localplaylist/LocalPlaylistSongList.kt | 24 +- .../ui/screens/player/PlayerBottomSheet.kt | 23 +- .../vimusic/ui/screens/player/Thumbnail.kt | 3 +- .../ui/screens/playlist/PlaylistScreen.kt | 39 ++ .../ui/screens/playlist/PlaylistSongList.kt | 317 ++++++++++++ .../searchresult/SearchResultScreen.kt | 6 +- .../it/vfsfitvnm/vimusic/ui/views/SongItem.kt | 3 +- .../vimusic/utils/ProduceSaveableState.kt | 24 + .../it/vfsfitvnm/vimusic/utils/Utils.kt | 31 -- .../it/vfsfitvnm/youtubemusic/YouTube.kt | 127 ++--- 21 files changed, 537 insertions(+), 609 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index ea007bb..aa9bd0a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -285,6 +285,9 @@ interface Database { @Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query") fun search(query: String): Flow> + @Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)") + fun isImportedPlaylist(browseId: String): Flow + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(format: Format) @@ -315,6 +318,9 @@ interface Database { @Insert(onConflict = OnConflictStrategy.ABORT) fun insert(queuedMediaItems: List) + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertSongPlaylistMaps(songPlaylistMaps: List) + @Transaction fun insert(mediaItem: MediaItem, block: (Song) -> Song = { it }) { val song = Song( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ThumbnailRoundness.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ThumbnailRoundness.kt index 01bcb6e..9fd8ba3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ThumbnailRoundness.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/enums/ThumbnailRoundness.kt @@ -22,11 +22,4 @@ enum class ThumbnailRoundness { Heavy -> RoundedCornerShape(8.dp) } } - - companion object { - val shape: Shape - @Composable - @ReadOnlyComposable - get() = LocalAppearance.current.thumbnailShape - } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt index 4b9eea3..b3cf4b3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/AlbumResultSaver.kt @@ -1,3 +1,3 @@ package it.vfsfitvnm.vimusic.savers -val AlbumResultSaver = ResultSaver.of(AlbumSaver) +val AlbumResultSaver = resultSaver(AlbumSaver) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt index e750098..763d2c8 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt @@ -3,16 +3,14 @@ package it.vfsfitvnm.vimusic.savers import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope -interface ResultSaver : Saver?, Pair> { - companion object { - fun of(saver: Saver) = - object : Saver?, Pair> { - override fun restore(value: Pair) = - value.first?.let(saver::restore)?.let(Result.Companion::success) - ?: value.second?.let(Result.Companion::failure) +interface ResultSaver : Saver?, Pair> - override fun SaverScope.save(value: Result?) = - with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull() - } +fun resultSaver(saver: Saver) = + object : Saver?, Pair> { + override fun restore(value: Pair) = + value.first?.let(saver::restore)?.let(Result.Companion::success) + ?: value.second?.let(Result.Companion::failure) + + override fun SaverScope.save(value: Result?) = + with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull() } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt index f7da5d5..37c0c69 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringListResultSaver.kt @@ -2,4 +2,4 @@ package it.vfsfitvnm.vimusic.savers import androidx.compose.runtime.saveable.autoSaver -val StringListResultSaver = ResultSaver.of(autoSaver?>()) +val StringListResultSaver = resultSaver(autoSaver?>()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt index a5b35aa..1db4d43 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/StringResultSaver.kt @@ -2,4 +2,4 @@ package it.vfsfitvnm.vimusic.savers import androidx.compose.runtime.saveable.autoSaver -val StringResultSaver = ResultSaver.of(autoSaver()) +val StringResultSaver = resultSaver(autoSaver()) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt new file mode 100644 index 0000000..2601e8b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistOrAlbumSaver.kt @@ -0,0 +1,27 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubePlaylistOrAlbumSaver : Saver> { + override fun SaverScope.save(value: YouTube.PlaylistOrAlbum): List = listOf( + value.title, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } } , + value.year, + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } , + value.songs?.let { with(YouTubeSongListSaver) { save(it) } }, + value.url + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.PlaylistOrAlbum( + title = value[0] as String?, + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), + year = value[2] as String?, + thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore), + songs = (value[4] as List>?)?.let(YouTubeSongListSaver::restore), + url = value[5] as String?, + continuation = null + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt index c475ea2..27ba93f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt @@ -2,6 +2,7 @@ package it.vfsfitvnm.vimusic.ui.screens import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -40,6 +41,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Menu import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog +import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -53,6 +55,7 @@ import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +@ExperimentalFoundationApi @ExperimentalAnimationApi @Composable fun IntentUriScreen(uri: Uri) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt deleted file mode 100644 index 9a0e48c..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/PlaylistScreen.kt +++ /dev/null @@ -1,457 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import android.content.Intent -import androidx.compose.animation.ExperimentalAnimationApi -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.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.IntrinsicSize -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight -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.lazy.rememberLazyListState -import androidx.compose.foundation.shape.CircleShape -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.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.platform.LocalContext -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 coil.compose.AsyncImage -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.ThumbnailRoundness -import it.vfsfitvnm.vimusic.models.Playlist -import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.transaction -import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError -import it.vfsfitvnm.vimusic.ui.components.themed.Menu -import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry -import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder -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.styling.shimmer -import it.vfsfitvnm.vimusic.ui.views.SongItem -import it.vfsfitvnm.vimusic.utils.bold -import it.vfsfitvnm.vimusic.utils.center -import it.vfsfitvnm.vimusic.utils.enqueue -import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex -import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.relaunchableEffect -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.vimusic.utils.toMediaItem -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@ExperimentalAnimationApi -@Composable -fun PlaylistScreen(browseId: String) { - val lazyListState = rememberLazyListState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val context = LocalContext.current - val binder = LocalPlayerServiceBinder.current - - val (colorPalette, typography) = LocalAppearance.current - val menuState = LocalMenuState.current - - val thumbnailSizePx = Dimensions.thumbnails.playlist.px - val songThumbnailSizePx = Dimensions.thumbnails.song.px - - var playlist by remember { - mutableStateOf?>(null) - } - - val onLoad = relaunchableEffect(Unit) { - playlist = withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { - it.next() - }?.map { playlist -> - playlist.copy(items = playlist.items?.filter { it.info.endpoint != null }) - } - } - } - - LazyColumn( - state = lazyListState, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - } - } - - item { - playlist?.getOrNull()?.let { playlist -> - Column { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .fillMaxWidth() - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 8.dp) - ) { - AsyncImage( - model = playlist.thumbnail?.size(thumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(Dimensions.thumbnails.playlist) - ) - - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxSize() - ) { - Column { - BasicText( - text = playlist.title ?: "Unknown", - style = typography.m.semiBold - ) - - BasicText( - text = playlist.authors?.joinToString("") { it.name } - ?: "", - style = typography.xs.secondary.semiBold, - maxLines = 2, - overflow = TextOverflow.Ellipsis, - ) - } - - playlist.year?.let { year -> - BasicText( - text = year, - style = typography.xs.secondary, - maxLines = 1, - modifier = Modifier - .padding(top = 8.dp) - ) - } - } - } - - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .zIndex(1f) - .padding(horizontal = 8.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - binder?.stopRadio() - playlist.items - ?.shuffled() - ?.mapNotNull { song -> - song.toMediaItem(browseId, playlist) - } - ?.let { mediaItems -> - binder?.player?.forcePlayFromBeginning( - mediaItems - ) - } - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - onClick = { - menuState.hide() - playlist.items - ?.mapNotNull { song -> - song.toMediaItem( - browseId, - playlist - ) - } - ?.let { mediaItems -> - binder?.player?.enqueue( - mediaItems - ) - } - } - ) - - MenuEntry( - icon = R.drawable.playlist, - text = "Import", - onClick = { - menuState.hide() - transaction { - val playlistId = - Database.insert( - Playlist( - name = playlist.title - ?: "Unknown", - browseId = browseId - ) - ) - - playlist.items?.forEachIndexed { index, song -> - song - .toMediaItem( - browseId, - playlist - ) - ?.let { mediaItem -> - Database.insert( - mediaItem - ) - - Database.insert( - SongPlaylistMap( - songId = mediaItem.mediaId, - playlistId = playlistId, - position = index - ) - ) - } - } - } - } - ) - - MenuEntry( - icon = R.drawable.share_social, - text = "Share", - onClick = { - menuState.hide() - - (playlist.url - ?: "https://music.youtube.com/playlist?list=${ - browseId.removePrefix( - "VL" - ) - }").let { url -> - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, url) - } - - context.startActivity( - Intent.createChooser( - sendIntent, - null - ) - ) - } - } - ) - } - } - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - } - } - } ?: playlist?.exceptionOrNull()?.let { throwable -> - LoadingOrError( - errorMessage = throwable.javaClass.canonicalName, - onRetry = onLoad - ) - } ?: LoadingOrError() - } - - itemsIndexed( - items = playlist?.getOrNull()?.items ?: emptyList(), - contentType = { _, song -> song } - ) { index, song -> - SongItem( - title = song.info.name, - authors = (song.authors - ?: playlist?.getOrNull()?.authors)?.joinToString("") { it.name }, - durationText = song.durationText, - onClick = { - binder?.stopRadio() - playlist?.getOrNull()?.items?.mapNotNull { song -> - song.toMediaItem(browseId, playlist?.getOrNull()!!) - }?.let { mediaItems -> - binder?.player?.forcePlayAtIndex(mediaItems, index) - } - }, - startContent = { - if (song.thumbnail == null) { - BasicText( - text = "${index + 1}", - style = typography.xs.secondary.bold.center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .width(36.dp) - ) - } else { - AsyncImage( - model = song.thumbnail!!.size(songThumbnailSizePx), - contentDescription = null, - contentScale = ContentScale.Crop, - modifier = Modifier - .clip(ThumbnailRoundness.shape) - .size(Dimensions.thumbnails.song) - ) - } - }, - menuContent = { - NonQueuedMediaItemMenu( - mediaItem = song.toMediaItem( - browseId, - playlist?.getOrNull()!! - ) - ?: return@SongItem, - onDismiss = menuState::hide, - ) - } - ) - } - } - } - } -} - -@Composable -private fun LoadingOrError( - errorMessage: String? = null, - onRetry: (() -> Unit)? = null -) { - val (colorPalette) = LocalAppearance.current - - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .height(IntrinsicSize.Max) - .padding(vertical = 8.dp, horizontal = 16.dp) - .padding(bottom = 16.dp) - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = ThumbnailRoundness.shape) - .size(Dimensions.thumbnails.playlist) - ) - - Column( - verticalArrangement = Arrangement.SpaceEvenly, - modifier = Modifier - .fillMaxHeight() - ) { - Column { - TextPlaceholder() - - TextPlaceholder( - modifier = Modifier - .alpha(0.7f) - ) - } - } - } - - repeat(3) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier - .alpha(0.6f - it * 0.1f) - .height(Dimensions.thumbnails.song) - .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 16.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .size(36.dp) - ) { - Spacer( - modifier = Modifier - .size(8.dp) - .background(color = Color.Black, shape = CircleShape) - ) - } - - Column( - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - TextPlaceholder() - - TextPlaceholder( - modifier = Modifier - .alpha(0.7f) - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index 65520d5..3f641b5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -6,7 +6,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints @@ -30,7 +29,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.style.TextOverflow @@ -67,7 +65,6 @@ import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.thumbnail -import it.vfsfitvnm.vimusic.utils.toMediaItem import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn @@ -101,16 +98,16 @@ fun AlbumOverview( shareUrl = youtubeAlbum.url, timestamp = System.currentTimeMillis() ), - youtubeAlbum.items?.mapIndexedNotNull { position, albumItem -> - albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem -> - Database.insert(mediaItem) + youtubeAlbum.songs + ?.map(YouTube.Item.Song::asMediaItem) + ?.onEach(Database::insert) + ?.mapIndexed { position, mediaItem -> SongAlbumMap( songId = mediaItem.mediaId, albumId = browseId, position = position ) - } - } ?: emptyList() + } ?: emptyList() ) null @@ -298,11 +295,6 @@ fun AlbumOverview( } ?: albumResult?.exceptionOrNull()?.let { Box( modifier = Modifier - .pointerInput(Unit) { - detectTapGestures { -// viewModel.fetch(browseId) - } - } .align(Alignment.Center) .fillMaxSize() ) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index bc2a12d..9a1c867 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -42,7 +42,6 @@ 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.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header @@ -68,7 +67,7 @@ import kotlinx.coroutines.flow.flowOn @ExperimentalAnimationApi @Composable fun HomeSongList() { - val (colorPalette, typography) = LocalAppearance.current + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val thumbnailSize = Dimensions.thumbnails.song.px @@ -193,7 +192,7 @@ fun HomeSongList() { Color.Black.copy(alpha = 0.75f) ) ), - shape = ThumbnailRoundness.shape + shape = thumbnailShape ) .padding( horizontal = 8.dp, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt index a8c17be..5aaec96 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -53,7 +53,6 @@ import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableState -import it.vfsfitvnm.vimusic.utils.toMediaItem import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn @@ -177,25 +176,22 @@ fun LocalPlaylistSongList( YouTube.playlist(browseId)?.map { it.next() }?.map { playlist -> - playlist.copy(items = playlist.items?.filter { it.info.endpoint != null }) + playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null }) } } }?.getOrNull()?.let { remotePlaylist -> Database.clearPlaylist(playlistId) - remotePlaylist.items?.forEachIndexed { index, song -> - song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem -> - Database.insert(mediaItem) - - Database.insert( - SongPlaylistMap( - songId = mediaItem.mediaId, - playlistId = playlistId, - position = index - ) + remotePlaylist.songs + ?.map(YouTube.Item.Song::asMediaItem) + ?.onEach(Database::insert) + ?.mapIndexed { position, mediaItem -> + SongPlaylistMap( + songId = mediaItem.mediaId, + playlistId = playlistId, + position = position ) - } - } + }?.let(Database::insertSongPlaylistMaps) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index 11d51a5..c9c9150 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -8,7 +8,21 @@ 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.* +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.calculateEndPadding +import androidx.compose.foundation.layout.calculateStartPadding +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape @@ -35,16 +49,15 @@ import it.vfsfitvnm.reordering.rememberReorderingState import it.vfsfitvnm.reordering.reorder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheetState import it.vfsfitvnm.vimusic.ui.components.MusicBars import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu -import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.onOverlay import it.vfsfitvnm.vimusic.ui.styling.px +import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex @@ -63,7 +76,7 @@ fun PlayerBottomSheet( modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { - val (colorPalette, typography) = LocalAppearance.current + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current BottomSheet( state = layoutState, @@ -168,7 +181,7 @@ fun PlayerBottomSheet( modifier = Modifier .background( color = Color.Black.copy(alpha = 0.25f), - shape = ThumbnailRoundness.shape + shape = thumbnailShape ) .size(Dimensions.thumbnails.song) ) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt index 70375e6..5815994 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt @@ -33,6 +33,7 @@ import it.vfsfitvnm.vimusic.service.LoginRequiredException import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException import it.vfsfitvnm.vimusic.service.UnplayableException 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.rememberError import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex @@ -99,7 +100,7 @@ fun Thumbnail( Box( modifier = modifier .aspectRatio(1f) - .clip(ThumbnailRoundness.shape) + .clip(LocalAppearance.current.thumbnailShape) .size(thumbnailSizeDp) ) { AsyncImage( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt new file mode 100644 index 0000000..0e53acb --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt @@ -0,0 +1,39 @@ +package it.vfsfitvnm.vimusic.ui.screens.playlist + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes + +@ExperimentalFoundationApi +@ExperimentalAnimationApi +@Composable +fun PlaylistScreen(browseId: String) { + val saveableStateHolder = rememberSaveableStateHolder() + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = 0, + onTabChanged = { }, + tabColumnContent = { Item -> + Item(0, "Songs", R.drawable.musical_notes) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { + PlaylistSongList( + browseId = browseId + ) + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt new file mode 100644 index 0000000..db94305 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -0,0 +1,317 @@ +package it.vfsfitvnm.vimusic.ui.screens.playlist + +import android.content.Intent +import androidx.compose.animation.ExperimentalAnimationApi +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.Box +import androidx.compose.foundation.layout.BoxWithConstraints +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.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.saveable.autoSaver +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import com.valentinilk.shimmer.shimmer +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.models.Playlist +import it.vfsfitvnm.vimusic.models.SongPlaylistMap +import it.vfsfitvnm.vimusic.savers.YouTubePlaylistOrAlbumSaver +import it.vfsfitvnm.vimusic.savers.resultSaver +import it.vfsfitvnm.vimusic.transaction +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder +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.styling.shimmer +import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.enqueue +import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.youtubemusic.YouTube +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.withContext + +@ExperimentalAnimationApi +@ExperimentalFoundationApi +@Composable +fun PlaylistSongList( + browseId: String, +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + val context = LocalContext.current + + val playlistResult by produceSaveableOneShotState( + initialValue = null, + stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver), + ) { + value = withContext(Dispatchers.IO) { + YouTube.playlist(browseId)?.map { + it.next() + }?.map { playlist -> + playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null }) + } + } + } + + val isImported by produceSaveableState( + initialValue = null, + stateSaver = autoSaver(), + ) { + Database + .isImportedPlaylist(browseId) + .flowOn(Dispatchers.IO) + .collect { value = it } + } + + BoxWithConstraints { + val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth + val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px + + val songThumbnailSizeDp = Dimensions.thumbnails.song + val songThumbnailSizePx = songThumbnailSizeDp.px + + playlistResult?.getOrNull()?.let { playlist -> + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Column { + Header(title = playlist.title ?: "Unknown") { + if (playlist.songs?.isNotEmpty() == true) { + BasicText( + text = "Enqueue", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable { + playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + binder?.player?.enqueue(mediaItems) + } + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + } + + Spacer( + modifier = Modifier + .weight(1f) + ) + + Image( + painter = painterResource( + if (isImported == true) R.drawable.bookmark else R.drawable.bookmark_outline + ), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.accent), + modifier = Modifier + .clickable(enabled = isImported == false) { + transaction { + val playlistId = + Database.insert( + Playlist( + name = playlist.title ?: "Unknown", + browseId = browseId + ) + ) + + playlist.songs + ?.map(YouTube.Item.Song::asMediaItem) + ?.onEach(Database::insert) + ?.mapIndexed { index, mediaItem -> + SongPlaylistMap( + songId = mediaItem.mediaId, + playlistId = playlistId, + position = index + ) + }?.let(Database::insertSongPlaylistMaps) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + + Image( + painter = painterResource(R.drawable.share_social), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + (playlist.url ?: "https://music.youtube.com/playlist?list=${browseId.removePrefix("VL")}").let { url -> + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, url) + } + + context.startActivity(Intent.createChooser(sendIntent, null)) + } + } + .padding(all = 4.dp) + .size(18.dp) + ) + } + + AsyncImage( + model = playlist.thumbnail?.size(thumbnailSizePx), + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + ) + } + } + + itemsIndexed(items = playlist.songs ?: emptyList()) { index, song -> + SongItem( + title = song.info.name, + authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name }, + durationText = song.durationText, + onClick = { + playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayAtIndex(mediaItems, index) + } + }, + startContent = { + AsyncImage( + model = song.thumbnail?.size(songThumbnailSizePx), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .clip(thumbnailShape) + .size(Dimensions.thumbnails.song) + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = playlist.songs?.isNotEmpty() == true) { + playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(mediaItems.shuffled()) + } + } + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } + } ?: playlistResult?.exceptionOrNull()?.let { + Box( + modifier = Modifier + .align(Alignment.Center) + .fillMaxSize() + ) { + BasicText( + text = "An error has occurred.\nTap to retry", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) + } + } ?: Column( + modifier = Modifier + .padding(LocalPlayerAwarePaddingValues.current) + .shimmer() + .fillMaxSize() + ) { + HeaderPlaceholder() + + Spacer( + modifier = Modifier + .align(Alignment.CenterHorizontally) + .padding(all = 16.dp) + .clip(thumbnailShape) + .size(thumbnailSizeDp) + .background(colorPalette.shimmer) + ) + + repeat(3) { index -> + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .alpha(1f - index * 0.25f) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) + .height(Dimensions.thumbnails.song) + ) { + Spacer( + modifier = Modifier + .background(color = colorPalette.shimmer, shape = thumbnailShape) + .size(Dimensions.thumbnails.song) + ) + + Column { + TextPlaceholder() + TextPlaceholder() + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index 4b3ce98..e46c1a7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -19,10 +19,10 @@ import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold -import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.playlistRoute import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.px @@ -173,7 +173,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailHeightDp = 72.dp val thumbnailWidthDp = 128.dp - SearchResult( + SearchResult( query = query, filter = searchFilter, stateSaver = YouTubeVideoListSaver, @@ -203,7 +203,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { val thumbnailSizeDp = 108.dp val thumbnailSizePx = thumbnailSizeDp.px - SearchResult( + SearchResult( query = query, filter = searchFilter, stateSaver = YouTubePlaylistListSaver, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt index 121d7d9..a0b7672 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt @@ -27,7 +27,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.media3.common.MediaItem import coil.compose.AsyncImage -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -115,7 +114,7 @@ fun SongItem( contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier - .clip(ThumbnailRoundness.shape) + .clip(LocalAppearance.current.thumbnailShape) .fillMaxSize() ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt index 0b73804..32ec81f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt @@ -51,6 +51,30 @@ fun produceSaveableState( return state } +@Composable +fun produceSaveableOneShotState( + initialValue: T, + stateSaver: Saver, + @BuilderInference producer: suspend ProduceStateScope.() -> Unit +): State { + val state = rememberSaveable(stateSaver = stateSaver) { + mutableStateOf(initialValue) + } + + var produced by rememberSaveable { + mutableStateOf(false) + } + + LaunchedEffect(Unit) { + if (!produced) { + ProduceSaveableStateScope(state, coroutineContext).producer() + produced = true + } + } + + return state +} + @Composable fun produceSaveableOneShotState( initialValue: T, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 82ea165..0f88037 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -89,37 +89,6 @@ val DetailedSong.asMediaItem: MediaItem .setCustomCacheKey(id) .build() -fun YouTube.PlaylistOrAlbum.Item.toMediaItem( - albumId: String, - playlistOrAlbum: YouTube.PlaylistOrAlbum -): MediaItem? { - val isFromAlbum = thumbnail == null - - return MediaItem.Builder() - .setMediaMetadata( - MediaMetadata.Builder() - .setTitle(info.name) - .setArtist((authors ?: playlistOrAlbum.authors)?.joinToString("") { it.name }) - .setAlbumTitle(if (isFromAlbum) playlistOrAlbum.title else album?.name) - .setArtworkUri(if (isFromAlbum) playlistOrAlbum.thumbnail?.url?.toUri() else thumbnail?.url?.toUri()) - .setExtras( - bundleOf( - "videoId" to info.endpoint?.videoId, - "playlistId" to info.endpoint?.playlistId, - "albumId" to (if (isFromAlbum) albumId else album?.endpoint?.browseId), - "durationText" to durationText, - "artistNames" to (authors ?: playlistOrAlbum.authors)?.filter { it.endpoint != null }?.map { it.name }, - "artistIds" to (authors ?: playlistOrAlbum.authors)?.mapNotNull { it.endpoint?.browseId } - ) - ) - .build() - ) - .setMediaId(info.endpoint?.videoId ?: return null) - .setUri(info.endpoint?.videoId ?: return null) - .setCustomCacheKey(info.endpoint?.videoId ?: return null) - .build() -} - fun String?.thumbnail(size: Int): String? { return when { this?.startsWith("https://lh3.googleusercontent.com") == true -> "$this-w$size-h$size" diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index 40ec4bf..fdc38e8 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -226,6 +226,49 @@ object YouTube { .thumbnail ) } + + fun from(renderer: MusicResponsiveListItemRenderer): Song? { + return Song( + info = renderer + .flexColumns + .getOrNull(0) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.getOrNull(0) + ?.let { Info.from(it) } ?: return null, + authors = renderer + .flexColumns + .getOrNull(1) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.map { Info.from(it) } + ?.takeIf { it.isNotEmpty() }, + durationText = renderer + .fixedColumns + ?.getOrNull(0) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.getOrNull(0) + ?.text, + album = renderer + .flexColumns + .getOrNull(2) + ?.musicResponsiveListItemFlexColumnRenderer + ?.text + ?.runs + ?.firstOrNull() + ?.let { Info.from(it) }, + thumbnail = renderer + .thumbnail + ?.musicThumbnailRenderer + ?.thumbnail + ?.thumbnails + ?.firstOrNull() + ) + } } } @@ -817,63 +860,10 @@ object YouTube { val authors: List>?, val year: String?, val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?, - val items: List?, + val songs: List?, val url: String?, val continuation: String?, ) { - data class Item( - val info: Info, - val authors: List>?, - val durationText: String?, - val album: Info?, - val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?, - ) { - companion object { - fun from(renderer: MusicResponsiveListItemRenderer): Item? { - return Item( - info = renderer - .flexColumns - .getOrNull(0) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.getOrNull(0) - ?.let { Info.from(it) } ?: return null, - authors = renderer - .flexColumns - .getOrNull(1) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.map { Info.from(it) } - ?.takeIf { it.isNotEmpty() }, - durationText = renderer - .fixedColumns - ?.getOrNull(0) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.getOrNull(0) - ?.text, - album = renderer - .flexColumns - .getOrNull(2) - ?.musicResponsiveListItemFlexColumnRenderer - ?.text - ?.runs - ?.firstOrNull() - ?.let { Info.from(it) }, - thumbnail = renderer - .thumbnail - ?.musicThumbnailRenderer - ?.thumbnail - ?.thumbnails - ?.firstOrNull() - ) - } - } - } - suspend fun next(): PlaylistOrAlbum { return continuation?.let { runCatching { @@ -885,12 +875,12 @@ object YouTube { parameter("continuation", continuation) }.body().let { continuationResponse -> copy( - items = items?.plus(continuationResponse + songs = songs?.plus(continuationResponse .continuationContents .musicShelfContinuation ?.contents ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - ?.mapNotNull(Item.Companion::from) ?: emptyList()), + ?.mapNotNull(Item.Song.Companion::from) ?: emptyList()), continuation = continuationResponse .continuationContents .musicShelfContinuation @@ -909,9 +899,28 @@ object YouTube { return playlistOrAlbum(browseId)?.map { album -> album.url?.let { Url(it).parameters["list"] }?.let { playlistId -> playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist -> - album.copy(items = playlist.items) + album.copy(songs = playlist.songs) } } ?: album + }?.map { album -> + val albumInfo = Info( + name = album.title ?: "", + endpoint = NavigationEndpoint.Endpoint.Browse( + browseId = browseId, + params = null, + browseEndpointContextSupportedConfigs = null + ) + ) + + album.copy( + songs = album.songs?.map { song -> + song.copy( + authors = song.authors ?: album.authors, + album = albumInfo, + thumbnail = album.thumbnail + ) + } + ) } } @@ -950,7 +959,7 @@ object YouTube { ?.getOrNull(2) ?.firstOrNull() ?.text, - items = body + songs = body .contents .singleColumnBrowseResultsRenderer ?.tabs @@ -963,7 +972,7 @@ object YouTube { ?.musicShelfRenderer ?.contents ?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer) - ?.mapNotNull(PlaylistOrAlbum.Item.Companion::from) + ?.mapNotNull(Item.Song.Companion::from) // ?.filter { it.info.endpoint != null } , url = body From 0d3ce95c801c389f21a68f2a7a820d5f466db269 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 27 Sep 2022 17:29:32 +0200 Subject: [PATCH 020/100] Tweak code --- .../it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt | 6 +++--- .../it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index 3f641b5..0f41d76 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -87,7 +87,7 @@ fun AlbumOverview( withContext(Dispatchers.IO) { Database.album(browseId).collect { album -> if (album?.timestamp == null) { - YouTube.album(browseId)?.map { youtubeAlbum -> + YouTube.album(browseId)?.onSuccess { youtubeAlbum -> Database.upsert( Album( id = browseId, @@ -109,8 +109,8 @@ fun AlbumOverview( ) } ?: emptyList() ) - - null + }?.onFailure { throwable -> + value = Result.failure(throwable) } } else { value = Result.success(album) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt index 5815994..708a6b2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Thumbnail.kt @@ -28,7 +28,6 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder -import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness import it.vfsfitvnm.vimusic.service.LoginRequiredException import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException import it.vfsfitvnm.vimusic.service.UnplayableException From acc2768eb44f775311ecad90679443e72d295db1 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 27 Sep 2022 17:51:46 +0200 Subject: [PATCH 021/100] Redesign BuiltInPlaylistScreen (#172) --- .../ui/screens/BuiltInPlaylistScreen.kt | 207 ------------------ .../builtinplaylist/BuiltInPlaylistScreen.kt | 49 +++++ .../builtinplaylist/LocalPlaylistSongList.kt | 162 ++++++++++++++ .../vimusic/ui/screens/home/HomeScreen.kt | 4 +- 4 files changed, 213 insertions(+), 209 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistScreen.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt deleted file mode 100644 index 3493473..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/BuiltInPlaylistScreen.kt +++ /dev/null @@ -1,207 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import androidx.compose.animation.ExperimentalAnimationApi -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.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.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.text.BasicText -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.zIndex -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.models.DetailedSong -import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.Menu -import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry -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.SongItem -import it.vfsfitvnm.vimusic.utils.asMediaItem -import it.vfsfitvnm.vimusic.utils.enqueue -import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex -import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.semiBold -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.map - -@ExperimentalAnimationApi -@Composable -fun BuiltInPlaylistScreen(builtInPlaylist: BuiltInPlaylist) { - val lazyListState = rememberLazyListState() - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val menuState = LocalMenuState.current - - val binder = LocalPlayerServiceBinder.current - val (colorPalette, typography) = LocalAppearance.current - - val thumbnailSize = Dimensions.thumbnails.song.px - - val songs by remember(binder?.cache, builtInPlaylist) { - when (builtInPlaylist) { - BuiltInPlaylist.Favorites -> Database.favorites() - BuiltInPlaylist.Offline -> Database.songsWithContentLength().map { songs -> - songs.filter { song -> - song.contentLength?.let { - binder?.cache?.isCached(song.id, 0, song.contentLength) - } ?: false - } - } - } - }.collectAsState(initial = emptyList(), context = Dispatchers.IO) - - LazyColumn( - state = lazyListState, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp, horizontal = 16.dp) - .size(24.dp) - ) - } - } - - item { - Column( - modifier = Modifier - .padding(top = 16.dp, bottom = 8.dp) - .padding(horizontal = 16.dp) - ) { - BasicText( - text = when (builtInPlaylist) { - BuiltInPlaylist.Favorites -> "Favorites" - BuiltInPlaylist.Offline -> "Offline" - }, - style = typography.m.semiBold - ) - - BasicText( - text = "${songs.size} songs", - style = typography.xxs.semiBold.secondary - ) - } - } - - item { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.End, - modifier = Modifier - .fillMaxWidth() - .zIndex(1f) - .padding(horizontal = 8.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(enabled = songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs - .shuffled() - .map(DetailedSong::asMediaItem) - ) - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - isEnabled = songs.isNotEmpty(), - onClick = { - menuState.hide() - binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) - } - ) - } - } - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) - ) - } - } - - itemsIndexed( - items = songs, - key = { _, song -> song.id }, - contentType = { _, song -> song }, - ) { index, song -> - SongItem( - song = song, - thumbnailSize = thumbnailSize, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - songs.map(DetailedSong::asMediaItem), - index - ) - }, - menuContent = { - when (builtInPlaylist) { - BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu(song = song) - BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(song = song) - } - } - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistScreen.kt new file mode 100644 index 0000000..2a69d59 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistScreen.kt @@ -0,0 +1,49 @@ +package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import it.vfsfitvnm.route.RouteHandler +import it.vfsfitvnm.vimusic.R +import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist +import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold +import it.vfsfitvnm.vimusic.ui.screens.globalRoutes + +@ExperimentalAnimationApi +@Composable +fun BuiltInPlaylistScreen(builtInPlaylist: BuiltInPlaylist) { + val saveableStateHolder = rememberSaveableStateHolder() + + val (tabIndex, onTabIndexChanged) = rememberSaveable { + mutableStateOf(when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> 0 + BuiltInPlaylist.Offline -> 1 + }) + } + + RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + + host { + Scaffold( + topIconButtonId = R.drawable.chevron_back, + onTopIconButtonClick = pop, + tabIndex = tabIndex, + onTabChanged = onTabIndexChanged, + tabColumnContent = { Item -> + Item(0, "Favorites", R.drawable.heart) + Item(1, "Offline", R.drawable.airplane) + } + ) { currentTabIndex -> + saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { + when (currentTabIndex) { + 0 -> BuiltInPlaylistSongList(builtInPlaylist = BuiltInPlaylist.Favorites) + 1 -> BuiltInPlaylistSongList(builtInPlaylist = BuiltInPlaylist.Offline) + } + } + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt new file mode 100644 index 0000000..3d6c6c9 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt @@ -0,0 +1,162 @@ +package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist + +import androidx.compose.animation.ExperimentalAnimationApi +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.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +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.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.dp +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.models.DetailedSong +import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu +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.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.enqueue +import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex +import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning +import it.vfsfitvnm.vimusic.utils.medium +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@ExperimentalAnimationApi +@Composable +fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) { + val (colorPalette, typography) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + + val songs by produceSaveableState( + initialValue = emptyList(), + stateSaver = DetailedSongListSaver + ) { + when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> Database + .favorites() + .flowOn(Dispatchers.IO) + BuiltInPlaylist.Offline -> Database + .songsWithContentLength() + .flowOn(Dispatchers.IO) + .map { songs -> + songs.filter { song -> + song.contentLength?.let { + binder?.cache?.isCached(song.id, 0, song.contentLength) + } ?: false + } + } + }.collect { value = it } + } + + val thumbnailSize = Dimensions.thumbnails.song.px + + Box { + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header( + title = when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> "Favorites" + BuiltInPlaylist.Offline -> "Offline" + } + ) { + BasicText( + text = "Enqueue", + style = typography.xxs.medium, + modifier = Modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = songs.isNotEmpty()) { + binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) + } + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) + + Spacer( + modifier = Modifier + .weight(1f) + ) + } + } + + itemsIndexed( + items = songs, + key = { _, song -> song.id }, + contentType = { _, song -> song }, + ) { index, song -> + SongItem( + song = song, + thumbnailSize = thumbnailSize, + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayAtIndex( + songs.map(DetailedSong::asMediaItem), + index + ) + }, + menuContent = { + when (builtInPlaylist) { + BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu(song = song) + BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(song = song) + } + } + ) + } + } + + Box( + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = songs.isNotEmpty()) { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(songs.shuffled().map(DetailedSong::asMediaItem)) + } + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(R.drawable.shuffle), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 8140ab5..5dacb65 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -11,15 +11,15 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold -import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen -import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute +import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute +import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute import it.vfsfitvnm.vimusic.ui.screens.searchRoute From a600c8b457a3bd408f8ebffb288b272af608354f Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 12:43:57 +0200 Subject: [PATCH 022/100] Rework url management (#172) --- .../it/vfsfitvnm/vimusic/MainActivity.kt | 91 ++++-- .../ui/components/themed/MediaItemMenu.kt | 13 +- .../vimusic/ui/screens/IntentUriScreen.kt | 274 ------------------ .../it/vfsfitvnm/vimusic/ui/screens/Routes.kt | 9 +- .../vimusic/ui/screens/home/HomeScreen.kt | 19 +- .../ui/screens/player/PlayerBottomSheet.kt | 4 +- .../vimusic/ui/screens/player/PlayerView.kt | 9 +- .../ui/screens/playlist/PlaylistScreen.kt | 6 +- .../ui/screens/playlist/PlaylistSongList.kt | 2 - .../ui/screens/search/LocalSongSearch.kt | 4 - .../vimusic/ui/screens/search/OnlineSearch.kt | 22 +- .../vimusic/ui/screens/search/SearchScreen.kt | 28 +- .../it/vfsfitvnm/vimusic/utils/Utils.kt | 4 + .../kotlin/it/vfsfitvnm/route/GlobalRoute.kt | 14 + .../main/kotlin/it/vfsfitvnm/route/Route.kt | 27 +- .../kotlin/it/vfsfitvnm/route/RouteHandler.kt | 16 +- 16 files changed, 142 insertions(+), 400 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt create mode 100644 compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index 67c712b..e7dd936 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -6,10 +6,10 @@ import android.content.Intent import android.content.ServiceConnection import android.content.SharedPreferences import android.graphics.Bitmap -import android.net.Uri import android.os.Build import android.os.Bundle import android.os.IBinder +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi @@ -37,7 +37,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -49,6 +48,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.valentinilk.shimmer.LocalShimmerTheme @@ -63,22 +63,26 @@ import it.vfsfitvnm.vimusic.ui.components.collapsedAnchor import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState -import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen +import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView +import it.vfsfitvnm.vimusic.ui.screens.playlistRoute import it.vfsfitvnm.vimusic.ui.styling.Appearance import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.typographyOf +import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey +import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.getEnum import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.listener import it.vfsfitvnm.vimusic.utils.preferences import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey +import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -102,7 +106,6 @@ class MainActivity : ComponentActivity() { } private var binder by mutableStateOf(null) - private var uri by mutableStateOf(null, neverEqualPolicy()) override fun onStart() { super.onStart() @@ -120,14 +123,13 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) + val playerBottomSheetAnchor = when { intent?.extras?.getBoolean("expandPlayerBottomSheet") == true -> expandedAnchor alreadyRunning -> collapsedAnchor else -> dismissedAnchor.also { alreadyRunning = true } } - uri = intent?.data - setContent { val coroutineScope = rememberCoroutineScope() val isSystemInDarkTheme = isSystemInDarkTheme() @@ -324,30 +326,29 @@ class MainActivity : ComponentActivity() { LocalPlayerServiceBinder provides binder, LocalPlayerAwarePaddingValues provides playerAwarePaddingValues ) { - when (val uri = uri) { - null -> { - HomeScreen() - - PlayerView( - layoutState = playerBottomSheetState, - modifier = Modifier - .align(Alignment.BottomCenter) - ) - - DisposableEffect(binder?.player) { - binder?.player?.listener(object : Player.Listener { - override fun onMediaItemTransition( - mediaItem: MediaItem?, - reason: Int - ) { - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) { - playerBottomSheetState.expand(tween(500)) - } - } - }) ?: onDispose { } - } + HomeScreen( + onPlaylistUrl = { url -> + onNewIntent(Intent.parseUri(url, 0)) } - else -> IntentUriScreen(uri = uri) + ) + + PlayerView( + layoutState = playerBottomSheetState, + modifier = Modifier + .align(Alignment.BottomCenter) + ) + + DisposableEffect(binder?.player) { + binder?.player?.listener(object : Player.Listener { + override fun onMediaItemTransition( + mediaItem: MediaItem?, + reason: Int + ) { + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) { + playerBottomSheetState.expand(tween(500)) + } + } + }) ?: onDispose { } } BottomSheetMenu( @@ -358,11 +359,41 @@ class MainActivity : ComponentActivity() { } } } + + onNewIntent(intent) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - uri = intent?.data + + val uri = intent?.data ?: return + + intent.data = null + this.intent = null + + Toast.makeText(this, "Opening url...", Toast.LENGTH_SHORT).show() + + lifecycleScope.launch(Dispatchers.IO) { + uri.getQueryParameter("list")?.let { playlistId -> + val browseId = "VL$playlistId" + + if (playlistId.startsWith("OLAK5uy_")) { + YouTube.playlist(browseId)?.getOrNull()?.let { playlist -> + playlist.songs?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId -> + albumRoute.ensureGlobal(browseId) + } + } + } else { + playlistRoute.ensureGlobal(browseId) + } + } ?: (uri.getQueryParameter("v") ?: uri.takeIf { uri.host == "youtu.be" }?.path?.drop(1))?.let { videoId -> + YouTube.song(videoId)?.getOrNull()?.let { song -> + withContext(Dispatchers.Main) { + binder?.player?.forcePlay(song.asMediaItem) + } + } + } + } } private fun setSystemBarAppearance(isDark: Boolean) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index b10c0ce..6b9e7cf 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -181,8 +181,7 @@ fun QueuedMediaItemMenu( mediaItem: MediaItem, indexInQueue: Int?, modifier: Modifier = Modifier, - onDismiss: (() -> Unit)? = null, - onGlobalRouteEmitted: (() -> Unit)? = null + onDismiss: (() -> Unit)? = null ) { val menuState = LocalMenuState.current val binder = LocalPlayerServiceBinder.current @@ -193,7 +192,6 @@ fun QueuedMediaItemMenu( onRemoveFromQueue = if (indexInQueue != null) ({ binder?.player?.removeMediaItem(indexInQueue) }) else null, - onGlobalRouteEmitted = onGlobalRouteEmitted, modifier = modifier ) } @@ -212,8 +210,7 @@ fun BaseMediaItemMenu( onRemoveFromQueue: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null, - onRemoveFromFavorites: (() -> Unit)? = null, - onGlobalRouteEmitted: (() -> Unit)? = null, + onRemoveFromFavorites: (() -> Unit)? = null ) { val context = LocalContext.current @@ -246,7 +243,6 @@ fun BaseMediaItemMenu( onShare = { context.shareAsYouTubeSong(mediaItem) }, - onGlobalRouteEmitted = onGlobalRouteEmitted, modifier = modifier ) } @@ -269,8 +265,7 @@ fun MediaItemMenu( onAddToPlaylist: ((Playlist, Int) -> Unit)? = null, onGoToAlbum: ((String) -> Unit)? = null, onGoToArtist: ((String) -> Unit)? = null, - onShare: (() -> Unit)? = null, - onGlobalRouteEmitted: (() -> Unit)? = null, + onShare: (() -> Unit)? = null ) { Menu(modifier = modifier) { RouteHandler( @@ -566,7 +561,6 @@ fun MediaItemMenu( text = "Go to album", onClick = { onDismiss() - onGlobalRouteEmitted?.invoke() onGoToAlbum(albumId) } ) @@ -586,7 +580,6 @@ fun MediaItemMenu( text = "More of $authorName", onClick = { onDismiss() - onGlobalRouteEmitted?.invoke() onGoToArtist(authorId) } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt deleted file mode 100644 index 27ba93f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ /dev/null @@ -1,274 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import android.net.Uri -import androidx.compose.animation.ExperimentalAnimationApi -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.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.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -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.alpha -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -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.models.Playlist -import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.transaction -import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError -import it.vfsfitvnm.vimusic.ui.components.themed.Menu -import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry -import it.vfsfitvnm.vimusic.ui.components.themed.TextCard -import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog -import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen -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.SmallSongItem -import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer -import it.vfsfitvnm.vimusic.utils.asMediaItem -import it.vfsfitvnm.vimusic.utils.enqueue -import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex -import it.vfsfitvnm.vimusic.utils.relaunchableEffect -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@ExperimentalFoundationApi -@ExperimentalAnimationApi -@Composable -fun IntentUriScreen(uri: Uri) { - - val lazyListState = rememberLazyListState() - - var itemsResult by remember(uri) { - mutableStateOf>?>(null) - } - - var playlistBrowseId by rememberSaveable { - mutableStateOf(null) - } - - val onLoad = relaunchableEffect(uri) { - withContext(Dispatchers.IO) { - itemsResult = uri.getQueryParameter("list")?.let { playlistId -> - if (playlistId.startsWith("OLAK5uy_")) { - YouTube.queue(playlistId)?.map { songList -> - songList ?: emptyList() - } - } else { - playlistBrowseId = "VL$playlistId" - null - } - } ?: uri.getQueryParameter("v")?.let { videoId -> - YouTube.song(videoId)?.map { song -> - song?.let { listOf(song) } ?: emptyList() - } - } ?: uri.takeIf { - uri.host == "youtu.be" - }?.path?.drop(1)?.let { videoId -> - YouTube.song(videoId)?.map { song -> - song?.let { listOf(song) } ?: emptyList() - } - } ?: Result.failure(Error("Missing URL parameters")) - } - } - - playlistBrowseId?.let { browseId -> - PlaylistScreen(browseId = browseId) - return - } - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val menuState = LocalMenuState.current - val (colorPalette) = LocalAppearance.current - val binder = LocalPlayerServiceBinder.current - - val thumbnailSizePx = Dimensions.thumbnails.song.px - - var isImportingAsPlaylist by remember(uri) { - mutableStateOf(false) - } - - - if (isImportingAsPlaylist) { - TextFieldDialog( - hintText = "Enter the playlist name", - onDismiss = { - isImportingAsPlaylist = false - }, - onDone = { text -> - menuState.hide() - - transaction { - val playlistId = Database.insert(Playlist(name = text)) - - itemsResult - ?.getOrNull() - ?.map(YouTube.Item.Song::asMediaItem) - ?.forEachIndexed { index, mediaItem -> - Database.insert(mediaItem) - - Database.insert( - SongPlaylistMap( - songId = mediaItem.mediaId, - playlistId = playlistId, - position = index - ) - ) - } - } - } - ) - } - - LazyColumn( - state = lazyListState, - horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - onClick = { - menuState.hide() - - itemsResult - ?.getOrNull() - ?.map(YouTube.Item.Song::asMediaItem) - ?.let { mediaItems -> - binder?.player?.enqueue( - mediaItems - ) - } - } - ) - - MenuEntry( - icon = R.drawable.playlist, - text = "Import as playlist", - onClick = { - isImportingAsPlaylist = true - } - ) - } - } - } - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - } - - itemsResult?.getOrNull()?.let { items -> - if (items.isEmpty()) { - item { - TextCard(icon = R.drawable.sad) { - Title(text = "No songs found") - Text(text = "Please try a different query or category.") - } - } - } else { - itemsIndexed( - items = items, - contentType = { _, item -> item } - ) { index, item -> - SmallSongItem( - song = item, - thumbnailSizePx = thumbnailSizePx, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - items.map(YouTube.Item.Song::asMediaItem), - index - ) - } - ) - } - } - } ?: itemsResult?.exceptionOrNull()?.let { throwable -> - item { - LoadingOrError( - errorMessage = throwable.javaClass.canonicalName, - onRetry = onLoad - ) - } - } ?: item { - LoadingOrError() - } - } - } - } -} - -@Composable -private fun LoadingOrError( - errorMessage: String? = null, - onRetry: (() -> Unit)? = null -) { - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry - ) { - repeat(5) { index -> - SmallSongItemShimmer( - thumbnailSizeDp = Dimensions.thumbnails.song, - modifier = Modifier - .alpha(1f - index * 0.175f) - .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 16.dp) - ) - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index 8f8e16f..264f28b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -1,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens import android.annotation.SuppressLint -import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import it.vfsfitvnm.route.Route0 @@ -10,11 +9,11 @@ import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen +import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen val albumRoute = Route1("albumRoute") val artistRoute = Route1("artistRoute") val builtInPlaylistRoute = Route1("builtInPlaylistRoute") -val intentUriRoute = Route1("intentUriRoute") val localPlaylistRoute = Route1("localPlaylistRoute") val playlistRoute = Route1("playlistRoute") val searchResultRoute = Route1("searchResultRoute") @@ -38,4 +37,10 @@ inline fun RouteHandlerScope.globalRoutes() { browseId = browseId ?: error("browseId cannot be null") ) } + + playlistRoute { browseId -> + PlaylistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 5dacb65..cbb720f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -1,6 +1,5 @@ package it.vfsfitvnm.vimusic.ui.screens.home -import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable @@ -11,13 +10,11 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold -import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen @@ -32,10 +29,12 @@ import it.vfsfitvnm.vimusic.utils.rememberPreference @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun HomeScreen() { +fun HomeScreen(onPlaylistUrl: (String) -> Unit) { val saveableStateHolder = rememberSaveableStateHolder() RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + settingsRoute { SettingsScreen() } @@ -71,17 +70,7 @@ fun HomeScreen() { Database.insert(SearchQuery(query = query)) } }, - onUri = { uri -> - intentUriRoute(uri) - } - ) - } - - globalRoutes() - - intentUriRoute { uri -> - IntentUriScreen( - uri = uri ?: Uri.EMPTY + onViewPlaylist = onPlaylistUrl ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index c9c9150..ba63067 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -72,7 +72,6 @@ import kotlinx.coroutines.launch fun PlayerBottomSheet( backgroundColorProvider: () -> Color, layoutState: BottomSheetState, - onGlobalRouteEmitted: () -> Unit, modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { @@ -166,8 +165,7 @@ fun PlayerBottomSheet( menuContent = { QueuedMediaItemMenu( mediaItem = window.mediaItem, - indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex, - onGlobalRouteEmitted = onGlobalRouteEmitted + indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex ) }, onThumbnailContent = { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt index 7c73bff..83dc4d3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.media3.common.Player import coil.compose.AsyncImage +import it.vfsfitvnm.route.OnGlobalRoute import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.BottomSheet @@ -95,6 +96,10 @@ fun PlayerView( val shouldBePlaying by rememberShouldBePlaying(binder.player) val positionAndDuration by rememberPositionAndDuration(binder.player) + OnGlobalRoute { + layoutState.collapseSoft() + } + BottomSheet( state = layoutState, modifier = modifier, @@ -321,7 +326,6 @@ fun PlayerView( PlayerBottomSheet( layoutState = playerBottomSheetState, - onGlobalRouteEmitted = layoutState::collapseSoft, content = { Row( verticalAlignment = Alignment.CenterVertically, @@ -385,8 +389,7 @@ fun PlayerView( } }, onSetSleepTimer = {}, - onDismiss = menuState::hide, - onGlobalRouteEmitted = layoutState::collapseSoft, + onDismiss = menuState::hide ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt index 0e53acb..4f8720d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt @@ -1,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens.playlist import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import it.vfsfitvnm.route.RouteHandler @@ -9,7 +8,6 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -@ExperimentalFoundationApi @ExperimentalAnimationApi @Composable fun PlaylistScreen(browseId: String) { @@ -29,9 +27,7 @@ fun PlaylistScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - PlaylistSongList( - browseId = browseId - ) + PlaylistSongList(browseId = browseId) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index db94305..b663834 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -2,7 +2,6 @@ package it.vfsfitvnm.vimusic.ui.screens.playlist import android.content.Intent import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -68,7 +67,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext @ExperimentalAnimationApi -@ExperimentalFoundationApi @Composable fun PlaylistSongList( browseId: String, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 200c7ec..9405e33 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -47,10 +47,6 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn -//context(ProduceStateScope) -//fun Flow.distinctUntilChangedWithProducedState() = -// distinctUntilChanged { old, new -> new != old && new != value } - @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 35b091a..382caff 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R @@ -65,9 +66,8 @@ import kotlinx.coroutines.flow.flowOn fun OnlineSearch( textFieldValue: TextFieldValue, onTextFieldValueChanged: (TextFieldValue) -> Unit, - isOpenableUrl: Boolean, onSearch: (String) -> Unit, - onUri: () -> Unit + onViewPlaylist: (String) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current @@ -92,6 +92,16 @@ fun OnlineSearch( } } + val playlistId = remember(textFieldValue.text) { + val isPlaylistUrl = listOf( + "https://www.youtube.com/playlist?", + "https://music.youtube.com/playlist?", + "https://m.youtube.com/playlist?", + ).any(textFieldValue.text::startsWith) + + if (isPlaylistUrl) textFieldValue.text.toUri().getQueryParameter("list") else null + } + val timeIconPainter = painterResource(R.drawable.time) val closeIconPainter = painterResource(R.drawable.close) val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) @@ -156,13 +166,15 @@ fun OnlineSearch( ) }, actionsContent = { - if (isOpenableUrl) { + if (playlistId != null) { + val isAlbum = playlistId.startsWith("OLAK5uy_") + BasicText( - text = "Open url", + text = "View ${if (isAlbum) "album" else "playlist"}", style = typography.xxs.medium, modifier = Modifier .clip(RoundedCornerShape(16.dp)) - .clickable(onClick = onUri) + .clickable { onViewPlaylist(textFieldValue.text) } .background(colorPalette.background2) .padding(all = 8.dp) .padding(horizontal = 8.dp) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt index c5b025b..a6f65fd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt @@ -1,16 +1,13 @@ package it.vfsfitvnm.vimusic.ui.screens.search -import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import androidx.core.net.toUri import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold @@ -19,7 +16,11 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) { +fun SearchScreen( + initialTextInput: String, + onSearch: (String) -> Unit, + onViewPlaylist: (String) -> Unit +) { val saveableStateHolder = rememberSaveableStateHolder() val (tabIndex, onTabChanged) = rememberSaveable { @@ -42,18 +43,6 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U globalRoutes() host { - val isOpenableUrl = remember(textFieldValue.text) { - listOf( - "https://www.youtube.com/watch?", - "https://music.youtube.com/watch?", - "https://m.youtube.com/watch?", - "https://www.youtube.com/playlist?", - "https://music.youtube.com/playlist?", - "https://m.youtube.com/playlist?", - "https://youtu.be/", - ).any(textFieldValue.text::startsWith) - } - Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, @@ -69,13 +58,8 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U 0 -> OnlineSearch( textFieldValue = textFieldValue, onTextFieldValueChanged = onTextFieldValueChanged, - isOpenableUrl = isOpenableUrl, onSearch = onSearch, - onUri = { - if (isOpenableUrl) { - onUri(textFieldValue.text.toUri()) - } - } + onViewPlaylist = onViewPlaylist ) 1 -> LocalSongSearch( textFieldValue = textFieldValue, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 0f88037..933d87e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -22,6 +22,10 @@ fun Context.shareAsYouTubeSong(mediaItem: MediaItem) { val YouTube.Item.Song.asMediaItem: MediaItem get() = MediaItem.Builder() + .also { +// println("$this") +// println(info.endpoint?.videoId) + } .setMediaId(info.endpoint!!.videoId!!) .setUri(info.endpoint!!.videoId) .setCustomCacheKey(info.endpoint!!.videoId) diff --git a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt new file mode 100644 index 0000000..00a17af --- /dev/null +++ b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt @@ -0,0 +1,14 @@ +package it.vfsfitvnm.route + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.MutableSharedFlow + +internal val globalRouteFlow = MutableSharedFlow>>(extraBufferCapacity = 1) + +@Composable +fun OnGlobalRoute(block: suspend (Pair>) -> Unit) { + LaunchedEffect(Unit) { + globalRouteFlow.collect(block) + } +} diff --git a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt index 9fe83b2..992286d 100644 --- a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt +++ b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt @@ -4,10 +4,9 @@ package it.vfsfitvnm.route import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.SaverScope -import androidx.compose.runtime.saveable.rememberSaveable +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first @Immutable open class Route internal constructor(val tag: String) { @@ -23,23 +22,12 @@ open class Route internal constructor(val tag: String) { return tag.hashCode() } - object GlobalEmitter { - var listener: ((Route, Array) -> Unit)? = null - } - object Saver : androidx.compose.runtime.saveable.Saver { override fun restore(value: String): Route? = value.takeIf(String::isNotEmpty)?.let(::Route) override fun SaverScope.save(value: Route?): String = value?.tag ?: "" } } -@Composable -fun rememberRoute(route: Route? = null): MutableState { - return rememberSaveable(stateSaver = Route.Saver) { - mutableStateOf(route) - } -} - @Immutable class Route0(tag: String) : Route(tag) { context(RouteHandlerScope) @@ -51,7 +39,7 @@ class Route0(tag: String) : Route(tag) { } fun global() { - GlobalEmitter.listener?.invoke(this, emptyArray()) + globalRouteFlow.tryEmit(this to emptyArray()) } } @@ -66,7 +54,12 @@ class Route1(tag: String) : Route(tag) { } fun global(p0: P0) { - GlobalEmitter.listener?.invoke(this, arrayOf(p0)) + globalRouteFlow.tryEmit(this to arrayOf(p0)) + } + + suspend fun ensureGlobal(p0: P0) { + globalRouteFlow.subscriptionCount.filter { it > 0 }.first() + globalRouteFlow.emit(this to arrayOf(p0)) } } @@ -81,6 +74,6 @@ class Route2(tag: String) : Route(tag) { } fun global(p0: P0, p1: P1) { - GlobalEmitter.listener?.invoke(this, arrayOf(p0, p1)) + globalRouteFlow.tryEmit(this to arrayOf(p0, p1)) } } diff --git a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt index d7b432c..a71b1ee 100644 --- a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt +++ b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt @@ -8,8 +8,8 @@ import androidx.compose.animation.ContentTransform import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.updateTransition import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect 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 @@ -24,7 +24,9 @@ fun RouteHandler( transitionSpec: AnimatedContentScope.() -> ContentTransform = { fastFade }, content: @Composable RouteHandlerScope.() -> Unit ) { - var route by rememberRoute() + var route by rememberSaveable(stateSaver = Route.Saver) { + mutableStateOf(null) + } RouteHandler( route = route, @@ -63,12 +65,10 @@ fun RouteHandler( ) } - if (listenToGlobalEmitter) { - LaunchedEffect(route) { - Route.GlobalEmitter.listener = if (route == null) ({ newRoute, newParameters -> - newParameters.forEachIndexed(parameters::set) - onRouteChanged(newRoute) - }) else null + if (listenToGlobalEmitter && route == null) { + OnGlobalRoute { (newRoute, newParameters) -> + newParameters.forEachIndexed(parameters::set) + onRouteChanged(newRoute) } } From 6a69eb57e990103dc6fb7b2ee78c3c7f2994c4bf Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 14:40:54 +0200 Subject: [PATCH 023/100] Remove unused composables --- .../vfsfitvnm/vimusic/ui/components/Badge.kt | 22 ---- .../vimusic/ui/components/ChunkyChipGroup.kt | 50 -------- .../vimusic/ui/components/TopAppBar.kt | 23 ---- .../ui/components/themed/LoadingOrError.kt | 41 ------- .../vimusic/ui/components/themed/TextCard.kt | 113 ------------------ .../vimusic/ui/screens/album/AlbumOverview.kt | 2 +- .../vimusic/ui/screens/artist/ArtistScreen.kt | 79 ++---------- .../ui/screens/searchresult/SearchResult.kt | 65 +++++++--- .../vimusic/ui/views/YouTubeItems.kt | 42 ------- 9 files changed, 62 insertions(+), 375 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/Badge.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ChunkyChipGroup.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TopAppBar.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LoadingOrError.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/TextCard.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/Badge.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/Badge.kt deleted file mode 100644 index 0e6d10d..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/Badge.kt +++ /dev/null @@ -1,22 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components - -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp - -fun Modifier.badge(color: Color, isDisplayed: Boolean = true, radius: Dp = 4.dp) = - if (isDisplayed) { - drawWithContent { - drawContent() - drawCircle( - color = color, - center = Offset(x = size.width, y = 0.dp.toPx()), - radius = radius.toPx() - ) - } - } else { - this - } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ChunkyChipGroup.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ChunkyChipGroup.kt deleted file mode 100644 index 9eafc96..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/ChunkyChipGroup.kt +++ /dev/null @@ -1,50 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components - -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.dp - -@Composable -fun ChipGroup( - items: List>, - value: T, - selectedBackgroundColor: Color, - unselectedBackgroundColor: Color, - selectedTextStyle: TextStyle, - unselectedTextStyle: TextStyle, - modifier: Modifier = Modifier, - shape: Shape = RoundedCornerShape(16.dp), - onValueChanged: (T) -> Unit -) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier - .horizontalScroll(rememberScrollState()) - .then(modifier) - ) { - items.forEach { chipItem -> - ChunkyButton( - text = chipItem.text, - textStyle = if (chipItem.value == value) selectedTextStyle else unselectedTextStyle, - backgroundColor = if (chipItem.value == value) selectedBackgroundColor else unselectedBackgroundColor, - shape = shape, - onClick = { - onValueChanged(chipItem.value) - } - ) - } - } -} - -data class ChipItem( - val text: String, - val value: T -) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TopAppBar.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TopAppBar.kt deleted file mode 100644 index 238d304..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TopAppBar.kt +++ /dev/null @@ -1,23 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier - -@Composable -inline fun TopAppBar( - modifier: Modifier = Modifier, - content: @Composable RowScope.() -> Unit -) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - modifier = modifier - .fillMaxWidth(), - content = content - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LoadingOrError.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LoadingOrError.kt deleted file mode 100644 index d81efa8..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LoadingOrError.kt +++ /dev/null @@ -1,41 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components.themed - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import com.valentinilk.shimmer.shimmer -import it.vfsfitvnm.vimusic.R - -@Composable -fun LoadingOrError( - errorMessage: String? = null, - onRetry: (() -> Unit)? = null, - horizontalAlignment: Alignment.Horizontal = Alignment.Start, - loadingContent: @Composable ColumnScope.() -> Unit -) { - Box { - Column( - horizontalAlignment = horizontalAlignment, - modifier = Modifier - .alpha(if (errorMessage == null) 1f else 0f) - .shimmer(), - content = loadingContent - ) - - errorMessage?.let { - TextCard( - icon = R.drawable.alert_circle, - onClick = onRetry, - modifier = Modifier - .align(Alignment.Center) - ) { - Title(text = onRetry?.let { "Tap to retry" } ?: "Error") - Text(text = "An error has occurred:\n$errorMessage") - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/TextCard.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/TextCard.kt deleted file mode 100644 index d87311a..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/TextCard.kt +++ /dev/null @@ -1,113 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components.themed - -import androidx.annotation.DrawableRes -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.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.BasicText -import androidx.compose.material.ripple.rememberRipple -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.align -import it.vfsfitvnm.vimusic.utils.secondary -import it.vfsfitvnm.vimusic.utils.semiBold - -@Composable -fun TextCard( - modifier: Modifier = Modifier, - @DrawableRes icon: Int? = null, - iconColor: ColorFilter? = null, - onClick: (() -> Unit)? = null, - content: @Composable TextCardScope.() -> Unit, -) { - val (colorPalette) = LocalAppearance.current - - Column( - modifier = modifier - .padding(horizontal = 16.dp, vertical = 16.dp) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = rememberRipple(bounded = true), - enabled = onClick != null, - onClick = onClick ?: {} - ) - .background(colorPalette.background1) - .padding(horizontal = 16.dp, vertical = 16.dp) - ) { - icon?.let { - Image( - painter = painterResource(icon), - contentDescription = null, - colorFilter = iconColor ?: ColorFilter.tint(Color.Red), - modifier = Modifier - .padding(bottom = 16.dp) - .size(24.dp) - ) - } - - (icon?.let { IconTextCardScopeImpl } ?: TextCardScopeImpl).content() - } -} - -interface TextCardScope { - @Composable - fun Title(text: String) - - @Composable - fun Text(text: String) -} - -private object TextCardScopeImpl : TextCardScope { - @Composable - override fun Title(text: String) { - val (_, typography) = LocalAppearance.current - BasicText( - text = text, - style = typography.xxs.semiBold, - ) - } - - @Composable - override fun Text(text: String) { - val (_, typography) = LocalAppearance.current - BasicText( - text = text, - style = typography.xxs.secondary.align(TextAlign.Justify), - ) - } -} - -private object IconTextCardScopeImpl : TextCardScope { - @Composable - override fun Title(text: String) { - val (_, typography) = LocalAppearance.current - BasicText( - text = text, - style = typography.xxs.semiBold, - modifier = Modifier - .padding(horizontal = 16.dp) - ) - } - - @Composable - override fun Text(text: String) { - val (_, typography) = LocalAppearance.current - BasicText( - text = text, - style = typography.xxs.secondary, - modifier = Modifier - .padding(horizontal = 16.dp) - ) - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index 0f41d76..21a8215 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -299,7 +299,7 @@ fun AlbumOverview( .fillMaxSize() ) { BasicText( - text = "An error has occurred.\nTap to retry", + text = "An error has occurred.", style = typography.s.medium.secondary.center, modifier = Modifier .align(Alignment.Center) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index ca61383..d9bf3e0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -10,7 +10,6 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -32,7 +31,6 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.center import androidx.compose.ui.graphics.ColorFilter @@ -50,17 +48,12 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.query -import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold -import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder -import it.vfsfitvnm.vimusic.ui.screens.album.AlbumOverview import it.vfsfitvnm.vimusic.ui.screens.globalRoutes 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.styling.shimmer import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex @@ -73,7 +66,6 @@ import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.runBlocking @@ -147,21 +139,7 @@ fun ArtistScreen2(browseId: String) { .fillMaxSize() ) { item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - } + } item { @@ -234,17 +212,17 @@ fun ArtistScreen2(browseId: String) { ) } } ?: artistResult?.exceptionOrNull()?.let { throwable -> - LoadingOrError( - errorMessage = throwable.javaClass.canonicalName, - onRetry = { - query { - runBlocking { - Database.artist(browseId).first()?.let(Database::update) - } - } - } - ) - } ?: LoadingOrError() +// LoadingOrError( +// errorMessage = throwable.javaClass.canonicalName, +// onRetry = { +// query { +// runBlocking { +// Database.artist(browseId).first()?.let(Database::update) +// } +// } +// } +// ) + } } item("songs") { @@ -367,39 +345,6 @@ fun ArtistScreen2(browseId: String) { } } -@Composable -private fun LoadingOrError( - errorMessage: String? = null, - onRetry: (() -> Unit)? = null -) { - val (colorPalette) = LocalAppearance.current - - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer( - modifier = Modifier - .background(color = colorPalette.shimmer, shape = CircleShape) - .size(Dimensions.thumbnails.artist) - ) - - TextPlaceholder( - modifier = Modifier - .alpha(0.9f) - .padding(vertical = 8.dp, horizontal = 16.dp) - ) - - repeat(3) { - TextPlaceholder( - modifier = Modifier - .alpha(0.8f) - .padding(horizontal = 16.dp) - ) - } - } -} private suspend fun fetchArtist(browseId: String): Result? { return YouTube.artist(browseId) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index 9c039c7..5cf6c1d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -2,27 +2,32 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.alpha import androidx.compose.ui.input.pointer.pointerInput import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.savers.ListSaver import it.vfsfitvnm.vimusic.savers.StringResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.components.themed.TextCard -import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.center +import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableRelaunchableOneShotState +import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -37,6 +42,8 @@ inline fun SearchResult( crossinline itemContent: @Composable LazyItemScope.(T) -> Unit, noinline itemShimmer: @Composable BoxScope.() -> Unit, ) { + val (_, typography) = LocalAppearance.current + var items by rememberSaveable(query, filter, stateSaver = stateSaver) { mutableStateOf(listOf()) } @@ -93,28 +100,54 @@ inline fun SearchResult( SideEffect(fetch) } } - } ?: continuationResult?.exceptionOrNull()?.let { throwable -> + } ?: continuationResult?.exceptionOrNull()?.let { item { - SearchResultLoadingOrError( - errorMessage = throwable.javaClass.canonicalName, - onRetry = fetch, - shimmerContent = {} - ) + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + fetch() + } + } + .fillMaxSize() + ) { + BasicText( + text = "An error has occurred.\nTap to retry", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) + } } } ?: continuationResult?.let { if (items.isEmpty()) { item { - TextCard(icon = R.drawable.sad) { - Title(text = "No results found") - Text(text = "Please try a different query or category.") + Box( + modifier = Modifier + .pointerInput(Unit) { + detectTapGestures { + fetch() + } + } + .fillMaxSize() + ) { + BasicText( + text = "No results found.\nPlease try a different query or category", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) } } } } ?: item(key = "loading") { - SearchResultLoadingOrError( - itemCount = if (items.isEmpty()) 8 else 3, - shimmerContent = itemShimmer - ) + repeat(if (items.isEmpty()) 8 else 3) { index -> + Box( + modifier = Modifier + .alpha(1f - index * 0.125f), + content = itemShimmer + ) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index f6f2126..d62ad6c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource 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.Row import androidx.compose.foundation.layout.Spacer @@ -22,7 +21,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.style.TextOverflow @@ -30,7 +28,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions @@ -458,42 +455,3 @@ fun ArtistItemShimmer( } } } - -@Composable -fun SearchResultLoadingOrError( - itemCount: Int = 0, - errorMessage: String? = null, - onRetry: (() -> Unit)? = null, - shimmerContent: @Composable BoxScope.() -> Unit, -) { - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry, - horizontalAlignment = Alignment.CenterHorizontally - ) { - repeat(itemCount) { index -> - Box( - modifier = Modifier - .alpha(1f - index * 0.125f), - content = shimmerContent - ) -// if (isLoadingArtists) { -// SmallArtistItemShimmer( -// thumbnailSizeDp = Dimensions.thumbnails.song, -// modifier = Modifier -// .alpha(1f - index * 0.125f) -// .fillMaxWidth() -// .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) -// ) -// } else { -// SmallSongItemShimmer( -// thumbnailSizeDp = Dimensions.thumbnails.song, -// modifier = Modifier -// .alpha(1f - index * 0.125f) -// .fillMaxWidth() -// .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) -// ) -// } - } - } -} From 752b29c93acd8081c8cd0d2bb97fc119b02c2e27 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 15:43:42 +0200 Subject: [PATCH 024/100] Tweak code --- .../vimusic/ui/components/TabColumn.kt | 105 -------------- .../vimusic/ui/components/themed/Header.kt | 3 +- .../ui/components/themed/NavigationRail.kt | 134 ++++++++++++++++++ .../vimusic/ui/components/themed/Scaffold.kt | 11 +- .../ui/components/themed/VerticalBar.kt | 66 --------- .../vimusic/ui/screens/album/AlbumOverview.kt | 2 +- .../vimusic/ui/screens/home/HomeSongList.kt | 2 +- .../ui/screens/playlist/PlaylistSongList.kt | 2 +- .../vimusic/ui/screens/search/OnlineSearch.kt | 14 +- .../ui/screens/searchresult/SearchResult.kt | 19 ++- .../vimusic/ui/styling/Dimensions.kt | 4 +- 11 files changed, 173 insertions(+), 189 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt deleted file mode 100644 index 60170e8..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/TabColumn.kt +++ /dev/null @@ -1,105 +0,0 @@ -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) - ) - } - } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt index c174126..0f4a05c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.shimmer import it.vfsfitvnm.vimusic.utils.medium @@ -55,7 +56,7 @@ fun Header( horizontalAlignment = Alignment.End, modifier = modifier .padding(horizontal = 16.dp) - .height(128.dp) + .height(Dimensions.headerHeight) .fillMaxWidth() ) { Spacer( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt new file mode 100644 index 0000000..eb46bbf --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt @@ -0,0 +1,134 @@ +package it.vfsfitvnm.vimusic.ui.components.themed + +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.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +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.ColorFilter +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.layout +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import it.vfsfitvnm.vimusic.ui.styling.Dimensions +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.semiBold + +@Composable +fun NavigationRail( + topIconButtonId: Int, + onTopIconButtonClick: () -> Unit, + tabIndex: Int, + onTabIndexChanged: (Int) -> Unit, + content: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit, + modifier: Modifier = Modifier +) { + val (colorPalette, typography) = LocalAppearance.current + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier + ) { + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier + .size(width = Dimensions.navigationRailWidth, height = Dimensions.headerHeight) + ) { + Image( + painter = painterResource(topIconButtonId), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.textSecondary), + modifier = Modifier + .offset(x = Dimensions.navigationRailIconOffset, y = 48.dp) + .clip(CircleShape) + .clickable(onClick = onTopIconButtonClick) + .padding(all = 12.dp) + .size(22.dp) + ) + } + + 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) colorPalette.text else colorPalette.textDisabled + } + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clip(RoundedCornerShape(24.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(colorPalette.text), + modifier = Modifier + .vertical() + .graphicsLayer { + alpha = dothAlpha + translationX = (1f - dothAlpha) * -48.dp.toPx() + rotationZ = -90f + } + .size(Dimensions.navigationRailIconOffset * 2) + ) + + BasicText( + text = text, + style = typography.xs.semiBold.copy(color = textColor), + modifier = Modifier + .vertical() + .rotate(-90f) + .padding(horizontal = 16.dp) + ) + } + } + } + } +} + +private 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) + ) + } + } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt index 8ef5ea0..29b46ea 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -13,6 +13,7 @@ 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.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding @@ -37,7 +38,7 @@ fun Scaffold( onTopIconButtonClick: () -> Unit, tabIndex: Int, onTabChanged: (Int) -> Unit, - tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit, + tabColumnContent: @Composable ColumnScope.(@Composable (Int, String, Int) -> Unit) -> Unit, primaryIconButtonId: Int? = null, onPrimaryIconButtonClick: () -> Unit = {}, modifier: Modifier = Modifier, @@ -54,14 +55,14 @@ fun Scaffold( modifier = modifier .fillMaxSize() ) { - VerticalBar( + NavigationRail( topIconButtonId = topIconButtonId, onTopIconButtonClick = onTopIconButtonClick, tabIndex = tabIndex, - onTabChanged = onTabChanged, - tabColumnContent = tabColumnContent, + onTabIndexChanged = onTabChanged, modifier = Modifier - .padding(LocalPlayerAwarePaddingValues.current) + .padding(LocalPlayerAwarePaddingValues.current), + content = tabColumnContent ) AnimatedContent( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt deleted file mode 100644 index ef9e254..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/VerticalBar.kt +++ /dev/null @@ -1,66 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.components.themed - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -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.shape.CircleShape -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.dp -import it.vfsfitvnm.vimusic.ui.components.TabColumn -import it.vfsfitvnm.vimusic.ui.styling.Dimensions -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.utils.semiBold - -@Composable -fun VerticalBar( - topIconButtonId: Int, - onTopIconButtonClick: () -> Unit, - tabIndex: Int, - onTabChanged: (Int) -> Unit, - tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit, - modifier: Modifier = Modifier -) { - val (colorPalette, typography) = LocalAppearance.current - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = modifier - .padding(vertical = 16.dp) - ) { - Image( - painter = painterResource(topIconButtonId), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.textSecondary), - modifier = Modifier - .clip(CircleShape) - .clickable(onClick = onTopIconButtonClick) - .padding(all = 12.dp) - .size(22.dp) - ) - - Spacer( - modifier = Modifier - .width(Dimensions.verticalBarWidth) - .height(32.dp) - ) - - TabColumn( - tabIndex = tabIndex, - onTabIndexChanged = onTabChanged, - selectedTextColor = colorPalette.text, - disabledTextColor = colorPalette.textDisabled, - textStyle = typography.xs.semiBold, - content = tabColumnContent, - ) - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index 21a8215..dc40c54 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -130,7 +130,7 @@ fun AlbumOverview( } BoxWithConstraints { - val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth + val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px albumResult?.getOrNull()?.let { album -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index 9a1c867..142065a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -210,7 +210,7 @@ fun HomeSongList() { ScrollToTop( lazyListState = lazyListState, modifier = Modifier - .offset(x = -Dimensions.verticalBarWidth) + .offset(x = Dimensions.navigationRailIconOffset - Dimensions.navigationRailWidth) .align(Alignment.BottomStart) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index b663834..6c69858 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -99,7 +99,7 @@ fun PlaylistSongList( } BoxWithConstraints { - val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth + val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px val songThumbnailSizeDp = Dimensions.thumbnails.song diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 382caff..274b2be 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -49,9 +49,9 @@ import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver import it.vfsfitvnm.vimusic.savers.StringListResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.align +import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableState @@ -320,7 +320,17 @@ fun OnlineSearch( } } ?: suggestionsResult?.exceptionOrNull()?.let { throwable -> item { - LoadingOrError(errorMessage = throwable.javaClass.canonicalName) {} + Box( + modifier = Modifier + .fillMaxSize() + ) { + BasicText( + text = "An error has occurred.", + style = typography.s.medium.secondary.center, + modifier = Modifier + .align(Alignment.Center) + ) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index 5cf6c1d..a291171 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.gestures.detectTapGestures 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.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyItemScope @@ -19,6 +20,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.pointer.pointerInput +import com.valentinilk.shimmer.shimmer import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.savers.ListSaver import it.vfsfitvnm.vimusic.savers.StringResultSaver @@ -141,12 +143,17 @@ inline fun SearchResult( } } } ?: item(key = "loading") { - repeat(if (items.isEmpty()) 8 else 3) { index -> - Box( - modifier = Modifier - .alpha(1f - index * 0.125f), - content = itemShimmer - ) + Column( + modifier = Modifier + .shimmer() + ) { + repeat(if (items.isEmpty()) 8 else 3) { index -> + Box( + modifier = Modifier + .alpha(1f - index * 0.125f), + content = itemShimmer + ) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt index f503f5b..7f34514 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt @@ -10,7 +10,9 @@ import androidx.compose.ui.unit.dp object Dimensions { val itemsVerticalPadding = 8.dp - val verticalBarWidth = 64.dp + val navigationRailWidth = 64.dp + val navigationRailIconOffset = 6.dp + val headerHeight = 128.dp object thumbnails { val album = 128.dp From c03a3bcc03598937763d27ad2374526b691fa0d9 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 16:38:51 +0200 Subject: [PATCH 025/100] Tweak UI --- .../ui/components/themed/NavigationRail.kt | 94 +++++++++++++------ .../vimusic/ui/components/themed/Scaffold.kt | 2 - .../ui/screens/search/LocalSongSearch.kt | 10 +- .../vimusic/ui/screens/search/OnlineSearch.kt | 10 +- .../vimusic/ui/styling/Dimensions.kt | 1 + 5 files changed, 75 insertions(+), 42 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt index eb46bbf..0b89cf8 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt @@ -1,5 +1,6 @@ package it.vfsfitvnm.vimusic.ui.components.themed +import android.content.res.Configuration import androidx.compose.animation.animateColor import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.updateTransition @@ -29,8 +30,10 @@ import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.layout +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.semiBold @@ -45,22 +48,33 @@ fun NavigationRail( modifier: Modifier = Modifier ) { val (colorPalette, typography) = LocalAppearance.current + val configuration = LocalConfiguration.current + + val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier + .padding(LocalPlayerAwarePaddingValues.current) + .verticalScroll(rememberScrollState()) ) { Box( contentAlignment = Alignment.TopCenter, modifier = Modifier - .size(width = Dimensions.navigationRailWidth, height = Dimensions.headerHeight) + .size( + width = if (isLandscape) Dimensions.navigationRailWidthLandscape else Dimensions.navigationRailWidth, + height = Dimensions.headerHeight + ) ) { Image( painter = painterResource(topIconButtonId), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.textSecondary), modifier = Modifier - .offset(x = Dimensions.navigationRailIconOffset, y = 48.dp) + .offset( + x = if (isLandscape) 0.dp else Dimensions.navigationRailIconOffset, + y = 48.dp + ) .clip(CircleShape) .clickable(onClick = onTopIconButtonClick) .padding(all = 12.dp) @@ -68,10 +82,7 @@ fun NavigationRail( ) } - Column( - modifier = modifier - .verticalScroll(rememberScrollState()) - ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { val transition = updateTransition(targetState = tabIndex, label = null) content { index, text, icon -> @@ -83,52 +94,73 @@ fun NavigationRail( if (it == index) colorPalette.text else colorPalette.textDisabled } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clip(RoundedCornerShape(24.dp)) - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() }, - onClick = { onTabIndexChanged(index) } - ) - .padding(horizontal = 8.dp) - ) { + val iconContent: @Composable () -> Unit = { Image( painter = painterResource(icon), contentDescription = null, colorFilter = ColorFilter.tint(colorPalette.text), modifier = Modifier - .vertical() + .vertical(enabled = !isLandscape) .graphicsLayer { alpha = dothAlpha translationX = (1f - dothAlpha) * -48.dp.toPx() - rotationZ = -90f + rotationZ = if (isLandscape) 0f else -90f } .size(Dimensions.navigationRailIconOffset * 2) ) + } + val textContent: @Composable () -> Unit = { BasicText( text = text, style = typography.xs.semiBold.copy(color = textColor), modifier = Modifier - .vertical() - .rotate(-90f) + .vertical(enabled = !isLandscape) + .rotate(if (isLandscape) 0f else -90f) .padding(horizontal = 16.dp) ) } + + val contentModifier = Modifier + .clip(RoundedCornerShape(24.dp)) + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onTabIndexChanged(index) } + ) + + if (isLandscape) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = contentModifier + .padding(vertical = 8.dp) + ) { + iconContent() + textContent() + } + } else { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = contentModifier + .padding(horizontal = 8.dp) + ) { + iconContent() + textContent() + } + } } } } } -private 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) - ) - } - } +private fun Modifier.vertical(enabled: Boolean = true) = + if (enabled) + 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) + ) + } + } else this diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt index 29b46ea..362fa1c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -60,8 +60,6 @@ fun Scaffold( onTopIconButtonClick = onTopIconButtonClick, tabIndex = tabIndex, onTabIndexChanged = onTabChanged, - modifier = Modifier - .padding(LocalPlayerAwarePaddingValues.current), content = tabColumnContent ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 9405e33..7fc4180 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -62,10 +62,12 @@ fun LocalSongSearch( stateSaver = DetailedSongListSaver, key1 = textFieldValue.text ) { - Database - .search("%${textFieldValue.text}%") - .flowOn(Dispatchers.IO) - .collect { value = it } + if (textFieldValue.text.length > 1) { + Database + .search("%${textFieldValue.text}%") + .flowOn(Dispatchers.IO) + .collect { value = it } + } } val thumbnailSize = Dimensions.thumbnails.song.px diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 274b2be..0e51d18 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -111,11 +111,6 @@ fun OnlineSearch( FocusRequester() } - LaunchedEffect(Unit) { - delay(300) - focusRequester.requestFocus() - } - LazyColumn( contentPadding = LocalPlayerAwarePaddingValues.current, modifier = Modifier @@ -334,4 +329,9 @@ fun OnlineSearch( } } } + + LaunchedEffect(Unit) { + delay(300) + focusRequester.requestFocus() + } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt index 7f34514..0cee656 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/styling/Dimensions.kt @@ -11,6 +11,7 @@ object Dimensions { val itemsVerticalPadding = 8.dp val navigationRailWidth = 64.dp + val navigationRailWidthLandscape = 128.dp val navigationRailIconOffset = 6.dp val headerHeight = 128.dp From 42a4d5b43e6ee1325b80969fbf26a2c52c7969c5 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 16:49:41 +0200 Subject: [PATCH 026/100] Create PrimaryButton composable --- .../ui/components/themed/PrimaryButton.kt | 50 ++++ .../vimusic/ui/components/themed/Scaffold.kt | 25 +- .../vimusic/ui/screens/album/AlbumOverview.kt | 37 +-- .../ui/screens/artist/ArtistOverview.kt | 222 ------------------ .../builtinplaylist/LocalPlaylistSongList.kt | 38 +-- .../localplaylist/LocalPlaylistSongList.kt | 42 ++-- .../ui/screens/playlist/PlaylistSongList.kt | 33 +-- 7 files changed, 102 insertions(+), 345 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt new file mode 100644 index 0000000..28524e3 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/PrimaryButton.kt @@ -0,0 +1,50 @@ +package it.vfsfitvnm.vimusic.ui.components.themed + +import androidx.annotation.DrawableRes +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.BoxScope +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.dp +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance + +@Composable +fun BoxScope.PrimaryButton( + onClick: () -> Unit, + @DrawableRes iconId: Int, + modifier: Modifier = Modifier, + isEnabled: Boolean = true, +) { + val (colorPalette) = LocalAppearance.current + + Box( + modifier = modifier + .align(Alignment.BottomEnd) + .padding(all = 16.dp) + .padding(LocalPlayerAwarePaddingValues.current) + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = isEnabled, onClick = onClick) + .background(colorPalette.background2) + .size(62.dp) + ) { + Image( + painter = painterResource(iconId), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .align(Alignment.Center) + .size(20.dp) + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt index 362fa1c..11ffa02 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Scaffold.kt @@ -1,6 +1,7 @@ package it.vfsfitvnm.vimusic.ui.components.themed import android.annotation.SuppressLint +import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.AnimatedVisibilityScope @@ -13,6 +14,7 @@ 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.BoxScope import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -85,25 +87,10 @@ fun Scaffold( } 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) - ) - } + PrimaryButton( + iconId = primaryIconButtonId, + onClick = onPrimaryIconButtonClick + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index dc40c54..237359b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -48,6 +48,7 @@ import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -266,32 +267,16 @@ fun AlbumOverview( } } - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(all = 16.dp) - .padding(LocalPlayerAwarePaddingValues.current) - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning( - songs - .shuffled() - .map(DetailedSong::asMediaItem) - ) - } - .background(colorPalette.background2) - .size(62.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .align(Alignment.Center) - .size(20.dp) - ) - } + PrimaryButton( + iconId = R.drawable.shuffle, + isEnabled = songs.isNotEmpty(), + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + songs.shuffled().map(DetailedSong::asMediaItem) + ) + } + ) } ?: albumResult?.exceptionOrNull()?.let { Box( modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt index ba04073..95ba666 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -73,226 +73,4 @@ fun ArtistOverview( val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val context = LocalContext.current - -// BoxWithConstraints { -// val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth -// val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px -// -// viewModel.result?.getOrNull()?.let { albumWithSongs -> -// LazyColumn( -// contentPadding = LocalPlayerAwarePaddingValues.current, -// modifier = Modifier -// .background(colorPalette.background0) -// .fillMaxSize() -// ) { -// item( -// key = "header", -// contentType = 0 -// ) { -// Column { -// Header(title = albumWithSongs.album.title ?: "Unknown") { -// if (albumWithSongs.songs.isNotEmpty()) { -// BasicText( -// text = "Enqueue", -// style = typography.xxs.medium, -// modifier = Modifier -// .clip(RoundedCornerShape(16.dp)) -// .clickable { -// binder?.player?.enqueue( -// albumWithSongs.songs.map(DetailedSong::asMediaItem) -// ) -// } -// .background(colorPalette.background2) -// .padding(all = 8.dp) -// .padding(horizontal = 8.dp) -// ) -// } -// -// Spacer( -// modifier = Modifier -// .weight(1f) -// ) -// -// Image( -// painter = painterResource( -// if (albumWithSongs.album.bookmarkedAt == null) { -// R.drawable.bookmark_outline -// } else { -// R.drawable.bookmark -// } -// ), -// contentDescription = null, -// colorFilter = ColorFilter.tint(colorPalette.accent), -// modifier = Modifier -// .clickable { -// query { -// Database.update( -// albumWithSongs.album.copy( -// bookmarkedAt = if (albumWithSongs.album.bookmarkedAt == null) { -// System.currentTimeMillis() -// } else { -// null -// } -// ) -// ) -// } -// } -// .padding(all = 4.dp) -// .size(18.dp) -// ) -// -// Image( -// painter = painterResource(R.drawable.share_social), -// contentDescription = null, -// colorFilter = ColorFilter.tint(colorPalette.text), -// modifier = Modifier -// .clickable { -// albumWithSongs.album.shareUrl?.let { url -> -// val sendIntent = Intent().apply { -// action = Intent.ACTION_SEND -// type = "text/plain" -// putExtra(Intent.EXTRA_TEXT, url) -// } -// -// context.startActivity( -// Intent.createChooser( -// sendIntent, -// null -// ) -// ) -// } -// } -// .padding(all = 4.dp) -// .size(18.dp) -// ) -// } -// -// AsyncImage( -// model = albumWithSongs.album.thumbnailUrl?.thumbnail(thumbnailSizePx), -// contentDescription = null, -// modifier = Modifier -// .align(Alignment.CenterHorizontally) -// .padding(all = 16.dp) -// .clip(thumbnailShape) -// .size(thumbnailSizeDp) -// ) -// } -// } -// -// itemsIndexed( -// items = albumWithSongs.songs, -// key = { _, song -> song.id } -// ) { index, song -> -// SongItem( -// title = song.title, -// authors = song.artistsText ?: albumWithSongs.album.authorsText, -// durationText = song.durationText, -// onClick = { -// binder?.stopRadio() -// binder?.player?.forcePlayAtIndex( -// albumWithSongs.songs.map(DetailedSong::asMediaItem), -// index -// ) -// }, -// startContent = { -// BasicText( -// text = "${index + 1}", -// style = typography.s.semiBold.center.color(colorPalette.textDisabled), -// maxLines = 1, -// overflow = TextOverflow.Ellipsis, -// modifier = Modifier -// .width(Dimensions.thumbnails.song) -// ) -// }, -// menuContent = { -// NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) -// } -// ) -// } -// } -// -// Box( -// modifier = Modifier -// .align(Alignment.BottomEnd) -// .padding(all = 16.dp) -// .padding(LocalPlayerAwarePaddingValues.current) -// .clip(RoundedCornerShape(16.dp)) -// .clickable(enabled = albumWithSongs.songs.isNotEmpty()) { -// binder?.stopRadio() -// binder?.player?.forcePlayFromBeginning( -// albumWithSongs.songs -// .shuffled() -// .map(DetailedSong::asMediaItem) -// ) -// } -// .background(colorPalette.background2) -// .size(62.dp) -// ) { -// Image( -// painter = painterResource(R.drawable.shuffle), -// contentDescription = null, -// colorFilter = ColorFilter.tint(colorPalette.text), -// modifier = Modifier -// .align(Alignment.Center) -// .size(20.dp) -// ) -// } -// } ?: viewModel.result?.exceptionOrNull()?.let { -// Box( -// modifier = Modifier -// .pointerInput(Unit) { -// detectTapGestures { -// viewModel.fetch(browseId) -// } -// } -// .align(Alignment.Center) -// .fillMaxSize() -// ) { -// BasicText( -// text = "An error has occurred.\nTap to retry", -// style = typography.s.medium.secondary.center, -// modifier = Modifier -// .align(Alignment.Center) -// ) -// } -// } ?: Column( -// modifier = Modifier -// .padding(LocalPlayerAwarePaddingValues.current) -// .shimmer() -// ) { -// HeaderPlaceholder() -// -// Spacer( -// modifier = Modifier -// .align(Alignment.CenterHorizontally) -// .padding(all = 16.dp) -// .clip(thumbnailShape) -// .size(thumbnailSizeDp) -// .background(colorPalette.shimmer) -// ) -// -// repeat(3) { index -> -// Row( -// verticalAlignment = Alignment.CenterVertically, -// horizontalArrangement = Arrangement.spacedBy(12.dp), -// modifier = Modifier -// .alpha(1f - index * 0.25f) -// .fillMaxWidth() -// .padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding) -// .height(Dimensions.thumbnails.song) -// ) { -// Spacer( -// modifier = Modifier -// .background(color = colorPalette.shimmer, shape = thumbnailShape) -// .size(Dimensions.thumbnails.song) -// ) -// -// Column { -// TextPlaceholder() -// TextPlaceholder() -// } -// } -// } -// } -// } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt index 3d6c6c9..fa1f7dc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt @@ -1,25 +1,20 @@ package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist import androidx.compose.animation.ExperimentalAnimationApi -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.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size 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.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.dp import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues @@ -31,6 +26,7 @@ import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -136,27 +132,15 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) { } } - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(all = 16.dp) - .padding(LocalPlayerAwarePaddingValues.current) - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = songs.isNotEmpty()) { - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(songs.shuffled().map(DetailedSong::asMediaItem)) - } - .background(colorPalette.background2) - .size(62.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .align(Alignment.Center) - .size(20.dp) - ) - } + PrimaryButton( + iconId = R.drawable.shuffle, + isEnabled = songs.isNotEmpty(), + onClick = { + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning( + songs.shuffled().map(DetailedSong::asMediaItem) + ) + } + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt index 5aaec96..7b12acc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -19,7 +19,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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 @@ -42,6 +41,7 @@ import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -267,32 +267,18 @@ fun LocalPlaylistSongList( } } - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(all = 16.dp) - .padding(LocalPlayerAwarePaddingValues.current) - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = playlistWithSongs?.songs?.isNotEmpty() == true) { - playlistWithSongs?.songs - ?.shuffled() - ?.map(DetailedSong::asMediaItem) - ?.let { mediaItems -> - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(mediaItems) - } - } - .background(colorPalette.background2) - .size(62.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .align(Alignment.Center) - .size(20.dp) - ) - } + PrimaryButton( + iconId = R.drawable.shuffle, + isEnabled = playlistWithSongs?.songs?.isNotEmpty() == true, + onClick = { + playlistWithSongs?.songs + ?.shuffled() + ?.map(DetailedSong::asMediaItem) + ?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(mediaItems) + } + } + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 6c69858..fd69b95 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -46,6 +46,7 @@ import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -234,30 +235,16 @@ fun PlaylistSongList( } } - Box( - modifier = Modifier - .align(Alignment.BottomEnd) - .padding(all = 16.dp) - .padding(LocalPlayerAwarePaddingValues.current) - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = playlist.songs?.isNotEmpty() == true) { - playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> - binder?.stopRadio() - binder?.player?.forcePlayFromBeginning(mediaItems.shuffled()) - } + PrimaryButton( + iconId = R.drawable.shuffle, + isEnabled = playlist.songs?.isNotEmpty() == true, + onClick = { + playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + binder?.stopRadio() + binder?.player?.forcePlayFromBeginning(mediaItems.shuffled()) } - .background(colorPalette.background2) - .size(62.dp) - ) { - Image( - painter = painterResource(R.drawable.shuffle), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .align(Alignment.Center) - .size(20.dp) - ) - } + } + ) } ?: playlistResult?.exceptionOrNull()?.let { Box( modifier = Modifier From 6f56cab12929f0e459600e67c175c90cfd9b31e6 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 16:59:07 +0200 Subject: [PATCH 027/100] Create SecondaryTextButton composable --- .../components/themed/SecondaryTextButton.kt | 34 +++++++++++++++++++ .../vimusic/ui/screens/album/AlbumOverview.kt | 25 +++++--------- .../builtinplaylist/LocalPlaylistSongList.kt | 25 ++++---------- .../ui/screens/home/HomePlaylistList.kt | 17 +++------- .../localplaylist/LocalPlaylistSongList.kt | 30 ++++++---------- .../ui/screens/playlist/PlaylistSongList.kt | 27 ++++++--------- .../ui/screens/search/LocalSongSearch.kt | 19 +++-------- .../vimusic/ui/screens/search/OnlineSearch.kt | 28 ++++----------- 8 files changed, 85 insertions(+), 120 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt new file mode 100644 index 0000000..2ac9aef --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/SecondaryTextButton.kt @@ -0,0 +1,34 @@ +package it.vfsfitvnm.vimusic.ui.components.themed + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.medium + +@Composable +fun SecondaryTextButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + isEnabled: Boolean = true +) { + val (colorPalette, typography) = LocalAppearance.current + + BasicText( + text = text, + style = typography.xxs.medium, + modifier = modifier + .clip(RoundedCornerShape(16.dp)) + .clickable(enabled = isEnabled, onClick = onClick) + .background(colorPalette.background2) + .padding(all = 8.dp) + .padding(horizontal = 8.dp) + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index 237359b..bb04e88 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -20,7 +20,6 @@ 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 @@ -49,6 +48,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -147,22 +147,13 @@ fun AlbumOverview( ) { Column { Header(title = album.title ?: "Unknown") { - if (songs.isNotEmpty()) { - BasicText( - text = "Enqueue", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable { - binder?.player?.enqueue( - songs.map(DetailedSong::asMediaItem) - ) - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) - ) - } + SecondaryTextButton( + text = "Enqueue", + isEnabled = songs.isNotEmpty(), + onClick = { + binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) + } + ) Spacer( modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt index fa1f7dc..50c9802 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt @@ -2,20 +2,14 @@ package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding 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.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.unit.dp import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder @@ -27,6 +21,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -35,7 +30,6 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableState import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn @@ -44,7 +38,7 @@ import kotlinx.coroutines.flow.map @ExperimentalAnimationApi @Composable fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) { - val (colorPalette, typography) = LocalAppearance.current + val (colorPalette) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val songs by produceSaveableState( @@ -87,17 +81,12 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) { BuiltInPlaylist.Offline -> "Offline" } ) { - BasicText( + SecondaryTextButton( text = "Enqueue", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = songs.isNotEmpty()) { - binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) + isEnabled = songs.isNotEmpty(), + onClick = { + binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) + } ) Spacer( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 7d49b05..4fcdd18 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -19,8 +19,6 @@ 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 @@ -30,7 +28,6 @@ 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 @@ -45,12 +42,12 @@ import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton 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.views.BuiltInPlaylistItem import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem -import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.playlistSortByKey import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey import it.vfsfitvnm.vimusic.utils.produceSaveableState @@ -64,7 +61,7 @@ fun HomePlaylistList( onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit, onPlaylistClicked: (Playlist) -> Unit, ) { - val (colorPalette, typography) = LocalAppearance.current + val (colorPalette) = LocalAppearance.current var isCreatingANewPlaylist by rememberSaveable { mutableStateOf(false) @@ -137,15 +134,9 @@ fun HomePlaylistList( ) } - BasicText( + SecondaryTextButton( 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) + onClick = { isCreatingANewPlaylist = true } ) Spacer( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt index 7b12acc..9153fc5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -12,15 +12,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -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.saveable.rememberSaveable import androidx.compose.runtime.setValue 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.dp @@ -42,6 +39,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -51,7 +49,6 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning -import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers @@ -66,7 +63,7 @@ fun LocalPlaylistSongList( playlistId: Long, onDelete: () -> Unit, ) { - val (colorPalette, typography) = LocalAppearance.current + val (colorPalette) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current val playlistWithSongs by produceSaveableState( @@ -141,21 +138,16 @@ fun LocalPlaylistSongList( contentType = 0 ) { Header(title = playlistWithSongs?.playlist?.name ?: "Unknown") { - BasicText( + SecondaryTextButton( text = "Enqueue", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable(enabled = playlistWithSongs?.songs?.isNotEmpty() == true) { - playlistWithSongs?.songs - ?.map(DetailedSong::asMediaItem) - ?.let { mediaItems -> - binder?.player?.enqueue(mediaItems) - } - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) + isEnabled = playlistWithSongs?.songs?.isNotEmpty() == true, + onClick = { + playlistWithSongs?.songs + ?.map(DetailedSong::asMediaItem) + ?.let { mediaItems -> + binder?.player?.enqueue(mediaItems) + } + } ) Spacer( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index fd69b95..719ebd8 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size 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 @@ -47,6 +46,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance @@ -119,22 +119,15 @@ fun PlaylistSongList( ) { Column { Header(title = playlist.title ?: "Unknown") { - if (playlist.songs?.isNotEmpty() == true) { - BasicText( - text = "Enqueue", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable { - playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> - binder?.player?.enqueue(mediaItems) - } - } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) - ) - } + SecondaryTextButton( + text = "Enqueue", + isEnabled = playlist.songs?.isNotEmpty() == true, + onClick = { + playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> + binder?.player?.enqueue(mediaItems) + } + } + ) Spacer( modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 7fc4180..a52fdeb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -5,14 +5,10 @@ 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.background -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions @@ -20,12 +16,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder @@ -33,6 +27,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.px @@ -114,15 +109,9 @@ fun LocalSongSearch( }, actionsContent = { if (textFieldValue.text.isNotEmpty()) { - BasicText( - text = "Clear", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable { onTextFieldValueChanged(TextFieldValue()) } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) + SecondaryTextButton( + text = "Clear", + onClick = { onTextFieldValueChanged(TextFieldValue()) } ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 0e51d18..df587a4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -3,7 +3,6 @@ package it.vfsfitvnm.vimusic.ui.screens.search import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -15,7 +14,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions @@ -27,7 +25,6 @@ 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.paint import androidx.compose.ui.draw.rotate import androidx.compose.ui.focus.FocusRequester @@ -49,6 +46,7 @@ import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.savers.SearchQueryListSaver import it.vfsfitvnm.vimusic.savers.StringListResultSaver import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.center @@ -164,15 +162,9 @@ fun OnlineSearch( if (playlistId != null) { val isAlbum = playlistId.startsWith("OLAK5uy_") - BasicText( - text = "View ${if (isAlbum) "album" else "playlist"}", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable { onViewPlaylist(textFieldValue.text) } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) + SecondaryTextButton( + text = "View ${if (isAlbum) "album" else "playlist"}", + onClick = { onViewPlaylist(textFieldValue.text) } ) } @@ -182,15 +174,9 @@ fun OnlineSearch( ) if (textFieldValue.text.isNotEmpty()) { - BasicText( - text = "Clear", - style = typography.xxs.medium, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .clickable { onTextFieldValueChanged(TextFieldValue()) } - .background(colorPalette.background2) - .padding(all = 8.dp) - .padding(horizontal = 8.dp) + SecondaryTextButton( + text = "Clear", + onClick = { onTextFieldValueChanged(TextFieldValue()) } ) } } From 7a3c0ca11027f04ba72d064c6a356b9c46961d74 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 17:06:25 +0200 Subject: [PATCH 028/100] Tweak SearchScreen code --- .../ui/screens/search/LocalSongSearch.kt | 30 ++------------- .../vimusic/ui/screens/search/OnlineSearch.kt | 28 ++------------ .../vimusic/ui/screens/search/SearchScreen.kt | 37 ++++++++++++++++++- 3 files changed, 42 insertions(+), 53 deletions(-) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index a52fdeb..6abda8e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -1,20 +1,14 @@ package it.vfsfitvnm.vimusic.ui.screens.search import androidx.compose.animation.ExperimentalAnimationApi -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.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.input.ImeAction @@ -37,7 +31,6 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.produceSaveableState -import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn @@ -47,7 +40,8 @@ import kotlinx.coroutines.flow.flowOn @Composable fun LocalSongSearch( textFieldValue: TextFieldValue, - onTextFieldValueChanged: (TextFieldValue) -> Unit + onTextFieldValueChanged: (TextFieldValue) -> Unit, + decorationBox: @Composable (@Composable () -> Unit) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current val binder = LocalPlayerServiceBinder.current @@ -86,25 +80,7 @@ fun LocalSongSearch( maxLines = 1, keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done), cursorBrush = SolidColor(colorPalette.text), - decorationBox = { innerTextField -> - Box { - androidx.compose.animation.AnimatedVisibility( - visible = textFieldValue.text.isEmpty(), - enter = fadeIn(tween(200)), - exit = fadeOut(tween(200)), - modifier = Modifier - .align(Alignment.CenterEnd) - ) { - BasicText( - text = "Enter a name", - maxLines = 1, - style = typography.xxl.secondary - ) - } - - innerTextField() - } - } + decorationBox = decorationBox ) }, actionsContent = { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index df587a4..d2e21e3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -1,8 +1,5 @@ package it.vfsfitvnm.vimusic.ui.screens.search -import androidx.compose.animation.core.tween -import androidx.compose.animation.fadeIn -import androidx.compose.animation.fadeOut import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box @@ -65,7 +62,8 @@ fun OnlineSearch( textFieldValue: TextFieldValue, onTextFieldValueChanged: (TextFieldValue) -> Unit, onSearch: (String) -> Unit, - onViewPlaylist: (String) -> Unit + onViewPlaylist: (String) -> Unit, + decorationBox: @Composable (@Composable () -> Unit) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current @@ -135,25 +133,7 @@ fun OnlineSearch( } ), cursorBrush = SolidColor(colorPalette.text), - decorationBox = { innerTextField -> - Box { - androidx.compose.animation.AnimatedVisibility( - visible = textFieldValue.text.isEmpty(), - enter = fadeIn(tween(200)), - exit = fadeOut(tween(200)), - modifier = Modifier - .align(Alignment.CenterEnd) - ) { - BasicText( - text = "Enter a name", - maxLines = 1, - style = typography.xxl.secondary - ) - } - - innerTextField() - } - }, + decorationBox = decorationBox, modifier = Modifier .focusRequester(focusRequester) ) @@ -299,7 +279,7 @@ fun OnlineSearch( ) } } - } ?: suggestionsResult?.exceptionOrNull()?.let { throwable -> + } ?: suggestionsResult?.exceptionOrNull()?.let { item { Box( modifier = Modifier diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt index a6f65fd..3f5e01d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt @@ -1,17 +1,27 @@ package it.vfsfitvnm.vimusic.ui.screens.search +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi +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.layout.Box +import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes +import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance +import it.vfsfitvnm.vimusic.utils.secondary @ExperimentalFoundationApi @ExperimentalAnimationApi @@ -43,6 +53,26 @@ fun SearchScreen( globalRoutes() host { + val decorationBox: @Composable (@Composable () -> Unit) -> Unit = { innerTextField -> + Box { + AnimatedVisibility( + visible = textFieldValue.text.isEmpty(), + enter = fadeIn(tween(300)), + exit = fadeOut(tween(300)), + modifier = Modifier + .align(Alignment.CenterEnd) + ) { + BasicText( + text = "Enter a name", + maxLines = 1, + style = LocalAppearance.current.typography.xxl.secondary + ) + } + + innerTextField() + } + } + Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, @@ -59,11 +89,14 @@ fun SearchScreen( textFieldValue = textFieldValue, onTextFieldValueChanged = onTextFieldValueChanged, onSearch = onSearch, - onViewPlaylist = onViewPlaylist + onViewPlaylist = onViewPlaylist, + decorationBox = decorationBox ) + 1 -> LocalSongSearch( textFieldValue = textFieldValue, - onTextFieldValueChanged = onTextFieldValueChanged + onTextFieldValueChanged = onTextFieldValueChanged, + decorationBox = decorationBox ) } } From 33778b33ddf1d7bf3d42a491d3d7f6155c062a43 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 21:46:56 +0200 Subject: [PATCH 029/100] Start working on QuickPicks screen --- .../19.json | 670 ++++++++++++++++++ .../kotlin/it/vfsfitvnm/vimusic/Database.kt | 18 +- .../it/vfsfitvnm/vimusic/models/Event.kt | 25 + .../vimusic/savers/DetailedSongSaver.kt | 2 +- .../it/vfsfitvnm/vimusic/savers/InfoSaver.kt | 5 +- .../vfsfitvnm/vimusic/savers/NullableSaver.kt | 13 + .../vfsfitvnm/vimusic/savers/ResultSaver.kt | 2 - .../vimusic/savers/YouTubeAlbumSaver.kt | 8 +- .../vimusic/savers/YouTubeArtistSaver.kt | 4 +- .../vimusic/savers/YouTubeBrowseInfoSaver.kt | 4 +- .../vimusic/savers/YouTubePlaylistSaver.kt | 8 +- .../vimusic/savers/YouTubeRelatedSaver.kt | 22 + .../vimusic/savers/YouTubeSongSaver.kt | 12 +- .../vimusic/savers/YouTubeVideoSaver.kt | 10 +- .../vimusic/savers/YouTubeWatchInfoSaver.kt | 4 +- .../vimusic/service/PlayerService.kt | 15 +- .../ui/components/themed/MediaItemMenu.kt | 10 +- .../vimusic/ui/screens/album/AlbumOverview.kt | 2 +- .../vimusic/ui/screens/artist/ArtistScreen.kt | 4 +- .../builtinplaylist/LocalPlaylistSongList.kt | 2 +- .../ui/screens/home/HomePlaylistList.kt | 10 +- .../vimusic/ui/screens/home/HomeScreen.kt | 29 +- .../vimusic/ui/screens/home/HomeSongList.kt | 2 +- .../vimusic/ui/screens/home/QuickPicks.kt | 214 ++++++ .../localplaylist/LocalPlaylistSongList.kt | 8 +- .../vimusic/ui/screens/player/Lyrics.kt | 2 +- .../ui/screens/player/PlayerBottomSheet.kt | 2 +- .../ui/screens/playlist/PlaylistSongList.kt | 10 +- .../ui/screens/search/LocalSongSearch.kt | 2 +- .../ui/screens/searchresult/SearchResult.kt | 2 +- .../searchresult/SearchResultScreen.kt | 10 +- .../it/vfsfitvnm/vimusic/ui/views/SongItem.kt | 16 +- .../vimusic/ui/views/YouTubeItems.kt | 16 +- .../it/vfsfitvnm/vimusic/utils/Utils.kt | 44 +- .../it/vfsfitvnm/youtubemusic/YouTube.kt | 412 +++++++---- .../models/MusicCarouselShelfRenderer.kt | 5 +- .../youtubemusic/models/NavigationEndpoint.kt | 2 +- 37 files changed, 1354 insertions(+), 272 deletions(-) create mode 100644 app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt diff --git a/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json new file mode 100644 index 0000000..8196898 --- /dev/null +++ b/app/schemas/it.vfsfitvnm.vimusic.DatabaseInitializer/19.json @@ -0,0 +1,670 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "b9a9bb1674c7c50be2fab48de5afed43", + "entities": [ + { + "tableName": "Song", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `synchronizedLyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistsText", + "columnName": "artistsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "durationText", + "columnName": "durationText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lyrics", + "columnName": "lyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronizedLyrics", + "columnName": "synchronizedLyrics", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "likedAt", + "columnName": "likedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "totalPlayTimeMs", + "columnName": "totalPlayTimeMs", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongPlaylistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "playlistId", + "columnName": "playlistId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "playlistId" + ] + }, + "indices": [ + { + "name": "index_SongPlaylistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongPlaylistMap_playlistId", + "unique": false, + "columnNames": [ + "playlistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Playlist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "playlistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Playlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `browseId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "browseId", + "columnName": "browseId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Artist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "info", + "columnName": "info", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shuffleVideoId", + "columnName": "shuffleVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shufflePlaylistId", + "columnName": "shufflePlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioVideoId", + "columnName": "radioVideoId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "radioPlaylistId", + "columnName": "radioPlaylistId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongArtistMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "artistId", + "columnName": "artistId", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "artistId" + ] + }, + "indices": [ + { + "name": "index_SongArtistMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongArtistMap_artistId", + "unique": false, + "columnNames": [ + "artistId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Artist", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "artistId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Album", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, `bookmarkedAt` INTEGER, PRIMARY KEY(`id`))", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thumbnailUrl", + "columnName": "thumbnailUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "year", + "columnName": "year", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "authorsText", + "columnName": "authorsText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "shareUrl", + "columnName": "shareUrl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "bookmarkedAt", + "columnName": "bookmarkedAt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "SongAlbumMap", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "albumId", + "columnName": "albumId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId", + "albumId" + ] + }, + "indices": [ + { + "name": "index_SongAlbumMap_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)" + }, + { + "name": "index_SongAlbumMap_albumId", + "unique": false, + "columnNames": [ + "albumId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "Album", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "albumId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "SearchQuery", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "query", + "columnName": "query", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_SearchQuery_query", + "unique": true, + "columnNames": [ + "query" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "QueuedMediaItem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaItem", + "columnName": "mediaItem", + "affinity": "BLOB", + "notNull": true + }, + { + "fieldPath": "position", + "columnName": "position", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "Format", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `itag` INTEGER, `mimeType` TEXT, `bitrate` INTEGER, `contentLength` INTEGER, `lastModified` INTEGER, `loudnessDb` REAL, PRIMARY KEY(`songId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "itag", + "columnName": "itag", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mimeType", + "columnName": "mimeType", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bitrate", + "columnName": "bitrate", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "contentLength", + "columnName": "contentLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastModified", + "columnName": "lastModified", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "loudnessDb", + "columnName": "loudnessDb", + "affinity": "REAL", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "songId" + ] + }, + "indices": [], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "Event", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `songId` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `playTime` INTEGER NOT NULL, FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "songId", + "columnName": "songId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "playTime", + "columnName": "playTime", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_Event_songId", + "unique": false, + "columnNames": [ + "songId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_Event_songId` ON `${TABLE_NAME}` (`songId`)" + } + ], + "foreignKeys": [ + { + "table": "Song", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "songId" + ], + "referencedColumns": [ + "id" + ] + } + ] + } + ], + "views": [ + { + "viewName": "SortedSongPlaylistMap", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'b9a9bb1674c7c50be2fab48de5afed43')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index aa9bd0a..2dc43b6 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -37,6 +37,7 @@ import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.Artist import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSongWithContentLength +import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.Format import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.PlaylistPreview @@ -288,6 +289,19 @@ interface Database { @Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)") fun isImportedPlaylist(browseId: String): Flow + @Transaction + @Query("SELECT Song.* FROM Event JOIN Song ON Song.id = songId GROUP BY songId ORDER BY SUM(playTime / ((:now - timestamp) / 86400000.0)) LIMIT 1") + @RewriteQueriesToDropUnusedColumns + fun trending(now: Long = System.currentTimeMillis()): Flow + +// @Transaction +// @Query("SELECT songId FROM Event GROUP BY songId ORDER BY SUM(playTime / ((:now - timestamp) / 86400000.0)) LIMIT 1") +// @RewriteQueriesToDropUnusedColumns +// fun trending(now: Long = System.currentTimeMillis()): Flow + + @Insert(onConflict = OnConflictStrategy.ABORT) + fun insert(event: Event) + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(format: Format) @@ -427,11 +441,12 @@ interface Database { SearchQuery::class, QueuedMediaItem::class, Format::class, + Event::class, ], views = [ SortedSongPlaylistMap::class ], - version = 18, + version = 19, exportSchema = true, autoMigrations = [ AutoMigration(from = 1, to = 2), @@ -448,6 +463,7 @@ interface Database { AutoMigration(from = 15, to = 16), AutoMigration(from = 16, to = 17), AutoMigration(from = 17, to = 18), + AutoMigration(from = 18, to = 19), ], ) @TypeConverters(Converters::class) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt new file mode 100644 index 0000000..912b88a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/models/Event.kt @@ -0,0 +1,25 @@ +package it.vfsfitvnm.vimusic.models + +import androidx.compose.runtime.Immutable +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.PrimaryKey + +@Immutable +@Entity( + foreignKeys = [ + ForeignKey( + entity = Song::class, + parentColumns = ["id"], + childColumns = ["songId"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class Event( + @PrimaryKey(autoGenerate = true) val id: Long = 0, + @ColumnInfo(index = true) val songId: String, + val timestamp: Long, + val playTime: Long +) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt index db11ff5..5455701 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/DetailedSongSaver.kt @@ -26,6 +26,6 @@ object DetailedSongSaver : Saver> { thumbnailUrl = value[4] as String?, totalPlayTimeMs = value[5] as Long, albumId = value[6] as String?, - artists = InfoListSaver.restore(value[7] as List>) + artists = (value[7] as List>?)?.let(InfoListSaver::restore) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt index 3f59e7b..9eae23b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/InfoSaver.kt @@ -8,9 +8,6 @@ object InfoSaver : Saver> { override fun SaverScope.save(value: Info): List = listOf(value.id, value.name) override fun restore(value: List): Info? { - return if (value.size == 2) Info( - id = value[0], - name = value[1], - ) else null + return if (value.size == 2) Info(id = value[0], name = value[1]) else null } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt new file mode 100644 index 0000000..4f56b9b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/NullableSaver.kt @@ -0,0 +1,13 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope + +fun nullableSaver(saver: Saver) = + object : Saver { + override fun SaverScope.save(value: Original?): Saveable? = + value?.let { with(saver) { save(it) } } + + override fun restore(value: Saveable): Original? = + saver.restore(value) + } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt index 763d2c8..827f7eb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/ResultSaver.kt @@ -3,8 +3,6 @@ package it.vfsfitvnm.vimusic.savers import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.SaverScope -interface ResultSaver : Saver?, Pair> - fun resultSaver(saver: Saver) = object : Saver?, Pair> { override fun restore(value: Pair) = diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt index c4a5f8d..749db84 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeAlbumSaver.kt @@ -6,15 +6,15 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeAlbumSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Album): List = listOf( - with(YouTubeBrowseInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } }, value.year, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) @Suppress("UNCHECKED_CAST") override fun restore(value: List) = YouTube.Item.Album( - info = YouTubeBrowseInfoSaver.restore(value[0] as List), + info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), year = value[2] as String?, thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt index 98a1965..7f602a7 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeArtistSaver.kt @@ -6,13 +6,13 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeArtistSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Artist): List = listOf( - with(YouTubeBrowseInfoSaver) { save(value.info) }, + value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, value.subscribersCountText, with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } ) override fun restore(value: List) = YouTube.Item.Artist( - info = YouTubeBrowseInfoSaver.restore(value[0] as List), + info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), subscribersCountText = value[1] as String?, thumbnail = (value[2] as List?)?.let(YouTubeThumbnailSaver::restore) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt index a421e53..0e2bb9d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeBrowseInfoSaver.kt @@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint object YouTubeBrowseInfoSaver : Saver, List> { override fun SaverScope.save(value: YouTube.Info) = listOf( value.name, - with(YouTubeBrowseEndpointSaver) { value.endpoint?.let { save(it) } } + value.endpoint?.let { with(YouTubeBrowseEndpointSaver) { save(it) } } ) override fun restore(value: List) = YouTube.Info( - name = value[0] as String, + name = value[0] as String?, endpoint = (value[1] as List?)?.let(YouTubeBrowseEndpointSaver::restore) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt index 9767efe..599e6f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubePlaylistSaver.kt @@ -6,14 +6,14 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubePlaylistSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Playlist): List = listOf( - with(YouTubeBrowseInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoSaver) { value.channel?.let { save(it) } }, + value.info?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, + value.channel?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, value.songCount, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) override fun restore(value: List) = YouTube.Item.Playlist( - info = YouTubeBrowseInfoSaver.restore(value[0] as List), + info = (value[0] as List?)?.let(YouTubeBrowseInfoSaver::restore), channel = (value[1] as List?)?.let(YouTubeBrowseInfoSaver::restore), songCount = value[2] as Int?, thumbnail = (value[3] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt new file mode 100644 index 0000000..6024b90 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeRelatedSaver.kt @@ -0,0 +1,22 @@ +package it.vfsfitvnm.vimusic.savers + +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope +import it.vfsfitvnm.youtubemusic.YouTube + +object YouTubeRelatedSaver : Saver> { + override fun SaverScope.save(value: YouTube.Related): List = listOf( + value.songs?.let { with(YouTubeSongListSaver) { save(it) } }, + value.playlists?.let { with(YouTubePlaylistListSaver) { save(it) } }, + value.albums?.let { with(YouTubeAlbumListSaver) { save(it) } }, + value.artists?.let { with(YouTubeArtistListSaver) { save(it) } }, + ) + + @Suppress("UNCHECKED_CAST") + override fun restore(value: List) = YouTube.Related( + songs = (value[0] as List>?)?.let(YouTubeSongListSaver::restore), + playlists = (value[1] as List>?)?.let(YouTubePlaylistListSaver::restore), + albums = (value[2] as List>?)?.let(YouTubeAlbumListSaver::restore), + artists = (value[3] as List>?)?.let(YouTubeArtistListSaver::restore), + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt index 4b8cdf8..848efc0 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeSongSaver.kt @@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeSongSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Song): List = listOf( - with(YouTubeWatchInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, - with(YouTubeBrowseInfoSaver) { value.album?.let { save(it) } }, + value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } }, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } }, + value.album?.let { with(YouTubeBrowseInfoSaver) { save(it) } }, value.durationText, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) @Suppress("UNCHECKED_CAST") override fun restore(value: List) = YouTube.Item.Song( - info = YouTubeWatchInfoSaver.restore(value[0] as List), - authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + info = (value[0] as List?)?.let(YouTubeWatchInfoSaver::restore), + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), album = (value[2] as List?)?.let(YouTubeBrowseInfoSaver::restore), durationText = value[3] as String?, thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt index b745939..1150385 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeVideoSaver.kt @@ -6,17 +6,17 @@ import it.vfsfitvnm.youtubemusic.YouTube object YouTubeVideoSaver : Saver> { override fun SaverScope.save(value: YouTube.Item.Video): List = listOf( - with(YouTubeWatchInfoSaver) { save(value.info) }, - with(YouTubeBrowseInfoListSaver) { value.authors?.let { save(it) } }, + value.info?.let { with(YouTubeWatchInfoSaver) { save(it) } }, + value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } }, value.viewsText, value.durationText, - with(YouTubeThumbnailSaver) { value.thumbnail?.let { save(it) } } + value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ) @Suppress("UNCHECKED_CAST") override fun restore(value: List) = YouTube.Item.Video( - info = YouTubeWatchInfoSaver.restore(value[0] as List), - authors = YouTubeBrowseInfoListSaver.restore(value[1] as List>), + info = (value[0] as List?)?.let(YouTubeWatchInfoSaver::restore), + authors = (value[1] as List>?)?.let(YouTubeBrowseInfoListSaver::restore), viewsText = value[2] as String?, durationText = value[3] as String?, thumbnail = (value[4] as List?)?.let(YouTubeThumbnailSaver::restore) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt index a563724..11c09f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/savers/YouTubeWatchInfoSaver.kt @@ -8,11 +8,11 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint object YouTubeWatchInfoSaver : Saver, List> { override fun SaverScope.save(value: YouTube.Info) = listOf( value.name, - with(YouTubeWatchEndpointSaver) { value.endpoint?.let { save(it) } } + value.endpoint?.let { with(YouTubeWatchEndpointSaver) { save(it) } }, ) override fun restore(value: List) = YouTube.Info( - name = value[0] as String, + name = value[0] as String?, endpoint = (value[1] as List?)?.let(YouTubeWatchEndpointSaver::restore) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt index 5225fbe..a815f6e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/service/PlayerService.kt @@ -65,6 +65,7 @@ import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.MainActivity import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize +import it.vfsfitvnm.vimusic.models.Event import it.vfsfitvnm.vimusic.models.QueuedMediaItem import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.utils.InvincibleService @@ -285,11 +286,23 @@ class PlayerService : InvincibleService(), Player.Listener, PlaybackStatsListene val totalPlayTimeMs = playbackStats.totalPlayTimeMs - if (totalPlayTimeMs > 2000) { + if (totalPlayTimeMs > 5000) { query { Database.incrementTotalPlayTimeMs(mediaItem.mediaId, totalPlayTimeMs) } } + + if (totalPlayTimeMs > 30000) { + query { + Database.insert( + Event( + songId = mediaItem.mediaId, + timestamp = System.currentTimeMillis(), + playTime = totalPlayTimeMs + ) + ) + } + } } override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index 6b9e7cf..43f6a01 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -1,5 +1,6 @@ package it.vfsfitvnm.vimusic.ui.components.themed +import android.content.Intent import android.text.format.DateUtils import androidx.compose.animation.AnimatedContentScope import androidx.compose.animation.ExperimentalAnimationApi @@ -57,7 +58,6 @@ import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.semiBold -import it.vfsfitvnm.vimusic.utils.shareAsYouTubeSong import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOf @@ -241,7 +241,13 @@ fun BaseMediaItemMenu( onGoToAlbum = albumRoute::global, onGoToArtist = artistRoute::global, onShare = { - context.shareAsYouTubeSong(mediaItem) + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}") + } + + context.startActivity(Intent.createChooser(sendIntent, null)) }, modifier = modifier ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt index bb04e88..75a430d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumOverview.kt @@ -95,7 +95,7 @@ fun AlbumOverview( title = youtubeAlbum.title, thumbnailUrl = youtubeAlbum.thumbnail?.url, year = youtubeAlbum.year, - authorsText = youtubeAlbum.authors?.joinToString("") { it.name }, + authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" }, shareUrl = youtubeAlbum.url, timestamp = System.currentTimeMillis() ), diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index d9bf3e0..7f70d1a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -271,7 +271,7 @@ fun ArtistScreen2(browseId: String) { ) { index, song -> SongItem( song = song, - thumbnailSize = songThumbnailSizePx, + thumbnailSizePx = songThumbnailSizePx, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex( @@ -351,7 +351,7 @@ private suspend fun fetchArtist(browseId: String): Result? { ?.map { youtubeArtist -> Artist( id = browseId, - name = youtubeArtist.name, + name = youtubeArtist.name ?: "", thumbnailUrl = youtubeArtist.thumbnail?.url, info = youtubeArtist.description, shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt index 50c9802..fc0cd19 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/LocalPlaylistSongList.kt @@ -103,7 +103,7 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) { ) { index, song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex( diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt index 4fcdd18..3f391af 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylistList.kt @@ -58,8 +58,8 @@ import kotlinx.coroutines.flow.flowOn @ExperimentalFoundationApi @Composable fun HomePlaylistList( - onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit, - onPlaylistClicked: (Playlist) -> Unit, + onBuiltInPlaylist: (BuiltInPlaylist) -> Unit, + onPlaylistClick: (Playlist) -> Unit, ) { val (colorPalette) = LocalAppearance.current @@ -186,7 +186,7 @@ fun HomePlaylistList( .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) } + onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) } ) ) } @@ -200,7 +200,7 @@ fun HomePlaylistList( .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) } + onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) } ) .animateItemPlacement() ) @@ -216,7 +216,7 @@ fun HomePlaylistList( .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { onPlaylistClicked(playlistPreview.playlist) } + onClick = { onPlaylistClick(playlistPreview.playlist) } ) .animateItemPlacement() ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index cbb720f..2be6d05 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -86,30 +86,27 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) { 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) + Item(0, "Quick picks", R.drawable.sparkles) + Item(1, "Songs", R.drawable.musical_notes) + Item(2, "Playlists", R.drawable.playlist) + Item(3, "Artists", R.drawable.person) + Item(4, "Albums", R.drawable.disc) }, primaryIconButtonId = R.drawable.search, onPrimaryIconButtonClick = { searchRoute("") } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { - 1 -> HomePlaylistList( - onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) }, - onPlaylistClicked = { localPlaylistRoute(it.id) } + 0 -> QuickPicks( + onAlbumClick = { albumRoute(it) }, ) - - 2 -> HomeArtistList( - onArtistClick = { artistRoute(it.id) } + 1 -> HomeSongList() + 2 -> HomePlaylistList( + onBuiltInPlaylist = { builtInPlaylistRoute(it) }, + onPlaylistClick = { localPlaylistRoute(it.id) } ) - - 3 -> HomeAlbumList( - onAlbumClick = { albumRoute(it.id) } - ) - - else -> HomeSongList() + 3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) }) + 4 -> HomeAlbumList(onAlbumClick = { albumRoute(it.id) }) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt index 142065a..bd23931 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongList.kt @@ -162,7 +162,7 @@ fun HomeSongList() { ) { index, song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { binder?.stopRadio() binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt new file mode 100644 index 0000000..6321a37 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt @@ -0,0 +1,214 @@ +package it.vfsfitvnm.vimusic.ui.screens.home + +import androidx.compose.animation.ExperimentalAnimationApi +import androidx.compose.animation.animateContentSize +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.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +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.items +import androidx.compose.foundation.rememberScrollState +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.layout.ContentScale +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage +import it.vfsfitvnm.vimusic.Database +import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues +import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder +import it.vfsfitvnm.vimusic.savers.DetailedSongSaver +import it.vfsfitvnm.vimusic.savers.YouTubeRelatedSaver +import it.vfsfitvnm.vimusic.savers.nullableSaver +import it.vfsfitvnm.vimusic.savers.resultSaver +import it.vfsfitvnm.vimusic.ui.components.themed.Header +import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu +import it.vfsfitvnm.vimusic.ui.screens.albumRoute +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.AlbumItem +import it.vfsfitvnm.vimusic.ui.views.SmallSongItem +import it.vfsfitvnm.vimusic.ui.views.SongItem +import it.vfsfitvnm.vimusic.utils.asMediaItem +import it.vfsfitvnm.vimusic.utils.forcePlay +import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState +import it.vfsfitvnm.vimusic.utils.produceSaveableState +import it.vfsfitvnm.vimusic.utils.secondary +import it.vfsfitvnm.vimusic.utils.semiBold +import it.vfsfitvnm.youtubemusic.YouTube +import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn + +@ExperimentalAnimationApi +@Composable +fun QuickPicks( + onAlbumClick: (String) -> Unit +) { + val (colorPalette, typography, thumbnailShape) = LocalAppearance.current + val binder = LocalPlayerServiceBinder.current + + val trending by produceSaveableState( + initialValue = null, + stateSaver = nullableSaver(DetailedSongSaver), + ) { + Database.trending() + .flowOn(Dispatchers.IO) + .filterNotNull() + .distinctUntilChanged() + .collect { value = it } + } + + val relatedResult by produceSaveableOneShotState( + initialValue = null, + stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)), + trending?.id + ) { + println("trendingVideoId: ${trending?.id}") + trending?.id?.let { trendingVideoId -> + value = YouTube.related(trendingVideoId)?.map { related -> + related?.copy( + albums = related.albums?.map { album -> + album.copy( + authors = trending?.artists?.map { info -> + YouTube.Info( + name = info.name, + endpoint = NavigationEndpoint.Endpoint.Browse( + browseId = info.id, + params = null, + browseEndpointContextSupportedConfigs = null + ) + ) + } + ) + } + ) + } + } + } + + val songThumbnailSizePx = Dimensions.thumbnails.song.px + val albumThumbnailSizeDp = 108.dp + val albumThumbnailSizePx = albumThumbnailSizeDp.px +// val itemInHorizontalGridWidth = (LocalConfiguration.current.screenWidthDp.dp) * 0.8f + + LazyColumn( + contentPadding = LocalPlayerAwarePaddingValues.current, + modifier = Modifier + .background(colorPalette.background0) + .fillMaxSize() + ) { + item( + key = "header", + contentType = 0 + ) { + Header(title = "Quick picks") + } + + trending?.let { song -> + item(key = song.id) { + SongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + menuContent = { + NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + } + ) + } + } + + relatedResult?.getOrNull()?.let { related -> + items( + items = related.songs?.take(6) ?: emptyList(), + key = YouTube.Item::key + ) { song -> + SmallSongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + ) + } + + item( + key = "albums", + contentType = "LazyRow" + ) { + LazyRow { + items( + items = related.albums ?: emptyList(), + key = YouTube.Item::key + ) { album -> + AlbumItem( + album = album, + thumbnailSizePx = albumThumbnailSizePx, + thumbnailSizeDp = albumThumbnailSizeDp, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { onAlbumClick(album.key) } + ) + .fillMaxWidth() + ) + } + } + } + + items( + items = related.songs?.drop(6) ?: emptyList(), + key = YouTube.Item::key + ) { song -> + SmallSongItem( + song = song, + thumbnailSizePx = songThumbnailSizePx, + onClick = { + val mediaItem = song.asMediaItem + binder?.stopRadio() + binder?.player?.forcePlay(mediaItem) + binder?.setupRadio( + NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) + ) + }, + ) + } + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt index 9153fc5..4ecd231 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongList.kt @@ -165,11 +165,7 @@ fun LocalPlaylistSongList( transaction { runBlocking(Dispatchers.IO) { withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { - it.next() - }?.map { playlist -> - playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null }) - } + YouTube.playlist(browseId)?.map { it.next() } } }?.getOrNull()?.let { remotePlaylist -> Database.clearPlaylist(playlistId) @@ -222,7 +218,7 @@ fun LocalPlaylistSongList( ) { index, song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { playlistWithSongs?.songs?.map(DetailedSong::asMediaItem) ?.let { mediaItems -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt index 2196223..6267680 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Lyrics.kt @@ -135,7 +135,7 @@ fun Lyrics( )?.map { it?.value } } else { YouTube.next(mediaId, null) - ?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() } + ?.map { nextResult -> nextResult.lyrics()?.getOrNull() } }?.map { newLyrics -> onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "") state = state.copy(isLoading = false) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index ba63067..04220bb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -149,7 +149,7 @@ fun PlayerBottomSheet( SongItem( mediaItem = window.mediaItem, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { if (isPlayingThisMediaItem) { if (shouldBePlaying) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 719ebd8..ca88584 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -81,11 +81,7 @@ fun PlaylistSongList( stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver), ) { value = withContext(Dispatchers.IO) { - YouTube.playlist(browseId)?.map { - it.next() - }?.map { playlist -> - playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null }) - } + YouTube.playlist(browseId)?.map { it.next() } } } @@ -202,8 +198,8 @@ fun PlaylistSongList( itemsIndexed(items = playlist.songs ?: emptyList()) { index, song -> SongItem( - title = song.info.name, - authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name }, + title = song.info?.name, + authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" }, durationText = song.durationText, onClick = { playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 6abda8e..849b64e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -100,7 +100,7 @@ fun LocalSongSearch( ) { song -> SongItem( song = song, - thumbnailSize = thumbnailSize, + thumbnailSizePx = thumbnailSize, onClick = { val mediaItem = song.asMediaItem binder?.stopRadio() diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt index a291171..57b7015 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResult.kt @@ -92,7 +92,7 @@ inline fun SearchResult( items( items = items, - key = { it.key!! }, + key = YouTube.Item::key, itemContent = itemContent ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index e46c1a7..1a75ddb 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -102,7 +102,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { onClick = { binder?.stopRadio() binder?.player?.forcePlay(song.asMediaItem) - binder?.setupRadio(song.info.endpoint) + binder?.setupRadio(song.info?.endpoint) } ) }, @@ -130,7 +130,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { albumRoute(album.info.endpoint?.browseId) } + onClick = { albumRoute(album.info?.endpoint?.browseId) } ) ) @@ -159,7 +159,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { artistRoute(artist.info.endpoint?.browseId) } + onClick = { artistRoute(artist.info?.endpoint?.browseId) } ) ) }, @@ -186,7 +186,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { onClick = { binder?.stopRadio() binder?.player?.forcePlay(video.asMediaItem) - binder?.setupRadio(video.info.endpoint) + binder?.setupRadio(video.info?.endpoint) } ) }, @@ -217,7 +217,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) { .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() }, - onClick = { playlistRoute(playlist.info.endpoint?.browseId) } + onClick = { playlistRoute(playlist.info?.endpoint?.browseId) } ) ) }, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt index a0b7672..7b6749c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt @@ -40,7 +40,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail @NonRestartableComposable fun SongItem( mediaItem: MediaItem, - thumbnailSize: Int, + thumbnailSizePx: Int, onClick: () -> Unit, menuContent: @Composable () -> Unit, modifier: Modifier = Modifier, @@ -48,7 +48,7 @@ fun SongItem( trailingContent: (@Composable () -> Unit)? = null ) { SongItem( - thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSize), + thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx), title = mediaItem.mediaMetadata.title!!.toString(), authors = mediaItem.mediaMetadata.artist.toString(), durationText = mediaItem.mediaMetadata.extras?.getString("durationText") ?: "?", @@ -65,7 +65,7 @@ fun SongItem( @NonRestartableComposable fun SongItem( song: DetailedSong, - thumbnailSize: Int, + thumbnailSizePx: Int, onClick: () -> Unit, menuContent: @Composable () -> Unit, modifier: Modifier = Modifier, @@ -73,7 +73,7 @@ fun SongItem( trailingContent: (@Composable () -> Unit)? = null ) { SongItem( - thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSize), + thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSizePx), title = song.title, authors = song.artistsText ?: "", durationText = song.durationText, @@ -90,8 +90,8 @@ fun SongItem( @NonRestartableComposable fun SongItem( thumbnailModel: Any?, - title: String, - authors: String, + title: String?, + authors: String?, durationText: String?, onClick: () -> Unit, menuContent: @Composable () -> Unit, @@ -131,7 +131,7 @@ fun SongItem( @ExperimentalAnimationApi @Composable fun SongItem( - title: String, + title: String?, authors: String?, durationText: String?, onClick: () -> Unit, @@ -167,7 +167,7 @@ fun SongItem( .weight(1f) ) { BasicText( - text = title, + text = title ?: "", style = typography.xs.semiBold, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt index d62ad6c..35134f2 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/YouTubeItems.kt @@ -79,8 +79,8 @@ fun SmallSongItem( ) { SongItem( thumbnailModel = song.thumbnail?.size(thumbnailSizePx), - title = song.info.name, - authors = song.authors?.joinToString("") { it.name } ?: "", + title = song.info?.name, + authors = song.authors?.joinToString("") { it.name ?: "" }, durationText = song.durationText, onClick = onClick, menuContent = { @@ -148,14 +148,14 @@ fun VideoItem( Column { BasicText( - text = video.info.name, + text = video.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis, ) BasicText( - text = video.authors?.joinToString("") { it.name } ?: "", + text = video.authors?.joinToString("") { it.name ?: "" } ?: "", style = typography.xs.semiBold.secondary, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -252,7 +252,7 @@ fun PlaylistItem( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( - text = playlist.info.name, + text = playlist.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -322,14 +322,14 @@ fun AlbumItem( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( - text = album.info.name, + text = album.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis, ) BasicText( - text = album.authors?.joinToString("") { it.name } ?: "", + text = album.authors?.joinToString("") { it.name ?: "" } ?: "", style = typography.xs.semiBold.secondary, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -406,7 +406,7 @@ fun ArtistItem( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { BasicText( - text = artist.info.name, + text = artist.info?.name ?: "", style = typography.xs.semiBold, maxLines = 2, overflow = TextOverflow.Ellipsis diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 933d87e..8b83ac4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -1,7 +1,5 @@ package it.vfsfitvnm.vimusic.utils -import android.content.Context -import android.content.Intent import android.net.Uri import androidx.core.net.toUri import androidx.core.os.bundleOf @@ -10,37 +8,23 @@ import androidx.media3.common.MediaMetadata import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.youtubemusic.YouTube -fun Context.shareAsYouTubeSong(mediaItem: MediaItem) { - val sendIntent = Intent().apply { - action = Intent.ACTION_SEND - type = "text/plain" - putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}") - } - - startActivity(Intent.createChooser(sendIntent, null)) -} - val YouTube.Item.Song.asMediaItem: MediaItem get() = MediaItem.Builder() - .also { -// println("$this") -// println(info.endpoint?.videoId) - } - .setMediaId(info.endpoint!!.videoId!!) - .setUri(info.endpoint!!.videoId) - .setCustomCacheKey(info.endpoint!!.videoId) + .setMediaId(key) + .setUri(key) + .setCustomCacheKey(key) .setMediaMetadata( MediaMetadata.Builder() - .setTitle(info.name) - .setArtist(authors?.joinToString("") { it.name }) + .setTitle(info?.name) + .setArtist(authors?.joinToString("") { it.name ?: "" }) .setAlbumTitle(album?.name) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( bundleOf( - "videoId" to info.endpoint!!.videoId, + "videoId" to key, "albumId" to album?.endpoint?.browseId, "durationText" to durationText, - "artistNames" to authors?.filter { it.endpoint != null }?.map { it.name }, + "artistNames" to authors?.filter { it.endpoint != null }?.mapNotNull { it.name }, "artistIds" to authors?.mapNotNull { it.endpoint?.browseId }, ) ) @@ -50,19 +34,19 @@ val YouTube.Item.Song.asMediaItem: MediaItem val YouTube.Item.Video.asMediaItem: MediaItem get() = MediaItem.Builder() - .setMediaId(info.endpoint!!.videoId!!) - .setUri(info.endpoint!!.videoId) - .setCustomCacheKey(info.endpoint!!.videoId) + .setMediaId(key) + .setUri(key) + .setCustomCacheKey(key) .setMediaMetadata( MediaMetadata.Builder() - .setTitle(info.name) - .setArtist(authors?.joinToString("") { it.name }) + .setTitle(info?.name) + .setArtist(authors?.joinToString("") { it.name ?: "" }) .setArtworkUri(thumbnail?.url?.toUri()) .setExtras( bundleOf( - "videoId" to info.endpoint!!.videoId, + "videoId" to key, "durationText" to durationText, - "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.map { it.name } else null, + "artistNames" to if (isOfficialMusicVideo) authors?.filter { it.endpoint != null }?.mapNotNull { it.name } else null, "artistIds" to if (isOfficialMusicVideo) authors?.mapNotNull { it.endpoint?.browseId } else null, ) ) diff --git a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt index fdc38e8..5317c1a 100644 --- a/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt +++ b/youtube-music/src/main/kotlin/it/vfsfitvnm/youtubemusic/YouTube.kt @@ -20,13 +20,16 @@ import it.vfsfitvnm.youtubemusic.models.BrowseResponse import it.vfsfitvnm.youtubemusic.models.ContinuationResponse import it.vfsfitvnm.youtubemusic.models.GetQueueResponse import it.vfsfitvnm.youtubemusic.models.GetSearchSuggestionsResponse +import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer import it.vfsfitvnm.youtubemusic.models.MusicResponsiveListItemRenderer import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer +import it.vfsfitvnm.youtubemusic.models.MusicTwoRowItemRenderer import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NextResponse import it.vfsfitvnm.youtubemusic.models.PlayerResponse import it.vfsfitvnm.youtubemusic.models.Runs import it.vfsfitvnm.youtubemusic.models.SearchResponse +import it.vfsfitvnm.youtubemusic.models.SectionListRenderer import it.vfsfitvnm.youtubemusic.models.ThumbnailRenderer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable @@ -36,7 +39,7 @@ import kotlinx.serialization.json.Json object YouTube { private const val Key = "AIzaSyAO_FJ2SlqU8Q4STEHLGCilw_Y9_11qcW8" - val client = HttpClient(OkHttp) { + private val client = HttpClient(OkHttp) { BrowserUserAgent() expectSuccess = true @@ -162,37 +165,34 @@ object YouTube { } data class Info( - val name: String, + val name: String?, val endpoint: T? ) { - companion object { - inline fun from(run: Runs.Run): Info { - return Info( - name = run.text, - endpoint = run.navigationEndpoint?.endpoint as T? - ) - } - } + @Suppress("UNCHECKED_CAST") + constructor(run: Runs.Run) : this( + name = run.text, + endpoint = run.navigationEndpoint?.endpoint as T? + ) } sealed class Item { abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? - abstract val key: String? + abstract val key: String data class Song( - val info: Info, + val info: Info?, val authors: List>?, val album: Info?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { - override val key: String? - get() = info.endpoint?.videoId + override val key: String + get() = info!!.endpoint!!.videoId!! - companion object : FromMusicShelfRendererContent { + companion object { val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D") - override fun from(content: MusicShelfRenderer.Content): Song { + fun from(content: MusicShelfRenderer.Content): Song? { val (mainRuns, otherRuns) = content.runs // Possible configurations: @@ -210,21 +210,22 @@ object YouTube { ?.browseEndpoint ?.type == "MUSIC_PAGE_TYPE_ALBUM" } - ?.let(Info.Companion::from) + ?.let(::Info) return Song( - info = Info.from(mainRuns.first()), + info = mainRuns + .firstOrNull() + ?.let(::Info), authors = otherRuns .getOrNull(otherRuns.lastIndex - if (album == null) 1 else 2) - ?.map(Info.Companion::from) - ?: emptyList(), + ?.map(::Info), album = album, durationText = otherRuns .lastOrNull() ?.firstOrNull()?.text, thumbnail = content .thumbnail - ) + ).takeIf { it.info?.endpoint?.videoId != null } } fun from(renderer: MusicResponsiveListItemRenderer): Song? { @@ -236,15 +237,15 @@ object YouTube { ?.text ?.runs ?.getOrNull(0) - ?.let { Info.from(it) } ?: return null, + ?.let(::Info), authors = renderer .flexColumns .getOrNull(1) ?.musicResponsiveListItemFlexColumnRenderer ?.text ?.runs - ?.map { Info.from(it) } - ?.takeIf { it.isNotEmpty() }, + ?.map>(::Info) + ?.takeIf(List::isNotEmpty), durationText = renderer .fixedColumns ?.getOrNull(0) @@ -260,53 +261,55 @@ object YouTube { ?.text ?.runs ?.firstOrNull() - ?.let { Info.from(it) }, + ?.let(::Info), thumbnail = renderer .thumbnail ?.musicThumbnailRenderer ?.thumbnail ?.thumbnails ?.firstOrNull() - ) + ).takeIf { it.info?.endpoint?.videoId != null } } } } data class Video( - val info: Info, + val info: Info?, val authors: List>?, val viewsText: String?, val durationText: String?, override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? ) : Item() { - override val key: String? - get() = info.endpoint?.videoId + override val key: String + get() = info!!.endpoint!!.videoId!! val isOfficialMusicVideo: Boolean get() = info - .endpoint + ?.endpoint ?.watchEndpointMusicSupportedConfigs ?.watchEndpointMusicConfig ?.musicVideoType == "MUSIC_VIDEO_TYPE_OMV" val isUserGeneratedContent: Boolean get() = info - .endpoint + ?.endpoint ?.watchEndpointMusicSupportedConfigs ?.watchEndpointMusicConfig ?.musicVideoType == "MUSIC_VIDEO_TYPE_UGC" - companion object : FromMusicShelfRendererContent