Redesign SettingsScreen (#172)

This commit is contained in:
vfsfitvnm 2022-09-23 15:35:31 +02:00
parent 563c6175f7
commit 6a3b41ca28
19 changed files with 847 additions and 1557 deletions

View file

@ -84,6 +84,7 @@ dependencies {
implementation(libs.compose.ripple)
implementation(libs.compose.shimmer)
implementation(libs.compose.coil)
implementation(libs.compose.viewmodel)
implementation(libs.palette)
@ -97,6 +98,4 @@ dependencies {
implementation(projects.kugou)
coreLibraryDesugaring(libs.desugaring)
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.5.1")
}

View file

@ -1,106 +0,0 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.medium
@Composable
fun DropDownSection(content: @Composable ColumnScope.() -> Unit) {
val (colorPalette) = LocalAppearance.current
Column(
modifier = Modifier
.shadow(
elevation = 2.dp,
shape = RoundedCornerShape(16.dp)
)
.background(colorPalette.background1)
.width(IntrinsicSize.Max),
content = content
)
}
@Composable
fun DropDownSectionSpacer() {
Spacer(
modifier = Modifier
.height(4.dp)
)
}
@Composable
fun DropDownTextItem(
text: String,
isSelected: Boolean,
onClick: () -> Unit
) {
val (colorPalette) = LocalAppearance.current
DropDownTextItem(
text = text,
textColor = if (isSelected) {
colorPalette.onAccent
} else {
colorPalette.textSecondary
},
backgroundColor = if (isSelected) {
colorPalette.accent
} else {
colorPalette.background1
},
onClick = onClick
)
}
@Composable
fun DropDownTextItem(
text: String,
backgroundColor: Color? = null,
textColor: Color? = null,
onClick: () -> Unit
) {
val (colorPalette, typography) = LocalAppearance.current
BasicText(
text = text,
style = typography.xxs.medium.copy(
color = textColor ?: colorPalette.text,
letterSpacing = 1.sp
),
modifier = Modifier
.clip(RoundedCornerShape(16.dp))
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = onClick
)
.background(backgroundColor ?: colorPalette.background1)
.fillMaxWidth()
.widthIn(min = 124.dp, max = 248.dp)
.padding(
horizontal = 16.dp,
vertical = 8.dp
)
)
}

View file

@ -1,205 +0,0 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import androidx.compose.animation.core.LinearOutSlowInEasing
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.updateTransition
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.TransformOrigin
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntRect
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Popup
import androidx.compose.ui.window.PopupPositionProvider
import androidx.compose.ui.window.PopupProperties
import kotlin.math.max
import kotlin.math.min
@Composable
fun DropdownMenu(
isDisplayed: Boolean,
onDismissRequest: () -> Unit,
modifier: Modifier = Modifier,
offset: DpOffset = DpOffset(0.dp, 0.dp),
properties: PopupProperties = PopupProperties(focusable = true),
content: @Composable ColumnScope.() -> Unit
) {
val expandedStates = remember {
MutableTransitionState(false)
}.apply { targetState = isDisplayed }
if (expandedStates.currentState || expandedStates.targetState) {
val density = LocalDensity.current
var transformOrigin by remember {
mutableStateOf(TransformOrigin.Center)
}
val popupPositionProvider =
DropdownMenuPositionProvider(offset, density) { parentBounds, menuBounds ->
transformOrigin = calculateTransformOrigin(parentBounds, menuBounds)
}
Popup(
onDismissRequest = onDismissRequest,
popupPositionProvider = popupPositionProvider,
properties = properties
) {
DropdownMenuContent(
expandedStates = expandedStates,
transformOrigin = transformOrigin,
modifier = modifier,
content = content
)
}
}
}
@Composable
internal fun DropdownMenuContent(
expandedStates: MutableTransitionState<Boolean>,
transformOrigin: TransformOrigin,
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
val transition = updateTransition(expandedStates, "DropDownMenu")
val scale by transition.animateFloat(
transitionSpec = {
if (false isTransitioningTo true) {
// Dismissed to expanded
tween(
durationMillis = 128,
easing = LinearOutSlowInEasing
)
} else {
// Expanded to dismissed.
tween(
durationMillis = 64,
delayMillis = 64
)
}
}, label = ""
) { isDisplayed ->
if (isDisplayed) 1f else 0.9f
}
Column(
modifier = modifier
.graphicsLayer {
scaleX = scale
scaleY = scale
this.transformOrigin = transformOrigin
},
content = content,
)
}
@Immutable
private data class DropdownMenuPositionProvider(
val contentOffset: DpOffset,
val density: Density,
val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }
) : PopupPositionProvider {
override fun calculatePosition(
anchorBounds: IntRect,
windowSize: IntSize,
layoutDirection: LayoutDirection,
popupContentSize: IntSize
): IntOffset {
// The min margin above and below the menu, relative to the screen.
val verticalMargin = with(density) { 48.dp.roundToPx() }
// The content offset specified using the dropdown offset parameter.
val contentOffsetX = with(density) { contentOffset.x.roundToPx() }
val contentOffsetY = with(density) { contentOffset.y.roundToPx() }
// Compute horizontal position.
val toRight = anchorBounds.left + contentOffsetX
val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width
val toDisplayRight = windowSize.width - popupContentSize.width
val toDisplayLeft = 0
val x = if (layoutDirection == LayoutDirection.Ltr) {
sequenceOf(
toRight,
toLeft,
// If the anchor gets outside of the window on the left, we want to position
// toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight.
if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft
)
} else {
sequenceOf(
toLeft,
toRight,
// If the anchor gets outside of the window on the right, we want to position
// toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft.
if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight
)
}.firstOrNull {
it >= 0 && it + popupContentSize.width <= windowSize.width
} ?: toLeft
// Compute vertical position.
val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin)
val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height
val toCenter = anchorBounds.top - popupContentSize.height / 2
val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin
val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull {
it >= verticalMargin &&
it + popupContentSize.height <= windowSize.height - verticalMargin
} ?: toTop
onPositionCalculated(
anchorBounds,
IntRect(x, y, x + popupContentSize.width, y + popupContentSize.height)
)
return IntOffset(x, y)
}
}
fun calculateTransformOrigin(
parentBounds: IntRect,
menuBounds: IntRect
): TransformOrigin {
val pivotX = when {
menuBounds.left >= parentBounds.right -> 0f
menuBounds.right <= parentBounds.left -> 1f
menuBounds.width == 0 -> 0f
else -> {
val intersectionCenter =
(
max(parentBounds.left, menuBounds.left) +
min(parentBounds.right, menuBounds.right)
) / 2
(intersectionCenter - menuBounds.left).toFloat() / menuBounds.width
}
}
val pivotY = when {
menuBounds.top >= parentBounds.bottom -> 0f
menuBounds.bottom <= parentBounds.top -> 1f
menuBounds.height == 0 -> 0f
else -> {
val intersectionCenter =
(
max(parentBounds.top, menuBounds.top) +
min(parentBounds.bottom, menuBounds.bottom)
) / 2
(intersectionCenter - menuBounds.top).toFloat() / menuBounds.height
}
}
return TransformOrigin(pivotX, pivotY)
}

