4 Commits

Author SHA1 Message Date
meoware.exe e7b69a8deb Merge pull request #21 from meowarex/dev
Overhaul Developer Options <3
2026-05-24 21:49:49 +10:00
meoware.exe 324a6eb6c8 Overhaul Developer Options <3 2026-05-24 21:49:25 +10:00
meoware.exe c5962ad1a8 Merge pull request #20 from meowarex/dev
Overhaul Update System <3
2026-05-24 21:19:16 +10:00
meoware.exe 77ab041b97 Overhaul Update System <3 2026-05-24 21:18:53 +10:00
24 changed files with 839 additions and 82 deletions
+6 -4
View File
@@ -48,6 +48,8 @@ jobs:
run: |
mv ./dist/app-release.apk ./dist/rl-manager.apk
cp patches/data.json ./dist/data.json
# Point each release's data.json at its own assets so historical releases stay self-contained.
sed -i "s|releases/download/latest/|releases/download/v${{ needs.Version.outputs.version }}/|g" ./dist/data.json
cd patches && zip -r ../dist/patches.zip . -x "data.json" && cd ..
tidal_src=$(find tidal-apk -maxdepth 1 \( -name "*.apk" -o -name "*.apkm" \) | head -1)
@@ -67,10 +69,10 @@ jobs:
fi
- name: Publish release
uses: marvinpinto/action-automatic-releases@latest
uses: softprops/action-gh-release@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
automatic_release_tag: latest
tag_name: v${{ needs.Version.outputs.version }}
name: v${{ needs.Version.outputs.version }}
prerelease: false
title: v${{ needs.Version.outputs.version }}
make_latest: "true"
files: ./dist/**
@@ -142,7 +142,7 @@ class MainActivity : ComponentActivity() {
}
val targetDir = when (componentType) {
"injector" -> paths.customInjectorsDir
"tidal" -> paths.customTidalApksDir
"patches" -> paths.customPatchesDir
else -> {
Log.w(BuildConfig.TAG, "Extra $EXTRA_COMPONENT_TYPE is not a valid value!")
@@ -26,7 +26,7 @@ class PathManager(
val customComponentsDir = patchingDir.resolve("custom")
val customInjectorsDir = customComponentsDir.resolve("injector")
val customTidalApksDir = customComponentsDir.resolve("tidal")
val customPatchesDir = customComponentsDir.resolve("patches")
@@ -54,7 +54,7 @@ class PathManager(
.resolve("patches")
.resolve("$version.zip")
fun customInjectors() = customInjectorsDir.listFiles()?.asList() ?: emptyList()
fun customTidalApks() = customTidalApksDir.listFiles()?.asList() ?: emptyList()
fun customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
}
@@ -15,4 +15,8 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
var showNetworkWarning by booleanPreference("show_network_warning", true)
var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
var autoUpdateCheck by booleanPreference("auto_update_check", true)
var lastSeenManagerVersion by stringPreference("last_seen_manager_version", "")
var lastSeenPatchesVersion by stringPreference("last_seen_patches_version", "")
var lastSeenTidalVersionCode by intPreference("last_seen_tidal_version_code", -1)
}
@@ -17,7 +17,7 @@ class TidalPatchRunner(
RestoreDownloadsStep(),
// Download
DownloadTidalStep(),
DownloadTidalStep(options.customTidalApk),
DownloadPatchesStep(options.customPatches),
CopyDependenciesStep(),
@@ -5,12 +5,17 @@ import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.patcher.StepRunner
import com.meowarex.rlmobile.patcher.steps.base.DownloadStep
import com.meowarex.rlmobile.patcher.steps.base.StepState
import com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.FileNotFoundException
@Stable
class DownloadTidalStep : DownloadStep<Int>(), KoinComponent {
class DownloadTidalStep(
private val custom: PatchComponent?,
) : DownloadStep<Int>(), KoinComponent {
private val paths: PathManager by inject()
override val localizedName = R.string.patch_step_dl_tidal_apk
@@ -22,5 +27,23 @@ class DownloadTidalStep : DownloadStep<Int>(), KoinComponent {
container.getStep<FetchInfoStep>().data.tidalApkUrl
override fun getStoredFile(container: StepRunner) =
paths.cachedTidalApk(getVersion(container))
custom?.getFile(paths) ?: paths.cachedTidalApk(getVersion(container))
override suspend fun execute(container: StepRunner) {
if (custom != null) {
container.log("Using custom TIDAL APK with version ${custom.version} imported ${custom.timestamp}")
if (!custom.getFile(paths).exists()) {
throw FileNotFoundException(
"Selected custom TIDAL APK does not exist on disk! If this is an update, " +
"updates cannot occur when the originally selected custom component has been deleted."
)
}
state = StepState.Skipped
return
}
super.execute(container)
}
}
@@ -0,0 +1,25 @@
package com.meowarex.rlmobile.ui.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun Tag(text: String) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(6.dp),
) {
Text(
text = text,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp),
)
}
}
@@ -30,6 +30,12 @@ private fun ComponentOptionsScreenPreview(
selected = parameters.selected,
onSelectComponent = {},
onDeleteComponent = {},
onImportFromUri = {},
releasesExpanded = false,
releasesState = com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsModel.ReleasesState.Idle,
onToggleReleases = {},
onImportRelease = {},
importingReleaseTag = null,
onBackPressed = {},
)
}
@@ -44,27 +50,27 @@ private data class ComponentOptionsParameters(
private class ComponentOptionsParametersProvider : PreviewParameterProvider<ComponentOptionsParameters> {
private val components = persistentListOf(
PatchComponent(
type = PatchComponent.Type.Injector,
type = PatchComponent.Type.TidalApk,
version = SemVer(1, 2, 3),
timestamp = Clock.System.now(),
),
PatchComponent(
type = PatchComponent.Type.Injector,
type = PatchComponent.Type.TidalApk,
version = SemVer(2, 3, 1),
timestamp = Clock.System.now() - 10.minutes,
),
PatchComponent(
type = PatchComponent.Type.Injector,
type = PatchComponent.Type.TidalApk,
version = SemVer(2, 3, 1),
timestamp = Clock.System.now() - 1.days,
),
PatchComponent(
type = PatchComponent.Type.Injector,
type = PatchComponent.Type.TidalApk,
version = SemVer(0, 0, 1),
timestamp = Clock.System.now() - 10.hours,
),
PatchComponent(
type = PatchComponent.Type.Injector,
type = PatchComponent.Type.TidalApk,
version = SemVer(3, 0, 2),
timestamp = Clock.System.now() - 7.days,
),
@@ -72,7 +78,7 @@ private class ComponentOptionsParametersProvider : PreviewParameterProvider<Comp
override val values = sequenceOf(
ComponentOptionsParameters(
componentType = PatchComponent.Type.Injector,
componentType = PatchComponent.Type.TidalApk,
components = components,
selected = null,
),
@@ -30,8 +30,8 @@ private fun PatchOptionsScreenPreview(
packageName = parameters.packageName,
packageNameState = parameters.packageNameState,
setPackageName = {},
customInjector = parameters.customInjector,
onSelectCustomInjector = {},
customTidalApk = parameters.customTidalApk,
onSelectCustomTidalApk = {},
customPatches = parameters.customPatches,
onSelectCustomPatches = {},
enabledPatchCount = KnownPatch.All.size,
@@ -51,7 +51,7 @@ private data class PatchOptionsParameters(
val appNameIsError: Boolean,
val packageName: String,
val packageNameState: PackageNameState,
val customInjector: PatchComponent?,
val customTidalApk: PatchComponent?,
val customPatches: PatchComponent?,
val isConfigValid: Boolean,
)
@@ -66,7 +66,7 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
appNameIsError = false,
packageName = PatchOptions.Default.packageName,
packageNameState = PackageNameState.Ok,
customInjector = null,
customTidalApk = null,
customPatches = null,
isConfigValid = true,
),
@@ -78,7 +78,7 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
appNameIsError = true,
packageName = "a b",
packageNameState = PackageNameState.Invalid,
customInjector = null,
customTidalApk = null,
customPatches = null,
isConfigValid = false,
),
@@ -90,8 +90,8 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
appNameIsError = false,
packageName = PatchOptions.Default.packageName,
packageNameState = PackageNameState.Taken,
customInjector = PatchComponent(
type = PatchComponent.Type.Injector,
customTidalApk = PatchComponent(
type = PatchComponent.Type.TidalApk,
version = SemVer(1, 2, 3),
timestamp = Clock.System.now(),
),
@@ -1,26 +1,44 @@
package com.meowarex.rlmobile.ui.screens.componentopts
import android.app.Application
import android.net.Uri
import android.provider.OpenableColumns
import android.util.Log
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.screenModelScope
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.manager.download.KtorDownloadManager
import com.meowarex.rlmobile.network.models.GithubRelease
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
import com.meowarex.rlmobile.network.utils.SemVer
import com.meowarex.rlmobile.network.utils.fold
import com.meowarex.rlmobile.ui.util.ScreenModelWithResult
import com.meowarex.rlmobile.ui.util.ScreenResultKey
import com.meowarex.rlmobile.util.*
import kotlinx.coroutines.launch
import java.io.File
import kotlin.time.Instant
class ComponentOptionsModel(
screenResultKey: ScreenResultKey,
private val paths: PathManager,
private val context: Application,
private val github: RadiantLyricsGithubService,
private val downloader: KtorDownloadManager,
) : ScreenModelWithResult<PatchComponent?>(screenResultKey) {
val components = mutableStateListOf<PatchComponent>()
var selected by mutableStateOf<PatchComponent?>(null)
private set
var releasesExpanded by mutableStateOf(false)
private set
var releasesState by mutableStateOf<ReleasesState>(ReleasesState.Idle)
private set
var importingReleaseTag by mutableStateOf<String?>(null)
private set
fun selectComponent(component: PatchComponent?) {
selected = component
}
@@ -39,7 +57,7 @@ class ComponentOptionsModel(
*/
suspend fun refreshComponents(type: PatchComponent.Type) {
val files = when (type) {
PatchComponent.Type.Injector -> paths.customInjectors()
PatchComponent.Type.TidalApk -> paths.customTidalApks()
PatchComponent.Type.Patches -> paths.customSmaliPatches()
}
@@ -64,7 +82,157 @@ class ComponentOptionsModel(
}
}
fun importFromUri(uri: Uri, type: PatchComponent.Type) = screenModelScope.launchIO {
try {
val targetDir = when (type) {
PatchComponent.Type.TidalApk -> paths.customTidalApksDir
PatchComponent.Type.Patches -> paths.customPatchesDir
}
val ext = when (type) {
PatchComponent.Type.TidalApk -> "apk"
PatchComponent.Type.Patches -> "zip"
}
targetDir.mkdirs()
val tempFile = targetDir.resolve("import-${System.currentTimeMillis()}.tmp")
context.contentResolver.openInputStream(uri)?.use { input ->
tempFile.outputStream().use { output -> input.copyTo(output) }
} ?: throw IllegalStateException("Could not open input stream for $uri")
val sourceDisplayName = queryDisplayName(uri)
val version = when (type) {
PatchComponent.Type.TidalApk -> readApkVersion(tempFile)
?: extractVersionFromName(sourceDisplayName)
?: FALLBACK_VERSION
PatchComponent.Type.Patches -> extractVersionFromName(sourceDisplayName)
?: FALLBACK_VERSION
}
val finalName = "${System.currentTimeMillis()}_$version.$ext"
val finalFile = targetDir.resolve(finalName)
if (!tempFile.renameTo(finalFile)) {
tempFile.copyTo(finalFile, overwrite = true)
tempFile.delete()
}
refreshComponents(type)
mainThread { context.showToast(R.string.intent_import_component_success, finalName) }
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to import custom component from $uri", t)
mainThread { context.showToast(R.string.intent_import_component_failure) }
}
}
private fun queryDisplayName(uri: Uri): String? = try {
context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
?.use { cursor ->
if (cursor.moveToFirst()) cursor.getString(0) else null
}
} catch (_: Throwable) {
null
}
private fun readApkVersion(apkFile: File): String? = try {
@Suppress("DEPRECATION")
context.packageManager.getPackageArchiveInfo(apkFile.absolutePath, 0)
?.versionName
?.let { SemVer.parseOrNull(it) }
?.toString()
} catch (_: Throwable) {
null
}
private fun extractVersionFromName(name: String?): String? {
if (name == null) return null
return """(\d+\.\d+\.\d+)""".toRegex().find(name)?.value
}
fun toggleReleasesExpanded(type: PatchComponent.Type) {
releasesExpanded = !releasesExpanded
if (releasesExpanded && releasesState is ReleasesState.Idle) {
loadReleases(type)
}
}
private fun loadReleases(type: PatchComponent.Type) = screenModelScope.launchIO {
releasesState = ReleasesState.Loading
github.getManagerReleases().fold(
success = { all ->
val assetName = assetNameFor(type)
val filtered = all.filter { release ->
release.assets.any { it.name == assetName }
}
releasesState = ReleasesState.Loaded(filtered)
},
fail = {
Log.w(BuildConfig.TAG, "Failed to load GitHub releases", it)
releasesState = ReleasesState.Failed
},
)
}
fun importFromRelease(release: GithubRelease, type: PatchComponent.Type) = screenModelScope.launchIO {
val assetName = assetNameFor(type)
val asset = release.assets.find { it.name == assetName } ?: run {
mainThread { context.showToast(R.string.intent_import_component_failure) }
return@launchIO
}
val targetDir = when (type) {
PatchComponent.Type.TidalApk -> paths.customTidalApksDir
PatchComponent.Type.Patches -> paths.customPatchesDir
}
targetDir.mkdirs()
importingReleaseTag = release.tagName
try {
val tempFile = targetDir.resolve("release-${System.currentTimeMillis()}.tmp")
val result = downloader.download(asset.browserDownloadUrl, tempFile)
if (result !is com.meowarex.rlmobile.manager.download.IDownloadManager.Result.Success) {
tempFile.delete()
mainThread { context.showToast(R.string.intent_import_component_failure) }
return@launchIO
}
val ext = when (type) {
PatchComponent.Type.TidalApk -> "apk"
PatchComponent.Type.Patches -> "zip"
}
val version = SemVer.parseOrNull(release.tagName.removePrefix("v"))?.toString()
?: FALLBACK_VERSION
val finalName = "${System.currentTimeMillis()}_$version.$ext"
val finalFile = targetDir.resolve(finalName)
if (!tempFile.renameTo(finalFile)) {
tempFile.copyTo(finalFile, overwrite = true)
tempFile.delete()
}
refreshComponents(type)
mainThread { context.showToast(R.string.intent_import_component_success, finalName) }
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to import release ${release.tagName}", t)
mainThread { context.showToast(R.string.intent_import_component_failure) }
} finally {
importingReleaseTag = null
}
}
private fun assetNameFor(type: PatchComponent.Type) = when (type) {
PatchComponent.Type.TidalApk -> "tidal-stock.apk"
PatchComponent.Type.Patches -> "patches.zip"
}
override fun onDispose() {
screenModelScope.launch { setResult(selected) }
}
sealed interface ReleasesState {
data object Idle : ReleasesState
data object Loading : ReleasesState
data class Loaded(val releases: List<GithubRelease>) : ReleasesState
data object Failed : ReleasesState
}
companion object {
private const val FALLBACK_VERSION = "0.0.0"
}
}
@@ -1,13 +1,20 @@
package com.meowarex.rlmobile.ui.screens.componentopts
import android.net.Uri
import android.os.Parcelable
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.koin.koinScreenModel
@@ -61,6 +68,12 @@ class ComponentOptionsScreen(
selected = model.selected,
onSelectComponent = model::selectComponent,
onDeleteComponent = model::deleteComponent,
onImportFromUri = { uri -> model.importFromUri(uri, componentType) },
releasesExpanded = model.releasesExpanded,
releasesState = model.releasesState,
onToggleReleases = { model.toggleReleasesExpanded(componentType) },
onImportRelease = { release -> model.importFromRelease(release, componentType) },
importingReleaseTag = model.importingReleaseTag,
onBackPressed = { navigator.back(null) },
)
}
@@ -73,8 +86,22 @@ fun ComponentOptionsScreenContent(
selected: PatchComponent?,
onSelectComponent: (PatchComponent?) -> Unit,
onDeleteComponent: (PatchComponent) -> Unit,
onImportFromUri: (Uri) -> Unit,
releasesExpanded: Boolean,
releasesState: ComponentOptionsModel.ReleasesState,
onToggleReleases: () -> Unit,
onImportRelease: (com.meowarex.rlmobile.network.models.GithubRelease) -> Unit,
importingReleaseTag: String?,
onBackPressed: () -> Unit,
) {
val mime = when (componentType) {
PatchComponent.Type.TidalApk -> "application/vnd.android.package-archive"
PatchComponent.Type.Patches -> "application/zip"
}
val filePicker = rememberLauncherForActivityResult(
ActivityResultContracts.OpenDocument(),
) { uri -> uri?.let(onImportFromUri) }
Scaffold(
topBar = { ComponentOptionsAppBar(componentType = componentType) },
) { paddingValues ->
@@ -98,6 +125,16 @@ fun ComponentOptionsScreenContent(
}
}
item(key = "RELEASES_ACCORDION") {
ReleasesAccordion(
expanded = releasesExpanded,
state = releasesState,
importingTag = importingReleaseTag,
onToggle = onToggleReleases,
onImport = onImportRelease,
)
}
items(
items = components,
contentType = { "COMPONENT" },
@@ -112,6 +149,10 @@ fun ComponentOptionsScreenContent(
)
}
item(key = "BROWSE") {
BrowseImportCard(onClick = { filePicker.launch(arrayOf(mime)) })
}
item("EXIT_BTN") {
Row(
horizontalArrangement = Arrangement.End,
@@ -129,3 +170,194 @@ fun ComponentOptionsScreenContent(
}
}
}
@Composable
private fun ReleasesAccordion(
expanded: Boolean,
state: ComponentOptionsModel.ReleasesState,
importingTag: String?,
onToggle: () -> Unit,
onImport: (com.meowarex.rlmobile.network.models.GithubRelease) -> Unit,
) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth(),
) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onToggle)
.padding(horizontal = 16.dp, vertical = 14.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.componentopts_releases_title),
style = MaterialTheme.typography.titleMedium,
)
Text(
text = stringResource(R.string.componentopts_releases_desc),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.65f),
)
}
Icon(
painter = painterResource(
if (expanded) R.drawable.ic_arrow_up_small else R.drawable.ic_arrow_down_small
),
contentDescription = null,
tint = LocalContentColor.current.copy(alpha = 0.7f),
)
}
if (expanded) {
HorizontalDivider(
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f),
)
ReleasesContent(
state = state,
importingTag = importingTag,
onImport = onImport,
)
}
}
}
}
@Composable
private fun ReleasesContent(
state: ComponentOptionsModel.ReleasesState,
importingTag: String?,
onImport: (com.meowarex.rlmobile.network.models.GithubRelease) -> Unit,
) {
when (state) {
is ComponentOptionsModel.ReleasesState.Idle,
ComponentOptionsModel.ReleasesState.Loading -> Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
) { CircularProgressIndicator(modifier = Modifier.size(24.dp)) }
ComponentOptionsModel.ReleasesState.Failed -> Text(
text = stringResource(R.string.network_load_fail),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.error,
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
)
is ComponentOptionsModel.ReleasesState.Loaded -> {
if (state.releases.isEmpty()) {
Text(
text = stringResource(R.string.componentopts_releases_empty),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.65f),
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
)
} else {
Column {
state.releases.forEachIndexed { index, release ->
ReleaseRow(
release = release,
importing = importingTag == release.tagName,
anyImporting = importingTag != null,
onImport = { onImport(release) },
)
if (index != state.releases.lastIndex) {
HorizontalDivider(
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f),
)
}
}
}
}
}
}
}
@Composable
private fun ReleaseRow(
release: com.meowarex.rlmobile.network.models.GithubRelease,
importing: Boolean,
anyImporting: Boolean,
onImport: () -> Unit,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = release.tagName,
style = MaterialTheme.typography.titleSmall,
)
Text(
text = release.name ?: release.tagName,
style = MaterialTheme.typography.bodySmall,
color = LocalContentColor.current.copy(alpha = 0.55f),
)
}
FilledTonalButton(
onClick = onImport,
enabled = !anyImporting,
) {
if (importing) {
CircularProgressIndicator(
strokeWidth = 2.dp,
modifier = Modifier.size(16.dp),
)
} else {
Text(stringResource(R.string.action_install))
}
}
}
}
@Composable
private fun BrowseImportCard(onClick: () -> Unit) {
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_add),
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.componentopts_browse_title),
style = MaterialTheme.typography.titleMedium,
)
Text(
text = stringResource(R.string.componentopts_browse_desc),
style = MaterialTheme.typography.bodyMedium,
color = LocalContentColor.current.copy(alpha = 0.65f),
)
}
}
}
}
@@ -34,8 +34,8 @@ data class PatchComponent(
@Parcelize
@Serializable
enum class Type : Parcelable {
@SerialName("injector")
Injector,
@SerialName("tidal")
TidalApk,
@SerialName("patches")
Patches,
@@ -47,11 +47,11 @@ data class PatchComponent(
*/
fun getFile(paths: PathManager): File {
val dir = when (type) {
Type.Injector -> paths.customInjectorsDir
Type.TidalApk -> paths.customTidalApksDir
Type.Patches -> paths.customPatchesDir
}
val ext = when (type) {
Type.Injector -> "dex"
Type.TidalApk -> "apk"
Type.Patches -> "zip"
}
@@ -19,6 +19,7 @@ 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.PreferencesManager
import com.meowarex.rlmobile.network.models.GithubCommit
import com.meowarex.rlmobile.network.models.RLBuildInfo
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
@@ -28,6 +29,7 @@ import com.meowarex.rlmobile.patcher.InstallMetadata
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsScreen
import com.meowarex.rlmobile.ui.util.TidalVersion
import com.meowarex.rlmobile.ui.widgets.managerupdate.VersionDelta
import com.meowarex.rlmobile.util.*
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
@@ -39,11 +41,15 @@ class HomeModel(
private val application: Application,
private val github: RadiantLyricsGithubService,
private val json: Json,
private val prefs: PreferencesManager,
) : ScreenModel {
var state by mutableStateOf<HomeState>(HomeState.Loading)
private set
var managerUpdateDeltas by mutableStateOf<List<VersionDelta>?>(null)
private set
val commits = Pager(PagingConfig(pageSize = 30)) {
CommitsPagingSource(github)
}.flow.cachedIn(screenModelScope)
@@ -51,10 +57,103 @@ class HomeModel(
private val refreshingLock = Mutex()
private var remoteDataJson: RLBuildInfo? = null
private val initialPrefManagerVersion: String = prefs.lastSeenManagerVersion
private val initialPrefPatchesVersion: String = prefs.lastSeenPatchesVersion
private val initialPrefTidalVersionCode: Int = prefs.lastSeenTidalVersionCode
private var managerUpdateChecked = false
init {
refresh()
}
fun dismissManagerUpdate() {
managerUpdateDeltas = null
commitVersionPrefs()
}
private fun commitVersionPrefs() {
prefs.lastSeenManagerVersion = BuildConfig.VERSION_NAME
remoteDataJson?.let {
prefs.lastSeenPatchesVersion = it.patchesVersion.toString()
prefs.lastSeenTidalVersionCode = it.tidalVersionCode
}
}
private fun maybeCheckManagerUpdate(installedPkg: PackageInfo?) {
if (managerUpdateChecked) return
managerUpdateChecked = true
val installMetadata = installedPkg?.packageName?.let(::loadInstallMetadata)
val current = BuildConfig.VERSION_NAME
val previousManager = initialPrefManagerVersion.ifEmpty {
installMetadata?.managerVersion?.toString().orEmpty()
}
when {
previousManager.isEmpty() -> commitVersionPrefs()
previousManager == current -> commitVersionPrefs()
else -> managerUpdateDeltas =
buildDeltas(previousManager, current, installMetadata, installedPkg)
}
}
private fun buildDeltas(
previousManager: String,
currentManager: String,
installMetadata: InstallMetadata?,
installedPkg: PackageInfo?,
): List<VersionDelta> = buildList {
add(
VersionDelta(
label = application.getString(R.string.manager_update_row_manager),
iconRes = R.drawable.ic_sparkle,
from = previousManager,
to = currentManager,
tag = application.getString(R.string.manager_update_tag_complete),
)
)
val remote = remoteDataJson
val currentPatches = remote?.patchesVersion?.toString()
val previousPatches = initialPrefPatchesVersion.ifEmpty {
installMetadata?.patchesVersion?.toString().orEmpty()
}
val patchesFrom = previousPatches.takeIf { it.isNotEmpty() }
val patchesTo = currentPatches ?: previousPatches.ifEmpty { "?" }
add(
VersionDelta(
label = application.getString(R.string.manager_update_row_patches),
iconRes = R.drawable.ic_extension,
from = patchesFrom,
to = patchesTo,
tag = if (patchesFrom != null && patchesFrom != patchesTo)
application.getString(R.string.manager_update_tag_available) else null,
)
)
val currentTidal = remote?.tidalVersionCode
@Suppress("DEPRECATION")
val installedTidalVersionCode = installedPkg?.versionCode ?: -1
val previousTidal = if (initialPrefTidalVersionCode > 0) initialPrefTidalVersionCode
else installedTidalVersionCode
val tidalFrom = previousTidal.takeIf { it > 0 }?.toString()
val tidalTo = currentTidal?.toString()
?: previousTidal.takeIf { it > 0 }?.toString()
?: "?"
add(
VersionDelta(
label = application.getString(R.string.manager_update_row_tidal),
iconRes = R.drawable.ic_music_note,
from = tidalFrom,
to = tidalTo,
tag = if (tidalFrom != null && tidalFrom != tidalTo)
application.getString(R.string.manager_update_tag_available) else null,
)
)
}
fun refresh(delay: Boolean = false) = screenModelScope.launchIO {
if (refreshingLock.isLocked) return@launchIO
if (delay) {
@@ -75,6 +174,7 @@ class HomeModel(
install = install,
latestTidalVersionCode = latest,
)
maybeCheckManagerUpdate(pkg)
}
}
}
@@ -89,7 +189,7 @@ class HomeModel(
openAppInfo(current.packageName)
}
fun createReinstallScreen(): PatchOptionsScreen? {
fun createRepatchScreen(): PatchOptionsScreen? {
val current = (state as? HomeState.Loaded)?.install ?: return null
return createPrefilledPatchOptsScreen(current.packageName)
}
@@ -112,20 +212,21 @@ class HomeModel(
}
fun createPrefilledPatchOptsScreen(packageName: String): PatchOptionsScreen {
val metadata = try {
val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0)
val metadataFile = ZipReader(applicationInfo.publicSourceDir)
.use { it.openEntry("rlmobile.json")?.read() }
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to parse install metadata for $packageName", t)
null
}
val patchOptions = metadata?.options ?: PatchOptions.Default.copy(packageName = packageName)
val patchOptions = loadInstallMetadata(packageName)?.options
?: PatchOptions.Default.copy(packageName = packageName)
return PatchOptionsScreen(prefilledOptions = patchOptions)
}
private fun loadInstallMetadata(packageName: String): InstallMetadata? = try {
val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0)
val metadataBytes = ZipReader(applicationInfo.publicSourceDir)
.use { it.openEntry("rlmobile.json")?.read() }
metadataBytes?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to parse install metadata for $packageName", t)
null
}
private fun fetchInstalled(): PackageInfo? = application.packageManager
.getInstalledPackages(PackageManager.GET_META_DATA)
.firstOrNull { it.applicationInfo?.metaData?.containsKey("isRadiantLyrics") == true }
@@ -138,7 +239,8 @@ class HomeModel(
return InstallData(
name = pm.getApplicationLabel(info).toString(),
packageName = packageName,
isUpToDate = isInstallationUpToDate(this),
tidalUpToDate = isTidalUpToDate(this),
patchesUpToDate = isPatchesUpToDate(this),
icon = pm.getApplicationIcon(info).toBitmap().asImageBitmap().let(::BitmapPainter),
version = TidalVersion.Existing(
type = TidalVersion.parseVersionType(versionCode),
@@ -170,11 +272,14 @@ class HomeModel(
)
}
private fun isInstallationUpToDate(pkg: PackageInfo): Boolean? {
private fun isTidalUpToDate(pkg: PackageInfo): Boolean? {
val remote = remoteDataJson ?: return null
@Suppress("DEPRECATION") val versionCode = pkg.versionCode
if (remote.tidalVersionCode != versionCode) return false
return remote.tidalVersionCode == versionCode
}
private fun isPatchesUpToDate(pkg: PackageInfo): Boolean? {
val remote = remoteDataJson ?: return null
val apkPath = pkg.applicationInfo?.publicSourceDir ?: return false
val installMetadata = try {
val mf = ZipReader(apkPath).use { it.openEntry("rlmobile.json")?.read() } ?: return false
@@ -182,8 +287,6 @@ class HomeModel(
} catch (t: Throwable) {
return false
}
if (installMetadata.options.customPatches != null) return true
return remote.patchesVersion == installMetadata.patchesVersion
}
}
@@ -29,11 +29,13 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.SegmentedButton
import com.meowarex.rlmobile.ui.components.Tag
import com.meowarex.rlmobile.ui.screens.about.AboutScreen
import com.meowarex.rlmobile.ui.screens.home.components.CommitList
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreen
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsScreen
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
import com.meowarex.rlmobile.ui.widgets.managerupdate.ManagerUpdateDialog
import com.meowarex.rlmobile.util.*
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@@ -106,9 +108,9 @@ class HomeScreen : Screen, Parcelable {
state = state,
commits = model.commits,
onInstall = { navigator.pushOnce(PatchOptionsScreen()) },
onReinstall = {
onRepatch = {
scope.launchIO {
val screen = model.createReinstallScreen() ?: return@launchIO
val screen = model.createRepatchScreen() ?: return@launchIO
mainThread { navigator.push(screen) }
}
},
@@ -116,6 +118,13 @@ class HomeScreen : Screen, Parcelable {
onInfo = model::openCurrentAppInfo,
)
}
model.managerUpdateDeltas?.let { deltas ->
ManagerUpdateDialog(
deltas = deltas,
onDismiss = model::dismissManagerUpdate,
)
}
}
}
}
@@ -126,12 +135,13 @@ private fun ColumnScope.HomeContent(
state: HomeState.Loaded,
commits: kotlinx.coroutines.flow.Flow<androidx.paging.PagingData<com.meowarex.rlmobile.network.models.GithubCommit>>,
onInstall: () -> Unit,
onReinstall: () -> Unit,
onRepatch: () -> Unit,
onLaunch: () -> Unit,
onInfo: () -> Unit,
) {
val install = state.install
val currentVersionName = install?.version?.let { "v${it.toString()}" }
val currentVersionName = (install?.version as? com.meowarex.rlmobile.ui.util.TidalVersion.Existing)
?.let { "v${it.name} (build ${it.code})" }
val latestVersionName = state.latestTidalVersionCode?.let { "build $it" }
val fallbackPainter = if (install?.icon == null) {
@@ -181,8 +191,17 @@ private fun ColumnScope.HomeContent(
}
}
val patchesBehind = install != null && install.patchesUpToDate == false
val tidalBehind = install != null && install.tidalUpToDate == false
AnimatedVisibility(visible = patchesBehind || tidalBehind) {
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
if (patchesBehind) Tag(text = "New Patches!")
if (tidalBehind) Tag(text = "TIDAL Update!")
}
}
Button(
onClick = if (install == null) onInstall else onReinstall,
onClick = if (install == null) onInstall else onRepatch,
enabled = state.latestTidalVersionCode != null,
modifier = Modifier.fillMaxWidth(),
) {
@@ -190,7 +209,7 @@ private fun ColumnScope.HomeContent(
state.latestTidalVersionCode == null -> "Loading…"
install == null -> "Install"
install.isUpToDate == false -> "Update"
else -> "Reinstall"
else -> "Repatch"
}
Text(
text = label,
@@ -10,5 +10,12 @@ data class InstallData(
val packageName: String,
val version: TidalVersion,
val icon: BitmapPainter,
val isUpToDate: Boolean?,
)
val tidalUpToDate: Boolean?,
val patchesUpToDate: Boolean?,
) {
val isUpToDate: Boolean?
get() = when {
tidalUpToDate == null || patchesUpToDate == null -> null
else -> tidalUpToDate && patchesUpToDate
}
}
@@ -25,10 +25,7 @@ data class PatchOptions(
*/
val debuggable: Boolean,
/**
* A custom build of injector that was used rather than the latest.
*/
val customInjector: PatchComponent? = null,
val customTidalApk: PatchComponent? = null,
/**
* A custom smali patches bundle that was used rather than the latest.
@@ -42,9 +39,9 @@ data class PatchOptions(
appName = "TIDAL",
packageName = "com.aspiro.tidal",
debuggable = false,
customInjector = null,
customTidalApk = null,
customPatches = null,
disabledPatches = emptySet(),
disabledPatches = (KnownPatch.DebugMenuUnlock.fileNames + KnownPatch.EnableLegacyUi.fileNames).toSet(),
)
}
}
@@ -81,16 +81,16 @@ class PatchOptionsModel(
val enabledPatchCount: Int
get() = KnownPatch.All.count { isPatchEnabled(it) }
var customInjector by mutableStateOf<PatchComponent?>(null)
var customTidalApk by mutableStateOf<PatchComponent?>(null)
private set
var customPatches by mutableStateOf<PatchComponent?>(null)
private set
fun selectCustomInjector(navigator: Navigator) = screenModelScope.launch {
customInjector = navigator.pushForResult(
fun selectCustomTidalApk(navigator: Navigator) = screenModelScope.launch {
customTidalApk = navigator.pushForResult(
ComponentOptionsScreen(
default = customInjector,
componentType = PatchComponent.Type.Injector,
default = customTidalApk,
componentType = PatchComponent.Type.TidalApk,
)
)
}
@@ -120,7 +120,7 @@ class PatchOptionsModel(
appName = appName,
packageName = packageName,
debuggable = debuggable,
customInjector = customInjector,
customTidalApk = customTidalApk,
customPatches = customPatches,
disabledPatches = disabledPatches,
)
@@ -55,9 +55,9 @@ class PatchOptionsScreen(
packageNameState = model.packageNameState,
setPackageName = model::changePackageName,
customInjector = model.customInjector,
customTidalApk = model.customTidalApk,
customPatches = model.customPatches,
onSelectCustomInjector = { model.selectCustomInjector(navigator) },
onSelectCustomTidalApk = { model.selectCustomTidalApk(navigator) },
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
enabledPatchCount = model.enabledPatchCount,
@@ -88,8 +88,8 @@ fun PatchOptionsScreenContent(
packageNameState: PackageNameState,
setPackageName: (String) -> Unit,
customInjector: PatchComponent?,
onSelectCustomInjector: () -> Unit,
customTidalApk: PatchComponent?,
onSelectCustomTidalApk: () -> Unit,
customPatches: PatchComponent?,
onSelectCustomPatches: () -> Unit,
@@ -180,14 +180,14 @@ fun PatchOptionsScreenContent(
)
IconPatchOption(
icon = painterResource(R.drawable.ic_extension),
name = stringResource(R.string.patchopts_custom_injector_title),
description = stringResource(R.string.patchopts_custom_injector_desc),
modifier = Modifier.clickable(onClick = onSelectCustomInjector),
icon = painterResource(R.drawable.ic_music_note),
name = stringResource(R.string.patchopts_custom_tidal_apk_title),
description = stringResource(R.string.patchopts_custom_tidal_apk_desc),
modifier = Modifier.clickable(onClick = onSelectCustomTidalApk),
) {
FilledTonalButton(onClick = onSelectCustomInjector) {
FilledTonalButton(onClick = onSelectCustomTidalApk) {
Text(
text = customInjector?.version?.toString()
text = customTidalApk?.version?.toString()
?: stringResource(R.string.componentopts_selected_none)
)
}
@@ -0,0 +1,140 @@
package com.meowarex.rlmobile.ui.widgets.managerupdate
import androidx.annotation.DrawableRes
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.Tag
data class VersionDelta(
val label: String,
@DrawableRes val iconRes: Int,
val from: String?,
val to: String,
val tag: String? = null,
)
@Composable
fun ManagerUpdateDialog(
deltas: List<VersionDelta>,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {
FilledTonalButton(
onClick = onDismiss,
colors = ButtonDefaults.filledTonalButtonColors(
containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
),
) {
Text(stringResource(R.string.action_continue))
}
},
title = {
Text(
text = stringResource(R.string.manager_update_title),
textAlign = TextAlign.Center,
)
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.manager_update_subtitle),
textAlign = TextAlign.Center,
style = MaterialTheme.typography.bodyMedium,
)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
deltas.forEach { delta ->
DeltaCard(delta = delta)
}
}
}
},
icon = {
Icon(
painter = painterResource(R.drawable.ic_update),
contentDescription = null,
modifier = Modifier.size(36.dp),
)
},
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = false,
),
)
}
@Composable
private fun DeltaCard(delta: VersionDelta) {
val changed = delta.from != null && delta.from != delta.to
val subtitle = when {
delta.from == null || delta.from == delta.to -> delta.to
else -> "${delta.from}${delta.to}"
}
Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth(),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp),
) {
DeltaIcon(delta)
Column(modifier = Modifier.weight(1f)) {
Text(
text = delta.label,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,
color = if (changed) MaterialTheme.colorScheme.primary
else LocalContentColor.current.copy(alpha = 0.65f),
fontWeight = if (changed) FontWeight.SemiBold else FontWeight.Normal,
)
}
delta.tag?.let { Tag(text = it) }
}
}
}
@Composable
private fun DeltaIcon(delta: VersionDelta) {
Surface(
shape = CircleShape,
color = MaterialTheme.colorScheme.secondaryContainer,
modifier = Modifier.size(40.dp),
) {
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) {
Icon(
painter = painterResource(delta.iconRes),
contentDescription = null,
tint = MaterialTheme.colorScheme.onSecondaryContainer,
modifier = Modifier.size(22.dp),
)
}
}
}
@@ -21,4 +21,4 @@
android:pathData="M5.209,1.737C5.313,1.473 5.687,1.473 5.791,1.737L6.157,2.667C6.189,2.747 6.253,2.811 6.333,2.843L7.263,3.209C7.527,3.313 7.527,3.687 7.263,3.791L6.333,4.157C6.253,4.189 6.189,4.253 6.157,4.333L5.791,5.263C5.687,5.527 5.313,5.527 5.209,5.263L4.843,4.333C4.811,4.253 4.747,4.189 4.667,4.157L3.737,3.791C3.473,3.687 3.473,3.313 3.737,3.209L4.667,2.843C4.747,2.811 4.811,2.747 4.843,2.667L5.209,1.737Z" />
</group>
</vector>
aight
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,3L9,5v9.85c-0.59,-0.34 -1.27,-0.55 -2,-0.55c-2.21,0 -4,1.79 -4,4s1.79,4 4,4s4,-1.79 4,-4V7.36l8,-1.6v6.49c-0.59,-0.34 -1.27,-0.55 -2,-0.55c-2.21,0 -4,1.79 -4,4s1.79,4 4,4s4,-1.79 4,-4V3z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M19,9l1.25,-2.75L23,5l-2.75,-1.25L19,1l-1.25,2.75L15,5l2.75,1.25L19,9zm-7.5,0.5L9,4 6.5,9.5 1,12l5.5,2.5L9,20l2.5,-5.5L17,12l-5.5,-2.5zM19,15l-1.25,2.75L15,19l2.75,1.25L19,23l1.25,-2.75L23,19l-2.75,-1.25z" />
</vector>
+18 -5
View File
@@ -43,10 +43,18 @@
<string name="action_open_info">Open Info</string>
<string name="action_reset_default">Reset to default</string>
<string name="intent_reinstall_fail">Failed to automatically reinstall! Please try doing it manually.</string>
<string name="intent_reinstall_fail">Failed to automatically repatch! Please try doing it manually.</string>
<string name="intent_import_component_success">Successfully imported %s</string>
<string name="intent_import_component_failure">Failed to import custom component!</string>
<string name="manager_update_title">Update Complete</string>
<string name="manager_update_subtitle">Manager was successfully updated!</string>
<string name="manager_update_row_manager">Manager</string>
<string name="manager_update_row_patches">Patches</string>
<string name="manager_update_row_tidal">TIDAL</string>
<string name="manager_update_tag_complete">Complete</string>
<string name="manager_update_tag_available">Available</string>
<string name="permissions_title">Grant Permissions</string>
<string name="permissions_subtitle">Radiant Lyrics Manager requires permissions:</string>
<string name="permissions_legend">%s indicates required permissions!</string>
@@ -63,7 +71,7 @@
<string name="permissions_notifs_title">Notifications</string>
<string name="permissions_battery_title">Background Battery</string>
<string name="permissions_battery_desc">Ensures the installation process does not get automatically cancelled if the app is minimized.</string>
<string name="permissions_notifs_desc">Used only to show the download progress if Radiant Lyrics Manager is minimized during installation.</string>
<string name="permissions_notifs_desc">Used to notify you when updates are available!</string>
<string name="permissions_root_denied">Failed to obtain root permissions</string>
<string name="permissions_shizuku_denied">Failed to obtain Shizuku permissions</string>
@@ -235,10 +243,10 @@
>The app name is what\'s displayed in your home launcher. This should be changed on secondary installations for ease of use.</string>
<string name="patchopts_debuggable_title">Debuggable</string>
<string name="patchopts_debuggable_desc">Enable the debuggable manifest flag. Only use this if you know what you are doing!</string>
<string name="patchopts_custom_injector_title">Custom Injector</string>
<string name="patchopts_custom_injector_desc">A custom injector build that was imported by Manager.</string>
<string name="patchopts_custom_tidal_apk_title">Custom TIDAL APK</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_desc">A custom smali patch bundle that was imported by Manager.</string>
<string name="patchopts_custom_patches_desc">Provide your own Patches Zip to use instead of the ones from Github.</string>
<string name="patchopts_divider_basic">Basic</string>
<string name="patchopts_divider_advanced">Advanced</string>
<string name="patchopts_patches_title">Patches</string>
@@ -270,6 +278,11 @@
<string name="componentopts_screen_desc">Select a custom build that was imported by Manager.</string>
<string name="componentopts_selected_none">Latest</string>
<string name="componentopts_deleted">Successfully deleted component!</string>
<string name="componentopts_browse_title">Browse files…</string>
<string name="componentopts_browse_desc">Import a file from your device</string>
<string name="componentopts_releases_title">GitHub Releases</string>
<string name="componentopts_releases_desc">Pick a historical release from the repo</string>
<string name="componentopts_releases_empty">No releases with this component found</string>
<string name="log_title">Log</string>
<string name="log_section_install_info">Installation Info</string>
+1 -1
View File
@@ -1,5 +1,5 @@
{
"tidalVersionCode": 9089,
"tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk",
"patchesVersion": "0.5.0"
"patchesVersion": "0.6.0"
}