Code tweaks

This commit is contained in:
vfsfitvnm
2022-09-26 21:36:05 +02:00
parent f981725062
commit 82c2a952aa
15 changed files with 284 additions and 337 deletions

View File

@@ -8,11 +8,11 @@ import androidx.room.PrimaryKey
@Entity @Entity
data class Album( data class Album(
@PrimaryKey val id: String, @PrimaryKey val id: String,
val title: String?, val title: String? = null,
val thumbnailUrl: String? = null, val thumbnailUrl: String? = null,
val year: String? = null, val year: String? = null,
val authorsText: String? = null, val authorsText: String? = null,
val shareUrl: String? = null, val shareUrl: String? = null,
val timestamp: Long?, val timestamp: Long? = null,
val bookmarkedAt: Long? = null val bookmarkedAt: Long? = null
) )

View File

@@ -43,7 +43,9 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Album import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.DetailedSong import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.models.SongAlbumMap
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.AlbumResultSaver
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder 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.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.medium 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.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail 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 @ExperimentalAnimationApi
@ExperimentalFoundationApi @ExperimentalFoundationApi
@Composable @Composable
fun AlbumOverview( fun AlbumOverview(
albumResult: Result<Album>?,
browseId: String, browseId: String,
) { ) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current val context = LocalContext.current
val songs by produceSaveableListState( val albumResult by produceSaveableState(
flowProvider = { initialValue = null,
Database.albumSongs(browseId) 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 stateSaver = DetailedSongListSaver
) {
) Database
.albumSongs(browseId)
.flowOn(Dispatchers.IO)
.collect { value = it }
}
BoxWithConstraints { BoxWithConstraints {
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
@@ -270,6 +317,7 @@ fun AlbumOverview(
modifier = Modifier modifier = Modifier
.padding(LocalPlayerAwarePaddingValues.current) .padding(LocalPlayerAwarePaddingValues.current)
.shimmer() .shimmer()
.fillMaxSize()
) { ) {
HeaderPlaceholder() HeaderPlaceholder()

View File

@@ -3,21 +3,95 @@ package it.vfsfitvnm.vimusic.ui.screens.album
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.R 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.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.toMediaItem //@Stable
import it.vfsfitvnm.youtubemusic.YouTube //class AlbumScreenState(
import kotlinx.coroutines.Dispatchers // initialIsLoading: Boolean = false,
import kotlinx.coroutines.withContext // 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<NavigationEndpoint.Endpoint.Browse>::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<AlbumScreenState, List<Any?>> {
// override fun restore(value: List<Any?>) = AlbumScreenState(
// initialIsLoading = value[0] as Boolean,
// initialError = value[1] as Throwable?,
// initialAlbum = (value[1] as List<Any?>?)?.let(AlbumSaver::restore),
// )
//
// override fun SaverScope.save(value: AlbumScreenState): List<Any?> =
// listOf(
// value.isLoading,
// value.error,
// value.album?.let { with(AlbumSaver) { save(it) } },
//// value.youtubeAlbum?.let { with(YouTubeAlbumSaver) { save(it) } },
// )
//}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@ExperimentalAnimationApi @ExperimentalAnimationApi
@@ -29,59 +103,17 @@ fun AlbumScreen(browseId: String) {
globalRoutes() globalRoutes()
host { 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( Scaffold(
topIconButtonId = R.drawable.chevron_back, topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop, onTopIconButtonClick = pop,
tabIndex = 0, tabIndex = 0,
onTabChanged = {}, onTabChanged = { },
tabColumnContent = { Item -> tabColumnContent = { Item ->
Item(0, "Overview", R.drawable.sparkles) Item(0, "Overview", R.drawable.sparkles)
} }
) { currentTabIndex -> ) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
AlbumOverview( AlbumOverview(browseId = browseId)
albumResult = albumResult,
browseId = browseId,
)
} }
} }
} }

View File

@@ -97,6 +97,9 @@ fun ArtistScreen(browseId: String) {
onTabChanged = onTabIndexChanged, onTabChanged = onTabIndexChanged,
tabColumnContent = { Item -> tabColumnContent = { Item ->
Item(0, "Overview", R.drawable.sparkles) 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 -> ) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {

View File

@@ -50,11 +50,13 @@ import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.albumSortByKey import it.vfsfitvnm.vimusic.utils.albumSortByKey
import it.vfsfitvnm.vimusic.utils.albumSortOrderKey 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.rememberPreference
import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@@ -67,12 +69,16 @@ fun HomeAlbumList(
var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded) var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded)
var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending) var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending)
val items by produceSaveableListState( val items by produceSaveableState(
flowProvider = { Database.albums(sortBy, sortOrder) }, initialValue = emptyList(),
stateSaver = AlbumListSaver, stateSaver = AlbumListSaver,
key1 = sortBy, sortBy, sortOrder,
key2 = sortOrder ) {
) Database
.albums(sortBy, sortOrder)
.flowOn(Dispatchers.IO)
.collect { value = it }
}
val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizeDp = Dimensions.thumbnails.song * 2
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px

View File

@@ -53,10 +53,12 @@ import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.artistSortByKey import it.vfsfitvnm.vimusic.utils.artistSortByKey
import it.vfsfitvnm.vimusic.utils.artistSortOrderKey import it.vfsfitvnm.vimusic.utils.artistSortOrderKey
import it.vfsfitvnm.vimusic.utils.center 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.rememberPreference
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@@ -69,12 +71,16 @@ fun HomeArtistList(
var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded) var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded)
var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending) var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending)
val items by produceSaveableListState( val items by produceSaveableState(
flowProvider = { Database.artists(sortBy, sortOrder) }, initialValue = emptyList(),
stateSaver = ArtistListSaver, stateSaver = ArtistListSaver,
key1 = sortBy, sortBy, sortOrder,
key2 = sortOrder ) {
) Database
.artists(sortBy, sortOrder)
.flowOn(Dispatchers.IO)
.collect { value = it }
}
val thumbnailSizeDp = Dimensions.thumbnails.song * 2 val thumbnailSizeDp = Dimensions.thumbnails.song * 2
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px