View file

@ -0,0 +1,68 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.unit.dp
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.medium
@Composable
fun Header(
title: String,
modifier: Modifier = Modifier,
actionsContent: @Composable RowScope.() -> Unit = {},
) {
val typography = LocalAppearance.current.typography
Header(
modifier = modifier,
titleContent = {
BasicText(
text = title,
style = typography.xxl.medium
)
},
actionsContent = actionsContent
)
}
@Composable
fun Header(
modifier: Modifier = Modifier,
titleContent: @Composable ColumnScope.() -> Unit,
actionsContent: @Composable RowScope.() -> Unit,
) {
Column(
horizontalAlignment = Alignment.End,
modifier = modifier
.padding(horizontal = 16.dp)
.height(128.dp)
.fillMaxWidth()
) {
Spacer(
modifier = Modifier
.height(48.dp),
)
titleContent()
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.height(48.dp),
content = actionsContent,
)
}
}

View file

@ -1,25 +1,22 @@
package it.vfsfitvnm.vimusic.ui.screens
import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.*
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.*
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.badge
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.components.themed.Switch
import it.vfsfitvnm.vimusic.ui.components.themed.ValueSelectorDialog
import it.vfsfitvnm.vimusic.ui.screens.settings.*
@ -29,192 +26,38 @@ import it.vfsfitvnm.vimusic.utils.*
@ExperimentalAnimationApi
@Composable
fun SettingsScreen() {
val scrollState = rememberScrollState()
val saveableStateHolder = rememberSaveableStateHolder()
RouteHandler(
listenToGlobalEmitter = true,
transitionSpec = {
when (targetState.route) {
albumRoute, artistRoute -> fastFade
else -> when (initialState.route) {
albumRoute, artistRoute -> fastFade
null -> leftSlide
else -> rightSlide
}
}
}
) {
val (tabIndex, onTabChanged) = rememberSaveable {
mutableStateOf(0)
}
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
appearanceSettingsRoute {
AppearanceSettingsScreen()
}
playerSettingsRoute {
PlayerSettingsScreen()
}
backupAndRestoreRoute {
BackupAndRestoreScreen()
}
cacheSettingsRoute {
CacheSettingsScreen()
}
otherSettingsRoute {
OtherSettingsScreen()
}
aboutRoute {
AboutScreen()
}
host {
val (colorPalette, typography) = LocalAppearance.current
var isFirstLaunch by rememberPreference(isFirstLaunchKey, true)
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
Scaffold(
topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop,
tabIndex = tabIndex,
onTabChanged = onTabChanged,
tabColumnContent = { Item ->
Item(0, "Appearance", R.drawable.color_palette)
Item(1, "Player", R.drawable.play)
Item(2, "Cache", R.drawable.server)
Item(3, "Other", R.drawable.shapes)
Item(4, "About", R.drawable.information)
}
BasicText(
text = "Settings",
style = typography.l.semiBold,
modifier = Modifier
.padding(start = 48.dp)
.padding(all = 16.dp)
)
@Composable
fun Entry(
@DrawableRes icon: Int,
color: Color,
title: String,
description: String,
route: Route0,
withAlert: Boolean = false,
onClick: (() -> Unit)? = null
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = {
route()
onClick?.invoke()
}
)
.padding(horizontal = 16.dp, vertical = 12.dp)
.fillMaxWidth()
) {
Box(
modifier = Modifier
.background(color = color, shape = CircleShape)
.size(36.dp)
.badge(color = colorPalette.red, isDisplayed = withAlert)
) {
Image(
painter = painterResource(icon),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.align(Alignment.Center)
.size(16.dp)
)
}
Column(
modifier = Modifier
.weight(1f)
) {
BasicText(
text = title,
style = typography.s.semiBold,
)
BasicText(
text = description,
style = typography.xs.secondary.medium,
maxLines = 2
)
}
) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(currentTabIndex) {
when (currentTabIndex) {
0 -> AppearanceSettingsTab()
1 -> PlayerSettingsTab()
2 -> CacheSettingsTab()
3 -> OtherSettingsTab()
4 -> AboutTab()
}
}
Entry(
color = colorPalette.background2,
icon = R.drawable.color_palette,
title = "Appearance",
description = "Change the colors and shapes",
route = appearanceSettingsRoute,
)
Entry(
color = colorPalette.background2,
icon = R.drawable.play,
title = "Player & Audio",
description = "Player and audio settings",
route = playerSettingsRoute,
)
Entry(
color = colorPalette.background2,
icon = R.drawable.server,
title = "Cache",
description = "Manage the used space",
route = cacheSettingsRoute
)
Entry(
color = colorPalette.background2,
icon = R.drawable.save,
title = "Backup & Restore",
description = "Backup and restore the database",
route = backupAndRestoreRoute
)
Entry(
color = colorPalette.background2,
icon = R.drawable.shapes,
title = "Other",
description = "Advanced settings",
route = otherSettingsRoute,
withAlert = isFirstLaunch,
onClick = {
isFirstLaunch = false
}
)
Entry(
color = colorPalette.background2,
icon = R.drawable.information,
title = "About",
description = "App version and social links",
route = aboutRoute
)
}
}
}
@ -300,8 +143,8 @@ fun SwitchSettingEntry(
enabled = isEnabled
)
.alpha(if (isEnabled) 1f else 0.5f)
.padding(start = 24.dp)
.padding(horizontal = 32.dp, vertical = 16.dp)
.padding(start = 16.dp)
.padding(all = 16.dp)
.fillMaxWidth()
) {
@ -332,8 +175,7 @@ fun SettingsEntry(
onClick: () -> Unit,
isEnabled: Boolean = true
) {
val (_, typography) = LocalAppearance.current
val (colorPalette) = LocalAppearance.current
val (colorPalette, typography) = LocalAppearance.current
Column(
modifier = modifier
@ -344,8 +186,8 @@ fun SettingsEntry(
enabled = isEnabled
)
.alpha(if (isEnabled) 1f else 0.5f)
.padding(start = 24.dp)
.padding(horizontal = 32.dp, vertical = 16.dp)
.padding(start = 16.dp)
.padding(all = 16.dp)
.fillMaxWidth()
) {
BasicText(
@ -360,22 +202,6 @@ fun SettingsEntry(
}
}
@Composable
fun SettingsTitle(
text: String,
modifier: Modifier = Modifier,
) {
val (_, typography) = LocalAppearance.current
BasicText(
text = text,
style = typography.m.semiBold,
modifier = modifier
.padding(start = 40.dp)
.padding(all = 16.dp)
)
}
@Composable
fun SettingsDescription(
text: String,
@ -387,24 +213,8 @@ fun SettingsDescription(
text = text,
style = typography.xxs.secondary,
modifier = modifier
.padding(start = 56.dp, end = 24.dp)
.padding(bottom = 16.dp)
)
}
@Composable
fun SettingsGroupDescription(
text: String,
modifier: Modifier = Modifier,
) {
val (_, typography) = LocalAppearance.current
BasicText(
text = text,
style = typography.xxs.secondary,
modifier = modifier
.padding(start = 56.dp, end = 24.dp)
.padding(vertical = 8.dp)
.padding(start = 16.dp)
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
@ -419,7 +229,17 @@ fun SettingsEntryGroupText(
text = title.uppercase(),
style = typography.xxs.semiBold.copy(colorPalette.accent),
modifier = modifier
.padding(start = 24.dp, top = 24.dp)
.padding(horizontal = 32.dp)
.padding(start = 16.dp)
.padding(horizontal = 16.dp)
)
}
@Composable
fun SettingsGroupSpacer(
modifier: Modifier = Modifier,
) {
Spacer(
modifier = modifier
.height(24.dp)
)
}

View file

@ -1,100 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.BuildConfig
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@ExperimentalAnimationApi
@Composable
fun AboutScreen() {
val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val (colorPalette) = LocalAppearance.current
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
SettingsTitle(text = "About")
SettingsDescription(text = "v${BuildConfig.VERSION_NAME}\nby vfsfitvnm")
SettingsEntryGroupText(title = "SOCIAL")
SettingsEntry(
title = "GitHub",
text = "View the source code",
onClick = {
uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic")
}
)
SettingsEntryGroupText(title = "TROUBLESHOOTING")
SettingsEntry(
title = "Report an issue",
text = "You will be redirected to GitHub",
onClick = {
uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=bug&template=bug_report.yaml")
}
)
SettingsEntry(
title = "Request a feature or suggest an idea",
text = "You will be redirected to GitHub",
onClick = {
uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=enhancement&template=feature_request.yaml")
}
)
}
}
}
}

View file

@ -0,0 +1,73 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import it.vfsfitvnm.vimusic.BuildConfig
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.secondary
@ExperimentalAnimationApi
@Composable
fun AboutTab() {
val (colorPalette, typography) = LocalAppearance.current
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
Header(title = "About") {
BasicText(
text = "v${BuildConfig.VERSION_NAME} by vfsfitvnm",
style = typography.s.secondary
)
}
SettingsEntryGroupText(title = "SOCIAL")
SettingsEntry(
title = "GitHub",
text = "View the source code",
onClick = {
uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic")
}
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "TROUBLESHOOTING")
SettingsEntry(
title = "Report an issue",
text = "You will be redirected to GitHub",
onClick = {
uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=bug&template=bug_report.yaml")
}
)
SettingsEntry(
title = "Request a feature or suggest an idea",
text = "You will be redirected to GitHub",
onClick = {
uriHandler.openUri("https://github.com/vfsfitvnm/ViMusic/issues/new?assignees=&labels=enhancement&template=feature_request.yaml")
}
)
}
}

