diff --git a/.phrasey/schema.toml b/.phrasey/schema.toml index f0657c70..8ee0d489 100644 --- a/.phrasey/schema.toml +++ b/.phrasey/schema.toml @@ -720,3 +720,9 @@ name = "Date" [[keys]] name = "TotalSamples" + +[[keys]] +name = "HomePage" + +[[keys]] +name = "NowPlayingPage" diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt index 758d29b7..ae280de1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumArtistTile.kt @@ -16,9 +16,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import io.github.zyrouge.symphony.services.groove.AlbumArtist -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute @Composable fun AlbumArtistTile(context: ViewContext, albumArtist: AlbumArtist) { @@ -45,7 +44,7 @@ fun AlbumArtistTile(context: ViewContext, albumArtist: AlbumArtist) { context.symphony.radio.shorty.playQueue(albumArtist.getSortedSongIds(context.symphony)) }, onClick = { - context.navController.navigateTo(Routes.AlbumArtist.build(albumArtist.name)) + context.navController.navigate(AlbumArtistViewRoute(albumArtist.name)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt index 30c8226d..081b3871 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/AlbumTile.kt @@ -17,9 +17,9 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import io.github.zyrouge.symphony.services.groove.Album -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.AlbumViewRoute +import io.github.zyrouge.symphony.ui.view.ArtistViewRoute @Composable fun AlbumTile(context: ViewContext, album: Album) { @@ -55,7 +55,7 @@ fun AlbumTile(context: ViewContext, album: Album) { context.symphony.radio.shorty.playQueue(album.getSortedSongIds(context.symphony)) }, onClick = { - context.navController.navigateTo(Routes.Album.build(album.id)) + context.navController.navigate(AlbumViewRoute(album.id)) } ) } @@ -137,7 +137,7 @@ fun AlbumDropdownMenu( }, onClick = { onDismissRequest() - context.navController.navigateTo(Routes.Artist.build(artistName)) + context.navController.navigate(ArtistViewRoute(artistName)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt index c876d5f6..7060469f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/ArtistTile.kt @@ -16,9 +16,8 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import io.github.zyrouge.symphony.services.groove.Artist -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.ArtistViewRoute @Composable fun ArtistTile(context: ViewContext, artist: Artist) { @@ -45,7 +44,7 @@ fun ArtistTile(context: ViewContext, artist: Artist) { context.symphony.radio.shorty.playQueue(artist.getSortedSongIds(context.symphony)) }, onClick = { - context.navController.navigateTo(Routes.Artist.build(artist.name)) + context.navController.navigate(ArtistViewRoute(artist.name)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt index edcd6397..78f0c15d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/GenreGrid.kt @@ -34,9 +34,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.services.groove.repositories.GenreRepository -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.GenreViewRoute private object GenreTile { val colors = listOf( @@ -131,7 +130,7 @@ fun GenreGrid( ), colors = GenreTile.cardColors(i), onClick = { - context.navController.navigateTo(Routes.Genre.build(genre.name)) + context.navController.navigate(GenreViewRoute(genre.name)) } ) { Box( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt index 9c3f55f6..e70a95e4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/NowPlayingBottomBar.kt @@ -64,10 +64,9 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.Song import io.github.zyrouge.symphony.ui.helpers.FadeTransition -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.TransitionDurations import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.NowPlayingViewRoute import io.github.zyrouge.symphony.utils.runIfOrThis import kotlin.math.absoluteValue @@ -145,7 +144,7 @@ fun NowPlayingBottomBar(context: ViewContext, insetPadding: Boolean = true) { .wrapContentHeight() .swipeable( onSwipeUp = { - context.navController.navigateTo(Routes.NowPlaying) + context.navController.navigate(NowPlayingViewRoute) }, onSwipeDown = { context.symphony.radio.stop() @@ -153,7 +152,7 @@ fun NowPlayingBottomBar(context: ViewContext, insetPadding: Boolean = true) { ), shape = RectangleShape, onClick = { - context.navController.navigateTo(Routes.NowPlaying) + context.navController.navigate(NowPlayingViewRoute) } ) { Row( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt index 4482b26e..1e638349 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/PlaylistTile.kt @@ -47,10 +47,9 @@ import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.MediaExposer import io.github.zyrouge.symphony.services.groove.Playlist -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo import io.github.zyrouge.symphony.ui.theme.ThemeColors +import io.github.zyrouge.symphony.ui.view.PlaylistViewRoute import io.github.zyrouge.symphony.utils.Logger import kotlinx.coroutines.launch @@ -62,7 +61,7 @@ fun PlaylistTile(context: ViewContext, playlist: Playlist) { .wrapContentHeight(), colors = CardDefaults.cardColors(containerColor = Color.Transparent), onClick = { - context.navController.navigateTo(Routes.Playlist.build(playlist.id)) + context.navController.navigate(PlaylistViewRoute(playlist.id)) } ) { Box(modifier = Modifier.padding(12.dp)) { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt index cdfbb8e5..cbea674a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongCard.kt @@ -49,9 +49,10 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute +import io.github.zyrouge.symphony.ui.view.AlbumViewRoute +import io.github.zyrouge.symphony.ui.view.ArtistViewRoute import io.github.zyrouge.symphony.utils.Logger @Composable @@ -273,7 +274,7 @@ fun SongDropdownMenu( }, onClick = { onDismissRequest() - context.navController.navigateTo(Routes.Artist.build(artistName)) + context.navController.navigate(ArtistViewRoute(artistName)) } ) } @@ -287,7 +288,7 @@ fun SongDropdownMenu( }, onClick = { onDismissRequest() - context.navController.navigateTo(Routes.AlbumArtist.build(albumArtist)) + context.navController.navigate(AlbumArtistViewRoute(albumArtist)) } ) } @@ -301,7 +302,7 @@ fun SongDropdownMenu( }, onClick = { onDismissRequest() - context.navController.navigateTo(Routes.Album.build(albumId)) + context.navController.navigate(AlbumViewRoute(albumId)) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt index a0f9e173..901d745f 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongInformationDialog.kt @@ -10,9 +10,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.text.style.TextDecoration import io.github.zyrouge.symphony.services.groove.Song -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute +import io.github.zyrouge.symphony.ui.view.AlbumViewRoute +import io.github.zyrouge.symphony.ui.view.ArtistViewRoute +import io.github.zyrouge.symphony.ui.view.GenreViewRoute import io.github.zyrouge.symphony.utils.ActivityUtils import io.github.zyrouge.symphony.utils.DurationUtils import java.text.SimpleDateFormat @@ -34,7 +36,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () InformationKeyValue(context.symphony.t.Artist) { LongPressCopyableAndTappableText(context, song.artists) { onDismissRequest() - context.navController.navigateTo(Routes.Artist.build(it)) + context.navController.navigate(ArtistViewRoute(it)) } } } @@ -42,7 +44,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () InformationKeyValue(context.symphony.t.AlbumArtist) { LongPressCopyableAndTappableText(context, song.albumArtists) { onDismissRequest() - context.navController.navigateTo(Routes.AlbumArtist.build(it)) + context.navController.navigate(AlbumArtistViewRoute(it)) } } } @@ -51,7 +53,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () // TODO composers page maybe? LongPressCopyableAndTappableText(context, song.composers) { onDismissRequest() - context.navController.navigateTo(Routes.Artist.build(it)) + context.navController.navigate(ArtistViewRoute(it)) } } } @@ -59,7 +61,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () InformationKeyValue(context.symphony.t.Album) { LongPressCopyableAndTappableText(context, setOf(song.album!!)) { onDismissRequest() - context.navController.navigateTo(Routes.Album.build(albumId)) + context.navController.navigate(AlbumViewRoute(albumId)) } } } @@ -67,7 +69,7 @@ fun SongInformationDialog(context: ViewContext, song: Song, onDismissRequest: () InformationKeyValue(context.symphony.t.Genre) { LongPressCopyableAndTappableText(context, song.genres) { onDismissRequest() - context.navController.navigateTo(Routes.Genre.build(it)) + context.navController.navigate(GenreViewRoute(it)) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt index 8718f6c0..0ac2ffb0 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/components/SongList.kt @@ -26,10 +26,9 @@ import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.services.groove.Song import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.Radio -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo import io.github.zyrouge.symphony.ui.view.SettingsViewElements +import io.github.zyrouge.symphony.ui.view.SettingsViewRoute enum class SongListType { Default, @@ -97,8 +96,8 @@ fun SongList( textAlign = TextAlign.Center, modifier = Modifier .clickable { - context.navController.navigateTo( - Routes.Settings.build(SettingsViewElements.MediaFolders) + context.navController.navigate( + SettingsViewRoute(SettingsViewElements.MediaFolders) ) } .padding(2.dp), diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Routes.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Routes.kt deleted file mode 100644 index 672fc81e..00000000 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/helpers/Routes.kt +++ /dev/null @@ -1,79 +0,0 @@ -package io.github.zyrouge.symphony.ui.helpers - -import android.net.Uri -import androidx.navigation.NamedNavArgument -import androidx.navigation.NavBackStackEntry -import androidx.navigation.NavHostController -import androidx.navigation.NavType -import androidx.navigation.navArgument -import io.github.zyrouge.symphony.services.groove.Groove -import io.github.zyrouge.symphony.ui.view.SettingsViewElements - -abstract class Route(val route: String) { - abstract class Simple(route: String) : Route(route) { - override fun template() = route - } - - abstract class Parameterized(route: String) : Route(route) { - override fun template() = "$route/{$PARAM_ARGUMENT_NAME}" - abstract fun buildParam(param: T): String - fun build(param: T) = "$route/${encodeArgument(buildParam(param))}" - } - - abstract class StringParameterized(route: String) : Parameterized(route) { - override fun template() = "$route/{$PARAM_ARGUMENT_NAME}" - override fun buildParam(param: String) = param - } - - abstract fun template(): String - open fun arguments(): List = emptyList() - - companion object { - const val PARAM_ARGUMENT_NAME = "param" - - fun encodeArgument(param: String): String = Uri.encode(param) - } -} - -object Routes { - object Home : Route.Simple("home") - - object NowPlaying : Route.Simple("now_playing") - object Queue : Route.Simple("queue") - - object Settings : Route("settings") { - const val ELEMENT_ARGUMENT_NAME = "element" - - override fun template() = "$route?$ELEMENT_ARGUMENT_NAME={$ELEMENT_ARGUMENT_NAME}" - override fun arguments() = listOf( - navArgument(ELEMENT_ARGUMENT_NAME) { - type = NavType.StringType - nullable = true - }, - ) - - fun build(settingsViewElement: SettingsViewElements? = null) = buildString { - append(route) - if (settingsViewElement != null) { - append("?$ELEMENT_ARGUMENT_NAME=${encodeArgument(settingsViewElement.name)}") - } - } - } - - object Search : Route.Parameterized("search") { - override fun buildParam(param: Groove.Kinds?) = param?.name ?: "null" - } - - object Artist : Route.StringParameterized("artist") - object Album : Route.StringParameterized("album") - object AlbumArtist : Route.StringParameterized("album_artist") - object Genre : Route.StringParameterized("genre") - object Playlist : Route.StringParameterized("playlist") - object Lyrics : Route.Simple("lyrics") -} - -fun NavHostController.navigateTo(route: Route.Simple) = navigate(route.route) -fun NavHostController.navigateTo(route: String) = navigate(route) - -fun NavBackStackEntry.getRouteArgument(key: String) = arguments?.getString(key) -fun NavBackStackEntry.getRouteParameter() = getRouteArgument(Route.PARAM_ARGUMENT_NAME) ?: "" diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt index 894c1f9e..0d5ee5ba 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Album.kt @@ -35,20 +35,24 @@ import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.SongListType import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.serialization.Serializable + +@Serializable +data class AlbumViewRoute(val albumId: String) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AlbumView(context: ViewContext, albumId: String) { +fun AlbumView(context: ViewContext, route: AlbumViewRoute) { val allAlbumIds by context.symphony.groove.album.all.collectAsState() val allSongIds by context.symphony.groove.song.all.collectAsState() val album by remember(allAlbumIds) { - derivedStateOf { context.symphony.groove.album.get(albumId) } + derivedStateOf { context.symphony.groove.album.get(route.albumId) } } val songIds by remember(album, allSongIds) { derivedStateOf { album?.getSongIds(context.symphony) ?: listOf() } } val isViable by remember(allAlbumIds) { - derivedStateOf { allAlbumIds.contains(albumId) } + derivedStateOf { allAlbumIds.contains(route.albumId) } } Scaffold( @@ -100,7 +104,7 @@ fun AlbumView(context: ViewContext, albumId: String) { }, cardThumbnailLabelStyle = SongCardThumbnailLabelStyle.Subtle, ) - } else UnknownAlbum(context, albumId) + } else UnknownAlbum(context, route.albumId) } }, bottomBar = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt index 440741fb..4ed1a55d 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/AlbumArtist.kt @@ -35,15 +35,19 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.serialization.Serializable + +@Serializable +data class AlbumArtistViewRoute(val albumArtistName: String) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AlbumArtistView(context: ViewContext, albumArtistName: String) { +fun AlbumArtistView(context: ViewContext, route: AlbumArtistViewRoute) { val allAlbumArtistNames by context.symphony.groove.albumArtist.all.collectAsState() val allSongIds by context.symphony.groove.song.all.collectAsState() val allAlbumIds = context.symphony.groove.album.all val albumArtist by remember(allAlbumArtistNames) { - derivedStateOf { context.symphony.groove.albumArtist.get(albumArtistName) } + derivedStateOf { context.symphony.groove.albumArtist.get(route.albumArtistName) } } val songIds by remember(albumArtist, allSongIds) { derivedStateOf { albumArtist?.getSongIds(context.symphony) ?: listOf() } @@ -108,7 +112,7 @@ fun AlbumArtistView(context: ViewContext, albumArtistName: String) { } } ) - } else UnknownAlbumArtist(context, albumArtistName) + } else UnknownAlbumArtist(context, route.albumArtistName) } }, bottomBar = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt index 53e34dce..423057ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Artist.kt @@ -35,15 +35,19 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.serialization.Serializable + +@Serializable +data class ArtistViewRoute(val artistName: String) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun ArtistView(context: ViewContext, artistName: String) { +fun ArtistView(context: ViewContext, route: ArtistViewRoute) { val allArtistNames by context.symphony.groove.artist.all.collectAsState() val allSongIds by context.symphony.groove.song.all.collectAsState() val allAlbumIds by context.symphony.groove.album.all.collectAsState() val artist by remember(allArtistNames) { - derivedStateOf { context.symphony.groove.artist.get(artistName) } + derivedStateOf { context.symphony.groove.artist.get(route.artistName) } } val songIds by remember(artist, allSongIds) { derivedStateOf { artist?.getSongIds(context.symphony) ?: listOf() } @@ -52,7 +56,7 @@ fun ArtistView(context: ViewContext, artistName: String) { derivedStateOf { artist?.getAlbumIds(context.symphony) ?: listOf() } } val isViable by remember(allArtistNames) { - derivedStateOf { allArtistNames.contains(artistName) } + derivedStateOf { allArtistNames.contains(route.artistName) } } Scaffold( @@ -107,7 +111,7 @@ fun ArtistView(context: ViewContext, artistName: String) { } } ) - } else UnknownArtist(context, artistName) + } else UnknownArtist(context, route.artistName) } }, bottomBar = { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt index 35cd1e21..59fde821 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Base.kt @@ -3,43 +3,56 @@ package io.github.zyrouge.symphony.ui.view import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute import io.github.zyrouge.symphony.MainActivity import io.github.zyrouge.symphony.Symphony -import io.github.zyrouge.symphony.services.groove.Groove import io.github.zyrouge.symphony.ui.helpers.FadeTransition -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ScaleTransition import io.github.zyrouge.symphony.ui.helpers.SlideTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.getRouteArgument -import io.github.zyrouge.symphony.ui.helpers.getRouteParameter import io.github.zyrouge.symphony.ui.theme.SymphonyTheme +import io.github.zyrouge.symphony.ui.view.settings.AppearanceSettingsView +import io.github.zyrouge.symphony.ui.view.settings.AppearanceSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.GrooveSettingsView +import io.github.zyrouge.symphony.ui.view.settings.GrooveSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.HomePageSettingsView +import io.github.zyrouge.symphony.ui.view.settings.HomePageSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.MiniPlayerSettingsView +import io.github.zyrouge.symphony.ui.view.settings.MiniPlayerSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.NowPlayingSettingsView +import io.github.zyrouge.symphony.ui.view.settings.NowPlayingSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.PlayerSettingsView +import io.github.zyrouge.symphony.ui.view.settings.PlayerSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.UpdateSettingsView +import io.github.zyrouge.symphony.ui.view.settings.UpdateSettingsViewRoute @Composable fun BaseView(symphony: Symphony, activity: MainActivity) { - val context = ViewContext( - symphony = symphony, - activity = activity, - navController = rememberNavController(), - ) + val navController = rememberNavController() + val context = remember { + ViewContext( + symphony = symphony, + activity = activity, + navController = navController, + ) + } SymphonyTheme(context) { Surface(color = MaterialTheme.colorScheme.background) { NavHost( - navController = context.navController, - startDestination = Routes.Home.route, + navController = navController, + startDestination = HomeViewRoute, ) { - composable( - Routes.Home.template(), + composable( enterTransition = { FadeTransition.enterTransition() }, ) { HomeView(context) } - composable( - Routes.NowPlaying.template(), + composable( enterTransition = { SlideTransition.slideUp.enterTransition() }, exitTransition = { FadeTransition.exitTransition() }, popEnterTransition = { FadeTransition.enterTransition() }, @@ -47,79 +60,102 @@ fun BaseView(symphony: Symphony, activity: MainActivity) { ) { NowPlayingView(context) } - composable( - Routes.Queue.template(), + composable( enterTransition = { SlideTransition.slideUp.enterTransition() }, exitTransition = { SlideTransition.slideDown.exitTransition() }, ) { QueueView(context) } - composable( - Routes.Settings.template(), - arguments = Routes.Settings.arguments(), - enterTransition = { ScaleTransition.scaleDown.enterTransition() }, - exitTransition = { ScaleTransition.scaleUp.exitTransition() }, - ) { backStackEntry -> - SettingsView( - context, - initialElement = backStackEntry.getRouteArgument(Routes.Settings.ELEMENT_ARGUMENT_NAME) - ?.let { SettingsViewElements.valueOf(it) }, - ) - } - composable( - Routes.Artist.template(), + composable( enterTransition = { SlideTransition.slideLeft.enterTransition() }, exitTransition = { FadeTransition.exitTransition() }, ) { backStackEntry -> - ArtistView(context, backStackEntry.getRouteParameter()) + ArtistView(context, backStackEntry.toRoute()) } - composable( - Routes.Album.template(), + composable( enterTransition = { SlideTransition.slideLeft.enterTransition() }, exitTransition = { FadeTransition.exitTransition() }, ) { backStackEntry -> - AlbumView(context, backStackEntry.getRouteParameter()) + AlbumView(context, backStackEntry.toRoute()) } - composable( - Routes.Search.template(), + composable( enterTransition = { SlideTransition.slideDown.enterTransition() }, exitTransition = { SlideTransition.slideUp.exitTransition() }, ) { backStackEntry -> - SearchView( - context, - backStackEntry.getRouteParameter() - .takeIf { it != "null" } - ?.let { Groove.Kinds.valueOf(it) } - ) - } - composable( - Routes.AlbumArtist.template(), + SearchView(context, backStackEntry.toRoute()) + } + composable( enterTransition = { SlideTransition.slideLeft.enterTransition() }, exitTransition = { FadeTransition.exitTransition() }, ) { backStackEntry -> - AlbumArtistView(context, backStackEntry.getRouteParameter()) + AlbumArtistView(context, backStackEntry.toRoute()) } - composable( - Routes.Genre.template(), + composable( enterTransition = { SlideTransition.slideLeft.enterTransition() }, exitTransition = { FadeTransition.exitTransition() }, ) { backStackEntry -> - GenreView(context, backStackEntry.getRouteParameter()) + GenreView(context, backStackEntry.toRoute()) } - composable( - Routes.Playlist.template(), + composable( enterTransition = { SlideTransition.slideLeft.enterTransition() }, exitTransition = { FadeTransition.exitTransition() }, ) { backStackEntry -> - PlaylistView(context, backStackEntry.getRouteParameter()) + PlaylistView(context, backStackEntry.toRoute()) } - composable( - Routes.Lyrics.template(), + composable( enterTransition = { SlideTransition.slideUp.enterTransition() }, exitTransition = { SlideTransition.slideDown.exitTransition() }, ) { LyricsView(context) } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { backStackEntry -> + SettingsView(context, backStackEntry.toRoute()) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { + AppearanceSettingsView(context) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { backStackEntry -> + GrooveSettingsView(context, backStackEntry.toRoute()) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { + HomePageSettingsView(context) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { + MiniPlayerSettingsView(context) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { + NowPlayingSettingsView(context) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { + PlayerSettingsView(context) + } + composable( + enterTransition = { ScaleTransition.scaleDown.enterTransition() }, + exitTransition = { ScaleTransition.scaleUp.exitTransition() }, + ) { + UpdateSettingsView(context) + } } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt index 80d1f766..e3c485ef 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Genre.kt @@ -29,20 +29,24 @@ import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.SongList import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext +import kotlinx.serialization.Serializable + +@Serializable +data class GenreViewRoute(val genreName: String) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun GenreView(context: ViewContext, genreName: String) { +fun GenreView(context: ViewContext, route: GenreViewRoute) { val allGenreNames by context.symphony.groove.genre.all.collectAsState() val allSongIds by context.symphony.groove.song.all.collectAsState() val genre by remember(allGenreNames) { - derivedStateOf { context.symphony.groove.genre.get(genreName) } + derivedStateOf { context.symphony.groove.genre.get(route.genreName) } } val songIds by remember(genre, allSongIds) { derivedStateOf { genre?.getSongIds(context.symphony) ?: listOf() } } val isViable by remember(allGenreNames) { - derivedStateOf { allGenreNames.contains(genreName) } + derivedStateOf { allGenreNames.contains(route.genreName) } } Scaffold( @@ -94,7 +98,7 @@ fun GenreView(context: ViewContext, genreName: String) { ) { when { isViable -> SongList(context, songIds = songIds) - else -> UnknownGenre(context, genreName) + else -> UnknownGenre(context, route.genreName) } } }, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt index b4876c17..d28e2f3a 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Home.kt @@ -79,11 +79,9 @@ import io.github.zyrouge.symphony.ui.components.IntroductoryDialog import io.github.zyrouge.symphony.ui.components.NowPlayingBottomBar import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.components.swipeable -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ScaleTransition import io.github.zyrouge.symphony.ui.helpers.SlideTransition import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo import io.github.zyrouge.symphony.ui.view.home.AlbumArtistsView import io.github.zyrouge.symphony.ui.view.home.AlbumsView import io.github.zyrouge.symphony.ui.view.home.ArtistsView @@ -95,6 +93,7 @@ import io.github.zyrouge.symphony.ui.view.home.PlaylistsView import io.github.zyrouge.symphony.ui.view.home.SongsView import io.github.zyrouge.symphony.ui.view.home.TreeView import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable enum class HomePages( val kind: Groove.Kinds? = null, @@ -166,6 +165,9 @@ enum class HomePageBottomBarLabelVisibility { INVISIBLE, } +@Serializable +object HomeViewRoute + @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeView(context: ViewContext) { @@ -195,7 +197,7 @@ fun HomeView(context: ViewContext) { Icon(Icons.Filled.Search, null) }, onClick = { - context.navController.navigateTo(Routes.Search.build(currentTab.kind)) + context.navController.navigate(SearchViewRoute(currentTab.kind)) } ) }, @@ -250,7 +252,7 @@ fun HomeView(context: ViewContext) { }, onClick = { showOptionsDropdown = false - context.navController.navigateTo(Routes.Settings.build()) + context.navController.navigate(SettingsViewRoute()) } ) } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt index 3190ae93..e2a011b4 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Lyrics.kt @@ -34,6 +34,10 @@ import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlaying import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingSeekBar import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingTraditionalControls import io.github.zyrouge.symphony.ui.view.nowPlaying.defaultHorizontalPadding +import kotlinx.serialization.Serializable + +@Serializable +object LyricsViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt index ee013a08..afacbb39 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/NowPlaying.kt @@ -13,6 +13,7 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlaying import io.github.zyrouge.symphony.ui.view.nowPlaying.NowPlayingBody import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.serialization.Serializable @Immutable data class NowPlayingData( @@ -54,6 +55,9 @@ enum class NowPlayingLyricsLayout { SeparatePage, } +@Serializable +object NowPlayingViewRoute + @Composable fun NowPlayingView(context: ViewContext) { BackHandler { diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt index bef87225..08a0b8dc 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Playlist.kt @@ -37,22 +37,26 @@ import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.theme.ThemeColors import io.github.zyrouge.symphony.utils.mutate import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable +data class PlaylistViewRoute(val playlistId: String) @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PlaylistView(context: ViewContext, playlistId: String) { +fun PlaylistView(context: ViewContext, route: PlaylistViewRoute) { val coroutineScope = rememberCoroutineScope() val allPlaylistIds by context.symphony.groove.playlist.all.collectAsState() val updateId by context.symphony.groove.playlist.updateId.collectAsState() var updateCounter by remember { mutableIntStateOf(0) } - val playlist by remember(playlistId, updateId) { - derivedStateOf { context.symphony.groove.playlist.get(playlistId) } + val playlist by remember(route.playlistId, updateId) { + derivedStateOf { context.symphony.groove.playlist.get(route.playlistId) } } val songIds by remember(playlist) { derivedStateOf { playlist?.getSongIds(context.symphony) ?: emptyList() } } - val isViable by remember(allPlaylistIds, playlistId) { - derivedStateOf { allPlaylistIds.contains(playlistId) } + val isViable by remember(allPlaylistIds, route.playlistId) { + derivedStateOf { allPlaylistIds.contains(route.playlistId) } } var showOptionsMenu by remember { mutableStateOf(false) } val isFavoritesPlaylist by remember(playlist) { @@ -158,7 +162,7 @@ fun PlaylistView(context: ViewContext, playlistId: String) { }, ) - else -> UnknownPlaylist(context, playlistId) + else -> UnknownPlaylist(context, route.playlistId) } } }, diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt index ad219c73..c0e4a1ce 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Queue.kt @@ -45,6 +45,10 @@ import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle import io.github.zyrouge.symphony.ui.helpers.ViewContext import io.github.zyrouge.symphony.ui.view.nowPlaying.NothingPlayingBody import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable +object QueueViewRoute @OptIn(ExperimentalMaterial3Api::class) @Composable diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt index 31ce5ddb..47cbae99 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Search.kt @@ -62,15 +62,14 @@ import io.github.zyrouge.symphony.ui.components.GenericGrooveCard import io.github.zyrouge.symphony.ui.components.IconTextBody import io.github.zyrouge.symphony.ui.components.PlaylistDropdownMenu import io.github.zyrouge.symphony.ui.components.SongCard -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo import io.github.zyrouge.symphony.utils.joinToStringIfNotEmpty import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.serialization.Serializable private data class SearchResult( val songIds: List, @@ -81,15 +80,18 @@ private data class SearchResult( val playlistIds: List, ) +@Serializable +data class SearchViewRoute(val initialChip: Groove.Kinds?) + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { +fun SearchView(context: ViewContext, route: SearchViewRoute) { val coroutineScope = rememberCoroutineScope() var terms by rememberSaveable { mutableStateOf("") } var isSearching by remember { mutableStateOf(false) } var results by remember { mutableStateOf(null) } - var selectedChip by rememberSaveable { mutableStateOf(initialChip) } + var selectedChip by rememberSaveable { mutableStateOf(route.initialChip) } fun isChipSelected(kind: Groove.Kinds) = selectedChip == null || selectedChip == kind var currentTermsRoutine: Job? = null @@ -244,7 +246,7 @@ fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { Text(it.label(context)) }, modifier = Modifier.onGloballyPositioned { coordinates -> - if (!initialScroll && initialChip == it) { + if (!initialScroll && route.initialChip == it) { val windowWidth = with(density) { configuration.screenWidthDp.dp.toPx() } @@ -363,8 +365,8 @@ fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { ) }, onClick = { - context.navController.navigateTo( - Routes.Artist.build(artist.name) + context.navController.navigate( + ArtistViewRoute(artist.name) ) } ) @@ -395,8 +397,8 @@ fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { ) }, onClick = { - context.navController.navigateTo( - Routes.Album.build(album.id) + context.navController.navigate( + AlbumViewRoute(album.id) ) } ) @@ -424,10 +426,8 @@ fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { ) }, onClick = { - context.navController.navigateTo( - Routes.AlbumArtist.build( - albumArtist.name - ) + context.navController.navigate( + AlbumArtistViewRoute(albumArtist.name) ) } ) @@ -455,8 +455,8 @@ fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { ) }, onClick = { - context.navController.navigateTo( - Routes.Playlist.build(playlist.id) + context.navController.navigate( + PlaylistViewRoute(playlist.id) ) } ) @@ -480,8 +480,8 @@ fun SearchView(context: ViewContext, initialChip: Groove.Kinds?) { }, options = null, onClick = { - context.navController.navigateTo( - Routes.Genre.build(genre.name) + context.navController.navigate( + GenreViewRoute(genre.name) ) } ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt index 17d619e0..e05044e1 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/Settings.kt @@ -1,9 +1,6 @@ package io.github.zyrouge.symphony.ui.view import android.net.Uri -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.animation.core.repeatable -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -15,49 +12,20 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.relocation.BringIntoViewRequester -import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.Label -import androidx.compose.material.icons.automirrored.filled.Wysiwyg -import androidx.compose.material.icons.automirrored.outlined.Article -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.CenterFocusWeak -import androidx.compose.material.icons.filled.Code -import androidx.compose.material.icons.filled.Colorize -import androidx.compose.material.icons.filled.Dashboard import androidx.compose.material.icons.filled.East -import androidx.compose.material.icons.filled.Face -import androidx.compose.material.icons.filled.FastForward -import androidx.compose.material.icons.filled.FastRewind import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.Forum -import androidx.compose.material.icons.filled.Forward30 -import androidx.compose.material.icons.filled.GraphicEq -import androidx.compose.material.icons.filled.Headset -import androidx.compose.material.icons.filled.HeadsetOff import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.KeyboardDoubleArrowRight -import androidx.compose.material.icons.filled.Language import androidx.compose.material.icons.filled.LibraryMusic import androidx.compose.material.icons.filled.MusicNote import androidx.compose.material.icons.filled.Palette -import androidx.compose.material.icons.filled.PhotoSizeSelectLarge -import androidx.compose.material.icons.filled.Recommend -import androidx.compose.material.icons.filled.RuleFolder -import androidx.compose.material.icons.filled.SkipNext -import androidx.compose.material.icons.filled.SpaceBar -import androidx.compose.material.icons.filled.Storage -import androidx.compose.material.icons.filled.TextFormat -import androidx.compose.material.icons.filled.TextIncrease +import androidx.compose.material.icons.filled.Radio import androidx.compose.material.icons.filled.Update import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme @@ -67,101 +35,41 @@ import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import io.github.zyrouge.symphony.Symphony import io.github.zyrouge.symphony.services.AppMeta -import io.github.zyrouge.symphony.services.i18n.CommonTranslation import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle -import io.github.zyrouge.symphony.ui.components.settings.SettingsFloatInputTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsLinkTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiGrooveFolderTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiOptionTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiSystemFolderTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiTextOptionTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsOptionTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading import io.github.zyrouge.symphony.ui.components.settings.SettingsSimpleTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsSliderTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsSwitchTile -import io.github.zyrouge.symphony.ui.components.settings.SettingsTextInputTile -import io.github.zyrouge.symphony.ui.helpers.TransitionDurations import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.theme.PrimaryThemeColors -import io.github.zyrouge.symphony.ui.theme.SymphonyTypography -import io.github.zyrouge.symphony.ui.theme.ThemeColors -import io.github.zyrouge.symphony.ui.theme.ThemeMode -import io.github.zyrouge.symphony.ui.view.home.ForYou +import io.github.zyrouge.symphony.ui.view.settings.AppearanceSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.GrooveSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.HomePageSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.MiniPlayerSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.NowPlayingSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.PlayerSettingsViewRoute +import io.github.zyrouge.symphony.ui.view.settings.UpdateSettingsViewRoute import io.github.zyrouge.symphony.utils.ActivityUtils -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch -import kotlin.math.roundToInt - -private val scalingPresets = listOf( - 0.25f, 0.5f, 0.75f, 0.9f, 1f, - 1.1f, 1.25f, 1.5f, 1.75f, 2f, - 2.25f, 2.5f, 2.75f, 3f, -) +import kotlinx.serialization.Serializable enum class SettingsViewElements { MediaFolders, } +@Serializable +data class SettingsViewRoute(val initialElement: SettingsViewElements? = null) + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsView(context: ViewContext, initialElement: SettingsViewElements?) { - val coroutineScope = rememberCoroutineScope() +fun SettingsView(context: ViewContext, route: SettingsViewRoute) { val snackbarHostState = remember { SnackbarHostState() } val scrollState = rememberScrollState() - val language by context.symphony.settings.language.flow.collectAsState() - val fontFamily by context.symphony.settings.fontFamily.flow.collectAsState() - val themeMode by context.symphony.settings.themeMode.flow.collectAsState() - val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsState() - val primaryColor by context.symphony.settings.primaryColor.flow.collectAsState() - val homeTabs by context.symphony.settings.homeTabs.flow.collectAsState() - val forYouContents by context.symphony.settings.forYouContents.flow.collectAsState() - val homePageBottomBarLabelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() - val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsState() - val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsState() - val requireAudioFocus by context.symphony.settings.requireAudioFocus.flow.collectAsState() - val ignoreAudioFocusLoss by context.symphony.settings.ignoreAudioFocusLoss.flow.collectAsState() - val playOnHeadphonesConnect by context.symphony.settings.playOnHeadphonesConnect.flow.collectAsState() - val pauseOnHeadphonesDisconnect by context.symphony.settings.pauseOnHeadphonesDisconnect.flow.collectAsState() - val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() - val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() - val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsState() - val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsState() - val miniPlayerTextMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsState() - val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsState() - val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsState() - val nowPlayingSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsState() - val nowPlayingLyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsState() - val songsFilterPattern by context.symphony.settings.songsFilterPattern.flow.collectAsState() - val blacklistFolders by context.symphony.settings.blacklistFolders.flow.collectAsState() - val whitelistFolders by context.symphony.settings.whitelistFolders.flow.collectAsState() - val artistTagSeparators by context.symphony.settings.artistTagSeparators.flow.collectAsState() - val genreTagSeparators by context.symphony.settings.genreTagSeparators.flow.collectAsState() - val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsState() - val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsState() - val fontScale by context.symphony.settings.fontScale.flow.collectAsState() - val contentScale by context.symphony.settings.contentScale.flow.collectAsState() - val mediaFolders by context.symphony.settings.mediaFolders.flow.collectAsState() - val artworkQuality by context.symphony.settings.artworkQuality.flow.collectAsState() - Scaffold( modifier = Modifier.fillMaxSize(), snackbarHost = { @@ -202,11 +110,6 @@ fun SettingsView(context: ViewContext, initialElement: SettingsViewElements?) { Column(modifier = Modifier.verticalScroll(scrollState)) { val contentColor = MaterialTheme.colorScheme.onPrimary - val seekDurationRange = 3f..60f - val defaultSongsFilterPattern = ".*" - val isLatestVersion = - AppMeta.latestVersion == null || AppMeta.latestVersion == AppMeta.version - Box( modifier = Modifier .fillMaxWidth() @@ -253,710 +156,87 @@ fun SettingsView(context: ViewContext, initialElement: SettingsViewElements?) { ) } } - SettingsSideHeading(context.symphony.t.Appearance) - SettingsOptionTile( - icon = { - Icon(Icons.Filled.Language, null) - }, - title = { - Text(context.symphony.t.Language_) - }, - value = language ?: "", - values = run { - val defaultLocaleNativeName = - context.symphony.translator.getDefaultLocaleNativeName() - mapOf( - "" to "${context.symphony.t.System} (${defaultLocaleNativeName})" - ) + context.symphony.translator.translations.localeNativeNames - }, - captions = run { - val defaultLocaleDisplayName = - context.symphony.translator.getDefaultLocaleDisplayName() - mapOf( - "" to "${CommonTranslation.System} (${defaultLocaleDisplayName})" - ) + context.symphony.translator.translations.localeDisplayNames - }, - onChange = { value -> - context.symphony.settings.language.setValue(value.takeUnless { it == "" }) - } - ) - SettingsOptionTile( - icon = { - Icon(Icons.Filled.TextFormat, null) - }, - title = { - Text(context.symphony.t.Font) - }, - value = SymphonyTypography.resolveFont(fontFamily).fontName, - values = SymphonyTypography.all.keys.associateWith { it }, - onChange = { value -> - context.symphony.settings.fontFamily.setValue(value) - } - ) - SettingsFloatInputTile( - context, + SettingsSimpleTile( icon = { - Icon(Icons.Filled.TextIncrease, null) + Icon(Icons.Filled.LibraryMusic, null) }, title = { - Text(context.symphony.t.FontScale) + Text(context.symphony.t.Groove) }, - value = fontScale, - presets = scalingPresets, - labelText = { "x$it" }, - onReset = { - context.symphony.settings.fontScale.setValue( - context.symphony.settings.fontScale.defaultValue, + onClick = { + context.navController.navigate( + GrooveSettingsViewRoute(route.initialElement) ) }, - onChange = { value -> - context.symphony.settings.fontScale.setValue(value) - } ) - SettingsFloatInputTile( - context, + SettingsSimpleTile( icon = { - Icon(Icons.Filled.PhotoSizeSelectLarge, null) + Icon(Icons.Filled.Radio, null) }, title = { - Text(context.symphony.t.ContentScale) + Text(context.symphony.t.Player) }, - value = contentScale, - presets = scalingPresets, - labelText = { "x$it" }, - onReset = { - context.symphony.settings.contentScale.setValue( - context.symphony.settings.contentScale.defaultValue, - ) + onClick = { + context.navController.navigate(PlayerSettingsViewRoute) }, - onChange = { value -> - context.symphony.settings.contentScale.setValue(value) - } ) - SettingsOptionTile( + SettingsSimpleTile( icon = { Icon(Icons.Filled.Palette, null) }, title = { - Text(context.symphony.t.Theme) - }, - value = themeMode, - values = mapOf( - ThemeMode.SYSTEM to context.symphony.t.SystemLightDark, - ThemeMode.SYSTEM_BLACK to context.symphony.t.SystemLightBlack, - ThemeMode.LIGHT to context.symphony.t.Light, - ThemeMode.DARK to context.symphony.t.Dark, - ThemeMode.BLACK to context.symphony.t.Black, - ), - onChange = { value -> - context.symphony.settings.themeMode.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.Face, null) - }, - title = { - Text(context.symphony.t.MaterialYou) + Text(context.symphony.t.Appearance) }, - value = useMaterialYou, - onChange = { value -> - context.symphony.settings.useMaterialYou.setValue(value) - } - ) - SettingsOptionTile( - icon = { - Icon(Icons.Filled.Colorize, null) - }, - title = { - Text(context.symphony.t.PrimaryColor) + onClick = { + context.navController.navigate(AppearanceSettingsViewRoute) }, - value = ThemeColors.resolvePrimaryColorKey(primaryColor), - values = PrimaryThemeColors.entries.associateWith { it.label(context) }, - enabled = !useMaterialYou, - onChange = { value -> - context.symphony.settings.primaryColor.setValue(value.name) - } ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.Interface) - SettingsMultiOptionTile( - context, + SettingsSimpleTile( icon = { Icon(Icons.Filled.Home, null) }, title = { - Text(context.symphony.t.HomeTabs) - }, - note = { - Text(context.symphony.t.SelectAtleast2orAtmost5Tabs) - }, - value = homeTabs, - values = HomePages.entries.associateWith { it.label(context) }, - satisfies = { it.size in 2..5 }, - onChange = { value -> - context.symphony.settings.homeTabs.setValue(value) - } - ) - SettingsMultiOptionTile( - context, - icon = { - Icon(Icons.Filled.Recommend, null) - }, - title = { - Text(context.symphony.t.ForYou) - }, - value = forYouContents, - values = ForYou.entries.associateWith { it.label(context) }, - onChange = { value -> - context.symphony.settings.forYouContents.setValue(value) - } - ) - SettingsOptionTile( - icon = { - Icon(Icons.AutoMirrored.Filled.Label, null) - }, - title = { - Text(context.symphony.t.BottomBarLabelVisibility) - }, - value = homePageBottomBarLabelVisibility, - values = HomePageBottomBarLabelVisibility.entries - .associateWith { it.label(context) }, - onChange = { value -> - context.symphony.settings.homePageBottomBarLabelVisibility.setValue( - value, - ) - } - ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.Player) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.GraphicEq, null) - }, - title = { - Text(context.symphony.t.FadePlaybackInOut) - }, - value = fadePlayback, - onChange = { value -> - context.symphony.settings.fadePlayback.setValue(value) - } - ) - SettingsSliderTile( - context, - icon = { - Icon(Icons.Filled.GraphicEq, null) + Text(context.symphony.t.HomePage) }, - title = { - Text(context.symphony.t.FadePlaybackInOut) - }, - label = { value -> - Text(context.symphony.t.XSecs(value.toString())) - }, - range = 0.5f..6f, - initialValue = fadePlaybackDuration, - onValue = { value -> - value.times(2).roundToInt().toFloat().div(2) - }, - onChange = { value -> - context.symphony.settings.fadePlaybackDuration.setValue(value) - }, - onReset = { - context.symphony.settings.fadePlaybackDuration.setValue( - context.symphony.settings.fadePlaybackDuration.defaultValue, - ) - }, - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.CenterFocusWeak, null) - }, - title = { - Text(context.symphony.t.RequireAudioFocus) - }, - value = requireAudioFocus, - onChange = { value -> - context.symphony.settings.requireAudioFocus.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.CenterFocusWeak, null) - }, - title = { - Text(context.symphony.t.IgnoreAudioFocusLoss) - }, - value = ignoreAudioFocusLoss, - onChange = { value -> - context.symphony.settings.ignoreAudioFocusLoss.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.Headset, null) - }, - title = { - Text(context.symphony.t.PlayOnHeadphonesConnect) - }, - value = playOnHeadphonesConnect, - onChange = { value -> - context.symphony.settings.playOnHeadphonesConnect.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.HeadsetOff, null) - }, - title = { - Text(context.symphony.t.PauseOnHeadphonesDisconnect) - }, - value = pauseOnHeadphonesDisconnect, - onChange = { value -> - context.symphony.settings.pauseOnHeadphonesDisconnect.setValue(value) - } - ) - SettingsSliderTile( - context, - icon = { - Icon(Icons.Filled.FastRewind, null) - }, - title = { - Text(context.symphony.t.FastRewindDuration) - }, - label = { value -> - Text(context.symphony.t.XSecs(value.toString())) - }, - range = seekDurationRange, - initialValue = seekBackDuration.toFloat(), - onValue = { value -> - value.roundToInt().toFloat() - }, - onChange = { value -> - context.symphony.settings.seekBackDuration.setValue(value.toInt()) - }, - onReset = { - context.symphony.settings.seekBackDuration.setValue( - context.symphony.settings.seekBackDuration.defaultValue, - ) - }, - ) - SettingsSliderTile( - context, - icon = { - Icon(Icons.Filled.FastForward, null) - }, - title = { - Text(context.symphony.t.FastForwardDuration) - }, - label = { value -> - Text(context.symphony.t.XSecs(value.toString())) - }, - range = seekDurationRange, - initialValue = seekForwardDuration.toFloat(), - onValue = { value -> - value.roundToInt().toFloat() - }, - onChange = { value -> - context.symphony.settings.seekForwardDuration.setValue(value.toInt()) - }, - onReset = { - context.symphony.settings.seekForwardDuration.setValue( - context.symphony.settings.seekForwardDuration.defaultValue, - ) - }, - ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.MiniPlayer) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.SkipNext, null) - }, - title = { - Text(context.symphony.t.ShowTrackControls) - }, - value = miniPlayerTrackControls, - onChange = { value -> - context.symphony.settings.miniPlayerTrackControls.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.Forward30, null) - }, - title = { - Text(context.symphony.t.ShowSeekControls) - }, - value = miniPlayerSeekControls, - onChange = { value -> - context.symphony.settings.miniPlayerSeekControls.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.KeyboardDoubleArrowRight, null) - }, - title = { - Text(context.symphony.t.MiniPlayerTextMarquee) - }, - value = miniPlayerTextMarquee, - onChange = { value -> - context.symphony.settings.miniPlayerTextMarquee.setValue(value) - } - ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.NowPlaying) - SettingsOptionTile( - icon = { - Icon(Icons.Filled.Dashboard, null) - }, - title = { - Text(context.symphony.t.ControlsLayout) - }, - value = nowPlayingControlsLayout, - values = NowPlayingControlsLayout.entries - .associateWith { it.label(context) }, - onChange = { value -> - context.symphony.settings.nowPlayingControlsLayout.setValue(value) - } - ) - SettingsOptionTile( - icon = { - Icon(Icons.AutoMirrored.Outlined.Article, null) - }, - title = { - Text(context.symphony.t.LyricsLayout) - }, - value = nowPlayingLyricsLayout, - values = NowPlayingLyricsLayout.entries - .associateWith { it.label(context) }, - onChange = { value -> - context.symphony.settings.nowPlayingLyricsLayout.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.AutoMirrored.Filled.Wysiwyg, null) - }, - title = { - Text(context.symphony.t.ShowAudioInformation) - }, - value = nowPlayingAdditionalInfo, - onChange = { value -> - context.symphony.settings.nowPlayingAdditionalInfo.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.Forward30, null) - }, - title = { - Text(context.symphony.t.ShowSeekControls) - }, - value = nowPlayingSeekControls, - onChange = { value -> - context.symphony.settings.nowPlayingSeekControls.setValue(value) - } - ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.Groove) - SpotlightTile(initialElement == SettingsViewElements.MediaFolders) { - SettingsMultiSystemFolderTile( - context, - icon = { - Icon(Icons.Filled.LibraryMusic, null) - }, - title = { - Text(context.symphony.t.MediaFolders) - }, - initialValues = mediaFolders, - onChange = { values -> - context.symphony.settings.mediaFolders.setValue(values) - refetchMediaLibrary(coroutineScope, context.symphony) - } - ) - } - SettingsTextInputTile( - context, - icon = { - Icon(Icons.Filled.FilterAlt, null) - }, - title = { - Text(context.symphony.t.SongsFilterPattern) - }, - value = songsFilterPattern ?: defaultSongsFilterPattern, - onReset = { - context.symphony.settings.songsFilterPattern.setValue(null) - }, - onChange = { value -> - context.symphony.settings.songsFilterPattern.setValue( - when (value) { - defaultSongsFilterPattern -> null - else -> value - } - ) - refetchMediaLibrary(coroutineScope, context.symphony) - } - ) - SettingsMultiGrooveFolderTile( - context, - icon = { - Icon(Icons.Filled.RuleFolder, null) - }, - title = { - Text(context.symphony.t.BlacklistFolders) - }, - explorer = context.symphony.groove.exposer.explorer, - initialValues = blacklistFolders, - onChange = { values -> - context.symphony.settings.blacklistFolders.setValue(values) - refetchMediaLibrary(coroutineScope, context.symphony) - } - ) - SettingsMultiGrooveFolderTile( - context, - icon = { - Icon(Icons.Filled.RuleFolder, null) - }, - title = { - Text(context.symphony.t.WhitelistFolders) - }, - explorer = context.symphony.groove.exposer.explorer, - initialValues = whitelistFolders, - onChange = { values -> - context.symphony.settings.whitelistFolders.setValue(values) - refetchMediaLibrary(coroutineScope, context.symphony) - } - ) - SettingsMultiTextOptionTile( - context, - icon = { - Icon(Icons.Filled.SpaceBar, null) - }, - title = { - Text(context.symphony.t.ArtistTagValueSeparators) - }, - values = artistTagSeparators.toList(), - onChange = { - context.symphony.settings.artistTagSeparators.setValue(it.toSet()) - refetchMediaLibrary(coroutineScope, context.symphony) - }, - ) - SettingsMultiTextOptionTile( - context, - icon = { - Icon(Icons.Filled.SpaceBar, null) - }, - title = { - Text(context.symphony.t.GenreTagValueSeparators) - }, - values = genreTagSeparators.toList(), - onChange = { - context.symphony.settings.genreTagSeparators.setValue(it.toSet()) - refetchMediaLibrary(coroutineScope, context.symphony) + onClick = { + context.navController.navigate(HomePageSettingsViewRoute) }, ) SettingsSimpleTile( icon = { - Icon(Icons.Filled.Storage, null) + Icon(Icons.Filled.MusicNote, null) }, title = { - Text(context.symphony.t.ClearSongCache) + Text(context.symphony.t.MiniPlayer) }, onClick = { - coroutineScope.launch { - context.symphony.database.songCache.clear() - context.symphony.database.artworkCache.clear() - context.symphony.database.lyricsCache.clear() - refetchMediaLibrary(coroutineScope, context.symphony) - snackbarHostState.showSnackbar( - context.symphony.t.SongCacheCleared, - withDismissAction = true, - ) - } - } - ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.Updates) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.Update, null) - }, - title = { - Text(context.symphony.t.CheckForUpdates) - }, - value = checkForUpdates, - onChange = { value -> - context.symphony.settings.checkForUpdates.setValue(value) - } - ) - SettingsSwitchTile( - icon = { - Icon(Icons.Filled.Update, null) - }, - title = { - Text(context.symphony.t.ShowUpdateToast) - }, - value = showUpdateToast, - onChange = { value -> - context.symphony.settings.showUpdateToast.setValue(value) - } - ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.Help) - SettingsLinkTile( - context, - icon = { - Icon(Icons.Filled.BugReport, null) - }, - title = { - Text(context.symphony.t.ReportAnIssue) - }, - url = AppMeta.githubIssuesUrl - ) - SettingsLinkTile( - context, - icon = { - Icon(Icons.Filled.Forum, null) - }, - title = { - Text(context.symphony.t.Discord) - }, - url = AppMeta.discordUrl - ) - SettingsLinkTile( - context, - icon = { - Icon(Icons.Filled.Forum, null) + context.navController.navigate(MiniPlayerSettingsViewRoute) }, - title = { - Text(context.symphony.t.Reddit) - }, - url = AppMeta.redditUrl ) - HorizontalDivider() - SettingsSideHeading(context.symphony.t.About) SettingsSimpleTile( icon = { Icon(Icons.Filled.MusicNote, null) }, title = { - Text("${AppMeta.appName} ${AppMeta.version}") - }, - subtitle = when { - !isLatestVersion -> ({ - Text(context.symphony.t.NewVersionAvailableX(AppMeta.latestVersion!!)) - }) - - else -> null + Text(context.symphony.t.NowPlayingPage) }, onClick = { - ActivityUtils.startBrowserActivity( - context.activity, - Uri.parse(if (isLatestVersion) AppMeta.githubRepositoryUrl else AppMeta.githubLatestReleaseUrl) - ) - } - ) - SettingsLinkTile( - context, - icon = { - Icon( - Icons.Filled.Favorite, - null, - tint = Color.Red, - ) + context.navController.navigate(NowPlayingSettingsViewRoute) }, - title = { - Text(context.symphony.t.MadeByX(AppMeta.author)) - }, - url = AppMeta.githubProfileUrl ) - SettingsLinkTile( - context, + SettingsSimpleTile( icon = { - Icon(Icons.Filled.Code, null) + Icon(Icons.Filled.Update, null) }, title = { - Text(context.symphony.t.Github) + Text(context.symphony.t.Updates) + }, + onClick = { + context.navController.navigate(UpdateSettingsViewRoute) }, - url = AppMeta.githubRepositoryUrl ) } } } ) } - -fun HomePageBottomBarLabelVisibility.label(context: ViewContext) = when (this) { - HomePageBottomBarLabelVisibility.ALWAYS_VISIBLE -> context.symphony.t.AlwaysVisible - HomePageBottomBarLabelVisibility.VISIBLE_WHEN_ACTIVE -> context.symphony.t.VisibleWhenActive - HomePageBottomBarLabelVisibility.INVISIBLE -> context.symphony.t.Invisible -} - -fun NowPlayingControlsLayout.label(context: ViewContext) = when (this) { - NowPlayingControlsLayout.Default -> context.symphony.t.Default - NowPlayingControlsLayout.Traditional -> context.symphony.t.Traditional -} - -fun NowPlayingLyricsLayout.label(context: ViewContext) = when (this) { - NowPlayingLyricsLayout.ReplaceArtwork -> context.symphony.t.ReplaceArtwork - NowPlayingLyricsLayout.SeparatePage -> context.symphony.t.SeparatePage -} - -fun PrimaryThemeColors.label(context: ViewContext) = when (this) { - PrimaryThemeColors.Red -> context.symphony.t.Red - PrimaryThemeColors.Orange -> context.symphony.t.Orange - PrimaryThemeColors.Amber -> context.symphony.t.Amber - PrimaryThemeColors.Yellow -> context.symphony.t.Yellow - PrimaryThemeColors.Lime -> context.symphony.t.Lime - PrimaryThemeColors.Green -> context.symphony.t.Green - PrimaryThemeColors.Emerald -> context.symphony.t.Emerald - PrimaryThemeColors.Teal -> context.symphony.t.Teal - PrimaryThemeColors.Cyan -> context.symphony.t.Cyan - PrimaryThemeColors.Sky -> context.symphony.t.Sky - PrimaryThemeColors.Blue -> context.symphony.t.Blue - PrimaryThemeColors.Indigo -> context.symphony.t.Indigo - PrimaryThemeColors.Violet -> context.symphony.t.Violet - PrimaryThemeColors.Purple -> context.symphony.t.Purple - PrimaryThemeColors.Fuchsia -> context.symphony.t.Fuchsia - PrimaryThemeColors.Pink -> context.symphony.t.Pink - PrimaryThemeColors.Rose -> context.symphony.t.Rose -} - -private fun refetchMediaLibrary(coroutineScope: CoroutineScope, symphony: Symphony) { - symphony.radio.stop() - coroutineScope.launch { - symphony.groove.refetch() - } -} - -@OptIn(ExperimentalFoundationApi::class) -@Composable -private fun SpotlightTile(isInSpotlight: Boolean, content: @Composable (() -> Unit)) { - val bringIntoViewRequester = remember { BringIntoViewRequester() } - var animateSpotlightEffect by remember { mutableStateOf(false) } - val highlightAlphaAnimated by animateFloatAsState( - targetValue = if (animateSpotlightEffect) 0.25f else 0f, - label = "spotlight-outline-alpha", - animationSpec = repeatable(2, TransitionDurations.Slow.asTween()), - ) - val highlightColor = MaterialTheme.colorScheme.surfaceTint - - LaunchedEffect(isInSpotlight) { - if (isInSpotlight) { - animateSpotlightEffect = true - bringIntoViewRequester.bringIntoView() - animateSpotlightEffect = false - } - } - - Box( - modifier = Modifier - .bringIntoViewRequester(bringIntoViewRequester) - .drawWithContent { - drawContent() - drawRect(color = highlightColor, alpha = highlightAlphaAnimated) - } - ) { - content() - } -} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt index d27e792c..31647436 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/home/ForYou.kt @@ -51,9 +51,10 @@ import coil.compose.AsyncImage import io.github.zyrouge.symphony.services.groove.repositories.SongRepository import io.github.zyrouge.symphony.services.radio.Radio import io.github.zyrouge.symphony.ui.components.IconTextBody -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.AlbumArtistViewRoute +import io.github.zyrouge.symphony.ui.view.AlbumViewRoute +import io.github.zyrouge.symphony.ui.view.ArtistViewRoute import io.github.zyrouge.symphony.utils.randomSubList import io.github.zyrouge.symphony.utils.runIfOrDefault import io.github.zyrouge.symphony.utils.subListNonStrict @@ -440,7 +441,7 @@ private fun SuggestedAlbums( StatedSixGrid(context, isLoading, albums) { album -> Card( onClick = { - context.navController.navigateTo(Routes.Album.build(album.id)) + context.navController.navigate(AlbumViewRoute(album.id)) } ) { AsyncImage( @@ -478,7 +479,7 @@ private fun SuggestedArtists( StatedSixGrid(context, isLoading, artists) { artist -> Card( onClick = { - context.navController.navigateTo(Routes.Artist.build(artist.name)) + context.navController.navigate(ArtistViewRoute(artist.name)) } ) { AsyncImage( @@ -516,7 +517,7 @@ private fun SuggestedAlbumArtists( StatedSixGrid(context, isLoading, albumArtists) { albumArtist -> Card( onClick = { - context.navController.navigateTo(Routes.AlbumArtist.build(albumArtist.name)) + context.navController.navigate(AlbumArtistViewRoute(albumArtist.name)) } ) { AsyncImage( diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt index 9924d1e9..bbc95e12 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyContent.kt @@ -53,9 +53,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.ui.components.SongDropdownMenu import io.github.zyrouge.symphony.ui.helpers.FadeTransition -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.ArtistViewRoute import io.github.zyrouge.symphony.ui.view.NowPlayingControlsLayout import io.github.zyrouge.symphony.ui.view.NowPlayingData import io.github.zyrouge.symphony.utils.DurationUtils @@ -97,9 +96,7 @@ fun NowPlayingBodyContent(context: ViewContext, data: NowPlayingData) { overflow = TextOverflow.Ellipsis, modifier = Modifier.pointerInput(Unit) { detectTapGestures { _ -> - context.navController.navigateTo( - Routes.Artist.build(it) - ) + context.navController.navigate(ArtistViewRoute(it)) } }, ) diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt index 366ff9d6..3d84fece 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BodyCover.kt @@ -31,10 +31,9 @@ import io.github.zyrouge.symphony.ui.components.LyricsText import io.github.zyrouge.symphony.ui.components.TimedContentTextStyle import io.github.zyrouge.symphony.ui.components.swipeable import io.github.zyrouge.symphony.ui.helpers.FadeTransition -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ScreenOrientation import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.AlbumViewRoute import io.github.zyrouge.symphony.ui.view.NowPlayingData import io.github.zyrouge.symphony.ui.view.NowPlayingStates @@ -138,9 +137,7 @@ private fun NowPlayingBodyCoverArtwork(context: ViewContext, song: Song) { context.symphony.groove.album .getIdFromSong(song) ?.let { - context.navController.navigateTo( - Routes.Album.build(it) - ) + context.navController.navigate(AlbumViewRoute(it)) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt index f5dca088..e4698a93 100644 --- a/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/nowPlaying/BottomBar.kt @@ -47,13 +47,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.github.zyrouge.symphony.services.radio.RadioLoopMode -import io.github.zyrouge.symphony.ui.helpers.Routes import io.github.zyrouge.symphony.ui.helpers.ViewContext -import io.github.zyrouge.symphony.ui.helpers.navigateTo +import io.github.zyrouge.symphony.ui.view.LyricsViewRoute import io.github.zyrouge.symphony.ui.view.NowPlayingData import io.github.zyrouge.symphony.ui.view.NowPlayingDefaults import io.github.zyrouge.symphony.ui.view.NowPlayingLyricsLayout import io.github.zyrouge.symphony.ui.view.NowPlayingStates +import io.github.zyrouge.symphony.ui.view.QueueViewRoute import io.github.zyrouge.symphony.utils.Logger import kotlinx.coroutines.launch @@ -88,7 +88,7 @@ fun NowPlayingBodyBottomBar( ) { TextButton( onClick = { - context.navController.navigateTo(Routes.Queue) + context.navController.navigate(QueueViewRoute) } ) { Icon( @@ -119,7 +119,7 @@ fun NowPlayingBodyBottomBar( } NowPlayingLyricsLayout.SeparatePage -> { - context.navController.navigateTo(Routes.Lyrics) + context.navController.navigate(LyricsViewRoute) } } } diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt new file mode 100644 index 00000000..b7ec1da1 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/AppearanceSettingsView.kt @@ -0,0 +1,322 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Colorize +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.Face +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Language +import androidx.compose.material.icons.filled.Palette +import androidx.compose.material.icons.filled.PhotoSizeSelectLarge +import androidx.compose.material.icons.filled.TextFormat +import androidx.compose.material.icons.filled.TextIncrease +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.services.i18n.CommonTranslation +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsFloatInputTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsOptionTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.components.settings.SettingsSwitchTile +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.theme.PrimaryThemeColors +import io.github.zyrouge.symphony.ui.theme.SymphonyTypography +import io.github.zyrouge.symphony.ui.theme.ThemeColors +import io.github.zyrouge.symphony.ui.theme.ThemeMode +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.serialization.Serializable + +private val scalingPresets = listOf( + 0.25f, 0.5f, 0.75f, 0.9f, 1f, + 1.1f, 1.25f, 1.5f, 1.75f, 2f, + 2.25f, 2.5f, 2.75f, 3f, +) + +@Serializable +object AppearanceSettingsViewRoute + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AppearanceSettingsView(context: ViewContext) { + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val language by context.symphony.settings.language.flow.collectAsState() + val fontFamily by context.symphony.settings.fontFamily.flow.collectAsState() + val themeMode by context.symphony.settings.themeMode.flow.collectAsState() + val useMaterialYou by context.symphony.settings.useMaterialYou.flow.collectAsState() + val primaryColor by context.symphony.settings.primaryColor.flow.collectAsState() + val fontScale by context.symphony.settings.fontScale.flow.collectAsState() + val contentScale by context.symphony.settings.contentScale.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.Appearance) + SettingsOptionTile( + icon = { + Icon(Icons.Filled.Language, null) + }, + title = { + Text(context.symphony.t.Language_) + }, + value = language ?: "", + values = run { + val defaultLocaleNativeName = + context.symphony.translator.getDefaultLocaleNativeName() + mapOf( + "" to "${context.symphony.t.System} (${defaultLocaleNativeName})" + ) + context.symphony.translator.translations.localeNativeNames + }, + captions = run { + val defaultLocaleDisplayName = + context.symphony.translator.getDefaultLocaleDisplayName() + mapOf( + "" to "${CommonTranslation.System} (${defaultLocaleDisplayName})" + ) + context.symphony.translator.translations.localeDisplayNames + }, + onChange = { value -> + context.symphony.settings.language.setValue(value.takeUnless { it == "" }) + } + ) + SettingsOptionTile( + icon = { + Icon(Icons.Filled.TextFormat, null) + }, + title = { + Text(context.symphony.t.Font) + }, + value = SymphonyTypography.resolveFont(fontFamily).fontName, + values = SymphonyTypography.all.keys.associateWith { it }, + onChange = { value -> + context.symphony.settings.fontFamily.setValue(value) + } + ) + SettingsFloatInputTile( + context, + icon = { + Icon(Icons.Filled.TextIncrease, null) + }, + title = { + Text(context.symphony.t.FontScale) + }, + value = fontScale, + presets = scalingPresets, + labelText = { "x$it" }, + onReset = { + context.symphony.settings.fontScale.setValue( + context.symphony.settings.fontScale.defaultValue, + ) + }, + onChange = { value -> + context.symphony.settings.fontScale.setValue(value) + } + ) + SettingsFloatInputTile( + context, + icon = { + Icon(Icons.Filled.PhotoSizeSelectLarge, null) + }, + title = { + Text(context.symphony.t.ContentScale) + }, + value = contentScale, + presets = scalingPresets, + labelText = { "x$it" }, + onReset = { + context.symphony.settings.contentScale.setValue( + context.symphony.settings.contentScale.defaultValue, + ) + }, + onChange = { value -> + context.symphony.settings.contentScale.setValue(value) + } + ) + SettingsOptionTile( + icon = { + Icon(Icons.Filled.Palette, null) + }, + title = { + Text(context.symphony.t.Theme) + }, + value = themeMode, + values = mapOf( + ThemeMode.SYSTEM to context.symphony.t.SystemLightDark, + ThemeMode.SYSTEM_BLACK to context.symphony.t.SystemLightBlack, + ThemeMode.LIGHT to context.symphony.t.Light, + ThemeMode.DARK to context.symphony.t.Dark, + ThemeMode.BLACK to context.symphony.t.Black, + ), + onChange = { value -> + context.symphony.settings.themeMode.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.Face, null) + }, + title = { + Text(context.symphony.t.MaterialYou) + }, + value = useMaterialYou, + onChange = { value -> + context.symphony.settings.useMaterialYou.setValue(value) + } + ) + SettingsOptionTile( + icon = { + Icon(Icons.Filled.Colorize, null) + }, + title = { + Text(context.symphony.t.PrimaryColor) + }, + value = ThemeColors.resolvePrimaryColorKey(primaryColor), + values = PrimaryThemeColors.entries.associateWith { it.label(context) }, + enabled = !useMaterialYou, + onChange = { value -> + context.symphony.settings.primaryColor.setValue(value.name) + } + ) + } + } + } + ) +} + +fun PrimaryThemeColors.label(context: ViewContext) = when (this) { + PrimaryThemeColors.Red -> context.symphony.t.Red + PrimaryThemeColors.Orange -> context.symphony.t.Orange + PrimaryThemeColors.Amber -> context.symphony.t.Amber + PrimaryThemeColors.Yellow -> context.symphony.t.Yellow + PrimaryThemeColors.Lime -> context.symphony.t.Lime + PrimaryThemeColors.Green -> context.symphony.t.Green + PrimaryThemeColors.Emerald -> context.symphony.t.Emerald + PrimaryThemeColors.Teal -> context.symphony.t.Teal + PrimaryThemeColors.Cyan -> context.symphony.t.Cyan + PrimaryThemeColors.Sky -> context.symphony.t.Sky + PrimaryThemeColors.Blue -> context.symphony.t.Blue + PrimaryThemeColors.Indigo -> context.symphony.t.Indigo + PrimaryThemeColors.Violet -> context.symphony.t.Violet + PrimaryThemeColors.Purple -> context.symphony.t.Purple + PrimaryThemeColors.Fuchsia -> context.symphony.t.Fuchsia + PrimaryThemeColors.Pink -> context.symphony.t.Pink + PrimaryThemeColors.Rose -> context.symphony.t.Rose +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt new file mode 100644 index 00000000..5f346ba7 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/GrooveSettingsView.kt @@ -0,0 +1,338 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.repeatable +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.relocation.BringIntoViewRequester +import androidx.compose.foundation.relocation.bringIntoViewRequester +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.FilterAlt +import androidx.compose.material.icons.filled.LibraryMusic +import androidx.compose.material.icons.filled.RuleFolder +import androidx.compose.material.icons.filled.SpaceBar +import androidx.compose.material.icons.filled.Storage +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawWithContent +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.Symphony +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiGrooveFolderTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiSystemFolderTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiTextOptionTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.components.settings.SettingsSimpleTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsTextInputTile +import io.github.zyrouge.symphony.ui.helpers.TransitionDurations +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.view.SettingsViewElements +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import kotlinx.serialization.Serializable + +@Serializable +data class GrooveSettingsViewRoute(val initialElement: SettingsViewElements?) + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun GrooveSettingsView(context: ViewContext, route: GrooveSettingsViewRoute) { + val coroutineScope = rememberCoroutineScope() + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val songsFilterPattern by context.symphony.settings.songsFilterPattern.flow.collectAsState() + val blacklistFolders by context.symphony.settings.blacklistFolders.flow.collectAsState() + val whitelistFolders by context.symphony.settings.whitelistFolders.flow.collectAsState() + val artistTagSeparators by context.symphony.settings.artistTagSeparators.flow.collectAsState() + val genreTagSeparators by context.symphony.settings.genreTagSeparators.flow.collectAsState() + val mediaFolders by context.symphony.settings.mediaFolders.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + val defaultSongsFilterPattern = ".*" + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.Groove) + SpotlightTile(route.initialElement == SettingsViewElements.MediaFolders) { + SettingsMultiSystemFolderTile( + context, + icon = { + Icon(Icons.Filled.LibraryMusic, null) + }, + title = { + Text(context.symphony.t.MediaFolders) + }, + initialValues = mediaFolders, + onChange = { values -> + context.symphony.settings.mediaFolders.setValue(values) + refetchMediaLibrary(coroutineScope, context.symphony) + } + ) + } + SettingsTextInputTile( + context, + icon = { + Icon(Icons.Filled.FilterAlt, null) + }, + title = { + Text(context.symphony.t.SongsFilterPattern) + }, + value = songsFilterPattern ?: defaultSongsFilterPattern, + onReset = { + context.symphony.settings.songsFilterPattern.setValue(null) + }, + onChange = { value -> + context.symphony.settings.songsFilterPattern.setValue( + when (value) { + defaultSongsFilterPattern -> null + else -> value + } + ) + refetchMediaLibrary(coroutineScope, context.symphony) + } + ) + SettingsMultiGrooveFolderTile( + context, + icon = { + Icon(Icons.Filled.RuleFolder, null) + }, + title = { + Text(context.symphony.t.BlacklistFolders) + }, + explorer = context.symphony.groove.exposer.explorer, + initialValues = blacklistFolders, + onChange = { values -> + context.symphony.settings.blacklistFolders.setValue(values) + refetchMediaLibrary(coroutineScope, context.symphony) + } + ) + SettingsMultiGrooveFolderTile( + context, + icon = { + Icon(Icons.Filled.RuleFolder, null) + }, + title = { + Text(context.symphony.t.WhitelistFolders) + }, + explorer = context.symphony.groove.exposer.explorer, + initialValues = whitelistFolders, + onChange = { values -> + context.symphony.settings.whitelistFolders.setValue(values) + refetchMediaLibrary(coroutineScope, context.symphony) + } + ) + SettingsMultiTextOptionTile( + context, + icon = { + Icon(Icons.Filled.SpaceBar, null) + }, + title = { + Text(context.symphony.t.ArtistTagValueSeparators) + }, + values = artistTagSeparators.toList(), + onChange = { + context.symphony.settings.artistTagSeparators.setValue(it.toSet()) + refetchMediaLibrary(coroutineScope, context.symphony) + }, + ) + SettingsMultiTextOptionTile( + context, + icon = { + Icon(Icons.Filled.SpaceBar, null) + }, + title = { + Text(context.symphony.t.GenreTagValueSeparators) + }, + values = genreTagSeparators.toList(), + onChange = { + context.symphony.settings.genreTagSeparators.setValue(it.toSet()) + refetchMediaLibrary(coroutineScope, context.symphony) + }, + ) + SettingsSimpleTile( + icon = { + Icon(Icons.Filled.Storage, null) + }, + title = { + Text(context.symphony.t.ClearSongCache) + }, + onClick = { + coroutineScope.launch { + context.symphony.database.songCache.clear() + context.symphony.database.artworkCache.clear() + context.symphony.database.lyricsCache.clear() + refetchMediaLibrary(coroutineScope, context.symphony) + snackbarHostState.showSnackbar( + context.symphony.t.SongCacheCleared, + withDismissAction = true, + ) + } + } + ) + } + } + } + ) +} + +private fun refetchMediaLibrary(coroutineScope: CoroutineScope, symphony: Symphony) { + symphony.radio.stop() + coroutineScope.launch { + symphony.groove.refetch() + } +} + +@OptIn(ExperimentalFoundationApi::class) +@Composable +private fun SpotlightTile(isInSpotlight: Boolean, content: @Composable (() -> Unit)) { + val bringIntoViewRequester = remember { BringIntoViewRequester() } + var animateSpotlightEffect by remember { mutableStateOf(false) } + val highlightAlphaAnimated by animateFloatAsState( + targetValue = if (animateSpotlightEffect) 0.25f else 0f, + label = "spotlight-outline-alpha", + animationSpec = repeatable(2, TransitionDurations.Slow.asTween()), + ) + val highlightColor = MaterialTheme.colorScheme.surfaceTint + + LaunchedEffect(isInSpotlight) { + if (isInSpotlight) { + animateSpotlightEffect = true + bringIntoViewRequester.bringIntoView() + animateSpotlightEffect = false + } + } + + Box( + modifier = Modifier + .bringIntoViewRequester(bringIntoViewRequester) + .drawWithContent { + drawContent() + drawRect(color = highlightColor, alpha = highlightAlphaAnimated) + } + ) { + content() + } +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt new file mode 100644 index 00000000..5958684c --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/HomePageSettingsView.kt @@ -0,0 +1,215 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Label +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Recommend +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsMultiOptionTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsOptionTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.view.HomePageBottomBarLabelVisibility +import io.github.zyrouge.symphony.ui.view.HomePages +import io.github.zyrouge.symphony.ui.view.home.ForYou +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.serialization.Serializable + +@Serializable +object HomePageSettingsViewRoute + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomePageSettingsView(context: ViewContext) { + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val homeTabs by context.symphony.settings.homeTabs.flow.collectAsState() + val forYouContents by context.symphony.settings.forYouContents.flow.collectAsState() + val homePageBottomBarLabelVisibility by context.symphony.settings.homePageBottomBarLabelVisibility.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.Interface) + SettingsMultiOptionTile( + context, + icon = { + Icon(Icons.Filled.Home, null) + }, + title = { + Text(context.symphony.t.HomeTabs) + }, + note = { + Text(context.symphony.t.SelectAtleast2orAtmost5Tabs) + }, + value = homeTabs, + values = HomePages.entries.associateWith { it.label(context) }, + satisfies = { it.size in 2..5 }, + onChange = { value -> + context.symphony.settings.homeTabs.setValue(value) + } + ) + SettingsMultiOptionTile( + context, + icon = { + Icon(Icons.Filled.Recommend, null) + }, + title = { + Text(context.symphony.t.ForYou) + }, + value = forYouContents, + values = ForYou.entries.associateWith { it.label(context) }, + onChange = { value -> + context.symphony.settings.forYouContents.setValue(value) + } + ) + SettingsOptionTile( + icon = { + Icon(Icons.AutoMirrored.Filled.Label, null) + }, + title = { + Text(context.symphony.t.BottomBarLabelVisibility) + }, + value = homePageBottomBarLabelVisibility, + values = HomePageBottomBarLabelVisibility.entries + .associateWith { it.label(context) }, + onChange = { value -> + context.symphony.settings.homePageBottomBarLabelVisibility.setValue( + value, + ) + } + ) + } + } + } + ) +} + +fun HomePageBottomBarLabelVisibility.label(context: ViewContext) = when (this) { + HomePageBottomBarLabelVisibility.ALWAYS_VISIBLE -> context.symphony.t.AlwaysVisible + HomePageBottomBarLabelVisibility.VISIBLE_WHEN_ACTIVE -> context.symphony.t.VisibleWhenActive + HomePageBottomBarLabelVisibility.INVISIBLE -> context.symphony.t.Invisible +} \ No newline at end of file diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt new file mode 100644 index 00000000..a844ec41 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/MiniPlayerSettingsView.kt @@ -0,0 +1,193 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Forward30 +import androidx.compose.material.icons.filled.KeyboardDoubleArrowRight +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.components.settings.SettingsSwitchTile +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.serialization.Serializable + +@Serializable +object MiniPlayerSettingsViewRoute + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MiniPlayerSettingsView(context: ViewContext) { + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val miniPlayerTrackControls by context.symphony.settings.miniPlayerTrackControls.flow.collectAsState() + val miniPlayerSeekControls by context.symphony.settings.miniPlayerSeekControls.flow.collectAsState() + val miniPlayerTextMarquee by context.symphony.settings.miniPlayerTextMarquee.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.MiniPlayer) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.SkipNext, null) + }, + title = { + Text(context.symphony.t.ShowTrackControls) + }, + value = miniPlayerTrackControls, + onChange = { value -> + context.symphony.settings.miniPlayerTrackControls.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.Forward30, null) + }, + title = { + Text(context.symphony.t.ShowSeekControls) + }, + value = miniPlayerSeekControls, + onChange = { value -> + context.symphony.settings.miniPlayerSeekControls.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.KeyboardDoubleArrowRight, null) + }, + title = { + Text(context.symphony.t.MiniPlayerTextMarquee) + }, + value = miniPlayerTextMarquee, + onChange = { value -> + context.symphony.settings.miniPlayerTextMarquee.setValue(value) + } + ) + } + } + } + ) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt new file mode 100644 index 00000000..14a9d001 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/NowPlayingSettingsView.kt @@ -0,0 +1,224 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Wysiwyg +import androidx.compose.material.icons.automirrored.outlined.Article +import androidx.compose.material.icons.filled.Dashboard +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Forward30 +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsOptionTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.components.settings.SettingsSwitchTile +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.ui.view.NowPlayingControlsLayout +import io.github.zyrouge.symphony.ui.view.NowPlayingLyricsLayout +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.serialization.Serializable + +@Serializable +object NowPlayingSettingsViewRoute + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun NowPlayingSettingsView(context: ViewContext) { + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val nowPlayingControlsLayout by context.symphony.settings.nowPlayingControlsLayout.flow.collectAsState() + val nowPlayingAdditionalInfo by context.symphony.settings.nowPlayingAdditionalInfo.flow.collectAsState() + val nowPlayingSeekControls by context.symphony.settings.nowPlayingSeekControls.flow.collectAsState() + val nowPlayingLyricsLayout by context.symphony.settings.nowPlayingLyricsLayout.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.NowPlaying) + SettingsOptionTile( + icon = { + Icon(Icons.Filled.Dashboard, null) + }, + title = { + Text(context.symphony.t.ControlsLayout) + }, + value = nowPlayingControlsLayout, + values = NowPlayingControlsLayout.entries + .associateWith { it.label(context) }, + onChange = { value -> + context.symphony.settings.nowPlayingControlsLayout.setValue(value) + } + ) + SettingsOptionTile( + icon = { + Icon(Icons.AutoMirrored.Outlined.Article, null) + }, + title = { + Text(context.symphony.t.LyricsLayout) + }, + value = nowPlayingLyricsLayout, + values = NowPlayingLyricsLayout.entries + .associateWith { it.label(context) }, + onChange = { value -> + context.symphony.settings.nowPlayingLyricsLayout.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.AutoMirrored.Filled.Wysiwyg, null) + }, + title = { + Text(context.symphony.t.ShowAudioInformation) + }, + value = nowPlayingAdditionalInfo, + onChange = { value -> + context.symphony.settings.nowPlayingAdditionalInfo.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.Forward30, null) + }, + title = { + Text(context.symphony.t.ShowSeekControls) + }, + value = nowPlayingSeekControls, + onChange = { value -> + context.symphony.settings.nowPlayingSeekControls.setValue(value) + } + ) + } + } + } + ) +} + +fun NowPlayingControlsLayout.label(context: ViewContext) = when (this) { + NowPlayingControlsLayout.Default -> context.symphony.t.Default + NowPlayingControlsLayout.Traditional -> context.symphony.t.Traditional +} + +fun NowPlayingLyricsLayout.label(context: ViewContext) = when (this) { + NowPlayingLyricsLayout.ReplaceArtwork -> context.symphony.t.ReplaceArtwork + NowPlayingLyricsLayout.SeparatePage -> context.symphony.t.SeparatePage +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt new file mode 100644 index 00000000..75ebcbe5 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/PlayerSettingsView.kt @@ -0,0 +1,303 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.CenterFocusWeak +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.FastForward +import androidx.compose.material.icons.filled.FastRewind +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.GraphicEq +import androidx.compose.material.icons.filled.Headset +import androidx.compose.material.icons.filled.HeadsetOff +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.components.settings.SettingsSliderTile +import io.github.zyrouge.symphony.ui.components.settings.SettingsSwitchTile +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.serialization.Serializable +import kotlin.math.roundToInt + +@Serializable +object PlayerSettingsViewRoute + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PlayerSettingsView(context: ViewContext) { + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val fadePlayback by context.symphony.settings.fadePlayback.flow.collectAsState() + val fadePlaybackDuration by context.symphony.settings.fadePlaybackDuration.flow.collectAsState() + val requireAudioFocus by context.symphony.settings.requireAudioFocus.flow.collectAsState() + val ignoreAudioFocusLoss by context.symphony.settings.ignoreAudioFocusLoss.flow.collectAsState() + val playOnHeadphonesConnect by context.symphony.settings.playOnHeadphonesConnect.flow.collectAsState() + val pauseOnHeadphonesDisconnect by context.symphony.settings.pauseOnHeadphonesDisconnect.flow.collectAsState() + val seekBackDuration by context.symphony.settings.seekBackDuration.flow.collectAsState() + val seekForwardDuration by context.symphony.settings.seekForwardDuration.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + val seekDurationRange = 3f..60f + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.Player) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.GraphicEq, null) + }, + title = { + Text(context.symphony.t.FadePlaybackInOut) + }, + value = fadePlayback, + onChange = { value -> + context.symphony.settings.fadePlayback.setValue(value) + } + ) + SettingsSliderTile( + context, + icon = { + Icon(Icons.Filled.GraphicEq, null) + }, + title = { + Text(context.symphony.t.FadePlaybackInOut) + }, + label = { value -> + Text(context.symphony.t.XSecs(value.toString())) + }, + range = 0.5f..6f, + initialValue = fadePlaybackDuration, + onValue = { value -> + value.times(2).roundToInt().toFloat().div(2) + }, + onChange = { value -> + context.symphony.settings.fadePlaybackDuration.setValue(value) + }, + onReset = { + context.symphony.settings.fadePlaybackDuration.setValue( + context.symphony.settings.fadePlaybackDuration.defaultValue, + ) + }, + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.CenterFocusWeak, null) + }, + title = { + Text(context.symphony.t.RequireAudioFocus) + }, + value = requireAudioFocus, + onChange = { value -> + context.symphony.settings.requireAudioFocus.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.CenterFocusWeak, null) + }, + title = { + Text(context.symphony.t.IgnoreAudioFocusLoss) + }, + value = ignoreAudioFocusLoss, + onChange = { value -> + context.symphony.settings.ignoreAudioFocusLoss.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.Headset, null) + }, + title = { + Text(context.symphony.t.PlayOnHeadphonesConnect) + }, + value = playOnHeadphonesConnect, + onChange = { value -> + context.symphony.settings.playOnHeadphonesConnect.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.HeadsetOff, null) + }, + title = { + Text(context.symphony.t.PauseOnHeadphonesDisconnect) + }, + value = pauseOnHeadphonesDisconnect, + onChange = { value -> + context.symphony.settings.pauseOnHeadphonesDisconnect.setValue(value) + } + ) + SettingsSliderTile( + context, + icon = { + Icon(Icons.Filled.FastRewind, null) + }, + title = { + Text(context.symphony.t.FastRewindDuration) + }, + label = { value -> + Text(context.symphony.t.XSecs(value.toString())) + }, + range = seekDurationRange, + initialValue = seekBackDuration.toFloat(), + onValue = { value -> + value.roundToInt().toFloat() + }, + onChange = { value -> + context.symphony.settings.seekBackDuration.setValue(value.toInt()) + }, + onReset = { + context.symphony.settings.seekBackDuration.setValue( + context.symphony.settings.seekBackDuration.defaultValue, + ) + }, + ) + SettingsSliderTile( + context, + icon = { + Icon(Icons.Filled.FastForward, null) + }, + title = { + Text(context.symphony.t.FastForwardDuration) + }, + label = { value -> + Text(context.symphony.t.XSecs(value.toString())) + }, + range = seekDurationRange, + initialValue = seekForwardDuration.toFloat(), + onValue = { value -> + value.roundToInt().toFloat() + }, + onChange = { value -> + context.symphony.settings.seekForwardDuration.setValue(value.toInt()) + }, + onReset = { + context.symphony.settings.seekForwardDuration.setValue( + context.symphony.settings.seekForwardDuration.defaultValue, + ) + }, + ) + } + } + } + ) +} diff --git a/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt new file mode 100644 index 00000000..5cd39008 --- /dev/null +++ b/app/src/main/java/io/github/zyrouge/symphony/ui/view/settings/UpdateSettingsView.kt @@ -0,0 +1,178 @@ +package io.github.zyrouge.symphony.ui.view.settings + +import android.net.Uri +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.East +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.Update +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +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.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.zyrouge.symphony.services.AppMeta +import io.github.zyrouge.symphony.ui.components.AdaptiveSnackbar +import io.github.zyrouge.symphony.ui.components.IconButtonPlaceholder +import io.github.zyrouge.symphony.ui.components.TopAppBarMinimalTitle +import io.github.zyrouge.symphony.ui.components.settings.SettingsSideHeading +import io.github.zyrouge.symphony.ui.components.settings.SettingsSwitchTile +import io.github.zyrouge.symphony.ui.helpers.ViewContext +import io.github.zyrouge.symphony.utils.ActivityUtils +import kotlinx.serialization.Serializable + +@Serializable +object UpdateSettingsViewRoute + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun UpdateSettingsView(context: ViewContext) { + val snackbarHostState = remember { SnackbarHostState() } + val scrollState = rememberScrollState() + + val checkForUpdates by context.symphony.settings.checkForUpdates.flow.collectAsState() + val showUpdateToast by context.symphony.settings.showUpdateToast.flow.collectAsState() + + Scaffold( + modifier = Modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(snackbarHostState) { + AdaptiveSnackbar(it) + } + }, + topBar = { + CenterAlignedTopAppBar( + title = { + TopAppBarMinimalTitle { + Text(context.symphony.t.Settings) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent + ), + navigationIcon = { + IconButton( + onClick = { + context.navController.popBackStack() + } + ) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, null) + } + }, + actions = { + IconButtonPlaceholder() + }, + ) + }, + content = { contentPadding -> + Box( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + ) { + Column(modifier = Modifier.verticalScroll(scrollState)) { + val contentColor = MaterialTheme.colorScheme.onPrimary + + Box( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.primary) + .clickable { + ActivityUtils.startBrowserActivity( + context.activity, + Uri.parse(AppMeta.contributingUrl) + ) + } + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp, 8.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.Favorite, + null, + tint = contentColor, + modifier = Modifier.size(12.dp), + ) + Box(modifier = Modifier.width(4.dp)) + Text( + context.symphony.t.ConsiderContributing, + style = MaterialTheme.typography.labelLarge.copy( + fontWeight = FontWeight.Bold, + color = contentColor, + ), + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(8.dp, 0.dp) + ) { + Icon( + Icons.Filled.East, + null, + tint = contentColor, + modifier = Modifier.size(20.dp), + ) + } + } + SettingsSideHeading(context.symphony.t.Updates) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.Update, null) + }, + title = { + Text(context.symphony.t.CheckForUpdates) + }, + value = checkForUpdates, + onChange = { value -> + context.symphony.settings.checkForUpdates.setValue(value) + } + ) + SettingsSwitchTile( + icon = { + Icon(Icons.Filled.Update, null) + }, + title = { + Text(context.symphony.t.ShowUpdateToast) + }, + value = showUpdateToast, + onChange = { value -> + context.symphony.settings.showUpdateToast.setValue(value) + } + ) + } + } + } + ) +} diff --git a/i18n/en.toml b/i18n/en.toml index 977d68e7..e16b7492 100644 --- a/i18n/en.toml +++ b/i18n/en.toml @@ -241,3 +241,5 @@ MinimumBitrate = "Minimum bitrate" MaximumBitrate = "Maximum bitrate" Date = "Date" TotalSamples = "Total samples" +HomePage = "Home page" +NowPlayingPage = "Now playing page"