Offline Repatch + Force Incompatible (Dev Option)

This commit is contained in:
2026-07-05 23:00:14 +10:00
parent aae2f54fab
commit 189e3d28a2
25 changed files with 607 additions and 493 deletions
+10
View File
@@ -228,3 +228,13 @@ fun ProviderFactory.execIgnoreCode(vararg command: String): String = try {
} 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 cacheDownloadDir = context.cacheDir.resolve("downloads")
// Persistent Stash of assets for Offline Repatching
val cacheDownloadDir = patchingDir.resolve("downloads-cache")
val customComponentsDir = patchingDir.resolve("custom")
@@ -34,11 +35,15 @@ class PathManager(
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() {
val targets = arrayOf(
patchingDownloadDir,
patchingWorkingDir,
cacheDownloadDir,
cachedReleaseInfo,
context.cacheDir,
)
for (dir in targets) {
@@ -54,6 +59,16 @@ class PathManager(
.resolve("patches")
.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 customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
@@ -12,6 +12,8 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
var devMode by booleanPreference("dev_mode", false)
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
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 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)
}
}
@@ -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.download.CopyDependenciesStep
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.github.diamondminer88.zip.ZipReader
import com.github.diamondminer88.zip.ZipWriter
@@ -27,8 +26,9 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
?: throw IllegalArgumentException("No manifest found in APK")
container.log("Patching manifest")
val enableWazeIntegration =
options.patchStates[KnownPatch.WazeIntegration.name] ?: KnownPatch.WazeIntegration.default.isEnabled
val wazeDefault = container.getStep<SmaliPatchStep>().specs
.firstOrNull { it.id == "WazeIntegration" }?.defaultEnabled ?: false
val enableWazeIntegration = options.patchStates["WazeIntegration"] ?: wazeDefault
val patchedManifest = ManifestPatcher.patchManifest(
manifestBytes = manifest,
@@ -1,5 +1,6 @@
package com.meowarex.rlmobile.patcher.steps.patch
import android.content.Context
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PathManager
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.ui.screens.patchopts.PatchManifest
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.android.tools.smali.baksmali.Baksmali
import com.android.tools.smali.baksmali.BaksmaliOptions
@@ -31,6 +33,10 @@ class SmaliPatchStep(
) : Step(), IDexProvider, KoinComponent {
private val paths: PathManager 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 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")
null
}
val specs = manifestSpecs ?: builtinPatchSpecs { "" }
specs = manifestSpecs ?: builtinPatchSpecs(context, json)
container.log(
"Loaded ${specs.size} patch specs from " +
if (manifestSpecs != null) "manifest.json" else "built-in list"
@@ -1,19 +1,30 @@
package com.meowarex.rlmobile.patcher.steps.prepare
import android.app.Application
import android.util.Log
import androidx.compose.runtime.Stable
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.services.RadiantLyricsGithubService
import com.meowarex.rlmobile.network.utils.getOrThrow
import com.meowarex.rlmobile.patcher.StepRunner
import com.meowarex.rlmobile.patcher.steps.StepGroup
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.inject
import kotlin.coroutines.cancellation.CancellationException
@Stable
class FetchInfoStep : Step(), KoinComponent {
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 localizedName = R.string.patch_step_fetch_info
@@ -25,6 +36,33 @@ class FetchInfoStep : Step(), KoinComponent {
private set
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}")
val release = github.getLatestRelease(force = true).getOrThrow()
@@ -33,14 +71,19 @@ class FetchInfoStep : Step(), KoinComponent {
?.browserDownloadUrl
?: 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 }
?.browserDownloadUrl
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
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("Patches asset URL: $patchesAssetUrl")
ReleaseInfoCache.save(paths, json, CachedReleaseInfo(buildInfo, patchesUrl))
}
}
@@ -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.patchopts.*
import com.meowarex.rlmobile.ui.theme.ManagerTheme
import kotlinx.serialization.json.Json
import kotlin.time.Clock
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
@@ -21,7 +22,7 @@ private fun PatchOptionsScreenPreview(
parameters: PatchOptionsParameters,
) {
val context = LocalContext.current
val specs = remember { builtinPatchSpecs { context.getString(it) } }
val specs = remember { builtinPatchSpecs(context, Json { ignoreUnknownKeys = true }) }
ManagerTheme {
PatchOptionsScreenContent(
@@ -29,6 +30,8 @@ private fun PatchOptionsScreenPreview(
isDevMode = parameters.isDevMode,
debuggable = parameters.debuggable,
setDebuggable = {},
bypassIncompatible = false,
setBypassIncompatible = {},
appName = parameters.appName,
appNameIsError = parameters.appNameIsError,
setAppName = {},
@@ -19,7 +19,9 @@ import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diamondminer88.zip.ZipReader
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PathManager
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.RLBuildInfo
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
@@ -42,6 +44,7 @@ class HomeModel(
private val github: RadiantLyricsGithubService,
private val json: Json,
private val prefs: PreferencesManager,
private val paths: PathManager,
) : ScreenModel {
var state by mutableStateOf<HomeState>(HomeState.Loading)
@@ -170,16 +173,23 @@ class HomeModel(
refreshingLock.withLock {
val pkg = fetchInstalled()
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
remote.await()
// Skip the remote fetch entirely when offline
val online = application.isOnline()
if (online) {
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
remote.await()
}
val install = pkg?.toInstallData()
val latest = remoteDataJson?.tidalVersionCode
val offlineRepatchReady = hasOfflineRepatchAssets()
mainThread {
state = HomeState.Loaded(
install = install,
latestTidalVersionCode = latest,
offline = !online,
offlineRepatchReady = offlineRepatchReady,
)
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() {
val release = try {
github.getLatestRelease().fold(
@@ -210,14 +210,19 @@ private fun ColumnScope.HomeContent(
}
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(
onClick = if (install == null) onInstall else onRepatch,
enabled = state.latestTidalVersionCode != null && !blockedByManagerUpdate,
enabled = buttonEnabled,
modifier = Modifier.fillMaxWidth(),
) {
val label = when {
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"
patchesBehind && tidalBehind -> "Update Patches & TIDAL"
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) {
Row(
horizontalArrangement = Arrangement.spacedBy(2.dp),
@@ -8,5 +8,7 @@ sealed interface HomeState {
data class Loaded(
val install: InstallData?,
val latestTidalVersionCode: Int?,
val offline: Boolean = false,
val offlineRepatchReady: Boolean = false,
) : 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,
)
@@ -45,7 +45,7 @@ class PatchOptionsModel(
/** Patches flagged [PatchSpec.pathLocked] only work under the stock package name. */
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 */
var packageNameLocked by mutableStateOf(prefilledOptions.packageName == PatchOptions.Default.packageName)
@@ -79,10 +79,20 @@ class PatchOptionsModel(
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
// 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)
private set
@@ -281,6 +291,7 @@ class PatchOptionsModel(
val bytes = ZipReader(file).use { it.openEntry(MANIFEST_NAME)?.read() } ?: return null
json.decodeFromString(PatchManifest.serializer(), bytes.decodeToString())
.patches
.sortedBy { it.order } // manifest is authoritative for UI order (not KnownPatch)
.takeIf { it.isNotEmpty() }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to parse $MANIFEST_NAME; using built-in list", t)
@@ -320,10 +331,19 @@ class PatchOptionsModel(
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 {
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 {
specs = loaded
validatePatchSelection()
@@ -48,6 +48,8 @@ class PatchOptionsScreen(
debuggable = model.debuggable,
setDebuggable = model::changeDebuggable,
bypassIncompatible = model.bypassIncompatible,
setBypassIncompatible = model::changeBypassIncompatible,
appName = model.appName,
appNameIsError = model.appNameIsError,
@@ -89,6 +91,8 @@ fun PatchOptionsScreenContent(
debuggable: Boolean,
setDebuggable: (Boolean) -> Unit,
bypassIncompatible: Boolean,
setBypassIncompatible: (Boolean) -> Unit,
appName: String,
appNameIsError: Boolean,
@@ -121,7 +125,8 @@ fun PatchOptionsScreenContent(
var showPkgUnlockDialog by rememberSaveable { mutableStateOf(false) }
if (showPkgUnlockDialog) {
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 = {
showPkgUnlockDialog = false
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))
@@ -1,9 +1,11 @@
package com.meowarex.rlmobile.ui.screens.patchopts
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.math.roundToInt
@Serializable
@@ -157,74 +159,17 @@ data class SmaliEncode(
}
}
fun builtinPatchSpecs(resolve: (Int) -> String): List<PatchSpec> =
KnownPatch.All.map { patch ->
PatchSpec(
id = patch.name,
order = patch.order,
fileNames = patch.fileNames,
extensionFiles = patch.extensionFiles,
title = resolve(patch.titleRes),
description = resolve(patch.descRes),
defaultEnabled = patch.default.isEnabled,
requires = patch.requires.map { it.name },
disables = patch.disables.map { it.name },
variants = patch.variants.map { VariantSpec(resolve(it.titleRes), it.fileNames, it.extensionFiles) },
defaultVariantIndex = patch.defaultVariantIndex,
advancedOptions = patch.advancedOptions.map { it.toSpec(resolve) },
category = patch.category,
pathLocked = patch.pathLocked,
)
}
/** Asset filename of the bundled fallback manifest snapshot (a copy of the release manifest.json). */
const val BUNDLED_MANIFEST_ASSET = "patches-manifest.json"
private fun PatchOption.toSpec(resolve: (Int) -> String): OptionSpec = when (this) {
is PatchOption.Toggle -> OptionSpec.Toggle(
key = key,
title = resolve(titleRes),
description = resolve(descRes),
default = default,
fileNames = fileNames,
extensionFiles = extensionFiles,
inline = inline,
requiresVariant = requiresVariant,
requiresOption = requiresOption,
hidesVariants = hidesVariants,
relabelVariants = relabelVariants.mapValues { resolve(it.value) },
token = token,
)
is PatchOption.Slider -> OptionSpec.Slider(
key = key,
title = resolve(titleRes),
description = resolve(descRes),
default = default,
min = valueRange.start,
max = valueRange.endInclusive,
steps = steps,
displayAsPercent = displayAsPercent,
unit = unitRes?.let(resolve),
token = token,
encode = encode,
)
is PatchOption.Choice -> OptionSpec.Choice(
key = key,
title = resolve(titleRes),
description = 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,
)
// Fallback patch list (snapshot of the release manifest bundled in the app's assets)
fun builtinPatchSpecs(context: Context, json: Json): List<PatchSpec> = try {
context.assets.open(BUNDLED_MANIFEST_ASSET).use { it.readBytes() }
.let { json.decodeFromString(PatchManifest.serializer(), it.decodeToString()) }
.patches
.sortedBy { it.order }
} catch (t: Throwable) {
emptyList()
}
@Immutable
@@ -5,6 +5,8 @@ import android.app.Activity
import android.content.*
import android.content.pm.PackageManager
import android.content.res.Resources
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.*
import android.provider.Settings
@@ -63,6 +65,15 @@ fun Context.getPackageVersion(pkg: String): Pair<String?, Int> {
.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 {
return try {
packageManager.getPackageInfo(packageName, 0)
+3 -1
View File
@@ -180,7 +180,7 @@
<string name="install_error_timeout">Installer timed out</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_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_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_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_advanced">Advanced</string>
<string name="patchopts_patches_title">Patches</string>
+16 -16
View File
@@ -103,8 +103,21 @@
"default": true
},
{
"id": "PlayerOneHanded",
"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"
],
@@ -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)",
"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",
"order": 50,
@@ -307,7 +307,7 @@
"category": "integration",
"pathLocked": true,
"fileNames": [
"waze-media-browser.patch"
"integration-waze-playback.patch"
],
"extensionFiles": [
"radiant/WazeInitReceiver.smali",
@@ -354,4 +354,4 @@
"default": false
}
]
}
}
+11
View File
@@ -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
-9
View File
@@ -22,15 +22,6 @@
iget-object v0, v3, Lcom/tidal/android/feature/playerscreen/ui/s$a;->c:Lcom/tidal/android/feature/playerscreen/ui/d;
.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
+++ b/com/tidal/android/feature/playerscreen/ui/composables/k2.smali
@@ -632,6 +632,39 @@