View file

@ -1,126 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
import it.vfsfitvnm.vimusic.enums.ColorPaletteName
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
import it.vfsfitvnm.vimusic.utils.isShowingThumbnailInLockscreenKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
@ExperimentalAnimationApi
@Composable
fun AppearanceSettingsScreen() {
val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val (colorPalette) = LocalAppearance.current
var colorPaletteName by rememberPreference(colorPaletteNameKey, ColorPaletteName.Dynamic)
var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System)
var thumbnailRoundness by rememberPreference(
thumbnailRoundnessKey,
ThumbnailRoundness.Light
)
var isShowingThumbnailInLockscreen by rememberPreference(
isShowingThumbnailInLockscreenKey,
false
)
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
SettingsTitle(text = "Appearance")
SettingsEntryGroupText(title = "COLORS")
EnumValueSelectorSettingsEntry(
title = "Theme",
selectedValue = colorPaletteName,
onValueSelected = {
colorPaletteName = it
}
)
EnumValueSelectorSettingsEntry(
title = "Theme mode",
selectedValue = colorPaletteMode,
isEnabled = colorPaletteName != ColorPaletteName.PureBlack,
onValueSelected = {
colorPaletteMode = it
}
)
SettingsEntryGroupText(title = "SHAPES")
EnumValueSelectorSettingsEntry(
title = "Thumbnail roundness",
selectedValue = thumbnailRoundness,
onValueSelected = {
thumbnailRoundness = it
}
)
SettingsEntryGroupText(title = "LOCKSCREEN")
SwitchSettingEntry(
title = "Show song cover",
text = "Use the playing song cover as the lockscreen wallpaper",
isChecked = isShowingThumbnailInLockscreen,
onCheckedChange = { isShowingThumbnailInLockscreen = it }
)
}
}
}
}

