diff --git a/app/src/main/java/com/zionhuang/music/MainActivity.kt b/app/src/main/java/com/zionhuang/music/MainActivity.kt index cbd3f45d3..db7d04134 100644 --- a/app/src/main/java/com/zionhuang/music/MainActivity.kt +++ b/app/src/main/java/com/zionhuang/music/MainActivity.kt @@ -344,6 +344,8 @@ class MainActivity : ComponentActivity() { songs.firstOrNull()?.album?.id?.let { browseId -> navController.navigate("album/$browseId") } + }.onFailure { + it.printStackTrace() } } } else { @@ -365,6 +367,8 @@ class MainActivity : ComponentActivity() { YouTube.queue(listOf(videoId)) }.onSuccess { sharedSong = it.firstOrNull() + }.onFailure { + it.printStackTrace() } } } diff --git a/app/src/main/java/com/zionhuang/music/constants/StatPeriod.kt b/app/src/main/java/com/zionhuang/music/constants/StatPeriod.kt new file mode 100644 index 000000000..6127aae46 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/constants/StatPeriod.kt @@ -0,0 +1,18 @@ +package com.zionhuang.music.constants + +import java.time.LocalDateTime +import java.time.ZoneOffset + +enum class StatPeriod { + `1_WEEK`, `1_MONTH`, `3_MONTH`, `6_MONTH`, `1_YEAR`, ALL; + + fun toTimeMillis(): Long = + when (this) { + `1_WEEK` -> LocalDateTime.now().minusWeeks(1).toInstant(ZoneOffset.UTC).toEpochMilli() + `1_MONTH` -> LocalDateTime.now().minusMonths(1).toInstant(ZoneOffset.UTC).toEpochMilli() + `3_MONTH` -> LocalDateTime.now().minusMonths(3).toInstant(ZoneOffset.UTC).toEpochMilli() + `6_MONTH` -> LocalDateTime.now().minusMonths(6).toInstant(ZoneOffset.UTC).toEpochMilli() + `1_YEAR` -> LocalDateTime.now().minusMonths(12).toInstant(ZoneOffset.UTC).toEpochMilli() + ALL -> 0 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt b/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt index 961dc5191..72a016433 100644 --- a/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt +++ b/app/src/main/java/com/zionhuang/music/db/DatabaseDao.kt @@ -43,6 +43,7 @@ interface DatabaseDao { song.artists.joinToString(separator = "") { it.name } } } + SongSortType.PLAY_TIME -> songsByPlayTimeAsc() }.map { it.reversed(descending) } @@ -71,6 +72,7 @@ interface DatabaseDao { song.artists.joinToString(separator = "") { it.name } } } + SongSortType.PLAY_TIME -> likedSongsByPlayTimeAsc() }.map { it.reversed(descending) } @@ -142,29 +144,44 @@ interface DatabaseDao { fun quickPicks(now: Long = System.currentTimeMillis()): Flow> @Transaction - @Query("SELECT * FROM song ORDER BY totalPlayTime DESC LIMIT :limit") - fun mostPlayedSongs(limit: Int = 6): Flow> + @Query( + """ + SELECT * + FROM song + WHERE id IN (SELECT songId + FROM event + WHERE timestamp > :fromTimeStamp + GROUP BY songId + ORDER BY SUM(playTime) DESC + LIMIT :limit) + """ + ) + fun mostPlayedSongs(fromTimeStamp: Long, limit: Int = 6): Flow> @Transaction @Query( """ - SELECT artist.*, + SELECT *, (SELECT COUNT(1) FROM song_artist_map JOIN song ON song_artist_map.songId = song.id WHERE artistId = artist.id AND song.inLibrary IS NOT NULL) AS songCount - FROM (SELECT artistId, SUM(playtime) AS totalPlaytime - FROM (SELECT *, (SELECT totalPlayTime FROM song WHERE id = songId) AS playtime - FROM song_artist_map) - GROUP BY artistId) - JOIN artist - ON artist.id = artistId - ORDER BY totalPlaytime DESC - LIMIT :limit + FROM artist + JOIN(SELECT artistId, SUM(songTotalPlayTime) AS totalPlayTime + FROM song_artist_map + JOIN (SELECT songId, SUM(playTime) AS songTotalPlayTime + FROM event + WHERE timestamp > :fromTimeStamp + GROUP BY songId) AS e + ON song_artist_map.songId = e.songId + GROUP BY artistId + ORDER BY totalPlayTime DESC + LIMIT :limit) + ON artist.id = artistId """ ) - fun mostPlayedArtists(limit: Int = 6): Flow> + fun mostPlayedArtists(fromTimeStamp: Long, limit: Int = 6): Flow> @Transaction @Query("SELECT * FROM song WHERE id = :songId") diff --git a/app/src/main/java/com/zionhuang/music/ui/component/HideOnScrollFAB.kt b/app/src/main/java/com/zionhuang/music/ui/component/HideOnScrollFAB.kt new file mode 100644 index 000000000..64581e51b --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/component/HideOnScrollFAB.kt @@ -0,0 +1,82 @@ +package com.zionhuang.music.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.zionhuang.music.LocalPlayerAwareWindowInsets +import com.zionhuang.music.ui.utils.isScrollingUp + +@Composable +fun BoxScope.HideOnScrollFAB( + visible: Boolean = true, + lazyListState: LazyListState, + @DrawableRes icon: Int, + onClick: () -> Unit, +) { + AnimatedVisibility( + visible = visible && lazyListState.isScrollingUp(), + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + modifier = Modifier + .align(Alignment.BottomEnd) + .windowInsetsPadding( + LocalPlayerAwareWindowInsets.current + .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) + ) + ) { + FloatingActionButton( + modifier = Modifier.padding(16.dp), + onClick = onClick + ) { + Icon( + painter = painterResource(icon), + contentDescription = null + ) + } + } +} + +@Composable +fun BoxScope.HideOnScrollFAB( + visible: Boolean = true, + scrollState: ScrollState, + @DrawableRes icon: Int, + onClick: () -> Unit, +) { + AnimatedVisibility( + visible = visible && scrollState.isScrollingUp(), + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + modifier = Modifier + .align(Alignment.BottomEnd) + .windowInsetsPadding( + LocalPlayerAwareWindowInsets.current + .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) + ) + ) { + FloatingActionButton( + modifier = Modifier.padding(16.dp), + onClick = onClick + ) { + Icon( + painter = painterResource(icon), + contentDescription = null + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/component/NavigationTile.kt b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTile.kt new file mode 100644 index 000000000..ecd3771f9 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTile.kt @@ -0,0 +1,47 @@ +package com.zionhuang.music.ui.component + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp + +@Composable +fun NavigationTile( + title: String, + @DrawableRes icon: Int, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier + .clip(RoundedCornerShape(4.dp)) + .clickable(onClick = onClick) + .padding(4.dp), + ) { + Icon( + painter = painterResource(icon), + contentDescription = null + ) + + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/component/NavigationTitle.kt b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTitle.kt new file mode 100644 index 000000000..37551bdc8 --- /dev/null +++ b/app/src/main/java/com/zionhuang/music/ui/component/NavigationTitle.kt @@ -0,0 +1,55 @@ +package com.zionhuang.music.ui.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.WindowInsetsSides +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.only +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.layout.windowInsetsPadding +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.zionhuang.music.R + +@Composable +fun NavigationTitle( + title: String, + modifier: Modifier = Modifier, + onClick: (() -> Unit)? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth() + .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)) + .clickable(enabled = onClick != null) { + onClick?.invoke() + } + .padding(12.dp) + ) { + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall + ) + } + + if (onClick != null) { + Icon( + painter = painterResource(R.drawable.navigate_next), + contentDescription = null + ) + } + } +} diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt index 65437edf3..56ccaf206 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/HistoryScreen.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -23,8 +22,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.innertube.models.WatchEndpoint @@ -35,6 +32,7 @@ import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.queues.YouTubeQueue import com.zionhuang.music.ui.component.LocalMenuState +import com.zionhuang.music.ui.component.NavigationTitle import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.menu.SongMenu import com.zionhuang.music.viewmodels.DateAgo @@ -60,20 +58,17 @@ fun HistoryScreen( ) { events.forEach { (dateAgo, events) -> stickyHeader { - Text( - text = when (dateAgo) { + NavigationTitle( + title = when (dateAgo) { DateAgo.Today -> stringResource(R.string.today) DateAgo.Yesterday -> stringResource(R.string.yesterday) DateAgo.ThisWeek -> stringResource(R.string.this_week) DateAgo.LastWeek -> stringResource(R.string.last_week) is DateAgo.Other -> dateAgo.date.format(DateTimeFormatter.ofPattern("yyyy/MM")) }, - style = MaterialTheme.typography.headlineMedium, - overflow = TextOverflow.Ellipsis, modifier = Modifier .fillMaxWidth() .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 12.dp, vertical = 8.dp) ) } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt index e4dbc133e..bb14d2757 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/HomeScreen.kt @@ -1,6 +1,5 @@ package com.zionhuang.music.ui.screens -import androidx.annotation.DrawableRes import androidx.compose.foundation.* import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior import androidx.compose.foundation.layout.* @@ -10,15 +9,12 @@ import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController @@ -36,7 +32,10 @@ import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.queues.YouTubeAlbumRadio import com.zionhuang.music.playback.queues.YouTubeQueue +import com.zionhuang.music.ui.component.HideOnScrollFAB import com.zionhuang.music.ui.component.LocalMenuState +import com.zionhuang.music.ui.component.NavigationTile +import com.zionhuang.music.ui.component.NavigationTitle import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.component.YouTubeGridItem import com.zionhuang.music.ui.menu.SongMenu @@ -60,7 +59,7 @@ fun HomeScreen( val mediaMetadata by playerConnection.mediaMetadata.collectAsState() val quickPicks by viewModel.quickPicks.collectAsState() - val newReleaseAlbums by viewModel.newReleaseAlbums.collectAsState() + val explorePage by viewModel.explorePage.collectAsState() val isRefreshing by viewModel.isRefreshing.collectAsState() val mostPlayedLazyGridState = rememberLazyGridState() @@ -70,6 +69,8 @@ fun HomeScreen( "SAPISID" in parseCookieString(innerTubeCookie) } + val scrollState = rememberScrollState() + SwipeRefresh( state = rememberSwipeRefreshState(isRefreshing), onRefresh = viewModel::refresh, @@ -90,7 +91,7 @@ fun HomeScreen( } Column( - modifier = Modifier.verticalScroll(rememberScrollState()) + modifier = Modifier.verticalScroll(scrollState) ) { Spacer(Modifier.height(LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateTopPadding())) @@ -114,13 +115,6 @@ fun HomeScreen( modifier = Modifier.weight(1f) ) - NavigationTile( - title = stringResource(R.string.mood_and_genres), - icon = R.drawable.mood, - onClick = { navController.navigate("mood_and_genres") }, - modifier = Modifier.weight(1f) - ) - if (isLoggedIn) { NavigationTile( title = stringResource(R.string.account), @@ -133,12 +127,8 @@ fun HomeScreen( } } - Text( - text = stringResource(R.string.quick_picks), - style = MaterialTheme.typography.headlineSmall, - modifier = Modifier - .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)) - .padding(12.dp) + NavigationTitle( + title = stringResource(R.string.quick_picks) ) quickPicks?.let { quickPicks -> @@ -211,31 +201,13 @@ fun HomeScreen( } } - if (newReleaseAlbums.isNotEmpty()) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Horizontal)) - .clickable { - navController.navigate("new_release") - } - .padding(12.dp) - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(R.string.new_release_albums), - style = MaterialTheme.typography.headlineSmall - ) + explorePage?.newReleaseAlbums?.let { newReleaseAlbums -> + NavigationTitle( + title = stringResource(R.string.new_release_albums), + onClick = { + navController.navigate("new_release") } - - Icon( - painter = painterResource(R.drawable.navigate_next), - contentDescription = null - ) - } + ) LazyRow( contentPadding = WindowInsets.systemBars @@ -272,62 +244,50 @@ fun HomeScreen( } } - Spacer(Modifier.height(LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateBottomPadding())) - } - - if (!quickPicks.isNullOrEmpty() || newReleaseAlbums.isNotEmpty()) { - FloatingActionButton( - modifier = Modifier - .align(Alignment.BottomEnd) - .windowInsetsPadding( - LocalPlayerAwareWindowInsets.current - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - ) - .padding(16.dp), - onClick = { - if (Random.nextBoolean() && !quickPicks.isNullOrEmpty()) { - val song = quickPicks!!.random() - playerConnection.playQueue(YouTubeQueue(WatchEndpoint(videoId = song.id), song.toMediaMetadata())) - } else if (newReleaseAlbums.isNotEmpty()) { - val album = newReleaseAlbums.random() - playerConnection.playQueue(YouTubeAlbumRadio(album.playlistId)) + explorePage?.moodAndGenres?.let { moodAndGenres -> + NavigationTitle( + title = stringResource(R.string.mood_and_genres), + onClick = { + navController.navigate("mood_and_genres") } - }) { - Icon( - painter = painterResource(R.drawable.casino), - contentDescription = null ) + + LazyHorizontalGrid( + rows = GridCells.Fixed(4), + contentPadding = PaddingValues(6.dp), + modifier = Modifier.height(MoodAndGenresButtonHeight * 4 + 12.dp) + ) { + items(moodAndGenres) { + MoodAndGenresButton( + title = it.title, + onClick = { + navController.navigate("youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}") + }, + modifier = Modifier + .padding(6.dp) + .width(180.dp) + ) + } + } } - } - } - } -} -@Composable -fun NavigationTile( - title: String, - @DrawableRes icon: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(4.dp), - modifier = modifier - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = onClick) - .padding(4.dp), - ) { - Icon( - painter = painterResource(icon), - contentDescription = null - ) + Spacer(Modifier.height(LocalPlayerAwareWindowInsets.current.asPaddingValues().calculateBottomPadding())) + } - Text( - text = title, - style = MaterialTheme.typography.labelMedium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) + HideOnScrollFAB( + visible = !quickPicks.isNullOrEmpty() || explorePage?.newReleaseAlbums?.isNotEmpty() == true, + scrollState = scrollState, + icon = R.drawable.casino, + onClick = { + if (Random.nextBoolean() && !quickPicks.isNullOrEmpty()) { + val song = quickPicks!!.random() + playerConnection.playQueue(YouTubeQueue(WatchEndpoint(videoId = song.id), song.toMediaMetadata())) + } else if (explorePage?.newReleaseAlbums?.isNotEmpty() == true) { + val album = explorePage?.newReleaseAlbums!!.random() + playerConnection.playQueue(YouTubeAlbumRadio(album.playlistId)) + } + } + ) + } } } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt index beef4b710..429b666d6 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/MoodAndGenresScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape @@ -23,6 +24,7 @@ import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalConfiguration @@ -84,7 +86,9 @@ fun MoodAndGenresScreen( onClick = { navController.navigate("youtube_browse/${it.endpoint.browseId}?params=${it.endpoint.params}") }, - modifier = Modifier.weight(1f) + modifier = Modifier + .weight(1f) + .padding(6.dp) ) } @@ -119,8 +123,9 @@ fun MoodAndGenresButton( modifier: Modifier = Modifier, ) { Box( + contentAlignment = Alignment.CenterStart, modifier = modifier - .padding(6.dp) + .height(MoodAndGenresButtonHeight) .clip(RoundedCornerShape(6.dp)) .background(MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp)) .clickable(onClick = onClick) @@ -134,3 +139,5 @@ fun MoodAndGenresButton( ) } } + +val MoodAndGenresButtonHeight = 54.dp \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt index 46c6143ac..6c0bec951 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/StatsScreen.kt @@ -1,18 +1,23 @@ package com.zionhuang.music.ui.screens import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilterChip +import androidx.compose.material3.FilterChipDefaults import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -23,8 +28,8 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController @@ -32,11 +37,13 @@ import com.zionhuang.innertube.models.WatchEndpoint import com.zionhuang.music.LocalPlayerAwareWindowInsets import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R +import com.zionhuang.music.constants.StatPeriod import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.queues.YouTubeQueue import com.zionhuang.music.ui.component.ArtistListItem import com.zionhuang.music.ui.component.LocalMenuState +import com.zionhuang.music.ui.component.NavigationTitle import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.menu.SongMenu import com.zionhuang.music.viewmodels.StatsViewModel @@ -52,6 +59,7 @@ fun StatsScreen( val isPlaying by playerConnection.isPlaying.collectAsState() val mediaMetadata by playerConnection.mediaMetadata.collectAsState() + val statPeriod by viewModel.statPeriod.collectAsState() val mostPlayedSongs by viewModel.mostPlayedSongs.collectAsState() val mostPlayedArtists by viewModel.mostPlayedArtists.collectAsState() @@ -60,15 +68,39 @@ fun StatsScreen( modifier = Modifier.windowInsetsPadding(LocalPlayerAwareWindowInsets.current.only(WindowInsetsSides.Top)) ) { item { - Text( - text = stringResource(R.string.most_played_songs), - style = MaterialTheme.typography.headlineMedium, - overflow = TextOverflow.Ellipsis, + Row( modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 12.dp, vertical = 8.dp) - ) + .horizontalScroll(rememberScrollState()) + ) { + Spacer(Modifier.width(8.dp)) + + StatPeriod.values().forEach { period -> + FilterChip( + label = { + Text( + when (period) { + StatPeriod.`1_WEEK` -> pluralStringResource(R.plurals.n_week, 1, 1) + StatPeriod.`1_MONTH` -> pluralStringResource(R.plurals.n_month, 1, 1) + StatPeriod.`3_MONTH` -> pluralStringResource(R.plurals.n_month, 3, 3) + StatPeriod.`6_MONTH` -> pluralStringResource(R.plurals.n_month, 6, 6) + StatPeriod.`1_YEAR` -> pluralStringResource(R.plurals.n_year, 1, 1) + StatPeriod.ALL -> stringResource(R.string.filter_all) + } + ) + }, + selected = statPeriod == period, + colors = FilterChipDefaults.filterChipColors(containerColor = MaterialTheme.colorScheme.background), + onClick = { + viewModel.statPeriod.value = period + } + ) + Spacer(Modifier.width(8.dp)) + } + } + } + item { + NavigationTitle(stringResource(R.string.most_played_songs)) } items( items = mostPlayedSongs, @@ -115,15 +147,7 @@ fun StatsScreen( ) } item { - Text( - text = stringResource(R.string.most_played_artists), - style = MaterialTheme.typography.headlineMedium, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .fillMaxWidth() - .background(MaterialTheme.colorScheme.background) - .padding(horizontal = 12.dp, vertical = 8.dp) - ) + NavigationTitle(stringResource(R.string.most_played_artists)) } items( items = mostPlayedArtists, diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt index 7efc0238e..6b337fa52 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistScreen.kt @@ -69,6 +69,7 @@ import com.zionhuang.music.playback.queues.YouTubeQueue import com.zionhuang.music.ui.component.AutoResizeText import com.zionhuang.music.ui.component.FontSizeRange import com.zionhuang.music.ui.component.LocalMenuState +import com.zionhuang.music.ui.component.NavigationTitle import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.component.YouTubeGridItem import com.zionhuang.music.ui.component.YouTubeListItem @@ -197,28 +198,12 @@ fun ArtistScreen( if (librarySongs.isNotEmpty()) { item { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable { - navController.navigate("artist/${viewModel.artistId}/songs") - } - .padding(12.dp) - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = stringResource(R.string.from_your_library), - style = MaterialTheme.typography.headlineMedium - ) + NavigationTitle( + title = stringResource(R.string.from_your_library), + onClick = { + navController.navigate("artist/${viewModel.artistId}/songs") } - Icon( - painter = painterResource(R.drawable.navigate_next), - contentDescription = null - ) - } + ) } items( @@ -264,30 +249,12 @@ fun ArtistScreen( artistPage.sections.fastForEach { section -> item { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .clickable(enabled = section.moreEndpoint != null) { - navController.navigate("artist/${viewModel.artistId}/items?browseId=${section.moreEndpoint?.browseId}?params=${section.moreEndpoint?.params}") - } - .padding(12.dp) - ) { - Column( - modifier = Modifier.weight(1f) - ) { - Text( - text = section.title, - style = MaterialTheme.typography.headlineMedium - ) - } - if (section.moreEndpoint != null) { - Icon( - painter = painterResource(R.drawable.navigate_next), - contentDescription = null - ) + NavigationTitle( + title = section.title, + onClick = { + navController.navigate("artist/${viewModel.artistId}/items?browseId=${section.moreEndpoint?.browseId}?params=${section.moreEndpoint?.params}") } - } + ) } if ((section.items.firstOrNull() as? SongItem)?.album != null) { diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt index c839f6301..0e3ba8458 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/artist/ArtistSongsScreen.kt @@ -1,23 +1,15 @@ package com.zionhuang.music.ui.screens.artist -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -26,12 +18,10 @@ import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.music.LocalPlayerAwareWindowInsets @@ -45,11 +35,11 @@ import com.zionhuang.music.constants.CONTENT_TYPE_SONG import com.zionhuang.music.extensions.toMediaItem import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.playback.queues.ListQueue +import com.zionhuang.music.ui.component.HideOnScrollFAB import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.component.SortHeader import com.zionhuang.music.ui.menu.SongMenu -import com.zionhuang.music.ui.utils.isScrollingUp import com.zionhuang.music.utils.rememberEnumPreference import com.zionhuang.music.utils.rememberPreference import com.zionhuang.music.viewmodels.ArtistSongsViewModel @@ -163,33 +153,17 @@ fun ArtistSongsScreen( scrollBehavior = scrollBehavior ) - AnimatedVisibility( - visible = lazyListState.isScrollingUp(), - enter = slideInVertically { it }, - exit = slideOutVertically { it }, - modifier = Modifier - .align(Alignment.BottomEnd) - .windowInsetsPadding( - LocalPlayerAwareWindowInsets.current - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - ) - ) { - FloatingActionButton( - modifier = Modifier.padding(16.dp), - onClick = { - playerConnection.playQueue( - ListQueue( - title = artist?.name, - items = songs.shuffled().map { it.toMediaItem() }, - ) + HideOnScrollFAB( + lazyListState = lazyListState, + icon = R.drawable.shuffle, + onClick = { + playerConnection.playQueue( + ListQueue( + title = artist?.name, + items = songs.shuffled().map { it.toMediaItem() }, ) - } - ) { - Icon( - painter = painterResource(R.drawable.shuffle), - contentDescription = null ) } - } + ) } } \ No newline at end of file diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt index 65942a76b..68580b118 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibraryPlaylistsScreen.kt @@ -1,23 +1,15 @@ package com.zionhuang.music.ui.screens.library -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -29,12 +21,10 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.music.LocalDatabase @@ -49,13 +39,13 @@ import com.zionhuang.music.constants.PlaylistSortTypeKey import com.zionhuang.music.db.entities.PlaylistEntity import com.zionhuang.music.db.entities.PlaylistEntity.Companion.DOWNLOADED_PLAYLIST_ID import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_ID +import com.zionhuang.music.ui.component.HideOnScrollFAB import com.zionhuang.music.ui.component.ListItem import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.PlaylistListItem import com.zionhuang.music.ui.component.SortHeader import com.zionhuang.music.ui.component.TextFieldDialog import com.zionhuang.music.ui.menu.PlaylistMenu -import com.zionhuang.music.ui.utils.isScrollingUp import com.zionhuang.music.utils.rememberEnumPreference import com.zionhuang.music.utils.rememberPreference import com.zionhuang.music.viewmodels.LibraryPlaylistsViewModel @@ -208,26 +198,12 @@ fun LibraryPlaylistsScreen( } } - AnimatedVisibility( - visible = lazyListState.isScrollingUp(), - enter = slideInVertically { it }, - exit = slideOutVertically { it }, - modifier = Modifier - .align(Alignment.BottomEnd) - .windowInsetsPadding( - LocalPlayerAwareWindowInsets.current - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - ) - ) { - FloatingActionButton( - modifier = Modifier.padding(16.dp), - onClick = { showAddPlaylistDialog = true } - ) { - Icon( - painter = painterResource(R.drawable.add), - contentDescription = null - ) + HideOnScrollFAB( + lazyListState = lazyListState, + icon = R.drawable.add, + onClick = { + showAddPlaylistDialog = true } - } + ) } } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt index 90d0a44ae..4503ffc35 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/library/LibrarySongsScreen.kt @@ -1,26 +1,20 @@ package com.zionhuang.music.ui.screens.library -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController import com.zionhuang.music.LocalPlayerAwareWindowInsets @@ -30,11 +24,11 @@ import com.zionhuang.music.constants.* import com.zionhuang.music.extensions.toMediaItem import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.playback.queues.ListQueue +import com.zionhuang.music.ui.component.HideOnScrollFAB import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.component.SortHeader import com.zionhuang.music.ui.menu.SongMenu -import com.zionhuang.music.ui.utils.isScrollingUp import com.zionhuang.music.utils.rememberEnumPreference import com.zionhuang.music.utils.rememberPreference import com.zionhuang.music.viewmodels.LibrarySongsViewModel @@ -134,33 +128,18 @@ fun LibrarySongsScreen( } } - AnimatedVisibility( - visible = songs.isNotEmpty() && lazyListState.isScrollingUp(), - enter = slideInVertically { it }, - exit = slideOutVertically { it }, - modifier = Modifier - .align(Alignment.BottomEnd) - .windowInsetsPadding( - LocalPlayerAwareWindowInsets.current - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - ) - ) { - FloatingActionButton( - modifier = Modifier.padding(16.dp), - onClick = { - playerConnection.playQueue( - ListQueue( - title = context.getString(R.string.queue_all_songs), - items = songs.shuffled().map { it.toMediaItem() }, - ) + HideOnScrollFAB( + visible = songs.isNotEmpty(), + lazyListState = lazyListState, + icon = R.drawable.shuffle, + onClick = { + playerConnection.playQueue( + ListQueue( + title = context.getString(R.string.queue_all_songs), + items = songs.shuffled().map { it.toMediaItem() }, ) - } - ) { - Icon( - painter = painterResource(R.drawable.shuffle), - contentDescription = null ) } - } + ) } } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt index f930a7ec4..41e68f994 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/playlist/BuiltInPlaylistScreen.kt @@ -1,23 +1,15 @@ package com.zionhuang.music.ui.screens.playlist -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.slideInVertically -import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.only -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text @@ -27,12 +19,10 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource -import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastSumBy import androidx.hilt.navigation.compose.hiltViewModel import androidx.navigation.NavController @@ -50,11 +40,11 @@ import com.zionhuang.music.db.entities.PlaylistEntity.Companion.LIKED_PLAYLIST_I import com.zionhuang.music.extensions.toMediaItem import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.playback.queues.ListQueue +import com.zionhuang.music.ui.component.HideOnScrollFAB import com.zionhuang.music.ui.component.LocalMenuState import com.zionhuang.music.ui.component.SongListItem import com.zionhuang.music.ui.component.SortHeader import com.zionhuang.music.ui.menu.SongMenu -import com.zionhuang.music.ui.utils.isScrollingUp import com.zionhuang.music.utils.joinByBullet import com.zionhuang.music.utils.makeTimeString import com.zionhuang.music.utils.rememberEnumPreference @@ -207,33 +197,18 @@ fun BuiltInPlaylistScreen( scrollBehavior = scrollBehavior ) - AnimatedVisibility( - visible = songs.isNotEmpty() && lazyListState.isScrollingUp(), - enter = slideInVertically { it }, - exit = slideOutVertically { it }, - modifier = Modifier - .align(Alignment.BottomEnd) - .windowInsetsPadding( - LocalPlayerAwareWindowInsets.current - .only(WindowInsetsSides.Bottom + WindowInsetsSides.Horizontal) - ) - ) { - FloatingActionButton( - modifier = Modifier.padding(16.dp), - onClick = { - playerConnection.playQueue( - ListQueue( - title = playlistName, - items = songs.shuffled().map { it.toMediaItem() }, - ) + HideOnScrollFAB( + visible = songs.isNotEmpty(), + lazyListState = lazyListState, + icon = R.drawable.shuffle, + onClick = { + playerConnection.playQueue( + ListQueue( + title = playlistName, + items = songs.shuffled().map { it.toMediaItem() }, ) - } - ) { - Icon( - painter = painterResource(R.drawable.shuffle), - contentDescription = null ) } - } + ) } } diff --git a/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt b/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt index f02282d0a..ffec47295 100644 --- a/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt +++ b/app/src/main/java/com/zionhuang/music/ui/screens/search/OnlineSearchResult.kt @@ -3,7 +3,6 @@ package com.zionhuang.music.ui.screens.search import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -11,7 +10,6 @@ import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars @@ -37,7 +35,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -60,13 +57,13 @@ import com.zionhuang.music.LocalPlayerAwareWindowInsets import com.zionhuang.music.LocalPlayerConnection import com.zionhuang.music.R import com.zionhuang.music.constants.AppBarHeight -import com.zionhuang.music.constants.ListItemHeight import com.zionhuang.music.constants.SearchFilterHeight import com.zionhuang.music.extensions.togglePlayPause import com.zionhuang.music.models.toMediaMetadata import com.zionhuang.music.playback.queues.YouTubeQueue import com.zionhuang.music.ui.component.EmptyPlaceholder import com.zionhuang.music.ui.component.LocalMenuState +import com.zionhuang.music.ui.component.NavigationTitle import com.zionhuang.music.ui.component.YouTubeListItem import com.zionhuang.music.ui.component.shimmer.ListItemPlaceHolder import com.zionhuang.music.ui.component.shimmer.ShimmerHost @@ -189,20 +186,7 @@ fun OnlineSearchResult( if (searchFilter == null) { searchSummary?.summaries?.forEach { summary -> item { - Box( - contentAlignment = Alignment.CenterStart, - modifier = Modifier - .fillMaxWidth() - .height(ListItemHeight) - .padding(12.dp) - .animateItemPlacement() - ) { - Text( - text = summary.title, - style = MaterialTheme.typography.headlineMedium, - maxLines = 1 - ) - } + NavigationTitle(summary.title) } items( diff --git a/app/src/main/java/com/zionhuang/music/ui/utils/LazyListStateUtils.kt b/app/src/main/java/com/zionhuang/music/ui/utils/ScrollUtils.kt similarity index 73% rename from app/src/main/java/com/zionhuang/music/ui/utils/LazyListStateUtils.kt rename to app/src/main/java/com/zionhuang/music/ui/utils/ScrollUtils.kt index cc7061c2e..975e195f4 100644 --- a/app/src/main/java/com/zionhuang/music/ui/utils/LazyListStateUtils.kt +++ b/app/src/main/java/com/zionhuang/music/ui/utils/ScrollUtils.kt @@ -1,5 +1,6 @@ package com.zionhuang.music.ui.utils +import androidx.compose.foundation.ScrollState import androidx.compose.foundation.lazy.LazyListState import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf @@ -24,4 +25,16 @@ fun LazyListState.isScrollingUp(): Boolean { } } }.value -} \ No newline at end of file +} + +@Composable +fun ScrollState.isScrollingUp(): Boolean { + var previousScrollOffset by remember(this) { mutableStateOf(value) } + return remember(this) { + derivedStateOf { + (previousScrollOffset >= value).also { + previousScrollOffset = value + } + } + }.value +} diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt index 9aa7de8c4..cabe66ff9 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/HomeViewModel.kt @@ -3,7 +3,7 @@ package com.zionhuang.music.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube -import com.zionhuang.innertube.models.AlbumItem +import com.zionhuang.innertube.pages.ExplorePage import com.zionhuang.music.db.MusicDatabase import com.zionhuang.music.db.entities.Song import dagger.hilt.android.lifecycle.HiltViewModel @@ -20,12 +20,14 @@ class HomeViewModel @Inject constructor( val isRefreshing = MutableStateFlow(false) val quickPicks = MutableStateFlow?>(null) - val newReleaseAlbums = MutableStateFlow>(emptyList()) + val explorePage = MutableStateFlow(null) private suspend fun load() { quickPicks.value = database.quickPicks().first().shuffled().take(20) - YouTube.newReleaseAlbumsPreview().onSuccess { - newReleaseAlbums.value = it + YouTube.explore().onSuccess { + explorePage.value = it + }.onFailure { + it.printStackTrace() } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt index 8153e8cb6..8770dfe64 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/NewReleaseViewModel.kt @@ -19,6 +19,8 @@ class NewReleaseViewModel @Inject constructor() : ViewModel() { viewModelScope.launch { YouTube.newReleaseAlbums().onSuccess { _newReleaseAlbums.value = it + }.onFailure { + it.printStackTrace() } } } diff --git a/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt b/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt index 499a08417..a254e442d 100644 --- a/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt +++ b/app/src/main/java/com/zionhuang/music/viewmodels/StatsViewModel.kt @@ -3,23 +3,33 @@ package com.zionhuang.music.viewmodels import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.zionhuang.innertube.YouTube +import com.zionhuang.music.constants.StatPeriod import com.zionhuang.music.db.MusicDatabase import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import java.time.Duration import java.time.LocalDateTime import javax.inject.Inject +@OptIn(ExperimentalCoroutinesApi::class) @HiltViewModel class StatsViewModel @Inject constructor( val database: MusicDatabase, ) : ViewModel() { - val mostPlayedSongs = database.mostPlayedSongs() - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) - val mostPlayedArtists = database.mostPlayedArtists() - .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + val statPeriod = MutableStateFlow(StatPeriod.`1_WEEK`) + + val mostPlayedSongs = statPeriod.flatMapLatest { period -> + database.mostPlayedSongs(period.toTimeMillis()) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) + + val mostPlayedArtists = statPeriod.flatMapLatest { period -> + database.mostPlayedArtists(period.toTimeMillis()) + }.stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) init { viewModelScope.launch { @@ -39,4 +49,4 @@ class StatsViewModel @Inject constructor( } } } -} \ No newline at end of file +} diff --git a/app/src/main/res/values-DE/strings.xml b/app/src/main/res/values-DE/strings.xml index 94ab83ee2..53f29c240 100644 --- a/app/src/main/res/values-DE/strings.xml +++ b/app/src/main/res/values-DE/strings.xml @@ -140,6 +140,18 @@ %d Wiedergabeliste %d Wiedergabelisten + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Wiedergabeliste importiert diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index 4b7927c5b..167a94f29 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -20,7 +20,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -150,6 +150,18 @@ %d playlistů %d playlistů + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist importován diff --git a/app/src/main/res/values-es-rUS/strings.xml b/app/src/main/res/values-es-rUS/strings.xml index 3e9e24c86..c27187883 100644 --- a/app/src/main/res/values-es-rUS/strings.xml +++ b/app/src/main/res/values-es-rUS/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -140,6 +140,18 @@ %d playlist %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist imported diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 7c3f536fb..9ccd681af 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -140,6 +140,18 @@ %d lista de reproducción %d listas de reproducción + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Lista de reproducción importada diff --git a/app/src/main/res/values-fa-rIR/strings.xml b/app/src/main/res/values-fa-rIR/strings.xml index e9d39f78a..966a6920f 100644 --- a/app/src/main/res/values-fa-rIR/strings.xml +++ b/app/src/main/res/values-fa-rIR/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -140,6 +140,18 @@ %d لیست‌پخش %d لیست‌پخش‌ها + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + لیست‌پخش واردشد diff --git a/app/src/main/res/values-fi-rFI/strings.xml b/app/src/main/res/values-fi-rFI/strings.xml index de3f9d596..9749aaff2 100644 --- a/app/src/main/res/values-fi-rFI/strings.xml +++ b/app/src/main/res/values-fi-rFI/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -140,6 +140,18 @@ %d playlist %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist imported diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 0eab2292e..278d56822 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -19,7 +19,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -145,6 +145,18 @@ %d playlists %d albums + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist imported diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index f204afadf..a674853c1 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -140,6 +140,18 @@ %d lejátszólista %d lejátszólista + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Lista importálva diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index 55d6bc0b4..e7986caa8 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -17,7 +17,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -135,6 +135,18 @@ %d daftar putar + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Daftar putar dipindahkan diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 3dd7caa7a..e241a1005 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -140,6 +140,18 @@ %d playlist %d playlist + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist importata diff --git a/app/src/main/res/values-ja-rJP/strings.xml b/app/src/main/res/values-ja-rJP/strings.xml index 32807e68b..19434a6ab 100644 --- a/app/src/main/res/values-ja-rJP/strings.xml +++ b/app/src/main/res/values-ja-rJP/strings.xml @@ -135,6 +135,18 @@ %d プレイリスト + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + プレイリストをインポートしました diff --git a/app/src/main/res/values-ko-rKR/strings.xml b/app/src/main/res/values-ko-rKR/strings.xml index 93bae5aa3..9eb032c6e 100644 --- a/app/src/main/res/values-ko-rKR/strings.xml +++ b/app/src/main/res/values-ko-rKR/strings.xml @@ -17,7 +17,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -135,6 +135,18 @@ %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist imported diff --git a/app/src/main/res/values-ml-rIN/strings.xml b/app/src/main/res/values-ml-rIN/strings.xml index 0bd5d2416..8b88c3337 100644 --- a/app/src/main/res/values-ml-rIN/strings.xml +++ b/app/src/main/res/values-ml-rIN/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -140,6 +140,18 @@ %d പ്ലേലിസ്റ്റ് %d പ്ലേലിസ്റ്റുകൾ + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + പ്ലേലിസ്റ്റ് ഇറക്കുമതി ചെയ്തു diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 95ac85fbe..1a98acd54 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -140,6 +140,18 @@ %d afspeellijst %d afspeellijsten + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Afspeellijst geimporteerd diff --git a/app/src/main/res/values-or-rIN/strings.xml b/app/src/main/res/values-or-rIN/strings.xml index 94d79a9f2..188ba3e14 100644 --- a/app/src/main/res/values-or-rIN/strings.xml +++ b/app/src/main/res/values-or-rIN/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -140,6 +140,18 @@ %d playlist %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + ପ୍ଲେଲିଷ୍ଟ ଆମଦାନୀ ହୋଇଛି diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml index 8466b0527..d69e44aff 100644 --- a/app/src/main/res/values-pa/strings.xml +++ b/app/src/main/res/values-pa/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account ਉਂਗਲਾਂ ਤੇ ਚੁਣੇ ਗੀਤ - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks ਨਵੀਆਂ ਜਾਰੀ ਐਲਬਮਾਂ @@ -140,6 +140,18 @@ %d ਪਲੇਲਿਸਟ %d ਪਲੇਲਿਸਟਾਂ + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + ਪਲੇਲਿਸਟ ਅਯਾਤ ਕੀਤੀ ਗਈ diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 6afbd4bed..a037c9533 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release álbuns @@ -140,6 +140,18 @@ %d playlist %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist importada diff --git a/app/src/main/res/values-ru-rRU/strings.xml b/app/src/main/res/values-ru-rRU/strings.xml index 9c5b4ef2e..c39f4080a 100644 --- a/app/src/main/res/values-ru-rRU/strings.xml +++ b/app/src/main/res/values-ru-rRU/strings.xml @@ -150,6 +150,18 @@ %d плейлистов %d плейлистов + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Плейлист импортирован diff --git a/app/src/main/res/values-sv-rSE/strings.xml b/app/src/main/res/values-sv-rSE/strings.xml index a2920c4ff..5a7779493 100644 --- a/app/src/main/res/values-sv-rSE/strings.xml +++ b/app/src/main/res/values-sv-rSE/strings.xml @@ -18,7 +18,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -140,6 +140,18 @@ %d playlist %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist imported diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index cf5ff59cc..3f086830b 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -140,6 +140,18 @@ %d çalma listesi %d çalma listesi + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Çalma listesi içe aktarıldı diff --git a/app/src/main/res/values-uk-rUA/strings.xml b/app/src/main/res/values-uk-rUA/strings.xml index cda207870..33eea2f38 100644 --- a/app/src/main/res/values-uk-rUA/strings.xml +++ b/app/src/main/res/values-uk-rUA/strings.xml @@ -150,6 +150,18 @@ %d плейлистів %d плейлистів + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Плейлист імпортовано diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 21728b7b9..8a6eb95aa 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -17,7 +17,7 @@ Mood and Genres Account 歌曲快选 - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks 新专辑 @@ -135,6 +135,18 @@ %d 个播放列表 + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + 已导入此播放列表 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index ab03c867c..0aa96aad6 100755 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -135,6 +135,15 @@ %d 個播放清單 + + %d 週 + + + %d 個月 + + + %d 年 + 已匯入此播放清單 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0a5351245..9e9b4640e 100755 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -17,7 +17,7 @@ Mood and Genres Account Quick picks - Listen some songs to generate your quick picks + Listen to songs to generate your quick picks New release albums @@ -139,6 +139,18 @@ %d playlist %d playlists + + %d week + %d weeks + + + %d month + %d months + + + %d year + %d years + Playlist imported diff --git a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt index 093c7c434..78d3f663e 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/YouTube.kt @@ -34,6 +34,7 @@ import com.zionhuang.innertube.pages.ArtistItemsContinuationPage import com.zionhuang.innertube.pages.ArtistItemsPage import com.zionhuang.innertube.pages.ArtistPage import com.zionhuang.innertube.pages.BrowseResult +import com.zionhuang.innertube.pages.ExplorePage import com.zionhuang.innertube.pages.MoodAndGenres import com.zionhuang.innertube.pages.NewReleaseAlbumPage import com.zionhuang.innertube.pages.NextPage @@ -281,14 +282,20 @@ object YouTube { ) } - - suspend fun newReleaseAlbumsPreview(): Result> = runCatching { + suspend fun explore(): Result = runCatching { val response = innerTube.browse(WEB_REMIX, browseId = "FEmusic_explore").body() - response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.getOrNull(1)?.musicCarouselShelfRenderer?.contents?.mapNotNull { - it.musicTwoRowItemRenderer?.let { renderer -> - NewReleaseAlbumPage.fromMusicTwoRowItemRenderer(renderer) - } - }.orEmpty() + ExplorePage( + newReleaseAlbums = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.getOrNull(1)?.musicCarouselShelfRenderer?.contents + ?.mapNotNull { + it.musicTwoRowItemRenderer?.let { renderer -> + NewReleaseAlbumPage.fromMusicTwoRowItemRenderer(renderer) + } + }.orEmpty(), + moodAndGenres = response.contents?.singleColumnBrowseResultsRenderer?.tabs?.firstOrNull()?.tabRenderer?.content?.sectionListRenderer?.contents?.getOrNull(2)?.musicCarouselShelfRenderer?.contents + ?.mapNotNull(MusicCarouselShelfRenderer.Content::musicNavigationButtonRenderer) + ?.mapNotNull(MoodAndGenres.Companion::fromMusicNavigationButtonRenderer) + .orEmpty() + ) } suspend fun newReleaseAlbums(): Result> = runCatching { diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/ExplorePage.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/ExplorePage.kt new file mode 100644 index 000000000..faee829de --- /dev/null +++ b/innertube/src/main/java/com/zionhuang/innertube/pages/ExplorePage.kt @@ -0,0 +1,8 @@ +package com.zionhuang.innertube.pages + +import com.zionhuang.innertube.models.AlbumItem + +data class ExplorePage( + val newReleaseAlbums: List, + val moodAndGenres: List, +) diff --git a/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt b/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt index 8288790b7..ecaed16e4 100644 --- a/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt +++ b/innertube/src/main/java/com/zionhuang/innertube/pages/MoodAndGenres.kt @@ -1,6 +1,8 @@ package com.zionhuang.innertube.pages import com.zionhuang.innertube.models.BrowseEndpoint +import com.zionhuang.innertube.models.GridRenderer +import com.zionhuang.innertube.models.MusicNavigationButtonRenderer import com.zionhuang.innertube.models.SectionListRenderer data class MoodAndGenres( @@ -17,13 +19,17 @@ data class MoodAndGenres( fun fromSectionListRendererContent(content: SectionListRenderer.Content): MoodAndGenres? { return MoodAndGenres( title = content.gridRenderer?.header?.gridHeaderRenderer?.title?.runs?.firstOrNull()?.text ?: return null, - items = content.gridRenderer.items.map { - Item( - title = it.musicNavigationButtonRenderer?.buttonText?.runs?.firstOrNull()?.text ?: return null, - stripeColor = it.musicNavigationButtonRenderer.solid?.leftStripeColor ?: return null, - endpoint = it.musicNavigationButtonRenderer.clickCommand.browseEndpoint ?: return null, - ) - } + items = content.gridRenderer.items + .mapNotNull(GridRenderer.Item::musicNavigationButtonRenderer) + .mapNotNull(::fromMusicNavigationButtonRenderer) + ) + } + + fun fromMusicNavigationButtonRenderer(renderer: MusicNavigationButtonRenderer): Item? { + return Item( + title = renderer.buttonText.runs?.firstOrNull()?.text ?: return null, + stripeColor = renderer.solid?.leftStripeColor ?: return null, + endpoint = renderer.clickCommand.browseEndpoint ?: return null, ) } }