Drop ViewModel
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user