From a600c8b457a3bd408f8ebffb288b272af608354f Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Wed, 28 Sep 2022 12:43:57 +0200 Subject: [PATCH] Rework url management (#172) --- .../it/vfsfitvnm/vimusic/MainActivity.kt | 91 ++++-- .../ui/components/themed/MediaItemMenu.kt | 13 +- .../vimusic/ui/screens/IntentUriScreen.kt | 274 ------------------ .../it/vfsfitvnm/vimusic/ui/screens/Routes.kt | 9 +- .../vimusic/ui/screens/home/HomeScreen.kt | 19 +- .../ui/screens/player/PlayerBottomSheet.kt | 4 +- .../vimusic/ui/screens/player/PlayerView.kt | 9 +- .../ui/screens/playlist/PlaylistScreen.kt | 6 +- .../ui/screens/playlist/PlaylistSongList.kt | 2 - .../ui/screens/search/LocalSongSearch.kt | 4 - .../vimusic/ui/screens/search/OnlineSearch.kt | 22 +- .../vimusic/ui/screens/search/SearchScreen.kt | 28 +- .../it/vfsfitvnm/vimusic/utils/Utils.kt | 4 + .../kotlin/it/vfsfitvnm/route/GlobalRoute.kt | 14 + .../main/kotlin/it/vfsfitvnm/route/Route.kt | 27 +- .../kotlin/it/vfsfitvnm/route/RouteHandler.kt | 16 +- 16 files changed, 142 insertions(+), 400 deletions(-) delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt create mode 100644 compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index 67c712b..e7dd936 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -6,10 +6,10 @@ import android.content.Intent import android.content.ServiceConnection import android.content.SharedPreferences import android.graphics.Bitmap -import android.net.Uri import android.os.Build import android.os.Bundle import android.os.IBinder +import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.animation.ExperimentalAnimationApi @@ -37,7 +37,6 @@ import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.neverEqualPolicy import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue @@ -49,6 +48,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.core.view.WindowCompat +import androidx.lifecycle.lifecycleScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import com.valentinilk.shimmer.LocalShimmerTheme @@ -63,22 +63,26 @@ import it.vfsfitvnm.vimusic.ui.components.collapsedAnchor import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState -import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen +import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView +import it.vfsfitvnm.vimusic.ui.screens.playlistRoute import it.vfsfitvnm.vimusic.ui.styling.Appearance import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.typographyOf +import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey +import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.getEnum import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.listener import it.vfsfitvnm.vimusic.utils.preferences import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey +import it.vfsfitvnm.youtubemusic.YouTube import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch @@ -102,7 +106,6 @@ class MainActivity : ComponentActivity() { } private var binder by mutableStateOf(null) - private var uri by mutableStateOf(null, neverEqualPolicy()) override fun onStart() { super.onStart() @@ -120,14 +123,13 @@ class MainActivity : ComponentActivity() { WindowCompat.setDecorFitsSystemWindows(window, false) + val playerBottomSheetAnchor = when { intent?.extras?.getBoolean("expandPlayerBottomSheet") == true -> expandedAnchor alreadyRunning -> collapsedAnchor else -> dismissedAnchor.also { alreadyRunning = true } } - uri = intent?.data - setContent { val coroutineScope = rememberCoroutineScope() val isSystemInDarkTheme = isSystemInDarkTheme() @@ -324,30 +326,29 @@ class MainActivity : ComponentActivity() { LocalPlayerServiceBinder provides binder, LocalPlayerAwarePaddingValues provides playerAwarePaddingValues ) { - when (val uri = uri) { - null -> { - HomeScreen() - - PlayerView( - layoutState = playerBottomSheetState, - modifier = Modifier - .align(Alignment.BottomCenter) - ) - - DisposableEffect(binder?.player) { - binder?.player?.listener(object : Player.Listener { - override fun onMediaItemTransition( - mediaItem: MediaItem?, - reason: Int - ) { - if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) { - playerBottomSheetState.expand(tween(500)) - } - } - }) ?: onDispose { } - } + HomeScreen( + onPlaylistUrl = { url -> + onNewIntent(Intent.parseUri(url, 0)) } - else -> IntentUriScreen(uri = uri) + ) + + PlayerView( + layoutState = playerBottomSheetState, + modifier = Modifier + .align(Alignment.BottomCenter) + ) + + DisposableEffect(binder?.player) { + binder?.player?.listener(object : Player.Listener { + override fun onMediaItemTransition( + mediaItem: MediaItem?, + reason: Int + ) { + if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_PLAYLIST_CHANGED && mediaItem != null) { + playerBottomSheetState.expand(tween(500)) + } + } + }) ?: onDispose { } } BottomSheetMenu( @@ -358,11 +359,41 @@ class MainActivity : ComponentActivity() { } } } + + onNewIntent(intent) } override fun onNewIntent(intent: Intent?) { super.onNewIntent(intent) - uri = intent?.data + + val uri = intent?.data ?: return + + intent.data = null + this.intent = null + + Toast.makeText(this, "Opening url...", Toast.LENGTH_SHORT).show() + + lifecycleScope.launch(Dispatchers.IO) { + uri.getQueryParameter("list")?.let { playlistId -> + val browseId = "VL$playlistId" + + if (playlistId.startsWith("OLAK5uy_")) { + YouTube.playlist(browseId)?.getOrNull()?.let { playlist -> + playlist.songs?.firstOrNull()?.album?.endpoint?.browseId?.let { browseId -> + albumRoute.ensureGlobal(browseId) + } + } + } else { + playlistRoute.ensureGlobal(browseId) + } + } ?: (uri.getQueryParameter("v") ?: uri.takeIf { uri.host == "youtu.be" }?.path?.drop(1))?.let { videoId -> + YouTube.song(videoId)?.getOrNull()?.let { song -> + withContext(Dispatchers.Main) { + binder?.player?.forcePlay(song.asMediaItem) + } + } + } + } } private fun setSystemBarAppearance(isDark: Boolean) { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt index b10c0ce..6b9e7cf 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/MediaItemMenu.kt @@ -181,8 +181,7 @@ fun QueuedMediaItemMenu( mediaItem: MediaItem, indexInQueue: Int?, modifier: Modifier = Modifier, - onDismiss: (() -> Unit)? = null, - onGlobalRouteEmitted: (() -> Unit)? = null + onDismiss: (() -> Unit)? = null ) { val menuState = LocalMenuState.current val binder = LocalPlayerServiceBinder.current @@ -193,7 +192,6 @@ fun QueuedMediaItemMenu( onRemoveFromQueue = if (indexInQueue != null) ({ binder?.player?.removeMediaItem(indexInQueue) }) else null, - onGlobalRouteEmitted = onGlobalRouteEmitted, modifier = modifier ) } @@ -212,8 +210,7 @@ fun BaseMediaItemMenu( onRemoveFromQueue: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null, - onRemoveFromFavorites: (() -> Unit)? = null, - onGlobalRouteEmitted: (() -> Unit)? = null, + onRemoveFromFavorites: (() -> Unit)? = null ) { val context = LocalContext.current @@ -246,7 +243,6 @@ fun BaseMediaItemMenu( onShare = { context.shareAsYouTubeSong(mediaItem) }, - onGlobalRouteEmitted = onGlobalRouteEmitted, modifier = modifier ) } @@ -269,8 +265,7 @@ fun MediaItemMenu( onAddToPlaylist: ((Playlist, Int) -> Unit)? = null, onGoToAlbum: ((String) -> Unit)? = null, onGoToArtist: ((String) -> Unit)? = null, - onShare: (() -> Unit)? = null, - onGlobalRouteEmitted: (() -> Unit)? = null, + onShare: (() -> Unit)? = null ) { Menu(modifier = modifier) { RouteHandler( @@ -566,7 +561,6 @@ fun MediaItemMenu( text = "Go to album", onClick = { onDismiss() - onGlobalRouteEmitted?.invoke() onGoToAlbum(albumId) } ) @@ -586,7 +580,6 @@ fun MediaItemMenu( text = "More of $authorName", onClick = { onDismiss() - onGlobalRouteEmitted?.invoke() onGoToArtist(authorId) } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt deleted file mode 100644 index 27ba93f..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ /dev/null @@ -1,274 +0,0 @@ -package it.vfsfitvnm.vimusic.ui.screens - -import android.net.Uri -import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp -import it.vfsfitvnm.route.RouteHandler -import it.vfsfitvnm.vimusic.Database -import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues -import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder -import it.vfsfitvnm.vimusic.R -import it.vfsfitvnm.vimusic.models.Playlist -import it.vfsfitvnm.vimusic.models.SongPlaylistMap -import it.vfsfitvnm.vimusic.transaction -import it.vfsfitvnm.vimusic.ui.components.LocalMenuState -import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError -import it.vfsfitvnm.vimusic.ui.components.themed.Menu -import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry -import it.vfsfitvnm.vimusic.ui.components.themed.TextCard -import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog -import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen -import it.vfsfitvnm.vimusic.ui.styling.Dimensions -import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance -import it.vfsfitvnm.vimusic.ui.styling.px -import it.vfsfitvnm.vimusic.ui.views.SmallSongItem -import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer -import it.vfsfitvnm.vimusic.utils.asMediaItem -import it.vfsfitvnm.vimusic.utils.enqueue -import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex -import it.vfsfitvnm.vimusic.utils.relaunchableEffect -import it.vfsfitvnm.youtubemusic.YouTube -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -@ExperimentalFoundationApi -@ExperimentalAnimationApi -@Composable -fun IntentUriScreen(uri: Uri) { - - val lazyListState = rememberLazyListState() - - var itemsResult by remember(uri) { - mutableStateOf>?>(null) - } - - var playlistBrowseId by rememberSaveable { - mutableStateOf(null) - } - - val onLoad = relaunchableEffect(uri) { - withContext(Dispatchers.IO) { - itemsResult = uri.getQueryParameter("list")?.let { playlistId -> - if (playlistId.startsWith("OLAK5uy_")) { - YouTube.queue(playlistId)?.map { songList -> - songList ?: emptyList() - } - } else { - playlistBrowseId = "VL$playlistId" - null - } - } ?: uri.getQueryParameter("v")?.let { videoId -> - YouTube.song(videoId)?.map { song -> - song?.let { listOf(song) } ?: emptyList() - } - } ?: uri.takeIf { - uri.host == "youtu.be" - }?.path?.drop(1)?.let { videoId -> - YouTube.song(videoId)?.map { song -> - song?.let { listOf(song) } ?: emptyList() - } - } ?: Result.failure(Error("Missing URL parameters")) - } - } - - playlistBrowseId?.let { browseId -> - PlaylistScreen(browseId = browseId) - return - } - - RouteHandler(listenToGlobalEmitter = true) { - globalRoutes() - - host { - val menuState = LocalMenuState.current - val (colorPalette) = LocalAppearance.current - val binder = LocalPlayerServiceBinder.current - - val thumbnailSizePx = Dimensions.thumbnails.song.px - - var isImportingAsPlaylist by remember(uri) { - mutableStateOf(false) - } - - - if (isImportingAsPlaylist) { - TextFieldDialog( - hintText = "Enter the playlist name", - onDismiss = { - isImportingAsPlaylist = false - }, - onDone = { text -> - menuState.hide() - - transaction { - val playlistId = Database.insert(Playlist(name = text)) - - itemsResult - ?.getOrNull() - ?.map(YouTube.Item.Song::asMediaItem) - ?.forEachIndexed { index, mediaItem -> - Database.insert(mediaItem) - - Database.insert( - SongPlaylistMap( - songId = mediaItem.mediaId, - playlistId = playlistId, - position = index - ) - ) - } - } - } - ) - } - - LazyColumn( - state = lazyListState, - horizontalAlignment = Alignment.CenterHorizontally, - contentPadding = LocalPlayerAwarePaddingValues.current, - modifier = Modifier - .background(colorPalette.background0) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.chevron_back), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable(onClick = pop) - .padding(vertical = 8.dp) - .padding(horizontal = 16.dp) - .size(24.dp) - ) - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - Menu { - MenuEntry( - icon = R.drawable.enqueue, - text = "Enqueue", - onClick = { - menuState.hide() - - itemsResult - ?.getOrNull() - ?.map(YouTube.Item.Song::asMediaItem) - ?.let { mediaItems -> - binder?.player?.enqueue( - mediaItems - ) - } - } - ) - - MenuEntry( - icon = R.drawable.playlist, - text = "Import as playlist", - onClick = { - isImportingAsPlaylist = true - } - ) - } - } - } - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - } - - itemsResult?.getOrNull()?.let { items -> - if (items.isEmpty()) { - item { - TextCard(icon = R.drawable.sad) { - Title(text = "No songs found") - Text(text = "Please try a different query or category.") - } - } - } else { - itemsIndexed( - items = items, - contentType = { _, item -> item } - ) { index, item -> - SmallSongItem( - song = item, - thumbnailSizePx = thumbnailSizePx, - onClick = { - binder?.stopRadio() - binder?.player?.forcePlayAtIndex( - items.map(YouTube.Item.Song::asMediaItem), - index - ) - } - ) - } - } - } ?: itemsResult?.exceptionOrNull()?.let { throwable -> - item { - LoadingOrError( - errorMessage = throwable.javaClass.canonicalName, - onRetry = onLoad - ) - } - } ?: item { - LoadingOrError() - } - } - } - } -} - -@Composable -private fun LoadingOrError( - errorMessage: String? = null, - onRetry: (() -> Unit)? = null -) { - LoadingOrError( - errorMessage = errorMessage, - onRetry = onRetry - ) { - repeat(5) { index -> - SmallSongItemShimmer( - thumbnailSizeDp = Dimensions.thumbnails.song, - modifier = Modifier - .alpha(1f - index * 0.175f) - .fillMaxWidth() - .padding(vertical = 4.dp, horizontal = 16.dp) - ) - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt index 8f8e16f..264f28b 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/Routes.kt @@ -1,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens import android.annotation.SuppressLint -import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.runtime.Composable import it.vfsfitvnm.route.Route0 @@ -10,11 +9,11 @@ import it.vfsfitvnm.route.RouteHandlerScope import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen +import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen val albumRoute = Route1("albumRoute") val artistRoute = Route1("artistRoute") val builtInPlaylistRoute = Route1("builtInPlaylistRoute") -val intentUriRoute = Route1("intentUriRoute") val localPlaylistRoute = Route1("localPlaylistRoute") val playlistRoute = Route1("playlistRoute") val searchResultRoute = Route1("searchResultRoute") @@ -38,4 +37,10 @@ inline fun RouteHandlerScope.globalRoutes() { browseId = browseId ?: error("browseId cannot be null") ) } + + playlistRoute { browseId -> + PlaylistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt index 5dacb65..cbb720f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeScreen.kt @@ -1,6 +1,5 @@ package it.vfsfitvnm.vimusic.ui.screens.home -import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable @@ -11,13 +10,11 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold -import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -import it.vfsfitvnm.vimusic.ui.screens.intentUriRoute import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen @@ -32,10 +29,12 @@ import it.vfsfitvnm.vimusic.utils.rememberPreference @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun HomeScreen() { +fun HomeScreen(onPlaylistUrl: (String) -> Unit) { val saveableStateHolder = rememberSaveableStateHolder() RouteHandler(listenToGlobalEmitter = true) { + globalRoutes() + settingsRoute { SettingsScreen() } @@ -71,17 +70,7 @@ fun HomeScreen() { Database.insert(SearchQuery(query = query)) } }, - onUri = { uri -> - intentUriRoute(uri) - } - ) - } - - globalRoutes() - - intentUriRoute { uri -> - IntentUriScreen( - uri = uri ?: Uri.EMPTY + onViewPlaylist = onPlaylistUrl ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index c9c9150..ba63067 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -72,7 +72,6 @@ import kotlinx.coroutines.launch fun PlayerBottomSheet( backgroundColorProvider: () -> Color, layoutState: BottomSheetState, - onGlobalRouteEmitted: () -> Unit, modifier: Modifier = Modifier, content: @Composable BoxScope.() -> Unit, ) { @@ -166,8 +165,7 @@ fun PlayerBottomSheet( menuContent = { QueuedMediaItemMenu( mediaItem = window.mediaItem, - indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex, - onGlobalRouteEmitted = onGlobalRouteEmitted + indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex ) }, onThumbnailContent = { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt index 7c73bff..83dc4d3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerView.kt @@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.media3.common.Player import coil.compose.AsyncImage +import it.vfsfitvnm.route.OnGlobalRoute import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.BottomSheet @@ -95,6 +96,10 @@ fun PlayerView( val shouldBePlaying by rememberShouldBePlaying(binder.player) val positionAndDuration by rememberPositionAndDuration(binder.player) + OnGlobalRoute { + layoutState.collapseSoft() + } + BottomSheet( state = layoutState, modifier = modifier, @@ -321,7 +326,6 @@ fun PlayerView( PlayerBottomSheet( layoutState = playerBottomSheetState, - onGlobalRouteEmitted = layoutState::collapseSoft, content = { Row( verticalAlignment = Alignment.CenterVertically, @@ -385,8 +389,7 @@ fun PlayerView( } }, onSetSleepTimer = {}, - onDismiss = menuState::hide, - onGlobalRouteEmitted = layoutState::collapseSoft, + onDismiss = menuState::hide ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt index 0e53acb..4f8720d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistScreen.kt @@ -1,7 +1,6 @@ package it.vfsfitvnm.vimusic.ui.screens.playlist import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import it.vfsfitvnm.route.RouteHandler @@ -9,7 +8,6 @@ import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.screens.globalRoutes -@ExperimentalFoundationApi @ExperimentalAnimationApi @Composable fun PlaylistScreen(browseId: String) { @@ -29,9 +27,7 @@ fun PlaylistScreen(browseId: String) { } ) { currentTabIndex -> saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { - PlaylistSongList( - browseId = browseId - ) + PlaylistSongList(browseId = browseId) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index db94305..b663834 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -2,7 +2,6 @@ package it.vfsfitvnm.vimusic.ui.screens.playlist import android.content.Intent import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -68,7 +67,6 @@ import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.withContext @ExperimentalAnimationApi -@ExperimentalFoundationApi @Composable fun PlaylistSongList( browseId: String, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 200c7ec..9405e33 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -47,10 +47,6 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.flowOn -//context(ProduceStateScope) -//fun Flow.distinctUntilChangedWithProducedState() = -// distinctUntilChanged { old, new -> new != old && new != value } - @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt index 35b091a..382caff 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/OnlineSearch.kt @@ -40,6 +40,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.R @@ -65,9 +66,8 @@ import kotlinx.coroutines.flow.flowOn fun OnlineSearch( textFieldValue: TextFieldValue, onTextFieldValueChanged: (TextFieldValue) -> Unit, - isOpenableUrl: Boolean, onSearch: (String) -> Unit, - onUri: () -> Unit + onViewPlaylist: (String) -> Unit ) { val (colorPalette, typography) = LocalAppearance.current @@ -92,6 +92,16 @@ fun OnlineSearch( } } + val playlistId = remember(textFieldValue.text) { + val isPlaylistUrl = listOf( + "https://www.youtube.com/playlist?", + "https://music.youtube.com/playlist?", + "https://m.youtube.com/playlist?", + ).any(textFieldValue.text::startsWith) + + if (isPlaylistUrl) textFieldValue.text.toUri().getQueryParameter("list") else null + } + val timeIconPainter = painterResource(R.drawable.time) val closeIconPainter = painterResource(R.drawable.close) val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) @@ -156,13 +166,15 @@ fun OnlineSearch( ) }, actionsContent = { - if (isOpenableUrl) { + if (playlistId != null) { + val isAlbum = playlistId.startsWith("OLAK5uy_") + BasicText( - text = "Open url", + text = "View ${if (isAlbum) "album" else "playlist"}", style = typography.xxs.medium, modifier = Modifier .clip(RoundedCornerShape(16.dp)) - .clickable(onClick = onUri) + .clickable { onViewPlaylist(textFieldValue.text) } .background(colorPalette.background2) .padding(all = 8.dp) .padding(horizontal = 8.dp) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt index c5b025b..a6f65fd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/SearchScreen.kt @@ -1,16 +1,13 @@ package it.vfsfitvnm.vimusic.ui.screens.search -import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import androidx.core.net.toUri import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold @@ -19,7 +16,11 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes @ExperimentalFoundationApi @ExperimentalAnimationApi @Composable -fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (Uri) -> Unit) { +fun SearchScreen( + initialTextInput: String, + onSearch: (String) -> Unit, + onViewPlaylist: (String) -> Unit +) { val saveableStateHolder = rememberSaveableStateHolder() val (tabIndex, onTabChanged) = rememberSaveable { @@ -42,18 +43,6 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U globalRoutes() host { - val isOpenableUrl = remember(textFieldValue.text) { - listOf( - "https://www.youtube.com/watch?", - "https://music.youtube.com/watch?", - "https://m.youtube.com/watch?", - "https://www.youtube.com/playlist?", - "https://music.youtube.com/playlist?", - "https://m.youtube.com/playlist?", - "https://youtu.be/", - ).any(textFieldValue.text::startsWith) - } - Scaffold( topIconButtonId = R.drawable.chevron_back, onTopIconButtonClick = pop, @@ -69,13 +58,8 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U 0 -> OnlineSearch( textFieldValue = textFieldValue, onTextFieldValueChanged = onTextFieldValueChanged, - isOpenableUrl = isOpenableUrl, onSearch = onSearch, - onUri = { - if (isOpenableUrl) { - onUri(textFieldValue.text.toUri()) - } - } + onViewPlaylist = onViewPlaylist ) 1 -> LocalSongSearch( textFieldValue = textFieldValue, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt index 0f88037..933d87e 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Utils.kt @@ -22,6 +22,10 @@ fun Context.shareAsYouTubeSong(mediaItem: MediaItem) { val YouTube.Item.Song.asMediaItem: MediaItem get() = MediaItem.Builder() + .also { +// println("$this") +// println(info.endpoint?.videoId) + } .setMediaId(info.endpoint!!.videoId!!) .setUri(info.endpoint!!.videoId) .setCustomCacheKey(info.endpoint!!.videoId) diff --git a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt new file mode 100644 index 0000000..00a17af --- /dev/null +++ b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/GlobalRoute.kt @@ -0,0 +1,14 @@ +package it.vfsfitvnm.route + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.MutableSharedFlow + +internal val globalRouteFlow = MutableSharedFlow>>(extraBufferCapacity = 1) + +@Composable +fun OnGlobalRoute(block: suspend (Pair>) -> Unit) { + LaunchedEffect(Unit) { + globalRouteFlow.collect(block) + } +} diff --git a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt index 9fe83b2..992286d 100644 --- a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt +++ b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/Route.kt @@ -4,10 +4,9 @@ package it.vfsfitvnm.route import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.SaverScope -import androidx.compose.runtime.saveable.rememberSaveable +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first @Immutable open class Route internal constructor(val tag: String) { @@ -23,23 +22,12 @@ open class Route internal constructor(val tag: String) { return tag.hashCode() } - object GlobalEmitter { - var listener: ((Route, Array) -> Unit)? = null - } - object Saver : androidx.compose.runtime.saveable.Saver { override fun restore(value: String): Route? = value.takeIf(String::isNotEmpty)?.let(::Route) override fun SaverScope.save(value: Route?): String = value?.tag ?: "" } } -@Composable -fun rememberRoute(route: Route? = null): MutableState { - return rememberSaveable(stateSaver = Route.Saver) { - mutableStateOf(route) - } -} - @Immutable class Route0(tag: String) : Route(tag) { context(RouteHandlerScope) @@ -51,7 +39,7 @@ class Route0(tag: String) : Route(tag) { } fun global() { - GlobalEmitter.listener?.invoke(this, emptyArray()) + globalRouteFlow.tryEmit(this to emptyArray()) } } @@ -66,7 +54,12 @@ class Route1(tag: String) : Route(tag) { } fun global(p0: P0) { - GlobalEmitter.listener?.invoke(this, arrayOf(p0)) + globalRouteFlow.tryEmit(this to arrayOf(p0)) + } + + suspend fun ensureGlobal(p0: P0) { + globalRouteFlow.subscriptionCount.filter { it > 0 }.first() + globalRouteFlow.emit(this to arrayOf(p0)) } } @@ -81,6 +74,6 @@ class Route2(tag: String) : Route(tag) { } fun global(p0: P0, p1: P1) { - GlobalEmitter.listener?.invoke(this, arrayOf(p0, p1)) + globalRouteFlow.tryEmit(this to arrayOf(p0, p1)) } } diff --git a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt index d7b432c..a71b1ee 100644 --- a/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt +++ b/compose-routing/src/main/kotlin/it/vfsfitvnm/route/RouteHandler.kt @@ -8,8 +8,8 @@ import androidx.compose.animation.ContentTransform import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.updateTransition import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue @@ -24,7 +24,9 @@ fun RouteHandler( transitionSpec: AnimatedContentScope.() -> ContentTransform = { fastFade }, content: @Composable RouteHandlerScope.() -> Unit ) { - var route by rememberRoute() + var route by rememberSaveable(stateSaver = Route.Saver) { + mutableStateOf(null) + } RouteHandler( route = route, @@ -63,12 +65,10 @@ fun RouteHandler( ) } - if (listenToGlobalEmitter) { - LaunchedEffect(route) { - Route.GlobalEmitter.listener = if (route == null) ({ newRoute, newParameters -> - newParameters.forEachIndexed(parameters::set) - onRouteChanged(newRoute) - }) else null + if (listenToGlobalEmitter && route == null) { + OnGlobalRoute { (newRoute, newParameters) -> + newParameters.forEachIndexed(parameters::set) + onRouteChanged(newRoute) } }