mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-06-18 05:23:12 +10:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e7b69a8deb | |||
| 324a6eb6c8 | |||
| c5962ad1a8 | |||
| 77ab041b97 |
@@ -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(),
|
||||
|
||||
|
||||
+25
-2
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
+12
-6
@@ -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,
|
||||
),
|
||||
|
||||
+7
-7
@@ -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(),
|
||||
),
|
||||
|
||||
+169
-1
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
+232
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-4
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
+3
-6
@@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+6
-6
@@ -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,
|
||||
)
|
||||
|
||||
+10
-10
@@ -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)
|
||||
)
|
||||
}
|
||||
|
||||
+140
@@ -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>
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user