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.ServiceConnection
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.widget.Toast
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
@@ -37,7 +37,6 @@ import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.neverEqualPolicy
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue 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.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat import androidx.core.view.WindowCompat
import androidx.lifecycle.lifecycleScope
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import com.valentinilk.shimmer.LocalShimmerTheme 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.dismissedAnchor
import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState 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.home.HomeScreen
import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView 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.Appearance
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf
import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf
import it.vfsfitvnm.vimusic.ui.styling.typographyOf import it.vfsfitvnm.vimusic.ui.styling.typographyOf
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.getEnum import it.vfsfitvnm.vimusic.utils.getEnum
import it.vfsfitvnm.vimusic.utils.intent import it.vfsfitvnm.vimusic.utils.intent
import it.vfsfitvnm.vimusic.utils.listener import it.vfsfitvnm.vimusic.utils.listener
import it.vfsfitvnm.vimusic.utils.preferences import it.vfsfitvnm.vimusic.utils.preferences
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -102,7 +106,6 @@ class MainActivity : ComponentActivity() {
} }
private var binder by mutableStateOf<PlayerService.Binder?>(null) private var binder by mutableStateOf<PlayerService.Binder?>(null)
private var uri by mutableStateOf<Uri?>(null, neverEqualPolicy())
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
@@ -120,14 +123,13 @@ class MainActivity : ComponentActivity() {
WindowCompat.setDecorFitsSystemWindows(window, false) WindowCompat.setDecorFitsSystemWindows(window, false)
val playerBottomSheetAnchor = when { val playerBottomSheetAnchor = when {
intent?.extras?.getBoolean("expandPlayerBottomSheet") == true -> expandedAnchor intent?.extras?.getBoolean("expandPlayerBottomSheet") == true -> expandedAnchor
alreadyRunning -> collapsedAnchor alreadyRunning -> collapsedAnchor
else -> dismissedAnchor.also { alreadyRunning = true } else -> dismissedAnchor.also { alreadyRunning = true }
} }
uri = intent?.data
setContent { setContent {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val isSystemInDarkTheme = isSystemInDarkTheme() val isSystemInDarkTheme = isSystemInDarkTheme()
@@ -324,30 +326,29 @@ class MainActivity : ComponentActivity() {
LocalPlayerServiceBinder provides binder, LocalPlayerServiceBinder provides binder,
LocalPlayerAwarePaddingValues provides playerAwarePaddingValues LocalPlayerAwarePaddingValues provides playerAwarePaddingValues
) { ) {
when (val uri = uri) { HomeScreen(
null -> { onPlaylistUrl = { url ->
HomeScreen() onNewIntent(Intent.parseUri(url, 0))
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 { }
}
} }
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( BottomSheetMenu(
@@ -358,11 +359,41 @@ class MainActivity : ComponentActivity() {
} }
} }
} }
onNewIntent(intent)
} }
override fun onNewIntent(intent: Intent?) { override fun onNewIntent(intent: Intent?) {
super.onNewIntent(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) { private fun setSystemBarAppearance(isDark: Boolean) {

View File

@@ -181,8 +181,7 @@ fun QueuedMediaItemMenu(
mediaItem: MediaItem, mediaItem: MediaItem,
indexInQueue: Int?, indexInQueue: Int?,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
onDismiss: (() -> Unit)? = null, onDismiss: (() -> Unit)? = null
onGlobalRouteEmitted: (() -> Unit)? = null
) { ) {
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@@ -193,7 +192,6 @@ fun QueuedMediaItemMenu(
onRemoveFromQueue = if (indexInQueue != null) ({ onRemoveFromQueue = if (indexInQueue != null) ({
binder?.player?.removeMediaItem(indexInQueue) binder?.player?.removeMediaItem(indexInQueue)
}) else null, }) else null,
onGlobalRouteEmitted = onGlobalRouteEmitted,
modifier = modifier modifier = modifier
) )
} }
@@ -212,8 +210,7 @@ fun BaseMediaItemMenu(
onRemoveFromQueue: (() -> Unit)? = null, onRemoveFromQueue: (() -> Unit)? = null,
onRemoveFromPlaylist: (() -> Unit)? = null, onRemoveFromPlaylist: (() -> Unit)? = null,
onHideFromDatabase: (() -> Unit)? = null, onHideFromDatabase: (() -> Unit)? = null,
onRemoveFromFavorites: (() -> Unit)? = null, onRemoveFromFavorites: (() -> Unit)? = null
onGlobalRouteEmitted: (() -> Unit)? = null,
) { ) {
val context = LocalContext.current val context = LocalContext.current
@@ -246,7 +243,6 @@ fun BaseMediaItemMenu(
onShare = { onShare = {
context.shareAsYouTubeSong(mediaItem) context.shareAsYouTubeSong(mediaItem)
}, },
onGlobalRouteEmitted = onGlobalRouteEmitted,
modifier = modifier modifier = modifier
) )
} }
@@ -269,8 +265,7 @@ fun MediaItemMenu(
onAddToPlaylist: ((Playlist, Int) -> Unit)? = null, onAddToPlaylist: ((Playlist, Int) -> Unit)? = null,
onGoToAlbum: ((String) -> Unit)? = null, onGoToAlbum: ((String) -> Unit)? = null,
onGoToArtist: ((String) -> Unit)? = null, onGoToArtist: ((String) -> Unit)? = null,
onShare: (() -> Unit)? = null, onShare: (() -> Unit)? = null
onGlobalRouteEmitted: (() -> Unit)? = null,
) { ) {
Menu(modifier = modifier) { Menu(modifier = modifier) {
RouteHandler( RouteHandler(
@@ -566,7 +561,6 @@ fun MediaItemMenu(
text = "Go to album", text = "Go to album",
onClick = { onClick = {
onDismiss() onDismiss()
onGlobalRouteEmitted?.invoke()
onGoToAlbum(albumId) onGoToAlbum(albumId)
} }
) )
@@ -586,7 +580,6 @@ fun MediaItemMenu(
text = "More of $authorName", text = "More of $authorName",
onClick = { onClick = {
onDismiss() onDismiss()
onGlobalRouteEmitted?.invoke()
onGoToArtist(authorId) 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 package it.vfsfitvnm.vimusic.ui.screens
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.net.Uri
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import it.vfsfitvnm.route.Route0 import it.vfsfitvnm.route.Route0
@@ -10,11 +9,11 @@ import it.vfsfitvnm.route.RouteHandlerScope
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen import it.vfsfitvnm.vimusic.ui.screens.album.AlbumScreen
import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen import it.vfsfitvnm.vimusic.ui.screens.artist.ArtistScreen
import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen
val albumRoute = Route1<String?>("albumRoute") val albumRoute = Route1<String?>("albumRoute")
val artistRoute = Route1<String?>("artistRoute") val artistRoute = Route1<String?>("artistRoute")
val builtInPlaylistRoute = Route1<BuiltInPlaylist>("builtInPlaylistRoute") val builtInPlaylistRoute = Route1<BuiltInPlaylist>("builtInPlaylistRoute")
val intentUriRoute = Route1<Uri?>("intentUriRoute")
val localPlaylistRoute = Route1<Long?>("localPlaylistRoute") val localPlaylistRoute = Route1<Long?>("localPlaylistRoute")
val playlistRoute = Route1<String?>("playlistRoute") val playlistRoute = Route1<String?>("playlistRoute")
val searchResultRoute = Route1<String>("searchResultRoute") val searchResultRoute = Route1<String>("searchResultRoute")
@@ -38,4 +37,10 @@ inline fun RouteHandlerScope.globalRoutes() {
browseId = browseId ?: error("browseId cannot be null") 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 package it.vfsfitvnm.vimusic.ui.screens.home
import android.net.Uri
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -11,13 +10,11 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.SearchQuery import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.query import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold 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.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.artistRoute import it.vfsfitvnm.vimusic.ui.screens.artistRoute
import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute
import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes 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.localPlaylistRoute
import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen
import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen
@@ -32,10 +29,12 @@ import it.vfsfitvnm.vimusic.utils.rememberPreference
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun HomeScreen() { fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
val saveableStateHolder = rememberSaveableStateHolder() val saveableStateHolder = rememberSaveableStateHolder()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
settingsRoute { settingsRoute {
SettingsScreen() SettingsScreen()
} }
@@ -71,17 +70,7 @@ fun HomeScreen() {
Database.insert(SearchQuery(query = query)) Database.insert(SearchQuery(query = query))
} }
}, },
onUri = { uri -> onViewPlaylist = onPlaylistUrl
intentUriRoute(uri)
}
)
}
globalRoutes()
intentUriRoute { uri ->
IntentUriScreen(
uri = uri ?: Uri.EMPTY
) )
} }

View File

@@ -72,7 +72,6 @@ import kotlinx.coroutines.launch
fun PlayerBottomSheet( fun PlayerBottomSheet(
backgroundColorProvider: () -> Color, backgroundColorProvider: () -> Color,
layoutState: BottomSheetState, layoutState: BottomSheetState,
onGlobalRouteEmitted: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit, content: @Composable BoxScope.() -> Unit,
) { ) {
@@ -166,8 +165,7 @@ fun PlayerBottomSheet(
menuContent = { menuContent = {
QueuedMediaItemMenu( QueuedMediaItemMenu(
mediaItem = window.mediaItem, mediaItem = window.mediaItem,
indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex, indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex
onGlobalRouteEmitted = onGlobalRouteEmitted
) )
}, },
onThumbnailContent = { onThumbnailContent = {

View File

@@ -50,6 +50,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.media3.common.Player import androidx.media3.common.Player
import coil.compose.AsyncImage import coil.compose.AsyncImage
import it.vfsfitvnm.route.OnGlobalRoute
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.BottomSheet import it.vfsfitvnm.vimusic.ui.components.BottomSheet
@@ -95,6 +96,10 @@ fun PlayerView(
val shouldBePlaying by rememberShouldBePlaying(binder.player) val shouldBePlaying by rememberShouldBePlaying(binder.player)
val positionAndDuration by rememberPositionAndDuration(binder.player) val positionAndDuration by rememberPositionAndDuration(binder.player)
OnGlobalRoute {
layoutState.collapseSoft()
}
BottomSheet( BottomSheet(
state = layoutState, state = layoutState,
modifier = modifier, modifier = modifier,
@@ -321,7 +326,6 @@ fun PlayerView(
PlayerBottomSheet( PlayerBottomSheet(
layoutState = playerBottomSheetState, layoutState = playerBottomSheetState,
onGlobalRouteEmitted = layoutState::collapseSoft,
content = { content = {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -385,8 +389,7 @@ fun PlayerView(
} }
}, },
onSetSleepTimer = {}, onSetSleepTimer = {},
onDismiss = menuState::hide, onDismiss = menuState::hide
onGlobalRouteEmitted = layoutState::collapseSoft,
) )
} }
} }

View File

@@ -1,7 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens.playlist package it.vfsfitvnm.vimusic.ui.screens.playlist
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import it.vfsfitvnm.route.RouteHandler 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.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
@ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun PlaylistScreen(browseId: String) { fun PlaylistScreen(browseId: String) {
@@ -29,9 +27,7 @@ fun PlaylistScreen(browseId: String) {
} }
) { currentTabIndex -> ) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) { saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
PlaylistSongList( PlaylistSongList(browseId = browseId)
browseId = browseId
)
} }
} }
} }

