Rework url management (#172)
This commit is contained in:
parent
acc2768eb4
commit
a600c8b457
16 changed files with 142 additions and 400 deletions
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue