diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt index efb9016..2b6c2ce 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/Database.kt @@ -153,9 +153,6 @@ interface Database { @Query("SELECT * FROM Artist WHERE id = :id") fun artist(id: String): Flow - @Query("SELECT timestamp FROM Artist WHERE id = :id") - fun artistTimestamp(id: String): Long? - @Query("SELECT * FROM Artist WHERE bookmarkedAt IS NOT NULL ORDER BY name DESC") fun artistsByNameDesc(): Flow> 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 6401e4f..a89dc62 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 @@ -6,10 +6,13 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Spacer import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -38,12 +41,11 @@ import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage 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.produceSaveableState import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.requests.albumPage import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.withContext @ExperimentalFoundationApi @@ -52,63 +54,61 @@ import kotlinx.coroutines.withContext fun AlbumScreen(browseId: String) { val saveableStateHolder = rememberSaveableStateHolder() - val (tabIndex, onTabChanged) = rememberSaveable { + var tabIndex by rememberSaveable { mutableStateOf(0) } - val album by produceSaveableState( - initialValue = null, - stateSaver = nullableSaver(AlbumSaver), - ) { - Database - .album(browseId) - .flowOn(Dispatchers.IO) - .collect { value = it } + var album by rememberSaveable(stateSaver = nullableSaver(AlbumSaver)) { + mutableStateOf(null) } - val innertubeAlbum by produceSaveableState( - initialValue = null, - stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver), - tabIndex > 0 - ) { - if (value != null || (tabIndex == 0 && withContext(Dispatchers.IO) { - Database.albumTimestamp( - browseId - ) - } != null)) return@produceSaveableState + var albumPage by rememberSaveable(stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver)) { + mutableStateOf(null) + } - withContext(Dispatchers.IO) { - Innertube.albumPage(BrowseBody(browseId = browseId)) - }?.onSuccess { albumPage -> - value = albumPage + LaunchedEffect(Unit) { + Database + .album(browseId) + .combine(snapshotFlow { tabIndex }) { album, tabIndex -> album to tabIndex } + .collect { (currentAlbum, tabIndex) -> + album = currentAlbum - query { - Database.upsert( - Album( - id = browseId, - title = albumPage.title, - thumbnailUrl = albumPage.thumbnail?.url, - year = albumPage.year, - authorsText = albumPage.authors?.joinToString("") { it.name ?: "" }, - shareUrl = albumPage.url, - timestamp = System.currentTimeMillis(), - bookmarkedAt = album?.bookmarkedAt - ), - albumPage - .songsPage - ?.items - ?.map(Innertube.SongItem::asMediaItem) - ?.onEach(Database::insert) - ?.mapIndexed { position, mediaItem -> - SongAlbumMap( - songId = mediaItem.mediaId, - albumId = browseId, - position = position - ) - } ?: emptyList() - ) + if (albumPage == null && (currentAlbum?.timestamp == null || tabIndex == 1)) { + withContext(Dispatchers.IO) { + Innertube.albumPage(BrowseBody(browseId = browseId)) + ?.onSuccess { currentAlbumPage -> + albumPage = currentAlbumPage + + Database.upsert( + Album( + id = browseId, + title = currentAlbumPage.title, + thumbnailUrl = currentAlbumPage.thumbnail?.url, + year = currentAlbumPage.year, + authorsText = currentAlbumPage.authors + ?.joinToString("") { it.name ?: "" }, + shareUrl = currentAlbumPage.url, + timestamp = System.currentTimeMillis(), + bookmarkedAt = album?.bookmarkedAt + ), + currentAlbumPage + .songsPage + ?.items + ?.map(Innertube.SongItem::asMediaItem) + ?.onEach(Database::insert) + ?.mapIndexed { position, mediaItem -> + SongAlbumMap( + songId = mediaItem.mediaId, + albumId = browseId, + position = position + ) + } ?: emptyList() + ) + } + } + + } } - } } RouteHandler(listenToGlobalEmitter = true) { @@ -184,7 +184,7 @@ fun AlbumScreen(browseId: String) { topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, tabIndex = tabIndex, - onTabChanged = onTabChanged, + onTabChanged = { tabIndex = it }, tabColumnContent = { Item -> Item(0, "Songs", R.drawable.musical_notes) Item(1, "Other versions", R.drawable.disc) @@ -208,11 +208,11 @@ fun AlbumScreen(browseId: String) { initialPlaceholderCount = 1, continuationPlaceholderCount = 1, emptyItemsText = "This album doesn't have any alternative version", - itemsPageProvider = innertubeAlbum?.let { + itemsPageProvider = albumPage?.let { ({ Result.success( Innertube.ItemsPage( - items = innertubeAlbum?.otherVersions, + items = albumPage?.otherVersions, continuation = null ) ) 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 3ce5af4..905515f 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 @@ -8,8 +8,13 @@ import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.shape.CircleShape import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -45,7 +50,6 @@ import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.forcePlay -import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody @@ -54,7 +58,9 @@ import it.vfsfitvnm.youtubemusic.requests.artistPage import it.vfsfitvnm.youtubemusic.requests.itemsPage import it.vfsfitvnm.youtubemusic.utils.from import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.withContext @ExperimentalFoundationApi @@ -63,49 +69,43 @@ import kotlinx.coroutines.withContext fun ArtistScreen(browseId: String) { val saveableStateHolder = rememberSaveableStateHolder() - val (tabIndex, onTabIndexChanged) = rememberPreference( - artistScreenTabIndexKey, - defaultValue = 0 - ) + var tabIndex by rememberPreference(artistScreenTabIndexKey, defaultValue = 0) - val artist by produceSaveableState( - initialValue = null, - stateSaver = nullableSaver(ArtistSaver), - ) { - Database - .artist(browseId) - .flowOn(Dispatchers.IO) - .collect { value = it } + var artist by rememberSaveable(stateSaver = nullableSaver(ArtistSaver)) { + mutableStateOf(null) } - val youtubeArtist by produceSaveableState( - initialValue = null, - stateSaver = nullableSaver(InnertubeArtistPageSaver), - tabIndex < 4 - ) { - if (value != null || (tabIndex == 4 && withContext(Dispatchers.IO) { - Database.artistTimestamp( - browseId - ) - } != null)) return@produceSaveableState + var artistPage by rememberSaveable(stateSaver = nullableSaver(InnertubeArtistPageSaver)) { + mutableStateOf(null) + } - withContext(Dispatchers.IO) { - Innertube.artistPage(BrowseBody(browseId = browseId)) - }?.onSuccess { artistPage -> - value = artistPage + LaunchedEffect(Unit) { + Database + .artist(browseId) + .combine(snapshotFlow { tabIndex }.map { it != 4 }) { artist, mustFetch -> artist to mustFetch } + .distinctUntilChanged() + .collect { (currentArtist, mustFetch) -> + artist = currentArtist - query { - Database.upsert( - Artist( - id = browseId, - name = artistPage.name, - thumbnailUrl = artistPage.thumbnail?.url, - timestamp = System.currentTimeMillis(), - bookmarkedAt = artist?.bookmarkedAt - ) - ) + if (artistPage == null && (currentArtist?.timestamp == null || mustFetch)) { + withContext(Dispatchers.IO) { + Innertube.artistPage(BrowseBody(browseId = browseId)) + ?.onSuccess { currentArtistPage -> + artistPage = currentArtistPage + + Database.upsert( + Artist( + id = browseId, + name = currentArtistPage.name, + thumbnailUrl = currentArtistPage.thumbnail?.url, + timestamp = System.currentTimeMillis(), + bookmarkedAt = currentArtist?.bookmarkedAt + ) + ) + } + } + } } - } } RouteHandler(listenToGlobalEmitter = true) { @@ -181,7 +181,7 @@ fun ArtistScreen(browseId: String) { topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, tabIndex = tabIndex, - onTabChanged = onTabIndexChanged, + onTabChanged = { tabIndex = it }, tabColumnContent = { Item -> Item(0, "Overview", R.drawable.sparkles) Item(1, "Songs", R.drawable.musical_notes) @@ -193,13 +193,13 @@ fun ArtistScreen(browseId: String) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { when (currentTabIndex) { 0 -> ArtistOverview( - youtubeArtistPage = youtubeArtist, + youtubeArtistPage = artistPage, thumbnailContent = thumbnailContent, headerContent = headerContent, onAlbumClick = { albumRoute(it) }, - onViewAllSongsClick = { onTabIndexChanged(1) }, - onViewAllAlbumsClick = { onTabIndexChanged(2) }, - onViewAllSinglesClick = { onTabIndexChanged(3) }, + onViewAllSongsClick = { tabIndex = 1 }, + onViewAllAlbumsClick = { tabIndex = 2 }, + onViewAllSinglesClick = { tabIndex = 3 }, ) 1 -> { @@ -211,14 +211,14 @@ fun ArtistScreen(browseId: String) { ItemsPage( stateSaver = InnertubeSongsPageSaver, headerContent = headerContent, - itemsPageProvider = youtubeArtist?.let { + itemsPageProvider = artistPage?.let { ({ continuation -> continuation?.let { Innertube.itemsPage( body = ContinuationBody(continuation = continuation), fromMusicResponsiveListItemRenderer = Innertube.SongItem::from, ) - } ?: youtubeArtist + } ?: artistPage ?.songsEndpoint ?.takeIf { it.browseId != null } ?.let { endpoint -> @@ -232,7 +232,7 @@ fun ArtistScreen(browseId: String) { } ?: Result.success( Innertube.ItemsPage( - items = youtubeArtist?.songs, + items = artistPage?.songs, continuation = null ) ) @@ -275,14 +275,14 @@ fun ArtistScreen(browseId: String) { stateSaver = InnertubeAlbumsPageSaver, headerContent = headerContent, emptyItemsText = "This artist didn't release any album", - itemsPageProvider = youtubeArtist?.let { + itemsPageProvider = artistPage?.let { ({ continuation -> continuation?.let { Innertube.itemsPage( body = ContinuationBody(continuation = continuation), fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, ) - } ?: youtubeArtist + } ?: artistPage ?.albumsEndpoint ?.takeIf { it.browseId != null } ?.let { endpoint -> @@ -296,7 +296,7 @@ fun ArtistScreen(browseId: String) { } ?: Result.success( Innertube.ItemsPage( - items = youtubeArtist?.albums, + items = artistPage?.albums, continuation = null ) ) @@ -325,14 +325,14 @@ fun ArtistScreen(browseId: String) { stateSaver = InnertubeAlbumsPageSaver, headerContent = headerContent, emptyItemsText = "This artist didn't release any single", - itemsPageProvider = youtubeArtist?.let { + itemsPageProvider = artistPage?.let { ({ continuation -> continuation?.let { Innertube.itemsPage( body = ContinuationBody(continuation = continuation), fromMusicTwoRowItemRenderer = Innertube.AlbumItem::from, ) - } ?: youtubeArtist + } ?: artistPage ?.singlesEndpoint ?.takeIf { it.browseId != null } ?.let { endpoint -> @@ -346,7 +346,7 @@ fun ArtistScreen(browseId: String) { } ?: Result.success( Innertube.ItemsPage( - items = youtubeArtist?.singles, + items = artistPage?.singles, continuation = null ) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt index 8eaa887..a5729af 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt @@ -9,8 +9,11 @@ import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn @@ -29,11 +32,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.only import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.enums.SongSortBy diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt index 2495f56..ce03e3f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/Controls.kt @@ -21,10 +21,11 @@ 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.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.autoSaver +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -47,7 +48,6 @@ import it.vfsfitvnm.vimusic.ui.styling.favoritesIcon import it.vfsfitvnm.vimusic.utils.bold import it.vfsfitvnm.vimusic.utils.forceSeekToNext import it.vfsfitvnm.vimusic.utils.forceSeekToPrevious -import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.rememberRepeatMode import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.semiBold @@ -76,16 +76,16 @@ fun Controls( mutableStateOf(null) } - val likedAt by produceSaveableState( - initialValue = null, - stateSaver = autoSaver(), - mediaId - ) { + var likedAt by rememberSaveable { + mutableStateOf(null) + } + + LaunchedEffect(mediaId) { Database .likedAt(mediaId) .flowOn(Dispatchers.IO) .distinctUntilChanged() - .collect { value = it } + .collect { likedAt = it } } val shouldBePlayingTransition = updateTransition(shouldBePlaying, label = "shouldBePlaying") 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 f9f8ae0..1712173 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 @@ -7,8 +7,11 @@ 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.WindowInsetsSides +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -22,8 +25,11 @@ 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.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.autoSaver +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 @@ -41,9 +47,6 @@ import androidx.compose.ui.unit.dp import androidx.core.net.toUri import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwareWindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides -import androidx.compose.foundation.layout.asPaddingValues -import androidx.compose.foundation.layout.only import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query @@ -57,7 +60,6 @@ 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 import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.youtubemusic.Innertube @@ -90,13 +92,15 @@ fun OnlineSearch( .collect { value = it } } - val suggestionsResult by produceSaveableOneShotState( - initialValue = null, - stateSaver = resultSaver(autoSaver?>()), - textFieldValue.text - ) { + var suggestionsResult by rememberSaveable(stateSaver = resultSaver(autoSaver?>())) { + mutableStateOf(null) + } + + LaunchedEffect(textFieldValue.text) { if (textFieldValue.text.isNotEmpty()) { - value = Innertube.searchSuggestions(SearchSuggestionsBody(input = textFieldValue.text)) + delay(200) + suggestionsResult = + Innertube.searchSuggestions(SearchSuggestionsBody(input = textFieldValue.text)) } } 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 cf5ac25..0e566d1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/ProduceSaveableState.kt @@ -7,11 +7,9 @@ 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 @@ -51,31 +49,6 @@ fun produceSaveableState( 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 -} - @Composable fun produceSaveableState( initialValue: T,