Rework url management (#172)

This commit is contained in:
vfsfitvnm 2022-09-28 12:43:57 +02:00
parent acc2768eb4
commit a600c8b457
16 changed files with 142 additions and 400 deletions

View file

@ -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<PlayerService.Binder?>(null)
private var uri by mutableStateOf<Uri?>(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) {

View file

@ -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)
}
)

View file

@ -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<Result<List<YouTube.Item.Song>>?>(null)
}
var playlistBrowseId by rememberSaveable {
mutableStateOf<String?>(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)
)
}
}
}

View file

@ -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<String?>("albumRoute")
val artistRoute = Route1<String?>("artistRoute")
val builtInPlaylistRoute = Route1<BuiltInPlaylist>("builtInPlaylistRoute")
val intentUriRoute = Route1<Uri?>("intentUriRoute")
val localPlaylistRoute = Route1<Long?>("localPlaylistRoute")
val playlistRoute = Route1<String?>("playlistRoute")
val searchResultRoute = Route1<String>("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")
)
}
}

View file

@ -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
)
}

View file

@ -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 = {

View file

@ -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
)
}
}

View file

@ -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)
}
}
}

View file

@ -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,

View file

@ -47,10 +47,6 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
//context(ProduceStateScope<T>)
//fun <T> Flow<T>.distinctUntilChangedWithProducedState() =
// distinctUntilChanged { old, new -> new != old && new != value }
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable

View file

@ -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)

View file

@ -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,

View file

@ -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)

View file

@ -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<Pair<Route, Array<Any?>>>(extraBufferCapacity = 1)
@Composable
fun OnGlobalRoute(block: suspend (Pair<Route, Array<Any?>>) -> Unit) {
LaunchedEffect(Unit) {
globalRouteFlow.collect(block)
}
}

View file

@ -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<Any?>) -> Unit)? = null
}
object Saver : androidx.compose.runtime.saveable.Saver<Route?, String> {
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<Route?> {
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<P0>(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<P0, P1>(tag: String) : Route(tag) {
}
fun global(p0: P0, p1: P1) {
GlobalEmitter.listener?.invoke(this, arrayOf(p0, p1))
globalRouteFlow.tryEmit(this to arrayOf(p0, p1))
}
}

View file

@ -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<RouteHandlerScope>.() -> 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)
}
}