Start UI redesign (#172)
This commit is contained in:
parent
b0e5344560
commit
563c6175f7
20 changed files with 1219 additions and 525 deletions
|
@ -97,4 +97,6 @@ dependencies {
|
||||||
implementation(projects.kugou)
|
implementation(projects.kugou)
|
||||||
|
|
||||||
coreLibraryDesugaring(libs.desugaring)
|
coreLibraryDesugaring(libs.desugaring)
|
||||||
|
|
||||||
|
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
|
||||||
}
|
}
|
||||||
|
|
|
@ -386,7 +386,7 @@ abstract class DatabaseInitializer protected constructor() : RoomDatabase() {
|
||||||
lateinit var Instance: DatabaseInitializer
|
lateinit var Instance: DatabaseInitializer
|
||||||
|
|
||||||
context(Context)
|
context(Context)
|
||||||
operator fun invoke() {
|
operator fun invoke() {
|
||||||
if (!::Instance.isInitialized) {
|
if (!::Instance.isInitialized) {
|
||||||
Instance = Room
|
Instance = Room
|
||||||
.databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db")
|
.databaseBuilder(this@Context, DatabaseInitializer::class.java, "data.db")
|
||||||
|
|
|
@ -0,0 +1,105 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateColor
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.updateTransition
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.layout.layout
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TabColumn(
|
||||||
|
tabIndex: Int,
|
||||||
|
onTabIndexChanged: (Int) -> Unit,
|
||||||
|
selectedTextColor: Color,
|
||||||
|
disabledTextColor: Color,
|
||||||
|
textStyle: TextStyle,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
val transition = updateTransition(targetState = tabIndex, label = null)
|
||||||
|
|
||||||
|
content { index, text, icon ->
|
||||||
|
val dothAlpha by transition.animateFloat(label = "") {
|
||||||
|
if (it == index) 1f else 0f
|
||||||
|
}
|
||||||
|
|
||||||
|
val textColor by transition.animateColor(label = "") {
|
||||||
|
if (it == index) selectedTextColor else disabledTextColor
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onTabIndexChanged(index) }
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(icon),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(selectedTextColor),
|
||||||
|
modifier = Modifier
|
||||||
|
.vertical()
|
||||||
|
.graphicsLayer {
|
||||||
|
alpha = dothAlpha
|
||||||
|
translationX = (1f - dothAlpha) * -48.dp.toPx()
|
||||||
|
rotationZ = -90f
|
||||||
|
}
|
||||||
|
.size(12.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = text,
|
||||||
|
style = textStyle.copy(color = textColor),
|
||||||
|
modifier = Modifier
|
||||||
|
.vertical()
|
||||||
|
.rotate(-90f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.vertical() =
|
||||||
|
layout { measurable, constraints ->
|
||||||
|
val placeable = measurable.measure(constraints)
|
||||||
|
layout(placeable.height, placeable.width) {
|
||||||
|
placeable.place(
|
||||||
|
x = -(placeable.width / 2 - placeable.height / 2),
|
||||||
|
y = -(placeable.height / 2 - placeable.width / 2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,148 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedContentScope
|
||||||
|
import androidx.compose.animation.AnimatedVisibilityScope
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.VisibilityThreshold
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.animation.with
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
|
||||||
|
@SuppressLint("ModifierParameter")
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun Scaffold(
|
||||||
|
topIconButtonId: Int,
|
||||||
|
onTopIconButtonClick: () -> Unit,
|
||||||
|
tabIndex: Int,
|
||||||
|
onTabChanged: (Int) -> Unit,
|
||||||
|
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||||
|
primaryIconButtonId: Int? = null,
|
||||||
|
onPrimaryIconButtonClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable AnimatedVisibilityScope.(Int) -> Unit
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
VerticalBar(
|
||||||
|
topIconButtonId = topIconButtonId,
|
||||||
|
onTopIconButtonClick = onTopIconButtonClick,
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabChanged = onTabChanged,
|
||||||
|
tabColumnContent = tabColumnContent,
|
||||||
|
// primaryIconButtonId = primaryIconButtonId,
|
||||||
|
// onPrimaryIconButtonClick = onPrimaryIconButtonClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = tabIndex,
|
||||||
|
transitionSpec = {
|
||||||
|
val slideDirection = when (targetState > initialState) {
|
||||||
|
true -> AnimatedContentScope.SlideDirection.Up
|
||||||
|
false -> AnimatedContentScope.SlideDirection.Down
|
||||||
|
}
|
||||||
|
|
||||||
|
val animationSpec = spring(
|
||||||
|
dampingRatio = 0.9f,
|
||||||
|
stiffness = Spring.StiffnessLow,
|
||||||
|
visibilityThreshold = IntOffset.VisibilityThreshold
|
||||||
|
)
|
||||||
|
|
||||||
|
slideIntoContainer(slideDirection, animationSpec) with
|
||||||
|
slideOutOfContainer(slideDirection, animationSpec)
|
||||||
|
},
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryIconButtonId?.let {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(onClick = onPrimaryIconButtonClick)
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.size(62.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(primaryIconButtonId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ModifierParameter")
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun SimpleScaffold(
|
||||||
|
topIconButtonId: Int,
|
||||||
|
onTopIconButtonClick: () -> Unit,
|
||||||
|
title: String = "",
|
||||||
|
primaryIconButtonId: Int? = null,
|
||||||
|
primaryIconButtonEnabled: Boolean = true,
|
||||||
|
onPrimaryIconButtonClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
val (colorPalette) = LocalAppearance.current
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
VerticalTitleBar(
|
||||||
|
topIconButtonId = topIconButtonId,
|
||||||
|
onTopIconButtonClick = onTopIconButtonClick,
|
||||||
|
title = title,
|
||||||
|
primaryIconButtonId = primaryIconButtonId,
|
||||||
|
primaryIconButtonEnabled = primaryIconButtonEnabled,
|
||||||
|
onPrimaryIconButtonClick = onPrimaryIconButtonClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
)
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,186 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.components.themed
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.TabColumn
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.vertical
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
|
||||||
|
@SuppressLint("ModifierParameter")
|
||||||
|
@Composable
|
||||||
|
fun VerticalBar(
|
||||||
|
topIconButtonId: Int,
|
||||||
|
onTopIconButtonClick: () -> Unit,
|
||||||
|
tabIndex: Int,
|
||||||
|
onTabChanged: (Int) -> Unit,
|
||||||
|
tabColumnContent: @Composable (@Composable (Int, String, Int) -> Unit) -> Unit,
|
||||||
|
// primaryIconButtonId: Int? = null,
|
||||||
|
// onPrimaryIconButtonClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .clip(RoundedCornerShape(16.dp))
|
||||||
|
// .clickable(onClick = onTopIconButtonClick)
|
||||||
|
// .background(color = colorPalette.background1)
|
||||||
|
// .size(48.dp)
|
||||||
|
// ) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(topIconButtonId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(onClick = onTopIconButtonClick)
|
||||||
|
.padding(all = 12.dp)
|
||||||
|
// .align(Alignment.Center)
|
||||||
|
.size(22.dp)
|
||||||
|
)
|
||||||
|
// }
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(64.dp)
|
||||||
|
.height(32.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
TabColumn(
|
||||||
|
tabIndex = tabIndex,
|
||||||
|
onTabIndexChanged = onTabChanged,
|
||||||
|
selectedTextColor = colorPalette.text,
|
||||||
|
disabledTextColor = colorPalette.textDisabled,
|
||||||
|
textStyle = typography.xs.semiBold,
|
||||||
|
content = tabColumnContent,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Spacer(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .weight(1f)
|
||||||
|
// )
|
||||||
|
|
||||||
|
// primaryIconButtonId?.let {
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .offset(x = 8.dp)
|
||||||
|
// .clip(RoundedCornerShape(16.dp))
|
||||||
|
// .clickable(onClick = onPrimaryIconButtonClick)
|
||||||
|
// .background(colorPalette.background1)
|
||||||
|
// .size(62.dp)
|
||||||
|
// ) {
|
||||||
|
// Image(
|
||||||
|
// painter = painterResource(primaryIconButtonId),
|
||||||
|
// contentDescription = null,
|
||||||
|
// colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
// modifier = Modifier
|
||||||
|
// .align(Alignment.Center)
|
||||||
|
// .size(20.dp)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ModifierParameter")
|
||||||
|
@Composable
|
||||||
|
fun VerticalTitleBar(
|
||||||
|
topIconButtonId: Int,
|
||||||
|
onTopIconButtonClick: () -> Unit,
|
||||||
|
title: String,
|
||||||
|
primaryIconButtonId: Int? = null,
|
||||||
|
primaryIconButtonEnabled: Boolean = true,
|
||||||
|
onPrimaryIconButtonClick: () -> Unit = {},
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(vertical = 16.dp)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(onClick = onTopIconButtonClick)
|
||||||
|
.background(color = colorPalette.background1)
|
||||||
|
.size(48.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(topIconButtonId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(22.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(78.dp)
|
||||||
|
.height(32.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = title,
|
||||||
|
style = typography.m.semiBold,
|
||||||
|
modifier = Modifier
|
||||||
|
.vertical()
|
||||||
|
.rotate(-90f)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
primaryIconButtonId?.let {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset(x = 8.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = primaryIconButtonEnabled, onClick = onPrimaryIconButtonClick)
|
||||||
|
.background(colorPalette.background1)
|
||||||
|
.size(62.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(primaryIconButtonId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,115 +1,26 @@
|
||||||
package it.vfsfitvnm.vimusic.ui.screens
|
package it.vfsfitvnm.vimusic.ui.screens
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.animation.animateContentSize
|
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
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.grid.GridCells
|
|
||||||
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
|
|
||||||
import androidx.compose.foundation.lazy.grid.items
|
|
||||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
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.collectAsState
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
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.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
import it.vfsfitvnm.vimusic.Database
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
|
||||||
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.models.Playlist
|
|
||||||
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.TopAppBar
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.components.badge
|
import it.vfsfitvnm.vimusic.ui.views.PlaylistsTab
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownSection
|
import it.vfsfitvnm.vimusic.ui.views.SongsTab
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownSectionSpacer
|
import it.vfsfitvnm.vimusic.utils.homeScreenTabIndexKey
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropDownTextItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.DropdownMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
|
||||||
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.BuiltInPlaylistItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.color
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.isFirstLaunchKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistGridExpandedKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
import it.vfsfitvnm.vimusic.utils.rememberPreference
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
|
||||||
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen() {
|
fun HomeScreen() {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
val lazyHorizontalGridState = rememberLazyGridState()
|
|
||||||
|
|
||||||
var playlistSortBy by rememberPreference(playlistSortByKey, PlaylistSortBy.DateAdded)
|
|
||||||
var playlistSortOrder by rememberPreference(playlistSortOrderKey, SortOrder.Descending)
|
|
||||||
var playlistGridExpanded by rememberPreference(playlistGridExpandedKey, false)
|
|
||||||
|
|
||||||
val playlistPreviews by remember(playlistSortBy, playlistSortOrder) {
|
|
||||||
Database.playlistPreviews(playlistSortBy, playlistSortOrder)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
var songSortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
|
|
||||||
var songSortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
|
|
||||||
|
|
||||||
val songCollection by remember(songSortBy, songSortOrder) {
|
|
||||||
Database.songs(songSortBy, songSortOrder)
|
|
||||||
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
settingsRoute {
|
settingsRoute {
|
||||||
|
@ -153,15 +64,7 @@ fun HomeScreen() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
albumRoute { browseId ->
|
globalRoutes()
|
||||||
AlbumScreen(browseId = browseId ?: error("browseId cannot be null"))
|
|
||||||
}
|
|
||||||
|
|
||||||
artistRoute { browseId ->
|
|
||||||
ArtistScreen(
|
|
||||||
browseId = browseId ?: error("browseId cannot be null")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
intentUriRoute { uri ->
|
intentUriRoute { uri ->
|
||||||
IntentUriScreen(
|
IntentUriScreen(
|
||||||
|
@ -170,392 +73,38 @@ fun HomeScreen() {
|
||||||
}
|
}
|
||||||
|
|
||||||
host {
|
host {
|
||||||
// This somehow prevents items to not be displayed sometimes...
|
val (tabIndex, onTabChanged) = rememberPreference(homeScreenTabIndexKey, defaultValue = 0)
|
||||||
@Suppress("UNUSED_EXPRESSION") playlistPreviews
|
|
||||||
@Suppress("UNUSED_EXPRESSION") songCollection
|
|
||||||
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.equalizer,
|
||||||
val isFirstLaunch by rememberPreference(isFirstLaunchKey, true)
|
onTopIconButtonClick = { settingsRoute() },
|
||||||
|
tabIndex = tabIndex,
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
onTabChanged = onTabChanged,
|
||||||
|
tabColumnContent = { Item ->
|
||||||
var isCreatingANewPlaylist by rememberSaveable {
|
Item(0, "Songs", R.drawable.musical_notes)
|
||||||
mutableStateOf(false)
|
Item(1, "Playlists", R.drawable.playlist)
|
||||||
}
|
Item(2, "Artists", R.drawable.person)
|
||||||
|
Item(3, "Albums", R.drawable.disc)
|
||||||
if (isCreatingANewPlaylist) {
|
},
|
||||||
TextFieldDialog(
|
primaryIconButtonId = R.drawable.search,
|
||||||
hintText = "Enter the playlist name",
|
onPrimaryIconButtonClick = { searchRoute("") }
|
||||||
onDismiss = {
|
) { currentTabIndex ->
|
||||||
isCreatingANewPlaylist = false
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
},
|
when (currentTabIndex) {
|
||||||
onDone = { text ->
|
0 -> SongsTab()
|
||||||
query {
|
1 -> PlaylistsTab(
|
||||||
Database.insert(Playlist(name = text))
|
onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) },
|
||||||
}
|
onPlaylistClicked = { localPlaylistRoute(it.id) }
|
||||||
|
)
|
||||||
|
// 2 -> ArtistsTab(
|
||||||
|
// lazyListState = lazyListStates[currentTabIndex],
|
||||||
|
// onArtistClicked = { artistRoute(it.id) }
|
||||||
|
// )
|
||||||
|
// 3 -> AlbumsTab(
|
||||||
|
// lazyListState = lazyListStates[currentTabIndex],
|
||||||
|
// onAlbumClicked = { albumRoute(it.id) }
|
||||||
|
// )
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item("topAppBar") {
|
|
||||||
TopAppBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.height(52.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.equalizer),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { settingsRoute() }
|
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
|
||||||
.badge(color = colorPalette.red, isDisplayed = isFirstLaunch)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
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("playlistsHeader") {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Your playlists",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.add),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { isCreatingANewPlaylist = true }
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box {
|
|
||||||
var isSortMenuDisplayed by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.sort),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { isSortMenuDisplayed = true }
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
isDisplayed = isSortMenuDisplayed,
|
|
||||||
onDismissRequest = { isSortMenuDisplayed = false }
|
|
||||||
) {
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "NAME",
|
|
||||||
isSelected = playlistSortBy == PlaylistSortBy.Name,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortBy = PlaylistSortBy.Name
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "DATE ADDED",
|
|
||||||
isSelected = playlistSortBy == PlaylistSortBy.DateAdded,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortBy = PlaylistSortBy.DateAdded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "SONG COUNT",
|
|
||||||
isSelected = playlistSortBy == PlaylistSortBy.SongCount,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortBy = PlaylistSortBy.SongCount
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDownSectionSpacer()
|
|
||||||
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = when (playlistSortOrder) {
|
|
||||||
SortOrder.Ascending -> "ASCENDING"
|
|
||||||
SortOrder.Descending -> "DESCENDING"
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistSortOrder = !playlistSortOrder
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
DropDownSectionSpacer()
|
|
||||||
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = when (playlistGridExpanded) {
|
|
||||||
true -> "COLLAPSE"
|
|
||||||
false -> "EXPAND"
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
playlistGridExpanded = !playlistGridExpanded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item("playlists") {
|
|
||||||
LazyHorizontalGrid(
|
|
||||||
state = lazyHorizontalGridState,
|
|
||||||
rows = GridCells.Fixed(if (playlistGridExpanded) 3 else 1),
|
|
||||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.animateContentSize()
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(124.dp * (if (playlistGridExpanded) 3 else 1))
|
|
||||||
) {
|
|
||||||
item(key = "favorites") {
|
|
||||||
BuiltInPlaylistItem(
|
|
||||||
icon = R.drawable.heart,
|
|
||||||
colorTint = colorPalette.red,
|
|
||||||
name = "Favorites",
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { builtInPlaylistRoute(BuiltInPlaylist.Favorites) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item(key = "offline") {
|
|
||||||
BuiltInPlaylistItem(
|
|
||||||
icon = R.drawable.airplane,
|
|
||||||
colorTint = colorPalette.blue,
|
|
||||||
name = "Offline",
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { builtInPlaylistRoute(BuiltInPlaylist.Offline) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
items(
|
|
||||||
items = playlistPreviews,
|
|
||||||
key = { it.playlist.id },
|
|
||||||
contentType = { it }
|
|
||||||
) { playlistPreview ->
|
|
||||||
PlaylistPreviewItem(
|
|
||||||
playlistPreview = playlistPreview,
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
.padding(all = 8.dp)
|
|
||||||
.clickable(
|
|
||||||
indication = rememberRipple(bounded = true),
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
onClick = { localPlaylistRoute(playlistPreview.playlist.id) }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item("songs") {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 32.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = "Songs",
|
|
||||||
style = typography.m.semiBold,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable(enabled = songCollection.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
songCollection
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Box {
|
|
||||||
var isSortMenuDisplayed by remember {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.sort),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
isSortMenuDisplayed = true
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
DropdownMenu(
|
|
||||||
isDisplayed = isSortMenuDisplayed,
|
|
||||||
onDismissRequest = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "PLAY TIME",
|
|
||||||
isSelected = songSortBy == SongSortBy.PlayTime,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortBy = SongSortBy.PlayTime
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "TITLE",
|
|
||||||
isSelected = songSortBy == SongSortBy.Title,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortBy = SongSortBy.Title
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
DropDownTextItem(
|
|
||||||
text = "DATE ADDED",
|
|
||||||
isSelected = songSortBy == SongSortBy.DateAdded,
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortBy = SongSortBy.DateAdded
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
DropDownSectionSpacer()
|
|
||||||
|
|
||||||
DropDownSection {
|
|
||||||
DropDownTextItem(
|
|
||||||
text = when (songSortOrder) {
|
|
||||||
SortOrder.Ascending -> "ASCENDING"
|
|
||||||
SortOrder.Descending -> "DESCENDING"
|
|
||||||
},
|
|
||||||
onClick = {
|
|
||||||
isSortMenuDisplayed = false
|
|
||||||
songSortOrder = !songSortOrder
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = songCollection,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song }
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSize = thumbnailSize,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
songCollection.map(DetailedSong::asMediaItem),
|
|
||||||
index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
InHistoryMediaItemMenu(song = song)
|
|
||||||
},
|
|
||||||
onThumbnailContent = {
|
|
||||||
AnimatedVisibility(
|
|
||||||
visible = songSortBy == SongSortBy.PlayTime,
|
|
||||||
enter = fadeIn(),
|
|
||||||
exit = fadeOut(),
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = 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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ data class Typography(
|
||||||
val s: TextStyle,
|
val s: TextStyle,
|
||||||
val m: TextStyle,
|
val m: TextStyle,
|
||||||
val l: TextStyle,
|
val l: TextStyle,
|
||||||
|
val xxl: TextStyle,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun typographyOf(color: Color): Typography {
|
fun typographyOf(color: Color): Typography {
|
||||||
|
@ -54,5 +55,6 @@ fun typographyOf(color: Color): Typography {
|
||||||
s = textStyle.copy(fontSize = 16.sp),
|
s = textStyle.copy(fontSize = 16.sp),
|
||||||
m = textStyle.copy(fontSize = 18.sp),
|
m = textStyle.copy(fontSize = 18.sp),
|
||||||
l = textStyle.copy(fontSize = 20.sp),
|
l = textStyle.copy(fontSize = 20.sp),
|
||||||
|
xxl = textStyle.copy(fontSize = 32.sp),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,11 +3,13 @@ package it.vfsfitvnm.vimusic.ui.views
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.requiredSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.requiredWidth
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.text.BasicText
|
import androidx.compose.foundation.text.BasicText
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -17,7 +19,6 @@ 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.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
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.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
@ -31,7 +32,7 @@ import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
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.utils.color
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -61,7 +62,6 @@ fun PlaylistPreviewItem(
|
||||||
|
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
name = playlistPreview.playlist.name,
|
name = playlistPreview.playlist.name,
|
||||||
textColor = Color.White,
|
|
||||||
thumbnailSize = thumbnailSize,
|
thumbnailSize = thumbnailSize,
|
||||||
imageContent = {
|
imageContent = {
|
||||||
if (thumbnails.toSet().size == 1) {
|
if (thumbnails.toSet().size == 1) {
|
||||||
|
@ -112,7 +112,6 @@ fun BuiltInPlaylistItem(
|
||||||
PlaylistItem(
|
PlaylistItem(
|
||||||
name = name,
|
name = name,
|
||||||
thumbnailSize = thumbnailSize,
|
thumbnailSize = thumbnailSize,
|
||||||
withGradient = false,
|
|
||||||
imageContent = {
|
imageContent = {
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(icon),
|
painter = painterResource(icon),
|
||||||
|
@ -131,48 +130,31 @@ fun BuiltInPlaylistItem(
|
||||||
fun PlaylistItem(
|
fun PlaylistItem(
|
||||||
name: String,
|
name: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
textColor: Color? = null,
|
|
||||||
thumbnailSize: Dp = Dimensions.thumbnails.song,
|
thumbnailSize: Dp = Dimensions.thumbnails.song,
|
||||||
withGradient: Boolean = true,
|
|
||||||
imageContent: @Composable BoxScope.() -> Unit
|
imageContent: @Composable BoxScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
Box(
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(thumbnailShape)
|
.requiredWidth(thumbnailSize * 2)
|
||||||
.background(colorPalette.background1)
|
|
||||||
.size(thumbnailSize * 2)
|
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(thumbnailSize * 2),
|
.clip(thumbnailShape)
|
||||||
|
.background(colorPalette.background1)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.requiredSize(thumbnailSize * 2),
|
||||||
content = imageContent
|
content = imageContent
|
||||||
)
|
)
|
||||||
|
|
||||||
BasicText(
|
BasicText(
|
||||||
text = name,
|
text = name,
|
||||||
style = typography.xxs.semiBold.color(textColor ?: colorPalette.text),
|
style = typography.xxs.semiBold.center,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.align(Alignment.BottomStart)
|
|
||||||
.run {
|
|
||||||
if (withGradient) {
|
|
||||||
background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colors = listOf(
|
|
||||||
Color.Transparent,
|
|
||||||
Color.Black.copy(alpha = 0.75f)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
this
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,297 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.views
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridItemSpan
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
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.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
|
||||||
|
import it.vfsfitvnm.vimusic.enums.PlaylistSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
import it.vfsfitvnm.vimusic.models.PlaylistPreview
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
||||||
|
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.preferences
|
||||||
|
import it.vfsfitvnm.vimusic.utils.putEnum
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class PlaylistsTabViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
var items by mutableStateOf(emptyList<PlaylistPreview>())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var sortBy by mutableStatePreferenceOf(
|
||||||
|
preferences.getEnum(
|
||||||
|
playlistSortByKey,
|
||||||
|
PlaylistSortBy.DateAdded
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
preferences.edit { putEnum(playlistSortByKey, it) }
|
||||||
|
collectItems(sortBy = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortOrder by mutableStatePreferenceOf(
|
||||||
|
preferences.getEnum(
|
||||||
|
playlistSortOrderKey,
|
||||||
|
SortOrder.Ascending
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
preferences.edit { putEnum(playlistSortOrderKey, it) }
|
||||||
|
collectItems(sortOrder = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences
|
||||||
|
get() = getApplication<Application>().preferences
|
||||||
|
|
||||||
|
init {
|
||||||
|
collectItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectItems(
|
||||||
|
sortBy: PlaylistSortBy = this.sortBy,
|
||||||
|
sortOrder: SortOrder = this.sortOrder
|
||||||
|
) {
|
||||||
|
job?.cancel()
|
||||||
|
job = viewModelScope.launch {
|
||||||
|
Database.playlistPreviews(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
||||||
|
items = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun PlaylistsTab(
|
||||||
|
viewModel: PlaylistsTabViewModel = viewModel(),
|
||||||
|
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
|
||||||
|
onPlaylistClicked: (Playlist) -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
|
||||||
|
var isCreatingANewPlaylist by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCreatingANewPlaylist) {
|
||||||
|
TextFieldDialog(
|
||||||
|
hintText = "Enter the playlist name",
|
||||||
|
onDismiss = {
|
||||||
|
isCreatingANewPlaylist = false
|
||||||
|
},
|
||||||
|
onDone = { text ->
|
||||||
|
query {
|
||||||
|
Database.insert(Playlist(name = text))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
|
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(Dimensions.itemsVerticalPadding * 2),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(
|
||||||
|
space = Dimensions.itemsVerticalPadding * 2,
|
||||||
|
alignment = Alignment.CenterHorizontally
|
||||||
|
),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0,
|
||||||
|
span = { GridItemSpan(maxLineSpan) }
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.Bottom,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.height(128.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Playlists",
|
||||||
|
style = typography.xxl.medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
@Composable
|
||||||
|
fun Item(
|
||||||
|
@DrawableRes iconId: Int,
|
||||||
|
sortBy: PlaylistSortBy
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(iconId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { viewModel.sortBy = sortBy }
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicText(
|
||||||
|
text = "New playlist",
|
||||||
|
style = typography.xxs.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable { isCreatingANewPlaylist = true }
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
iconId = R.drawable.medical,
|
||||||
|
sortBy = PlaylistSortBy.SongCount
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
iconId = R.drawable.text,
|
||||||
|
sortBy = PlaylistSortBy.Name
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
iconId = R.drawable.calendar,
|
||||||
|
sortBy = PlaylistSortBy.DateAdded
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.arrow_up),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "favorites") {
|
||||||
|
BuiltInPlaylistItem(
|
||||||
|
icon = R.drawable.heart,
|
||||||
|
colorTint = colorPalette.red,
|
||||||
|
name = "Favorites",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item(key = "offline") {
|
||||||
|
BuiltInPlaylistItem(
|
||||||
|
icon = R.drawable.airplane,
|
||||||
|
colorTint = colorPalette.blue,
|
||||||
|
name = "Offline",
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) }
|
||||||
|
)
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(
|
||||||
|
items = viewModel.items,
|
||||||
|
key = { it.playlist.id }
|
||||||
|
) { playlistPreview ->
|
||||||
|
PlaylistPreviewItem(
|
||||||
|
playlistPreview = playlistPreview,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = rememberRipple(bounded = true),
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
onClick = { onPlaylistClicked(playlistPreview.playlist) }
|
||||||
|
)
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
261
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt
Normal file
261
app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt
Normal file
|
@ -0,0 +1,261 @@
|
||||||
|
package it.vfsfitvnm.vimusic.ui.views
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
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.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.edit
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||||
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
|
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
|
||||||
|
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.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.color
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.getEnum
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.mutableStatePreferenceOf
|
||||||
|
import it.vfsfitvnm.vimusic.utils.preferences
|
||||||
|
import it.vfsfitvnm.vimusic.utils.putEnum
|
||||||
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
|
import it.vfsfitvnm.vimusic.utils.songSortByKey
|
||||||
|
import it.vfsfitvnm.vimusic.utils.songSortOrderKey
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
class SongsTabViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
var items by mutableStateOf(emptyList<DetailedSong>())
|
||||||
|
private set
|
||||||
|
|
||||||
|
var sortBy by mutableStatePreferenceOf(preferences.getEnum(songSortByKey, SongSortBy.DateAdded)) {
|
||||||
|
preferences.edit { putEnum(songSortByKey, it) }
|
||||||
|
collectItems(sortBy = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
var sortOrder by mutableStatePreferenceOf(preferences.getEnum(songSortOrderKey, SortOrder.Ascending)) {
|
||||||
|
preferences.edit { putEnum(songSortOrderKey, it) }
|
||||||
|
collectItems(sortOrder = it)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var job: Job? = null
|
||||||
|
|
||||||
|
private val preferences: SharedPreferences
|
||||||
|
get() = getApplication<Application>().preferences
|
||||||
|
|
||||||
|
init {
|
||||||
|
collectItems()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun collectItems(sortBy: SongSortBy = this.sortBy, sortOrder: SortOrder = this.sortOrder) {
|
||||||
|
job?.cancel()
|
||||||
|
job = viewModelScope.launch {
|
||||||
|
Database.songs(sortBy, sortOrder).flowOn(Dispatchers.IO).collect {
|
||||||
|
items = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun SongsTab(
|
||||||
|
viewModel: SongsTabViewModel = viewModel()
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
|
val sortOrderIconRotation by animateFloatAsState(
|
||||||
|
targetValue = if (viewModel.sortOrder == SortOrder.Ascending) 0f else 180f,
|
||||||
|
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
verticalArrangement = Arrangement.Bottom,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.height(128.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "Songs",
|
||||||
|
style = typography.xxl.medium
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 8.dp)
|
||||||
|
) {
|
||||||
|
@Composable
|
||||||
|
fun Item(
|
||||||
|
@DrawableRes iconId: Int,
|
||||||
|
sortBy: SongSortBy
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(iconId),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { viewModel.sortBy = sortBy }
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Item(
|
||||||
|
iconId = R.drawable.trending,
|
||||||
|
sortBy = SongSortBy.PlayTime
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
iconId = R.drawable.text,
|
||||||
|
sortBy = SongSortBy.Title
|
||||||
|
)
|
||||||
|
|
||||||
|
Item(
|
||||||
|
iconId = R.drawable.calendar,
|
||||||
|
sortBy = SongSortBy.DateAdded
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(2.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.arrow_up),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
.graphicsLayer { rotationZ = sortOrderIconRotation }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = viewModel.items,
|
||||||
|
key = { _, song -> song.id }
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSize = thumbnailSize,
|
||||||
|
onClick = {
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(
|
||||||
|
viewModel.items.map(DetailedSong::asMediaItem),
|
||||||
|
index
|
||||||
|
)
|
||||||
|
},
|
||||||
|
menuContent = {
|
||||||
|
InHistoryMediaItemMenu(song = song)
|
||||||
|
},
|
||||||
|
onThumbnailContent = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = viewModel.sortBy == SongSortBy.PlayTime,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = 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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.animateItemPlacement()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,7 +21,6 @@ const val songSortOrderKey = "songSortOrder"
|
||||||
const val songSortByKey = "songSortBy"
|
const val songSortByKey = "songSortBy"
|
||||||
const val playlistSortOrderKey = "playlistSortOrder"
|
const val playlistSortOrderKey = "playlistSortOrder"
|
||||||
const val playlistSortByKey = "playlistSortBy"
|
const val playlistSortByKey = "playlistSortBy"
|
||||||
const val playlistGridExpandedKey = "playlistGridExpanded"
|
|
||||||
const val searchFilterKey = "searchFilter"
|
const val searchFilterKey = "searchFilter"
|
||||||
const val repeatModeKey = "repeatMode"
|
const val repeatModeKey = "repeatMode"
|
||||||
const val skipSilenceKey = "skipSilence"
|
const val skipSilenceKey = "skipSilence"
|
||||||
|
@ -29,6 +28,7 @@ const val volumeNormalizationKey = "volumeNormalization"
|
||||||
const val persistentQueueKey = "persistentQueue"
|
const val persistentQueueKey = "persistentQueue"
|
||||||
const val isShowingSynchronizedLyricsKey = "isShowingSynchronizedLyrics"
|
const val isShowingSynchronizedLyricsKey = "isShowingSynchronizedLyrics"
|
||||||
const val isShowingThumbnailInLockscreenKey = "isShowingThumbnailInLockscreen"
|
const val isShowingThumbnailInLockscreenKey = "isShowingThumbnailInLockscreen"
|
||||||
|
const val homeScreenTabIndexKey = "homeScreenTabIndex"
|
||||||
|
|
||||||
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
inline fun <reified T : Enum<T>> SharedPreferences.getEnum(
|
||||||
key: String,
|
key: String,
|
||||||
|
@ -61,6 +61,16 @@ fun rememberPreference(key: String, defaultValue: Boolean): MutableState<Boolean
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberPreference(key: String, defaultValue: Int): MutableState<Int> {
|
||||||
|
val context = LocalContext.current
|
||||||
|
return remember {
|
||||||
|
mutableStatePreferenceOf(context.preferences.getInt(key, defaultValue)) {
|
||||||
|
context.preferences.edit { putInt(key, it) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberPreference(key: String, defaultValue: String): MutableState<String> {
|
fun rememberPreference(key: String, defaultValue: String): MutableState<String> {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
package it.vfsfitvnm.vimusic.utils
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.saveable.listSaver
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberLazyListStates(count: Int): List<LazyListState> {
|
||||||
|
return rememberSaveable(
|
||||||
|
saver = listSaver(
|
||||||
|
save = { states: List<LazyListState> ->
|
||||||
|
List(states.size * 2) {
|
||||||
|
when (it % 2) {
|
||||||
|
0 -> states[it / 2].firstVisibleItemIndex
|
||||||
|
1 -> states[it / 2].firstVisibleItemScrollOffset
|
||||||
|
else -> error("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restore = { states ->
|
||||||
|
List(states.size / 2) {
|
||||||
|
LazyListState(
|
||||||
|
firstVisibleItemIndex = states[it * 2],
|
||||||
|
firstVisibleItemScrollOffset = states[it * 2 + 1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
List(count) { LazyListState(0, 0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun rememberLazyGridStates(count: Int): List<LazyGridState> {
|
||||||
|
return rememberSaveable(
|
||||||
|
saver = listSaver(
|
||||||
|
save = { states: List<LazyGridState> ->
|
||||||
|
List(states.size * 2) {
|
||||||
|
when (it % 2) {
|
||||||
|
0 -> states[it / 2].firstVisibleItemIndex
|
||||||
|
1 -> states[it / 2].firstVisibleItemScrollOffset
|
||||||
|
else -> error("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
restore = { states ->
|
||||||
|
List(states.size / 2) {
|
||||||
|
LazyGridState(
|
||||||
|
firstVisibleItemIndex = states[it * 2],
|
||||||
|
firstVisibleItemScrollOffset = states[it * 2 + 1]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
List(count) { LazyGridState(0, 0) }
|
||||||
|
}
|
||||||
|
}
|
20
app/src/main/res/drawable/arrow_down.xml
Normal file
20
app/src/main/res/drawable/arrow_down.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<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,268l144,144l144,-144"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="48"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M256,392L256,100"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="48"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
20
app/src/main/res/drawable/arrow_up.xml
Normal file
20
app/src/main/res/drawable/arrow_up.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<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,244l144,-144l144,144"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="48"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M256,120L256,412"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="48"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
12
app/src/main/res/drawable/calendar.xml
Normal file
12
app/src/main/res/drawable/calendar.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="M480,128a64,64 0,0 0,-64 -64H400V48.45c0,-8.61 -6.62,-16 -15.23,-16.43A16,16 0,0 0,368 48V64H144V48.45c0,-8.61 -6.62,-16 -15.23,-16.43A16,16 0,0 0,112 48V64H96a64,64 0,0 0,-64 64v12a4,4 0,0 0,4 4H476a4,4 0,0 0,4 -4Z"/>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M32,416a64,64 0,0 0,64 64L416,480a64,64 0,0 0,64 -64L480,179a3,3 0,0 0,-3 -3L35,176a3,3 0,0 0,-3 3ZM376,208a24,24 0,1 1,-24 24A24,24 0,0 1,376 208ZM376,288a24,24 0,1 1,-24 24A24,24 0,0 1,376 288ZM296,208a24,24 0,1 1,-24 24A24,24 0,0 1,296 208ZM296,288a24,24 0,1 1,-24 24A24,24 0,0 1,296 288ZM296,368a24,24 0,1 1,-24 24A24,24 0,0 1,296 368ZM216,288a24,24 0,1 1,-24 24A24,24 0,0 1,216 288ZM216,368a24,24 0,1 1,-24 24A24,24 0,0 1,216 368ZM136,288a24,24 0,1 1,-24 24A24,24 0,0 1,136 288ZM136,368a24,24 0,1 1,-24 24A24,24 0,0 1,136 368Z"/>
|
||||||
|
</vector>
|
|
@ -1,6 +1,6 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:width="512dp"
|
android:width="24dp"
|
||||||
android:height="512dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="512"
|
android:viewportWidth="512"
|
||||||
android:viewportHeight="512">
|
android:viewportHeight="512">
|
||||||
<path
|
<path
|
||||||
|
|
9
app/src/main/res/drawable/medical.xml
Normal file
9
app/src/main/res/drawable/medical.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<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="M272,464L240,464a32,32 0,0 1,-32 -32l0.05,-85.82a4,4 0,0 0,-6 -3.47l-74.34,43.06a31.48,31.48 0,0 1,-43 -11.52L68.21,345.61l-0.06,-0.1a31.65,31.65 0,0 1,11.56 -42.8l74.61,-43.25a4,4 0,0 0,0 -6.92L79.78,209.33a31.41,31.41 0,0 1,-11.55 -43l16.44,-28.55a31.48,31.48 0,0 1,19.27 -14.74,31.14 31.14,0 0,1 23.8,3.2l74.31,43a4,4 0,0 0,6 -3.47L208,80a32,32 0,0 1,32 -32h32a32,32 0,0 1,32 32L304,165.72a4,4 0,0 0,6 3.47l74.34,-43.06a31.51,31.51 0,0 1,43 11.52l16.49,28.64 0.06,0.09a31.52,31.52 0,0 1,-11.64 42.86l-74.53,43.2a4,4 0,0 0,0 6.92l74.53,43.2a31.42,31.42 0,0 1,11.56 43l-16.44,28.55a31.48,31.48 0,0 1,-19.27 14.74,31.14 31.14,0 0,1 -23.8,-3.2l-74.31,-43a4,4 0,0 0,-6 3.46L304,432A32,32 0,0 1,272 464ZM178.44,266.52h0ZM178.44,245.52h0ZM333.54,245.44ZM333.54,245.44h0Z"/>
|
||||||
|
</vector>
|
9
app/src/main/res/drawable/musical_notes.xml
Normal file
9
app/src/main/res/drawable/musical_notes.xml
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
<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="M421.84,37.37a25.86,25.86 0,0 0,-22.6 -4.46L199.92,86.49A32.3,32.3 0,0 0,176 118v226c0,6.74 -4.36,12.56 -11.11,14.83l-0.12,0.05 -52,18C92.88,383.53 80,402 80,423.91a55.54,55.54 0,0 0,23.23 45.63A54.78,54.78 0,0 0,135.34 480a55.82,55.82 0,0 0,17.75 -2.93l0.38,-0.13L175.31,469A47.84,47.84 0,0 0,208 423.91v-212c0,-7.29 4.77,-13.21 12.16,-15.07l0.21,-0.06L395,150.14a4,4 0,0 1,5 3.86V295.93c0,6.75 -4.25,12.38 -11.11,14.68l-0.25,0.09 -50.89,18.11A49.09,49.09 0,0 0,304 375.92a55.67,55.67 0,0 0,23.23 45.8,54.63 54.63,0 0,0 49.88,7.35l0.36,-0.12L399.31,421A47.83,47.83 0,0 0,432 375.92V58A25.74,25.74 0,0 0,421.84 37.37Z"/>
|
||||||
|
</vector>
|
20
app/src/main/res/drawable/trending.xml
Normal file
20
app/src/main/res/drawable/trending.xml
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<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="M352,144l112,0l0,112"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="32"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
<path
|
||||||
|
android:pathData="M48,368 L169.37,246.63a32,32 0,0 1,45.26 0l50.74,50.74a32,32 0,0 0,45.26 0L448,160"
|
||||||
|
android:strokeLineJoin="round"
|
||||||
|
android:strokeWidth="32"
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:strokeColor="#000"
|
||||||
|
android:strokeLineCap="round"/>
|
||||||
|
</vector>
|
|
@ -6,6 +6,7 @@ dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
|
maven { setUrl("https://jitpack.io") }
|
||||||
}
|
}
|
||||||
|
|
||||||
versionCatalogs {
|
versionCatalogs {
|
||||||
|
|
Loading…
Reference in a new issue