View file

@ -0,0 +1,91 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.enums.ColorPaletteMode
import it.vfsfitvnm.vimusic.enums.ColorPaletteName
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
import it.vfsfitvnm.vimusic.utils.isShowingThumbnailInLockscreenKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.thumbnailRoundnessKey
@ExperimentalAnimationApi
@Composable
fun AppearanceSettingsTab() {
val (colorPalette) = LocalAppearance.current
var colorPaletteName by rememberPreference(colorPaletteNameKey, ColorPaletteName.Dynamic)
var colorPaletteMode by rememberPreference(colorPaletteModeKey, ColorPaletteMode.System)
var thumbnailRoundness by rememberPreference(
thumbnailRoundnessKey,
ThumbnailRoundness.Light
)
var isShowingThumbnailInLockscreen by rememberPreference(
isShowingThumbnailInLockscreenKey,
false
)
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
Header(title = "Appearance")
SettingsEntryGroupText(title = "COLORS")
EnumValueSelectorSettingsEntry(
title = "Theme",
selectedValue = colorPaletteName,
onValueSelected = { colorPaletteName = it }
)
EnumValueSelectorSettingsEntry(
title = "Theme mode",
selectedValue = colorPaletteMode,
isEnabled = colorPaletteName != ColorPaletteName.PureBlack,
onValueSelected = { colorPaletteMode = it }
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "SHAPES")
EnumValueSelectorSettingsEntry(
title = "Thumbnail roundness",
selectedValue = thumbnailRoundness,
onValueSelected = { thumbnailRoundness = it }
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "LOCKSCREEN")
SwitchSettingEntry(
title = "Show song cover",
text = "Use the playing song cover as the lockscreen wallpaper",
isChecked = isShowingThumbnailInLockscreen,
onCheckedChange = { isShowingThumbnailInLockscreen = it }
)
}
}

View file

