Code Review for UI Rewrite

This commit is contained in:
2026-05-21 00:38:27 +10:00
parent 630154ed78
commit c95bb0633f
10 changed files with 66 additions and 26 deletions
+2 -1
View File
@@ -57,7 +57,8 @@ jobs:
fi fi
if [[ "$tidal_src" == *.apkm ]]; then if [[ "$tidal_src" == *.apkm ]]; then
echo "Merging splits from $tidal_src via APKEditor" echo "Merging splits from $tidal_src via APKEditor"
curl -sLo /tmp/APKEditor.jar https://github.com/REAndroid/APKEditor/releases/download/V1.4.3/APKEditor-1.4.3.jar curl --fail --location --retry 3 --retry-delay 2 -sSo /tmp/APKEditor.jar \
https://github.com/REAndroid/APKEditor/releases/download/V1.4.3/APKEditor-1.4.3.jar
java -jar /tmp/APKEditor.jar m -i "$tidal_src" -o ./dist/tidal-stock.apk java -jar /tmp/APKEditor.jar m -i "$tidal_src" -o ./dist/tidal-stock.apk
echo "Merged tidal-stock.apk:" echo "Merged tidal-stock.apk:"
ls -la ./dist/tidal-stock.apk ls -la ./dist/tidal-stock.apk
@@ -26,6 +26,7 @@ import com.meowarex.rlmobile.ui.screens.settings.SettingsModel
import com.meowarex.rlmobile.ui.widgets.updater.UpdaterViewModel import com.meowarex.rlmobile.ui.widgets.updater.UpdaterViewModel
import com.meowarex.rlmobile.updatechecker.UpdateCheckWorker import com.meowarex.rlmobile.updatechecker.UpdateCheckWorker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.koin.android.ext.android.get
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.module.dsl.* import org.koin.core.module.dsl.*
@@ -101,7 +102,12 @@ class ManagerApplication : Application() {
.build() .build()
} }
// Schedule periodic update check // Schedule periodic update check only when the user has opted in,
// so the disabled state survives app restarts instead of being re-enqueued.
if (get<PreferencesManager>().autoUpdateCheck) {
UpdateCheckWorker.schedule(this) UpdateCheckWorker.schedule(this)
} else {
UpdateCheckWorker.cancel(this)
}
} }
} }
@@ -10,7 +10,10 @@ class CommitsPagingSource(
) : PagingSource<Int, GithubCommit>() { ) : PagingSource<Int, GithubCommit>() {
override fun getRefreshKey(state: PagingState<Int, GithubCommit>): Int? = override fun getRefreshKey(state: PagingState<Int, GithubCommit>): Int? =
state.anchorPosition?.let { state.closestPageToPosition(it)?.prevKey } state.anchorPosition?.let {
val page = state.closestPageToPosition(it) ?: return null
page.prevKey?.plus(1) ?: page.nextKey?.minus(1)
}
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GithubCommit> { override suspend fun load(params: LoadParams<Int>): LoadResult<Int, GithubCommit> {
val page = params.key ?: 0 val page = params.key ?: 0
@@ -51,8 +51,16 @@ class SmaliPatchStep : Step(), IDexProvider, KoinComponent {
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) { if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
val relative = patchFile.removePrefix("extension/") val relative = patchFile.removePrefix("extension/")
val out = smaliDir.resolve(relative) val out = smaliDir.resolve(relative)
out.parentFile?.mkdirs() // Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
out.writeBytes(zip.openEntry(patchFile)!!.read()) val baseCanonical = smaliDir.canonicalPath + File.separator
val outCanonical = out.canonicalPath
if (!outCanonical.startsWith(baseCanonical)) {
throw SecurityException("Zip entry escapes target directory: $patchFile")
}
val entry = zip.openEntry(patchFile)
?: throw FileNotFoundException("Missing zip entry: $patchFile")
out.canonicalFile.parentFile?.mkdirs()
out.writeBytes(entry.read())
container.log("Extracted extension smali: $relative") container.log("Extracted extension smali: $relative")
continue continue
} }
@@ -35,7 +35,8 @@ object ManifestPatcher {
} }
} }
}) })
val origPkg = originalPackage ?: "com.aspiro.tidal" val origPkg = originalPackage
?: throw IllegalStateException("Manifest has no package attribute; refusing to rewrite authorities/permissions without a known originalPackage")
val reader = AxmlReader(manifestBytes) val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter() val writer = AxmlWriter()
@@ -104,7 +105,7 @@ object ManifestPatcher {
super.attr( super.attr(
ns, name, resourceId, type, ns, name, resourceId, type,
when (name) { when (name) {
"name" -> (value as String).replace(origPkg, packageName) "name" -> (value as? String)?.replace(origPkg, packageName) ?: value
else -> value else -> value
} }
) )
@@ -155,7 +156,7 @@ object ManifestPatcher {
super.attr( super.attr(
ns, name, resourceId, type, ns, name, resourceId, type,
if (name == "authorities") { if (name == "authorities") {
(value as String).replace(origPkg, packageName) (value as? String)?.replace(origPkg, packageName) ?: value
} else { } else {
value value
} }
@@ -97,6 +97,7 @@ class HomeModel(
fun openApp(packageName: String) { fun openApp(packageName: String) {
val launchIntent = application.packageManager.getLaunchIntentForPackage(packageName) val launchIntent = application.packageManager.getLaunchIntentForPackage(packageName)
if (launchIntent != null) { if (launchIntent != null) {
launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
application.startActivity(launchIntent) application.startActivity(launchIntent)
} else { } else {
application.showToast(R.string.launch_app_fail) application.showToast(R.string.launch_app_fail)
@@ -12,10 +12,15 @@ import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
@@ -55,16 +60,28 @@ class HomeScreen : Screen, Parcelable {
title = { Text(stringResource(R.string.navigation_home)) }, title = { Text(stringResource(R.string.navigation_home)) },
actions = { actions = {
IconButton(onClick = { model.refresh() }) { IconButton(onClick = { model.refresh() }) {
Icon(painterResource(R.drawable.ic_refresh), contentDescription = null) Icon(
painterResource(R.drawable.ic_refresh),
contentDescription = stringResource(R.string.navigation_refresh),
)
} }
IconButton(onClick = { navigator.push(AboutScreen()) }) { IconButton(onClick = { navigator.push(AboutScreen()) }) {
Icon(painterResource(R.drawable.ic_info), contentDescription = null) Icon(
painterResource(R.drawable.ic_info),
contentDescription = stringResource(R.string.navigation_about),
)
} }
IconButton(onClick = { navigator.push(LogsListScreen()) }) { IconButton(onClick = { navigator.push(LogsListScreen()) }) {
Icon(painterResource(R.drawable.ic_receipt), contentDescription = null) Icon(
painterResource(R.drawable.ic_receipt),
contentDescription = stringResource(R.string.navigation_logs),
)
} }
IconButton(onClick = { navigator.push(SettingsScreen()) }) { IconButton(onClick = { navigator.push(SettingsScreen()) }) {
Icon(painterResource(R.drawable.ic_settings), contentDescription = null) Icon(
painterResource(R.drawable.ic_settings),
contentDescription = stringResource(R.string.navigation_settings),
)
} }
}, },
) )
@@ -117,17 +134,19 @@ private fun ColumnScope.HomeContent(
val currentVersionName = install?.version?.let { "v${it.toString()}" } val currentVersionName = install?.version?.let { "v${it.toString()}" }
val latestVersionName = state.latestTidalVersionCode?.let { "build $it" } val latestVersionName = state.latestTidalVersionCode?.let { "build $it" }
if (install?.icon != null) { val fallbackPainter = if (install?.icon == null) {
// R.mipmap.ic_launcher is an adaptive-icon XML on API 26+, which painterResource cannot decode.
val context = LocalContext.current
remember {
val drawable = ContextCompat.getDrawable(context, R.mipmap.ic_launcher)
drawable?.toBitmap()?.asImageBitmap()?.let(::BitmapPainter)
}
} else null
val iconPainter = install?.icon ?: fallbackPainter
if (iconPainter != null) {
Image( Image(
painter = install.icon, painter = iconPainter,
contentDescription = null,
modifier = Modifier
.size(60.dp)
.clip(CircleShape),
)
} else {
Image(
painter = painterResource(R.mipmap.ic_launcher),
contentDescription = null, contentDescription = null,
modifier = Modifier modifier = Modifier
.size(60.dp) .size(60.dp)
@@ -38,7 +38,7 @@ data class PatchOptions(
companion object { companion object {
val Default = PatchOptions( val Default = PatchOptions(
appName = "TIDAL", appName = "TIDAL",
packageName = "com.tidal.music", packageName = "com.aspiro.tidal",
debuggable = false, debuggable = false,
customInjector = null, customInjector = null,
customPatches = null, customPatches = null,
@@ -7,6 +7,7 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
@@ -14,7 +15,6 @@ import androidx.core.app.NotificationCompat
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.work.* import androidx.work.*
import com.meowarex.rlmobile.BuildConfig import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.MainActivity
import com.meowarex.rlmobile.R import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PreferencesManager import com.meowarex.rlmobile.manager.PreferencesManager
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
@@ -81,7 +81,7 @@ class UpdateCheckWorker(
val pendingIntent = PendingIntent.getActivity( val pendingIntent = PendingIntent.getActivity(
applicationContext, applicationContext,
0, 0,
Intent(applicationContext, MainActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, Intent(Intent.ACTION_VIEW, Uri.parse(releaseUrl)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) },
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
) )
@@ -117,6 +117,7 @@
<string name="navigation_about">About</string> <string name="navigation_about">About</string>
<string name="navigation_logs">Logs</string> <string name="navigation_logs">Logs</string>
<string name="navigation_settings">Settings</string> <string name="navigation_settings">Settings</string>
<string name="navigation_refresh">Refresh</string>
<string name="contributors_lead">Lead</string> <string name="contributors_lead">Lead</string>
<string name="contributors">Contributors</string> <string name="contributors">Contributors</string>