Improve IntentUriScreen and handle links in SearchScreen

This commit is contained in:
vfsfitvnm
2022-06-11 16:59:19 +02:00
parent 13357d10d7
commit baea3d252c
7 changed files with 410 additions and 332 deletions

View File

@@ -21,15 +21,17 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true" android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTask"
android:theme="@style/Theme.ViMusic.NoActionBar" android:theme="@style/Theme.ViMusic.NoActionBar"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />

View File

@@ -1,7 +1,8 @@
package it.vfsfitvnm.vimusic package it.vfsfitvnm.vimusic
import android.content.ComponentName import android.content.ComponentName
import android.content.Context import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
@@ -15,7 +16,7 @@ import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.LocalOverScrollConfiguration import androidx.compose.foundation.gestures.LocalOverScrollConfiguration
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.ripple.LocalRippleTheme import androidx.compose.material.ripple.LocalRippleTheme
import androidx.compose.material.ripple.RippleAlpha import androidx.compose.material.ripple.RippleAlpha
@@ -27,7 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.ExperimentalTextApi
import androidx.datastore.preferences.preferencesDataStore import androidx.compose.ui.unit.dp
import androidx.media3.session.MediaController import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken import androidx.media3.session.SessionToken
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
@@ -38,12 +39,14 @@ import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
import it.vfsfitvnm.vimusic.services.PlayerService import it.vfsfitvnm.vimusic.services.PlayerService
import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu import it.vfsfitvnm.vimusic.ui.components.BottomSheetMenu
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
import it.vfsfitvnm.vimusic.ui.components.rememberMenuState import it.vfsfitvnm.vimusic.ui.components.rememberMenuState
import it.vfsfitvnm.vimusic.ui.screens.HomeScreen import it.vfsfitvnm.vimusic.ui.screens.HomeScreen
import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen
import it.vfsfitvnm.vimusic.ui.styling.* import it.vfsfitvnm.vimusic.ui.styling.*
import it.vfsfitvnm.vimusic.ui.views.PlayerView
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
private val Context.dataStore by preferencesDataStore(name = "preferences")
@ExperimentalAnimationApi @ExperimentalAnimationApi
@ExperimentalFoundationApi @ExperimentalFoundationApi
@@ -51,12 +54,16 @@ private val Context.dataStore by preferencesDataStore(name = "preferences")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private lateinit var mediaControllerFuture: ListenableFuture<MediaController> private lateinit var mediaControllerFuture: ListenableFuture<MediaController>
private var uri by mutableStateOf<Uri?>(null)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java)) val sessionToken = SessionToken(this, ComponentName(this, PlayerService::class.java))
mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync() mediaControllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
uri = intent?.data
setContent { setContent {
val preferences = rememberPreferences() val preferences = rememberPreferences()
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
@@ -119,16 +126,26 @@ class MainActivity : ComponentActivity() {
LocalTypography provides rememberTypography(colorPalette.text), LocalTypography provides rememberTypography(colorPalette.text),
LocalYoutubePlayer provides rememberYoutubePlayer(mediaControllerFuture) { LocalYoutubePlayer provides rememberYoutubePlayer(mediaControllerFuture) {
it.repeatMode = preferences.repeatMode it.repeatMode = preferences.repeatMode
}, },
LocalMenuState provides rememberMenuState(), LocalMenuState provides rememberMenuState(),
LocalHapticFeedback provides rememberHapticFeedback() LocalHapticFeedback provides rememberHapticFeedback()
) { ) {
Box( BoxWithConstraints(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(LocalColorPalette.current.background) .background(colorPalette.background)
) { ) {
HomeScreen(intentUri = intent?.data) uri?.let {
IntentUriScreen(uri = it)
} ?: HomeScreen()
PlayerView(
layoutState = rememberBottomSheetState(
lowerBound = 64.dp, upperBound = maxHeight
),
modifier = Modifier
.align(Alignment.BottomCenter)
)
BottomSheetMenu( BottomSheetMenu(
state = LocalMenuState.current, state = LocalMenuState.current,
@@ -140,9 +157,13 @@ class MainActivity : ComponentActivity() {
} }
} }
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
uri = intent?.data
}
override fun onDestroy() { override fun onDestroy() {
MediaController.releaseFuture(mediaControllerFuture) MediaController.releaseFuture(mediaControllerFuture)
super.onDestroy() super.onDestroy()
} }
} }

View File

@@ -28,6 +28,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import androidx.media3.common.Player import androidx.media3.common.Player
import it.vfsfitvnm.route.Route
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.route.rememberRoute import it.vfsfitvnm.route.rememberRoute
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
@@ -39,23 +40,20 @@ import it.vfsfitvnm.vimusic.models.SearchQuery
import it.vfsfitvnm.vimusic.models.SongWithInfo import it.vfsfitvnm.vimusic.models.SongWithInfo
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
import it.vfsfitvnm.vimusic.ui.components.themed.* import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.ui.views.PlayerView
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun HomeScreen( fun HomeScreen(
intentUri: Uri? // uri: Uri?,
) { ) {
val colorPalette = LocalColorPalette.current val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current val typography = LocalTypography.current
@@ -64,7 +62,7 @@ fun HomeScreen(
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val intentUriRoute = rememberIntentUriRoute(intentUri) val intentUriRoute = rememberIntentUriRoute()
val settingsRoute = rememberSettingsRoute() val settingsRoute = rememberSettingsRoute()
val playlistRoute = rememberLocalPlaylistRoute() val playlistRoute = rememberLocalPlaylistRoute()
val searchRoute = rememberSearchRoute() val searchRoute = rememberSearchRoute()
@@ -72,7 +70,7 @@ fun HomeScreen(
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberPlaylistOrAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val (route, onRouteChanged) = rememberRoute(intentUri?.let { intentUriRoute }) // val (route, onRouteChanged) = rememberRoute(uri?.let { intentUriRoute })
val playlistPreviews by remember { val playlistPreviews by remember {
Database.playlistPreviews() Database.playlistPreviews()
@@ -88,224 +86,223 @@ fun HomeScreen(
} }
}.collectAsState(initial = emptyList(), context = Dispatchers.IO) }.collectAsState(initial = emptyList(), context = Dispatchers.IO)
BoxWithConstraints( RouteHandler(
modifier = Modifier // route = route,
.fillMaxSize() // onRouteChanged = onRouteChanged,
listenToGlobalEmitter = true
) { ) {
RouteHandler( println("route: ${this.route?.tag}")
route = route,
onRouteChanged = onRouteChanged, settingsRoute {
listenToGlobalEmitter = true SettingsScreen()
) { }
intentUriRoute { uri ->
IntentUriScreen( playlistRoute { playlistId ->
uri = uri ?: error("uri must be not null") LocalPlaylistScreen(
) playlistId = playlistId ?: error("playlistId cannot be null")
)
}
searchResultRoute { query ->
SearchResultScreen(
query = query,
onSearchAgain = {
searchRoute(query)
}
)
}
searchRoute { initialTextInput ->
SearchScreen(
initialTextInput = initialTextInput,
onSearch = { query ->
searchResultRoute(query)
coroutineScope.launch(Dispatchers.IO) {
Database.insert(SearchQuery(query = query))
}
},
onUri = { uri ->
intentUriRoute(uri)
}
)
}
albumRoute { browseId ->
PlaylistOrAlbumScreen(browseId = browseId ?: error("browseId cannot be null"))
}
artistRoute { browseId ->
ArtistScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
intentUriRoute { uri ->
IntentUriScreen(
uri = uri ?: Uri.EMPTY
)
}
host {
val player = LocalYoutubePlayer.current
val menuState = LocalMenuState.current
val density = LocalDensity.current
val thumbnailSize = remember {
density.run {
54.dp.roundToPx()
}
} }
settingsRoute { var isCreatingANewPlaylist by rememberSaveable {
SettingsScreen() mutableStateOf(false)
} }
playlistRoute { playlistId -> if (isCreatingANewPlaylist) {
LocalPlaylistScreen( TextFieldDialog(
playlistId = playlistId ?: error("playlistId cannot be null") hintText = "Enter the playlist name",
) onDismiss = {
} isCreatingANewPlaylist = false
searchResultRoute { query ->
SearchResultScreen(
query = query,
onSearchAgain = {
searchRoute(query)
}, },
) onDone = { text ->
}
searchRoute { initialTextInput ->
SearchScreen(
initialTextInput = initialTextInput,
onSearch = { query ->
searchResultRoute(query)
coroutineScope.launch(Dispatchers.IO) { coroutineScope.launch(Dispatchers.IO) {
Database.insert(SearchQuery(query = query)) Database.insert(Playlist(name = text))
} }
} }
) )
} }
albumRoute { browseId -> LazyColumn(
PlaylistOrAlbumScreen(browseId = browseId ?: error("browseId cannot be null")) state = lazyListState,
} contentPadding = PaddingValues(bottom = 72.dp),
modifier = Modifier
.background(colorPalette.background)
.fillMaxSize()
) {
item {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.cog),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
settingsRoute()
}
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
artistRoute { browseId -> Image(
ArtistScreen( painter = painterResource(R.drawable.search),
browseId = browseId ?: error("browseId cannot be null") contentDescription = null,
) colorFilter = ColorFilter.tint(colorPalette.text),
} modifier = Modifier
.clickable {
host { searchRoute("")
val player = LocalYoutubePlayer.current }
val menuState = LocalMenuState.current .padding(horizontal = 16.dp, vertical = 8.dp)
val density = LocalDensity.current .size(24.dp)
)
val thumbnailSize = remember {
density.run {
54.dp.roundToPx()
} }
} }
var isCreatingANewPlaylist by rememberSaveable { item {
mutableStateOf(false) BasicText(
} text = "Your playlists",
style = typography.m.semiBold,
if (isCreatingANewPlaylist) { modifier = Modifier
TextFieldDialog( .padding(horizontal = 16.dp)
hintText = "Enter the playlist name",
onDismiss = {
isCreatingANewPlaylist = false
},
onDone = { text ->
coroutineScope.launch(Dispatchers.IO) {
Database.insert(Playlist(name = text))
}
}
) )
} }
LazyColumn( item {
state = lazyListState, LazyHorizontalGrid(
contentPadding = PaddingValues(bottom = 72.dp), rows = GridCells.Fixed(2),
modifier = Modifier contentPadding = PaddingValues(horizontal = 16.dp),
.background(colorPalette.background) modifier = Modifier
.fillMaxSize() .height(248.dp)
) { ) {
item { item {
TopAppBar( Column(
modifier = Modifier horizontalAlignment = Alignment.CenterHorizontally,
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.cog),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier modifier = Modifier
.clickable { .padding(all = 8.dp)
settingsRoute() .width(108.dp)
} ) {
.padding(horizontal = 16.dp, vertical = 8.dp) Box(
.size(24.dp) contentAlignment = Alignment.Center,
)
Image(
painter = painterResource(R.drawable.search),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
searchRoute("")
}
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
}
item {
BasicText(
text = "Your playlists",
style = typography.m.semiBold,
modifier = Modifier
.padding(horizontal = 16.dp)
)
}
item {
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
contentPadding = PaddingValues(horizontal = 16.dp),
modifier = Modifier
.height(248.dp)
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier modifier = Modifier
.padding(all = 8.dp)
.width(108.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() }
) {
isCreatingANewPlaylist = true
}
.background(colorPalette.lightBackground)
.size(108.dp)
) {
Image(
painter = painterResource(R.drawable.add),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.size(24.dp)
)
}
}
}
items(
items = playlistPreviews,
contentType = { it }
) { playlistPreview ->
PlaylistPreviewItem(
playlistPreview = playlistPreview,
modifier = Modifier
.padding(all = 8.dp)
.clickable( .clickable(
indication = rememberRipple(bounded = true), indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() } interactionSource = remember { MutableInteractionSource() }
) { ) {
playlistRoute(playlistPreview.playlist.id) isCreatingANewPlaylist = true
} }
) .background(colorPalette.lightBackground)
.size(108.dp)
) {
Image(
painter = painterResource(R.drawable.add),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.size(24.dp)
)
}
} }
} }
}
if (!preferences.isReady) return@LazyColumn items(
items = playlistPreviews,
item { contentType = { it }
Row( ) { playlistPreview ->
verticalAlignment = Alignment.Bottom, PlaylistPreviewItem(
modifier = Modifier playlistPreview = playlistPreview,
.zIndex(1f)
.padding(horizontal = 8.dp)
.padding(top = 32.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier modifier = Modifier
.weight(1f) .padding(all = 8.dp)
.padding(horizontal = 8.dp) .clickable(
) { indication = rememberRipple(bounded = true),
BasicText( interactionSource = remember { MutableInteractionSource() }
text = when (preferences.homePageSongCollection) { ) {
SongCollection.MostPlayed -> "Most played" playlistRoute(playlistPreview.playlist.id)
SongCollection.Favorites -> "Favorites" }
SongCollection.History -> "History" )
}, }
style = typography.m.semiBold, }
modifier = Modifier }
.animateContentSize()
)
val songCollections = enumValues<SongCollection>() item {
val nextSongCollection = songCollections[(preferences.homePageSongCollection.ordinal + 1) % songCollections.size] Row(
verticalAlignment = Alignment.Bottom,
modifier = Modifier
.zIndex(1f)
.padding(horizontal = 8.dp)
.padding(top = 32.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.weight(1f)
.padding(horizontal = 8.dp)
) {
BasicText(
text = when (preferences.homePageSongCollection) {
SongCollection.MostPlayed -> "Most played"
SongCollection.Favorites -> "Favorites"
SongCollection.History -> "History"
},
style = typography.m.semiBold,
modifier = Modifier
.animateContentSize()
)
val songCollections = enumValues<SongCollection>()
val nextSongCollection = songCollections[(preferences.homePageSongCollection.ordinal + 1) % songCollections.size]
BasicText( BasicText(
text = when (nextSongCollection) { text = when (nextSongCollection) {
@@ -321,131 +318,125 @@ fun HomeScreen(
onClick = { onClick = {
preferences.homePageSongCollection = nextSongCollection preferences.homePageSongCollection = nextSongCollection
} }
)
// .alignByBaseline() // .alignByBaseline()
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.animateContentSize() .animateContentSize()
)
}
Image(
painter = painterResource(R.drawable.ellipsis_horizontal),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
menuState.display {
BasicMenu(onDismiss = menuState::hide) {
MenuEntry(
icon = R.drawable.play,
text = "Play",
enabled = songCollection.isNotEmpty(),
onClick = {
menuState.hide()
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
songCollection
.map(SongWithInfo::asMediaItem)
)
}
)
MenuEntry(
icon = R.drawable.shuffle,
text = "Shuffle",
enabled = songCollection.isNotEmpty(),
onClick = {
menuState.hide()
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
songCollection
.shuffled()
.map(SongWithInfo::asMediaItem)
)
}
)
MenuEntry(
icon = R.drawable.time,
text = "Enqueue",
enabled = songCollection.isNotEmpty() && player?.playbackState == Player.STATE_READY,
onClick = {
menuState.hide()
player?.mediaController?.enqueue(
songCollection.map(SongWithInfo::asMediaItem)
)
}
)
}
}
}
.padding(horizontal = 8.dp, vertical = 8.dp)
.size(20.dp)
) )
} }
}
itemsIndexed( Image(
items = songCollection, painter = painterResource(R.drawable.ellipsis_horizontal),
key = { _, song -> contentDescription = null,
song.song.id colorFilter = ColorFilter.tint(colorPalette.text),
}, modifier = Modifier
contentType = { _, song -> song } .clickable {
) { index, song -> menuState.display {
SongItem( BasicMenu(onDismiss = menuState::hide) {
song = song, MenuEntry(
thumbnailSize = thumbnailSize, icon = R.drawable.play,
onClick = { text = "Play",
YoutubePlayer.Radio.reset() enabled = songCollection.isNotEmpty(),
player?.mediaController?.forcePlayAtIndex( onClick = {
songCollection.map(SongWithInfo::asMediaItem), menuState.hide()
index YoutubePlayer.Radio.reset()
) player?.mediaController?.forcePlayFromBeginning(
}, songCollection
menuContent = { .map(SongWithInfo::asMediaItem)
when (preferences.homePageSongCollection) {
SongCollection.MostPlayed -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
SongCollection.Favorites -> InFavoritesMediaItemMenu(song = song)
SongCollection.History -> InHistoryMediaItemMenu(song = song)
}
},
onThumbnailContent = {
AnimatedVisibility(
visible = preferences.homePageSongCollection == SongCollection.MostPlayed,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.align(Alignment.BottomCenter)
) {
BasicText(
text = song.song.formattedTotalPlayTime,
style = typography.xxs.semiBold.center.color(Color.White),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.75f)
) )
), }
shape = ThumbnailRoundness.shape
) )
.padding(horizontal = 8.dp, vertical = 4.dp)
) MenuEntry(
icon = R.drawable.shuffle,
text = "Shuffle",
enabled = songCollection.isNotEmpty(),
onClick = {
menuState.hide()
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayFromBeginning(
songCollection
.shuffled()
.map(SongWithInfo::asMediaItem)
)
}
)
MenuEntry(
icon = R.drawable.time,
text = "Enqueue",
enabled = songCollection.isNotEmpty() && player?.playbackState == Player.STATE_READY,
onClick = {
menuState.hide()
player?.mediaController?.enqueue(
songCollection.map(SongWithInfo::asMediaItem)
)
}
)
}
}
} }
} .padding(horizontal = 8.dp, vertical = 8.dp)
.size(20.dp)
) )
} }
} }
itemsIndexed(
items = songCollection,
key = { _, song ->
song.song.id
},
contentType = { _, song -> song }
) { index, song ->
SongItem(
song = song,
thumbnailSize = thumbnailSize,
onClick = {
YoutubePlayer.Radio.reset()
player?.mediaController?.forcePlayAtIndex(
songCollection.map(SongWithInfo::asMediaItem),
index
)
},
menuContent = {
when (preferences.homePageSongCollection) {
SongCollection.MostPlayed -> NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
SongCollection.Favorites -> InFavoritesMediaItemMenu(song = song)
SongCollection.History -> InHistoryMediaItemMenu(song = song)
}
},
onThumbnailContent = {
AnimatedVisibility(
visible = preferences.homePageSongCollection == SongCollection.MostPlayed,
enter = fadeIn(),
exit = fadeOut(),
modifier = Modifier
.align(Alignment.BottomCenter)
) {
BasicText(
text = song.song.formattedTotalPlayTime,
style = typography.xxs.semiBold.center.color(Color.White),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.75f)
)
),
shape = ThumbnailRoundness.shape
)
.padding(horizontal = 8.dp, vertical = 4.dp)
)
}
}
)
}
} }
} }
PlayerView(
layoutState = rememberBottomSheetState(lowerBound = 64.dp, upperBound = maxHeight),
modifier = Modifier
.align(Alignment.BottomCenter)
)
} }
} }