@ -1,173 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.annotation.SuppressLint
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.checkpoint
import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.path
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.service.PlayerService
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.intent
import java.io.FileInputStream
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.system.exitProcess
@ExperimentalAnimationApi
@Composable
fun BackupAndRestoreScreen() {
val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val (colorPalette) = LocalAppearance.current
val context = LocalContext.current
val backupLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/vnd.sqlite3")) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
query {
Database.internal.checkpoint()
context.applicationContext.contentResolver.openOutputStream(uri)
?.use { outputStream ->
FileInputStream(Database.internal.path).use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
val restoreLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
query {
Database.internal.checkpoint()
Database.internal.close()
FileOutputStream(Database.internal.path).use { outputStream ->
context.applicationContext.contentResolver.openInputStream(uri)
?.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
context.stopService(context.intent<PlayerService>())
exitProcess(0)
}
}
var isShowingRestoreDialog by rememberSaveable {
mutableStateOf(false)
}
if (isShowingRestoreDialog) {
ConfirmationDialog(
text = "The application will automatically close itself to avoid problems after restoring the database.",
onDismiss = {
isShowingRestoreDialog = false
},
onConfirm = {
restoreLauncher.launch(
arrayOf(
"application/x-sqlite3",
"application/vnd.sqlite3",
"application/octet-stream"
)
)
},
confirmText = "Ok"
)
}
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
SettingsTitle(text = "Backup & Restore")
SettingsEntryGroupText(title = "BACKUP")
SettingsGroupDescription(text = "Personal preferences (i.e. the theme mode) and the cache are excluded.")
SettingsEntry(
title = "Backup",
text = "Export the database to the external storage",
onClick = {
@SuppressLint("SimpleDateFormat")
val dateFormat = SimpleDateFormat("yyyyMMddHHmmss")
backupLauncher.launch("vimusic_${dateFormat.format(Date())}.db")
}
)
SettingsEntryGroupText(title = "RESTORE")
SettingsGroupDescription(text = "Existing data will be overwritten.")
SettingsEntry(
title = "Restore",
text = "Import the database from the external storage",
onClick = {
isShowingRestoreDialog = true
}
)
}
}
}
}

View file

@ -1,144 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.text.format.Formatter
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import coil.Coil
import coil.annotation.ExperimentalCoilApi
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.CoilDiskCacheMaxSize
import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.coilDiskCacheMaxSizeKey
import it.vfsfitvnm.vimusic.utils.exoPlayerDiskCacheMaxSizeKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
@OptIn(ExperimentalCoilApi::class)
@ExperimentalAnimationApi
@Composable
fun CacheSettingsScreen() {
val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val context = LocalContext.current
val (colorPalette, _) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
var coilDiskCacheMaxSize by rememberPreference(
coilDiskCacheMaxSizeKey,
CoilDiskCacheMaxSize.`128MB`
)
var exoPlayerDiskCacheMaxSize by rememberPreference(
exoPlayerDiskCacheMaxSizeKey,
ExoPlayerDiskCacheMaxSize.`2GB`
)
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
SettingsTitle(text = "Cache")
SettingsDescription(text = "When the cache runs out of space, the resources that haven't been accessed for the longest time are cleared.")
Coil.imageLoader(context).diskCache?.let { diskCache ->
val diskCacheSize = remember(diskCache) {
diskCache.size
}
SettingsEntryGroupText(title = "IMAGE CACHE")
SettingsGroupDescription(text = "${Formatter.formatShortFileSize(context, diskCacheSize)} used (${diskCacheSize * 100 / coilDiskCacheMaxSize.bytes.coerceAtLeast(1)}%)")
EnumValueSelectorSettingsEntry(
title = "Max size",
selectedValue = coilDiskCacheMaxSize,
onValueSelected = {
coilDiskCacheMaxSize = it
}
)
}
binder?.cache?.let { cache ->
val diskCacheSize by remember {
derivedStateOf {
cache.cacheSpace
}
}
SettingsEntryGroupText(title = "SONG CACHE")
SettingsGroupDescription(
text = buildString {
append(Formatter.formatShortFileSize(context, diskCacheSize))
append(" used")
when (val size = exoPlayerDiskCacheMaxSize) {
ExoPlayerDiskCacheMaxSize.Unlimited -> {}
else -> append(" (${diskCacheSize * 100 / size.bytes}%)")
}
}
)
EnumValueSelectorSettingsEntry(
title = "Max size",
selectedValue = exoPlayerDiskCacheMaxSize,
onValueSelected = {
exoPlayerDiskCacheMaxSize = it
}
)
}
}
}
}
}

View file

