mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-06-18 05:23:12 +10:00
New Patch: Mini-Player Redesign
This commit is contained in:
+3
@@ -37,6 +37,9 @@ private fun PatchOptionsScreenPreview(
|
||||
enabledPatchCount = KnownPatch.All.size,
|
||||
isPatchEnabled = { true },
|
||||
onTogglePatch = { _, _ -> },
|
||||
patchLockState = { PatchLock.Free },
|
||||
variantIndex = { 0 },
|
||||
onSelectVariant = { _, _ -> },
|
||||
isConfigValid = parameters.isConfigValid,
|
||||
onInstall = {},
|
||||
)
|
||||
|
||||
+32
-1
@@ -3,6 +3,11 @@ package com.meowarex.rlmobile.ui.screens.patchopts
|
||||
import androidx.annotation.StringRes
|
||||
import com.meowarex.rlmobile.R
|
||||
|
||||
data class PatchVariant(
|
||||
@StringRes val titleRes: Int,
|
||||
val fileNames: List<String>,
|
||||
)
|
||||
|
||||
enum class KnownPatch(
|
||||
/**
|
||||
* Numeric display order in the patch options list. Lower = higher up.
|
||||
@@ -18,6 +23,8 @@ enum class KnownPatch(
|
||||
@StringRes val descRes: Int,
|
||||
val requires: List<KnownPatch> = emptyList(),
|
||||
val disables: List<KnownPatch> = emptyList(),
|
||||
val variants: List<PatchVariant> = emptyList(),
|
||||
val defaultVariantIndex: Int = 0,
|
||||
) {
|
||||
// Dependency-first order (later refs need backward resolution).
|
||||
// The `order` field controls display order; declaration order doesn't matter.
|
||||
@@ -89,6 +96,27 @@ enum class KnownPatch(
|
||||
descRes = R.string.patch_lyrics_progress_pill_desc,
|
||||
requires = listOf(LyricsDisableCover, LyricsReplaceLyricsButton, LyricsReplaceShareButton),
|
||||
),
|
||||
MiniPlayerRedesign(
|
||||
order = 50,
|
||||
fileNames = emptyList(),
|
||||
titleRes = R.string.patch_mini_player_redesign_title,
|
||||
descRes = R.string.patch_mini_player_redesign_desc,
|
||||
defaultVariantIndex = 2,
|
||||
variants = listOf(
|
||||
PatchVariant(
|
||||
titleRes = R.string.patch_mini_player_variant_floating_title,
|
||||
fileNames = listOf("mini-player-floating.patch"),
|
||||
),
|
||||
PatchVariant(
|
||||
titleRes = R.string.patch_mini_player_variant_square_grey_title,
|
||||
fileNames = listOf("mini-player-grey.patch"),
|
||||
),
|
||||
PatchVariant(
|
||||
titleRes = R.string.patch_mini_player_variant_square_black_title,
|
||||
fileNames = listOf("mini-player-black.patch"),
|
||||
),
|
||||
),
|
||||
),
|
||||
EnableLegacyUi(
|
||||
order = 10,
|
||||
fileNames = listOf("enable-legacy-ui.patch"),
|
||||
@@ -107,13 +135,16 @@ enum class KnownPatch(
|
||||
),
|
||||
);
|
||||
|
||||
val allVariantFileNames: Set<String>
|
||||
get() = variants.flatMapTo(mutableSetOf()) { it.fileNames }
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Sorted by `order` ascending. Tie-breaks fall back to the first filename
|
||||
* (alphabetical) so the order is always deterministic.
|
||||
*/
|
||||
val All: List<KnownPatch> = entries.sortedWith(
|
||||
compareBy({ it.order }, { it.fileNames.first() })
|
||||
compareBy({ it.order }, { it.fileNames.firstOrNull() ?: it.name })
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+19
-8
@@ -33,15 +33,26 @@ data class PatchOptions(
|
||||
val customPatches: PatchComponent? = null,
|
||||
|
||||
val disabledPatches: Set<String> = emptySet(),
|
||||
|
||||
val selectedVariants: Map<String, Int> = emptyMap(),
|
||||
) : Parcelable {
|
||||
companion object {
|
||||
val Default = PatchOptions(
|
||||
appName = "TIDAL",
|
||||
packageName = "com.aspiro.tidal",
|
||||
debuggable = false,
|
||||
customTidalApk = null,
|
||||
customPatches = null,
|
||||
disabledPatches = (KnownPatch.DebugMenuUnlock.fileNames + KnownPatch.EnableLegacyUi.fileNames).toSet(),
|
||||
)
|
||||
val Default: PatchOptions = run {
|
||||
val miniPlayerFiles = KnownPatch.MiniPlayerRedesign.allVariantFileNames
|
||||
val disabled = (
|
||||
KnownPatch.DebugMenuUnlock.fileNames +
|
||||
KnownPatch.EnableLegacyUi.fileNames +
|
||||
miniPlayerFiles
|
||||
).toSet()
|
||||
|
||||
PatchOptions(
|
||||
appName = "TIDAL",
|
||||
packageName = "com.aspiro.tidal",
|
||||
debuggable = false,
|
||||
customTidalApk = null,
|
||||
customPatches = null,
|
||||
disabledPatches = disabled,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+71
-2
@@ -50,10 +50,32 @@ class PatchOptionsModel(
|
||||
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches)
|
||||
private set
|
||||
|
||||
fun isPatchEnabled(patch: KnownPatch): Boolean =
|
||||
patch.fileNames.none { it in disabledPatches }
|
||||
var selectedVariants by mutableStateOf(prefilledOptions.selectedVariants)
|
||||
private set
|
||||
|
||||
fun variantIndex(patch: KnownPatch): Int = selectedVariants[patch.name]
|
||||
?.coerceIn(0, patch.variants.lastIndex.coerceAtLeast(0))
|
||||
?: patch.defaultVariantIndex.coerceIn(0, patch.variants.lastIndex.coerceAtLeast(0))
|
||||
|
||||
fun isPatchEnabled(patch: KnownPatch): Boolean = if (patch.variants.isNotEmpty()) {
|
||||
val v = patch.variants[variantIndex(patch)]
|
||||
v.fileNames.isNotEmpty() && v.fileNames.none { it in disabledPatches }
|
||||
} else {
|
||||
patch.fileNames.isNotEmpty() && patch.fileNames.none { it in disabledPatches }
|
||||
}
|
||||
|
||||
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
|
||||
if (patch.variants.isNotEmpty()) {
|
||||
val all = patch.allVariantFileNames
|
||||
val selected = patch.variants[variantIndex(patch)].fileNames.toSet()
|
||||
disabledPatches = if (enabled) {
|
||||
(disabledPatches + all) - selected
|
||||
} else {
|
||||
disabledPatches + all
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
|
||||
buildSet {
|
||||
fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) }
|
||||
@@ -78,6 +100,37 @@ class PatchOptionsModel(
|
||||
disabledPatches = (disabledPatches - enableFiles) + disableFiles
|
||||
}
|
||||
|
||||
fun selectVariant(patch: KnownPatch, index: Int) {
|
||||
if (patch.variants.isEmpty() || index !in patch.variants.indices) return
|
||||
val wasOn = isPatchEnabled(patch)
|
||||
selectedVariants = selectedVariants + (patch.name to index)
|
||||
if (wasOn) setPatchEnabled(patch, true)
|
||||
}
|
||||
|
||||
fun lockState(patch: KnownPatch): PatchLock {
|
||||
if (patch.variants.isNotEmpty()) return PatchLock.Free
|
||||
|
||||
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
|
||||
buildSet {
|
||||
fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) }
|
||||
walk(seed)
|
||||
}
|
||||
|
||||
for (other in KnownPatch.All) {
|
||||
if (other == patch || !isPatchEnabled(other)) continue
|
||||
|
||||
val requiresClosure = closure(other) { it.requires }
|
||||
if (patch in requiresClosure - other) return PatchLock.LockedOn(other)
|
||||
|
||||
val disablesClosure = requiresClosure.flatMap { it.disables }
|
||||
.flatMapTo(mutableSetOf()) { d ->
|
||||
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } }
|
||||
}
|
||||
if (patch in disablesClosure) return PatchLock.LockedOff(other)
|
||||
}
|
||||
return PatchLock.Free
|
||||
}
|
||||
|
||||
val enabledPatchCount: Int
|
||||
get() = KnownPatch.All.count { isPatchEnabled(it) }
|
||||
|
||||
@@ -123,6 +176,7 @@ class PatchOptionsModel(
|
||||
customTidalApk = customTidalApk,
|
||||
customPatches = customPatches,
|
||||
disabledPatches = disabledPatches,
|
||||
selectedVariants = selectedVariants,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -147,7 +201,16 @@ class PatchOptionsModel(
|
||||
mainThread { packageNameState = state }
|
||||
}
|
||||
|
||||
private fun validatePatchSelection() {
|
||||
for (patch in KnownPatch.All) {
|
||||
if (isPatchEnabled(patch)) {
|
||||
setPatchEnabled(patch, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
validatePatchSelection()
|
||||
screenModelScope.launchBlock { fetchPkgNameState() }
|
||||
}
|
||||
|
||||
@@ -162,3 +225,9 @@ enum class PackageNameState {
|
||||
Invalid,
|
||||
Taken,
|
||||
}
|
||||
|
||||
sealed class PatchLock {
|
||||
object Free : PatchLock()
|
||||
data class LockedOn(val by: KnownPatch) : PatchLock()
|
||||
data class LockedOff(val by: KnownPatch) : PatchLock()
|
||||
}
|
||||
|
||||
+9
@@ -63,6 +63,9 @@ class PatchOptionsScreen(
|
||||
enabledPatchCount = model.enabledPatchCount,
|
||||
isPatchEnabled = model::isPatchEnabled,
|
||||
onTogglePatch = model::setPatchEnabled,
|
||||
patchLockState = model::lockState,
|
||||
variantIndex = model::variantIndex,
|
||||
onSelectVariant = model::selectVariant,
|
||||
|
||||
isConfigValid = model.isConfigValid,
|
||||
onInstall = {
|
||||
@@ -96,6 +99,9 @@ fun PatchOptionsScreenContent(
|
||||
enabledPatchCount: Int,
|
||||
isPatchEnabled: (KnownPatch) -> Boolean,
|
||||
onTogglePatch: (KnownPatch, Boolean) -> Unit,
|
||||
patchLockState: (KnownPatch) -> PatchLock,
|
||||
variantIndex: (KnownPatch) -> Int,
|
||||
onSelectVariant: (KnownPatch, Int) -> Unit,
|
||||
|
||||
isConfigValid: Boolean,
|
||||
onInstall: () -> Unit,
|
||||
@@ -162,6 +168,9 @@ fun PatchOptionsScreenContent(
|
||||
totalCount = KnownPatch.All.size,
|
||||
isEnabled = isPatchEnabled,
|
||||
onToggle = onTogglePatch,
|
||||
lockState = patchLockState,
|
||||
variantIndex = variantIndex,
|
||||
onSelectVariant = onSelectVariant,
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
)
|
||||
|
||||
|
||||
+124
-13
@@ -18,9 +18,14 @@ import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.semantics.Role
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.fromHtml
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock
|
||||
|
||||
private data class LockInfo(val patch: KnownPatch, val lock: PatchLock)
|
||||
|
||||
@Composable
|
||||
fun PatchSelectionAccordion(
|
||||
@@ -28,6 +33,9 @@ fun PatchSelectionAccordion(
|
||||
totalCount: Int,
|
||||
isEnabled: (KnownPatch) -> Boolean,
|
||||
onToggle: (KnownPatch, Boolean) -> Unit,
|
||||
lockState: (KnownPatch) -> PatchLock,
|
||||
variantIndex: (KnownPatch) -> Int,
|
||||
onSelectVariant: (KnownPatch, Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||
@@ -36,6 +44,8 @@ fun PatchSelectionAccordion(
|
||||
label = "patch-accordion-arrow",
|
||||
)
|
||||
|
||||
var lockInfo by remember { mutableStateOf<LockInfo?>(null) }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -80,6 +90,7 @@ fun PatchSelectionAccordion(
|
||||
|
||||
AnimatedVisibility(visible = expanded) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.background.copy(0.4f))
|
||||
@@ -94,45 +105,73 @@ fun PatchSelectionAccordion(
|
||||
)
|
||||
|
||||
for (patch in KnownPatch.All) key(patch) {
|
||||
PatchCheckboxRow(
|
||||
val checked = isEnabled(patch)
|
||||
val lock = lockState(patch)
|
||||
PatchSwitchRow(
|
||||
title = stringResource(patch.titleRes),
|
||||
description = stringResource(patch.descRes),
|
||||
checked = isEnabled(patch),
|
||||
checked = checked,
|
||||
lock = lock,
|
||||
onCheckedChange = { onToggle(patch, it) },
|
||||
onLockedTap = { lockInfo = LockInfo(patch, lock) },
|
||||
)
|
||||
|
||||
if (patch.variants.isNotEmpty()) {
|
||||
AnimatedVisibility(visible = checked) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp),
|
||||
) {
|
||||
PatchVariantSelector(
|
||||
variants = patch.variants,
|
||||
selectedIndex = variantIndex(patch),
|
||||
onSelect = { idx -> onSelectVariant(patch, idx) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lockInfo?.let { info ->
|
||||
PatchLockDialog(
|
||||
thisPatch = info.patch,
|
||||
lock = info.lock,
|
||||
onDismiss = { lockInfo = null },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PatchCheckboxRow(
|
||||
private fun PatchSwitchRow(
|
||||
title: String,
|
||||
description: String,
|
||||
checked: Boolean,
|
||||
lock: PatchLock,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
onLockedTap: () -> Unit,
|
||||
) {
|
||||
val interactionSource = remember(::MutableInteractionSource)
|
||||
val isLocked = lock !is PatchLock.Free
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = null,
|
||||
role = Role.Checkbox,
|
||||
) { onCheckedChange(!checked) }
|
||||
.padding(vertical = 4.dp),
|
||||
role = Role.Switch,
|
||||
) {
|
||||
if (isLocked) onLockedTap() else onCheckedChange(!checked)
|
||||
}
|
||||
.alpha(if (isLocked) 0.45f else 1f)
|
||||
.padding(vertical = 6.dp),
|
||||
) {
|
||||
Checkbox(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier.weight(1f),
|
||||
@@ -147,5 +186,77 @@ private fun PatchCheckboxRow(
|
||||
modifier = Modifier.alpha(.7f),
|
||||
)
|
||||
}
|
||||
|
||||
Box {
|
||||
Switch(
|
||||
checked = checked,
|
||||
enabled = !isLocked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
interactionSource = interactionSource,
|
||||
)
|
||||
if (isLocked) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.matchParentSize()
|
||||
.clickable(
|
||||
interactionSource = remember(::MutableInteractionSource),
|
||||
indication = null,
|
||||
role = Role.Switch,
|
||||
) { onLockedTap() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun PatchLockDialog(
|
||||
thisPatch: KnownPatch,
|
||||
lock: PatchLock,
|
||||
onDismiss: () -> Unit,
|
||||
) {
|
||||
val (titleRes, msgRes, blockerTitle) = when (lock) {
|
||||
is PatchLock.LockedOn -> Triple(
|
||||
R.string.patch_lock_required_title,
|
||||
R.string.patch_lock_required_msg,
|
||||
stringResource(lock.by.titleRes),
|
||||
)
|
||||
is PatchLock.LockedOff -> Triple(
|
||||
R.string.patch_lock_blocked_title,
|
||||
R.string.patch_lock_blocked_msg,
|
||||
stringResource(lock.by.titleRes),
|
||||
)
|
||||
PatchLock.Free -> return
|
||||
}
|
||||
|
||||
BasicAlertDialog(onDismissRequest = onDismiss) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
tonalElevation = 6.dp,
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(titleRes),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
)
|
||||
Text(
|
||||
text = AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle)),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
)
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
TextButton(
|
||||
onClick = onDismiss,
|
||||
modifier = Modifier.align(Alignment.CenterEnd),
|
||||
) {
|
||||
Text(stringResource(R.string.action_got_it))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
package com.meowarex.rlmobile.ui.screens.patchopts.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchVariant
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PatchVariantSelector(
|
||||
variants: List<PatchVariant>,
|
||||
selectedIndex: Int,
|
||||
onSelect: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
SingleChoiceSegmentedButtonRow(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
) {
|
||||
variants.forEachIndexed { index, variant ->
|
||||
SegmentedButton(
|
||||
selected = index == selectedIndex,
|
||||
onClick = { onSelect(index) },
|
||||
shape = SegmentedButtonDefaults.itemShape(
|
||||
index = index,
|
||||
count = variants.size,
|
||||
),
|
||||
icon = {},
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(variant.titleRes),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
textAlign = TextAlign.Center,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -283,6 +283,18 @@
|
||||
name="patch_enable_legacy_ui_desc"
|
||||
>[This patch will stop working soon] - Replaces the New Compose based UI with the Legacy UI (disables player-market-ui feature flag)</string>
|
||||
|
||||
<string name="patch_mini_player_redesign_title">Redesigned Mini-Player</string>
|
||||
<string name="patch_mini_player_redesign_desc">Integrates the Seekbar into the control panel at the bottom of the screen.</string>
|
||||
<string name="patch_mini_player_variant_floating_title">Floating</string>
|
||||
<string name="patch_mini_player_variant_square_grey_title">Grey</string>
|
||||
<string name="patch_mini_player_variant_square_black_title">Black</string>
|
||||
|
||||
<string name="patch_lock_required_title">Patch Required!</string>
|
||||
<string name="patch_lock_blocked_title">Patch Blocked!</string>
|
||||
<string name="patch_lock_required_msg">Patch Required by <b>%s</b>.</string>
|
||||
<string name="patch_lock_blocked_msg">Patch Blocked by <b>%s</b>.</string>
|
||||
<string name="action_got_it">Got it</string>
|
||||
|
||||
<string name="componentopts_screen_title">Custom Component (%s)</string>
|
||||
<string name="componentopts_screen_desc">Select a custom build that was imported by Manager.</string>
|
||||
<string name="componentopts_selected_none">Latest</string>
|
||||
|
||||
Reference in New Issue
Block a user