From 3788eb20087e1a57b629457e4a52649e57c15712 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Tue, 23 Jun 2026 14:18:38 +0200 Subject: [PATCH 1/7] feat(patch): mini player gestures patch + patch suboptions --- .../patcher/steps/patch/SmaliPatchStep.kt | 7 + .../screens/PatchOptionsScreenPreview.kt | 2 + .../ui/screens/patchopts/KnownPatch.kt | 40 + .../ui/screens/patchopts/PatchOptions.kt | 32 + .../ui/screens/patchopts/PatchOptionsModel.kt | 8 + .../screens/patchopts/PatchOptionsScreen.kt | 6 + .../components/PatchSelectionAccordion.kt | 26 + Manager/app/src/main/res/values/strings.xml | 4 + .../android/feature/appscaffold/ui/q$c.smali | 74 ++ .../MiniPlayerGestures$ApplyPending.smali | 62 + .../radiant/MiniPlayerGestures$Gesture.smali | 408 +++++++ .../MiniPlayerGestures$RootGesture.smali | 129 ++ .../radiant/MiniPlayerGestures.smali | 1049 +++++++++++++++++ .../MiniPlayerTrackGestures$Gesture.smali | 220 ++++ .../radiant/MiniPlayerTrackGestures.smali | 82 ++ patches/mini-player-gestures-left-right.patch | 83 ++ patches/mini-player-gestures.patch | 118 ++ 17 files changed, 2350 insertions(+) create mode 100644 patches/extension/com/tidal/android/feature/appscaffold/ui/q$c.smali create mode 100644 patches/extension/radiant/MiniPlayerGestures$ApplyPending.smali create mode 100644 patches/extension/radiant/MiniPlayerGestures$Gesture.smali create mode 100644 patches/extension/radiant/MiniPlayerGestures$RootGesture.smali create mode 100644 patches/extension/radiant/MiniPlayerGestures.smali create mode 100644 patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali create mode 100644 patches/extension/radiant/MiniPlayerTrackGestures.smali create mode 100644 patches/mini-player-gestures-left-right.patch create mode 100644 patches/mini-player-gestures.patch diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt index 6e76762..0f5a914 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt @@ -42,6 +42,8 @@ class SmaliPatchStep( val patches = mutableListOf() val localsBumps = mutableMapOf, Int>() val disabledFiles = options.disabledPatchFiles() + val knownExtensionFiles = options.knownExtensionFiles() + val enabledExtensionFiles = options.enabledExtensionFiles() // Load and parse all the patches from the smali archive. container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}") @@ -54,6 +56,11 @@ class SmaliPatchStep( if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) { val relative = patchFile.removePrefix("extension/") + if (relative in knownExtensionFiles && relative !in enabledExtensionFiles) { + container.log("Skipping disabled extension smali: $relative") + continue + } + val out = smaliDir.resolve(relative) // Guard against zip-slip: a crafted entry could otherwise escape smaliDir. val baseCanonical = smaliDir.canonicalPath + File.separator diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt index 2e9d038..95afaaf 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt @@ -40,6 +40,8 @@ private fun PatchOptionsScreenPreview( patchLockState = { PatchLock.Free }, variantIndex = { 0 }, onSelectVariant = { _, _ -> }, + isSubOptionEnabled = { _, _ -> true }, + onToggleSubOption = { _, _, _ -> }, isConfigValid = parameters.isConfigValid, onInstall = {}, ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 1471ed6..ffdcc31 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -8,12 +8,23 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchDefault.Enabled data class PatchVariant( @StringRes val titleRes: Int, val fileNames: List, + val extensionFileNames: List = emptyList(), +) + +data class PatchSubOption( + val key: String, + @StringRes val titleRes: Int, + @StringRes val descRes: Int, + val fileNames: List, + val default: PatchDefault, + val extensionFileNames: List = emptyList(), ) enum class KnownPatch( val order: Int, // Patch order in the UI List (lower = higher up) [Main Patches: multiples of 10 | Sub Patches: multiples of 1] val fileNames: List, + val extensionFileNames: List = emptyList(), @StringRes val titleRes: Int, @StringRes val descRes: Int, val default: PatchDefault, // Default state of the patch in the UI List (enabled/disabled) @@ -21,6 +32,7 @@ enum class KnownPatch( val disables: List = emptyList(), val variants: List = emptyList(), val defaultVariantIndex: Int = 0, + val subOptions: List = emptyList(), ) { LyricsDisableCover( order = 41, @@ -135,6 +147,33 @@ enum class KnownPatch( ), ), ), + MiniPlayerGestures( + order = 51, + fileNames = listOf("mini-player-gestures.patch"), + extensionFileNames = listOf( + "radiant/MiniPlayerGestures.smali", + "radiant/MiniPlayerGestures\$Gesture.smali", + "radiant/MiniPlayerGestures\$RootGesture.smali", + "radiant/MiniPlayerGestures\$ApplyPending.smali", + ), + titleRes = R.string.patch_mini_player_gestures_title, + descRes = R.string.patch_mini_player_gestures_desc, + default = Enabled, + subOptions = listOf( + PatchSubOption( + key = "MiniPlayerGestures.LeftRight", + titleRes = R.string.patch_mini_player_left_right_gestures_title, + descRes = R.string.patch_mini_player_left_right_gestures_desc, + fileNames = listOf("mini-player-gestures-left-right.patch"), + default = Disabled, + extensionFileNames = listOf( + "radiant/MiniPlayerTrackGestures.smali", + "radiant/MiniPlayerTrackGestures\$Gesture.smali", + "com/tidal/android/feature/appscaffold/ui/q\$c.smali", + ), + ), + ), + ), EnableLegacyUi( order = 10, fileNames = listOf("enable-legacy-ui.patch"), @@ -152,6 +191,7 @@ enum class KnownPatch( QualityBadgeColors, LyricsProgressPill, CoverEverywhere, + MiniPlayerGestures, ), ); diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt index 36955a2..7a7f5ac 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt @@ -40,6 +40,9 @@ data class PatchOptions( fun isEnabled(patch: KnownPatch): Boolean = patchStates[patch.name] ?: patch.default.isEnabled + fun isEnabled(subOption: PatchSubOption): Boolean = + patchStates[subOption.key] ?: subOption.default.isEnabled + fun disabledPatchFiles(): Set = buildSet { for (patch in KnownPatch.All) { val enabled = isEnabled(patch) @@ -52,6 +55,35 @@ data class PatchOptions( if (!enabled || index != selected) addAll(variant.fileNames) } } + for (subOption in patch.subOptions) { + if (!enabled || !isEnabled(subOption)) addAll(subOption.fileNames) + } + } + } + + fun knownExtensionFiles(): Set = buildSet { + for (patch in KnownPatch.All) { + addAll(patch.extensionFileNames) + patch.variants.forEach { addAll(it.extensionFileNames) } + patch.subOptions.forEach { addAll(it.extensionFileNames) } + } + } + + fun enabledExtensionFiles(): Set = buildSet { + for (patch in KnownPatch.All) { + if (!isEnabled(patch)) continue + + addAll(patch.extensionFileNames) + + if (patch.variants.isNotEmpty()) { + val selected = (selectedVariants[patch.name] ?: patch.defaultVariantIndex) + .coerceIn(0, patch.variants.lastIndex) + addAll(patch.variants[selected].extensionFileNames) + } + + for (subOption in patch.subOptions) { + if (isEnabled(subOption)) addAll(subOption.extensionFileNames) + } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt index 78b90fd..ab28588 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt @@ -60,6 +60,14 @@ class PatchOptionsModel( fun isPatchEnabled(patch: KnownPatch): Boolean = patchStates[patch.name] ?: patch.default.isEnabled + fun isSubOptionEnabled(patch: KnownPatch, subOption: PatchSubOption): Boolean = + isPatchEnabled(patch) && (patchStates[subOption.key] ?: subOption.default.isEnabled) + + fun setSubOptionEnabled(patch: KnownPatch, subOption: PatchSubOption, enabled: Boolean) { + if (subOption !in patch.subOptions) return + patchStates = patchStates + (subOption.key to enabled) + } + fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) { fun closure(seed: KnownPatch, step: (KnownPatch) -> List): Set = buildSet { diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt index bd3eb26..0b4a0f3 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt @@ -66,6 +66,8 @@ class PatchOptionsScreen( patchLockState = model::lockState, variantIndex = model::variantIndex, onSelectVariant = model::selectVariant, + isSubOptionEnabled = model::isSubOptionEnabled, + onToggleSubOption = model::setSubOptionEnabled, isConfigValid = model.isConfigValid, onInstall = { @@ -102,6 +104,8 @@ fun PatchOptionsScreenContent( patchLockState: (KnownPatch) -> PatchLock, variantIndex: (KnownPatch) -> Int, onSelectVariant: (KnownPatch, Int) -> Unit, + isSubOptionEnabled: (KnownPatch, PatchSubOption) -> Boolean, + onToggleSubOption: (KnownPatch, PatchSubOption, Boolean) -> Unit, isConfigValid: Boolean, onInstall: () -> Unit, @@ -171,6 +175,8 @@ fun PatchOptionsScreenContent( lockState = patchLockState, variantIndex = variantIndex, onSelectVariant = onSelectVariant, + isSubOptionEnabled = isSubOptionEnabled, + onToggleSubOption = onToggleSubOption, modifier = Modifier.padding(top = 4.dp), ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt index c706022..99d66f0 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt @@ -24,6 +24,7 @@ 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 +import com.meowarex.rlmobile.ui.screens.patchopts.PatchSubOption private data class LockInfo(val patch: KnownPatch, val lock: PatchLock) @@ -36,6 +37,8 @@ fun PatchSelectionAccordion( lockState: (KnownPatch) -> PatchLock, variantIndex: (KnownPatch) -> Int, onSelectVariant: (KnownPatch, Int) -> Unit, + isSubOptionEnabled: (KnownPatch, PatchSubOption) -> Boolean, + onToggleSubOption: (KnownPatch, PatchSubOption, Boolean) -> Unit, modifier: Modifier = Modifier, ) { var expanded by rememberSaveable { mutableStateOf(false) } @@ -131,6 +134,29 @@ fun PatchSelectionAccordion( } } } + + if (patch.subOptions.isNotEmpty()) { + AnimatedVisibility(visible = checked) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), + ) { + for (subOption in patch.subOptions) key(subOption) { + PatchSwitchRow( + title = stringResource(subOption.titleRes), + description = stringResource(subOption.descRes), + checked = isSubOptionEnabled(patch, subOption), + lock = PatchLock.Free, + onCheckedChange = { + onToggleSubOption(patch, subOption, it) + }, + onLockedTap = {}, + ) + } + } + } + } } } } diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index abe8cef..dee85e4 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -289,6 +289,10 @@ Redesigned Mini-Player Integrates the Seekbar into the control panel at the bottom of the screen. + Mini-Player Gestures + Swipe up on the mini-player to open the full player. + Next/Previous Gestures + Swipe left or right on the mini-player to skip tracks. Floating Grey Black diff --git a/patches/extension/com/tidal/android/feature/appscaffold/ui/q$c.smali b/patches/extension/com/tidal/android/feature/appscaffold/ui/q$c.smali new file mode 100644 index 0000000..2b3b99d --- /dev/null +++ b/patches/extension/com/tidal/android/feature/appscaffold/ui/q$c.smali @@ -0,0 +1,74 @@ +.class public final Lcom/tidal/android/feature/appscaffold/ui/q$c; +.super Lcom/tidal/android/feature/appscaffold/ui/q; +.source "SourceFile" + +# Synthetic mini-player event used by right swipe (previous/rewind). +# TIDAL already exposes q$a for next in this path + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lcom/tidal/android/feature/appscaffold/ui/q; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "c" +.end annotation + + +# static fields +.field public static final a:Lcom/tidal/android/feature/appscaffold/ui/q$c; + + +# direct methods +.method static constructor ()V + .locals 1 + + new-instance v0, Lcom/tidal/android/feature/appscaffold/ui/q$c; + + invoke-direct {v0}, Lcom/tidal/android/feature/appscaffold/ui/q;->()V + + sput-object v0, Lcom/tidal/android/feature/appscaffold/ui/q$c;->a:Lcom/tidal/android/feature/appscaffold/ui/q$c; + + return-void +.end method + + +# virtual methods +.method public final equals(Ljava/lang/Object;)Z + .locals 1 + + const/4 v0, 0x1 + + if-ne p0, p1, :cond_0 + + return v0 + + :cond_0 + instance-of p1, p1, Lcom/tidal/android/feature/appscaffold/ui/q$c; + + if-nez p1, :cond_1 + + const/4 p1, 0x0 + + return p1 + + :cond_1 + return v0 +.end method + +.method public final hashCode()I + .locals 1 + + const v0, -0x5d6a0f8 + + return v0 +.end method + +.method public final toString()Ljava/lang/String; + .locals 1 + + const-string v0, "PreviousButtonClicked" + + return-object v0 +.end method diff --git a/patches/extension/radiant/MiniPlayerGestures$ApplyPending.smali b/patches/extension/radiant/MiniPlayerGestures$ApplyPending.smali new file mode 100644 index 0000000..d54eaaf --- /dev/null +++ b/patches/extension/radiant/MiniPlayerGestures$ApplyPending.smali @@ -0,0 +1,62 @@ +.class public final Lradiant/MiniPlayerGestures$ApplyPending; +.super Ljava/lang/Object; +.implements Ljava/lang/Runnable; + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "ApplyPending" +.end annotation + + +# instance fields +.field public final a:Landroidx/compose/material3/SheetState; + +.field public final b:I + + +# direct methods +.method public constructor (Landroidx/compose/material3/SheetState;I)V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + iput-object p1, p0, Lradiant/MiniPlayerGestures$ApplyPending;->a:Landroidx/compose/material3/SheetState; + + iput p2, p0, Lradiant/MiniPlayerGestures$ApplyPending;->b:I + + return-void +.end method + + +# virtual methods +# Apply once, then re schedule if there is still pending movement +# -> Sheet anchors can appear one (or more) frames after opening the player, so this keeps applying the latest drag until no delta remains +.method public final run()V + .locals 3 + + iget-object v0, p0, Lradiant/MiniPlayerGestures$ApplyPending;->a:Landroidx/compose/material3/SheetState; + + invoke-static {v0}, Lradiant/MiniPlayerGestures;->applyPendingDrag(Landroidx/compose/material3/SheetState;)V + + iget v1, p0, Lradiant/MiniPlayerGestures$ApplyPending;->b:I + + add-int/lit8 v1, v1, -0x1 + + if-lez v1, :done + + invoke-static {v0}, Lradiant/MiniPlayerGestures;->needsApply(Landroidx/compose/material3/SheetState;)Z + + move-result v2 + + if-eqz v2, :done + + invoke-static {v0, v1}, Lradiant/MiniPlayerGestures;->scheduleApply(Landroidx/compose/material3/SheetState;I)V + + :done + return-void +.end method diff --git a/patches/extension/radiant/MiniPlayerGestures$Gesture.smali b/patches/extension/radiant/MiniPlayerGestures$Gesture.smali new file mode 100644 index 0000000..f7b00e2 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerGestures$Gesture.smali @@ -0,0 +1,408 @@ +.class public final Lradiant/MiniPlayerGestures$Gesture; +.super Ljava/lang/Object; +.implements Lyl0/l; + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "Gesture" +.end annotation + + +# instance fields +.field public final a:Lyl0/l; + +# Y at ACTION_DOWN +.field public b:F + +# Last Y seen (on this pointer stream) +.field public c:F + +# True while this listener owns an active pointer stream +.field public d:Z + +# True when sheet drag has started +.field public e:Z + +.field public final f:Landroidx/compose/material3/SheetState; + +.field public g:F + +# X at ACTION_DOWN +.field public i:F + +# True once horizontal movement wins (prevents later vertical start) +.field public j:Z + + +# direct methods +.method public constructor (Lyl0/l;Landroidx/compose/material3/SheetState;)V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + iput-object p1, p0, Lradiant/MiniPlayerGestures$Gesture;->a:Lyl0/l; + + iput-object p2, p0, Lradiant/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState; + + return-void +.end method + +.method private static dp(F)F + .locals 1 + + invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources; + + move-result-object v0 + + invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics; + + move-result-object v0 + + iget v0, v0, Landroid/util/DisplayMetrics;->density:F + + mul-float/2addr p0, v0 + + return p0 +.end method + +.method private dragSheet(FJ)V + .locals 1 + + iget-object v0, p0, Lradiant/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState; + + invoke-static {v0, p1, p2, p3}, Lradiant/MiniPlayerGestures;->updateDragTimed(Landroidx/compose/material3/SheetState;FJ)V + + return-void +.end method + +.method private finishSheet(F)V + .locals 2 + + iget-object v0, p0, Lradiant/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState; + + iget-object v1, p0, Lradiant/MiniPlayerGestures$Gesture;->a:Lyl0/l; + + invoke-static {v0, v1, p1}, Lradiant/MiniPlayerGestures;->finishDrag(Landroidx/compose/material3/SheetState;Lyl0/l;F)V + + return-void +.end method + +.method private openPlayer()V + .locals 2 + + iget-object v0, p0, Lradiant/MiniPlayerGestures$Gesture;->a:Lyl0/l; + + sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/b$b;->a:Lcom/tidal/android/feature/appscaffold/ui/b$b; + + invoke-interface {v0, v1}, Lyl0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object; + + return-void +.end method + +.method private reset()V + .locals 1 + + const/4 v0, 0x0 + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->d:Z + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->j:Z + + const/4 v0, 0x0 + + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->i:F + + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->g:F + + return-void +.end method + +.method private startSheet(FJ)V + .locals 2 + + # empty player guard + invoke-static {}, Lradiant/MiniPlayerGestures;->hasMiniPlayerMedia()Z + + move-result v0 + + if-nez v0, :start_guard + + return-void + + :start_guard + + # avoid starting twice + iget-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + if-eqz v0, :start + + return-void + + :start + const/4 v0, 0x1 + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + iget-object v1, p0, Lradiant/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState; + + iget v0, p0, Lradiant/MiniPlayerGestures$Gesture;->b:F + + invoke-static {v1, p1, v0, p2, p3}, Lradiant/MiniPlayerGestures;->beginDrag(Landroidx/compose/material3/SheetState;FFJ)V + + invoke-direct {p0}, Lradiant/MiniPlayerGestures$Gesture;->openPlayer()V + + return-void +.end method + + +# virtual methods +# Listens for MotionEvents +.method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u; + .locals 11 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I + + move-result v0 + + if-eqz v0, :down + + const/4 v1, 0x1 + + if-eq v0, v1, :up + + const/4 v1, 0x2 + + if-eq v0, v1, :move + + const/4 v1, 0x3 + + if-eq v0, v1, :cancel + + goto :done + + # ACTION_DOWN + :down + invoke-static {p1}, Lradiant/MiniPlayerGestures;->beginMotion(Landroid/view/MotionEvent;)V + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v0 + + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->b:F + + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->c:F + + const/4 v0, 0x1 + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->d:Z + + const/4 v0, 0x0 + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + const/4 v0, 0x0 + + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->g:F + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F + + move-result v0 + + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->i:F + + const/4 v0, 0x0 + + iput-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->j:Z + + goto :done + + # ACTION_MOVE + :move + iget-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->d:Z + + if-eqz v0, :done + + invoke-static {p1}, Lradiant/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v0 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J + + move-result-wide v5 + + iget v1, p0, Lradiant/MiniPlayerGestures$Gesture;->c:F + + sub-float v1, v0, v1 + + iput v1, p0, Lradiant/MiniPlayerGestures$Gesture;->g:F + + iget v2, p0, Lradiant/MiniPlayerGestures$Gesture;->b:F + + sub-float v2, v0, v2 + + iget-boolean v3, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + if-eqz v3, :maybe_start + + invoke-direct {p0, v2, v5, v6}, Lradiant/MiniPlayerGestures$Gesture;->dragSheet(FJ)V + + goto :store_last + + :maybe_start + iget-boolean v3, p0, Lradiant/MiniPlayerGestures$Gesture;->j:Z + + if-eqz v3, :axis_check + + goto :store_last + + :axis_check + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F + + move-result v7 + + iget v8, p0, Lradiant/MiniPlayerGestures$Gesture;->i:F + + sub-float/2addr v7, v8 + + invoke-static {v7}, Ljava/lang/Math;->abs(F)F + + move-result v7 + + invoke-static {v2}, Ljava/lang/Math;->abs(F)F + + move-result v8 + + const/high16 v9, 0x41800000 # 16.0f + + invoke-static {v9}, Lradiant/MiniPlayerGestures$Gesture;->dp(F)F + + move-result v9 + + cmpg-float v10, v7, v9 + + if-gez v10, :vertical_check + + const/high16 v10, 0x3fc00000 # 1.5f + + mul-float/2addr v10, v8 + + cmpl-float v10, v7, v10 + + if-lez v10, :vertical_check + + const/4 v10, 0x1 + + iput-boolean v10, p0, Lradiant/MiniPlayerGestures$Gesture;->j:Z + + goto :store_last + + # At least 16dp up to open the player + :vertical_check + neg-float v9, v9 + + cmpg-float v10, v2, v9 + + if-lez v10, :vertical_dominance + + goto :store_last + + :vertical_dominance + const/high16 v10, 0x3fc00000 # 1.5f -> has to dominate horizontal mov. by 1.5x + + mul-float/2addr v7, v10 + + cmpl-float v10, v8, v7 + + if-gtz v10, :begin + + goto :store_last + + :begin + invoke-direct {p0, v2, v5, v6}, Lradiant/MiniPlayerGestures$Gesture;->startSheet(FJ)V + + invoke-direct {p0, v2, v5, v6}, Lradiant/MiniPlayerGestures$Gesture;->dragSheet(FJ)V + + + :store_last + iput v0, p0, Lradiant/MiniPlayerGestures$Gesture;->c:F + + goto :done + + # ACTION_UP + :up + iget-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->d:Z + + if-eqz v0, :done + + iget-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + if-eqz v0, :finish + + invoke-static {p1}, Lradiant/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v0 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J + + move-result-wide v5 + + iget-object v1, p0, Lradiant/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState; + + invoke-static {v1, v0, v5, v6}, Lradiant/MiniPlayerGestures;->updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V + + invoke-static {v1}, Lradiant/MiniPlayerGestures;->releaseVelocity(Landroidx/compose/material3/SheetState;)F + + move-result v0 + + invoke-direct {p0, v0}, Lradiant/MiniPlayerGestures$Gesture;->finishSheet(F)V + + + :finish + invoke-direct {p0}, Lradiant/MiniPlayerGestures$Gesture;->reset()V + + goto :done + + # ACTION_CANCEL + :cancel + iget-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z + + if-eqz v0, :cancel_reset + + iget-object v0, p0, Lradiant/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState; + + iget-object v1, p0, Lradiant/MiniPlayerGestures$Gesture;->a:Lyl0/l; + + invoke-static {v0, v1}, Lradiant/MiniPlayerGestures;->cancelDrag(Landroidx/compose/material3/SheetState;Lyl0/l;)V + + :cancel_reset + invoke-direct {p0}, Lradiant/MiniPlayerGestures$Gesture;->reset()V + + + :done + sget-object p1, Lkotlin/u;->a:Lkotlin/u; + + return-object p1 +.end method + +.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; + .locals 0 + + check-cast p1, Landroid/view/MotionEvent; + + invoke-virtual {p0, p1}, Lradiant/MiniPlayerGestures$Gesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u; + + move-result-object p1 + + return-object p1 +.end method diff --git a/patches/extension/radiant/MiniPlayerGestures$RootGesture.smali b/patches/extension/radiant/MiniPlayerGestures$RootGesture.smali new file mode 100644 index 0000000..27b0319 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerGestures$RootGesture.smali @@ -0,0 +1,129 @@ +.class public final Lradiant/MiniPlayerGestures$RootGesture; +.super Ljava/lang/Object; +.implements Lyl0/l; + + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "RootGesture" +.end annotation + + +# instance fields +.field public final a:Lyl0/l; + +.field public final b:Landroidx/compose/material3/SheetState; + + +# direct methods +.method public constructor (Lyl0/l;Landroidx/compose/material3/SheetState;)V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + iput-object p1, p0, Lradiant/MiniPlayerGestures$RootGesture;->a:Lyl0/l; + + iput-object p2, p0, Lradiant/MiniPlayerGestures$RootGesture;->b:Landroidx/compose/material3/SheetState; + + return-void +.end method + + +# virtual methods +.method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u; + .locals 6 + + iget-object v0, p0, Lradiant/MiniPlayerGestures$RootGesture;->b:Landroidx/compose/material3/SheetState; + + invoke-static {v0}, Lradiant/MiniPlayerGestures;->isDragging(Landroidx/compose/material3/SheetState;)Z + + move-result v1 + + if-eqz v1, :done + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I + + move-result v1 + + const/4 v2, 0x1 + + if-eq v1, v2, :up + + const/4 v2, 0x2 + + if-eq v1, v2, :move + + const/4 v2, 0x3 + + if-eq v1, v2, :cancel + + goto :done + + # ACTION_MOVE + :move + invoke-static {p1}, Lradiant/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v2 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J + + move-result-wide v4 + + invoke-static {v0, v2, v4, v5}, Lradiant/MiniPlayerGestures;->updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V + + goto :done + + # ACTION_UP + :up + invoke-static {p1}, Lradiant/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v1 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J + + move-result-wide v4 + + invoke-static {v0, v1, v4, v5}, Lradiant/MiniPlayerGestures;->updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V + + invoke-static {v0}, Lradiant/MiniPlayerGestures;->releaseVelocity(Landroidx/compose/material3/SheetState;)F + + move-result v1 + + iget-object v2, p0, Lradiant/MiniPlayerGestures$RootGesture;->a:Lyl0/l; + + invoke-static {v0, v2, v1}, Lradiant/MiniPlayerGestures;->finishDrag(Landroidx/compose/material3/SheetState;Lyl0/l;F)V + + goto :done + + # ACTION_CANCEL + :cancel + iget-object v2, p0, Lradiant/MiniPlayerGestures$RootGesture;->a:Lyl0/l; + + invoke-static {v0, v2}, Lradiant/MiniPlayerGestures;->cancelDrag(Landroidx/compose/material3/SheetState;Lyl0/l;)V + + :done + sget-object p1, Lkotlin/u;->a:Lkotlin/u; + + return-object p1 +.end method + +.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; + .locals 0 + + check-cast p1, Landroid/view/MotionEvent; + + invoke-virtual {p0, p1}, Lradiant/MiniPlayerGestures$RootGesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u; + + move-result-object p1 + + return-object p1 +.end method diff --git a/patches/extension/radiant/MiniPlayerGestures.smali b/patches/extension/radiant/MiniPlayerGestures.smali new file mode 100644 index 0000000..a98c909 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerGestures.smali @@ -0,0 +1,1049 @@ +.class public final Lradiant/MiniPlayerGestures; +.super Ljava/lang/Object; + + +# static fields +.field private static a:Landroidx/compose/material3/SheetState; + +# True while a swipe up drag owns the sheet +.field private static b:Z + +# Current drag offset (relative Y) +.field private static c:F + +# Amount of drag already dispatched to AnchoredDraggableState +.field private static d:F + +# Y where the swipe up drag started +.field private static e:F + +# Fallback Y velocity +.field private static f:F + +.field private static g:Landroidx/compose/material3/SheetState; + +# Material3 settle lambda +.field private static h:Lyl0/l; + +# Latest MotionEvent eventTime for drag updates +.field private static i:J + +# Previous drag offset for fallback velocity +.field private static j:F + +# Previous eventTime for fallback velocity +.field private static k:J + +# Fallback X velocity (used to prevent horizontal swipe triggering drag) +.field private static l:F + +# Previous X for fallback velocity +.field private static m:F + +# Latest X seen by the drag tracker +.field private static n:F + +.field private static o:Landroid/view/VelocityTracker; + +.field private static p:I + +# Mini player media state +.field private static r:Z + + +# direct methods +.method private constructor ()V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + +.method public static applyPendingDrag(Landroidx/compose/material3/SheetState;)V + .locals 5 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + if-eqz v0, :done + + sget-object v0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :done + + invoke-virtual {p0}, Landroidx/compose/material3/SheetState;->getHasExpandedState()Z + + move-result v0 + + if-eqz v0, :done + + invoke-virtual {p0}, Landroidx/compose/material3/SheetState;->getAnchoredDraggableState$material3_release()Landroidx/compose/material3/internal/AnchoredDraggableState; + + move-result-object p0 + + invoke-virtual {p0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->getAnchors()Landroidx/compose/material3/internal/DraggableAnchors; + + move-result-object v4 + + invoke-interface {v4}, Landroidx/compose/material3/internal/DraggableAnchors;->maxAnchor()F + + move-result v4 + + # targetOffset = startRawY + dragOffset - hiddenAnchor. + sget v0, Lradiant/MiniPlayerGestures;->e:F + + sget v1, Lradiant/MiniPlayerGestures;->c:F + + add-float/2addr v0, v1 + + sub-float/2addr v0, v4 + + sget v1, Lradiant/MiniPlayerGestures;->d:F + + sub-float v2, v0, v1 + + const/4 v3, 0x0 + + cmpl-float v3, v2, v3 + + if-eqz v3, :done + + invoke-virtual {p0, v2}, Landroidx/compose/material3/internal/AnchoredDraggableState;->dispatchRawDelta(F)F + + move-result v2 + + sget v0, Lradiant/MiniPlayerGestures;->d:F + + add-float/2addr v0, v2 + + sput v0, Lradiant/MiniPlayerGestures;->d:F + + :done + return-void +.end method + +.method private static addRawMovement(Landroid/view/VelocityTracker;Landroid/view/MotionEvent;)V + .locals 4 + + invoke-static {p1}, Landroid/view/MotionEvent;->obtain(Landroid/view/MotionEvent;)Landroid/view/MotionEvent; + + move-result-object v0 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F + + move-result v1 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getX()F + + move-result v2 + + sub-float/2addr v1, v2 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v2 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getY()F + + move-result v3 + + sub-float/2addr v2, v3 + + invoke-virtual {v0, v1, v2}, Landroid/view/MotionEvent;->offsetLocation(FF)V + + invoke-virtual {p0, v0}, Landroid/view/VelocityTracker;->addMovement(Landroid/view/MotionEvent;)V + + invoke-virtual {v0}, Landroid/view/MotionEvent;->recycle()V + + return-void +.end method + +.method public static beginDrag(Landroidx/compose/material3/SheetState;FFJ)V + .locals 1 + + sput-object p0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + const/4 v0, 0x1 + + sput-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + sput p1, Lradiant/MiniPlayerGestures;->c:F + + const/4 v0, 0x0 + + sput-object v0, Lradiant/MiniPlayerGestures;->g:Landroidx/compose/material3/SheetState; + + sput-object v0, Lradiant/MiniPlayerGestures;->h:Lyl0/l; + + sput v0, Lradiant/MiniPlayerGestures;->d:F + + sput p2, Lradiant/MiniPlayerGestures;->e:F + + sput v0, Lradiant/MiniPlayerGestures;->f:F + + sput p1, Lradiant/MiniPlayerGestures;->j:F + + sput v0, Lradiant/MiniPlayerGestures;->l:F + + sget v0, Lradiant/MiniPlayerGestures;->n:F + + sput v0, Lradiant/MiniPlayerGestures;->m:F + + sput-wide p3, Lradiant/MiniPlayerGestures;->i:J + + sput-wide p3, Lradiant/MiniPlayerGestures;->k:J + + # Try immediately then retry because anchors can be created one frame later + invoke-static {p0}, Lradiant/MiniPlayerGestures;->applyPendingDrag(Landroidx/compose/material3/SheetState;)V + + const/16 v0, 0xa + + invoke-static {p0, v0}, Lradiant/MiniPlayerGestures;->scheduleApply(Landroidx/compose/material3/SheetState;I)V + + return-void +.end method + +.method public static beginMotion(Landroid/view/MotionEvent;)V + .locals 2 + + sget-object v0, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + if-eqz v0, :obtain + + invoke-virtual {v0}, Landroid/view/VelocityTracker;->recycle()V + + :obtain + invoke-static {}, Landroid/view/VelocityTracker;->obtain()Landroid/view/VelocityTracker; + + move-result-object v0 + + sput-object v0, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + const/4 v1, 0x0 + + invoke-virtual {p0, v1}, Landroid/view/MotionEvent;->getPointerId(I)I + + move-result v1 + + sput v1, Lradiant/MiniPlayerGestures;->p:I + + invoke-static {v0, p0}, Lradiant/MiniPlayerGestures;->addRawMovement(Landroid/view/VelocityTracker;Landroid/view/MotionEvent;)V + + invoke-virtual {p0}, Landroid/view/MotionEvent;->getRawX()F + + move-result v1 + + sput v1, Lradiant/MiniPlayerGestures;->n:F + + return-void +.end method + +.method public static endDrag(Landroidx/compose/material3/SheetState;)V + .locals 3 + + sget-object v0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :done + + const/4 v0, 0x0 + + sput-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + sput v0, Lradiant/MiniPlayerGestures;->c:F + + sput v0, Lradiant/MiniPlayerGestures;->d:F + + sput v0, Lradiant/MiniPlayerGestures;->e:F + + sput v0, Lradiant/MiniPlayerGestures;->f:F + + sput v0, Lradiant/MiniPlayerGestures;->j:F + + sput v0, Lradiant/MiniPlayerGestures;->l:F + + sput v0, Lradiant/MiniPlayerGestures;->m:F + + sput v0, Lradiant/MiniPlayerGestures;->n:F + + sget-object v2, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + if-eqz v2, :motion_recycled + + invoke-virtual {v2}, Landroid/view/VelocityTracker;->recycle()V + + const/4 v2, 0x0 + + sput-object v2, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + :motion_recycled + + const/4 v1, 0x0 + + sput-object v1, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + const-wide/16 v0, 0x0 + + sput-wide v0, Lradiant/MiniPlayerGestures;->k:J + + :done + return-void +.end method + +.method public static cancelDrag(Landroidx/compose/material3/SheetState;Lyl0/l;)V + .locals 1 + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->isDragging(Landroidx/compose/material3/SheetState;)Z + + move-result v0 + + if-eqz v0, :done + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->endDrag(Landroidx/compose/material3/SheetState;)V + + sget-object p0, Lcom/tidal/android/feature/appscaffold/ui/b$a;->a:Lcom/tidal/android/feature/appscaffold/ui/b$a; + + invoke-interface {p1, p0}, Lyl0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object; + + :done + return-void +.end method + +.method public static finishDrag(Landroidx/compose/material3/SheetState;Lyl0/l;F)V + .locals 6 + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->isDragging(Landroidx/compose/material3/SheetState;)Z + + move-result v0 + + if-eqz v0, :done + + sget-object v0, Lradiant/MiniPlayerGestures;->g:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :fallback_snap + + sget-object v0, Lradiant/MiniPlayerGestures;->h:Lyl0/l; + + if-eqz v0, :fallback_snap + + invoke-static {p0, p2}, Lradiant/MiniPlayerGestures;->prepareSettleVelocity(Landroidx/compose/material3/SheetState;F)F + + move-result p2 + + :native_settle + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->endDrag(Landroidx/compose/material3/SheetState;)V + + invoke-static {p2}, Ljava/lang/Float;->valueOf(F)Ljava/lang/Float; + + move-result-object p0 + + invoke-interface {v0, p0}, Lyl0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object; + + goto/16 :done + + :fallback_snap + + invoke-virtual {p0}, Landroidx/compose/material3/SheetState;->getAnchoredDraggableState$material3_release()Landroidx/compose/material3/internal/AnchoredDraggableState; + + move-result-object v0 + + invoke-virtual {v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->getAnchors()Landroidx/compose/material3/internal/DraggableAnchors; + + move-result-object v1 + + const/4 v5, 0x0 + + cmpl-float v3, p2, v5 + + if-gtz v3, :target_hidden + + cmpg-float v3, p2, v5 + + if-ltz v3, :target_expanded + + invoke-virtual {v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->getOffset()F + + move-result v4 + + invoke-interface {v1}, Landroidx/compose/material3/internal/DraggableAnchors;->minAnchor()F + + move-result v5 + + invoke-interface {v1}, Landroidx/compose/material3/internal/DraggableAnchors;->maxAnchor()F + + move-result p2 + + add-float/2addr v5, p2 + + const/high16 p2, 0x40000000 # 2.0f + + div-float/2addr v5, p2 + + cmpg-float p2, v4, v5 + + if-ltz p2, :target_expanded + + goto :target_hidden + + :target_hidden + sget-object v2, Landroidx/compose/material3/SheetValue;->Hidden:Landroidx/compose/material3/SheetValue; + + goto :target + + :target_expanded + + sget-object v2, Landroidx/compose/material3/SheetValue;->Expanded:Landroidx/compose/material3/SheetValue; + + :target + invoke-interface {v1, v2}, Landroidx/compose/material3/internal/DraggableAnchors;->hasAnchorFor(Ljava/lang/Object;)Z + + move-result p2 + + if-eqz p2, :close + + invoke-interface {v1, v2}, Landroidx/compose/material3/internal/DraggableAnchors;->positionOf(Ljava/lang/Object;)F + + move-result v3 + + invoke-virtual {v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->getOffset()F + + move-result v4 + + invoke-static {v4}, Ljava/lang/Float;->isNaN(F)Z + + move-result v5 + + if-eqz v5, :have_current + + const/4 v4, 0x0 + + :have_current + sub-float/2addr v3, v4 + + invoke-virtual {v0, v3}, Landroidx/compose/material3/internal/AnchoredDraggableState;->dispatchRawDelta(F)F + + invoke-static {v0, v2}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setCurrentValue(Landroidx/compose/material3/internal/AnchoredDraggableState;Ljava/lang/Object;)V + + invoke-static {v0, v2}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setDragTarget(Landroidx/compose/material3/internal/AnchoredDraggableState;Ljava/lang/Object;)V + + const/4 v3, 0x0 + + invoke-static {v0, v3}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setLastVelocity(Landroidx/compose/material3/internal/AnchoredDraggableState;F)V + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->endDrag(Landroidx/compose/material3/SheetState;)V + + sget-object v3, Landroidx/compose/material3/SheetValue;->Hidden:Landroidx/compose/material3/SheetValue; + + if-ne v2, v3, :done + + :close + invoke-static {p0}, Lradiant/MiniPlayerGestures;->endDrag(Landroidx/compose/material3/SheetState;)V + + sget-object p0, Lcom/tidal/android/feature/appscaffold/ui/b$a;->a:Lcom/tidal/android/feature/appscaffold/ui/b$a; + + invoke-interface {p1, p0}, Lyl0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object; + + :done + return-void +.end method + +.method public static isDragging(Landroidx/compose/material3/SheetState;)Z + .locals 1 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + if-eqz v0, :no + + sget-object v0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :no + + const/4 v0, 0x1 + + return v0 + + :no + const/4 v0, 0x0 + + return v0 +.end method + +.method public static hasMiniPlayerMedia()Z + .locals 1 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->r:Z + + return v0 +.end method + +.method public static setHasMiniPlayerMedia(Z)V + .locals 0 + + sput-boolean p0, Lradiant/MiniPlayerGestures;->r:Z + + return-void +.end method + +.method private static clampReleaseVelocity(F)F + .locals 4 + + invoke-static {p0}, Ljava/lang/Math;->abs(F)F + + move-result v0 + + invoke-static {}, Lradiant/MiniPlayerGestures;->minFlingVelocity()F + + move-result v1 + + cmpg-float v2, v0, v1 + + if-gez v2, :check_max + + const/4 p0, 0x0 + + return p0 + + :check_max + const v1, 0x469c4000 # 20000.0f + + cmpl-float v2, v0, v1 + + if-lez v2, :return_velocity + + const/4 v2, 0x0 + + cmpl-float v3, p0, v2 + + if-lez v3, :negative + + move p0, v1 + + return p0 + + :negative + neg-float p0, v1 + + :return_velocity + return p0 +.end method + +.method public static modifier(Landroidx/compose/ui/Modifier;Lyl0/l;Landroidx/compose/material3/SheetState;)Landroidx/compose/ui/Modifier; + .locals 1 + + new-instance v0, Lradiant/MiniPlayerGestures$Gesture; + + invoke-direct {v0, p1, p2}, Lradiant/MiniPlayerGestures$Gesture;->(Lyl0/l;Landroidx/compose/material3/SheetState;)V + + invoke-static {p0, v0}, Landroidx/compose/ui/input/pointer/PointerInteropFilter_androidKt;->motionEventSpy(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; + + move-result-object p0 + + return-object p0 +.end method + +.method private static minFlingVelocity()F + .locals 2 + + invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources; + + move-result-object v0 + + invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics; + + move-result-object v0 + + iget v0, v0, Landroid/util/DisplayMetrics;->density:F + + const/high16 v1, 0x42480000 # 50.0f + + mul-float/2addr v0, v1 + + return v0 +.end method + +.method private static prepareSettleVelocity(Landroidx/compose/material3/SheetState;F)F + .locals 7 + + invoke-virtual {p0}, Landroidx/compose/material3/SheetState;->getAnchoredDraggableState$material3_release()Landroidx/compose/material3/internal/AnchoredDraggableState; + + move-result-object p0 + + const/4 v6, 0x0 + + cmpl-float v0, p1, v6 + + if-gtz v0, :target_hidden + + cmpg-float v0, p1, v6 + + if-ltz v0, :target_expanded + + invoke-virtual {p0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->getAnchors()Landroidx/compose/material3/internal/DraggableAnchors; + + move-result-object v0 + + invoke-virtual {p0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->getOffset()F + + move-result v1 + + invoke-interface {v0}, Landroidx/compose/material3/internal/DraggableAnchors;->minAnchor()F + + move-result v2 + + invoke-interface {v0}, Landroidx/compose/material3/internal/DraggableAnchors;->maxAnchor()F + + move-result v3 + + add-float/2addr v2, v3 + + const/high16 v3, 0x40000000 # 2.0f + + div-float/2addr v2, v3 + + cmpg-float v0, v1, v2 + + if-ltz v0, :target_expanded + + goto :target_hidden + + :target_expanded + sget-object v0, Landroidx/compose/material3/SheetValue;->Hidden:Landroidx/compose/material3/SheetValue; + + invoke-static {p0, v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setCurrentValue(Landroidx/compose/material3/internal/AnchoredDraggableState;Ljava/lang/Object;)V + + invoke-static {p0, v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setDragTarget(Landroidx/compose/material3/internal/AnchoredDraggableState;Ljava/lang/Object;)V + + invoke-static {p1}, Ljava/lang/Math;->abs(F)F + + move-result p1 + + const v0, 0x453b8000 # 3000.0f + + cmpg-float v1, p1, v0 + + if-gez v1, :open_have_min + + move p1, v0 + + :open_have_min + neg-float p1, p1 + + invoke-static {p0, p1}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setLastVelocity(Landroidx/compose/material3/internal/AnchoredDraggableState;F)V + + return p1 + + :target_hidden + sget-object v0, Landroidx/compose/material3/SheetValue;->Expanded:Landroidx/compose/material3/SheetValue; + + invoke-static {p0, v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setCurrentValue(Landroidx/compose/material3/internal/AnchoredDraggableState;Ljava/lang/Object;)V + + invoke-static {p0, v0}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setDragTarget(Landroidx/compose/material3/internal/AnchoredDraggableState;Ljava/lang/Object;)V + + invoke-static {p1}, Ljava/lang/Math;->abs(F)F + + move-result p1 + + const v0, 0x453b8000 # 3000.0f + + cmpg-float v1, p1, v0 + + if-gez v1, :hidden_have_min + + move p1, v0 + + :hidden_have_min + invoke-static {p0, p1}, Landroidx/compose/material3/internal/AnchoredDraggableState;->access$setLastVelocity(Landroidx/compose/material3/internal/AnchoredDraggableState;F)V + + return p1 +.end method + +.method public static releaseVelocity(Landroidx/compose/material3/SheetState;)F + .locals 6 + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->isDragging(Landroidx/compose/material3/SheetState;)Z + + move-result p0 + + if-eqz p0, :zero + + sget-object v0, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + if-eqz v0, :sampled_velocity + + const/16 v1, 0x3e8 + + const v2, 0x469c4000 # 20000.0f + + invoke-virtual {v0, v1, v2}, Landroid/view/VelocityTracker;->computeCurrentVelocity(IF)V + + sget v1, Lradiant/MiniPlayerGestures;->p:I + + invoke-virtual {v0, v1}, Landroid/view/VelocityTracker;->getYVelocity(I)F + + move-result p0 + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->clampReleaseVelocity(F)F + + move-result p0 + + sget v1, Lradiant/MiniPlayerGestures;->p:I + + invoke-virtual {v0, v1}, Landroid/view/VelocityTracker;->getXVelocity(I)F + + move-result v0 + + invoke-static {v0}, Lradiant/MiniPlayerGestures;->clampReleaseVelocity(F)F + + move-result v0 + + goto :have_velocity + + :sampled_velocity + sget p0, Lradiant/MiniPlayerGestures;->f:F + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->clampReleaseVelocity(F)F + + move-result p0 + + sget v0, Lradiant/MiniPlayerGestures;->l:F + + invoke-static {v0}, Lradiant/MiniPlayerGestures;->clampReleaseVelocity(F)F + + move-result v0 + + :have_velocity + + const/4 v3, 0x0 + + cmpl-float v1, p0, v3 + + if-lez v1, :return_done + + invoke-static {v0}, Ljava/lang/Math;->abs(F)F + + move-result v0 + + invoke-static {p0}, Ljava/lang/Math;->abs(F)F + + move-result v1 + + cmpl-float v2, v0, v1 + + if-lez v2, :return_done + + const/4 p0, 0x0 + + :return_done + + return p0 + + :zero + const/4 p0, 0x0 + + return p0 +.end method + +.method public static rootModifier(Landroidx/compose/ui/Modifier;Lyl0/l;Landroidx/compose/material3/SheetState;)Landroidx/compose/ui/Modifier; + .locals 1 + + new-instance v0, Lradiant/MiniPlayerGestures$RootGesture; + + invoke-direct {v0, p1, p2}, Lradiant/MiniPlayerGestures$RootGesture;->(Lyl0/l;Landroidx/compose/material3/SheetState;)V + + invoke-static {p0, v0}, Landroidx/compose/ui/input/pointer/PointerInteropFilter_androidKt;->motionEventSpy(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; + + move-result-object p0 + + return-object p0 +.end method + +.method public static setSettleCallback(Landroidx/compose/material3/SheetState;Lyl0/l;)V + .locals 0 + + sput-object p0, Lradiant/MiniPlayerGestures;->g:Landroidx/compose/material3/SheetState; + + sput-object p1, Lradiant/MiniPlayerGestures;->h:Lyl0/l; + + return-void +.end method + +.method public static trackMotion(Landroid/view/MotionEvent;)V + .locals 2 + + sget-object v0, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + if-nez v0, :have_tracker + + invoke-static {}, Landroid/view/VelocityTracker;->obtain()Landroid/view/VelocityTracker; + + move-result-object v0 + + sput-object v0, Lradiant/MiniPlayerGestures;->o:Landroid/view/VelocityTracker; + + :have_tracker + invoke-static {v0, p0}, Lradiant/MiniPlayerGestures;->addRawMovement(Landroid/view/VelocityTracker;Landroid/view/MotionEvent;)V + + invoke-virtual {p0}, Landroid/view/MotionEvent;->getRawX()F + + move-result v1 + + sput v1, Lradiant/MiniPlayerGestures;->n:F + + return-void +.end method + +.method public static needsApply(Landroidx/compose/material3/SheetState;)Z + .locals 3 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + if-eqz v0, :no + + sget-object v0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :no + + sget v0, Lradiant/MiniPlayerGestures;->c:F + + sget v1, Lradiant/MiniPlayerGestures;->d:F + + sub-float/2addr v0, v1 + + invoke-static {v0}, Ljava/lang/Math;->abs(F)F + + move-result v0 + + const/high16 v1, 0x3f000000 # 0.5f + + cmpl-float v2, v0, v1 + + if-lez v2, :no + + const/4 v0, 0x1 + + return v0 + + :no + const/4 v0, 0x0 + + return v0 +.end method + +.method public static scheduleApply(Landroidx/compose/material3/SheetState;I)V + .locals 4 + + if-lez p1, :done + + new-instance v0, Landroid/os/Handler; + + invoke-static {}, Landroid/os/Looper;->getMainLooper()Landroid/os/Looper; + + move-result-object v1 + + invoke-direct {v0, v1}, Landroid/os/Handler;->(Landroid/os/Looper;)V + + new-instance v1, Lradiant/MiniPlayerGestures$ApplyPending; + + invoke-direct {v1, p0, p1}, Lradiant/MiniPlayerGestures$ApplyPending;->(Landroidx/compose/material3/SheetState;I)V + + const-wide/16 v2, 0x10 + + invoke-virtual {v0, v1, v2, v3}, Landroid/os/Handler;->postDelayed(Ljava/lang/Runnable;J)Z + + :done + return-void +.end method + +.method public static updateDragTimed(Landroidx/compose/material3/SheetState;FJ)V + .locals 8 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + if-eqz v0, :done + + sget-object v0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :done + + sget v0, Lradiant/MiniPlayerGestures;->c:F + + sub-float v0, p1, v0 + + const/4 v1, 0x0 + + cmpl-float v1, v0, v1 + + if-eqz v1, :no_movement + + sput-wide p2, Lradiant/MiniPlayerGestures;->i:J + + sput p1, Lradiant/MiniPlayerGestures;->c:F + + sget-wide v1, Lradiant/MiniPlayerGestures;->k:J + + sub-long v1, p2, v1 + + const-wide/16 v3, 0x28 + + cmp-long v5, v1, v3 + + if-gez v5, :apply + + :sample_velocity + sget v0, Lradiant/MiniPlayerGestures;->j:F + + sub-float v0, p1, v0 + + long-to-float v1, v1 + + const/high16 v2, 0x447a0000 # 1000.0f + + sget v6, Lradiant/MiniPlayerGestures;->n:F + + sget v7, Lradiant/MiniPlayerGestures;->m:F + + sub-float v6, v6, v7 + + mul-float/2addr v6, v2 + + div-float/2addr v6, v1 + + mul-float/2addr v0, v2 + + div-float/2addr v0, v1 + + const v1, 0x469c4000 # 20000.0f + + cmpl-float v2, v0, v1 + + if-lez v2, :check_min_velocity + + move v0, v1 + + goto :store_velocity + + :check_min_velocity + + const v1, -0x3963c000 # -20000.0f + + cmpg-float v2, v0, v1 + + if-gez v2, :store_velocity + + move v0, v1 + + :store_velocity + sput v0, Lradiant/MiniPlayerGestures;->f:F + + const v1, 0x469c4000 # 20000.0f + + cmpl-float v2, v6, v1 + + if-lez v2, :check_min_x_velocity + + move v6, v1 + + goto :store_x_velocity + + :check_min_x_velocity + + const v1, -0x3963c000 # -20000.0f + + cmpg-float v2, v6, v1 + + if-gez v2, :store_x_velocity + + move v6, v1 + + :store_x_velocity + sput v6, Lradiant/MiniPlayerGestures;->l:F + + sput p1, Lradiant/MiniPlayerGestures;->j:F + + sget v6, Lradiant/MiniPlayerGestures;->n:F + + sput v6, Lradiant/MiniPlayerGestures;->m:F + + sput-wide p2, Lradiant/MiniPlayerGestures;->k:J + + goto :apply + + :no_movement + sget-wide v1, Lradiant/MiniPlayerGestures;->k:J + + sub-long v1, p2, v1 + + const-wide/16 v3, 0x50 + + cmp-long v5, v1, v3 + + if-gtz v5, :reset_velocity + + goto :apply + + :reset_velocity + + const/4 v0, 0x0 + + sput v0, Lradiant/MiniPlayerGestures;->f:F + + sput v0, Lradiant/MiniPlayerGestures;->l:F + + sput p1, Lradiant/MiniPlayerGestures;->j:F + + sget v0, Lradiant/MiniPlayerGestures;->n:F + + sput v0, Lradiant/MiniPlayerGestures;->m:F + + sput-wide p2, Lradiant/MiniPlayerGestures;->k:J + + :apply + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->applyPendingDrag(Landroidx/compose/material3/SheetState;)V + + const/4 v0, 0x3 + + invoke-static {p0, v0}, Lradiant/MiniPlayerGestures;->scheduleApply(Landroidx/compose/material3/SheetState;I)V + + :done + return-void +.end method + +.method public static updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V + .locals 1 + + sget v0, Lradiant/MiniPlayerGestures;->e:F + + sub-float/2addr p1, v0 + + invoke-static {p0, p1, p2, p3}, Lradiant/MiniPlayerGestures;->updateDragTimed(Landroidx/compose/material3/SheetState;FJ)V + + return-void +.end method + +.method public static suppressAutoShow(Landroidx/compose/material3/SheetState;)Z + .locals 1 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + if-eqz v0, :no + + sget-object v0, Lradiant/MiniPlayerGestures;->a:Landroidx/compose/material3/SheetState; + + if-ne v0, p0, :no + + invoke-static {p0}, Lradiant/MiniPlayerGestures;->applyPendingDrag(Landroidx/compose/material3/SheetState;)V + + const/4 v0, 0x5 + + invoke-static {p0, v0}, Lradiant/MiniPlayerGestures;->scheduleApply(Landroidx/compose/material3/SheetState;I)V + + const/4 v0, 0x1 + + return v0 + + :no + const/4 v0, 0x0 + + return v0 +.end method diff --git a/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali b/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali new file mode 100644 index 0000000..44239a3 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali @@ -0,0 +1,220 @@ +.class public final Lradiant/MiniPlayerTrackGestures$Gesture; +.super Ljava/lang/Object; +.implements Lyl0/l; + + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerTrackGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "Gesture" +.end annotation + + +# instance fields +.field public final a:Lyl0/l; + +# X at ACTION_DOWN +.field public b:F + +# Y at ACTION_DOWN +.field public c:F + +.field public d:Z + + +# direct methods +.method public constructor (Lyl0/l;)V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + iput-object p1, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->a:Lyl0/l; + + return-void +.end method + +.method private static dp(F)F + .locals 1 + + invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources; + + move-result-object v0 + + invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics; + + move-result-object v0 + + iget v0, v0, Landroid/util/DisplayMetrics;->density:F + + mul-float/2addr p0, v0 + + return p0 +.end method + +.method private nextTrack()V + .locals 2 + + iget-object v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->a:Lyl0/l; + + sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/q$a;->a:Lcom/tidal/android/feature/appscaffold/ui/q$a; # Tidal's built-in next track mini-player event + + invoke-interface {v0, v1}, Lyl0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object; + + return-void +.end method + +.method private previousTrack()V + .locals 2 + + iget-object v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->a:Lyl0/l; + + sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/q$c;->a:Lcom/tidal/android/feature/appscaffold/ui/q$c; # Our synthetic previous track event + + invoke-interface {v0, v1}, Lyl0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object; + + return-void +.end method + + +# virtual methods +.method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u; + .locals 6 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I + + move-result v0 + + if-eqz v0, :down + + const/4 v1, 0x1 + + if-eq v0, v1, :up + + const/4 v1, 0x3 + + if-eq v0, v1, :cancel + + goto :done + + # ACTION_DOWN + :down + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F + + move-result v0 + + iput v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->b:F + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v0 + + iput v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->c:F + + const/4 v0, 0x1 + + iput-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z + + goto :done + + # ACTION_UP + :up + iget-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z + + if-eqz v0, :done + + const/4 v0, 0x0 + + iput-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F + + move-result v0 + + iget v1, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->b:F + + sub-float/2addr v0, v1 + + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + + move-result v1 + + iget v2, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->c:F + + sub-float/2addr v1, v2 + + invoke-static {v0}, Ljava/lang/Math;->abs(F)F + + move-result v2 + + const/high16 v3, 0x42400000 # 48.0f -> at least 48dp horizontal swipe + + invoke-static {v3}, Lradiant/MiniPlayerTrackGestures$Gesture;->dp(F)F + + move-result v3 + + cmpg-float v4, v2, v3 + + if-gez v4, :check_axis + + goto :done + + :check_axis + invoke-static {v1}, Ljava/lang/Math;->abs(F)F + + move-result v1 + + const/high16 v3, 0x3fc00000 # 1.5f -> horizontal distance must dominate vertical mov. by 1.5x + + mul-float/2addr v1, v3 + + cmpg-float v2, v2, v1 + + if-lez v2, :done + + const/4 v1, 0x0 + + cmpg-float v0, v0, v1 + + # Negative dx = swipe left -> next. Positive dx = swipe right -> previous + if-ltz v0, :next + + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V + + invoke-direct {p0}, Lradiant/MiniPlayerTrackGestures$Gesture;->previousTrack()V + + goto :done + + :next + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V + + invoke-direct {p0}, Lradiant/MiniPlayerTrackGestures$Gesture;->nextTrack()V + + goto :done + + # ACTION_CANCEL + :cancel + const/4 v0, 0x0 + + iput-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z + + :done + sget-object p1, Lkotlin/u;->a:Lkotlin/u; + + return-object p1 +.end method + +.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; + .locals 0 + + check-cast p1, Landroid/view/MotionEvent; + + invoke-virtual {p0, p1}, Lradiant/MiniPlayerTrackGestures$Gesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u; + + move-result-object p1 + + return-object p1 +.end method diff --git a/patches/extension/radiant/MiniPlayerTrackGestures.smali b/patches/extension/radiant/MiniPlayerTrackGestures.smali new file mode 100644 index 0000000..03b93c6 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerTrackGestures.smali @@ -0,0 +1,82 @@ +.class public final Lradiant/MiniPlayerTrackGestures; +.super Ljava/lang/Object; + + +# static fields +.field private static a:J + + +# direct methods +.method private constructor ()V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + +.method public static consumeTapOpenSuppression()Z + .locals 5 + + sget-wide v0, Lradiant/MiniPlayerTrackGestures;->a:J + + const-wide/16 v2, 0x0 + + cmp-long v4, v0, v2 + + if-nez v4, :check_time + + const/4 v0, 0x0 + + return v0 + + :check_time + sput-wide v2, Lradiant/MiniPlayerTrackGestures;->a:J + + invoke-static {}, Landroid/os/SystemClock;->uptimeMillis()J + + move-result-wide v2 + + cmp-long v4, v2, v0 + + if-lez v4, :yes + + const/4 v0, 0x0 + + return v0 + + :yes + const/4 v0, 0x1 + + return v0 +.end method + +.method public static suppressTapOpen()V + .locals 4 + + invoke-static {}, Landroid/os/SystemClock;->uptimeMillis()J + + move-result-wide v0 + + const-wide/16 v2, 0x258 + + add-long/2addr v0, v2 + + sput-wide v0, Lradiant/MiniPlayerTrackGestures;->a:J + + return-void +.end method + +.method public static trackModifier(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; + .locals 1 + + new-instance v0, Lradiant/MiniPlayerTrackGestures$Gesture; + + invoke-direct {v0, p1}, Lradiant/MiniPlayerTrackGestures$Gesture;->(Lyl0/l;)V + + invoke-static {p0, v0}, Landroidx/compose/ui/input/pointer/PointerInteropFilter_androidKt;->motionEventSpy(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; + + move-result-object p0 + + return-object p0 +.end method diff --git a/patches/mini-player-gestures-left-right.patch b/patches/mini-player-gestures-left-right.patch new file mode 100644 index 0000000..1dc9a9c --- /dev/null +++ b/patches/mini-player-gestures-left-right.patch @@ -0,0 +1,83 @@ +--- a/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali ++++ b/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali +@@ -1746,6 +1746,10 @@ + invoke-static {v9, v10, v15, v6, v8}, Landroidx/compose/foundation/layout/PaddingKt;->padding-qDBjuR0(Landroidx/compose/ui/Modifier;FFFF)Landroidx/compose/ui/Modifier; + + move-result-object v6 ++ ++ invoke-static {v6, v1}, Lradiant/MiniPlayerTrackGestures;->trackModifier(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; # attach horizontal swipe listener ++ ++ move-result-object v6 + + .line 38 + sget-object v21, Landroidx/compose/ui/Alignment;->Companion:Landroidx/compose/ui/Alignment$Companion; +--- a/com/tidal/android/feature/appscaffold/ui/MiniPlayerViewModel.smali ++++ b/com/tidal/android/feature/appscaffold/ui/MiniPlayerViewModel.smali +@@ -816,10 +816,10 @@ + .line 111 + .line 112 + .line 113 +- move-result p1 ++ move-result v0 + + .line 114 +- if-eqz p1, :cond_f ++ if-eqz v0, :cond_f + + .line 115 + .line 116 +@@ -997,11 +997,32 @@ + :cond_e + return-void + +- .line 195 + :cond_f ++ sget-object v0, Lcom/tidal/android/feature/appscaffold/ui/q$c;->a:Lcom/tidal/android/feature/appscaffold/ui/q$c; ++ ++ invoke-virtual {p1, v0}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z ++ ++ move-result p1 ++ ++ if-eqz p1, :unknown_event ++ ++ invoke-interface {v3}, Lei/e;->canSkipToPreviousOrRewind()Z ++ ++ move-result p1 ++ ++ if-eqz p1, :previous_done ++ ++ const/4 p1, 0x0 ++ ++ invoke-interface {v3, p1}, Lei/e;->h(Z)V # false = previous/rewind ++ ++ :previous_done ++ return-void ++ ++ :unknown_event + invoke-static {}, Landroidx/compose/ui/graphics/y;->b()V + + .line 196 + .line 197 + .line 198 + return-void +--- a/com/tidal/android/feature/appscaffold/ui/composable/e.smali ++++ b/com/tidal/android/feature/appscaffold/ui/composable/e.smali +@@ -75,6 +75,18 @@ + .line 22 + :pswitch_0 ++ invoke-static {}, Lradiant/MiniPlayerTrackGestures;->consumeTapOpenSuppression()Z # prevent tap open after horizontal swipe ++ ++ move-result v0 ++ ++ if-eqz v0, :continue_open ++ ++ sget-object v0, Lkotlin/u;->a:Lkotlin/u; ++ ++ return-object v0 ++ ++ :continue_open ++ + iget-object v0, p0, Lcom/tidal/android/feature/appscaffold/ui/composable/e;->b:Ljava/lang/Object; + + .line 23 + .line 24 diff --git a/patches/mini-player-gestures.patch b/patches/mini-player-gestures.patch new file mode 100644 index 0000000..d6a8f5d --- /dev/null +++ b/patches/mini-player-gestures.patch @@ -0,0 +1,118 @@ +--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali ++++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali +@@ -2829,31 +2829,10 @@ + + move-result-object v0 + +- .line 150 +- sget-object v8, Lkotlin/u;->a:Lkotlin/u; +- +- .line 151 +- invoke-interface {v15}, Landroidx/compose/runtime/Composer;->rememberedValue()Ljava/lang/Object; +- +- move-result-object v10 +- +- .line 152 +- invoke-virtual/range {v22 .. v22}, Landroidx/compose/runtime/Composer$Companion;->getEmpty()Ljava/lang/Object; +- +- move-result-object v11 +- +- if-ne v10, v11, :cond_48 +- +- .line 153 +- sget-object v10, Lcom/tidal/android/feature/appscaffold/ui/composable/AppScaffoldKt$AppScaffold$3$2$1$1;->a:Lcom/tidal/android/feature/appscaffold/ui/composable/AppScaffoldKt$AppScaffold$3$2$1$1; +- +- invoke-interface {v15, v10}, Landroidx/compose/runtime/Composer;->updateRememberedValue(Ljava/lang/Object;)V +- +- .line 154 +- :cond_48 +- check-cast v10, Landroidx/compose/ui/input/pointer/PointerInputEventHandler; +- +- invoke-static {v0, v8, v10}, Landroidx/compose/ui/input/pointer/SuspendingPointerInputFilterKt;->pointerInput(Landroidx/compose/ui/Modifier;Ljava/lang/Object;Landroidx/compose/ui/input/pointer/PointerInputEventHandler;)Landroidx/compose/ui/Modifier; ++ move-object/from16 v8, v25 # SheetState from AppScaffold ++ ++ invoke-static {v0, v6, v8}, Lradiant/MiniPlayerGestures;->modifier(Landroidx/compose/ui/Modifier;Lyl0/l;Landroidx/compose/material3/SheetState;)Landroidx/compose/ui/Modifier; # attach swipe up listener + + move-result-object v0 + +@@ -2427,6 +2406,12 @@ + move-object/from16 v16, v9 + + .line 99 ++ move-object/from16 v9, v25 # same SheetState used by the modal sheet ++ ++ invoke-static {v5, v6, v9}, Lradiant/MiniPlayerGestures;->rootModifier(Landroidx/compose/ui/Modifier;Lyl0/l;Landroidx/compose/material3/SheetState;)Landroidx/compose/ui/Modifier; # continue drag after player recomposition ++ ++ move-result-object v5 ++ + invoke-static {v15, v5}, Landroidx/compose/ui/ComposedModifierKt;->materializeModifier(Landroidx/compose/runtime/Composer;Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + + move-result-object v9 + +--- a/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali ++++ b/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali +@@ -1243,5 +1243,21 @@ + invoke-virtual {v0}, Ljava/lang/Object;->getClass()Ljava/lang/Class; + + iget-object v3, v0, Lcom/tidal/android/feature/appscaffold/ui/s;->c:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState; ++ ++ iget-object v5, v0, Lcom/tidal/android/feature/appscaffold/ui/s;->a:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType; # currently rendered mini player media type ++ ++ sget-object v6, Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType;->Unknown:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType; # Unknown = no media playing. other media types : Track, Video. ++ ++ if-ne v5, v6, :rl_has_media # used to prevent swipe up when nothing is playing ++ ++ const/4 v5, 0x0 ++ ++ goto :rl_set_media ++ ++ :rl_has_media ++ const/4 v5, 0x1 ++ ++ :rl_set_media ++ invoke-static {v5}, Lradiant/MiniPlayerGestures;->setHasMiniPlayerMedia(Z)V + + invoke-virtual {v1}, Ljava/lang/Object;->getClass()Ljava/lang/Class; + +--- a/androidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1.smali ++++ b/androidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1.smali +@@ -210,8 +210,10 @@ + .line 22 + .line 23 + .line 24 + iget-object p1, p0, Landroidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1;->$sheetState:Landroidx/compose/material3/SheetState; +- ++ invoke-static {p1}, Lradiant/MiniPlayerGestures;->suppressAutoShow(Landroidx/compose/material3/SheetState;)Z # avoid Material3 show() taking over manual drag ++ move-result v1 ++ if-nez v1, :skip_show + .line 25 + .line 26 + iput v2, p0, Landroidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1;->label:I +@@ -231,7 +233,7 @@ + .line 33 + .line 34 + return-object v0 +- ++ :skip_show + .line 35 + :cond_2 + :goto_0 +--- a/androidx/compose/material3/ModalBottomSheetKt.smali ++++ b/androidx/compose/material3/ModalBottomSheetKt.smali +@@ -1370,3 +1370,6 @@ + .line 34 + :cond_47 ++ move-object v14, v12 ++ check-cast v14, Lyl0/l; ++ invoke-static {v13, v14}, Lradiant/MiniPlayerGestures;->setSettleCallback(Landroidx/compose/material3/SheetState;Lyl0/l;)V # reuse native settle animation on release + move-object/from16 v32, v12 +--- a/androidx/compose/material3/SheetDefaultsKt.smali ++++ b/androidx/compose/material3/SheetDefaultsKt.smali +@@ -100,6 +100,5 @@ + .line 16 +- const/16 v3, 0x12c +- ++ const/16 v3, 0x64 # 100ms + .line 17 + .line 18 + const/4 v4, 0x0 From 75450aad60f810db2d716b8bb22f48fe88ce92a7 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Tue, 23 Jun 2026 14:51:12 +0200 Subject: [PATCH 2/7] fix(mini player gestures patch): support redeisgned mp floating seek bar variant --- patches/mini-player-gestures.patch | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/patches/mini-player-gestures.patch b/patches/mini-player-gestures.patch index d6a8f5d..b9af323 100644 --- a/patches/mini-player-gestures.patch +++ b/patches/mini-player-gestures.patch @@ -1,9 +1,6 @@ --- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali +++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali -@@ -2829,31 +2829,10 @@ - - move-result-object v0 - +@@ -2832,27 +2832,5 @@ - .line 150 - sget-object v8, Lkotlin/u;->a:Lkotlin/u; - @@ -34,7 +31,6 @@ + invoke-static {v0, v6, v8}, Lradiant/MiniPlayerGestures;->modifier(Landroidx/compose/ui/Modifier;Lyl0/l;Landroidx/compose/material3/SheetState;)Landroidx/compose/ui/Modifier; # attach swipe up listener move-result-object v0 - @@ -2427,6 +2406,12 @@ move-object/from16 v16, v9 From 3758ba19f3a4923249810f6e6653fafe4e0d69b3 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Tue, 23 Jun 2026 23:08:02 +0200 Subject: [PATCH 3/7] feat(mini player gestures): track swipe feedback + fix stuck offsets --- .../ui/screens/patchopts/KnownPatch.kt | 2 + .../radiant/MiniPlayerGestures.smali | 8 + .../MiniPlayerTrackGestures$Gesture.smali | 124 +---- .../MiniPlayerTrackGestures$OffsetLayer.smali | 70 +++ ...iniPlayerTrackGestures$ResetAnimator.smali | 60 +++ .../radiant/MiniPlayerTrackGestures.smali | 470 ++++++++++++++++++ patches/mini-player-gestures-left-right.patch | 23 + 7 files changed, 660 insertions(+), 97 deletions(-) create mode 100644 patches/extension/radiant/MiniPlayerTrackGestures$OffsetLayer.smali create mode 100644 patches/extension/radiant/MiniPlayerTrackGestures$ResetAnimator.smali diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index ffdcc31..11061fa 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -169,6 +169,8 @@ enum class KnownPatch( extensionFileNames = listOf( "radiant/MiniPlayerTrackGestures.smali", "radiant/MiniPlayerTrackGestures\$Gesture.smali", + "radiant/MiniPlayerTrackGestures\$OffsetLayer.smali", + "radiant/MiniPlayerTrackGestures\$ResetAnimator.smali", "com/tidal/android/feature/appscaffold/ui/q\$c.smali", ), ), diff --git a/patches/extension/radiant/MiniPlayerGestures.smali b/patches/extension/radiant/MiniPlayerGestures.smali index a98c909..2be4df3 100644 --- a/patches/extension/radiant/MiniPlayerGestures.smali +++ b/patches/extension/radiant/MiniPlayerGestures.smali @@ -468,6 +468,14 @@ return v0 .end method +.method public static isAnyDragging()Z + .locals 1 + + sget-boolean v0, Lradiant/MiniPlayerGestures;->b:Z + + return v0 +.end method + .method public static hasMiniPlayerMedia()Z .locals 1 diff --git a/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali b/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali index 44239a3..433ef45 100644 --- a/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali +++ b/patches/extension/radiant/MiniPlayerTrackGestures$Gesture.smali @@ -17,14 +17,6 @@ # instance fields .field public final a:Lyl0/l; -# X at ACTION_DOWN -.field public b:F - -# Y at ACTION_DOWN -.field public c:F - -.field public d:Z - # direct methods .method public constructor (Lyl0/l;)V @@ -37,24 +29,6 @@ return-void .end method -.method private static dp(F)F - .locals 1 - - invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources; - - move-result-object v0 - - invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics; - - move-result-object v0 - - iget v0, v0, Landroid/util/DisplayMetrics;->density:F - - mul-float/2addr p0, v0 - - return p0 -.end method - .method private nextTrack()V .locals 2 @@ -82,7 +56,7 @@ # virtual methods .method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u; - .locals 6 + .locals 3 invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I @@ -94,6 +68,10 @@ if-eq v0, v1, :up + const/4 v1, 0x2 + + if-eq v0, v1, :move + const/4 v1, 0x3 if-eq v0, v1, :cancel @@ -105,101 +83,53 @@ invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F move-result v0 - - iput v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->b:F - invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F + move-result v1 + invoke-static {v0, v1}, Lradiant/MiniPlayerTrackGestures;->beginDrag(FF)V + goto :done + + # ACTION_MOVE + :move + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F + move-result v0 + invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F - iput v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->c:F - - const/4 v0, 0x1 - - iput-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z - + move-result v1 + invoke-static {v0, v1}, Lradiant/MiniPlayerTrackGestures;->moveDrag(FF)V goto :done # ACTION_UP :up - iget-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z - - if-eqz v0, :done - - const/4 v0, 0x0 - - iput-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z - invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F move-result v0 - - iget v1, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->b:F - - sub-float/2addr v0, v1 - invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F move-result v1 + invoke-static {v0, v1}, Lradiant/MiniPlayerTrackGestures;->finishDrag(FF)I - iget v2, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->c:F + move-result v0 - sub-float/2addr v1, v2 + if-eqz v0, :done - invoke-static {v0}, Ljava/lang/Math;->abs(F)F - - move-result v2 - - const/high16 v3, 0x42400000 # 48.0f -> at least 48dp horizontal swipe - - invoke-static {v3}, Lradiant/MiniPlayerTrackGestures$Gesture;->dp(F)F - - move-result v3 - - cmpg-float v4, v2, v3 - - if-gez v4, :check_axis - - goto :done - - :check_axis - invoke-static {v1}, Ljava/lang/Math;->abs(F)F - - move-result v1 - - const/high16 v3, 0x3fc00000 # 1.5f -> horizontal distance must dominate vertical mov. by 1.5x - - mul-float/2addr v1, v3 - - cmpg-float v2, v2, v1 - - if-lez v2, :done - - const/4 v1, 0x0 - - cmpg-float v0, v0, v1 - - # Negative dx = swipe left -> next. Positive dx = swipe right -> previous - if-ltz v0, :next + # Negative result = swipe left -> next. Positive result = swipe right -> previous + if-gez v0, :previous invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V - - invoke-direct {p0}, Lradiant/MiniPlayerTrackGestures$Gesture;->previousTrack()V - - goto :done - - :next - invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V - invoke-direct {p0}, Lradiant/MiniPlayerTrackGestures$Gesture;->nextTrack()V + goto :done + :previous + + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V + invoke-direct {p0}, Lradiant/MiniPlayerTrackGestures$Gesture;->previousTrack()V goto :done # ACTION_CANCEL :cancel - const/4 v0, 0x0 - - iput-boolean v0, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->d:Z + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->cancelDrag()V :done sget-object p1, Lkotlin/u;->a:Lkotlin/u; diff --git a/patches/extension/radiant/MiniPlayerTrackGestures$OffsetLayer.smali b/patches/extension/radiant/MiniPlayerTrackGestures$OffsetLayer.smali new file mode 100644 index 0000000..97bc03f --- /dev/null +++ b/patches/extension/radiant/MiniPlayerTrackGestures$OffsetLayer.smali @@ -0,0 +1,70 @@ +.class public final Lradiant/MiniPlayerTrackGestures$OffsetLayer; +.super Ljava/lang/Object; +.implements Lyl0/l; + + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerTrackGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "OffsetLayer" +.end annotation + + +# static fields +.field public static final INSTANCE:Lradiant/MiniPlayerTrackGestures$OffsetLayer; + + +# direct methods +.method static constructor ()V + .locals 1 + + new-instance v0, Lradiant/MiniPlayerTrackGestures$OffsetLayer; + + invoke-direct {v0}, Lradiant/MiniPlayerTrackGestures$OffsetLayer;->()V + + sput-object v0, Lradiant/MiniPlayerTrackGestures$OffsetLayer;->INSTANCE:Lradiant/MiniPlayerTrackGestures$OffsetLayer; + + return-void +.end method + +.method private constructor ()V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + + +# virtual methods +.method public final invoke(Landroidx/compose/ui/graphics/GraphicsLayerScope;)Lkotlin/u; + .locals 1 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState; + + invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F + + move-result v0 + + invoke-interface {p1, v0}, Landroidx/compose/ui/graphics/GraphicsLayerScope;->setTranslationX(F)V + + sget-object p1, Lkotlin/u;->a:Lkotlin/u; + + return-object p1 +.end method + +.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; + .locals 0 + + check-cast p1, Landroidx/compose/ui/graphics/GraphicsLayerScope; + + invoke-virtual {p0, p1}, Lradiant/MiniPlayerTrackGestures$OffsetLayer;->invoke(Landroidx/compose/ui/graphics/GraphicsLayerScope;)Lkotlin/u; + + move-result-object p1 + + return-object p1 +.end method diff --git a/patches/extension/radiant/MiniPlayerTrackGestures$ResetAnimator.smali b/patches/extension/radiant/MiniPlayerTrackGestures$ResetAnimator.smali new file mode 100644 index 0000000..2f839b0 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerTrackGestures$ResetAnimator.smali @@ -0,0 +1,60 @@ +.class public final Lradiant/MiniPlayerTrackGestures$ResetAnimator; +.super Ljava/lang/Object; +.implements Landroid/animation/ValueAnimator$AnimatorUpdateListener; + + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerTrackGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "ResetAnimator" +.end annotation + + +# direct methods +.method public constructor ()V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + + +# virtual methods +.method public final onAnimationUpdate(Landroid/animation/ValueAnimator;)V + .locals 3 + + move-object v0, p1 + + invoke-virtual {p1}, Landroid/animation/ValueAnimator;->getAnimatedValue()Ljava/lang/Object; + + move-result-object v1 + + check-cast v1, Ljava/lang/Float; + + invoke-virtual {v1}, Ljava/lang/Float;->floatValue()F + + move-result v1 + + invoke-static {v1}, Lradiant/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V + + invoke-virtual {v0}, Landroid/animation/ValueAnimator;->getAnimatedFraction()F + + move-result v1 + + const/high16 v2, 0x3f800000 # 1.0f + + cmpg-float v1, v1, v2 + + if-gez v1, :done + + invoke-static {v0}, Lradiant/MiniPlayerTrackGestures;->clearResetAnimator(Landroid/animation/ValueAnimator;)V + + :done + + return-void +.end method diff --git a/patches/extension/radiant/MiniPlayerTrackGestures.smali b/patches/extension/radiant/MiniPlayerTrackGestures.smali index 03b93c6..e9294a5 100644 --- a/patches/extension/radiant/MiniPlayerTrackGestures.smali +++ b/patches/extension/radiant/MiniPlayerTrackGestures.smali @@ -5,8 +5,38 @@ # static fields .field private static a:J +.field public static b:Landroidx/compose/runtime/MutableFloatState; + +.field private static c:Landroid/animation/ValueAnimator; + +.field private static d:Ljava/lang/Object; + +# X at ACTION_DOWN +.field private static e:F + +# Y at ACTION_DOWN +.field private static f:F + +.field private static g:Z + +.field private static h:Z + # direct methods +.method static constructor ()V + .locals 1 + + const/4 v0, 0x0 + + invoke-static {v0}, Landroidx/compose/runtime/PrimitiveSnapshotStateKt;->mutableFloatStateOf(F)Landroidx/compose/runtime/MutableFloatState; + + move-result-object v0 + + sput-object v0, Lradiant/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState; + + return-void +.end method + .method private constructor ()V .locals 0 @@ -15,6 +45,372 @@ return-void .end method +.method public static animateReset()V + .locals 7 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + if-eqz v0, :read_offset + + invoke-virtual {v0}, Landroid/animation/ValueAnimator;->cancel()V + + const/4 v0, 0x0 + + sput-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + :read_offset + sget-object v0, Lradiant/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState; + + invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F + + move-result v0 + + invoke-static {v0}, Ljava/lang/Math;->abs(F)F + + move-result v1 + + const/4 v3, 0x0 + + const/high16 v2, 0x3f000000 # 0.5f + + cmpg-float v1, v1, v2 + + if-lez v1, :snap_zero + + const/4 v1, 0x2 + + new-array v1, v1, [F + + const/4 v2, 0x0 + + aput v0, v1, v2 + + const/4 v0, 0x1 + + aput v3, v1, v0 + + invoke-static {v1}, Landroid/animation/ValueAnimator;->ofFloat([F)Landroid/animation/ValueAnimator; + + move-result-object v1 + + const-wide/16 v4, 0xb4 # 180ms + + invoke-virtual {v1, v4, v5}, Landroid/animation/ValueAnimator;->setDuration(J)Landroid/animation/ValueAnimator; + + new-instance v0, Landroid/view/animation/DecelerateInterpolator; + + const/high16 v4, 0x3fc00000 # 1.5f + + invoke-direct {v0, v4}, Landroid/view/animation/DecelerateInterpolator;->(F)V + + invoke-virtual {v1, v0}, Landroid/animation/ValueAnimator;->setInterpolator(Landroid/animation/TimeInterpolator;)V + + new-instance v0, Lradiant/MiniPlayerTrackGestures$ResetAnimator; + + invoke-direct {v0}, Lradiant/MiniPlayerTrackGestures$ResetAnimator;->()V + + invoke-virtual {v1, v0}, Landroid/animation/ValueAnimator;->addUpdateListener(Landroid/animation/ValueAnimator$AnimatorUpdateListener;)V + + sput-object v1, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + invoke-virtual {v1}, Landroid/animation/ValueAnimator;->start()V + + return-void + + :snap_zero + invoke-static {v3}, Lradiant/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V + + return-void +.end method + +.method public static beginDrag(FF)V + .locals 2 + + sput p0, Lradiant/MiniPlayerTrackGestures;->e:F + + sput p1, Lradiant/MiniPlayerTrackGestures;->f:F + + const/4 v0, 0x1 + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->g:Z + + const/4 v0, 0x0 + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->h:Z + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + if-eqz v0, :zero + + invoke-virtual {v0}, Landroid/animation/ValueAnimator;->cancel()V + + const/4 v0, 0x0 + + sput-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + :zero + const/4 v0, 0x0 + + invoke-static {v0}, Lradiant/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V + + return-void +.end method + +.method public static cancelDrag()V + .locals 1 + + const/4 v0, 0x0 + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->g:Z + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->h:Z + + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->animateReset()V + + return-void +.end method + +.method public static clearResetAnimator(Landroid/animation/ValueAnimator;)V + .locals 1 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + if-ne v0, p0, :done + + const/4 v0, 0x0 + + sput-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + :done + return-void +.end method + +.method public static finishDrag(FF)I + .locals 5 + + sget-boolean v0, Lradiant/MiniPlayerTrackGestures;->g:Z + + if-nez v0, :active + + const/4 v0, 0x0 + + return v0 + + :active + const/4 v0, 0x0 + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->g:Z + + sget-boolean v1, Lradiant/MiniPlayerTrackGestures;->h:Z + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->h:Z + + sget v2, Lradiant/MiniPlayerTrackGestures;->e:F + + sub-float/2addr p0, v2 + + sget v2, Lradiant/MiniPlayerTrackGestures;->f:F + + sub-float/2addr p1, v2 + + if-eqz v1, :reset + + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V + + :reset + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->animateReset()V + + invoke-static {p0}, Ljava/lang/Math;->abs(F)F + + move-result v1 + + const/high16 v2, 0x42400000 # 48.0f -> at least 48dp horizontal swipe + + invoke-static {v2}, Lradiant/MiniPlayerTrackGestures;->dp(F)F + + move-result v2 + + cmpg-float v3, v1, v2 + + if-gez v3, :check_axis + + return v0 + + :check_axis + invoke-static {p1}, Ljava/lang/Math;->abs(F)F + + move-result p1 + + const/high16 v2, 0x3fc00000 # 1.5f -> horizontal distance must dominate vertical mov. by 1.5x + + mul-float/2addr p1, v2 + + cmpg-float v1, v1, p1 + + if-lez v1, :no_swipe + + const/4 p1, 0x0 + + cmpg-float p0, p0, p1 + + if-ltz p0, :next + + const/4 v0, 0x1 + + return v0 + + :next + const/4 v0, -0x1 + + return v0 + + :no_swipe + return v0 +.end method + +.method public static moveDrag(FF)V + .locals 6 + + sget-boolean v0, Lradiant/MiniPlayerTrackGestures;->g:Z + + if-eqz v0, :done + + invoke-static {}, Lradiant/MiniPlayerGestures;->isAnyDragging()Z + + move-result v0 + + if-eqz v0, :move_continue + + const/4 v0, 0x0 + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->g:Z + + sput-boolean v0, Lradiant/MiniPlayerTrackGestures;->h:Z + + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->animateReset()V + + goto :done + + :move_continue + sget v0, Lradiant/MiniPlayerTrackGestures;->e:F + + sub-float/2addr p0, v0 + + sget v0, Lradiant/MiniPlayerTrackGestures;->f:F + + sub-float/2addr p1, v0 + + invoke-static {p0}, Ljava/lang/Math;->abs(F)F + + move-result v0 + + invoke-static {p1}, Ljava/lang/Math;->abs(F)F + + move-result v1 + + sget-boolean v2, Lradiant/MiniPlayerTrackGestures;->h:Z + + if-nez v2, :publish + + const/high16 v2, 0x40c00000 # 6.0f dp dead zone + + invoke-static {v2}, Lradiant/MiniPlayerTrackGestures;->dp(F)F + + move-result v2 + + cmpg-float v3, v0, v2 + + if-gez v3, :move_axis + + goto :done + + :move_axis + const v2, 0x3f8ccccd # 1.1f horizontal dominance while dragging + + mul-float/2addr v1, v2 + + cmpg-float v2, v0, v1 + + if-gez v2, :accept + + goto :done + + :accept + const/4 v2, 0x1 + + sput-boolean v2, Lradiant/MiniPlayerTrackGestures;->h:Z + + :publish + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V + + invoke-static {p0}, Lradiant/MiniPlayerTrackGestures;->setDragOffset(F)V + + :done + return-void +.end method + +.method private static dp(F)F + .locals 1 + + invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources; + + move-result-object v0 + + invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics; + + move-result-object v0 + + iget v0, v0, Landroid/util/DisplayMetrics;->density:F + + mul-float/2addr p0, v0 + + return p0 +.end method + +.method public static feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + .locals 2 + + const/high16 v0, 0x3f800000 # 1.0f + + invoke-static {p0, v0}, Landroidx/compose/ui/ZIndexModifierKt;->zIndex(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; + + move-result-object p0 + + sget-object v0, Lradiant/MiniPlayerTrackGestures$OffsetLayer;->INSTANCE:Lradiant/MiniPlayerTrackGestures$OffsetLayer; + + invoke-static {p0, v0}, Landroidx/compose/ui/graphics/GraphicsLayerModifierKt;->graphicsLayer(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; + + move-result-object p0 + + return-object p0 +.end method + +.method public static onRenderedItem(Ljava/lang/Object;)V + .locals 2 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->d:Ljava/lang/Object; + + if-eqz v0, :store_initial + + invoke-virtual {v0, p0}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z + + move-result v1 + + if-nez v1, :done + + sput-object p0, Lradiant/MiniPlayerTrackGestures;->d:Ljava/lang/Object; + + invoke-static {}, Lradiant/MiniPlayerTrackGestures;->animateReset()V + + return-void + + :store_initial + sput-object p0, Lradiant/MiniPlayerTrackGestures;->d:Ljava/lang/Object; + + :done + return-void +.end method + .method public static consumeTapOpenSuppression()Z .locals 5 @@ -67,6 +463,80 @@ return-void .end method +.method public static setDragOffset(F)V + .locals 5 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + if-eqz v0, :rubber_band + + invoke-virtual {v0}, Landroid/animation/ValueAnimator;->cancel()V + + const/4 v0, 0x0 + + sput-object v0, Lradiant/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator; + + :rubber_band + invoke-static {p0}, Ljava/lang/Math;->abs(F)F + + move-result v0 + + const/high16 v1, 0x42800000 # 64.0f dp free travel + + invoke-static {v1}, Lradiant/MiniPlayerTrackGestures;->dp(F)F + + move-result v1 + + cmpg-float v2, v0, v1 + + if-lez v2, :publish + + sub-float/2addr v0, v1 + + const/high16 v2, 0x3e800000 # 0.25f resistance after 64dp + + mul-float/2addr v0, v2 + + const v2, 0x3eb33333 # 0.35f max extra travel + + mul-float/2addr v2, v1 + + invoke-static {v0, v2}, Ljava/lang/Math;->min(FF)F + + move-result v0 + + add-float/2addr v0, v1 + + const/4 v1, 0x0 + + cmpg-float v2, p0, v1 + + if-gez v2, :positive + + neg-float p0, v0 + + goto :publish + + :positive + move p0, v0 + + + :publish + invoke-static {p0}, Lradiant/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V + + return-void +.end method + +.method public static setDragOffsetDirect(F)V + .locals 1 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState; + + invoke-interface {v0, p0}, Landroidx/compose/runtime/MutableFloatState;->setFloatValue(F)V + + return-void +.end method + .method public static trackModifier(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; .locals 1 diff --git a/patches/mini-player-gestures-left-right.patch b/patches/mini-player-gestures-left-right.patch index 1dc9a9c..7957ebf 100644 --- a/patches/mini-player-gestures-left-right.patch +++ b/patches/mini-player-gestures-left-right.patch @@ -11,6 +11,29 @@ .line 38 sget-object v21, Landroidx/compose/ui/Alignment;->Companion:Landroidx/compose/ui/Alignment$Companion; +@@ -1884,5 +1888,5 @@ + .line 56 + iget-object v6, v0, Lcom/tidal/android/feature/appscaffold/ui/s;->b:Lcom/tidal/android/feature/appscaffold/ui/p; +- ++ invoke-static {v6}, Lradiant/MiniPlayerTrackGestures;->onRenderedItem(Ljava/lang/Object;)V # reset stale drag offset if track metadata changes mid-gesture + .line 57 + invoke-virtual/range {v16 .. v16}, Lcom/squareup/ui/market/core/theme/MarketStylesheet;->getBorderRadii()Lcom/squareup/ui/market/core/theme/MarketStylesheet$b; +@@ -1919,5 +1923,6 @@ + invoke-static {v5, v4}, Landroidx/compose/foundation/layout/SizeKt;->size-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; +- + move-result-object v4 +- ++ invoke-static {v4}, Lradiant/MiniPlayerTrackGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move the whole cover container with horizontal swipe ++ move-result-object v4 ++ + .line 63 +@@ -2032,3 +2038,5 @@ + move-result-object v4 +- ++ invoke-static {v4}, Lradiant/MiniPlayerTrackGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist with horizontal swipe ++ move-result-object v4 ++ + .line 72 --- a/com/tidal/android/feature/appscaffold/ui/MiniPlayerViewModel.smali +++ b/com/tidal/android/feature/appscaffold/ui/MiniPlayerViewModel.smali @@ -816,10 +816,10 @@ From 276dd0be2e91229d463a16522a2db692fe10dac4 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Wed, 24 Jun 2026 00:30:14 +0200 Subject: [PATCH 4/7] fix(miniplayer gestures): clip text on right side --- .../ui/screens/patchopts/KnownPatch.kt | 1 + .../MiniPlayerTrackGestures$TextDraw.smali | 128 ++++++++++++++++++ .../radiant/MiniPlayerTrackGestures.smali | 12 ++ patches/mini-player-gestures-left-right.patch | 2 +- 4 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 patches/extension/radiant/MiniPlayerTrackGestures$TextDraw.smali diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 11061fa..36538dc 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -171,6 +171,7 @@ enum class KnownPatch( "radiant/MiniPlayerTrackGestures\$Gesture.smali", "radiant/MiniPlayerTrackGestures\$OffsetLayer.smali", "radiant/MiniPlayerTrackGestures\$ResetAnimator.smali", + "radiant/MiniPlayerTrackGestures\$TextDraw.smali", "com/tidal/android/feature/appscaffold/ui/q\$c.smali", ), ), diff --git a/patches/extension/radiant/MiniPlayerTrackGestures$TextDraw.smali b/patches/extension/radiant/MiniPlayerTrackGestures$TextDraw.smali new file mode 100644 index 0000000..876a36e --- /dev/null +++ b/patches/extension/radiant/MiniPlayerTrackGestures$TextDraw.smali @@ -0,0 +1,128 @@ +.class public final Lradiant/MiniPlayerTrackGestures$TextDraw; +.super Ljava/lang/Object; +.implements Lyl0/l; + + +# annotations +.annotation system Ldalvik/annotation/EnclosingClass; + value = Lradiant/MiniPlayerTrackGestures; +.end annotation + +.annotation system Ldalvik/annotation/InnerClass; + accessFlags = 0x19 + name = "TextDraw" +.end annotation + + +# static fields +.field public static final INSTANCE:Lradiant/MiniPlayerTrackGestures$TextDraw; + + +# direct methods +.method static constructor ()V + .locals 1 + + new-instance v0, Lradiant/MiniPlayerTrackGestures$TextDraw; + + invoke-direct {v0}, Lradiant/MiniPlayerTrackGestures$TextDraw;->()V + + sput-object v0, Lradiant/MiniPlayerTrackGestures$TextDraw;->INSTANCE:Lradiant/MiniPlayerTrackGestures$TextDraw; + + return-void +.end method + +.method private constructor ()V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + + +# virtual methods +.method public final invoke(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)Lkotlin/u; + .locals 12 + + sget-object v0, Lradiant/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState; + + invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F + + move-result v10 + + invoke-interface {p1}, Landroidx/compose/ui/graphics/drawscope/DrawScope;->getDrawContext()Landroidx/compose/ui/graphics/drawscope/DrawContext; + + move-result-object v7 + + invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getSize-NH-jbRc()J + + move-result-wide v8 + + const/16 v0, 0x20 + + shr-long v1, v8, v0 + + long-to-int v1, v1 + + invoke-static {v1}, Ljava/lang/Float;->intBitsToFloat(I)F + + move-result v4 + + invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getCanvas()Landroidx/compose/ui/graphics/Canvas; + + move-result-object v0 + + invoke-interface {v0}, Landroidx/compose/ui/graphics/Canvas;->save()V + + :try_start_0 + sget-object v0, Landroidx/compose/ui/graphics/ClipOp;->Companion:Landroidx/compose/ui/graphics/ClipOp$Companion; + + invoke-virtual {v0}, Landroidx/compose/ui/graphics/ClipOp$Companion;->getIntersect-rtfAjoo()I + + move-result v6 + + invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getTransform()Landroidx/compose/ui/graphics/drawscope/DrawTransform; + + move-result-object v1 + + const v2, -0x800001 # -Float.MAX_VALUE -> unbounded left + + const v3, -0x800001 # -Float.MAX_VALUE -> unbounded top + + const v5, 0x7f7fffff # Float.MAX_VALUE -> unbounded bottom + + invoke-interface/range {v1 .. v6}, Landroidx/compose/ui/graphics/drawscope/DrawTransform;->clipRect-N_I0leg(FFFFI)V + + const/4 v0, 0x0 + + invoke-interface {v1, v10, v0}, Landroidx/compose/ui/graphics/drawscope/DrawTransform;->translate(FF)V + + invoke-interface {p1}, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;->drawContent()V + :try_end_0 + .catchall {:try_start_0 .. :try_end_0} :catchall_0 + + invoke-static {v7, v8, v9}, Landroidx/compose/animation/i;->b(Landroidx/compose/ui/graphics/drawscope/DrawContext;J)V + + sget-object p1, Lkotlin/u;->a:Lkotlin/u; + + return-object p1 + + :catchall_0 + move-exception v0 + + invoke-static {v7, v8, v9}, Landroidx/compose/animation/i;->b(Landroidx/compose/ui/graphics/drawscope/DrawContext;J)V + + throw v0 +.end method + +.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object; + .locals 0 + + check-cast p1, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope; + + invoke-virtual {p0, p1}, Lradiant/MiniPlayerTrackGestures$TextDraw;->invoke(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)Lkotlin/u; + + move-result-object p1 + + return-object p1 +.end method diff --git a/patches/extension/radiant/MiniPlayerTrackGestures.smali b/patches/extension/radiant/MiniPlayerTrackGestures.smali index e9294a5..5cfa1ff 100644 --- a/patches/extension/radiant/MiniPlayerTrackGestures.smali +++ b/patches/extension/radiant/MiniPlayerTrackGestures.smali @@ -385,6 +385,18 @@ return-object p0 .end method +.method public static textFeedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; + .locals 1 + + sget-object v0, Lradiant/MiniPlayerTrackGestures$TextDraw;->INSTANCE:Lradiant/MiniPlayerTrackGestures$TextDraw; # text clip on right only + + invoke-static {p0, v0}, Landroidx/compose/ui/draw/DrawModifierKt;->drawWithContent(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; + + move-result-object p0 + + return-object p0 +.end method + .method public static onRenderedItem(Ljava/lang/Object;)V .locals 2 diff --git a/patches/mini-player-gestures-left-right.patch b/patches/mini-player-gestures-left-right.patch index 7957ebf..6e9be1a 100644 --- a/patches/mini-player-gestures-left-right.patch +++ b/patches/mini-player-gestures-left-right.patch @@ -30,7 +30,7 @@ @@ -2032,3 +2038,5 @@ move-result-object v4 - -+ invoke-static {v4}, Lradiant/MiniPlayerTrackGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist with horizontal swipe ++ invoke-static {v4}, Lradiant/MiniPlayerTrackGestures;->textFeedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist + move-result-object v4 + .line 72 From a9611a1667e00dc9b3f416029c667fc8ed2c7af9 Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Wed, 24 Jun 2026 01:01:57 +0200 Subject: [PATCH 5/7] feat(patch): mini player dynamic bg color --- .../ui/screens/patchopts/KnownPatch.kt | 8 + Manager/app/src/main/res/values/strings.xml | 2 + patches/data.json | 2 +- .../radiant/MiniPlayerBackground.smali | 139 ++++++++++++++++++ patches/mini-player-dynamic-bg.patch | 56 +++++++ 5 files changed, 206 insertions(+), 1 deletion(-) create mode 100644 patches/extension/radiant/MiniPlayerBackground.smali create mode 100644 patches/mini-player-dynamic-bg.patch diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 1471ed6..d05da44 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -135,6 +135,13 @@ enum class KnownPatch( ), ), ), + MiniPlayerDynamicBackground( + order = 51, + fileNames = listOf("mini-player-dynamic-bg.patch"), + titleRes = R.string.patch_mini_player_dynamic_bg_title, + descRes = R.string.patch_mini_player_dynamic_bg_desc, + default = Disabled, + ), EnableLegacyUi( order = 10, fileNames = listOf("enable-legacy-ui.patch"), @@ -151,6 +158,7 @@ enum class KnownPatch( PlayerBackdrop, QualityBadgeColors, LyricsProgressPill, + MiniPlayerDynamicBackground, CoverEverywhere, ), ); diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index abe8cef..35a768f 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -289,6 +289,8 @@ Redesigned Mini-Player Integrates the Seekbar into the control panel at the bottom of the screen. + Mini-Player Dynamic Background + Colorizes the Mini-Player depending on the currently playing track. Floating Grey Black diff --git a/patches/data.json b/patches/data.json index cd58bee..786689d 100644 --- a/patches/data.json +++ b/patches/data.json @@ -1,5 +1,5 @@ { "tidalVersionCode": 9090, "tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk", - "patchesVersion": "0.9.10" + "patchesVersion": "0.9.11" } diff --git a/patches/extension/radiant/MiniPlayerBackground.smali b/patches/extension/radiant/MiniPlayerBackground.smali new file mode 100644 index 0000000..62c6999 --- /dev/null +++ b/patches/extension/radiant/MiniPlayerBackground.smali @@ -0,0 +1,139 @@ +.class public final Lradiant/MiniPlayerBackground; +.super Ljava/lang/Object; + + +# static fields +.field public static final colorState:Landroidx/compose/runtime/MutableState; # player bg color + + +# direct methods +.method static constructor ()V + .locals 2 + + const/4 v0, 0x0 + + const/4 v1, 0x2 + + invoke-static {v0, v0, v1, v0}, Landroidx/compose/runtime/SnapshotStateKt;->mutableStateOf$default(Ljava/lang/Object;Landroidx/compose/runtime/SnapshotMutationPolicy;ILjava/lang/Object;)Landroidx/compose/runtime/MutableState; + + move-result-object v0 + + sput-object v0, Lradiant/MiniPlayerBackground;->colorState:Landroidx/compose/runtime/MutableState; + + return-void +.end method + + +.method private constructor ()V + .locals 0 + + invoke-direct {p0}, Ljava/lang/Object;->()V + + return-void +.end method + + +.method public static color(JLandroidx/compose/runtime/Composer;I)J + .locals 8 + .annotation build Landroidx/compose/runtime/Composable; + .end annotation + + const v0, 0x4d504442 # compose group key + + invoke-interface {p2, v0}, Landroidx/compose/runtime/Composer;->startReplaceGroup(I)V + + sget-object v0, Lradiant/MiniPlayerBackground;->colorState:Landroidx/compose/runtime/MutableState; + + invoke-interface {v0}, Landroidx/compose/runtime/MutableState;->getValue()Ljava/lang/Object; # read player color + + move-result-object v0 + + check-cast v0, Landroidx/compose/ui/graphics/Color; + + if-eqz v0, :fallback + + invoke-virtual {v0}, Landroidx/compose/ui/graphics/Color;->unbox-impl()J + + move-result-wide p0 + + :fallback + const/16 v0, 0x2ee # 750ms + + const/4 v1, 0x0 + + const/4 v2, 0x0 + + const/4 v3, 0x6 + + const/4 v4, 0x0 + + invoke-static {v0, v1, v2, v3, v4}, Landroidx/compose/animation/core/AnimationSpecKt;->tween$default(IILandroidx/compose/animation/core/Easing;ILjava/lang/Object;)Landroidx/compose/animation/core/TweenSpec; + + move-result-object v2 + + move-wide v0, p0 + + const-string v3, "miniPlayerBackgroundColorAnimation" + + const/4 v4, 0x0 + + move-object v5, p2 + + const/16 v6, 0x1b0 + + const/16 v7, 0x8 + + invoke-static/range {v0 .. v7}, Landroidx/compose/animation/SingleValueAnimationKt;->animateColorAsState-euL9pac(JLandroidx/compose/animation/core/AnimationSpec;Ljava/lang/String;Lyl0/l;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State; + + move-result-object v0 + + invoke-interface {v0}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object; + + move-result-object v0 + + check-cast v0, Landroidx/compose/ui/graphics/Color; + + invoke-virtual {v0}, Landroidx/compose/ui/graphics/Color;->unbox-impl()J + + move-result-wide p0 + + invoke-interface {p2}, Landroidx/compose/runtime/Composer;->endReplaceGroup()V + + return-wide p0 +.end method + + +.method public static setHex(Ljava/lang/String;)V + .locals 3 + + const/4 v0, 0x0 + + if-eqz p0, :set_color + + :try_start + invoke-static {p0}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I + + move-result p0 + + invoke-static {p0}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack ARGB color + + move-result-wide v1 + + invoke-static {v1, v2}, Landroidx/compose/ui/graphics/Color;->box-impl(J)Landroidx/compose/ui/graphics/Color; + + move-result-object v0 + :try_end + .catch Ljava/lang/Throwable; {:try_start .. :try_end} :catch_parse + + goto :set_color + + :catch_parse + move-exception p0 + + :set_color + sget-object p0, Lradiant/MiniPlayerBackground;->colorState:Landroidx/compose/runtime/MutableState; + + invoke-interface {p0, v0}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V + + return-void +.end method diff --git a/patches/mini-player-dynamic-bg.patch b/patches/mini-player-dynamic-bg.patch new file mode 100644 index 0000000..9c31f1f --- /dev/null +++ b/patches/mini-player-dynamic-bg.patch @@ -0,0 +1,56 @@ +--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali ++++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali +@@ -2819,4 +2819,10 @@ + .line 147 ++ const/4 v8, 0x0 ++ ++ invoke-static {v10, v11, v15, v8}, Lradiant/MiniPlayerBackground;->color(JLandroidx/compose/runtime/Composer;I)J # dynamic bg or fallback ++ ++ move-result-wide v10 ++ + sget-object v8, Lcom/tidal/android/feature/appscaffold/ui/composable/a;->c:Landroidx/compose/foundation/shape/RoundedCornerShape; + + .line 148 +--- a/com/tidal/android/feature/playerscreen/ui/PlayerViewModel.smali ++++ b/com/tidal/android/feature/playerscreen/ui/PlayerViewModel.smali +@@ -1047,9 +1047,13 @@ + invoke-static {v2, v1, v4, v3}, Lkotlinx/coroutines/flow/FlowKt;->stateIn(Lkotlinx/coroutines/flow/Flow;Lkotlinx/coroutines/CoroutineScope;Lkotlinx/coroutines/flow/SharingStarted;Ljava/lang/Object;)Lkotlinx/coroutines/flow/StateFlow; + + move-result-object v1 + + iput-object v1, v0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->U:Lkotlinx/coroutines/flow/StateFlow; + ++ iget-object v1, v0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->O:Lkotlin/h; ++ ++ invoke-interface {v1}, Lkotlin/h;->getValue()Ljava/lang/Object; # init player bg color ++ + return-void + .end method + +@@ -1607,6 +1611,8 @@ + :goto_6 + invoke-interface {v9, v10}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V + ++ invoke-static {v10}, Lradiant/MiniPlayerBackground;->setHex(Ljava/lang/String;)V # store player bg hex ++ + .line 263 + .line 264 + .line 265 +@@ -2306,6 +2312,8 @@ + .line 598 + invoke-interface {v1, v5}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V + ++ invoke-static {v5}, Lradiant/MiniPlayerBackground;->setHex(Ljava/lang/String;)V # clear bg color ++ + .line 599 + .line 600 + .line 601 +@@ -2523,6 +2531,8 @@ + .line 704 + invoke-interface {v1, v5}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V + ++ invoke-static {v5}, Lradiant/MiniPlayerBackground;->setHex(Ljava/lang/String;)V # clear bg color ++ + .line 705 + .line 706 + .line 707 From 6ad6cb2fe0b46a8d3ea314e8a9c03ffa7843318c Mon Sep 17 00:00:00 2001 From: Se7en-dev Date: Wed, 24 Jun 2026 01:04:45 +0200 Subject: [PATCH 6/7] fix(patch): update order and patches version --- .../com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt | 2 +- patches/data.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index 36538dc..43c37a0 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -148,7 +148,7 @@ enum class KnownPatch( ), ), MiniPlayerGestures( - order = 51, + order = 52, fileNames = listOf("mini-player-gestures.patch"), extensionFileNames = listOf( "radiant/MiniPlayerGestures.smali", diff --git a/patches/data.json b/patches/data.json index cd58bee..d4af060 100644 --- a/patches/data.json +++ b/patches/data.json @@ -1,5 +1,5 @@ { "tidalVersionCode": 9090, "tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk", - "patchesVersion": "0.9.10" + "patchesVersion": "0.9.12" } From c89827c7b274c50eb08d4b376b9473b88d91ee3e Mon Sep 17 00:00:00 2001 From: meowarex Date: Thu, 25 Jun 2026 01:03:28 +1000 Subject: [PATCH 7/7] Advanced Options + New Patch Options <3 --- .../patcher/steps/patch/SmaliPatchStep.kt | 60 ++- .../screens/PatchOptionsScreenPreview.kt | 11 +- .../ui/screens/patchopts/KnownPatch.kt | 117 ++++-- .../ui/screens/patchopts/PatchOption.kt | 68 +++ .../ui/screens/patchopts/PatchOptions.kt | 94 +++-- .../ui/screens/patchopts/PatchOptionsModel.kt | 257 +++++++++--- .../screens/patchopts/PatchOptionsScreen.kt | 24 +- .../ui/screens/patchopts/PatchSpec.kt | 268 ++++++++++++ .../components/PatchAdvancedOptionsSheet.kt | 386 ++++++++++++++++++ .../components/PatchSelectionAccordion.kt | 170 ++++++-- .../components/PatchVariantSelector.kt | 7 +- Manager/app/src/main/res/drawable/ic_lock.xml | 9 + .../src/main/res/drawable/ic_lock_open.xml | 9 + Manager/app/src/main/res/drawable/ic_tune.xml | 9 + Manager/app/src/main/res/values/strings.xml | 16 + patches/manifest.json | 212 ++++++++++ ...atch => mini-player-floating-border.patch} | 0 patches/player-backdrop.patch | 4 +- 18 files changed, 1542 insertions(+), 179 deletions(-) create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt create mode 100644 Manager/app/src/main/res/drawable/ic_lock.xml create mode 100644 Manager/app/src/main/res/drawable/ic_lock_open.xml create mode 100644 Manager/app/src/main/res/drawable/ic_tune.xml create mode 100644 patches/manifest.json rename patches/{mini-player-floating.patch => mini-player-floating-border.patch} (100%) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt index 0f5a914..8e4361e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/patcher/steps/patch/SmaliPatchStep.kt @@ -8,7 +8,9 @@ import com.meowarex.rlmobile.patcher.steps.base.IDexProvider import com.meowarex.rlmobile.patcher.steps.base.Step import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep +import com.meowarex.rlmobile.ui.screens.patchopts.PatchManifest import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions +import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs import com.android.tools.smali.baksmali.Baksmali import com.android.tools.smali.baksmali.BaksmaliOptions import com.android.tools.smali.dexlib2.Opcodes @@ -19,6 +21,7 @@ import com.github.diamondminer88.zip.ZipReader import com.github.difflib.DiffUtils import com.github.difflib.UnifiedDiffUtils import com.github.difflib.patch.Patch +import kotlinx.serialization.json.Json import org.koin.core.component.KoinComponent import org.koin.core.component.inject import java.io.* @@ -27,6 +30,7 @@ class SmaliPatchStep( private val options: PatchOptions, ) : Step(), IDexProvider, KoinComponent { private val paths: PathManager by inject() + private val json: Json by inject() override val group = StepGroup.Patch override val localizedName = R.string.patch_step_patch_smali @@ -41,9 +45,29 @@ class SmaliPatchStep( val patches = mutableListOf() val localsBumps = mutableMapOf, Int>() - val disabledFiles = options.disabledPatchFiles() - val knownExtensionFiles = options.knownExtensionFiles() - val enabledExtensionFiles = options.enabledExtensionFiles() + + // The patch list/options metadata lives in the zip's manifest.json + val manifestSpecs = try { + ZipReader(patchesZip).use { it.openEntry("manifest.json")?.read() } + ?.let { json.decodeFromString(PatchManifest.serializer(), it.decodeToString()).patches } + ?.takeIf { it.isNotEmpty() } + } catch (t: Throwable) { + container.log("Failed to parse manifest.json (${t.message}); using built-in patch specs") + null + } + val specs = manifestSpecs ?: builtinPatchSpecs { "" } + container.log( + "Loaded ${specs.size} patch specs from " + + if (manifestSpecs != null) "manifest.json" else "built-in list" + ) + + val disabledFiles = options.disabledPatchFiles(specs) + val knownExtensionFiles = options.knownExtensionFiles(specs) + val enabledExtensionFiles = options.enabledExtensionFiles(specs) + val substitutions = options.smaliSubstitutions(specs) + if (substitutions.isNotEmpty()) { + container.log("Patch option substitutions: ${substitutions.entries.joinToString { "${it.key}=${it.value}" }}") + } // Load and parse all the patches from the smali archive. container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}") @@ -56,11 +80,11 @@ class SmaliPatchStep( if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) { val relative = patchFile.removePrefix("extension/") + // Only bundle helper smali patch/variant/sub-option is enabled if (relative in knownExtensionFiles && relative !in enabledExtensionFiles) { container.log("Skipping disabled extension smali: $relative") continue } - val out = smaliDir.resolve(relative) // Guard against zip-slip: a crafted entry could otherwise escape smaliDir. val baseCanonical = smaliDir.canonicalPath + File.separator @@ -84,11 +108,34 @@ class SmaliPatchStep( continue } - val lines = zip.openEntry(patchFile)!!.read() + var patchText = zip.openEntry(patchFile)!!.read() .decodeToString() .replace("\r\n", "\n") // Replace CRLF endings with LF .trimEnd { it == '\n' } // Remove trailing new lines - .split('\n') + + // Bake advanced option values into the patch + for ((token, value) in substitutions) { + if (patchText.contains(token)) { + patchText = patchText.replace(token, value) + container.log("Applied substitution $token -> $value in $patchFile") + } + } + + // Fail fast + UNRESOLVED_TOKEN.find(patchText)?.let { match -> + throw Error("Unresolved option placeholder ${match.value} in $patchFile") + } + + // Skip a patch that references a helper class which won't be bundled + val missingHelper = (knownExtensionFiles - enabledExtensionFiles).firstOrNull { rel -> + patchText.contains("L${rel.removeSuffix(".smali")};") + } + if (missingHelper != null) { + container.log("Skipping $patchFile: references gated-off helper class $missingHelper") + continue + } + + val lines = patchText.split('\n') try { for (directive in lines) { @@ -328,6 +375,7 @@ class SmaliPatchStep( private companion object { val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""") + val UNRESOLVED_TOKEN = Regex("""__RL_[A-Z0-9_]+__""") } /** diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt index 95afaaf..d0c8379 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/previews/screens/PatchOptionsScreenPreview.kt @@ -2,6 +2,8 @@ package com.meowarex.rlmobile.ui.previews.screens import android.content.res.Configuration import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.* import com.meowarex.rlmobile.network.utils.SemVer import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent @@ -18,6 +20,9 @@ private fun PatchOptionsScreenPreview( @PreviewParameter(PatchOptionsParametersProvider::class) parameters: PatchOptionsParameters, ) { + val context = LocalContext.current + val specs = remember { builtinPatchSpecs { context.getString(it) } } + ManagerTheme { PatchOptionsScreenContent( isUpdate = parameters.isUpdate, @@ -34,14 +39,14 @@ private fun PatchOptionsScreenPreview( onSelectCustomTidalApk = {}, customPatches = parameters.customPatches, onSelectCustomPatches = {}, - enabledPatchCount = KnownPatch.All.size, + specs = specs, + enabledPatchCount = specs.size, isPatchEnabled = { true }, onTogglePatch = { _, _ -> }, patchLockState = { PatchLock.Free }, variantIndex = { 0 }, onSelectVariant = { _, _ -> }, - isSubOptionEnabled = { _, _ -> true }, - onToggleSubOption = { _, _, _ -> }, + optionState = PatchOptionState.Preview, isConfigValid = parameters.isConfigValid, onInstall = {}, ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt index b3ffa32..5d2ec5d 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/KnownPatch.kt @@ -8,23 +8,14 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchDefault.Enabled data class PatchVariant( @StringRes val titleRes: Int, val fileNames: List, - val extensionFileNames: List = emptyList(), -) - -data class PatchSubOption( - val key: String, - @StringRes val titleRes: Int, - @StringRes val descRes: Int, - val fileNames: List, - val default: PatchDefault, - val extensionFileNames: List = emptyList(), + val extensionFiles: List = emptyList(), ) enum class KnownPatch( val order: Int, // Patch order in the UI List (lower = higher up) [Main Patches: multiples of 10 | Sub Patches: multiples of 1] val fileNames: List, - val extensionFileNames: List = emptyList(), + val extensionFiles: List = emptyList(), @StringRes val titleRes: Int, @StringRes val descRes: Int, val default: PatchDefault, // Default state of the patch in the UI List (enabled/disabled) @@ -32,7 +23,7 @@ enum class KnownPatch( val disables: List = emptyList(), val variants: List = emptyList(), val defaultVariantIndex: Int = 0, - val subOptions: List = emptyList(), + val advancedOptions: List = emptyList(), ) { LyricsDisableCover( order = 41, @@ -81,6 +72,30 @@ enum class KnownPatch( titleRes = R.string.patch_player_backdrop_title, descRes = R.string.patch_player_backdrop_desc, default = Enabled, + advancedOptions = listOf( + PatchOption.Slider( + key = "blur_strength", + titleRes = R.string.patch_opt_backdrop_blur_title, + descRes = R.string.patch_opt_backdrop_blur_desc, + default = 50f, + valueRange = 0f..100f, + steps = 19, // dots every 5% (0,5,…,100); snap only when locked + displayAsPercent = true, + token = "RL_BLUR_BITS", + encode = SmaliEncode(EncodeKind.FloatBits, scale = 1.8f), + ), + PatchOption.Slider( + key = "dimming", + titleRes = R.string.patch_opt_backdrop_dimming_title, + descRes = R.string.patch_opt_backdrop_dimming_desc, + default = 50f, // 50% == the original -0x80000000 + valueRange = 0f..100f, + steps = 19, // dots every 5% + displayAsPercent = true, + token = "RL_SCRIM_ARGB", + encode = SmaliEncode(EncodeKind.ArgbAlpha), + ), + ), ), QualityBadgeColors( order = 36, @@ -133,40 +148,64 @@ enum class KnownPatch( default = Disabled, defaultVariantIndex = 2, variants = listOf( - PatchVariant( + PatchVariant( // 0: Floating — stock rounded pill (no patch; the progress border is an option) titleRes = R.string.patch_mini_player_variant_floating_title, - fileNames = listOf("mini-player-floating.patch"), + fileNames = emptyList(), ), - PatchVariant( + PatchVariant( // 1: Grey — square, theme background (shown as "Legacy" when Dynamic BG is on) titleRes = R.string.patch_mini_player_variant_square_grey_title, fileNames = listOf("mini-player-grey.patch"), ), - PatchVariant( + PatchVariant( // 2: Black — square, forced black background (hidden when Dynamic BG is on) titleRes = R.string.patch_mini_player_variant_square_black_title, fileNames = listOf("mini-player-black.patch"), ), ), - ), - MiniPlayerGestures( - order = 52, - fileNames = listOf("mini-player-gestures.patch"), - extensionFileNames = listOf( - "radiant/MiniPlayerGestures.smali", - "radiant/MiniPlayerGestures\$Gesture.smali", - "radiant/MiniPlayerGestures\$RootGesture.smali", - "radiant/MiniPlayerGestures\$ApplyPending.smali", - ), - titleRes = R.string.patch_mini_player_gestures_title, - descRes = R.string.patch_mini_player_gestures_desc, - default = Enabled, - subOptions = listOf( - PatchSubOption( - key = "MiniPlayerGestures.LeftRight", + advancedOptions = listOf( + PatchOption.Toggle( + key = "dynamic_bg", + titleRes = R.string.patch_mini_player_dynamic_bg_title, + descRes = R.string.patch_mini_player_dynamic_bg_desc, + default = false, + inline = true, + fileNames = listOf("mini-player-dynamic-bg.patch"), + extensionFiles = listOf("radiant/MiniPlayerBackground.smali"), + hidesVariants = listOf(2), + relabelVariants = mapOf(1 to R.string.patch_mini_player_variant_legacy_title), + ), + // Animated progress border around the floating pill + PatchOption.Toggle( + key = "border", + titleRes = R.string.patch_mini_player_border_title, + descRes = R.string.patch_mini_player_border_desc, + default = false, + requiresVariant = 0, + fileNames = listOf("mini-player-floating-border.patch"), + extensionFiles = listOf("radiant/MiniSeekerFloating.smali"), + ), + // Swipe up to open the full player + PatchOption.Toggle( + key = "gestures", + titleRes = R.string.patch_mini_player_gestures_title, + descRes = R.string.patch_mini_player_gestures_desc, + default = true, + fileNames = listOf("mini-player-gestures.patch"), + extensionFiles = listOf( + "radiant/MiniPlayerGestures.smali", + "radiant/MiniPlayerGestures\$Gesture.smali", + "radiant/MiniPlayerGestures\$RootGesture.smali", + "radiant/MiniPlayerGestures\$ApplyPending.smali", + ), + ), + // Swipe left/right to skip + PatchOption.Toggle( + key = "next_prev", titleRes = R.string.patch_mini_player_left_right_gestures_title, descRes = R.string.patch_mini_player_left_right_gestures_desc, + default = false, + requiresOption = "gestures", fileNames = listOf("mini-player-gestures-left-right.patch"), - default = Disabled, - extensionFileNames = listOf( + extensionFiles = listOf( "radiant/MiniPlayerTrackGestures.smali", "radiant/MiniPlayerTrackGestures\$Gesture.smali", "radiant/MiniPlayerTrackGestures\$OffsetLayer.smali", @@ -177,13 +216,6 @@ enum class KnownPatch( ), ), ), - MiniPlayerDynamicBackground( - order = 51, - fileNames = listOf("mini-player-dynamic-bg.patch"), - titleRes = R.string.patch_mini_player_dynamic_bg_title, - descRes = R.string.patch_mini_player_dynamic_bg_desc, - default = Disabled, - ), EnableLegacyUi( order = 10, fileNames = listOf("enable-legacy-ui.patch"), @@ -200,9 +232,8 @@ enum class KnownPatch( PlayerBackdrop, QualityBadgeColors, LyricsProgressPill, - MiniPlayerDynamicBackground, CoverEverywhere, - MiniPlayerGestures, + MiniPlayerRedesign, ), ); diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt new file mode 100644 index 0000000..b32a26e --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOption.kt @@ -0,0 +1,68 @@ +package com.meowarex.rlmobile.ui.screens.patchopts + +import androidx.annotation.StringRes + +sealed interface PatchOption { + /** Stable identifier, unique within a single patch. */ + val key: String + + @get:StringRes + val titleRes: Int + + @get:StringRes + val descRes: Int + + /** A simple on/off switch. */ + data class Toggle( + override val key: String, + @StringRes override val titleRes: Int, + @StringRes override val descRes: Int, + val default: Boolean, + /** Patch files applied only while this toggle is on (and its patch is enabled). */ + val fileNames: List = emptyList(), + /** Helper smali extracted only while this toggle is on. */ + val extensionFiles: List = emptyList(), + /** Render inline beneath the variant selector instead of inside the advanced sheet. */ + val inline: Boolean = false, + /** Greyed out (with the lock dialog) unless this variant index is the selected one. */ + val requiresVariant: Int? = null, + /** Greyed out unless the sibling option with this key is currently on. */ + val requiresOption: String? = null, + /** Variant indices hidden from the picker while this toggle is on. */ + val hidesVariants: List = emptyList(), + /** Variant title overrides (index -> @StringRes) applied while this toggle is on. */ + val relabelVariants: Map = emptyMap(), + ) : PatchOption + + /** A continuous (or stepped) numeric value within [valueRange]. */ + data class Slider( + override val key: String, + @StringRes override val titleRes: Int, + @StringRes override val descRes: Int, + val default: Float, + val valueRange: ClosedFloatingPointRange, + /** Number of discrete steps between the range ends (0 = continuous). */ + val steps: Int = 0, + /** Render the value as a rounded percentage (e.g. "50%"). */ + val displayAsPercent: Boolean = false, + /** Optional unit suffix shown after the value (e.g. "dp"). */ + @StringRes val unitRes: Int? = null, + /** Placeholder name (without the surrounding `__`) baked into the `.patch` files. */ + val token: String? = null, + /** How the chosen value becomes the smali literal that replaces the [token]. */ + val encode: SmaliEncode? = null, + ) : PatchOption + + /** A single choice out of a small list, rendered as a segmented control. */ + data class Choice( + override val key: String, + @StringRes override val titleRes: Int, + @StringRes override val descRes: Int, + val entries: List, + val defaultIndex: Int = 0, + ) : PatchOption +} + +data class ChoiceEntry( + @StringRes val labelRes: Int, +) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt index 7a7f5ac..0676722 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptions.kt @@ -35,54 +35,86 @@ data class PatchOptions( val patchStates: Map = emptyMap(), val selectedVariants: Map = emptyMap(), + val optionFloats: Map = emptyMap(), + val optionBools: Map = emptyMap(), + val optionInts: Map = emptyMap(), ) : Parcelable { - fun isEnabled(patch: KnownPatch): Boolean = - patchStates[patch.name] ?: patch.default.isEnabled + fun isEnabled(spec: PatchSpec): Boolean = + patchStates[spec.id] ?: spec.defaultEnabled - fun isEnabled(subOption: PatchSubOption): Boolean = - patchStates[subOption.key] ?: subOption.default.isEnabled + fun variantIndex(spec: PatchSpec): Int { + val stored = (selectedVariants[spec.id] ?: spec.defaultVariantIndex) + .coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0)) + return spec.resolveVariantIndex(stored) { isToggleOn(spec, it) } + } - fun disabledPatchFiles(): Set = buildSet { - for (patch in KnownPatch.All) { - val enabled = isEnabled(patch) - if (patch.variants.isEmpty()) { - if (!enabled) addAll(patch.fileNames) + fun sliderValue(spec: PatchSpec, option: OptionSpec.Slider): Float = + (optionFloats["${spec.id}/${option.key}"] ?: option.default).coerceIn(option.min, option.max) + + fun isToggleOn(spec: PatchSpec, option: OptionSpec.Toggle): Boolean = + optionBools["${spec.id}/${option.key}"] ?: option.default + + fun isToggleActive(spec: PatchSpec, option: OptionSpec.Toggle): Boolean { + if (!isToggleOn(spec, option)) return false + option.requiresVariant?.let { if (variantIndex(spec) != it) return false } + option.requiresOption?.let { key -> + val required = spec.advancedOptions.filterIsInstance() + .firstOrNull { it.key == key } + if (required != null && !isToggleOn(spec, required)) return false + } + return true + } + + fun disabledPatchFiles(specs: List): Set = buildSet { + for (spec in specs) { + val enabled = isEnabled(spec) + if (spec.variants.isEmpty()) { + if (!enabled) addAll(spec.fileNames) } else { - val selected = (selectedVariants[patch.name] ?: patch.defaultVariantIndex) - .coerceIn(0, patch.variants.lastIndex) - patch.variants.forEachIndexed { index, variant -> + val selected = variantIndex(spec) + spec.variants.forEachIndexed { index, variant -> if (!enabled || index != selected) addAll(variant.fileNames) } } - for (subOption in patch.subOptions) { - if (!enabled || !isEnabled(subOption)) addAll(subOption.fileNames) + // Toggle sub-options that gate patch files. + for (option in spec.advancedOptions) { + if (option is OptionSpec.Toggle && option.fileNames.isNotEmpty()) { + if (!enabled || !isToggleActive(spec, option)) addAll(option.fileNames) + } } } } - fun knownExtensionFiles(): Set = buildSet { - for (patch in KnownPatch.All) { - addAll(patch.extensionFileNames) - patch.variants.forEach { addAll(it.extensionFileNames) } - patch.subOptions.forEach { addAll(it.extensionFileNames) } + fun knownExtensionFiles(specs: List): Set = buildSet { + for (spec in specs) { + addAll(spec.extensionFiles) + spec.variants.forEach { addAll(it.extensionFiles) } + spec.advancedOptions.forEach { if (it is OptionSpec.Toggle) addAll(it.extensionFiles) } } } - fun enabledExtensionFiles(): Set = buildSet { - for (patch in KnownPatch.All) { - if (!isEnabled(patch)) continue - - addAll(patch.extensionFileNames) - - if (patch.variants.isNotEmpty()) { - val selected = (selectedVariants[patch.name] ?: patch.defaultVariantIndex) - .coerceIn(0, patch.variants.lastIndex) - addAll(patch.variants[selected].extensionFileNames) + fun enabledExtensionFiles(specs: List): Set = buildSet { + for (spec in specs) { + if (!isEnabled(spec)) continue + addAll(spec.extensionFiles) + if (spec.variants.isNotEmpty()) { + spec.variants.getOrNull(variantIndex(spec))?.let { addAll(it.extensionFiles) } } + for (option in spec.advancedOptions) { + if (option is OptionSpec.Toggle && isToggleActive(spec, option)) addAll(option.extensionFiles) + } + } + } - for (subOption in patch.subOptions) { - if (isEnabled(subOption)) addAll(subOption.extensionFileNames) + fun smaliSubstitutions(specs: List): Map = buildMap { + for (spec in specs) { + if (!isEnabled(spec)) continue + for (option in spec.advancedOptions) { + if (option !is OptionSpec.Slider) continue + val token = option.token ?: continue + val encode = option.encode ?: continue + put("__${token}__", encode.encode(sliderValue(spec, option))) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt index ab28588..3f6462e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt @@ -2,21 +2,35 @@ package com.meowarex.rlmobile.ui.screens.patchopts import android.content.Context import android.content.pm.PackageManager.NameNotFoundException +import android.util.Log import androidx.compose.runtime.* import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.Navigator +import com.github.diamondminer88.zip.ZipReader +import com.meowarex.rlmobile.BuildConfig +import com.meowarex.rlmobile.manager.PathManager import com.meowarex.rlmobile.manager.PreferencesManager import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsScreen import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent import com.meowarex.rlmobile.ui.util.pushForResult import com.meowarex.rlmobile.util.* +import com.meowarex.rlmobile.manager.download.IDownloadManager +import com.meowarex.rlmobile.manager.download.KtorDownloadManager +import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService +import com.meowarex.rlmobile.network.utils.getOrThrow import kotlinx.coroutines.launch +import kotlinx.serialization.json.Json +import java.io.File class PatchOptionsModel( prefilledOptions: PatchOptions, private val context: Context, private val prefs: PreferencesManager, + private val paths: PathManager, + private val json: Json, + private val github: RadiantLyricsGithubService, + private val downloader: KtorDownloadManager, ) : ScreenModel { var packageName by mutableStateOf(prefilledOptions.packageName) private set @@ -47,84 +61,153 @@ class PatchOptionsModel( debuggable = value } + // The accordion renders from [specs]. It defaults to the compiled-in (localized) list and is + // rebuilt from a custom zip's manifest.json whenever a custom patch set is selected. (Rminder for others about the new custom patch selection flow) + + private val builtinSpecs: List = builtinPatchSpecs { context.getString(it) } + + var specs by mutableStateOf(builtinSpecs) + private set + + var specsLoading by mutableStateOf(false) + private set + var patchStates by mutableStateOf(prefilledOptions.patchStates) private set 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 variantIndex(spec: PatchSpec): Int = + (selectedVariants[spec.id] ?: spec.defaultVariantIndex) + .coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0)) - fun isPatchEnabled(patch: KnownPatch): Boolean = - patchStates[patch.name] ?: patch.default.isEnabled + fun isPatchEnabled(spec: PatchSpec): Boolean = + patchStates[spec.id] ?: spec.defaultEnabled - fun isSubOptionEnabled(patch: KnownPatch, subOption: PatchSubOption): Boolean = - isPatchEnabled(patch) && (patchStates[subOption.key] ?: subOption.default.isEnabled) - - fun setSubOptionEnabled(patch: KnownPatch, subOption: PatchSubOption, enabled: Boolean) { - if (subOption !in patch.subOptions) return - patchStates = patchStates + (subOption.key to enabled) - } - - fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) { - fun closure(seed: KnownPatch, step: (KnownPatch) -> List): Set = + fun setPatchEnabled(spec: PatchSpec, enabled: Boolean) { + val byId = specs.associateBy { it.id } + fun closure(seed: PatchSpec, step: (PatchSpec) -> List): Set = buildSet { - fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) } + fun walk(p: PatchSpec) { if (add(p)) step(p).forEach(::walk) } walk(seed) } - val enableUnits: Set - val disableUnits: Set + val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) } + val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } } + + val enableUnits: Set + val disableUnits: Set if (enabled) { - enableUnits = closure(patch) { it.requires } + enableUnits = closure(spec, requiresOf) disableUnits = enableUnits.flatMap { it.disables } - .flatMapTo(mutableSetOf()) { d -> - closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } } - } + .mapNotNull(byId::get) + .flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) } } else { enableUnits = emptySet() - disableUnits = closure(patch) { p -> KnownPatch.All.filter { p in it.requires } } + disableUnits = closure(spec, dependentsOf) } patchStates = patchStates.toMutableMap().apply { - enableUnits.forEach { this[it.name] = true } - disableUnits.forEach { this[it.name] = false } + enableUnits.forEach { this[it.id] = true } + disableUnits.forEach { this[it.id] = false } } } - fun selectVariant(patch: KnownPatch, index: Int) { - if (patch.variants.isEmpty() || index !in patch.variants.indices) return - selectedVariants = selectedVariants + (patch.name to index) + fun selectVariant(spec: PatchSpec, index: Int) { + if (spec.variants.isEmpty() || index !in spec.variants.indices) return + selectedVariants = selectedVariants + (spec.id to index) } - fun lockState(patch: KnownPatch): PatchLock { - if (patch.variants.isNotEmpty()) return PatchLock.Free + // Advanced (per-patch) options + // Values are seeded from the prefilled config, edited here, and written back out in + // generateConfig(). At patch time PatchOptions.smaliSubstitutions() turns slider values into + // the literals baked into the matching `.patch` files (see SmaliPatchStep). () - fun closure(seed: KnownPatch, step: (KnownPatch) -> List): Set = + private var optionBools by mutableStateOf(prefilledOptions.optionBools) + private var optionFloats by mutableStateOf(prefilledOptions.optionFloats) + private var optionInts by mutableStateOf(prefilledOptions.optionInts) + + private fun keyOf(spec: PatchSpec, option: OptionSpec): String = "${spec.id}/${option.key}" + + fun toggleValue(spec: PatchSpec, option: OptionSpec.Toggle): Boolean = + optionBools[keyOf(spec, option)] ?: option.default + + fun setToggleValue(spec: PatchSpec, option: OptionSpec.Toggle, value: Boolean) { + optionBools = optionBools + (keyOf(spec, option) to value) + } + + fun sliderValue(spec: PatchSpec, option: OptionSpec.Slider): Float = + (optionFloats[keyOf(spec, option)] ?: option.default).coerceIn(option.min, option.max) + + fun setSliderValue(spec: PatchSpec, option: OptionSpec.Slider, value: Float) { + optionFloats = optionFloats + (keyOf(spec, option) to value) + } + + fun choiceValue(spec: PatchSpec, option: OptionSpec.Choice): Int = + (optionInts[keyOf(spec, option)] ?: option.defaultIndex) + .coerceIn(0, option.entries.lastIndex.coerceAtLeast(0)) + + fun setChoiceValue(spec: PatchSpec, option: OptionSpec.Choice, index: Int) { + if (index !in option.entries.indices) return + optionInts = optionInts + (keyOf(spec, option) to index) + } + + fun isAdvancedModified(spec: PatchSpec): Boolean = spec.advancedOptions.any { option -> + when (option) { + is OptionSpec.Toggle -> toggleValue(spec, option) != option.default + is OptionSpec.Slider -> sliderValue(spec, option) != option.default + is OptionSpec.Choice -> choiceValue(spec, option) != option.defaultIndex + } + } + + fun resetAdvanced(spec: PatchSpec) { + val prefix = "${spec.id}/" + optionBools = optionBools.filterKeys { !it.startsWith(prefix) } + optionFloats = optionFloats.filterKeys { !it.startsWith(prefix) } + optionInts = optionInts.filterKeys { !it.startsWith(prefix) } + } + + val optionState: PatchOptionState = PatchOptionState( + toggle = ::toggleValue, + setToggle = ::setToggleValue, + slider = ::sliderValue, + setSlider = ::setSliderValue, + choice = ::choiceValue, + setChoice = ::setChoiceValue, + isModified = ::isAdvancedModified, + reset = ::resetAdvanced, + ) + + fun lockState(spec: PatchSpec): PatchLock { + if (spec.variants.isNotEmpty()) return PatchLock.Free + + val byId = specs.associateBy { it.id } + fun closure(seed: PatchSpec, step: (PatchSpec) -> List): Set = buildSet { - fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) } + fun walk(p: PatchSpec) { if (add(p)) step(p).forEach(::walk) } walk(seed) } - for (other in KnownPatch.All) { - if (other == patch || !isPatchEnabled(other)) continue + val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) } + val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } } - val requiresClosure = closure(other) { it.requires } - if (patch in requiresClosure - other) return PatchLock.LockedOn(other) + for (other in specs) { + if (other.id == spec.id || !isPatchEnabled(other)) continue + + val requiresClosure = closure(other, requiresOf) + if (spec 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) + .mapNotNull(byId::get) + .flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) } + if (spec in disablesClosure) return PatchLock.LockedOff(other) } return PatchLock.Free } val enabledPatchCount: Int - get() = KnownPatch.All.count { isPatchEnabled(it) } + get() = specs.count { isPatchEnabled(it) } var customTidalApk by mutableStateOf(null) private set @@ -147,6 +230,79 @@ class PatchOptionsModel( componentType = PatchComponent.Type.Patches, ) ) + reloadSpecs(customPatches) + } + + /** + * Rebuilds [specs] from the selected patch set + */ + private fun reloadSpecs(component: PatchComponent?) = screenModelScope.launchIO { + mainThread { specsLoading = true } + val loaded = if (component == null) builtinSpecs else loadManifestSpecs(component) ?: builtinSpecs + mainThread { + specs = loaded + validatePatchSelection() + specsLoading = false + } + } + + private fun loadManifestSpecs(component: PatchComponent): List? = + loadManifestSpecs(component.getFile(paths)) + + private fun loadManifestSpecs(file: File): List? { + if (!file.exists()) return null + return try { + val bytes = ZipReader(file).use { it.openEntry(MANIFEST_NAME)?.read() } ?: return null + json.decodeFromString(PatchManifest.serializer(), bytes.decodeToString()) + .patches + .takeIf { it.isNotEmpty() } + } catch (t: Throwable) { + Log.w(BuildConfig.TAG, "Failed to parse $MANIFEST_NAME; using built-in list", t) + null + } + } + + /** + * Pulls the patch list from the latest GitHub release's patches.zip manifest + */ + private suspend fun loadLatestReleaseSpecs(): List? = try { + val release = github.getLatestRelease().getOrThrow() + val url = release.assets + .find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME } + ?.browserDownloadUrl + if (url == null) { + Log.w(BuildConfig.TAG, "Latest release ${release.tagName} has no patches.zip asset; using built-in list") + null + } else { + // Cached per release tag so re-opening the screen doesn't re-download the same zip. + val dest = paths.cacheDownloadDir.resolve("manifest-${release.tagName}.zip") + .apply { parentFile?.mkdirs() } + if (dest.exists() || downloader.download(url, dest) is IDownloadManager.Result.Success) { + loadManifestSpecs(dest).also { loaded -> + if (loaded != null) { + Log.i(BuildConfig.TAG, "Loaded ${loaded.size} patches from latest release ${release.tagName} manifest") + } else { + Log.w(BuildConfig.TAG, "Latest release ${release.tagName} ships no manifest.json; using built-in list") + } + } + } else { + null + } + } + } catch (t: Throwable) { + Log.w(BuildConfig.TAG, "Failed to load latest release manifest; using built-in list", t) + null + } + + /** Default (non-custom) source: the latest release manifest, falling back to the built-in list. */ + private fun loadDefaultSpecs() = screenModelScope.launchIO { + mainThread { specsLoading = true } + val loaded = loadLatestReleaseSpecs() ?: builtinSpecs + mainThread { + specs = loaded + validatePatchSelection() + specsLoading = false + } } val isConfigValid by derivedStateOf { @@ -169,6 +325,9 @@ class PatchOptionsModel( customPatches = customPatches, patchStates = patchStates, selectedVariants = selectedVariants, + optionFloats = optionFloats, + optionBools = optionBools, + optionInts = optionInts, ) } @@ -194,9 +353,9 @@ class PatchOptionsModel( } private fun validatePatchSelection() { - for (patch in KnownPatch.All) { - if (isPatchEnabled(patch)) { - setPatchEnabled(patch, true) + for (spec in specs) { + if (isPatchEnabled(spec)) { + setPatchEnabled(spec, true) } } } @@ -204,9 +363,13 @@ class PatchOptionsModel( init { validatePatchSelection() screenModelScope.launchBlock { fetchPkgNameState() } + // Default source is the latest release's manifest; custom selections drive themselves. + if (customPatches == null) loadDefaultSpecs() } companion object { + private const val MANIFEST_NAME = "manifest.json" + private val PACKAGE_REGEX = """^[a-z]\w*(\.[a-z]\w*)+$""" .toRegex(RegexOption.IGNORE_CASE) } @@ -219,7 +382,7 @@ enum class PackageNameState { } sealed class PatchLock { - object Free : PatchLock() - data class LockedOn(val by: KnownPatch) : PatchLock() - data class LockedOff(val by: KnownPatch) : PatchLock() + data object Free : PatchLock() + data class LockedOn(val by: PatchSpec) : PatchLock() + data class LockedOff(val by: PatchSpec) : PatchLock() } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt index 0b4a0f3..91c204c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt @@ -60,14 +60,14 @@ class PatchOptionsScreen( onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) }, onSelectCustomPatches = { model.selectCustomPatches(navigator) }, + specs = model.specs, enabledPatchCount = model.enabledPatchCount, isPatchEnabled = model::isPatchEnabled, onTogglePatch = model::setPatchEnabled, patchLockState = model::lockState, variantIndex = model::variantIndex, onSelectVariant = model::selectVariant, - isSubOptionEnabled = model::isSubOptionEnabled, - onToggleSubOption = model::setSubOptionEnabled, + optionState = model.optionState, isConfigValid = model.isConfigValid, onInstall = { @@ -98,14 +98,14 @@ fun PatchOptionsScreenContent( customPatches: PatchComponent?, onSelectCustomPatches: () -> Unit, + specs: List, enabledPatchCount: Int, - isPatchEnabled: (KnownPatch) -> Boolean, - onTogglePatch: (KnownPatch, Boolean) -> Unit, - patchLockState: (KnownPatch) -> PatchLock, - variantIndex: (KnownPatch) -> Int, - onSelectVariant: (KnownPatch, Int) -> Unit, - isSubOptionEnabled: (KnownPatch, PatchSubOption) -> Boolean, - onToggleSubOption: (KnownPatch, PatchSubOption, Boolean) -> Unit, + isPatchEnabled: (PatchSpec) -> Boolean, + onTogglePatch: (PatchSpec, Boolean) -> Unit, + patchLockState: (PatchSpec) -> PatchLock, + variantIndex: (PatchSpec) -> Int, + onSelectVariant: (PatchSpec, Int) -> Unit, + optionState: PatchOptionState, isConfigValid: Boolean, onInstall: () -> Unit, @@ -168,15 +168,15 @@ fun PatchOptionsScreenContent( } PatchSelectionAccordion( + specs = specs, enabledCount = enabledPatchCount, - totalCount = KnownPatch.All.size, + totalCount = specs.size, isEnabled = isPatchEnabled, onToggle = onTogglePatch, lockState = patchLockState, variantIndex = variantIndex, onSelectVariant = onSelectVariant, - isSubOptionEnabled = isSubOptionEnabled, - onToggleSubOption = onToggleSubOption, + optionState = optionState, modifier = Modifier.padding(top = 4.dp), ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt new file mode 100644 index 0000000..03a7780 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchSpec.kt @@ -0,0 +1,268 @@ +package com.meowarex.rlmobile.ui.screens.patchopts + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import kotlin.math.roundToInt + +@Serializable +data class PatchManifest( + val version: Int = 1, + val patches: List = emptyList(), +) + +@Immutable +@Serializable +data class PatchSpec( + val id: String, + val order: Int = 0, + val fileNames: List = emptyList(), + val extensionFiles: List = emptyList(), + val title: String, + val description: String = "", + @SerialName("default") val defaultEnabled: Boolean = false, + /** ids of patches that get force-enabled when this one is enabled. */ + val requires: List = emptyList(), + /** ids of patches that get force-disabled when this one is enabled. */ + val disables: List = emptyList(), + val variants: List = emptyList(), + val defaultVariantIndex: Int = 0, + val advancedOptions: List = emptyList(), +) + +@Immutable +@Serializable +data class VariantSpec( + val title: String, + val fileNames: List = emptyList(), + val extensionFiles: List = emptyList(), +) + +/** A single advanced option. Discriminated by `"type"` in JSON. */ +@Serializable +sealed interface OptionSpec { + val key: String + val title: String + val description: String + + @Immutable + @Serializable + @SerialName("toggle") + data class Toggle( + override val key: String, + override val title: String, + override val description: String = "", + val default: Boolean = false, + /** Patch files applied only while this toggle is on (and its patch is enabled). */ + val fileNames: List = emptyList(), + /** Helper smali extracted only while this toggle is on. */ + val extensionFiles: List = emptyList(), + /** Render inline beneath the variant selector instead of inside the advanced sheet. */ + val inline: Boolean = false, + /** Greyed out (with the lock dialog) unless this variant index is the selected one. */ + val requiresVariant: Int? = null, + /** Greyed out unless the sibling option with this key is currently on. */ + val requiresOption: String? = null, + /** Variant indices hidden from the picker while this toggle is on. */ + val hidesVariants: List = emptyList(), + /** Variant title overrides (index -> title) applied while this toggle is on. */ + val relabelVariants: Map = emptyMap(), + ) : OptionSpec + + @Immutable + @Serializable + @SerialName("slider") + data class Slider( + override val key: String, + override val title: String, + override val description: String = "", + val default: Float = 0f, + val min: Float = 0f, + val max: Float = 100f, + val steps: Int = 0, + val displayAsPercent: Boolean = false, + val unit: String? = null, + /** Placeholder name (without the surrounding `__`) baked into the `.patch` files. */ + val token: String? = null, + /** How the chosen value becomes the smali literal that replaces the token. */ + val encode: SmaliEncode? = null, + ) : OptionSpec { + val valueRange: ClosedFloatingPointRange get() = min..max + } + + @Immutable + @Serializable + @SerialName("choice") + data class Choice( + override val key: String, + override val title: String, + override val description: String = "", + val entries: List = emptyList(), + val defaultIndex: Int = 0, + ) : OptionSpec +} + +@Serializable +enum class EncodeKind { + @SerialName("floatBits") + FloatBits, + + @SerialName("argbAlpha") + ArgbAlpha, + + @SerialName("int") + IntValue, +} + +@Immutable +@Serializable +data class SmaliEncode( + val kind: EncodeKind, + val scale: Float = 1f, +) { + fun encode(value: Float): String = when (kind) { + // Float dp bit-pattern: const vX, ; Dp.constructor-impl(F)F + EncodeKind.FloatBits -> (value * scale).toRawBits().toString() + // ARGB int with black RGB and a percentage-derived alpha: const vX, + EncodeKind.ArgbAlpha -> ((value / 100f * 255f).roundToInt() shl 24).toString() + // Plain (optionally scaled) integer literal. + EncodeKind.IntValue -> (value * scale).roundToInt().toString() + } +} + +fun builtinPatchSpecs(resolve: (Int) -> String): List = + KnownPatch.All.map { patch -> + PatchSpec( + id = patch.name, + order = patch.order, + fileNames = patch.fileNames, + extensionFiles = patch.extensionFiles, + title = resolve(patch.titleRes), + description = resolve(patch.descRes), + defaultEnabled = patch.default.isEnabled, + requires = patch.requires.map { it.name }, + disables = patch.disables.map { it.name }, + variants = patch.variants.map { VariantSpec(resolve(it.titleRes), it.fileNames, it.extensionFiles) }, + defaultVariantIndex = patch.defaultVariantIndex, + advancedOptions = patch.advancedOptions.map { it.toSpec(resolve) }, + ) + } + +private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (this) { + is PatchOption.Toggle -> OptionSpec.Toggle( + key = key, + title = resolve(titleRes), + description = resolve(descRes), + default = default, + fileNames = fileNames, + extensionFiles = extensionFiles, + inline = inline, + requiresVariant = requiresVariant, + requiresOption = requiresOption, + hidesVariants = hidesVariants, + relabelVariants = relabelVariants.mapValues { resolve(it.value) }, + ) + + is PatchOption.Slider -> OptionSpec.Slider( + key = key, + title = resolve(titleRes), + description = resolve(descRes), + default = default, + min = valueRange.start, + max = valueRange.endInclusive, + steps = steps, + displayAsPercent = displayAsPercent, + unit = unitRes?.let(resolve), + token = token, + encode = encode, + ) + + is PatchOption.Choice -> OptionSpec.Choice( + key = key, + title = resolve(titleRes), + description = resolve(descRes), + entries = entries.map { resolve(it.labelRes) }, + defaultIndex = defaultIndex, + ) +} + +@Immutable +data class EffectiveVariant(val originalIndex: Int, val title: String) + +fun PatchSpec.effectiveVariants(isOptionOn: (OptionSpec.Toggle) -> Boolean): List { + if (variants.isEmpty()) return emptyList() + val hidden = mutableSetOf() + val relabel = mutableMapOf() + for (option in advancedOptions) { + if (option is OptionSpec.Toggle && isOptionOn(option)) { + hidden += option.hidesVariants + relabel += option.relabelVariants + } + } + return variants.mapIndexedNotNull { index, variant -> + if (index in hidden) null else EffectiveVariant(index, relabel[index] ?: variant.title) + } +} + +fun PatchSpec.resolveVariantIndex(stored: Int, isOptionOn: (OptionSpec.Toggle) -> Boolean): Int { + val visible = effectiveVariants(isOptionOn) + if (visible.isEmpty() || visible.any { it.originalIndex == stored }) return stored + val relabelKeys = buildSet { + for (option in advancedOptions) { + if (option is OptionSpec.Toggle && isOptionOn(option)) addAll(option.relabelVariants.keys) + } + } + return visible.firstOrNull { it.originalIndex in relabelKeys }?.originalIndex + ?: visible.first().originalIndex +} + +sealed interface OptionLock { + data object Free : OptionLock + data class NeedsVariant(val title: String) : OptionLock + data class NeedsOption(val title: String) : OptionLock +} + +fun PatchSpec.optionLock( + option: OptionSpec, + selectedVariant: Int, + isOptionOn: (OptionSpec.Toggle) -> Boolean, +): OptionLock { + if (option !is OptionSpec.Toggle) return OptionLock.Free + option.requiresVariant?.let { required -> + if (selectedVariant != required) { + variants.getOrNull(required)?.let { return OptionLock.NeedsVariant(it.title) } + } + } + option.requiresOption?.let { key -> + val required = advancedOptions.filterIsInstance().firstOrNull { it.key == key } + if (required != null && !isOptionOn(required)) return OptionLock.NeedsOption(required.title) + } + return OptionLock.Free +} + +@Stable +class PatchOptionState( + val toggle: (PatchSpec, OptionSpec.Toggle) -> Boolean, + val setToggle: (PatchSpec, OptionSpec.Toggle, Boolean) -> Unit, + val slider: (PatchSpec, OptionSpec.Slider) -> Float, + val setSlider: (PatchSpec, OptionSpec.Slider, Float) -> Unit, + val choice: (PatchSpec, OptionSpec.Choice) -> Int, + val setChoice: (PatchSpec, OptionSpec.Choice, Int) -> Unit, + val isModified: (PatchSpec) -> Boolean, + val reset: (PatchSpec) -> Unit, +) { + companion object { + /** Read-only stub returning defaults — for Compose previews. */ + val Preview = PatchOptionState( + toggle = { _, option -> option.default }, + setToggle = { _, _, _ -> }, + slider = { _, option -> option.default }, + setSlider = { _, _, _ -> }, + choice = { _, option -> option.defaultIndex }, + setChoice = { _, _, _ -> }, + isModified = { false }, + reset = {}, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt new file mode 100644 index 0000000..b6a0e74 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt @@ -0,0 +1,386 @@ +package com.meowarex.rlmobile.ui.screens.patchopts.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +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.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.ResetToDefaultButton +import com.meowarex.rlmobile.ui.screens.patchopts.OptionLock +import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState +import com.meowarex.rlmobile.ui.screens.patchopts.optionLock +import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatchAdvancedOptionsSheet( + patch: PatchSpec, + state: PatchOptionState, + selectedVariant: Int, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() + var lockDialog by remember { mutableStateOf(null) } + + fun dismiss() { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) onDismiss() + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp) + .padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.patchopts_advanced_sheet_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = patch.title, + style = MaterialTheme.typography.headlineSmall, + ) + } + + ResetToDefaultButton( + enabled = state.isModified(patch), + onClick = { state.reset(patch) }, + ) + } + + HorizontalDivider() + + val sheetOptions = patch.advancedOptions.filter { it !is OptionSpec.Toggle || !it.inline } + for (option in sheetOptions) key(option.key) { + val lock = patch.optionLock(option, selectedVariant) { state.toggle(patch, it) } + when (option) { + is OptionSpec.Toggle -> ToggleOptionRow( + title = option.title, + description = option.description, + checked = state.toggle(patch, option), + onCheckedChange = { state.setToggle(patch, option, it) }, + locked = lock != OptionLock.Free, + onLockedTap = { lockDialog = lock }, + ) + + is OptionSpec.Slider -> SliderOptionRow( + title = option.title, + description = option.description, + value = state.slider(patch, option), + valueLabel = formatSliderValue(option, state.slider(patch, option)), + valueRange = option.valueRange, + steps = option.steps, + onValueChange = { state.setSlider(patch, option, it) }, + ) + + is OptionSpec.Choice -> ChoiceOptionRow( + title = option.title, + description = option.description, + entries = option.entries, + selectedIndex = state.choice(patch, option), + onSelect = { state.setChoice(patch, option, it) }, + ) + } + } + + FilledTonalButton( + onClick = { dismiss() }, + colors = ButtonDefaults.filledTonalButtonColors( + contentColor = MaterialTheme.colorScheme.primary, + ), + modifier = Modifier + .align(Alignment.End) + .padding(top = 4.dp), + ) { + Text(stringResource(R.string.action_done)) + } + } + } + + lockDialog?.let { lock -> + OptionLockDialog(lock = lock, onDismiss = { lockDialog = null }) + } +} + +@Composable +private fun ToggleOptionRow( + title: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + locked: Boolean = false, + onLockedTap: () -> Unit = {}, +) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Switch, + ) { if (locked) onLockedTap() else onCheckedChange(!checked) } + .alpha(if (locked) 0.45f else 1f), + ) { + OptionText( + title = title, + description = description, + modifier = Modifier.weight(1f), + ) + + Box { + Switch( + checked = checked, + enabled = !locked, + onCheckedChange = onCheckedChange, + interactionSource = interactionSource, + ) + if (locked) { + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = remember(::MutableInteractionSource), + indication = null, + role = Role.Switch, + ) { onLockedTap() } + ) + } + } + } +} + +@Composable +private fun SliderOptionRow( + title: String, + description: String, + value: Float, + valueLabel: String, + valueRange: ClosedFloatingPointRange, + steps: Int, + onValueChange: (Float) -> Unit, +) { + // Whether the slider snaps to the step dots + var snap by rememberSaveable { mutableStateOf(true) } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f), + ) + Text( + text = valueLabel, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + if (steps > 0) { + IconButton( + onClick = { + snap = !snap + if (snap) onValueChange(snapToNearestStep(value, valueRange, steps)) + }, + modifier = Modifier.size(32.dp), + ) { + Icon( + painter = painterResource( + if (snap) R.drawable.ic_lock else R.drawable.ic_lock_open, + ), + contentDescription = stringResource(R.string.patchopts_toggle_snap), + tint = if (snap) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + } + } + + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + + // Locked -> native stepped slider (dots + snapping), left exactly as-is + // Unlocked -> plain continuous slider with the trailing stop-indicator dot removed + if (snap) { + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = steps, + ) + } else { + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + track = { SliderDefaults.Track(sliderState = it, drawStopIndicator = null) }, + ) + } + } +} + +private fun snapToNearestStep( + value: Float, + range: ClosedFloatingPointRange, + steps: Int, +): Float { + if (steps <= 0) return value + val stepSize = (range.endInclusive - range.start) / (steps + 1) + if (stepSize <= 0f) return value + val snapped = range.start + Math.round((value - range.start) / stepSize) * stepSize + return snapped.coerceIn(range.start, range.endInclusive) +} + +@Composable +private fun ChoiceOptionRow( + title: String, + description: String, + entries: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + OptionText(title = title, description = description) + + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + entries.forEachIndexed { index, entry -> + SegmentedButton( + selected = index == selectedIndex, + onClick = { onSelect(index) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = entries.size), + icon = {}, + label = { + Text( + text = entry, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelMedium, + ) + }, + ) + } + } + } +} + +@Composable +private fun OptionText( + title: String, + description: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } +} + +private fun formatSliderValue(option: OptionSpec.Slider, value: Float): String { + val rounded = value.roundToInt() + val unit = option.unit + return when { + option.displayAsPercent -> "$rounded%" + unit != null -> "$rounded $unit" + else -> rounded.toString() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun OptionLockDialog(lock: OptionLock, onDismiss: () -> Unit) { + val message = when (lock) { + is OptionLock.NeedsVariant -> stringResource(R.string.patch_opt_lock_needs_variant, lock.title) + is OptionLock.NeedsOption -> stringResource(R.string.patch_opt_lock_needs_option, lock.title) + OptionLock.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(R.string.patch_opt_lock_title), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = AnnotatedString.fromHtml(message), + style = MaterialTheme.typography.titleMedium, + ) + Box(modifier = Modifier.fillMaxWidth()) { + TextButton( + onClick = onDismiss, + modifier = Modifier.align(Alignment.CenterEnd), + ) { + Text(stringResource(R.string.action_got_it)) + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt index 99d66f0..cfadac6 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -22,23 +23,26 @@ 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.OptionSpec import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock -import com.meowarex.rlmobile.ui.screens.patchopts.PatchSubOption +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState +import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec +import com.meowarex.rlmobile.ui.screens.patchopts.effectiveVariants +import com.meowarex.rlmobile.ui.screens.patchopts.resolveVariantIndex -private data class LockInfo(val patch: KnownPatch, val lock: PatchLock) +private data class LockInfo(val patch: PatchSpec, val lock: PatchLock) @Composable fun PatchSelectionAccordion( + specs: List, enabledCount: Int, totalCount: Int, - isEnabled: (KnownPatch) -> Boolean, - onToggle: (KnownPatch, Boolean) -> Unit, - lockState: (KnownPatch) -> PatchLock, - variantIndex: (KnownPatch) -> Int, - onSelectVariant: (KnownPatch, Int) -> Unit, - isSubOptionEnabled: (KnownPatch, PatchSubOption) -> Boolean, - onToggleSubOption: (KnownPatch, PatchSubOption, Boolean) -> Unit, + isEnabled: (PatchSpec) -> Boolean, + onToggle: (PatchSpec, Boolean) -> Unit, + lockState: (PatchSpec) -> PatchLock, + variantIndex: (PatchSpec) -> Int, + onSelectVariant: (PatchSpec, Int) -> Unit, + optionState: PatchOptionState, modifier: Modifier = Modifier, ) { var expanded by rememberSaveable { mutableStateOf(false) } @@ -48,6 +52,7 @@ fun PatchSelectionAccordion( ) var lockInfo by remember { mutableStateOf(null) } + var advancedFor by remember { mutableStateOf(null) } Column( modifier = modifier @@ -107,12 +112,12 @@ fun PatchSelectionAccordion( .padding(bottom = 8.dp), ) - for (patch in KnownPatch.All) key(patch) { + for (patch in specs) key(patch.id) { val checked = isEnabled(patch) val lock = lockState(patch) PatchSwitchRow( - title = stringResource(patch.titleRes), - description = stringResource(patch.descRes), + title = patch.title, + description = patch.description, checked = checked, lock = lock, onCheckedChange = { onToggle(patch, it) }, @@ -126,37 +131,62 @@ fun PatchSelectionAccordion( .fillMaxWidth() .padding(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), ) { + val effective = patch.effectiveVariants { optionState.toggle(patch, it) } + val resolved = patch.resolveVariantIndex(variantIndex(patch)) { + optionState.toggle(patch, it) + } PatchVariantSelector( - variants = patch.variants, - selectedIndex = variantIndex(patch), - onSelect = { idx -> onSelectVariant(patch, idx) }, + variants = effective, + selectedIndex = effective + .indexOfFirst { it.originalIndex == resolved } + .coerceAtLeast(0), + onSelect = { pos -> + effective.getOrNull(pos)?.let { onSelectVariant(patch, it.originalIndex) } + }, ) } } } - if (patch.subOptions.isNotEmpty()) { + val inlineToggles = patch.advancedOptions + .filterIsInstance() + .filter { it.inline } + if (inlineToggles.isNotEmpty()) { AnimatedVisibility(visible = checked) { Column( modifier = Modifier .fillMaxWidth() - .padding(start = 12.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), + .padding(horizontal = 4.dp), + verticalArrangement = Arrangement.spacedBy(2.dp), ) { - for (subOption in patch.subOptions) key(subOption) { - PatchSwitchRow( - title = stringResource(subOption.titleRes), - description = stringResource(subOption.descRes), - checked = isSubOptionEnabled(patch, subOption), - lock = PatchLock.Free, - onCheckedChange = { - onToggleSubOption(patch, subOption, it) - }, - onLockedTap = {}, + for (option in inlineToggles) key(option.key) { + InlineToggleRow( + title = option.title, + description = option.description, + checked = optionState.toggle(patch, option), + onCheckedChange = { optionState.setToggle(patch, option, it) }, ) } } } } + + val hasSheetOptions = patch.advancedOptions.any { it !is OptionSpec.Toggle || !it.inline } + if (hasSheetOptions) { + AnimatedVisibility(visible = checked) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + contentAlignment = Alignment.Center, + ) { + AdvancedOptionsButton( + modified = optionState.isModified(patch), + onClick = { advancedFor = patch }, + ) + } + } + } } } } @@ -169,6 +199,84 @@ fun PatchSelectionAccordion( onDismiss = { lockInfo = null }, ) } + + advancedFor?.let { patch -> + PatchAdvancedOptionsSheet( + patch = patch, + state = optionState, + selectedVariant = patch.resolveVariantIndex(variantIndex(patch)) { optionState.toggle(patch, it) }, + onDismiss = { advancedFor = null }, + ) + } +} + +@Composable +private fun AdvancedOptionsButton( + modified: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + FilledTonalButton( + onClick = onClick, + modifier = modifier, + ) { + Icon( + painter = painterResource(R.drawable.ic_tune), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.patchopts_advanced_button)) + if (modified) { + Spacer(Modifier.width(8.dp)) + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + ) + } + } +} + +@Composable +private fun InlineToggleRow( + title: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Switch, + ) { onCheckedChange(!checked) } + .padding(vertical = 4.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f), + ) { + Text(text = title, style = MaterialTheme.typography.titleSmall) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + interactionSource = interactionSource, + ) + } } @Composable @@ -238,7 +346,7 @@ private fun PatchSwitchRow( @OptIn(ExperimentalMaterial3Api::class) @Composable private fun PatchLockDialog( - thisPatch: KnownPatch, + thisPatch: PatchSpec, lock: PatchLock, onDismiss: () -> Unit, ) { @@ -246,12 +354,12 @@ private fun PatchLockDialog( is PatchLock.LockedOn -> Triple( R.string.patch_lock_required_title, R.string.patch_lock_required_msg, - stringResource(lock.by.titleRes), + lock.by.title, ) is PatchLock.LockedOff -> Triple( R.string.patch_lock_blocked_title, R.string.patch_lock_blocked_msg, - stringResource(lock.by.titleRes), + lock.by.title, ) PatchLock.Free -> return } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt index 9f9274d..a22d37b 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt @@ -4,15 +4,14 @@ 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 +import com.meowarex.rlmobile.ui.screens.patchopts.EffectiveVariant @OptIn(ExperimentalMaterial3Api::class) @Composable fun PatchVariantSelector( - variants: List, + variants: List, selectedIndex: Int, onSelect: (Int) -> Unit, modifier: Modifier = Modifier, @@ -31,7 +30,7 @@ fun PatchVariantSelector( icon = {}, label = { Text( - text = stringResource(variant.titleRes), + text = variant.title, maxLines = 1, overflow = TextOverflow.Ellipsis, textAlign = TextAlign.Center, diff --git a/Manager/app/src/main/res/drawable/ic_lock.xml b/Manager/app/src/main/res/drawable/ic_lock.xml new file mode 100644 index 0000000..ecf7b11 --- /dev/null +++ b/Manager/app/src/main/res/drawable/ic_lock.xml @@ -0,0 +1,9 @@ + + + diff --git a/Manager/app/src/main/res/drawable/ic_lock_open.xml b/Manager/app/src/main/res/drawable/ic_lock_open.xml new file mode 100644 index 0000000..8492e43 --- /dev/null +++ b/Manager/app/src/main/res/drawable/ic_lock_open.xml @@ -0,0 +1,9 @@ + + + diff --git a/Manager/app/src/main/res/drawable/ic_tune.xml b/Manager/app/src/main/res/drawable/ic_tune.xml new file mode 100644 index 0000000..81e11ae --- /dev/null +++ b/Manager/app/src/main/res/drawable/ic_tune.xml @@ -0,0 +1,9 @@ + + + diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index 0ebc611..9b7c46e 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -22,6 +22,7 @@ Retry installation Open error log Apply + Done Confirm Dismiss Install @@ -251,6 +252,9 @@ Patches Toggle which patches are applied during installation. Unchecked patches are skipped entirely. %1$d of %2$d enabled + Advanced Options + Advanced Options + Toggle snapping to steps Disable Lyrics Cover Removes the mini track cover from the lyrics screen. @@ -298,11 +302,23 @@ Floating Grey Black + Legacy + Progress Border + Draws an animated progress ring around the floating mini-player. Requires the Floating style. + + + Blur Strength + How strongly the album art behind the player is blurred. + Backdrop Dimming + Adjusts the level of darkening on the backdrop to keep foreground controls legible. Patch Required! Patch Blocked! Patch Required by <b>%s</b>. Patch Blocked by <b>%s</b>. + Option Locked + Requires the <b>%s</b> style. + Requires the <b>%s</b> option. Got it Custom Component (%s) diff --git a/patches/manifest.json b/patches/manifest.json new file mode 100644 index 0000000..273f80d --- /dev/null +++ b/patches/manifest.json @@ -0,0 +1,212 @@ +{ + "version": 1, + "patches": [ + { + "id": "EnableLegacyUi", + "order": 10, + "fileNames": ["enable-legacy-ui.patch"], + "title": "Enable Legacy UI", + "description": "[This patch will stop working soon] - Replaces the New Compose based UI with the Legacy UI (disables player-market-ui feature flag)", + "default": false, + "requires": ["DebugMenuUnlock"], + "disables": [ + "LyricsDisableCover", + "LyricsReplaceLyricsButton", + "LyricsReplaceShareButton", + "LyricsRlApi", + "LyricsKeepControlsVisible", + "PlayerBackdrop", + "QualityBadgeColors", + "LyricsProgressPill", + "CoverEverywhere", + "MiniPlayerRedesign" + ] + }, + { + "id": "LyricsRlApi", + "order": 20, + "fileNames": ["lyrics-rl-api.patch", "lyrics-rl-api-observer.patch"], + "title": "Radiant Lyrics API - WIP", + "description": "Use Radiant Lyrics API to fetch Lyrics (Higher quality & More providers)", + "default": false + }, + { + "id": "PlayerBackdrop", + "order": 30, + "fileNames": ["player-backdrop.patch"], + "title": "Player Backdrop", + "description": "Restores the legacy translucent backdrop blur behind the player.", + "default": true, + "advancedOptions": [ + { + "type": "slider", + "key": "blur_strength", + "title": "Blur Strength", + "description": "How strongly the album art behind the player is blurred. 0% is sharp, 50% matches the original look.", + "default": 50, + "min": 0, + "max": 100, + "steps": 19, + "displayAsPercent": true, + "token": "RL_BLUR_BITS", + "encode": { "kind": "floatBits", "scale": 1.8 } + }, + { + "type": "slider", + "key": "dimming", + "title": "Backdrop Dimming", + "description": "Darkens the backdrop so the foreground controls stay legible.", + "default": 50, + "min": 0, + "max": 100, + "steps": 19, + "displayAsPercent": true, + "token": "RL_SCRIM_ARGB", + "encode": { "kind": "argbAlpha" } + } + ] + }, + { + "id": "CoverEverywhere", + "order": 35, + "fileNames": ["home-backdrop.patch", "collection-backdrop.patch", "cover-capture.patch"], + "title": "Cover Everywhere - WIP", + "description": "Applies the blurred backdrop to every Compose based page (Home & Collection)", + "default": false + }, + { + "id": "QualityBadgeColors", + "order": 36, + "fileNames": ["player-quality-badge-colors.patch"], + "title": "Quality Badge Colors", + "description": "Brings color back to the player quality badge.", + "default": true + }, + { + "id": "PlayerOneHanded", + "order": 37, + "fileNames": ["player-one-handed.patch"], + "title": "One-Handed Mode", + "description": "Mirrors the bottom player buttons left-to-right for easier one-handed reach.", + "default": false + }, + { + "id": "LyricsProgressPill", + "order": 40, + "fileNames": ["lyrics-progress-pill.patch", "lyrics-fade-region.patch"], + "title": "Lyrics Progress Pill", + "description": "Restores the old Track Progress Pill in the top of the Lyrics screen!", + "default": true, + "requires": ["LyricsDisableCover", "LyricsReplaceLyricsButton", "LyricsReplaceShareButton"] + }, + { + "id": "LyricsDisableCover", + "order": 41, + "fileNames": ["lyrics-disable-cover.patch"], + "title": "Disable Lyrics Cover", + "description": "Removes the mini track cover from the lyrics screen.", + "default": true + }, + { + "id": "LyricsReplaceLyricsButton", + "order": 42, + "fileNames": ["lyrics-replace-lyrics-button.patch", "lyrics-sparkle-conditional-visibility.patch"], + "title": "Replace Lyrics Button", + "description": "Replaces the Lyrics button with the RL Sparkle!", + "default": true + }, + { + "id": "LyricsReplaceShareButton", + "order": 43, + "fileNames": ["lyrics-replace-share-button.patch"], + "title": "Replace Share Button", + "description": "Replaces the share button with the Tidal Connect button from the top of the screen. (it's in the menu already)", + "default": true + }, + { + "id": "MiniPlayerRedesign", + "order": 50, + "fileNames": [], + "title": "Redesigned Mini-Player", + "description": "Integrates the Seekbar into the control panel at the bottom of the screen.", + "default": false, + "defaultVariantIndex": 2, + "variants": [ + { "title": "Floating", "fileNames": [] }, + { "title": "Grey", "fileNames": ["mini-player-grey.patch"] }, + { "title": "Black", "fileNames": ["mini-player-black.patch"] } + ], + "advancedOptions": [ + { + "type": "toggle", + "key": "dynamic_bg", + "title": "Mini-Player Dynamic Background", + "description": "Colorizes the Mini-Player depending on the currently playing track.", + "default": false, + "inline": true, + "fileNames": ["mini-player-dynamic-bg.patch"], + "extensionFiles": ["radiant/MiniPlayerBackground.smali"], + "hidesVariants": [2], + "relabelVariants": { "1": "Legacy" } + }, + { + "type": "toggle", + "key": "border", + "title": "Progress Border", + "description": "Draws an animated progress ring around the floating mini-player. Requires the Floating style.", + "default": false, + "requiresVariant": 0, + "fileNames": ["mini-player-floating-border.patch"], + "extensionFiles": ["radiant/MiniSeekerFloating.smali"] + }, + { + "type": "toggle", + "key": "gestures", + "title": "Mini-Player Gestures", + "description": "Swipe up on the mini-player to open the full player.", + "default": true, + "fileNames": ["mini-player-gestures.patch"], + "extensionFiles": [ + "radiant/MiniPlayerGestures.smali", + "radiant/MiniPlayerGestures$Gesture.smali", + "radiant/MiniPlayerGestures$RootGesture.smali", + "radiant/MiniPlayerGestures$ApplyPending.smali" + ] + }, + { + "type": "toggle", + "key": "next_prev", + "title": "Next/Previous Gestures", + "description": "Swipe left or right on the mini-player to skip tracks.", + "default": false, + "requiresOption": "gestures", + "fileNames": ["mini-player-gestures-left-right.patch"], + "extensionFiles": [ + "radiant/MiniPlayerTrackGestures.smali", + "radiant/MiniPlayerTrackGestures$Gesture.smali", + "radiant/MiniPlayerTrackGestures$OffsetLayer.smali", + "radiant/MiniPlayerTrackGestures$ResetAnimator.smali", + "radiant/MiniPlayerTrackGestures$TextDraw.smali", + "com/tidal/android/feature/appscaffold/ui/q$c.smali" + ] + } + ] + }, + { + "id": "LyricsKeepControlsVisible", + "order": 60, + "fileNames": ["lyrics-keep-controls-visible.patch"], + "title": "Keep Controls Visible", + "description": "Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off.", + "default": true + }, + { + "id": "DebugMenuUnlock", + "order": 100, + "fileNames": ["debug-menu-unlock.patch"], + "title": "Unlock Debug Menu", + "description": "Reveals TIDAL's hidden Debug Menu in Settings. (Unlocks Feature Flags & the legacy Debug Options)", + "default": false + } + ] +} diff --git a/patches/mini-player-floating.patch b/patches/mini-player-floating-border.patch similarity index 100% rename from patches/mini-player-floating.patch rename to patches/mini-player-floating-border.patch diff --git a/patches/player-backdrop.patch b/patches/player-backdrop.patch index b04ad0c..7fc65af 100644 --- a/patches/player-backdrop.patch +++ b/patches/player-backdrop.patch @@ -70,7 +70,7 @@ + + move-result-object v5 # filled modifier + -+ const/high16 v7, 0x42b40000 # 90f blur dp ++ const v7, __RL_BLUR_BITS__ # blur dp bits (Advanced Options: Blur Strength) + + invoke-static {v7}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F # to Dp + @@ -126,7 +126,7 @@ + + move-result-object v3 # fullscreen modifier + -+ const v6, -0x80000000 # 50% black ARGB ++ const v6, __RL_SCRIM_ARGB__ # scrim ARGB (Advanced Options: Backdrop Dimming) + + invoke-static {v6}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color long +