Rework url management (#172)
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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
|
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")
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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.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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user