Compare commits

..
52 Commits
Author SHA1 Message Date
meoware.exe fb4c776c61 Merge pull request #60 from meowarex/dev
Fold Cover Everywhere -> Player Backdrop
2026-06-26 23:02:05 +10:00
meoware.exe cbf04d9fa7 Fold Cover Everywhere -> Player Backdrop 2026-06-26 23:01:26 +10:00
meoware.exe 266cd184dd Merge pull request #59 from meowarex/dev
Improve Swipe Gstures <3
2026-06-26 22:38:36 +10:00
meoware.exe 04ea66acdb Improve Swipe Gstures <3 2026-06-26 22:38:04 +10:00
meoware.exe 22526f4e0b Merge pull request #58 from Se7en-dev/fix/mini-player-gestures-swipe-up-android-gestures-nav-fix
Fix : Mini Player Gestures - Android gesture navigation swipe up gesture fix + enable drag option + visual swipe up feedback
2026-06-26 19:02:36 +10:00
Se7en-dev 5a3ad2df7a fix(MP gestures): axis locking + swipe up feedback reset fixes 2026-06-26 10:59:11 +02:00
Se7en-dev fa99d10134 lint(MP gestures enable drag option): remove now useless text 2026-06-26 00:00:12 +02:00
Se7en-dev 4e8af5960c fix(mini player gestures): scope swipe up gesture to track area 2026-06-25 23:57:46 +02:00
Se7en-dev 09f6e8d34f feat(mini player gestures): enable drag option 2026-06-25 21:58:27 +02:00
meoware.exe 160be11ac4 Merge pull request #55 from meowarex/dev
Improve Patch Option Style <3
2026-06-25 01:28:34 +10:00
meoware.exe 4486e52117 Improve Patch Option Style <3 2026-06-25 01:27:32 +10:00
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
meoware.exe 4d5fea843e Merge pull request #40 from meowarex/dev
Rework Update Notification
2026-06-16 02:31:19 +10:00
meoware.exe d6bc889387 Rework Update Notification 2026-06-16 02:19:33 +10:00
meoware.exe 8501ccb0a9 Merge pull request #39 from meowarex/dev
Dev
2026-06-16 02:13:17 +10:00
meoware.exe ad6ebac6ce Bump Patch Version 2026-06-16 02:13:02 +10:00
meoware.exe 320148e774 Update RL-API Platform (DX) <3 2026-06-16 02:11:23 +10:00
meoware.exe d8e119eb4b Merge pull request #34 from meowarex/dev
Forgot to Bump Patches Version <3
2026-06-02 11:02:34 +10:00
meoware.exe 10e5dcf365 Forgot to Bump Patches Version <3 2026-06-02 11:02:12 +10:00
meoware.exe 1c65c5669a Merge pull request #32 from meowarex/dev
Fix Tidal Connect Share Bug <3
2026-06-01 20:59:31 +10:00
meoware.exe 961291166c Fix Tidal Connect button triggering Share Dialog 2026-06-01 20:52:16 +10:00
meoware.exe 25852aaea1 Merge pull request #29 from meowarex/dev
New Patch: Mini-Player Redesign
2026-05-26 22:53:46 +10:00
meoware.exe c6b18d1aa9 New Patch: Mini-Player Redesign 2026-05-26 22:49:21 +10:00
meoware.exe ee8b25330d Merge pull request #28 from meowarex/dev
New Patch: Cover Everywhere - WIP
2026-05-25 21:13:34 +10:00
meoware.exe 193d5ad90d Overhaul Manager Updates & Logging 2026-05-25 21:13:00 +10:00
meoware.exe 942742e86e New Patch: Cover Everywhere - WIP 2026-05-25 20:27:03 +10:00
meoware.exe 0b039110c9 Merge pull request #26 from meowarex/dev
Fix Missing Dependency
2026-05-25 04:35:38 +10:00
meoware.exe 045bbbab9a Fix Missing Dependency 2026-05-25 04:35:06 +10:00
71 changed files with 8791 additions and 706 deletions
Binary file not shown.
File diff suppressed because it is too large Load Diff
@@ -18,7 +18,6 @@ import com.meowarex.rlmobile.ui.screens.about.AboutModel
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsModel
import com.meowarex.rlmobile.ui.screens.home.HomeModel
import com.meowarex.rlmobile.ui.screens.log.LogScreenModel
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreenModel
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreenModel
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsModel
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
@@ -64,7 +63,6 @@ class ManagerApplication : Application() {
factoryOf(::PatchOptionsModel)
factoryOf(::ComponentOptionsModel)
factoryOf(::LogScreenModel)
factoryOf(::LogsListScreenModel)
factoryOf(::PermissionsModel)
viewModelOf(::UpdaterViewModel)
})
@@ -12,8 +12,6 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.util.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -21,52 +19,19 @@ import kotlin.time.Duration
import kotlin.time.Instant
/**
* Central manager for storing all attempted installations and
* their associated logs/crashes (not including manager crashes themselves).
* In-memory store for the most recent install attempts
*/
class InstallLogManager(
private val application: Application,
private val prefs: PreferencesManager,
private val json: Json,
@Suppress("unused") private val json: Json,
) {
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
private val entries = linkedMapOf<String, InstallLogData>()
/**
* Lists all the install data entries that exist on disk, sorted decreasing by
* the file creation date.
* @return List of installation ids, most recent installation first.
*/
fun fetchInstallDataEntries(): List<String> {
val files = logsDir.listFiles { it.extension == "json" } ?: emptyArray()
return files
.sortedByDescending { it.lastModified() }
.map { it.nameWithoutExtension }
fun fetchInstallData(id: String): InstallLogData? = synchronized(entries) {
entries[id]
}
/**
* Loads the install log from disk, if it exists.
*/
fun fetchInstallData(id: String): InstallLogData? {
val path = logsDir.resolve("$id.json")
if (!path.exists()) return null
return try {
json.decodeFromStream(path.inputStream())
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to open install log $id", t)
null
}
}
fun deleteAllEntries() {
logsDir.deleteRecursively()
logsDir.mkdir()
}
/**
* Writes an install log entry to disk.
*/
suspend fun storeInstallData(
id: String,
installDate: Instant,
@@ -75,8 +40,6 @@ class InstallLogManager(
log: String,
error: Throwable?,
) {
val path = logsDir.resolve("$id.json")
val data = InstallLogData(
id = id,
installDate = installDate,
@@ -87,16 +50,16 @@ class InstallLogManager(
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
)
try {
path.writeText(json.encodeToString(data))
} catch (e: IOException) {
Log.e(BuildConfig.TAG, "Failed to write log to disk", e)
synchronized(entries) {
entries[id] = data
// Keep only the most recent few in memory.
while (entries.size > MAX_ENTRIES) {
val oldest = entries.keys.iterator().next()
entries.remove(oldest)
}
}
}
/**
* Creates a list of details about the current installation environment.
*/
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
@SuppressLint("UsableSpace")
suspend fun getEnvironmentInfo(): String {
@@ -139,6 +102,10 @@ class InstallLogManager(
SOC: $soc
""".trimIndent()
}
private companion object {
const val MAX_ENTRIES = 8
}
}
@Immutable
@@ -8,7 +8,9 @@ import com.meowarex.rlmobile.patcher.steps.base.IDexProvider
import com.meowarex.rlmobile.patcher.steps.base.Step
import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
import com.meowarex.rlmobile.ui.screens.patchopts.PatchManifest
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs
import com.android.tools.smali.baksmali.Baksmali
import com.android.tools.smali.baksmali.BaksmaliOptions
import com.android.tools.smali.dexlib2.Opcodes
@@ -19,6 +21,7 @@ import com.github.diamondminer88.zip.ZipReader
import com.github.difflib.DiffUtils
import com.github.difflib.UnifiedDiffUtils
import com.github.difflib.patch.Patch
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.*
@@ -27,6 +30,7 @@ class SmaliPatchStep(
private val options: PatchOptions,
) : Step(), IDexProvider, KoinComponent {
private val paths: PathManager by inject()
private val json: Json by inject()
override val group = StepGroup.Patch
override val localizedName = R.string.patch_step_patch_smali
@@ -42,6 +46,29 @@ class SmaliPatchStep(
val patches = mutableListOf<LoadedPatch>()
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.
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
smaliDir.mkdirs()
@@ -53,6 +80,11 @@ class SmaliPatchStep(
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
val relative = patchFile.removePrefix("extension/")
// Only bundle helper smali patch/variant/sub-option is enabled
if (relative in knownExtensionFiles && relative !in enabledExtensionFiles) {
container.log("Skipping disabled extension smali: $relative")
continue
}
val out = smaliDir.resolve(relative)
// Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
val baseCanonical = smaliDir.canonicalPath + File.separator
@@ -71,16 +103,39 @@ class SmaliPatchStep(
if (!patchFile.endsWith(".patch")) continue
val basename = patchFile.substringAfterLast('/')
if (basename in options.disabledPatches) {
if (basename in disabledFiles) {
container.log("Skipping disabled patch $patchFile")
continue
}
val lines = zip.openEntry(patchFile)!!.read()
var patchText = zip.openEntry(patchFile)!!.read()
.decodeToString()
.replace("\r\n", "\n") // Replace CRLF endings with LF
.trimEnd { it == '\n' } // Remove trailing new lines
.split('\n')
// Bake advanced option values into the patch
for ((token, value) in substitutions) {
if (patchText.contains(token)) {
patchText = patchText.replace(token, value)
container.log("Applied substitution $token -> $value in $patchFile")
}
}
// Fail fast
UNRESOLVED_TOKEN.find(patchText)?.let { match ->
throw Error("Unresolved option placeholder ${match.value} in $patchFile")
}
// Skip a patch that references a helper class which won't be bundled
val missingHelper = (knownExtensionFiles - enabledExtensionFiles).firstOrNull { rel ->
patchText.contains("L${rel.removeSuffix(".smali")};")
}
if (missingHelper != null) {
container.log("Skipping $patchFile: references gated-off helper class $missingHelper")
continue
}
val lines = patchText.split('\n')
try {
for (directive in lines) {
@@ -92,18 +147,25 @@ class SmaliPatchStep(
container.log("Recorded rl-locals bump: $smaliPath method≈\"$methodSubstring\" >= $newValue")
}
val targetLine = lines.firstOrNull { it.startsWith("--- a/") }
?: throw Error("Patch $patchFile is missing a '--- a/...' header")
val fullClassName = targetLine
.removePrefix("--- a/")
.removeSuffix(".smali")
.trim()
val patch = LoadedPatch(
fullClassName = fullClassName,
patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
)
patches.add(patch)
container.log("Loaded patch file $patchFile for class ${patch.fullClassName}")
// Split into per-target sections — a single .patch may contain multiple
// `--- a/...` blocks targeting different classes.
val sections = splitMultiTargetPatch(lines)
if (sections.isEmpty()) {
throw Error("Patch $patchFile is missing a '--- a/...' header")
}
for (section in sections) {
val targetLine = section.first { it.startsWith("--- a/") }
val fullClassName = targetLine
.removePrefix("--- a/")
.removeSuffix(".smali")
.trim()
val patch = LoadedPatch(
fullClassName = fullClassName,
patch = UnifiedDiffUtils.parseUnifiedDiff(section),
)
patches.add(patch)
container.log("Loaded patch file $patchFile for class ${patch.fullClassName}")
}
} catch (t: Throwable) {
throw Error("Failed to parse patch file $patchFile", t)
}
@@ -313,6 +375,29 @@ class SmaliPatchStep(
private companion object {
val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""")
val UNRESOLVED_TOKEN = Regex("""__RL_[A-Z0-9_]+__""")
}
/**
* Splits a unified diff into per-target sections. A single `.patch` file may bundle
* multiple file diffs (each starting with `--- a/...`); each section becomes its own
* patch with its own target class. The header lines before the first `--- a/` (and
* any `# rl-locals:` directives) are preserved by being copied into every section so
* that `UnifiedDiffUtils.parseUnifiedDiff` can still parse the section in isolation.
*/
private fun splitMultiTargetPatch(lines: List<String>): List<List<String>> {
val headerEnd = lines.indexOfFirst { it.startsWith("--- a/") }
if (headerEnd < 0) return emptyList()
val header = lines.subList(0, headerEnd)
val sectionStarts = lines.withIndex()
.filter { (_, line) -> line.startsWith("--- a/") }
.map { it.index }
return sectionStarts.mapIndexed { i, start ->
val end = sectionStarts.getOrNull(i + 1) ?: lines.size
header + lines.subList(start, end)
}
}
}
@@ -1,19 +0,0 @@
package com.meowarex.rlmobile.ui.previews.dialogs
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.meowarex.rlmobile.ui.screens.logs.components.dialogs.DeleteLogsDialog
import com.meowarex.rlmobile.ui.theme.ManagerTheme
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun DeleteLogsDialogPreview() {
ManagerTheme {
DeleteLogsDialog(
onConfirm = {},
onDismiss = {},
)
}
}
@@ -2,6 +2,8 @@ package com.meowarex.rlmobile.ui.previews.screens
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.*
import com.meowarex.rlmobile.network.utils.SemVer
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
@@ -18,6 +20,9 @@ private fun PatchOptionsScreenPreview(
@PreviewParameter(PatchOptionsParametersProvider::class)
parameters: PatchOptionsParameters,
) {
val context = LocalContext.current
val specs = remember { builtinPatchSpecs { context.getString(it) } }
ManagerTheme {
PatchOptionsScreenContent(
isUpdate = parameters.isUpdate,
@@ -34,9 +39,14 @@ private fun PatchOptionsScreenPreview(
onSelectCustomTidalApk = {},
customPatches = parameters.customPatches,
onSelectCustomPatches = {},
enabledPatchCount = KnownPatch.All.size,
specs = specs,
enabledPatchCount = specs.size,
isPatchEnabled = { true },
onTogglePatch = { _, _ -> },
patchLockState = { PatchLock.Free },
variantIndex = { 0 },
onSelectVariant = { _, _ -> },
optionState = PatchOptionState.Preview,
isConfigValid = parameters.isConfigValid,
onInstall = {},
)
@@ -1,78 +0,0 @@
package com.meowarex.rlmobile.ui.previews.screens.logs
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.tooling.preview.Preview
import com.meowarex.rlmobile.ui.screens.logs.LogEntry
import com.meowarex.rlmobile.ui.screens.logs.LogsScreenContent
import com.meowarex.rlmobile.ui.theme.ManagerTheme
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun LogsListScreenNonePreview() {
ManagerTheme {
LogsScreenContent(
logs = logs,
onOpenLog = {},
onDeleteLogs = {},
)
}
}
private val logs: SnapshotStateList<LogEntry> = mutableStateListOf(
LogEntry(
id = UUID.randomUUID().toString(),
isError = false,
installDate = "5 min. ago, 10:18AM",
durationSecs = 18.555f,
stacktracePreview = null,
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = true,
installDate = "7 min. ago, 10:17AM",
durationSecs = 73.095f,
stacktracePreview = persistentListOf(
"kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@833e76f]",
),
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = false,
installDate = "Yesterday, 11:37PM",
durationSecs = 58.439f,
stacktracePreview = null,
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = true,
installDate = "Yesterday, 11:17PM",
durationSecs = 24.405f,
stacktracePreview = persistentListOf(
"java.lang.Error: Installation was aborted or cancelled",
),
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = true,
installDate = "Yesterday, 11:17PM",
durationSecs = 0.057f,
stacktracePreview = persistentListOf(
"java.lang.IllegalStateException: balls",
"\tat com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep.execute(FetchInfoStep.kt:31)",
"\tat com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep\$execute\\$1.invokeSuspend(Unknown Source:15)",
),
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = false,
installDate = "Yesterday, 1:11PM",
durationSecs = 210.539f,
stacktracePreview = null,
),
)
@@ -1,20 +0,0 @@
package com.meowarex.rlmobile.ui.previews.screens.logs
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.meowarex.rlmobile.ui.screens.logs.LogsScreenContent
import com.meowarex.rlmobile.ui.theme.ManagerTheme
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun LogsListScreenNonePreview() {
ManagerTheme {
LogsScreenContent(
logs = remember { mutableStateListOf() },
onOpenLog = {},
onDeleteLogs = {},
)
}
}
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.activity.ComponentActivity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -32,13 +33,14 @@ import com.meowarex.rlmobile.ui.components.SegmentedButton
import com.meowarex.rlmobile.ui.components.Tag
import com.meowarex.rlmobile.ui.screens.about.AboutScreen
import com.meowarex.rlmobile.ui.screens.home.components.CommitList
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreen
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsScreen
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
import com.meowarex.rlmobile.ui.widgets.managerupdate.ManagerUpdateDialog
import com.meowarex.rlmobile.ui.widgets.updater.UpdaterViewModel
import com.meowarex.rlmobile.util.*
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import org.koin.androidx.compose.koinViewModel
@Parcelize
class HomeScreen : Screen, Parcelable {
@@ -50,6 +52,9 @@ class HomeScreen : Screen, Parcelable {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val model = koinScreenModel<HomeModel>()
val activity = LocalContext.current as ComponentActivity
val updater = koinViewModel<UpdaterViewModel>(viewModelStoreOwner = activity)
val managerUpdateAvailable = updater.targetVersion != null
LifecycleResumeEffect(Unit) {
model.refresh(delay = true)
@@ -67,18 +72,20 @@ class HomeScreen : Screen, Parcelable {
contentDescription = stringResource(R.string.navigation_refresh),
)
}
if (managerUpdateAvailable) {
IconButton(onClick = updater::reopenDialog) {
Icon(
painterResource(R.drawable.ic_update),
contentDescription = stringResource(R.string.action_update),
)
}
}
IconButton(onClick = { navigator.push(AboutScreen()) }) {
Icon(
painterResource(R.drawable.ic_info),
contentDescription = stringResource(R.string.navigation_about),
)
}
IconButton(onClick = { navigator.push(LogsListScreen()) }) {
Icon(
painterResource(R.drawable.ic_receipt),
contentDescription = stringResource(R.string.navigation_logs),
)
}
IconButton(onClick = { navigator.push(SettingsScreen()) }) {
Icon(
painterResource(R.drawable.ic_settings),
@@ -107,6 +114,7 @@ class HomeScreen : Screen, Parcelable {
is HomeState.Loaded -> HomeContent(
state = state,
commits = model.commits,
managerUpdateAvailable = managerUpdateAvailable,
onInstall = { navigator.pushOnce(PatchOptionsScreen()) },
onRepatch = {
scope.launchIO {
@@ -134,6 +142,7 @@ class HomeScreen : Screen, Parcelable {
private fun ColumnScope.HomeContent(
state: HomeState.Loaded,
commits: kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<com.meowarex.rlmobile.network.models.GithubCommit>>,
managerUpdateAvailable: Boolean,
onInstall: () -> Unit,
onRepatch: () -> Unit,
onLaunch: () -> Unit,
@@ -200,12 +209,14 @@ private fun ColumnScope.HomeContent(
}
}
val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind)
Button(
onClick = if (install == null) onInstall else onRepatch,
enabled = state.latestTidalVersionCode != null,
enabled = state.latestTidalVersionCode != null && !blockedByManagerUpdate,
modifier = Modifier.fillMaxWidth(),
) {
val label = when {
blockedByManagerUpdate -> "Manager Update Required"
state.latestTidalVersionCode == null -> "Loading…"
install == null -> "Install"
patchesBehind && tidalBehind -> "Update Patches & TIDAL"
@@ -1,99 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs
import android.os.Parcelable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.core.screen.ScreenKey
import cafe.adriel.voyager.koin.koinScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.meowarex.rlmobile.ui.screens.log.LogScreen
import com.meowarex.rlmobile.ui.screens.logs.components.*
import com.meowarex.rlmobile.ui.screens.logs.components.dialogs.DeleteLogsDialog
import com.meowarex.rlmobile.ui.util.paddings.PaddingValuesSides
import com.meowarex.rlmobile.ui.util.paddings.exclude
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class LogsListScreen : Screen, Parcelable {
@IgnoredOnParcel
override val key: ScreenKey
get() = "LogsScreen"
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val model = koinScreenModel<LogsListScreenModel>()
var showWipeConfirmDialog by remember { mutableStateOf(false) }
if (showWipeConfirmDialog) {
DeleteLogsDialog(
onConfirm = {
showWipeConfirmDialog = false
model.deleteLogs()
},
onDismiss = {
showWipeConfirmDialog = false
},
)
}
LogsScreenContent(
logs = model.logEntries,
onOpenLog = { navigator.push(LogScreen(installId = it)) },
onDeleteLogs = { showWipeConfirmDialog = true },
)
}
}
@Composable
fun LogsScreenContent(
logs: SnapshotStateList<LogEntry>,
onOpenLog: (id: String) -> Unit,
onDeleteLogs: () -> Unit,
) {
Scaffold(
topBar = {
LogsListAppBar(
onDeleteLogs = onDeleteLogs,
)
},
) { paddingValues ->
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = paddingValues.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top),
modifier = Modifier
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(vertical = 12.dp, horizontal = 22.dp)
) {
if (logs.isEmpty()) {
item(key = "EMPTY") {
LogsNone(
modifier = Modifier.fillParentMaxSize(),
)
}
}
items(
items = logs,
contentType = { "LOG" },
key = { it.id },
) { data ->
LogEntryCard(
data = data,
onClick = remember(onOpenLog, data.id) { { onOpenLog(data.id) } },
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
@@ -1,71 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs
import android.app.Application
import android.text.format.DateUtils
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateListOf
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.InstallLogManager
import com.meowarex.rlmobile.util.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
class LogsListScreenModel(
private val logsManager: InstallLogManager,
private val application: Application,
) : ScreenModel {
/**
* All the loaded log entries sorted descending by creation date.
*/
val logEntries = mutableStateListOf<LogEntry>()
init {
loadLogsList()
}
fun deleteLogs() = screenModelScope.launchIO {
logsManager.deleteAllEntries()
mainThread {
logEntries.clear()
application.showToast(R.string.logs_status_delete_success)
}
}
private fun loadLogsList() = screenModelScope.launchIO {
for (installId in logsManager.fetchInstallDataEntries()) {
val data = logsManager.fetchInstallData(id = installId)
?: continue
val entry = LogEntry(
id = data.id,
isError = data.isError,
installDate = DateUtils.getRelativeDateTimeString(
/* c = */ application,
/* time = */ data.installDate.toEpochMilliseconds(),
/* minResolution = */ DateUtils.SECOND_IN_MILLIS,
/* transitionResolution = */ DateUtils.WEEK_IN_MILLIS,
/* flags = */ DateUtils.FORMAT_ABBREV_ALL,
).toString(),
durationSecs = data.installDuration.inWholeMilliseconds / 1000f,
stacktracePreview = data.errorStacktrace
?.splitToSequence('\n')
?.take(3)
?.toImmutableList(),
)
mainThread { logEntries += entry }
}
}
}
@Immutable
data class LogEntry(
val id: String,
val isError: Boolean,
val installDate: String,
val durationSecs: Float,
val stacktracePreview: ImmutableList<String>?,
)
@@ -1,122 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.patcher.steps.base.StepState
import com.meowarex.rlmobile.ui.screens.logs.LogEntry
import com.meowarex.rlmobile.ui.screens.patching.components.StepStateIcon
import com.meowarex.rlmobile.ui.screens.patching.components.TimeElapsed
@Composable
fun LogEntryCard(
data: LogEntry,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val errorColor = MaterialTheme.colorScheme.error
ElevatedCard(
shape = RectangleShape,
modifier = modifier
.clickable(onClick = onClick)
.clip(RoundedCornerShape(topStart = 6.dp, 12.0.dp, bottomStart = 6.dp, bottomEnd = 12.0.dp))
.drawWithCache {
val color = when (data.isError) {
true -> errorColor
false -> Color(0xFF59B463)
}
onDrawWithContent {
drawContent()
drawRect(
color = color,
alpha = .8f,
topLeft = Offset.Zero,
size = Size(4.dp.toPx(), size.height),
)
}
}
) {
Row(
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = 18.dp, horizontal = 22.dp),
) {
StepStateIcon(
state = if (data.isError) StepState.Error else StepState.Success,
size = 24.dp,
)
Column {
Text(
text = when (data.isError) {
true -> stringResource(R.string.status_failed)
false -> stringResource(R.string.status_success)
},
)
Text(
text = data.installDate,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.alpha(.6f),
)
}
Spacer(Modifier.weight(1f, fill = true))
TimeElapsed(
seconds = data.durationSecs,
modifier = Modifier.alpha(.9f),
)
}
if (data.stacktracePreview != null) {
Column(
modifier = Modifier
.padding(start = 26.dp, end = 20.dp, bottom = 18.dp)
// https://stackoverflow.com/a/76270310/13964629
.graphicsLayer(
alpha = .95f,
compositingStrategy = CompositingStrategy.Offscreen,
)
.drawWithContent {
val colors = listOf(Color.Black, Color.Black, Color.Transparent)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn,
)
}
) {
// The stacktrace is separated into multiple Text elements because ellipsis is not supported per-line
for (line in data.stacktracePreview) key(line) {
Text(
text = line,
softWrap = false,
overflow = TextOverflow.Ellipsis,
lineHeight = 18.sp,
style = MaterialTheme.typography.labelSmall,
fontFamily = FontFamily.Companion.Monospace,
)
}
}
}
}
}
@@ -1,37 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.components
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.BackButton
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
@Composable
fun LogsListAppBar(
onDeleteLogs: () -> Unit,
) {
TopAppBar(
navigationIcon = { BackButton() },
title = { Text(stringResource(R.string.logs_title)) },
actions = {
val navigator = LocalNavigator.current
IconButton(onClick = onDeleteLogs) {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = stringResource(R.string.logs_action_delete_all)
)
}
IconButton(onClick = { navigator?.push(SettingsScreen()) }) {
Icon(
painter = painterResource(R.drawable.ic_settings),
contentDescription = stringResource(R.string.navigation_settings)
)
}
}
)
}
@@ -1,33 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
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.unit.dp
import com.meowarex.rlmobile.R
@Composable
fun LogsNone(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Column(
modifier = Modifier.align(Alignment.Center),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(R.drawable.ic_reciept_off),
contentDescription = null,
)
Text(
text = stringResource(R.string.logs_none),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.alpha(.8f),
)
}
}
}
@@ -1,53 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.components.dialogs
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R
@Composable
fun DeleteLogsDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
},
title = { Text(stringResource(R.string.logs_wipe_title)) },
text = {
Text(
text = stringResource(R.string.logs_wipe_desc),
textAlign = TextAlign.Center,
)
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(R.string.action_cancel))
}
}
)
}
@@ -2,30 +2,35 @@ package com.meowarex.rlmobile.ui.screens.patchopts
import androidx.annotation.StringRes
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(
@StringRes val titleRes: Int,
val fileNames: List<String>,
val extensionFiles: List<String> = emptyList(),
)
enum class KnownPatch(
/**
* 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 order: Int, // Patch order in the UI List (lower = higher up) [Main Patches: multiples of 10 | Sub Patches: multiples of 1]
val fileNames: List<String>,
val extensionFiles: List<String> = emptyList(),
@StringRes val titleRes: 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 disables: List<KnownPatch> = emptyList(),
val variants: List<PatchVariant> = emptyList(),
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(
order = 41,
fileNames = listOf("lyrics-disable-cover.patch"),
titleRes = R.string.patch_lyrics_disable_cover_title,
descRes = R.string.patch_lyrics_disable_cover_desc,
default = Enabled,
),
LyricsReplaceLyricsButton(
order = 42,
@@ -35,12 +40,14 @@ enum class KnownPatch(
),
titleRes = R.string.patch_lyrics_replace_button_title,
descRes = R.string.patch_lyrics_replace_button_desc,
default = Enabled,
),
LyricsReplaceShareButton(
order = 43,
fileNames = listOf("lyrics-replace-share-button.patch"),
titleRes = R.string.patch_lyrics_replace_share_button_title,
descRes = R.string.patch_lyrics_replace_share_button_desc,
default = Enabled,
),
LyricsRlApi(
order = 20,
@@ -50,24 +57,78 @@ enum class KnownPatch(
),
titleRes = R.string.patch_lyrics_rl_api_title,
descRes = R.string.patch_lyrics_rl_api_desc,
default = Disabled,
),
LyricsKeepControlsVisible(
order = 60,
fileNames = listOf("lyrics-keep-controls-visible.patch"),
titleRes = R.string.patch_lyrics_keep_controls_title,
descRes = R.string.patch_lyrics_keep_controls_desc,
default = Enabled,
),
PlayerBackdrop(
order = 30,
fileNames = listOf("player-backdrop.patch"),
titleRes = R.string.patch_player_backdrop_title,
descRes = R.string.patch_player_backdrop_desc,
default = Enabled,
advancedOptions = listOf(
PatchOption.Slider(
key = "blur_strength",
titleRes = R.string.patch_opt_backdrop_blur_title,
descRes = R.string.patch_opt_backdrop_blur_desc,
default = 50f,
valueRange = 0f..100f,
steps = 19, // dots every 5% (0,5,…,100); snap only when locked
displayAsPercent = true,
token = "RL_BLUR_BITS",
encode = SmaliEncode(EncodeKind.FloatBits, scale = 1.8f),
),
PatchOption.Slider(
key = "dimming",
titleRes = R.string.patch_opt_backdrop_dimming_title,
descRes = R.string.patch_opt_backdrop_dimming_desc,
default = 50f, // 50% == the original -0x80000000
valueRange = 0f..100f,
steps = 19, // dots every 5%
displayAsPercent = true,
token = "RL_SCRIM_ARGB",
encode = SmaliEncode(EncodeKind.ArgbAlpha),
),
PatchOption.Toggle(
key = "cover_everywhere",
titleRes = R.string.patch_cover_everywhere_title,
descRes = R.string.patch_cover_everywhere_desc,
default = false,
inline = true,
fileNames = listOf(
"home-backdrop.patch",
"collection-backdrop.patch",
"cover-capture.patch",
),
),
),
),
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,
),
DebugMenuUnlock(
order = 100,
fileNames = listOf("debug-menu-unlock.patch"),
titleRes = R.string.patch_debug_menu_unlock_title,
descRes = R.string.patch_debug_menu_unlock_desc,
default = Disabled,
),
LyricsProgressPill(
order = 40,
@@ -77,13 +138,105 @@ enum class KnownPatch(
),
titleRes = R.string.patch_lyrics_progress_pill_title,
descRes = R.string.patch_lyrics_progress_pill_desc,
requires = listOf(LyricsDisableCover, LyricsReplaceShareButton),
default = Enabled,
requires = listOf(LyricsDisableCover, LyricsReplaceLyricsButton, LyricsReplaceShareButton),
),
MiniPlayerRedesign(
order = 50,
fileNames = emptyList(),
titleRes = R.string.patch_mini_player_redesign_title,
descRes = R.string.patch_mini_player_redesign_desc,
default = Disabled,
defaultVariantIndex = 2,
variants = listOf(
PatchVariant( // 0: Floating — stock rounded pill
titleRes = R.string.patch_mini_player_variant_floating_title,
fileNames = emptyList(),
),
PatchVariant( // 1: Grey — square
titleRes = R.string.patch_mini_player_variant_square_grey_title,
fileNames = listOf("mini-player-grey.patch"),
),
PatchVariant( // 2: Black — square black background
titleRes = R.string.patch_mini_player_variant_square_black_title,
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\$FeedbackLayer.smali",
"radiant/MiniPlayerGestures\$FeedbackResetAnimator.smali",
"radiant/MiniPlayerGestures\$RootGesture.smali",
"radiant/MiniPlayerGestures\$ApplyPending.smali",
),
),
PatchOption.Choice(
key = "swipe_up_drag",
titleRes = R.string.patch_mini_player_drag_title,
descRes = R.string.patch_mini_player_drag_desc,
entries = listOf(
ChoiceEntry(R.string.patch_mini_player_drag_static, value = "0x0"),
ChoiceEntry(R.string.patch_mini_player_drag_drag, value = "0x1"),
),
defaultIndex = 1,
requiresOption = "gestures",
token = "RL_MINI_PLAYER_SWIPE_UP_DRAG",
),
// 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(
order = 10,
fileNames = listOf("enable-legacy-ui.patch"),
titleRes = R.string.patch_enable_legacy_ui_title,
descRes = R.string.patch_enable_legacy_ui_desc,
default = Disabled,
requires = listOf(DebugMenuUnlock),
disables = listOf(
LyricsDisableCover,
@@ -92,17 +245,15 @@ enum class KnownPatch(
LyricsRlApi,
LyricsKeepControlsVisible,
PlayerBackdrop,
QualityBadgeColors,
LyricsProgressPill,
MiniPlayerRedesign,
),
);
companion object {
/**
* Sorted by `order` ascending. Tie-breaks fall back to the first filename
* (alphabetical) so the order is always deterministic.
*/
val All: List<KnownPatch> = entries.sortedWith(
compareBy({ it.order }, { it.fileNames.first() })
compareBy({ it.order }, { it.fileNames.firstOrNull() ?: it.name })
)
}
}
@@ -0,0 +1,6 @@
package com.meowarex.rlmobile.ui.screens.patchopts
enum class PatchDefault(val isEnabled: Boolean) {
Enabled(true),
Disabled(false),
}
@@ -0,0 +1,73 @@
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(),
/** Placeholder name (without the surrounding `__`) baked into the `.patch` files. */
val token: String? = null,
) : 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,
val requiresOption: String? = null,
val token: String? = null,
) : PatchOption
}
data class ChoiceEntry(
@StringRes val labelRes: Int,
val value: String? = null,
)
@@ -32,16 +32,119 @@ data class PatchOptions(
*/
val customPatches: PatchComponent? = null,
val disabledPatches: Set<String> = emptySet(),
val patchStates: Map<String, Boolean> = 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 {
fun isEnabled(spec: PatchSpec): Boolean =
patchStates[spec.id] ?: spec.defaultEnabled
fun variantIndex(spec: PatchSpec): Int {
val stored = (selectedVariants[spec.id] ?: spec.defaultVariantIndex)
.coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
return spec.resolveVariantIndex(stored) { isToggleOn(spec, it) }
}
fun sliderValue(spec: PatchSpec, option: OptionSpec.Slider): Float =
(optionFloats["${spec.id}/${option.key}"] ?: option.default).coerceIn(option.min, option.max)
fun choiceIndex(spec: PatchSpec, option: OptionSpec.Choice): Int =
(optionInts["${spec.id}/${option.key}"] ?: option.defaultIndex)
.coerceIn(0, option.entries.lastIndex.coerceAtLeast(0))
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) {
when (option) {
is OptionSpec.Toggle -> {
val token = option.token ?: continue
put("__${token}__", if (isToggleActive(spec, option)) "0x1" else "0x0")
}
is OptionSpec.Slider -> {
val token = option.token ?: continue
val encode = option.encode ?: continue
put("__${token}__", encode.encode(sliderValue(spec, option)))
}
is OptionSpec.Choice -> {
val token = option.token ?: continue
val gatedOff = option.requiresOption?.let { key ->
spec.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.firstOrNull { it.key == key }
?.let { !isToggleOn(spec, it) }
} ?: false
val index = if (gatedOff) 0 else choiceIndex(spec, option)
put("__${token}__", option.values.getOrNull(index) ?: continue)
}
}
}
}
}
companion object {
val Default = PatchOptions(
val Default: PatchOptions = PatchOptions(
appName = "TIDAL",
packageName = "com.aspiro.tidal",
debuggable = false,
customTidalApk = null,
customPatches = null,
disabledPatches = (KnownPatch.DebugMenuUnlock.fileNames + KnownPatch.EnableLegacyUi.fileNames).toSet(),
)
}
}
@@ -2,21 +2,35 @@ package com.meowarex.rlmobile.ui.screens.patchopts
import android.content.Context
import android.content.pm.PackageManager.NameNotFoundException
import android.util.Log
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
import com.github.diamondminer88.zip.ZipReader
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.manager.PreferencesManager
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsScreen
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.util.pushForResult
import com.meowarex.rlmobile.util.*
import com.meowarex.rlmobile.manager.download.IDownloadManager
import com.meowarex.rlmobile.manager.download.KtorDownloadManager
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
import com.meowarex.rlmobile.network.utils.getOrThrow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File
class PatchOptionsModel(
prefilledOptions: PatchOptions,
private val context: Context,
private val prefs: PreferencesManager,
private val paths: PathManager,
private val json: Json,
private val github: RadiantLyricsGithubService,
private val downloader: KtorDownloadManager,
) : ScreenModel {
var packageName by mutableStateOf(prefilledOptions.packageName)
private set
@@ -47,39 +61,153 @@ class PatchOptionsModel(
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
fun isPatchEnabled(patch: KnownPatch): Boolean =
patch.fileNames.none { it in disabledPatches }
var specsLoading by mutableStateOf(false)
private set
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
var patchStates by mutableStateOf(prefilledOptions.patchStates)
private set
var selectedVariants by mutableStateOf(prefilledOptions.selectedVariants)
private set
fun variantIndex(spec: PatchSpec): Int =
(selectedVariants[spec.id] ?: spec.defaultVariantIndex)
.coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
fun isPatchEnabled(spec: PatchSpec): Boolean =
patchStates[spec.id] ?: spec.defaultEnabled
fun setPatchEnabled(spec: PatchSpec, enabled: Boolean) {
val byId = specs.associateBy { it.id }
fun closure(seed: PatchSpec, step: (PatchSpec) -> List<PatchSpec>): Set<PatchSpec> =
buildSet {
fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) }
fun walk(p: PatchSpec) { if (add(p)) step(p).forEach(::walk) }
walk(seed)
}
val enableUnits: Set<KnownPatch>
val disableUnits: Set<KnownPatch>
val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) }
val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } }
val enableUnits: Set<PatchSpec>
val disableUnits: Set<PatchSpec>
if (enabled) {
enableUnits = closure(patch) { it.requires }
enableUnits = closure(spec, requiresOf)
disableUnits = enableUnits.flatMap { it.disables }
.flatMapTo(mutableSetOf()) { d ->
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } }
}
.mapNotNull(byId::get)
.flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) }
} else {
enableUnits = emptySet()
disableUnits = closure(patch) { p -> KnownPatch.All.filter { p in it.requires } }
disableUnits = closure(spec, dependentsOf)
}
val enableFiles = enableUnits.flatMap { it.fileNames }.toSet()
val disableFiles = disableUnits.flatMap { it.fileNames }.toSet()
disabledPatches = (disabledPatches - enableFiles) + disableFiles
patchStates = patchStates.toMutableMap().apply {
enableUnits.forEach { this[it.id] = true }
disableUnits.forEach { this[it.id] = false }
}
}
fun selectVariant(spec: PatchSpec, index: Int) {
if (spec.variants.isEmpty() || index !in spec.variants.indices) return
selectedVariants = selectedVariants + (spec.id to index)
}
// Advanced (per-patch) options
// Values are seeded from the prefilled config, edited here, and written back out in
// generateConfig(). At patch time PatchOptions.smaliSubstitutions() turns slider values into
// the literals baked into the matching `.patch` files (see SmaliPatchStep). ()
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 {
fun walk(p: PatchSpec) { if (add(p)) step(p).forEach(::walk) }
walk(seed)
}
val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) }
val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } }
for (other in specs) {
if (other.id == spec.id || !isPatchEnabled(other)) continue
val requiresClosure = closure(other, requiresOf)
if (spec in requiresClosure - other) return PatchLock.LockedOn(other)
val disablesClosure = requiresClosure.flatMap { it.disables }
.mapNotNull(byId::get)
.flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) }
if (spec in disablesClosure) return PatchLock.LockedOff(other)
}
return PatchLock.Free
}
val enabledPatchCount: Int
get() = KnownPatch.All.count { isPatchEnabled(it) }
get() = specs.count { isPatchEnabled(it) }
var customTidalApk by mutableStateOf<PatchComponent?>(null)
private set
@@ -102,6 +230,79 @@ class PatchOptionsModel(
componentType = PatchComponent.Type.Patches,
)
)
reloadSpecs(customPatches)
}
/**
* Rebuilds [specs] from the selected patch set
*/
private fun reloadSpecs(component: PatchComponent?) = screenModelScope.launchIO {
mainThread { specsLoading = true }
val loaded = if (component == null) builtinSpecs else loadManifestSpecs(component) ?: builtinSpecs
mainThread {
specs = loaded
validatePatchSelection()
specsLoading = false
}
}
private fun loadManifestSpecs(component: PatchComponent): List<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 {
@@ -122,7 +323,11 @@ class PatchOptionsModel(
debuggable = debuggable,
customTidalApk = customTidalApk,
customPatches = customPatches,
disabledPatches = disabledPatches,
patchStates = patchStates,
selectedVariants = selectedVariants,
optionFloats = optionFloats,
optionBools = optionBools,
optionInts = optionInts,
)
}
@@ -147,11 +352,24 @@ class PatchOptionsModel(
mainThread { packageNameState = state }
}
private fun validatePatchSelection() {
for (spec in specs) {
if (isPatchEnabled(spec)) {
setPatchEnabled(spec, true)
}
}
}
init {
validatePatchSelection()
screenModelScope.launchBlock { fetchPkgNameState() }
// Default source is the latest release's manifest; custom selections drive themselves.
if (customPatches == null) loadDefaultSpecs()
}
companion object {
private const val MANIFEST_NAME = "manifest.json"
private val PACKAGE_REGEX = """^[a-z]\w*(\.[a-z]\w*)+$"""
.toRegex(RegexOption.IGNORE_CASE)
}
@@ -162,3 +380,9 @@ enum class PackageNameState {
Invalid,
Taken,
}
sealed class PatchLock {
data object Free : PatchLock()
data class LockedOn(val by: PatchSpec) : PatchLock()
data class LockedOff(val by: PatchSpec) : PatchLock()
}
@@ -60,9 +60,14 @@ class PatchOptionsScreen(
onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) },
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
specs = model.specs,
enabledPatchCount = model.enabledPatchCount,
isPatchEnabled = model::isPatchEnabled,
onTogglePatch = model::setPatchEnabled,
patchLockState = model::lockState,
variantIndex = model::variantIndex,
onSelectVariant = model::selectVariant,
optionState = model.optionState,
isConfigValid = model.isConfigValid,
onInstall = {
@@ -93,9 +98,14 @@ fun PatchOptionsScreenContent(
customPatches: PatchComponent?,
onSelectCustomPatches: () -> Unit,
specs: List<PatchSpec>,
enabledPatchCount: Int,
isPatchEnabled: (KnownPatch) -> Boolean,
onTogglePatch: (KnownPatch, Boolean) -> Unit,
isPatchEnabled: (PatchSpec) -> Boolean,
onTogglePatch: (PatchSpec, Boolean) -> Unit,
patchLockState: (PatchSpec) -> PatchLock,
variantIndex: (PatchSpec) -> Int,
onSelectVariant: (PatchSpec, Int) -> Unit,
optionState: PatchOptionState,
isConfigValid: Boolean,
onInstall: () -> Unit,
@@ -158,10 +168,15 @@ fun PatchOptionsScreenContent(
}
PatchSelectionAccordion(
specs = specs,
enabledCount = enabledPatchCount,
totalCount = KnownPatch.All.size,
totalCount = specs.size,
isEnabled = isPatchEnabled,
onToggle = onTogglePatch,
lockState = patchLockState,
variantIndex = variantIndex,
onSelectVariant = onSelectVariant,
optionState = optionState,
modifier = Modifier.padding(top = 4.dp),
)
@@ -0,0 +1,277 @@
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(),
/** Placeholder name (without the surrounding `__`) baked into the `.patch` files. */
val token: String? = null,
) : 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,
val values: List<String> = emptyList(),
val requiresOption: String? = null,
val token: String? = null,
) : 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) },
token = token,
)
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,
values = entries.map { it.value ?: "" },
requiresOption = requiresOption,
token = token,
)
}
@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,473 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.draw.clip
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 parentKeys = patch.advancedOptions
.filterIsInstance<OptionSpec.Toggle>()
.filter { !it.inline }
.map { it.key }
.toSet()
val sheetOptions = patch.advancedOptions.filter { option ->
when {
option is OptionSpec.Toggle && option.inline -> false
option is OptionSpec.Toggle && option.requiresOption in parentKeys -> false
else -> true
}
}
for (option in sheetOptions) key(option.key) {
val lock = patch.optionLock(option, selectedVariant) { state.toggle(patch, it) }
when (option) {
is OptionSpec.Toggle -> {
val toggleOn = state.toggle(patch, option)
val gatedChoices = patch.advancedOptions.filterIsInstance<OptionSpec.Choice>()
.filter { it.requiresOption == option.key }
val gatedToggles = patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.filter { it.requiresOption == option.key && !it.inline }
val hasSubOptions = gatedChoices.isNotEmpty() || gatedToggles.isNotEmpty()
val carded = hasSubOptions && toggleOn
Column(
modifier = if (carded) {
Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f))
.border(
width = 1.5.dp,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f),
shape = MaterialTheme.shapes.medium,
)
.padding(horizontal = 12.dp, vertical = 4.dp)
} else {
Modifier.fillMaxWidth()
},
) {
ToggleOptionRow(
title = option.title,
description = option.description,
checked = toggleOn,
onCheckedChange = { state.setToggle(patch, option, it) },
locked = lock != OptionLock.Free,
onLockedTap = { lockDialog = lock },
modifier = if (hasSubOptions) Modifier.padding(vertical = 6.dp) else Modifier,
)
if (hasSubOptions) {
AnimatedVisibility(visible = toggleOn) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
) {
gatedChoices.forEach { choice ->
key(choice.key) {
ChoiceOptionRow(
title = choice.title,
entries = choice.entries,
selectedIndex = state.choice(patch, choice),
onSelect = { state.setChoice(patch, choice, it) },
)
}
}
gatedToggles.forEach { sub ->
key(sub.key) {
val subLock = patch.optionLock(sub, selectedVariant) { state.toggle(patch, it) }
ToggleOptionRow(
title = sub.title,
description = sub.description,
checked = state.toggle(patch, sub),
onCheckedChange = { state.setToggle(patch, sub, it) },
locked = subLock != OptionLock.Free,
onLockedTap = { lockDialog = subLock },
)
}
}
}
}
}
}
}
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 ->
if (option.requiresOption == null) {
ChoiceOptionRow(
title = option.title,
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 = {},
modifier: Modifier = Modifier,
) {
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(
entries: List<String>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
title: String = "",
modifier: Modifier = Modifier,
) {
Column(
modifier = modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(6.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
if (title.isNotEmpty()) {
Text(
text = title,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
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))
}
}
}
}
}
}
@@ -4,9 +4,11 @@ package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@@ -18,16 +20,30 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.fromHtml
import androidx.compose.ui.unit.dp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch
import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec
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: PatchSpec, val lock: PatchLock)
@Composable
fun PatchSelectionAccordion(
specs: List<PatchSpec>,
enabledCount: Int,
totalCount: Int,
isEnabled: (KnownPatch) -> Boolean,
onToggle: (KnownPatch, Boolean) -> Unit,
isEnabled: (PatchSpec) -> Boolean,
onToggle: (PatchSpec, Boolean) -> Unit,
lockState: (PatchSpec) -> PatchLock,
variantIndex: (PatchSpec) -> Int,
onSelectVariant: (PatchSpec, Int) -> Unit,
optionState: PatchOptionState,
modifier: Modifier = Modifier,
) {
var expanded by rememberSaveable { mutableStateOf(false) }
@@ -36,6 +52,9 @@ fun PatchSelectionAccordion(
label = "patch-accordion-arrow",
)
var lockInfo by remember { mutableStateOf<LockInfo?>(null) }
var advancedFor by remember { mutableStateOf<PatchSpec?>(null) }
Column(
modifier = modifier
.fillMaxWidth()
@@ -80,6 +99,7 @@ fun PatchSelectionAccordion(
AnimatedVisibility(visible = expanded) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.background.copy(0.4f))
@@ -93,21 +113,145 @@ fun PatchSelectionAccordion(
.padding(bottom = 8.dp),
)
for (patch in KnownPatch.All) key(patch) {
PatchCheckboxRow(
title = stringResource(patch.titleRes),
description = stringResource(patch.descRes),
checked = isEnabled(patch),
onCheckedChange = { onToggle(patch, it) },
)
for (patch in specs) key(patch.id) {
val checked = isEnabled(patch)
val lock = lockState(patch)
val inlineToggles = patch.advancedOptions
.filterIsInstance<OptionSpec.Toggle>()
.filter { it.inline }
val hasSheetOptions = patch.advancedOptions.any { it !is OptionSpec.Toggle || !it.inline }
val hasSettings = patch.variants.isNotEmpty() || inlineToggles.isNotEmpty() || hasSheetOptions
// When a patch is enabled and has settings show a carded view of the settings
val carded = checked && hasSettings
Column(
modifier = if (carded) {
Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f))
.border(
width = 1.5.dp,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f),
shape = MaterialTheme.shapes.medium,
)
.padding(horizontal = 12.dp, vertical = 4.dp)
} else {
Modifier.fillMaxWidth()
},
) {
PatchSwitchRow(
title = patch.title,
description = patch.description,
checked = checked,
lock = lock,
onCheckedChange = { onToggle(patch, it) },
onLockedTap = { lockInfo = LockInfo(patch, lock) },
)
if (hasSettings) {
AnimatedVisibility(visible = checked) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
) {
if (patch.variants.isNotEmpty()) {
val effective = patch.effectiveVariants { optionState.toggle(patch, it) }
val resolved = patch.resolveVariantIndex(variantIndex(patch)) {
optionState.toggle(patch, it)
}
PatchVariantSelector(
variants = effective,
selectedIndex = effective
.indexOfFirst { it.originalIndex == resolved }
.coerceAtLeast(0),
onSelect = { pos ->
effective.getOrNull(pos)?.let { onSelectVariant(patch, it.originalIndex) }
},
)
}
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) },
)
}
if (hasSheetOptions) {
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
AdvancedOptionsButton(
modified = optionState.isModified(patch),
onClick = { advancedFor = patch },
)
}
}
}
}
}
}
}
}
}
}
lockInfo?.let { info ->
PatchLockDialog(
thisPatch = info.patch,
lock = info.lock,
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 PatchCheckboxRow(
private fun InlineToggleRow(
title: String,
description: String,
checked: Boolean,
@@ -117,22 +261,62 @@ private fun PatchCheckboxRow(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = interactionSource,
indication = null,
role = Role.Checkbox,
role = Role.Switch,
) { onCheckedChange(!checked) }
.padding(vertical = 4.dp),
) {
Checkbox(
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
private fun PatchSwitchRow(
title: String,
description: String,
checked: Boolean,
lock: PatchLock,
onCheckedChange: (Boolean) -> Unit,
onLockedTap: () -> Unit,
) {
val interactionSource = remember(::MutableInteractionSource)
val isLocked = lock !is PatchLock.Free
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = interactionSource,
indication = null,
role = Role.Switch,
) {
if (isLocked) onLockedTap() else onCheckedChange(!checked)
}
.alpha(if (isLocked) 0.45f else 1f)
.padding(vertical = 6.dp),
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f),
@@ -147,5 +331,77 @@ private fun PatchCheckboxRow(
modifier = Modifier.alpha(.7f),
)
}
Box {
Switch(
checked = checked,
enabled = !isLocked,
onCheckedChange = onCheckedChange,
interactionSource = interactionSource,
)
if (isLocked) {
Box(
modifier = Modifier
.matchParentSize()
.clickable(
interactionSource = remember(::MutableInteractionSource),
indication = null,
role = Role.Switch,
) { onLockedTap() }
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun PatchLockDialog(
thisPatch: PatchSpec,
lock: PatchLock,
onDismiss: () -> Unit,
) {
val (titleRes, msgRes, blockerTitle) = when (lock) {
is PatchLock.LockedOn -> Triple(
R.string.patch_lock_required_title,
R.string.patch_lock_required_msg,
lock.by.title,
)
is PatchLock.LockedOff -> Triple(
R.string.patch_lock_blocked_title,
R.string.patch_lock_blocked_msg,
lock.by.title,
)
PatchLock.Free -> return
}
BasicAlertDialog(onDismissRequest = onDismiss) {
Surface(
shape = MaterialTheme.shapes.large,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
tonalElevation = 6.dp,
) {
Column(
modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp),
verticalArrangement = Arrangement.spacedBy(6.dp),
) {
Text(
text = stringResource(titleRes),
style = MaterialTheme.typography.titleLarge,
)
Text(
text = AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle)),
style = MaterialTheme.typography.titleMedium,
)
Box(modifier = Modifier.fillMaxWidth()) {
TextButton(
onClick = onDismiss,
modifier = Modifier.align(Alignment.CenterEnd),
) {
Text(stringResource(R.string.action_got_it))
}
}
}
}
}
}
@@ -0,0 +1,43 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.meowarex.rlmobile.ui.screens.patchopts.EffectiveVariant
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatchVariantSelector(
variants: List<EffectiveVariant>,
selectedIndex: Int,
onSelect: (Int) -> Unit,
modifier: Modifier = Modifier,
) {
SingleChoiceSegmentedButtonRow(
modifier = modifier.fillMaxWidth(),
) {
variants.forEachIndexed { index, variant ->
SegmentedButton(
selected = index == selectedIndex,
onClick = { onSelect(index) },
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = variants.size,
),
icon = {},
label = {
Text(
text = variant.title,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
)
},
)
}
}
}
@@ -55,6 +55,10 @@ class UpdaterViewModel(
showDialog = false
}
fun reopenDialog() {
if (targetVersion != null) showDialog = true
}
fun triggerUpdate() = viewModelScope.launchIO {
if (!isWorking.compareAndSet(expect = false, update = true))
return@launchIO
@@ -7,7 +7,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
@@ -51,15 +50,15 @@ class UpdateCheckWorker(
.maxByOrNull { it.first }
?: return Result.success()
val (version, release) = latestVersion
val (version, _) = latestVersion
if (current >= version) return Result.success()
Log.i(BuildConfig.TAG, "Update available: $version (installed $current)")
postUpdateNotification(version.toString(), release.htmlUrl)
postUpdateNotification(version.toString())
return Result.success()
}
private fun postUpdateNotification(version: String, releaseUrl: String) {
private fun postUpdateNotification(version: String) {
val nm = applicationContext.getSystemService<NotificationManager>() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -78,10 +77,15 @@ class UpdateCheckWorker(
return
}
val launchIntent = applicationContext.packageManager
.getLaunchIntentForPackage(applicationContext.packageName)
?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
?: return
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
Intent(Intent.ACTION_VIEW, Uri.parse(releaseUrl)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) },
launchIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
@@ -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>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M120,880v-800l60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60v800l-60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60ZM240,680h480v-80L240,600v80ZM240,520h480v-80L240,440v80ZM240,360h480v-80L240,280v80Z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="m819,932 l-59,-59q-10,3 -19.5,5T720,880L240,880q-50,0 -85,-35t-35,-85v-120h120v-287L27,140l57,-57L876,875l-57,57ZM840,727 L760,647v-447L313,200l-73,-73v-47l60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60v647ZM320,640h207L320,433v207ZM475,360 L395,280h205v80L475,360ZM595,480 L515,400h85v80h-5ZM680,480q-17,0 -28.5,-11.5T640,440q0,-17 11.5,-28.5T680,400q17,0 28.5,11.5T720,440q0,17 -11.5,28.5T680,480ZM680,360q-17,0 -28.5,-11.5T640,320q0,-17 11.5,-28.5T680,280q17,0 28.5,11.5T720,320q0,17 -11.5,28.5T680,360Z" />
</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>
@@ -5,5 +5,5 @@
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M480,840q-75,0 -140.5,-28.5t-114,-77q-48.5,-48.5 -77,-114T120,480q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,120q82,0 155.5,35T760,254v-54q0,-17 11.5,-28.5T800,160q17,0 28.5,11.5T840,200v160q0,17 -11.5,28.5T800,400L640,400q-17,0 -28.5,-11.5T600,360q0,-17 11.5,-28.5T640,320h70q-41,-56 -101,-88t-129,-32q-117,0 -198.5,81.5T200,480q0,117 81.5,198.5T480,760q95,0 170,-57t99,-147q5,-16 18,-24t29,-6q17,2 27,14.5t6,27.5q-29,119 -126,195.5T480,840ZM520,464 L620,564q11,11 11,28t-11,28q-11,11 -28,11t-28,-11L452,508q-6,-6 -9,-13.5t-3,-15.5v-159q0,-17 11.5,-28.5T480,280q17,0 28.5,11.5T520,320v144Z" />
android:pathData="M480,640q17,0 28.5,-11.5T520,600v-184l64,64q11,11 28,11t28,-11q11,-11 11,-28t-11,-28L508,308q-12,-12 -28,-12t-28,12L324,424q-11,11 -11,28t11,28q11,11 28,11t28,-11l64,-64v184q0,17 11.5,28.5T480,640Zm0,240q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>
+45 -9
View File
@@ -22,6 +22,7 @@
<string name="action_retry_install">Retry installation</string>
<string name="action_open_error_log">Open error log</string>
<string name="action_apply">Apply</string>
<string name="action_done">Done</string>
<string name="action_confirm">Confirm</string>
<string name="action_dismiss">Dismiss</string>
<string name="action_install">Install</string>
@@ -129,7 +130,6 @@
<string name="navigation_back">Back</string>
<string name="navigation_about">About</string>
<string name="navigation_logs">Logs</string>
<string name="navigation_settings">Settings</string>
<string name="navigation_refresh">Refresh</string>
@@ -252,6 +252,9 @@
<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_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_desc">Removes the mini track cover from the lyrics screen.</string>
@@ -259,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_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_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_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>
@@ -269,6 +272,12 @@
>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_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_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_desc"
@@ -282,6 +291,40 @@
name="patch_enable_legacy_ui_desc"
>[This patch will stop working soon] - Replaces the New Compose based UI with the Legacy UI (disables player-market-ui feature flag)</string>
<string name="patch_mini_player_redesign_title">Redesigned Mini-Player</string>
<string name="patch_mini_player_redesign_desc">Integrates the Seekbar into the control panel at the bottom of the screen.</string>
<string name="patch_mini_player_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_drag_title">Swipe Up Style</string>
<string name="patch_mini_player_drag_desc">Whether the player follows your finger when swiping up to open it.</string>
<string name="patch_mini_player_drag_static">Static</string>
<string name="patch_mini_player_drag_drag">Drag</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_square_grey_title">Grey</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_blocked_title">Patch Blocked!</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_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="componentopts_screen_title">Custom Component (%s)</string>
<string name="componentopts_screen_desc">Select a custom build that was imported by Manager.</string>
<string name="componentopts_selected_none">Latest</string>
@@ -301,13 +344,6 @@
<string name="log_action_export_apk">Export APK</string>
<string name="log_action_share">Share log</string>
<string name="logs_title">Logs</string>
<string name="logs_none">No logs present!</string>
<string name="logs_action_delete_all">Delete all logs</string>
<string name="logs_status_delete_success">Successfully deleted all logs</string>
<string name="logs_wipe_title">Clear Logs</string>
<string name="logs_wipe_desc">Are you sure you want to permanently delete all the logs?</string>
<string name="notif_group_install_title">Active installation</string>
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>
<string name="notif_install_ready_title">Radiant Lyrics installation ready!</string>
+22
View File
@@ -0,0 +1,22 @@
--- a/com/tidal/wave2/foundation/WaveScaffoldKt.smali
+++ b/com/tidal/wave2/foundation/WaveScaffoldKt.smali
@@ -889,6 +889,10 @@
.line 386
.line 387
:cond_20
+ const/4 v0, 0x0 # render $changed flag
+
+ invoke-static {v1, v0}, Lradiant/HomeBackdrop;->render(Landroidx/compose/runtime/Composer;I)V # render album-art backdrop
+
new-instance v0, Lcom/tidal/wave2/foundation/WaveScaffoldKt$WaveScaffold$2;
.line 388
@@ -991,7 +995,7 @@
.line 436
.line 437
- move-wide v15, v10
+ const-wide/16 v15, 0x0 # transparent containerColor
.line 438
const/4 v11, 0x0
+11
View File
@@ -0,0 +1,11 @@
--- a/com/tidal/android/feature/appscaffold/ui/p.smali
+++ b/com/tidal/android/feature/appscaffold/ui/p.smali
@@ -51,6 +51,8 @@
.line 10
iput-object p4, p0, Lcom/tidal/android/feature/appscaffold/ui/p;->d:Ljava/lang/String;
+ invoke-static {p3, p4}, Lradiant/HomeBackdrop;->fromMiniPlayer(ILjava/lang/String;)V # capture cover state
+
.line 11
.line 12
iput-boolean p5, p0, Lcom/tidal/android/feature/appscaffold/ui/p;->e:Z
+1 -1
View File
@@ -1,5 +1,5 @@
{
"tidalVersionCode": 9090,
"tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk",
"patchesVersion": "0.8.7"
"patchesVersion": "0.9.14"
}
@@ -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,370 @@
.class public final Lradiant/HomeBackdrop;
.super Ljava/lang/Object;
# static fields
.field public static volatile currentAlbumId:I
.field public static coverUuidState:Landroidx/compose/runtime/MutableState;
.annotation system Ldalvik/annotation/Signature;
value = {
"Landroidx/compose/runtime/MutableState<",
"Ljava/lang/String;",
">;"
}
.end annotation
.end field
# direct methods
.method static constructor <clinit>()V
.locals 4
const/4 v0, 0x0
sput v0, Lradiant/HomeBackdrop;->currentAlbumId:I
const/4 v0, 0x0
const/4 v1, 0x0
const/4 v2, 0x2
const/4 v3, 0x0
invoke-static {v0, v1, v2, v3}, 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/HomeBackdrop;->coverUuidState: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 dlog(Ljava/lang/String;)V
.locals 1
const-string v0, "RLHomeBackdrop"
invoke-static {v0, p0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
.method private static isDebugMenuStack()Z
.locals 6
new-instance v0, Ljava/lang/Throwable;
invoke-direct {v0}, Ljava/lang/Throwable;-><init>()V
invoke-virtual {v0}, Ljava/lang/Throwable;->getStackTrace()[Ljava/lang/StackTraceElement;
move-result-object v0
array-length v1, v0
const/4 v2, 0x0
:loop_start
if-ge v2, v1, :loop_end
aget-object v3, v0, v2
invoke-virtual {v3}, Ljava/lang/StackTraceElement;->getClassName()Ljava/lang/String;
move-result-object v3
const-string v4, "debugmenu"
invoke-virtual {v3, v4}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v5
if-eqz v5, :next
const/4 v0, 0x1
return v0
:next
add-int/lit8 v2, v2, 0x1
goto :loop_start
:loop_end
const/4 v0, 0x0
return v0
.end method
.method public static fromMiniPlayer(ILjava/lang/String;)V
.locals 2
new-instance v0, Ljava/lang/StringBuilder;
const-string v1, "fromMiniPlayer: id="
invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v1, " cover="
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
sput p0, Lradiant/HomeBackdrop;->currentAlbumId:I
sget-object v0, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
invoke-interface {v0, p1}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
return-void
.end method
.method public static onTrack(Lcom/aspiro/wamp/model/Track;)V
.locals 3
const-string v0, "onTrack: entered"
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
if-eqz p0, :clear
:try_start
invoke-virtual {p0}, Lcom/aspiro/wamp/model/MediaItem;->getAlbum()Lcom/aspiro/wamp/model/Album;
move-result-object v0
if-eqz v0, :clear
invoke-virtual {v0}, Lcom/aspiro/wamp/model/Album;->getId()I
move-result v1
sput v1, Lradiant/HomeBackdrop;->currentAlbumId:I
invoke-virtual {v0}, Lcom/aspiro/wamp/model/Album;->getCover()Ljava/lang/String;
move-result-object v0
sget-object v1, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
invoke-interface {v1, v0}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
new-instance v1, Ljava/lang/StringBuilder;
const-string v2, "onTrack: set uuid="
invoke-direct {v1, v2}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :swallow
return-void
:clear
const-string v0, "onTrack: clearing (null track/album)"
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
const/4 v0, 0x0
sput v0, Lradiant/HomeBackdrop;->currentAlbumId:I
sget-object v1, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
const/4 v2, 0x0
invoke-interface {v1, v2}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
return-void
:swallow
const-string v0, "onTrack: caught throwable"
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
return-void
.end method
.method public static render(Landroidx/compose/runtime/Composer;I)V
.locals 16
.annotation build Landroidx/compose/runtime/Composable;
.end annotation
invoke-static {}, Lradiant/HomeBackdrop;->isDebugMenuStack()Z
move-result v0
if-eqz v0, :proceed
return-void
:proceed
move-object/from16 v0, p0
const-string v1, "render: entered"
invoke-static {v1}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
const v1, 0x52414449
invoke-interface {v0, v1}, Landroidx/compose/runtime/Composer;->startReplaceGroup(I)V
sget-object v1, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
invoke-interface {v1}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/String;
new-instance v2, Ljava/lang/StringBuilder;
const-string v3, "render: uuid="
invoke-direct {v2, v3}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v2
invoke-static {v2}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
if-eqz v1, :skip
invoke-virtual {v1}, Ljava/lang/String;->length()I
move-result v2
if-eqz v2, :skip
sget v2, Lradiant/HomeBackdrop;->currentAlbumId:I
new-instance v3, Lcom/tidal/android/feature/playerscreen/ui/composables/n0;
invoke-direct {v3, v2, v1}, Lcom/tidal/android/feature/playerscreen/ui/composables/n0;-><init>(ILjava/lang/String;)V
sget-object v4, Landroidx/compose/ui/Modifier;->Companion:Landroidx/compose/ui/Modifier$Companion;
const/4 v5, 0x0
const/4 v6, 0x1
const/4 v7, 0x0
invoke-static {v4, v5, v6, v7}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxSize$default(Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier;
move-result-object v4
const/high16 v5, 0x42200000
invoke-static {v5}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result v5
sget-object v6, Landroidx/compose/ui/draw/BlurredEdgeTreatment;->Companion:Landroidx/compose/ui/draw/BlurredEdgeTreatment$Companion;
invoke-virtual {v6}, Landroidx/compose/ui/draw/BlurredEdgeTreatment$Companion;->getRectangle---Goahg()Landroidx/compose/ui/graphics/Shape;
move-result-object v6
invoke-static {v4, v5, v6}, Landroidx/compose/ui/draw/BlurKt;->blur-F8QBwvs(Landroidx/compose/ui/Modifier;FLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier;
move-result-object v4
sget-object v5, Landroidx/compose/ui/layout/ContentScale;->Companion:Landroidx/compose/ui/layout/ContentScale$Companion;
invoke-virtual {v5}, Landroidx/compose/ui/layout/ContentScale$Companion;->getCrop()Landroidx/compose/ui/layout/ContentScale;
move-result-object v5
move-object/from16 v6, v3
const/4 v7, 0x0
move-object/from16 v8, v4
const/4 v9, 0x0
move-object/from16 v10, v5
move-object/from16 v11, v1 # uuid as cache key
const/4 v12, 0x0
move-object/from16 v13, v0
const/4 v14, 0x0
const/16 v15, 0x48
invoke-static/range {v6 .. v15}, Lxd0/f;->a(Lyl0/l;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/ui/graphics/ColorFilter;Landroidx/compose/ui/layout/ContentScale;Ljava/lang/Object;Lyl0/a;Landroidx/compose/runtime/Composer;II)V
sget-object v3, Landroidx/compose/ui/Modifier;->Companion:Landroidx/compose/ui/Modifier$Companion;
const/4 v4, 0x0
const/4 v5, 0x1
const/4 v6, 0x0
invoke-static {v3, v4, v5, v6}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxSize$default(Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier;
move-result-object v3
const v4, -0x80000000
invoke-static {v4}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J
move-result-wide v4
invoke-static {}, Landroidx/compose/ui/graphics/RectangleShapeKt;->getRectangleShape()Landroidx/compose/ui/graphics/Shape;
move-result-object v6
invoke-static {v3, v4, v5, v6}, Landroidx/compose/foundation/BackgroundKt;->background-bw27NRU(Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier;
move-result-object v3
const/4 v4, 0x0
invoke-static {v3, v0, v4}, Landroidx/compose/foundation/layout/SpacerKt;->Spacer(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V
:skip
invoke-interface {v0}, Landroidx/compose/runtime/Composer;->endReplaceGroup()V
return-void
.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,70 @@
.class public final Lradiant/MiniPlayerGestures$FeedbackLayer;
.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 = "FeedbackLayer"
.end annotation
# static fields
.field public static final INSTANCE:Lradiant/MiniPlayerGestures$FeedbackLayer;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/MiniPlayerGestures$FeedbackLayer;
invoke-direct {v0}, Lradiant/MiniPlayerGestures$FeedbackLayer;-><init>()V
sput-object v0, Lradiant/MiniPlayerGestures$FeedbackLayer;->INSTANCE:Lradiant/MiniPlayerGestures$FeedbackLayer;
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/MiniPlayerGestures;->v: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;->setTranslationY(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/MiniPlayerGestures$FeedbackLayer;->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/MiniPlayerGestures$FeedbackResetAnimator;
.super Ljava/lang/Object;
.implements Landroid/animation/ValueAnimator$AnimatorUpdateListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/MiniPlayerGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "FeedbackResetAnimator"
.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/MiniPlayerGestures;->setSwipeFeedbackDirect(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/MiniPlayerGestures;->clearSwipeFeedbackAnimator(Landroid/animation/ValueAnimator;)V
:done
return-void
.end method
@@ -0,0 +1,521 @@
.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
# Drag option enabled
.field public final k:Z
# direct methods
.method public constructor <init>(Lyl0/l;Landroidx/compose/material3/SheetState;Z)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;
iput-boolean p3, p0, Lradiant/MiniPlayerGestures$Gesture;->k:Z
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-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->k:Z
if-eqz v0, :done
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
:done
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 {}, Lradiant/MiniPlayerGestures;->resetGestureLock()V
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
iget v0, p0, Lradiant/MiniPlayerGestures$Gesture;->b:F
invoke-static {v0}, Lradiant/MiniPlayerGestures;->beginSwipeGesture(F)V
:skip_feedback_down
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-static {}, Lradiant/MiniPlayerGestures;->suppressHorizontalGestures()V
iget-boolean v3, p0, Lradiant/MiniPlayerGestures$Gesture;->k:Z
if-eqz v3, :feedback_active
invoke-direct {p0, v2, v5, v6}, Lradiant/MiniPlayerGestures$Gesture;->dragSheet(FJ)V
:feedback_active
invoke-static {v2}, Lradiant/MiniPlayerGestures;->setSwipeFeedbackFromDrag(F)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, 0x40c00000 # 6.0f axis deadzone
invoke-static {v9}, Lradiant/MiniPlayerGestures$Gesture;->dp(F)F
move-result v9
invoke-static {}, Lradiant/MiniPlayerGestures;->isHorizontalGestureLocked()Z
move-result v10
if-eqz v10, :horizontal_check
const/4 v10, 0x0
invoke-static {v10}, Lradiant/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
goto :store_last
:horizontal_check
cmpg-float v10, v7, v9
if-gez v10, :horizontal_dominance
goto :vertical_check
:horizontal_dominance
const v10, 0x3f8ccccd # 1.1f
mul-float/2addr v10, v8
cmpg-float v10, v7, v10
if-gez v10, :horizontal_wins
goto :vertical_check
:horizontal_wins
invoke-static {}, Lradiant/MiniPlayerGestures;->lockHorizontalGesture()Z
move-result v10
if-eqz v10, :store_last
const/4 v10, 0x1
iput-boolean v10, p0, Lradiant/MiniPlayerGestures$Gesture;->j:Z
const/4 v10, 0x0
invoke-static {v10}, Lradiant/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
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
invoke-static {v2}, Lradiant/MiniPlayerGestures;->setSwipeFeedbackFromDrag(F)V
goto :store_last
:vertical_dominance
const v10, 0x3f8ccccd # 1.1f -> early vertical axis lock
mul-float/2addr v7, v10
cmpl-float v10, v8, v7
if-gtz v10, :begin
goto :store_last
:begin
invoke-static {}, Lradiant/MiniPlayerGestures;->lockVerticalGesture()Z
move-result v10
if-eqz v10, :store_last
invoke-direct {p0, v2, v5, v6}, Lradiant/MiniPlayerGestures$Gesture;->startSheet(FJ)V
iget-boolean v10, p0, Lradiant/MiniPlayerGestures$Gesture;->e:Z
if-eqz v10, :store_last
invoke-static {}, Lradiant/MiniPlayerGestures;->suppressHorizontalGestures()V
iget-boolean v10, p0, Lradiant/MiniPlayerGestures$Gesture;->k:Z
if-eqz v10, :feedback_begin
invoke-direct {p0, v2, v5, v6}, Lradiant/MiniPlayerGestures$Gesture;->dragSheet(FJ)V
:feedback_begin
invoke-static {v2}, Lradiant/MiniPlayerGestures;->setSwipeFeedbackFromDrag(F)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, :up_no_drag
iget-boolean v0, p0, Lradiant/MiniPlayerGestures$Gesture;->k:Z
if-eqz v0, :open_simple
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
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :finish
:up_no_drag
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :finish
:open_simple
invoke-static {}, Lradiant/MiniPlayerGestures;->suppressHorizontalGestures()V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v0
iget-object v1, p0, Lradiant/MiniPlayerGestures$Gesture;->a:Lyl0/l;
invoke-static {v1, v0}, Lradiant/MiniPlayerGestures;->completeSimpleSwipe(Lyl0/l;F)Z
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()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_no_active
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()V
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
goto :cancel_reset
:cancel_no_active
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()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,178 @@
.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-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I
move-result v1
invoke-static {v0}, Lradiant/MiniPlayerGestures;->isDragging(Landroidx/compose/material3/SheetState;)Z
move-result v2
if-eqz v2, :maybe_reset_only
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
:maybe_reset_only
const/4 v2, 0x1
if-eq v1, v2, :simple_up
const/4 v2, 0x3
if-eq v1, v2, :reset_only
const/4 v2, 0x2
if-eq v1, v2, :simple_move
goto :done
:reset_only
invoke-static {}, Lradiant/MiniPlayerGestures;->resetGestureLock()V
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :done
:simple_move
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-static {v1}, Lradiant/MiniPlayerGestures;->updateSimpleSwipe(F)Z
goto :done
:simple_up
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
iget-object v2, p0, Lradiant/MiniPlayerGestures$RootGesture;->a:Lyl0/l;
invoke-static {v2, v1}, Lradiant/MiniPlayerGestures;->completeSimpleSwipe(Lyl0/l;F)Z
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()V
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
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()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
invoke-static {}, Lradiant/MiniPlayerGestures;->animateSwipeFeedbackReset()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,605 @@
.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
invoke-static {}, Lradiant/MiniPlayerGestures;->resetGestureLock()V
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
invoke-static {}, Lradiant/MiniPlayerGestures;->isAnyDragging()Z
move-result v0
if-eqz v0, :check_active
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 v0
:check_active
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
invoke-static {}, Lradiant/MiniPlayerGestures;->lockHorizontalGesture()Z
move-result v2
if-eqz v2, :cancel_locked
const/4 v2, 0x1
sput-boolean v2, Lradiant/MiniPlayerTrackGestures;->h:Z
:publish
const/4 v2, 0x0
invoke-static {v2}, Lradiant/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
invoke-static {}, Lradiant/MiniPlayerTrackGestures;->suppressTapOpen()V
invoke-static {p0}, Lradiant/MiniPlayerTrackGestures;->setDragOffset(F)V
:done
return-void
:cancel_locked
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
.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,436 @@
.class public final Lradiant/MiniSeekerFloating;
.super Ljava/lang/Object;
.implements Lyl0/l;
# static fields
.field public static final INSTANCE:Lradiant/MiniSeekerFloating;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/MiniSeekerFloating; # singleton
invoke-direct {v0}, Lradiant/MiniSeekerFloating;-><init>()V # ctor
sput-object v0, Lradiant/MiniSeekerFloating;->INSTANCE:Lradiant/MiniSeekerFloating; # publish
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 final invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 2
check-cast p1, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope; # narrow type
invoke-interface {p1}, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;->drawContent()V # draw pill content
invoke-static {}, Lradiant/MiniSeekerLine;->start()V # lazy subscribe
:try_start
invoke-static {p1}, Lradiant/MiniSeekerFloating;->drawBorder(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V # draw progress
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :swallow
:swallow
sget-object v0, Lkotlin/u;->a:Lkotlin/u; # Unit
return-object v0
.end method
.method private static drawBorder(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
.locals 60
sget-object v0, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # state
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F # read tracked
move-result v0 # raw progress
const/4 v1, 0x0 # lower bound
invoke-static {v0, v1}, Ljava/lang/Math;->max(FF)F # clamp low
move-result v0 # clamped low
const/high16 v1, 0x3f800000 # 1.0f upper
invoke-static {v0, v1}, Ljava/lang/Math;->min(FF)F # clamp high
move-result v0 # final progress
const/4 v1, 0x0 # zero
cmpg-float v2, v0, v1 # compare to zero
if-lez v2, :done # skip if empty
move/from16 v16, v0 # stash progress
invoke-interface/range {p0 .. p0}, Landroidx/compose/ui/graphics/drawscope/DrawScope;->getSize-NH-jbRc()J # size long
move-result-wide v0 # size pair
invoke-static {v0, v1}, Landroidx/compose/ui/geometry/Size;->getWidth-impl(J)F # extract width
move-result v2 # width px
move/from16 v17, v2 # stash width
invoke-static {v0, v1}, Landroidx/compose/ui/geometry/Size;->getHeight-impl(J)F # extract height
move-result v2 # height px
move/from16 v18, v2 # stash height
invoke-interface/range {p0 .. p0}, Landroidx/compose/ui/unit/Density;->getDensity()F # density
move-result v2 # px per dp
move/from16 v19, v2 # stash density
const/high16 v0, 0x42100000 # 36.0f dp
move/from16 v1, v19 # density
mul-float v2, v0, v1 # to px
move/from16 v20, v2 # stash r
const/high16 v0, 0x40000000 # 2.0f
move/from16 v1, v20 # r
mul-float v2, v1, v0 # 2*r
move/from16 v21, v2 # stash 2r
move/from16 v0, v17 # w
move/from16 v1, v20 # r
sub-float v2, v0, v1 # w-r
move/from16 v22, v2 # stash w-r
move/from16 v0, v18 # h
move/from16 v1, v20 # r
sub-float v2, v0, v1 # h-r
move/from16 v23, v2 # stash h-r
move/from16 v0, v17 # w
move/from16 v1, v21 # 2r
sub-float v2, v0, v1 # w-2r
move/from16 v24, v2 # stash w-2r
move/from16 v0, v18 # h
move/from16 v1, v21 # 2r
sub-float v2, v0, v1 # h-2r
move/from16 v25, v2 # stash h-2r
move/from16 v0, v17 # w
const/high16 v1, 0x40000000 # 2.0f
div-float v2, v0, v1 # w/2
move/from16 v26, v2 # stash top-center x
const/high16 v0, 0x42b40000 # 90.0f
move/from16 v27, v0 # stash 90
const/4 v0, 0x0 # zero
move/from16 v1, v27 # 90
sub-float v2, v0, v1 # -90
move/from16 v28, v2 # stash -90
const/high16 v0, 0x43340000 # 180.0f
move/from16 v29, v0 # stash 180
invoke-static {}, Landroidx/compose/ui/graphics/AndroidPath_androidKt;->Path()Landroidx/compose/ui/graphics/Path; # new path
move-result-object v0 # path
move-object/from16 v30, v0 # stash path
move-object/from16 v0, v30 # path
move/from16 v1, v26 # w/2
const/4 v2, 0x0 # 0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->moveTo(FF)V # start top-center
move-object/from16 v0, v30 # path
move/from16 v1, v22 # w-r
const/4 v2, 0x0 # 0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # top edge right
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
move/from16 v4, v24 # w-2r
const/4 v5, 0x0 # 0
move/from16 v6, v17 # w
move/from16 v7, v21 # 2r
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # TR rect
move-object/from16 v0, v30 # path
move/from16 v4, v28 # -90
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # TR corner
move-object/from16 v0, v30 # path
move/from16 v1, v17 # w
move/from16 v2, v23 # h-r
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # right edge
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
move/from16 v4, v24 # w-2r
move/from16 v5, v25 # h-2r
move/from16 v6, v17 # w
move/from16 v7, v18 # h
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # BR rect
move-object/from16 v0, v30 # path
const/4 v4, 0x0 # 0 deg
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # BR corner
move-object/from16 v0, v30 # path
move/from16 v1, v20 # r
move/from16 v2, v18 # h
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # bottom edge
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
const/4 v4, 0x0 # 0
move/from16 v5, v25 # h-2r
move/from16 v6, v21 # 2r
move/from16 v7, v18 # h
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # BL rect
move-object/from16 v0, v30 # path
move/from16 v4, v27 # 90
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # BL corner
move-object/from16 v0, v30 # path
const/4 v1, 0x0 # 0
move/from16 v2, v20 # r
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # left edge
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
const/4 v4, 0x0 # 0
const/4 v5, 0x0 # 0
move/from16 v6, v21 # 2r
move/from16 v7, v21 # 2r
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # TL rect
move-object/from16 v0, v30 # path
move/from16 v4, v29 # 180
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # TL corner
move-object/from16 v0, v30 # path
move/from16 v1, v26 # w/2
const/4 v2, 0x0 # 0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # close to start
const/high16 v0, 0x40000000 # 2.0f
move/from16 v1, v24 # w-2r
mul-float v2, v1, v0 # 2*(w-2r)
move/from16 v1, v25 # h-2r
mul-float v3, v1, v0 # 2*(h-2r)
add-float v4, v2, v3 # straight edges
const v5, 0x40c90fdb # 2*pi
move/from16 v6, v20 # r
mul-float v7, v6, v5 # arc length
add-float v8, v4, v7 # total perim
move/from16 v31, v8 # stash perim
move/from16 v0, v16 # progress
move/from16 v1, v31 # perim
mul-float v2, v0, v1 # visible length
move/from16 v32, v2 # stash visible
sget-object v0, Landroidx/compose/ui/graphics/PathEffect;->Companion:Landroidx/compose/ui/graphics/PathEffect$Companion; # companion
const/4 v1, 0x2 # length 2
new-array v1, v1, [F # intervals
const/4 v2, 0x0 # index 0
move/from16 v3, v32 # visible
aput v3, v1, v2 # dash length
const/4 v2, 0x1 # index 1
move/from16 v3, v31 # perim
aput v3, v1, v2 # gap length
const/4 v2, 0x0 # phase int
int-to-float v2, v2 # phase float
invoke-virtual {v0, v1, v2}, Landroidx/compose/ui/graphics/PathEffect$Companion;->dashPathEffect([FF)Landroidx/compose/ui/graphics/PathEffect; # dash effect
move-result-object v0 # effect
move-object/from16 v33, v0 # stash effect
const/high16 v0, 0x40000000 # 2.0f
move/from16 v1, v19 # density
mul-float v2, v0, v1 # stroke px
move/from16 v34, v2 # stash stroke width
new-instance v40, Landroidx/compose/ui/graphics/drawscope/Stroke; # stroke obj
move/from16 v41, v34 # width
const/16 v42, 0x0 # miter default
const/16 v43, 0x0 # cap default
const/16 v44, 0x0 # join default
move-object/from16 v45, v33 # effect
const/16 v46, 0xe # default mask
const/16 v47, 0x0 # marker null
invoke-direct/range {v40 .. v47}, Landroidx/compose/ui/graphics/drawscope/Stroke;-><init>(FFIILandroidx/compose/ui/graphics/PathEffect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V # stroke ctor
move-object/from16 v35, v40 # stash stroke
const v0, -0x1 # white ARGB
invoke-static {v0}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color
move-result-wide v0 # color long
move-wide/from16 v36, v0 # stash color
move-object/from16 v50, p0 # scope
move-object/from16 v51, v30 # path
move-wide/from16 v52, v36 # color
const/16 v54, 0x0 # alpha default
move-object/from16 v55, v35 # style
const/16 v56, 0x0 # filter null
const/16 v57, 0x0 # blend default
const/16 v58, 0x34 # default mask
const/16 v59, 0x0 # marker null
invoke-static/range {v50 .. v59}, Landroidx/compose/ui/graphics/drawscope/DrawScope;->drawPath-LG529CI$default(Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/graphics/Path;JFLandroidx/compose/ui/graphics/drawscope/DrawStyle;Landroidx/compose/ui/graphics/ColorFilter;IILjava/lang/Object;)V # draw stroke
:done
return-void
.end method
@@ -0,0 +1,200 @@
.class public final Lradiant/MiniSeekerLine;
.super Ljava/lang/Object;
# static fields
.field public static progressState:Landroidx/compose/runtime/MutableFloatState;
.field public static volatile vm:Ljh/h;
.field public static volatile sub:Lio/reactivex/disposables/Disposable;
# direct methods
.method static constructor <clinit>()V
.locals 1
const/4 v0, 0x0 # initial progress
invoke-static {v0}, Landroidx/compose/runtime/PrimitiveSnapshotStateKt;->mutableFloatStateOf(F)Landroidx/compose/runtime/MutableFloatState; # state holder
move-result-object v0 # state instance
sput-object v0, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # publish state
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method private static dlog(Ljava/lang/String;)V
.locals 1
const-string v0, "RLProgressLine" # log tag
invoke-static {v0, p0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I # info log
return-void
.end method
.method public static start()V
.locals 3
sget-object v0, Lradiant/MiniSeekerLine;->sub:Lio/reactivex/disposables/Disposable; # current sub
if-eqz v0, :start_block # only run once
return-void
:start_block
:try_start
sget-object v0, Lradiant/MiniSeekerLine;->vm:Ljh/h; # cached vm
if-nez v0, :got_vm # reuse if present
invoke-static {}, Lcom/aspiro/wamp/App$a;->a()Lcom/aspiro/wamp/App; # app singleton
move-result-object v0 # app
invoke-virtual {v0}, Lcom/aspiro/wamp/App;->e()Lf5/c; # dagger component
move-result-object v0 # component
check-cast v0, Lf5/d0$n2; # narrow type
iget-object v0, v0, Lf5/d0$n2;->z0:Ldagger/internal/j; # playqueue provider
invoke-interface {v0}, Lol0/a;->get()Ljava/lang/Object; # resolve
move-result-object v0 # playqueue
check-cast v0, Lcom/aspiro/wamp/playqueue/z0; # narrow type
new-instance v1, Ljh/h; # build vm
invoke-direct {v1, v0}, Ljh/h;-><init>(Lcom/aspiro/wamp/playqueue/z0;)V # ctor
sput-object v1, Lradiant/MiniSeekerLine;->vm:Ljh/h; # cache
:got_vm
sget-object v0, Lradiant/MiniSeekerLine;->vm:Ljh/h; # vm ref
invoke-virtual {v0}, Ljh/h;->a()V # activate emission
iget-object v0, v0, Ljh/h;->d:Lio/reactivex/subjects/BehaviorSubject; # position subject
invoke-static {}, Lio/reactivex/android/schedulers/AndroidSchedulers;->mainThread()Lio/reactivex/Scheduler; # main scheduler
move-result-object v1 # scheduler
invoke-virtual {v0, v1}, Lio/reactivex/Observable;->observeOn(Lio/reactivex/Scheduler;)Lio/reactivex/Observable; # observe main
move-result-object v0 # observable
new-instance v1, Lradiant/SeekerConsumer; # consumer
invoke-direct {v1}, Lradiant/SeekerConsumer;-><init>()V # ctor
invoke-virtual {v0, v1}, Lio/reactivex/Observable;->subscribe(Lio/reactivex/functions/Consumer;)Lio/reactivex/disposables/Disposable; # subscribe
move-result-object v0 # disposable
sput-object v0, Lradiant/MiniSeekerLine;->sub:Lio/reactivex/disposables/Disposable; # cache
const-string v0, "started" # log msg
invoke-static {v0}, Lradiant/MiniSeekerLine;->dlog(Ljava/lang/String;)V # info log
return-void
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :swallow
:swallow
move-exception v0 # error
const-string v1, "start failed" # log msg
invoke-static {v1}, Lradiant/MiniSeekerLine;->dlog(Ljava/lang/String;)V # info log
return-void
.end method
.method public static render(Landroidx/compose/runtime/Composer;I)V
.locals 5
.annotation build Landroidx/compose/runtime/Composable;
.end annotation
invoke-static {}, Lradiant/MiniSeekerLine;->start()V # lazy subscribe
sget-object v0, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # state
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F # read tracked
move-result v0 # raw progress
const/4 v1, 0x0 # lower bound
invoke-static {v0, v1}, Ljava/lang/Math;->max(FF)F # clamp low
move-result v0 # clamped low
const/high16 v1, 0x3f800000 # 1.0f upper
invoke-static {v0, v1}, Ljava/lang/Math;->min(FF)F # clamp high
move-result v0 # final progress
const v1, 0x52414c49 # 'RALI' slot key
invoke-interface {p0, v1}, Landroidx/compose/runtime/Composer;->startReplaceGroup(I)V # open group
sget-object v1, Landroidx/compose/ui/Modifier;->Companion:Landroidx/compose/ui/Modifier$Companion; # base modifier
invoke-static {v1, v0}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxWidth(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; # width fraction
move-result-object v0 # filled modifier
const/4 v1, 0x2 # 2 dp
int-to-float v1, v1 # to float
invoke-static {v1}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F # box as Dp
move-result v1 # dp value
invoke-static {v0, v1}, Landroidx/compose/foundation/layout/SizeKt;->height-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; # 2dp tall
move-result-object v0 # sized modifier
const v1, -0x1 # 0xFFFFFFFF white
invoke-static {v1}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color
move-result-wide v1 # color long
invoke-static {}, Landroidx/compose/ui/graphics/RectangleShapeKt;->getRectangleShape()Landroidx/compose/ui/graphics/Shape; # rect shape
move-result-object v3 # shape
invoke-static {v0, v1, v2, v3}, Landroidx/compose/foundation/BackgroundKt;->background-bw27NRU(Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier; # white bg
move-result-object v0 # final modifier
const/4 v1, 0x0 # composer $changed
invoke-static {v0, p0, v1}, Landroidx/compose/foundation/layout/SpacerKt;->Spacer(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V # draw line
invoke-interface {p0}, Landroidx/compose/runtime/Composer;->endReplaceGroup()V # close group
return-void
.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
@@ -63,6 +63,8 @@
.method public static onWampTrack(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;Lcom/aspiro/wamp/model/Track;)V
.locals 11
invoke-static {p1}, Lradiant/HomeBackdrop;->onTrack(Lcom/aspiro/wamp/model/Track;)V
const/4 v3, 0x0
sput-boolean v3, Lradiant/RLAPILyricsHook;->isRlState:Z
@@ -344,7 +344,7 @@
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v0, "&platform=Radiant%20Lyrics"
const-string v0, "&platform=rl-mobile"
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
@@ -0,0 +1,60 @@
.class public final Lradiant/SeekerConsumer;
.super Ljava/lang/Object;
.implements Lio/reactivex/functions/Consumer;
# 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 accept(Ljava/lang/Object;)V
.locals 3
if-eqz p1, :ret # null guard
instance-of v0, p1, Ljh/b; # type guard
if-eqz v0, :ret # skip if wrong type
check-cast p1, Ljh/b; # narrow type
iget v0, p1, Ljh/b;->b:F # raw progress
invoke-static {v0}, Ljava/lang/Float;->isNaN(F)Z # nan check
move-result v1 # nan flag
if-eqz v1, :not_nan # skip clamp if finite
const/4 v0, 0x0 # default to zero
:not_nan
const/4 v1, 0x0 # lower bound
invoke-static {v0, v1}, Ljava/lang/Math;->max(FF)F # clamp low
move-result v0 # clamped low
const/high16 v2, 0x3f800000 # 1.0f upper bound
invoke-static {v0, v2}, Ljava/lang/Math;->min(FF)F # clamp high
move-result v0 # final value
sget-object v1, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # shared state
if-eqz v1, :ret # safety
invoke-interface {v1, v0}, Landroidx/compose/runtime/MutableFloatState;->setFloatValue(F)V # publish
:ret
return-void
.end method
+43
View File
@@ -0,0 +1,43 @@
--- a/com/tidal/android/feature/home/ui/k.smali
+++ b/com/tidal/android/feature/home/ui/k.smali
@@ -424,7 +424,7 @@
const/4 v6, 0x0
.line 181
- move-wide v7, v8
+ const-wide/16 v7, 0x0 # transparent containerColor
.line 182
const-wide/16 v9, 0x0
--- a/com/tidal/android/feature/home/ui/p.smali
+++ b/com/tidal/android/feature/home/ui/p.smali
@@ -187,6 +187,10 @@
.line 61
.line 62
:cond_3
+ const/4 v0, 0x0 # render $changed flag
+
+ invoke-static {v4, v0}, Lradiant/HomeBackdrop;->render(Landroidx/compose/runtime/Composer;I)V # render album-art backdrop
+
invoke-interface {p1}, Landroidx/compose/foundation/layout/PaddingValues;->calculateTopPadding-D9Ej5fM()F
.line 63
--- a/com/tidal/android/feature/home/ui/composables/e.smali
+++ b/com/tidal/android/feature/home/ui/composables/e.smali
@@ -790,6 +790,8 @@
.line 104
move-result-wide v14
+ const-wide/16 v14, 0x0 # transparent topBar background
+
.line 105
const/16 v17, 0x2
@@ -850,6 +852,8 @@
.line 134
move-result-wide v14
+ const-wide/16 v14, 0x0 # transparent TopAppBar containerColor
+
.line 135
sget-object v3, Landroidx/compose/ui/graphics/Color;->Companion:Landroidx/compose/ui/graphics/Color$Companion;
+2 -2
View File
@@ -30,13 +30,13 @@
- const/4 v2, 0x0
-
- invoke-static {v13, v7, v2, v4}, Lcom/tidal/android/feature/playerscreen/ui/composables/x4;->a(ILandroidx/compose/runtime/Composer;Landroidx/compose/ui/Modifier;Lyl0/a;)V
+ new-instance v4, Lcom/aspiro/wamp/tidalconnect/playback/i; # connect click lambda factory
+ new-instance v4, Landroidx/navigation/fragment/k; # connect click lambda factory
+
+ move-object/from16 v6, p5 # action dispatcher
+
+ const/4 v8, 0x1 # connect-clicked disc
+
+ invoke-direct {v4, v6, v8}, Lcom/aspiro/wamp/tidalconnect/playback/i;-><init>(Ljava/lang/Object;I)V # build lambda
+ invoke-direct {v4, v6, v8}, Landroidx/navigation/fragment/k;-><init>(Ljava/lang/Object;I)V # build lambda
+
+ invoke-interface {v7, v4}, Landroidx/compose/runtime/Composer;->updateRememberedValue(Ljava/lang/Object;)V # cache lambda
+
+224
View File
@@ -0,0 +1,224 @@
{
"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",
"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",
"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" }
},
{
"type": "toggle",
"key": "cover_everywhere",
"title": "Cover Everywhere - WIP",
"description": "Applies the blurred backdrop to every Compose based page (Home & Collection)",
"default": false,
"inline": true,
"fileNames": ["home-backdrop.patch", "collection-backdrop.patch", "cover-capture.patch"]
}
]
},
{
"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": "Adds swipe up gesture to the mini-player to reopen the player",
"default": true,
"fileNames": ["mini-player-gestures.patch"],
"extensionFiles": [
"radiant/MiniPlayerGestures.smali",
"radiant/MiniPlayerGestures$Gesture.smali",
"radiant/MiniPlayerGestures$FeedbackLayer.smali",
"radiant/MiniPlayerGestures$FeedbackResetAnimator.smali",
"radiant/MiniPlayerGestures$RootGesture.smali",
"radiant/MiniPlayerGestures$ApplyPending.smali"
]
},
{
"type": "choice",
"key": "swipe_up_drag",
"title": "Swipe Up Style",
"entries": ["Static", "Drag"],
"values": ["0x0", "0x1"],
"defaultIndex": 1,
"requiresOption": "gestures",
"token": "RL_MINI_PLAYER_SWIPE_UP_DRAG"
},
{
"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
}
]
}
+80
View File
@@ -0,0 +1,80 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/a.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/a.smali
@@ -116,7 +116,7 @@
.line 46
.line 47
- const/16 v0, 0x24
+ const/16 v0, 0x0 # zero pill corner radius
.line 48
.line 49
@@ -162,7 +162,7 @@
.line 68
.line 69
- const/16 v0, 0x8
+ const/16 v0, 0x0 # zero bottom padding
.line 70
.line 71
--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali
@@ -2756,12 +2756,7 @@
invoke-static {v0, v8, v11, v12}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxWidth$default(Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier;
move-result-object v0
-
- .line 140
- invoke-static {v0}, Landroidx/compose/foundation/layout/WindowInsetsPadding_androidKt;->navigationBarsPadding(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
-
- move-result-object v0
-
+ # flush bottom; skip nav inset
.line 141
invoke-interface/range {v17 .. v17}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
@@ -2773,6 +2768,8 @@
move-result v10
+ const/4 v10, 0x0 # zero horizontal padding
+
const/4 v11, 0x2
.line 142
@@ -2816,6 +2813,12 @@
move-result-wide v10
+ const v8, -0x1000000 # 0xFF000000 opaque black
+
+ invoke-static {v8}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack to color long
+
+ move-result-wide v10 # override theme color
+
.line 147
sget-object v8, Lcom/tidal/android/feature/appscaffold/ui/composable/a;->c:Landroidx/compose/foundation/shape/RoundedCornerShape;
@@ -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
+ const/4 v8, 0x0 # composer $changed flags
+
+ invoke-static {v15, v8}, Lradiant/MiniSeekerLine;->render(Landroidx/compose/runtime/Composer;I)V # draw top-edge seeker
+
.line 172
sget-object v0, Landroidx/compose/foundation/layout/ColumnScopeInstance;->INSTANCE:Landroidx/compose/foundation/layout/ColumnScopeInstance;
+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
+15
View File
@@ -0,0 +1,15 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali
@@ -2829,6 +2829,12 @@
move-result-object v0
+ sget-object v8, Lradiant/MiniSeekerFloating;->INSTANCE:Lradiant/MiniSeekerFloating; # draw lambda
+
+ invoke-static {v0, v8}, Landroidx/compose/ui/draw/DrawModifierKt;->drawWithContent(Landroidx/compose/ui/Modifier;Lyl0/l;)Landroidx/compose/ui/Modifier; # overlay perimeter stroke
+
+ move-result-object v0 # updated modifier
+
.line 150
sget-object v8, Lkotlin/u;->a:Lkotlin/u;
@@ -0,0 +1,104 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali
@@ -1768,3 +1768,7 @@
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
@@ -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
+112
View File
@@ -0,0 +1,112 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali
@@ -2427,6 +2427,12 @@
move-object/from16 v16, v9
.line 99
+ move-object/from16 v9, v25 # SheetState
+
+ 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
@@ -2832,2 +2838,8 @@
+ move-object/from16 v8, v25 # SheetState from AppScaffold
+
+ const/4 v10, __RL_MINI_PLAYER_SWIPE_UP_DRAG__ # Enable Drag option
+
+ invoke-static {v6, v8, v10}, Lradiant/MiniPlayerGestures;->configure(Lyl0/l;Landroidx/compose/material3/SheetState;Z)V
+
.line 150
sget-object v8, Lkotlin/u;->a:Lkotlin/u;
--- 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;
@@ -1750,2 +1766,6 @@
+ invoke-static {v6}, Lradiant/MiniPlayerGestures;->trackAreaModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # attach swipe up listener
+
+ move-result-object v6
+
.line 38
sget-object v21, Landroidx/compose/ui/Alignment;->Companion:Landroidx/compose/ui/Alignment$Companion;
@@ -1923,2 +1943,6 @@
+ invoke-static {v4}, Lradiant/MiniPlayerGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move cover (swipe up feedback)
+
+ move-result-object v4
+
.line 63
invoke-virtual/range {v22 .. v22}, Lcom/squareup/ui/market/core/theme/MarketStylesheet$c;->p0()Lc20/a;
@@ -2034,2 +2058,6 @@
+ invoke-static {v4}, Lradiant/MiniPlayerGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist (swipe up feedback)
+
+ move-result-object v4
+
.line 72
invoke-virtual/range {p2 .. p2}, Landroidx/compose/foundation/layout/Arrangement;->getTop()Landroidx/compose/foundation/layout/Arrangement$Vertical;
--- 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
+67
View File
@@ -0,0 +1,67 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/a.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/a.smali
@@ -116,7 +116,7 @@
.line 46
.line 47
- const/16 v0, 0x24
+ const/16 v0, 0x0 # zero pill corner radius
.line 48
.line 49
@@ -162,7 +162,7 @@
.line 68
.line 69
- const/16 v0, 0x8
+ const/16 v0, 0x0 # zero bottom padding
.line 70
.line 71
--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali
@@ -2756,12 +2756,7 @@
invoke-static {v0, v8, v11, v12}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxWidth$default(Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier;
move-result-object v0
-
- .line 140
- invoke-static {v0}, Landroidx/compose/foundation/layout/WindowInsetsPadding_androidKt;->navigationBarsPadding(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
-
- move-result-object v0
-
+ # flush bottom; skip nav inset
.line 141
invoke-interface/range {v17 .. v17}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
@@ -2773,6 +2768,8 @@
move-result v10
+ const/4 v10, 0x0 # zero horizontal padding
+
const/4 v11, 0x2
.line 142
@@ -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
+ const/4 v8, 0x0 # composer $changed flags
+
+ invoke-static {v15, v8}, Lradiant/MiniSeekerLine;->render(Landroidx/compose/runtime/Composer;I)V # draw top-edge seeker
+
.line 172
sget-object v0, Landroidx/compose/foundation/layout/ColumnScopeInstance;->INSTANCE:Landroidx/compose/foundation/layout/ColumnScopeInstance;
+20 -5
View File
@@ -1,7 +1,7 @@
# rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 71
--- a/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
@@ -29,9 +29,9 @@
+
+ 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
+
@@ -39,6 +39,21 @@
+
+ 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
+
+ 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
+
+ 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
+
@@ -111,7 +126,7 @@
+
+ move-result-object v3 # fullscreen modifier
+
+ const v6, -0x80000000 # 50% black ARGB
+ const v6, __RL_SCRIM_ARGB__ # scrim ARGB (Advanced Options: Backdrop Dimming)
+
+ invoke-static {v6}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color long
+
+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