@ -0,0 +1,113 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.text.format.Formatter
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import coil.Coil
import coil.annotation.ExperimentalCoilApi
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.enums.CoilDiskCacheMaxSize
import it.vfsfitvnm.vimusic.enums.ExoPlayerDiskCacheMaxSize
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.EnumValueSelectorSettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.coilDiskCacheMaxSizeKey
import it.vfsfitvnm.vimusic.utils.exoPlayerDiskCacheMaxSizeKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
@OptIn(ExperimentalCoilApi::class)
@ExperimentalAnimationApi
@Composable
fun CacheSettingsTab() {
val context = LocalContext.current
val (colorPalette) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
var coilDiskCacheMaxSize by rememberPreference(
coilDiskCacheMaxSizeKey,
CoilDiskCacheMaxSize.`128MB`
)
var exoPlayerDiskCacheMaxSize by rememberPreference(
exoPlayerDiskCacheMaxSizeKey,
ExoPlayerDiskCacheMaxSize.`2GB`
)
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
Header(title = "Cache")
SettingsDescription(text = "When the cache runs out of space, the resources that haven't been accessed for the longest time are cleared")
Coil.imageLoader(context).diskCache?.let { diskCache ->
val diskCacheSize = remember(diskCache) {
diskCache.size
}
SettingsGroupSpacer()
SettingsEntryGroupText(title = "IMAGE CACHE")
SettingsDescription(text = "${Formatter.formatShortFileSize(context, diskCacheSize)} used (${diskCacheSize * 100 / coilDiskCacheMaxSize.bytes.coerceAtLeast(1)}%)")
EnumValueSelectorSettingsEntry(
title = "Max size",
selectedValue = coilDiskCacheMaxSize,
onValueSelected = {
coilDiskCacheMaxSize = it
}
)
}
binder?.cache?.let { cache ->
val diskCacheSize by remember {
derivedStateOf {
cache.cacheSpace
}
}
SettingsGroupSpacer()
SettingsEntryGroupText(title = "SONG CACHE")
SettingsDescription(
text = buildString {
append(Formatter.formatShortFileSize(context, diskCacheSize))
append(" used")
when (val size = exoPlayerDiskCacheMaxSize) {
ExoPlayerDiskCacheMaxSize.Unlimited -> {}
else -> append(" (${diskCacheSize * 100 / size.bytes}%)")
}
}
)
EnumValueSelectorSettingsEntry(
title = "Max size",
selectedValue = exoPlayerDiskCacheMaxSize,
onValueSelected = {
exoPlayerDiskCacheMaxSize = it
}
)
}
}
}

View file

@ -1,183 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupDescription
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.isIgnoringBatteryOptimizations
import it.vfsfitvnm.vimusic.utils.isInvincibilityEnabledKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.semiBold
import kotlinx.coroutines.Dispatchers
@ExperimentalAnimationApi
@Composable
fun OtherSettingsScreen() {
val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val context = LocalContext.current
val (colorPalette, typography) = LocalAppearance.current
val queriesCount by remember {
Database.queriesCount()
}.collectAsState(initial = 0, context = Dispatchers.IO)
var isInvincibilityEnabled by rememberPreference(isInvincibilityEnabledKey, false)
var isIgnoringBatteryOptimizations by remember {
mutableStateOf(context.isIgnoringBatteryOptimizations)
}
val activityResultLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
isIgnoringBatteryOptimizations = context.isIgnoringBatteryOptimizations
}
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
BasicText(
text = "Other",
style = typography.m.semiBold,
modifier = Modifier
.padding(start = 40.dp)
.padding(all = 16.dp)
)
SettingsEntryGroupText(title = "SEARCH HISTORY")
SettingsEntry(
title = "Clear search history",
text = if (queriesCount > 0) {
"Delete $queriesCount search queries"
} else {
"History is empty"
},
isEnabled = queriesCount > 0,
onClick = {
query {
Database.clearQueries()
}
}
)
SettingsEntryGroupText(title = "SERVICE LIFETIME")
SettingsGroupDescription(text = "If battery optimizations are applied, the playback notification can suddenly disappear when paused.")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SettingsDescription(text = "Since Android 12, disabling battery optimizations is required for the \"Invincible service\" option to take effect.")
}
SettingsEntry(
title = "Ignore battery optimizations",
isEnabled = !isIgnoringBatteryOptimizations,
text = if (isIgnoringBatteryOptimizations) {
"Already unrestricted"
} else {
"Disable background restrictions"
},
onClick = {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return@SettingsEntry
@SuppressLint("BatteryLife")
val intent =
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
}
if (intent.resolveActivity(context.packageManager) != null) {
activityResultLauncher.launch(intent)
} else {
val fallbackIntent =
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
if (fallbackIntent.resolveActivity(context.packageManager) != null) {
activityResultLauncher.launch(fallbackIntent)
} else {
Toast.makeText(
context,
"Couldn't find battery optimization settings, please whitelist ViMusic manually",
Toast.LENGTH_SHORT
).show()
}
}
}
)
SwitchSettingEntry(
title = "Invincible service",
text = "When turning off battery optimizations is not enough",
isChecked = isInvincibilityEnabled,
onCheckedChange = {
isInvincibilityEnabled = it
}
)
}
}
}
}

View file

