mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-06-17 21:13:11 +10:00
Merge pull request #18 from meowarex/dev
Custom Patch Selection & UI Tweaks
This commit is contained in:
@@ -35,8 +35,15 @@ class PathManager(
|
|||||||
val patchedApk = patchingWorkingDir.resolve("patched.apk")
|
val patchedApk = patchingWorkingDir.resolve("patched.apk")
|
||||||
|
|
||||||
fun clearCache() {
|
fun clearCache() {
|
||||||
for (dir in arrayOf(patchingDir, cacheDownloadDir, context.cacheDir))
|
val targets = arrayOf(
|
||||||
dir.deleteRecursively()
|
patchingDownloadDir,
|
||||||
|
patchingWorkingDir,
|
||||||
|
cacheDownloadDir,
|
||||||
|
context.cacheDir,
|
||||||
|
)
|
||||||
|
for (dir in targets) {
|
||||||
|
if (dir.exists()) dir.deleteRecursively()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun cachedTidalApk(version: Int, split: String = "base"): File = patchingDownloadDir
|
fun cachedTidalApk(version: Int, split: String = "base"): File = patchingDownloadDir
|
||||||
|
|||||||
+11
-1
@@ -8,6 +8,8 @@ import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
|||||||
class CommitsPagingSource(
|
class CommitsPagingSource(
|
||||||
private val github: RadiantLyricsGithubService,
|
private val github: RadiantLyricsGithubService,
|
||||||
) : PagingSource<Int, GithubCommit>() {
|
) : PagingSource<Int, GithubCommit>() {
|
||||||
|
private val seenShas = mutableSetOf<String>()
|
||||||
|
private val seenTitles = mutableSetOf<String>()
|
||||||
|
|
||||||
override fun getRefreshKey(state: PagingState<Int, GithubCommit>): Int? =
|
override fun getRefreshKey(state: PagingState<Int, GithubCommit>): Int? =
|
||||||
state.anchorPosition?.let {
|
state.anchorPosition?.let {
|
||||||
@@ -19,7 +21,15 @@ class CommitsPagingSource(
|
|||||||
val page = params.key ?: 0
|
val page = params.key ?: 0
|
||||||
return when (val r = github.getCommits(page)) {
|
return when (val r = github.getCommits(page)) {
|
||||||
is ApiResponse.Success -> LoadResult.Page(
|
is ApiResponse.Success -> LoadResult.Page(
|
||||||
data = r.data,
|
data = r.data.filter { commit ->
|
||||||
|
val title = commit.commit.message.lineSequence().first().trim()
|
||||||
|
when {
|
||||||
|
title.startsWith("Merge ") -> false
|
||||||
|
!seenShas.add(commit.sha) -> false
|
||||||
|
!seenTitles.add(title) -> false
|
||||||
|
else -> true
|
||||||
|
}
|
||||||
|
},
|
||||||
prevKey = if (page > 0) page - 1 else null,
|
prevKey = if (page > 0) page - 1 else null,
|
||||||
nextKey = if (r.data.isNotEmpty()) page + 1 else null,
|
nextKey = if (r.data.isNotEmpty()) page + 1 else null,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TidalPatchRunner(
|
|||||||
CopyDependenciesStep(),
|
CopyDependenciesStep(),
|
||||||
|
|
||||||
// Patch
|
// Patch
|
||||||
SmaliPatchStep(),
|
SmaliPatchStep(options),
|
||||||
ReorganizeDexStep(),
|
ReorganizeDexStep(),
|
||||||
PatchManifestStep(options),
|
PatchManifestStep(options),
|
||||||
PatchCertsStep(),
|
PatchCertsStep(),
|
||||||
|
|||||||
+149
-20
@@ -8,6 +8,7 @@ import com.meowarex.rlmobile.patcher.steps.base.IDexProvider
|
|||||||
import com.meowarex.rlmobile.patcher.steps.base.Step
|
import com.meowarex.rlmobile.patcher.steps.base.Step
|
||||||
import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
|
import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
|
||||||
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
|
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
|
||||||
|
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||||
import com.android.tools.smali.baksmali.Baksmali
|
import com.android.tools.smali.baksmali.Baksmali
|
||||||
import com.android.tools.smali.baksmali.BaksmaliOptions
|
import com.android.tools.smali.baksmali.BaksmaliOptions
|
||||||
import com.android.tools.smali.dexlib2.Opcodes
|
import com.android.tools.smali.dexlib2.Opcodes
|
||||||
@@ -22,7 +23,9 @@ import org.koin.core.component.KoinComponent
|
|||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.*
|
import java.io.*
|
||||||
|
|
||||||
class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
class SmaliPatchStep(
|
||||||
|
private val options: PatchOptions,
|
||||||
|
) : Step(), IDexProvider, KoinComponent {
|
||||||
private val paths: PathManager by inject()
|
private val paths: PathManager by inject()
|
||||||
|
|
||||||
override val group = StepGroup.Patch
|
override val group = StepGroup.Patch
|
||||||
@@ -37,17 +40,13 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
val patchesZip = container.getStep<DownloadPatchesStep>().getStoredFile(container)
|
val patchesZip = container.getStep<DownloadPatchesStep>().getStoredFile(container)
|
||||||
|
|
||||||
val patches = mutableListOf<LoadedPatch>()
|
val patches = mutableListOf<LoadedPatch>()
|
||||||
|
val localsBumps = mutableMapOf<Pair<String, String>, Int>()
|
||||||
|
|
||||||
// Load and parse all the patches from the smali patch archive.
|
// Load and parse all the patches from the smali archive.
|
||||||
// Extension classes (extension/**/*.smali) are extracted into smaliDir
|
|
||||||
// so they get assembled into the new dex alongside patched classes.
|
|
||||||
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
||||||
smaliDir.mkdirs()
|
smaliDir.mkdirs()
|
||||||
ZipReader(patchesZip).use { zip ->
|
ZipReader(patchesZip).use { zip ->
|
||||||
// Iterate in filename order so patches apply deterministically, matching
|
// Iterate in filename order so patches apply deterministically
|
||||||
// the apply-order contract in patches/README. Zip iteration order would
|
|
||||||
// otherwise depend on archive layout and could break ordered patches that
|
|
||||||
// share a target file.
|
|
||||||
for (patchFile in zip.entryNames.sorted()) {
|
for (patchFile in zip.entryNames.sorted()) {
|
||||||
container.log("Parsing patch file $patchFile")
|
container.log("Parsing patch file $patchFile")
|
||||||
if (patchFile.endsWith("/")) continue
|
if (patchFile.endsWith("/")) continue
|
||||||
@@ -71,13 +70,28 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
|
|
||||||
if (!patchFile.endsWith(".patch")) continue
|
if (!patchFile.endsWith(".patch")) continue
|
||||||
|
|
||||||
|
val basename = patchFile.substringAfterLast('/')
|
||||||
|
if (basename in options.disabledPatches) {
|
||||||
|
container.log("Skipping disabled patch $patchFile")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
val lines = zip.openEntry(patchFile)!!.read()
|
val lines = zip.openEntry(patchFile)!!.read()
|
||||||
.decodeToString()
|
.decodeToString()
|
||||||
.replace("\r\n", "\n") // Replace CRLF endings with LF endings to be sure here
|
.replace("\r\n", "\n") // Replace CRLF endings with LF
|
||||||
.trimEnd { it == '\n' } // Remove trailing new lines to work with diff output properly
|
.trimEnd { it == '\n' } // Remove trailing new lines
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
for (directive in lines) {
|
||||||
|
val match = LOCALS_DIRECTIVE.matchEntire(directive) ?: continue
|
||||||
|
val (smaliPath, methodSubstring, value) = match.destructured
|
||||||
|
val key = smaliPath.removeSuffix(".smali") to methodSubstring
|
||||||
|
val newValue = value.toInt()
|
||||||
|
localsBumps[key] = maxOf(localsBumps[key] ?: 0, newValue)
|
||||||
|
container.log("Recorded rl-locals bump: $smaliPath method≈\"$methodSubstring\" >= $newValue")
|
||||||
|
}
|
||||||
|
|
||||||
val targetLine = lines.firstOrNull { it.startsWith("--- a/") }
|
val targetLine = lines.firstOrNull { it.startsWith("--- a/") }
|
||||||
?: throw Error("Patch $patchFile is missing a '--- a/...' header")
|
?: throw Error("Patch $patchFile is missing a '--- a/...' header")
|
||||||
val fullClassName = targetLine
|
val fullClassName = targetLine
|
||||||
@@ -96,7 +110,7 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Disassemble all the classes we have patches for from all the dex files
|
// Disassemble all the classes
|
||||||
container.log("Disassembling target classes in APK")
|
container.log("Disassembling target classes in APK")
|
||||||
ZipReader(apk).use { zip ->
|
ZipReader(apk).use { zip ->
|
||||||
for (file in zip.entryNames) {
|
for (file in zip.entryNames) {
|
||||||
@@ -117,11 +131,10 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
/* dexFile = */ dexFile,
|
/* dexFile = */ dexFile,
|
||||||
/* outputDir = */ smaliDir,
|
/* outputDir = */ smaliDir,
|
||||||
/* jobs = */ coreCount - 1,
|
/* jobs = */ coreCount - 1,
|
||||||
/* options = */ BaksmaliOptions().apply {
|
/* options = */
|
||||||
|
BaksmaliOptions().apply {
|
||||||
localsDirective = true
|
localsDirective = true
|
||||||
// Match apktool's label naming (:cond_0, :cond_1, ...) so patches
|
// Match apktool label naming
|
||||||
// authored from `apktool d` decompilation apply cleanly. Default
|
|
||||||
// would emit offset-based labels like :cond_8de.
|
|
||||||
sequentialLabels = true
|
sequentialLabels = true
|
||||||
},
|
},
|
||||||
/* classes = */ patches.map { "L${it.fullClassName};" },
|
/* classes = */ patches.map { "L${it.fullClassName};" },
|
||||||
@@ -135,7 +148,32 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply all the patches to the smali files
|
if (localsBumps.isNotEmpty()) {
|
||||||
|
container.log("Applying ${localsBumps.size} rl-locals bump(s)")
|
||||||
|
for ((key, minLocals) in localsBumps) {
|
||||||
|
val (classPath, methodSubstring) = key
|
||||||
|
val smaliFile = smaliDir.resolve("$classPath.smali")
|
||||||
|
if (!smaliFile.exists()) {
|
||||||
|
throw FileNotFoundException("rl-locals target $classPath.smali not found")
|
||||||
|
}
|
||||||
|
val applied = bumpLocals(
|
||||||
|
lines = smaliFile.readLines(),
|
||||||
|
methodSubstring = methodSubstring,
|
||||||
|
minLocals = minLocals,
|
||||||
|
) ?: throw Error("rl-locals: no .locals line found in $classPath for method≈\"$methodSubstring\"")
|
||||||
|
|
||||||
|
if (applied.changed) {
|
||||||
|
smaliFile.bufferedWriter().use { writer ->
|
||||||
|
applied.lines.forEach(writer::appendLine)
|
||||||
|
}
|
||||||
|
container.log("Bumped .locals in $classPath (method≈\"$methodSubstring\") ${applied.previous} -> $minLocals")
|
||||||
|
} else {
|
||||||
|
container.log("Skipped .locals bump in $classPath (method≈\"$methodSubstring\"): already ${applied.previous} >= $minLocals")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply all the patches to smali files
|
||||||
container.log("Applying smali patches to disassembled files")
|
container.log("Applying smali patches to disassembled files")
|
||||||
for ((fullClassName, patch) in patches) {
|
for ((fullClassName, patch) in patches) {
|
||||||
container.log("Applying patch to class $fullClassName")
|
container.log("Applying patch to class $fullClassName")
|
||||||
@@ -145,10 +183,16 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!")
|
throw FileNotFoundException("Target smali file $fullClassName.smali not found for patching!")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val source = smaliFile.readLines()
|
||||||
val patched = try {
|
val patched = try {
|
||||||
DiffUtils.patch(smaliFile.readLines(), patch)
|
DiffUtils.patch(source, patch)
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
throw Error("Failed to smali patch $fullClassName", t)
|
container.log("Strict apply failed for $fullClassName, retrying with fuzzy context match: ${t.message}")
|
||||||
|
try {
|
||||||
|
applyPatchFuzzy(source, patch)
|
||||||
|
} catch (fuzzy: Throwable) {
|
||||||
|
throw Error("Failed to smali patch $fullClassName", fuzzy)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
smaliFile.bufferedWriter().use { writer ->
|
smaliFile.bufferedWriter().use { writer ->
|
||||||
@@ -156,11 +200,11 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Assemble the patched classes back into a single dex
|
// Assemble patched dex
|
||||||
container.log("Reassembling patches smali classes into new dex")
|
container.log("Reassembling patches smali classes into new dex")
|
||||||
smaliDir.mkdir()
|
smaliDir.mkdir()
|
||||||
|
|
||||||
// Capture stdout/stderr while assembling smali
|
// Capture stdout/stderr assembling smali
|
||||||
val originalStdout = System.out
|
val originalStdout = System.out
|
||||||
val originalStderr = System.err
|
val originalStderr = System.err
|
||||||
val captured = ByteArrayOutputStream()
|
val captured = ByteArrayOutputStream()
|
||||||
@@ -185,6 +229,91 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
|
|||||||
override val dexPriority = 2
|
override val dexPriority = 2
|
||||||
override val dexCount = 1
|
override val dexCount = 1
|
||||||
override fun getDexFiles(container: StepRunner) = listOf(outDex.readBytes())
|
override fun getDexFiles(container: StepRunner) = listOf(outDex.readBytes())
|
||||||
|
|
||||||
|
private fun bumpLocals(
|
||||||
|
lines: List<String>,
|
||||||
|
methodSubstring: String,
|
||||||
|
minLocals: Int,
|
||||||
|
): BumpResult? {
|
||||||
|
val result = lines.toMutableList()
|
||||||
|
var inTargetMethod = false
|
||||||
|
for (i in result.indices) {
|
||||||
|
val raw = result[i]
|
||||||
|
val trimmed = raw.trimStart()
|
||||||
|
|
||||||
|
if (trimmed.startsWith(".method ")) {
|
||||||
|
inTargetMethod = methodSubstring in trimmed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!inTargetMethod) continue
|
||||||
|
|
||||||
|
if (trimmed.startsWith(".locals ")) {
|
||||||
|
val current = trimmed.removePrefix(".locals ").substringBefore(' ').toInt()
|
||||||
|
if (current >= minLocals) return BumpResult(lines, current, changed = false)
|
||||||
|
val indent = raw.substring(0, raw.length - trimmed.length)
|
||||||
|
result[i] = "$indent.locals $minLocals # rl-locals bump (was $current)"
|
||||||
|
return BumpResult(result, current, changed = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith(".end method")) return null
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class BumpResult(val lines: List<String>, val previous: Int, val changed: Boolean)
|
||||||
|
|
||||||
|
private fun applyPatchFuzzy(target: List<String>, patch: Patch<String>): List<String> {
|
||||||
|
val result = target.toMutableList()
|
||||||
|
val deltas = patch.deltas.sortedByDescending { it.source.position }
|
||||||
|
|
||||||
|
for (delta in deltas) {
|
||||||
|
val sourceLines = delta.source.lines
|
||||||
|
val matchPos = findContextMatch(result, sourceLines, delta.source.position)
|
||||||
|
?: throw Exception(
|
||||||
|
"Fuzzy match failed: could not locate context for hunk near line " +
|
||||||
|
"${delta.source.position + 1} (${sourceLines.size} lines)"
|
||||||
|
)
|
||||||
|
|
||||||
|
repeat(sourceLines.size) { result.removeAt(matchPos) }
|
||||||
|
result.addAll(matchPos, delta.target.lines)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findContextMatch(target: List<String>, sourceLines: List<String>, hint: Int): Int? {
|
||||||
|
if (sourceLines.isEmpty()) return hint.coerceIn(0, target.size)
|
||||||
|
|
||||||
|
// Try at the exact hint first.
|
||||||
|
if (matchesAt(target, sourceLines, hint)) return hint
|
||||||
|
|
||||||
|
// Walk outward from the hint, alternating below/above, until we find a unique match.
|
||||||
|
val maxRadius = target.size
|
||||||
|
for (offset in 1..maxRadius) {
|
||||||
|
val below = hint + offset
|
||||||
|
if (below + sourceLines.size <= target.size && matchesAt(target, sourceLines, below)) {
|
||||||
|
return below
|
||||||
|
}
|
||||||
|
val above = hint - offset
|
||||||
|
if (above >= 0 && matchesAt(target, sourceLines, above)) {
|
||||||
|
return above
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun matchesAt(target: List<String>, sourceLines: List<String>, pos: Int): Boolean {
|
||||||
|
if (pos < 0 || pos + sourceLines.size > target.size) return false
|
||||||
|
for ((i, line) in sourceLines.withIndex()) {
|
||||||
|
if (target[pos + i] != line) return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class LoadedPatch(
|
private data class LoadedPatch(
|
||||||
|
|||||||
+3
@@ -34,6 +34,9 @@ private fun PatchOptionsScreenPreview(
|
|||||||
onSelectCustomInjector = {},
|
onSelectCustomInjector = {},
|
||||||
customPatches = parameters.customPatches,
|
customPatches = parameters.customPatches,
|
||||||
onSelectCustomPatches = {},
|
onSelectCustomPatches = {},
|
||||||
|
enabledPatchCount = KnownPatch.All.size,
|
||||||
|
isPatchEnabled = { true },
|
||||||
|
onTogglePatch = { _, _ -> },
|
||||||
isConfigValid = parameters.isConfigValid,
|
isConfigValid = parameters.isConfigValid,
|
||||||
onInstall = {},
|
onInstall = {},
|
||||||
)
|
)
|
||||||
|
|||||||
+2
-2
@@ -87,9 +87,9 @@ class PatchingScreenModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCache() = screenModelScope.launchIO {
|
fun clearCache() {
|
||||||
paths.clearCache()
|
paths.clearCache()
|
||||||
mainThread { application.showToast(R.string.action_cleared_cache) }
|
application.showToast(R.string.action_cleared_cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentInstallId(): String? = installId
|
fun getCurrentInstallId(): String? = installId
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.meowarex.rlmobile.ui.screens.patchopts
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import com.meowarex.rlmobile.R
|
||||||
|
|
||||||
|
enum class KnownPatch(
|
||||||
|
val fileNames: List<String>,
|
||||||
|
@StringRes val titleRes: Int,
|
||||||
|
@StringRes val descRes: Int,
|
||||||
|
val requires: List<KnownPatch> = emptyList(),
|
||||||
|
) {
|
||||||
|
LyricsDisableCover(
|
||||||
|
fileNames = listOf("lyrics-disable-cover.patch"),
|
||||||
|
titleRes = R.string.patch_lyrics_disable_cover_title,
|
||||||
|
descRes = R.string.patch_lyrics_disable_cover_desc,
|
||||||
|
),
|
||||||
|
LyricsProgressPill(
|
||||||
|
fileNames = listOf(
|
||||||
|
"lyrics-progress-pill.patch",
|
||||||
|
"lyrics-fade-region.patch",
|
||||||
|
"lyrics-active-line-only.patch",
|
||||||
|
),
|
||||||
|
titleRes = R.string.patch_lyrics_progress_pill_title,
|
||||||
|
descRes = R.string.patch_lyrics_progress_pill_desc,
|
||||||
|
requires = listOf(LyricsDisableCover),
|
||||||
|
),
|
||||||
|
LyricsReplaceLyricButton(
|
||||||
|
fileNames = listOf(
|
||||||
|
"lyrics-replace-lyric-button-1-remove.patch",
|
||||||
|
"lyrics-replace-lyric-button-2-sparkle.patch",
|
||||||
|
),
|
||||||
|
titleRes = R.string.patch_lyrics_replace_button_title,
|
||||||
|
descRes = R.string.patch_lyrics_replace_button_desc,
|
||||||
|
),
|
||||||
|
PlayerBackdrop(
|
||||||
|
fileNames = listOf("player-backdrop.patch"),
|
||||||
|
titleRes = R.string.patch_player_backdrop_title,
|
||||||
|
descRes = R.string.patch_player_backdrop_desc,
|
||||||
|
);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val All: List<KnownPatch> = entries.sortedBy { it.fileNames.first() }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,6 +34,8 @@ data class PatchOptions(
|
|||||||
* A custom smali patches bundle that was used rather than the latest.
|
* A custom smali patches bundle that was used rather than the latest.
|
||||||
*/
|
*/
|
||||||
val customPatches: PatchComponent? = null,
|
val customPatches: PatchComponent? = null,
|
||||||
|
|
||||||
|
val disabledPatches: Set<String> = emptySet(),
|
||||||
) : Parcelable {
|
) : Parcelable {
|
||||||
companion object {
|
companion object {
|
||||||
val Default = PatchOptions(
|
val Default = PatchOptions(
|
||||||
@@ -42,6 +44,7 @@ data class PatchOptions(
|
|||||||
debuggable = false,
|
debuggable = false,
|
||||||
customInjector = null,
|
customInjector = null,
|
||||||
customPatches = null,
|
customPatches = null,
|
||||||
|
disabledPatches = emptySet(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+40
@@ -50,6 +50,45 @@ class PatchOptionsModel(
|
|||||||
debuggable = value
|
debuggable = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- Patch selection state ----------
|
||||||
|
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches)
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun isPatchEnabled(patch: KnownPatch): Boolean =
|
||||||
|
patch.fileNames.none { it in disabledPatches }
|
||||||
|
|
||||||
|
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
|
||||||
|
val units = if (enabled) {
|
||||||
|
buildSet {
|
||||||
|
fun addWithDeps(p: KnownPatch) {
|
||||||
|
if (add(p)) p.requires.forEach(::addWithDeps)
|
||||||
|
}
|
||||||
|
addWithDeps(patch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildSet {
|
||||||
|
fun addWithDependents(p: KnownPatch) {
|
||||||
|
if (add(p)) {
|
||||||
|
KnownPatch.All
|
||||||
|
.filter { p in it.requires }
|
||||||
|
.forEach(::addWithDependents)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
addWithDependents(patch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val affectedFiles = units.flatMap { it.fileNames }.toSet()
|
||||||
|
disabledPatches = if (enabled) {
|
||||||
|
disabledPatches - affectedFiles
|
||||||
|
} else {
|
||||||
|
disabledPatches + affectedFiles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val enabledPatchCount: Int
|
||||||
|
get() = KnownPatch.All.count { isPatchEnabled(it) }
|
||||||
|
|
||||||
// ---------- Custom components state ----------
|
// ---------- Custom components state ----------
|
||||||
var customInjector by mutableStateOf<PatchComponent?>(null)
|
var customInjector by mutableStateOf<PatchComponent?>(null)
|
||||||
private set
|
private set
|
||||||
@@ -93,6 +132,7 @@ class PatchOptionsModel(
|
|||||||
debuggable = debuggable,
|
debuggable = debuggable,
|
||||||
customInjector = customInjector,
|
customInjector = customInjector,
|
||||||
customPatches = customPatches,
|
customPatches = customPatches,
|
||||||
|
disabledPatches = disabledPatches,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
@@ -21,6 +21,7 @@ import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
|||||||
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen
|
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameStateLabel
|
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameStateLabel
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchOptionsAppBar
|
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchOptionsAppBar
|
||||||
|
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchSelectionAccordion
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.components.options.*
|
import com.meowarex.rlmobile.ui.screens.patchopts.components.options.*
|
||||||
import com.meowarex.rlmobile.ui.util.spacedByLastAtBottom
|
import com.meowarex.rlmobile.ui.util.spacedByLastAtBottom
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
@@ -59,6 +60,10 @@ class PatchOptionsScreen(
|
|||||||
onSelectCustomInjector = { model.selectCustomInjector(navigator) },
|
onSelectCustomInjector = { model.selectCustomInjector(navigator) },
|
||||||
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
|
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
|
||||||
|
|
||||||
|
enabledPatchCount = model.enabledPatchCount,
|
||||||
|
isPatchEnabled = model::isPatchEnabled,
|
||||||
|
onTogglePatch = model::setPatchEnabled,
|
||||||
|
|
||||||
isConfigValid = model.isConfigValid,
|
isConfigValid = model.isConfigValid,
|
||||||
onInstall = {
|
onInstall = {
|
||||||
navigator.push(PatchingScreen(model.generateConfig()))
|
navigator.push(PatchingScreen(model.generateConfig()))
|
||||||
@@ -88,6 +93,10 @@ fun PatchOptionsScreenContent(
|
|||||||
customPatches: PatchComponent?,
|
customPatches: PatchComponent?,
|
||||||
onSelectCustomPatches: () -> Unit,
|
onSelectCustomPatches: () -> Unit,
|
||||||
|
|
||||||
|
enabledPatchCount: Int,
|
||||||
|
isPatchEnabled: (KnownPatch) -> Boolean,
|
||||||
|
onTogglePatch: (KnownPatch, Boolean) -> Unit,
|
||||||
|
|
||||||
isConfigValid: Boolean,
|
isConfigValid: Boolean,
|
||||||
onInstall: () -> Unit,
|
onInstall: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -148,6 +157,14 @@ fun PatchOptionsScreenContent(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PatchSelectionAccordion(
|
||||||
|
enabledCount = enabledPatchCount,
|
||||||
|
totalCount = KnownPatch.All.size,
|
||||||
|
isEnabled = isPatchEnabled,
|
||||||
|
onToggle = onTogglePatch,
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
)
|
||||||
|
|
||||||
if (isDevMode) {
|
if (isDevMode) {
|
||||||
TextDivider(
|
TextDivider(
|
||||||
text = stringResource(R.string.patchopts_divider_advanced),
|
text = stringResource(R.string.patchopts_divider_advanced),
|
||||||
|
|||||||
+150
@@ -0,0 +1,150 @@
|
|||||||
|
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.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
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.draw.rotate
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PatchSelectionAccordion(
|
||||||
|
enabledCount: Int,
|
||||||
|
totalCount: Int,
|
||||||
|
isEnabled: (KnownPatch) -> Boolean,
|
||||||
|
onToggle: (KnownPatch, Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
var expanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
val rotation by animateFloatAsState(
|
||||||
|
targetValue = if (expanded) 180f else 0f,
|
||||||
|
label = "patch-accordion-arrow",
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerLow),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(role = Role.Button) { expanded = !expanded }
|
||||||
|
.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.patchopts_patches_title),
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(
|
||||||
|
R.string.patchopts_patches_summary,
|
||||||
|
enabledCount,
|
||||||
|
totalCount,
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier.alpha(.7f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.ic_arrow_down_small),
|
||||||
|
contentDescription = stringResource(
|
||||||
|
if (expanded) R.string.action_collapse else R.string.action_expand,
|
||||||
|
),
|
||||||
|
modifier = Modifier.rotate(rotation),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = expanded) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.background.copy(0.4f))
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.patchopts_patches_desc),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(.7f)
|
||||||
|
.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) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PatchCheckboxRow(
|
||||||
|
title: String,
|
||||||
|
description: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onCheckedChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember(::MutableInteractionSource)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
role = Role.Checkbox,
|
||||||
|
) { onCheckedChange(!checked) }
|
||||||
|
.padding(vertical = 4.dp),
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = onCheckedChange,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
)
|
||||||
|
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-4
@@ -64,14 +64,11 @@ class SettingsModel(
|
|||||||
if (value) UpdateCheckWorker.schedule(application) else UpdateCheckWorker.cancel(application)
|
if (value) UpdateCheckWorker.schedule(application) else UpdateCheckWorker.cancel(application)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearCache() = screenModelScope.launchIO {
|
fun clearCache() {
|
||||||
paths.clearCache()
|
paths.clearCache()
|
||||||
|
|
||||||
mainThread {
|
|
||||||
patchedApkExists = false
|
patchedApkExists = false
|
||||||
application.showToast(R.string.action_cleared_cache)
|
application.showToast(R.string.action_cleared_cache)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
fun copyInstallInfo() {
|
fun copyInstallInfo() {
|
||||||
application.copyToClipboard(installInfo)
|
application.copyToClipboard(installInfo)
|
||||||
|
|||||||
@@ -219,6 +219,18 @@
|
|||||||
<string name="patchopts_custom_patches_desc">A custom smali patch bundle that was imported by Manager.</string>
|
<string name="patchopts_custom_patches_desc">A custom smali patch bundle that was imported by Manager.</string>
|
||||||
<string name="patchopts_divider_basic">Basic</string>
|
<string name="patchopts_divider_basic">Basic</string>
|
||||||
<string name="patchopts_divider_advanced">Advanced</string>
|
<string name="patchopts_divider_advanced">Advanced</string>
|
||||||
|
<string name="patchopts_patches_title">Patches</string>
|
||||||
|
<string name="patchopts_patches_desc">Toggle which patches are applied during installation. Unchecked patches are skipped entirely.</string>
|
||||||
|
<string name="patchopts_patches_summary">%1$d of %2$d enabled</string>
|
||||||
|
|
||||||
|
<string name="patch_lyrics_disable_cover_title">Disable Lyrics Cover</string>
|
||||||
|
<string name="patch_lyrics_disable_cover_desc">Prevents the album cover from being hidden when lyrics are showing.</string>
|
||||||
|
<string name="patch_lyrics_progress_pill_title">Lyrics Progress Pill</string>
|
||||||
|
<string name="patch_lyrics_progress_pill_desc">Shows a progress pill while the upcoming lyric line loads in, with a soft fade above and below the lyrics list.</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_player_backdrop_title">Player Backdrop</string>
|
||||||
|
<string name="patch_player_backdrop_desc">Restores the legacy translucent backdrop blur behind the player.</string>
|
||||||
|
|
||||||
<string name="componentopts_screen_title">Custom Component (%s)</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_screen_desc">Select a custom build that was imported by Manager.</string>
|
||||||
|
|||||||
+1
@@ -1,3 +1,4 @@
|
|||||||
|
# rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 79
|
||||||
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
@@ -4931,7 +4931,11 @@
|
@@ -4931,7 +4931,11 @@
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
|
# rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 79
|
||||||
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
@@ -2966,7 +2966,7 @@
|
|
||||||
.end method
|
|
||||||
|
|
||||||
.method public static final e(ILandroidx/compose/runtime/Composer;Lcom/tidal/android/feature/playerscreen/ui/k;Lcom/tidal/android/feature/playerscreen/ui/r$a;Ltl0/a;Ltl0/l;Z)V
|
|
||||||
- .locals 71 # extra regs for blur backdrop
|
|
||||||
+ .locals 89 # extra regs for sparkle button
|
|
||||||
.annotation build Landroidx/compose/runtime/Composable;
|
|
||||||
.end annotation
|
|
||||||
|
|
||||||
@@ -5838,6 +5838,22 @@
|
@@ -5838,6 +5838,22 @@
|
||||||
:cond_51
|
:cond_51
|
||||||
check-cast v4, Ltl0/a;
|
check-cast v4, Ltl0/a;
|
||||||
@@ -1,14 +1,6 @@
|
|||||||
|
# rl-locals: com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali e( 71
|
||||||
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
@@ -2966,7 +2966,7 @@
|
|
||||||
.end method
|
|
||||||
|
|
||||||
.method public static final e(ILandroidx/compose/runtime/Composer;Lcom/tidal/android/feature/playerscreen/ui/k;Lcom/tidal/android/feature/playerscreen/ui/r$a;Ltl0/a;Ltl0/l;Z)V
|
|
||||||
- .locals 61
|
|
||||||
+ .locals 71 # extra regs for blur backdrop
|
|
||||||
.annotation build Landroidx/compose/runtime/Composable;
|
|
||||||
.end annotation
|
|
||||||
|
|
||||||
@@ -4164,6 +4164,133 @@
|
@@ -4164,6 +4164,133 @@
|
||||||
|
|
||||||
invoke-static {v5, v3, v4}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Ltl0/p;)V
|
invoke-static {v5, v3, v4}, Landroidx/compose/runtime/Updater;->set-impl(Landroidx/compose/runtime/Composer;Ljava/lang/Object;Ltl0/p;)V
|
||||||
|
|||||||
Reference in New Issue
Block a user