View File

@@ -53,8 +53,10 @@ import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
import it.vfsfitvnm.vimusic.utils.medium import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.playlistSortByKey import it.vfsfitvnm.vimusic.utils.playlistSortByKey
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey 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 it.vfsfitvnm.vimusic.utils.rememberPreference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi @ExperimentalFoundationApi
@Composable @Composable
@@ -85,12 +87,16 @@ fun HomePlaylistList(
var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded) var sortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending) var sortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
val items by produceSaveableListState( val items by produceSaveableState(
flowProvider = { Database.playlistPreviews(sortBy, sortOrder) }, initialValue = emptyList(),
stateSaver = PlaylistPreviewListSaver, stateSaver = PlaylistPreviewListSaver,
key1 = sortBy, sortBy, sortOrder,
key2 = sortOrder ) {
) Database
.playlistPreviews(sortBy, sortOrder)
.flowOn(Dispatchers.IO)
.collect { value = it }
}
val sortOrderIconRotation by animateFloatAsState( val sortOrderIconRotation by animateFloatAsState(
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,

View File

@@ -52,17 +52,18 @@ import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.color import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex 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.rememberPreference
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.songSortByKey import it.vfsfitvnm.vimusic.utils.songSortByKey
import it.vfsfitvnm.vimusic.utils.songSortOrderKey import it.vfsfitvnm.vimusic.utils.songSortOrderKey
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun HomeSongList() { fun HomeSongList() {
println("[${System.currentTimeMillis()}] HomeSongList")
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@@ -71,33 +72,16 @@ fun HomeSongList() {
var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded) var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending) var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
val items by produceSaveableListState( val items by produceSaveableState(
flowProvider = { Database.songs(sortBy, sortOrder) }, initialValue = emptyList(),
stateSaver = DetailedSongListSaver, stateSaver = DetailedSongListSaver,
key1 = sortBy, sortBy, sortOrder,
key2 = sortOrder ) {
) Database
.songs(sortBy, sortOrder)
// var items by rememberSaveable(stateSaver = DetailedSongListSaver) { .flowOn(Dispatchers.IO)
// mutableStateOf(emptyList()) .collect { value = it }
// } }
//
// 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( val sortOrderIconRotation by animateFloatAsState(
targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f, targetValue = if (sortOrder == SortOrder.Ascending) 0f else 180f,
@@ -110,8 +94,6 @@ fun HomeSongList() {
.background(colorPalette.background0) .background(colorPalette.background0)
.fillMaxSize() .fillMaxSize()
) { ) {
// println("[${System.currentTimeMillis()}] LazyColumn")
item( item(
key = "header", key = "header",
contentType = 0 contentType = 0
@@ -174,10 +156,8 @@ fun HomeSongList() {
song = song, song = song,
thumbnailSize = thumbnailSize, thumbnailSize = thumbnailSize,
onClick = { onClick = {
items.map(DetailedSong::asMediaItem)?.let { mediaItems -> binder?.stopRadio()
binder?.stopRadio() binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
}, },
menuContent = { menuContent = {
InHistoryMediaItemMenu(song = song) InHistoryMediaItemMenu(song = song)
@@ -192,9 +172,7 @@ fun HomeSongList() {
) { ) {
BasicText( BasicText(
text = song.formattedTotalPlayTime, text = song.formattedTotalPlayTime,
style = typography.xxs.semiBold.center.color( style = typography.xxs.semiBold.center.color(Color.White),
Color.White
),
maxLines = 2, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier

View File

@@ -41,9 +41,15 @@ import it.vfsfitvnm.vimusic.utils.align
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.medium 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.secondary
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
//context(ProduceStateScope<T>)
//fun <T> Flow<T>.distinctUntilChangedWithProducedState() =
// distinctUntilChanged { old, new -> new != old && new != value }
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@@ -55,13 +61,16 @@ fun LocalSongSearch(
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val items by produceSaveableListState( val items by produceSaveableState(
flowProvider = { initialValue = emptyList(),
Database.search("%${textFieldValue.text}%")
},
stateSaver = DetailedSongListSaver, stateSaver = DetailedSongListSaver,
key1 = textFieldValue.text key1 = textFieldValue.text
) ) {
Database
.search("%${textFieldValue.text}%")
.flowOn(Dispatchers.IO)
.collect { value = it }
}
val thumbnailSize = Dimensions.thumbnails.song.px val thumbnailSize = Dimensions.thumbnails.song.px

View File

@@ -52,12 +52,14 @@ import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.align import it.vfsfitvnm.vimusic.utils.align
import it.vfsfitvnm.vimusic.utils.medium 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.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
@Composable @Composable
fun OnlineSearch( fun OnlineSearch(
@@ -69,17 +71,18 @@ fun OnlineSearch(
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val history by produceSaveableListState( val history by produceSaveableState(
flowProvider = { initialValue = emptyList(),
Database.queries("%${textFieldValue.text}%").distinctUntilChanged { old, new ->
old.size == new.size
}
},
stateSaver = SearchQueryListSaver, stateSaver = SearchQueryListSaver,
key1 = textFieldValue.text 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, initialValue = null,
stateSaver = StringListResultSaver, stateSaver = StringListResultSaver,
key1 = textFieldValue.text key1 = textFieldValue.text

View File

@@ -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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
import it.vfsfitvnm.vimusic.ui.views.SearchResultLoadingOrError 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 it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@@ -41,11 +41,10 @@ inline fun <T : YouTube.Item> SearchResult(
mutableStateOf(listOf()) mutableStateOf(listOf())
} }
val (continuationResultState, fetch) = produceSaveableRelaunchableState( val (continuationResultState, fetch) = produceSaveableRelaunchableOneShotState(
initialValue = null, initialValue = null,
stateSaver = StringResultSaver, stateSaver = StringResultSaver,
key1 = query, query, filter
key2 = filter
) { ) {
val token = value?.getOrNull() val token = value?.getOrNull()

View File

@@ -90,7 +90,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px
SearchResult<YouTube.Item.Song>( SearchResult(
query = query, query = query,
filter = searchFilter, filter = searchFilter,
onSearchAgain = onSearchAgain, onSearchAgain = onSearchAgain,

View File

@@ -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 <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
): State<List<T>> {
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 <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
key1: Any?,
): State<List<T>> {
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 <T> produceSaveableListState(
flowProvider: () -> Flow<List<T>>,
stateSaver: ListSaver<T, List<Any?>>,
key1: Any?,
key2: Any?,
): State<List<T>> {
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
}

View File

@@ -1,3 +1,5 @@
@file:OptIn(ExperimentalTypeInference::class)
package it.vfsfitvnm.vimusic.utils package it.vfsfitvnm.vimusic.utils
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -14,27 +16,23 @@ import kotlin.coroutines.CoroutineContext
import kotlin.experimental.ExperimentalTypeInference import kotlin.experimental.ExperimentalTypeInference
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
@OptIn(ExperimentalTypeInference::class)
@Composable @Composable
fun <T> produceSaveableState( fun <T> produceSaveableState(
initialValue: T, initialValue: T,
stateSaver: Saver<T, out Any>, stateSaver: Saver<T, out Any>,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> { ): State<T> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } val result = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(initialValue)
var hasToFetch by rememberSaveable { mutableStateOf(true) } }
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (hasToFetch) { ProduceSaveableStateScope(result, coroutineContext).producer()
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
} }
return result return result
} }
@OptIn(ExperimentalTypeInference::class)
@Composable @Composable
fun <T> produceSaveableState( fun <T> produceSaveableState(
initialValue: T, initialValue: T,
@@ -42,20 +40,42 @@ fun <T> produceSaveableState(
key1: Any?, key1: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> { ): State<T> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } val state = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(initialValue)
var hasToFetch by rememberSaveable(key1) { mutableStateOf(true) } }
LaunchedEffect(key1) { LaunchedEffect(key1) {
if (hasToFetch) { ProduceSaveableStateScope(state, coroutineContext).producer()
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
} }
return result
return state
}
@Composable
fun <T> produceSaveableOneShotState(
initialValue: T,
stateSaver: Saver<T, out Any>,
key1: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> {
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 @Composable
fun <T> produceSaveableState( fun <T> produceSaveableState(
initialValue: T, initialValue: T,
@@ -64,42 +84,42 @@ fun <T> produceSaveableState(
key2: Any?, key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): State<T> { ): State<T> {
val result = rememberSaveable(stateSaver = stateSaver) { mutableStateOf(initialValue) } val result = rememberSaveable(stateSaver = stateSaver) {
mutableStateOf(initialValue)
}
var hasToFetch by rememberSaveable(key1, key2) { mutableStateOf(true) } LaunchedEffect(key1, key2) {
ProduceSaveableStateScope(result, coroutineContext).producer()
LaunchedEffect(Unit) {
if (hasToFetch) {
ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false
}
} }
return result return result
} }
@OptIn(ExperimentalTypeInference::class)
@Composable @Composable
fun <T> produceSaveableRelaunchableState( fun <T> produceSaveableRelaunchableOneShotState(
initialValue: T, initialValue: T,
stateSaver: Saver<T, out Any>, stateSaver: Saver<T, out Any>,
key1: Any?, key1: Any?,
key2: Any?, key2: Any?,
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit @BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
): Pair<State<T>, () -> Unit> { ): Pair<State<T>, () -> 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) { val relaunchableEffect = relaunchableEffect(key1, key2) {
if (hasToFetch) { if (!produced) {
ProduceSaveableStateScope(result, coroutineContext).producer() ProduceSaveableStateScope(result, coroutineContext).producer()
hasToFetch = false produced = true
} }
} }
return result to { return result to {
hasToFetch = true produced = false
relaunchableEffect() relaunchableEffect()
} }
} }

View File

@@ -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<LazyListState> {
return rememberSaveable(
saver = listSaver(
save = { states: List<LazyListState> ->
List(states.size * 2) {
when (it % 2) {
0 -> states[it / 2].firstVisibleItemIndex
1 -> states[it / 2].firstVisibleItemScrollOffset
else -> error("unreachable")
}
}
},
restore = { states ->
List(states.size / 2) {
LazyListState(
firstVisibleItemIndex = states[it * 2],
firstVisibleItemScrollOffset = states[it * 2 + 1]
)
}
}
)
) {
List(count) { LazyListState(0, 0) }
}
}
@Composable
fun rememberLazyGridStates(count: Int): List<LazyGridState> {
return rememberSaveable(
saver = listSaver(
save = { states: List<LazyGridState> ->
List(states.size * 2) {
when (it % 2) {
0 -> states[it / 2].firstVisibleItemIndex
1 -> states[it / 2].firstVisibleItemScrollOffset
else -> error("unreachable")
}
}
},
restore = { states ->
List(states.size / 2) {
LazyGridState(
firstVisibleItemIndex = states[it * 2],
firstVisibleItemScrollOffset = states[it * 2 + 1]
)
}
}
)
) {
List(count) { LazyGridState(0, 0) }
}
}