@ -0,0 +1,241 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.annotation.SuppressLint
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
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.Modifier
import androidx.compose.ui.platform.LocalContext
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.checkpoint
import it.vfsfitvnm.vimusic.internal
import it.vfsfitvnm.vimusic.path
import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.service.PlayerService
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.SettingsDescription
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.intent
import it.vfsfitvnm.vimusic.utils.isIgnoringBatteryOptimizations
import it.vfsfitvnm.vimusic.utils.isInvincibilityEnabledKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
import java.io.FileInputStream
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import kotlin.system.exitProcess
import kotlinx.coroutines.Dispatchers
@ExperimentalAnimationApi
@Composable
fun OtherSettingsTab() {
val context = LocalContext.current
val (colorPalette) = LocalAppearance.current
val queriesCount by remember {
Database.queriesCount()
}.collectAsState(initial = 0, context = Dispatchers.IO)
var isInvincibilityEnabled by rememberPreference(isInvincibilityEnabledKey, false)
var isIgnoringBatteryOptimizations by remember {
mutableStateOf(context.isIgnoringBatteryOptimizations)
}
var isShowingRestoreDialog by rememberSaveable {
mutableStateOf(false)
}
val activityResultLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
isIgnoringBatteryOptimizations = context.isIgnoringBatteryOptimizations
}
val backupLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.CreateDocument("application/vnd.sqlite3")) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
query {
Database.internal.checkpoint()
context.applicationContext.contentResolver.openOutputStream(uri)
?.use { outputStream ->
FileInputStream(Database.internal.path).use { inputStream ->
inputStream.copyTo(outputStream)
}
}
}
}
val restoreLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
query {
Database.internal.checkpoint()
Database.internal.close()
FileOutputStream(Database.internal.path).use { outputStream ->
context.applicationContext.contentResolver.openInputStream(uri)
?.use { inputStream ->
inputStream.copyTo(outputStream)
}
}
context.stopService(context.intent<PlayerService>())
exitProcess(0)
}
}
if (isShowingRestoreDialog) {
ConfirmationDialog(
text = "The application will automatically close itself to avoid problems after restoring the database.",
onDismiss = {
isShowingRestoreDialog = false
},
onConfirm = {
restoreLauncher.launch(
arrayOf(
"application/x-sqlite3",
"application/vnd.sqlite3",
"application/octet-stream"
)
)
},
confirmText = "Ok"
)
}
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
Header(title = "Other")
SettingsEntryGroupText(title = "SEARCH HISTORY")
SettingsEntry(
title = "Clear search history",
text = if (queriesCount > 0) {
"Delete $queriesCount search queries"
} else {
"History is empty"
},
isEnabled = queriesCount > 0,
onClick = {
query {
Database.clearQueries()
}
}
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "SERVICE LIFETIME")
SettingsDescription(text = "If battery optimizations are applied, the playback notification can suddenly disappear when paused.")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
SettingsDescription(text = "Since Android 12, disabling battery optimizations is required for the \"Invincible service\" option to take effect.")
}
SettingsEntry(
title = "Ignore battery optimizations",
isEnabled = !isIgnoringBatteryOptimizations,
text = if (isIgnoringBatteryOptimizations) {
"Already unrestricted"
} else {
"Disable background restrictions"
},
onClick = {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return@SettingsEntry
@SuppressLint("BatteryLife")
val intent =
Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
data = Uri.parse("package:${context.packageName}")
}
if (intent.resolveActivity(context.packageManager) != null) {
activityResultLauncher.launch(intent)
} else {
val fallbackIntent =
Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
if (fallbackIntent.resolveActivity(context.packageManager) != null) {
activityResultLauncher.launch(fallbackIntent)
} else {
Toast.makeText(
context,
"Couldn't find battery optimization settings, please whitelist ViMusic manually",
Toast.LENGTH_SHORT
).show()
}
}
}
)
SwitchSettingEntry(
title = "Invincible service",
text = "When turning off battery optimizations is not enough",
isChecked = isInvincibilityEnabled,
onCheckedChange = { isInvincibilityEnabled = it }
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "BACKUP")
SettingsDescription(text = "Personal preferences (i.e. the theme mode) and the cache are excluded.")
SettingsEntry(
title = "Backup",
text = "Export the database to the external storage",
onClick = {
@SuppressLint("SimpleDateFormat")
val dateFormat = SimpleDateFormat("yyyyMMddHHmmss")
backupLauncher.launch("vimusic_${dateFormat.format(Date())}.db")
}
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "RESTORE")
SettingsDescription(text = "Existing data will be overwritten.")
SettingsEntry(
title = "Restore",
text = "Import the database from the external storage",
onClick = {
isShowingRestoreDialog = true
}
)
}
}

View file

@ -1,148 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsTitle
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.persistentQueueKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.skipSilenceKey
import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey
@ExperimentalAnimationApi
@Composable
fun PlayerSettingsScreen() {
val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) {
globalRoutes()
host {
val context = LocalContext.current
val (colorPalette) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
var persistentQueue by rememberPreference(persistentQueueKey, false)
var skipSilence by rememberPreference(skipSilenceKey, false)
var volumeNormalization by rememberPreference(volumeNormalizationKey, false)
val activityResultLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
}
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(scrollState)
.padding(LocalPlayerAwarePaddingValues.current)
) {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
SettingsTitle(text = "Player & Audio")
SettingsEntryGroupText(title = "PLAYER")
SwitchSettingEntry(
title = "Persistent queue",
text = "Save and restore playing songs",
isChecked = persistentQueue,
onCheckedChange = {
persistentQueue = it
}
)
SettingsEntryGroupText(title = "AUDIO")
SwitchSettingEntry(
title = "Skip silence",
text = "Skip silent parts during playback",
isChecked = skipSilence,
onCheckedChange = {
skipSilence = it
}
)
SwitchSettingEntry(
title = "Loudness normalization",
text = "Lower the volume to a standard level",
isChecked = volumeNormalization,
onCheckedChange = {
volumeNormalization = it
}
)
SettingsEntry(
title = "Equalizer",
text = "Interact with the system equalizer",
onClick = {
val intent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(
AudioEffect.EXTRA_AUDIO_SESSION,
binder?.player?.audioSessionId
)
putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
putExtra(
AudioEffect.EXTRA_CONTENT_TYPE,
AudioEffect.CONTENT_TYPE_MUSIC
)
}
if (intent.resolveActivity(context.packageManager) != null) {
activityResultLauncher.launch(intent)
} else {
Toast.makeText(context, "No equalizer app found!", Toast.LENGTH_SHORT)
.show()
}
}
)
}
}
}
}

