Drop ViewModel

This commit is contained in:
vfsfitvnm
2022-09-26 14:52:39 +02:00
parent 29b4a8f5da
commit f981725062
69 changed files with 1269 additions and 2174 deletions

View File

@@ -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<String?>("albumRoute")
val artistRoute = Route1<String?>("artistRoute")

View File

@@ -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<Album>?,
browseId: String,
viewModel: AlbumOverviewViewModel = viewModel(
key = browseId,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): 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)

View File

@@ -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<Result<AlbumWithSongs?>?>(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)
}
}
}
}
}

View File

@@ -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,
)
}
}
}

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): 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()
// }
// }
// }
// }
// }
}

View File

@@ -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<Result<AlbumWithSongs?>?>(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)
}
}
}
}
}

View File

@@ -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)

View File

@@ -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(

View File

@@ -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<Album>())
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<Application>().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
}
}
}
}

View File

@@ -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(

View File

@@ -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<Artist>())
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<Application>().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
}
}
}
}

View File

@@ -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(

View File

@@ -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<PlaylistPreview>())
private set
var sortBy by mutableStatePreferenceOf(
preferences.getEnum(
playlistSortByKey,
PlaylistSortBy.DateAdded
)
) {
preferences.edit { putEnum(playlistSortByKey, it) }
collectItems(sortBy = it)
}
var sortOrder by mutableStatePreferenceOf(
preferences.getEnum(
playlistSortOrderKey,
SortOrder.Ascending
)
) {
preferences.edit { putEnum(playlistSortOrderKey, it) }
collectItems(sortOrder = it)
}
private var job: Job? = null
private val preferences: SharedPreferences
get() = getApplication<Application>().preferences
init {
collectItems()
}
private fun collectItems(
sortBy: PlaylistSortBy = this.sortBy,
sortOrder: SortOrder = this.sortOrder
) {
job?.cancel()
job = viewModelScope.launch {
Database.playlistPreviews(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
items = it
}
}
}
}

View File

@@ -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

View File

@@ -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<DetailedSong>())
private set
var sortBy by mutableStatePreferenceOf(
preferences.getEnum(
songSortByKey,
SongSortBy.DateAdded
)
) {
preferences.edit { putEnum(songSortByKey, it) }
collectItems(sortBy = it)
}
var sortOrder by mutableStatePreferenceOf(
preferences.getEnum(
songSortOrderKey,
SortOrder.Ascending
)
) {
preferences.edit { putEnum(songSortOrderKey, it) }
collectItems(sortOrder = it)
}
private var job: Job? = null
private val preferences: SharedPreferences
get() = getApplication<Application>().preferences
init {
collectItems()
}
private fun collectItems(sortBy: SongSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
job?.cancel()
job = viewModelScope.launch {
Database.songs(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
items = it
}
}
}
}

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): 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(

View File

@@ -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<DetailedSong>())
private set
init {
if (text.isNotEmpty()) {
viewModelScope.launch {
Database.search("%$text%").collect {
items = it
}
}
}
}
}

View File

@@ -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 <T : ViewModel> create(modelClass: Class<T>): 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) {}
}

View File

@@ -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<SearchQuery>())
private set
var suggestionsResult by mutableStateOf<Result<List<String>?>?>(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)
}
}
}
}

View File

@@ -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 <I : YouTube.Item> ItemSearchResult(
inline fun <T : YouTube.Item> SearchResult(
query: String,
filter: String,
stateSaver: ListSaver<T, List<Any?>>,
crossinline onSearchAgain: () -> Unit,
viewModel: SearchResultViewModel<I> = viewModel(
key = query + filter,
factory = object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
@Suppress("UNCHECKED_CAST")
return SearchResultViewModel<I>(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<T>).distinctBy(YouTube.Item::key)
searchResult.continuation
}
}
val continuationResult by continuationResultState
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
@@ -60,27 +83,27 @@ inline fun <I : YouTube.Item> 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 <I : YouTube.Item> ItemSearchResult(
}
} ?: item(key = "loading") {
SearchResultLoadingOrError(
itemCount = if (viewModel.items.isEmpty()) 8 else 3,
itemCount = if (items.isEmpty()) 8 else 3,
shimmerContent = itemShimmer
)
}

View File

@@ -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<YouTube.Item.Song>(
SearchResult<YouTube.Item.Song>(
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<YouTube.Item.Album>(
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<YouTube.Item.Artist>(
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<YouTube.Item.Video>(
SearchResult<YouTube.Item.Video>(
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<YouTube.Item.Playlist>(
SearchResult<YouTube.Item.Playlist>(
query = query,
filter = searchFilter,
stateSaver = YouTubePlaylistListSaver,
onSearchAgain = onSearchAgain,
itemContent = { playlist ->
PlaylistItem(

View File

@@ -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<T : YouTube.Item>(
private val query: String,
private val filter: String
) : ViewModel() {
var items by mutableStateOf(listOf<T>())
var continuationResult by mutableStateOf<Result<String?>?>(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<T>).distinctBy(YouTube.Item::key)
searchResult.continuation
}
}
}
}

View File

@@ -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,