mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-26 22:17:44 +10:00
Offline Repatch + Force Incompatible (Dev Option)
This commit is contained in:
@@ -228,3 +228,13 @@ fun ProviderFactory.execIgnoreCode(vararg command: String): String = try {
|
|||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sync Local Cached Assets with Repo Releases to keep them up to date
|
||||||
|
val syncBundledManifest by tasks.registering(Copy::class) {
|
||||||
|
val src = rootProject.rootDir.resolve("../patches/manifest.json")
|
||||||
|
onlyIf { src.exists() }
|
||||||
|
from(src)
|
||||||
|
into(layout.projectDirectory.dir("src/main/assets"))
|
||||||
|
rename { "patches-manifest.json" }
|
||||||
|
}
|
||||||
|
tasks.named("preBuild") { dependsOn(syncBundledManifest) }
|
||||||
|
|||||||
@@ -0,0 +1,357 @@
|
|||||||
|
{
|
||||||
|
"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": "PlayerMovePlayingFrom",
|
||||||
|
"order": 37,
|
||||||
|
"fileNames": [
|
||||||
|
"player-move-playing-from.patch"
|
||||||
|
],
|
||||||
|
"extensionFiles": [
|
||||||
|
"radiant/PlayingFrom.smali"
|
||||||
|
],
|
||||||
|
"title": "Playing From",
|
||||||
|
"description": "Shows the source name (playlist/album/mix) above the track title",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "PlayerOneHanded",
|
||||||
|
"order": 38,
|
||||||
|
"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/s$c.smali"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "LyricsKeepControlsVisible",
|
||||||
|
"order": 60,
|
||||||
|
"fileNames": [
|
||||||
|
"lyrics-keep-controls-visible.patch"
|
||||||
|
],
|
||||||
|
"extensionFiles": [
|
||||||
|
"radiant/LyricsReinitGuard.smali"
|
||||||
|
],
|
||||||
|
"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. Also stops the open lyrics view from randomly closing when the player re-fetches the same track.",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "WazeIntegration",
|
||||||
|
"order": 90,
|
||||||
|
"category": "integration",
|
||||||
|
"pathLocked": true,
|
||||||
|
"fileNames": [
|
||||||
|
"integration-waze-playback.patch"
|
||||||
|
],
|
||||||
|
"extensionFiles": [
|
||||||
|
"radiant/WazeInitReceiver.smali",
|
||||||
|
"radiant/WazeServiceConnection.smali"
|
||||||
|
],
|
||||||
|
"title": "Waze Integration",
|
||||||
|
"description": "Control TIDAL playback from the Waze app",
|
||||||
|
"default": true,
|
||||||
|
"advancedOptions": [
|
||||||
|
{
|
||||||
|
"type": "choice",
|
||||||
|
"key": "browse_root",
|
||||||
|
"title": "Media Menu",
|
||||||
|
"entries": [
|
||||||
|
"Auto Menu",
|
||||||
|
"Home",
|
||||||
|
"Recents"
|
||||||
|
],
|
||||||
|
"values": [
|
||||||
|
"ROOT_AUTO",
|
||||||
|
"HOME_V2::home_page_v2_id",
|
||||||
|
"RECENTLY_PLAYED::home/pages/CONTINUE_LISTEN_TO/view-all"
|
||||||
|
],
|
||||||
|
"defaultIndex": 0,
|
||||||
|
"token": "RL_WAZE_ROOT_ID"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "color",
|
||||||
|
"key": "accent_color",
|
||||||
|
"title": "Accent Color",
|
||||||
|
"default": -16747037,
|
||||||
|
"token": "RL_WAZE_THEME_COLOR"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -22,7 +22,8 @@ class PathManager(
|
|||||||
|
|
||||||
val patchingDownloadDir = patchingDir.resolve("downloads")
|
val patchingDownloadDir = patchingDir.resolve("downloads")
|
||||||
|
|
||||||
val cacheDownloadDir = context.cacheDir.resolve("downloads")
|
// Persistent Stash of assets for Offline Repatching
|
||||||
|
val cacheDownloadDir = patchingDir.resolve("downloads-cache")
|
||||||
|
|
||||||
val customComponentsDir = patchingDir.resolve("custom")
|
val customComponentsDir = patchingDir.resolve("custom")
|
||||||
|
|
||||||
@@ -34,11 +35,15 @@ class PathManager(
|
|||||||
|
|
||||||
val patchedApk = patchingWorkingDir.resolve("patched.apk")
|
val patchedApk = patchingWorkingDir.resolve("patched.apk")
|
||||||
|
|
||||||
|
// Persistent snapshot of the last successful release fetch (build info + patches asset URL)
|
||||||
|
val cachedReleaseInfo = patchingDir.resolve("release-info.json")
|
||||||
|
|
||||||
fun clearCache() {
|
fun clearCache() {
|
||||||
val targets = arrayOf(
|
val targets = arrayOf(
|
||||||
patchingDownloadDir,
|
patchingDownloadDir,
|
||||||
patchingWorkingDir,
|
patchingWorkingDir,
|
||||||
cacheDownloadDir,
|
cacheDownloadDir,
|
||||||
|
cachedReleaseInfo,
|
||||||
context.cacheDir,
|
context.cacheDir,
|
||||||
)
|
)
|
||||||
for (dir in targets) {
|
for (dir in targets) {
|
||||||
@@ -54,6 +59,16 @@ class PathManager(
|
|||||||
.resolve("patches")
|
.resolve("patches")
|
||||||
.resolve("$version.zip")
|
.resolve("$version.zip")
|
||||||
|
|
||||||
|
fun hasCachedTidalApk(version: Int, split: String = "base"): Boolean {
|
||||||
|
val rel = "tidal/$version/$split.apk"
|
||||||
|
return patchingDownloadDir.resolve(rel).exists() || cacheDownloadDir.resolve(rel).exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun hasCachedSmaliPatches(version: SemVer): Boolean {
|
||||||
|
val rel = "patches/$version.zip"
|
||||||
|
return patchingDownloadDir.resolve(rel).exists() || cacheDownloadDir.resolve(rel).exists()
|
||||||
|
}
|
||||||
|
|
||||||
fun customTidalApks() = customTidalApksDir.listFiles()?.asList() ?: emptyList()
|
fun customTidalApks() = customTidalApksDir.listFiles()?.asList() ?: emptyList()
|
||||||
|
|
||||||
fun customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
|
fun customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
|
|||||||
var devMode by booleanPreference("dev_mode", false)
|
var devMode by booleanPreference("dev_mode", false)
|
||||||
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
|
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
|
||||||
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
||||||
|
// Dev: force-apply path-gated ("incompatible") patches even under a non-stock package name
|
||||||
|
var bypassIncompatible by booleanPreference("bypass_incompatible", false)
|
||||||
var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
|
var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
|
||||||
var autoUpdateCheck by booleanPreference("auto_update_check", true)
|
var autoUpdateCheck by booleanPreference("auto_update_check", true)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.meowarex.rlmobile.manager
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import com.meowarex.rlmobile.BuildConfig
|
||||||
|
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
// Snapshot of the last successful release fetch (build info + patches asset URL)
|
||||||
|
@Serializable
|
||||||
|
data class CachedReleaseInfo(
|
||||||
|
val data: RLBuildInfo,
|
||||||
|
val patchesAssetUrl: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
object ReleaseInfoCache {
|
||||||
|
fun load(paths: PathManager, json: Json): CachedReleaseInfo? = try {
|
||||||
|
paths.cachedReleaseInfo
|
||||||
|
.takeIf { it.exists() }
|
||||||
|
?.let { json.decodeFromString<CachedReleaseInfo>(it.readText()) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.w(BuildConfig.TAG, "Failed to read cached release info", t)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(paths: PathManager, json: Json, info: CachedReleaseInfo) = try {
|
||||||
|
paths.cachedReleaseInfo.parentFile?.mkdirs()
|
||||||
|
paths.cachedReleaseInfo.writeText(json.encodeToString(info))
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.w(BuildConfig.TAG, "Failed to cache release info", t)
|
||||||
|
}
|
||||||
|
}
|
||||||
+3
-3
@@ -6,7 +6,6 @@ import com.meowarex.rlmobile.patcher.steps.StepGroup
|
|||||||
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.util.ManifestPatcher
|
import com.meowarex.rlmobile.patcher.util.ManifestPatcher
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.KnownPatch
|
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||||
import com.github.diamondminer88.zip.ZipReader
|
import com.github.diamondminer88.zip.ZipReader
|
||||||
import com.github.diamondminer88.zip.ZipWriter
|
import com.github.diamondminer88.zip.ZipWriter
|
||||||
@@ -27,8 +26,9 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
|||||||
?: throw IllegalArgumentException("No manifest found in APK")
|
?: throw IllegalArgumentException("No manifest found in APK")
|
||||||
|
|
||||||
container.log("Patching manifest")
|
container.log("Patching manifest")
|
||||||
val enableWazeIntegration =
|
val wazeDefault = container.getStep<SmaliPatchStep>().specs
|
||||||
options.patchStates[KnownPatch.WazeIntegration.name] ?: KnownPatch.WazeIntegration.default.isEnabled
|
.firstOrNull { it.id == "WazeIntegration" }?.defaultEnabled ?: false
|
||||||
|
val enableWazeIntegration = options.patchStates["WazeIntegration"] ?: wazeDefault
|
||||||
|
|
||||||
val patchedManifest = ManifestPatcher.patchManifest(
|
val patchedManifest = ManifestPatcher.patchManifest(
|
||||||
manifestBytes = manifest,
|
manifestBytes = manifest,
|
||||||
|
|||||||
+7
-1
@@ -1,5 +1,6 @@
|
|||||||
package com.meowarex.rlmobile.patcher.steps.patch
|
package com.meowarex.rlmobile.patcher.steps.patch
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
import com.meowarex.rlmobile.manager.PathManager
|
import com.meowarex.rlmobile.manager.PathManager
|
||||||
import com.meowarex.rlmobile.patcher.StepRunner
|
import com.meowarex.rlmobile.patcher.StepRunner
|
||||||
@@ -10,6 +11,7 @@ import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
|
|||||||
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
|
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchManifest
|
import com.meowarex.rlmobile.ui.screens.patchopts.PatchManifest
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||||
|
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs
|
import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs
|
||||||
import com.android.tools.smali.baksmali.Baksmali
|
import com.android.tools.smali.baksmali.Baksmali
|
||||||
import com.android.tools.smali.baksmali.BaksmaliOptions
|
import com.android.tools.smali.baksmali.BaksmaliOptions
|
||||||
@@ -31,6 +33,10 @@ class SmaliPatchStep(
|
|||||||
) : Step(), IDexProvider, KoinComponent {
|
) : Step(), IDexProvider, KoinComponent {
|
||||||
private val paths: PathManager by inject()
|
private val paths: PathManager by inject()
|
||||||
private val json: Json by inject()
|
private val json: Json by inject()
|
||||||
|
private val context: Context by inject()
|
||||||
|
|
||||||
|
var specs: List<PatchSpec> = emptyList()
|
||||||
|
private set
|
||||||
|
|
||||||
override val group = StepGroup.Patch
|
override val group = StepGroup.Patch
|
||||||
override val localizedName = R.string.patch_step_patch_smali
|
override val localizedName = R.string.patch_step_patch_smali
|
||||||
@@ -55,7 +61,7 @@ class SmaliPatchStep(
|
|||||||
container.log("Failed to parse manifest.json (${t.message}); using built-in patch specs")
|
container.log("Failed to parse manifest.json (${t.message}); using built-in patch specs")
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val specs = manifestSpecs ?: builtinPatchSpecs { "" }
|
specs = manifestSpecs ?: builtinPatchSpecs(context, json)
|
||||||
container.log(
|
container.log(
|
||||||
"Loaded ${specs.size} patch specs from " +
|
"Loaded ${specs.size} patch specs from " +
|
||||||
if (manifestSpecs != null) "manifest.json" else "built-in list"
|
if (manifestSpecs != null) "manifest.json" else "built-in list"
|
||||||
|
|||||||
+45
-2
@@ -1,19 +1,30 @@
|
|||||||
package com.meowarex.rlmobile.patcher.steps.prepare
|
package com.meowarex.rlmobile.patcher.steps.prepare
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.manager.CachedReleaseInfo
|
||||||
|
import com.meowarex.rlmobile.manager.PathManager
|
||||||
|
import com.meowarex.rlmobile.manager.ReleaseInfoCache
|
||||||
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
||||||
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
||||||
import com.meowarex.rlmobile.network.utils.getOrThrow
|
import com.meowarex.rlmobile.network.utils.getOrThrow
|
||||||
import com.meowarex.rlmobile.patcher.StepRunner
|
import com.meowarex.rlmobile.patcher.StepRunner
|
||||||
import com.meowarex.rlmobile.patcher.steps.StepGroup
|
import com.meowarex.rlmobile.patcher.steps.StepGroup
|
||||||
import com.meowarex.rlmobile.patcher.steps.base.Step
|
import com.meowarex.rlmobile.patcher.steps.base.Step
|
||||||
|
import com.meowarex.rlmobile.util.isOnline
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class FetchInfoStep : Step(), KoinComponent {
|
class FetchInfoStep : Step(), KoinComponent {
|
||||||
private val github: RadiantLyricsGithubService by inject()
|
private val github: RadiantLyricsGithubService by inject()
|
||||||
|
private val context: Application by inject()
|
||||||
|
private val paths: PathManager by inject()
|
||||||
|
private val json: Json by inject()
|
||||||
|
|
||||||
override val group = StepGroup.Prepare
|
override val group = StepGroup.Prepare
|
||||||
override val localizedName = R.string.patch_step_fetch_info
|
override val localizedName = R.string.patch_step_fetch_info
|
||||||
@@ -25,6 +36,33 @@ class FetchInfoStep : Step(), KoinComponent {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
override suspend fun execute(container: StepRunner) {
|
||||||
|
// Fetch the latest release and refresh the cache (when online)
|
||||||
|
if (context.isOnline()) {
|
||||||
|
try {
|
||||||
|
fetchFromRemote(container)
|
||||||
|
return
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
throw e
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
container.log("Failed to fetch latest release: ${Log.getStackTraceString(t)}")
|
||||||
|
container.log("Falling back to cached release info for a local re-patch")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
container.log("No network connection — using cached release info for a local re-patch")
|
||||||
|
}
|
||||||
|
|
||||||
|
val cached = ReleaseInfoCache.load(paths, json)
|
||||||
|
?: throw IllegalStateException(
|
||||||
|
"Could not fetch the latest release and no cached release info is available. " +
|
||||||
|
"Connect to the internet and try again."
|
||||||
|
)
|
||||||
|
data = cached.data
|
||||||
|
patchesAssetUrl = cached.patchesAssetUrl
|
||||||
|
container.log("Using cached build info: $data")
|
||||||
|
container.log("Cached patches asset URL: $patchesAssetUrl")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchFromRemote(container: StepRunner) {
|
||||||
container.log("Fetching latest release from ${RadiantLyricsGithubService.REPO_OWNER}/${RadiantLyricsGithubService.REPO_NAME}")
|
container.log("Fetching latest release from ${RadiantLyricsGithubService.REPO_OWNER}/${RadiantLyricsGithubService.REPO_NAME}")
|
||||||
val release = github.getLatestRelease(force = true).getOrThrow()
|
val release = github.getLatestRelease(force = true).getOrThrow()
|
||||||
|
|
||||||
@@ -33,14 +71,19 @@ class FetchInfoStep : Step(), KoinComponent {
|
|||||||
?.browserDownloadUrl
|
?.browserDownloadUrl
|
||||||
?: throw IllegalStateException("No ${RadiantLyricsGithubService.DATA_JSON_ASSET_NAME} asset found in latest release ${release.tagName}")
|
?: throw IllegalStateException("No ${RadiantLyricsGithubService.DATA_JSON_ASSET_NAME} asset found in latest release ${release.tagName}")
|
||||||
|
|
||||||
patchesAssetUrl = release.assets
|
val patchesUrl = release.assets
|
||||||
.find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME }
|
.find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME }
|
||||||
?.browserDownloadUrl
|
?.browserDownloadUrl
|
||||||
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
|
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
|
||||||
|
|
||||||
container.log("Fetching build info from $dataJsonUrl")
|
container.log("Fetching build info from $dataJsonUrl")
|
||||||
data = github.getBuildInfo(dataJsonUrl, force = true).getOrThrow()
|
val buildInfo = github.getBuildInfo(dataJsonUrl, force = true).getOrThrow()
|
||||||
|
|
||||||
|
data = buildInfo
|
||||||
|
patchesAssetUrl = patchesUrl
|
||||||
container.log("Fetched build info: $data")
|
container.log("Fetched build info: $data")
|
||||||
container.log("Patches asset URL: $patchesAssetUrl")
|
container.log("Patches asset URL: $patchesAssetUrl")
|
||||||
|
|
||||||
|
ReleaseInfoCache.save(paths, json, CachedReleaseInfo(buildInfo, patchesUrl))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-1
@@ -9,6 +9,7 @@ import com.meowarex.rlmobile.network.utils.SemVer
|
|||||||
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.*
|
import com.meowarex.rlmobile.ui.screens.patchopts.*
|
||||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
|
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
|
||||||
@@ -21,7 +22,7 @@ private fun PatchOptionsScreenPreview(
|
|||||||
parameters: PatchOptionsParameters,
|
parameters: PatchOptionsParameters,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val specs = remember { builtinPatchSpecs { context.getString(it) } }
|
val specs = remember { builtinPatchSpecs(context, Json { ignoreUnknownKeys = true }) }
|
||||||
|
|
||||||
ManagerTheme {
|
ManagerTheme {
|
||||||
PatchOptionsScreenContent(
|
PatchOptionsScreenContent(
|
||||||
@@ -29,6 +30,8 @@ private fun PatchOptionsScreenPreview(
|
|||||||
isDevMode = parameters.isDevMode,
|
isDevMode = parameters.isDevMode,
|
||||||
debuggable = parameters.debuggable,
|
debuggable = parameters.debuggable,
|
||||||
setDebuggable = {},
|
setDebuggable = {},
|
||||||
|
bypassIncompatible = false,
|
||||||
|
setBypassIncompatible = {},
|
||||||
appName = parameters.appName,
|
appName = parameters.appName,
|
||||||
appNameIsError = parameters.appNameIsError,
|
appNameIsError = parameters.appNameIsError,
|
||||||
setAppName = {},
|
setAppName = {},
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
|||||||
import com.github.diamondminer88.zip.ZipReader
|
import com.github.diamondminer88.zip.ZipReader
|
||||||
import com.meowarex.rlmobile.BuildConfig
|
import com.meowarex.rlmobile.BuildConfig
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.manager.PathManager
|
||||||
import com.meowarex.rlmobile.manager.PreferencesManager
|
import com.meowarex.rlmobile.manager.PreferencesManager
|
||||||
|
import com.meowarex.rlmobile.manager.ReleaseInfoCache
|
||||||
import com.meowarex.rlmobile.network.models.GithubCommit
|
import com.meowarex.rlmobile.network.models.GithubCommit
|
||||||
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
||||||
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
||||||
@@ -42,6 +44,7 @@ class HomeModel(
|
|||||||
private val github: RadiantLyricsGithubService,
|
private val github: RadiantLyricsGithubService,
|
||||||
private val json: Json,
|
private val json: Json,
|
||||||
private val prefs: PreferencesManager,
|
private val prefs: PreferencesManager,
|
||||||
|
private val paths: PathManager,
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
var state by mutableStateOf<HomeState>(HomeState.Loading)
|
var state by mutableStateOf<HomeState>(HomeState.Loading)
|
||||||
@@ -170,16 +173,23 @@ class HomeModel(
|
|||||||
|
|
||||||
refreshingLock.withLock {
|
refreshingLock.withLock {
|
||||||
val pkg = fetchInstalled()
|
val pkg = fetchInstalled()
|
||||||
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
|
// Skip the remote fetch entirely when offline
|
||||||
remote.await()
|
val online = application.isOnline()
|
||||||
|
if (online) {
|
||||||
|
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
|
||||||
|
remote.await()
|
||||||
|
}
|
||||||
|
|
||||||
val install = pkg?.toInstallData()
|
val install = pkg?.toInstallData()
|
||||||
val latest = remoteDataJson?.tidalVersionCode
|
val latest = remoteDataJson?.tidalVersionCode
|
||||||
|
val offlineRepatchReady = hasOfflineRepatchAssets()
|
||||||
|
|
||||||
mainThread {
|
mainThread {
|
||||||
state = HomeState.Loaded(
|
state = HomeState.Loaded(
|
||||||
install = install,
|
install = install,
|
||||||
latestTidalVersionCode = latest,
|
latestTidalVersionCode = latest,
|
||||||
|
offline = !online,
|
||||||
|
offlineRepatchReady = offlineRepatchReady,
|
||||||
)
|
)
|
||||||
maybeCheckManagerUpdate(pkg)
|
maybeCheckManagerUpdate(pkg)
|
||||||
}
|
}
|
||||||
@@ -257,6 +267,13 @@ class HomeModel(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if an offline "Local Repatch" is possible
|
||||||
|
private fun hasOfflineRepatchAssets(): Boolean {
|
||||||
|
val info = ReleaseInfoCache.load(paths, json) ?: return false
|
||||||
|
return paths.hasCachedTidalApk(info.data.tidalVersionCode) &&
|
||||||
|
paths.hasCachedSmaliPatches(info.data.patchesVersion)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun fetchRemoteData() {
|
private suspend fun fetchRemoteData() {
|
||||||
val release = try {
|
val release = try {
|
||||||
github.getLatestRelease().fold(
|
github.getLatestRelease().fold(
|
||||||
|
|||||||
@@ -210,14 +210,19 @@ private fun ColumnScope.HomeContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind)
|
val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind)
|
||||||
|
val canLocalRepatch = install != null && state.offlineRepatchReady
|
||||||
|
val onlineEnabled = state.latestTidalVersionCode != null || install != null
|
||||||
|
val buttonEnabled = !blockedByManagerUpdate && (if (state.offline) canLocalRepatch else onlineEnabled)
|
||||||
Button(
|
Button(
|
||||||
onClick = if (install == null) onInstall else onRepatch,
|
onClick = if (install == null) onInstall else onRepatch,
|
||||||
enabled = state.latestTidalVersionCode != null && !blockedByManagerUpdate,
|
enabled = buttonEnabled,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
val label = when {
|
val label = when {
|
||||||
blockedByManagerUpdate -> "Manager Update Required"
|
blockedByManagerUpdate -> "Manager Update Required"
|
||||||
state.latestTidalVersionCode == null -> "Loading…"
|
state.offline && install != null -> "Local Repatch"
|
||||||
|
state.offline -> "No Network"
|
||||||
|
install == null && state.latestTidalVersionCode == null -> "Loading…"
|
||||||
install == null -> "Install"
|
install == null -> "Install"
|
||||||
patchesBehind && tidalBehind -> "Update Patches & TIDAL"
|
patchesBehind && tidalBehind -> "Update Patches & TIDAL"
|
||||||
patchesBehind -> "Update Patches"
|
patchesBehind -> "Update Patches"
|
||||||
@@ -234,6 +239,18 @@ private fun ColumnScope.HomeContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.offline && install != null) {
|
||||||
|
Text(
|
||||||
|
text = "No Network Connection",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 6.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
AnimatedVisibility(visible = install != null) {
|
AnimatedVisibility(visible = install != null) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
|||||||
@@ -8,5 +8,7 @@ sealed interface HomeState {
|
|||||||
data class Loaded(
|
data class Loaded(
|
||||||
val install: InstallData?,
|
val install: InstallData?,
|
||||||
val latestTidalVersionCode: Int?,
|
val latestTidalVersionCode: Int?,
|
||||||
|
val offline: Boolean = false,
|
||||||
|
val offlineRepatchReady: Boolean = false,
|
||||||
) : HomeState
|
) : HomeState
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,296 +0,0 @@
|
|||||||
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(
|
|
||||||
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(),
|
|
||||||
val category: String = PatchSpec.CATEGORY_PATCH,
|
|
||||||
val pathLocked: Boolean = false,
|
|
||||||
) {
|
|
||||||
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,
|
|
||||||
fileNames = listOf(
|
|
||||||
"lyrics-replace-lyrics-button.patch",
|
|
||||||
"lyrics-sparkle-conditional-visibility.patch",
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
fileNames = listOf(
|
|
||||||
"lyrics-rl-api.patch",
|
|
||||||
"lyrics-rl-api-observer.patch",
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
WazeIntegration(
|
|
||||||
order = 90,
|
|
||||||
fileNames = listOf("waze-media-browser.patch"),
|
|
||||||
extensionFiles = listOf(
|
|
||||||
"radiant/WazeInitReceiver.smali",
|
|
||||||
"radiant/WazeServiceConnection.smali",
|
|
||||||
),
|
|
||||||
titleRes = R.string.patch_waze_integration_title,
|
|
||||||
descRes = R.string.patch_waze_integration_desc,
|
|
||||||
default = Disabled,
|
|
||||||
category = PatchSpec.CATEGORY_INTEGRATION,
|
|
||||||
pathLocked = true,
|
|
||||||
advancedOptions = listOf(
|
|
||||||
PatchOption.Choice(
|
|
||||||
key = "browse_root",
|
|
||||||
titleRes = R.string.patch_waze_browse_root_title,
|
|
||||||
entries = listOf(
|
|
||||||
ChoiceEntry(R.string.patch_waze_root_auto, value = "ROOT_AUTO"),
|
|
||||||
ChoiceEntry(R.string.patch_waze_root_home, value = "HOME_V2::home_page_v2_id"),
|
|
||||||
ChoiceEntry(
|
|
||||||
R.string.patch_waze_root_recents,
|
|
||||||
value = "RECENTLY_PLAYED::home/pages/CONTINUE_LISTEN_TO/view-all",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
defaultIndex = 0,
|
|
||||||
token = "RL_WAZE_ROOT_ID",
|
|
||||||
),
|
|
||||||
PatchOption.Color(
|
|
||||||
key = "accent_color",
|
|
||||||
titleRes = R.string.patch_waze_accent_color_title,
|
|
||||||
default = -16747037, // Waze default #ff0075e3
|
|
||||||
token = "RL_WAZE_THEME_COLOR",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
LyricsProgressPill(
|
|
||||||
order = 40,
|
|
||||||
fileNames = listOf(
|
|
||||||
"lyrics-progress-pill.patch",
|
|
||||||
"lyrics-fade-region.patch",
|
|
||||||
),
|
|
||||||
titleRes = R.string.patch_lyrics_progress_pill_title,
|
|
||||||
descRes = R.string.patch_lyrics_progress_pill_desc,
|
|
||||||
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,
|
|
||||||
LyricsReplaceLyricsButton,
|
|
||||||
LyricsReplaceShareButton,
|
|
||||||
LyricsRlApi,
|
|
||||||
LyricsKeepControlsVisible,
|
|
||||||
PlayerBackdrop,
|
|
||||||
QualityBadgeColors,
|
|
||||||
LyricsProgressPill,
|
|
||||||
MiniPlayerRedesign,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
val All: List<KnownPatch> = entries.sortedWith(
|
|
||||||
compareBy({ it.order }, { it.fileNames.firstOrNull() ?: it.name })
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package com.meowarex.rlmobile.ui.screens.patchopts
|
|
||||||
|
|
||||||
enum class PatchDefault(val isEnabled: Boolean) {
|
|
||||||
Enabled(true),
|
|
||||||
Disabled(false),
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
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 = 0,
|
|
||||||
val entries: List<ChoiceEntry>,
|
|
||||||
val defaultIndex: Int = 0,
|
|
||||||
val requiresOption: String? = null,
|
|
||||||
val token: String? = null,
|
|
||||||
) : PatchOption
|
|
||||||
|
|
||||||
data class Color(
|
|
||||||
override val key: String,
|
|
||||||
@StringRes override val titleRes: Int,
|
|
||||||
@StringRes override val descRes: Int = 0,
|
|
||||||
val default: Int,
|
|
||||||
val token: String? = null,
|
|
||||||
) : PatchOption
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ChoiceEntry(
|
|
||||||
@StringRes val labelRes: Int,
|
|
||||||
val value: String? = null,
|
|
||||||
)
|
|
||||||
+24
-4
@@ -45,7 +45,7 @@ class PatchOptionsModel(
|
|||||||
|
|
||||||
/** Patches flagged [PatchSpec.pathLocked] only work under the stock package name. */
|
/** Patches flagged [PatchSpec.pathLocked] only work under the stock package name. */
|
||||||
fun isBlockedByPackageName(spec: PatchSpec): Boolean =
|
fun isBlockedByPackageName(spec: PatchSpec): Boolean =
|
||||||
spec.pathLocked && packageName != PatchOptions.Default.packageName
|
spec.pathLocked && packageName != PatchOptions.Default.packageName && !bypassIncompatible
|
||||||
|
|
||||||
/** The package-name field starts locked & editing requires confirming the unlock dialog */
|
/** The package-name field starts locked & editing requires confirming the unlock dialog */
|
||||||
var packageNameLocked by mutableStateOf(prefilledOptions.packageName == PatchOptions.Default.packageName)
|
var packageNameLocked by mutableStateOf(prefilledOptions.packageName == PatchOptions.Default.packageName)
|
||||||
@@ -79,10 +79,20 @@ class PatchOptionsModel(
|
|||||||
debuggable = value
|
debuggable = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Dev override: force-allow path-gated ("incompatible") patches under a non-stock package name. */
|
||||||
|
var bypassIncompatible by mutableStateOf(prefs.bypassIncompatible)
|
||||||
|
private set
|
||||||
|
|
||||||
|
fun changeBypassIncompatible(value: Boolean) {
|
||||||
|
bypassIncompatible = value
|
||||||
|
prefs.bypassIncompatible = value
|
||||||
|
validatePatchSelection()
|
||||||
|
}
|
||||||
|
|
||||||
// The accordion renders from [specs]. It defaults to the compiled-in (localized) list and is
|
// 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)
|
// 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) }
|
private val builtinSpecs: List<PatchSpec> = builtinPatchSpecs(context, json)
|
||||||
|
|
||||||
var specs by mutableStateOf(builtinSpecs)
|
var specs by mutableStateOf(builtinSpecs)
|
||||||
private set
|
private set
|
||||||
@@ -281,6 +291,7 @@ class PatchOptionsModel(
|
|||||||
val bytes = ZipReader(file).use { it.openEntry(MANIFEST_NAME)?.read() } ?: return null
|
val bytes = ZipReader(file).use { it.openEntry(MANIFEST_NAME)?.read() } ?: return null
|
||||||
json.decodeFromString(PatchManifest.serializer(), bytes.decodeToString())
|
json.decodeFromString(PatchManifest.serializer(), bytes.decodeToString())
|
||||||
.patches
|
.patches
|
||||||
|
.sortedBy { it.order } // manifest is authoritative for UI order (not KnownPatch)
|
||||||
.takeIf { it.isNotEmpty() }
|
.takeIf { it.isNotEmpty() }
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
Log.w(BuildConfig.TAG, "Failed to parse $MANIFEST_NAME; using built-in list", t)
|
Log.w(BuildConfig.TAG, "Failed to parse $MANIFEST_NAME; using built-in list", t)
|
||||||
@@ -320,10 +331,19 @@ class PatchOptionsModel(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Default (non-custom) source: the latest release manifest, falling back to the built-in list. */
|
private fun loadCachedReleaseManifestSpecs(): List<PatchSpec>? =
|
||||||
|
paths.cacheDownloadDir
|
||||||
|
.listFiles { f -> f.name.startsWith("manifest-") && f.name.endsWith(".zip") }
|
||||||
|
?.maxByOrNull { it.lastModified() }
|
||||||
|
?.let { loadManifestSpecs(it) }
|
||||||
|
?.also { Log.i(BuildConfig.TAG, "Loaded ${it.size} patches from cached release manifest (offline)") }
|
||||||
|
|
||||||
|
/** Default (non-custom) source: latest release manifest → last cached manifest → built-in list. */
|
||||||
private fun loadDefaultSpecs() = screenModelScope.launchIO {
|
private fun loadDefaultSpecs() = screenModelScope.launchIO {
|
||||||
mainThread { specsLoading = true }
|
mainThread { specsLoading = true }
|
||||||
val loaded = loadLatestReleaseSpecs() ?: builtinSpecs
|
// Only hit the network when actually online
|
||||||
|
val loaded = (if (context.isOnline()) loadLatestReleaseSpecs() else null)
|
||||||
|
?: loadCachedReleaseManifestSpecs() ?: builtinSpecs
|
||||||
mainThread {
|
mainThread {
|
||||||
specs = loaded
|
specs = loaded
|
||||||
validatePatchSelection()
|
validatePatchSelection()
|
||||||
|
|||||||
+14
-1
@@ -48,6 +48,8 @@ class PatchOptionsScreen(
|
|||||||
|
|
||||||
debuggable = model.debuggable,
|
debuggable = model.debuggable,
|
||||||
setDebuggable = model::changeDebuggable,
|
setDebuggable = model::changeDebuggable,
|
||||||
|
bypassIncompatible = model.bypassIncompatible,
|
||||||
|
setBypassIncompatible = model::changeBypassIncompatible,
|
||||||
|
|
||||||
appName = model.appName,
|
appName = model.appName,
|
||||||
appNameIsError = model.appNameIsError,
|
appNameIsError = model.appNameIsError,
|
||||||
@@ -89,6 +91,8 @@ fun PatchOptionsScreenContent(
|
|||||||
|
|
||||||
debuggable: Boolean,
|
debuggable: Boolean,
|
||||||
setDebuggable: (Boolean) -> Unit,
|
setDebuggable: (Boolean) -> Unit,
|
||||||
|
bypassIncompatible: Boolean,
|
||||||
|
setBypassIncompatible: (Boolean) -> Unit,
|
||||||
|
|
||||||
appName: String,
|
appName: String,
|
||||||
appNameIsError: Boolean,
|
appNameIsError: Boolean,
|
||||||
@@ -121,7 +125,8 @@ fun PatchOptionsScreenContent(
|
|||||||
var showPkgUnlockDialog by rememberSaveable { mutableStateOf(false) }
|
var showPkgUnlockDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
if (showPkgUnlockDialog) {
|
if (showPkgUnlockDialog) {
|
||||||
PackageNameUnlockDialog(
|
PackageNameUnlockDialog(
|
||||||
blockedTitles = specs.filter { it.pathLocked }.map { it.title },
|
// With "Bypass Incompatible" on, path-gated patches are force-applied, not disabled.
|
||||||
|
blockedTitles = if (bypassIncompatible) emptyList() else specs.filter { it.pathLocked }.map { it.title },
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
showPkgUnlockDialog = false
|
showPkgUnlockDialog = false
|
||||||
onUnlockPackageName()
|
onUnlockPackageName()
|
||||||
@@ -268,6 +273,14 @@ fun PatchOptionsScreenContent(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SwitchPatchOption(
|
||||||
|
icon = painterResource(R.drawable.ic_lock_open),
|
||||||
|
name = stringResource(R.string.patchopts_bypass_incompatible_title),
|
||||||
|
description = stringResource(R.string.patchopts_bypass_incompatible_desc),
|
||||||
|
value = bypassIncompatible,
|
||||||
|
onValueChange = setBypassIncompatible,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.weight(1f))
|
Spacer(Modifier.weight(1f))
|
||||||
|
|||||||
+12
-67
@@ -1,9 +1,11 @@
|
|||||||
package com.meowarex.rlmobile.ui.screens.patchopts
|
package com.meowarex.rlmobile.ui.screens.patchopts
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
@@ -157,74 +159,17 @@ data class SmaliEncode(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun builtinPatchSpecs(resolve: (Int) -> String): List<PatchSpec> =
|
/** Asset filename of the bundled fallback manifest snapshot (a copy of the release manifest.json). */
|
||||||
KnownPatch.All.map { patch ->
|
const val BUNDLED_MANIFEST_ASSET = "patches-manifest.json"
|
||||||
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) },
|
|
||||||
category = patch.category,
|
|
||||||
pathLocked = patch.pathLocked,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (this) {
|
// Fallback patch list (snapshot of the release manifest bundled in the app's assets)
|
||||||
is PatchOption.Toggle -> OptionSpec.Toggle(
|
fun builtinPatchSpecs(context: Context, json: Json): List<PatchSpec> = try {
|
||||||
key = key,
|
context.assets.open(BUNDLED_MANIFEST_ASSET).use { it.readBytes() }
|
||||||
title = resolve(titleRes),
|
.let { json.decodeFromString(PatchManifest.serializer(), it.decodeToString()) }
|
||||||
description = resolve(descRes),
|
.patches
|
||||||
default = default,
|
.sortedBy { it.order }
|
||||||
fileNames = fileNames,
|
} catch (t: Throwable) {
|
||||||
extensionFiles = extensionFiles,
|
emptyList()
|
||||||
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 = if (descRes != 0) resolve(descRes) else "",
|
|
||||||
entries = entries.map { resolve(it.labelRes) },
|
|
||||||
defaultIndex = defaultIndex,
|
|
||||||
values = entries.map { it.value ?: "" },
|
|
||||||
requiresOption = requiresOption,
|
|
||||||
token = token,
|
|
||||||
)
|
|
||||||
|
|
||||||
is PatchOption.Color -> OptionSpec.Color(
|
|
||||||
key = key,
|
|
||||||
title = resolve(titleRes),
|
|
||||||
description = if (descRes != 0) resolve(descRes) else "",
|
|
||||||
default = default,
|
|
||||||
token = token,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import android.app.Activity
|
|||||||
import android.content.*
|
import android.content.*
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
import android.net.ConnectivityManager
|
||||||
|
import android.net.NetworkCapabilities
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.*
|
import android.os.*
|
||||||
import android.provider.Settings
|
import android.provider.Settings
|
||||||
@@ -63,6 +65,15 @@ fun Context.getPackageVersion(pkg: String): Pair<String?, Int> {
|
|||||||
.let { it.versionName to it.versionCode }
|
.let { it.versionName to it.versionCode }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Whether the device currently has a validated internet connection
|
||||||
|
fun Context.isOnline(): Boolean {
|
||||||
|
val cm = getSystemService<ConnectivityManager>() ?: return true
|
||||||
|
val network = cm.activeNetwork ?: return false
|
||||||
|
val caps = cm.getNetworkCapabilities(network) ?: return false
|
||||||
|
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||||
|
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||||
|
}
|
||||||
|
|
||||||
fun Context.isPackageInstalled(packageName: String): Boolean {
|
fun Context.isPackageInstalled(packageName: String): Boolean {
|
||||||
return try {
|
return try {
|
||||||
packageManager.getPackageInfo(packageName, 0)
|
packageManager.getPackageInfo(packageName, 0)
|
||||||
|
|||||||
@@ -180,7 +180,7 @@
|
|||||||
<string name="install_error_timeout">Installer timed out</string>
|
<string name="install_error_timeout">Installer timed out</string>
|
||||||
|
|
||||||
<string name="install_group_prepare">Prepare</string>
|
<string name="install_group_prepare">Prepare</string>
|
||||||
<string name="install_group_download">Download dependencies</string>
|
<string name="install_group_download">Download Batteries</string>
|
||||||
<string name="install_group_patch">Patch APK</string>
|
<string name="install_group_patch">Patch APK</string>
|
||||||
<string name="install_group_install">Install APK</string>
|
<string name="install_group_install">Install APK</string>
|
||||||
|
|
||||||
@@ -247,6 +247,8 @@
|
|||||||
<string name="patchopts_custom_tidal_apk_desc">Provide your own Stock TIDAL APK to patch instead of the version from Github.</string>
|
<string name="patchopts_custom_tidal_apk_desc">Provide your own Stock TIDAL APK to patch instead of the version from Github.</string>
|
||||||
<string name="patchopts_custom_patches_title">Custom Patches</string>
|
<string name="patchopts_custom_patches_title">Custom Patches</string>
|
||||||
<string name="patchopts_custom_patches_desc">Provide your own Patches Zip to use instead of the ones from Github.</string>
|
<string name="patchopts_custom_patches_desc">Provide your own Patches Zip to use instead of the ones from Github.</string>
|
||||||
|
<string name="patchopts_bypass_incompatible_title">Bypass Incompatible</string>
|
||||||
|
<string name="patchopts_bypass_incompatible_desc">Force-allow path-gated patches. This can be VERY DANGEROUS!</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_title">Patches</string>
|
||||||
|
|||||||
+15
-15
@@ -103,8 +103,21 @@
|
|||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "PlayerOneHanded",
|
"id": "PlayerMovePlayingFrom",
|
||||||
"order": 37,
|
"order": 37,
|
||||||
|
"fileNames": [
|
||||||
|
"player-move-playing-from.patch"
|
||||||
|
],
|
||||||
|
"extensionFiles": [
|
||||||
|
"radiant/PlayingFrom.smali"
|
||||||
|
],
|
||||||
|
"title": "Playing From",
|
||||||
|
"description": "Shows the source name (playlist/album/mix) above the track title",
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "PlayerOneHanded",
|
||||||
|
"order": 38,
|
||||||
"fileNames": [
|
"fileNames": [
|
||||||
"player-one-handed.patch"
|
"player-one-handed.patch"
|
||||||
],
|
],
|
||||||
@@ -159,19 +172,6 @@
|
|||||||
"description": "Replaces the share button with the Tidal Connect button from the top of the screen. (it's in the menu already)",
|
"description": "Replaces the share button with the Tidal Connect button from the top of the screen. (it's in the menu already)",
|
||||||
"default": true
|
"default": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"id": "PlayerMovePlayingFrom",
|
|
||||||
"order": 44,
|
|
||||||
"fileNames": [
|
|
||||||
"player-move-playing-from.patch"
|
|
||||||
],
|
|
||||||
"extensionFiles": [
|
|
||||||
"radiant/PlayingFrom.smali"
|
|
||||||
],
|
|
||||||
"title": "Move Playing From",
|
|
||||||
"description": "Shows the source name (e.g. the playlist or album you're playing from) just above the track title, in the artist text style, instead of at the bottom of the player.",
|
|
||||||
"default": false
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"id": "MiniPlayerRedesign",
|
"id": "MiniPlayerRedesign",
|
||||||
"order": 50,
|
"order": 50,
|
||||||
@@ -307,7 +307,7 @@
|
|||||||
"category": "integration",
|
"category": "integration",
|
||||||
"pathLocked": true,
|
"pathLocked": true,
|
||||||
"fileNames": [
|
"fileNames": [
|
||||||
"waze-media-browser.patch"
|
"integration-waze-playback.patch"
|
||||||
],
|
],
|
||||||
"extensionFiles": [
|
"extensionFiles": [
|
||||||
"radiant/WazeInitReceiver.smali",
|
"radiant/WazeInitReceiver.smali",
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
--- a/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
|
+++ b/com/tidal/android/feature/playerscreen/ui/PlayerScreenKt.smali
|
||||||
|
@@ -5794,6 +5794,8 @@
|
||||||
|
.line 336
|
||||||
|
iget-object v3, v10, Lcom/tidal/android/feature/playerscreen/ui/s$a;->l:Lcom/tidal/android/feature/playerscreen/ui/p;
|
||||||
|
|
||||||
|
+ const/4 v3, 0x0 # Never show the "Playing from" source at the bottom (relocated above the title, or hidden when off)
|
||||||
|
+
|
||||||
|
if-nez v3, :cond_52
|
||||||
|
|
||||||
|
const v3, 0x26ba353b
|
||||||
@@ -22,15 +22,6 @@
|
|||||||
iget-object v0, v3, Lcom/tidal/android/feature/playerscreen/ui/s$a;->c:Lcom/tidal/android/feature/playerscreen/ui/d;
|
iget-object v0, v3, Lcom/tidal/android/feature/playerscreen/ui/s$a;->c:Lcom/tidal/android/feature/playerscreen/ui/d;
|
||||||
|
|
||||||
.line 234
|
.line 234
|
||||||
@@ -5794,6 +5814,8 @@
|
|
||||||
.line 336
|
|
||||||
iget-object v3, v10, Lcom/tidal/android/feature/playerscreen/ui/s$a;->l:Lcom/tidal/android/feature/playerscreen/ui/p;
|
|
||||||
|
|
||||||
+ const/4 v3, 0x0 # RL move-playing-from: force null -> hide source at bottom (rendered above the title instead)
|
|
||||||
+
|
|
||||||
if-nez v3, :cond_52
|
|
||||||
|
|
||||||
const v3, 0x26ba353b
|
|
||||||
--- a/com/tidal/android/feature/playerscreen/ui/composables/k2.smali
|
--- a/com/tidal/android/feature/playerscreen/ui/composables/k2.smali
|
||||||
+++ b/com/tidal/android/feature/playerscreen/ui/composables/k2.smali
|
+++ b/com/tidal/android/feature/playerscreen/ui/composables/k2.smali
|
||||||
@@ -632,6 +632,39 @@
|
@@ -632,6 +632,39 @@
|
||||||
|
|||||||
Reference in New Issue
Block a user