View File

@@ -73,12 +73,11 @@ fun IntentUriScreen(uri: Uri) {
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window) val shimmer = rememberShimmer(shimmerBounds = ShimmerBounds.Window)
var items by remember(uri) {
var items by remember {
mutableStateOf<Outcome<List<YouTube.Item.Song>>>(Outcome.Loading) mutableStateOf<Outcome<List<YouTube.Item.Song>>>(Outcome.Loading)
} }
val onLoad = relaunchableEffect(Unit) { val onLoad = relaunchableEffect(uri) {
items = withContext(Dispatchers.IO) { items = withContext(Dispatchers.IO) {
uri.getQueryParameter("list")?.let { playlistId -> uri.getQueryParameter("list")?.let { playlistId ->
YouTube.queue(playlistId).toNullable()?.map { songList -> YouTube.queue(playlistId).toNullable()?.map { songList ->
@@ -90,7 +89,7 @@ fun IntentUriScreen(uri: Uri) {
} }
} }
var isImportingAsPlaylist by remember { var isImportingAsPlaylist by remember(uri) {
mutableStateOf(false) mutableStateOf(false)
} }
@@ -246,7 +245,6 @@ fun IntentUriScreen(uri: Uri) {
YouTube.Item.Song::asMediaItem YouTube.Item.Song::asMediaItem
), index ), index
) )
pop()
} }
) )
} }

