diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 58bf36d..165754e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt deleted file mode 100644 index 6ce771e..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropDownSection.kt +++ /dev/null @@ -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 - ) - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt deleted file mode 100644 index bd86c6d..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/DropdownMenu.kt +++ /dev/null @@ -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, - 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) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt new file mode 100644 index 0000000..4d308a0 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/Header.kt @@ -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, + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt index f26b944..0e0755d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/SettingsScreen.kt @@ -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) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt deleted file mode 100644 index b6a39e1..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutScreen.kt +++ /dev/null @@ -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") - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt new file mode 100644 index 0000000..c5ed846 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AboutTab.kt @@ -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") + } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt deleted file mode 100644 index 6dc7abf..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsScreen.kt +++ /dev/null @@ -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 } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt new file mode 100644 index 0000000..352ef21 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/AppearanceSettingsTab.kt @@ -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 } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt deleted file mode 100644 index ed9938d..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/BackupAndRestoreScreen.kt +++ /dev/null @@ -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()) - 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 - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt deleted file mode 100644 index 0e511e4..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsScreen.kt +++ /dev/null @@ -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 - } - ) - } - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt new file mode 100644 index 0000000..0ced60e --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/CacheSettingsTab.kt @@ -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 + } + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt deleted file mode 100644 index 0729292..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsScreen.kt +++ /dev/null @@ -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 - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt new file mode 100644 index 0000000..9ec669a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/OtherSettingsTab.kt @@ -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()) + 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 + } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt deleted file mode 100644 index 17cd475..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsScreen.kt +++ /dev/null @@ -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() - } - } - ) - } - } - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt new file mode 100644 index 0000000..8be656f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/settings/PlayerSettingsTab.kt @@ -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() + } + } + ) + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt index 095c7d1..6fa6a15 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistsTab.kt @@ -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 } + ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt index ccfe8c1..76f9f0c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongsTab.kt @@ -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 } + ) } } diff --git a/settings.gradle.kts b/settings.gradle.kts index 176589f..ececf2e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")