View file

@ -0,0 +1,116 @@
package it.vfsfitvnm.vimusic.ui.screens.settings
import android.content.Intent
import android.media.audiofx.AudioEffect
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntry
import it.vfsfitvnm.vimusic.ui.screens.SettingsEntryGroupText
import it.vfsfitvnm.vimusic.ui.screens.SettingsGroupSpacer
import it.vfsfitvnm.vimusic.ui.screens.SwitchSettingEntry
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.persistentQueueKey
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.skipSilenceKey
import it.vfsfitvnm.vimusic.utils.volumeNormalizationKey
@ExperimentalAnimationApi
@Composable
fun PlayerSettingsTab() {
val context = LocalContext.current
val (colorPalette) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
var persistentQueue by rememberPreference(persistentQueueKey, false)
var skipSilence by rememberPreference(skipSilenceKey, false)
var volumeNormalization by rememberPreference(volumeNormalizationKey, false)
val activityResultLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
}
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
Header(title = "Player & Audio")
SettingsEntryGroupText(title = "PLAYER")
SwitchSettingEntry(
title = "Persistent queue",
text = "Save and restore playing songs",
isChecked = persistentQueue,
onCheckedChange = {
persistentQueue = it
}
)
SettingsGroupSpacer()
SettingsEntryGroupText(title = "AUDIO")
SwitchSettingEntry(
title = "Skip silence",
text = "Skip silent parts during playback",
isChecked = skipSilence,
onCheckedChange = {
skipSilence = it
}
)
SwitchSettingEntry(
title = "Loudness normalization",
text = "Lower the volume to a standard level",
isChecked = volumeNormalization,
onCheckedChange = {
volumeNormalization = it
}
)
SettingsEntry(
title = "Equalizer",
text = "Interact with the system equalizer",
onClick = {
val intent =
Intent(AudioEffect.ACTION_DISPLAY_AUDIO_EFFECT_CONTROL_PANEL).apply {
putExtra(
AudioEffect.EXTRA_AUDIO_SESSION,
binder?.player?.audioSessionId
)
putExtra(AudioEffect.EXTRA_PACKAGE_NAME, context.packageName)
putExtra(
AudioEffect.EXTRA_CONTENT_TYPE,
AudioEffect.CONTENT_TYPE_MUSIC
)
}
if (intent.resolveActivity(context.packageManager) != null) {
activityResultLauncher.launch(intent)
} else {
Toast.makeText(context, "No equalizer app found!", Toast.LENGTH_SHORT)
.show()
}
}
)
}
}

View file

@ -12,12 +12,8 @@ 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
@ -54,6 +50,7 @@ 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@ -164,88 +161,69 @@ fun PlaylistsTab(
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)
Header(title = "Playlists") {
@Composable
fun Item(
@DrawableRes iconId: Int,
sortBy: PlaylistSortBy
) {
@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),
painter = painterResource(iconId),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
modifier = Modifier
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
.clickable { viewModel.sortBy = sortBy }
.padding(all = 4.dp)
.size(18.dp)
.graphicsLayer { rotationZ = sortOrderIconRotation }
)
}
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 }
)
}
}

View file

@ -14,19 +14,14 @@ 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
@ -34,7 +29,6 @@ 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
@ -54,6 +48,7 @@ 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.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
@ -63,7 +58,6 @@ 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
@ -134,72 +128,53 @@ fun SongsTab(
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)
Header(title = "Songs") {
@Composable
fun Item(
@DrawableRes iconId: Int,
sortBy: SongSortBy
) {
@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),
painter = painterResource(iconId),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
colorFilter = ColorFilter.tint(if (viewModel.sortBy == sortBy) colorPalette.text else colorPalette.textDisabled),
modifier = Modifier
.clickable { viewModel.sortOrder = !viewModel.sortOrder }
.clickable { viewModel.sortBy = sortBy }
.padding(all = 4.dp)
.size(18.dp)
.graphicsLayer { rotationZ = sortOrderIconRotation }
)
}
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 }
)
}
}

View file

@ -26,6 +26,7 @@ dependencyResolutionManagement {
library("compose-shimmer", "com.valentinilk.shimmer", "compose-shimmer").version("1.0.3")
library("compose-viewmodel", "androidx.lifecycle", "lifecycle-viewmodel-compose").version("2.6.0-alpha02")
library("compose-activity", "androidx.activity", "activity-compose").version("1.5.1")
library("compose-coil", "io.coil-kt", "coil-compose").version("2.2.1")