View File

@@ -1,19 +1,17 @@
package it.vfsfitvnm.vimusic.ui.screens package it.vfsfitvnm.vimusic.ui.screens
import android.net.Uri
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.tween import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.Image import androidx.compose.foundation.*
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -30,6 +28,7 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
@@ -47,11 +46,13 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun SearchScreen( fun SearchScreen(
initialTextInput: String, initialTextInput: String,
onSearch: (String) -> Unit onSearch: (String) -> Unit,
onUri: (Uri) -> Unit,
) { ) {
var textFieldValue by rememberSaveable(initialTextInput, stateSaver = TextFieldValue.Saver) { var textFieldValue by rememberSaveable(initialTextInput, stateSaver = TextFieldValue.Saver) {
mutableStateOf( mutableStateOf(
@@ -107,6 +108,10 @@ fun SearchScreen(
val coroutineScope = rememberCoroutineScope() val coroutineScope = rememberCoroutineScope()
val isOpenableUrl = remember(textFieldValue.text) {
Regex("""https://(music|www)\.youtube.com/(watch|playlist).*""").matches(textFieldValue.text)
}
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
delay(300) delay(300)
focusRequester.requestFocus() focusRequester.requestFocus()
@@ -184,6 +189,40 @@ fun SearchScreen(
) )
} }
if (isOpenableUrl) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = {
onUri(textFieldValue.text.toUri())
}
)
.fillMaxWidth()
.background(colorPalette.lightBackground)
.padding(vertical = 16.dp, horizontal = 8.dp)
) {
Image(
painter = painterResource(R.drawable.link),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.darkGray),
modifier = Modifier
.padding(horizontal = 8.dp)
.size(20.dp)
)
BasicText(
text = "Open URL",
style = typography.s.secondary,
modifier = Modifier
.padding(horizontal = 8.dp)
.weight(1f)
)
}
}
Column( Column(
modifier = Modifier modifier = Modifier
.verticalScroll(rememberScrollState()) .verticalScroll(rememberScrollState())

View File

@@ -9,12 +9,12 @@ import it.vfsfitvnm.route.Route0
import it.vfsfitvnm.route.Route1 import it.vfsfitvnm.route.Route1
@Composable @Composable
fun rememberIntentUriRoute(intentUri: Uri?): Route1<Uri?> { fun rememberIntentUriRoute(): Route1<Uri?> {
val uri = rememberSaveable { val uri = rememberSaveable {
mutableStateOf(intentUri) mutableStateOf<Uri?>(null)
} }
return remember { return remember {
Route1("rememberIntentUriRoute", uri) Route1("IntentUriRoute", uri)
} }
} }

View File

@@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="512"
android:viewportHeight="512">
<path
android:pathData="M200.66,352H144a96,96 0,0 1,0 -192h55.41"
android:strokeLineJoin="round"
android:strokeWidth="48"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M312.59,160H368a96,96 0,0 1,0 192H311.34"
android:strokeLineJoin="round"
android:strokeWidth="48"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
<path
android:pathData="M169.07,256L344.93,256"
android:strokeLineJoin="round"
android:strokeWidth="48"
android:fillColor="#00000000"
android:strokeColor="#000"
android:strokeLineCap="round"/>
</vector>