Compare commits

..
25 Commits
Author SHA1 Message Date
meoware.exe bafd40a20c Merge pull request #54 from meowarex/dev
Advanced Options + New Patch Options <3
2026-06-25 01:05:04 +10:00
meoware.exe c89827c7b2 Advanced Options + New Patch Options <3 2026-06-25 01:03:28 +10:00
meoware.exe 90b49cc571 Merge pull request #49 from Se7en-dev/feat/mini-player-dynamic-color-patch
[PATCH] mini player dynamic bg color
2026-06-24 18:31:38 +10:00
meoware.exe 8e86c0d2e1 Merge branch 'dev' into feat/mini-player-dynamic-color-patch 2026-06-24 18:20:49 +10:00
meoware.exe 8ac9fb0db5 Merge pull request #50 from Se7en-dev/feat/mini-player-gestures-patch
[PATCH] mini player gestures patch
2026-06-24 18:05:05 +10:00
Se7en-dev 6ad6cb2fe0 fix(patch): update order and patches version 2026-06-24 01:04:45 +02:00
Se7en-dev a9611a1667 feat(patch): mini player dynamic bg color 2026-06-24 01:01:57 +02:00
Se7en-dev 276dd0be2e fix(miniplayer gestures): clip text on right side 2026-06-24 00:30:14 +02:00
Se7en-dev 3758ba19f3 feat(mini player gestures): track swipe feedback + fix stuck offsets 2026-06-23 23:08:02 +02:00
Se7en-dev 75450aad60 fix(mini player gestures patch): support redeisgned mp floating seek bar variant 2026-06-23 14:51:12 +02:00
Se7en-dev 3788eb2008 feat(patch): mini player gestures patch + patch suboptions 2026-06-23 14:18:38 +02:00
meoware.exe 95be099821 Merge pull request #48 from meowarex/dev
Overhaul Patch Defaults <3
2026-06-22 00:17:01 +10:00
meoware.exe 7a55787894 Bump Patch Version.. 2026-06-22 00:15:37 +10:00
meoware.exe bd09c2798e Overhaul Patch Defaults <3 2026-06-22 00:15:04 +10:00
meoware.exe b0b614dc6f Merge pull request #47 from meowarex/dev
New Patch: One-Handed Mode <3
2026-06-21 23:16:23 +10:00
meoware.exe 6a7d5fe90f Support for Animated Covers <3 2026-06-21 17:21:07 +10:00
meoware.exe 4791e69bec New Patch: One-Handed Mode <3 2026-06-21 01:36:05 +10:00
meoware.exe 7f3618aeae Fix Nav Bleed <3 2026-06-21 00:37:24 +10:00
meoware.exe ea9c6458c2 Update "Add Track" Icon <3 2026-06-20 23:53:33 +10:00
meoware.exe bdbcc4fc62 Merge pull request #44 from meowarex/dev
New Patch: Quality Badge Color
2026-06-20 23:06:36 +10:00
meoware.exe 3b893c12ff Renamed Patch File <3 2026-06-20 23:05:28 +10:00
meoware.exe 9d60e3dc80 Merge pull request #43 from Se7en-dev/feat/quality-badge-colors-patch
[PATCH] Quality Badge Colors
2026-06-20 22:55:24 +10:00
Se7en-dev 36382437f1 feat(patches): added quality badge colors patch 2026-06-19 17:29:19 +02:00
meoware.exe 5dc9188772 Merge pull request #41 from meowarex/dev
Mark RL API as WIP
2026-06-16 02:44:23 +10:00
meoware.exe 7823a54d11 Mark RL API as WIP 2026-06-16 02:43:35 +10:00
40 changed files with 5116 additions and 137 deletions
@@ -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.base.Step
import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep 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.PatchOptions
import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs
import com.android.tools.smali.baksmali.Baksmali import com.android.tools.smali.baksmali.Baksmali
import com.android.tools.smali.baksmali.BaksmaliOptions import com.android.tools.smali.baksmali.BaksmaliOptions
import com.android.tools.smali.dexlib2.Opcodes 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.DiffUtils
import com.github.difflib.UnifiedDiffUtils import com.github.difflib.UnifiedDiffUtils
import com.github.difflib.patch.Patch import com.github.difflib.patch.Patch
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import java.io.* import java.io.*
@@ -27,6 +30,7 @@ class SmaliPatchStep(
private val options: PatchOptions, private val options: PatchOptions,
) : Step(), IDexProvider, KoinComponent { ) : Step(), IDexProvider, KoinComponent {
private val paths: PathManager by inject() private val paths: PathManager by inject()
private val json: Json by inject()
override val group = StepGroup.Patch override val group = StepGroup.Patch
override val localizedName = R.string.patch_step_patch_smali override val localizedName = R.string.patch_step_patch_smali
@@ -42,6 +46,29 @@ class SmaliPatchStep(
val patches = mutableListOf<LoadedPatch>() val patches = mutableListOf<LoadedPatch>()
val localsBumps = mutableMapOf<Pair<String, String>, Int>() val localsBumps = mutableMapOf<Pair<String, String>, Int>()
// 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. // Load and parse all the patches from the smali archive.
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}") container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
smaliDir.mkdirs() smaliDir.mkdirs()
@@ -53,6 +80,11 @@ class SmaliPatchStep(
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) { if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
val relative = patchFile.removePrefix("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) val out = smaliDir.resolve(relative)
// Guard against zip-slip: a crafted entry could otherwise escape smaliDir. // Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
val baseCanonical = smaliDir.canonicalPath + File.separator val baseCanonical = smaliDir.canonicalPath + File.separator
@@ -71,16 +103,39 @@ class SmaliPatchStep(
if (!patchFile.endsWith(".patch")) continue if (!patchFile.endsWith(".patch")) continue
val basename = patchFile.substringAfterLast('/') val basename = patchFile.substringAfterLast('/')
if (basename in options.disabledPatches) { if (basename in disabledFiles) {
container.log("Skipping disabled patch $patchFile") container.log("Skipping disabled patch $patchFile")
continue continue
} }
val lines = zip.openEntry(patchFile)!!.read() var patchText = zip.openEntry(patchFile)!!.read()
.decodeToString() .decodeToString()
.replace("\r\n", "\n") // Replace CRLF endings with LF .replace("\r\n", "\n") // Replace CRLF endings with LF
.trimEnd { it == '\n' } // Remove trailing new lines .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 { try {
for (directive in lines) { for (directive in lines) {
@@ -320,6 +375,7 @@ class SmaliPatchStep(
private companion object { private companion object {
val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""") val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""")
val UNRESOLVED_TOKEN = Regex("""__RL_[A-Z0-9_]+__""")
} }
/** /**
@@ -2,6 +2,8 @@ package com.meowarex.rlmobile.ui.previews.screens
import android.content.res.Configuration import android.content.res.Configuration
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.* import androidx.compose.ui.tooling.preview.*
import com.meowarex.rlmobile.network.utils.SemVer import com.meowarex.rlmobile.network.utils.SemVer
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
@@ -18,6 +20,9 @@ private fun PatchOptionsScreenPreview(
@PreviewParameter(PatchOptionsParametersProvider::class) @PreviewParameter(PatchOptionsParametersProvider::class)
parameters: PatchOptionsParameters, parameters: PatchOptionsParameters,
) { ) {
val context = LocalContext.current
val specs = remember { builtinPatchSpecs { context.getString(it) } }
ManagerTheme { ManagerTheme {
PatchOptionsScreenContent( PatchOptionsScreenContent(
isUpdate = parameters.isUpdate, isUpdate = parameters.isUpdate,
@@ -34,12 +39,14 @@ private fun PatchOptionsScreenPreview(
onSelectCustomTidalApk = {}, onSelectCustomTidalApk = {},
customPatches = parameters.customPatches, customPatches = parameters.customPatches,
onSelectCustomPatches = {}, onSelectCustomPatches = {},
enabledPatchCount = KnownPatch.All.size, specs = specs,
enabledPatchCount = specs.size,
isPatchEnabled = { true }, isPatchEnabled = { true },
onTogglePatch = { _, _ -> }, onTogglePatch = { _, _ -> },
patchLockState = { PatchLock.Free }, patchLockState = { PatchLock.Free },
variantIndex = { 0 }, variantIndex = { 0 },
onSelectVariant = { _, _ -> }, onSelectVariant = { _, _ -> },
optionState = PatchOptionState.Preview,
isConfigValid = parameters.isConfigValid, isConfigValid = parameters.isConfigValid,
onInstall = {}, onInstall = {},
) )
@@ -2,37 +2,35 @@ package com.meowarex.rlmobile.ui.screens.patchopts
import androidx.annotation.StringRes import androidx.annotation.StringRes
import com.meowarex.rlmobile.R import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.screens.patchopts.PatchDefault.Disabled
import com.meowarex.rlmobile.ui.screens.patchopts.PatchDefault.Enabled
data class PatchVariant( data class PatchVariant(
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
val fileNames: List<String>, val fileNames: List<String>,
val extensionFiles: List<String> = emptyList(),
) )
enum class KnownPatch( 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]
* Numeric display order in the patch options list. Lower = higher up.
*
* Convention: main patches use multiples of 10 (10, 20, 30, …). Patches
* that act as helpers/dependencies of a main patch get offsets adjacent to
* the requirer (e.g. main at 40, helpers at 41, 42, 43). DebugMenuUnlock
* is pinned to 100 to keep it at the bottom of the list.
*/
val order: Int,
val fileNames: List<String>, val fileNames: List<String>,
val extensionFiles: List<String> = emptyList(),
@StringRes val titleRes: Int, @StringRes val titleRes: Int,
@StringRes val descRes: Int, @StringRes val descRes: Int,
val default: PatchDefault, // Default state of the patch in the UI List (enabled/disabled)
val requires: List<KnownPatch> = emptyList(), val requires: List<KnownPatch> = emptyList(),
val disables: List<KnownPatch> = emptyList(), val disables: List<KnownPatch> = emptyList(),
val variants: List<PatchVariant> = emptyList(), val variants: List<PatchVariant> = emptyList(),
val defaultVariantIndex: Int = 0, val defaultVariantIndex: Int = 0,
val advancedOptions: List<PatchOption> = emptyList(),
) { ) {
// Dependency-first order (later refs need backward resolution).
// The `order` field controls display order; declaration order doesn't matter.
LyricsDisableCover( LyricsDisableCover(
order = 41, order = 41,
fileNames = listOf("lyrics-disable-cover.patch"), fileNames = listOf("lyrics-disable-cover.patch"),
titleRes = R.string.patch_lyrics_disable_cover_title, titleRes = R.string.patch_lyrics_disable_cover_title,
descRes = R.string.patch_lyrics_disable_cover_desc, descRes = R.string.patch_lyrics_disable_cover_desc,
default = Enabled,
), ),
LyricsReplaceLyricsButton( LyricsReplaceLyricsButton(
order = 42, order = 42,
@@ -42,12 +40,14 @@ enum class KnownPatch(
), ),
titleRes = R.string.patch_lyrics_replace_button_title, titleRes = R.string.patch_lyrics_replace_button_title,
descRes = R.string.patch_lyrics_replace_button_desc, descRes = R.string.patch_lyrics_replace_button_desc,
default = Enabled,
), ),
LyricsReplaceShareButton( LyricsReplaceShareButton(
order = 43, order = 43,
fileNames = listOf("lyrics-replace-share-button.patch"), fileNames = listOf("lyrics-replace-share-button.patch"),
titleRes = R.string.patch_lyrics_replace_share_button_title, titleRes = R.string.patch_lyrics_replace_share_button_title,
descRes = R.string.patch_lyrics_replace_share_button_desc, descRes = R.string.patch_lyrics_replace_share_button_desc,
default = Enabled,
), ),
LyricsRlApi( LyricsRlApi(
order = 20, order = 20,
@@ -57,18 +57,59 @@ enum class KnownPatch(
), ),
titleRes = R.string.patch_lyrics_rl_api_title, titleRes = R.string.patch_lyrics_rl_api_title,
descRes = R.string.patch_lyrics_rl_api_desc, descRes = R.string.patch_lyrics_rl_api_desc,
default = Disabled,
), ),
LyricsKeepControlsVisible( LyricsKeepControlsVisible(
order = 60, order = 60,
fileNames = listOf("lyrics-keep-controls-visible.patch"), fileNames = listOf("lyrics-keep-controls-visible.patch"),
titleRes = R.string.patch_lyrics_keep_controls_title, titleRes = R.string.patch_lyrics_keep_controls_title,
descRes = R.string.patch_lyrics_keep_controls_desc, descRes = R.string.patch_lyrics_keep_controls_desc,
default = Enabled,
), ),
PlayerBackdrop( PlayerBackdrop(
order = 30, order = 30,
fileNames = listOf("player-backdrop.patch"), fileNames = listOf("player-backdrop.patch"),
titleRes = R.string.patch_player_backdrop_title, titleRes = R.string.patch_player_backdrop_title,
descRes = R.string.patch_player_backdrop_desc, 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,
fileNames = listOf("player-quality-badge-colors.patch"),
titleRes = R.string.patch_quality_badge_colors_title,
descRes = R.string.patch_quality_badge_colors_desc,
default = Enabled,
),
PlayerOneHanded(
order = 37,
fileNames = listOf("player-one-handed.patch"),
titleRes = R.string.patch_player_one_handed_title,
descRes = R.string.patch_player_one_handed_desc,
default = Disabled,
), ),
CoverEverywhere( CoverEverywhere(
order = 35, order = 35,
@@ -79,12 +120,14 @@ enum class KnownPatch(
), ),
titleRes = R.string.patch_cover_everywhere_title, titleRes = R.string.patch_cover_everywhere_title,
descRes = R.string.patch_cover_everywhere_desc, descRes = R.string.patch_cover_everywhere_desc,
default = Disabled,
), ),
DebugMenuUnlock( DebugMenuUnlock(
order = 100, order = 100,
fileNames = listOf("debug-menu-unlock.patch"), fileNames = listOf("debug-menu-unlock.patch"),
titleRes = R.string.patch_debug_menu_unlock_title, titleRes = R.string.patch_debug_menu_unlock_title,
descRes = R.string.patch_debug_menu_unlock_desc, descRes = R.string.patch_debug_menu_unlock_desc,
default = Disabled,
), ),
LyricsProgressPill( LyricsProgressPill(
order = 40, order = 40,
@@ -94,6 +137,7 @@ enum class KnownPatch(
), ),
titleRes = R.string.patch_lyrics_progress_pill_title, titleRes = R.string.patch_lyrics_progress_pill_title,
descRes = R.string.patch_lyrics_progress_pill_desc, descRes = R.string.patch_lyrics_progress_pill_desc,
default = Enabled,
requires = listOf(LyricsDisableCover, LyricsReplaceLyricsButton, LyricsReplaceShareButton), requires = listOf(LyricsDisableCover, LyricsReplaceLyricsButton, LyricsReplaceShareButton),
), ),
MiniPlayerRedesign( MiniPlayerRedesign(
@@ -101,27 +145,83 @@ enum class KnownPatch(
fileNames = emptyList(), fileNames = emptyList(),
titleRes = R.string.patch_mini_player_redesign_title, titleRes = R.string.patch_mini_player_redesign_title,
descRes = R.string.patch_mini_player_redesign_desc, descRes = R.string.patch_mini_player_redesign_desc,
default = Disabled,
defaultVariantIndex = 2, defaultVariantIndex = 2,
variants = listOf( 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, 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, titleRes = R.string.patch_mini_player_variant_square_grey_title,
fileNames = listOf("mini-player-grey.patch"), 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, titleRes = R.string.patch_mini_player_variant_square_black_title,
fileNames = listOf("mini-player-black.patch"), fileNames = listOf("mini-player-black.patch"),
), ),
), ),
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"),
extensionFiles = listOf(
"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",
),
),
),
), ),
EnableLegacyUi( EnableLegacyUi(
order = 10, order = 10,
fileNames = listOf("enable-legacy-ui.patch"), fileNames = listOf("enable-legacy-ui.patch"),
titleRes = R.string.patch_enable_legacy_ui_title, titleRes = R.string.patch_enable_legacy_ui_title,
descRes = R.string.patch_enable_legacy_ui_desc, descRes = R.string.patch_enable_legacy_ui_desc,
default = Disabled,
requires = listOf(DebugMenuUnlock), requires = listOf(DebugMenuUnlock),
disables = listOf( disables = listOf(
LyricsDisableCover, LyricsDisableCover,
@@ -130,19 +230,14 @@ enum class KnownPatch(
LyricsRlApi, LyricsRlApi,
LyricsKeepControlsVisible, LyricsKeepControlsVisible,
PlayerBackdrop, PlayerBackdrop,
QualityBadgeColors,
LyricsProgressPill, LyricsProgressPill,
CoverEverywhere, CoverEverywhere,
MiniPlayerRedesign,
), ),
); );
val allVariantFileNames: Set<String>
get() = variants.flatMapTo(mutableSetOf()) { it.fileNames }
companion object { companion object {
/**
* Sorted by `order` ascending. Tie-breaks fall back to the first filename
* (alphabetical) so the order is always deterministic.
*/
val All: List<KnownPatch> = entries.sortedWith( val All: List<KnownPatch> = entries.sortedWith(
compareBy({ it.order }, { it.fileNames.firstOrNull() ?: it.name }) compareBy({ it.order }, { it.fileNames.firstOrNull() ?: it.name })
) )
@@ -0,0 +1,6 @@
package com.meowarex.rlmobile.ui.screens.patchopts
enum class PatchDefault(val isEnabled: Boolean) {
Enabled(true),
Disabled(false),
}
@@ -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<String> = emptyList(),
/** Helper smali extracted only while this toggle is on. */
val extensionFiles: List<String> = 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<Int> = emptyList(),
/** Variant title overrides (index -> @StringRes) applied while this toggle is on. */
val relabelVariants: Map<Int, Int> = 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<Float>,
/** 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<ChoiceEntry>,
val defaultIndex: Int = 0,
) : PatchOption
}
data class ChoiceEntry(
@StringRes val labelRes: Int,
)
@@ -32,27 +32,98 @@ data class PatchOptions(
*/ */
val customPatches: PatchComponent? = null, val customPatches: PatchComponent? = null,
val disabledPatches: Set<String> = emptySet(), val patchStates: Map<String, Boolean> = emptyMap(),
val selectedVariants: Map<String, Int> = emptyMap(), val selectedVariants: Map<String, Int> = emptyMap(),
val optionFloats: Map<String, Float> = emptyMap(),
val optionBools: Map<String, Boolean> = emptyMap(),
val optionInts: Map<String, Int> = emptyMap(),
) : Parcelable { ) : Parcelable {
companion object {
val Default: PatchOptions = run {
val miniPlayerFiles = KnownPatch.MiniPlayerRedesign.allVariantFileNames
val disabled = (
KnownPatch.DebugMenuUnlock.fileNames +
KnownPatch.EnableLegacyUi.fileNames +
miniPlayerFiles
).toSet()
PatchOptions( fun isEnabled(spec: PatchSpec): Boolean =
appName = "TIDAL", patchStates[spec.id] ?: spec.defaultEnabled
packageName = "com.aspiro.tidal",
debuggable = false, fun variantIndex(spec: PatchSpec): Int {
customTidalApk = null, val stored = (selectedVariants[spec.id] ?: spec.defaultVariantIndex)
customPatches = null, .coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
disabledPatches = disabled, return spec.resolveVariantIndex(stored) { isToggleOn(spec, it) }
) }
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<OptionSpec.Toggle>()
.firstOrNull { it.key == key }
if (required != null && !isToggleOn(spec, required)) return false
}
return true
}
fun disabledPatchFiles(specs: List<PatchSpec>): Set<String> = buildSet {
for (spec in specs) {
val enabled = isEnabled(spec)
if (spec.variants.isEmpty()) {
if (!enabled) addAll(spec.fileNames)
} else {
val selected = variantIndex(spec)
spec.variants.forEachIndexed { index, variant ->
if (!enabled || index != selected) addAll(variant.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(specs: List<PatchSpec>): Set<String> = 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(specs: List<PatchSpec>): Set<String> = 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)
}
}
}
fun smaliSubstitutions(specs: List<PatchSpec>): Map<String, String> = 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)))
}
}
}
companion object {
val Default: PatchOptions = PatchOptions(
appName = "TIDAL",
packageName = "com.aspiro.tidal",
debuggable = false,
)
}
} }
@@ -2,21 +2,35 @@ package com.meowarex.rlmobile.ui.screens.patchopts
import android.content.Context import android.content.Context
import android.content.pm.PackageManager.NameNotFoundException import android.content.pm.PackageManager.NameNotFoundException
import android.util.Log
import androidx.compose.runtime.* import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator 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.manager.PreferencesManager
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsScreen import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsScreen
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.util.pushForResult import com.meowarex.rlmobile.ui.util.pushForResult
import com.meowarex.rlmobile.util.* 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.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File
class PatchOptionsModel( class PatchOptionsModel(
prefilledOptions: PatchOptions, prefilledOptions: PatchOptions,
private val context: Context, private val context: Context,
private val prefs: PreferencesManager, private val prefs: PreferencesManager,
private val paths: PathManager,
private val json: Json,
private val github: RadiantLyricsGithubService,
private val downloader: KtorDownloadManager,
) : ScreenModel { ) : ScreenModel {
var packageName by mutableStateOf(prefilledOptions.packageName) var packageName by mutableStateOf(prefilledOptions.packageName)
private set private set
@@ -47,92 +61,153 @@ class PatchOptionsModel(
debuggable = value debuggable = value
} }
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches) // 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<PatchSpec> = 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 private set
var selectedVariants by mutableStateOf(prefilledOptions.selectedVariants) var selectedVariants by mutableStateOf(prefilledOptions.selectedVariants)
private set private set
fun variantIndex(patch: KnownPatch): Int = selectedVariants[patch.name] fun variantIndex(spec: PatchSpec): Int =
?.coerceIn(0, patch.variants.lastIndex.coerceAtLeast(0)) (selectedVariants[spec.id] ?: spec.defaultVariantIndex)
?: patch.defaultVariantIndex.coerceIn(0, patch.variants.lastIndex.coerceAtLeast(0)) .coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
fun isPatchEnabled(patch: KnownPatch): Boolean = if (patch.variants.isNotEmpty()) { fun isPatchEnabled(spec: PatchSpec): Boolean =
val v = patch.variants[variantIndex(patch)] patchStates[spec.id] ?: spec.defaultEnabled
v.fileNames.isNotEmpty() && v.fileNames.none { it in disabledPatches }
} else {
patch.fileNames.isNotEmpty() && patch.fileNames.none { it in disabledPatches }
}
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) { fun setPatchEnabled(spec: PatchSpec, enabled: Boolean) {
if (patch.variants.isNotEmpty()) { val byId = specs.associateBy { it.id }
val all = patch.allVariantFileNames fun closure(seed: PatchSpec, step: (PatchSpec) -> List<PatchSpec>): Set<PatchSpec> =
val selected = patch.variants[variantIndex(patch)].fileNames.toSet()
disabledPatches = if (enabled) {
(disabledPatches + all) - selected
} else {
disabledPatches + all
}
return
}
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
buildSet { 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) walk(seed)
} }
val enableUnits: Set<KnownPatch> val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) }
val disableUnits: Set<KnownPatch> val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } }
val enableUnits: Set<PatchSpec>
val disableUnits: Set<PatchSpec>
if (enabled) { if (enabled) {
enableUnits = closure(patch) { it.requires } enableUnits = closure(spec, requiresOf)
disableUnits = enableUnits.flatMap { it.disables } disableUnits = enableUnits.flatMap { it.disables }
.flatMapTo(mutableSetOf()) { d -> .mapNotNull(byId::get)
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } } .flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) }
}
} else { } else {
enableUnits = emptySet() enableUnits = emptySet()
disableUnits = closure(patch) { p -> KnownPatch.All.filter { p in it.requires } } disableUnits = closure(spec, dependentsOf)
} }
val enableFiles = enableUnits.flatMap { it.fileNames }.toSet() patchStates = patchStates.toMutableMap().apply {
val disableFiles = disableUnits.flatMap { it.fileNames }.toSet() enableUnits.forEach { this[it.id] = true }
disabledPatches = (disabledPatches - enableFiles) + disableFiles disableUnits.forEach { this[it.id] = false }
}
} }
fun selectVariant(patch: KnownPatch, index: Int) { fun selectVariant(spec: PatchSpec, index: Int) {
if (patch.variants.isEmpty() || index !in patch.variants.indices) return if (spec.variants.isEmpty() || index !in spec.variants.indices) return
val wasOn = isPatchEnabled(patch) selectedVariants = selectedVariants + (spec.id to index)
selectedVariants = selectedVariants + (patch.name to index)
if (wasOn) setPatchEnabled(patch, true)
} }
fun lockState(patch: KnownPatch): PatchLock { // Advanced (per-patch) options
if (patch.variants.isNotEmpty()) return PatchLock.Free // 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<KnownPatch>): Set<KnownPatch> = 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<PatchSpec>): Set<PatchSpec> =
buildSet { 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) walk(seed)
} }
for (other in KnownPatch.All) { val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) }
if (other == patch || !isPatchEnabled(other)) continue val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } }
val requiresClosure = closure(other) { it.requires } for (other in specs) {
if (patch in requiresClosure - other) return PatchLock.LockedOn(other) 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 } val disablesClosure = requiresClosure.flatMap { it.disables }
.flatMapTo(mutableSetOf()) { d -> .mapNotNull(byId::get)
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } } .flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) }
} if (spec in disablesClosure) return PatchLock.LockedOff(other)
if (patch in disablesClosure) return PatchLock.LockedOff(other)
} }
return PatchLock.Free return PatchLock.Free
} }
val enabledPatchCount: Int val enabledPatchCount: Int
get() = KnownPatch.All.count { isPatchEnabled(it) } get() = specs.count { isPatchEnabled(it) }
var customTidalApk by mutableStateOf<PatchComponent?>(null) var customTidalApk by mutableStateOf<PatchComponent?>(null)
private set private set
@@ -155,6 +230,79 @@ class PatchOptionsModel(
componentType = PatchComponent.Type.Patches, 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<PatchSpec>? =
loadManifestSpecs(component.getFile(paths))
private fun loadManifestSpecs(file: File): List<PatchSpec>? {
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<PatchSpec>? = 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 { val isConfigValid by derivedStateOf {
@@ -175,8 +323,11 @@ class PatchOptionsModel(
debuggable = debuggable, debuggable = debuggable,
customTidalApk = customTidalApk, customTidalApk = customTidalApk,
customPatches = customPatches, customPatches = customPatches,
disabledPatches = disabledPatches, patchStates = patchStates,
selectedVariants = selectedVariants, selectedVariants = selectedVariants,
optionFloats = optionFloats,
optionBools = optionBools,
optionInts = optionInts,
) )
} }
@@ -202,9 +353,9 @@ class PatchOptionsModel(
} }
private fun validatePatchSelection() { private fun validatePatchSelection() {
for (patch in KnownPatch.All) { for (spec in specs) {
if (isPatchEnabled(patch)) { if (isPatchEnabled(spec)) {
setPatchEnabled(patch, true) setPatchEnabled(spec, true)
} }
} }
} }
@@ -212,9 +363,13 @@ class PatchOptionsModel(
init { init {
validatePatchSelection() validatePatchSelection()
screenModelScope.launchBlock { fetchPkgNameState() } screenModelScope.launchBlock { fetchPkgNameState() }
// Default source is the latest release's manifest; custom selections drive themselves.
if (customPatches == null) loadDefaultSpecs()
} }
companion object { companion object {
private const val MANIFEST_NAME = "manifest.json"
private val PACKAGE_REGEX = """^[a-z]\w*(\.[a-z]\w*)+$""" private val PACKAGE_REGEX = """^[a-z]\w*(\.[a-z]\w*)+$"""
.toRegex(RegexOption.IGNORE_CASE) .toRegex(RegexOption.IGNORE_CASE)
} }
@@ -227,7 +382,7 @@ enum class PackageNameState {
} }
sealed class PatchLock { sealed class PatchLock {
object Free : PatchLock() data object Free : PatchLock()
data class LockedOn(val by: KnownPatch) : PatchLock() data class LockedOn(val by: PatchSpec) : PatchLock()
data class LockedOff(val by: KnownPatch) : PatchLock() data class LockedOff(val by: PatchSpec) : PatchLock()
} }
@@ -60,12 +60,14 @@ class PatchOptionsScreen(
onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) }, onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) },
onSelectCustomPatches = { model.selectCustomPatches(navigator) }, onSelectCustomPatches = { model.selectCustomPatches(navigator) },
specs = model.specs,
enabledPatchCount = model.enabledPatchCount, enabledPatchCount = model.enabledPatchCount,
isPatchEnabled = model::isPatchEnabled, isPatchEnabled = model::isPatchEnabled,
onTogglePatch = model::setPatchEnabled, onTogglePatch = model::setPatchEnabled,
patchLockState = model::lockState, patchLockState = model::lockState,
variantIndex = model::variantIndex, variantIndex = model::variantIndex,
onSelectVariant = model::selectVariant, onSelectVariant = model::selectVariant,
optionState = model.optionState,
isConfigValid = model.isConfigValid, isConfigValid = model.isConfigValid,
onInstall = { onInstall = {
@@ -96,12 +98,14 @@ fun PatchOptionsScreenContent(
customPatches: PatchComponent?, customPatches: PatchComponent?,
onSelectCustomPatches: () -> Unit, onSelectCustomPatches: () -> Unit,
specs: List<PatchSpec>,
enabledPatchCount: Int, enabledPatchCount: Int,
isPatchEnabled: (KnownPatch) -> Boolean, isPatchEnabled: (PatchSpec) -> Boolean,
onTogglePatch: (KnownPatch, Boolean) -> Unit, onTogglePatch: (PatchSpec, Boolean) -> Unit,
patchLockState: (KnownPatch) -> PatchLock, patchLockState: (PatchSpec) -> PatchLock,
variantIndex: (KnownPatch) -> Int, variantIndex: (PatchSpec) -> Int,
onSelectVariant: (KnownPatch, Int) -> Unit, onSelectVariant: (PatchSpec, Int) -> Unit,
optionState: PatchOptionState,
isConfigValid: Boolean, isConfigValid: Boolean,
onInstall: () -> Unit, onInstall: () -> Unit,
@@ -164,13 +168,15 @@ fun PatchOptionsScreenContent(
} }
PatchSelectionAccordion( PatchSelectionAccordion(
specs = specs,
enabledCount = enabledPatchCount, enabledCount = enabledPatchCount,
totalCount = KnownPatch.All.size, totalCount = specs.size,
isEnabled = isPatchEnabled, isEnabled = isPatchEnabled,
onToggle = onTogglePatch, onToggle = onTogglePatch,
lockState = patchLockState, lockState = patchLockState,
variantIndex = variantIndex, variantIndex = variantIndex,
onSelectVariant = onSelectVariant, onSelectVariant = onSelectVariant,
optionState = optionState,
modifier = Modifier.padding(top = 4.dp), modifier = Modifier.padding(top = 4.dp),
) )
@@ -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<PatchSpec> = emptyList(),
)
@Immutable
@Serializable
data class PatchSpec(
val id: String,
val order: Int = 0,
val fileNames: List<String> = emptyList(),
val extensionFiles: List<String> = 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<String> = emptyList(),
/** ids of patches that get force-disabled when this one is enabled. */
val disables: List<String> = emptyList(),
val variants: List<VariantSpec> = emptyList(),
val defaultVariantIndex: Int = 0,
val advancedOptions: List<OptionSpec> = emptyList(),
)
@Immutable
@Serializable
data class VariantSpec(
val title: String,
val fileNames: List<String> = emptyList(),
val extensionFiles: List<String> = 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<String> = emptyList(),
/** Helper smali extracted only while this toggle is on. */
val extensionFiles: List<String> = 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<Int> = emptyList(),
/** Variant title overrides (index -> title) applied while this toggle is on. */
val relabelVariants: Map<Int, String> = 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<Float> 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<String> = 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, <bits>; Dp.constructor-impl(F)F
EncodeKind.FloatBits -> (value * scale).toRawBits().toString()
// ARGB int with black RGB and a percentage-derived alpha: const vX, <argb>
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<PatchSpec> =
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<EffectiveVariant> {
if (variants.isEmpty()) return emptyList()
val hidden = mutableSetOf<Int>()
val relabel = mutableMapOf<Int, String>()
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<OptionSpec.Toggle>().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 = {},
)
}
}
@@ -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<OptionLock?>(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<Float>,
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<Float>,
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<String>,
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))
}
}
}
}
}
}
@@ -7,6 +7,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
@@ -22,20 +23,26 @@ import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R 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.PatchLock
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 @Composable
fun PatchSelectionAccordion( fun PatchSelectionAccordion(
specs: List<PatchSpec>,
enabledCount: Int, enabledCount: Int,
totalCount: Int, totalCount: Int,
isEnabled: (KnownPatch) -> Boolean, isEnabled: (PatchSpec) -> Boolean,
onToggle: (KnownPatch, Boolean) -> Unit, onToggle: (PatchSpec, Boolean) -> Unit,
lockState: (KnownPatch) -> PatchLock, lockState: (PatchSpec) -> PatchLock,
variantIndex: (KnownPatch) -> Int, variantIndex: (PatchSpec) -> Int,
onSelectVariant: (KnownPatch, Int) -> Unit, onSelectVariant: (PatchSpec, Int) -> Unit,
optionState: PatchOptionState,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
var expanded by rememberSaveable { mutableStateOf(false) } var expanded by rememberSaveable { mutableStateOf(false) }
@@ -45,6 +52,7 @@ fun PatchSelectionAccordion(
) )
var lockInfo by remember { mutableStateOf<LockInfo?>(null) } var lockInfo by remember { mutableStateOf<LockInfo?>(null) }
var advancedFor by remember { mutableStateOf<PatchSpec?>(null) }
Column( Column(
modifier = modifier modifier = modifier
@@ -104,12 +112,12 @@ fun PatchSelectionAccordion(
.padding(bottom = 8.dp), .padding(bottom = 8.dp),
) )
for (patch in KnownPatch.All) key(patch) { for (patch in specs) key(patch.id) {
val checked = isEnabled(patch) val checked = isEnabled(patch)
val lock = lockState(patch) val lock = lockState(patch)
PatchSwitchRow( PatchSwitchRow(
title = stringResource(patch.titleRes), title = patch.title,
description = stringResource(patch.descRes), description = patch.description,
checked = checked, checked = checked,
lock = lock, lock = lock,
onCheckedChange = { onToggle(patch, it) }, onCheckedChange = { onToggle(patch, it) },
@@ -123,10 +131,58 @@ fun PatchSelectionAccordion(
.fillMaxWidth() .fillMaxWidth()
.padding(start = 4.dp, end = 4.dp, top = 4.dp, bottom = 4.dp), .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( PatchVariantSelector(
variants = patch.variants, variants = effective,
selectedIndex = variantIndex(patch), selectedIndex = effective
onSelect = { idx -> onSelectVariant(patch, idx) }, .indexOfFirst { it.originalIndex == resolved }
.coerceAtLeast(0),
onSelect = { pos ->
effective.getOrNull(pos)?.let { onSelectVariant(patch, it.originalIndex) }
},
)
}
}
}
val inlineToggles = patch.advancedOptions
.filterIsInstance<OptionSpec.Toggle>()
.filter { it.inline }
if (inlineToggles.isNotEmpty()) {
AnimatedVisibility(visible = checked) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
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 },
) )
} }
} }
@@ -143,6 +199,84 @@ fun PatchSelectionAccordion(
onDismiss = { lockInfo = null }, 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 @Composable
@@ -212,7 +346,7 @@ private fun PatchSwitchRow(
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
private fun PatchLockDialog( private fun PatchLockDialog(
thisPatch: KnownPatch, thisPatch: PatchSpec,
lock: PatchLock, lock: PatchLock,
onDismiss: () -> Unit, onDismiss: () -> Unit,
) { ) {
@@ -220,12 +354,12 @@ private fun PatchLockDialog(
is PatchLock.LockedOn -> Triple( is PatchLock.LockedOn -> Triple(
R.string.patch_lock_required_title, R.string.patch_lock_required_title,
R.string.patch_lock_required_msg, R.string.patch_lock_required_msg,
stringResource(lock.by.titleRes), lock.by.title,
) )
is PatchLock.LockedOff -> Triple( is PatchLock.LockedOff -> Triple(
R.string.patch_lock_blocked_title, R.string.patch_lock_blocked_title,
R.string.patch_lock_blocked_msg, R.string.patch_lock_blocked_msg,
stringResource(lock.by.titleRes), lock.by.title,
) )
PatchLock.Free -> return PatchLock.Free -> return
} }
@@ -4,15 +4,14 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun PatchVariantSelector( fun PatchVariantSelector(
variants: List<PatchVariant>, variants: List<EffectiveVariant>,
selectedIndex: Int, selectedIndex: Int,
onSelect: (Int) -> Unit, onSelect: (Int) -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -31,7 +30,7 @@ fun PatchVariantSelector(
icon = {}, icon = {},
label = { label = {
Text( Text(
text = stringResource(variant.titleRes), text = variant.title,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#e8eaed"
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8H8.9V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#e8eaed"
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM18,20H6V10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#e8eaed"
android:pathData="M3,17v2h6v-2H3zM3,5v2h10V5H3zM13,21v-2h8v-2h-8v-2h-2v6h2zM7,9v2H3v2h4v2h2V9H7zM21,13v-2H11v2h10zM15,7h2V5h4V3h-4V1h-2v6z" />
</vector>
+27 -1
View File
@@ -22,6 +22,7 @@
<string name="action_retry_install">Retry installation</string> <string name="action_retry_install">Retry installation</string>
<string name="action_open_error_log">Open error log</string> <string name="action_open_error_log">Open error log</string>
<string name="action_apply">Apply</string> <string name="action_apply">Apply</string>
<string name="action_done">Done</string>
<string name="action_confirm">Confirm</string> <string name="action_confirm">Confirm</string>
<string name="action_dismiss">Dismiss</string> <string name="action_dismiss">Dismiss</string>
<string name="action_install">Install</string> <string name="action_install">Install</string>
@@ -251,6 +252,9 @@
<string name="patchopts_patches_title">Patches</string> <string name="patchopts_patches_title">Patches</string>
<string name="patchopts_patches_desc">Toggle which patches are applied during installation. Unchecked patches are skipped entirely.</string> <string name="patchopts_patches_desc">Toggle which patches are applied during installation. Unchecked patches are skipped entirely.</string>
<string name="patchopts_patches_summary">%1$d of %2$d enabled</string> <string name="patchopts_patches_summary">%1$d of %2$d enabled</string>
<string name="patchopts_advanced_button">Advanced Options</string>
<string name="patchopts_advanced_sheet_label">Advanced Options</string>
<string name="patchopts_toggle_snap">Toggle snapping to steps</string>
<string name="patch_lyrics_disable_cover_title">Disable Lyrics Cover</string> <string name="patch_lyrics_disable_cover_title">Disable Lyrics Cover</string>
<string name="patch_lyrics_disable_cover_desc">Removes the mini track cover from the lyrics screen.</string> <string name="patch_lyrics_disable_cover_desc">Removes the mini track cover from the lyrics screen.</string>
@@ -258,7 +262,7 @@
<string name="patch_lyrics_progress_pill_desc">Restores the old Track Progress Pill in the top of the Lyrics screen!</string> <string name="patch_lyrics_progress_pill_desc">Restores the old Track Progress Pill in the top of the Lyrics screen!</string>
<string name="patch_lyrics_replace_button_title">Replace Lyrics Button</string> <string name="patch_lyrics_replace_button_title">Replace Lyrics Button</string>
<string name="patch_lyrics_replace_button_desc">Replaces the Lyrics button with the RL Sparkle!</string> <string name="patch_lyrics_replace_button_desc">Replaces the Lyrics button with the RL Sparkle!</string>
<string name="patch_lyrics_rl_api_title">Radiant Lyrics API</string> <string name="patch_lyrics_rl_api_title">Radiant Lyrics API - WIP</string>
<string name="patch_lyrics_rl_api_desc">Use Radiant Lyrics API to fetch Lyrics (Higher quality &amp; More providers)</string> <string name="patch_lyrics_rl_api_desc">Use Radiant Lyrics API to fetch Lyrics (Higher quality &amp; More providers)</string>
<string name="patch_sticky_lyrics_title">Sticky Lyrics</string> <string name="patch_sticky_lyrics_title">Sticky Lyrics</string>
<string name="patch_sticky_lyrics_desc">Always Forces the Lyrics page to be opened (aslong as the track has lyrics)</string> <string name="patch_sticky_lyrics_desc">Always Forces the Lyrics page to be opened (aslong as the track has lyrics)</string>
@@ -268,6 +272,10 @@
>Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off.</string> >Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off.</string>
<string name="patch_player_backdrop_title">Player Backdrop</string> <string name="patch_player_backdrop_title">Player Backdrop</string>
<string name="patch_player_backdrop_desc">Restores the legacy translucent backdrop blur behind the player.</string> <string name="patch_player_backdrop_desc">Restores the legacy translucent backdrop blur behind the player.</string>
<string name="patch_quality_badge_colors_title">Quality Badge Colors</string>
<string name="patch_quality_badge_colors_desc">Brings color back to the player quality badge.</string>
<string name="patch_player_one_handed_title">One-Handed Mode</string>
<string name="patch_player_one_handed_desc">Mirrors the bottom player buttons left-to-right for easier one-handed reach.</string>
<string name="patch_cover_everywhere_title">Cover Everywhere - WIP</string> <string name="patch_cover_everywhere_title">Cover Everywhere - WIP</string>
<string name="patch_cover_everywhere_desc">Applies the blurred backdrop to every Compose based page (Home &amp; Collection)</string> <string name="patch_cover_everywhere_desc">Applies the blurred backdrop to every Compose based page (Home &amp; Collection)</string>
<string name="patch_lyrics_replace_share_button_title">Replace Share Button</string> <string name="patch_lyrics_replace_share_button_title">Replace Share Button</string>
@@ -285,14 +293,32 @@
<string name="patch_mini_player_redesign_title">Redesigned Mini-Player</string> <string name="patch_mini_player_redesign_title">Redesigned Mini-Player</string>
<string name="patch_mini_player_redesign_desc">Integrates the Seekbar into the control panel at the bottom of the screen.</string> <string name="patch_mini_player_redesign_desc">Integrates the Seekbar into the control panel at the bottom of the screen.</string>
<string name="patch_mini_player_gestures_title">Mini-Player Gestures</string>
<string name="patch_mini_player_gestures_desc">Swipe up on the mini-player to open the full player.</string>
<string name="patch_mini_player_left_right_gestures_title">Next/Previous Gestures</string>
<string name="patch_mini_player_left_right_gestures_desc">Swipe left or right on the mini-player to skip tracks.</string>
<string name="patch_mini_player_dynamic_bg_title">Mini-Player Dynamic Background</string>
<string name="patch_mini_player_dynamic_bg_desc">Colorizes the Mini-Player depending on the currently playing track.</string>
<string name="patch_mini_player_variant_floating_title">Floating</string> <string name="patch_mini_player_variant_floating_title">Floating</string>
<string name="patch_mini_player_variant_square_grey_title">Grey</string> <string name="patch_mini_player_variant_square_grey_title">Grey</string>
<string name="patch_mini_player_variant_square_black_title">Black</string> <string name="patch_mini_player_variant_square_black_title">Black</string>
<string name="patch_mini_player_variant_legacy_title">Legacy</string>
<string name="patch_mini_player_border_title">Progress Border</string>
<string name="patch_mini_player_border_desc">Draws an animated progress ring around the floating mini-player. Requires the Floating style.</string>
<!-- Advanced option labels -->
<string name="patch_opt_backdrop_blur_title">Blur Strength</string>
<string name="patch_opt_backdrop_blur_desc">How strongly the album art behind the player is blurred.</string>
<string name="patch_opt_backdrop_dimming_title">Backdrop Dimming</string>
<string name="patch_opt_backdrop_dimming_desc">Adjusts the level of darkening on the backdrop to keep foreground controls legible.</string>
<string name="patch_lock_required_title">Patch Required!</string> <string name="patch_lock_required_title">Patch Required!</string>
<string name="patch_lock_blocked_title">Patch Blocked!</string> <string name="patch_lock_blocked_title">Patch Blocked!</string>
<string name="patch_lock_required_msg">Patch Required by &lt;b>%s&lt;/b>.</string> <string name="patch_lock_required_msg">Patch Required by &lt;b>%s&lt;/b>.</string>
<string name="patch_lock_blocked_msg">Patch Blocked by &lt;b>%s&lt;/b>.</string> <string name="patch_lock_blocked_msg">Patch Blocked by &lt;b>%s&lt;/b>.</string>
<string name="patch_opt_lock_title">Option Locked</string>
<string name="patch_opt_lock_needs_variant">Requires the &lt;b>%s&lt;/b> style.</string>
<string name="patch_opt_lock_needs_option">Requires the &lt;b>%s&lt;/b> option.</string>
<string name="action_got_it">Got it</string> <string name="action_got_it">Got it</string>
<string name="componentopts_screen_title">Custom Component (%s)</string> <string name="componentopts_screen_title">Custom Component (%s)</string>
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"tidalVersionCode": 9090, "tidalVersionCode": 9090,
"tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk", "tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk",
"patchesVersion": "0.9.7" "patchesVersion": "0.9.12"
} }
@@ -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 <clinit>()V
.locals 1
new-instance v0, Lcom/tidal/android/feature/appscaffold/ui/q$c;
invoke-direct {v0}, Lcom/tidal/android/feature/appscaffold/ui/q;-><init>()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
@@ -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 <clinit>()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 <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
@@ -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 <init>(Landroidx/compose/material3/SheetState;I)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
@@ -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 <init>(Lyl0/l;Landroidx/compose/material3/SheetState;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
@@ -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 <init>(Lyl0/l;Landroidx/compose/material3/SheetState;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
.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;
# direct methods
.method public constructor <init>(Lyl0/l;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/MiniPlayerTrackGestures$Gesture;->a:Lyl0/l;
return-void
.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 3
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-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v0
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
move-result v1
invoke-static {v0, v1}, Lradiant/MiniPlayerTrackGestures;->moveDrag(FF)V
goto :done
# ACTION_UP
:up
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v0
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-static {v0, v1}, Lradiant/MiniPlayerTrackGestures;->finishDrag(FF)I
move-result v0
if-eqz v0, :done
# 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;->nextTrack()V
goto :done
:previous
invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V
invoke-direct {p0}, Lradiant/MiniPlayerTrackGestures$Gesture;->previousTrack()V
goto :done
# ACTION_CANCEL
:cancel
invoke-static {}, Lradiant/MiniPlayerTrackGestures;->cancelDrag()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/MiniPlayerTrackGestures$Gesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
@@ -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 <clinit>()V
.locals 1
new-instance v0, Lradiant/MiniPlayerTrackGestures$OffsetLayer;
invoke-direct {v0}, Lradiant/MiniPlayerTrackGestures$OffsetLayer;-><init>()V
sput-object v0, Lradiant/MiniPlayerTrackGestures$OffsetLayer;->INSTANCE:Lradiant/MiniPlayerTrackGestures$OffsetLayer;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
@@ -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 <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
@@ -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 <clinit>()V
.locals 1
new-instance v0, Lradiant/MiniPlayerTrackGestures$TextDraw;
invoke-direct {v0}, Lradiant/MiniPlayerTrackGestures$TextDraw;-><init>()V
sput-object v0, Lradiant/MiniPlayerTrackGestures$TextDraw;->INSTANCE:Lradiant/MiniPlayerTrackGestures$TextDraw;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()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
@@ -0,0 +1,564 @@
.class public final Lradiant/MiniPlayerTrackGestures;
.super Ljava/lang/Object;
# 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 <clinit>()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 <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
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;-><init>(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;-><init>()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 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
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
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 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
new-instance v0, Lradiant/MiniPlayerTrackGestures$Gesture;
invoke-direct {v0, p1}, Lradiant/MiniPlayerTrackGestures$Gesture;-><init>(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
@@ -0,0 +1,124 @@
.class public final Lradiant/QualityBadge;
.super Ljava/lang/Object;
# direct methods
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# qualities : 0 = LOW / HIGH, 1 = LOSSLESS, 2 = HI_RES_LOSSLESS
.method private static backgroundArgb(I)I
.locals 0
packed-switch p0, :quality_switch
const p0, 0x66475569 # LOW / HIGH bg
return p0
:lossless
const p0, 0x3306b6d4 # LOSSLESS bg
return p0
:hires
const p0, 0x40f59e0b # HI_RES_LOSSLESS bg
return p0
:quality_switch
.packed-switch 0x1
:lossless
:hires
.end packed-switch
.end method
.method private static textArgb(I)I
.locals 0
packed-switch p0, :quality_switch
const p0, -0x1d1710 # LOW / HIGH text
return p0
:lossless
const p0, -0x5a0c04 # LOSSLESS text
return p0
:hires
const p0, -0x21976 # HI_RES_LOSSLESS text
return p0
:quality_switch
.packed-switch 0x1
:lossless
:hires
.end packed-switch
.end method
.method public static modifier(Landroidx/compose/ui/Modifier;I)Landroidx/compose/ui/Modifier;
.locals 3
invoke-static {p1}, Lradiant/QualityBadge;->backgroundArgb(I)I
move-result p1
invoke-static {p1}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J
move-result-wide v0
const/high16 p1, 0x40800000 # 4.0f
invoke-static {p1}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result p1
invoke-static {p1}, Landroidx/compose/foundation/shape/RoundedCornerShapeKt;->RoundedCornerShape-0680j_4(F)Landroidx/compose/foundation/shape/RoundedCornerShape;
move-result-object v2
invoke-static {p0, v0, v1, v2}, Landroidx/compose/foundation/BackgroundKt;->background-bw27NRU(Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier;
move-result-object p0
const/high16 p1, 0x40c00000 # 6.0f
invoke-static {p1}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result p1
const/high16 v0, 0x40000000 # 2.0f
invoke-static {v0}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result v0
invoke-static {p0, p1, v0}, Landroidx/compose/foundation/layout/PaddingKt;->padding-VpY3zN4(Landroidx/compose/ui/Modifier;FF)Landroidx/compose/ui/Modifier;
move-result-object p0
return-object p0
.end method
.method public static style(ILn20/e2;)Ln20/e2;
.registers 16
invoke-static {p0}, Lradiant/QualityBadge;->textArgb(I)I
move-result v0
new-instance v1, Lc20/a;
invoke-direct {v1, v0}, Lc20/a;-><init>(I)V
new-instance v0, Lc20/d;
const/4 v2, 0x0
const/4 v3, 0x0
const/4 v4, 0x0
const/4 v5, 0x0
const/4 v6, 0x0
const/4 v7, 0x0
const/4 v8, 0x0
const/4 v9, 0x0
const/4 v10, 0x0
const/4 v11, 0x0
const/4 v12, 0x0
const/16 v13, 0xffe
const/4 v14, 0x0
invoke-direct/range {v0 .. v14}, Lc20/d;-><init>(Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;Lc20/a;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
move-object v9, v0
move-object/from16 v0, p1
const/4 v1, 0x0
move-object v2, v9
const/4 v3, 0x0
const/4 v4, 0x0
const/4 v5, 0x0
const/16 v6, 0x1d
const/4 v7, 0x0
invoke-static/range {v0 .. v7}, Ln20/e2;->g(Ln20/e2;Ln20/u4;Lc20/d;Lcom/squareup/ui/market/core/text/MarketTextAlignment;Lcom/squareup/ui/market/core/text/MarketTextTransform;Ln20/t4;ILjava/lang/Object;)Ln20/e2;
move-result-object p0
return-object p0
.end method
+212
View File
@@ -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
}
]
}
+12 -1
View File
@@ -56,7 +56,18 @@
.line 147 .line 147
sget-object v8, Lcom/tidal/android/feature/appscaffold/ui/composable/a;->c:Landroidx/compose/foundation/shape/RoundedCornerShape; sget-object v8, Lcom/tidal/android/feature/appscaffold/ui/composable/a;->c:Landroidx/compose/foundation/shape/RoundedCornerShape;
@@ -2969,6 +2972,10 @@ @@ -2857,6 +2860,10 @@
move-result-object v0
+ invoke-static {v0}, Landroidx/compose/foundation/layout/WindowInsetsPadding_androidKt;->navigationBarsPadding(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # inset bar content above nav bar (Prevents Bleeding behind Nav Buttons)
+
+ move-result-object v0
+
.line 155
sget-object v8, Landroidx/compose/foundation/layout/Arrangement;->INSTANCE:Landroidx/compose/foundation/layout/Arrangement;
@@ -2969,6 +2976,10 @@
invoke-static {v12, v0, v8}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lyl0/p;)V invoke-static {v12, v0, v8}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lyl0/p;)V
+56
View File
@@ -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
@@ -0,0 +1,106 @@
--- 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;
@@ -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;->textFeedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist
+ 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 @@
.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
+114
View File
@@ -0,0 +1,114 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali
@@ -2832,27 +2832,5 @@
- .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
+12 -1
View File
@@ -43,7 +43,18 @@
const/4 v11, 0x2 const/4 v11, 0x2
.line 142 .line 142
@@ -2969,6 +2966,10 @@ @@ -2857,6 +2854,10 @@
move-result-object v0
+ invoke-static {v0}, Landroidx/compose/foundation/layout/WindowInsetsPadding_androidKt;->navigationBarsPadding(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # inset bar content above nav bar (Prevents Bleeding behind Nav Buttons)
+
+ move-result-object v0
+
.line 155
sget-object v8, Landroidx/compose/foundation/layout/Arrangement;->INSTANCE:Landroidx/compose/foundation/layout/Arrangement;
@@ -2969,6 +2970,10 @@
invoke-static {v12, v0, v8}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lyl0/p;)V invoke-static {v12, v0, v8}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lyl0/p;)V
+20 -5
View File
@@ -1,7 +1,7 @@
# rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 71 # rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 71
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali --- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali +++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
@@ -4172,6 +4172,133 @@ @@ -4172,6 +4172,148 @@
invoke-static {v5, v3, v4}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lyl0/p;)V invoke-static {v5, v3, v4}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Lyl0/p;)V
@@ -29,9 +29,9 @@
+ +
+ move-result-object v4 # current item + move-result-object v4 # current item
+ +
+ instance-of v6, v4, Lcom/tidal/android/feature/playerscreen/ui/c$a; # only album covers + instance-of v6, v4, Lcom/tidal/android/feature/playerscreen/ui/c$a; # still album cover?
+ +
+ if-eqz v6, :radiant_skip # skip non-albums + if-eqz v6, :radiant_try_animated # if not, try animated album cover
+ +
+ check-cast v4, Lcom/tidal/android/feature/playerscreen/ui/c$a; # narrow type + check-cast v4, Lcom/tidal/android/feature/playerscreen/ui/c$a; # narrow type
+ +
@@ -39,6 +39,21 @@
+ +
+ iget-object v4, v4, Lcom/tidal/android/feature/playerscreen/ui/c$a;->c:Ljava/lang/String; # cover uuid + iget-object v4, v4, Lcom/tidal/android/feature/playerscreen/ui/c$a;->c:Ljava/lang/String; # cover uuid
+ +
+ goto :radiant_have_cover # cover resolved
+
+ :radiant_try_animated # animated album cover (c$b) shares albumId/cover layout
+ instance-of v6, v4, Lcom/tidal/android/feature/playerscreen/ui/c$b; # animated album cover?
+
+ if-eqz v6, :radiant_skip # neither -> skip (music video / unknown)
+
+ check-cast v4, Lcom/tidal/android/feature/playerscreen/ui/c$b; # narrow type
+
+ iget v5, v4, Lcom/tidal/android/feature/playerscreen/ui/c$b;->b:I # album id
+
+ iget-object v4, v4, Lcom/tidal/android/feature/playerscreen/ui/c$b;->c:Ljava/lang/String; # still cover uuid (blur this)
+
+ :radiant_have_cover # v5 = album id, v4 = cover uuid
+
+ new-instance v6, Lcom/tidal/android/feature/playerscreen/ui/composables/n0; # cover request lambda + new-instance v6, Lcom/tidal/android/feature/playerscreen/ui/composables/n0; # cover request lambda
+ +
+ invoke-direct {v6, v5, v4}, Lcom/tidal/android/feature/playerscreen/ui/composables/n0;-><init>(ILjava/lang/String;)V # build request + invoke-direct {v6, v5, v4}, Lcom/tidal/android/feature/playerscreen/ui/composables/n0;-><init>(ILjava/lang/String;)V # build request
@@ -55,7 +70,7 @@
+ +
+ move-result-object v5 # filled modifier + 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 + invoke-static {v7}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F # to Dp
+ +
@@ -111,7 +126,7 @@
+ +
+ move-result-object v3 # fullscreen modifier + 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 + invoke-static {v6}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color long
+ +
+24
View File
@@ -0,0 +1,24 @@
--- a/com/tidal/android/feature/playerscreen/ui/composables/c1.smali
+++ b/com/tidal/android/feature/playerscreen/ui/composables/c1.smali
@@ -259,7 +259,7 @@
.line 109
.line 110
- sget p2, Lcom/tidal/android/feature/playerscreen/ui/R$drawable;->ic_circle_checked:I
+ sget p2, Lcom/tidal/android/feature/playerscreen/ui/R$drawable;->ic_shapes_heart_filled_medium:I # favorited: filled heart (was circle-check)
.line 111
.line 112
@@ -267,11 +267,7 @@
.line 113
:cond_5
- sget-object p2, Lx20/b;->O:Lx20/a;
-
- .line 114
- .line 115
- iget p2, p2, Lx20/a;->a:I
+ sget p2, Lcom/tidal/android/feature/playerscreen/ui/R$drawable;->ic_shapes_heart_medium:I # not favorited: outline heart (was market plus)
.line 116
.line 117
+45
View File
@@ -0,0 +1,45 @@
# rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 80
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
@@ -5650,12 +5650,26 @@
invoke-static {v2, v15, v7, v9}, Lcom/aspiro/wamp/dynamicpages/ui/compose/f;->a(FLandroidx/compose/ui/Modifier$Companion;Landroidx/compose/runtime/Composer;I)V
const/4 v2, 0x0
const/4 v9, 0x0
+ invoke-static {}, Landroidx/compose/ui/platform/CompositionLocalsKt;->getLocalLayoutDirection()Landroidx/compose/runtime/ProvidableCompositionLocal; # layout-direction local
+
+ move-result-object v77 # the local
+
+ sget-object v78, Landroidx/compose/ui/unit/LayoutDirection;->Rtl:Landroidx/compose/ui/unit/LayoutDirection; # right-to-left value
+
+ invoke-virtual/range {v77 .. v78}, Landroidx/compose/runtime/ProvidableCompositionLocal;->provides(Ljava/lang/Object;)Landroidx/compose/runtime/ProvidedValue; # provide Rtl to local
+
+ move-result-object v77 # the provided value
+
+ move-object/from16 v76, v7 # composer
+
+ invoke-interface/range {v76 .. v77}, Landroidx/compose/runtime/Composer;->startProvider(Landroidx/compose/runtime/ProvidedValue;)V # open Rtl scope (mirror row)
+
.line 302
invoke-static {v15, v9, v1, v2}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxWidth$default(Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier;
move-result-object v4
move/from16 v5, v58
@@ -6001,12 +6015,14 @@
invoke-static {v13, v7, v2, v4}, Lcom/tidal/android/feature/playerscreen/ui/composables/m0;->a(ILandroidx/compose/runtime/Composer;Landroidx/compose/ui/Modifier;Lyl0/a;)V
.line 343
invoke-interface {v7}, Landroidx/compose/runtime/Composer;->endNode()V
+ invoke-interface {v7}, Landroidx/compose/runtime/Composer;->endProvider()V # close Rtl scope (end mirror)
+
.line 344
invoke-static/range {v40 .. v40}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result v20
const/16 v21, 0x7
+116
View File
@@ -0,0 +1,116 @@
--- a/com/tidal/android/feature/playerscreen/ui/composables/SeekbarAndTimeKt.smali
+++ b/com/tidal/android/feature/playerscreen/ui/composables/SeekbarAndTimeKt.smali
@@ -372,6 +372,10 @@
.line 158
sget v4, Lcom/tidal/android/feature/playerscreen/ui/composables/o4;->g:F
+ const/high16 v11, 0x40000000 # 2.0f
+
+ mul-float/2addr v4, v11 # scale atmos icon 2x as it is too small in stock app
+
.line 159
.line 160
invoke-static {v2, v4}, Landroidx/compose/foundation/layout/SizeKt;->height-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier;
@@ -461,6 +465,8 @@
.line 200
check-cast v3, Lcom/tidal/android/feature/playerscreen/ui/i$a;
+ iget v13, v3, Lcom/tidal/android/feature/playerscreen/ui/i$a;->b:I
+
.line 201
.line 202
iget-object v3, v3, Lcom/tidal/android/feature/playerscreen/ui/i$a;->a:Ljava/lang/String;
@@ -481,6 +487,16 @@
.line 210
move-object v1, v3
+ invoke-static {v2, v13}, Lradiant/QualityBadge;->modifier(Landroidx/compose/ui/Modifier;I)Landroidx/compose/ui/Modifier;
+
+ move-result-object v2
+
+ move-object/from16 v3, p1
+
+ invoke-static {v13, v3}, Lradiant/QualityBadge;->style(ILn20/e2;)Ln20/e2;
+
+ move-result-object v8
+
.line 211
const/4 v3, 0x0
--- a/com/tidal/android/feature/playerscreen/ui/i$a.smali
+++ b/com/tidal/android/feature/playerscreen/ui/i$a.smali
@@ -21,9 +21,21 @@
# instance fields
.field public final a:Ljava/lang/String;
+.field public final b:I
+
# direct methods
.method public constructor <init>(Ljava/lang/String;)V
+ .locals 1
+
+ const/4 v0, 0x0
+
+ invoke-direct {p0, p1, v0}, Lcom/tidal/android/feature/playerscreen/ui/i$a;-><init>(Ljava/lang/String;I)V
+
+ return-void
+.end method
+
+.method public constructor <init>(Ljava/lang/String;I)V
.locals 0
.line 1
@@ -34,6 +46,8 @@
.line 4
iput-object p1, p0, Lcom/tidal/android/feature/playerscreen/ui/i$a;->a:Ljava/lang/String;
+ iput p2, p0, Lcom/tidal/android/feature/playerscreen/ui/i$a;->b:I
+
.line 5
.line 6
return-void
--- a/com/tidal/android/feature/playerscreen/ui/PlayerViewModel$special$$inlined$mapNotNull$1$2.smali
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerViewModel$special$$inlined$mapNotNull$1$2.smali
@@ -1356,6 +1356,14 @@
.line 617
- move-result v10
+ move-result v10 # isCurrentStreamHiResLosslessQuality() result
+ const/16 v31, 0x0 # LOW / HIGH quality
+
+ if-eqz v10, :radiant_qbc_not_hires
+
+ const/16 v31, 0x2 # HI_RES_LOSSLESS quality
+
+ :radiant_qbc_not_hires
+
.line 618
if-nez v10, :cond_19
@@ -1368,6 +1376,12 @@
.line 623
- move-result v10
+ move-result v10 # isCurrentStreamLossless() result
+ if-eqz v10, :radiant_qbc_not_lossless
+
+ const/16 v31, 0x1 # LOSSLESS quality
+
+ :radiant_qbc_not_lossless
+
.line 624
if-eqz v10, :cond_18
@@ -1695,7 +1709,10 @@
.line 771
.line 772
- invoke-direct {v8, v5}, Lcom/tidal/android/feature/playerscreen/ui/i$a;-><init>(Ljava/lang/String;)V
+ move/from16 v10, v31 # quality
+
+ invoke-direct {v8, v5, v10}, Lcom/tidal/android/feature/playerscreen/ui/i$a;-><init>(Ljava/lang/String;I)V
+
.line 773
.line 774