From baea3d252c10c232fd38ae039203df37bc229241 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Sat, 11 Jun 2022 16:59:19 +0200 Subject: [PATCH] Improve IntentUriScreen and handle links in SearchScreen --- app/src/main/AndroidManifest.xml | 8 +- .../it/vfsfitvnm/vimusic/MainActivity.kt | 39 +- .../vimusic/ui/screens/HomeScreen.kt | 605 +++++++++--------- .../vimusic/ui/screens/IntentUriScreen.kt | 8 +- .../vimusic/ui/screens/SearchScreen.kt | 49 +- .../it/vfsfitvnm/vimusic/ui/screens/routes.kt | 6 +- app/src/main/res/drawable/link.xml | 27 + 7 files changed, 410 insertions(+), 332 deletions(-) create mode 100644 app/src/main/res/drawable/link.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 16374f4..df20870 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,15 +21,17 @@ - - + + + + diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt index 5f78c56..5d3f0af 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/MainActivity.kt @@ -1,7 +1,8 @@ package it.vfsfitvnm.vimusic import android.content.ComponentName -import android.content.Context +import android.content.Intent +import android.net.Uri import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent @@ -15,7 +16,7 @@ import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.gestures.LocalOverScrollConfiguration import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.RippleAlpha @@ -27,7 +28,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.text.ExperimentalTextApi -import androidx.datastore.preferences.preferencesDataStore +import androidx.compose.ui.unit.dp import androidx.media3.session.MediaController import androidx.media3.session.SessionToken import com.google.accompanist.systemuicontroller.rememberSystemUiController @@ -38,12 +39,14 @@ import it.vfsfitvnm.vimusic.enums.ColorPaletteMode import it.vfsfitvnm.vimusic.services.PlayerService import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu import it.vfsfitvnm.vimusic.ui.components.LocalMenuState +import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState import it.vfsfitvnm.vimusic.ui.components.rememberMenuState import it.vfsfitvnm.vimusic.ui.screens.HomeScreen +import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen import it.vfsfitvnm.vimusic.ui.styling.* +import it.vfsfitvnm.vimusic.ui.views.PlayerView import it.vfsfitvnm.vimusic.utils.* -private val Context.dataStore by preferencesDataStore(name = "preferences") @ExperimentalAnimationApi @ExperimentalFoundationApi @@ -51,12 +54,16 @@ private val Context.dataStore by preferencesDataStore(name = "preferences") class MainActivity : ComponentActivity() { private lateinit var mediaControllerFuture: ListenableFuture + private var uri by mutableStateOf(null) + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java)) mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync() + uri = intent?.data + setContent { val preferences = rememberPreferences() val systemUiController = rememberSystemUiController() @@ -119,16 +126,26 @@ class MainActivity : ComponentActivity() { LocalTypography provides rememberTypography(colorPalette.text), LocalYoutubePlayer provides rememberYoutubePlayer(mediaControllerFuture) { it.repeatMode = preferences.repeatMode - }, + }, LocalMenuState provides rememberMenuState(), LocalHapticFeedback provides rememberHapticFeedback() ) { - Box( + BoxWithConstraints( modifier = Modifier .fillMaxSize() - .background(LocalColorPalette.current.background) + .background(colorPalette.background) ) { - HomeScreen(intentUri = intent?.data) + uri?.let { + IntentUriScreen(uri = it) + } ?: HomeScreen() + + PlayerView( + layoutState = rememberBottomSheetState( + lowerBound = 64.dp, upperBound = maxHeight + ), + modifier = Modifier + .align(Alignment.BottomCenter) + ) BottomSheetMenu( state = LocalMenuState.current, @@ -140,9 +157,13 @@ class MainActivity : ComponentActivity() { } } + override fun onNewIntent(intent: Intent?) { + super.onNewIntent(intent) + uri = intent?.data + } + override fun onDestroy() { MediaController.releaseFuture(mediaControllerFuture) super.onDestroy() } } - diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt index 261db34..83c9e67 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/HomeScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.media3.common.Player +import it.vfsfitvnm.route.Route import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.rememberRoute import it.vfsfitvnm.vimusic.Database @@ -39,23 +40,20 @@ import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.models.SongWithInfo import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.TopAppBar -import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState import it.vfsfitvnm.vimusic.ui.components.themed.* import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalTypography -import it.vfsfitvnm.vimusic.ui.views.PlayerView import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.utils.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch @ExperimentalAnimationApi @Composable fun HomeScreen( - intentUri: Uri? +// uri: Uri?, ) { val colorPalette = LocalColorPalette.current val typography = LocalTypography.current @@ -64,7 +62,7 @@ fun HomeScreen( val lazyListState = rememberLazyListState() - val intentUriRoute = rememberIntentUriRoute(intentUri) + val intentUriRoute = rememberIntentUriRoute() val settingsRoute = rememberSettingsRoute() val playlistRoute = rememberLocalPlaylistRoute() val searchRoute = rememberSearchRoute() @@ -72,7 +70,7 @@ fun HomeScreen( val albumRoute = rememberPlaylistOrAlbumRoute() val artistRoute = rememberArtistRoute() - val (route, onRouteChanged) = rememberRoute(intentUri?.let { intentUriRoute }) +// val (route, onRouteChanged) = rememberRoute(uri?.let { intentUriRoute }) val playlistPreviews by remember { Database.playlistPreviews() @@ -88,224 +86,223 @@ fun HomeScreen( } }.collectAsState(initial = emptyList(), context = Dispatchers.IO) - BoxWithConstraints( - modifier = Modifier - .fillMaxSize() + RouteHandler( +// route = route, +// onRouteChanged = onRouteChanged, + listenToGlobalEmitter = true ) { - RouteHandler( - route = route, - onRouteChanged = onRouteChanged, - listenToGlobalEmitter = true - ) { - intentUriRoute { uri -> - IntentUriScreen( - uri = uri ?: error("uri must be not null") - ) + println("route: ${this.route?.tag}") + + settingsRoute { + SettingsScreen() + } + + playlistRoute { playlistId -> + LocalPlaylistScreen( + playlistId = playlistId ?: error("playlistId cannot be null") + ) + } + + searchResultRoute { query -> + SearchResultScreen( + query = query, + onSearchAgain = { + searchRoute(query) + } + ) + } + + searchRoute { initialTextInput -> + SearchScreen( + initialTextInput = initialTextInput, + onSearch = { query -> + searchResultRoute(query) + + coroutineScope.launch(Dispatchers.IO) { + Database.insert(SearchQuery(query = query)) + } + }, + onUri = { uri -> + intentUriRoute(uri) + } + ) + } + + albumRoute { browseId -> + PlaylistOrAlbumScreen(browseId = browseId ?: error("browseId cannot be null")) + } + + artistRoute { browseId -> + ArtistScreen( + browseId = browseId ?: error("browseId cannot be null") + ) + } + + intentUriRoute { uri -> + IntentUriScreen( + uri = uri ?: Uri.EMPTY + ) + } + + host { + val player = LocalYoutubePlayer.current + val menuState = LocalMenuState.current + val density = LocalDensity.current + + val thumbnailSize = remember { + density.run { + 54.dp.roundToPx() + } } - settingsRoute { - SettingsScreen() + var isCreatingANewPlaylist by rememberSaveable { + mutableStateOf(false) } - playlistRoute { playlistId -> - LocalPlaylistScreen( - playlistId = playlistId ?: error("playlistId cannot be null") - ) - } - - searchResultRoute { query -> - SearchResultScreen( - query = query, - onSearchAgain = { - searchRoute(query) + if (isCreatingANewPlaylist) { + TextFieldDialog( + hintText = "Enter the playlist name", + onDismiss = { + isCreatingANewPlaylist = false }, - ) - } - - searchRoute { initialTextInput -> - SearchScreen( - initialTextInput = initialTextInput, - onSearch = { query -> - searchResultRoute(query) - + onDone = { text -> coroutineScope.launch(Dispatchers.IO) { - Database.insert(SearchQuery(query = query)) + Database.insert(Playlist(name = text)) } } ) } - albumRoute { browseId -> - PlaylistOrAlbumScreen(browseId = browseId ?: error("browseId cannot be null")) - } + LazyColumn( + state = lazyListState, + contentPadding = PaddingValues(bottom = 72.dp), + modifier = Modifier + .background(colorPalette.background) + .fillMaxSize() + ) { + item { + TopAppBar( + modifier = Modifier + .height(52.dp) + ) { + Image( + painter = painterResource(R.drawable.cog), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + settingsRoute() + } + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) - artistRoute { browseId -> - ArtistScreen( - browseId = browseId ?: error("browseId cannot be null") - ) - } - - host { - val player = LocalYoutubePlayer.current - val menuState = LocalMenuState.current - val density = LocalDensity.current - - val thumbnailSize = remember { - density.run { - 54.dp.roundToPx() + Image( + painter = painterResource(R.drawable.search), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + searchRoute("") + } + .padding(horizontal = 16.dp, vertical = 8.dp) + .size(24.dp) + ) } } - var isCreatingANewPlaylist by rememberSaveable { - mutableStateOf(false) - } - - if (isCreatingANewPlaylist) { - TextFieldDialog( - hintText = "Enter the playlist name", - onDismiss = { - isCreatingANewPlaylist = false - }, - onDone = { text -> - coroutineScope.launch(Dispatchers.IO) { - Database.insert(Playlist(name = text)) - } - } + item { + BasicText( + text = "Your playlists", + style = typography.m.semiBold, + modifier = Modifier + .padding(horizontal = 16.dp) ) } - LazyColumn( - state = lazyListState, - contentPadding = PaddingValues(bottom = 72.dp), - modifier = Modifier - .background(colorPalette.background) - .fillMaxSize() - ) { - item { - TopAppBar( - modifier = Modifier - .height(52.dp) - ) { - Image( - painter = painterResource(R.drawable.cog), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), + item { + LazyHorizontalGrid( + rows = GridCells.Fixed(2), + contentPadding = PaddingValues(horizontal = 16.dp), + modifier = Modifier + .height(248.dp) + ) { + item { + Column( + horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier - .clickable { - settingsRoute() - } - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - - Image( - painter = painterResource(R.drawable.search), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - searchRoute("") - } - .padding(horizontal = 16.dp, vertical = 8.dp) - .size(24.dp) - ) - } - } - - item { - BasicText( - text = "Your playlists", - style = typography.m.semiBold, - modifier = Modifier - .padding(horizontal = 16.dp) - ) - } - - item { - LazyHorizontalGrid( - rows = GridCells.Fixed(2), - contentPadding = PaddingValues(horizontal = 16.dp), - modifier = Modifier - .height(248.dp) - ) { - item { - Column( - horizontalAlignment = Alignment.CenterHorizontally, + .padding(all = 8.dp) + .width(108.dp) + ) { + Box( + contentAlignment = Alignment.Center, modifier = Modifier - .padding(all = 8.dp) - .width(108.dp) - ) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier - .clickable( - indication = rememberRipple(bounded = true), - interactionSource = remember { MutableInteractionSource() } - ) { - isCreatingANewPlaylist = true - } - .background(colorPalette.lightBackground) - .size(108.dp) - ) { - Image( - painter = painterResource(R.drawable.add), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .size(24.dp) - ) - } - } - } - - items( - items = playlistPreviews, - contentType = { it } - ) { playlistPreview -> - PlaylistPreviewItem( - playlistPreview = playlistPreview, - modifier = Modifier - .padding(all = 8.dp) .clickable( indication = rememberRipple(bounded = true), interactionSource = remember { MutableInteractionSource() } ) { - playlistRoute(playlistPreview.playlist.id) + isCreatingANewPlaylist = true } - ) + .background(colorPalette.lightBackground) + .size(108.dp) + ) { + Image( + painter = painterResource(R.drawable.add), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .size(24.dp) + ) + } } } - } - if (!preferences.isReady) return@LazyColumn - - item { - Row( - verticalAlignment = Alignment.Bottom, - modifier = Modifier - .zIndex(1f) - .padding(horizontal = 8.dp) - .padding(top = 32.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, + items( + items = playlistPreviews, + contentType = { it } + ) { playlistPreview -> + PlaylistPreviewItem( + playlistPreview = playlistPreview, modifier = Modifier - .weight(1f) - .padding(horizontal = 8.dp) - ) { - BasicText( - text = when (preferences.homePageSongCollection) { - SongCollection.MostPlayed -> "Most played" - SongCollection.Favorites -> "Favorites" - SongCollection.History -> "History" - }, - style = typography.m.semiBold, - modifier = Modifier - .animateContentSize() - ) + .padding(all = 8.dp) + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() } + ) { + playlistRoute(playlistPreview.playlist.id) + } + ) + } + } + } - val songCollections = enumValues() - val nextSongCollection = songCollections[(preferences.homePageSongCollection.ordinal + 1) % songCollections.size] + item { + Row( + verticalAlignment = Alignment.Bottom, + modifier = Modifier + .zIndex(1f) + .padding(horizontal = 8.dp) + .padding(top = 32.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .weight(1f) + .padding(horizontal = 8.dp) + ) { + BasicText( + text = when (preferences.homePageSongCollection) { + SongCollection.MostPlayed -> "Most played" + SongCollection.Favorites -> "Favorites" + SongCollection.History -> "History" + }, + style = typography.m.semiBold, + modifier = Modifier + .animateContentSize() + ) + + val songCollections = enumValues() + val nextSongCollection = songCollections[(preferences.homePageSongCollection.ordinal + 1) % songCollections.size] BasicText( text = when (nextSongCollection) { @@ -321,131 +318,125 @@ fun HomeScreen( onClick = { preferences.homePageSongCollection = nextSongCollection } + ) // .alignByBaseline() - .padding(horizontal = 16.dp) - .animateContentSize() - ) - } - - Image( - painter = painterResource(R.drawable.ellipsis_horizontal), - contentDescription = null, - colorFilter = ColorFilter.tint(colorPalette.text), - modifier = Modifier - .clickable { - menuState.display { - BasicMenu(onDismiss = menuState::hide) { - MenuEntry( - icon = R.drawable.play, - text = "Play", - enabled = songCollection.isNotEmpty(), - onClick = { - menuState.hide() - YoutubePlayer.Radio.reset() - player?.mediaController?.forcePlayFromBeginning( - songCollection - .map(SongWithInfo::asMediaItem) - ) - } - ) - - MenuEntry( - icon = R.drawable.shuffle, - text = "Shuffle", - enabled = songCollection.isNotEmpty(), - onClick = { - menuState.hide() - YoutubePlayer.Radio.reset() - player?.mediaController?.forcePlayFromBeginning( - songCollection - .shuffled() - .map(SongWithInfo::asMediaItem) - ) - } - ) - - MenuEntry( - icon = R.drawable.time, - text = "Enqueue", - enabled = songCollection.isNotEmpty() && player?.playbackState == Player.STATE_READY, - onClick = { - menuState.hide() - player?.mediaController?.enqueue( - songCollection.map(SongWithInfo::asMediaItem) - ) - } - ) - } - } - } - .padding(horizontal = 8.dp, vertical = 8.dp) - .size(20.dp) + .padding(horizontal = 16.dp) + .animateContentSize() ) } - } - itemsIndexed( - items = songCollection, - key = { _, song -> - song.song.id - }, - contentType = { _, song -> song } - ) { index, song -> - SongItem( - song = song, - thumbnailSize = thumbnailSize, - onClick = { - YoutubePlayer.Radio.reset() - player?.mediaController?.forcePlayAtIndex( - songCollection.map(SongWithInfo::asMediaItem), - index - ) - }, - menuContent = { - when (preferences.homePageSongCollection) { - SongCollection.MostPlayed -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) - SongCollection.Favorites -> InFavoritesMediaItemMenu(song = song) - SongCollection.History -> InHistoryMediaItemMenu(song = song) - } - }, - onThumbnailContent = { - AnimatedVisibility( - visible = preferences.homePageSongCollection == SongCollection.MostPlayed, - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier - .align(Alignment.BottomCenter) - ) { - BasicText( - text = song.song.formattedTotalPlayTime, - style = typography.xxs.semiBold.center.color(Color.White), - maxLines = 2, - overflow = TextOverflow.Ellipsis, - modifier = Modifier - .fillMaxWidth() - .background( - brush = Brush.verticalGradient( - colors = listOf( - Color.Transparent, - Color.Black.copy(alpha = 0.75f) + Image( + painter = painterResource(R.drawable.ellipsis_horizontal), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.text), + modifier = Modifier + .clickable { + menuState.display { + BasicMenu(onDismiss = menuState::hide) { + MenuEntry( + icon = R.drawable.play, + text = "Play", + enabled = songCollection.isNotEmpty(), + onClick = { + menuState.hide() + YoutubePlayer.Radio.reset() + player?.mediaController?.forcePlayFromBeginning( + songCollection + .map(SongWithInfo::asMediaItem) ) - ), - shape = ThumbnailRoundness.shape + } ) - .padding(horizontal = 8.dp, vertical = 4.dp) - ) + + MenuEntry( + icon = R.drawable.shuffle, + text = "Shuffle", + enabled = songCollection.isNotEmpty(), + onClick = { + menuState.hide() + YoutubePlayer.Radio.reset() + player?.mediaController?.forcePlayFromBeginning( + songCollection + .shuffled() + .map(SongWithInfo::asMediaItem) + ) + } + ) + + MenuEntry( + icon = R.drawable.time, + text = "Enqueue", + enabled = songCollection.isNotEmpty() && player?.playbackState == Player.STATE_READY, + onClick = { + menuState.hide() + player?.mediaController?.enqueue( + songCollection.map(SongWithInfo::asMediaItem) + ) + } + ) + } + } } - } + .padding(horizontal = 8.dp, vertical = 8.dp) + .size(20.dp) ) } } + + itemsIndexed( + items = songCollection, + key = { _, song -> + song.song.id + }, + contentType = { _, song -> song } + ) { index, song -> + SongItem( + song = song, + thumbnailSize = thumbnailSize, + onClick = { + YoutubePlayer.Radio.reset() + player?.mediaController?.forcePlayAtIndex( + songCollection.map(SongWithInfo::asMediaItem), + index + ) + }, + menuContent = { + when (preferences.homePageSongCollection) { + SongCollection.MostPlayed -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) + SongCollection.Favorites -> InFavoritesMediaItemMenu(song = song) + SongCollection.History -> InHistoryMediaItemMenu(song = song) + } + }, + onThumbnailContent = { + AnimatedVisibility( + visible = preferences.homePageSongCollection == SongCollection.MostPlayed, + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier + .align(Alignment.BottomCenter) + ) { + BasicText( + text = song.song.formattedTotalPlayTime, + style = typography.xxs.semiBold.center.color(Color.White), + maxLines = 2, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color.Transparent, + Color.Black.copy(alpha = 0.75f) + ) + ), + shape = ThumbnailRoundness.shape + ) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + ) + } } } - - PlayerView( - layoutState = rememberBottomSheetState(lowerBound = 64.dp, upperBound = maxHeight), - modifier = Modifier - .align(Alignment.BottomCenter) - ) } } 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 index d93d173..f4b867f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/IntentUriScreen.kt @@ -73,12 +73,11 @@ fun IntentUriScreen(uri: Uri) { val coroutineScope = rememberCoroutineScope() val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window) - - var items by remember { + var items by remember(uri) { mutableStateOf>>(Outcome.Loading) } - val onLoad = relaunchableEffect(Unit) { + val onLoad = relaunchableEffect(uri) { items = withContext(Dispatchers.IO) { uri.getQueryParameter("list")?.let { playlistId -> YouTube.queue(playlistId).toNullable()?.map { songList -> @@ -90,7 +89,7 @@ fun IntentUriScreen(uri: Uri) { } } - var isImportingAsPlaylist by remember { + var isImportingAsPlaylist by remember(uri) { mutableStateOf(false) } @@ -246,7 +245,6 @@ fun IntentUriScreen(uri: Uri) { YouTube.Item.Song::asMediaItem ), index ) - pop() } ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt index 15ba1f3..e394cb5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SearchScreen.kt @@ -1,19 +1,17 @@ package it.vfsfitvnm.vimusic.ui.screens +import android.net.Uri import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable +import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.verticalScroll import androidx.compose.material.ripple.rememberRipple import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -30,6 +28,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.R @@ -47,11 +46,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import kotlinx.coroutines.withContext + @ExperimentalAnimationApi @Composable fun SearchScreen( initialTextInput: String, - onSearch: (String) -> Unit + onSearch: (String) -> Unit, + onUri: (Uri) -> Unit, ) { var textFieldValue by rememberSaveable(initialTextInput, stateSaver = TextFieldValue.Saver) { mutableStateOf( @@ -107,6 +108,10 @@ fun SearchScreen( val coroutineScope = rememberCoroutineScope() + val isOpenableUrl = remember(textFieldValue.text) { + Regex("""https://(music|www)\.youtube.com/(watch|playlist).*""").matches(textFieldValue.text) + } + LaunchedEffect(Unit) { delay(300) focusRequester.requestFocus() @@ -184,6 +189,40 @@ fun SearchScreen( ) } + if (isOpenableUrl) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + indication = rememberRipple(bounded = true), + interactionSource = remember { MutableInteractionSource() }, + onClick = { + onUri(textFieldValue.text.toUri()) + } + ) + .fillMaxWidth() + .background(colorPalette.lightBackground) + .padding(vertical = 16.dp, horizontal = 8.dp) + ) { + Image( + painter = painterResource(R.drawable.link), + contentDescription = null, + colorFilter = ColorFilter.tint(colorPalette.darkGray), + modifier = Modifier + .padding(horizontal = 8.dp) + .size(20.dp) + ) + + BasicText( + text = "Open URL", + style = typography.s.secondary, + modifier = Modifier + .padding(horizontal = 8.dp) + .weight(1f) + ) + } + } + Column( modifier = Modifier .verticalScroll(rememberScrollState()) 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 06c4bd2..bafc0ab 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 @@ -9,12 +9,12 @@ import it.vfsfitvnm.route.Route0 import it.vfsfitvnm.route.Route1 @Composable -fun rememberIntentUriRoute(intentUri: Uri?): Route1 { +fun rememberIntentUriRoute(): Route1 { val uri = rememberSaveable { - mutableStateOf(intentUri) + mutableStateOf(null) } return remember { - Route1("rememberIntentUriRoute", uri) + Route1("IntentUriRoute", uri) } } diff --git a/app/src/main/res/drawable/link.xml b/app/src/main/res/drawable/link.xml new file mode 100644 index 0000000..c4de0e7 --- /dev/null +++ b/app/src/main/res/drawable/link.xml @@ -0,0 +1,27 @@ + + + + +