View File

@@ -2,7 +2,6 @@ package it.vfsfitvnm.vimusic.ui.screens.playlist
import android.content.Intent import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@@ -68,7 +67,6 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ExperimentalFoundationApi
@Composable @Composable
fun PlaylistSongList( fun PlaylistSongList(
browseId: String, browseId: String,

View File

@@ -47,10 +47,6 @@ import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.flowOn
//context(ProduceStateScope<T>)
//fun <T> Flow<T>.distinctUntilChangedWithProducedState() =
// distinctUntilChanged { old, new -> new != old && new != value }
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @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.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
@@ -65,9 +66,8 @@ import kotlinx.coroutines.flow.flowOn
fun OnlineSearch( fun OnlineSearch(
textFieldValue: TextFieldValue, textFieldValue: TextFieldValue,
onTextFieldValueChanged: (TextFieldValue) -> Unit, onTextFieldValueChanged: (TextFieldValue) -> Unit,
isOpenableUrl: Boolean,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
onUri: () -> Unit onViewPlaylist: (String) -> Unit
) { ) {
val (colorPalette, typography) = LocalAppearance.current 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 timeIconPainter = painterResource(R.drawable.time)
val closeIconPainter = painterResource(R.drawable.close) val closeIconPainter = painterResource(R.drawable.close)
val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward) val arrowForwardIconPainter = painterResource(R.drawable.arrow_forward)
@@ -156,13 +166,15 @@ fun OnlineSearch(
) )
}, },
actionsContent = { actionsContent = {
if (isOpenableUrl) { if (playlistId != null) {
val isAlbum = playlistId.startsWith("OLAK5uy_")
BasicText( BasicText(
text = "Open url", text = "View ${if (isAlbum) "album" else "playlist"}",
style = typography.xxs.medium, style = typography.xxs.medium,
modifier = Modifier modifier = Modifier
.clip(RoundedCornerShape(16.dp)) .clip(RoundedCornerShape(16.dp))
.clickable(onClick = onUri) .clickable { onViewPlaylist(textFieldValue.text) }
.background(colorPalette.background2) .background(colorPalette.background2)
.padding(all = 8.dp) .padding(all = 8.dp)
.padding(horizontal = 8.dp) .padding(horizontal = 8.dp)

View File

@@ -1,16 +1,13 @@
package it.vfsfitvnm.vimusic.ui.screens.search package it.vfsfitvnm.vimusic.ui.screens.search
import android.net.Uri
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.core.net.toUri
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
@@ -19,7 +16,11 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
@ExperimentalFoundationApi @ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @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 saveableStateHolder = rememberSaveableStateHolder()
val (tabIndex, onTabChanged) = rememberSaveable { val (tabIndex, onTabChanged) = rememberSaveable {
@@ -42,18 +43,6 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U
globalRoutes() globalRoutes()
host { 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( Scaffold(
topIconButtonId = R.drawable.chevron_back, topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop, onTopIconButtonClick = pop,
@@ -69,13 +58,8 @@ fun SearchScreen(initialTextInput: String, onSearch: (String) -> Unit, onUri: (U
0 -> OnlineSearch( 0 -> OnlineSearch(
textFieldValue = textFieldValue, textFieldValue = textFieldValue,
onTextFieldValueChanged = onTextFieldValueChanged, onTextFieldValueChanged = onTextFieldValueChanged,
isOpenableUrl = isOpenableUrl,
onSearch = onSearch, onSearch = onSearch,
onUri = { onViewPlaylist = onViewPlaylist
if (isOpenableUrl) {
onUri(textFieldValue.text.toUri())
}
}
) )
1 -> LocalSongSearch( 1 -> LocalSongSearch(
textFieldValue = textFieldValue, textFieldValue = textFieldValue,

View File

@@ -22,6 +22,10 @@ fun Context.shareAsYouTubeSong(mediaItem: MediaItem) {
val YouTube.Item.Song.asMediaItem: MediaItem val YouTube.Item.Song.asMediaItem: MediaItem
get() = MediaItem.Builder() get() = MediaItem.Builder()
.also {
// println("$this")
// println(info.endpoint?.videoId)
}
.setMediaId(info.endpoint!!.videoId!!) .setMediaId(info.endpoint!!.videoId!!)
.setUri(info.endpoint!!.videoId) .setUri(info.endpoint!!.videoId)
.setCustomCacheKey(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.Composable
import androidx.compose.runtime.Immutable 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.SaverScope
import androidx.compose.runtime.saveable.rememberSaveable import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
@Immutable @Immutable
open class Route internal constructor(val tag: String) { open class Route internal constructor(val tag: String) {
@@ -23,23 +22,12 @@ open class Route internal constructor(val tag: String) {
return tag.hashCode() return tag.hashCode()
} }
object GlobalEmitter {
var listener: ((Route, Array<Any?>) -> Unit)? = null
}
object Saver : androidx.compose.runtime.saveable.Saver<Route?, String> { object Saver : androidx.compose.runtime.saveable.Saver<Route?, String> {
override fun restore(value: String): Route? = value.takeIf(String::isNotEmpty)?.let(::Route) override fun restore(value: String): Route? = value.takeIf(String::isNotEmpty)?.let(::Route)
override fun SaverScope.save(value: Route?): String = value?.tag ?: "" 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 @Immutable
class Route0(tag: String) : Route(tag) { class Route0(tag: String) : Route(tag) {
context(RouteHandlerScope) context(RouteHandlerScope)
@@ -51,7 +39,7 @@ class Route0(tag: String) : Route(tag) {
} }
fun global() { 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) { 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) { 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.ExperimentalAnimationApi
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
@@ -24,7 +24,9 @@ fun RouteHandler(
transitionSpec: AnimatedContentScope<RouteHandlerScope>.() -> ContentTransform = { fastFade }, transitionSpec: AnimatedContentScope<RouteHandlerScope>.() -> ContentTransform = { fastFade },
content: @Composable RouteHandlerScope.() -> Unit content: @Composable RouteHandlerScope.() -> Unit
) { ) {
var route by rememberRoute() var route by rememberSaveable(stateSaver = Route.Saver) {
mutableStateOf(null)
}
RouteHandler( RouteHandler(
route = route, route = route,
@@ -63,12 +65,10 @@ fun RouteHandler(
) )
} }
if (listenToGlobalEmitter) { if (listenToGlobalEmitter && route == null) {
LaunchedEffect(route) { OnGlobalRoute { (newRoute, newParameters) ->
Route.GlobalEmitter.listener = if (route == null) ({ newRoute, newParameters -> newParameters.forEachIndexed(parameters::set)
newParameters.forEachIndexed(parameters::set) onRouteChanged(newRoute)
onRouteChanged(newRoute)
}) else null
} }
} }