Rework player view UI (#105)
This commit is contained in:
@@ -81,6 +81,18 @@ interface Database {
|
|||||||
@Query("SELECT * FROM Song WHERE id = :id")
|
@Query("SELECT * FROM Song WHERE id = :id")
|
||||||
fun song(id: String): Flow<Song?>
|
fun song(id: String): Flow<Song?>
|
||||||
|
|
||||||
|
@Query("SELECT likedAt FROM Song WHERE id = :songId")
|
||||||
|
fun likedAt(songId: String): Flow<Long?>
|
||||||
|
|
||||||
|
@Query("UPDATE Song SET likedAt = :likedAt WHERE id = :songId")
|
||||||
|
fun like(songId: String, likedAt: Long?): Int
|
||||||
|
|
||||||
|
@Query("SELECT lyrics FROM Song WHERE id = :songId")
|
||||||
|
fun lyrics(songId: String): Flow<String?>
|
||||||
|
|
||||||
|
@Query("UPDATE Song SET lyrics = :lyrics WHERE id = :songId")
|
||||||
|
fun updateLyrics(songId: String, lyrics: String): Int
|
||||||
|
|
||||||
@Query("SELECT * FROM Artist WHERE id = :id")
|
@Query("SELECT * FROM Artist WHERE id = :id")
|
||||||
fun artist(id: String): Flow<Artist?>
|
fun artist(id: String): Flow<Artist?>
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ fun BottomSheet(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class BottomSheetState(
|
class BottomSheetState(
|
||||||
draggableState: DraggableState,
|
draggableState: DraggableState,
|
||||||
|
|||||||
@@ -7,10 +7,13 @@ import androidx.compose.animation.fadeOut
|
|||||||
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
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -18,6 +21,8 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
@@ -36,8 +41,14 @@ import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
|||||||
import it.vfsfitvnm.vimusic.ui.components.MusicBars
|
import it.vfsfitvnm.vimusic.ui.components.MusicBars
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.SmallSongItemShimmer
|
import it.vfsfitvnm.vimusic.ui.screens.SmallSongItemShimmer
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.*
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LightColorPalette
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
import it.vfsfitvnm.vimusic.utils.PlayerState
|
import it.vfsfitvnm.vimusic.utils.PlayerState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@@ -50,7 +61,7 @@ fun CurrentPlaylistView(
|
|||||||
) {
|
) {
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val hapticFeedback = LocalHapticFeedback.current
|
val hapticFeedback = LocalHapticFeedback.current
|
||||||
val (colorPalette) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
@@ -63,120 +74,169 @@ fun CurrentPlaylistView(
|
|||||||
|
|
||||||
val reorderingState = rememberReorderingState(playerState?.mediaItems ?: emptyList())
|
val reorderingState = rememberReorderingState(playerState?.mediaItems ?: emptyList())
|
||||||
|
|
||||||
LazyColumn(
|
Box {
|
||||||
state = lazyListState,
|
LazyColumn(
|
||||||
modifier = modifier
|
state = lazyListState,
|
||||||
.nestedScroll(remember {
|
contentPadding = PaddingValues(top = 16.dp, bottom = 64.dp),
|
||||||
layoutState.nestedScrollConnection(lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0)
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
})
|
modifier = modifier
|
||||||
) {
|
.nestedScroll(remember {
|
||||||
itemsIndexed(
|
layoutState.nestedScrollConnection(lazyListState.firstVisibleItemIndex == 0 && lazyListState.firstVisibleItemScrollOffset == 0)
|
||||||
items = playerState?.mediaItems ?: emptyList()
|
})
|
||||||
) { index, mediaItem ->
|
) {
|
||||||
val isPlayingThisMediaItem by derivedStateOf {
|
itemsIndexed(
|
||||||
playerState?.mediaItemIndex == index
|
items = playerState?.mediaItems ?: emptyList()
|
||||||
}
|
) { index, mediaItem ->
|
||||||
|
val isPlayingThisMediaItem by derivedStateOf {
|
||||||
|
playerState?.mediaItemIndex == index
|
||||||
|
}
|
||||||
|
|
||||||
SongItem(
|
SongItem(
|
||||||
mediaItem = mediaItem,
|
mediaItem = mediaItem,
|
||||||
thumbnailSize = thumbnailSize,
|
thumbnailSize = thumbnailSize,
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isPlayingThisMediaItem) {
|
if (isPlayingThisMediaItem) {
|
||||||
if (isPaused) {
|
|
||||||
binder?.player?.play()
|
|
||||||
} else {
|
|
||||||
binder?.player?.pause()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
binder?.player?.playWhenReady = true
|
|
||||||
binder?.player?.seekToDefaultPosition(index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
QueuedMediaItemMenu(
|
|
||||||
mediaItem = mediaItem,
|
|
||||||
indexInQueue = if (isPlayingThisMediaItem) null else index,
|
|
||||||
onGlobalRouteEmitted = onGlobalRouteEmitted
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onThumbnailContent = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = isPlayingThisMediaItem,
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut(),
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
color = Color.Black.copy(alpha = 0.25f),
|
|
||||||
shape = ThumbnailRoundness.shape
|
|
||||||
)
|
|
||||||
.size(Dimensions.thumbnails.song)
|
|
||||||
) {
|
|
||||||
if (isPaused) {
|
if (isPaused) {
|
||||||
Image(
|
binder?.player?.play()
|
||||||
painter = painterResource(R.drawable.play),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(LightColorPalette.background),
|
|
||||||
modifier = Modifier
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
MusicBars(
|
binder?.player?.pause()
|
||||||
color = LightColorPalette.background,
|
}
|
||||||
modifier = Modifier
|
} else {
|
||||||
.height(24.dp)
|
binder?.player?.playWhenReady = true
|
||||||
)
|
binder?.player?.seekToDefaultPosition(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuContent = {
|
||||||
|
QueuedMediaItemMenu(
|
||||||
|
mediaItem = mediaItem,
|
||||||
|
indexInQueue = if (isPlayingThisMediaItem) null else index,
|
||||||
|
onGlobalRouteEmitted = onGlobalRouteEmitted
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onThumbnailContent = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isPlayingThisMediaItem,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color = Color.Black.copy(alpha = 0.25f),
|
||||||
|
shape = ThumbnailRoundness.shape
|
||||||
|
)
|
||||||
|
.size(Dimensions.thumbnails.song)
|
||||||
|
) {
|
||||||
|
if (isPaused) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.play),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(LightColorPalette.background),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
MusicBars(
|
||||||
|
color = LightColorPalette.background,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
},
|
trailingContent = {
|
||||||
trailingContent = {
|
Image(
|
||||||
Image(
|
painter = painterResource(R.drawable.reorder),
|
||||||
painter = painterResource(R.drawable.reorder),
|
contentDescription = null,
|
||||||
contentDescription = null,
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
backgroundColor = colorPalette.elevatedBackground,
|
|
||||||
modifier = Modifier
|
|
||||||
.verticalDragAfterLongPressToReorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index,
|
|
||||||
onDragStart = {
|
|
||||||
hapticFeedback.performHapticFeedback(
|
|
||||||
HapticFeedbackType.LongPress
|
|
||||||
)
|
|
||||||
},
|
|
||||||
onDragEnd = { reachedIndex ->
|
|
||||||
binder?.player?.moveMediaItem(index, reachedIndex)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
if (binder?.isLoadingRadio == true) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.shimmer()
|
|
||||||
) {
|
|
||||||
repeat(3) { index ->
|
|
||||||
SmallSongItemShimmer(
|
|
||||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.alpha(1f - index * 0.125f)
|
.clickable {}
|
||||||
.fillMaxWidth()
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
.padding(vertical = 4.dp, horizontal = 16.dp)
|
.size(20.dp)
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
backgroundColor = colorPalette.background,
|
||||||
|
modifier = Modifier
|
||||||
|
.verticalDragAfterLongPressToReorder(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
index = index,
|
||||||
|
onDragStart = {
|
||||||
|
hapticFeedback.performHapticFeedback(
|
||||||
|
HapticFeedbackType.LongPress
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDragEnd = { reachedIndex ->
|
||||||
|
binder?.player?.moveMediaItem(index, reachedIndex)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
if (binder?.isLoadingRadio == true) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.shimmer()
|
||||||
|
) {
|
||||||
|
repeat(3) { index ->
|
||||||
|
SmallSongItemShimmer(
|
||||||
|
thumbnailSizeDp = Dimensions.thumbnails.song,
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(1f - index * 0.125f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(vertical = 4.dp, horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterHorizontally),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = layoutState.collapse
|
||||||
|
)
|
||||||
|
.shadow(elevation = 8.dp)
|
||||||
|
.height(64.dp)
|
||||||
|
.background(colorPalette.elevatedBackground)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.chevron_up),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.rotate(180f)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
BasicText(
|
||||||
|
text = "Queue",
|
||||||
|
style = typography.s.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = "${playerState?.mediaItems?.size ?: 0} songs",
|
||||||
|
style = typography.xxs.semiBold.secondary,
|
||||||
|
modifier = Modifier
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,126 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.views
|
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.valentinilk.shimmer.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LyricsView(
|
|
||||||
lyrics: String?,
|
|
||||||
onInitialize: () -> Unit,
|
|
||||||
onSearchOnline: () -> Unit,
|
|
||||||
onLyricsUpdate: (String) -> Unit,
|
|
||||||
nestedScrollConnectionProvider: () -> NestedScrollConnection,
|
|
||||||
) {
|
|
||||||
val (_, typography) = LocalAppearance.current
|
|
||||||
|
|
||||||
var isEditingLyrics by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isEditingLyrics) {
|
|
||||||
TextFieldDialog(
|
|
||||||
hintText = "Enter the lyrics",
|
|
||||||
initialTextInput = lyrics ?: "",
|
|
||||||
singleLine = false,
|
|
||||||
maxLines = 10,
|
|
||||||
isTextInputValid = { true },
|
|
||||||
onDismiss = {
|
|
||||||
isEditingLyrics = false
|
|
||||||
},
|
|
||||||
onDone = onLyricsUpdate
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lyrics != null ) {
|
|
||||||
Column(
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 64.dp)
|
|
||||||
.nestedScroll(remember { nestedScrollConnectionProvider() })
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(vertical = 16.dp)
|
|
||||||
.padding(horizontal = 48.dp)
|
|
||||||
) {
|
|
||||||
if (lyrics.isEmpty()) {
|
|
||||||
TextCard(icon = R.drawable.sad) {
|
|
||||||
Title(text = "Lyrics not available")
|
|
||||||
Text(text = "...but you can always search them online and add them here!")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BasicText(
|
|
||||||
text = lyrics,
|
|
||||||
style = typography.xs.center,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 32.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Search online",
|
|
||||||
style = typography.xs.secondary.copy(textDecoration = TextDecoration.Underline),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = onSearchOnline
|
|
||||||
)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = "Edit",
|
|
||||||
style = typography.xs.secondary.copy(textDecoration = TextDecoration.Underline),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = null,
|
|
||||||
interactionSource = remember { MutableInteractionSource() }
|
|
||||||
) {
|
|
||||||
isEditingLyrics = true
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
SideEffect(onInitialize)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.shimmer()
|
|
||||||
) {
|
|
||||||
repeat(16) { index ->
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(1f - index * 0.05f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +1,22 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.views
|
package it.vfsfitvnm.vimusic.ui.views
|
||||||
|
|
||||||
import android.app.SearchManager
|
|
||||||
import android.content.Intent
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.material.ripple.rememberRipple
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.models.Song
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.HorizontalTabPager
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.rememberTabPagerState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.utils.PlayerState
|
import it.vfsfitvnm.vimusic.utils.PlayerState
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@@ -44,197 +24,89 @@ import kotlinx.coroutines.withContext
|
|||||||
fun PlayerBottomSheet(
|
fun PlayerBottomSheet(
|
||||||
playerState: PlayerState?,
|
playerState: PlayerState?,
|
||||||
layoutState: BottomSheetState,
|
layoutState: BottomSheetState,
|
||||||
padding: Dp,
|
onShowLyrics: () -> Unit,
|
||||||
song: Song?,
|
onShowStatsForNerds: () -> Unit,
|
||||||
onGlobalRouteEmitted: () -> Unit,
|
onGlobalRouteEmitted: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
|
|
||||||
val tabPagerState = rememberTabPagerState(initialPageIndex = 0, pageCount = 2)
|
|
||||||
|
|
||||||
var nextResult by remember(playerState?.mediaItem?.mediaId) {
|
|
||||||
mutableStateOf<Result<YouTube.NextResult>?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
BottomSheet(
|
BottomSheet(
|
||||||
state = layoutState,
|
state = layoutState,
|
||||||
peekHeight = padding,
|
|
||||||
elevation = 16.dp,
|
elevation = 16.dp,
|
||||||
shape = RoundedCornerShape(topStart = 8.dp, topEnd = 8.dp),
|
modifier = modifier,
|
||||||
handleOutsideInteractionsWhenExpanded = true,
|
|
||||||
modifier = modifier
|
|
||||||
.padding(bottom = padding),
|
|
||||||
collapsedContent = {
|
collapsedContent = {
|
||||||
Column(
|
Row(
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = 1f - (layoutState.progress * 16).coerceAtMost(1f)
|
||||||
|
}
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.height(layoutState.lowerBound)
|
.height(layoutState.lowerBound)
|
||||||
.background(colorPalette.elevatedBackground)
|
.background(colorPalette.background)
|
||||||
) {
|
) {
|
||||||
Spacer(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterHorizontally)
|
.padding(horizontal = 8.dp)
|
||||||
.background(
|
) {
|
||||||
color = colorPalette.textDisabled,
|
Spacer(
|
||||||
shape = RoundedCornerShape(16.dp)
|
modifier = Modifier
|
||||||
)
|
.padding(all = 8.dp)
|
||||||
.width(36.dp)
|
.size(20.dp)
|
||||||
.height(4.dp)
|
)
|
||||||
.padding(top = 8.dp)
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.chevron_up),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.size(18.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
) {
|
) {
|
||||||
@Composable
|
Image(
|
||||||
fun Element(
|
painter = painterResource(R.drawable.text),
|
||||||
text: String,
|
contentDescription = null,
|
||||||
pageIndex: Int
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
) {
|
modifier = Modifier
|
||||||
val color by animateColorAsState(
|
.clickable(onClick = onShowLyrics)
|
||||||
if (tabPagerState.pageIndex == pageIndex) {
|
.padding(all = 8.dp)
|
||||||
colorPalette.text
|
.size(20.dp)
|
||||||
} else {
|
|
||||||
colorPalette.textDisabled
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
|
||||||
if (pageIndex == pageIndex) {
|
|
||||||
1f
|
|
||||||
} else {
|
|
||||||
0.9f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = text,
|
|
||||||
style = typography.xs.medium.color(color).center,
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = {
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
layoutState.expand()
|
|
||||||
if (layoutState.isCollapsed) {
|
|
||||||
tabPagerState.pageIndex = pageIndex
|
|
||||||
} else {
|
|
||||||
tabPagerState.animateScrollTo(pageIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(vertical = 8.dp)
|
|
||||||
.scale(scale)
|
|
||||||
.weight(1f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Element(
|
|
||||||
text = "UP NEXT",
|
|
||||||
pageIndex = 0
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Element(
|
Image(
|
||||||
text = "LYRICS",
|
painter = painterResource(R.drawable.information),
|
||||||
pageIndex = 1
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(onClick = onShowStatsForNerds)
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.size(20.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
HorizontalTabPager(
|
CurrentPlaylistView(
|
||||||
state = tabPagerState,
|
playerState = playerState,
|
||||||
|
layoutState = layoutState,
|
||||||
|
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(colorPalette.elevatedBackground)
|
.background(colorPalette.background)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) { index ->
|
)
|
||||||
when (index) {
|
|
||||||
0 -> {
|
|
||||||
CurrentPlaylistView(
|
|
||||||
playerState = playerState,
|
|
||||||
layoutState = layoutState,
|
|
||||||
onGlobalRouteEmitted = onGlobalRouteEmitted,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 64.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
1 -> {
|
|
||||||
val player = LocalPlayerServiceBinder.current?.player
|
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var lyricsResult by remember(song) {
|
|
||||||
mutableStateOf(song?.lyrics?.let { Result.success(it) })
|
|
||||||
}
|
|
||||||
|
|
||||||
LyricsView(
|
|
||||||
lyrics = lyricsResult?.getOrNull(),
|
|
||||||
nestedScrollConnectionProvider = layoutState::nestedScrollConnection,
|
|
||||||
onInitialize = {
|
|
||||||
coroutineScope.launch(Dispatchers.Main) {
|
|
||||||
val mediaItem = player?.currentMediaItem!!
|
|
||||||
|
|
||||||
if (nextResult == null) {
|
|
||||||
val mediaItemIndex = player.currentMediaItemIndex
|
|
||||||
|
|
||||||
nextResult = withContext(Dispatchers.IO) {
|
|
||||||
YouTube.next(
|
|
||||||
mediaItem.mediaId,
|
|
||||||
mediaItem.mediaMetadata.extras?.getString("playlistId"),
|
|
||||||
mediaItemIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
lyricsResult = nextResult?.map { nextResult ->
|
|
||||||
nextResult.lyrics?.text()?.getOrNull() ?: ""
|
|
||||||
}?.map { lyrics ->
|
|
||||||
query {
|
|
||||||
song?.let {
|
|
||||||
Database.update(song.copy(lyrics = lyrics))
|
|
||||||
} ?: Database.insert(mediaItem) { song ->
|
|
||||||
song.copy(lyrics = lyrics)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lyrics
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSearchOnline = {
|
|
||||||
val mediaMetadata = player?.mediaMetadata ?: return@LyricsView
|
|
||||||
|
|
||||||
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
|
||||||
putExtra(SearchManager.QUERY, "${mediaMetadata.title} ${mediaMetadata.artist} lyrics")
|
|
||||||
}
|
|
||||||
|
|
||||||
if (intent.resolveActivity(context.packageManager) != null) {
|
|
||||||
context.startActivity(intent)
|
|
||||||
} else {
|
|
||||||
Toast.makeText(context, "No browser app found!", Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onLyricsUpdate = { lyrics ->
|
|
||||||
val mediaItem = player?.currentMediaItem
|
|
||||||
query {
|
|
||||||
song?.let {
|
|
||||||
Database.update(song.copy(lyrics = lyrics))
|
|
||||||
} ?: mediaItem?.let {
|
|
||||||
Database.insert(mediaItem) { song ->
|
|
||||||
song.copy(lyrics = lyrics)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else -> {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.views
|
package it.vfsfitvnm.vimusic.ui.views
|
||||||
|
|
||||||
|
import android.app.SearchManager
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.media.audiofx.AudioEffect
|
import android.media.audiofx.AudioEffect
|
||||||
@@ -9,9 +10,7 @@ import android.widget.Toast
|
|||||||
import androidx.activity.compose.LocalActivityResultRegistryOwner
|
import androidx.activity.compose.LocalActivityResultRegistryOwner
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.*
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -23,23 +22,28 @@ import androidx.compose.runtime.*
|
|||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.*
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
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.compose.ui.unit.dp
|
||||||
import androidx.media3.common.*
|
import androidx.media3.common.C
|
||||||
|
import androidx.media3.common.MediaMetadata
|
||||||
|
import androidx.media3.common.Player
|
||||||
import androidx.media3.datasource.cache.Cache
|
import androidx.media3.datasource.cache.Cache
|
||||||
import androidx.media3.datasource.cache.CacheSpan
|
import androidx.media3.datasource.cache.CacheSpan
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
@@ -47,16 +51,13 @@ import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|||||||
import it.vfsfitvnm.vimusic.models.Song
|
import it.vfsfitvnm.vimusic.models.Song
|
||||||
import it.vfsfitvnm.vimusic.query
|
import it.vfsfitvnm.vimusic.query
|
||||||
import it.vfsfitvnm.vimusic.ui.components.*
|
import it.vfsfitvnm.vimusic.ui.components.*
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.BaseMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.*
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
import it.vfsfitvnm.vimusic.ui.styling.*
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.BlackColorPalette
|
|
||||||
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.utils.*
|
import it.vfsfitvnm.vimusic.utils.*
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@@ -170,9 +171,13 @@ fun PlayerView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
val song by remember(playerState.mediaItem.mediaId) {
|
var isShowingLyrics by rememberSaveable {
|
||||||
playerState.mediaItem.mediaId.let(Database::song).distinctUntilChanged()
|
mutableStateOf(false)
|
||||||
}.collectAsState(initial = null, context = Dispatchers.IO)
|
}
|
||||||
|
|
||||||
|
var isShowingStatsForNerds by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
when (configuration.orientation) {
|
when (configuration.orientation) {
|
||||||
Configuration.ORIENTATION_LANDSCAPE -> {
|
Configuration.ORIENTATION_LANDSCAPE -> {
|
||||||
@@ -190,12 +195,17 @@ fun PlayerView(
|
|||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
.padding(bottom = 16.dp)
|
.padding(bottom = 16.dp)
|
||||||
) {
|
) {
|
||||||
Thumbnail()
|
Thumbnail(
|
||||||
|
isShowingLyrics = isShowingLyrics,
|
||||||
|
onShowLyrics = { isShowingLyrics = it },
|
||||||
|
isShowingStatsForNerds = isShowingStatsForNerds,
|
||||||
|
onShowStatsForNerds = { isShowingStatsForNerds = it },
|
||||||
|
nestedScrollConnectionProvider = layoutState::nestedScrollConnection,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Controls(
|
Controls(
|
||||||
playerState = playerState,
|
playerState = playerState,
|
||||||
song = song,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
@@ -217,12 +227,17 @@ fun PlayerView(
|
|||||||
.weight(1.25f)
|
.weight(1.25f)
|
||||||
.padding(horizontal = 32.dp, vertical = 8.dp)
|
.padding(horizontal = 32.dp, vertical = 8.dp)
|
||||||
) {
|
) {
|
||||||
Thumbnail()
|
Thumbnail(
|
||||||
|
isShowingLyrics = isShowingLyrics,
|
||||||
|
onShowLyrics = { isShowingLyrics = it },
|
||||||
|
isShowingStatsForNerds = isShowingStatsForNerds,
|
||||||
|
onShowStatsForNerds = { isShowingStatsForNerds = it },
|
||||||
|
nestedScrollConnectionProvider = layoutState::nestedScrollConnection,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Controls(
|
Controls(
|
||||||
playerState = playerState,
|
playerState = playerState,
|
||||||
song = song,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp)
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -297,10 +312,16 @@ fun PlayerView(
|
|||||||
|
|
||||||
PlayerBottomSheet(
|
PlayerBottomSheet(
|
||||||
playerState = playerState,
|
playerState = playerState,
|
||||||
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound * 0.9f),
|
layoutState = rememberBottomSheetState(64.dp, layoutState.upperBound),
|
||||||
|
onShowLyrics = {
|
||||||
|
isShowingStatsForNerds = false
|
||||||
|
isShowingLyrics = !isShowingLyrics
|
||||||
|
},
|
||||||
|
onShowStatsForNerds = {
|
||||||
|
isShowingLyrics = false
|
||||||
|
isShowingStatsForNerds = !isShowingStatsForNerds
|
||||||
|
},
|
||||||
onGlobalRouteEmitted = layoutState.collapse,
|
onGlobalRouteEmitted = layoutState.collapse,
|
||||||
padding = layoutState.upperBound * 0.1f,
|
|
||||||
song = song,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.BottomCenter)
|
.align(Alignment.BottomCenter)
|
||||||
)
|
)
|
||||||
@@ -310,6 +331,11 @@ fun PlayerView(
|
|||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
private fun Thumbnail(
|
private fun Thumbnail(
|
||||||
|
isShowingLyrics: Boolean,
|
||||||
|
onShowLyrics: (Boolean) -> Unit,
|
||||||
|
isShowingStatsForNerds: Boolean,
|
||||||
|
onShowStatsForNerds: (Boolean) -> Unit,
|
||||||
|
nestedScrollConnectionProvider: () -> NestedScrollConnection,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
@@ -323,10 +349,6 @@ private fun Thumbnail(
|
|||||||
|
|
||||||
val error by rememberError(player)
|
val error by rememberError(player)
|
||||||
|
|
||||||
var isShowingStatsForNerds by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error == null) {
|
if (error == null) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = mediaItemIndex,
|
targetState = mediaItemIndex,
|
||||||
@@ -339,6 +361,7 @@ private fun Thumbnail(
|
|||||||
SizeTransform(clip = false)
|
SizeTransform(clip = false)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
) { currentMediaItemIndex ->
|
) { currentMediaItemIndex ->
|
||||||
@@ -358,20 +381,44 @@ private fun Thumbnail(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.pointerInput(Unit) {
|
.pointerInput(Unit) {
|
||||||
detectTapGestures(
|
detectTapGestures(
|
||||||
|
onTap = {
|
||||||
|
onShowLyrics(true)
|
||||||
|
},
|
||||||
onLongPress = {
|
onLongPress = {
|
||||||
isShowingStatsForNerds = true
|
onShowStatsForNerds(true)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Lyrics(
|
||||||
|
mediaId = mediaItem.mediaId,
|
||||||
|
isDisplayed = isShowingLyrics,
|
||||||
|
onDismiss = {
|
||||||
|
onShowLyrics(false)
|
||||||
|
},
|
||||||
|
onLyricsUpdate = { mediaId, lyrics ->
|
||||||
|
if (Database.updateLyrics(mediaId, lyrics) == 0) {
|
||||||
|
if (mediaId == mediaItem.mediaId) {
|
||||||
|
Database.insert(mediaItem) { song ->
|
||||||
|
song.copy(lyrics = lyrics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
size = thumbnailSizeDp,
|
||||||
|
mediaMetadataProvider = mediaItem::mediaMetadata,
|
||||||
|
nestedScrollConnectionProvider = nestedScrollConnectionProvider,
|
||||||
|
)
|
||||||
|
|
||||||
StatsForNerds(
|
StatsForNerds(
|
||||||
mediaId = mediaItem.mediaId,
|
mediaId = mediaItem.mediaId,
|
||||||
isDisplayed = isShowingStatsForNerds,
|
isDisplayed = isShowingStatsForNerds,
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
isShowingStatsForNerds = false
|
onShowStatsForNerds(false)
|
||||||
}
|
},
|
||||||
|
modifier = Modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,6 +441,203 @@ private fun Thumbnail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Lyrics(
|
||||||
|
mediaId: String,
|
||||||
|
isDisplayed: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
size: Dp,
|
||||||
|
mediaMetadataProvider: () -> MediaMetadata,
|
||||||
|
onLyricsUpdate: (String, String) -> Unit,
|
||||||
|
nestedScrollConnectionProvider: () -> NestedScrollConnection,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (_, typography) = LocalAppearance.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = isDisplayed,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
var isLoading by remember(mediaId) {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isEditingLyrics by remember(mediaId) {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
val lyrics by remember(mediaId) {
|
||||||
|
Database.lyrics(mediaId).distinctUntilChanged().map flowMap@{ lyrics ->
|
||||||
|
if (lyrics != null) return@flowMap lyrics
|
||||||
|
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
YouTube.next(mediaId, null)?.map { nextResult ->
|
||||||
|
nextResult.lyrics?.text()?.map { newLyrics ->
|
||||||
|
onLyricsUpdate(mediaId, newLyrics ?: "")
|
||||||
|
isLoading = false
|
||||||
|
return@flowMap newLyrics ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading = false
|
||||||
|
null
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
}.collectAsState(initial = ".", context = Dispatchers.IO)
|
||||||
|
|
||||||
|
if (isEditingLyrics) {
|
||||||
|
TextFieldDialog(
|
||||||
|
hintText = "Enter the lyrics",
|
||||||
|
initialTextInput = lyrics ?: "",
|
||||||
|
singleLine = false,
|
||||||
|
maxLines = 10,
|
||||||
|
isTextInputValid = { true },
|
||||||
|
onDismiss = {
|
||||||
|
isEditingLyrics = false
|
||||||
|
},
|
||||||
|
onDone = {
|
||||||
|
query {
|
||||||
|
Database.updateLyrics(mediaId, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onTap = {
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Black.copy(0.8f))
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !isLoading && lyrics == null,
|
||||||
|
enter = slideInVertically { -it },
|
||||||
|
exit = slideOutVertically { -it },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred while fetching the lyrics",
|
||||||
|
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color.Black.copy(0.4f))
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = lyrics?.let(String::isEmpty) ?: false,
|
||||||
|
enter = slideInVertically { -it },
|
||||||
|
exit = slideOutVertically { -it },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopCenter)
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Lyrics are not available for this song",
|
||||||
|
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(Color.Black.copy(0.4f))
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = Modifier
|
||||||
|
.shimmer()
|
||||||
|
) {
|
||||||
|
repeat(4) { index ->
|
||||||
|
TextPlaceholder(
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(1f - index * 0.05f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
lyrics?.let { lyrics ->
|
||||||
|
if (lyrics.isNotEmpty()) {
|
||||||
|
BasicText(
|
||||||
|
text = lyrics,
|
||||||
|
style = typography.xs.center.medium.color(BlackColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(remember { nestedScrollConnectionProvider() })
|
||||||
|
.verticalFadingEdge()
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
.padding(vertical = size / 4, horizontal = 32.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.size(size)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.search),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(DarkColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.clickable {
|
||||||
|
val mediaMetadata = mediaMetadataProvider()
|
||||||
|
|
||||||
|
val intent = Intent(Intent.ACTION_WEB_SEARCH).apply {
|
||||||
|
putExtra(
|
||||||
|
SearchManager.QUERY,
|
||||||
|
"${mediaMetadata.title} ${mediaMetadata.artist} lyrics"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (intent.resolveActivity(context.packageManager) != null) {
|
||||||
|
context.startActivity(intent)
|
||||||
|
} else {
|
||||||
|
Toast
|
||||||
|
.makeText(
|
||||||
|
context,
|
||||||
|
"No browser app found!",
|
||||||
|
Toast.LENGTH_SHORT
|
||||||
|
)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.pencil),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(DarkColorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.clickable {
|
||||||
|
isEditingLyrics = true
|
||||||
|
}
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StatsForNerds(
|
private fun StatsForNerds(
|
||||||
mediaId: String,
|
mediaId: String,
|
||||||
@@ -465,50 +709,50 @@ private fun StatsForNerds(
|
|||||||
Column {
|
Column {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Id",
|
text = "Id",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Volume",
|
text = "Volume",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Loudness",
|
text = "Loudness",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Bitrate",
|
text = "Bitrate",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Size",
|
text = "Size",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "Cached",
|
text = "Cached",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = mediaId,
|
text = mediaId,
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "${volume.times(100).roundToInt()}%",
|
text = "${volume.times(100).roundToInt()}%",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = format?.loudnessDb?.let { loudnessDb ->
|
text = format?.loudnessDb?.let { loudnessDb ->
|
||||||
"%.2f dB".format(loudnessDb)
|
"%.2f dB".format(loudnessDb)
|
||||||
} ?: "Unknown",
|
} ?: "Unknown",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = format?.bitrate?.let { bitrate ->
|
text = format?.bitrate?.let { bitrate ->
|
||||||
"${bitrate / 1000} kbps"
|
"${bitrate / 1000} kbps"
|
||||||
} ?: "Unknown",
|
} ?: "Unknown",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = format?.contentLength?.let { contentLength ->
|
text = format?.contentLength?.let { contentLength ->
|
||||||
@@ -517,7 +761,7 @@ private fun StatsForNerds(
|
|||||||
contentLength
|
contentLength
|
||||||
)
|
)
|
||||||
} ?: "Unknown",
|
} ?: "Unknown",
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
BasicText(
|
BasicText(
|
||||||
text = buildString {
|
text = buildString {
|
||||||
@@ -527,7 +771,7 @@ private fun StatsForNerds(
|
|||||||
append(" (${(cachedBytes.toFloat() / contentLength * 100).roundToInt()}%)")
|
append(" (${(cachedBytes.toFloat() / contentLength * 100).roundToInt()}%)")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
style = typography.xs.semiBold.color(BlackColorPalette.text)
|
style = typography.xs.medium.color(BlackColorPalette.text)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,7 +779,7 @@ private fun StatsForNerds(
|
|||||||
if (format != null && format?.itag == null) {
|
if (format != null && format?.itag == null) {
|
||||||
BasicText(
|
BasicText(
|
||||||
text = "FETCH MISSING DATA",
|
text = "FETCH MISSING DATA",
|
||||||
style = typography.xxs.semiBold.color(BlackColorPalette.text),
|
style = typography.xxs.medium.color(BlackColorPalette.text),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable(
|
.clickable(
|
||||||
indication = rememberRipple(bounded = true),
|
indication = rememberRipple(bounded = true),
|
||||||
@@ -579,18 +823,22 @@ private fun StatsForNerds(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun Controls(
|
private fun Controls(
|
||||||
playerState: PlayerState,
|
playerState: PlayerState,
|
||||||
song: Song?,
|
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val player = binder?.player ?: return
|
val player = binder?.player ?: return
|
||||||
|
val mediaId = playerState.mediaItem?.mediaId ?: return
|
||||||
|
|
||||||
var scrubbingPosition by remember(playerState.mediaItemIndex) {
|
var scrubbingPosition by remember(playerState.mediaItemIndex) {
|
||||||
mutableStateOf<Long?>(null)
|
mutableStateOf<Long?>(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val likedAt by remember(mediaId) {
|
||||||
|
Database.likedAt(mediaId).distinctUntilChanged()
|
||||||
|
}.collectAsState(initial = null, context = Dispatchers.IO)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -688,15 +936,17 @@ private fun Controls(
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(R.drawable.heart),
|
painter = painterResource(R.drawable.heart),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
colorFilter = ColorFilter.tint(
|
colorFilter = ColorFilter.tint(if (likedAt != null) colorPalette.red else colorPalette.textDisabled),
|
||||||
song?.likedAt?.let { colorPalette.red } ?: colorPalette.textDisabled
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clickable {
|
.clickable {
|
||||||
query {
|
query {
|
||||||
song?.let { song ->
|
if (Database.like(
|
||||||
Database.update(song.toggleLike())
|
mediaId,
|
||||||
} ?: Database.insert(playerState.mediaItem!!, Song::toggleLike)
|
if (likedAt == null) System.currentTimeMillis() else null
|
||||||
|
) == 0
|
||||||
|
) {
|
||||||
|
Database.insert(playerState.mediaItem, Song::toggleLike)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
@@ -797,4 +1047,4 @@ private fun Controls(
|
|||||||
.weight(1f)
|
.weight(1f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
|
||||||
|
fun Modifier.verticalFadingEdge() =
|
||||||
|
graphicsLayer(alpha = 0.99f)
|
||||||
|
.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
brush = Brush.verticalGradient(
|
||||||
|
listOf(
|
||||||
|
Color.Transparent,
|
||||||
|
Color.Black, Color.Black, Color.Black,
|
||||||
|
Color.Transparent
|
||||||
|
)
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.DstIn
|
||||||
|
)
|
||||||
|
}
|
||||||
13
app/src/main/res/drawable-up/chevron_up.xml
Normal file
13
app/src/main/res/drawable-up/chevron_up.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<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="M112,328l144,-144l144,144"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="48"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
13
app/src/main/res/drawable/chevron_up.xml
Normal file
13
app/src/main/res/drawable/chevron_up.xml
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<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="M112,328l144,-144l144,144"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="48"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
||||||
12
app/src/main/res/drawable/text.xml
Normal file
12
app/src/main/res/drawable/text.xml
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="512"
|
||||||
|
android:viewportHeight="512">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M292.6,407.78l-120,-320a22,22 0,0 0,-41.2 0l-120,320a22,22 0,0 0,41.2 15.44L88.76,326.8a2,2 0,0 1,1.87 -1.3L213.37,325.5a2,2 0,0 1,1.87 1.3l36.16,96.42a22,22 0,0 0,41.2 -15.44ZM106.76,278.78 L150.13,163.13a2,2 0,0 1,3.74 0L197.24,278.8a2,2 0,0 1,-1.87 2.7L108.63,281.5A2,2 0,0 1,106.76 278.8Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M400.77,169.5c-41.72,-0.3 -79.08,23.87 -95,61.4a22,22 0,0 0,40.5 17.2c8.88,-20.89 29.77,-34.44 53.32,-34.6C431.91,213.28 458,240 458,272.35h0a1.5,1.5 0,0 1,-1.45 1.5c-21.92,0.61 -47.92,2.07 -71.12,4.8C330.68,285.09 298,314.94 298,358.5c0,23.19 8.76,44 24.67,58.68C337.6,430.93 358,438.5 380,438.5c31,0 57.69,-8 77.94,-23.22 0,0 0.06,0 0.06,0h0a22,22 0,1 0,44 0.19v-143C502,216.29 457,169.91 400.77,169.5ZM380,394.5c-17.53,0 -38,-9.43 -38,-36 0,-10.67 3.83,-18.14 12.43,-24.23 8.37,-5.93 21.2,-10.16 36.14,-11.92 21.12,-2.49 44.82,-3.86 65.14,-4.47a2,2 0,0 1,2 2.1C455,370.1 429.46,394.5 380,394.5Z"/>
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user