From 25a7ab44754c3c9f4bcab773eed258fa659d4edb Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:46:17 +0530 Subject: [PATCH 01/49] potential apkm fix --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e5cbb64..402927a 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ # Local configuration file (sdk path, etc) local.properties +gradle.properties # Log/OS Files *.log From f92d1c9583824860350dcbfb59f6355a49a4a7d1 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:54:32 +0530 Subject: [PATCH 02/49] Update PatchCommand.kt --- .../app/morphe/cli/command/PatchCommand.kt | 205 ++++++------------ 1 file changed, 71 insertions(+), 134 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index bf88cd4..397e05f 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -30,7 +30,6 @@ import java.io.PrintWriter import java.io.StringWriter import java.util.logging.Logger -@OptIn(ExperimentalSerializationApi::class) @CommandLine.Command( name = "patch", description = ["Patch an APK file."], @@ -127,17 +126,6 @@ internal object PatchCommand : Runnable { this.outputFilePath = outputFilePath?.absoluteFile } - private var patchingResultOutputFilePath: File? = null - - @CommandLine.Option( - names = ["-r", "--result-file"], - description = ["Path to save the patching result file to"], - ) - @Suppress("unused") - private fun setPatchingResultOutputFilePath(outputFilePath: File?) { - this.patchingResultOutputFilePath = outputFilePath?.absoluteFile - } - @CommandLine.Option( names = ["-i", "--install"], description = ["Serial of the ADB device to install to. If not specified, the first connected device will be used."], @@ -343,79 +331,53 @@ internal object PatchCommand : Runnable { apk } - val patchingResult = PatchingResult() - - try { - val (packageName, patcherResult) = Patcher( - PatcherConfig( - inputApk, - patcherTemporaryFilesPath, - aaptBinaryPath?.path, - patcherTemporaryFilesPath.absolutePath, - ), - ).use { patcher -> - val packageName = patcher.context.packageMetadata.packageName - val packageVersion = patcher.context.packageMetadata.packageVersion - - patchingResult.packageName = packageName - patchingResult.packageVersion = packageVersion - - val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) - - logger.info("Setting patch options") - - val patchesList = patches.toList() - selection.filter { it.enabled != null }.associate { - val enabledSelection = it.enabled!! - - (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to - enabledSelection.options - }.let(filteredPatches::setOptions) - - patcher += filteredPatches - - // Execute patches. - patchingResult.addStepResult( - PatchingStep.PATCHING, - { - runBlocking { - patcher().collect { patchResult -> - patchResult.exception?.let { exception -> - StringWriter().use { writer -> - exception.printStackTrace(PrintWriter(writer)) - - logger.severe("\"${patchResult.patch}\" failed:\n$writer") - - patchingResult.failedPatches.add( - FailedPatch( - patchResult.patch.toSerializablePatch(), - writer.toString() - ) - ) - patchingResult.success = false - } - } ?: patchResult.patch.let { - patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) - logger.info("\"${patchResult.patch}\" succeeded") - } - } - } - } - ) + val (packageName, patcherResult) = Patcher( + PatcherConfig( + inputApk, + patcherTemporaryFilesPath, + aaptBinaryPath?.path, + patcherTemporaryFilesPath.absolutePath, + ), + ).use { patcher -> + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion - patcher.context.packageMetadata.packageName to patcher.get() - } + val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) - // region Save. + logger.info("Setting patch options") - inputApk.copyTo(temporaryFilesPath.resolve(inputApk.name), overwrite = true).apply { - patchingResult.addStepResult( - PatchingStep.REBUILDING, - { - patcherResult.applyTo(this) + val patchesList = patches.toList() + selection.filter { it.enabled != null }.associate { + val enabledSelection = it.enabled!! + + (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to + enabledSelection.options + }.let(filteredPatches::setOptions) + + patcher += filteredPatches + + // Execute patches. + runBlocking { + patcher().collect { patchResult -> + val exception = patchResult.exception + ?: return@collect logger.info("\"${patchResult.patch}\" succeeded") + + StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + + logger.severe("\"${patchResult.patch}\" failed:\n$writer") } - ) - }.also { rebuiltApk -> + } + } + + patcher.context.packageMetadata.packageName to patcher.get() + } + + // region Save. + + inputApk.copyTo(temporaryFilesPath.resolve(inputApk.name), overwrite = true).apply { + patcherResult.applyTo(this) + }.also { rebuiltApk -> if (striplibs.isNotEmpty()) { patchingResult.addStepResult( PatchingStep.STRIPPING_LIBS, @@ -427,66 +389,41 @@ internal object PatchCommand : Runnable { ) } }.let { patchedApkFile -> - if (!mount && !unsigned) { - patchingResult.addStepResult( - PatchingStep.SIGNING, - { - ApkUtils.signApk( - patchedApkFile, - outputFilePath, - signer, - ApkUtils.KeyStoreDetails( - keystoreFilePath, - keyStorePassword, - keyStoreEntryAlias, - keyStoreEntryPassword, - ), - ) - } - ) - } else { - patchedApkFile.copyTo(outputFilePath, overwrite = true) - } - } - - logger.info("Saved to $outputFilePath") - - // endregion - - // region Install. - - deviceSerial?.let { - patchingResult.addStepResult( - PatchingStep.INSTALLING, - { - runBlocking { - val result = installer!!.install(Installer.Apk(outputFilePath, packageName)) - when (result) { - RootInstallerResult.FAILURE -> { - logger.severe("Failed to mount the patched APK file") - throw IllegalStateException("Failed to mount the patched APK file") - } - is AdbInstallerResult.Failure -> { - logger.severe(result.exception.toString()) - throw result.exception - } - else -> logger.info("Installed the patched APK file") - } - } - } + if (!mount && !unsigned) { + ApkUtils.signApk( + patchedApkFile, + outputFilePath, + signer, + ApkUtils.KeyStoreDetails( + keystoreFilePath, + keyStorePassword, + keyStoreEntryAlias, + keyStoreEntryPassword, + ), ) + } else { + patchedApkFile.copyTo(outputFilePath, overwrite = true) } + } + + logger.info("Saved to $outputFilePath") - // endregion - } finally { - patchingResultOutputFilePath?.let { outputFile -> - outputFile.outputStream().use { outputStream -> - Json.encodeToStream(patchingResult, outputStream) + // endregion + + // region Install. + + deviceSerial?.let { + runBlocking { + when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) { + RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file") + is AdbInstallerResult.Failure -> logger.severe(result.exception.toString()) + else -> logger.info("Installed the patched APK file") } - logger.info("Patching result saved to $outputFile") } } + // endregion + if (purge) { logger.info("Purging temporary files") purge(temporaryFilesPath) From 227bc14fbd189a2096b581c423612e15fc795f9f Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 1 Feb 2026 03:25:05 +0530 Subject: [PATCH 03/49] Update .gitignore --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 402927a..e5cbb64 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ build/ # Local configuration file (sdk path, etc) local.properties -gradle.properties # Log/OS Files *.log From b2e63beecd79057d5bd3df5aa99029bbdb87fa6a Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 1 Feb 2026 09:23:41 +0530 Subject: [PATCH 04/49] Update PatchCommand.kt --- .../app/morphe/cli/command/PatchCommand.kt | 205 ++++++++++++------ 1 file changed, 134 insertions(+), 71 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 397e05f..bf88cd4 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -30,6 +30,7 @@ import java.io.PrintWriter import java.io.StringWriter import java.util.logging.Logger +@OptIn(ExperimentalSerializationApi::class) @CommandLine.Command( name = "patch", description = ["Patch an APK file."], @@ -126,6 +127,17 @@ internal object PatchCommand : Runnable { this.outputFilePath = outputFilePath?.absoluteFile } + private var patchingResultOutputFilePath: File? = null + + @CommandLine.Option( + names = ["-r", "--result-file"], + description = ["Path to save the patching result file to"], + ) + @Suppress("unused") + private fun setPatchingResultOutputFilePath(outputFilePath: File?) { + this.patchingResultOutputFilePath = outputFilePath?.absoluteFile + } + @CommandLine.Option( names = ["-i", "--install"], description = ["Serial of the ADB device to install to. If not specified, the first connected device will be used."], @@ -331,53 +343,79 @@ internal object PatchCommand : Runnable { apk } - val (packageName, patcherResult) = Patcher( - PatcherConfig( - inputApk, - patcherTemporaryFilesPath, - aaptBinaryPath?.path, - patcherTemporaryFilesPath.absolutePath, - ), - ).use { patcher -> - val packageName = patcher.context.packageMetadata.packageName - val packageVersion = patcher.context.packageMetadata.packageVersion - - val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) - - logger.info("Setting patch options") - - val patchesList = patches.toList() - selection.filter { it.enabled != null }.associate { - val enabledSelection = it.enabled!! - - (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to - enabledSelection.options - }.let(filteredPatches::setOptions) - - patcher += filteredPatches - - // Execute patches. - runBlocking { - patcher().collect { patchResult -> - val exception = patchResult.exception - ?: return@collect logger.info("\"${patchResult.patch}\" succeeded") - - StringWriter().use { writer -> - exception.printStackTrace(PrintWriter(writer)) - - logger.severe("\"${patchResult.patch}\" failed:\n$writer") + val patchingResult = PatchingResult() + + try { + val (packageName, patcherResult) = Patcher( + PatcherConfig( + inputApk, + patcherTemporaryFilesPath, + aaptBinaryPath?.path, + patcherTemporaryFilesPath.absolutePath, + ), + ).use { patcher -> + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion + + patchingResult.packageName = packageName + patchingResult.packageVersion = packageVersion + + val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) + + logger.info("Setting patch options") + + val patchesList = patches.toList() + selection.filter { it.enabled != null }.associate { + val enabledSelection = it.enabled!! + + (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to + enabledSelection.options + }.let(filteredPatches::setOptions) + + patcher += filteredPatches + + // Execute patches. + patchingResult.addStepResult( + PatchingStep.PATCHING, + { + runBlocking { + patcher().collect { patchResult -> + patchResult.exception?.let { exception -> + StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + + logger.severe("\"${patchResult.patch}\" failed:\n$writer") + + patchingResult.failedPatches.add( + FailedPatch( + patchResult.patch.toSerializablePatch(), + writer.toString() + ) + ) + patchingResult.success = false + } + } ?: patchResult.patch.let { + patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) + logger.info("\"${patchResult.patch}\" succeeded") + } + } + } } - } - } + ) - patcher.context.packageMetadata.packageName to patcher.get() - } + patcher.context.packageMetadata.packageName to patcher.get() + } - // region Save. + // region Save. - inputApk.copyTo(temporaryFilesPath.resolve(inputApk.name), overwrite = true).apply { - patcherResult.applyTo(this) - }.also { rebuiltApk -> + inputApk.copyTo(temporaryFilesPath.resolve(inputApk.name), overwrite = true).apply { + patchingResult.addStepResult( + PatchingStep.REBUILDING, + { + patcherResult.applyTo(this) + } + ) + }.also { rebuiltApk -> if (striplibs.isNotEmpty()) { patchingResult.addStepResult( PatchingStep.STRIPPING_LIBS, @@ -389,41 +427,66 @@ internal object PatchCommand : Runnable { ) } }.let { patchedApkFile -> - if (!mount && !unsigned) { - ApkUtils.signApk( - patchedApkFile, - outputFilePath, - signer, - ApkUtils.KeyStoreDetails( - keystoreFilePath, - keyStorePassword, - keyStoreEntryAlias, - keyStoreEntryPassword, - ), - ) - } else { - patchedApkFile.copyTo(outputFilePath, overwrite = true) + if (!mount && !unsigned) { + patchingResult.addStepResult( + PatchingStep.SIGNING, + { + ApkUtils.signApk( + patchedApkFile, + outputFilePath, + signer, + ApkUtils.KeyStoreDetails( + keystoreFilePath, + keyStorePassword, + keyStoreEntryAlias, + keyStoreEntryPassword, + ), + ) + } + ) + } else { + patchedApkFile.copyTo(outputFilePath, overwrite = true) + } } - } - logger.info("Saved to $outputFilePath") - - // endregion - - // region Install. + logger.info("Saved to $outputFilePath") + + // endregion + + // region Install. + + deviceSerial?.let { + patchingResult.addStepResult( + PatchingStep.INSTALLING, + { + runBlocking { + val result = installer!!.install(Installer.Apk(outputFilePath, packageName)) + when (result) { + RootInstallerResult.FAILURE -> { + logger.severe("Failed to mount the patched APK file") + throw IllegalStateException("Failed to mount the patched APK file") + } + is AdbInstallerResult.Failure -> { + logger.severe(result.exception.toString()) + throw result.exception + } + else -> logger.info("Installed the patched APK file") + } + } + } + ) + } - deviceSerial?.let { - runBlocking { - when (val result = installer!!.install(Installer.Apk(outputFilePath, packageName))) { - RootInstallerResult.FAILURE -> logger.severe("Failed to mount the patched APK file") - is AdbInstallerResult.Failure -> logger.severe(result.exception.toString()) - else -> logger.info("Installed the patched APK file") + // endregion + } finally { + patchingResultOutputFilePath?.let { outputFile -> + outputFile.outputStream().use { outputStream -> + Json.encodeToStream(patchingResult, outputStream) } + logger.info("Patching result saved to $outputFile") } } - // endregion - if (purge) { logger.info("Purging temporary files") purge(temporaryFilesPath) From ee63a1489b68d0add25f38c0ca4460a6c585b05e Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Fri, 6 Feb 2026 17:06:27 +0530 Subject: [PATCH 05/49] feat: GUI Update --- .gitignore | 7 + build.gradle.kts | 113 +- gradle.properties | 1 + gradle/libs.versions.toml | 72 +- settings.gradle.kts | 13 + src/main/composeResources/drawable/morphe.svg | 40 + .../composeResources/drawable/morphe_logo.png | Bin 0 -> 5905 bytes src/main/composeResources/drawable/reddit.svg | 10 + .../composeResources/drawable/youtube.svg | 1 + .../drawable/youtube_music.svg | 1 + src/main/kotlin/app/morphe/MorpheLauncher.kt | 14 + .../app/morphe/cli/command/MainCommand.kt | 4 +- src/main/kotlin/app/morphe/gui/App.kt | 123 ++ src/main/kotlin/app/morphe/gui/GuiMain.kt | 99 ++ .../morphe/gui/data/constants/AppConstants.kt | 150 +++ .../app/morphe/gui/data/model/AppConfig.kt | 39 + .../kotlin/app/morphe/gui/data/model/Patch.kt | 83 ++ .../app/morphe/gui/data/model/Release.kt | 70 + .../app/morphe/gui/data/model/SupportedApp.kt | 71 + .../gui/data/repository/ConfigRepository.kt | 129 ++ .../gui/data/repository/PatchRepository.kt | 179 +++ .../kotlin/app/morphe/gui/di/AppModule.kt | 63 + .../morphe/gui/ui/components/ErrorDialog.kt | 154 +++ .../gui/ui/components/SettingsButton.kt | 81 ++ .../gui/ui/components/SettingsDialog.kt | 305 +++++ .../morphe/gui/ui/screens/home/HomeScreen.kt | 1165 +++++++++++++++++ .../gui/ui/screens/home/HomeViewModel.kt | 519 ++++++++ .../ui/screens/home/components/ApkDropZone.kt | 212 +++ .../ui/screens/home/components/ApkInfoCard.kt | 414 ++++++ .../home/components/FullScreenDropZone.kt | 74 ++ .../screens/patches/PatchSelectionScreen.kt | 707 ++++++++++ .../patches/PatchSelectionViewModel.kt | 325 +++++ .../gui/ui/screens/patches/PatchesScreen.kt | 478 +++++++ .../ui/screens/patches/PatchesViewModel.kt | 211 +++ .../gui/ui/screens/patching/PatchingScreen.kt | 457 +++++++ .../ui/screens/patching/PatchingViewModel.kt | 236 ++++ .../gui/ui/screens/quick/QuickPatchScreen.kt | 963 ++++++++++++++ .../ui/screens/quick/QuickPatchViewModel.kt | 470 +++++++ .../gui/ui/screens/result/ResultScreen.kt | 753 +++++++++++ .../kotlin/app/morphe/gui/ui/theme/Theme.kt | 79 ++ .../app/morphe/gui/ui/theme/ThemeState.kt | 17 + .../kotlin/app/morphe/gui/util/AdbManager.kt | 359 +++++ .../app/morphe/gui/util/ChecksumUtils.kt | 58 + .../kotlin/app/morphe/gui/util/FileUtils.kt | 149 +++ src/main/kotlin/app/morphe/gui/util/Logger.kt | 219 ++++ .../app/morphe/gui/util/PatchService.kt | 307 +++++ .../morphe/gui/util/SupportedAppExtractor.kt | 68 + src/main/resources/morphe_logo.icns | Bin 0 -> 230839 bytes src/main/resources/morphe_logo.ico | Bin 0 -> 605 bytes src/main/resources/morphe_logo.png | Bin 0 -> 5905 bytes 50 files changed, 10037 insertions(+), 25 deletions(-) create mode 100644 src/main/composeResources/drawable/morphe.svg create mode 100755 src/main/composeResources/drawable/morphe_logo.png create mode 100644 src/main/composeResources/drawable/reddit.svg create mode 100644 src/main/composeResources/drawable/youtube.svg create mode 100644 src/main/composeResources/drawable/youtube_music.svg create mode 100644 src/main/kotlin/app/morphe/MorpheLauncher.kt create mode 100644 src/main/kotlin/app/morphe/gui/App.kt create mode 100644 src/main/kotlin/app/morphe/gui/GuiMain.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/model/Patch.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/model/Release.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt create mode 100644 src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt create mode 100644 src/main/kotlin/app/morphe/gui/di/AppModule.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/ErrorDialog.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkDropZone.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/theme/ThemeState.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/AdbManager.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/ChecksumUtils.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/FileUtils.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/Logger.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/PatchService.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt create mode 100644 src/main/resources/morphe_logo.icns create mode 100644 src/main/resources/morphe_logo.ico create mode 100755 src/main/resources/morphe_logo.png diff --git a/.gitignore b/.gitignore index e5cbb64..f32e01e 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ build/ # Local configuration file (sdk path, etc) local.properties +old_build.gradle.kts # Log/OS Files *.log @@ -23,6 +24,9 @@ misc.xml deploymentTargetDropDown.xml render.experimental.xml +# Kotlin +.kotlin/ + # Keystore files *.jks *.keystore @@ -32,3 +36,6 @@ google-services.json # Android Profiling *.hprof + +# Mac files +.DS_Store diff --git a/build.gradle.kts b/build.gradle.kts index 7fb4ed5..8af331b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,8 +1,10 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.kotlin) + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.compose) alias(libs.plugins.shadow) application `maven-publish` @@ -11,10 +13,33 @@ plugins { group = "app.morphe" +// ============================================================================ +// JVM / Kotlin Configuration +// ============================================================================ +kotlin { + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(17)) + vendor.set(JvmVendorSpec.ADOPTIUM) + } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_17) + } +} + +// ============================================================================ +// Application Entry Point +// ============================================================================ +// Shadow JAR reads this for Main-Class manifest attribute. +// +// No args / double-click → GUI (Compose Desktop) +// With args (terminal) → CLI (PicoCLI) application { - mainClass = "app.morphe.cli.command.MainCommandKt" + mainClass.set("app.morphe.MorpheLauncherKt") } +// ============================================================================ +// Repositories +// ============================================================================ repositories { mavenLocal() mavenCentral() @@ -23,8 +48,10 @@ repositories { // A repository must be specified for some reason. "registry" is a dummy. url = uri("https://maven.pkg.github.com/MorpheApp/registry") credentials { - username = project.findProperty("gpr.user") as String? ?: System.getenv("GITHUB_ACTOR") - password = project.findProperty("gpr.key") as String? ?: System.getenv("GITHUB_TOKEN") + username = project.findProperty("gpr.user") as String? + ?: System.getenv("GITHUB_ACTOR") + password = project.findProperty("gpr.key") as String? + ?: System.getenv("GITHUB_TOKEN") } } // Obtain baksmali/smali from source builds - https://github.com/iBotPeaches/smali @@ -32,6 +59,9 @@ repositories { maven { url = uri("https://jitpack.io") } } +// ============================================================================ +// Dependencies +// ============================================================================ val apkEditorLib by configurations.creating val strippedApkEditorLib by tasks.registering(org.gradle.jvm.tasks.Jar::class) { @@ -52,27 +82,55 @@ val strippedApkEditorLib by tasks.registering(org.gradle.jvm.tasks.Jar::class) { } dependencies { + // -- CLI / Core -------------------------------------------------------- implementation(libs.morphe.patcher) implementation(libs.morphe.library) - implementation(libs.kotlinx.coroutines.core) - implementation(libs.kotlinx.serialization.json) implementation(libs.picocli) apkEditorLib(files("$rootDir/libs/APKEditor-1.4.7.jar")) implementation(files(strippedApkEditorLib)) - testImplementation(libs.kotlin.test) -} + // -- Compose Desktop --------------------------------------------------- + // OS-specific: JAR only runs on the OS it was built on. + // Build once per target OS (macOS, Linux, Windows). + implementation(compose.desktop.currentOs) + implementation(compose.components.resources) + @Suppress("DEPRECATION") + implementation(compose.material3) + implementation(compose.materialIconsExtended) -kotlin { - compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) - } -} + // -- Async / Serialization --------------------------------------------- + implementation(libs.kotlinx.coroutines.core) + implementation(libs.kotlinx.coroutines.swing) + implementation(libs.kotlinx.serialization.json) + + // -- Networking (GUI) -------------------------------------------------- + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.cio) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.client.logging) + + // -- DI / Navigation (GUI) --------------------------------------------- + implementation(platform(libs.koin.bom)) + implementation(libs.koin.core) + implementation(libs.koin.compose) -java { - targetCompatibility = JavaVersion.VERSION_11 + implementation(libs.voyager.navigator) + implementation(libs.voyager.screenmodel) + implementation(libs.voyager.koin) + implementation(libs.voyager.transitions) + + // -- APK Parsing (GUI) ------------------------------------------------- + implementation(libs.apk.parser) + + // -- Testing ----------------------------------------------------------- + testImplementation(libs.kotlin.test) + testImplementation(libs.mockk) } +// ============================================================================ +// Tasks +// ============================================================================ tasks { test { useJUnitPlatform() @@ -82,19 +140,39 @@ tasks { } processResources { - expand("projectVersion" to project.version) + // Only expand properties files, not binary files like PNG/ICO + filesMatching("**/*.properties") { + expand("projectVersion" to project.version) + } } + // ------------------------------------------------------------------------- + // Shadow JAR — the only distribution artifact + // ------------------------------------------------------------------------- shadowJar { exclude( "/prebuilt/linux/aapt", "/prebuilt/windows/aapt.exe", "/prebuilt/*/aapt_*", ) + exclude("/prebuilt/linux/aapt") + exclude("/prebuilt/windows/aapt.exe") + exclude("/prebuilt/*/aapt_*") + minimize { exclude(dependency("org.bouncycastle:.*")) exclude(dependency("app.morphe:morphe-patcher")) + // Compose / Skiko / Swing — cannot be minimized (reflection, native libs) + exclude(dependency("org.jetbrains.compose.*:.*")) + exclude(dependency("org.jetbrains.skiko:.*")) + exclude(dependency("org.jetbrains.kotlinx:kotlinx-coroutines-swing:.*")) + // Ktor uses ServiceLoader + exclude(dependency("io.ktor:.*")) + // Koin uses reflection + exclude(dependency("io.insert-koin:.*")) } + + mergeServiceFiles() } publish { @@ -102,6 +180,9 @@ tasks { } } +// ============================================================================ +// Publishing / Signing +// ============================================================================ // Needed by gradle-semantic-release-plugin. // Tracking: https://github.com/KengoTODA/gradle-semantic-release-plugin/issues/435 diff --git a/gradle.properties b/gradle.properties index fd63a01..9b4b907 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,3 +2,4 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official version = 1.4.0-dev.2 +compose.resources.generated.internal = never diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 53481b5..8ad4fa6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,20 +1,78 @@ [versions] -shadow = "8.3.9" +# Core kotlin = "2.3.0" -kotlinx = "1.9.0" +shadow = "8.3.9" + +# CLI picocli = "4.7.7" morphe-patcher = "1.1.1" morphe-library = "1.2.0" +# Compose Desktop +compose = "1.10.0" + +# Networking +ktor = "3.4.0" + +# DI +koin-bom = "4.1.1" + +# Navigation +voyager = "1.1.0-beta03" + +# Async / Serialization +coroutines = "1.10.2" +kotlinx-serialization = "1.9.0" + +# APK +apk-parser = "2.6.10" +arsclib = "1.3.8" + +# Testing +mockk = "1.14.3" + [libraries] -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx" } +# Morphe Core picocli = { module = "info.picocli:picocli", version.ref = "picocli" } morphe-patcher = { module = "app.morphe:morphe-patcher", version.ref = "morphe-patcher" } morphe-library = { module = "app.morphe:morphe-library-jvm", version.ref = "morphe-library" } +# Ktor Client +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } + +# Koin +koin-bom = { module = "io.insert-koin:koin-bom", version.ref = "koin-bom" } +koin-core = { module = "io.insert-koin:koin-core" } +koin-compose = { module = "io.insert-koin:koin-compose" } + +# Voyager Navigation +voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } +voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" } +voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } +voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } + +# Coroutines +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" } + +# Serialization +kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } + +# APK +apk-parser = { module = "net.dongliu:apk-parser", version.ref = "apk-parser" } +arsclib = { module = "io.github.reandroid:ARSCLib", version.ref = "arsclib" } + +# Testing +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } + [plugins] -shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } -kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } +compose = { id = "org.jetbrains.compose", version.ref = "compose" } +shadow = { id = "com.gradleup.shadow", version.ref = "shadow" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3afed46..9a2941c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1,14 @@ +pluginManagement { + repositories { + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + rootProject.name = "morphe-cli" diff --git a/src/main/composeResources/drawable/morphe.svg b/src/main/composeResources/drawable/morphe.svg new file mode 100644 index 0000000..96ce4ec --- /dev/null +++ b/src/main/composeResources/drawable/morphe.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/composeResources/drawable/morphe_logo.png b/src/main/composeResources/drawable/morphe_logo.png new file mode 100755 index 0000000000000000000000000000000000000000..1c211b7b0c98fe89f990f214cedc899a93878ff7 GIT binary patch literal 5905 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 + + + + + + + + + \ No newline at end of file diff --git a/src/main/composeResources/drawable/youtube.svg b/src/main/composeResources/drawable/youtube.svg new file mode 100644 index 0000000..f125ec3 --- /dev/null +++ b/src/main/composeResources/drawable/youtube.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/composeResources/drawable/youtube_music.svg b/src/main/composeResources/drawable/youtube_music.svg new file mode 100644 index 0000000..2257e05 --- /dev/null +++ b/src/main/composeResources/drawable/youtube_music.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/main/kotlin/app/morphe/MorpheLauncher.kt b/src/main/kotlin/app/morphe/MorpheLauncher.kt new file mode 100644 index 0000000..3098e7f --- /dev/null +++ b/src/main/kotlin/app/morphe/MorpheLauncher.kt @@ -0,0 +1,14 @@ +package app.morphe + +import app.morphe.library.logging.Logger + +fun main(args: Array) { + if (args.isEmpty()) { + app.morphe.gui.launchGui(args) + } else { + Logger.setDefault() + picocli.CommandLine(app.morphe.cli.command.MainCommand) + .execute(*args) + .let(System::exit) + } +} diff --git a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt index d8eff33..40bc787 100644 --- a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt @@ -7,7 +7,7 @@ import picocli.CommandLine.Command import picocli.CommandLine.IVersionProvider import java.util.* -fun main(args: Array) { +fun cliMain(args: Array) { Logger.setDefault() CommandLine(MainCommand).execute(*args).let(System::exit) } @@ -39,4 +39,4 @@ private object CLIVersionProvider : IVersionProvider { UtilityCommand::class, ], ) -private object MainCommand +internal object MainCommand diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt new file mode 100644 index 0000000..8bd8504 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -0,0 +1,123 @@ +package app.morphe.gui + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.SlideTransition +import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.util.PatchService +import app.morphe.gui.di.appModule +import kotlinx.coroutines.launch +import org.koin.compose.KoinApplication +import org.koin.compose.koinInject +import app.morphe.gui.ui.screens.home.HomeScreen +import app.morphe.gui.ui.screens.quick.QuickPatchContent +import app.morphe.gui.ui.screens.quick.QuickPatchViewModel +import app.morphe.gui.ui.theme.LocalThemeState +import app.morphe.gui.ui.theme.MorpheTheme +import app.morphe.gui.ui.theme.ThemePreference +import app.morphe.gui.ui.theme.ThemeState +import app.morphe.gui.util.Logger + +/** + * Mode state for switching between simplified and full mode. + */ +data class ModeState( + val isSimplified: Boolean, + val onChange: (Boolean) -> Unit +) + +val LocalModeState = staticCompositionLocalOf { + error("No ModeState provided") +} + +@Composable +fun App(initialSimplifiedMode: Boolean = true) { + LaunchedEffect(Unit) { + Logger.init() + } + + KoinApplication(application = { + modules(appModule) + }) { + AppContent(initialSimplifiedMode) + } +} + +@Composable +private fun AppContent(initialSimplifiedMode: Boolean) { + val configRepository: ConfigRepository = koinInject() + val patchRepository: PatchRepository = koinInject() + val patchService: PatchService = koinInject() + val scope = rememberCoroutineScope() + + var themePreference by remember { mutableStateOf(ThemePreference.SYSTEM) } + var isSimplifiedMode by remember { mutableStateOf(initialSimplifiedMode) } + var isLoading by remember { mutableStateOf(true) } + + // Load config on startup + LaunchedEffect(Unit) { + val config = configRepository.loadConfig() + themePreference = config.getThemePreference() + isSimplifiedMode = config.useSimplifiedMode + isLoading = false + } + + // Callback for changing theme + val onThemeChange: (ThemePreference) -> Unit = { newTheme -> + themePreference = newTheme + scope.launch { + configRepository.setThemePreference(newTheme) + Logger.info("Theme changed to: ${newTheme.name}") + } + } + + // Callback for changing mode + val onModeChange: (Boolean) -> Unit = { simplified -> + isSimplifiedMode = simplified + scope.launch { + configRepository.setUseSimplifiedMode(simplified) + Logger.info("Mode changed to: ${if (simplified) "Simplified" else "Full"}") + } + } + + val themeState = ThemeState( + current = themePreference, + onChange = onThemeChange + ) + + val modeState = ModeState( + isSimplified = isSimplifiedMode, + onChange = onModeChange + ) + + MorpheTheme(themePreference = themePreference) { + CompositionLocalProvider( + LocalThemeState provides themeState, + LocalModeState provides modeState + ) { + Surface(modifier = Modifier.fillMaxSize()) { + if (!isLoading) { + Crossfade(targetState = isSimplifiedMode) { simplified -> + if (simplified) { + // Quick/Simplified mode + val quickViewModel = remember { + QuickPatchViewModel(patchRepository, patchService, configRepository) + } + QuickPatchContent(quickViewModel) + } else { + // Full mode + Navigator(HomeScreen()) { navigator -> + SlideTransition(navigator) + } + } + } + } + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/GuiMain.kt b/src/main/kotlin/app/morphe/gui/GuiMain.kt new file mode 100644 index 0000000..7e3b77a --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/GuiMain.kt @@ -0,0 +1,99 @@ +package app.morphe.gui + +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.compose.ui.graphics.toComposeImageBitmap +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.WindowPosition +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import app.morphe.gui.data.model.AppConfig +import kotlinx.serialization.json.Json +import org.jetbrains.skia.Image +import app.morphe.gui.util.FileUtils + +/** + * Main entry point. + * The app switches between simplified and full mode dynamically via settings. + */ +fun launchGui(args: Array) = application { + // Determine initial mode from args or config + val initialSimplifiedMode = when { + args.contains("--quick") || args.contains("-q") -> true + args.contains("--full") || args.contains("-f") -> false + else -> loadConfigSync().useSimplifiedMode + } + + val windowState = rememberWindowState( + size = DpSize(1024.dp, 768.dp), + position = WindowPosition(Alignment.Center) + ) + + val appIcon = remember { loadAppIcon() } + + Window( + onCloseRequest = ::exitApplication, + title = "Morphe", + state = windowState, + icon = appIcon + ) { + window.minimumSize = java.awt.Dimension(600, 400) + App(initialSimplifiedMode = initialSimplifiedMode) + } +} + +/** + * Load config synchronously (needed before app starts). + */ +private fun loadConfigSync(): AppConfig { + return try { + val configFile = FileUtils.getConfigFile() + if (configFile.exists()) { + val json = Json { ignoreUnknownKeys = true } + json.decodeFromString(configFile.readText()) + } else { + AppConfig() // Defaults: useSimplifiedMode = true + } + } catch (e: Exception) { + AppConfig() // Defaults on error + } +} + +/** + * Load the app icon from resources. + * Tries multiple classloaders and paths to handle different resource packaging. + */ +private fun loadAppIcon(): BitmapPainter? { + val possiblePaths = listOf( + "/morphe_logo.png", + "morphe_logo.png", + "/composeResources/app.morphe.morphe_cli.generated.resources/drawable/morphe_logo.png", + "composeResources/app.morphe.morphe_cli.generated.resources/drawable/morphe_logo.png" + ) + + // Try different classloader approaches + val classLoaders = listOf( + { path: String -> object {}.javaClass.getResourceAsStream(path) }, + { path: String -> Thread.currentThread().contextClassLoader.getResourceAsStream(path) }, + { path: String -> ClassLoader.getSystemResourceAsStream(path) } + ) + + for (loader in classLoaders) { + for (path in possiblePaths) { + try { + val stream = loader(path) + if (stream != null) { + return stream.use { + BitmapPainter(Image.makeFromEncoded(it.readBytes()).toComposeImageBitmap()) + } + } + } catch (e: Exception) { + // Try next combination + } + } + } + return null +} diff --git a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt new file mode 100644 index 0000000..95163f2 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt @@ -0,0 +1,150 @@ +package app.morphe.gui.data.constants + +/** + * Centralized configuration for supported apps. + * Update version, URL, and checksum here - changes will reflect throughout the app. + */ +object AppConstants { + + // ==================== APP INFO ==================== + const val APP_NAME = "Morphe GUI" + const val APP_VERSION = "1.4.0" // Keep in sync with build.gradle.kts + + // ==================== YOUTUBE ==================== + object YouTube { + const val DISPLAY_NAME = "YouTube" + const val PACKAGE_NAME = "com.google.android.youtube" + const val SUGGESTED_VERSION = "20.40.45" + const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/google-inc/youtube/youtube-20-40-45-release/youtube-20-40-45-2-android-apk-download/" + + // SHA-256 checksum from APKMirror (leave null if not verified) + // You can find this on the APKMirror download page under "File SHA-256" + val SHA256_CHECKSUM: String? = "b7659da492a1ebd8bd7cea2909be4ee1f58e00a2586d65a1c91b2e1e5ec6acd1" + } + + // ==================== YOUTUBE MUSIC ==================== + object YouTubeMusic { + const val DISPLAY_NAME = "YouTube Music" + const val PACKAGE_NAME = "com.google.android.apps.youtube.music" + const val SUGGESTED_VERSION = "8.40.54" + const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/google-inc/youtube-music/youtube-music-8-40-54-release/" + val SHA256_CHECKSUMS: Map = mapOf( + "arm64-v8a" to "d5b44919a5cd5648b01e392115fe68b9569b1c7847f3cdf65b1ace1302d005d2", + "armeabi-v7a" to "6f5181e8aaa2595af6c421b86ffffcc1c7a4e97968d7be89d04b46776392eaec", + "x86" to "03b1eb6993d43b1de6a9416828df7864be975ca6dd3a82468c431e3c193f3a80", + "x86_64" to "eab4cd51220b28c7108343cdb95a063251029f9a137d052a519d007a9321c848" + ) + } + + // ==================== REDDIT ==================== + object Reddit { + const val DISPLAY_NAME = "Reddit" + const val PACKAGE_NAME = "com.reddit.frontpage" + // APKMirror URL - to be updated with specific version when known + const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/redditinc/reddit/" + } + + /** + * List of all supported package names for quick lookup. + */ + val SUPPORTED_PACKAGES = listOf( + YouTube.PACKAGE_NAME, + YouTubeMusic.PACKAGE_NAME, + Reddit.PACKAGE_NAME + ) + + /** + * Get suggested version for a package name. + */ + fun getSuggestedVersion(packageName: String): String? { + return when (packageName) { + YouTube.PACKAGE_NAME -> YouTube.SUGGESTED_VERSION + YouTubeMusic.PACKAGE_NAME -> YouTubeMusic.SUGGESTED_VERSION + else -> null + } + } + + /** + * Get checksum for a package name, version, and architecture. + * @param packageName The app's package name + * @param version The app version + * @param architectures List of architectures in the APK (from lib/ folder) + * @return The expected checksum, or null if not configured/version mismatch + */ + fun getChecksum(packageName: String, version: String, architectures: List = emptyList()): String? { + return when (packageName) { + YouTube.PACKAGE_NAME -> { + // YouTube has a universal APK with single checksum + if (version == YouTube.SUGGESTED_VERSION) YouTube.SHA256_CHECKSUM else null + } + YouTubeMusic.PACKAGE_NAME -> { + if (version != YouTubeMusic.SUGGESTED_VERSION) return null + if (YouTubeMusic.SHA256_CHECKSUMS.isEmpty()) return null + + // Try to find matching architecture checksum + // Check for universal first, then specific architectures + YouTubeMusic.SHA256_CHECKSUMS["universal"] + ?: architectures.firstNotNullOfOrNull { arch -> + YouTubeMusic.SHA256_CHECKSUMS[arch] + } + } + else -> null + } + } + + /** + * Check if we have any checksum configured for this package/version/architecture combo. + */ + fun hasChecksumConfigured(packageName: String, version: String, architectures: List = emptyList()): Boolean { + return getChecksum(packageName, version, architectures) != null + } + + /** + * Check if this is the recommended version. + */ + fun isRecommendedVersion(packageName: String, version: String): Boolean { + return getSuggestedVersion(packageName) == version + } + + // ==================== PATCH RECOMMENDATIONS ==================== + + /** + * Patches that are commonly disabled by users. + * These patches change default behavior in ways that some users may not want. + * The key is a partial match (case-insensitive) against patch names. + */ + object PatchRecommendations { + /** + * Patches commonly disabled for YouTube. + * Pair of (patch name pattern, reason for commonly disabling) + */ + val YOUTUBE_COMMONLY_DISABLED: List> = listOf( + "Custom Branding" to "Keeps the original name and logo for the app", +// "Hide ads" to "Some users prefer keeping ads to support creators", +// "Premium heading" to "Changes the YouTube logo/heading appearance", +// "Navigation buttons" to "Modifies bottom navigation bar layout", +// "Spoof client" to "May cause playback issues on some devices", +// "Change start page" to "Modifies the default landing page", +// "Disable auto captions" to "Some users rely on auto-generated captions" + ) + + /** + * Patches commonly disabled for YouTube Music. + */ + val YOUTUBE_MUSIC_COMMONLY_DISABLED: List> = listOf( + "Custom Branding" to "Keeps the original name and logo for the app", +// "Spoof client" to "May cause playback issues on some devices" + ) + + /** + * Get commonly disabled patches for a package. + */ + fun getCommonlyDisabled(packageName: String): List> { + return when (packageName) { + YouTube.PACKAGE_NAME -> YOUTUBE_COMMONLY_DISABLED + YouTubeMusic.PACKAGE_NAME -> YOUTUBE_MUSIC_COMMONLY_DISABLED + else -> emptyList() + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt b/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt new file mode 100644 index 0000000..bd23a27 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt @@ -0,0 +1,39 @@ +package app.morphe.gui.data.model + +import kotlinx.serialization.Serializable +import app.morphe.gui.ui.theme.ThemePreference + +/** + * Application configuration stored in config.json + */ +@Serializable +data class AppConfig( + val themePreference: String = ThemePreference.SYSTEM.name, + val lastCliVersion: String? = null, + val lastPatchesVersion: String? = null, + val preferredPatchChannel: String = PatchChannel.STABLE.name, + val defaultOutputDirectory: String? = null, + val autoCleanupTempFiles: Boolean = true, // Default ON + val useSimplifiedMode: Boolean = true // Default to Quick/Simplified mode +) { + fun getThemePreference(): ThemePreference { + return try { + ThemePreference.valueOf(themePreference) + } catch (e: Exception) { + ThemePreference.SYSTEM + } + } + + fun getPatchChannel(): PatchChannel { + return try { + PatchChannel.valueOf(preferredPatchChannel) + } catch (e: Exception) { + PatchChannel.STABLE + } + } +} + +enum class PatchChannel { + STABLE, + DEV +} diff --git a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt new file mode 100644 index 0000000..42b1396 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt @@ -0,0 +1,83 @@ +package app.morphe.gui.data.model + +import kotlinx.serialization.Serializable + +/** + * Represents a single patch from Morphe patches bundle. + */ +@Serializable +data class Patch( + val name: String, + val description: String = "", + val compatiblePackages: List = emptyList(), + val options: List = emptyList(), + val isEnabled: Boolean = true +) { + /** + * Unique identifier for this patch. + * Combines name, packages, and description hash for true uniqueness. + */ + val uniqueId: String + get() { + val packages = compatiblePackages.joinToString(",") { it.name } + val descHash = description.hashCode().toString(16) + return "$name|$packages|$descHash" + } + + /** + * Check if patch is compatible with a given package. + * Patches with no compatible packages listed are NOT shown (they're system patches). + */ + fun isCompatibleWith(packageName: String, versionName: String? = null): Boolean { + // Patches without explicit package compatibility are excluded + if (compatiblePackages.isEmpty()) return false + + return compatiblePackages.any { pkg -> + pkg.name == packageName && ( + versionName == null || + pkg.versions.isEmpty() || + pkg.versions.contains(versionName) + ) + } + } +} + +@Serializable +data class CompatiblePackage( + val name: String, + val versions: List = emptyList() +) + +@Serializable +data class PatchOption( + val key: String, + val title: String, + val description: String = "", + val type: PatchOptionType = PatchOptionType.STRING, + val default: String? = null, + val required: Boolean = false +) + +@Serializable +enum class PatchOptionType { + STRING, + BOOLEAN, + INT, + LONG, + FLOAT, + LIST +} + +/** + * Configuration for a patching session. + */ +@Serializable +data class PatchConfig( + val inputApkPath: String, + val outputApkPath: String, + val patchesFilePath: String, + val enabledPatches: List = emptyList(), + val disabledPatches: List = emptyList(), + val patchOptions: Map = emptyMap(), + val useExclusiveMode: Boolean = false +) diff --git a/src/main/kotlin/app/morphe/gui/data/model/Release.kt b/src/main/kotlin/app/morphe/gui/data/model/Release.kt new file mode 100644 index 0000000..941b5e9 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/model/Release.kt @@ -0,0 +1,70 @@ +package app.morphe.gui.data.model + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents a GitHub release (for CLI or Patches) + */ +@Serializable +data class Release( + val id: Long, + @SerialName("tag_name") + val tagName: String, + val name: String, + @SerialName("prerelease") + val isPrerelease: Boolean, + val draft: Boolean = false, + @SerialName("published_at") + val publishedAt: String, + val assets: List = emptyList(), + val body: String? = null +) { + /** + * Get the version string (removes 'v' prefix if present) + */ + fun getVersion(): String { + return tagName.removePrefix("v") + } + + /** + * Check if this is a dev/pre-release version + */ + fun isDevRelease(): Boolean { + return isPrerelease || tagName.contains("dev", ignoreCase = true) || + tagName.contains("alpha", ignoreCase = true) || + tagName.contains("beta", ignoreCase = true) + } +} + +@Serializable +data class ReleaseAsset( + val id: Long, + val name: String, + @SerialName("browser_download_url") + val downloadUrl: String, + val size: Long, + @SerialName("content_type") + val contentType: String +) { + /** + * Check if this is a JAR file + */ + fun isJar(): Boolean = name.endsWith(".jar", ignoreCase = true) + + /** + * Check if this is an MPP (Morphe Patches) file + */ + fun isMpp(): Boolean = name.endsWith(".mpp", ignoreCase = true) + + /** + * Get human-readable file size + */ + fun getFormattedSize(): String { + return when { + size < 1024 -> "$size B" + size < 1024 * 1024 -> "${size / 1024} KB" + else -> "${size / (1024 * 1024)} MB" + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt new file mode 100644 index 0000000..d32745a --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -0,0 +1,71 @@ +package app.morphe.gui.data.model + +/** + * Represents a supported app extracted dynamically from patch metadata. + * This is populated by parsing the .mpp file's compatible packages. + */ +data class SupportedApp( + val packageName: String, + val displayName: String, + val supportedVersions: List, + val recommendedVersion: String?, + val apkMirrorUrl: String? = null +) { + companion object { + /** + * Derive display name from package name. + */ + fun getDisplayName(packageName: String): String { + return when (packageName) { + "com.google.android.youtube" -> "YouTube" + "com.google.android.apps.youtube.music" -> "YouTube Music" + "com.reddit.frontpage" -> "Reddit" + else -> { + // Fallback: Extract last part of package name and capitalize + packageName.substringAfterLast(".") + .replaceFirstChar { it.uppercase() } + } + } + } + + /** + * Get APK Mirror URL for a package name. + */ + fun getApkMirrorUrl(packageName: String): String? { + return when (packageName) { + "com.google.android.youtube" -> "https://www.apkmirror.com/apk/google-inc/youtube/" + "com.google.android.apps.youtube.music" -> "https://www.apkmirror.com/apk/google-inc/youtube-music/" + "com.reddit.frontpage" -> "https://www.apkmirror.com/apk/redditinc/reddit/" + else -> null + } + } + + /** + * Get the recommended version from a list of supported versions. + * Returns the highest version number. + */ + fun getRecommendedVersion(versions: List): String? { + if (versions.isEmpty()) return null + + return versions.sortedWith { v1, v2 -> + compareVersions(v2, v1) // Descending order + }.firstOrNull() + } + + /** + * Compare two version strings. + * Returns positive if v1 > v2, negative if v1 < v2, 0 if equal. + */ + private fun compareVersions(v1: String, v2: String): Int { + val parts1 = v1.split(".").mapNotNull { it.toIntOrNull() } + val parts2 = v2.split(".").mapNotNull { it.toIntOrNull() } + + for (i in 0 until maxOf(parts1.size, parts2.size)) { + val p1 = parts1.getOrElse(i) { 0 } + val p2 = parts2.getOrElse(i) { 0 } + if (p1 != p2) return p1.compareTo(p2) + } + return 0 + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt new file mode 100644 index 0000000..a298b0c --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt @@ -0,0 +1,129 @@ +package app.morphe.gui.data.repository + +import app.morphe.gui.data.model.AppConfig +import app.morphe.gui.data.model.PatchChannel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import app.morphe.gui.ui.theme.ThemePreference +import app.morphe.gui.util.FileUtils +import app.morphe.gui.util.Logger + +/** + * Repository for managing app configuration (config.json) + */ +class ConfigRepository { + + private val json = Json { + prettyPrint = true + ignoreUnknownKeys = true + encodeDefaults = true + } + + private var cachedConfig: AppConfig? = null + + /** + * Load config from file, or return default if not exists. + */ + suspend fun loadConfig(): AppConfig = withContext(Dispatchers.IO) { + cachedConfig?.let { return@withContext it } + + val configFile = FileUtils.getConfigFile() + + try { + if (configFile.exists()) { + val content = configFile.readText() + val config = json.decodeFromString(content) + cachedConfig = config + Logger.info("Config loaded from ${configFile.absolutePath}") + config + } else { + Logger.info("No config file found, using defaults") + val default = AppConfig() + saveConfig(default) + default + } + } catch (e: Exception) { + Logger.error("Failed to load config, using defaults", e) + AppConfig() + } + } + + /** + * Save config to file. + */ + suspend fun saveConfig(config: AppConfig) = withContext(Dispatchers.IO) { + try { + val configFile = FileUtils.getConfigFile() + val content = json.encodeToString(AppConfig.serializer(), config) + configFile.writeText(content) + cachedConfig = config + Logger.info("Config saved to ${configFile.absolutePath}") + } catch (e: Exception) { + Logger.error("Failed to save config", e) + } + } + + /** + * Update theme preference. + */ + suspend fun setThemePreference(theme: ThemePreference) { + val current = loadConfig() + saveConfig(current.copy(themePreference = theme.name)) + } + + /** + * Update patch channel preference. + */ + suspend fun setPatchChannel(channel: PatchChannel) { + val current = loadConfig() + saveConfig(current.copy(preferredPatchChannel = channel.name)) + } + + /** + * Update last used CLI version. + */ + suspend fun setLastCliVersion(version: String) { + val current = loadConfig() + saveConfig(current.copy(lastCliVersion = version)) + } + + /** + * Update last used patches version. + */ + suspend fun setLastPatchesVersion(version: String) { + val current = loadConfig() + saveConfig(current.copy(lastPatchesVersion = version)) + } + + /** + * Update default output directory. + */ + suspend fun setDefaultOutputDirectory(path: String?) { + val current = loadConfig() + saveConfig(current.copy(defaultOutputDirectory = path)) + } + + /** + * Update auto-cleanup temp files setting. + */ + suspend fun setAutoCleanupTempFiles(enabled: Boolean) { + val current = loadConfig() + saveConfig(current.copy(autoCleanupTempFiles = enabled)) + } + + /** + * Update simplified mode setting. + */ + suspend fun setUseSimplifiedMode(enabled: Boolean) { + val current = loadConfig() + saveConfig(current.copy(useSimplifiedMode = enabled)) + } + + /** + * Clear cached config (for testing). + */ + fun clearCache() { + cachedConfig = null + } +} diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt new file mode 100644 index 0000000..7881939 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt @@ -0,0 +1,179 @@ +package app.morphe.gui.data.repository + +import app.morphe.gui.data.model.Release +import app.morphe.gui.data.model.ReleaseAsset +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import app.morphe.gui.util.FileUtils +import app.morphe.gui.util.Logger +import java.io.File + +/** + * Repository for fetching Morphe patches from GitHub releases. + */ +class PatchRepository( + private val httpClient: HttpClient +) { + companion object { + private const val GITHUB_API_BASE = "https://api.github.com" + private const val PATCHES_REPO = "MorpheApp/morphe-patches" + private const val RELEASES_ENDPOINT = "$GITHUB_API_BASE/repos/$PATCHES_REPO/releases" + } + + /** + * Fetch all releases from GitHub. + */ + suspend fun fetchReleases(): Result> = withContext(Dispatchers.IO) { + try { + Logger.info("Fetching releases from $RELEASES_ENDPOINT") + val response: HttpResponse = httpClient.get(RELEASES_ENDPOINT) { + headers { + append(HttpHeaders.Accept, "application/vnd.github+json") + append("X-GitHub-Api-Version", "2022-11-28") + } + } + + if (response.status.isSuccess()) { + val releases: List = response.body() + Logger.info("Fetched ${releases.size} releases") + Result.success(releases) + } else { + val error = "Failed to fetch releases: ${response.status}" + Logger.error(error) + Result.failure(Exception(error)) + } + } catch (e: Exception) { + Logger.error("Error fetching releases", e) + Result.failure(e) + } + } + + /** + * Get stable releases only (non-prerelease). + */ + suspend fun fetchStableReleases(): Result> { + return fetchReleases().map { releases -> + releases.filter { !it.isDevRelease() } + } + } + + /** + * Get dev/prerelease versions only. + */ + suspend fun fetchDevReleases(): Result> { + return fetchReleases().map { releases -> + releases.filter { it.isDevRelease() } + } + } + + /** + * Get the latest stable release. + */ + suspend fun getLatestStableRelease(): Result { + return fetchStableReleases().map { it.firstOrNull() } + } + + /** + * Get the latest dev release. + */ + suspend fun getLatestDevRelease(): Result { + return fetchDevReleases().map { it.firstOrNull() } + } + + /** + * Find the .mpp asset in a release. + */ + fun findMppAsset(release: Release): ReleaseAsset? { + return release.assets.find { it.isMpp() } + } + + /** + * Download the .mpp patch file from a release. + * Returns the path to the downloaded file. + */ + suspend fun downloadPatches(release: Release, onProgress: (Float) -> Unit = {}): Result = withContext(Dispatchers.IO) { + val asset = findMppAsset(release) + if (asset == null) { + val error = "No .mpp file found in release ${release.tagName}" + Logger.error(error) + return@withContext Result.failure(Exception(error)) + } + + val patchesDir = FileUtils.getPatchesDir() + val targetFile = File(patchesDir, asset.name) + + // Check if already cached + if (targetFile.exists() && targetFile.length() == asset.size) { + Logger.info("Using cached patches: ${targetFile.absolutePath}") + onProgress(1f) + return@withContext Result.success(targetFile) + } + + try { + Logger.info("Downloading patches from ${asset.downloadUrl}") + + val response: HttpResponse = httpClient.get(asset.downloadUrl) { + headers { + append(HttpHeaders.Accept, "application/octet-stream") + } + } + + if (!response.status.isSuccess()) { + val error = "Failed to download patches: ${response.status}" + Logger.error(error) + return@withContext Result.failure(Exception(error)) + } + + val bytes = response.readRawBytes() + targetFile.writeBytes(bytes) + onProgress(1f) + + Logger.info("Patches downloaded to ${targetFile.absolutePath}") + Result.success(targetFile) + } catch (e: Exception) { + Logger.error("Error downloading patches", e) + // Clean up partial download + if (targetFile.exists()) { + targetFile.delete() + } + Result.failure(e) + } + } + + /** + * Get cached patch file for a specific version. + */ + fun getCachedPatches(version: String): File? { + val patchesDir = FileUtils.getPatchesDir() + return patchesDir.listFiles()?.find { + it.name.contains(version) && it.name.endsWith(".mpp") + } + } + + /** + * List all cached patch versions. + */ + fun listCachedPatches(): List { + val patchesDir = FileUtils.getPatchesDir() + return patchesDir.listFiles()?.filter { it.name.endsWith(".mpp") } ?: emptyList() + } + + /** + * Delete cached patches. + */ + fun clearCache(): Boolean { + return try { + FileUtils.getPatchesDir().listFiles()?.forEach { it.delete() } + Logger.info("Patches cache cleared") + true + } catch (e: Exception) { + Logger.error("Failed to clear patches cache", e) + false + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/di/AppModule.kt b/src/main/kotlin/app/morphe/gui/di/AppModule.kt new file mode 100644 index 0000000..b542bca --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/di/AppModule.kt @@ -0,0 +1,63 @@ +package app.morphe.gui.di + +import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.util.PatchService +import io.ktor.client.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.plugins.logging.* +import io.ktor.serialization.kotlinx.json.* +import kotlinx.serialization.json.Json +import org.koin.dsl.module +import app.morphe.gui.ui.screens.home.HomeViewModel +import app.morphe.gui.ui.screens.patches.PatchesViewModel +import app.morphe.gui.ui.screens.patches.PatchSelectionViewModel +import app.morphe.gui.ui.screens.patching.PatchingViewModel + +/** + * Main Koin module for dependency injection. + */ +val appModule = module { + + // JSON serialization + single { + Json { + prettyPrint = true + ignoreUnknownKeys = true + encodeDefaults = true + isLenient = true + } + } + + // Ktor HTTP Client + single { + HttpClient(CIO) { + install(ContentNegotiation) { + json(get()) + } + install(Logging) { + level = LogLevel.INFO + logger = object : Logger { + override fun log(message: String) { + app.morphe.gui.util.Logger.debug("HTTP: $message") + } + } + } + engine { + requestTimeout = 60_000 + } + } + } + + // Repositories and Services + single { ConfigRepository() } + single { PatchRepository(get()) } + single { PatchService() } + + // ViewModels (ScreenModels) + factory { HomeViewModel(get(), get(), get()) } + factory { params -> PatchesViewModel(params.get(), params.get(), get(), get()) } + factory { params -> PatchSelectionViewModel(params.get(), params.get(), params.get(), get(), get()) } + factory { params -> PatchingViewModel(params.get(), get(), get()) } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/ErrorDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/ErrorDialog.kt new file mode 100644 index 0000000..804ece1 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/ErrorDialog.kt @@ -0,0 +1,154 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material.icons.filled.WifiOff +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.morphe.gui.ui.theme.MorpheColors + +enum class ErrorType { + NETWORK, + FILE, + CLI, + GENERIC +} + +@Composable +fun ErrorDialog( + title: String, + message: String, + errorType: ErrorType = ErrorType.GENERIC, + onDismiss: () -> Unit, + onRetry: (() -> Unit)? = null, + dismissText: String = "OK", + retryText: String = "Retry" +) { + val icon = when (errorType) { + ErrorType.NETWORK -> Icons.Default.WifiOff + ErrorType.FILE -> Icons.Default.Error + ErrorType.CLI -> Icons.Default.Error + ErrorType.GENERIC -> Icons.Default.Warning + } + + AlertDialog( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(16.dp), + icon = { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(48.dp) + ) + }, + title = { + Text( + text = title, + fontWeight = FontWeight.SemiBold, + textAlign = TextAlign.Center + ) + }, + text = { + Text( + text = message, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + confirmButton = { + if (onRetry != null) { + Button( + onClick = onRetry, + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text(retryText) + } + } else { + Button( + onClick = onDismiss, + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text(dismissText) + } + } + }, + dismissButton = if (onRetry != null) { + { + TextButton(onClick = onDismiss) { + Text(dismissText) + } + } + } else null + ) +} + +/** + * Helper function to determine error type from exception or message. + */ +fun getErrorType(error: String): ErrorType { + val lowerError = error.lowercase() + return when { + lowerError.contains("network") || + lowerError.contains("connect") || + lowerError.contains("timeout") || + lowerError.contains("unreachable") || + lowerError.contains("internet") -> ErrorType.NETWORK + + lowerError.contains("file") || + lowerError.contains("permission") || + lowerError.contains("access") || + lowerError.contains("read") || + lowerError.contains("write") -> ErrorType.FILE + + lowerError.contains("cli") || + lowerError.contains("patch") || + lowerError.contains("exit code") -> ErrorType.CLI + + else -> ErrorType.GENERIC + } +} + +/** + * Get user-friendly error message. + */ +fun getFriendlyErrorMessage(error: String): String { + val lowerError = error.lowercase() + return when { + lowerError.contains("timeout") -> + "The connection timed out. Please check your internet connection and try again." + + lowerError.contains("unreachable") || lowerError.contains("connect") -> + "Unable to connect to the server. Please check your internet connection." + + lowerError.contains("permission") || lowerError.contains("access denied") -> + "Permission denied. Please check that you have access to the file or folder." + + lowerError.contains("not found") -> + "The requested file or resource was not found." + + lowerError.contains("disk full") || lowerError.contains("no space") -> + "Not enough disk space. Please free up some space and try again." + + lowerError.contains("exit code") -> + "The patching process encountered an error. Check the logs for details." + + else -> error + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt new file mode 100644 index 0000000..ac1411b --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -0,0 +1,81 @@ +package app.morphe.gui.ui.components + +import app.morphe.gui.LocalModeState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import app.morphe.gui.data.repository.ConfigRepository +import kotlinx.coroutines.launch +import org.koin.compose.koinInject +import app.morphe.gui.ui.theme.LocalThemeState + +/** + * Reusable settings button that can be placed on any screen. + * @param allowCacheClear Whether to allow cache clearing (disable on patches screen and beyond) + */ +@Composable +fun SettingsButton( + modifier: Modifier = Modifier, + allowCacheClear: Boolean = true +) { + val themeState = LocalThemeState.current + val modeState = LocalModeState.current + val configRepository: ConfigRepository = koinInject() + val scope = rememberCoroutineScope() + + var showSettingsDialog by remember { mutableStateOf(false) } + var autoCleanupTempFiles by remember { mutableStateOf(true) } + + // Load config when dialog is shown + LaunchedEffect(showSettingsDialog) { + if (showSettingsDialog) { + val config = configRepository.loadConfig() + autoCleanupTempFiles = config.autoCleanupTempFiles + } + } + + Box(modifier = modifier) { + IconButton( + onClick = { showSettingsDialog = true }, + modifier = Modifier + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + if (showSettingsDialog) { + SettingsDialog( + currentTheme = themeState.current, + onThemeChange = { themeState.onChange(it) }, + autoCleanupTempFiles = autoCleanupTempFiles, + onAutoCleanupChange = { enabled -> + autoCleanupTempFiles = enabled + scope.launch { + configRepository.setAutoCleanupTempFiles(enabled) + } + }, + useSimplifiedMode = modeState.isSimplified, + onSimplifiedModeChange = { enabled -> + modeState.onChange(enabled) + }, + onDismiss = { showSettingsDialog = false }, + allowCacheClear = allowCacheClear + ) + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt new file mode 100644 index 0000000..dd88e88 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -0,0 +1,305 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.BugReport +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.ui.theme.ThemePreference +import app.morphe.gui.util.FileUtils +import app.morphe.gui.util.Logger +import java.awt.Desktop +import java.io.File + +@Composable +fun SettingsDialog( + currentTheme: ThemePreference, + onThemeChange: (ThemePreference) -> Unit, + autoCleanupTempFiles: Boolean, + onAutoCleanupChange: (Boolean) -> Unit, + useSimplifiedMode: Boolean, + onSimplifiedModeChange: (Boolean) -> Unit, + onDismiss: () -> Unit, + allowCacheClear: Boolean = true +) { + var showClearCacheConfirm by remember { mutableStateOf(false) } + var cacheCleared by remember { mutableStateOf(false) } + + AlertDialog( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(16.dp), + title = { + Text( + text = "Settings", + fontWeight = FontWeight.SemiBold + ) + }, + text = { + Column( + modifier = Modifier + .verticalScroll(rememberScrollState()) + .widthIn(min = 300.dp), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Theme selection + Text( + text = "Theme", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + ThemePreference.entries.forEach { theme -> + FilterChip( + selected = currentTheme == theme, + onClick = { onThemeChange(theme) }, + label = { Text(theme.toDisplayName()) } + ) + } + } + + HorizontalDivider() + + // Simplified mode setting + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Simplified mode", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "Quick one-click patching with default settings", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = useSimplifiedMode, + onCheckedChange = onSimplifiedModeChange, + colors = SwitchDefaults.colors( + checkedThumbColor = MorpheColors.Blue, + checkedTrackColor = MorpheColors.Blue.copy(alpha = 0.5f) + ) + ) + } + + HorizontalDivider() + + // Auto-cleanup setting + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Auto-cleanup temp files", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "Automatically delete temporary files after patching", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Switch( + checked = autoCleanupTempFiles, + onCheckedChange = onAutoCleanupChange, + colors = SwitchDefaults.colors( + checkedThumbColor = MorpheColors.Teal, + checkedTrackColor = MorpheColors.Teal.copy(alpha = 0.5f) + ) + ) + } + + HorizontalDivider() + + // Actions + Text( + text = "Actions", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + + // Export logs button + OutlinedButton( + onClick = { + try { + val logsDir = FileUtils.getLogsDir() + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(logsDir) + } + } catch (e: Exception) { + Logger.error("Failed to open logs folder", e) + } + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.BugReport, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Open Logs Folder") + } + + // Open app data folder + OutlinedButton( + onClick = { + try { + val appDataDir = FileUtils.getAppDataDir() + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(appDataDir) + } + } catch (e: Exception) { + Logger.error("Failed to open app data folder", e) + } + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.FolderOpen, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Open App Data Folder") + } + + // Clear cache button + OutlinedButton( + onClick = { showClearCacheConfirm = true }, + enabled = allowCacheClear && !cacheCleared, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(8.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = if (cacheCleared) MorpheColors.Teal else MaterialTheme.colorScheme.error, + disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + when { + !allowCacheClear -> "Clear Cache (disabled during patching)" + cacheCleared -> "Cache Cleared" + else -> "Clear Cache" + } + ) + } + + // Cache info + val cacheSize = calculateCacheSize() + Text( + text = "Cache: $cacheSize (CLI + Patches)", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + HorizontalDivider() + + // About + Text( + text = "${AppConstants.APP_NAME} v${AppConstants.APP_VERSION}", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text("Close") + } + } + ) + + // Clear cache confirmation dialog + if (showClearCacheConfirm) { + AlertDialog( + onDismissRequest = { showClearCacheConfirm = false }, + shape = RoundedCornerShape(16.dp), + title = { Text("Clear Cache?") }, + text = { + Text("This will delete downloaded CLI and patch files. They will be re-downloaded when needed.") + }, + confirmButton = { + Button( + onClick = { + clearAllCache() + cacheCleared = true + showClearCacheConfirm = false + }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) + ) { + Text("Clear") + } + }, + dismissButton = { + TextButton(onClick = { showClearCacheConfirm = false }) { + Text("Cancel") + } + } + ) + } +} + +private fun ThemePreference.toDisplayName(): String { + return when (this) { + ThemePreference.LIGHT -> "Light" + ThemePreference.DARK -> "Dark" + ThemePreference.SYSTEM -> "System" + } +} + +private fun calculateCacheSize(): String { + val patchesSize = FileUtils.getPatchesDir().walkTopDown().filter { it.isFile }.sumOf { it.length() } + + return when { + patchesSize < 1024 -> "$patchesSize B" + patchesSize < 1024 * 1024 -> "%.1f KB".format(patchesSize / 1024.0) + else -> "%.1f MB".format(patchesSize / (1024.0 * 1024.0)) + } +} + +private fun clearAllCache() { + try { + FileUtils.getPatchesDir().listFiles()?.forEach { it.delete() } + FileUtils.cleanupAllTempDirs() + Logger.info("Cache cleared successfully") + } catch (e: Exception) { + Logger.error("Failed to clear cache", e) + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt new file mode 100644 index 0000000..5844a51 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -0,0 +1,1165 @@ +package app.morphe.gui.ui.screens.home + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Warning +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.morphe.morphe_cli.generated.resources.Res +import app.morphe.morphe_cli.generated.resources.morphe +import app.morphe.morphe_cli.generated.resources.reddit +import app.morphe.morphe_cli.generated.resources.youtube +import app.morphe.morphe_cli.generated.resources.youtube_music +import org.jetbrains.compose.resources.painterResource +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.data.model.SupportedApp +import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.screens.home.components.ApkInfoCard +import app.morphe.gui.ui.screens.home.components.FullScreenDropZone +import app.morphe.gui.ui.screens.patches.PatchesScreen +import app.morphe.gui.ui.screens.patches.PatchSelectionScreen +import app.morphe.gui.ui.theme.MorpheColors +import java.awt.FileDialog +import java.awt.Frame +import java.io.File + +class HomeScreen : Screen { + + @Composable + override fun Content() { + val viewModel = koinScreenModel() + HomeScreenContent(viewModel = viewModel) + } +} + +@Composable +fun HomeScreenContent( + viewModel: HomeViewModel +) { + val navigator = LocalNavigator.currentOrThrow + val uiState by viewModel.uiState.collectAsState() + + // Refresh patches when returning from PatchesScreen (in case user selected a different version) + // Use navigator.items.size as key so this triggers when navigation stack changes (e.g., pop back) + val navStackSize = navigator.items.size + LaunchedEffect(navStackSize) { + viewModel.refreshPatchesIfNeeded() + } + + // Show error snackbar + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(uiState.error) { + uiState.error?.let { error -> + snackbarHostState.showSnackbar( + message = error, + duration = SnackbarDuration.Short + ) + viewModel.clearError() + } + } + + // Full screen drop zone wrapper + FullScreenDropZone( + isDragHovering = uiState.isDragHovering, + onDragHoverChange = { viewModel.setDragHover(it) }, + onFilesDropped = { viewModel.onFilesDropped(it) } + ) { + BoxWithConstraints( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + val isCompact = maxWidth < 500.dp + val isSmall = maxHeight < 600.dp + val padding = if (isCompact) 16.dp else 24.dp + + // Version warning dialog state + var showVersionWarningDialog by remember { mutableStateOf(false) } + + // Version warning dialog + if (showVersionWarningDialog && uiState.apkInfo != null) { + VersionWarningDialog( + versionStatus = uiState.apkInfo!!.versionStatus, + currentVersion = uiState.apkInfo!!.versionName, + suggestedVersion = uiState.apkInfo!!.suggestedVersion ?: "", + onConfirm = { + showVersionWarningDialog = false + val patchesFile = viewModel.getCachedPatchesFile() + if (patchesFile != null && uiState.apkInfo != null) { + navigator.push(PatchSelectionScreen( + apkPath = uiState.apkInfo!!.filePath, + apkName = uiState.apkInfo!!.appName, + patchesFilePath = patchesFile.absolutePath + )) + } + }, + onDismiss = { showVersionWarningDialog = false } + ) + } + + val scrollState = rememberScrollState() + + // Estimate content heights to calculate flexible spacer + val brandingHeight = if (isCompact) 48.dp else 60.dp + val topSpacing = if (isSmall) 24.dp else 48.dp // top spacer + after branding + val middleContentHeight = if (uiState.apkInfo != null) { + // ApkInfoCard (~250dp) + buttons (~72dp) + spacer + if (isCompact) 340.dp else 380.dp + } else { + // Drop prompt section + if (isCompact) 160.dp else 200.dp + } + val supportedAppsHeight = if (isCompact) 220.dp else 280.dp + val bottomSpacing = if (isSmall) 24.dp else 40.dp // spacers around supported apps + + val totalFixedHeight = brandingHeight + topSpacing + middleContentHeight + supportedAppsHeight + bottomSpacing + (padding * 2) + + // Extra space to push supported apps to bottom on large screens + val extraSpace = (maxHeight - totalFixedHeight).coerceAtLeast(0.dp) + + Box(modifier = Modifier.fillMaxSize()) { + // Always scrollable - but on large screens extraSpace fills the gap + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(padding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) + BrandingSection(isCompact = isCompact) + + // Patches version selector card - right under logo + if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + PatchesVersionCard( + patchesVersion = uiState.patchesVersion!!, + isLatest = uiState.isUsingLatestPatches, + onChangePatchesClick = { + // Navigate to patches version selection screen + // Pass empty apk info since user hasn't selected an APK yet + navigator.push(PatchesScreen( + apkPath = uiState.apkInfo?.filePath ?: "", + apkName = uiState.apkInfo?.appName ?: "Select APK first" + )) + }, + isCompact = isCompact, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + ) + } else if (uiState.isLoadingPatches) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Loading patches...", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + Spacer(modifier = Modifier.height(if (isSmall) 16.dp else 32.dp)) + + MiddleContent( + uiState = uiState, + isCompact = isCompact, + patchesLoaded = !uiState.isLoadingPatches && viewModel.getCachedPatchesFile() != null, + onClearClick = { viewModel.clearSelection() }, + onChangeClick = { + openFilePicker()?.let { file -> + viewModel.onFileSelected(file) + } + }, + onContinueClick = { + val patchesFile = viewModel.getCachedPatchesFile() + if (patchesFile == null) { + // Patches not ready yet + return@MiddleContent + } + + val versionStatus = uiState.apkInfo?.versionStatus + if (versionStatus != null && versionStatus != VersionStatus.EXACT_MATCH && versionStatus != VersionStatus.UNKNOWN) { + showVersionWarningDialog = true + } else { + uiState.apkInfo?.let { info -> + navigator.push(PatchSelectionScreen( + apkPath = info.filePath, + apkName = info.appName, + patchesFilePath = patchesFile.absolutePath + )) + } + } + } + ) + + // Flexible spacer - expands on large screens, minimal on small screens + Spacer(modifier = Modifier.height(extraSpace + if (isSmall) 16.dp else 24.dp)) + + SupportedAppsSection( + isCompact = isCompact, + maxWidth = this@BoxWithConstraints.maxWidth, + isLoading = uiState.isLoadingPatches, + supportedApps = uiState.supportedApps, + loadError = uiState.patchLoadError, + onRetry = { viewModel.retryLoadPatches() } + ) + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) + } + + // Settings button in top-right corner + SettingsButton( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(padding), + allowCacheClear = true + ) + + // Snackbar host + SnackbarHost( + hostState = snackbarHostState, + modifier = Modifier.align(Alignment.BottomCenter) + ) + + // Drag overlay + if (uiState.isDragHovering) { + DragOverlay() + } + } + } + } +} + +@Composable +private fun MiddleContent( + uiState: HomeUiState, + isCompact: Boolean, + patchesLoaded: Boolean, + onClearClick: () -> Unit, + onChangeClick: () -> Unit, + onContinueClick: () -> Unit +) { + if (uiState.apkInfo != null) { + ApkSelectedSection( + patchesLoaded = patchesLoaded, + apkInfo = uiState.apkInfo, + isCompact = isCompact, + onClearClick = onClearClick, + onChangeClick = onChangeClick, + onContinueClick = onContinueClick + ) + } else { + DropPromptSection( + isDragHovering = uiState.isDragHovering, + isCompact = isCompact, + onBrowseClick = onChangeClick + ) + } +} + +@Composable +private fun ApkSelectedSection( + patchesLoaded: Boolean, + apkInfo: ApkInfo, + isCompact: Boolean, + onClearClick: () -> Unit, + onChangeClick: () -> Unit, + onContinueClick: () -> Unit +) { + val showWarning = apkInfo.versionStatus != VersionStatus.EXACT_MATCH && + apkInfo.versionStatus != VersionStatus.UNKNOWN + val warningColor = when (apkInfo.versionStatus) { + VersionStatus.NEWER_VERSION -> MaterialTheme.colorScheme.error + VersionStatus.OLDER_VERSION -> Color(0xFFFF9800) + else -> MorpheColors.Blue + } + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.widthIn(max = 500.dp) + ) { + ApkInfoCard( + apkInfo = apkInfo, + onClearClick = onClearClick, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(if (isCompact) 16.dp else 24.dp)) + + // Action buttons - stack vertically on compact + if (isCompact) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + Button( + onClick = onContinueClick, + enabled = patchesLoaded, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (showWarning) warningColor else MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + if (!patchesLoaded) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Loading patches...", + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + } else { + if (showWarning) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Warning", + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + "Continue", + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + } + } + OutlinedButton( + onClick = onChangeClick, + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Text( + "Change APK", + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + } + } + } else { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedButton( + onClick = onChangeClick, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) { + Text( + "Change APK", + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + } + + Button( + onClick = onContinueClick, + enabled = patchesLoaded, + modifier = Modifier + .widthIn(min = 160.dp) + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = if (showWarning) warningColor else MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + if (!patchesLoaded) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "Loading...", + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + } else { + if (showWarning) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Warning", + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + "Continue", + fontSize = 15.sp, + fontWeight = FontWeight.Medium + ) + } + } + } + } + } +} + +@Composable +private fun VersionWarningDialog( + versionStatus: VersionStatus, + currentVersion: String, + suggestedVersion: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val (title, message) = when (versionStatus) { + VersionStatus.NEWER_VERSION -> Pair( + "Version Too New", + "You're using v$currentVersion, but the recommended version is v$suggestedVersion.\n\n" + + "Patching newer versions may cause issues or some patches might not work correctly.\n\n" + + "Do you want to continue anyway?" + ) + VersionStatus.OLDER_VERSION -> Pair( + "Older Version Detected", + "You're using v$currentVersion, but newer patches are available for v$suggestedVersion.\n\n" + + "You may be missing out on new features and bug fixes.\n\n" + + "Do you want to continue with this version?" + ) + else -> Pair("Version Notice", "Continue with v$currentVersion?") + } + + AlertDialog( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(16.dp), + icon = { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = if (versionStatus == VersionStatus.NEWER_VERSION) + MaterialTheme.colorScheme.error + else + Color(0xFFFF9800), + modifier = Modifier.size(32.dp) + ) + }, + title = { + Text( + text = title, + fontWeight = FontWeight.SemiBold + ) + }, + text = { + Text( + text = message, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + confirmButton = { + Button( + onClick = onConfirm, + colors = ButtonDefaults.buttonColors( + containerColor = if (versionStatus == VersionStatus.NEWER_VERSION) + MaterialTheme.colorScheme.error + else + Color(0xFFFF9800) + ) + ) { + Text("Continue Anyway") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} + +@Composable +private fun BrandingSection(isCompact: Boolean = false) { + Image( + painter = painterResource(Res.drawable.morphe), + contentDescription = "Morphe Logo", + modifier = Modifier.height(if (isCompact) 48.dp else 60.dp) + ) +} + +@Composable +private fun DropPromptSection( + isDragHovering: Boolean, + isCompact: Boolean = false, + onBrowseClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = if (isCompact) 16.dp else 32.dp) + ) { + Text( + text = if (isDragHovering) "Release to drop" else "Drop your APK here", + fontSize = if (isCompact) 18.sp else 22.sp, + fontWeight = FontWeight.Medium, + color = if (isDragHovering) + MorpheColors.Blue + else + MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + Text( + text = "or", + fontSize = if (isCompact) 12.sp else 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + OutlinedButton( + onClick = onBrowseClick, + modifier = Modifier.height(if (isCompact) 44.dp else 48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MorpheColors.Blue + ) + ) { + Text( + "Browse Files", + fontSize = if (isCompact) 14.sp else 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) + + Text( + text = "Supported: .apk files from APKMirror", + fontSize = if (isCompact) 11.sp else 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } +} + +@Composable +private fun SupportedAppsSection( + isCompact: Boolean = false, + maxWidth: Dp = 800.dp, + isLoading: Boolean = false, + supportedApps: List = emptyList(), + loadError: String? = null, + onRetry: () -> Unit = {} +) { + // Stack vertically if very narrow + val useVerticalLayout = maxWidth < 400.dp + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "SUPPORTED APPS", + fontSize = if (isCompact) 11.sp else 12.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + letterSpacing = 2.sp + ) + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + // Important notice about APK handling + Text( + text = "Download the exact version from APKMirror and drop it here directly. Do not rename or modify the file.", + fontSize = if (isCompact) 10.sp else 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + modifier = Modifier + .widthIn(max = if (useVerticalLayout) 280.dp else 500.dp) + .padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) + + when { + isLoading -> { + // Loading state + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(32.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(32.dp), + color = MorpheColors.Blue, + strokeWidth = 3.dp + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "Loading patches...", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + loadError != null -> { + // Error state + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + Text( + text = "Could not load supported apps", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.error + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = loadError, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + Spacer(modifier = Modifier.height(12.dp)) + OutlinedButton( + onClick = onRetry, + shape = RoundedCornerShape(8.dp) + ) { + Text("Retry") + } + } + } + supportedApps.isEmpty() -> { + // Empty state (shouldn't happen normally) + Text( + text = "No supported apps found", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + else -> { + // Display supported apps dynamically + if (useVerticalLayout) { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(horizontal = 16.dp) + .widthIn(max = 300.dp) + ) { + supportedApps.forEach { app -> + SupportedAppCardDynamic( + supportedApp = app, + isCompact = isCompact, + modifier = Modifier.fillMaxWidth() + ) + } + } + } else { + Row( + horizontalArrangement = Arrangement.spacedBy(if (isCompact) 12.dp else 16.dp), + verticalAlignment = Alignment.Top, + modifier = Modifier + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + .widthIn(max = 600.dp) + ) { + supportedApps.forEach { app -> + SupportedAppCardDynamic( + supportedApp = app, + isCompact = isCompact, + modifier = Modifier.weight(1f) + ) + } + } + } + } + } + } +} + +/** + * Card showing current patches version with option to change. + */ +@Composable +private fun PatchesVersionCard( + patchesVersion: String, + isLatest: Boolean, + onChangePatchesClick: () -> Unit, + isCompact: Boolean = false, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .clickable(onClick = onChangePatchesClick), + colors = CardDefaults.cardColors( + containerColor = MorpheColors.Blue.copy(alpha = 0.1f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = if (isCompact) 10.dp else 12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Using patches", + fontSize = if (isCompact) 12.sp else 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(8.dp)) + Surface( + color = MorpheColors.Blue.copy(alpha = 0.2f), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = patchesVersion, + fontSize = if (isCompact) 11.sp else 12.sp, + fontWeight = FontWeight.SemiBold, + color = MorpheColors.Blue, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + if (isLatest) { + Spacer(modifier = Modifier.width(6.dp)) + Surface( + color = MorpheColors.Teal.copy(alpha = 0.2f), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "Latest", + fontSize = if (isCompact) 9.sp else 10.sp, + color = MorpheColors.Teal, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } + } +} + +/** + * Dynamic supported app card that uses SupportedApp data from patches. + */ +@Composable +private fun SupportedAppCardDynamic( + supportedApp: SupportedApp, + isCompact: Boolean = false, + modifier: Modifier = Modifier +) { + var showAllVersions by remember { mutableStateOf(false) } + + val cardPadding = if (isCompact) 12.dp else 16.dp + val iconSize = if (isCompact) 48.dp else 56.dp + val iconInnerSize = if (isCompact) 32.dp else 40.dp + + // Get icon resource based on package name + val iconRes = when (supportedApp.packageName) { + AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube + AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music + AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit + else -> null + } + + // Get APKMirror URL from AppConstants (still hardcoded) + val apkMirrorUrl = when (supportedApp.packageName) { + AppConstants.YouTube.PACKAGE_NAME -> AppConstants.YouTube.APK_MIRROR_URL + AppConstants.YouTubeMusic.PACKAGE_NAME -> AppConstants.YouTubeMusic.APK_MIRROR_URL + AppConstants.Reddit.PACKAGE_NAME -> AppConstants.Reddit.APK_MIRROR_URL + else -> null + } + + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(if (isCompact) 12.dp else 16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(cardPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // App icon + Box( + modifier = Modifier + .size(iconSize) + .clip(RoundedCornerShape(if (isCompact) 10.dp else 12.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + if (iconRes != null) { + Image( + painter = painterResource(iconRes), + contentDescription = "${supportedApp.displayName} icon", + modifier = Modifier.size(iconInnerSize) + ) + } else { + // Fallback: show first letter of app name + Text( + text = supportedApp.displayName.first().toString(), + fontSize = if (isCompact) 20.sp else 24.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Blue + ) + } + } + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + // App name + Text( + text = supportedApp.displayName, + fontSize = if (isCompact) 14.sp else 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + + // Recommended version badge (dynamic from patches) + if (supportedApp.recommendedVersion != null) { + val cornerRadius = if (isCompact) 6.dp else 8.dp + Surface( + color = MorpheColors.Teal.copy(alpha = 0.15f), + shape = RoundedCornerShape(cornerRadius), + modifier = Modifier + .clip(RoundedCornerShape(cornerRadius)) + .clickable { showAllVersions = !showAllVersions } + ) { + Column( + modifier = Modifier.padding( + horizontal = if (isCompact) 10.dp else 12.dp, + vertical = if (isCompact) 6.dp else 8.dp + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Recommended", + fontSize = if (isCompact) 9.sp else 10.sp, + color = MorpheColors.Teal.copy(alpha = 0.8f), + letterSpacing = 0.5.sp + ) + Text( + text = "v${supportedApp.recommendedVersion}", + fontSize = if (isCompact) 12.sp else 14.sp, + fontWeight = FontWeight.SemiBold, + color = MorpheColors.Teal + ) + // Show version count if more than 1 (excluding recommended) + val otherVersionsCount = supportedApp.supportedVersions.count { it != supportedApp.recommendedVersion } + if (otherVersionsCount > 0) { + Text( + text = if (showAllVersions) "▲ Hide versions" else "▼ +$otherVersionsCount more", + fontSize = if (isCompact) 9.sp else 10.sp, + color = MorpheColors.Teal.copy(alpha = 0.6f) + ) + } + } + } + + // Expandable versions list (excluding recommended version) + val otherVersions = supportedApp.supportedVersions.filter { it != supportedApp.recommendedVersion } + if (showAllVersions && otherVersions.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Surface( + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), + shape = RoundedCornerShape(6.dp) + ) { + Column( + modifier = Modifier.padding(8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Other supported versions:", + fontSize = 9.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + // Show versions in a compact grid-like format + val versionsText = otherVersions.joinToString(", ") { "v$it" } + Text( + text = versionsText, + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + lineHeight = 14.sp + ) + } + } + } + } else { + // No specific version recommended + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp) + ) { + Text( + text = "Any version", + fontSize = if (isCompact) 11.sp else 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding( + horizontal = if (isCompact) 10.dp else 12.dp, + vertical = if (isCompact) 6.dp else 8.dp + ) + ) + } + } + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + // Download from APKMirror button (only if URL is configured) + if (apkMirrorUrl != null) { + OutlinedButton( + onClick = { + try { + java.awt.Desktop.getDesktop().browse(java.net.URI(apkMirrorUrl)) + } catch (e: Exception) { + // Ignore errors + } + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp), + contentPadding = PaddingValues( + horizontal = if (isCompact) 8.dp else 12.dp, + vertical = if (isCompact) 6.dp else 8.dp + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MorpheColors.Blue + ) + ) { + Text( + text = if (isCompact) "APKMirror" else "Get from APKMirror", + fontSize = if (isCompact) 11.sp else 12.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + } + + // Package name + Text( + text = supportedApp.packageName, + fontSize = if (isCompact) 9.sp else 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + } +} + +@Composable +private fun SupportedAppCard( + appType: AppType, + iconRes: org.jetbrains.compose.resources.DrawableResource, + isCompact: Boolean = false, + modifier: Modifier = Modifier +) { + val cardPadding = if (isCompact) 12.dp else 16.dp + val iconSize = if (isCompact) 48.dp else 56.dp + val iconInnerSize = if (isCompact) 32.dp else 40.dp + + Card( + modifier = modifier, + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(if (isCompact) 12.dp else 16.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(cardPadding), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // App icon + Box( + modifier = Modifier + .size(iconSize) + .clip(RoundedCornerShape(if (isCompact) 10.dp else 12.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource(iconRes), + contentDescription = "${appType.displayName} icon", + modifier = Modifier.size(iconInnerSize) + ) + } + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + // App name + Text( + text = appType.displayName, + fontSize = if (isCompact) 14.sp else 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + + // Suggested version badge + Surface( + color = MorpheColors.Teal.copy(alpha = 0.15f), + shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp) + ) { + Column( + modifier = Modifier.padding( + horizontal = if (isCompact) 10.dp else 12.dp, + vertical = if (isCompact) 6.dp else 8.dp + ), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Recommended", + fontSize = if (isCompact) 9.sp else 10.sp, + color = MorpheColors.Teal.copy(alpha = 0.8f), + letterSpacing = 0.5.sp + ) + Text( + text = "v${appType.suggestedVersion}", + fontSize = if (isCompact) 12.sp else 14.sp, + fontWeight = FontWeight.SemiBold, + color = MorpheColors.Teal + ) + } + } + + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + + // Download from APKMirror button + OutlinedButton( + onClick = { + try { + java.awt.Desktop.getDesktop().browse(java.net.URI(appType.apkMirrorUrl)) + } catch (e: Exception) { + // Ignore errors + } + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp), + contentPadding = PaddingValues( + horizontal = if (isCompact) 8.dp else 12.dp, + vertical = if (isCompact) 6.dp else 8.dp + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MorpheColors.Blue + ) + ) { + Text( + text = if (isCompact) "APKMirror" else "Get from APKMirror", + fontSize = if (isCompact) 11.sp else 12.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + + // Package name + Text( + text = appType.packageName, + fontSize = if (isCompact) 9.sp else 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center, + maxLines = 1 + ) + } + } +} + +@Composable +private fun DragOverlay() { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + MorpheColors.Blue.copy(alpha = 0.15f), + MorpheColors.Blue.copy(alpha = 0.05f) + ) + ) + ), + contentAlignment = Alignment.Center + ) { + Card( + modifier = Modifier.padding(32.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surface + ), + elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), + shape = RoundedCornerShape(24.dp) + ) { + Column( + modifier = Modifier.padding(48.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Drop APK here", + fontSize = 24.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Blue + ) + } + } + } +} + +private fun openFilePicker(): File? { + val fileDialog = FileDialog(null as Frame?, "Select APK File", FileDialog.LOAD).apply { + isMultipleMode = false + setFilenameFilter { _, name -> name.lowercase().endsWith(".apk") } + isVisible = true + } + + val directory = fileDialog.directory + val file = fileDialog.file + + return if (directory != null && file != null) { + File(directory, file) + } else { + null + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt new file mode 100644 index 0000000..1a622a8 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -0,0 +1,519 @@ +package app.morphe.gui.ui.screens.home + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import app.morphe.gui.data.model.Patch +import app.morphe.gui.data.model.SupportedApp +import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchRepository +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import net.dongliu.apk.parser.ApkFile +import app.morphe.gui.util.FileUtils +import app.morphe.gui.util.Logger +import app.morphe.gui.util.PatchService +import app.morphe.gui.util.SupportedAppExtractor +import java.io.File + +class HomeViewModel( + private val patchRepository: PatchRepository, + private val patchService: PatchService, + private val configRepository: ConfigRepository +) : ScreenModel { + + private val _uiState = MutableStateFlow(HomeUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + // Cached patches and supported apps + private var cachedPatches: List = emptyList() + private var cachedPatchesFile: File? = null + + init { + // Auto-fetch patches on startup + loadPatchesAndSupportedApps() + } + + // Track the last loaded version to avoid reloading unnecessarily + private var lastLoadedVersion: String? = null + + /** + * Load patches from GitHub and extract supported apps. + * If a saved version exists in config, load that version instead of latest. + */ + private fun loadPatchesAndSupportedApps(forceRefresh: Boolean = false) { + screenModelScope.launch { + _uiState.value = _uiState.value.copy(isLoadingPatches = true, patchLoadError = null) + + try { + // Check if there's a saved patches version in config + val config = configRepository.loadConfig() + val savedVersion = config.lastPatchesVersion + + // 1. Fetch all releases to find the right one + val releasesResult = patchRepository.fetchReleases() + val releases = releasesResult.getOrNull() + + if (releases.isNullOrEmpty()) { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Could not fetch patches: ${releasesResult.exceptionOrNull()?.message}" + ) + return@launch + } + + // Find the latest stable release for reference + val latestStable = releases.firstOrNull { !it.isDevRelease() } + val latestVersion = latestStable?.tagName + + // 2. Find the release to use - prefer saved version, fallback to latest stable + val release = if (savedVersion != null) { + releases.find { it.tagName == savedVersion } + ?: latestStable // Fallback to latest stable + } else { + latestStable // Latest stable + } + + if (release == null) { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "No suitable release found" + ) + return@launch + } + + // Skip reload if we've already loaded this version (unless forced) + if (!forceRefresh && lastLoadedVersion == release.tagName && cachedPatchesFile?.exists() == true) { + Logger.info("Skipping reload - already loaded version ${release.tagName}") + _uiState.value = _uiState.value.copy(isLoadingPatches = false) + return@launch + } + + Logger.info("Loading patches version: ${release.tagName} (saved=$savedVersion)") + + // 3. Download patches + val patchFileResult = patchRepository.downloadPatches(release) + val patchFile = patchFileResult.getOrNull() + + if (patchFile == null) { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Could not download patches: ${patchFileResult.exceptionOrNull()?.message}" + ) + return@launch + } + + cachedPatchesFile = patchFile + lastLoadedVersion = release.tagName + + // 3. Load patches using PatchService (direct library call) + val patchesResult = patchService.listPatches(patchFile.absolutePath) + val patches = patchesResult.getOrNull() + + if (patches == null || patches.isEmpty()) { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Could not load patches: ${patchesResult.exceptionOrNull()?.message}" + ) + return@launch + } + + cachedPatches = patches + + // 5. Extract supported apps + val supportedApps = SupportedAppExtractor.extractSupportedApps(patches) + Logger.info("Loaded ${supportedApps.size} supported apps from patches: ${supportedApps.map { "${it.displayName} (${it.recommendedVersion})" }}") + + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + supportedApps = supportedApps, + patchesVersion = release.tagName, + latestPatchesVersion = latestVersion, + patchLoadError = null + ) + } catch (e: Exception) { + Logger.error("Failed to load patches and supported apps", e) + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = e.message ?: "Unknown error" + ) + } + } + } + + /** + * Retry loading patches. + */ + fun retryLoadPatches() { + loadPatchesAndSupportedApps(forceRefresh = true) + } + + /** + * Refresh patches if a different version was selected. + * Called when returning to HomeScreen from PatchesScreen. + */ + fun refreshPatchesIfNeeded() { + screenModelScope.launch { + val config = configRepository.loadConfig() + val savedVersion = config.lastPatchesVersion + + // If saved version differs from currently loaded version, reload + if (savedVersion != null && savedVersion != lastLoadedVersion) { + Logger.info("Patches version changed: $lastLoadedVersion -> $savedVersion, reloading...") + loadPatchesAndSupportedApps(forceRefresh = true) + } + } + } + + /** + * Get the cached patches file path for navigation to next screen. + */ + fun getCachedPatchesFile(): File? = cachedPatchesFile + + /** + * Get recommended version for a package from loaded patches. + */ + fun getRecommendedVersion(packageName: String): String? { + return SupportedAppExtractor.getRecommendedVersion(cachedPatches, packageName) + } + + fun onFileSelected(file: File) { + screenModelScope.launch { + Logger.info("File selected: ${file.absolutePath}") + + _uiState.value = _uiState.value.copy(isAnalyzing = true) + + val validationResult = withContext(Dispatchers.IO) { + validateAndAnalyzeApk(file) + } + + if (validationResult.isValid) { + _uiState.value = _uiState.value.copy( + selectedApk = file, + apkInfo = validationResult.apkInfo, + error = null, + isReady = true, + isAnalyzing = false + ) + Logger.info("APK analyzed successfully: ${validationResult.apkInfo?.appName ?: file.name}") + } else { + _uiState.value = _uiState.value.copy( + selectedApk = null, + apkInfo = null, + error = validationResult.errorMessage, + isReady = false, + isAnalyzing = false + ) + Logger.warn("APK validation failed: ${validationResult.errorMessage}") + } + } + } + + fun onFilesDropped(files: List) { + val apkFile = files.firstOrNull { FileUtils.isApkFile(it) } + if (apkFile != null) { + onFileSelected(apkFile) + } else { + _uiState.value = _uiState.value.copy( + error = "No valid APK file found. Please drop an .apk file.", + isReady = false + ) + } + } + + fun clearSelection() { + // Preserve loaded patches state when clearing APK selection + _uiState.value = _uiState.value.copy( + selectedApk = null, + apkInfo = null, + error = null, + isDragHovering = false, + isReady = false, + isAnalyzing = false + ) + Logger.info("APK selection cleared") + } + + fun clearError() { + _uiState.value = _uiState.value.copy(error = null) + } + + fun setDragHover(isHovering: Boolean) { + _uiState.value = _uiState.value.copy(isDragHovering = isHovering) + } + + private fun validateAndAnalyzeApk(file: File): ApkValidationResult { + if (!file.exists()) { + return ApkValidationResult(false, errorMessage = "File does not exist") + } + + if (!file.isFile) { + return ApkValidationResult(false, errorMessage = "Selected item is not a file") + } + + if (!FileUtils.isApkFile(file)) { + return ApkValidationResult(false, errorMessage = "File must have .apk extension") + } + + if (file.length() < 1024) { + return ApkValidationResult(false, errorMessage = "File is too small to be a valid APK") + } + + // Parse APK info from AndroidManifest.xml using apk-parser + val apkInfo = parseApkManifest(file) + + return if (apkInfo != null) { + ApkValidationResult(true, apkInfo = apkInfo) + } else { + ApkValidationResult(false, errorMessage = "Could not parse APK. The file may be corrupted or not a valid APK.") + } + } + + /** + * Parse APK metadata directly from AndroidManifest.xml using apk-parser library. + * This works with APKs from any source, not just APKMirror. + */ + private fun parseApkManifest(file: File): ApkInfo? { + return try { + ApkFile(file).use { apk -> + val meta = apk.apkMeta + + val packageName = meta.packageName + val versionName = meta.versionName ?: "Unknown" + val minSdk = meta.minSdkVersion?.toIntOrNull() + + // Check if package is supported - first check dynamic, then fallback to hardcoded + val dynamicSupportedApp = _uiState.value.supportedApps.find { it.packageName == packageName } + val isSupported = dynamicSupportedApp != null || + packageName in listOf( + app.morphe.gui.data.constants.AppConstants.YouTube.PACKAGE_NAME, + app.morphe.gui.data.constants.AppConstants.YouTubeMusic.PACKAGE_NAME + ) + + if (!isSupported) { + Logger.warn("Unsupported package: $packageName") + return null + } + + // Get app display name - prefer dynamic, fallback to hardcoded + val appName = dynamicSupportedApp?.displayName + ?: SupportedApp.getDisplayName(packageName) + + // Get recommended version - prefer dynamic, fallback to hardcoded + val suggestedVersion = dynamicSupportedApp?.recommendedVersion + ?: app.morphe.gui.data.constants.AppConstants.getSuggestedVersion(packageName) + + // Determine AppType for backward compatibility (still used in some places) + val appType = when (packageName) { + app.morphe.gui.data.constants.AppConstants.YouTube.PACKAGE_NAME -> AppType.YOUTUBE + app.morphe.gui.data.constants.AppConstants.YouTubeMusic.PACKAGE_NAME -> AppType.YOUTUBE_MUSIC + else -> null + } + + // Compare versions if we have a suggested version + val versionStatus = if (suggestedVersion != null) { + compareVersions(versionName, suggestedVersion) + } else { + VersionStatus.UNKNOWN + } + + // Get supported architectures from native libraries in the APK + val architectures = extractArchitectures(file) + + // Verify checksum (still uses AppConstants for now) + val checksumStatus = verifyChecksum(file, packageName, versionName, architectures, suggestedVersion) + + Logger.info("Parsed APK: $packageName v$versionName (recommended=$suggestedVersion, minSdk=$minSdk, archs=$architectures)") + + ApkInfo( + fileName = file.name, + filePath = file.absolutePath, + fileSize = file.length(), + formattedSize = formatFileSize(file.length()), + appName = appName, + appType = appType, + packageName = packageName, + versionName = versionName, + architectures = architectures, + minSdk = minSdk, + suggestedVersion = suggestedVersion, + versionStatus = versionStatus, + checksumStatus = checksumStatus + ) + } + } catch (e: Exception) { + Logger.error("Failed to parse APK manifest", e) + null + } + } + + /** + * Extract supported CPU architectures from native libraries in the APK. + * Uses ZipFile to scan for lib// directories. + */ + private fun extractArchitectures(file: File): List { + return try { + java.util.zip.ZipFile(file).use { zip -> + val archDirs = zip.entries().asSequence() + .map { it.name } + .filter { it.startsWith("lib/") } + .mapNotNull { path -> + val parts = path.split("/") + if (parts.size >= 2) parts[1] else null + } + .distinct() + .toList() + + archDirs.ifEmpty { + // No native libs - likely a universal APK + listOf("universal") + } + } + } catch (e: Exception) { + Logger.warn("Could not extract architectures: ${e.message}") + emptyList() + } + } + + /** + * Verify the APK checksum against expected values. + */ + private fun verifyChecksum( + file: File, + packageName: String, + version: String, + architectures: List, + recommendedVersion: String? + ): app.morphe.gui.util.ChecksumStatus { + // Check if this is a non-recommended version (use dynamic recommended version) + if (recommendedVersion != null && version != recommendedVersion) { + return app.morphe.gui.util.ChecksumStatus.NonRecommendedVersion + } + + // Get expected checksum (still from AppConstants - checksums are manually maintained) + val expectedChecksum = app.morphe.gui.data.constants.AppConstants.getChecksum(packageName, version, architectures) + if (expectedChecksum == null) { + return app.morphe.gui.util.ChecksumStatus.NotConfigured + } + + // Calculate actual checksum + return try { + val actualChecksum = app.morphe.gui.util.ChecksumUtils.calculateSha256(file) + Logger.info("Checksum verification - Expected: $expectedChecksum, Actual: $actualChecksum") + + if (actualChecksum.equals(expectedChecksum, ignoreCase = true)) { + app.morphe.gui.util.ChecksumStatus.Verified + } else { + app.morphe.gui.util.ChecksumStatus.Mismatch(expectedChecksum, actualChecksum) + } + } catch (e: Exception) { + Logger.error("Checksum calculation failed", e) + app.morphe.gui.util.ChecksumStatus.Error(e.message ?: "Unknown error") + } + } + + private fun formatFileSize(bytes: Long): String { + return when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) + bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) + else -> "%.2f GB".format(bytes / (1024.0 * 1024.0 * 1024.0)) + } + } + + /** + * Compares two version strings (e.g., "19.16.39" vs "20.40.45") + * Returns the version status of the current version relative to suggested. + */ + private fun compareVersions(current: String, suggested: String): VersionStatus { + return try { + val currentParts = current.split(".").map { it.toInt() } + val suggestedParts = suggested.split(".").map { it.toInt() } + + // Compare each part + for (i in 0 until maxOf(currentParts.size, suggestedParts.size)) { + val currentPart = currentParts.getOrElse(i) { 0 } + val suggestedPart = suggestedParts.getOrElse(i) { 0 } + + when { + currentPart > suggestedPart -> return VersionStatus.NEWER_VERSION + currentPart < suggestedPart -> return VersionStatus.OLDER_VERSION + } + } + VersionStatus.EXACT_MATCH + } catch (e: Exception) { + Logger.warn("Failed to compare versions: $current vs $suggested") + VersionStatus.UNKNOWN + } + } +} + +data class HomeUiState( + val selectedApk: File? = null, + val apkInfo: ApkInfo? = null, + val error: String? = null, + val isDragHovering: Boolean = false, + val isReady: Boolean = false, + val isAnalyzing: Boolean = false, + // Dynamic patches data + val isLoadingPatches: Boolean = true, + val supportedApps: List = emptyList(), + val patchesVersion: String? = null, + val latestPatchesVersion: String? = null, // Track the latest available version + val patchLoadError: String? = null +) { + val isUsingLatestPatches: Boolean + get() = patchesVersion != null && patchesVersion == latestPatchesVersion +} + +enum class AppType( + val displayName: String, + val packageName: String, + val suggestedVersion: String, + val apkMirrorUrl: String +) { + YOUTUBE( + displayName = app.morphe.gui.data.constants.AppConstants.YouTube.DISPLAY_NAME, + packageName = app.morphe.gui.data.constants.AppConstants.YouTube.PACKAGE_NAME, + suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTube.SUGGESTED_VERSION, + apkMirrorUrl = app.morphe.gui.data.constants.AppConstants.YouTube.APK_MIRROR_URL + ), + YOUTUBE_MUSIC( + displayName = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.DISPLAY_NAME, + packageName = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.PACKAGE_NAME, + suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.SUGGESTED_VERSION, + apkMirrorUrl = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.APK_MIRROR_URL + ) +} + +data class ApkInfo( + val fileName: String, + val filePath: String, + val fileSize: Long, + val formattedSize: String, + val appName: String, + val appType: AppType?, // Nullable for dynamically supported apps not in the enum + val packageName: String, + val versionName: String, + val architectures: List = emptyList(), + val minSdk: Int? = null, + val suggestedVersion: String? = null, + val versionStatus: VersionStatus = VersionStatus.UNKNOWN, + val checksumStatus: app.morphe.gui.util.ChecksumStatus = app.morphe.gui.util.ChecksumStatus.NotConfigured +) + +enum class VersionStatus { + EXACT_MATCH, // Using the suggested version + OLDER_VERSION, // Using an older version (newer patches available) + NEWER_VERSION, // Using a newer version (might have issues) + UNKNOWN // Could not determine +} + +data class ApkValidationResult( + val isValid: Boolean, + val apkInfo: ApkInfo? = null, + val errorMessage: String? = null +) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkDropZone.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkDropZone.kt new file mode 100644 index 0000000..13f7c01 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkDropZone.kt @@ -0,0 +1,212 @@ +package app.morphe.gui.ui.screens.home.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.awtTransferable +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import app.morphe.gui.ui.screens.home.ApkInfo +import java.awt.datatransfer.DataFlavor +import java.io.File + +@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) +@Composable +fun ApkDropZone( + apkInfo: ApkInfo?, + isDragHovering: Boolean, + onDragHoverChange: (Boolean) -> Unit, + onFilesDropped: (List) -> Unit, + onBrowseClick: () -> Unit, + onClearClick: () -> Unit, + modifier: Modifier = Modifier +) { + val borderColor = when { + apkInfo != null -> MaterialTheme.colorScheme.primary + isDragHovering -> MaterialTheme.colorScheme.primary + else -> MaterialTheme.colorScheme.outline + } + + val backgroundColor = when { + apkInfo != null -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + isDragHovering -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.5f) + else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + } + + val dragAndDropTarget = remember { + object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + onDragHoverChange(true) + } + + override fun onEnded(event: DragAndDropEvent) { + onDragHoverChange(false) + } + + override fun onExited(event: DragAndDropEvent) { + onDragHoverChange(false) + } + + override fun onEntered(event: DragAndDropEvent) { + onDragHoverChange(true) + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + onDragHoverChange(false) + val transferable = event.awtTransferable + return try { + if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + @Suppress("UNCHECKED_CAST") + val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List + if (files.isNotEmpty()) { + onFilesDropped(files) + true + } else { + false + } + } else { + false + } + } catch (e: Exception) { + false + } + } + } + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(200.dp) + .clip(RoundedCornerShape(16.dp)) + .border( + width = 2.dp, + color = borderColor, + shape = RoundedCornerShape(16.dp) + ) + .background(backgroundColor) + .dragAndDropTarget( + shouldStartDragAndDrop = { true }, + target = dragAndDropTarget + ), + contentAlignment = Alignment.Center + ) { + if (apkInfo != null) { + ApkSelectedContent( + apkInfo = apkInfo, + onClearClick = onClearClick + ) + } else { + DropZoneEmptyContent( + isDragHovering = isDragHovering, + onBrowseClick = onBrowseClick + ) + } + } +} + +@Composable +private fun DropZoneEmptyContent( + isDragHovering: Boolean, + onBrowseClick: () -> Unit +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.padding(24.dp) + ) { + Text( + text = if (isDragHovering) "Drop here" else "Drag & drop APK file here", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Text( + text = "or", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Button( + onClick = onBrowseClick, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Text("Browse Files") + } + } +} + +@Composable +private fun ApkSelectedContent( + apkInfo: ApkInfo, + onClearClick: () -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Selected", + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(48.dp) + ) + + Column( + modifier = Modifier.weight(1f) + ) { + Text( + text = apkInfo.fileName, + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = apkInfo.formattedSize, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = apkInfo.filePath, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + + IconButton(onClick = onClearClick) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Clear selection", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt new file mode 100644 index 0000000..cf4202e --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -0,0 +1,414 @@ +package app.morphe.gui.ui.screens.home.components + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.morphe.morphe_cli.generated.resources.Res +import app.morphe.morphe_cli.generated.resources.reddit +import app.morphe.morphe_cli.generated.resources.youtube +import app.morphe.morphe_cli.generated.resources.youtube_music +import org.jetbrains.compose.resources.painterResource +import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.ui.screens.home.ApkInfo +import app.morphe.gui.ui.screens.home.AppType +import app.morphe.gui.ui.screens.home.VersionStatus +import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.ChecksumStatus + +@Composable +fun ApkInfoCard( + apkInfo: ApkInfo, + onClearClick: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(16.dp) + ) { + Column( + modifier = Modifier.padding(20.dp) + ) { + // Header with app icon and close button + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.weight(1f) + ) { + // App icon - determine from appType or packageName + val iconRes = when { + apkInfo.appType == AppType.YOUTUBE -> Res.drawable.youtube + apkInfo.appType == AppType.YOUTUBE_MUSIC -> Res.drawable.youtube_music + apkInfo.packageName == AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube + apkInfo.packageName == AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music + apkInfo.packageName == AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit + else -> null + } + + Box( + modifier = Modifier + .size(64.dp) + .clip(RoundedCornerShape(14.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + if (iconRes != null) { + Image( + painter = painterResource(iconRes), + contentDescription = "${apkInfo.appName} icon", + modifier = Modifier.size(48.dp) + ) + } else { + // Fallback: show first letter of app name + Text( + text = apkInfo.appName.first().toString(), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Blue + ) + } + } + + Column { + // App name + Text( + text = apkInfo.appName, + fontSize = 22.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(2.dp)) + + // Version + Text( + text = "v${apkInfo.versionName}", + fontSize = 15.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Close button + IconButton( + onClick = onClearClick, + modifier = Modifier + .size(32.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.8f)) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) + ) + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + // Info grid + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Size + InfoColumn( + label = "Size", + value = apkInfo.formattedSize, + modifier = Modifier.weight(1f) + ) + + // Architecture + InfoColumn( + label = "Architecture", + value = formatArchitectures(apkInfo.architectures), + modifier = Modifier.weight(1f) + ) + + // Min SDK + if (apkInfo.minSdk != null) { + InfoColumn( + label = "Min SDK", + value = "API ${apkInfo.minSdk}", + modifier = Modifier.weight(1f) + ) + } + } + + // Version and checksum status section + Spacer(modifier = Modifier.height(16.dp)) + + HorizontalDivider( + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + // Version status + if (apkInfo.suggestedVersion != null && apkInfo.versionStatus != VersionStatus.EXACT_MATCH) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + VersionStatusBanner( + versionStatus = apkInfo.versionStatus, + currentVersion = apkInfo.versionName, + suggestedVersion = apkInfo.suggestedVersion + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Checksum warning for non-recommended versions + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Checksum verification unavailable for non-recommended versions", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + textAlign = TextAlign.Center + ) + } + } else if (apkInfo.versionStatus == VersionStatus.EXACT_MATCH) { + // Show checksum status for recommended version + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center + ) { + ChecksumStatusBanner(checksumStatus = apkInfo.checksumStatus) + } + } + } + } +} + +@Composable +private fun ChecksumStatusBanner(checksumStatus: ChecksumStatus) { + when (checksumStatus) { + is ChecksumStatus.Verified -> { + Surface( + color = MorpheColors.Teal.copy(alpha = 0.15f), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Recommended version - Verified", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal + ) + Text( + text = "Checksum matches APKMirror", + fontSize = 10.sp, + color = MorpheColors.Teal.copy(alpha = 0.8f) + ) + } + } + } + + is ChecksumStatus.Mismatch -> { + Surface( + color = MaterialTheme.colorScheme.error.copy(alpha = 0.15f), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Checksum Mismatch", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.error + ) + Text( + text = "File may be corrupted or modified. Re-download from APKMirror.", + fontSize = 10.sp, + color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f), + textAlign = TextAlign.Center + ) + } + } + } + + is ChecksumStatus.NotConfigured -> { + Surface( + color = MorpheColors.Teal.copy(alpha = 0.15f), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Using recommended version", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) + ) + } + } + + is ChecksumStatus.Error -> { + Surface( + color = Color(0xFFFF9800).copy(alpha = 0.15f), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Using recommended version", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFFFF9800) + ) + Text( + text = "Could not verify checksum", + fontSize = 10.sp, + color = Color(0xFFFF9800).copy(alpha = 0.8f) + ) + } + } + } + + is ChecksumStatus.NonRecommendedVersion -> { + // This shouldn't happen in this branch, but handle it gracefully + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Using non-recommended version", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) + ) + } + } + } +} + +@Composable +private fun InfoColumn( + label: String, + value: String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.Start + ) { + Text( + text = label, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = value, + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } +} + +@Composable +private fun VersionStatusBanner( + versionStatus: VersionStatus, + currentVersion: String, + suggestedVersion: String +) { + val (backgroundColor, textColor, message) = when (versionStatus) { + VersionStatus.OLDER_VERSION -> Triple( + Color(0xFFFF9800).copy(alpha = 0.15f), + Color(0xFFFF9800), + "Newer patches available for v$suggestedVersion" + ) + VersionStatus.NEWER_VERSION -> Triple( + MaterialTheme.colorScheme.error.copy(alpha = 0.15f), + MaterialTheme.colorScheme.error, + "Version too new. Recommended: v$suggestedVersion" + ) + else -> Triple( + MaterialTheme.colorScheme.surfaceVariant, + MaterialTheme.colorScheme.onSurfaceVariant, + "Suggested version: v$suggestedVersion" + ) + } + + Surface( + color = backgroundColor, + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = message, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = textColor, + textAlign = TextAlign.Center + ) + if (versionStatus == VersionStatus.NEWER_VERSION) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Patching may not work correctly with newer versions", + fontSize = 11.sp, + color = textColor.copy(alpha = 0.8f), + textAlign = TextAlign.Center + ) + } + } + } +} + +private fun formatArchitectures(archs: List): String { + if (archs.isEmpty()) return "Unknown" + + // Show full architecture names for clarity + val formatted = archs.map { arch -> + when (arch) { + "arm64-v8a" -> "arm64-v8a" + "armeabi-v7a" -> "armeabi-v7a" + "x86_64" -> "x86_64" + "x86" -> "x86" + else -> arch + } + } + + return formatted.joinToString(", ") +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt new file mode 100644 index 0000000..8db0374 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt @@ -0,0 +1,74 @@ +package app.morphe.gui.ui.screens.home.components + +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.awtTransferable +import java.awt.datatransfer.DataFlavor +import java.io.File + +@OptIn(androidx.compose.ui.ExperimentalComposeUiApi::class) +@Composable +fun FullScreenDropZone( + isDragHovering: Boolean, + onDragHoverChange: (Boolean) -> Unit, + onFilesDropped: (List) -> Unit, + content: @Composable () -> Unit +) { + val dragAndDropTarget = remember { + object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + onDragHoverChange(true) + } + + override fun onEnded(event: DragAndDropEvent) { + onDragHoverChange(false) + } + + override fun onExited(event: DragAndDropEvent) { + onDragHoverChange(false) + } + + override fun onEntered(event: DragAndDropEvent) { + onDragHoverChange(true) + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + onDragHoverChange(false) + val transferable = event.awtTransferable + return try { + if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + @Suppress("UNCHECKED_CAST") + val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List + if (files.isNotEmpty()) { + onFilesDropped(files) + true + } else { + false + } + } else { + false + } + } catch (e: Exception) { + false + } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .dragAndDropTarget( + shouldStartDragAndDrop = { true }, + target = dragAndDropTarget + ) + ) { + content() + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt new file mode 100644 index 0000000..0ccab99 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -0,0 +1,707 @@ +package app.morphe.gui.ui.screens.patches + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.ExpandLess +import androidx.compose.material.icons.filled.ExpandMore +import androidx.compose.material.icons.filled.Terminal +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import app.morphe.gui.data.model.Patch +import org.koin.core.parameter.parametersOf +import app.morphe.gui.ui.components.ErrorDialog +import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.getErrorType +import app.morphe.gui.ui.components.getFriendlyErrorMessage +import app.morphe.gui.ui.screens.patching.PatchingScreen +import app.morphe.gui.ui.theme.MorpheColors +import java.awt.Toolkit +import java.awt.datatransfer.StringSelection + +/** + * Screen for selecting which patches to apply. + */ +data class PatchSelectionScreen( + val apkPath: String, + val apkName: String, + val patchesFilePath: String +) : Screen { + + @Composable + override fun Content() { + val viewModel = koinScreenModel { + parametersOf(apkPath, apkName, patchesFilePath) + } + PatchSelectionScreenContent(viewModel = viewModel) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { + val navigator = LocalNavigator.currentOrThrow + val uiState by viewModel.uiState.collectAsState() + + var showErrorDialog by remember { mutableStateOf(false) } + var currentError by remember { mutableStateOf(null) } + + LaunchedEffect(uiState.error) { + uiState.error?.let { error -> + currentError = error + showErrorDialog = true + } + } + + // Error dialog + if (showErrorDialog && currentError != null) { + ErrorDialog( + title = "Error Loading Patches", + message = getFriendlyErrorMessage(currentError!!), + errorType = getErrorType(currentError!!), + onDismiss = { + showErrorDialog = false + viewModel.clearError() + }, + onRetry = { + showErrorDialog = false + viewModel.clearError() + viewModel.loadPatches() + } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Text("Select Patches", fontWeight = FontWeight.SemiBold) + Text( + text = "${uiState.selectedCount} of ${uiState.totalCount} selected", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + navigationIcon = { + IconButton(onClick = { navigator.pop() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + // Select all / Deselect all + TextButton(onClick = { + if (uiState.selectedPatches.size == uiState.allPatches.size) { + viewModel.deselectAll() + } else { + viewModel.selectAll() + } + }) { + Text( + if (uiState.selectedPatches.size == uiState.allPatches.size) "Deselect All" else "Select All", + color = MorpheColors.Blue + ) + } + SettingsButton(allowCacheClear = false) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + }, + ) { paddingValues -> + // State for command preview + var cleanMode by remember { mutableStateOf(false) } + var isCollapsed by remember { mutableStateOf(false) } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + // Command preview at the top - updates in real-time + if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { + val commandPreview = remember(uiState.selectedPatches, cleanMode) { + viewModel.getCommandPreview(cleanMode) + } + CommandPreview( + command = commandPreview, + cleanMode = cleanMode, + isCollapsed = isCollapsed, + onToggleMode = { cleanMode = !cleanMode }, + onToggleCollapse = { isCollapsed = !isCollapsed }, + onCopy = { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + clipboard.setContents(StringSelection(commandPreview), null) + }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + // Search bar + SearchBar( + query = uiState.searchQuery, + onQueryChange = { viewModel.setSearchQuery(it) }, + showOnlySelected = uiState.showOnlySelected, + onShowOnlySelectedChange = { viewModel.setShowOnlySelected(it) }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + // Commonly disabled patches suggestion + val commonlyDisabledPatches = remember(uiState.selectedPatches, uiState.allPatches) { + viewModel.getCommonlyDisabledPatches() + } + var suggestionDismissed by remember { mutableStateOf(false) } + + AnimatedVisibility( + visible = commonlyDisabledPatches.isNotEmpty() && !suggestionDismissed && !uiState.isLoading, + enter = expandVertically(), + exit = shrinkVertically() + ) { + CommonlyDisabledSuggestion( + patches = commonlyDisabledPatches, + onDeselectAll = { viewModel.deselectCommonlyDisabled() }, + onDismiss = { suggestionDismissed = true }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CircularProgressIndicator(color = MorpheColors.Blue) + Text( + text = "Loading patches...", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + uiState.filteredPatches.isEmpty() && !uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = if (uiState.searchQuery.isNotBlank()) "No patches match your search" else "No patches found", + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + else -> { + // Patch list + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items( + items = uiState.filteredPatches, + key = { it.uniqueId } + ) { patch -> + PatchListItem( + patch = patch, + isSelected = uiState.selectedPatches.contains(patch.uniqueId), + onToggle = { viewModel.togglePatch(patch.uniqueId) } + ) + } + } + + // Bottom action bar + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 3.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Button( + onClick = { + val config = viewModel.createPatchConfig() + navigator.push(PatchingScreen(config)) + }, + enabled = uiState.selectedPatches.isNotEmpty(), + modifier = Modifier + .weight(1f) + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Patch (${uiState.selectedCount})", + fontWeight = FontWeight.Medium + ) + } + } + } + } + } + } + } +} + +@Composable +private fun SearchBar( + query: String, + onQueryChange: (String) -> Unit, + showOnlySelected: Boolean, + onShowOnlySelectedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + OutlinedTextField( + value = query, + onValueChange = onQueryChange, + modifier = Modifier.weight(1f), + placeholder = { Text("Search patches...") }, + leadingIcon = { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + trailingIcon = { + if (query.isNotEmpty()) { + IconButton(onClick = { onQueryChange("") }) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MorpheColors.Blue, + unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) + ) + ) + + FilterChip( + selected = showOnlySelected, + onClick = { onShowOnlySelectedChange(!showOnlySelected) }, + label = { Text("Selected") }, + leadingIcon = if (showOnlySelected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + } + } else null + ) + } +} + +@Composable +private fun PatchListItem( + patch: Patch, + isSelected: Boolean, + onToggle: () -> Unit +) { + val backgroundColor = if (isSelected) { + MorpheColors.Blue.copy(alpha = 0.1f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onToggle), + colors = CardDefaults.cardColors(containerColor = backgroundColor), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = isSelected, + onCheckedChange = { onToggle() }, + colors = CheckboxDefaults.colors( + checkedColor = MorpheColors.Blue, + uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = patch.name, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + + if (patch.description.isNotBlank()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = patch.description, + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + + // Show compatible packages if any + if (patch.compatiblePackages.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + patch.compatiblePackages.take(2).forEach { pkg -> + Surface( + color = MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = pkg.name.substringAfterLast("."), + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } + } + + // Show options indicator if patch has options + if (patch.options.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${patch.options.size} option${if (patch.options.size > 1) "s" else ""} available", + fontSize = 11.sp, + color = MorpheColors.Teal + ) + } + } + } + } +} + +@Composable +private fun CommonlyDisabledSuggestion( + patches: List>, + onDeselectAll: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = Color(0xFFFF9800).copy(alpha = 0.1f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = Color(0xFFFF9800), + modifier = Modifier.size(18.dp) + ) + Text( + text = "Commonly disabled patches", + fontWeight = FontWeight.Medium, + fontSize = 13.sp, + color = Color(0xFFFF9800) + ) + } + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Dismiss", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "These ${patches.size} patch${if (patches.size > 1) "es are" else " is"} commonly disabled by users:", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(6.dp)) + + // List patch names + patches.take(4).forEach { (patch, _) -> + Text( + text = "• ${patch.name}", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (patches.size > 4) { + Text( + text = "• +${patches.size - 4} more", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(10.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + TextButton( + onClick = onDismiss, + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text("Keep all", fontSize = 12.sp) + } + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = { + onDeselectAll() + onDismiss() + }, + colors = ButtonDefaults.buttonColors( + containerColor = Color(0xFFFF9800) + ), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), + shape = RoundedCornerShape(8.dp) + ) { + Text("Deselect these", fontSize = 12.sp) + } + } + } + } +} + +/** + * Terminal-style command preview showing the CLI command that will be executed. + */ +@Composable +private fun CommandPreview( + command: String, + cleanMode: Boolean, + isCollapsed: Boolean, + onToggleMode: () -> Unit, + onToggleCollapse: () -> Unit, + onCopy: () -> Unit, + modifier: Modifier = Modifier +) { + val terminalBackground = Color(0xFF1E1E1E) + val terminalGreen = Color(0xFF4EC9B0) + val terminalText = Color(0xFFD4D4D4) + val terminalDim = Color(0xFF6A9955) + + var showCopied by remember { mutableStateOf(false) } + + // Reset "Copied!" message after a delay + LaunchedEffect(showCopied) { + if (showCopied) { + kotlinx.coroutines.delay(1500) + showCopied = false + } + } + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors(containerColor = terminalBackground), + shape = RoundedCornerShape(8.dp) + ) { + Column( + modifier = Modifier.padding(12.dp) + ) { + // Header with terminal icon, controls, and collapse toggle + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + // Left side - icon, title, and collapse toggle + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .clip(RoundedCornerShape(4.dp)) + .clickable(onClick = onToggleCollapse) + .padding(end = 8.dp) + ) { + Icon( + imageVector = Icons.Default.Terminal, + contentDescription = null, + tint = terminalGreen, + modifier = Modifier.size(14.dp) + ) + Text( + text = "Command Preview", + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = terminalGreen + ) + Icon( + imageVector = if (isCollapsed) Icons.Default.ExpandMore else Icons.Default.ExpandLess, + contentDescription = if (isCollapsed) "Expand" else "Collapse", + tint = terminalDim, + modifier = Modifier.size(16.dp) + ) + } + + // Right side - controls + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Copy button + Surface( + onClick = { + onCopy() + showCopied = true + }, + color = Color.Transparent, + shape = RoundedCornerShape(4.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = "Copy", + tint = if (showCopied) terminalGreen else terminalDim, + modifier = Modifier.size(12.dp) + ) + Text( + text = if (showCopied) "Copied!" else "Copy", + fontSize = 10.sp, + color = if (showCopied) terminalGreen else terminalDim + ) + } + } + + // Mode toggle (only show when not collapsed) + if (!isCollapsed) { + Surface( + onClick = onToggleMode, + color = Color.Transparent, + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = if (cleanMode) "compact" else "expand", + fontSize = 10.sp, + color = terminalDim, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } + } + + // Command text - collapsible, vertically scrollable + AnimatedVisibility( + visible = !isCollapsed, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Column { + Spacer(modifier = Modifier.height(8.dp)) + + // Vertically scrollable command text with max height + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 120.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = command, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = terminalText, + lineHeight = 16.sp + ) + } + } + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt new file mode 100644 index 0000000..3136327 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -0,0 +1,325 @@ +package app.morphe.gui.ui.screens.patches + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.data.model.Patch +import app.morphe.gui.data.model.PatchConfig +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import app.morphe.gui.util.Logger +import app.morphe.gui.util.PatchService +import app.morphe.gui.data.repository.PatchRepository +import java.io.File + +class PatchSelectionViewModel( + private val apkPath: String, + private val apkName: String, + private val patchesFilePath: String, + private val patchService: PatchService, + private val patchRepository: PatchRepository +) : ScreenModel { + + // Actual path to use - may differ from patchesFilePath if we had to re-download + private var actualPatchesFilePath: String = patchesFilePath + + private val _uiState = MutableStateFlow(PatchSelectionUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadPatches() + } + + fun getApkPath(): String = apkPath + fun getPatchesFilePath(): String = actualPatchesFilePath + + fun loadPatches() { + screenModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + + // First, ensure the patches file exists - download if missing + val patchesFile = File(patchesFilePath) + if (!patchesFile.exists()) { + Logger.info("Patches file not found at $patchesFilePath, attempting to download...") + + // Try to extract version from the filename and find a matching release + // Filename format: morphe-patches-x.x.x.mpp or similar + val downloadResult = downloadMissingPatches(patchesFile.name) + if (downloadResult.isFailure) { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = "Patches file missing and could not be downloaded: ${downloadResult.exceptionOrNull()?.message}" + ) + return@launch + } + actualPatchesFilePath = downloadResult.getOrNull()!!.absolutePath + } + + val packageName = getPackageNameFromApk() + + // Load patches using PatchService (direct library call) + val patchesResult = patchService.listPatches(actualPatchesFilePath, packageName) + + patchesResult.fold( + onSuccess = { patches -> + // Deduplicate by uniqueId in case of true duplicates + val deduplicatedPatches = patches.distinctBy { it.uniqueId } + + Logger.info("Loaded ${deduplicatedPatches.size} patches for $packageName") + + _uiState.value = _uiState.value.copy( + isLoading = false, + allPatches = deduplicatedPatches, + filteredPatches = deduplicatedPatches, + selectedPatches = deduplicatedPatches.map { it.uniqueId }.toSet() + ) + }, + onFailure = { e -> + _uiState.value = _uiState.value.copy( + isLoading = false, + error = "Failed to list patches: ${e.message}" + ) + Logger.error("Failed to list patches", e) + } + ) + } + } + + fun togglePatch(patchId: String) { + val current = _uiState.value.selectedPatches + val newSelection = if (current.contains(patchId)) { + current - patchId + } else { + current + patchId + } + _uiState.value = _uiState.value.copy(selectedPatches = newSelection) + } + + fun selectAll() { + val allIds = _uiState.value.filteredPatches.map { it.uniqueId }.toSet() + _uiState.value = _uiState.value.copy(selectedPatches = allIds) + } + + fun deselectAll() { + _uiState.value = _uiState.value.copy(selectedPatches = emptySet()) + } + + fun setSearchQuery(query: String) { + val filtered = if (query.isBlank()) { + _uiState.value.allPatches + } else { + _uiState.value.allPatches.filter { + it.name.contains(query, ignoreCase = true) || + it.description.contains(query, ignoreCase = true) + } + } + _uiState.value = _uiState.value.copy( + searchQuery = query, + filteredPatches = filtered + ) + } + + fun setShowOnlySelected(show: Boolean) { + val filtered = if (show) { + _uiState.value.allPatches.filter { _uiState.value.selectedPatches.contains(it.uniqueId) } + } else if (_uiState.value.searchQuery.isNotBlank()) { + _uiState.value.allPatches.filter { + it.name.contains(_uiState.value.searchQuery, ignoreCase = true) || + it.description.contains(_uiState.value.searchQuery, ignoreCase = true) + } + } else { + _uiState.value.allPatches + } + _uiState.value = _uiState.value.copy( + showOnlySelected = show, + filteredPatches = filtered + ) + } + + fun clearError() { + _uiState.value = _uiState.value.copy(error = null) + } + + /** + * Get patches that match the commonly disabled list and are currently selected. + * Returns list of (patch, reason) pairs. + */ + fun getCommonlyDisabledPatches(): List> { + val packageName = getPackageNameFromApk() + val commonlyDisabled = AppConstants.PatchRecommendations.getCommonlyDisabled(packageName) + + return _uiState.value.allPatches + .filter { patch -> _uiState.value.selectedPatches.contains(patch.uniqueId) } + .mapNotNull { patch -> + // Find matching commonly disabled entry + val match = commonlyDisabled.find { (pattern, _) -> + patch.name.contains(pattern, ignoreCase = true) + } + if (match != null) { + patch to match.second + } else { + null + } + } + } + + /** + * Deselect all commonly disabled patches at once. + */ + fun deselectCommonlyDisabled() { + val patchesToDeselect = getCommonlyDisabledPatches().map { it.first.uniqueId }.toSet() + val newSelection = _uiState.value.selectedPatches - patchesToDeselect + _uiState.value = _uiState.value.copy(selectedPatches = newSelection) + } + + fun createPatchConfig(): PatchConfig { + // Create app folder in the same location as the input APK + val inputFile = File(apkPath) + val appFolderName = apkName.replace(" ", "-") + val outputDir = File(inputFile.parentFile, appFolderName) + outputDir.mkdirs() + + // Extract version from APK filename for output name + val version = extractVersionFromFilename(inputFile.name) ?: "patched" + val outputFileName = "${appFolderName}-${version}-patched.apk" + val outputPath = File(outputDir, outputFileName).absolutePath + + // Convert unique IDs back to patch names for CLI + val selectedPatchNames = _uiState.value.allPatches + .filter { _uiState.value.selectedPatches.contains(it.uniqueId) } + .map { it.name } + + val disabledPatchNames = _uiState.value.allPatches + .filter { !_uiState.value.selectedPatches.contains(it.uniqueId) } + .map { it.name } + + return PatchConfig( + inputApkPath = apkPath, + outputApkPath = outputPath, + patchesFilePath = actualPatchesFilePath, + enabledPatches = selectedPatchNames, + disabledPatches = disabledPatchNames, + useExclusiveMode = true + ) + } + + private fun extractVersionFromFilename(fileName: String): String? { + // Extract version from APKMirror format: com.google.android.youtube_20.40.45-xxx + return try { + val afterPackage = fileName.substringAfter("_") + afterPackage.substringBefore("-").takeIf { it.isNotEmpty() } + } catch (e: Exception) { + null + } + } + + fun getApkName(): String = apkName + + /** + * Generate a preview of the CLI command that will be executed. + * @param cleanMode If true, formats with newlines for readability. If false, compact single-line format. + */ + fun getCommandPreview(cleanMode: Boolean = false): String { + val inputFile = File(apkPath) + val patchesFile = File(actualPatchesFilePath) + val appFolderName = apkName.replace(" ", "-") + val version = extractVersionFromFilename(inputFile.name) ?: "patched" + val outputFileName = "${appFolderName}-${version}-patched.apk" + + val selectedPatchNames = _uiState.value.allPatches + .filter { _uiState.value.selectedPatches.contains(it.uniqueId) } + .map { it.name } + + return if (cleanMode) { + val sb = StringBuilder() + sb.append("java -jar morphe-cli.jar patch \\\n") + sb.append(" -p ${patchesFile.name} \\\n") + sb.append(" -o ${outputFileName} \\\n") + sb.append(" --exclusive \\\n") + + selectedPatchNames.forEachIndexed { index, patch -> + val isLast = index == selectedPatchNames.lastIndex + sb.append(" -e \"$patch\"") + if (!isLast) { + sb.append(" \\") + } + sb.append("\n") + } + + sb.append(" ${inputFile.name}") + sb.toString() + } else { + // Compact mode - single line that wraps naturally + val patches = selectedPatchNames.joinToString(" ") { "-e \"$it\"" } + "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --exclusive $patches ${inputFile.name}" + } + } + + /** + * Download patches file if it's missing (e.g., after cache clear). + * Tries to find a release matching the expected filename, or falls back to latest stable. + */ + private suspend fun downloadMissingPatches(expectedFilename: String): Result { + // Try to extract version from filename (e.g., "morphe-patches-1.9.0.mpp" -> "1.9.0") + val versionRegex = Regex("""(\d+\.\d+\.\d+(?:-dev\.\d+)?)""") + val versionMatch = versionRegex.find(expectedFilename) + val expectedVersion = versionMatch?.groupValues?.get(1) + + Logger.info("Looking for patches version: ${expectedVersion ?: "latest"}") + + // Fetch releases + val releasesResult = patchRepository.fetchReleases() + if (releasesResult.isFailure) { + return Result.failure(releasesResult.exceptionOrNull() + ?: Exception("Failed to fetch releases")) + } + + val releases = releasesResult.getOrNull() ?: emptyList() + if (releases.isEmpty()) { + return Result.failure(Exception("No releases found")) + } + + // Find matching release by version, or use latest stable + val targetRelease = if (expectedVersion != null) { + releases.find { it.tagName.contains(expectedVersion) } + ?: releases.firstOrNull { !it.isDevRelease() } // Fallback to latest stable + } else { + releases.firstOrNull { !it.isDevRelease() } // Latest stable + } + + if (targetRelease == null) { + return Result.failure(Exception("No suitable release found")) + } + + Logger.info("Downloading patches from release: ${targetRelease.tagName}") + + // Download the patches + return patchRepository.downloadPatches(targetRelease) + } + + private fun getPackageNameFromApk(): String { + // Extract package name from APK filename (APKMirror format) + val fileName = File(apkPath).name + return when { + fileName.startsWith("com.google.android.youtube_") -> "com.google.android.youtube" + fileName.startsWith("com.google.android.apps.youtube.music_") -> "com.google.android.apps.youtube.music" + fileName.startsWith("com.reddit.frontpage_") -> "com.reddit.frontpage" + else -> "" + } + } +} + +data class PatchSelectionUiState( + val isLoading: Boolean = false, + val allPatches: List = emptyList(), + val filteredPatches: List = emptyList(), + val selectedPatches: Set = emptySet(), + val searchQuery: String = "", + val showOnlySelected: Boolean = false, + val error: String? = null +) { + val selectedCount: Int get() = selectedPatches.size + val totalCount: Int get() = allPatches.size +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt new file mode 100644 index 0000000..7149f72 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -0,0 +1,478 @@ +package app.morphe.gui.ui.screens.patches + +import androidx.compose.foundation.background +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.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import app.morphe.gui.data.model.Release +import org.koin.core.parameter.parametersOf +import cafe.adriel.voyager.koin.koinScreenModel +import app.morphe.gui.ui.components.ErrorDialog +import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.getErrorType +import app.morphe.gui.ui.components.getFriendlyErrorMessage +import app.morphe.gui.ui.theme.MorpheColors +import java.io.File + +/** + * Screen for selecting patches to apply. + */ +data class PatchesScreen( + val apkPath: String, + val apkName: String +) : Screen { + + @Composable + override fun Content() { + val viewModel = koinScreenModel { parametersOf(apkPath, apkName) } + PatchesScreenContent(viewModel = viewModel) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatchesScreenContent(viewModel: PatchesViewModel) { + val navigator = LocalNavigator.currentOrThrow + val uiState by viewModel.uiState.collectAsState() + + var showErrorDialog by remember { mutableStateOf(false) } + var currentError by remember { mutableStateOf(null) } + + LaunchedEffect(uiState.error) { + uiState.error?.let { error -> + currentError = error + showErrorDialog = true + } + } + + // Error dialog + if (showErrorDialog && currentError != null) { + ErrorDialog( + title = "Error", + message = getFriendlyErrorMessage(currentError!!), + errorType = getErrorType(currentError!!), + onDismiss = { + showErrorDialog = false + viewModel.clearError() + }, + onRetry = { + showErrorDialog = false + viewModel.clearError() + viewModel.loadReleases() + } + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Text("Select Patches", fontWeight = FontWeight.SemiBold) + Text( + text = viewModel.getApkName(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + navigationIcon = { + IconButton(onClick = { navigator.pop() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + IconButton( + onClick = { viewModel.loadReleases() }, + enabled = !uiState.isLoading + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh" + ) + } + SettingsButton(allowCacheClear = true) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + }, + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + // Channel selector + ChannelSelector( + selectedChannel = uiState.selectedChannel, + onChannelSelected = { viewModel.setChannel(it) }, + stableCount = uiState.stableReleases.size, + devCount = uiState.devReleases.size, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + CircularProgressIndicator(color = MorpheColors.Blue) + Text( + text = "Fetching releases...", + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + uiState.currentReleases.isEmpty() && !uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "No releases found", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + OutlinedButton(onClick = { viewModel.loadReleases() }) { + Text("Retry") + } + } + } + } + + else -> { + // Releases list + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(uiState.currentReleases) { release -> + ReleaseCard( + release = release, + isSelected = release == uiState.selectedRelease, + onClick = { viewModel.selectRelease(release) } + ) + } + } + + // Bottom action bar + BottomActionBar( + uiState = uiState, + onDownloadClick = { viewModel.downloadPatches() }, + onSelectClick = { + // Save the selected version to config before navigating back + viewModel.confirmSelection() + // Go back to HomeScreen - the new patches file is now cached + navigator.pop() + } + ) + } + } + } + } +} + +@Composable +private fun ChannelSelector( + selectedChannel: ReleaseChannel, + onChannelSelected: (ReleaseChannel) -> Unit, + stableCount: Int, + devCount: Int, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + ChannelChip( + label = "Stable", + count = stableCount, + isSelected = selectedChannel == ReleaseChannel.STABLE, + onClick = { onChannelSelected(ReleaseChannel.STABLE) }, + modifier = Modifier.weight(1f) + ) + ChannelChip( + label = "Dev", + count = devCount, + isSelected = selectedChannel == ReleaseChannel.DEV, + onClick = { onChannelSelected(ReleaseChannel.DEV) }, + modifier = Modifier.weight(1f) + ) + } +} + +@Composable +private fun ChannelChip( + label: String, + count: Int, + isSelected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val backgroundColor = if (isSelected) { + MorpheColors.Blue.copy(alpha = 0.15f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + } + + val borderColor = if (isSelected) { + MorpheColors.Blue + } else { + MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + } + + Surface( + modifier = modifier + .clip(RoundedCornerShape(12.dp)) + .clickable(onClick = onClick), + color = backgroundColor, + shape = RoundedCornerShape(12.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, borderColor) + ) { + Row( + modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = label, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + color = if (isSelected) MorpheColors.Blue else MaterialTheme.colorScheme.onSurface + ) + if (count > 0) { + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "($count)", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +private fun ReleaseCard( + release: Release, + isSelected: Boolean, + onClick: () -> Unit +) { + val backgroundColor = if (isSelected) { + MorpheColors.Blue.copy(alpha = 0.1f) + } else { + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + colors = CardDefaults.cardColors(containerColor = backgroundColor), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = release.tagName, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + if (release.isDevRelease()) { + Surface( + color = MorpheColors.Teal.copy(alpha = 0.2f), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "DEV", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Teal, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Show .mpp file info if available + release.assets.find { it.isMpp() }?.let { mppAsset -> + Text( + text = "${mppAsset.name} (${mppAsset.getFormattedSize()})", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Text( + text = "Published: ${formatDate(release.publishedAt)}", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + tint = MorpheColors.Blue, + modifier = Modifier.size(24.dp) + ) + } + } + } +} + +@Composable +private fun BottomActionBar( + uiState: PatchesUiState, + onDownloadClick: () -> Unit, + onSelectClick: () -> Unit +) { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 3.dp + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + // Download progress + if (uiState.isDownloading) { + LinearProgressIndicator( + progress = { uiState.downloadProgress }, + modifier = Modifier + .fillMaxWidth() + .height(4.dp) + .clip(RoundedCornerShape(2.dp)), + color = MorpheColors.Blue, + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Downloading patches...", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(12.dp)) + } + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Download button + if (uiState.downloadedPatchFile == null) { + Button( + onClick = onDownloadClick, + enabled = uiState.selectedRelease != null && !uiState.isDownloading, + modifier = Modifier + .weight(1f) + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = if (uiState.isDownloading) "Downloading..." else "Download Patches", + fontWeight = FontWeight.Medium + ) + } + } else { + // Select button (patches downloaded) + Button( + onClick = onSelectClick, + modifier = Modifier + .weight(1f) + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Teal + ), + shape = RoundedCornerShape(12.dp) + ) { + Text( + text = "Select", + fontWeight = FontWeight.Medium + ) + } + } + } + + // Downloaded file info + uiState.downloadedPatchFile?.let { file -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Downloaded: ${file.name}", + fontSize = 12.sp, + color = MorpheColors.Teal + ) + } + } + } +} + +private fun formatDate(isoDate: String): String { + return try { + // Simple date formatting - takes "2024-01-15T10:30:00Z" and returns "Jan 15, 2024" + val datePart = isoDate.substringBefore("T") + val parts = datePart.split("-") + if (parts.size == 3) { + val months = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") + val month = months.getOrElse(parts[1].toInt() - 1) { "???" } + val day = parts[2].toInt() + val year = parts[0] + "$month $day, $year" + } else { + datePart + } + } catch (e: Exception) { + isoDate + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt new file mode 100644 index 0000000..b8c3365 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt @@ -0,0 +1,211 @@ +package app.morphe.gui.ui.screens.patches + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import app.morphe.gui.data.model.Release +import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import app.morphe.gui.util.Logger +import java.io.File + +class PatchesViewModel( + private val apkPath: String, + private val apkName: String, + private val patchRepository: PatchRepository, + private val configRepository: ConfigRepository +) : ScreenModel { + + private val _uiState = MutableStateFlow(PatchesUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + loadReleases() + } + + fun loadReleases() { + screenModelScope.launch { + _uiState.value = _uiState.value.copy(isLoading = true, error = null) + + val result = patchRepository.fetchReleases() + + result.fold( + onSuccess = { releases -> + val stableReleases = releases.filter { !it.isDevRelease() } + val devReleases = releases.filter { it.isDevRelease() } + + // Check config for previously selected version + val config = configRepository.loadConfig() + val savedVersion = config.lastPatchesVersion + + // Find the saved release, or fall back to latest stable + val initialRelease = if (savedVersion != null) { + // Try to find in stable first, then dev + stableReleases.find { it.tagName == savedVersion } + ?: devReleases.find { it.tagName == savedVersion } + ?: stableReleases.firstOrNull() + } else { + stableReleases.firstOrNull() + } + + // Determine initial channel based on selected release + val initialChannel = if (initialRelease != null && initialRelease.isDevRelease()) { + ReleaseChannel.DEV + } else { + ReleaseChannel.STABLE + } + + // Check if patches for the initial release are already cached + val cachedFile = initialRelease?.let { checkCachedPatches(it) } + + _uiState.value = _uiState.value.copy( + isLoading = false, + stableReleases = stableReleases, + devReleases = devReleases, + selectedChannel = initialChannel, + selectedRelease = initialRelease, + downloadedPatchFile = cachedFile + ) + Logger.info("Loaded ${stableReleases.size} stable and ${devReleases.size} dev releases, saved=$savedVersion, selected=${initialRelease?.tagName}, cached: ${cachedFile != null}") + }, + onFailure = { e -> + _uiState.value = _uiState.value.copy( + isLoading = false, + error = e.message ?: "Failed to load releases" + ) + Logger.error("Failed to load releases", e) + } + ) + } + } + + fun selectRelease(release: Release) { + // Check if patches for this release are already cached + val cachedFile = checkCachedPatches(release) + + _uiState.value = _uiState.value.copy( + selectedRelease = release, + downloadedPatchFile = cachedFile + ) + Logger.info("Selected release: ${release.tagName}, cached: ${cachedFile != null}") + } + + /** + * Check if patches for a release are already downloaded and valid. + */ + private fun checkCachedPatches(release: Release): File? { + val asset = patchRepository.findMppAsset(release) ?: return null + val patchesDir = app.morphe.gui.util.FileUtils.getPatchesDir() + val cachedFile = File(patchesDir, asset.name) + + // Verify file exists and size matches (size check acts as basic integrity verification) + return if (cachedFile.exists() && cachedFile.length() == asset.size) { + Logger.info("Found cached patches: ${cachedFile.absolutePath}") + cachedFile + } else { + null + } + } + + fun setChannel(channel: ReleaseChannel) { + val newRelease = when (channel) { + ReleaseChannel.STABLE -> _uiState.value.stableReleases.firstOrNull() + ReleaseChannel.DEV -> _uiState.value.devReleases.firstOrNull() + } + + // Check if patches for the new release are already cached + val cachedFile = newRelease?.let { checkCachedPatches(it) } + + _uiState.value = _uiState.value.copy( + selectedChannel = channel, + selectedRelease = newRelease, + downloadedPatchFile = cachedFile + ) + } + + fun downloadPatches() { + val release = _uiState.value.selectedRelease ?: return + + screenModelScope.launch { + _uiState.value = _uiState.value.copy( + isDownloading = true, + downloadProgress = 0f, + error = null + ) + + val result = patchRepository.downloadPatches(release) { progress -> + _uiState.value = _uiState.value.copy(downloadProgress = progress) + } + + result.fold( + onSuccess = { patchFile -> + _uiState.value = _uiState.value.copy( + isDownloading = false, + downloadedPatchFile = patchFile, + downloadProgress = 1f + ) + Logger.info("Patches downloaded: ${patchFile.absolutePath}") + + // Save the selected version to config so HomeScreen can pick it up + configRepository.setLastPatchesVersion(release.tagName) + Logger.info("Saved selected patches version to config: ${release.tagName}") + }, + onFailure = { e -> + _uiState.value = _uiState.value.copy( + isDownloading = false, + error = e.message ?: "Failed to download patches" + ) + Logger.error("Failed to download patches", e) + } + ) + } + } + + fun clearError() { + _uiState.value = _uiState.value.copy(error = null) + } + + /** + * Confirm the current selection and save it to config. + * Called when user clicks "Select" button. + */ + fun confirmSelection() { + val release = _uiState.value.selectedRelease ?: return + screenModelScope.launch { + configRepository.setLastPatchesVersion(release.tagName) + Logger.info("Confirmed patches selection: ${release.tagName}") + } + } + + fun getApkPath(): String = apkPath + fun getApkName(): String = apkName +} + +enum class ReleaseChannel { + STABLE, + DEV +} + +data class PatchesUiState( + val isLoading: Boolean = false, + val stableReleases: List = emptyList(), + val devReleases: List = emptyList(), + val selectedChannel: ReleaseChannel = ReleaseChannel.STABLE, + val selectedRelease: Release? = null, + val isDownloading: Boolean = false, + val downloadProgress: Float = 0f, + val downloadedPatchFile: File? = null, + val error: String? = null +) { + val currentReleases: List + get() = when (selectedChannel) { + ReleaseChannel.STABLE -> stableReleases + ReleaseChannel.DEV -> devReleases + } + + val isReady: Boolean + get() = downloadedPatchFile != null +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt new file mode 100644 index 0000000..8e0978e --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt @@ -0,0 +1,457 @@ +package app.morphe.gui.ui.screens.patching + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import app.morphe.gui.data.model.PatchConfig +import org.koin.core.parameter.parametersOf +import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.screens.result.ResultScreen +import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.FileUtils +import app.morphe.gui.util.Logger +import java.awt.Desktop + +/** + * Screen showing patching progress with real-time logs. + */ +data class PatchingScreen( + val config: PatchConfig +) : Screen { + + @Composable + override fun Content() { + val viewModel = koinScreenModel { parametersOf(config) } + PatchingScreenContent(viewModel = viewModel) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatchingScreenContent(viewModel: PatchingViewModel) { + val navigator = LocalNavigator.currentOrThrow + val uiState by viewModel.uiState.collectAsState() + + // Auto-start patching when screen loads + LaunchedEffect(Unit) { + viewModel.startPatching() + } + + // Auto-scroll to bottom of logs + val listState = rememberLazyListState() + LaunchedEffect(uiState.logs.size) { + if (uiState.logs.isNotEmpty()) { + listState.animateScrollToItem(uiState.logs.size - 1) + } + } + + // Auto-navigate to result screen on successful completion + LaunchedEffect(uiState.status) { + if (uiState.status == PatchingStatus.COMPLETED && uiState.outputPath != null) { + // Small delay to let user see the success message + kotlinx.coroutines.delay(1500) + navigator.push(ResultScreen(outputPath = uiState.outputPath!!)) + } + } + + Scaffold( + topBar = { + TopAppBar( + title = { + Column { + Text("Patching", fontWeight = FontWeight.SemiBold) + Text( + text = getStatusText(uiState.status), + style = MaterialTheme.typography.bodySmall, + color = getStatusColor(uiState.status) + ) + } + }, + navigationIcon = { + IconButton( + onClick = { navigator.pop() }, + enabled = !uiState.isInProgress + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back" + ) + } + }, + actions = { + if (uiState.canCancel) { + TextButton( + onClick = { viewModel.cancelPatching() }, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.error + ) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(4.dp)) + Text("Cancel") + } + } + SettingsButton(allowCacheClear = false) + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface + ) + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + ) { + // Progress indicator + if (uiState.isInProgress) { + Column { + if (uiState.hasProgress) { + // Show determinate progress when we have progress info + LinearProgressIndicator( + progress = { uiState.progress }, + modifier = Modifier + .fillMaxWidth() + .height(4.dp), + color = MorpheColors.Blue, + ) + // Show progress text + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = uiState.currentPatch ?: "Applying patches...", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + modifier = Modifier.weight(1f) + ) + Text( + text = "${uiState.patchedCount}/${uiState.totalPatches}", + fontSize = 11.sp, + color = MorpheColors.Blue, + fontWeight = FontWeight.Medium + ) + } + } else { + // Show indeterminate progress when we don't have progress info + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(4.dp), + color = MorpheColors.Blue + ) + } + } + } + + // Log output + LazyColumn( + state = listState, + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(16.dp) + .clip(RoundedCornerShape(12.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)), + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(uiState.logs, key = { it.id }) { entry -> + LogEntryRow(entry) + } + } + + // Bottom action bar (only for failed/cancelled - success auto-navigates) + when (uiState.status) { + PatchingStatus.COMPLETED -> { + // Show brief success message while auto-navigating + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 3.dp + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + strokeWidth = 2.dp, + color = MorpheColors.Teal + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = "Patching completed! Loading result...", + color = MorpheColors.Teal, + fontWeight = FontWeight.Medium + ) + } + } + } + + PatchingStatus.FAILED, PatchingStatus.CANCELLED -> { + FailureBottomBar( + status = uiState.status, + error = uiState.error, + onStartOver = { navigator.popUntilRoot() }, + onGoBack = { navigator.pop() } + ) + } + + else -> { + // Show nothing for in-progress states + } + } + } + } +} + +@Composable +private fun FailureBottomBar( + status: PatchingStatus, + error: String?, + onStartOver: () -> Unit, + onGoBack: () -> Unit +) { + var tempFilesCleared by remember { mutableStateOf(false) } + val hasTempFiles = remember { FileUtils.hasTempFiles() } + val tempFilesSize = remember { FileUtils.getTempDirSize() } + val logFile = remember { Logger.getLogFile() } + + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.surface, + tonalElevation = 3.dp + ) { + Column( + modifier = Modifier.padding(16.dp) + ) { + // Error message + Text( + text = if (status == PatchingStatus.CANCELLED) + "Patching was cancelled" + else + error ?: "Patching failed", + color = MaterialTheme.colorScheme.error, + fontWeight = FontWeight.Medium + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // Log file location + if (logFile != null && logFile.exists()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Log file", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = logFile.absolutePath, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + fontFamily = FontFamily.Monospace, + maxLines = 1 + ) + } + TextButton( + onClick = { + try { + if (Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(logFile.parentFile) + } + } catch (e: Exception) { + Logger.error("Failed to open logs folder", e) + } + } + ) { + Text("Open", fontSize = 12.sp) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + } + + // Cleanup option + if (hasTempFiles && !tempFilesCleared) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "Temporary files", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "${formatFileSize(tempFilesSize)} can be freed", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + TextButton( + onClick = { + FileUtils.cleanupAllTempDirs() + tempFilesCleared = true + Logger.info("Cleaned temp files after failed patching") + } + ) { + Text("Clean up", fontSize = 12.sp) + } + } + + Spacer(modifier = Modifier.height(12.dp)) + } else if (tempFilesCleared) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(8.dp)) + .background(MorpheColors.Teal.copy(alpha = 0.1f)) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Temp files cleaned", + fontSize = 12.sp, + color = MorpheColors.Teal + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + } + + // Action buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = onStartOver, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(12.dp) + ) { + Text("Start Over") + } + Button( + onClick = onGoBack, + modifier = Modifier + .weight(1f) + .height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + Text("Go Back", fontWeight = FontWeight.Medium) + } + } + } + } +} + +private fun formatFileSize(bytes: Long): String { + return when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) + bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) + else -> "%.2f GB".format(bytes / (1024.0 * 1024.0 * 1024.0)) + } +} + +@Composable +private fun LogEntryRow(entry: LogEntry) { + val color = when (entry.level) { + LogLevel.SUCCESS -> MorpheColors.Teal + LogLevel.ERROR -> MaterialTheme.colorScheme.error + LogLevel.WARNING -> Color(0xFFFF9800) + LogLevel.PROGRESS -> MorpheColors.Blue + LogLevel.INFO -> MaterialTheme.colorScheme.onSurfaceVariant + } + + val prefix = when (entry.level) { + LogLevel.SUCCESS -> "[OK]" + LogLevel.ERROR -> "[ERR]" + LogLevel.WARNING -> "[WARN]" + LogLevel.PROGRESS -> "[...]" + LogLevel.INFO -> "[i]" + } + + Text( + text = "$prefix ${entry.message}", + fontFamily = FontFamily.Monospace, + fontSize = 12.sp, + color = color, + lineHeight = 18.sp + ) +} + +private fun getStatusText(status: PatchingStatus): String { + return when (status) { + PatchingStatus.IDLE -> "Ready" + PatchingStatus.PREPARING -> "Preparing..." + PatchingStatus.PATCHING -> "Patching in progress..." + PatchingStatus.COMPLETED -> "Completed" + PatchingStatus.FAILED -> "Failed" + PatchingStatus.CANCELLED -> "Cancelled" + } +} + +@Composable +private fun getStatusColor(status: PatchingStatus): Color { + return when (status) { + PatchingStatus.COMPLETED -> MorpheColors.Teal + PatchingStatus.FAILED -> MaterialTheme.colorScheme.error + PatchingStatus.CANCELLED -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.onSurfaceVariant + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt new file mode 100644 index 0000000..30726bf --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt @@ -0,0 +1,236 @@ +package app.morphe.gui.ui.screens.patching + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import app.morphe.gui.data.model.PatchConfig +import app.morphe.gui.data.repository.ConfigRepository +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import app.morphe.gui.util.Logger +import app.morphe.gui.util.PatchService +import java.io.File + +class PatchingViewModel( + private val config: PatchConfig, + private val patchService: PatchService, + private val configRepository: ConfigRepository +) : ScreenModel { + + private val _uiState = MutableStateFlow(PatchingUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var patchingJob: Job? = null + + fun startPatching() { + if (_uiState.value.status != PatchingStatus.IDLE) return + + patchingJob = screenModelScope.launch { + _uiState.value = _uiState.value.copy( + status = PatchingStatus.PREPARING, + logs = listOf(LogEntry("Preparing to patch...", LogLevel.INFO)) + ) + + addLog("Initializing patcher...", LogLevel.INFO) + + // Start patching + _uiState.value = _uiState.value.copy( + status = PatchingStatus.PATCHING, + totalPatches = config.enabledPatches.size, + patchedCount = 0, + progress = 0f + ) + addLog("Starting patch process...", LogLevel.INFO) + addLog("Input: ${File(config.inputApkPath).name}", LogLevel.INFO) + addLog("Output: ${File(config.outputApkPath).name}", LogLevel.INFO) + addLog("Patches: ${config.enabledPatches.size} enabled", LogLevel.INFO) + + // Use PatchService for direct library patching + val result = patchService.patch( + patchesFilePath = config.patchesFilePath, + inputApkPath = config.inputApkPath, + outputApkPath = config.outputApkPath, + enabledPatches = config.enabledPatches, + disabledPatches = config.disabledPatches, + options = config.patchOptions, + exclusiveMode = config.useExclusiveMode, + onProgress = { message -> + parseAndAddLog(message) + } + ) + + result.fold( + onSuccess = { patchResult -> + if (patchResult.success) { + addLog("Patching completed successfully!", LogLevel.SUCCESS) + addLog("Applied ${patchResult.appliedPatches.size} patches", LogLevel.SUCCESS) + _uiState.value = _uiState.value.copy( + status = PatchingStatus.COMPLETED, + outputPath = config.outputApkPath, + progress = 1f + ) + Logger.info("Patching completed: ${config.outputApkPath}") + } else { + val failedMsg = if (patchResult.failedPatches.isNotEmpty()) { + "Failed patches: ${patchResult.failedPatches.joinToString(", ")}" + } else { + "Patching failed" + } + addLog(failedMsg, LogLevel.ERROR) + _uiState.value = _uiState.value.copy( + status = PatchingStatus.FAILED, + error = "Patching failed. Check logs for details." + ) + Logger.error("Patching failed: ${patchResult.failedPatches}") + } + }, + onFailure = { e -> + addLog("Error: ${e.message}", LogLevel.ERROR) + _uiState.value = _uiState.value.copy( + status = PatchingStatus.FAILED, + error = e.message ?: "Unknown error occurred" + ) + Logger.error("Patching error", e) + } + ) + } + } + + fun cancelPatching() { + patchingJob?.cancel() + patchingJob = null + addLog("Patching cancelled by user", LogLevel.WARNING) + _uiState.value = _uiState.value.copy( + status = PatchingStatus.CANCELLED + ) + Logger.info("Patching cancelled by user") + } + + private fun addLog(message: String, level: LogLevel) { + val entry = LogEntry(message, level) + _uiState.value = _uiState.value.copy( + logs = _uiState.value.logs + entry + ) + } + + private fun parseAndAddLog(line: String) { + val level = when { + line.contains("error", ignoreCase = true) -> LogLevel.ERROR + line.contains("warning", ignoreCase = true) -> LogLevel.WARNING + line.contains("success", ignoreCase = true) || + line.contains("completed", ignoreCase = true) || + line.contains("done", ignoreCase = true) -> LogLevel.SUCCESS + line.contains("patching", ignoreCase = true) || + line.contains("applying", ignoreCase = true) -> LogLevel.PROGRESS + else -> LogLevel.INFO + } + addLog(line, level) + + // Try to extract progress information + parseProgress(line) + } + + private fun parseProgress(line: String) { + // Pattern: "Executing patch X of Y: PatchName" or similar + val executingPattern = Regex("""(?:Executing|Applying)\s+patch\s+(\d+)\s+of\s+(\d+)(?::\s*(.+))?""", RegexOption.IGNORE_CASE) + val executingMatch = executingPattern.find(line) + if (executingMatch != null) { + val current = executingMatch.groupValues[1].toIntOrNull() ?: 0 + val total = executingMatch.groupValues[2].toIntOrNull() ?: 0 + val patchName = executingMatch.groupValues.getOrNull(3)?.trim() + + if (total > 0) { + val progress = current.toFloat() / total.toFloat() + _uiState.value = _uiState.value.copy( + progress = progress, + patchedCount = current, + totalPatches = total, + currentPatch = patchName, + hasReceivedProgressUpdate = true + ) + } + return + } + + // Pattern: "[X/Y]" or "(X/Y)" + val fractionPattern = Regex("""[\[\(](\d+)/(\d+)[\]\)]""") + val fractionMatch = fractionPattern.find(line) + if (fractionMatch != null) { + val current = fractionMatch.groupValues[1].toIntOrNull() ?: 0 + val total = fractionMatch.groupValues[2].toIntOrNull() ?: 0 + + if (total > 0) { + val progress = current.toFloat() / total.toFloat() + _uiState.value = _uiState.value.copy( + progress = progress, + patchedCount = current, + totalPatches = total, + hasReceivedProgressUpdate = true + ) + } + return + } + + // Pattern: "X%" percentage + val percentPattern = Regex("""(\d+(?:\.\d+)?)\s*%""") + val percentMatch = percentPattern.find(line) + if (percentMatch != null) { + val percent = percentMatch.groupValues[1].toFloatOrNull() ?: 0f + if (percent > 0) { + _uiState.value = _uiState.value.copy( + progress = percent / 100f, + hasReceivedProgressUpdate = true + ) + } + } + } + + fun getConfig(): PatchConfig = config +} + +enum class PatchingStatus { + IDLE, + PREPARING, + PATCHING, + COMPLETED, + FAILED, + CANCELLED +} + +enum class LogLevel { + INFO, + SUCCESS, + WARNING, + ERROR, + PROGRESS +} + +data class LogEntry( + val message: String, + val level: LogLevel, + val id: String = "${System.currentTimeMillis()}_${System.nanoTime()}" +) + +data class PatchingUiState( + val status: PatchingStatus = PatchingStatus.IDLE, + val logs: List = emptyList(), + val outputPath: String? = null, + val error: String? = null, + val progress: Float = 0f, + val currentPatch: String? = null, + val patchedCount: Int = 0, + val totalPatches: Int = 0, + val hasReceivedProgressUpdate: Boolean = false +) { + val isInProgress: Boolean + get() = status == PatchingStatus.PREPARING || status == PatchingStatus.PATCHING + + val canCancel: Boolean + get() = isInProgress + + // Only show determinate progress if we've actually received progress updates from CLI + val hasProgress: Boolean + get() = hasReceivedProgressUpdate && progress > 0f +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt new file mode 100644 index 0000000..4b5e298 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -0,0 +1,963 @@ +package app.morphe.gui.ui.screens.quick + +import androidx.compose.animation.* +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.OpenInNew +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.draganddrop.DragAndDropEvent +import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draganddrop.awtTransferable +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import app.morphe.morphe_cli.generated.resources.Res +import app.morphe.morphe_cli.generated.resources.reddit +import app.morphe.morphe_cli.generated.resources.youtube +import app.morphe.morphe_cli.generated.resources.youtube_music +import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.util.PatchService +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.koinInject +import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.AdbManager +import app.morphe.gui.util.ChecksumStatus +import java.awt.Desktop +import java.awt.datatransfer.DataFlavor +import java.io.File +import javax.swing.JFileChooser +import javax.swing.filechooser.FileNameExtensionFilter + +/** + * Quick Patch Mode - Single screen simplified patching. + */ +class QuickPatchScreen : Screen { + @Composable + override fun Content() { + val patchRepository: PatchRepository = koinInject() + val patchService: PatchService = koinInject() + val configRepository: ConfigRepository = koinInject() + + val viewModel = remember { + QuickPatchViewModel(patchRepository, patchService, configRepository) + } + + QuickPatchContent(viewModel) + } +} + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun QuickPatchContent(viewModel: QuickPatchViewModel) { + val uiState by viewModel.uiState.collectAsState() + val uriHandler = LocalUriHandler.current + + // Compose drag and drop target + val dragAndDropTarget = remember { + object : DragAndDropTarget { + override fun onStarted(event: DragAndDropEvent) { + viewModel.setDragHover(true) + } + + override fun onEnded(event: DragAndDropEvent) { + viewModel.setDragHover(false) + } + + override fun onExited(event: DragAndDropEvent) { + viewModel.setDragHover(false) + } + + override fun onEntered(event: DragAndDropEvent) { + viewModel.setDragHover(true) + } + + override fun onDrop(event: DragAndDropEvent): Boolean { + viewModel.setDragHover(false) + val transferable = event.awtTransferable + return try { + if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { + @Suppress("UNCHECKED_CAST") + val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List + val apkFile = files.firstOrNull { it.name.endsWith(".apk", ignoreCase = true) } + if (apkFile != null) { + viewModel.onFileSelected(apkFile) + true + } else { + false + } + } else { + false + } + } catch (e: Exception) { + false + } + } + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .dragAndDropTarget( + shouldStartDragAndDrop = { true }, + target = dragAndDropTarget + ) + ) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Morphe Quick Patch", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Mode indicator + Surface( + color = MorpheColors.Blue.copy(alpha = 0.1f), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "QUICK MODE", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Blue, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + + // Settings button + SettingsButton() + } + } + + Spacer(modifier = Modifier.height(20.dp)) + + // Main content based on phase + // Remember last valid data for safe animation transitions + val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } + val lastOutputPath = remember(uiState.outputPath) { uiState.outputPath } + + AnimatedContent( + targetState = uiState.phase, + modifier = Modifier.weight(1f) + ) { phase -> + when (phase) { + QuickPatchPhase.IDLE, QuickPatchPhase.ANALYZING -> { + IdleContent( + isAnalyzing = phase == QuickPatchPhase.ANALYZING, + isDragHovering = uiState.isDragHovering, + error = uiState.error, + onFileSelected = { viewModel.onFileSelected(it) }, + onDragHover = { viewModel.setDragHover(it) }, + onClearError = { viewModel.clearError() } + ) + } + QuickPatchPhase.READY -> { + // Use current or last known apkInfo to prevent crash during animation + val apkInfo = uiState.apkInfo ?: lastApkInfo + if (apkInfo != null) { + ReadyContent( + apkInfo = apkInfo, + error = uiState.error, + onPatch = { viewModel.startPatching() }, + onClear = { viewModel.reset() }, + onClearError = { viewModel.clearError() } + ) + } + } + QuickPatchPhase.DOWNLOADING, QuickPatchPhase.PATCHING -> { + PatchingContent( + phase = phase, + progress = uiState.progress, + statusMessage = uiState.statusMessage, + onCancel = { viewModel.cancelPatching() } + ) + } + QuickPatchPhase.COMPLETED -> { + val apkInfo = uiState.apkInfo ?: lastApkInfo + val outputPath = uiState.outputPath ?: lastOutputPath + if (apkInfo != null && outputPath != null) { + CompletedContent( + outputPath = outputPath, + apkInfo = apkInfo, + onPatchAnother = { viewModel.reset() } + ) + } + } + } + } + + // Bottom app cards (only show in IDLE phase) + if (uiState.phase == QuickPatchPhase.IDLE) { + Spacer(modifier = Modifier.height(16.dp)) + SupportedAppsRow( + supportedApps = uiState.supportedApps, + isLoading = uiState.isLoadingPatches, + patchesVersion = uiState.patchesVersion, + onOpenUrl = { url -> uriHandler.openUri(url) } + ) + } + } + + // Error snackbar + uiState.error?.let { error -> + Snackbar( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp), + action = { + TextButton(onClick = { viewModel.clearError() }) { + Text("Dismiss", color = MaterialTheme.colorScheme.inversePrimary) + } + }, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) { + Text(error) + } + } + } +} + +@Composable +private fun IdleContent( + isAnalyzing: Boolean, + isDragHovering: Boolean, + error: String?, + onFileSelected: (File) -> Unit, + onDragHover: (Boolean) -> Unit, + onClearError: () -> Unit +) { + val dropZoneColor = when { + isDragHovering -> MorpheColors.Blue.copy(alpha = 0.2f) + else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + } + + val borderColor = when { + isDragHovering -> MorpheColors.Blue + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + } + + Box( + modifier = Modifier + .fillMaxSize() + .clip(RoundedCornerShape(16.dp)) + .background(dropZoneColor) + .border(2.dp, borderColor, RoundedCornerShape(16.dp)) + .clickable(enabled = !isAnalyzing) { + openFilePicker()?.let { onFileSelected(it) } + }, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + if (isAnalyzing) { + CircularProgressIndicator( + modifier = Modifier.size(48.dp), + color = MorpheColors.Blue, + strokeWidth = 3.dp + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Analyzing APK...", + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + Icon( + imageVector = Icons.Default.CloudUpload, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = if (isDragHovering) MorpheColors.Blue else MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Drop APK here", + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "or click to browse", + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} + +@Composable +private fun ReadyContent( + apkInfo: QuickApkInfo, + error: String?, + onPatch: () -> Unit, + onClear: () -> Unit, + onClearError: () -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // APK Info Card + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // App icon + Box( + modifier = Modifier + .size(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource( + when (apkInfo.packageName) { + AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube + AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music + AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit + else -> Res.drawable.youtube // Fallback + } + ), + contentDescription = "${apkInfo.displayName} icon", + modifier = Modifier.size(36.dp) + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = apkInfo.displayName, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = "v${apkInfo.versionName} • ${apkInfo.formattedSize}", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + // Checksum status + when (apkInfo.checksumStatus) { + is ChecksumStatus.Verified -> { + Icon( + imageVector = Icons.Default.VerifiedUser, + contentDescription = "Verified", + tint = MorpheColors.Teal, + modifier = Modifier.size(24.dp) + ) + } + is ChecksumStatus.Mismatch -> { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Checksum mismatch", + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(24.dp) + ) + } + else -> {} + } + + Spacer(modifier = Modifier.width(8.dp)) + + IconButton(onClick = onClear) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Verification status banner + VerificationStatusBanner( + checksumStatus = apkInfo.checksumStatus, + isRecommendedVersion = apkInfo.isRecommendedVersion, + currentVersion = apkInfo.versionName, + suggestedVersion = apkInfo.recommendedVersion ?: "Unknown" + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Patch button + Button( + onClick = onPatch, + modifier = Modifier + .fillMaxWidth() + .height(52.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.Default.AutoFixHigh, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Patch with Defaults", + fontSize = 16.sp, + fontWeight = FontWeight.Medium + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Uses latest patches with recommended settings", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + } +} + +@Composable +private fun PatchingContent( + phase: QuickPatchPhase, + progress: Float, + statusMessage: String, + onCancel: () -> Unit +) { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + // Progress indicator + Box(contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = { progress }, + modifier = Modifier.size(100.dp), + strokeWidth = 6.dp, + color = MorpheColors.Teal, + trackColor = MaterialTheme.colorScheme.surfaceVariant + ) + Text( + text = "${(progress * 100).toInt()}%", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = when (phase) { + QuickPatchPhase.DOWNLOADING -> "Preparing..." + QuickPatchPhase.PATCHING -> "Patching..." + else -> "" + }, + fontSize = 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = statusMessage, + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + TextButton(onClick = onCancel) { + Text("Cancel", color = MaterialTheme.colorScheme.error) + } + } +} + +@Composable +private fun CompletedContent( + outputPath: String, + apkInfo: QuickApkInfo, + onPatchAnother: () -> Unit +) { + val outputFile = File(outputPath) + val adbManager = remember { AdbManager() } + var isAdbAvailable by remember { mutableStateOf(null) } + + LaunchedEffect(Unit) { + isAdbAvailable = adbManager.isAdbAvailable() + } + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Success", + tint = MorpheColors.Teal, + modifier = Modifier.size(64.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text( + text = "Patching Complete!", + fontSize = 22.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = outputFile.name, + fontSize = 14.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center + ) + + if (outputFile.exists()) { + Text( + text = formatFileSize(outputFile.length()), + fontSize = 13.sp, + color = MorpheColors.Teal + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = { + try { + val folder = outputFile.parentFile + if (folder != null && Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(folder) + } + } catch (e: Exception) { } + }, + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.FolderOpen, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text("Open Folder") + } + + Button( + onClick = onPatchAnother, + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(8.dp) + ) { + Text("Patch Another") + } + } + + if (isAdbAvailable == true) { + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "Connect your device via USB to install with ADB", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } +} + +@Composable +private fun SupportedAppsRow( + supportedApps: List, + isLoading: Boolean, + patchesVersion: String?, + onOpenUrl: (String) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "Get the APK from APKMirror:", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (patchesVersion != null) { + Text( + text = "Patches: $patchesVersion", + fontSize = 11.sp, + color = MorpheColors.Blue.copy(alpha = 0.8f) + ) + } + } + + Spacer(modifier = Modifier.height(8.dp)) + + if (isLoading) { + // Loading state + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Loading supported apps...", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else if (supportedApps.isEmpty()) { + // No apps loaded + Text( + text = "Could not load supported apps", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } else { + // Show supported apps dynamically + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + supportedApps.forEach { app -> + val url = app.apkMirrorUrl + if (url != null) { + OutlinedCard( + onClick = { onOpenUrl(url) }, + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(24.dp) + .clip(RoundedCornerShape(4.dp)) + .background(Color.White), + contentAlignment = Alignment.Center + ) { + Image( + painter = painterResource( + when (app.packageName) { + AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube + AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music + AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit + else -> Res.drawable.youtube // Fallback + } + ), + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + } + Spacer(modifier = Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = app.displayName, + fontSize = 13.sp, + fontWeight = FontWeight.Medium + ) + app.recommendedVersion?.let { version -> + Text( + text = "v$version", + fontSize = 10.sp, + color = MorpheColors.Teal + ) + } + } + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = "Open", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } + } + } +} + +/** + * Shows verification status (version + checksum) in a compact banner. + */ +@Composable +private fun VerificationStatusBanner( + checksumStatus: ChecksumStatus, + isRecommendedVersion: Boolean, + currentVersion: String, + suggestedVersion: String +) { + when { + // Recommended version with verified checksum + checksumStatus is ChecksumStatus.Verified -> { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MorpheColors.Teal.copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.VerifiedUser, + contentDescription = null, + tint = MorpheColors.Teal, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = "Recommended version • Verified", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal + ) + Text( + text = "Checksum matches APKMirror", + fontSize = 11.sp, + color = MorpheColors.Teal.copy(alpha = 0.8f) + ) + } + } + } + } + + // Checksum mismatch - warning + checksumStatus is ChecksumStatus.Mismatch -> { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = "Checksum mismatch", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.error + ) + Text( + text = "File may be corrupted. Re-download from APKMirror.", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f) + ) + } + } + } + } + + // Recommended version but no checksum configured + isRecommendedVersion && checksumStatus is ChecksumStatus.NotConfigured -> { + Surface( + modifier = Modifier.fillMaxWidth(), + color = MorpheColors.Teal.copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MorpheColors.Teal, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Using recommended version", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal + ) + } + } + } + + // Non-recommended version (older or newer) + !isRecommendedVersion -> { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color(0xFFFF9800).copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = Color(0xFFFF9800), + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column { + Text( + text = "Version $currentVersion", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFFFF9800) + ) + Text( + text = "Recommended: v$suggestedVersion. Patching may have issues.", + fontSize = 11.sp, + color = Color(0xFFFF9800).copy(alpha = 0.8f) + ) + } + } + } + } + + // Checksum error + checksumStatus is ChecksumStatus.Error -> { + Surface( + modifier = Modifier.fillMaxWidth(), + color = Color(0xFFFF9800).copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier.padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = Color(0xFFFF9800), + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Recommended version (checksum unavailable)", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = Color(0xFFFF9800) + ) + } + } + } + } +} + +/** + * Open native file picker. + */ +private fun openFilePicker(): File? { + val chooser = JFileChooser().apply { + dialogTitle = "Select APK" + fileFilter = FileNameExtensionFilter("APK Files", "apk") + isAcceptAllFileFilterUsed = false + } + + return if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { + chooser.selectedFile + } else null +} + +private fun formatFileSize(bytes: Long): String { + return when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) + bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) + else -> "%.2f GB".format(bytes / (1024.0 * 1024.0 * 1024.0)) + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt new file mode 100644 index 0000000..ed391d9 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -0,0 +1,470 @@ +package app.morphe.gui.ui.screens.quick + +import cafe.adriel.voyager.core.model.ScreenModel +import cafe.adriel.voyager.core.model.screenModelScope +import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.data.model.Patch +import app.morphe.gui.data.model.PatchConfig +import app.morphe.gui.data.model.SupportedApp +import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchRepository +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import net.dongliu.apk.parser.ApkFile +import app.morphe.gui.util.ChecksumStatus +import app.morphe.gui.util.ChecksumUtils +import app.morphe.gui.util.Logger +import app.morphe.gui.util.PatchService +import app.morphe.gui.util.SupportedAppExtractor +import java.io.File + +/** + * ViewModel for Quick Patch mode - handles the entire flow in one screen. + */ +class QuickPatchViewModel( + private val patchRepository: PatchRepository, + private val patchService: PatchService, + private val configRepository: ConfigRepository +) : ScreenModel { + + private val _uiState = MutableStateFlow(QuickPatchUiState()) + val uiState: StateFlow = _uiState.asStateFlow() + + private var patchingJob: Job? = null + + // Cached dynamic data from patches + private var cachedPatches: List = emptyList() + private var cachedSupportedApps: List = emptyList() + private var cachedPatchesFile: File? = null + + init { + // Load patches on startup to get dynamic app info + loadPatchesAndSupportedApps() + } + + /** + * Load patches from GitHub and extract supported apps dynamically. + */ + private fun loadPatchesAndSupportedApps() { + screenModelScope.launch { + _uiState.value = _uiState.value.copy(isLoadingPatches = true) + + try { + // Check for saved version in config + val config = configRepository.loadConfig() + val savedVersion = config.lastPatchesVersion + + // Fetch releases + val releasesResult = patchRepository.fetchReleases() + val releases = releasesResult.getOrNull() + + if (releases.isNullOrEmpty()) { + Logger.warn("Quick mode: Could not fetch releases") + _uiState.value = _uiState.value.copy(isLoadingPatches = false) + return@launch + } + + // Find release to use + val latestStable = releases.firstOrNull { !it.isDevRelease() } + val release = if (savedVersion != null) { + releases.find { it.tagName == savedVersion } ?: latestStable + } else { + latestStable + } + + if (release == null) { + Logger.warn("Quick mode: No suitable release found") + _uiState.value = _uiState.value.copy(isLoadingPatches = false) + return@launch + } + + // Download patches + val patchFileResult = patchRepository.downloadPatches(release) + val patchFile = patchFileResult.getOrNull() + + if (patchFile == null) { + Logger.warn("Quick mode: Could not download patches") + _uiState.value = _uiState.value.copy(isLoadingPatches = false) + return@launch + } + + cachedPatchesFile = patchFile + + // Load patches using PatchService (direct library call) + val patchesResult = patchService.listPatches(patchFile.absolutePath) + val patches = patchesResult.getOrNull() + + if (patches.isNullOrEmpty()) { + Logger.warn("Quick mode: Could not load patches: ${patchesResult.exceptionOrNull()?.message}") + _uiState.value = _uiState.value.copy(isLoadingPatches = false) + return@launch + } + + cachedPatches = patches + + // Extract supported apps dynamically + val supportedApps = SupportedAppExtractor.extractSupportedApps(patches) + cachedSupportedApps = supportedApps + + Logger.info("Quick mode: Loaded ${supportedApps.size} supported apps: ${supportedApps.map { "${it.displayName} (${it.recommendedVersion})" }}") + + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + supportedApps = supportedApps, + patchesVersion = release.tagName + ) + } catch (e: Exception) { + Logger.error("Quick mode: Failed to load patches", e) + _uiState.value = _uiState.value.copy(isLoadingPatches = false) + } + } + } + + /** + * Handle file drop or selection. + */ + fun onFileSelected(file: File) { + screenModelScope.launch { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.ANALYZING, + error = null + ) + + val result = analyzeApk(file) + if (result != null) { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.READY, + apkFile = file, + apkInfo = result + ) + } else { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.IDLE, + error = _uiState.value.error ?: "Failed to analyze APK" + ) + } + } + } + + /** + * Analyze the APK file using dynamic data from patches. + */ + private suspend fun analyzeApk(file: File): QuickApkInfo? { + if (!file.exists() || !file.name.endsWith(".apk", ignoreCase = true)) { + _uiState.value = _uiState.value.copy(error = "Please select a valid APK file") + return null + } + + return try { + ApkFile(file).use { apk -> + val meta = apk.apkMeta + val packageName = meta.packageName + val versionName = meta.versionName ?: "Unknown" + + // Check if supported using dynamic data + val dynamicAppInfo = cachedSupportedApps.find { it.packageName == packageName } + + if (dynamicAppInfo == null) { + // Fallback to hardcoded check if patches not loaded yet + val supportedPackages = if (cachedSupportedApps.isEmpty()) { + listOf( + AppConstants.YouTube.PACKAGE_NAME, + AppConstants.YouTubeMusic.PACKAGE_NAME, + AppConstants.Reddit.PACKAGE_NAME + ) + } else { + cachedSupportedApps.map { it.packageName } + } + + if (packageName !in supportedPackages) { + _uiState.value = _uiState.value.copy( + error = "Unsupported app: $packageName\n\nSupported apps: ${cachedSupportedApps.map { it.displayName }.ifEmpty { listOf("YouTube", "YouTube Music", "Reddit") }.joinToString(", ")}" + ) + return null + } + } + + // Get display name and recommended version from dynamic data, fallback to constants + val displayName = dynamicAppInfo?.displayName + ?: SupportedApp.getDisplayName(packageName) + + val recommendedVersion = dynamicAppInfo?.recommendedVersion + ?: AppConstants.getSuggestedVersion(packageName) + + // Version check + val isRecommendedVersion = recommendedVersion != null && versionName == recommendedVersion + val versionWarning = if (!isRecommendedVersion && recommendedVersion != null) { + "Version $versionName may have compatibility issues. Recommended: $recommendedVersion" + } else null + + // Checksum verification (still uses AppConstants - checksums are manually maintained) + val checksumStatus = verifyChecksum(file, packageName, versionName, recommendedVersion) + + Logger.info("Quick mode: Analyzed $displayName v$versionName (recommended: $recommendedVersion)") + + QuickApkInfo( + fileName = file.name, + packageName = packageName, + versionName = versionName, + fileSize = file.length(), + displayName = displayName, + recommendedVersion = recommendedVersion, + isRecommendedVersion = isRecommendedVersion, + versionWarning = versionWarning, + checksumStatus = checksumStatus + ) + } + } catch (e: Exception) { + Logger.error("Quick mode: Failed to analyze APK", e) + _uiState.value = _uiState.value.copy(error = "Failed to read APK: ${e.message}") + null + } + } + + /** + * Verify checksum against known values. + */ + private fun verifyChecksum(file: File, packageName: String, version: String, recommendedVersion: String?): ChecksumStatus { + // Check if this is a non-recommended version (use dynamic recommended version) + if (recommendedVersion != null && version != recommendedVersion) { + return ChecksumStatus.NonRecommendedVersion + } + + val expectedChecksum = AppConstants.getChecksum(packageName, version, emptyList()) + ?: return ChecksumStatus.NotConfigured + + return try { + val actualChecksum = ChecksumUtils.calculateSha256(file) + if (actualChecksum.equals(expectedChecksum, ignoreCase = true)) { + ChecksumStatus.Verified + } else { + ChecksumStatus.Mismatch(expectedChecksum, actualChecksum) + } + } catch (e: Exception) { + ChecksumStatus.Error(e.message ?: "Unknown error") + } + } + + /** + * Start the patching process with defaults. + */ + fun startPatching() { + val apkFile = _uiState.value.apkFile ?: return + val apkInfo = _uiState.value.apkInfo ?: return + + patchingJob = screenModelScope.launch { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.DOWNLOADING, + progress = 0f, + statusMessage = "Preparing patches..." + ) + + // Use cached patches file if available, otherwise download + val patchFile = if (cachedPatchesFile?.exists() == true) { + _uiState.value = _uiState.value.copy(progress = 0.3f) + cachedPatchesFile!! + } else { + // Download patches + val patchesResult = patchRepository.getLatestStableRelease() + val patchRelease = patchesResult.getOrNull() + if (patchRelease == null) { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.READY, + error = "Failed to fetch patches. Check your internet connection." + ) + return@launch + } + + _uiState.value = _uiState.value.copy( + statusMessage = "Downloading patches ${patchRelease.tagName}..." + ) + + val patchFileResult = patchRepository.downloadPatches(patchRelease) { progress -> + _uiState.value = _uiState.value.copy(progress = progress * 0.3f) + } + + val downloadedFile = patchFileResult.getOrNull() + if (downloadedFile == null) { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.READY, + error = "Failed to download patches: ${patchFileResult.exceptionOrNull()?.message}" + ) + return@launch + } + cachedPatchesFile = downloadedFile + downloadedFile + } + + // 2. Start patching + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.PATCHING, + statusMessage = "Patching...", + progress = 0.4f + ) + + // Generate output path + val outputDir = apkFile.parentFile ?: File(System.getProperty("user.home")) + val baseName = apkInfo.displayName.replace(" ", "-") + val outputFileName = "$baseName-Morphe-${apkInfo.versionName}.apk" + val outputPath = File(outputDir, outputFileName).absolutePath + + // Use PatchService for direct library patching (no CLI subprocess) + val patchResult = patchService.patch( + patchesFilePath = patchFile.absolutePath, + inputApkPath = apkFile.absolutePath, + outputApkPath = outputPath, + enabledPatches = emptyList(), // Empty = use defaults + disabledPatches = emptyList(), + options = emptyMap(), + exclusiveMode = false, // Include all default patches + onProgress = { message -> + // Update status with current operation + if (message.contains("patch", ignoreCase = true) || + message.contains("applying", ignoreCase = true) || + message.contains("Applied", ignoreCase = true)) { + _uiState.value = _uiState.value.copy(statusMessage = message.take(60)) + } + // Parse progress + parseProgress(message) + } + ) + + patchResult.fold( + onSuccess = { result -> + if (result.success) { + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.COMPLETED, + outputPath = outputPath, + progress = 1f, + statusMessage = "Patching complete! Applied ${result.appliedPatches.size} patches." + ) + Logger.info("Quick mode: Patching completed - $outputPath (${result.appliedPatches.size} patches)") + } else { + val errorMsg = if (result.failedPatches.isNotEmpty()) { + "Patching had failures: ${result.failedPatches.joinToString(", ")}" + } else { + "Patching failed. Please try the full mode for more details." + } + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.READY, + error = errorMsg + ) + } + }, + onFailure = { e -> + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.READY, + error = "Error: ${e.message}" + ) + } + ) + } + } + + /** + * Parse progress from CLI output. + */ + private fun parseProgress(line: String) { + // Pattern: "Executing patch X of Y" + val executingPattern = Regex("""(?:Executing|Applying)\s+patch\s+(\d+)\s+of\s+(\d+)""", RegexOption.IGNORE_CASE) + val match = executingPattern.find(line) + if (match != null) { + val current = match.groupValues[1].toIntOrNull() ?: 0 + val total = match.groupValues[2].toIntOrNull() ?: 1 + val patchProgress = current.toFloat() / total.toFloat() + // Patching is 50-100% of total progress + _uiState.value = _uiState.value.copy( + progress = 0.5f + patchProgress * 0.5f + ) + } + } + + /** + * Cancel patching. + */ + fun cancelPatching() { + patchingJob?.cancel() + patchingJob = null + _uiState.value = _uiState.value.copy( + phase = QuickPatchPhase.READY, + statusMessage = "Cancelled" + ) + } + + /** + * Reset to start over. + */ + fun reset() { + patchingJob?.cancel() + patchingJob = null + _uiState.value = QuickPatchUiState() + } + + /** + * Clear error message. + */ + fun clearError() { + _uiState.value = _uiState.value.copy(error = null) + } + + fun setDragHover(isHovering: Boolean) { + _uiState.value = _uiState.value.copy(isDragHovering = isHovering) + } +} + +/** + * Phases of the quick patch flow. + */ +enum class QuickPatchPhase { + IDLE, // Waiting for APK + ANALYZING, // Reading APK info + READY, // APK validated, ready to patch + DOWNLOADING, // Downloading patches/CLI + PATCHING, // Running patch command + COMPLETED // Done! +} + +/** + * Simplified APK info for quick mode. + * Uses dynamic data from patches instead of hardcoded values. + */ +data class QuickApkInfo( + val fileName: String, + val packageName: String, + val versionName: String, + val fileSize: Long, + val displayName: String, + val recommendedVersion: String?, + val isRecommendedVersion: Boolean, + val versionWarning: String?, + val checksumStatus: ChecksumStatus +) { + val formattedSize: String + get() = when { + fileSize < 1024 -> "$fileSize B" + fileSize < 1024 * 1024 -> "%.1f KB".format(fileSize / 1024.0) + fileSize < 1024 * 1024 * 1024 -> "%.1f MB".format(fileSize / (1024.0 * 1024.0)) + else -> "%.2f GB".format(fileSize / (1024.0 * 1024.0 * 1024.0)) + } +} + +/** + * UI state for quick patch mode. + */ +data class QuickPatchUiState( + val phase: QuickPatchPhase = QuickPatchPhase.IDLE, + val apkFile: File? = null, + val apkInfo: QuickApkInfo? = null, + val error: String? = null, + val isDragHovering: Boolean = false, + val progress: Float = 0f, + val statusMessage: String = "", + val outputPath: String? = null, + // Dynamic data from patches + val isLoadingPatches: Boolean = true, + val supportedApps: List = emptyList(), + val patchesVersion: String? = null +) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt new file mode 100644 index 0000000..ba5a4e2 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt @@ -0,0 +1,753 @@ +package app.morphe.gui.ui.screens.result + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.ui.graphics.Color +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.PhoneAndroid +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.Usb +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import app.morphe.gui.data.repository.ConfigRepository +import kotlinx.coroutines.launch +import org.koin.compose.koinInject +import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.AdbDevice +import app.morphe.gui.util.AdbException +import app.morphe.gui.util.AdbManager +import app.morphe.gui.util.DeviceStatus +import app.morphe.gui.util.FileUtils +import app.morphe.gui.util.Logger +import java.awt.Desktop +import java.io.File + +/** + * Screen showing the result of patching. + */ +data class ResultScreen( + val outputPath: String +) : Screen { + + @Composable + override fun Content() { + ResultScreenContent(outputPath = outputPath) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ResultScreenContent(outputPath: String) { + val navigator = LocalNavigator.currentOrThrow + val outputFile = File(outputPath) + val scope = rememberCoroutineScope() + val adbManager = remember { AdbManager() } + val configRepository: ConfigRepository = koinInject() + + // ADB state + var isAdbAvailable by remember { mutableStateOf(null) } + var connectedDevices by remember { mutableStateOf>(emptyList()) } + var selectedDevice by remember { mutableStateOf(null) } + var isLoadingDevices by remember { mutableStateOf(false) } + var isInstalling by remember { mutableStateOf(false) } + var installProgress by remember { mutableStateOf("") } + var installError by remember { mutableStateOf(null) } + var installSuccess by remember { mutableStateOf(false) } + + // Cleanup state + var hasTempFiles by remember { mutableStateOf(false) } + var tempFilesSize by remember { mutableStateOf(0L) } + var tempFilesCleared by remember { mutableStateOf(false) } + var autoCleanupEnabled by remember { mutableStateOf(false) } + + // Check for temp files and auto-cleanup setting + LaunchedEffect(Unit) { + val config = configRepository.loadConfig() + autoCleanupEnabled = config.autoCleanupTempFiles + hasTempFiles = FileUtils.hasTempFiles() + tempFilesSize = FileUtils.getTempDirSize() + + // Auto-cleanup if enabled + if (autoCleanupEnabled && hasTempFiles) { + FileUtils.cleanupAllTempDirs() + hasTempFiles = false + tempFilesCleared = true + Logger.info("Auto-cleaned temp files after successful patching") + } + } + + // Function to refresh device list + fun refreshDevices() { + scope.launch { + isLoadingDevices = true + val result = adbManager.getConnectedDevices() + result.fold( + onSuccess = { devices -> + connectedDevices = devices + // Auto-select if only one ready device + val readyDevices = devices.filter { it.isReady } + if (readyDevices.size == 1) { + selectedDevice = readyDevices.first() + } else if (selectedDevice != null && !readyDevices.any { it.id == selectedDevice?.id }) { + // Clear selection if previously selected device is no longer available + selectedDevice = null + } + }, + onFailure = { + connectedDevices = emptyList() + selectedDevice = null + } + ) + isLoadingDevices = false + } + } + + // Check ADB availability and fetch devices on load + LaunchedEffect(Unit) { + isAdbAvailable = adbManager.isAdbAvailable() + if (isAdbAvailable == true) { + refreshDevices() + } + } + + // Install function + fun installViaAdb() { + val device = selectedDevice ?: return + scope.launch { + isInstalling = true + installError = null + installProgress = "Installing on ${device.displayName}..." + + val result = adbManager.installApk( + apkPath = outputPath, + deviceId = device.id, + onProgress = { installProgress = it } + ) + + result.fold( + onSuccess = { + installSuccess = true + installProgress = "Installation successful!" + }, + onFailure = { exception -> + installError = (exception as? AdbException)?.message ?: exception.message ?: "Unknown error" + } + ) + + isInstalling = false + } + } + + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + BoxWithConstraints( + modifier = Modifier.fillMaxSize() + ) { + val scrollState = rememberScrollState() + + // Estimate content height for dynamic spacing + val contentHeight = 600.dp // Approximate height of all content + val extraSpace = (maxHeight - contentHeight).coerceAtLeast(0.dp) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(32.dp) + ) { + // Add top spacing to center content on large screens + Spacer(modifier = Modifier.height(extraSpace / 2)) + // Success icon + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = "Success", + tint = MorpheColors.Teal, + modifier = Modifier.size(80.dp) + ) + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = "Patching Complete!", + fontSize = 28.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = "Your patched APK is ready", + fontSize = 16.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + Spacer(modifier = Modifier.height(32.dp)) + + // Output file info card + Card( + modifier = Modifier.widthIn(max = 500.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ), + shape = RoundedCornerShape(16.dp) + ) { + Column( + modifier = Modifier.padding(20.dp) + ) { + Text( + text = "Output File", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = outputFile.name, + fontSize = 16.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = outputFile.parent ?: "", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (outputFile.exists()) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = formatFileSize(outputFile.length()), + fontSize = 13.sp, + color = MorpheColors.Teal + ) + } + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ADB Install Section + if (isAdbAvailable == true) { + AdbInstallSection( + devices = connectedDevices, + selectedDevice = selectedDevice, + isLoadingDevices = isLoadingDevices, + isInstalling = isInstalling, + installProgress = installProgress, + installError = installError, + installSuccess = installSuccess, + onDeviceSelected = { selectedDevice = it }, + onRefreshDevices = { refreshDevices() }, + onInstallClick = { installViaAdb() }, + onRetryClick = { + installError = null + installSuccess = false + installViaAdb() + }, + onDismissError = { installError = null } + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + + // Cleanup section + if (hasTempFiles || tempFilesCleared) { + CleanupSection( + hasTempFiles = hasTempFiles, + tempFilesSize = tempFilesSize, + tempFilesCleared = tempFilesCleared, + autoCleanupEnabled = autoCleanupEnabled, + onCleanupClick = { + FileUtils.cleanupAllTempDirs() + hasTempFiles = false + tempFilesCleared = true + Logger.info("Manually cleaned temp files after patching") + } + ) + + Spacer(modifier = Modifier.height(16.dp)) + } + + // Action buttons + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + OutlinedButton( + onClick = { + try { + val folder = outputFile.parentFile + if (folder != null && Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(folder) + } + } catch (e: Exception) { + // Ignore errors + } + }, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(12.dp) + ) { + Icon( + imageVector = Icons.Default.FolderOpen, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text("Open Folder") + } + + Button( + onClick = { navigator.popUntilRoot() }, + modifier = Modifier.height(48.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Blue + ), + shape = RoundedCornerShape(12.dp) + ) { + Text("Patch Another", fontWeight = FontWeight.Medium) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Help text (only show when ADB is not available) + if (isAdbAvailable == false) { + Text( + text = "ADB not found. Install Android SDK Platform Tools to enable direct installation.", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center + ) + } else if (isAdbAvailable == null) { + Text( + text = "Checking for ADB...", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + textAlign = TextAlign.Center + ) + } + + // Bottom spacing to center content on large screens + Spacer(modifier = Modifier.height(extraSpace / 2)) + } + } + + // Settings button in top-right corner + SettingsButton( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(24.dp), + allowCacheClear = false + ) + } +} + +@Composable +private fun AdbInstallSection( + devices: List, + selectedDevice: AdbDevice?, + isLoadingDevices: Boolean, + isInstalling: Boolean, + installProgress: String, + installError: String?, + installSuccess: Boolean, + onDeviceSelected: (AdbDevice) -> Unit, + onRefreshDevices: () -> Unit, + onInstallClick: () -> Unit, + onRetryClick: () -> Unit, + onDismissError: () -> Unit +) { + Card( + modifier = Modifier.widthIn(max = 500.dp), + colors = CardDefaults.cardColors( + containerColor = when { + installSuccess -> MorpheColors.Teal.copy(alpha = 0.1f) + installError != null -> MaterialTheme.colorScheme.error.copy(alpha = 0.1f) + else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + } + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // Header + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Usb, + contentDescription = null, + tint = MorpheColors.Blue, + modifier = Modifier.size(20.dp) + ) + Text( + text = "Install via ADB", + fontWeight = FontWeight.SemiBold, + fontSize = 15.sp + ) + } + // Refresh button + IconButton( + onClick = onRefreshDevices, + enabled = !isLoadingDevices && !isInstalling + ) { + if (isLoadingDevices) { + CircularProgressIndicator( + modifier = Modifier.size(18.dp), + strokeWidth = 2.dp + ) + } else { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh devices", + modifier = Modifier.size(20.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(12.dp)) + + when { + installSuccess -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MorpheColors.Teal, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Installed successfully on ${selectedDevice?.displayName ?: "device"}!", + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal + ) + } + } + + installError != null -> { + Text( + text = installError, + color = MaterialTheme.colorScheme.error, + fontSize = 14.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(12.dp)) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center + ) { + TextButton(onClick = onDismissError) { + Text("Dismiss") + } + Spacer(modifier = Modifier.width(8.dp)) + Button( + onClick = onRetryClick, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error + ) + ) { + Text("Retry") + } + } + } + + isInstalling -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = installProgress.ifEmpty { "Installing..." }, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + + else -> { + // Device list + val readyDevices = devices.filter { it.isReady } + val notReadyDevices = devices.filter { !it.isReady } + + if (devices.isEmpty()) { + // No devices + Column( + modifier = Modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "No devices connected", + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 14.sp + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Connect your Android device via USB with USB debugging enabled", + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + fontSize = 12.sp, + textAlign = TextAlign.Center + ) + } + } else { + // Show device list + Text( + text = if (readyDevices.size == 1) "Connected device:" else "Select a device:", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(8.dp)) + + // Ready devices + readyDevices.forEach { device -> + DeviceRow( + device = device, + isSelected = selectedDevice?.id == device.id, + onClick = { onDeviceSelected(device) } + ) + Spacer(modifier = Modifier.height(6.dp)) + } + + // Not ready devices (unauthorized/offline) + notReadyDevices.forEach { device -> + DeviceRow( + device = device, + isSelected = false, + onClick = { }, + enabled = false + ) + Spacer(modifier = Modifier.height(6.dp)) + } + + Spacer(modifier = Modifier.height(8.dp)) + + // Install button + Button( + onClick = onInstallClick, + modifier = Modifier.fillMaxWidth(), + enabled = selectedDevice != null, + colors = ButtonDefaults.buttonColors( + containerColor = MorpheColors.Teal + ), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = if (selectedDevice != null) + "Install on ${selectedDevice.displayName}" + else + "Select a device to install", + fontWeight = FontWeight.Medium + ) + } + } + } + } + } + } +} + +@Composable +private fun CleanupSection( + hasTempFiles: Boolean, + tempFilesSize: Long, + tempFilesCleared: Boolean, + autoCleanupEnabled: Boolean, + onCleanupClick: () -> Unit +) { + Card( + modifier = Modifier.widthIn(max = 500.dp), + colors = CardDefaults.cardColors( + containerColor = if (tempFilesCleared) + MorpheColors.Teal.copy(alpha = 0.1f) + else + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (tempFilesCleared) "Temp files cleaned" else "Temporary files", + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + color = if (tempFilesCleared) + MorpheColors.Teal + else + MaterialTheme.colorScheme.onSurface + ) + Text( + text = when { + tempFilesCleared && autoCleanupEnabled -> "Auto-cleanup is enabled" + tempFilesCleared -> "Freed up ${formatFileSize(tempFilesSize)}" + else -> "${formatFileSize(tempFilesSize)} can be freed" + }, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (hasTempFiles && !tempFilesCleared) { + OutlinedButton( + onClick = onCleanupClick, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) + ) { + Text("Clean up", fontSize = 13.sp) + } + } else if (tempFilesCleared) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MorpheColors.Teal, + modifier = Modifier.size(24.dp) + ) + } + } + } +} + +@Composable +private fun DeviceRow( + device: AdbDevice, + isSelected: Boolean, + onClick: () -> Unit, + enabled: Boolean = true +) { + OutlinedCard( + onClick = onClick, + modifier = Modifier.fillMaxWidth(), + enabled = enabled, + shape = RoundedCornerShape(8.dp), + border = BorderStroke( + width = if (isSelected) 2.dp else 1.dp, + color = when { + isSelected -> MorpheColors.Teal + !enabled -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) + } + ), + colors = CardDefaults.outlinedCardColors( + containerColor = if (isSelected) + MorpheColors.Teal.copy(alpha = 0.08f) + else + MaterialTheme.colorScheme.surface + ) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Default.PhoneAndroid, + contentDescription = null, + tint = when { + isSelected -> MorpheColors.Teal + device.isReady -> MorpheColors.Blue + else -> MaterialTheme.colorScheme.error.copy(alpha = 0.6f) + }, + modifier = Modifier.size(24.dp) + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = device.displayName, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, + color = if (enabled) + MaterialTheme.colorScheme.onSurface + else + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), + fontSize = 14.sp + ) + Text( + text = device.id, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + // Status badge + Surface( + color = when (device.status) { + DeviceStatus.DEVICE -> MorpheColors.Teal.copy(alpha = 0.15f) + DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800).copy(alpha = 0.15f) + else -> MaterialTheme.colorScheme.error.copy(alpha = 0.15f) + }, + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = when (device.status) { + DeviceStatus.DEVICE -> "Ready" + DeviceStatus.UNAUTHORIZED -> "Unauthorized" + DeviceStatus.OFFLINE -> "Offline" + DeviceStatus.UNKNOWN -> "Unknown" + }, + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + color = when (device.status) { + DeviceStatus.DEVICE -> MorpheColors.Teal + DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.error + }, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } + } +} + +private fun formatFileSize(bytes: Long): String { + return when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) + bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) + else -> "%.2f GB".format(bytes / (1024.0 * 1024.0 * 1024.0)) + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt new file mode 100644 index 0000000..f980d43 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt @@ -0,0 +1,79 @@ +package app.morphe.gui.ui.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +// Morphe Brand Colors +object MorpheColors { + val Blue = Color(0xFF2D62DD) + val Teal = Color(0xFF00A797) + val Cyan = Color(0xFF62E1FF) + val DeepBlack = Color(0xFF121212) + val SurfaceDark = Color(0xFF1E1E1E) + val SurfaceLight = Color(0xFFF5F5F5) + val TextLight = Color(0xFFE3E3E3) + val TextDark = Color(0xFF1C1C1C) +} + +private val MorpheDarkColorScheme = darkColorScheme( + primary = MorpheColors.Blue, + secondary = MorpheColors.Teal, + tertiary = MorpheColors.Cyan, + background = Color(0xFF121212), + surface = Color(0xFF1E1E1E), + surfaceVariant = Color(0xFF2A2A2A), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black, + onBackground = MorpheColors.TextLight, + onSurface = MorpheColors.TextLight, + onSurfaceVariant = Color(0xFFB0B0B0), + error = Color(0xFFCF6679), + onError = Color.Black +) + +private val MorpheLightColorScheme = lightColorScheme( + primary = MorpheColors.Blue, + secondary = MorpheColors.Teal, + tertiary = MorpheColors.Cyan, + background = Color(0xFFFAFAFA), + surface = MorpheColors.SurfaceLight, + surfaceVariant = Color(0xFFE8E8E8), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black, + onBackground = MorpheColors.TextDark, + onSurface = MorpheColors.TextDark, + onSurfaceVariant = Color(0xFF505050), + error = Color(0xFFB00020), + onError = Color.White +) + +enum class ThemePreference { + LIGHT, + DARK, + SYSTEM +} + +@Composable +fun MorpheTheme( + themePreference: ThemePreference = ThemePreference.SYSTEM, + content: @Composable () -> Unit +) { + val colorScheme = when (themePreference) { + ThemePreference.DARK -> MorpheDarkColorScheme + ThemePreference.LIGHT -> MorpheLightColorScheme + ThemePreference.SYSTEM -> { + if (isSystemInDarkTheme()) MorpheDarkColorScheme else MorpheLightColorScheme + } + } + + MaterialTheme( + colorScheme = colorScheme, + content = content + ) +} diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/ThemeState.kt b/src/main/kotlin/app/morphe/gui/ui/theme/ThemeState.kt new file mode 100644 index 0000000..838bb19 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/theme/ThemeState.kt @@ -0,0 +1,17 @@ +package app.morphe.gui.ui.theme + +import androidx.compose.runtime.compositionLocalOf + +/** + * Holds the current theme state and callback to change it. + * Provided via CompositionLocal so any screen can access it. + */ +data class ThemeState( + val current: ThemePreference = ThemePreference.SYSTEM, + val onChange: (ThemePreference) -> Unit = {} +) + +/** + * CompositionLocal for accessing theme state from any composable. + */ +val LocalThemeState = compositionLocalOf { ThemeState() } diff --git a/src/main/kotlin/app/morphe/gui/util/AdbManager.kt b/src/main/kotlin/app/morphe/gui/util/AdbManager.kt new file mode 100644 index 0000000..94f933c --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/AdbManager.kt @@ -0,0 +1,359 @@ +package app.morphe.gui.util + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File + +/** + * Manages ADB (Android Debug Bridge) operations for installing APKs. + * Works across macOS, Linux, and Windows. + */ +class AdbManager { + + private var adbPath: String? = null + + /** + * Find ADB binary in common locations or PATH. + * Returns the path to ADB if found, null otherwise. + */ + suspend fun findAdb(): String? = withContext(Dispatchers.IO) { + // Return cached path if already found + adbPath?.let { + if (File(it).exists()) return@withContext it + } + + val os = System.getProperty("os.name").lowercase() + val isWindows = os.contains("windows") + val isMac = os.contains("mac") + val adbName = if (isWindows) "adb.exe" else "adb" + + // Common ADB locations by platform + val searchPaths = mutableListOf() + + if (isMac) { + // macOS paths + val home = System.getProperty("user.home") + searchPaths.addAll(listOf( + "$home/Library/Android/sdk/platform-tools/$adbName", + "/opt/homebrew/bin/$adbName", + "/usr/local/bin/$adbName", + "/Applications/Android Studio.app/Contents/platform-tools/$adbName" + )) + } else if (isWindows) { + // Windows paths + val localAppData = System.getenv("LOCALAPPDATA") ?: "" + val userProfile = System.getenv("USERPROFILE") ?: "" + searchPaths.addAll(listOf( + "$localAppData\\Android\\Sdk\\platform-tools\\$adbName", + "$userProfile\\AppData\\Local\\Android\\Sdk\\platform-tools\\$adbName", + "C:\\Android\\sdk\\platform-tools\\$adbName", + "C:\\Program Files\\Android\\platform-tools\\$adbName" + )) + } else { + // Linux paths + val home = System.getProperty("user.home") + searchPaths.addAll(listOf( + "$home/Android/Sdk/platform-tools/$adbName", + "$home/android-sdk/platform-tools/$adbName", + "/opt/android-sdk/platform-tools/$adbName", + "/usr/bin/$adbName", + "/usr/local/bin/$adbName" + )) + } + + // Check each path + for (path in searchPaths) { + val file = File(path) + if (file.exists() && file.canExecute()) { + Logger.info("Found ADB at: $path") + adbPath = path + return@withContext path + } + } + + // Try to find in PATH + try { + val process = ProcessBuilder(if (isWindows) listOf("where", adbName) else listOf("which", adbName)) + .redirectErrorStream(true) + .start() + + val result = process.inputStream.bufferedReader().readText().trim() + process.waitFor() + + if (process.exitValue() == 0 && result.isNotEmpty()) { + val path = result.lines().first() + if (File(path).exists()) { + Logger.info("Found ADB in PATH: $path") + adbPath = path + return@withContext path + } + } + } catch (e: Exception) { + Logger.debug("Could not find ADB in PATH: ${e.message}") + } + + Logger.warn("ADB not found") + null + } + + /** + * Check if ADB is available. + */ + suspend fun isAdbAvailable(): Boolean = findAdb() != null + + /** + * Get list of connected devices. + * Returns list of device IDs and their status. + */ + suspend fun getConnectedDevices(): Result> = withContext(Dispatchers.IO) { + val adb = findAdb() ?: return@withContext Result.failure( + AdbException("ADB not found. Please install Android SDK Platform Tools.") + ) + + try { + // Use -l flag to get detailed device info including model + val process = ProcessBuilder(adb, "devices", "-l") + .redirectErrorStream(true) + .start() + + val output = process.inputStream.bufferedReader().readText() + val exitCode = process.waitFor() + + if (exitCode != 0) { + return@withContext Result.failure( + AdbException("Failed to get device list: $output") + ) + } + + val devices = parseDeviceList(output, adb) + Logger.info("Found ${devices.size} device(s)") + Result.success(devices) + } catch (e: Exception) { + Logger.error("Error getting devices", e) + Result.failure(AdbException("Failed to get devices: ${e.message}")) + } + } + + /** + * Install an APK on the specified device (or default device if only one connected). + */ + suspend fun installApk( + apkPath: String, + deviceId: String? = null, + allowDowngrade: Boolean = true, + onProgress: (String) -> Unit = {} + ): Result = withContext(Dispatchers.IO) { + val adb = findAdb() ?: return@withContext Result.failure( + AdbException("ADB not found. Please install Android SDK Platform Tools.") + ) + + val apkFile = File(apkPath) + if (!apkFile.exists()) { + return@withContext Result.failure(AdbException("APK file not found: $apkPath")) + } + + // Check connected devices + val devicesResult = getConnectedDevices() + if (devicesResult.isFailure) { + return@withContext Result.failure(devicesResult.exceptionOrNull()!!) + } + + val devices = devicesResult.getOrThrow() + val authorizedDevices = devices.filter { it.status == DeviceStatus.DEVICE } + + if (authorizedDevices.isEmpty()) { + val unauthorized = devices.filter { it.status == DeviceStatus.UNAUTHORIZED } + return@withContext Result.failure( + if (unauthorized.isNotEmpty()) { + AdbException("Device connected but not authorized. Please accept the USB debugging prompt on your device.") + } else { + AdbException("No devices connected. Please connect your Android device with USB debugging enabled.") + } + ) + } + + // Determine target device + val targetDevice = if (deviceId != null) { + authorizedDevices.find { it.id == deviceId } + ?: return@withContext Result.failure(AdbException("Device $deviceId not found")) + } else if (authorizedDevices.size == 1) { + authorizedDevices.first() + } else { + return@withContext Result.failure( + AdbMultipleDevicesException( + "Multiple devices connected. Please select one.", + authorizedDevices + ) + ) + } + + // Build install command + val command = mutableListOf(adb) + command.add("-s") + command.add(targetDevice.id) + command.add("install") + command.add("-r") // Replace existing + if (allowDowngrade) { + command.add("-d") // Allow downgrade + } + command.add(apkPath) + + onProgress("Installing on ${targetDevice.displayName}...") + Logger.info("Running: ${command.joinToString(" ")}") + + try { + val process = ProcessBuilder(command) + .redirectErrorStream(true) + .start() + + // Read output in real-time + val reader = process.inputStream.bufferedReader() + val output = StringBuilder() + reader.forEachLine { line -> + output.appendLine(line) + onProgress(line) + Logger.debug("ADB: $line") + } + + val exitCode = process.waitFor() + val outputStr = output.toString() + + if (exitCode == 0 && outputStr.contains("Success")) { + Logger.info("APK installed successfully") + Result.success(Unit) + } else { + val errorMessage = parseInstallError(outputStr) + Logger.error("Installation failed: $errorMessage") + Result.failure(AdbException(errorMessage)) + } + } catch (e: Exception) { + Logger.error("Error installing APK", e) + Result.failure(AdbException("Installation failed: ${e.message}")) + } + } + + /** + * Parse output from 'adb devices -l' command. + * Example line: "XXXXXXXX device usb:1-1 product:flame model:Pixel_4 device:flame transport_id:1" + */ + private fun parseDeviceList(output: String, adbPath: String): List { + return output.lines() + .drop(1) // Skip "List of devices attached" header + .filter { it.isNotBlank() } + .mapNotNull { line -> + val parts = line.split("\\s+".toRegex()) + if (parts.size >= 2) { + val id = parts[0] + val status = when (parts[1]) { + "device" -> DeviceStatus.DEVICE + "unauthorized" -> DeviceStatus.UNAUTHORIZED + "offline" -> DeviceStatus.OFFLINE + else -> DeviceStatus.UNKNOWN + } + + // Parse model from the -l output (format: model:Device_Name) + var model: String? = null + var product: String? = null + for (part in parts.drop(2)) { + when { + part.startsWith("model:") -> model = part.removePrefix("model:").replace("_", " ") + part.startsWith("product:") -> product = part.removePrefix("product:") + } + } + + // If device is authorized, try to get friendly device name + val deviceName = if (status == DeviceStatus.DEVICE) { + model ?: product ?: getDeviceName(adbPath, id) + } else { + model ?: product + } + + AdbDevice(id, status, deviceName) + } else null + } + } + + /** + * Get device name using adb shell command. + */ + private fun getDeviceName(adbPath: String, deviceId: String): String? { + return try { + val process = ProcessBuilder(adbPath, "-s", deviceId, "shell", "getprop", "ro.product.model") + .redirectErrorStream(true) + .start() + val result = process.inputStream.bufferedReader().readText().trim() + process.waitFor() + if (process.exitValue() == 0 && result.isNotBlank()) result else null + } catch (e: Exception) { + null + } + } + + private fun parseInstallError(output: String): String { + // Common ADB install errors + return when { + output.contains("INSTALL_FAILED_VERSION_DOWNGRADE") -> + "Cannot downgrade - a newer version is installed. Uninstall the existing app first." + output.contains("INSTALL_FAILED_ALREADY_EXISTS") -> + "App already exists. Try uninstalling it first." + output.contains("INSTALL_FAILED_INSUFFICIENT_STORAGE") -> + "Not enough storage space on device." + output.contains("INSTALL_FAILED_INVALID_APK") -> + "Invalid APK file." + output.contains("INSTALL_PARSE_FAILED_NO_CERTIFICATES") -> + "APK is not signed properly." + output.contains("INSTALL_FAILED_UPDATE_INCOMPATIBLE") -> + "Incompatible update - signatures don't match. Uninstall the existing app first." + output.contains("INSTALL_FAILED_USER_RESTRICTED") -> + "Installation restricted by user settings." + output.contains("INSTALL_FAILED_VERIFICATION_FAILURE") -> + "Package verification failed." + output.contains("Failure") -> { + // Extract the failure reason + val match = Regex("Failure \\[(.+)]").find(output) + match?.groupValues?.get(1) ?: "Installation failed: $output" + } + else -> "Installation failed: $output" + } + } +} + +data class AdbDevice( + val id: String, + val status: DeviceStatus, + val model: String? = null +) { + /** Device name (model or ID if model unknown) */ + val displayName: String + get() = model?.takeIf { it.isNotBlank() } ?: id + + /** Full display with status for UI */ + val displayNameWithStatus: String + get() { + val name = displayName + return when (status) { + DeviceStatus.DEVICE -> "$name (Connected)" + DeviceStatus.UNAUTHORIZED -> "$name (Unauthorized - check device)" + DeviceStatus.OFFLINE -> "$name (Offline)" + DeviceStatus.UNKNOWN -> "$name (Unknown status)" + } + } + + /** Whether device is ready for installation */ + val isReady: Boolean + get() = status == DeviceStatus.DEVICE +} + +enum class DeviceStatus { + DEVICE, // Connected and authorized + UNAUTHORIZED, // Connected but not authorized for debugging + OFFLINE, // Device offline + UNKNOWN // Unknown status +} + +open class AdbException(message: String) : Exception(message) + +class AdbMultipleDevicesException( + message: String, + val devices: List +) : AdbException(message) diff --git a/src/main/kotlin/app/morphe/gui/util/ChecksumUtils.kt b/src/main/kotlin/app/morphe/gui/util/ChecksumUtils.kt new file mode 100644 index 0000000..67cfa15 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/ChecksumUtils.kt @@ -0,0 +1,58 @@ +package app.morphe.gui.util + +import java.io.File +import java.io.FileInputStream +import java.security.MessageDigest + +/** + * Utility for calculating and verifying file checksums. + */ +object ChecksumUtils { + + /** + * Calculate SHA-256 checksum of a file. + * @return Lowercase hex string of the checksum + */ + fun calculateSha256(file: File): String { + val digest = MessageDigest.getInstance("SHA-256") + val buffer = ByteArray(8192) + + FileInputStream(file).use { fis -> + var bytesRead: Int + while (fis.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + } + + return digest.digest().joinToString("") { "%02x".format(it) } + } + + /** + * Verify a file's checksum against expected value. + * @return true if checksums match (case-insensitive comparison) + */ + fun verifyChecksum(file: File, expectedChecksum: String): Boolean { + val actualChecksum = calculateSha256(file) + return actualChecksum.equals(expectedChecksum, ignoreCase = true) + } +} + +/** + * Result of checksum verification. + */ +sealed class ChecksumStatus { + /** Checksum matches the expected value - file is verified */ + data object Verified : ChecksumStatus() + + /** Checksum doesn't match - file may be corrupted or modified */ + data class Mismatch(val expected: String, val actual: String) : ChecksumStatus() + + /** No checksum configured for this version - cannot verify */ + data object NotConfigured : ChecksumStatus() + + /** Non-recommended version - checksum verification not applicable */ + data object NonRecommendedVersion : ChecksumStatus() + + /** Checksum calculation failed */ + data class Error(val message: String) : ChecksumStatus() +} diff --git a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt new file mode 100644 index 0000000..3906045 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt @@ -0,0 +1,149 @@ +package app.morphe.gui.util + +import java.io.File +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Platform-agnostic file utilities. + * Handles app directories, temp files, and cross-platform path operations. + */ +object FileUtils { + + private const val APP_NAME = "morphe-gui" + + /** + * Get the app data directory based on OS. + * - Windows: %APPDATA%/morphe-gui + * - macOS: ~/Library/Application Support/morphe-gui + * - Linux: ~/.config/morphe-gui + */ + fun getAppDataDir(): File { + val osName = System.getProperty("os.name").lowercase() + val userHome = System.getProperty("user.home") + + val appDataPath = when { + osName.contains("win") -> { + val appData = System.getenv("APPDATA") ?: Paths.get(userHome, "AppData", "Roaming").toString() + Paths.get(appData, APP_NAME) + } + osName.contains("mac") -> { + Paths.get(userHome, "Library", "Application Support", APP_NAME) + } + else -> { + // Linux and others + Paths.get(userHome, ".config", APP_NAME) + } + } + + return appDataPath.toFile().also { it.mkdirs() } + } + + /** + * Get the patches cache directory. + */ + fun getPatchesDir(): File { + return File(getAppDataDir(), "patches").also { it.mkdirs() } + } + + /** + * Get the logs directory. + */ + fun getLogsDir(): File { + return File(getAppDataDir(), "logs").also { it.mkdirs() } + } + + /** + * Get the config file path. + */ + fun getConfigFile(): File { + return File(getAppDataDir(), "config.json") + } + + /** + * Get the app temp directory for patching operations. + */ + fun getTempDir(): File { + val systemTemp = System.getProperty("java.io.tmpdir") + return File(systemTemp, APP_NAME).also { it.mkdirs() } + } + + /** + * Create a unique temp directory for a patching session. + */ + fun createPatchingTempDir(): File { + val timestamp = System.currentTimeMillis() + return File(getTempDir(), "patching-$timestamp").also { it.mkdirs() } + } + + /** + * Clean up a temp directory. + */ + fun cleanupTempDir(dir: File): Boolean { + return try { + if (dir.exists() && dir.startsWith(getTempDir())) { + dir.deleteRecursively() + } else { + false + } + } catch (e: Exception) { + false + } + } + + /** + * Clean up all temp directories (call on app exit). + */ + fun cleanupAllTempDirs(): Boolean { + return try { + getTempDir().deleteRecursively() + true + } catch (e: Exception) { + false + } + } + + /** + * Get the size of all temp directories. + */ + fun getTempDirSize(): Long { + return try { + getTempDir().walkTopDown().filter { it.isFile }.sumOf { it.length() } + } catch (e: Exception) { + 0L + } + } + + /** + * Check if there are any temp files to clean. + */ + fun hasTempFiles(): Boolean { + return try { + val tempDir = getTempDir() + tempDir.exists() && (tempDir.listFiles()?.isNotEmpty() == true) + } catch (e: Exception) { + false + } + } + + /** + * Build a path using the system file separator. + */ + fun buildPath(vararg parts: String): String { + return parts.joinToString(File.separator) + } + + /** + * Get file extension. + */ + fun getExtension(file: File): String { + return file.extension.lowercase() + } + + /** + * Check if file is an APK. + */ + fun isApkFile(file: File): Boolean { + return file.isFile && getExtension(file) == "apk" + } +} diff --git a/src/main/kotlin/app/morphe/gui/util/Logger.kt b/src/main/kotlin/app/morphe/gui/util/Logger.kt new file mode 100644 index 0000000..f8c310c --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/Logger.kt @@ -0,0 +1,219 @@ +package app.morphe.gui.util + +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.text.SimpleDateFormat +import java.util.* + +/** + * Simple file logger with rotation support. + * Logs to ~/.morphe-gui/logs/morphe-gui.log + */ +object Logger { + + private const val MAX_LOG_SIZE = 2 * 1024 * 1024 // 2 MB + private const val MAX_LOG_FILES = 3 + private const val MAX_LINES_TO_KEEP = 5000 // Keep last 5000 lines on startup + private const val LOG_FILE_NAME = "morphe-gui.log" + + private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.US) + private var logFile: File? = null + private var initialized = false + + enum class Level { + DEBUG, INFO, WARN, ERROR + } + + /** + * Initialize the logger. Call once at app startup. + */ + fun init() { + if (initialized) return + + try { + val logsDir = FileUtils.getLogsDir() + logFile = File(logsDir, LOG_FILE_NAME) + + // Trim log file if it's too large (keep only last N lines) + trimLogFile() + + // Rotate if needed + rotateIfNeeded() + + // Log startup info + info("=".repeat(60)) + info("Morphe-GUI Started") + info("Version: 1.0.0") + info("OS: ${System.getProperty("os.name")} ${System.getProperty("os.version")}") + info("Java: ${System.getProperty("java.version")}") + info("User: ${System.getProperty("user.name")}") + info("App Data: ${FileUtils.getAppDataDir().absolutePath}") + info("=".repeat(60)) + + initialized = true + } catch (e: Exception) { + System.err.println("Failed to initialize logger: ${e.message}") + } + } + + /** + * Trim log file to keep only the last MAX_LINES_TO_KEEP lines. + */ + private fun trimLogFile() { + val file = logFile ?: return + if (!file.exists()) return + + try { + val lines = file.readLines() + if (lines.size > MAX_LINES_TO_KEEP) { + val trimmedLines = lines.takeLast(MAX_LINES_TO_KEEP) + file.writeText(trimmedLines.joinToString("\n") + "\n") + } + } catch (e: Exception) { + System.err.println("Failed to trim log file: ${e.message}") + } + } + + fun debug(message: String) = log(Level.DEBUG, message) + fun info(message: String) = log(Level.INFO, message) + fun warn(message: String) = log(Level.WARN, message) + fun error(message: String) = log(Level.ERROR, message) + + fun error(message: String, throwable: Throwable) { + val sw = StringWriter() + throwable.printStackTrace(PrintWriter(sw)) + log(Level.ERROR, "$message\n$sw") + } + + /** + * Log a CLI command execution. + */ + fun logCliCommand(command: List) { + info("CLI Command: ${command.joinToString(" ")}") + } + + /** + * Log CLI output. + */ + fun logCliOutput(output: String) { + if (output.isNotBlank()) { + debug("CLI Output: $output") + } + } + + private fun log(level: Level, message: String) { + val timestamp = dateFormat.format(Date()) + val logLine = "[$timestamp] [${level.name.padEnd(5)}] $message" + + // Print to console + when (level) { + Level.ERROR -> System.err.println(logLine) + else -> println(logLine) + } + + // Write to file + try { + logFile?.let { file -> + rotateIfNeeded() + file.appendText("$logLine\n") + } + } catch (e: Exception) { + System.err.println("Failed to write to log file: ${e.message}") + } + } + + private fun rotateIfNeeded() { + val file = logFile ?: return + if (!file.exists()) return + if (file.length() < MAX_LOG_SIZE) return + + try { + // Shift existing log files + for (i in MAX_LOG_FILES - 1 downTo 1) { + val older = File(file.parent, "$LOG_FILE_NAME.$i") + val newer = if (i == 1) file else File(file.parent, "$LOG_FILE_NAME.${i - 1}") + if (newer.exists()) { + if (older.exists()) older.delete() + newer.renameTo(older) + } + } + + // Create fresh log file + file.createNewFile() + } catch (e: Exception) { + System.err.println("Failed to rotate logs: ${e.message}") + } + } + + /** + * Get the current log file for export. + */ + fun getLogFile(): File? = logFile + + /** + * Get all log files for export. + */ + fun getAllLogFiles(): List { + val logsDir = FileUtils.getLogsDir() + return logsDir.listFiles() + ?.filter { it.name.startsWith(LOG_FILE_NAME) } + ?.sortedByDescending { it.lastModified() } + ?: emptyList() + } + + /** + * Export logs to a specified location. + */ + fun exportLogs(destination: File): Boolean { + return try { + val logs = getAllLogFiles() + if (logs.isEmpty()) return false + + if (logs.size == 1) { + logs.first().copyTo(destination, overwrite = true) + } else { + // Combine all logs into one file + destination.writeText("") + logs.reversed().forEach { log -> + destination.appendText("=== ${log.name} ===\n") + destination.appendText(log.readText()) + destination.appendText("\n") + } + } + true + } catch (e: Exception) { + error("Failed to export logs", e) + false + } + } + + /** + * Clear all log files. + */ + fun clearLogs(): Boolean { + return try { + val logsDir = FileUtils.getLogsDir() + logsDir.listFiles()?.forEach { it.delete() } + logFile?.createNewFile() + info("Logs cleared") + true + } catch (e: Exception) { + System.err.println("Failed to clear logs: ${e.message}") + false + } + } + + /** + * Get the total size of all log files. + */ + fun getLogsSize(): Long { + return try { + FileUtils.getLogsDir().walkTopDown() + .filter { it.isFile } + .sumOf { it.length() } + } catch (e: Exception) { + 0L + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt new file mode 100644 index 0000000..7fc5b87 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -0,0 +1,307 @@ +package app.morphe.gui.util + +import app.morphe.gui.data.model.CompatiblePackage +import app.morphe.gui.data.model.Patch +import app.morphe.gui.data.model.PatchOption +import app.morphe.gui.data.model.PatchOptionType +import app.morphe.library.ApkUtils +import app.morphe.library.ApkUtils.applyTo +import app.morphe.library.setOptions +import app.morphe.patcher.Patcher +import app.morphe.patcher.PatcherConfig +import app.morphe.patcher.patch.loadPatchesFromJar +import com.reandroid.apkeditor.merge.Merger +import com.reandroid.apkeditor.merge.MergerOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import kotlin.reflect.KType +import app.morphe.patcher.patch.Patch as LibraryPatch + +/** + * Bridge between GUI and morphe-patcher library. + * Replaces CliRunner with direct library calls. + */ +class PatchService { + + /** + * Load patches from an .mpp file and convert to GUI model. + * Optionally filter by package name. + */ + suspend fun listPatches( + patchesFilePath: String, + packageName: String? = null + ): Result> = withContext(Dispatchers.IO) { + try { + val patchFile = File(patchesFilePath) + if (!patchFile.exists()) { + return@withContext Result.failure(Exception("Patches file not found: $patchesFilePath")) + } + + Logger.info("Loading patches from: $patchesFilePath") + val patches = loadPatchesFromJar(setOf(patchFile)) + + // Convert library patches to GUI model + val guiPatches = patches.map { it.toGuiPatch() } + + // Filter by package name if specified + val filtered = if (packageName != null) { + guiPatches.filter { patch -> + patch.compatiblePackages.isEmpty() || // Universal patches + patch.compatiblePackages.any { it.name == packageName } + } + } else { + guiPatches + } + + Logger.info("Loaded ${filtered.size} patches" + (packageName?.let { " for $it" } ?: "")) + Result.success(filtered) + } catch (e: Exception) { + Logger.error("Failed to load patches", e) + Result.failure(e) + } + } + + /** + * Execute patching operation with progress callbacks. + */ + suspend fun patch( + patchesFilePath: String, + inputApkPath: String, + outputApkPath: String, + enabledPatches: List = emptyList(), + disabledPatches: List = emptyList(), + options: Map = emptyMap(), + exclusiveMode: Boolean = false, + onProgress: (String) -> Unit = {} + ): Result = withContext(Dispatchers.IO) { + val tempDir = FileUtils.createPatchingTempDir() + val tempOutputPath = File(tempDir, File(outputApkPath).name) + + try { + val patchFile = File(patchesFilePath) + val inputApk = File(inputApkPath) + val outputFile = File(outputApkPath) + + if (!patchFile.exists()) { + return@withContext Result.failure(Exception("Patches file not found")) + } + if (!inputApk.exists()) { + return@withContext Result.failure(Exception("Input APK not found")) + } + + onProgress("Loading patches...") + val patches = loadPatchesFromJar(setOf(patchFile)) + + // Handle APKM format (split APK bundle) + var mergedApkToCleanup: File? = null + val actualInputApk = if (inputApk.extension.equals("apkm", ignoreCase = true)) { + onProgress("Converting APKM to APK...") + val mergedApk = File(tempDir, "${inputApk.nameWithoutExtension}-merged.apk") + val mergerOptions = MergerOptions().apply { + this.inputFile = inputApk + this.outputFile = mergedApk + cleanMeta = true + } + Merger(mergerOptions).run() + mergedApkToCleanup = mergedApk + mergedApk + } else { + inputApk + } + + val patcherTempDir = File(tempDir, "patcher") + patcherTempDir.mkdirs() + + onProgress("Initializing patcher...") + val patcherConfig = PatcherConfig( + actualInputApk, + patcherTempDir, + null, // aapt binary path + patcherTempDir.absolutePath + ) + + val appliedPatches = mutableListOf() + val failedPatches = mutableListOf>() + + Patcher(patcherConfig).use { patcher -> + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion + + onProgress("Filtering patches for $packageName v$packageVersion...") + + // Filter patches based on compatibility and selection + val filteredPatches = patches.filter { patch -> + val patchName = patch.name ?: return@filter false + + // Check if explicitly disabled + if (patchName in disabledPatches) { + onProgress("Skipping disabled: $patchName") + return@filter false + } + + // Check package compatibility + val isCompatible = patch.compatiblePackages?.let { packages -> + packages.any { (name, versions) -> + name == packageName && (versions?.isEmpty() != false || versions.contains(packageVersion)) + } + } ?: true // Universal patches + + if (!isCompatible) { + return@filter false + } + + // In exclusive mode, only include explicitly enabled patches + if (exclusiveMode) { + patchName in enabledPatches + } else { + // Include if: enabled by default OR explicitly enabled + patch.use || patchName in enabledPatches + } + }.toSet() + + onProgress("Applying ${filteredPatches.size} patches...") + + // Set patch options if any + if (options.isNotEmpty()) { + val optionsMap = enabledPatches.associateWith { patchName -> + options.filterKeys { it.startsWith("$patchName.") } + .mapKeys { it.key.removePrefix("$patchName.") } + .mapValues { it.value as Any? } + .toMutableMap() + }.filter { it.value.isNotEmpty() } + + if (optionsMap.isNotEmpty()) { + filteredPatches.setOptions(optionsMap) + } + } + + patcher += filteredPatches + + // Execute patches + runBlocking { + patcher().collect { patchResult -> + val patchName = patchResult.patch.name ?: "Unknown" + patchResult.exception?.let { exception -> + val error = StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + writer.toString() + } + onProgress("FAILED: $patchName") + Logger.error("Patch failed: $patchName\n$error") + failedPatches.add(patchName to error) + } ?: run { + onProgress("Applied: $patchName") + Logger.info("Patch applied: $patchName") + appliedPatches.add(patchName) + } + } + } + + // Get patcher result + val patcherResult = patcher.get() + + onProgress("Rebuilding APK...") + val rebuiltApk = File(tempDir, "rebuilt.apk") + actualInputApk.copyTo(rebuiltApk, overwrite = true) + patcherResult.applyTo(rebuiltApk) + + onProgress("Signing APK...") + val keystorePath = File(tempDir, "morphe.keystore") + ApkUtils.signApk( + rebuiltApk, + tempOutputPath, + "Morphe", + ApkUtils.KeyStoreDetails( + keystorePath, + null, // password + "Morphe Key", + "" // entry password + ) + ) + + // Move to final location + outputFile.parentFile?.mkdirs() + tempOutputPath.copyTo(outputFile, overwrite = true) + + onProgress("Patching complete!") + Logger.info("Patched APK saved to: ${outputFile.absolutePath}") + + // Cleanup merged APK if created + mergedApkToCleanup?.delete() + } + + Result.success(PatchResult( + success = failedPatches.isEmpty(), + outputPath = outputFile.absolutePath, + appliedPatches = appliedPatches, + failedPatches = failedPatches.map { it.first } + )) + } catch (e: Exception) { + Logger.error("Patching failed", e) + Result.failure(e) + } finally { + // Cleanup temp directory + try { + tempDir.deleteRecursively() + } catch (e: Exception) { + Logger.warn("Failed to cleanup temp directory: ${e.message}") + } + } + } + + /** + * Convert library Patch to GUI Patch model. + */ + private fun LibraryPatch<*>.toGuiPatch(): Patch { + return Patch( + name = this.name ?: "Unknown", + description = this.description ?: "", + compatiblePackages = this.compatiblePackages?.map { (name, versions) -> + CompatiblePackage( + name = name, + versions = versions?.toList() ?: emptyList() + ) + } ?: emptyList(), + options = this.options.values.map { opt -> + PatchOption( + key = opt.key, + title = opt.title ?: opt.key, + description = opt.description ?: "", + type = mapKTypeToOptionType(opt.type), + default = opt.default?.toString(), + required = opt.required + ) + }, + isEnabled = this.use + ) + } + + /** + * Map Kotlin KType to GUI PatchOptionType. + */ + private fun mapKTypeToOptionType(kType: KType): PatchOptionType { + val typeName = kType.toString() + return when { + typeName.contains("Boolean") -> PatchOptionType.BOOLEAN + typeName.contains("Int") -> PatchOptionType.INT + typeName.contains("Long") -> PatchOptionType.LONG + typeName.contains("Float") || typeName.contains("Double") -> PatchOptionType.FLOAT + typeName.contains("List") || typeName.contains("Array") || typeName.contains("Set") -> PatchOptionType.LIST + else -> PatchOptionType.STRING + } + } +} + +/** + * Result of a patching operation. + */ +data class PatchResult( + val success: Boolean, + val outputPath: String, + val appliedPatches: List, + val failedPatches: List +) diff --git a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt new file mode 100644 index 0000000..a9802f1 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt @@ -0,0 +1,68 @@ +package app.morphe.gui.util + +import app.morphe.gui.data.model.Patch +import app.morphe.gui.data.model.SupportedApp + +/** + * Extracts supported apps from parsed patch data. + * This allows the app to dynamically determine which apps are supported + * based on the .mpp file contents rather than hardcoding. + */ +object SupportedAppExtractor { + + /** + * Extract all supported apps from a list of patches. + * Groups patches by package name and collects all supported versions. + */ + fun extractSupportedApps(patches: List): List { + // Collect all package names and their versions from all patches + val packageVersionsMap = mutableMapOf>() + + for (patch in patches) { + for (compatiblePackage in patch.compatiblePackages) { + val packageName = compatiblePackage.name + val versions = compatiblePackage.versions + + if (packageName.isNotBlank()) { + val existingVersions = packageVersionsMap.getOrPut(packageName) { mutableSetOf() } + existingVersions.addAll(versions) + } + } + } + + // Convert to SupportedApp list + return packageVersionsMap.map { (packageName, versions) -> + val versionList = versions.toList().sortedDescending() + SupportedApp( + packageName = packageName, + displayName = SupportedApp.getDisplayName(packageName), + supportedVersions = versionList, + recommendedVersion = SupportedApp.getRecommendedVersion(versionList), + apkMirrorUrl = SupportedApp.getApkMirrorUrl(packageName) + ) + }.sortedBy { it.displayName } + } + + /** + * Get supported app by package name. + */ + fun getSupportedApp(patches: List, packageName: String): SupportedApp? { + return extractSupportedApps(patches).find { it.packageName == packageName } + } + + /** + * Check if a package is supported by the patches. + */ + fun isPackageSupported(patches: List, packageName: String): Boolean { + return patches.any { patch -> + patch.compatiblePackages.any { it.name == packageName } + } + } + + /** + * Get recommended version for a package from patches. + */ + fun getRecommendedVersion(patches: List, packageName: String): String? { + return getSupportedApp(patches, packageName)?.recommendedVersion + } +} diff --git a/src/main/resources/morphe_logo.icns b/src/main/resources/morphe_logo.icns new file mode 100644 index 0000000000000000000000000000000000000000..9cbef394c7255367a1a37f0120cc5f12e84e31eb GIT binary patch literal 230839 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 Date: Fri, 6 Feb 2026 19:30:00 +0530 Subject: [PATCH 06/49] Removed hush hush svgs --- src/main/composeResources/drawable/reddit.svg | 10 - .../composeResources/drawable/youtube.svg | 1 - .../drawable/youtube_music.svg | 1 - src/main/kotlin/app/morphe/gui/GuiMain.kt | 17 ++ .../morphe/gui/ui/screens/home/HomeScreen.kt | 228 +++++++----------- .../ui/screens/home/components/ApkInfoCard.kt | 13 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 48 +--- 7 files changed, 123 insertions(+), 195 deletions(-) delete mode 100644 src/main/composeResources/drawable/reddit.svg delete mode 100644 src/main/composeResources/drawable/youtube.svg delete mode 100644 src/main/composeResources/drawable/youtube_music.svg diff --git a/src/main/composeResources/drawable/reddit.svg b/src/main/composeResources/drawable/reddit.svg deleted file mode 100644 index 9976548..0000000 --- a/src/main/composeResources/drawable/reddit.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/src/main/composeResources/drawable/youtube.svg b/src/main/composeResources/drawable/youtube.svg deleted file mode 100644 index f125ec3..0000000 --- a/src/main/composeResources/drawable/youtube.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/main/composeResources/drawable/youtube_music.svg b/src/main/composeResources/drawable/youtube_music.svg deleted file mode 100644 index 2257e05..0000000 --- a/src/main/composeResources/drawable/youtube_music.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/main/kotlin/app/morphe/gui/GuiMain.kt b/src/main/kotlin/app/morphe/gui/GuiMain.kt index 7e3b77a..1891611 100644 --- a/src/main/kotlin/app/morphe/gui/GuiMain.kt +++ b/src/main/kotlin/app/morphe/gui/GuiMain.kt @@ -34,6 +34,23 @@ fun launchGui(args: Array) = application { val appIcon = remember { loadAppIcon() } + // Set macOS dock icon + remember { + try { + if (java.awt.Taskbar.isTaskbarSupported()) { + val stream = Thread.currentThread().contextClassLoader + .getResourceAsStream("morphe_logo.png") + ?: ClassLoader.getSystemResourceAsStream("morphe_logo.png") + if (stream != null) { + java.awt.Taskbar.getTaskbar().iconImage = + javax.imageio.ImageIO.read(stream) + } + } + } catch (_: Exception) { + // Taskbar not supported or icon loading failed + } + } + Window( onCloseRequest = ::exitApplication, title = "Morphe", diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 5844a51..6a19f6b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -23,9 +23,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.morphe.morphe_cli.generated.resources.Res import app.morphe.morphe_cli.generated.resources.morphe -import app.morphe.morphe_cli.generated.resources.reddit -import app.morphe.morphe_cli.generated.resources.youtube -import app.morphe.morphe_cli.generated.resources.youtube_music import org.jetbrains.compose.resources.painterResource import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel @@ -119,121 +116,111 @@ fun HomeScreenContent( val scrollState = rememberScrollState() - // Estimate content heights to calculate flexible spacer - val brandingHeight = if (isCompact) 48.dp else 60.dp - val topSpacing = if (isSmall) 24.dp else 48.dp // top spacer + after branding - val middleContentHeight = if (uiState.apkInfo != null) { - // ApkInfoCard (~250dp) + buttons (~72dp) + spacer - if (isCompact) 340.dp else 380.dp - } else { - // Drop prompt section - if (isCompact) 160.dp else 200.dp - } - val supportedAppsHeight = if (isCompact) 220.dp else 280.dp - val bottomSpacing = if (isSmall) 24.dp else 40.dp // spacers around supported apps - - val totalFixedHeight = brandingHeight + topSpacing + middleContentHeight + supportedAppsHeight + bottomSpacing + (padding * 2) - - // Extra space to push supported apps to bottom on large screens - val extraSpace = (maxHeight - totalFixedHeight).coerceAtLeast(0.dp) - Box(modifier = Modifier.fillMaxSize()) { - // Always scrollable - but on large screens extraSpace fills the gap + // SpaceBetween + fillMaxSize pushes supported apps to the bottom + // when there's room; verticalScroll kicks in when content overflows. Column( modifier = Modifier .fillMaxSize() .verticalScroll(scrollState) .padding(padding), + verticalArrangement = Arrangement.SpaceBetween, horizontalAlignment = Alignment.CenterHorizontally ) { - Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) - BrandingSection(isCompact = isCompact) - - // Patches version selector card - right under logo - if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { - Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) - PatchesVersionCard( - patchesVersion = uiState.patchesVersion!!, - isLatest = uiState.isUsingLatestPatches, - onChangePatchesClick = { - // Navigate to patches version selection screen - // Pass empty apk info since user hasn't selected an APK yet - navigator.push(PatchesScreen( - apkPath = uiState.apkInfo?.filePath ?: "", - apkName = uiState.apkInfo?.appName ?: "Select APK first" - )) - }, - isCompact = isCompact, - modifier = Modifier - .widthIn(max = 400.dp) - .padding(horizontal = if (isCompact) 8.dp else 16.dp) - ) - } else if (uiState.isLoadingPatches) { - Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(14.dp), - strokeWidth = 2.dp, - color = MorpheColors.Blue - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Loading patches...", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + // Top group: branding + patches version + middle content + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) + BrandingSection(isCompact = isCompact) + + // Patches version selector card - right under logo + if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + PatchesVersionCard( + patchesVersion = uiState.patchesVersion!!, + isLatest = uiState.isUsingLatestPatches, + onChangePatchesClick = { + // Navigate to patches version selection screen + // Pass empty apk info since user hasn't selected an APK yet + navigator.push(PatchesScreen( + apkPath = uiState.apkInfo?.filePath ?: "", + apkName = uiState.apkInfo?.appName ?: "Select APK first" + )) + }, + isCompact = isCompact, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) ) + } else if (uiState.isLoadingPatches) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Loading patches...", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - } - Spacer(modifier = Modifier.height(if (isSmall) 16.dp else 32.dp)) + Spacer(modifier = Modifier.height(if (isSmall) 16.dp else 32.dp)) - MiddleContent( - uiState = uiState, - isCompact = isCompact, - patchesLoaded = !uiState.isLoadingPatches && viewModel.getCachedPatchesFile() != null, - onClearClick = { viewModel.clearSelection() }, - onChangeClick = { - openFilePicker()?.let { file -> - viewModel.onFileSelected(file) - } - }, - onContinueClick = { - val patchesFile = viewModel.getCachedPatchesFile() - if (patchesFile == null) { - // Patches not ready yet - return@MiddleContent - } + MiddleContent( + uiState = uiState, + isCompact = isCompact, + patchesLoaded = !uiState.isLoadingPatches && viewModel.getCachedPatchesFile() != null, + onClearClick = { viewModel.clearSelection() }, + onChangeClick = { + openFilePicker()?.let { file -> + viewModel.onFileSelected(file) + } + }, + onContinueClick = { + val patchesFile = viewModel.getCachedPatchesFile() + if (patchesFile == null) { + // Patches not ready yet + return@MiddleContent + } - val versionStatus = uiState.apkInfo?.versionStatus - if (versionStatus != null && versionStatus != VersionStatus.EXACT_MATCH && versionStatus != VersionStatus.UNKNOWN) { - showVersionWarningDialog = true - } else { - uiState.apkInfo?.let { info -> - navigator.push(PatchSelectionScreen( - apkPath = info.filePath, - apkName = info.appName, - patchesFilePath = patchesFile.absolutePath - )) + val versionStatus = uiState.apkInfo?.versionStatus + if (versionStatus != null && versionStatus != VersionStatus.EXACT_MATCH && versionStatus != VersionStatus.UNKNOWN) { + showVersionWarningDialog = true + } else { + uiState.apkInfo?.let { info -> + navigator.push(PatchSelectionScreen( + apkPath = info.filePath, + apkName = info.appName, + patchesFilePath = patchesFile.absolutePath + )) + } } } - } - ) - - // Flexible spacer - expands on large screens, minimal on small screens - Spacer(modifier = Modifier.height(extraSpace + if (isSmall) 16.dp else 24.dp)) + ) + } - SupportedAppsSection( - isCompact = isCompact, - maxWidth = this@BoxWithConstraints.maxWidth, - isLoading = uiState.isLoadingPatches, - supportedApps = uiState.supportedApps, - loadError = uiState.patchLoadError, - onRetry = { viewModel.retryLoadPatches() } - ) - Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) + // Bottom group: supported apps section + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(top = if (isSmall) 16.dp else 24.dp) + ) { + SupportedAppsSection( + isCompact = isCompact, + maxWidth = this@BoxWithConstraints.maxWidth, + isLoading = uiState.isLoadingPatches, + supportedApps = uiState.supportedApps, + loadError = uiState.patchLoadError, + onRetry = { viewModel.retryLoadPatches() } + ) + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) + } } // Settings button in top-right corner @@ -787,16 +774,6 @@ private fun SupportedAppCardDynamic( var showAllVersions by remember { mutableStateOf(false) } val cardPadding = if (isCompact) 12.dp else 16.dp - val iconSize = if (isCompact) 48.dp else 56.dp - val iconInnerSize = if (isCompact) 32.dp else 40.dp - - // Get icon resource based on package name - val iconRes = when (supportedApp.packageName) { - AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube - AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music - AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit - else -> null - } // Get APKMirror URL from AppConstants (still hardcoded) val apkMirrorUrl = when (supportedApp.packageName) { @@ -819,33 +796,6 @@ private fun SupportedAppCardDynamic( .padding(cardPadding), horizontalAlignment = Alignment.CenterHorizontally ) { - // App icon - Box( - modifier = Modifier - .size(iconSize) - .clip(RoundedCornerShape(if (isCompact) 10.dp else 12.dp)) - .background(Color.White), - contentAlignment = Alignment.Center - ) { - if (iconRes != null) { - Image( - painter = painterResource(iconRes), - contentDescription = "${supportedApp.displayName} icon", - modifier = Modifier.size(iconInnerSize) - ) - } else { - // Fallback: show first letter of app name - Text( - text = supportedApp.displayName.first().toString(), - fontSize = if (isCompact) 20.sp else 24.sp, - fontWeight = FontWeight.Bold, - color = MorpheColors.Blue - ) - } - } - - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) - // App name Text( text = supportedApp.displayName, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index cf4202e..f4b0365 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -19,9 +19,6 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.morphe.morphe_cli.generated.resources.Res -import app.morphe.morphe_cli.generated.resources.reddit -import app.morphe.morphe_cli.generated.resources.youtube -import app.morphe.morphe_cli.generated.resources.youtube_music import org.jetbrains.compose.resources.painterResource import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.ui.screens.home.ApkInfo @@ -59,11 +56,11 @@ fun ApkInfoCard( ) { // App icon - determine from appType or packageName val iconRes = when { - apkInfo.appType == AppType.YOUTUBE -> Res.drawable.youtube - apkInfo.appType == AppType.YOUTUBE_MUSIC -> Res.drawable.youtube_music - apkInfo.packageName == AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube - apkInfo.packageName == AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music - apkInfo.packageName == AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit + apkInfo.appType == AppType.YOUTUBE -> null + apkInfo.appType == AppType.YOUTUBE_MUSIC -> null + apkInfo.packageName == AppConstants.YouTube.PACKAGE_NAME -> null + apkInfo.packageName == AppConstants.YouTubeMusic.PACKAGE_NAME -> null + apkInfo.packageName == AppConstants.Reddit.PACKAGE_NAME -> null else -> null } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 4b5e298..bec551a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -31,9 +31,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen import app.morphe.morphe_cli.generated.resources.Res -import app.morphe.morphe_cli.generated.resources.reddit -import app.morphe.morphe_cli.generated.resources.youtube -import app.morphe.morphe_cli.generated.resources.youtube_music import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository @@ -361,18 +358,18 @@ private fun ReadyContent( .background(Color.White), contentAlignment = Alignment.Center ) { - Image( - painter = painterResource( - when (apkInfo.packageName) { - AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube - AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music - AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit - else -> Res.drawable.youtube // Fallback - } - ), - contentDescription = "${apkInfo.displayName} icon", - modifier = Modifier.size(36.dp) - ) +// Image( +// painter = painterResource( +// when (apkInfo.packageName) { +// AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube +// AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music +// AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit +// else -> Res.drawable.youtube // Fallback +// } +// ), +// contentDescription = "${apkInfo.displayName} icon", +// modifier = Modifier.size(36.dp) +// ) } Spacer(modifier = Modifier.width(16.dp)) @@ -711,27 +708,6 @@ private fun SupportedAppsRow( .padding(12.dp), verticalAlignment = Alignment.CenterVertically ) { - Box( - modifier = Modifier - .size(24.dp) - .clip(RoundedCornerShape(4.dp)) - .background(Color.White), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource( - when (app.packageName) { - AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube - AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music - AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit - else -> Res.drawable.youtube // Fallback - } - ), - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - } - Spacer(modifier = Modifier.width(8.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = app.displayName, From 3c9c11a1f660745161c626f3e8723840cfd684d9 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:29:05 +0530 Subject: [PATCH 07/49] Simplified version fixes + .apkm support fixed Fixed a bunch of issues with the simplified version. It should behave better now. --- .../morphe/gui/data/constants/AppConstants.kt | 10 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 4 +- .../gui/ui/screens/home/HomeViewModel.kt | 21 +- .../screens/patches/PatchSelectionScreen.kt | 40 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 380 +++++++++++------- .../ui/screens/quick/QuickPatchViewModel.kt | 81 ++-- .../kotlin/app/morphe/gui/util/FileUtils.kt | 28 +- 7 files changed, 383 insertions(+), 181 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt index 95163f2..8a42ca9 100644 --- a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt +++ b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt @@ -124,7 +124,6 @@ object AppConstants { // "Premium heading" to "Changes the YouTube logo/heading appearance", // "Navigation buttons" to "Modifies bottom navigation bar layout", // "Spoof client" to "May cause playback issues on some devices", -// "Change start page" to "Modifies the default landing page", // "Disable auto captions" to "Some users rely on auto-generated captions" ) @@ -136,6 +135,14 @@ object AppConstants { // "Spoof client" to "May cause playback issues on some devices" ) + /** + * Patches commonly disabled for Reddit. + */ + val REDDIT_COMMONLY_DISABLED: List> = listOf( + "Change package name" to "Doesn't work for reddit", + "Spoof signature" to "May cause issues on some devices" + ) + /** * Get commonly disabled patches for a package. */ @@ -143,6 +150,7 @@ object AppConstants { return when (packageName) { YouTube.PACKAGE_NAME -> YOUTUBE_COMMONLY_DISABLED YouTubeMusic.PACKAGE_NAME -> YOUTUBE_MUSIC_COMMONLY_DISABLED + Reddit.PACKAGE_NAME -> REDDIT_COMMONLY_DISABLED else -> emptyList() } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 6a19f6b..b91bbc6 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -556,7 +556,7 @@ private fun DropPromptSection( Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) Text( - text = "Supported: .apk files from APKMirror", + text = "Supported: .apk and .apkm files from APKMirror", fontSize = if (isCompact) 11.sp else 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) @@ -1100,7 +1100,7 @@ private fun DragOverlay() { private fun openFilePicker(): File? { val fileDialog = FileDialog(null as Frame?, "Select APK File", FileDialog.LOAD).apply { isMultipleMode = false - setFilenameFilter { _, name -> name.lowercase().endsWith(".apk") } + setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") } } isVisible = true } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 1a622a8..d403f6d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -218,7 +218,7 @@ class HomeViewModel( onFileSelected(apkFile) } else { _uiState.value = _uiState.value.copy( - error = "No valid APK file found. Please drop an .apk file.", + error = "Please drop a valid .apk or .apkm file", isReady = false ) } @@ -255,7 +255,7 @@ class HomeViewModel( } if (!FileUtils.isApkFile(file)) { - return ApkValidationResult(false, errorMessage = "File must have .apk extension") + return ApkValidationResult(false, errorMessage = "File must have .apk or .apkm extension") } if (file.length() < 1024) { @@ -277,8 +277,19 @@ class HomeViewModel( * This works with APKs from any source, not just APKMirror. */ private fun parseApkManifest(file: File): ApkInfo? { + // For .apkm files, extract base.apk first + val isApkm = file.extension.equals("apkm", ignoreCase = true) + val apkToParse = if (isApkm) { + FileUtils.extractBaseApkFromApkm(file) ?: run { + Logger.error("Failed to extract base.apk from APKM: ${file.name}") + return null + } + } else { + file + } + return try { - ApkFile(file).use { apk -> + ApkFile(apkToParse).use { apk -> val meta = apk.apkMeta val packageName = meta.packageName @@ -321,7 +332,7 @@ class HomeViewModel( } // Get supported architectures from native libraries in the APK - val architectures = extractArchitectures(file) + val architectures = extractArchitectures(apkToParse) // Verify checksum (still uses AppConstants for now) val checksumStatus = verifyChecksum(file, packageName, versionName, architectures, suggestedVersion) @@ -347,6 +358,8 @@ class HomeViewModel( } catch (e: Exception) { Logger.error("Failed to parse APK manifest", e) null + } finally { + if (isApkm) apkToParse.delete() } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 0ccab99..6a3b7fe 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -429,14 +429,26 @@ private fun PatchListItem( } } - // Show options indicator if patch has options + // Show options if patch has any if (patch.options.isNotEmpty()) { Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "${patch.options.size} option${if (patch.options.size > 1) "s" else ""} available", - fontSize = 11.sp, - color = MorpheColors.Teal - ) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + patch.options.forEach { option -> + Surface( + color = MorpheColors.Teal.copy(alpha = 0.1f), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = option.title.ifBlank { option.key }, + fontSize = 10.sp, + color = MorpheColors.Teal, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } } } } @@ -568,9 +580,11 @@ private fun CommandPreview( modifier: Modifier = Modifier ) { val terminalBackground = Color(0xFF1E1E1E) - val terminalGreen = Color(0xFF4EC9B0) +// val terminalGreen = Color(0xFF4EC9B0) + val terminalGreen = Color(0xFF6A9955) val terminalText = Color(0xFFD4D4D4) val terminalDim = Color(0xFF6A9955) +// val terminalDim = Color(0xFF4EC9B0) var showCopied by remember { mutableStateOf(false) } @@ -613,8 +627,8 @@ private fun CommandPreview( ) Text( text = "Command Preview", - fontSize = 11.sp, - fontWeight = FontWeight.Medium, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, color = terminalGreen ) Icon( @@ -652,7 +666,8 @@ private fun CommandPreview( ) Text( text = if (showCopied) "Copied!" else "Copy", - fontSize = 10.sp, + fontSize = 12.sp, + fontWeight = FontWeight.Bold, color = if (showCopied) terminalGreen else terminalDim ) } @@ -666,8 +681,9 @@ private fun CommandPreview( shape = RoundedCornerShape(4.dp) ) { Text( - text = if (cleanMode) "compact" else "expand", - fontSize = 10.sp, + text = if (cleanMode) "Compact" else "Expand", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, color = terminalDim, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index bec551a..0520ee0 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen import app.morphe.morphe_cli.generated.resources.Res +import app.morphe.morphe_cli.generated.resources.morphe import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository @@ -39,7 +40,10 @@ import org.jetbrains.compose.resources.painterResource import org.koin.compose.koinInject import app.morphe.gui.ui.components.SettingsButton import app.morphe.gui.ui.theme.MorpheColors +import androidx.compose.runtime.rememberCoroutineScope +import app.morphe.gui.util.AdbDevice import app.morphe.gui.util.AdbManager +import kotlinx.coroutines.launch import app.morphe.gui.util.ChecksumStatus import java.awt.Desktop import java.awt.datatransfer.DataFlavor @@ -97,7 +101,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { @Suppress("UNCHECKED_CAST") val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List - val apkFile = files.firstOrNull { it.name.endsWith(".apk", ignoreCase = true) } + val apkFile = files.firstOrNull { it.name.endsWith(".apk", ignoreCase = true) || it.name.endsWith(".apkm", ignoreCase = true) } if (apkFile != null) { viewModel.onFileSelected(apkFile) true @@ -123,132 +127,120 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { target = dragAndDropTarget ) ) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Header - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { + // Branding + Spacer(modifier = Modifier.height(8.dp)) + Image( + painter = painterResource(Res.drawable.morphe), + contentDescription = "Morphe Logo", + modifier = Modifier.height(48.dp) + ) Text( - text = "Morphe Quick Patch", - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + text = "Quick Patch", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // Mode indicator - Surface( - color = MorpheColors.Blue.copy(alpha = 0.1f), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = "QUICK MODE", - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - color = MorpheColors.Blue, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) - ) - } + Spacer(modifier = Modifier.height(16.dp)) - // Settings button - SettingsButton() - } - } + // Main content based on phase + // Remember last valid data for safe animation transitions + val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } + val lastOutputPath = remember(uiState.outputPath) { uiState.outputPath } - Spacer(modifier = Modifier.height(20.dp)) - - // Main content based on phase - // Remember last valid data for safe animation transitions - val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } - val lastOutputPath = remember(uiState.outputPath) { uiState.outputPath } - - AnimatedContent( - targetState = uiState.phase, - modifier = Modifier.weight(1f) - ) { phase -> - when (phase) { - QuickPatchPhase.IDLE, QuickPatchPhase.ANALYZING -> { - IdleContent( - isAnalyzing = phase == QuickPatchPhase.ANALYZING, - isDragHovering = uiState.isDragHovering, - error = uiState.error, - onFileSelected = { viewModel.onFileSelected(it) }, - onDragHover = { viewModel.setDragHover(it) }, - onClearError = { viewModel.clearError() } - ) - } - QuickPatchPhase.READY -> { - // Use current or last known apkInfo to prevent crash during animation - val apkInfo = uiState.apkInfo ?: lastApkInfo - if (apkInfo != null) { - ReadyContent( - apkInfo = apkInfo, + AnimatedContent( + targetState = uiState.phase, + modifier = Modifier.weight(1f) + ) { phase -> + when (phase) { + QuickPatchPhase.IDLE, QuickPatchPhase.ANALYZING -> { + IdleContent( + isAnalyzing = phase == QuickPatchPhase.ANALYZING, + isDragHovering = uiState.isDragHovering, error = uiState.error, - onPatch = { viewModel.startPatching() }, - onClear = { viewModel.reset() }, + onFileSelected = { viewModel.onFileSelected(it) }, + onDragHover = { viewModel.setDragHover(it) }, onClearError = { viewModel.clearError() } ) } - } - QuickPatchPhase.DOWNLOADING, QuickPatchPhase.PATCHING -> { - PatchingContent( - phase = phase, - progress = uiState.progress, - statusMessage = uiState.statusMessage, - onCancel = { viewModel.cancelPatching() } - ) - } - QuickPatchPhase.COMPLETED -> { - val apkInfo = uiState.apkInfo ?: lastApkInfo - val outputPath = uiState.outputPath ?: lastOutputPath - if (apkInfo != null && outputPath != null) { - CompletedContent( - outputPath = outputPath, - apkInfo = apkInfo, - onPatchAnother = { viewModel.reset() } + QuickPatchPhase.READY -> { + // Use current or last known apkInfo to prevent crash during animation + val apkInfo = uiState.apkInfo ?: lastApkInfo + if (apkInfo != null) { + ReadyContent( + apkInfo = apkInfo, + error = uiState.error, + onPatch = { viewModel.startPatching() }, + onClear = { viewModel.reset() }, + onClearError = { viewModel.clearError() } + ) + } + } + QuickPatchPhase.DOWNLOADING, QuickPatchPhase.PATCHING -> { + PatchingContent( + phase = phase, + progress = uiState.progress, + statusMessage = uiState.statusMessage, + onCancel = { viewModel.cancelPatching() } ) } + QuickPatchPhase.COMPLETED -> { + val apkInfo = uiState.apkInfo ?: lastApkInfo + val outputPath = uiState.outputPath ?: lastOutputPath + if (apkInfo != null && outputPath != null) { + CompletedContent( + outputPath = outputPath, + apkInfo = apkInfo, + onPatchAnother = { viewModel.reset() } + ) + } + } } } - } - // Bottom app cards (only show in IDLE phase) - if (uiState.phase == QuickPatchPhase.IDLE) { - Spacer(modifier = Modifier.height(16.dp)) - SupportedAppsRow( - supportedApps = uiState.supportedApps, - isLoading = uiState.isLoadingPatches, - patchesVersion = uiState.patchesVersion, - onOpenUrl = { url -> uriHandler.openUri(url) } - ) + // Bottom app cards (only show in IDLE phase) + if (uiState.phase == QuickPatchPhase.IDLE) { + Spacer(modifier = Modifier.height(16.dp)) + SupportedAppsRow( + supportedApps = uiState.supportedApps, + isLoading = uiState.isLoadingPatches, + loadError = uiState.patchLoadError, + patchesVersion = uiState.patchesVersion, + onOpenUrl = { url -> uriHandler.openUri(url) }, + onRetry = { viewModel.retryLoadPatches() } + ) + } } - } - // Error snackbar - uiState.error?.let { error -> - Snackbar( + // Settings button in top-right corner + SettingsButton( modifier = Modifier - .align(Alignment.BottomCenter) - .padding(16.dp), - action = { - TextButton(onClick = { viewModel.clearError() }) { - Text("Dismiss", color = MaterialTheme.colorScheme.inversePrimary) - } - }, - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer - ) { - Text(error) + .align(Alignment.TopEnd) + .padding(24.dp) + ) + + // Error snackbar + uiState.error?.let { error -> + Snackbar( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(16.dp), + action = { + TextButton(onClick = { viewModel.clearError() }) { + Text("Dismiss", color = MaterialTheme.colorScheme.inversePrimary) + } + }, + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) { + Text(error) + } } } } @@ -350,7 +342,7 @@ private fun ReadyContent( .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - // App icon + // App icon: first letter of display name Box( modifier = Modifier .size(48.dp) @@ -358,18 +350,12 @@ private fun ReadyContent( .background(Color.White), contentAlignment = Alignment.Center ) { -// Image( -// painter = painterResource( -// when (apkInfo.packageName) { -// AppConstants.YouTube.PACKAGE_NAME -> Res.drawable.youtube -// AppConstants.YouTubeMusic.PACKAGE_NAME -> Res.drawable.youtube_music -// AppConstants.Reddit.PACKAGE_NAME -> Res.drawable.reddit -// else -> Res.drawable.youtube // Fallback -// } -// ), -// contentDescription = "${apkInfo.displayName} icon", -// modifier = Modifier.size(36.dp) -// ) + Text( + text = apkInfo.displayName.first().toString(), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Blue + ) } Spacer(modifier = Modifier.width(16.dp)) @@ -536,11 +522,39 @@ private fun CompletedContent( onPatchAnother: () -> Unit ) { val outputFile = File(outputPath) + val scope = rememberCoroutineScope() val adbManager = remember { AdbManager() } var isAdbAvailable by remember { mutableStateOf(null) } + var connectedDevices by remember { mutableStateOf>(emptyList()) } + var selectedDevice by remember { mutableStateOf(null) } + var isInstalling by remember { mutableStateOf(false) } + var installError by remember { mutableStateOf(null) } + var installSuccess by remember { mutableStateOf(false) } + + fun refreshDevices() { + scope.launch { + val result = adbManager.getConnectedDevices() + result.fold( + onSuccess = { devices -> + connectedDevices = devices + val readyDevices = devices.filter { it.isReady } + if (readyDevices.size == 1) { + selectedDevice = readyDevices.first() + } + }, + onFailure = { + connectedDevices = emptyList() + selectedDevice = null + } + ) + } + } LaunchedEffect(Unit) { isAdbAvailable = adbManager.isAdbAvailable() + if (isAdbAvailable == true) { + refreshDevices() + } } Column( @@ -621,12 +635,90 @@ private fun CompletedContent( } if (isAdbAvailable == true) { - Spacer(modifier = Modifier.height(12.dp)) - Text( - text = "Connect your device via USB to install with ADB", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + Spacer(modifier = Modifier.height(16.dp)) + + val readyDevices = connectedDevices.filter { it.isReady } + + if (installSuccess) { + Surface( + color = MorpheColors.Teal.copy(alpha = 0.1f), + shape = RoundedCornerShape(8.dp) + ) { + Text( + text = "Installed successfully!", + fontSize = 13.sp, + color = MorpheColors.Teal, + fontWeight = FontWeight.Medium, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + } else if (isInstalling) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Installing...", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } else if (readyDevices.isNotEmpty()) { + val device = selectedDevice ?: readyDevices.first() + Button( + onClick = { + scope.launch { + isInstalling = true + installError = null + val result = adbManager.installApk( + apkPath = outputPath, + deviceId = device.id + ) + result.fold( + onSuccess = { installSuccess = true }, + onFailure = { installError = it.message } + ) + isInstalling = false + } + }, + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Teal), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.PhoneAndroid, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text("Install on ${device.displayName}") + } + } else { + Text( + text = "Connect your device via USB to install with ADB", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.height(4.dp)) + TextButton(onClick = { refreshDevices() }) { + Text("Refresh", fontSize = 12.sp) + } + } + + installError?.let { error -> + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = error, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center + ) + } } } } @@ -635,8 +727,10 @@ private fun CompletedContent( private fun SupportedAppsRow( supportedApps: List, isLoading: Boolean, + loadError: String? = null, patchesVersion: String?, - onOpenUrl: (String) -> Unit + onOpenUrl: (String) -> Unit, + onRetry: () -> Unit = {} ) { Column( modifier = Modifier.fillMaxWidth() @@ -681,13 +775,27 @@ private fun SupportedAppsRow( color = MaterialTheme.colorScheme.onSurfaceVariant ) } - } else if (supportedApps.isEmpty()) { - // No apps loaded - Text( - text = "Could not load supported apps", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + } else if (loadError != null || supportedApps.isEmpty()) { + // Error or no apps loaded + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = loadError ?: "Could not load supported apps", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Spacer(modifier = Modifier.width(8.dp)) + OutlinedButton( + onClick = onRetry, + shape = RoundedCornerShape(8.dp), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text("Retry", fontSize = 12.sp) + } + } } else { // Show supported apps dynamically Row( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index ed391d9..75a1b16 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.launch import net.dongliu.apk.parser.ApkFile import app.morphe.gui.util.ChecksumStatus import app.morphe.gui.util.ChecksumUtils +import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import app.morphe.gui.util.PatchService import app.morphe.gui.util.SupportedAppExtractor @@ -50,34 +51,25 @@ class QuickPatchViewModel( */ private fun loadPatchesAndSupportedApps() { screenModelScope.launch { - _uiState.value = _uiState.value.copy(isLoadingPatches = true) + _uiState.value = _uiState.value.copy(isLoadingPatches = true, patchLoadError = null) try { - // Check for saved version in config - val config = configRepository.loadConfig() - val savedVersion = config.lastPatchesVersion - // Fetch releases val releasesResult = patchRepository.fetchReleases() val releases = releasesResult.getOrNull() if (releases.isNullOrEmpty()) { Logger.warn("Quick mode: Could not fetch releases") - _uiState.value = _uiState.value.copy(isLoadingPatches = false) + _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "Could not fetch releases. Check your internet connection.") return@launch } - // Find release to use - val latestStable = releases.firstOrNull { !it.isDevRelease() } - val release = if (savedVersion != null) { - releases.find { it.tagName == savedVersion } ?: latestStable - } else { - latestStable - } + // Quick mode always uses the latest stable release + val release = releases.firstOrNull { !it.isDevRelease() } if (release == null) { Logger.warn("Quick mode: No suitable release found") - _uiState.value = _uiState.value.copy(isLoadingPatches = false) + _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "No suitable release found") return@launch } @@ -87,7 +79,7 @@ class QuickPatchViewModel( if (patchFile == null) { Logger.warn("Quick mode: Could not download patches") - _uiState.value = _uiState.value.copy(isLoadingPatches = false) + _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "Could not download patches") return@launch } @@ -99,7 +91,7 @@ class QuickPatchViewModel( if (patches.isNullOrEmpty()) { Logger.warn("Quick mode: Could not load patches: ${patchesResult.exceptionOrNull()?.message}") - _uiState.value = _uiState.value.copy(isLoadingPatches = false) + _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "Could not load patches") return@launch } @@ -114,15 +106,23 @@ class QuickPatchViewModel( _uiState.value = _uiState.value.copy( isLoadingPatches = false, supportedApps = supportedApps, - patchesVersion = release.tagName + patchesVersion = release.tagName, + patchLoadError = null ) } catch (e: Exception) { Logger.error("Quick mode: Failed to load patches", e) - _uiState.value = _uiState.value.copy(isLoadingPatches = false) + _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "Failed to load patches: ${e.message}") } } } + /** + * Retry loading patches after a failure. + */ + fun retryLoadPatches() { + loadPatchesAndSupportedApps() + } + /** * Handle file drop or selection. */ @@ -153,13 +153,24 @@ class QuickPatchViewModel( * Analyze the APK file using dynamic data from patches. */ private suspend fun analyzeApk(file: File): QuickApkInfo? { - if (!file.exists() || !file.name.endsWith(".apk", ignoreCase = true)) { - _uiState.value = _uiState.value.copy(error = "Please select a valid APK file") + if (!file.exists() || !(file.name.endsWith(".apk", ignoreCase = true) || file.name.endsWith(".apkm", ignoreCase = true))) { + _uiState.value = _uiState.value.copy(error = "Please drop a valid .apk or .apkm file") return null } + // For .apkm files, extract base.apk first + val isApkm = file.extension.equals("apkm", ignoreCase = true) + val apkToParse = if (isApkm) { + FileUtils.extractBaseApkFromApkm(file) ?: run { + _uiState.value = _uiState.value.copy(error = "Failed to extract base.apk from APKM bundle") + return null + } + } else { + file + } + return try { - ApkFile(file).use { apk -> + ApkFile(apkToParse).use { apk -> val meta = apk.apkMeta val packageName = meta.packageName val versionName = meta.versionName ?: "Unknown" @@ -221,6 +232,8 @@ class QuickPatchViewModel( Logger.error("Quick mode: Failed to analyze APK", e) _uiState.value = _uiState.value.copy(error = "Failed to read APK: ${e.message}") null + } finally { + if (isApkm) apkToParse.delete() } } @@ -311,13 +324,27 @@ class QuickPatchViewModel( val outputFileName = "$baseName-Morphe-${apkInfo.versionName}.apk" val outputPath = File(outputDir, outputFileName).absolutePath + // Auto-deselect commonly disabled patches for this app + val commonlyDisabled = AppConstants.PatchRecommendations.getCommonlyDisabled(apkInfo.packageName) + val disabledPatches = cachedPatches + .filter { patch -> + commonlyDisabled.any { (pattern, _) -> + patch.name.contains(pattern, ignoreCase = true) + } + } + .map { it.name } + + if (disabledPatches.isNotEmpty()) { + Logger.info("Quick mode: Auto-disabling patches: $disabledPatches") + } + // Use PatchService for direct library patching (no CLI subprocess) val patchResult = patchService.patch( patchesFilePath = patchFile.absolutePath, inputApkPath = apkFile.absolutePath, outputApkPath = outputPath, enabledPatches = emptyList(), // Empty = use defaults - disabledPatches = emptyList(), + disabledPatches = disabledPatches, options = emptyMap(), exclusiveMode = false, // Include all default patches onProgress = { message -> @@ -400,7 +427,12 @@ class QuickPatchViewModel( fun reset() { patchingJob?.cancel() patchingJob = null - _uiState.value = QuickPatchUiState() + _uiState.value = QuickPatchUiState( + // Preserve already-loaded patches data + isLoadingPatches = false, + supportedApps = cachedSupportedApps, + patchesVersion = _uiState.value.patchesVersion + ) } /** @@ -466,5 +498,6 @@ data class QuickPatchUiState( // Dynamic data from patches val isLoadingPatches: Boolean = true, val supportedApps: List = emptyList(), - val patchesVersion: String? = null + val patchesVersion: String? = null, + val patchLoadError: String? = null ) diff --git a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt index 3906045..245b589 100644 --- a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt +++ b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt @@ -3,6 +3,7 @@ package app.morphe.gui.util import java.io.File import java.nio.file.Path import java.nio.file.Paths +import java.util.zip.ZipFile /** * Platform-agnostic file utilities. @@ -141,9 +142,32 @@ object FileUtils { } /** - * Check if file is an APK. + * Check if file is an APK or APKM. */ fun isApkFile(file: File): Boolean { - return file.isFile && getExtension(file) == "apk" + val ext = getExtension(file) + return file.isFile && (ext == "apk" || ext == "apkm") + } + + /** + * Extract base.apk from an .apkm file to a temp directory. + * Returns the extracted base.apk file, or null if extraction fails. + * Caller is responsible for cleaning up the returned temp file. + */ + fun extractBaseApkFromApkm(apkmFile: File): File? { + return try { + ZipFile(apkmFile).use { zip -> + val baseEntry = zip.getEntry("base.apk") ?: return null + val tempFile = File(getTempDir(), "base-${System.currentTimeMillis()}.apk") + zip.getInputStream(baseEntry).use { input -> + tempFile.outputStream().use { output -> + input.copyTo(output) + } + } + tempFile + } + } catch (e: Exception) { + null + } } } From 5199e5ddded3293b1dc825942d7d504dcd439e89 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Fri, 6 Feb 2026 21:59:15 +0530 Subject: [PATCH 08/49] Minor UI updates Fixed minor UI bugs and URL pointing issues --- .../drawable/{morphe.svg => morphe_dark.svg} | 0 .../composeResources/drawable/morphe_light.svg | 15 +++++++++++++++ .../app/morphe/gui/data/constants/AppConstants.kt | 4 ++-- .../app/morphe/gui/data/model/SupportedApp.kt | 9 ++++++--- .../app/morphe/gui/ui/screens/home/HomeScreen.kt | 14 ++++++++++++-- .../gui/ui/screens/quick/QuickPatchScreen.kt | 14 ++++++++++++-- 6 files changed, 47 insertions(+), 9 deletions(-) rename src/main/composeResources/drawable/{morphe.svg => morphe_dark.svg} (100%) create mode 100644 src/main/composeResources/drawable/morphe_light.svg diff --git a/src/main/composeResources/drawable/morphe.svg b/src/main/composeResources/drawable/morphe_dark.svg similarity index 100% rename from src/main/composeResources/drawable/morphe.svg rename to src/main/composeResources/drawable/morphe_dark.svg diff --git a/src/main/composeResources/drawable/morphe_light.svg b/src/main/composeResources/drawable/morphe_light.svg new file mode 100644 index 0000000..b34f20c --- /dev/null +++ b/src/main/composeResources/drawable/morphe_light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt index 8a42ca9..3dcd174 100644 --- a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt +++ b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt @@ -8,7 +8,7 @@ object AppConstants { // ==================== APP INFO ==================== const val APP_NAME = "Morphe GUI" - const val APP_VERSION = "1.4.0" // Keep in sync with build.gradle.kts + const val APP_VERSION = "1.4.0" // Keep in sync with the release version numbers // ==================== YOUTUBE ==================== object YouTube { @@ -41,7 +41,7 @@ object AppConstants { const val DISPLAY_NAME = "Reddit" const val PACKAGE_NAME = "com.reddit.frontpage" // APKMirror URL - to be updated with specific version when known - const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/redditinc/reddit/" + const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/redditinc/reddit/reddit-2026-03-0-release/" } /** diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index d32745a..5bc573b 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -1,5 +1,7 @@ package app.morphe.gui.data.model +import app.morphe.gui.data.constants.AppConstants + /** * Represents a supported app extracted dynamically from patch metadata. * This is populated by parsing the .mpp file's compatible packages. @@ -30,12 +32,13 @@ data class SupportedApp( /** * Get APK Mirror URL for a package name. + * Uses the same version-specific URLs from AppConstants. */ fun getApkMirrorUrl(packageName: String): String? { return when (packageName) { - "com.google.android.youtube" -> "https://www.apkmirror.com/apk/google-inc/youtube/" - "com.google.android.apps.youtube.music" -> "https://www.apkmirror.com/apk/google-inc/youtube-music/" - "com.reddit.frontpage" -> "https://www.apkmirror.com/apk/redditinc/reddit/" + AppConstants.YouTube.PACKAGE_NAME -> AppConstants.YouTube.APK_MIRROR_URL + AppConstants.YouTubeMusic.PACKAGE_NAME -> AppConstants.YouTubeMusic.APK_MIRROR_URL + AppConstants.Reddit.PACKAGE_NAME -> AppConstants.Reddit.APK_MIRROR_URL else -> null } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index b91bbc6..c4b98bd 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -21,8 +21,12 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.compose.foundation.isSystemInDarkTheme import app.morphe.morphe_cli.generated.resources.Res -import app.morphe.morphe_cli.generated.resources.morphe +import app.morphe.morphe_cli.generated.resources.morphe_dark +import app.morphe.morphe_cli.generated.resources.morphe_light +import app.morphe.gui.ui.theme.LocalThemeState +import app.morphe.gui.ui.theme.ThemePreference import org.jetbrains.compose.resources.painterResource import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel @@ -500,8 +504,14 @@ private fun VersionWarningDialog( @Composable private fun BrandingSection(isCompact: Boolean = false) { + val themeState = LocalThemeState.current + val isDark = when (themeState.current) { + ThemePreference.DARK -> true + ThemePreference.LIGHT -> false + ThemePreference.SYSTEM -> isSystemInDarkTheme() + } Image( - painter = painterResource(Res.drawable.morphe), + painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), contentDescription = "Morphe Logo", modifier = Modifier.height(if (isCompact) 48.dp else 60.dp) ) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 0520ee0..e38cd2d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -30,8 +30,12 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen +import androidx.compose.foundation.isSystemInDarkTheme import app.morphe.morphe_cli.generated.resources.Res -import app.morphe.morphe_cli.generated.resources.morphe +import app.morphe.morphe_cli.generated.resources.morphe_dark +import app.morphe.morphe_cli.generated.resources.morphe_light +import app.morphe.gui.ui.theme.LocalThemeState +import app.morphe.gui.ui.theme.ThemePreference import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository @@ -136,8 +140,14 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ) { // Branding Spacer(modifier = Modifier.height(8.dp)) + val themeState = LocalThemeState.current + val isDark = when (themeState.current) { + ThemePreference.DARK -> true + ThemePreference.LIGHT -> false + ThemePreference.SYSTEM -> isSystemInDarkTheme() + } Image( - painter = painterResource(Res.drawable.morphe), + painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), contentDescription = "Morphe Logo", modifier = Modifier.height(48.dp) ) From 73151ae44b495c1574ee0d9ef3c156e73283f2b6 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 7 Feb 2026 11:45:31 +0530 Subject: [PATCH 09/49] apkmirror link builder Builds an apkmirror link to link the app . Might want to change the implementation later --- .../morphe/gui/data/constants/AppConstants.kt | 7 +-- .../app/morphe/gui/data/model/SupportedApp.kt | 16 ++--- .../morphe/gui/ui/screens/home/HomeScreen.kt | 62 +++++++++---------- .../gui/ui/screens/home/HomeViewModel.kt | 9 +-- .../morphe/gui/util/DownloadUrlResolver.kt | 29 +++++++++ .../morphe/gui/util/SupportedAppExtractor.kt | 5 +- 6 files changed, 73 insertions(+), 55 deletions(-) create mode 100644 src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt diff --git a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt index 3dcd174..3ca5a23 100644 --- a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt +++ b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt @@ -10,12 +10,14 @@ object AppConstants { const val APP_NAME = "Morphe GUI" const val APP_VERSION = "1.4.0" // Keep in sync with the release version numbers + // ==================== API ==================== + const val MORPHE_API_URL = "https://api.morphe.software" + // ==================== YOUTUBE ==================== object YouTube { const val DISPLAY_NAME = "YouTube" const val PACKAGE_NAME = "com.google.android.youtube" const val SUGGESTED_VERSION = "20.40.45" - const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/google-inc/youtube/youtube-20-40-45-release/youtube-20-40-45-2-android-apk-download/" // SHA-256 checksum from APKMirror (leave null if not verified) // You can find this on the APKMirror download page under "File SHA-256" @@ -27,7 +29,6 @@ object AppConstants { const val DISPLAY_NAME = "YouTube Music" const val PACKAGE_NAME = "com.google.android.apps.youtube.music" const val SUGGESTED_VERSION = "8.40.54" - const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/google-inc/youtube-music/youtube-music-8-40-54-release/" val SHA256_CHECKSUMS: Map = mapOf( "arm64-v8a" to "d5b44919a5cd5648b01e392115fe68b9569b1c7847f3cdf65b1ace1302d005d2", "armeabi-v7a" to "6f5181e8aaa2595af6c421b86ffffcc1c7a4e97968d7be89d04b46776392eaec", @@ -40,8 +41,6 @@ object AppConstants { object Reddit { const val DISPLAY_NAME = "Reddit" const val PACKAGE_NAME = "com.reddit.frontpage" - // APKMirror URL - to be updated with specific version when known - const val APK_MIRROR_URL = "https://www.apkmirror.com/apk/redditinc/reddit/reddit-2026-03-0-release/" } /** diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index 5bc573b..69adf66 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -1,6 +1,6 @@ package app.morphe.gui.data.model -import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.util.DownloadUrlResolver /** * Represents a supported app extracted dynamically from patch metadata. @@ -31,16 +31,12 @@ data class SupportedApp( } /** - * Get APK Mirror URL for a package name. - * Uses the same version-specific URLs from AppConstants. + * Get download URL for a package name and version. + * Returns a direct APKMirror search URL for the app + version. */ - fun getApkMirrorUrl(packageName: String): String? { - return when (packageName) { - AppConstants.YouTube.PACKAGE_NAME -> AppConstants.YouTube.APK_MIRROR_URL - AppConstants.YouTubeMusic.PACKAGE_NAME -> AppConstants.YouTubeMusic.APK_MIRROR_URL - AppConstants.Reddit.PACKAGE_NAME -> AppConstants.Reddit.APK_MIRROR_URL - else -> null - } + fun getDownloadUrl(packageName: String, version: String?): String? { + if (version == null) return null + return DownloadUrlResolver.buildUrl(packageName, version) } /** diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index c4b98bd..07c67d2 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -32,7 +32,6 @@ import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.ui.components.SettingsButton import app.morphe.gui.ui.screens.home.components.ApkInfoCard @@ -785,13 +784,7 @@ private fun SupportedAppCardDynamic( val cardPadding = if (isCompact) 12.dp else 16.dp - // Get APKMirror URL from AppConstants (still hardcoded) - val apkMirrorUrl = when (supportedApp.packageName) { - AppConstants.YouTube.PACKAGE_NAME -> AppConstants.YouTube.APK_MIRROR_URL - AppConstants.YouTubeMusic.PACKAGE_NAME -> AppConstants.YouTubeMusic.APK_MIRROR_URL - AppConstants.Reddit.PACKAGE_NAME -> AppConstants.Reddit.APK_MIRROR_URL - else -> null - } + val apkMirrorUrl = supportedApp.apkMirrorUrl Card( modifier = modifier, @@ -1030,32 +1023,35 @@ private fun SupportedAppCard( Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) // Download from APKMirror button - OutlinedButton( - onClick = { - try { - java.awt.Desktop.getDesktop().browse(java.net.URI(appType.apkMirrorUrl)) - } catch (e: Exception) { - // Ignore errors - } - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp), - contentPadding = PaddingValues( - horizontal = if (isCompact) 8.dp else 12.dp, - vertical = if (isCompact) 6.dp else 8.dp - ), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MorpheColors.Blue - ) - ) { - Text( - text = if (isCompact) "APKMirror" else "Get from APKMirror", - fontSize = if (isCompact) 11.sp else 12.sp, - fontWeight = FontWeight.Medium - ) - } + val downloadUrl = SupportedApp.getDownloadUrl(appType.packageName, appType.suggestedVersion) + if (downloadUrl != null) { + OutlinedButton( + onClick = { + try { + java.awt.Desktop.getDesktop().browse(java.net.URI(downloadUrl)) + } catch (e: Exception) { + // Ignore errors + } + }, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp), + contentPadding = PaddingValues( + horizontal = if (isCompact) 8.dp else 12.dp, + vertical = if (isCompact) 6.dp else 8.dp + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MorpheColors.Blue + ) + ) { + Text( + text = if (isCompact) "APKMirror" else "Get from APKMirror", + fontSize = if (isCompact) 11.sp else 12.sp, + fontWeight = FontWeight.Medium + ) + } - Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + } // Package name Text( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index d403f6d..7e33e9b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -485,20 +485,17 @@ data class HomeUiState( enum class AppType( val displayName: String, val packageName: String, - val suggestedVersion: String, - val apkMirrorUrl: String + val suggestedVersion: String ) { YOUTUBE( displayName = app.morphe.gui.data.constants.AppConstants.YouTube.DISPLAY_NAME, packageName = app.morphe.gui.data.constants.AppConstants.YouTube.PACKAGE_NAME, - suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTube.SUGGESTED_VERSION, - apkMirrorUrl = app.morphe.gui.data.constants.AppConstants.YouTube.APK_MIRROR_URL + suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTube.SUGGESTED_VERSION ), YOUTUBE_MUSIC( displayName = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.DISPLAY_NAME, packageName = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.PACKAGE_NAME, - suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.SUGGESTED_VERSION, - apkMirrorUrl = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.APK_MIRROR_URL + suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.SUGGESTED_VERSION ) } diff --git a/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt b/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt new file mode 100644 index 0000000..9a854e3 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt @@ -0,0 +1,29 @@ +package app.morphe.gui.util + +/** + * Builds direct APKMirror release page URLs from package name + version. + * Pattern: https://www.apkmirror.com/apk/{publisher}/{app}/{app}-{version}-release/ + */ +object DownloadUrlResolver { + + private data class ApkMirrorApp(val publisher: String, val name: String) + + private val PACKAGE_MAP = mapOf( + "com.google.android.youtube" to ApkMirrorApp("google-inc", "youtube"), + "com.google.android.apps.youtube.music" to ApkMirrorApp("google-inc", "youtube-music"), + "com.reddit.frontpage" to ApkMirrorApp("redditinc", "reddit") + ) + + fun buildUrl(packageName: String, version: String?): String { + if (version == null) return fallbackUrl(packageName) + + val app = PACKAGE_MAP[packageName] ?: return fallbackUrl(packageName) + val versionSlug = version.replace(".", "-") + + return "https://www.apkmirror.com/apk/${app.publisher}/${app.name}/${app.name}-$versionSlug-release/" + } + + private fun fallbackUrl(packageName: String): String { + return "${app.morphe.gui.data.constants.AppConstants.MORPHE_API_URL}/v2/web-search/$packageName" + } +} diff --git a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt index a9802f1..e7aa5ef 100644 --- a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt +++ b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt @@ -33,12 +33,13 @@ object SupportedAppExtractor { // Convert to SupportedApp list return packageVersionsMap.map { (packageName, versions) -> val versionList = versions.toList().sortedDescending() + val recommendedVersion = SupportedApp.getRecommendedVersion(versionList) SupportedApp( packageName = packageName, displayName = SupportedApp.getDisplayName(packageName), supportedVersions = versionList, - recommendedVersion = SupportedApp.getRecommendedVersion(versionList), - apkMirrorUrl = SupportedApp.getApkMirrorUrl(packageName) + recommendedVersion = recommendedVersion, + apkMirrorUrl = SupportedApp.getDownloadUrl(packageName, recommendedVersion) ) }.sortedBy { it.displayName } } From 67a8aab6be62c389b3e53f7ca72ba88f9ca339c5 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:16:02 +0100 Subject: [PATCH 10/49] fix build --- build.gradle.kts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 8af331b..bc728b6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -175,6 +175,14 @@ tasks { mergeServiceFiles() } + distTar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + + distZip { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + } + publish { dependsOn(shadowJar) } From d30d4a5feae2b9bfde92d02a1cde52f54f8a59e4 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Feb 2026 09:16:20 +0100 Subject: [PATCH 11/49] Add IDE launcher preset --- .run/CLI GUI.run.xml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .run/CLI GUI.run.xml diff --git a/.run/CLI GUI.run.xml b/.run/CLI GUI.run.xml new file mode 100644 index 0000000..048db85 --- /dev/null +++ b/.run/CLI GUI.run.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file From b6de86ba87b90807ced9cb81f53261f2aaa185da Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:03:08 +0100 Subject: [PATCH 12/49] Use web-search api --- .../app/morphe/gui/data/model/SupportedApp.kt | 4 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 4 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 12 ++- .../morphe/gui/util/DownloadUrlResolver.kt | 82 +++++++++++++++---- .../morphe/gui/util/SupportedAppExtractor.kt | 2 +- 5 files changed, 77 insertions(+), 27 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index 69adf66..a9d82c1 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -11,7 +11,7 @@ data class SupportedApp( val displayName: String, val supportedVersions: List, val recommendedVersion: String?, - val apkMirrorUrl: String? = null + val apkDownloadUrl: String? = null ) { companion object { /** @@ -36,7 +36,7 @@ data class SupportedApp( */ fun getDownloadUrl(packageName: String, version: String?): String? { if (version == null) return null - return DownloadUrlResolver.buildUrl(packageName, version) + return DownloadUrlResolver.getWebSearchDownloadLink(packageName, version) } /** diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 07c67d2..7dc4ed0 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -565,7 +565,7 @@ private fun DropPromptSection( Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) Text( - text = "Supported: .apk and .apkm files from APKMirror", + text = "Supported: .apk and .apkm files", fontSize = if (isCompact) 11.sp else 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) @@ -784,7 +784,7 @@ private fun SupportedAppCardDynamic( val cardPadding = if (isCompact) 12.dp else 16.dp - val apkMirrorUrl = supportedApp.apkMirrorUrl + val apkMirrorUrl = supportedApp.apkDownloadUrl Card( modifier = modifier, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index e38cd2d..fc2df41 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -36,7 +36,6 @@ import app.morphe.morphe_cli.generated.resources.morphe_dark import app.morphe.morphe_cli.generated.resources.morphe_light import app.morphe.gui.ui.theme.LocalThemeState import app.morphe.gui.ui.theme.ThemePreference -import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository import app.morphe.gui.util.PatchService @@ -49,6 +48,7 @@ import app.morphe.gui.util.AdbDevice import app.morphe.gui.util.AdbManager import kotlinx.coroutines.launch import app.morphe.gui.util.ChecksumStatus +import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects import java.awt.Desktop import java.awt.datatransfer.DataFlavor import java.io.File @@ -222,7 +222,11 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { isLoading = uiState.isLoadingPatches, loadError = uiState.patchLoadError, patchesVersion = uiState.patchesVersion, - onOpenUrl = { url -> uriHandler.openUri(url) }, + onOpenUrl = { url -> + openUrlAndFollowRedirects(url) { urlResolved -> + uriHandler.openUri(urlResolved) + } + }, onRetry = { viewModel.retryLoadPatches() } ) } @@ -751,7 +755,7 @@ private fun SupportedAppsRow( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Get the APK from APKMirror:", + text = "Download original APK:", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -813,7 +817,7 @@ private fun SupportedAppsRow( horizontalArrangement = Arrangement.spacedBy(12.dp) ) { supportedApps.forEach { app -> - val url = app.apkMirrorUrl + val url = app.apkDownloadUrl if (url != null) { OutlinedCard( onClick = { onOpenUrl(url) }, diff --git a/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt b/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt index 9a854e3..97b7abf 100644 --- a/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt +++ b/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt @@ -1,29 +1,75 @@ package app.morphe.gui.util -/** - * Builds direct APKMirror release page URLs from package name + version. - * Pattern: https://www.apkmirror.com/apk/{publisher}/{app}/{app}-{version}-release/ - */ +import app.morphe.gui.data.constants.AppConstants.MORPHE_API_URL +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.net.HttpURLConnection +import java.net.SocketTimeoutException +import java.net.URL + object DownloadUrlResolver { - private data class ApkMirrorApp(val publisher: String, val name: String) + fun getWebSearchDownloadLink(packageName: String, version: String, architecture: String? = null): String { + val architectureString = architecture ?: "all" + return "$MORPHE_API_URL/v2/web-search/$packageName:$version:$architectureString" + } + + fun openUrlAndFollowRedirects(url: String, handleResolvedUrl: (String) -> Unit) { + CoroutineScope(Dispatchers.Main).launch { + val result = withContext(Dispatchers.IO) { + resolveRedirects(url) + } - private val PACKAGE_MAP = mapOf( - "com.google.android.youtube" to ApkMirrorApp("google-inc", "youtube"), - "com.google.android.apps.youtube.music" to ApkMirrorApp("google-inc", "youtube-music"), - "com.reddit.frontpage" to ApkMirrorApp("redditinc", "reddit") - ) + handleResolvedUrl(result) + } + } - fun buildUrl(packageName: String, version: String?): String { - if (version == null) return fallbackUrl(packageName) + fun resolveRedirects(url: String, maxRedirectsToFollow : Int = 5): String { + if (maxRedirectsToFollow <= 0) return url - val app = PACKAGE_MAP[packageName] ?: return fallbackUrl(packageName) - val versionSlug = version.replace(".", "-") + try { + val originalUrl = URL(url) + val connection = originalUrl.openConnection() as HttpURLConnection + connection.instanceFollowRedirects = false + connection.requestMethod = "HEAD" + connection.connectTimeout = 5_000 + connection.readTimeout = 5_000 - return "https://www.apkmirror.com/apk/${app.publisher}/${app.name}/${app.name}-$versionSlug-release/" - } + val responseCode = connection.responseCode + if (responseCode in 300..399) { + val location = connection.getHeaderField("Location") + + if (location.isNullOrBlank()) { + // Log.d("Location tag is blank: ${connection.responseMessage}") + return url + } - private fun fallbackUrl(packageName: String): String { - return "${app.morphe.gui.data.constants.AppConstants.MORPHE_API_URL}/v2/web-search/$packageName" + val resolved = + if (location.startsWith("http://") || location.startsWith("https://")) { + location + } else { + val prefix = "${originalUrl.protocol}://${originalUrl.host}" + if (location.startsWith("/")) "$prefix$location" else "$prefix/$location" + } + //Log.d("Result: $resolved") + + if (!resolved.startsWith(MORPHE_API_URL)) { + return resolved + } + + return resolveRedirects(resolved, maxRedirectsToFollow - 1) + } + + //Log.d("Unexpected response code: $responseCode") + } catch (ex: SocketTimeoutException) { + //Log.d("Timeout while resolving search redirect: $ex") + } catch (ex: Exception) { + //Log.d("Exception while resolving search redirect: $ex") + } + + return url } + } diff --git a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt index e7aa5ef..dc713a4 100644 --- a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt +++ b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt @@ -39,7 +39,7 @@ object SupportedAppExtractor { displayName = SupportedApp.getDisplayName(packageName), supportedVersions = versionList, recommendedVersion = recommendedVersion, - apkMirrorUrl = SupportedApp.getDownloadUrl(packageName, recommendedVersion) + apkDownloadUrl = SupportedApp.getDownloadUrl(packageName, recommendedVersion) ) }.sortedBy { it.displayName } } From afb884622e33ba5d7aa56c947f65ca17d2146212 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Sat, 7 Feb 2026 10:58:28 +0100 Subject: [PATCH 13/49] Follow redirects in non simple mode --- .../app/morphe/gui/data/model/SupportedApp.kt | 3 +-- .../app/morphe/gui/ui/screens/home/HomeScreen.kt | 15 ++++++++------- .../gui/ui/screens/quick/QuickPatchScreen.kt | 2 +- .../app/morphe/gui/util/DownloadUrlResolver.kt | 7 +++---- 4 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index a9d82c1..e4a3b48 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -31,8 +31,7 @@ data class SupportedApp( } /** - * Get download URL for a package name and version. - * Returns a direct APKMirror search URL for the app + version. + * Get a web download URL for a package name and version. */ fun getDownloadUrl(packageName: String, version: String?): String? { if (version == null) return null diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 7dc4ed0..3cf33d9 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.ui.platform.LocalUriHandler import app.morphe.morphe_cli.generated.resources.Res import app.morphe.morphe_cli.generated.resources.morphe_dark import app.morphe.morphe_cli.generated.resources.morphe_light @@ -39,6 +40,7 @@ import app.morphe.gui.ui.screens.home.components.FullScreenDropZone import app.morphe.gui.ui.screens.patches.PatchesScreen import app.morphe.gui.ui.screens.patches.PatchSelectionScreen import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects import java.awt.FileDialog import java.awt.Frame import java.io.File @@ -784,7 +786,7 @@ private fun SupportedAppCardDynamic( val cardPadding = if (isCompact) 12.dp else 16.dp - val apkMirrorUrl = supportedApp.apkDownloadUrl + val downloadUrl = supportedApp.apkDownloadUrl Card( modifier = modifier, @@ -901,13 +903,12 @@ private fun SupportedAppCardDynamic( Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) // Download from APKMirror button (only if URL is configured) - if (apkMirrorUrl != null) { + if (downloadUrl != null) { + val uriHandler = LocalUriHandler.current OutlinedButton( onClick = { - try { - java.awt.Desktop.getDesktop().browse(java.net.URI(apkMirrorUrl)) - } catch (e: Exception) { - // Ignore errors + openUrlAndFollowRedirects(downloadUrl) { urlResolved -> + uriHandler.openUri(urlResolved) } }, modifier = Modifier.fillMaxWidth(), @@ -921,7 +922,7 @@ private fun SupportedAppCardDynamic( ) ) { Text( - text = if (isCompact) "APKMirror" else "Get from APKMirror", + text = "Download original APK", fontSize = if (isCompact) 11.sp else 12.sp, fontWeight = FontWeight.Medium ) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index fc2df41..e5d3482 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -755,7 +755,7 @@ private fun SupportedAppsRow( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Download original APK:", + text = "Download original APK", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) diff --git a/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt b/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt index 97b7abf..c0a6922 100644 --- a/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt +++ b/src/main/kotlin/app/morphe/gui/util/DownloadUrlResolver.kt @@ -42,7 +42,7 @@ object DownloadUrlResolver { val location = connection.getHeaderField("Location") if (location.isNullOrBlank()) { - // Log.d("Location tag is blank: ${connection.responseMessage}") + Logger.info("Location tag is blank: ${connection.responseMessage}") return url } @@ -53,7 +53,6 @@ object DownloadUrlResolver { val prefix = "${originalUrl.protocol}://${originalUrl.host}" if (location.startsWith("/")) "$prefix$location" else "$prefix/$location" } - //Log.d("Result: $resolved") if (!resolved.startsWith(MORPHE_API_URL)) { return resolved @@ -64,9 +63,9 @@ object DownloadUrlResolver { //Log.d("Unexpected response code: $responseCode") } catch (ex: SocketTimeoutException) { - //Log.d("Timeout while resolving search redirect: $ex") + Logger.info("Timeout while resolving search redirect: $ex") } catch (ex: Exception) { - //Log.d("Exception while resolving search redirect: $ex") + Logger.info("Exception while resolving search redirect: $ex") } return url From 3bfc2231b4854459b3726569038ea8e639411250 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 8 Feb 2026 09:56:28 +0530 Subject: [PATCH 14/49] Minor UI updates Fixed "Download original APK" button UI for reddit. Added more extensive timestamp for when patches are published. Added Patch notes section in the patches screen. Fixed minor UI problems --- .../morphe/gui/ui/screens/home/HomeScreen.kt | 2 +- .../screens/patches/PatchSelectionScreen.kt | 1 + .../gui/ui/screens/patches/PatchesScreen.kt | 245 ++++++++++++++---- 3 files changed, 200 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 3cf33d9..1a0ad2f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -693,7 +693,7 @@ private fun SupportedAppsSection( verticalAlignment = Alignment.Top, modifier = Modifier .padding(horizontal = if (isCompact) 8.dp else 16.dp) - .widthIn(max = 600.dp) + .widthIn(max = 700.dp) ) { supportedApps.forEach { app -> SupportedAppCardDynamic( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 6a3b7fe..c89e9ce 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -51,6 +51,7 @@ import java.awt.datatransfer.StringSelection /** * Screen for selecting which patches to apply. + * This screen is the one that selects which patch options need to be applied. Eg: Custom Branding, Spoof App Version, etc. */ data class PatchSelectionScreen( val apkPath: String, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 7149f72..4a46b47 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -8,6 +8,8 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* @@ -32,7 +34,8 @@ import app.morphe.gui.ui.theme.MorpheColors import java.io.File /** - * Screen for selecting patches to apply. + * Screen for selecting patch version to apply. + * This is the screen that selects the patches.mpp file */ data class PatchesScreen( val apkPath: String, @@ -299,77 +302,216 @@ private fun ReleaseCard( MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) } + var isExpanded by remember { mutableStateOf(false) } + val hasNotes = !release.body.isNullOrBlank() + Card( modifier = Modifier .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) .clickable(onClick = onClick), colors = CardDefaults.cardColors(containerColor = backgroundColor), shape = RoundedCornerShape(12.dp) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = release.tagName, + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + if (release.isDevRelease()) { + Surface( + color = MorpheColors.Teal.copy(alpha = 0.2f), + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = "DEV", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Teal, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } + } + } + + Spacer(modifier = Modifier.height(4.dp)) + + // Show .mpp file info if available + release.assets.find { it.isMpp() }?.let { mppAsset -> + Text( + text = "${mppAsset.name} (${mppAsset.getFormattedSize()})", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( - text = release.tagName, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + text = "Published: ${formatDate(release.publishedAt)}", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) - if (release.isDevRelease()) { + + if (hasNotes) { + Spacer(modifier = Modifier.height(4.dp)) Surface( - color = MorpheColors.Teal.copy(alpha = 0.2f), - shape = RoundedCornerShape(4.dp) + color = MorpheColors.Blue.copy(alpha = 0.1f), + shape = RoundedCornerShape(6.dp), + modifier = Modifier + .clip(RoundedCornerShape(6.dp)) + .clickable { isExpanded = !isExpanded } ) { - Text( - text = "DEV", - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - color = MorpheColors.Teal, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = if (isExpanded) "Hide patch notes" else "Patch notes", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Blue + ) + Icon( + imageVector = if (isExpanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, + contentDescription = null, + tint = MorpheColors.Blue, + modifier = Modifier.size(16.dp) + ) + } } } } - Spacer(modifier = Modifier.height(4.dp)) - - // Show .mpp file info if available - release.assets.find { it.isMpp() }?.let { mppAsset -> - Text( - text = "${mppAsset.name} (${mppAsset.getFormattedSize()})", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Selected", + tint = MorpheColors.Blue, + modifier = Modifier.size(24.dp) ) } + } - Text( - text = "Published: ${formatDate(release.publishedAt)}", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + // Expandable release notes + if (isExpanded && hasNotes) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) + ) + FormattedReleaseNotes( + markdown = release.body.orEmpty(), + modifier = Modifier.padding(16.dp) ) } + } + } +} - if (isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = "Selected", - tint = MorpheColors.Blue, - modifier = Modifier.size(24.dp) +/** + * Renders GitHub release notes markdown as formatted Compose text. + */ +@Composable +private fun FormattedReleaseNotes(markdown: String, modifier: Modifier = Modifier) { + val lines = parseMarkdown(markdown) + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + lines.forEach { line -> + when (line) { + is MdLine.Header -> Text( + text = line.text, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + is MdLine.SubHeader -> Text( + text = line.text, + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ) + is MdLine.Bullet -> { + Row { + Text( + text = "\u2022 ", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = line.text, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 18.sp + ) + } + } + is MdLine.Plain -> Text( + text = line.text, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 18.sp ) } } } } +private sealed class MdLine { + data class Header(val text: String) : MdLine() + data class SubHeader(val text: String) : MdLine() + data class Bullet(val text: String) : MdLine() + data class Plain(val text: String) : MdLine() +} + +private fun parseMarkdown(markdown: String): List { + return markdown.lines() + .filter { it.isNotBlank() } + .map { line -> + val trimmed = line.trim() + when { + trimmed.startsWith("# ") -> MdLine.Header(cleanMarkdown(trimmed.removePrefix("# "))) + trimmed.startsWith("## ") -> MdLine.Header(cleanMarkdown(trimmed.removePrefix("## "))) + trimmed.startsWith("### ") -> MdLine.SubHeader(cleanMarkdown(trimmed.removePrefix("### "))) + trimmed.startsWith("* ") -> MdLine.Bullet(cleanMarkdown(trimmed.removePrefix("* "))) + trimmed.startsWith("- ") -> MdLine.Bullet(cleanMarkdown(trimmed.removePrefix("- "))) + else -> MdLine.Plain(cleanMarkdown(trimmed)) + } + } +} + +/** + * Strip markdown syntax to plain readable text: + * - **bold** → bold + * - [text](url) → text + * - ([hash](url)) → remove entirely (commit refs) + */ +private fun cleanMarkdown(text: String): String { + var result = text + // Remove commit refs like ([abc1234](https://...)) + result = result.replace(Regex("""\(\[[\da-f]{7,}]\([^)]*\)\)"""), "") + // [text](url) → text + result = result.replace(Regex("""\[([^\]]*?)]\([^)]*\)"""), "$1") + // **bold** → bold + result = result.replace(Regex("""\*\*(.+?)\*\*"""), "$1") + // Clean up extra whitespace + result = result.replace(Regex("""\s+"""), " ").trim() + return result +} + @Composable private fun BottomActionBar( uiState: PatchesUiState, @@ -460,15 +602,24 @@ private fun BottomActionBar( private fun formatDate(isoDate: String): String { return try { - // Simple date formatting - takes "2024-01-15T10:30:00Z" and returns "Jan 15, 2024" + // Takes "2024-01-15T10:30:00Z" and returns "Jan 15, 2024 at 10:30 AM" val datePart = isoDate.substringBefore("T") + val timePart = isoDate.substringAfter("T").substringBefore("Z").substringBefore("+") val parts = datePart.split("-") if (parts.size == 3) { val months = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec") val month = months.getOrElse(parts[1].toInt() - 1) { "???" } val day = parts[2].toInt() val year = parts[0] - "$month $day, $year" + val timeParts = timePart.split(":") + val timeStr = if (timeParts.size >= 2) { + val hour = timeParts[0].toInt() + val minute = timeParts[1] + val amPm = if (hour >= 12) "PM" else "AM" + val hour12 = if (hour == 0) 12 else if (hour > 12) hour - 12 else hour + " at $hour12:$minute $amPm UTC" + } else "" + "$month $day, $year$timeStr" } else { datePart } From 930d4b35946db41e39eac9ee116a04a409b6355b Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 8 Feb 2026 23:03:04 +0530 Subject: [PATCH 15/49] Connected devices update Added a device indicator on the top which allows to see if a device is connected or not. Other minor UI and logic fixes. --- src/main/kotlin/app/morphe/gui/App.kt | 9 + .../gui/ui/components/DeviceIndicator.kt | 301 ++++++++++++++++++ .../gui/ui/components/SettingsButton.kt | 38 ++- .../gui/ui/components/SettingsDialog.kt | 42 ++- .../morphe/gui/ui/screens/home/HomeScreen.kt | 77 +++-- .../home/components/FullScreenDropZone.kt | 2 + .../screens/patches/PatchSelectionScreen.kt | 8 +- .../gui/ui/screens/patches/PatchesScreen.kt | 2 + .../gui/ui/screens/patching/PatchingScreen.kt | 5 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 49 +-- .../gui/ui/screens/result/ResultScreen.kt | 66 +--- .../kotlin/app/morphe/gui/util/AdbManager.kt | 30 +- .../app/morphe/gui/util/DeviceMonitor.kt | 83 +++++ 13 files changed, 579 insertions(+), 133 deletions(-) create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt create mode 100644 src/main/kotlin/app/morphe/gui/util/DeviceMonitor.kt diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index 8bd8504..5bd715d 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -21,6 +21,7 @@ import app.morphe.gui.ui.theme.LocalThemeState import app.morphe.gui.ui.theme.MorpheTheme import app.morphe.gui.ui.theme.ThemePreference import app.morphe.gui.ui.theme.ThemeState +import app.morphe.gui.util.DeviceMonitor import app.morphe.gui.util.Logger /** @@ -95,6 +96,14 @@ private fun AppContent(initialSimplifiedMode: Boolean) { onChange = onModeChange ) + // Start/stop DeviceMonitor with app lifecycle + DisposableEffect(Unit) { + DeviceMonitor.startMonitoring() + onDispose { + DeviceMonitor.stopMonitoring() + } + } + MorpheTheme(themePreference = themePreference) { CompositionLocalProvider( LocalThemeState provides themeState, diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt new file mode 100644 index 0000000..697ebd5 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt @@ -0,0 +1,301 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.PhoneAndroid +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.UsbOff +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.DeviceMonitor +import app.morphe.gui.util.DeviceStatus + +@Composable +fun DeviceIndicator(modifier: Modifier = Modifier) { + val monitorState by DeviceMonitor.state.collectAsState() + + val isAdbAvailable = monitorState.isAdbAvailable + val readyDevices = monitorState.devices.filter { it.isReady } + val unauthorizedDevices = monitorState.devices.filter { it.status == DeviceStatus.UNAUTHORIZED } + val selectedDevice = monitorState.selectedDevice + val hasDevices = monitorState.devices.isNotEmpty() + + var showPopup by remember { mutableStateOf(false) } + + Box(modifier = modifier) { + Surface( + onClick = { showPopup = !showPopup }, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Status dot + val dotColor = when { + isAdbAvailable == false -> MaterialTheme.colorScheme.error.copy(alpha = 0.6f) + selectedDevice != null && selectedDevice.isReady -> MorpheColors.Teal + unauthorizedDevices.isNotEmpty() -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) + } + + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(dotColor) + ) + + // Display text + val displayText = when { + isAdbAvailable == null -> "Checking..." + isAdbAvailable == false -> "No ADB" + selectedDevice != null -> { + val arch = selectedDevice.architecture?.let { " \u2022 $it" } ?: "" + "${selectedDevice.displayName}$arch" + } + unauthorizedDevices.isNotEmpty() -> "Unauthorized" + else -> "No device" + } + + Text( + text = displayText, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = when { + isAdbAvailable == false -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) + selectedDevice != null -> MaterialTheme.colorScheme.onSurface + unauthorizedDevices.isNotEmpty() -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.widthIn(max = 180.dp) + ) + + // Always show dropdown arrow — popup has useful info in every state + Icon( + imageVector = Icons.Default.ArrowDropDown, + contentDescription = "Device details", + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // Popup with device list / status info + DropdownMenu( + expanded = showPopup, + onDismissRequest = { showPopup = false } + ) { + when { + isAdbAvailable == false -> { + // ADB not found + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.UsbOff, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.error + ) + Column { + Text( + text = "ADB not found", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.error + ) + Text( + text = "Install Android SDK Platform Tools", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + onClick = { showPopup = false } + ) + } + + monitorState.devices.isEmpty() -> { + // ADB available but no devices visible + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.PhoneAndroid, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + Column { + Text( + text = "No devices detected", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "Only devices with USB debugging enabled will appear here", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + }, + onClick = { showPopup = false } + ) + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MorpheColors.Blue.copy(alpha = 0.7f) + ) + Column { + Text( + text = "How to enable USB debugging", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Blue + ) + Text( + text = "Settings > Developer Options > USB Debugging", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + }, + onClick = { showPopup = false } + ) + } + + else -> { + // Device list + monitorState.devices.forEach { device -> + val isSelected = device.id == selectedDevice?.id + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.PhoneAndroid, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = when { + isSelected -> MorpheColors.Teal + device.isReady -> MorpheColors.Blue + device.status == DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.error + } + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = device.displayName, + fontSize = 13.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal + ) + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + device.architecture?.let { arch -> + Text( + text = arch, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + Text( + text = when (device.status) { + DeviceStatus.DEVICE -> "Connected" + DeviceStatus.UNAUTHORIZED -> "Unauthorized" + DeviceStatus.OFFLINE -> "Offline" + DeviceStatus.UNKNOWN -> "Unknown" + }, + fontSize = 11.sp, + color = when (device.status) { + DeviceStatus.DEVICE -> MorpheColors.Teal + DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.error + } + ) + } + } + } + }, + onClick = { + if (device.isReady) { + DeviceMonitor.selectDevice(device) + } + showPopup = false + } + ) + } + + // USB debugging hint + HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) + DropdownMenuItem( + text = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + modifier = Modifier.size(16.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + Column { + Text( + text = "Device connected but not listed?", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "Enable USB Debugging in Developer Options", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + }, + onClick = { showPopup = false } + ) + } + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt index ac1411b..97827e9 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -2,7 +2,9 @@ package app.morphe.gui.ui.components import app.morphe.gui.LocalModeState import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons @@ -10,7 +12,9 @@ import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp @@ -44,20 +48,19 @@ fun SettingsButton( } } - Box(modifier = modifier) { - IconButton( - onClick = { showSettingsDialog = true }, - modifier = Modifier - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) + Surface( + onClick = { showSettingsDialog = true }, + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + modifier = modifier ) { Icon( imageVector = Icons.Default.Settings, contentDescription = "Settings", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp) ) } - } if (showSettingsDialog) { SettingsDialog( @@ -79,3 +82,22 @@ fun SettingsButton( ) } } + +/** + * Top bar row that places DeviceIndicator + SettingsButton together. + * Use this instead of standalone SettingsButton on screens. + */ +@Composable +fun TopBarRow( + modifier: Modifier = Modifier, + allowCacheClear: Boolean = true, +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + DeviceIndicator() + SettingsButton(allowCacheClear = allowCacheClear) + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index dd88e88..30b395d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -1,5 +1,7 @@ package app.morphe.gui.ui.components +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape @@ -12,6 +14,8 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -65,11 +69,29 @@ fun SettingsDialog( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { ThemePreference.entries.forEach { theme -> - FilterChip( - selected = currentTheme == theme, - onClick = { onThemeChange(theme) }, - label = { Text(theme.toDisplayName()) } - ) + val isSelected = currentTheme == theme + Surface( + shape = RoundedCornerShape(8.dp), + color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.15f) + else Color.Transparent, + border = BorderStroke( + width = 1.dp, + color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ), + modifier = Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { onThemeChange(theme) } + ) { + Text( + text = theme.toDisplayName(), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), + fontSize = 13.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, + color = if (isSelected) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurfaceVariant + ) + } } } @@ -238,8 +260,14 @@ fun SettingsDialog( } }, confirmButton = { - TextButton(onClick = onDismiss) { - Text("Close") + OutlinedButton( + onClick = onDismiss, + shape = RoundedCornerShape(8.dp) + ) { + Text( + "Close", + color = MaterialTheme.colorScheme.error + ) } } ) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 1a0ad2f..0354d76 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -34,7 +34,7 @@ import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.SupportedApp -import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.screens.home.components.ApkInfoCard import app.morphe.gui.ui.screens.home.components.FullScreenDropZone import app.morphe.gui.ui.screens.patches.PatchesScreen @@ -84,7 +84,8 @@ fun HomeScreenContent( FullScreenDropZone( isDragHovering = uiState.isDragHovering, onDragHoverChange = { viewModel.setDragHover(it) }, - onFilesDropped = { viewModel.onFilesDropped(it) } + onFilesDropped = { viewModel.onFilesDropped(it) }, + enabled = !uiState.isAnalyzing ) { BoxWithConstraints( modifier = Modifier @@ -228,8 +229,8 @@ fun HomeScreenContent( } } - // Settings button in top-right corner - SettingsButton( + // Top bar (device indicator + settings) in top-right corner + TopBarRow( modifier = Modifier .align(Alignment.TopEnd) .padding(padding), @@ -260,21 +261,27 @@ private fun MiddleContent( onChangeClick: () -> Unit, onContinueClick: () -> Unit ) { - if (uiState.apkInfo != null) { - ApkSelectedSection( - patchesLoaded = patchesLoaded, - apkInfo = uiState.apkInfo, - isCompact = isCompact, - onClearClick = onClearClick, - onChangeClick = onChangeClick, - onContinueClick = onContinueClick - ) - } else { - DropPromptSection( - isDragHovering = uiState.isDragHovering, - isCompact = isCompact, - onBrowseClick = onChangeClick - ) + when { + uiState.isAnalyzing -> { + AnalyzingSection(isCompact = isCompact) + } + uiState.apkInfo != null -> { + ApkSelectedSection( + patchesLoaded = patchesLoaded, + apkInfo = uiState.apkInfo, + isCompact = isCompact, + onClearClick = onClearClick, + onChangeClick = onChangeClick, + onContinueClick = onContinueClick + ) + } + else -> { + DropPromptSection( + isDragHovering = uiState.isDragHovering, + isCompact = isCompact, + onBrowseClick = onChangeClick + ) + } } } @@ -574,6 +581,38 @@ private fun DropPromptSection( } } +@Composable +private fun AnalyzingSection(isCompact: Boolean = false) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(horizontal = if (isCompact) 16.dp else 32.dp) + ) { + CircularProgressIndicator( + modifier = Modifier.size(if (isCompact) 36.dp else 44.dp), + color = MorpheColors.Blue, + strokeWidth = 3.dp + ) + + Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) + + Text( + text = "Analyzing APK...", + fontSize = if (isCompact) 16.sp else 18.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "Reading app information", + fontSize = if (isCompact) 12.sp else 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + @Composable private fun SupportedAppsSection( isCompact: Boolean = false, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt index 8db0374..489bc91 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/FullScreenDropZone.kt @@ -18,6 +18,7 @@ fun FullScreenDropZone( isDragHovering: Boolean, onDragHoverChange: (Boolean) -> Unit, onFilesDropped: (List) -> Unit, + enabled: Boolean = true, content: @Composable () -> Unit ) { val dragAndDropTarget = remember { @@ -40,6 +41,7 @@ fun FullScreenDropZone( override fun onDrop(event: DragAndDropEvent): Boolean { onDragHoverChange(false) + if (!enabled) return false val transferable = event.awtTransferable return try { if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index c89e9ce..67f5296 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -41,7 +41,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.Patch import org.koin.core.parameter.parametersOf import app.morphe.gui.ui.components.ErrorDialog -import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.components.getErrorType import app.morphe.gui.ui.components.getFriendlyErrorMessage import app.morphe.gui.ui.screens.patching.PatchingScreen @@ -137,7 +137,8 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { color = MorpheColors.Blue ) } - SettingsButton(allowCacheClear = false) + TopBarRow(allowCacheClear = false) + Spacer(Modifier.width(12.dp)) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface @@ -369,6 +370,7 @@ private fun PatchListItem( Card( modifier = Modifier .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) .clickable(onClick = onToggle), colors = CardDefaults.cardColors(containerColor = backgroundColor), shape = RoundedCornerShape(12.dp) @@ -382,7 +384,7 @@ private fun PatchListItem( ) { Checkbox( checked = isSelected, - onCheckedChange = { onToggle() }, + onCheckedChange = null, colors = CheckboxDefaults.colors( checkedColor = MorpheColors.Blue, uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 4a46b47..3289d04 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -27,6 +27,7 @@ import app.morphe.gui.data.model.Release import org.koin.core.parameter.parametersOf import cafe.adriel.voyager.koin.koinScreenModel import app.morphe.gui.ui.components.ErrorDialog +import app.morphe.gui.ui.components.DeviceIndicator import app.morphe.gui.ui.components.SettingsButton import app.morphe.gui.ui.components.getErrorType import app.morphe.gui.ui.components.getFriendlyErrorMessage @@ -105,6 +106,7 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { } }, actions = { + DeviceIndicator() IconButton( onClick = { viewModel.loadReleases() }, enabled = !uiState.isLoading diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt index 8e0978e..be0d351 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt @@ -25,7 +25,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.PatchConfig import org.koin.core.parameter.parametersOf +import app.morphe.gui.ui.components.DeviceIndicator import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.screens.result.ResultScreen import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.FileUtils @@ -115,7 +117,8 @@ fun PatchingScreenContent(viewModel: PatchingViewModel) { Text("Cancel") } } - SettingsButton(allowCacheClear = false) + TopBarRow(allowCacheClear = false) + Spacer(Modifier.width(12.dp)) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index e5d3482..5c336c7 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -41,11 +41,11 @@ import app.morphe.gui.data.repository.PatchRepository import app.morphe.gui.util.PatchService import org.jetbrains.compose.resources.painterResource import org.koin.compose.koinInject -import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.theme.MorpheColors import androidx.compose.runtime.rememberCoroutineScope -import app.morphe.gui.util.AdbDevice import app.morphe.gui.util.AdbManager +import app.morphe.gui.util.DeviceMonitor import kotlinx.coroutines.launch import app.morphe.gui.util.ChecksumStatus import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects @@ -232,8 +232,8 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { } } - // Settings button in top-right corner - SettingsButton( + // Top bar (device indicator + settings) in top-right corner + TopBarRow( modifier = Modifier .align(Alignment.TopEnd) .padding(24.dp) @@ -538,39 +538,11 @@ private fun CompletedContent( val outputFile = File(outputPath) val scope = rememberCoroutineScope() val adbManager = remember { AdbManager() } - var isAdbAvailable by remember { mutableStateOf(null) } - var connectedDevices by remember { mutableStateOf>(emptyList()) } - var selectedDevice by remember { mutableStateOf(null) } + val monitorState by DeviceMonitor.state.collectAsState() var isInstalling by remember { mutableStateOf(false) } var installError by remember { mutableStateOf(null) } var installSuccess by remember { mutableStateOf(false) } - fun refreshDevices() { - scope.launch { - val result = adbManager.getConnectedDevices() - result.fold( - onSuccess = { devices -> - connectedDevices = devices - val readyDevices = devices.filter { it.isReady } - if (readyDevices.size == 1) { - selectedDevice = readyDevices.first() - } - }, - onFailure = { - connectedDevices = emptyList() - selectedDevice = null - } - ) - } - } - - LaunchedEffect(Unit) { - isAdbAvailable = adbManager.isAdbAvailable() - if (isAdbAvailable == true) { - refreshDevices() - } - } - Column( modifier = Modifier .fillMaxSize() @@ -648,10 +620,11 @@ private fun CompletedContent( } } - if (isAdbAvailable == true) { + if (monitorState.isAdbAvailable == true) { Spacer(modifier = Modifier.height(16.dp)) - val readyDevices = connectedDevices.filter { it.isReady } + val readyDevices = monitorState.devices.filter { it.isReady } + val selectedDevice = monitorState.selectedDevice if (installSuccess) { Surface( @@ -718,10 +691,6 @@ private fun CompletedContent( fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) - Spacer(modifier = Modifier.height(4.dp)) - TextButton(onClick = { refreshDevices() }) { - Text("Refresh", fontSize = 12.sp) - } } installError?.let { error -> @@ -1042,7 +1011,7 @@ private fun VerificationStatusBanner( private fun openFilePicker(): File? { val chooser = JFileChooser().apply { dialogTitle = "Select APK" - fileFilter = FileNameExtensionFilter("APK Files", "apk") + fileFilter = FileNameExtensionFilter("APK Files (*.apk, *.apkm)", "apk", "apkm") isAcceptAllFileFilterUsed = false } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt index ba5a4e2..0409c25 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt @@ -27,11 +27,12 @@ import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.repository.ConfigRepository import kotlinx.coroutines.launch import org.koin.compose.koinInject -import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.AdbDevice import app.morphe.gui.util.AdbException import app.morphe.gui.util.AdbManager +import app.morphe.gui.util.DeviceMonitor import app.morphe.gui.util.DeviceStatus import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger @@ -60,11 +61,8 @@ fun ResultScreenContent(outputPath: String) { val adbManager = remember { AdbManager() } val configRepository: ConfigRepository = koinInject() - // ADB state - var isAdbAvailable by remember { mutableStateOf(null) } - var connectedDevices by remember { mutableStateOf>(emptyList()) } - var selectedDevice by remember { mutableStateOf(null) } - var isLoadingDevices by remember { mutableStateOf(false) } + // ADB state from DeviceMonitor + val monitorState by DeviceMonitor.state.collectAsState() var isInstalling by remember { mutableStateOf(false) } var installProgress by remember { mutableStateOf("") } var installError by remember { mutableStateOf(null) } @@ -92,43 +90,9 @@ fun ResultScreenContent(outputPath: String) { } } - // Function to refresh device list - fun refreshDevices() { - scope.launch { - isLoadingDevices = true - val result = adbManager.getConnectedDevices() - result.fold( - onSuccess = { devices -> - connectedDevices = devices - // Auto-select if only one ready device - val readyDevices = devices.filter { it.isReady } - if (readyDevices.size == 1) { - selectedDevice = readyDevices.first() - } else if (selectedDevice != null && !readyDevices.any { it.id == selectedDevice?.id }) { - // Clear selection if previously selected device is no longer available - selectedDevice = null - } - }, - onFailure = { - connectedDevices = emptyList() - selectedDevice = null - } - ) - isLoadingDevices = false - } - } - - // Check ADB availability and fetch devices on load - LaunchedEffect(Unit) { - isAdbAvailable = adbManager.isAdbAvailable() - if (isAdbAvailable == true) { - refreshDevices() - } - } - // Install function fun installViaAdb() { - val device = selectedDevice ?: return + val device = monitorState.selectedDevice ?: return scope.launch { isInstalling = true installError = null @@ -248,17 +212,17 @@ fun ResultScreenContent(outputPath: String) { Spacer(modifier = Modifier.height(24.dp)) // ADB Install Section - if (isAdbAvailable == true) { + if (monitorState.isAdbAvailable == true) { AdbInstallSection( - devices = connectedDevices, - selectedDevice = selectedDevice, - isLoadingDevices = isLoadingDevices, + devices = monitorState.devices, + selectedDevice = monitorState.selectedDevice, + isLoadingDevices = false, isInstalling = isInstalling, installProgress = installProgress, installError = installError, installSuccess = installSuccess, - onDeviceSelected = { selectedDevice = it }, - onRefreshDevices = { refreshDevices() }, + onDeviceSelected = { DeviceMonitor.selectDevice(it) }, + onRefreshDevices = { }, onInstallClick = { installViaAdb() }, onRetryClick = { installError = null @@ -331,14 +295,14 @@ fun ResultScreenContent(outputPath: String) { Spacer(modifier = Modifier.height(24.dp)) // Help text (only show when ADB is not available) - if (isAdbAvailable == false) { + if (monitorState.isAdbAvailable == false) { Text( text = "ADB not found. Install Android SDK Platform Tools to enable direct installation.", fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), textAlign = TextAlign.Center ) - } else if (isAdbAvailable == null) { + } else if (monitorState.isAdbAvailable == null) { Text( text = "Checking for ADB...", fontSize = 13.sp, @@ -352,8 +316,8 @@ fun ResultScreenContent(outputPath: String) { } } - // Settings button in top-right corner - SettingsButton( + // Top bar (device indicator + settings) in top-right corner + TopBarRow( modifier = Modifier .align(Alignment.TopEnd) .padding(24.dp), diff --git a/src/main/kotlin/app/morphe/gui/util/AdbManager.kt b/src/main/kotlin/app/morphe/gui/util/AdbManager.kt index 94f933c..998f4c0 100644 --- a/src/main/kotlin/app/morphe/gui/util/AdbManager.kt +++ b/src/main/kotlin/app/morphe/gui/util/AdbManager.kt @@ -261,14 +261,18 @@ class AdbManager { } } - // If device is authorized, try to get friendly device name + // If device is authorized, try to get friendly device name and architecture val deviceName = if (status == DeviceStatus.DEVICE) { model ?: product ?: getDeviceName(adbPath, id) } else { model ?: product } - AdbDevice(id, status, deviceName) + val architecture = if (status == DeviceStatus.DEVICE) { + getDeviceArchitecture(adbPath, id) + } else null + + AdbDevice(id, status, deviceName, architecture) } else null } } @@ -289,6 +293,22 @@ class AdbManager { } } + /** + * Get device CPU architecture using adb shell command. + */ + private fun getDeviceArchitecture(adbPath: String, deviceId: String): String? { + return try { + val process = ProcessBuilder(adbPath, "-s", deviceId, "shell", "getprop", "ro.product.cpu.abi") + .redirectErrorStream(true) + .start() + val result = process.inputStream.bufferedReader().readText().trim() + process.waitFor() + if (process.exitValue() == 0 && result.isNotBlank()) result else null + } catch (e: Exception) { + null + } + } + private fun parseInstallError(output: String): String { // Common ADB install errors return when { @@ -321,7 +341,8 @@ class AdbManager { data class AdbDevice( val id: String, val status: DeviceStatus, - val model: String? = null + val model: String? = null, + val architecture: String? = null ) { /** Device name (model or ID if model unknown) */ val displayName: String @@ -331,8 +352,9 @@ data class AdbDevice( val displayNameWithStatus: String get() { val name = displayName + val arch = architecture?.let { " ($it)" } ?: "" return when (status) { - DeviceStatus.DEVICE -> "$name (Connected)" + DeviceStatus.DEVICE -> "$name$arch (Connected)" DeviceStatus.UNAUTHORIZED -> "$name (Unauthorized - check device)" DeviceStatus.OFFLINE -> "$name (Offline)" DeviceStatus.UNKNOWN -> "$name (Unknown status)" diff --git a/src/main/kotlin/app/morphe/gui/util/DeviceMonitor.kt b/src/main/kotlin/app/morphe/gui/util/DeviceMonitor.kt new file mode 100644 index 0000000..8f892b3 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/DeviceMonitor.kt @@ -0,0 +1,83 @@ +package app.morphe.gui.util + +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +data class DeviceMonitorState( + val devices: List = emptyList(), + val selectedDevice: AdbDevice? = null, + val isAdbAvailable: Boolean? = null +) + +object DeviceMonitor { + private val _state = MutableStateFlow(DeviceMonitorState()) + val state: StateFlow = _state.asStateFlow() + + private val adbManager = AdbManager() + private var pollingJob: Job? = null + private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Default) + + fun startMonitoring() { + if (pollingJob?.isActive == true) return + + pollingJob = scope.launch { + // Initial ADB check + val adbAvailable = adbManager.isAdbAvailable() + _state.value = _state.value.copy(isAdbAvailable = adbAvailable) + + if (!adbAvailable) return@launch + + // Poll every 5 seconds + while (isActive) { + refreshDevices() + delay(5000) + } + } + } + + fun stopMonitoring() { + pollingJob?.cancel() + pollingJob = null + } + + fun selectDevice(device: AdbDevice) { + _state.value = _state.value.copy(selectedDevice = device) + } + + private suspend fun refreshDevices() { + val result = adbManager.getConnectedDevices() + result.fold( + onSuccess = { devices -> + val currentState = _state.value + val readyDevices = devices.filter { it.isReady } + + // Determine selected device + val selected = when { + // Keep current selection if it's still available + currentState.selectedDevice != null && + readyDevices.any { it.id == currentState.selectedDevice.id } -> + readyDevices.first { it.id == currentState.selectedDevice.id } + // Auto-select if only one ready device + readyDevices.size == 1 -> readyDevices.first() + // Clear selection if no ready devices + readyDevices.isEmpty() -> null + // Keep null if multiple devices and no prior selection + else -> currentState.selectedDevice + } + + _state.value = currentState.copy( + devices = devices, + selectedDevice = selected + ) + }, + onFailure = { + _state.value = _state.value.copy( + devices = emptyList(), + selectedDevice = null + ) + } + ) + } +} From 783dd7a8902d2377ff1dc3cb318cfc89d6eb03cd Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Mon, 9 Feb 2026 13:53:23 +0530 Subject: [PATCH 16/49] Patch Screen UI Improvements No more hardcoded disabled patches, directly read from the patches files Made a bunch of UI improvements for the patch screen. Simplified mode loading now shows a circular icon instead of making up progress numbers. --- .../gui/ui/components/SettingsButton.kt | 6 +- .../gui/ui/components/SettingsDialog.kt | 14 +- .../screens/patches/PatchSelectionScreen.kt | 300 +++++++----------- .../patches/PatchSelectionViewModel.kt | 40 +-- .../gui/ui/screens/quick/QuickPatchScreen.kt | 23 +- .../ui/screens/quick/QuickPatchViewModel.kt | 21 +- 6 files changed, 152 insertions(+), 252 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt index 97827e9..b5f70bd 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -73,9 +73,9 @@ fun SettingsButton( configRepository.setAutoCleanupTempFiles(enabled) } }, - useSimplifiedMode = modeState.isSimplified, - onSimplifiedModeChange = { enabled -> - modeState.onChange(enabled) + useExpertMode = !modeState.isSimplified, + onExpertModeChange = { enabled -> + modeState.onChange(!enabled) }, onDismiss = { showSettingsDialog = false }, allowCacheClear = allowCacheClear diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 30b395d..6730662 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -33,8 +33,8 @@ fun SettingsDialog( onThemeChange: (ThemePreference) -> Unit, autoCleanupTempFiles: Boolean, onAutoCleanupChange: (Boolean) -> Unit, - useSimplifiedMode: Boolean, - onSimplifiedModeChange: (Boolean) -> Unit, + useExpertMode: Boolean, + onExpertModeChange: (Boolean) -> Unit, onDismiss: () -> Unit, allowCacheClear: Boolean = true ) { @@ -97,7 +97,7 @@ fun SettingsDialog( HorizontalDivider() - // Simplified mode setting + // Expert mode setting Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -105,20 +105,20 @@ fun SettingsDialog( ) { Column(modifier = Modifier.weight(1f)) { Text( - text = "Simplified mode", + text = "Expert mode", fontSize = 14.sp, fontWeight = FontWeight.Medium, color = MaterialTheme.colorScheme.onSurface ) Text( - text = "Quick one-click patching with default settings", + text = "Full control over patch selection and configuration", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) } Switch( - checked = useSimplifiedMode, - onCheckedChange = onSimplifiedModeChange, + checked = useExpertMode, + onCheckedChange = onExpertModeChange, colors = SwitchDefaults.colors( checkedThumbColor = MorpheColors.Blue, checkedTrackColor = MorpheColors.Blue.copy(alpha = 0.5f) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 67f5296..2acc44e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -3,6 +3,7 @@ package app.morphe.gui.ui.screens.patches import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll @@ -20,8 +21,6 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.ContentCopy -import androidx.compose.material.icons.filled.ExpandLess -import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material.icons.filled.Terminal import androidx.compose.material3.* import androidx.compose.runtime.* @@ -52,6 +51,7 @@ import java.awt.datatransfer.StringSelection /** * Screen for selecting which patches to apply. * This screen is the one that selects which patch options need to be applied. Eg: Custom Branding, Spoof App Version, etc. + * TODO: Maybe relocate the 'Suggested Deselected Patches' section to the TopBar? */ data class PatchSelectionScreen( val apkPath: String, @@ -102,6 +102,10 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { ) } + // State for command preview + var cleanMode by remember { mutableStateOf(false) } + var showCommandPreview by remember { mutableStateOf(false) } + Scaffold( topBar = { TopAppBar( @@ -125,19 +129,51 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { }, actions = { // Select all / Deselect all - TextButton(onClick = { + TextButton( + onClick = { if (uiState.selectedPatches.size == uiState.allPatches.size) { viewModel.deselectAll() } else { viewModel.selectAll() } - }) { + }, + shape = RoundedCornerShape(12.dp) + ) { Text( if (uiState.selectedPatches.size == uiState.allPatches.size) "Deselect All" else "Select All", color = MorpheColors.Blue ) } + + Spacer(Modifier.width(12.dp)) + + // Command preview toggle + if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { + val isActive = showCommandPreview + Surface( + onClick = { showCommandPreview = !showCommandPreview }, + shape = RoundedCornerShape(8.dp), + color = if (isActive) MorpheColors.Teal.copy(alpha = 0.15f) + else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + border = BorderStroke( + width = 1.dp, + color = if (isActive) MorpheColors.Teal.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + ) { + Icon( + imageVector = Icons.Default.Terminal, + contentDescription = "Command Preview", + tint = if (isActive) MorpheColors.Teal else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp).size(20.dp) + ) + } + } + + Spacer(Modifier.width(12.dp)) + TopBarRow(allowCacheClear = false) + Spacer(Modifier.width(12.dp)) }, colors = TopAppBarDefaults.topAppBarColors( @@ -146,32 +182,32 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { ) }, ) { paddingValues -> - // State for command preview - var cleanMode by remember { mutableStateOf(false) } - var isCollapsed by remember { mutableStateOf(false) } - Column( modifier = Modifier .fillMaxSize() .padding(paddingValues) ) { - // Command preview at the top - updates in real-time + // Command preview - collapsible via top bar button if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { val commandPreview = remember(uiState.selectedPatches, cleanMode) { viewModel.getCommandPreview(cleanMode) } - CommandPreview( - command = commandPreview, - cleanMode = cleanMode, - isCollapsed = isCollapsed, - onToggleMode = { cleanMode = !cleanMode }, - onToggleCollapse = { isCollapsed = !isCollapsed }, - onCopy = { - val clipboard = Toolkit.getDefaultToolkit().systemClipboard - clipboard.setContents(StringSelection(commandPreview), null) - }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) + AnimatedVisibility( + visible = showCommandPreview, + enter = expandVertically(), + exit = shrinkVertically() + ) { + CommandPreview( + command = commandPreview, + cleanMode = cleanMode, + onToggleMode = { cleanMode = !cleanMode }, + onCopy = { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + clipboard.setContents(StringSelection(commandPreview), null) + }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } } // Search bar @@ -183,21 +219,20 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) - // Commonly disabled patches suggestion - val commonlyDisabledPatches = remember(uiState.selectedPatches, uiState.allPatches) { - viewModel.getCommonlyDisabledPatches() + // Info card about default-disabled patches + val defaultDisabledCount = remember(uiState.allPatches) { + viewModel.getDefaultDisabledCount() } - var suggestionDismissed by remember { mutableStateOf(false) } + var infoDismissed by remember { mutableStateOf(false) } AnimatedVisibility( - visible = commonlyDisabledPatches.isNotEmpty() && !suggestionDismissed && !uiState.isLoading, + visible = defaultDisabledCount > 0 && !infoDismissed && !uiState.isLoading, enter = expandVertically(), exit = shrinkVertically() ) { - CommonlyDisabledSuggestion( - patches = commonlyDisabledPatches, - onDeselectAll = { viewModel.deselectCommonlyDisabled() }, - onDismiss = { suggestionDismissed = true }, + DefaultDisabledInfoCard( + count = defaultDisabledCount, + onDismiss = { infoDismissed = true }, modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) } @@ -459,111 +494,47 @@ private fun PatchListItem( } @Composable -private fun CommonlyDisabledSuggestion( - patches: List>, - onDeselectAll: () -> Unit, +private fun DefaultDisabledInfoCard( + count: Int, onDismiss: () -> Unit, modifier: Modifier = Modifier ) { Card( modifier = modifier.fillMaxWidth(), colors = CardDefaults.cardColors( - containerColor = Color(0xFFFF9800).copy(alpha = 0.1f) + containerColor = MorpheColors.Blue.copy(alpha = 0.08f) ), shape = RoundedCornerShape(12.dp) ) { - Column( - modifier = Modifier.padding(12.dp) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = Color(0xFFFF9800), - modifier = Modifier.size(18.dp) - ) - Text( - text = "Commonly disabled patches", - fontWeight = FontWeight.Medium, - fontSize = 13.sp, - color = Color(0xFFFF9800) - ) - } - IconButton( - onClick = onDismiss, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Dismiss", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp) - ) - } - } - - Spacer(modifier = Modifier.height(8.dp)) - + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MorpheColors.Blue, + modifier = Modifier.size(18.dp) + ) Text( - text = "These ${patches.size} patch${if (patches.size > 1) "es are" else " is"} commonly disabled by users:", + text = "$count patch${if (count > 1) "es are" else " is"} unselected by default as they may cause issues or are not recommended by the patches team.", fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f) ) - - Spacer(modifier = Modifier.height(6.dp)) - - // List patch names - patches.take(4).forEach { (patch, _) -> - Text( - text = "• ${patch.name}", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - if (patches.size > 4) { - Text( - text = "• +${patches.size - 4} more", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - Spacer(modifier = Modifier.height(10.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End + IconButton( + onClick = onDismiss, + modifier = Modifier.size(24.dp) ) { - TextButton( - onClick = onDismiss, - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) - ) { - Text("Keep all", fontSize = 12.sp) - } - Spacer(modifier = Modifier.width(8.dp)) - Button( - onClick = { - onDeselectAll() - onDismiss() - }, - colors = ButtonDefaults.buttonColors( - containerColor = Color(0xFFFF9800) - ), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp), - shape = RoundedCornerShape(8.dp) - ) { - Text("Deselect these", fontSize = 12.sp) - } + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Dismiss", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) } } } @@ -576,18 +547,14 @@ private fun CommonlyDisabledSuggestion( private fun CommandPreview( command: String, cleanMode: Boolean, - isCollapsed: Boolean, onToggleMode: () -> Unit, - onToggleCollapse: () -> Unit, onCopy: () -> Unit, modifier: Modifier = Modifier ) { val terminalBackground = Color(0xFF1E1E1E) -// val terminalGreen = Color(0xFF4EC9B0) val terminalGreen = Color(0xFF6A9955) val terminalText = Color(0xFFD4D4D4) val terminalDim = Color(0xFF6A9955) -// val terminalDim = Color(0xFF4EC9B0) var showCopied by remember { mutableStateOf(false) } @@ -607,20 +574,16 @@ private fun CommandPreview( Column( modifier = Modifier.padding(12.dp) ) { - // Header with terminal icon, controls, and collapse toggle + // Header with terminal icon and controls Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { - // Left side - icon, title, and collapse toggle + // Left side - icon and title Row( verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier - .clip(RoundedCornerShape(4.dp)) - .clickable(onClick = onToggleCollapse) - .padding(end = 8.dp) + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( imageVector = Icons.Default.Terminal, @@ -634,12 +597,6 @@ private fun CommandPreview( fontWeight = FontWeight.Bold, color = terminalGreen ) - Icon( - imageVector = if (isCollapsed) Icons.Default.ExpandMore else Icons.Default.ExpandLess, - contentDescription = if (isCollapsed) "Expand" else "Collapse", - tint = terminalDim, - modifier = Modifier.size(16.dp) - ) } // Right side - controls @@ -676,51 +633,40 @@ private fun CommandPreview( } } - // Mode toggle (only show when not collapsed) - if (!isCollapsed) { - Surface( - onClick = onToggleMode, - color = Color.Transparent, - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = if (cleanMode) "Compact" else "Expand", - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = terminalDim, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) - } - } - } - } - - // Command text - collapsible, vertically scrollable - AnimatedVisibility( - visible = !isCollapsed, - enter = expandVertically(), - exit = shrinkVertically() - ) { - Column { - Spacer(modifier = Modifier.height(8.dp)) - - // Vertically scrollable command text with max height - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 120.dp) - .verticalScroll(rememberScrollState()) + // Mode toggle + Surface( + onClick = onToggleMode, + color = Color.Transparent, + shape = RoundedCornerShape(4.dp) ) { Text( - text = command, - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - color = terminalText, - lineHeight = 16.sp + text = if (cleanMode) "Compact" else "Expand", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + color = terminalDim, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } } } + + Spacer(modifier = Modifier.height(8.dp)) + + // Vertically scrollable command text with max height + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 120.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = command, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = terminalText, + lineHeight = 16.sp + ) + } } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt index 3136327..a4a9540 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -2,7 +2,6 @@ package app.morphe.gui.ui.screens.patches import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope -import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.model.Patch import app.morphe.gui.data.model.PatchConfig import kotlinx.coroutines.flow.MutableStateFlow @@ -69,11 +68,17 @@ class PatchSelectionViewModel( Logger.info("Loaded ${deduplicatedPatches.size} patches for $packageName") + // Only select patches that are enabled by default in the .mpp file + val defaultSelected = deduplicatedPatches + .filter { it.isEnabled } + .map { it.uniqueId } + .toSet() + _uiState.value = _uiState.value.copy( isLoading = false, allPatches = deduplicatedPatches, filteredPatches = deduplicatedPatches, - selectedPatches = deduplicatedPatches.map { it.uniqueId }.toSet() + selectedPatches = defaultSelected ) }, onFailure = { e -> @@ -143,35 +148,10 @@ class PatchSelectionViewModel( } /** - * Get patches that match the commonly disabled list and are currently selected. - * Returns list of (patch, reason) pairs. + * Count of patches that are disabled by default (from .mpp metadata). */ - fun getCommonlyDisabledPatches(): List> { - val packageName = getPackageNameFromApk() - val commonlyDisabled = AppConstants.PatchRecommendations.getCommonlyDisabled(packageName) - - return _uiState.value.allPatches - .filter { patch -> _uiState.value.selectedPatches.contains(patch.uniqueId) } - .mapNotNull { patch -> - // Find matching commonly disabled entry - val match = commonlyDisabled.find { (pattern, _) -> - patch.name.contains(pattern, ignoreCase = true) - } - if (match != null) { - patch to match.second - } else { - null - } - } - } - - /** - * Deselect all commonly disabled patches at once. - */ - fun deselectCommonlyDisabled() { - val patchesToDeselect = getCommonlyDisabledPatches().map { it.first.uniqueId }.toSet() - val newSelection = _uiState.value.selectedPatches - patchesToDeselect - _uiState.value = _uiState.value.copy(selectedPatches = newSelection) + fun getDefaultDisabledCount(): Int { + return _uiState.value.allPatches.count { !it.isEnabled } } fun createPatchConfig(): PatchConfig { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 5c336c7..1845e3d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -195,7 +195,6 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { QuickPatchPhase.DOWNLOADING, QuickPatchPhase.PATCHING -> { PatchingContent( phase = phase, - progress = uiState.progress, statusMessage = uiState.statusMessage, onCancel = { viewModel.cancelPatching() } ) @@ -471,7 +470,6 @@ private fun ReadyContent( @Composable private fun PatchingContent( phase: QuickPatchPhase, - progress: Float, statusMessage: String, onCancel: () -> Unit ) { @@ -480,22 +478,11 @@ private fun PatchingContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - // Progress indicator - Box(contentAlignment = Alignment.Center) { - CircularProgressIndicator( - progress = { progress }, - modifier = Modifier.size(100.dp), - strokeWidth = 6.dp, - color = MorpheColors.Teal, - trackColor = MaterialTheme.colorScheme.surfaceVariant - ) - Text( - text = "${(progress * 100).toInt()}%", - fontSize = 20.sp, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface - ) - } + CircularProgressIndicator( + modifier = Modifier.size(64.dp), + strokeWidth = 4.dp, + color = MorpheColors.Teal + ) Spacer(modifier = Modifier.height(24.dp)) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 75a1b16..2907fd7 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -324,29 +324,16 @@ class QuickPatchViewModel( val outputFileName = "$baseName-Morphe-${apkInfo.versionName}.apk" val outputPath = File(outputDir, outputFileName).absolutePath - // Auto-deselect commonly disabled patches for this app - val commonlyDisabled = AppConstants.PatchRecommendations.getCommonlyDisabled(apkInfo.packageName) - val disabledPatches = cachedPatches - .filter { patch -> - commonlyDisabled.any { (pattern, _) -> - patch.name.contains(pattern, ignoreCase = true) - } - } - .map { it.name } - - if (disabledPatches.isNotEmpty()) { - Logger.info("Quick mode: Auto-disabling patches: $disabledPatches") - } - // Use PatchService for direct library patching (no CLI subprocess) + // exclusiveMode = false means the library's patch.use field determines defaults val patchResult = patchService.patch( patchesFilePath = patchFile.absolutePath, inputApkPath = apkFile.absolutePath, outputApkPath = outputPath, - enabledPatches = emptyList(), // Empty = use defaults - disabledPatches = disabledPatches, + enabledPatches = emptyList(), + disabledPatches = emptyList(), options = emptyMap(), - exclusiveMode = false, // Include all default patches + exclusiveMode = false, onProgress = { message -> // Update status with current operation if (message.contains("patch", ignoreCase = true) || From fdba2c614cccd1240b8b5318e99aea09563f39db Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Mon, 9 Feb 2026 19:17:13 +0530 Subject: [PATCH 17/49] --riplibs update User can remove libs if they do not need it --- .../app/morphe/cli/command/PatchCommand.kt | 11 -- .../kotlin/app/morphe/gui/data/model/Patch.kt | 3 +- .../kotlin/app/morphe/gui/di/AppModule.kt | 2 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 6 +- .../gui/ui/screens/home/HomeViewModel.kt | 31 ++++- .../screens/patches/PatchSelectionScreen.kt | 123 +++++++++++++++++- .../patches/PatchSelectionViewModel.kt | 46 ++++++- .../ui/screens/patching/PatchingViewModel.kt | 1 + .../app/morphe/gui/util/PatchService.kt | 6 + 9 files changed, 199 insertions(+), 30 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index bf88cd4..43be69e 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -415,17 +415,6 @@ internal object PatchCommand : Runnable { patcherResult.applyTo(this) } ) - }.also { rebuiltApk -> - if (striplibs.isNotEmpty()) { - patchingResult.addStepResult( - PatchingStep.STRIPPING_LIBS, - { - ApkLibraryStripper.stripLibraries(rebuiltApk, striplibs) { msg -> - logger.info(msg) - } - } - ) - } }.let { patchedApkFile -> if (!mount && !unsigned) { patchingResult.addStepResult( diff --git a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt index 42b1396..e3c11e2 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt @@ -79,5 +79,6 @@ data class PatchConfig( val enabledPatches: List = emptyList(), val disabledPatches: List = emptyList(), val patchOptions: Map = emptyMap(), - val useExclusiveMode: Boolean = false + val useExclusiveMode: Boolean = false, + val riplibs: List = emptyList() ) diff --git a/src/main/kotlin/app/morphe/gui/di/AppModule.kt b/src/main/kotlin/app/morphe/gui/di/AppModule.kt index b542bca..b7a31d0 100644 --- a/src/main/kotlin/app/morphe/gui/di/AppModule.kt +++ b/src/main/kotlin/app/morphe/gui/di/AppModule.kt @@ -58,6 +58,6 @@ val appModule = module { // ViewModels (ScreenModels) factory { HomeViewModel(get(), get(), get()) } factory { params -> PatchesViewModel(params.get(), params.get(), get(), get()) } - factory { params -> PatchSelectionViewModel(params.get(), params.get(), params.get(), get(), get()) } + factory { params -> PatchSelectionViewModel(params.get(), params.get(), params.get(), params.get(), get(), get()) } factory { params -> PatchingViewModel(params.get(), get(), get()) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 0354d76..585aa9a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -112,7 +112,8 @@ fun HomeScreenContent( navigator.push(PatchSelectionScreen( apkPath = uiState.apkInfo!!.filePath, apkName = uiState.apkInfo!!.appName, - patchesFilePath = patchesFile.absolutePath + patchesFilePath = patchesFile.absolutePath, + apkArchitectures = uiState.apkInfo!!.architectures )) } }, @@ -204,7 +205,8 @@ fun HomeScreenContent( navigator.push(PatchSelectionScreen( apkPath = info.filePath, apkName = info.appName, - patchesFilePath = patchesFile.absolutePath + patchesFilePath = patchesFile.absolutePath, + apkArchitectures = info.architectures )) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 7e33e9b..141527a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -331,8 +331,9 @@ class HomeViewModel( VersionStatus.UNKNOWN } - // Get supported architectures from native libraries in the APK - val architectures = extractArchitectures(apkToParse) + // Get supported architectures from native libraries + // For .apkm files, scan the original bundle (splits contain the native libs, not base.apk) + val architectures = extractArchitectures(if (isApkm) file else apkToParse) // Verify checksum (still uses AppConstants for now) val checksumStatus = verifyChecksum(file, packageName, versionName, architectures, suggestedVersion) @@ -370,18 +371,34 @@ class HomeViewModel( private fun extractArchitectures(file: File): List { return try { java.util.zip.ZipFile(file).use { zip -> - val archDirs = zip.entries().asSequence() + val archDirs = mutableSetOf() + + // Scan for lib// entries directly (regular APK or merged APK) + zip.entries().asSequence() .map { it.name } .filter { it.startsWith("lib/") } .mapNotNull { path -> val parts = path.split("/") if (parts.size >= 2) parts[1] else null } - .distinct() - .toList() + .forEach { archDirs.add(it) } + + // For .apkm bundles: also detect arch from split APK names + // e.g. split_config.arm64_v8a.apk -> arm64-v8a + if (archDirs.isEmpty()) { + val knownArchs = setOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + zip.entries().asSequence() + .map { it.name } + .filter { it.endsWith(".apk") } + .forEach { name -> + // Convert split_config.arm64_v8a.apk format to arm64-v8a + val normalized = name.replace("_", "-") + knownArchs.filter { arch -> normalized.contains(arch) } + .forEach { archDirs.add(it) } + } + } - archDirs.ifEmpty { - // No native libs - likely a universal APK + archDirs.toList().ifEmpty { listOf("universal") } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 2acc44e..9cbfcd3 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -45,24 +45,25 @@ import app.morphe.gui.ui.components.getErrorType import app.morphe.gui.ui.components.getFriendlyErrorMessage import app.morphe.gui.ui.screens.patching.PatchingScreen import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.util.DeviceMonitor import java.awt.Toolkit import java.awt.datatransfer.StringSelection /** * Screen for selecting which patches to apply. * This screen is the one that selects which patch options need to be applied. Eg: Custom Branding, Spoof App Version, etc. - * TODO: Maybe relocate the 'Suggested Deselected Patches' section to the TopBar? */ data class PatchSelectionScreen( val apkPath: String, val apkName: String, - val patchesFilePath: String + val patchesFilePath: String, + val apkArchitectures: List = emptyList() ) : Screen { @Composable override fun Content() { val viewModel = koinScreenModel { - parametersOf(apkPath, apkName, patchesFilePath) + parametersOf(apkPath, apkName, patchesFilePath, apkArchitectures) } PatchSelectionScreenContent(viewModel = viewModel) } @@ -189,7 +190,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { ) { // Command preview - collapsible via top bar button if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { - val commandPreview = remember(uiState.selectedPatches, cleanMode) { + val commandPreview = remember(uiState.selectedPatches, uiState.selectedArchitectures, cleanMode) { viewModel.getCommandPreview(cleanMode) } AnimatedVisibility( @@ -278,6 +279,22 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { + // Architecture selector at the top of the list + // Disabled for .apkm files until properly tested with merged APKs + val isApkm = viewModel.getApkPath().endsWith(".apkm", ignoreCase = true) + val showArchSelector = !isApkm && + uiState.apkArchitectures.size > 1 && + !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") + if (showArchSelector) { + item(key = "arch_selector") { + ArchitectureSelectorCard( + architectures = uiState.apkArchitectures, + selectedArchitectures = uiState.selectedArchitectures, + onToggleArchitecture = { viewModel.toggleArchitecture(it) } + ) + } + } + items( items = uiState.filteredPatches, key = { it.uniqueId } @@ -670,3 +687,101 @@ private fun CommandPreview( } } } + +@Composable +private fun ArchitectureSelectorCard( + architectures: List, + selectedArchitectures: Set, + onToggleArchitecture: (String) -> Unit, + modifier: Modifier = Modifier +) { + // Get connected device architecture for hint + val deviceState by DeviceMonitor.state.collectAsState() + val deviceArch = deviceState.selectedDevice?.architecture + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MorpheColors.Teal.copy(alpha = 0.08f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MorpheColors.Teal, + modifier = Modifier.size(18.dp) + ) + Text( + text = "Strip native libraries", + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + } + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = "Uncheck architectures to remove from the output APK and reduce file size.", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + if (deviceArch != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "Your device: $deviceArch", + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + architectures.forEach { arch -> + val isSelected = selectedArchitectures.contains(arch) + FilterChip( + selected = isSelected, + onClick = { onToggleArchitecture(arch) }, + label = { + Text( + text = arch, + fontSize = 12.sp + ) + }, + leadingIcon = if (isSelected) { + { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + } + } else null, + colors = FilterChipDefaults.filterChipColors( + selectedContainerColor = MorpheColors.Teal.copy(alpha = 0.2f), + selectedLabelColor = MorpheColors.Teal + ) + ) + } + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt index a4a9540..3a86325 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -17,6 +17,7 @@ class PatchSelectionViewModel( private val apkPath: String, private val apkName: String, private val patchesFilePath: String, + private val apkArchitectures: List, private val patchService: PatchService, private val patchRepository: PatchRepository ) : ScreenModel { @@ -24,7 +25,10 @@ class PatchSelectionViewModel( // Actual path to use - may differ from patchesFilePath if we had to re-download private var actualPatchesFilePath: String = patchesFilePath - private val _uiState = MutableStateFlow(PatchSelectionUiState()) + private val _uiState = MutableStateFlow(PatchSelectionUiState( + apkArchitectures = apkArchitectures, + selectedArchitectures = apkArchitectures.toSet() + )) val uiState: StateFlow = _uiState.asStateFlow() init { @@ -147,6 +151,18 @@ class PatchSelectionViewModel( _uiState.value = _uiState.value.copy(error = null) } + fun toggleArchitecture(arch: String) { + val current = _uiState.value.selectedArchitectures + // Don't allow deselecting all architectures + if (current.contains(arch) && current.size <= 1) return + val newSelection = if (current.contains(arch)) { + current - arch + } else { + current + arch + } + _uiState.value = _uiState.value.copy(selectedArchitectures = newSelection) + } + /** * Count of patches that are disabled by default (from .mpp metadata). */ @@ -175,13 +191,21 @@ class PatchSelectionViewModel( .filter { !_uiState.value.selectedPatches.contains(it.uniqueId) } .map { it.name } + // Only set riplibs if user deselected any architecture (keeps = selected ones) + val riplibs = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { + _uiState.value.selectedArchitectures.toList() + } else { + emptyList() + } + return PatchConfig( inputApkPath = apkPath, outputApkPath = outputPath, patchesFilePath = actualPatchesFilePath, enabledPatches = selectedPatchNames, disabledPatches = disabledPatchNames, - useExclusiveMode = true + useExclusiveMode = true, + riplibs = riplibs ) } @@ -212,6 +236,13 @@ class PatchSelectionViewModel( .filter { _uiState.value.selectedPatches.contains(it.uniqueId) } .map { it.name } + // riplibs flag: only when user deselected at least one architecture + val riplibsArg = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { + _uiState.value.selectedArchitectures.joinToString(",") + } else { + null + } + return if (cleanMode) { val sb = StringBuilder() sb.append("java -jar morphe-cli.jar patch \\\n") @@ -219,6 +250,10 @@ class PatchSelectionViewModel( sb.append(" -o ${outputFileName} \\\n") sb.append(" --exclusive \\\n") + if (riplibsArg != null) { + sb.append(" --riplibs $riplibsArg \\\n") + } + selectedPatchNames.forEachIndexed { index, patch -> val isLast = index == selectedPatchNames.lastIndex sb.append(" -e \"$patch\"") @@ -233,7 +268,8 @@ class PatchSelectionViewModel( } else { // Compact mode - single line that wraps naturally val patches = selectedPatchNames.joinToString(" ") { "-e \"$it\"" } - "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --exclusive $patches ${inputFile.name}" + val riplibsPart = if (riplibsArg != null) " --riplibs $riplibsArg" else "" + "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --exclusive$riplibsPart $patches ${inputFile.name}" } } @@ -298,7 +334,9 @@ data class PatchSelectionUiState( val selectedPatches: Set = emptySet(), val searchQuery: String = "", val showOnlySelected: Boolean = false, - val error: String? = null + val error: String? = null, + val apkArchitectures: List = emptyList(), + val selectedArchitectures: Set = emptySet() ) { val selectedCount: Int get() = selectedPatches.size val totalCount: Int get() = allPatches.size diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt index 30726bf..edf093e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt @@ -56,6 +56,7 @@ class PatchingViewModel( disabledPatches = config.disabledPatches, options = config.patchOptions, exclusiveMode = config.useExclusiveMode, + riplibs = config.riplibs, onProgress = { message -> parseAndAddLog(message) } diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 7fc5b87..535cbb5 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -76,6 +76,7 @@ class PatchService { disabledPatches: List = emptyList(), options: Map = emptyMap(), exclusiveMode: Boolean = false, + riplibs: List = emptyList(), onProgress: (String) -> Unit = {} ): Result = withContext(Dispatchers.IO) { val tempDir = FileUtils.createPatchingTempDir() @@ -209,6 +210,11 @@ class PatchService { actualInputApk.copyTo(rebuiltApk, overwrite = true) patcherResult.applyTo(rebuiltApk) + if (riplibs.isNotEmpty()) { + onProgress("Stripping native libraries...") + ApkLibraryStripper.stripLibraries(rebuiltApk, riplibs) { onProgress(it) } + } + onProgress("Signing APK...") val keystorePath = File(tempDir, "morphe.keystore") ApkUtils.signApk( From ffa14f99f3b9d46a788210f884dd6db67b064030 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Tue, 10 Feb 2026 22:48:28 +0530 Subject: [PATCH 18/49] Minor release and windows fixes We can now generate a single cross platform jar file. Fixed an issue where cache wasn't clearing on windows --- build.gradle.kts | 10 +++-- .../gui/data/repository/PatchRepository.kt | 19 +++++++-- .../gui/ui/components/SettingsDialog.kt | 39 ++++++++++++++---- .../app/morphe/gui/util/PatchService.kt | 40 ++++++++++++------- 4 files changed, 80 insertions(+), 28 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index bc728b6..19c4ce3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -90,9 +90,13 @@ dependencies { implementation(files(strippedApkEditorLib)) // -- Compose Desktop --------------------------------------------------- - // OS-specific: JAR only runs on the OS it was built on. - // Build once per target OS (macOS, Linux, Windows). - implementation(compose.desktop.currentOs) + // Platform-independent: single JAR runs on all supported OSes. + // Skiko auto-detects the OS at runtime and loads the correct native library. + implementation(compose.desktop.macos_arm64) + implementation(compose.desktop.macos_x64) + implementation(compose.desktop.linux_x64) + implementation(compose.desktop.linux_arm64) + implementation(compose.desktop.windows_x64) implementation(compose.components.resources) @Suppress("DEPRECATION") implementation(compose.material3) diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt index 7881939..b2209c3 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt @@ -168,9 +168,22 @@ class PatchRepository( */ fun clearCache(): Boolean { return try { - FileUtils.getPatchesDir().listFiles()?.forEach { it.delete() } - Logger.info("Patches cache cleared") - true + var failedCount = 0 + FileUtils.getPatchesDir().listFiles()?.forEach { file -> + try { + java.nio.file.Files.delete(file.toPath()) + } catch (e: Exception) { + failedCount++ + Logger.error("Failed to delete ${file.name}: ${e.message}") + } + } + if (failedCount > 0) { + Logger.error("Patches cache clear incomplete: $failedCount file(s) locked") + false + } else { + Logger.info("Patches cache cleared") + true + } } catch (e: Exception) { Logger.error("Failed to clear patches cache", e) false diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 6730662..99e6e55 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -40,6 +40,7 @@ fun SettingsDialog( ) { var showClearCacheConfirm by remember { mutableStateOf(false) } var cacheCleared by remember { mutableStateOf(false) } + var cacheClearFailed by remember { mutableStateOf(false) } AlertDialog( onDismissRequest = onDismiss, @@ -222,8 +223,13 @@ fun SettingsDialog( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(8.dp), colors = ButtonDefaults.outlinedButtonColors( - contentColor = if (cacheCleared) MorpheColors.Teal else MaterialTheme.colorScheme.error, - disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + contentColor = when { + cacheCleared -> MorpheColors.Teal + cacheClearFailed -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.error + }, + disabledContentColor = if (cacheCleared) MorpheColors.Teal.copy(alpha = 0.7f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) ) { Icon( @@ -236,6 +242,7 @@ fun SettingsDialog( when { !allowCacheClear -> "Clear Cache (disabled during patching)" cacheCleared -> "Cache Cleared" + cacheClearFailed -> "Clear Cache Failed (files in use)" else -> "Clear Cache" } ) @@ -284,8 +291,9 @@ fun SettingsDialog( confirmButton = { Button( onClick = { - clearAllCache() - cacheCleared = true + val success = clearAllCache() + cacheCleared = success + cacheClearFailed = !success showClearCacheConfirm = false }, colors = ButtonDefaults.buttonColors( @@ -322,12 +330,27 @@ private fun calculateCacheSize(): String { } } -private fun clearAllCache() { - try { - FileUtils.getPatchesDir().listFiles()?.forEach { it.delete() } +private fun clearAllCache(): Boolean { + return try { + var failedCount = 0 + FileUtils.getPatchesDir().listFiles()?.forEach { file -> + try { + java.nio.file.Files.delete(file.toPath()) + } catch (e: Exception) { + failedCount++ + Logger.error("Failed to delete ${file.name}: ${e.message}") + } + } FileUtils.cleanupAllTempDirs() - Logger.info("Cache cleared successfully") + if (failedCount > 0) { + Logger.error("Cache clear incomplete: $failedCount file(s) could not be deleted (may be locked)") + false + } else { + Logger.info("Cache cleared successfully") + true + } } catch (e: Exception) { Logger.error("Failed to clear cache", e) + false } } diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 535cbb5..16478cc 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -42,23 +42,32 @@ class PatchService { } Logger.info("Loading patches from: $patchesFilePath") - val patches = loadPatchesFromJar(setOf(patchFile)) - // Convert library patches to GUI model - val guiPatches = patches.map { it.toGuiPatch() } + // Copy to temp file so URLClassLoader locks the copy, not the cached original. + // On Windows, the classloader holds the file locked and prevents deletion. + val tempCopy = File.createTempFile("morphe-patches-", ".mpp") + try { + patchFile.copyTo(tempCopy, overwrite = true) + val patches = loadPatchesFromJar(setOf(tempCopy)) + + // Convert library patches to GUI model + val guiPatches = patches.map { it.toGuiPatch() } - // Filter by package name if specified - val filtered = if (packageName != null) { - guiPatches.filter { patch -> - patch.compatiblePackages.isEmpty() || // Universal patches - patch.compatiblePackages.any { it.name == packageName } + // Filter by package name if specified + val filtered = if (packageName != null) { + guiPatches.filter { patch -> + patch.compatiblePackages.isEmpty() || // Universal patches + patch.compatiblePackages.any { it.name == packageName } + } + } else { + guiPatches } - } else { - guiPatches - } - Logger.info("Loaded ${filtered.size} patches" + (packageName?.let { " for $it" } ?: "")) - Result.success(filtered) + Logger.info("Loaded ${filtered.size} patches" + (packageName?.let { " for $it" } ?: "")) + Result.success(filtered) + } finally { + tempCopy.deleteOnExit() + } } catch (e: Exception) { Logger.error("Failed to load patches", e) Result.failure(e) @@ -95,7 +104,10 @@ class PatchService { } onProgress("Loading patches...") - val patches = loadPatchesFromJar(setOf(patchFile)) + // Copy to temp file so URLClassLoader locks the copy, not the cached original. + val patchTempCopy = File(tempDir, patchFile.name) + patchFile.copyTo(patchTempCopy, overwrite = true) + val patches = loadPatchesFromJar(setOf(patchTempCopy)) // Handle APKM format (split APK bundle) var mergedApkToCleanup: File? = null From 512f6f5859e676f7475e7fc7e870b5f9dba48072 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Wed, 11 Feb 2026 11:57:30 +0530 Subject: [PATCH 19/49] Patching Engine Fix No more code duplication. The patching logic is present in a central engine that both the cli and the gui can call to make it run however they want. --- build.gradle.kts | 4 - gradle.properties | 1 - .../app/morphe/cli/command/PatchCommand.kt | 339 ++++++------------ .../util => engine}/ApkLibraryStripper.kt | 2 +- .../kotlin/app/morphe/engine/PatchEngine.kt | 322 +++++++++++++++++ .../kotlin/app/morphe/gui/data/model/Patch.kt | 2 +- .../patches/PatchSelectionViewModel.kt | 16 +- .../ui/screens/patching/PatchingViewModel.kt | 2 +- .../app/morphe/gui/util/PatchService.kt | 206 ++--------- 9 files changed, 468 insertions(+), 426 deletions(-) rename src/main/kotlin/app/morphe/{gui/util => engine}/ApkLibraryStripper.kt (99%) create mode 100644 src/main/kotlin/app/morphe/engine/PatchEngine.kt diff --git a/build.gradle.kts b/build.gradle.kts index 19c4ce3..fc79f08 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -159,10 +159,6 @@ tasks { "/prebuilt/windows/aapt.exe", "/prebuilt/*/aapt_*", ) - exclude("/prebuilt/linux/aapt") - exclude("/prebuilt/windows/aapt.exe") - exclude("/prebuilt/*/aapt_*") - minimize { exclude(dependency("org.bouncycastle:.*")) exclude(dependency("app.morphe:morphe-patcher")) diff --git a/gradle.properties b/gradle.properties index 9b4b907..fd63a01 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,4 +2,3 @@ org.gradle.parallel = true org.gradle.caching = true kotlin.code.style = official version = 1.4.0-dev.2 -compose.resources.generated.internal = never diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 43be69e..7e1a424 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -1,21 +1,14 @@ package app.morphe.cli.command -import app.morphe.cli.command.model.FailedPatch import app.morphe.cli.command.model.PatchingResult import app.morphe.cli.command.model.PatchingStep +import app.morphe.cli.command.model.PatchingStepResult import app.morphe.cli.command.model.addStepResult import app.morphe.cli.command.model.toSerializablePatch -import app.morphe.gui.util.ApkLibraryStripper +import app.morphe.engine.PatchEngine import app.morphe.library.ApkUtils -import app.morphe.library.ApkUtils.applyTo import app.morphe.library.installation.installer.* -import app.morphe.library.setOptions -import app.morphe.patcher.Patcher -import app.morphe.patcher.PatcherConfig -import app.morphe.patcher.patch.Patch import app.morphe.patcher.patch.loadPatchesFromJar -import com.reandroid.apkeditor.merge.Merger -import com.reandroid.apkeditor.merge.MergerOptions import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -26,9 +19,8 @@ import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Spec import java.io.File -import java.io.PrintWriter -import java.io.StringWriter import java.util.logging.Logger +import app.morphe.cli.command.model.FailedPatch as CliFailedPatch @OptIn(ExperimentalSerializationApi::class) @CommandLine.Command( @@ -265,8 +257,6 @@ internal object PatchCommand : Runnable { private var striplibs: List = emptyList() override fun run() { - // region Setup - val outputFilePath = outputFilePath ?: File("").absoluteFile.resolve( "${apk.nameWithoutExtension}-patched.apk", @@ -281,6 +271,7 @@ internal object PatchCommand : Runnable { keyStoreFilePath ?: outputFilePath.parentFile .resolve("${outputFilePath.nameWithoutExtension}.keystore") + // Set up ADB installer (CLI-only) val installer = if (deviceSerial != null) { val deviceSerial = deviceSerial!!.ifEmpty { null } @@ -309,164 +300,113 @@ internal object PatchCommand : Runnable { null } - // endregion - - // region Load patches - - logger.info("Loading patches") - - val patches = loadPatchesFromJar(patchesFiles) - - // endregion - - val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") - - // Checking if the file is in apkm format (like reddit) - var mergedApkToCleanup: File? = null - val inputApk = if (apk.extension.equals("apkm", ignoreCase = true)) { - logger.info("Merging APKM bundle") + // Resolve --ei/--di indices to patch names by pre-loading patches + val patchesList = loadPatchesFromJar(patchesFiles).toList() - // Save merged APK to output directory (will be cleaned up after patching) - val outputApk = outputFilePath.parentFile.resolve("${apk.nameWithoutExtension}-merged.apk") - - // Use APKEditor's Merger directly (handles extraction and merging) - val mergerOptions = MergerOptions().apply { - inputFile = apk // Original APKM file - outputFile = outputApk - cleanMeta = true + val enabledPatchNames = selection.mapNotNull { sel -> + sel.enabled?.let { en -> + en.selector.name ?: patchesList.getOrNull(en.selector.index!!)?.name } - Merger(mergerOptions).run() + }.toSet() - mergedApkToCleanup = outputApk - outputApk - } else { - apk - } + val disabledPatchNames = selection.mapNotNull { sel -> + sel.disable?.let { dis -> + dis.selector.name ?: patchesList.getOrNull(dis.selector.index!!)?.name + } + }.filterNotNull().toSet() + + // Build options map: Map> + val patchOptions = selection.filter { it.enabled != null } + .associate { sel -> + val en = sel.enabled!! + val name = en.selector.name ?: patchesList[en.selector.index!!].name!! + name to en.options.toMap() + } + .filter { it.value.isNotEmpty() } + + val config = PatchEngine.Config( + inputApk = apk, + patches = patchesList.toSet(), + outputApk = outputFilePath, + enabledPatches = enabledPatchNames, + disabledPatches = disabledPatchNames, + exclusiveMode = exclusive, + forceCompatibility = force, + patchOptions = patchOptions, + unsigned = mount || unsigned, + signerName = signer, + keystoreDetails = ApkUtils.KeyStoreDetails( + keystoreFilePath, + keyStorePassword, + keyStoreEntryAlias, + keyStoreEntryPassword, + ), + architecturesToKeep = striplibs, + aaptBinaryPath = aaptBinaryPath, + tempDir = temporaryFilesPath, + ) val patchingResult = PatchingResult() try { - val (packageName, patcherResult) = Patcher( - PatcherConfig( - inputApk, - patcherTemporaryFilesPath, - aaptBinaryPath?.path, - patcherTemporaryFilesPath.absolutePath, - ), - ).use { patcher -> - val packageName = patcher.context.packageMetadata.packageName - val packageVersion = patcher.context.packageMetadata.packageVersion - - patchingResult.packageName = packageName - patchingResult.packageVersion = packageVersion - - val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) - - logger.info("Setting patch options") - - val patchesList = patches.toList() - selection.filter { it.enabled != null }.associate { - val enabledSelection = it.enabled!! - - (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to - enabledSelection.options - }.let(filteredPatches::setOptions) - - patcher += filteredPatches - - // Execute patches. - patchingResult.addStepResult( - PatchingStep.PATCHING, - { - runBlocking { - patcher().collect { patchResult -> - patchResult.exception?.let { exception -> - StringWriter().use { writer -> - exception.printStackTrace(PrintWriter(writer)) - - logger.severe("\"${patchResult.patch}\" failed:\n$writer") - - patchingResult.failedPatches.add( - FailedPatch( - patchResult.patch.toSerializablePatch(), - writer.toString() - ) - ) - patchingResult.success = false - } - } ?: patchResult.patch.let { - patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) - logger.info("\"${patchResult.patch}\" succeeded") - } - } - } - } - ) - - patcher.context.packageMetadata.packageName to patcher.get() + val engineResult = runBlocking { + PatchEngine.patch(config) { msg -> logger.info(msg) } } - // region Save. + patchingResult.packageName = engineResult.packageName + patchingResult.packageVersion = engineResult.packageVersion + patchingResult.success = engineResult.success + + // Map engine step results to CLI model for --result-file + engineResult.stepResults.forEach { step -> + val cliStep = when (step.step) { + PatchEngine.PatchStep.PATCHING -> PatchingStep.PATCHING + PatchEngine.PatchStep.REBUILDING -> PatchingStep.REBUILDING + PatchEngine.PatchStep.STRIPPING_LIBS -> PatchingStep.STRIPPING_LIBS + PatchEngine.PatchStep.SIGNING -> PatchingStep.SIGNING + } + patchingResult.patchingSteps.add(PatchingStepResult(cliStep, step.success, step.error)) + } - inputApk.copyTo(temporaryFilesPath.resolve(inputApk.name), overwrite = true).apply { - patchingResult.addStepResult( - PatchingStep.REBUILDING, - { - patcherResult.applyTo(this) - } - ) - }.let { patchedApkFile -> - if (!mount && !unsigned) { - patchingResult.addStepResult( - PatchingStep.SIGNING, - { - ApkUtils.signApk( - patchedApkFile, - outputFilePath, - signer, - ApkUtils.KeyStoreDetails( - keystoreFilePath, - keyStorePassword, - keyStoreEntryAlias, - keyStoreEntryPassword, - ), - ) - } - ) - } else { - patchedApkFile.copyTo(outputFilePath, overwrite = true) + engineResult.appliedPatches.forEach { name -> + patchesList.find { it.name == name }?.let { + patchingResult.appliedPatches.add(it.toSerializablePatch()) + } + } + engineResult.failedPatches.forEach { failed -> + patchesList.find { it.name == failed.name }?.let { + patchingResult.failedPatches.add(CliFailedPatch(it.toSerializablePatch(), failed.error)) } } logger.info("Saved to $outputFilePath") - // endregion - - // region Install. - - deviceSerial?.let { - patchingResult.addStepResult( - PatchingStep.INSTALLING, - { - runBlocking { - val result = installer!!.install(Installer.Apk(outputFilePath, packageName)) - when (result) { - RootInstallerResult.FAILURE -> { - logger.severe("Failed to mount the patched APK file") - throw IllegalStateException("Failed to mount the patched APK file") - } - is AdbInstallerResult.Failure -> { - logger.severe(result.exception.toString()) - throw result.exception + // ADB install (CLI-only) + if (engineResult.success) { + deviceSerial?.let { + patchingResult.addStepResult( + PatchingStep.INSTALLING, + { + runBlocking { + val result = installer!!.install( + Installer.Apk(outputFilePath, engineResult.packageName), + ) + when (result) { + RootInstallerResult.FAILURE -> { + logger.severe("Failed to mount the patched APK file") + throw IllegalStateException("Failed to mount the patched APK file") + } + is AdbInstallerResult.Failure -> { + logger.severe(result.exception.toString()) + throw result.exception + } + else -> logger.info("Installed the patched APK file") } - else -> logger.info("Installed the patched APK file") } - } - } - ) + }, + ) + } } - - // endregion } finally { patchingResultOutputFilePath?.let { outputFile -> outputFile.outputStream().use { outputStream -> @@ -478,92 +418,13 @@ internal object PatchCommand : Runnable { if (purge) { logger.info("Purging temporary files") - purge(temporaryFilesPath) - } - - // Clean up merged APK if we created one from APKM - mergedApkToCleanup?.let { - if (!it.delete()) { - logger.warning("Could not clean up merged APK: ${it.path}") - } - } - } - - /** - * Filter the patches based on the selection. - * - * @param packageName The package name of the APK file to be patched. - * @param packageVersion The version of the APK file to be patched. - * @return The filtered patches. - */ - private fun Set>.filterPatchSelection( - packageName: String, - packageVersion: String, - ): Set> = buildSet { - val enabledPatchesByName = - selection.mapNotNull { it.enabled?.selector?.name }.toSet() - val enabledPatchesByIndex = - selection.mapNotNull { it.enabled?.selector?.index }.toSet() - - val disabledPatches = - selection.mapNotNull { it.disable?.selector?.name }.toSet() - val disabledPatchesByIndex = - selection.mapNotNull { it.disable?.selector?.index }.toSet() - - this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) -> - val patchName = patch.name!! - - val isManuallyDisabled = patchName in disabledPatches || i in disabledPatchesByIndex - if (isManuallyDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually") - - // Make sure the patch is compatible with the supplied APK files package name and version. - patch.compatiblePackages?.let { packages -> - packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) -> - if (versions?.isEmpty() == true) { - return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"") - } - - val matchesVersion = - force || versions?.let { it.any { version -> version == packageVersion } } ?: true - - if (!matchesVersion) { - return@patchLoop logger.warning( - "\"$patchName\" incompatible with $packageName $packageVersion " + - "but compatible with " + - packages.joinToString("; ") { (packageName, versions) -> - packageName + " " + versions!!.joinToString(", ") - }, - ) - } - } ?: return@patchLoop logger.fine( - "\"$patchName\" incompatible with $packageName. " + - "It is only compatible with " + - packages.joinToString(", ") { (name, _) -> name }, - ) - - return@let - } ?: logger.fine("\"$patchName\" has no package constraints") - - val isEnabled = !exclusive && patch.use - val isManuallyEnabled = patchName in enabledPatchesByName || i in enabledPatchesByIndex - - if (!(isEnabled || isManuallyEnabled)) { - return@patchLoop logger.info("\"$patchName\" disabled") - } - - add(patch) - - logger.fine("\"$patchName\" added") + val result = + if (temporaryFilesPath.deleteRecursively()) { + "Purged resource cache directory" + } else { + "Failed to purge resource cache directory" + } + logger.info(result) } } - - private fun purge(resourceCachePath: File) { - val result = - if (resourceCachePath.deleteRecursively()) { - "Purged resource cache directory" - } else { - "Failed to purge resource cache directory" - } - logger.info(result) - } } diff --git a/src/main/kotlin/app/morphe/gui/util/ApkLibraryStripper.kt b/src/main/kotlin/app/morphe/engine/ApkLibraryStripper.kt similarity index 99% rename from src/main/kotlin/app/morphe/gui/util/ApkLibraryStripper.kt rename to src/main/kotlin/app/morphe/engine/ApkLibraryStripper.kt index 53dfbf4..f2a30d1 100644 --- a/src/main/kotlin/app/morphe/gui/util/ApkLibraryStripper.kt +++ b/src/main/kotlin/app/morphe/engine/ApkLibraryStripper.kt @@ -1,4 +1,4 @@ -package app.morphe.gui.util +package app.morphe.engine import java.io.File import java.util.logging.Logger diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt new file mode 100644 index 0000000..5a319a8 --- /dev/null +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -0,0 +1,322 @@ +package app.morphe.engine + +import app.morphe.library.ApkUtils +import app.morphe.library.ApkUtils.applyTo +import app.morphe.library.setOptions +import app.morphe.patcher.Patcher +import app.morphe.patcher.PatcherConfig +import app.morphe.patcher.patch.Patch +import com.reandroid.apkeditor.merge.Merger +import com.reandroid.apkeditor.merge.MergerOptions +import kotlinx.coroutines.ensureActive +import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.nio.file.Files +import kotlin.coroutines.coroutineContext + +/** + * Single patching pipeline shared by CLI and GUI. + */ +object PatchEngine { + + enum class PatchStep { + PATCHING, REBUILDING, STRIPPING_LIBS, SIGNING + } + + data class StepResult(val step: PatchStep, val success: Boolean, val error: String? = null) + + data class Config( + val inputApk: File, + val patches: Set>, + val outputApk: File, + val enabledPatches: Set = emptySet(), + val disabledPatches: Set = emptySet(), + val exclusiveMode: Boolean = false, + val forceCompatibility: Boolean = false, + val patchOptions: Map> = emptyMap(), + val unsigned: Boolean = false, + val signerName: String = "Morphe", + val keystoreDetails: ApkUtils.KeyStoreDetails? = null, + val architecturesToKeep: List = emptyList(), + val aaptBinaryPath: File? = null, + val tempDir: File? = null, + val failOnError: Boolean = true, + ) + + data class Result( + val success: Boolean, + val outputPath: String, + val packageName: String, + val packageVersion: String, + val appliedPatches: List, + val failedPatches: List, + val stepResults: List, + ) + + data class FailedPatch(val name: String, val error: String) + + /** + * The single patching pipeline. + * CLI wraps with runBlocking, GUI calls from coroutine scope. + * + * Always returns a [Result] — does not throw for pipeline step failures. + * Only throws for init errors (e.g. Patcher can't open the APK). + */ + suspend fun patch(config: Config, onProgress: (String) -> Unit = {}): Result { + val tempDir = config.tempDir ?: Files.createTempDirectory("morphe-patching").toFile() + var mergedApkToCleanup: File? = null + val stepResults = mutableListOf() + val appliedPatches = mutableListOf() + val failedPatches = mutableListOf() + + try { + // 1. Handle APKM format (split APK bundle) + val actualInputApk = if (config.inputApk.extension.equals("apkm", ignoreCase = true)) { + onProgress("Converting APKM to APK...") + val mergedApk = File(tempDir, "${config.inputApk.nameWithoutExtension}-merged.apk") + val mergerOptions = MergerOptions().apply { + inputFile = config.inputApk + outputFile = mergedApk + cleanMeta = true + } + Merger(mergerOptions).run() + mergedApkToCleanup = mergedApk + mergedApk + } else { + config.inputApk + } + + coroutineContext.ensureActive() + + // 2. Initialize patcher + val patcherTempDir = File(tempDir, "patcher") + patcherTempDir.mkdirs() + + onProgress("Initializing patcher...") + val patcherConfig = PatcherConfig( + actualInputApk, + patcherTempDir, + config.aaptBinaryPath?.path, + patcherTempDir.absolutePath, + ) + + Patcher(patcherConfig).use { patcher -> + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion + + coroutineContext.ensureActive() + + // 3. Filter patches + onProgress("Filtering patches for $packageName v$packageVersion...") + val filteredPatches = filterPatches( + patches = config.patches, + packageName = packageName, + packageVersion = packageVersion, + enabledPatches = config.enabledPatches, + disabledPatches = config.disabledPatches, + exclusiveMode = config.exclusiveMode, + forceCompatibility = config.forceCompatibility, + onProgress = onProgress, + ) + + coroutineContext.ensureActive() + + // 4. Set options + if (config.patchOptions.isNotEmpty()) { + val relevantOptions = config.patchOptions.filter { it.value.isNotEmpty() } + if (relevantOptions.isNotEmpty()) { + filteredPatches.setOptions(relevantOptions) + } + } + + patcher += filteredPatches + + coroutineContext.ensureActive() + + fun earlyResult() = Result( + success = false, + outputPath = config.outputApk.absolutePath, + packageName = packageName, + packageVersion = packageVersion, + appliedPatches = appliedPatches, + failedPatches = failedPatches, + stepResults = stepResults, + ) + + // 5. Execute patches + onProgress("Applying ${filteredPatches.size} patches...") + try { + patcher().collect { patchResult -> + val patchName = patchResult.patch.name ?: "Unknown" + patchResult.exception?.let { exception -> + val error = StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + writer.toString() + } + onProgress("FAILED: $patchName") + failedPatches.add(FailedPatch(patchName, error)) + + if (config.failOnError) { + throw PatchFailedException( + "Patch \"$patchName\" failed: ${exception.message}", + exception, + ) + } + } ?: run { + onProgress("Applied: $patchName") + appliedPatches.add(patchName) + } + } + stepResults.add(StepResult(PatchStep.PATCHING, failedPatches.isEmpty())) + } catch (e: PatchFailedException) { + stepResults.add(StepResult(PatchStep.PATCHING, false, e.message)) + return earlyResult() + } + + coroutineContext.ensureActive() + + // 6. Rebuild APK + onProgress("Rebuilding APK...") + try { + val patcherResult = patcher.get() + val rebuiltApk = File(tempDir, "rebuilt.apk") + actualInputApk.copyTo(rebuiltApk, overwrite = true) + patcherResult.applyTo(rebuiltApk) + stepResults.add(StepResult(PatchStep.REBUILDING, true)) + } catch (e: Exception) { + stepResults.add(StepResult(PatchStep.REBUILDING, false, e.toString())) + return earlyResult() + } + + val rebuiltApk = File(tempDir, "rebuilt.apk") + + coroutineContext.ensureActive() + + // 7. Strip libs (if configured) + if (config.architecturesToKeep.isNotEmpty()) { + onProgress("Stripping native libraries...") + try { + ApkLibraryStripper.stripLibraries(rebuiltApk, config.architecturesToKeep) { + onProgress(it) + } + stepResults.add(StepResult(PatchStep.STRIPPING_LIBS, true)) + } catch (e: Exception) { + stepResults.add(StepResult(PatchStep.STRIPPING_LIBS, false, e.toString())) + return earlyResult() + } + } + + coroutineContext.ensureActive() + + // 8. Sign APK (unless unsigned) + val tempOutput = File(tempDir, config.outputApk.name) + if (!config.unsigned) { + onProgress("Signing APK...") + try { + val keystoreDetails = config.keystoreDetails ?: ApkUtils.KeyStoreDetails( + File(tempDir, "morphe.keystore"), + null, + "Morphe Key", + "", + ) + ApkUtils.signApk( + rebuiltApk, + tempOutput, + config.signerName, + keystoreDetails, + ) + stepResults.add(StepResult(PatchStep.SIGNING, true)) + } catch (e: Exception) { + stepResults.add(StepResult(PatchStep.SIGNING, false, e.toString())) + return earlyResult() + } + } else { + rebuiltApk.copyTo(tempOutput, overwrite = true) + } + + // 9. Copy to final output + config.outputApk.parentFile?.mkdirs() + tempOutput.copyTo(config.outputApk, overwrite = true) + + onProgress("Patching complete!") + + return Result( + success = failedPatches.isEmpty(), + outputPath = config.outputApk.absolutePath, + packageName = packageName, + packageVersion = packageVersion, + appliedPatches = appliedPatches, + failedPatches = failedPatches, + stepResults = stepResults, + ) + } + } finally { + mergedApkToCleanup?.delete() + if (config.tempDir == null) { + try { + tempDir.deleteRecursively() + } catch (_: Exception) { + // Best effort cleanup + } + } + } + } + + /** + * Unified patch filtering logic. + * Filters patches based on compatibility, enabled/disabled lists, and exclusive mode. + */ + private fun filterPatches( + patches: Set>, + packageName: String, + packageVersion: String, + enabledPatches: Set, + disabledPatches: Set, + exclusiveMode: Boolean, + forceCompatibility: Boolean, + onProgress: (String) -> Unit, + ): Set> = buildSet { + patches.forEach patchLoop@{ patch -> + val patchName = patch.name ?: return@patchLoop + + // Check if explicitly disabled + if (patchName in disabledPatches) { + onProgress("Skipping disabled: $patchName") + return@patchLoop + } + + // Check package compatibility + patch.compatiblePackages?.let { packages -> + val matchingPkg = packages.singleOrNull { (name, _) -> name == packageName } + if (matchingPkg == null) { + return@patchLoop + } + + val (_, versions) = matchingPkg + if (versions?.isEmpty() == true) { + return@patchLoop + } + + val matchesVersion = forceCompatibility || + versions?.any { it == packageVersion } ?: true + + if (!matchesVersion) { + onProgress("Skipping \"$patchName\": incompatible with $packageName $packageVersion") + return@patchLoop + } + } + + val isManuallyEnabled = patchName in enabledPatches + val isEnabledByDefault = !exclusiveMode && patch.use + + if (!(isEnabledByDefault || isManuallyEnabled)) { + return@patchLoop + } + + add(patch) + } + } + + private class PatchFailedException(message: String, cause: Throwable) : Exception(message, cause) +} diff --git a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt index e3c11e2..2f940a2 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt @@ -80,5 +80,5 @@ data class PatchConfig( val disabledPatches: List = emptyList(), val patchOptions: Map = emptyMap(), val useExclusiveMode: Boolean = false, - val riplibs: List = emptyList() + val striplibs: List = emptyList() ) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt index 3a86325..278691b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -192,7 +192,7 @@ class PatchSelectionViewModel( .map { it.name } // Only set riplibs if user deselected any architecture (keeps = selected ones) - val riplibs = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { + val striplibs = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { _uiState.value.selectedArchitectures.toList() } else { emptyList() @@ -205,7 +205,7 @@ class PatchSelectionViewModel( enabledPatches = selectedPatchNames, disabledPatches = disabledPatchNames, useExclusiveMode = true, - riplibs = riplibs + striplibs = striplibs ) } @@ -236,8 +236,8 @@ class PatchSelectionViewModel( .filter { _uiState.value.selectedPatches.contains(it.uniqueId) } .map { it.name } - // riplibs flag: only when user deselected at least one architecture - val riplibsArg = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { + // striplibs flag: only when user deselected at least one architecture + val striplibsArg = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { _uiState.value.selectedArchitectures.joinToString(",") } else { null @@ -250,8 +250,8 @@ class PatchSelectionViewModel( sb.append(" -o ${outputFileName} \\\n") sb.append(" --exclusive \\\n") - if (riplibsArg != null) { - sb.append(" --riplibs $riplibsArg \\\n") + if (striplibsArg != null) { + sb.append(" --striplibs $striplibsArg \\\n") } selectedPatchNames.forEachIndexed { index, patch -> @@ -268,8 +268,8 @@ class PatchSelectionViewModel( } else { // Compact mode - single line that wraps naturally val patches = selectedPatchNames.joinToString(" ") { "-e \"$it\"" } - val riplibsPart = if (riplibsArg != null) " --riplibs $riplibsArg" else "" - "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --exclusive$riplibsPart $patches ${inputFile.name}" + val striplibsPart = if (striplibsArg != null) " --striplibs $striplibsArg" else "" + "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --exclusive$striplibsPart $patches ${inputFile.name}" } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt index edf093e..e5e8326 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt @@ -56,7 +56,7 @@ class PatchingViewModel( disabledPatches = config.disabledPatches, options = config.patchOptions, exclusiveMode = config.useExclusiveMode, - riplibs = config.riplibs, + striplibs = config.striplibs, onProgress = { message -> parseAndAddLog(message) } diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 16478cc..2902104 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -1,23 +1,14 @@ package app.morphe.gui.util +import app.morphe.engine.PatchEngine import app.morphe.gui.data.model.CompatiblePackage import app.morphe.gui.data.model.Patch import app.morphe.gui.data.model.PatchOption import app.morphe.gui.data.model.PatchOptionType -import app.morphe.library.ApkUtils -import app.morphe.library.ApkUtils.applyTo -import app.morphe.library.setOptions -import app.morphe.patcher.Patcher -import app.morphe.patcher.PatcherConfig import app.morphe.patcher.patch.loadPatchesFromJar -import com.reandroid.apkeditor.merge.Merger -import com.reandroid.apkeditor.merge.MergerOptions import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File -import java.io.PrintWriter -import java.io.StringWriter import kotlin.reflect.KType import app.morphe.patcher.patch.Patch as LibraryPatch @@ -76,6 +67,7 @@ class PatchService { /** * Execute patching operation with progress callbacks. + * Delegates to PatchEngine for the actual pipeline. */ suspend fun patch( patchesFilePath: String, @@ -85,12 +77,9 @@ class PatchService { disabledPatches: List = emptyList(), options: Map = emptyMap(), exclusiveMode: Boolean = false, - riplibs: List = emptyList(), + striplibs: List = emptyList(), onProgress: (String) -> Unit = {} ): Result = withContext(Dispatchers.IO) { - val tempDir = FileUtils.createPatchingTempDir() - val tempOutputPath = File(tempDir, File(outputApkPath).name) - try { val patchFile = File(patchesFilePath) val inputApk = File(inputApkPath) @@ -103,171 +92,46 @@ class PatchService { return@withContext Result.failure(Exception("Input APK not found")) } + // Load patches (copy to temp to avoid Windows file lock) onProgress("Loading patches...") - // Copy to temp file so URLClassLoader locks the copy, not the cached original. - val patchTempCopy = File(tempDir, patchFile.name) - patchFile.copyTo(patchTempCopy, overwrite = true) - val patches = loadPatchesFromJar(setOf(patchTempCopy)) - - // Handle APKM format (split APK bundle) - var mergedApkToCleanup: File? = null - val actualInputApk = if (inputApk.extension.equals("apkm", ignoreCase = true)) { - onProgress("Converting APKM to APK...") - val mergedApk = File(tempDir, "${inputApk.nameWithoutExtension}-merged.apk") - val mergerOptions = MergerOptions().apply { - this.inputFile = inputApk - this.outputFile = mergedApk - cleanMeta = true - } - Merger(mergerOptions).run() - mergedApkToCleanup = mergedApk - mergedApk - } else { - inputApk - } - - val patcherTempDir = File(tempDir, "patcher") - patcherTempDir.mkdirs() - - onProgress("Initializing patcher...") - val patcherConfig = PatcherConfig( - actualInputApk, - patcherTempDir, - null, // aapt binary path - patcherTempDir.absolutePath - ) - - val appliedPatches = mutableListOf() - val failedPatches = mutableListOf>() - - Patcher(patcherConfig).use { patcher -> - val packageName = patcher.context.packageMetadata.packageName - val packageVersion = patcher.context.packageMetadata.packageVersion - - onProgress("Filtering patches for $packageName v$packageVersion...") - - // Filter patches based on compatibility and selection - val filteredPatches = patches.filter { patch -> - val patchName = patch.name ?: return@filter false - - // Check if explicitly disabled - if (patchName in disabledPatches) { - onProgress("Skipping disabled: $patchName") - return@filter false - } - - // Check package compatibility - val isCompatible = patch.compatiblePackages?.let { packages -> - packages.any { (name, versions) -> - name == packageName && (versions?.isEmpty() != false || versions.contains(packageVersion)) - } - } ?: true // Universal patches - - if (!isCompatible) { - return@filter false - } - - // In exclusive mode, only include explicitly enabled patches - if (exclusiveMode) { - patchName in enabledPatches - } else { - // Include if: enabled by default OR explicitly enabled - patch.use || patchName in enabledPatches - } - }.toSet() - - onProgress("Applying ${filteredPatches.size} patches...") - - // Set patch options if any - if (options.isNotEmpty()) { - val optionsMap = enabledPatches.associateWith { patchName -> - options.filterKeys { it.startsWith("$patchName.") } - .mapKeys { it.key.removePrefix("$patchName.") } - .mapValues { it.value as Any? } - .toMutableMap() - }.filter { it.value.isNotEmpty() } - - if (optionsMap.isNotEmpty()) { - filteredPatches.setOptions(optionsMap) - } - } - - patcher += filteredPatches - - // Execute patches - runBlocking { - patcher().collect { patchResult -> - val patchName = patchResult.patch.name ?: "Unknown" - patchResult.exception?.let { exception -> - val error = StringWriter().use { writer -> - exception.printStackTrace(PrintWriter(writer)) - writer.toString() - } - onProgress("FAILED: $patchName") - Logger.error("Patch failed: $patchName\n$error") - failedPatches.add(patchName to error) - } ?: run { - onProgress("Applied: $patchName") - Logger.info("Patch applied: $patchName") - appliedPatches.add(patchName) - } - } - } - - // Get patcher result - val patcherResult = patcher.get() - - onProgress("Rebuilding APK...") - val rebuiltApk = File(tempDir, "rebuilt.apk") - actualInputApk.copyTo(rebuiltApk, overwrite = true) - patcherResult.applyTo(rebuiltApk) - - if (riplibs.isNotEmpty()) { - onProgress("Stripping native libraries...") - ApkLibraryStripper.stripLibraries(rebuiltApk, riplibs) { onProgress(it) } - } - - onProgress("Signing APK...") - val keystorePath = File(tempDir, "morphe.keystore") - ApkUtils.signApk( - rebuiltApk, - tempOutputPath, - "Morphe", - ApkUtils.KeyStoreDetails( - keystorePath, - null, // password - "Morphe Key", - "" // entry password - ) + val patchTempCopy = File.createTempFile("morphe-patches-", ".mpp") + try { + patchFile.copyTo(patchTempCopy, overwrite = true) + val loadedPatches = loadPatchesFromJar(setOf(patchTempCopy)) + + // Convert GUI's flat "patchName.optionKey" -> value map + // to engine's Map> format + val patchOptions = enabledPatches.associateWith { patchName -> + options.filterKeys { it.startsWith("$patchName.") } + .mapKeys { it.key.removePrefix("$patchName.") } + .mapValues { it.value as Any? } + }.filter { it.value.isNotEmpty() } + + val config = PatchEngine.Config( + inputApk = inputApk, + patches = loadedPatches, + outputApk = outputFile, + enabledPatches = enabledPatches.toSet(), + disabledPatches = disabledPatches.toSet(), + exclusiveMode = exclusiveMode, + patchOptions = patchOptions, + architecturesToKeep = striplibs, ) - // Move to final location - outputFile.parentFile?.mkdirs() - tempOutputPath.copyTo(outputFile, overwrite = true) - - onProgress("Patching complete!") - Logger.info("Patched APK saved to: ${outputFile.absolutePath}") + val engineResult = PatchEngine.patch(config, onProgress) - // Cleanup merged APK if created - mergedApkToCleanup?.delete() + Result.success(PatchResult( + success = engineResult.success, + outputPath = engineResult.outputPath, + appliedPatches = engineResult.appliedPatches, + failedPatches = engineResult.failedPatches.map { it.name }, + )) + } finally { + patchTempCopy.delete() } - - Result.success(PatchResult( - success = failedPatches.isEmpty(), - outputPath = outputFile.absolutePath, - appliedPatches = appliedPatches, - failedPatches = failedPatches.map { it.first } - )) } catch (e: Exception) { Logger.error("Patching failed", e) Result.failure(e) - } finally { - // Cleanup temp directory - try { - tempDir.deleteRecursively() - } catch (e: Exception) { - Logger.warn("Failed to cleanup temp directory: ${e.message}") - } } } From bf224f64b1be57a9b7d0c20448ff8be036092de3 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Thu, 12 Feb 2026 01:36:20 +0530 Subject: [PATCH 20/49] Minor Fixes Minor fixes for various stuff --- src/main/kotlin/app/morphe/gui/App.kt | 9 +- .../morphe/gui/data/constants/AppConstants.kt | 118 +--------------- .../gui/data/repository/PatchRepository.kt | 30 ++++- .../kotlin/app/morphe/gui/di/AppModule.kt | 2 +- .../gui/ui/components/SettingsDialog.kt | 25 +++- .../morphe/gui/ui/screens/home/HomeScreen.kt | 127 +----------------- .../gui/ui/screens/home/HomeViewModel.kt | 74 ++-------- .../ui/screens/home/components/ApkInfoCard.kt | 36 +---- .../screens/patches/PatchSelectionScreen.kt | 3 +- .../patches/PatchSelectionViewModel.kt | 60 +++++---- .../gui/ui/screens/quick/QuickPatchScreen.kt | 19 +-- .../ui/screens/quick/QuickPatchViewModel.kt | 38 ++---- 12 files changed, 134 insertions(+), 407 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index 5bd715d..8ff6286 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -111,12 +111,15 @@ private fun AppContent(initialSimplifiedMode: Boolean) { ) { Surface(modifier = Modifier.fillMaxSize()) { if (!isLoading) { + // Create QuickPatchViewModel outside Crossfade so it persists across mode switches. + // Otherwise every expert→simplified switch creates a new VM that re-fetches from GitHub. + val quickViewModel = remember { + QuickPatchViewModel(patchRepository, patchService, configRepository) + } + Crossfade(targetState = isSimplifiedMode) { simplified -> if (simplified) { // Quick/Simplified mode - val quickViewModel = remember { - QuickPatchViewModel(patchRepository, patchService, configRepository) - } QuickPatchContent(quickViewModel) } else { // Full mode diff --git a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt index 3ca5a23..ba11011 100644 --- a/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt +++ b/src/main/kotlin/app/morphe/gui/data/constants/AppConstants.kt @@ -2,7 +2,7 @@ package app.morphe.gui.data.constants /** * Centralized configuration for supported apps. - * Update version, URL, and checksum here - changes will reflect throughout the app. + * This file is massively outdated. Could be used for other things in the future but kinda useless now. */ object AppConstants { @@ -17,24 +17,12 @@ object AppConstants { object YouTube { const val DISPLAY_NAME = "YouTube" const val PACKAGE_NAME = "com.google.android.youtube" - const val SUGGESTED_VERSION = "20.40.45" - - // SHA-256 checksum from APKMirror (leave null if not verified) - // You can find this on the APKMirror download page under "File SHA-256" - val SHA256_CHECKSUM: String? = "b7659da492a1ebd8bd7cea2909be4ee1f58e00a2586d65a1c91b2e1e5ec6acd1" } // ==================== YOUTUBE MUSIC ==================== object YouTubeMusic { const val DISPLAY_NAME = "YouTube Music" const val PACKAGE_NAME = "com.google.android.apps.youtube.music" - const val SUGGESTED_VERSION = "8.40.54" - val SHA256_CHECKSUMS: Map = mapOf( - "arm64-v8a" to "d5b44919a5cd5648b01e392115fe68b9569b1c7847f3cdf65b1ace1302d005d2", - "armeabi-v7a" to "6f5181e8aaa2595af6c421b86ffffcc1c7a4e97968d7be89d04b46776392eaec", - "x86" to "03b1eb6993d43b1de6a9416828df7864be975ca6dd3a82468c431e3c193f3a80", - "x86_64" to "eab4cd51220b28c7108343cdb95a063251029f9a137d052a519d007a9321c848" - ) } // ==================== REDDIT ==================== @@ -52,106 +40,6 @@ object AppConstants { Reddit.PACKAGE_NAME ) - /** - * Get suggested version for a package name. - */ - fun getSuggestedVersion(packageName: String): String? { - return when (packageName) { - YouTube.PACKAGE_NAME -> YouTube.SUGGESTED_VERSION - YouTubeMusic.PACKAGE_NAME -> YouTubeMusic.SUGGESTED_VERSION - else -> null - } - } - - /** - * Get checksum for a package name, version, and architecture. - * @param packageName The app's package name - * @param version The app version - * @param architectures List of architectures in the APK (from lib/ folder) - * @return The expected checksum, or null if not configured/version mismatch - */ - fun getChecksum(packageName: String, version: String, architectures: List = emptyList()): String? { - return when (packageName) { - YouTube.PACKAGE_NAME -> { - // YouTube has a universal APK with single checksum - if (version == YouTube.SUGGESTED_VERSION) YouTube.SHA256_CHECKSUM else null - } - YouTubeMusic.PACKAGE_NAME -> { - if (version != YouTubeMusic.SUGGESTED_VERSION) return null - if (YouTubeMusic.SHA256_CHECKSUMS.isEmpty()) return null - - // Try to find matching architecture checksum - // Check for universal first, then specific architectures - YouTubeMusic.SHA256_CHECKSUMS["universal"] - ?: architectures.firstNotNullOfOrNull { arch -> - YouTubeMusic.SHA256_CHECKSUMS[arch] - } - } - else -> null - } - } - - /** - * Check if we have any checksum configured for this package/version/architecture combo. - */ - fun hasChecksumConfigured(packageName: String, version: String, architectures: List = emptyList()): Boolean { - return getChecksum(packageName, version, architectures) != null - } - - /** - * Check if this is the recommended version. - */ - fun isRecommendedVersion(packageName: String, version: String): Boolean { - return getSuggestedVersion(packageName) == version - } - - // ==================== PATCH RECOMMENDATIONS ==================== - - /** - * Patches that are commonly disabled by users. - * These patches change default behavior in ways that some users may not want. - * The key is a partial match (case-insensitive) against patch names. - */ - object PatchRecommendations { - /** - * Patches commonly disabled for YouTube. - * Pair of (patch name pattern, reason for commonly disabling) - */ - val YOUTUBE_COMMONLY_DISABLED: List> = listOf( - "Custom Branding" to "Keeps the original name and logo for the app", -// "Hide ads" to "Some users prefer keeping ads to support creators", -// "Premium heading" to "Changes the YouTube logo/heading appearance", -// "Navigation buttons" to "Modifies bottom navigation bar layout", -// "Spoof client" to "May cause playback issues on some devices", -// "Disable auto captions" to "Some users rely on auto-generated captions" - ) - - /** - * Patches commonly disabled for YouTube Music. - */ - val YOUTUBE_MUSIC_COMMONLY_DISABLED: List> = listOf( - "Custom Branding" to "Keeps the original name and logo for the app", -// "Spoof client" to "May cause playback issues on some devices" - ) - - /** - * Patches commonly disabled for Reddit. - */ - val REDDIT_COMMONLY_DISABLED: List> = listOf( - "Change package name" to "Doesn't work for reddit", - "Spoof signature" to "May cause issues on some devices" - ) - - /** - * Get commonly disabled patches for a package. - */ - fun getCommonlyDisabled(packageName: String): List> { - return when (packageName) { - YouTube.PACKAGE_NAME -> YOUTUBE_COMMONLY_DISABLED - YouTubeMusic.PACKAGE_NAME -> YOUTUBE_MUSIC_COMMONLY_DISABLED - Reddit.PACKAGE_NAME -> REDDIT_COMMONLY_DISABLED - else -> emptyList() - } - } - } + // TODO: Checksum verification will be re-enabled when checksums are added to .mpp files + // For now, checksums are not validated. See ChecksumUtils.kt for the verification logic. } diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt index b2209c3..c73baca 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt @@ -23,12 +23,25 @@ class PatchRepository( private const val GITHUB_API_BASE = "https://api.github.com" private const val PATCHES_REPO = "MorpheApp/morphe-patches" private const val RELEASES_ENDPOINT = "$GITHUB_API_BASE/repos/$PATCHES_REPO/releases" + private const val CACHE_TTL_MS = 5 * 60 * 1000L // 5 minutes } + // In-memory cache so multiple callers (both modes) don't re-fetch from GitHub + private var cachedReleases: List? = null + private var cacheTimestamp: Long = 0L + /** - * Fetch all releases from GitHub. + * Fetch all releases from GitHub. Returns cached result if still fresh. + * @param forceRefresh bypass the in-memory cache */ - suspend fun fetchReleases(): Result> = withContext(Dispatchers.IO) { + suspend fun fetchReleases(forceRefresh: Boolean = false): Result> = withContext(Dispatchers.IO) { + // Return cached releases if still fresh + val cached = cachedReleases + if (!forceRefresh && cached != null && (System.currentTimeMillis() - cacheTimestamp) < CACHE_TTL_MS) { + Logger.info("Using cached releases (${cached.size} releases, age=${(System.currentTimeMillis() - cacheTimestamp) / 1000}s)") + return@withContext Result.success(cached) + } + try { Logger.info("Fetching releases from $RELEASES_ENDPOINT") val response: HttpResponse = httpClient.get(RELEASES_ENDPOINT) { @@ -41,6 +54,8 @@ class PatchRepository( if (response.status.isSuccess()) { val releases: List = response.body() Logger.info("Fetched ${releases.size} releases") + cachedReleases = releases + cacheTimestamp = System.currentTimeMillis() Result.success(releases) } else { val error = "Failed to fetch releases: ${response.status}" @@ -49,7 +64,14 @@ class PatchRepository( } } catch (e: Exception) { Logger.error("Error fetching releases", e) - Result.failure(e) + // If we have stale cached data, return it rather than failing + val stale = cachedReleases + if (stale != null) { + Logger.info("Returning stale cached releases after fetch failure") + Result.success(stale) + } else { + Result.failure(e) + } } } @@ -167,6 +189,8 @@ class PatchRepository( * Delete cached patches. */ fun clearCache(): Boolean { + cachedReleases = null + cacheTimestamp = 0L return try { var failedCount = 0 FileUtils.getPatchesDir().listFiles()?.forEach { file -> diff --git a/src/main/kotlin/app/morphe/gui/di/AppModule.kt b/src/main/kotlin/app/morphe/gui/di/AppModule.kt index b7a31d0..87c7f57 100644 --- a/src/main/kotlin/app/morphe/gui/di/AppModule.kt +++ b/src/main/kotlin/app/morphe/gui/di/AppModule.kt @@ -58,6 +58,6 @@ val appModule = module { // ViewModels (ScreenModels) factory { HomeViewModel(get(), get(), get()) } factory { params -> PatchesViewModel(params.get(), params.get(), get(), get()) } - factory { params -> PatchSelectionViewModel(params.get(), params.get(), params.get(), params.get(), get(), get()) } + factory { params -> PatchSelectionViewModel(params.get(), params.get(), params.get(), params.get(), params.get(), get(), get()) } factory { params -> PatchingViewModel(params.get(), get(), get()) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 99e6e55..a03ba7a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -251,7 +251,7 @@ fun SettingsDialog( // Cache info val cacheSize = calculateCacheSize() Text( - text = "Cache: $cacheSize (CLI + Patches)", + text = "Cache: $cacheSize (Patches + Logs)", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -286,7 +286,7 @@ fun SettingsDialog( shape = RoundedCornerShape(16.dp), title = { Text("Clear Cache?") }, text = { - Text("This will delete downloaded CLI and patch files. They will be re-downloaded when needed.") + Text("This will delete downloaded patch files and log files. Patches will be re-downloaded when needed.") }, confirmButton = { Button( @@ -322,17 +322,21 @@ private fun ThemePreference.toDisplayName(): String { private fun calculateCacheSize(): String { val patchesSize = FileUtils.getPatchesDir().walkTopDown().filter { it.isFile }.sumOf { it.length() } + val logsSize = FileUtils.getLogsDir().walkTopDown().filter { it.isFile }.sumOf { it.length() } + val totalSize = patchesSize + logsSize return when { - patchesSize < 1024 -> "$patchesSize B" - patchesSize < 1024 * 1024 -> "%.1f KB".format(patchesSize / 1024.0) - else -> "%.1f MB".format(patchesSize / (1024.0 * 1024.0)) + totalSize < 1024 -> "$totalSize B" + totalSize < 1024 * 1024 -> "%.1f KB".format(totalSize / 1024.0) + else -> "%.1f MB".format(totalSize / (1024.0 * 1024.0)) } } private fun clearAllCache(): Boolean { return try { var failedCount = 0 + + // Delete patch files FileUtils.getPatchesDir().listFiles()?.forEach { file -> try { java.nio.file.Files.delete(file.toPath()) @@ -341,6 +345,17 @@ private fun clearAllCache(): Boolean { Logger.error("Failed to delete ${file.name}: ${e.message}") } } + + // Delete log files + FileUtils.getLogsDir().listFiles()?.forEach { file -> + try { + java.nio.file.Files.delete(file.toPath()) + } catch (e: Exception) { + failedCount++ + Logger.error("Failed to delete log ${file.name}: ${e.message}") + } + } + FileUtils.cleanupAllTempDirs() if (failedCount > 0) { Logger.error("Cache clear incomplete: $failedCount file(s) could not be deleted (may be locked)") diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 585aa9a..c7e879e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -113,6 +113,7 @@ fun HomeScreenContent( apkPath = uiState.apkInfo!!.filePath, apkName = uiState.apkInfo!!.appName, patchesFilePath = patchesFile.absolutePath, + packageName = uiState.apkInfo!!.packageName, apkArchitectures = uiState.apkInfo!!.architectures )) } @@ -206,6 +207,7 @@ fun HomeScreenContent( apkPath = info.filePath, apkName = info.appName, patchesFilePath = patchesFile.absolutePath, + packageName = info.packageName, apkArchitectures = info.architectures )) } @@ -643,7 +645,7 @@ private fun SupportedAppsSection( // Important notice about APK handling Text( - text = "Download the exact version from APKMirror and drop it here directly. Do not rename or modify the file.", + text = "Download the exact version from APKMirror and drop it here directly.", fontSize = if (isCompact) 10.sp else 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), textAlign = TextAlign.Center, @@ -984,129 +986,6 @@ private fun SupportedAppCardDynamic( } } -@Composable -private fun SupportedAppCard( - appType: AppType, - iconRes: org.jetbrains.compose.resources.DrawableResource, - isCompact: Boolean = false, - modifier: Modifier = Modifier -) { - val cardPadding = if (isCompact) 12.dp else 16.dp - val iconSize = if (isCompact) 48.dp else 56.dp - val iconInnerSize = if (isCompact) 32.dp else 40.dp - - Card( - modifier = modifier, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ), - shape = RoundedCornerShape(if (isCompact) 12.dp else 16.dp) - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(cardPadding), - horizontalAlignment = Alignment.CenterHorizontally - ) { - // App icon - Box( - modifier = Modifier - .size(iconSize) - .clip(RoundedCornerShape(if (isCompact) 10.dp else 12.dp)) - .background(Color.White), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(iconRes), - contentDescription = "${appType.displayName} icon", - modifier = Modifier.size(iconInnerSize) - ) - } - - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) - - // App name - Text( - text = appType.displayName, - fontSize = if (isCompact) 14.sp else 16.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) - - Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) - - // Suggested version badge - Surface( - color = MorpheColors.Teal.copy(alpha = 0.15f), - shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp) - ) { - Column( - modifier = Modifier.padding( - horizontal = if (isCompact) 10.dp else 12.dp, - vertical = if (isCompact) 6.dp else 8.dp - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Recommended", - fontSize = if (isCompact) 9.sp else 10.sp, - color = MorpheColors.Teal.copy(alpha = 0.8f), - letterSpacing = 0.5.sp - ) - Text( - text = "v${appType.suggestedVersion}", - fontSize = if (isCompact) 12.sp else 14.sp, - fontWeight = FontWeight.SemiBold, - color = MorpheColors.Teal - ) - } - } - - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) - - // Download from APKMirror button - val downloadUrl = SupportedApp.getDownloadUrl(appType.packageName, appType.suggestedVersion) - if (downloadUrl != null) { - OutlinedButton( - onClick = { - try { - java.awt.Desktop.getDesktop().browse(java.net.URI(downloadUrl)) - } catch (e: Exception) { - // Ignore errors - } - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp), - contentPadding = PaddingValues( - horizontal = if (isCompact) 8.dp else 12.dp, - vertical = if (isCompact) 6.dp else 8.dp - ), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MorpheColors.Blue - ) - ) { - Text( - text = if (isCompact) "APKMirror" else "Get from APKMirror", - fontSize = if (isCompact) 11.sp else 12.sp, - fontWeight = FontWeight.Medium - ) - } - - Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) - } - - // Package name - Text( - text = appType.packageName, - fontSize = if (isCompact) 9.sp else 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center, - maxLines = 1 - ) - } - } -} - @Composable private fun DragOverlay() { Box( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 141527a..244844b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -313,16 +313,8 @@ class HomeViewModel( val appName = dynamicSupportedApp?.displayName ?: SupportedApp.getDisplayName(packageName) - // Get recommended version - prefer dynamic, fallback to hardcoded + // Get recommended version from dynamic patches data (no hardcoded fallback) val suggestedVersion = dynamicSupportedApp?.recommendedVersion - ?: app.morphe.gui.data.constants.AppConstants.getSuggestedVersion(packageName) - - // Determine AppType for backward compatibility (still used in some places) - val appType = when (packageName) { - app.morphe.gui.data.constants.AppConstants.YouTube.PACKAGE_NAME -> AppType.YOUTUBE - app.morphe.gui.data.constants.AppConstants.YouTubeMusic.PACKAGE_NAME -> AppType.YOUTUBE_MUSIC - else -> null - } // Compare versions if we have a suggested version val versionStatus = if (suggestedVersion != null) { @@ -335,8 +327,8 @@ class HomeViewModel( // For .apkm files, scan the original bundle (splits contain the native libs, not base.apk) val architectures = extractArchitectures(if (isApkm) file else apkToParse) - // Verify checksum (still uses AppConstants for now) - val checksumStatus = verifyChecksum(file, packageName, versionName, architectures, suggestedVersion) + // TODO: Re-enable when checksums are provided via .mpp files + val checksumStatus = app.morphe.gui.util.ChecksumStatus.NotConfigured Logger.info("Parsed APK: $packageName v$versionName (recommended=$suggestedVersion, minSdk=$minSdk, archs=$architectures)") @@ -346,7 +338,6 @@ class HomeViewModel( fileSize = file.length(), formattedSize = formatFileSize(file.length()), appName = appName, - appType = appType, packageName = packageName, versionName = versionName, architectures = architectures, @@ -408,42 +399,11 @@ class HomeViewModel( } } - /** - * Verify the APK checksum against expected values. - */ - private fun verifyChecksum( - file: File, - packageName: String, - version: String, - architectures: List, - recommendedVersion: String? - ): app.morphe.gui.util.ChecksumStatus { - // Check if this is a non-recommended version (use dynamic recommended version) - if (recommendedVersion != null && version != recommendedVersion) { - return app.morphe.gui.util.ChecksumStatus.NonRecommendedVersion - } - - // Get expected checksum (still from AppConstants - checksums are manually maintained) - val expectedChecksum = app.morphe.gui.data.constants.AppConstants.getChecksum(packageName, version, architectures) - if (expectedChecksum == null) { - return app.morphe.gui.util.ChecksumStatus.NotConfigured - } - - // Calculate actual checksum - return try { - val actualChecksum = app.morphe.gui.util.ChecksumUtils.calculateSha256(file) - Logger.info("Checksum verification - Expected: $expectedChecksum, Actual: $actualChecksum") - - if (actualChecksum.equals(expectedChecksum, ignoreCase = true)) { - app.morphe.gui.util.ChecksumStatus.Verified - } else { - app.morphe.gui.util.ChecksumStatus.Mismatch(expectedChecksum, actualChecksum) - } - } catch (e: Exception) { - Logger.error("Checksum calculation failed", e) - app.morphe.gui.util.ChecksumStatus.Error(e.message ?: "Unknown error") - } - } + // TODO: Re-enable checksum verification when checksums are provided via .mpp files + // private fun verifyChecksum( + // file: File, packageName: String, version: String, + // architectures: List, recommendedVersion: String? + // ): app.morphe.gui.util.ChecksumStatus { ... } private fun formatFileSize(bytes: Long): String { return when { @@ -499,30 +459,12 @@ data class HomeUiState( get() = patchesVersion != null && patchesVersion == latestPatchesVersion } -enum class AppType( - val displayName: String, - val packageName: String, - val suggestedVersion: String -) { - YOUTUBE( - displayName = app.morphe.gui.data.constants.AppConstants.YouTube.DISPLAY_NAME, - packageName = app.morphe.gui.data.constants.AppConstants.YouTube.PACKAGE_NAME, - suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTube.SUGGESTED_VERSION - ), - YOUTUBE_MUSIC( - displayName = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.DISPLAY_NAME, - packageName = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.PACKAGE_NAME, - suggestedVersion = app.morphe.gui.data.constants.AppConstants.YouTubeMusic.SUGGESTED_VERSION - ) -} - data class ApkInfo( val fileName: String, val filePath: String, val fileSize: Long, val formattedSize: String, val appName: String, - val appType: AppType?, // Nullable for dynamically supported apps not in the enum val packageName: String, val versionName: String, val architectures: List = emptyList(), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index f4b0365..cdd794c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -1,6 +1,5 @@ package app.morphe.gui.ui.screens.home.components -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -18,11 +17,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.morphe.morphe_cli.generated.resources.Res -import org.jetbrains.compose.resources.painterResource -import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.ui.screens.home.ApkInfo -import app.morphe.gui.ui.screens.home.AppType import app.morphe.gui.ui.screens.home.VersionStatus import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.ChecksumStatus @@ -54,16 +49,6 @@ fun ApkInfoCard( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f) ) { - // App icon - determine from appType or packageName - val iconRes = when { - apkInfo.appType == AppType.YOUTUBE -> null - apkInfo.appType == AppType.YOUTUBE_MUSIC -> null - apkInfo.packageName == AppConstants.YouTube.PACKAGE_NAME -> null - apkInfo.packageName == AppConstants.YouTubeMusic.PACKAGE_NAME -> null - apkInfo.packageName == AppConstants.Reddit.PACKAGE_NAME -> null - else -> null - } - Box( modifier = Modifier .size(64.dp) @@ -71,21 +56,12 @@ fun ApkInfoCard( .background(Color.White), contentAlignment = Alignment.Center ) { - if (iconRes != null) { - Image( - painter = painterResource(iconRes), - contentDescription = "${apkInfo.appName} icon", - modifier = Modifier.size(48.dp) - ) - } else { - // Fallback: show first letter of app name - Text( - text = apkInfo.appName.first().toString(), - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = MorpheColors.Blue - ) - } + Text( + text = apkInfo.appName.first().toString(), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = MorpheColors.Blue + ) } Column { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 9cbfcd3..2b3438b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -57,13 +57,14 @@ data class PatchSelectionScreen( val apkPath: String, val apkName: String, val patchesFilePath: String, + val packageName: String, val apkArchitectures: List = emptyList() ) : Screen { @Composable override fun Content() { val viewModel = koinScreenModel { - parametersOf(apkPath, apkName, patchesFilePath, apkArchitectures) + parametersOf(apkPath, apkName, patchesFilePath, packageName, apkArchitectures) } PatchSelectionScreenContent(viewModel = viewModel) } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt index 278691b..d55bcb9 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -17,6 +17,7 @@ class PatchSelectionViewModel( private val apkPath: String, private val apkName: String, private val patchesFilePath: String, + private val packageName: String, private val apkArchitectures: List, private val patchService: PatchService, private val patchRepository: PatchRepository @@ -60,10 +61,8 @@ class PatchSelectionViewModel( actualPatchesFilePath = downloadResult.getOrNull()!!.absolutePath } - val packageName = getPackageNameFromApk() - // Load patches using PatchService (direct library call) - val patchesResult = patchService.listPatches(actualPatchesFilePath, packageName) + val patchesResult = patchService.listPatches(actualPatchesFilePath, packageName.ifEmpty { null }) patchesResult.fold( onSuccess = { patches -> @@ -177,9 +176,11 @@ class PatchSelectionViewModel( val outputDir = File(inputFile.parentFile, appFolderName) outputDir.mkdirs() - // Extract version from APK filename for output name + // Extract version from APK filename and patches version for output name val version = extractVersionFromFilename(inputFile.name) ?: "patched" - val outputFileName = "${appFolderName}-${version}-patched.apk" + val patchesVersion = extractPatchesVersion(File(actualPatchesFilePath).name) + val patchesSuffix = if (patchesVersion != null) "-patches-$patchesVersion" else "" + val outputFileName = "${appFolderName}-Morphe-${version}${patchesSuffix}.apk" val outputPath = File(outputDir, outputFileName).absolutePath // Convert unique IDs back to patch names for CLI @@ -219,6 +220,12 @@ class PatchSelectionViewModel( } } + private fun extractPatchesVersion(patchesFileName: String): String? { + // Extract version from patches filename: morphe-patches-1.13.0-dev.11.mpp -> 1.13.0-dev.11 + val regex = Regex("""(\d+\.\d+\.\d+(?:-dev\.\d+)?)""") + return regex.find(patchesFileName)?.groupValues?.get(1) + } + fun getApkName(): String = apkName /** @@ -230,12 +237,21 @@ class PatchSelectionViewModel( val patchesFile = File(actualPatchesFilePath) val appFolderName = apkName.replace(" ", "-") val version = extractVersionFromFilename(inputFile.name) ?: "patched" - val outputFileName = "${appFolderName}-${version}-patched.apk" + val patchesVersion = extractPatchesVersion(patchesFile.name) + val patchesSuffix = if (patchesVersion != null) "-patches-$patchesVersion" else "" + val outputFileName = "${appFolderName}-Morphe-${version}${patchesSuffix}.apk" val selectedPatchNames = _uiState.value.allPatches .filter { _uiState.value.selectedPatches.contains(it.uniqueId) } .map { it.name } + val disabledPatchNames = _uiState.value.allPatches + .filter { !_uiState.value.selectedPatches.contains(it.uniqueId) } + .map { it.name } + + // Use whichever produces fewer flags + val useExclusive = selectedPatchNames.size <= disabledPatchNames.size + // striplibs flag: only when user deselected at least one architecture val striplibsArg = if (_uiState.value.selectedArchitectures.size < apkArchitectures.size && apkArchitectures.size > 1) { _uiState.value.selectedArchitectures.joinToString(",") @@ -248,15 +264,21 @@ class PatchSelectionViewModel( sb.append("java -jar morphe-cli.jar patch \\\n") sb.append(" -p ${patchesFile.name} \\\n") sb.append(" -o ${outputFileName} \\\n") - sb.append(" --exclusive \\\n") + + if (useExclusive) { + sb.append(" --exclusive \\\n") + } if (striplibsArg != null) { sb.append(" --striplibs $striplibsArg \\\n") } - selectedPatchNames.forEachIndexed { index, patch -> - val isLast = index == selectedPatchNames.lastIndex - sb.append(" -e \"$patch\"") + val flagPatches = if (useExclusive) selectedPatchNames else disabledPatchNames + val flag = if (useExclusive) "-e" else "-d" + + flagPatches.forEachIndexed { index, patch -> + val isLast = index == flagPatches.lastIndex + sb.append(" $flag \"$patch\"") if (!isLast) { sb.append(" \\") } @@ -266,10 +288,12 @@ class PatchSelectionViewModel( sb.append(" ${inputFile.name}") sb.toString() } else { - // Compact mode - single line that wraps naturally - val patches = selectedPatchNames.joinToString(" ") { "-e \"$it\"" } + val flagPatches = if (useExclusive) selectedPatchNames else disabledPatchNames + val flag = if (useExclusive) "-e" else "-d" + val patches = flagPatches.joinToString(" ") { "$flag \"$it\"" } + val exclusivePart = if (useExclusive) " --exclusive" else "" val striplibsPart = if (striplibsArg != null) " --striplibs $striplibsArg" else "" - "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --exclusive$striplibsPart $patches ${inputFile.name}" + "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName$exclusivePart$striplibsPart $patches ${inputFile.name}" } } @@ -315,16 +339,6 @@ class PatchSelectionViewModel( return patchRepository.downloadPatches(targetRelease) } - private fun getPackageNameFromApk(): String { - // Extract package name from APK filename (APKMirror format) - val fileName = File(apkPath).name - return when { - fileName.startsWith("com.google.android.youtube_") -> "com.google.android.youtube" - fileName.startsWith("com.google.android.apps.youtube.music_") -> "com.google.android.apps.youtube.music" - fileName.startsWith("com.reddit.frontpage_") -> "com.reddit.frontpage" - else -> "" - } - } } data class PatchSelectionUiState( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 1845e3d..ff59acc 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -52,8 +52,8 @@ import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects import java.awt.Desktop import java.awt.datatransfer.DataFlavor import java.io.File -import javax.swing.JFileChooser -import javax.swing.filechooser.FileNameExtensionFilter +import java.awt.FileDialog +import java.awt.Frame /** * Quick Patch Mode - Single screen simplified patching. @@ -996,14 +996,17 @@ private fun VerificationStatusBanner( * Open native file picker. */ private fun openFilePicker(): File? { - val chooser = JFileChooser().apply { - dialogTitle = "Select APK" - fileFilter = FileNameExtensionFilter("APK Files (*.apk, *.apkm)", "apk", "apkm") - isAcceptAllFileFilterUsed = false + val fileDialog = FileDialog(null as Frame?, "Select APK", FileDialog.LOAD).apply { + isMultipleMode = false + setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") } } + isVisible = true } - return if (chooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) { - chooser.selectedFile + val directory = fileDialog.directory + val file = fileDialog.file + + return if (directory != null && file != null) { + File(directory, file) } else null } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 2907fd7..35a879f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -15,7 +15,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch import net.dongliu.apk.parser.ApkFile import app.morphe.gui.util.ChecksumStatus -import app.morphe.gui.util.ChecksumUtils import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import app.morphe.gui.util.PatchService @@ -203,7 +202,6 @@ class QuickPatchViewModel( ?: SupportedApp.getDisplayName(packageName) val recommendedVersion = dynamicAppInfo?.recommendedVersion - ?: AppConstants.getSuggestedVersion(packageName) // Version check val isRecommendedVersion = recommendedVersion != null && versionName == recommendedVersion @@ -211,8 +209,8 @@ class QuickPatchViewModel( "Version $versionName may have compatibility issues. Recommended: $recommendedVersion" } else null - // Checksum verification (still uses AppConstants - checksums are manually maintained) - val checksumStatus = verifyChecksum(file, packageName, versionName, recommendedVersion) + // TODO: Re-enable when checksums are provided via .mpp files + val checksumStatus = ChecksumStatus.NotConfigured Logger.info("Quick mode: Analyzed $displayName v$versionName (recommended: $recommendedVersion)") @@ -237,29 +235,10 @@ class QuickPatchViewModel( } } - /** - * Verify checksum against known values. - */ - private fun verifyChecksum(file: File, packageName: String, version: String, recommendedVersion: String?): ChecksumStatus { - // Check if this is a non-recommended version (use dynamic recommended version) - if (recommendedVersion != null && version != recommendedVersion) { - return ChecksumStatus.NonRecommendedVersion - } - - val expectedChecksum = AppConstants.getChecksum(packageName, version, emptyList()) - ?: return ChecksumStatus.NotConfigured - - return try { - val actualChecksum = ChecksumUtils.calculateSha256(file) - if (actualChecksum.equals(expectedChecksum, ignoreCase = true)) { - ChecksumStatus.Verified - } else { - ChecksumStatus.Mismatch(expectedChecksum, actualChecksum) - } - } catch (e: Exception) { - ChecksumStatus.Error(e.message ?: "Unknown error") - } - } + // TODO: Re-enable checksum verification when checksums are provided via .mpp files + // private fun verifyChecksum( + // file: File, packageName: String, version: String, recommendedVersion: String? + // ): ChecksumStatus { ... } /** * Start the patching process with defaults. @@ -321,7 +300,10 @@ class QuickPatchViewModel( // Generate output path val outputDir = apkFile.parentFile ?: File(System.getProperty("user.home")) val baseName = apkInfo.displayName.replace(" ", "-") - val outputFileName = "$baseName-Morphe-${apkInfo.versionName}.apk" + val patchesVersion = Regex("""(\d+\.\d+\.\d+(?:-dev\.\d+)?)""") + .find(patchFile.name)?.groupValues?.get(1) + val patchesSuffix = if (patchesVersion != null) "-patches-$patchesVersion" else "" + val outputFileName = "$baseName-Morphe-${apkInfo.versionName}${patchesSuffix}.apk" val outputPath = File(outputDir, outputFileName).absolutePath // Use PatchService for direct library patching (no CLI subprocess) From 46a45774c0b46f8d8bed22493c7692251ae43af4 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Thu, 12 Feb 2026 16:53:59 +0530 Subject: [PATCH 21/49] Use non zero Java exit code if patching fails --- .../app/morphe/cli/command/MainCommand.kt | 8 +- .../app/morphe/cli/command/PatchCommand.kt | 398 +++++++++++++----- 2 files changed, 296 insertions(+), 110 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt index 40bc787..f6c8a83 100644 --- a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt @@ -5,11 +5,13 @@ import app.morphe.library.logging.Logger import picocli.CommandLine import picocli.CommandLine.Command import picocli.CommandLine.IVersionProvider -import java.util.* +import java.util.Properties +import kotlin.system.exitProcess fun cliMain(args: Array) { Logger.setDefault() - CommandLine(MainCommand).execute(*args).let(System::exit) + val exitCode = CommandLine(MainCommand).execute(*args) + exitProcess(exitCode) } private object CLIVersionProvider : IVersionProvider { @@ -37,6 +39,6 @@ private object CLIVersionProvider : IVersionProvider { ListPatchesCommand::class, ListCompatibleVersions::class, UtilityCommand::class, - ], + ] ) internal object MainCommand diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 7e1a424..8791884 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -1,14 +1,21 @@ package app.morphe.cli.command +import app.morphe.cli.command.model.FailedPatch import app.morphe.cli.command.model.PatchingResult import app.morphe.cli.command.model.PatchingStep -import app.morphe.cli.command.model.PatchingStepResult import app.morphe.cli.command.model.addStepResult import app.morphe.cli.command.model.toSerializablePatch -import app.morphe.engine.PatchEngine +import app.morphe.engine.ApkLibraryStripper import app.morphe.library.ApkUtils +import app.morphe.library.ApkUtils.applyTo import app.morphe.library.installation.installer.* +import app.morphe.library.setOptions +import app.morphe.patcher.Patcher +import app.morphe.patcher.PatcherConfig +import app.morphe.patcher.patch.Patch import app.morphe.patcher.patch.loadPatchesFromJar +import com.reandroid.apkeditor.merge.Merger +import com.reandroid.apkeditor.merge.MergerOptions import kotlinx.coroutines.runBlocking import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json @@ -19,15 +26,21 @@ import picocli.CommandLine.Help.Visibility.ALWAYS import picocli.CommandLine.Model.CommandSpec import picocli.CommandLine.Spec import java.io.File +import java.io.PrintWriter +import java.io.StringWriter +import java.util.concurrent.Callable import java.util.logging.Logger -import app.morphe.cli.command.model.FailedPatch as CliFailedPatch @OptIn(ExperimentalSerializationApi::class) @CommandLine.Command( name = "patch", description = ["Patch an APK file."], ) -internal object PatchCommand : Runnable { +internal object PatchCommand : Callable { + + private const val EXIT_CODE_SUCCESS = 0 + private const val EXIT_CODE_ERROR = 1 + private val logger = Logger.getLogger(this::class.java.name) @Spec @@ -256,7 +269,16 @@ internal object PatchCommand : Runnable { ) private var striplibs: List = emptyList() - override fun run() { + @CommandLine.Option( + names = ["--continue-on-error"], + description = ["Continue patching even if a patch fails. By default, patching stops on the first error."], + showDefaultValue = ALWAYS, + ) + private var continueOnError: Boolean = false + + override fun call(): Int { + // region Setup + val outputFilePath = outputFilePath ?: File("").absoluteFile.resolve( "${apk.nameWithoutExtension}-patched.apk", @@ -271,7 +293,6 @@ internal object PatchCommand : Runnable { keyStoreFilePath ?: outputFilePath.parentFile .resolve("${outputFilePath.nameWithoutExtension}.keystore") - // Set up ADB installer (CLI-only) val installer = if (deviceSerial != null) { val deviceSerial = deviceSerial!!.ifEmpty { null } @@ -281,7 +302,7 @@ internal object PatchCommand : Runnable { } else { AdbInstaller(deviceSerial) } - } catch (e: DeviceNotFoundException) { + } catch (_: DeviceNotFoundException) { if (deviceSerial?.isNotEmpty() == true) { logger.severe( "Device with serial $deviceSerial not found to install to. " + @@ -294,119 +315,199 @@ internal object PatchCommand : Runnable { ) } - return + return EXIT_CODE_ERROR } } else { null } - // Resolve --ei/--di indices to patch names by pre-loading patches - val patchesList = loadPatchesFromJar(patchesFiles).toList() - - val enabledPatchNames = selection.mapNotNull { sel -> - sel.enabled?.let { en -> - en.selector.name ?: patchesList.getOrNull(en.selector.index!!)?.name - } - }.toSet() - - val disabledPatchNames = selection.mapNotNull { sel -> - sel.disable?.let { dis -> - dis.selector.name ?: patchesList.getOrNull(dis.selector.index!!)?.name - } - }.filterNotNull().toSet() - - // Build options map: Map> - val patchOptions = selection.filter { it.enabled != null } - .associate { sel -> - val en = sel.enabled!! - val name = en.selector.name ?: patchesList[en.selector.index!!].name!! - name to en.options.toMap() - } - .filter { it.value.isNotEmpty() } - - val config = PatchEngine.Config( - inputApk = apk, - patches = patchesList.toSet(), - outputApk = outputFilePath, - enabledPatches = enabledPatchNames, - disabledPatches = disabledPatchNames, - exclusiveMode = exclusive, - forceCompatibility = force, - patchOptions = patchOptions, - unsigned = mount || unsigned, - signerName = signer, - keystoreDetails = ApkUtils.KeyStoreDetails( - keystoreFilePath, - keyStorePassword, - keyStoreEntryAlias, - keyStoreEntryPassword, - ), - architecturesToKeep = striplibs, - aaptBinaryPath = aaptBinaryPath, - tempDir = temporaryFilesPath, - ) + // endregion val patchingResult = PatchingResult() + var mergedApkToCleanup: File? = null try { - val engineResult = runBlocking { - PatchEngine.patch(config) { msg -> logger.info(msg) } - } + // region Load patches - patchingResult.packageName = engineResult.packageName - patchingResult.packageVersion = engineResult.packageVersion - patchingResult.success = engineResult.success - - // Map engine step results to CLI model for --result-file - engineResult.stepResults.forEach { step -> - val cliStep = when (step.step) { - PatchEngine.PatchStep.PATCHING -> PatchingStep.PATCHING - PatchEngine.PatchStep.REBUILDING -> PatchingStep.REBUILDING - PatchEngine.PatchStep.STRIPPING_LIBS -> PatchingStep.STRIPPING_LIBS - PatchEngine.PatchStep.SIGNING -> PatchingStep.SIGNING - } - patchingResult.patchingSteps.add(PatchingStepResult(cliStep, step.success, step.error)) - } + logger.info("Loading patches") + + val patches = loadPatchesFromJar(patchesFiles) + + // endregion + + val patcherTemporaryFilesPath = temporaryFilesPath.resolve("patcher") - engineResult.appliedPatches.forEach { name -> - patchesList.find { it.name == name }?.let { - patchingResult.appliedPatches.add(it.toSerializablePatch()) + // Checking if the file is in apkm format (like reddit) + val inputApk = if (apk.extension.equals("apkm", ignoreCase = true)) { + logger.info("Merging APKM bundle") + + // Save merged APK to output directory (will be cleaned up after patching) + val outputApk = outputFilePath.parentFile.resolve("${apk.nameWithoutExtension}-merged.apk") + + // Use APKEditor's Merger directly (handles extraction and merging) + val mergerOptions = MergerOptions().apply { + inputFile = apk // Original APKM file + outputFile = outputApk + cleanMeta = true } + Merger(mergerOptions).run() + + mergedApkToCleanup = outputApk + outputApk + } else { + apk } - engineResult.failedPatches.forEach { failed -> - patchesList.find { it.name == failed.name }?.let { - patchingResult.failedPatches.add(CliFailedPatch(it.toSerializablePatch(), failed.error)) - } + + val (packageName, patcherResult) = Patcher( + PatcherConfig( + inputApk, + patcherTemporaryFilesPath, + aaptBinaryPath?.path, + patcherTemporaryFilesPath.absolutePath, + ), + ).use { patcher -> + val packageName = patcher.context.packageMetadata.packageName + val packageVersion = patcher.context.packageMetadata.packageVersion + + patchingResult.packageName = packageName + patchingResult.packageVersion = packageVersion + + val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) + + logger.info("Setting patch options") + + val patchesList = patches.toList() + selection.filter { it.enabled != null }.associate { + val enabledSelection = it.enabled!! + + (enabledSelection.selector.name ?: patchesList[enabledSelection.selector.index!!].name!!) to + enabledSelection.options + }.let(filteredPatches::setOptions) + + patcher += filteredPatches + + // Execute patches. + patchingResult.addStepResult( + PatchingStep.PATCHING, + { + runBlocking { + patcher().collect { patchResult -> + patchResult.exception?.let { exception -> + StringWriter().use { writer -> + exception.printStackTrace(PrintWriter(writer)) + + logger.severe("\"${patchResult.patch}\" failed:\n$writer") + + patchingResult.failedPatches.add( + FailedPatch( + patchResult.patch.toSerializablePatch(), + writer.toString() + ) + ) + patchingResult.success = false + + if (!continueOnError) { + throw PatchFailedException( + "\"${patchResult.patch}\" failed", + exception + ) + } + } + } ?: patchResult.patch.let { + patchingResult.appliedPatches.add(patchResult.patch.toSerializablePatch()) + logger.info("\"${patchResult.patch}\" succeeded") + } + } + } + } + ) + + patcher.context.packageMetadata.packageName to patcher.get() } - logger.info("Saved to $outputFilePath") + // region Save. - // ADB install (CLI-only) - if (engineResult.success) { - deviceSerial?.let { + inputApk.copyTo(temporaryFilesPath.resolve(inputApk.name), overwrite = true).apply { + patchingResult.addStepResult( + PatchingStep.REBUILDING, + { + patcherResult.applyTo(this) + } + ) + }.also { rebuiltApk -> + if (striplibs.isNotEmpty()) { patchingResult.addStepResult( - PatchingStep.INSTALLING, + PatchingStep.STRIPPING_LIBS, { - runBlocking { - val result = installer!!.install( - Installer.Apk(outputFilePath, engineResult.packageName), - ) - when (result) { - RootInstallerResult.FAILURE -> { - logger.severe("Failed to mount the patched APK file") - throw IllegalStateException("Failed to mount the patched APK file") - } - is AdbInstallerResult.Failure -> { - logger.severe(result.exception.toString()) - throw result.exception - } - else -> logger.info("Installed the patched APK file") - } + ApkLibraryStripper.stripLibraries(rebuiltApk, striplibs) { msg -> + logger.info(msg) } - }, + } + ) + } + }.let { patchedApkFile -> + if (!mount && !unsigned) { + patchingResult.addStepResult( + PatchingStep.SIGNING, + { + ApkUtils.signApk( + patchedApkFile, + outputFilePath, + signer, + ApkUtils.KeyStoreDetails( + keystoreFilePath, + keyStorePassword, + keyStoreEntryAlias, + keyStoreEntryPassword, + ), + ) + } ) + } else { + patchedApkFile.copyTo(outputFilePath, overwrite = true) } } + + logger.info("Saved to $outputFilePath") + + // endregion + + // region Install. + + deviceSerial?.let { + patchingResult.addStepResult( + PatchingStep.INSTALLING, + { + runBlocking { + val result = installer!!.install(Installer.Apk(outputFilePath, packageName)) + when (result) { + RootInstallerResult.FAILURE -> { + logger.severe("Failed to mount the patched APK file") + throw IllegalStateException("Failed to mount the patched APK file") + } + is AdbInstallerResult.Failure -> { + logger.severe(result.exception.toString()) + throw result.exception + } + else -> logger.info("Installed the patched APK file") + } + } + } + ) + } + + // endregion + } catch (e: PatchFailedException) { + logger.severe("Patching aborted: ${e.message}") + logger.info( + "Use --continue-on-error to skip failed patches and continue patching" + ) + return EXIT_CODE_ERROR + } catch (e: Exception) { + // Should never happen. + logger.severe("An unexpected error occurred: ${e.message}") + e.printStackTrace() + return EXIT_CODE_ERROR } finally { patchingResultOutputFilePath?.let { outputFile -> outputFile.outputStream().use { outputStream -> @@ -414,17 +515,100 @@ internal object PatchCommand : Runnable { } logger.info("Patching result saved to $outputFile") } - } - if (purge) { - logger.info("Purging temporary files") - val result = - if (temporaryFilesPath.deleteRecursively()) { - "Purged resource cache directory" - } else { - "Failed to purge resource cache directory" + if (purge) { + logger.info("Purging temporary files") + purge(temporaryFilesPath) + } + + // Clean up merged APK if we created one from APKM + mergedApkToCleanup?.let { + if (!it.delete()) { + logger.warning("Could not clean up merged APK: ${it.path}") } - logger.info(result) + } } + + return EXIT_CODE_SUCCESS + } + + /** + * Filter the patches based on the selection. + * + * @param packageName The package name of the APK file to be patched. + * @param packageVersion The version of the APK file to be patched. + * @return The filtered patches. + */ + private fun Set>.filterPatchSelection( + packageName: String, + packageVersion: String, + ): Set> = buildSet { + val enabledPatchesByName = + selection.mapNotNull { it.enabled?.selector?.name }.toSet() + val enabledPatchesByIndex = + selection.mapNotNull { it.enabled?.selector?.index }.toSet() + + val disabledPatches = + selection.mapNotNull { it.disable?.selector?.name }.toSet() + val disabledPatchesByIndex = + selection.mapNotNull { it.disable?.selector?.index }.toSet() + + this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) -> + val patchName = patch.name!! + + val isManuallyDisabled = patchName in disabledPatches || i in disabledPatchesByIndex + if (isManuallyDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually") + + // Make sure the patch is compatible with the supplied APK files package name and version. + patch.compatiblePackages?.let { packages -> + packages.singleOrNull { (name, _) -> name == packageName }?.let { (_, versions) -> + if (versions?.isEmpty() == true) { + return@patchLoop logger.warning("\"$patchName\" incompatible with \"$packageName\"") + } + + val matchesVersion = + force || versions?.let { it.any { version -> version == packageVersion } } ?: true + + if (!matchesVersion) { + return@patchLoop logger.warning( + "\"$patchName\" incompatible with $packageName $packageVersion " + + "but compatible with " + + packages.joinToString("; ") { (packageName, versions) -> + packageName + " " + versions!!.joinToString(", ") + }, + ) + } + } ?: return@patchLoop logger.fine( + "\"$patchName\" incompatible with $packageName. " + + "It is only compatible with " + + packages.joinToString(", ") { (name, _) -> name }, + ) + + return@let + } ?: logger.fine("\"$patchName\" has no package constraints") + + val isEnabled = !exclusive && patch.use + val isManuallyEnabled = patchName in enabledPatchesByName || i in enabledPatchesByIndex + + if (!(isEnabled || isManuallyEnabled)) { + return@patchLoop logger.info("\"$patchName\" disabled") + } + + add(patch) + + logger.fine("\"$patchName\" added") + } + } + + private fun purge(resourceCachePath: File) { + val result = + if (resourceCachePath.deleteRecursively()) { + "Purged resource cache directory" + } else { + "Failed to purge resource cache directory" + } + logger.info(result) } } + +private class PatchFailedException(message: String, cause: Throwable) : Exception(message, cause) From 7640a80c485cffe188db4dedf3acf37afbc2fd6d Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Thu, 12 Feb 2026 23:46:09 +0530 Subject: [PATCH 22/49] Minor Fixes Better patching logging Simplified version. Added --force for unsupported versions. Added a button for --continue-on-error to allow gui users to continue patching even when if a patch throws an error. --- .../kotlin/app/morphe/gui/data/model/Patch.kt | 3 +- .../screens/patches/PatchSelectionScreen.kt | 43 +++++++++++++++++-- .../patches/PatchSelectionViewModel.kt | 15 +++++-- .../ui/screens/patching/PatchingViewModel.kt | 1 + .../ui/screens/quick/QuickPatchViewModel.kt | 8 +--- .../app/morphe/gui/util/PatchService.kt | 3 ++ 6 files changed, 57 insertions(+), 16 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt index 2f940a2..ea413a7 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt @@ -80,5 +80,6 @@ data class PatchConfig( val disabledPatches: List = emptyList(), val patchOptions: Map = emptyMap(), val useExclusiveMode: Boolean = false, - val striplibs: List = emptyList() + val striplibs: List = emptyList(), + val continueOnError: Boolean = false ) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 2b3438b..3d6d725 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material.icons.filled.PlaylistRemove import androidx.compose.material.icons.filled.Terminal import androidx.compose.material3.* import androidx.compose.runtime.* @@ -107,6 +108,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { // State for command preview var cleanMode by remember { mutableStateOf(false) } var showCommandPreview by remember { mutableStateOf(false) } + var continueOnError by remember { mutableStateOf(false) } Scaffold( topBar = { @@ -149,7 +151,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { Spacer(Modifier.width(12.dp)) - // Command preview toggle + // Command preview toggle & continue-on-error toggle if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { val isActive = showCommandPreview Surface( @@ -170,6 +172,39 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { modifier = Modifier.padding(8.dp).size(20.dp) ) } + + Spacer(Modifier.width(6.dp)) + + // Continue on error toggle + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), + tooltip = { + PlainTooltip { + Text("Continue patching even if a patch fails") + } + }, + state = rememberTooltipState() + ) { + Surface( + onClick = { continueOnError = !continueOnError }, + shape = RoundedCornerShape(8.dp), + color = if (continueOnError) MaterialTheme.colorScheme.error.copy(alpha = 0.15f) + else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + border = BorderStroke( + width = 1.dp, + color = if (continueOnError) MaterialTheme.colorScheme.error.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + ) { + Icon( + imageVector = Icons.Default.PlaylistRemove, + contentDescription = "Continue on error", + tint = if (continueOnError) MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp).size(20.dp) + ) + } + } } Spacer(Modifier.width(12.dp)) @@ -191,8 +226,8 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { ) { // Command preview - collapsible via top bar button if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { - val commandPreview = remember(uiState.selectedPatches, uiState.selectedArchitectures, cleanMode) { - viewModel.getCommandPreview(cleanMode) + val commandPreview = remember(uiState.selectedPatches, uiState.selectedArchitectures, cleanMode, continueOnError) { + viewModel.getCommandPreview(cleanMode, continueOnError) } AnimatedVisibility( visible = showCommandPreview, @@ -322,7 +357,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { ) { Button( onClick = { - val config = viewModel.createPatchConfig() + val config = viewModel.createPatchConfig(continueOnError) navigator.push(PatchingScreen(config)) }, enabled = uiState.selectedPatches.isNotEmpty(), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt index d55bcb9..74b710b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -169,7 +169,7 @@ class PatchSelectionViewModel( return _uiState.value.allPatches.count { !it.isEnabled } } - fun createPatchConfig(): PatchConfig { + fun createPatchConfig(continueOnError: Boolean = false): PatchConfig { // Create app folder in the same location as the input APK val inputFile = File(apkPath) val appFolderName = apkName.replace(" ", "-") @@ -206,7 +206,8 @@ class PatchSelectionViewModel( enabledPatches = selectedPatchNames, disabledPatches = disabledPatchNames, useExclusiveMode = true, - striplibs = striplibs + striplibs = striplibs, + continueOnError = continueOnError ) } @@ -232,7 +233,7 @@ class PatchSelectionViewModel( * Generate a preview of the CLI command that will be executed. * @param cleanMode If true, formats with newlines for readability. If false, compact single-line format. */ - fun getCommandPreview(cleanMode: Boolean = false): String { + fun getCommandPreview(cleanMode: Boolean = false, continueOnError: Boolean = false): String { val inputFile = File(apkPath) val patchesFile = File(actualPatchesFilePath) val appFolderName = apkName.replace(" ", "-") @@ -264,6 +265,11 @@ class PatchSelectionViewModel( sb.append("java -jar morphe-cli.jar patch \\\n") sb.append(" -p ${patchesFile.name} \\\n") sb.append(" -o ${outputFileName} \\\n") + sb.append(" --force \\\n") + + if (continueOnError) { + sb.append(" --continue-on-error \\\n") + } if (useExclusive) { sb.append(" --exclusive \\\n") @@ -293,7 +299,8 @@ class PatchSelectionViewModel( val patches = flagPatches.joinToString(" ") { "$flag \"$it\"" } val exclusivePart = if (useExclusive) " --exclusive" else "" val striplibsPart = if (striplibsArg != null) " --striplibs $striplibsArg" else "" - "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName$exclusivePart$striplibsPart $patches ${inputFile.name}" + val continueOnErrorPart = if (continueOnError) " --continue-on-error" else "" + "java -jar morphe-cli.jar patch -p ${patchesFile.name} -o $outputFileName --force$continueOnErrorPart$exclusivePart$striplibsPart $patches ${inputFile.name}" } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt index e5e8326..8871a9b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt @@ -57,6 +57,7 @@ class PatchingViewModel( options = config.patchOptions, exclusiveMode = config.useExclusiveMode, striplibs = config.striplibs, + continueOnError = config.continueOnError, onProgress = { message -> parseAndAddLog(message) } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 35a879f..4c00950 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -317,13 +317,7 @@ class QuickPatchViewModel( options = emptyMap(), exclusiveMode = false, onProgress = { message -> - // Update status with current operation - if (message.contains("patch", ignoreCase = true) || - message.contains("applying", ignoreCase = true) || - message.contains("Applied", ignoreCase = true)) { - _uiState.value = _uiState.value.copy(statusMessage = message.take(60)) - } - // Parse progress + _uiState.value = _uiState.value.copy(statusMessage = message.take(60)) parseProgress(message) } ) diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 2902104..19d9830 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -78,6 +78,7 @@ class PatchService { options: Map = emptyMap(), exclusiveMode: Boolean = false, striplibs: List = emptyList(), + continueOnError: Boolean = false, onProgress: (String) -> Unit = {} ): Result = withContext(Dispatchers.IO) { try { @@ -114,8 +115,10 @@ class PatchService { enabledPatches = enabledPatches.toSet(), disabledPatches = disabledPatches.toSet(), exclusiveMode = exclusiveMode, + forceCompatibility = true, patchOptions = patchOptions, architecturesToKeep = striplibs, + failOnError = !continueOnError, ) val engineResult = PatchEngine.patch(config, onProgress) From 5fb0e1ab81e0d9eee01357f19dd9e6699b2e5600 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 15 Feb 2026 23:53:12 +0530 Subject: [PATCH 23/49] Minor Fixes and new theme added fixes from other branches and new mini theme --- .../kotlin/app/morphe/engine/PatchEngine.kt | 14 +++++++------- .../gui/ui/components/SettingsDialog.kt | 1 + .../morphe/gui/ui/screens/home/HomeScreen.kt | 2 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 2 +- .../kotlin/app/morphe/gui/ui/theme/Theme.kt | 19 +++++++++++++++++++ 5 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index 5a319a8..19b474e 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -280,13 +280,7 @@ object PatchEngine { patches.forEach patchLoop@{ patch -> val patchName = patch.name ?: return@patchLoop - // Check if explicitly disabled - if (patchName in disabledPatches) { - onProgress("Skipping disabled: $patchName") - return@patchLoop - } - - // Check package compatibility + // Check package compatibility first to avoid duplicate logs for multi-app patches. patch.compatiblePackages?.let { packages -> val matchingPkg = packages.singleOrNull { (name, _) -> name == packageName } if (matchingPkg == null) { @@ -307,6 +301,12 @@ object PatchEngine { } } + // Check if explicitly disabled + if (patchName in disabledPatches) { + onProgress("Skipping disabled: $patchName") + return@patchLoop + } + val isManuallyEnabled = patchName in enabledPatches val isEnabledByDefault = !exclusiveMode && patch.use diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index a03ba7a..6aab373 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -316,6 +316,7 @@ private fun ThemePreference.toDisplayName(): String { return when (this) { ThemePreference.LIGHT -> "Light" ThemePreference.DARK -> "Dark" + ThemePreference.AMOLED -> "AMOLED" ThemePreference.SYSTEM -> "System" } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index c7e879e..8d6fe8b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -518,7 +518,7 @@ private fun VersionWarningDialog( private fun BrandingSection(isCompact: Boolean = false) { val themeState = LocalThemeState.current val isDark = when (themeState.current) { - ThemePreference.DARK -> true + ThemePreference.DARK, ThemePreference.AMOLED -> true ThemePreference.LIGHT -> false ThemePreference.SYSTEM -> isSystemInDarkTheme() } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index ff59acc..45478be 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -142,7 +142,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { Spacer(modifier = Modifier.height(8.dp)) val themeState = LocalThemeState.current val isDark = when (themeState.current) { - ThemePreference.DARK -> true + ThemePreference.DARK, ThemePreference.AMOLED -> true ThemePreference.LIGHT -> false ThemePreference.SYSTEM -> isSystemInDarkTheme() } diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt index f980d43..3109a73 100644 --- a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt +++ b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt @@ -36,6 +36,23 @@ private val MorpheDarkColorScheme = darkColorScheme( onError = Color.Black ) +private val MorpheAmoledColorScheme = darkColorScheme( + primary = MorpheColors.Blue, + secondary = MorpheColors.Teal, + tertiary = MorpheColors.Cyan, + background = Color.Black, + surface = Color(0xFF0A0A0A), + surfaceVariant = Color(0xFF1A1A1A), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.Black, + onBackground = MorpheColors.TextLight, + onSurface = MorpheColors.TextLight, + onSurfaceVariant = Color(0xFFB0B0B0), + error = Color(0xFFCF6679), + onError = Color.Black +) + private val MorpheLightColorScheme = lightColorScheme( primary = MorpheColors.Blue, secondary = MorpheColors.Teal, @@ -56,6 +73,7 @@ private val MorpheLightColorScheme = lightColorScheme( enum class ThemePreference { LIGHT, DARK, + AMOLED, SYSTEM } @@ -66,6 +84,7 @@ fun MorpheTheme( ) { val colorScheme = when (themePreference) { ThemePreference.DARK -> MorpheDarkColorScheme + ThemePreference.AMOLED -> MorpheAmoledColorScheme ThemePreference.LIGHT -> MorpheLightColorScheme ThemePreference.SYSTEM -> { if (isSystemInDarkTheme()) MorpheDarkColorScheme else MorpheLightColorScheme From b8d3109fd6f27200664264395c29a9f092461dfd Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:43:49 +0530 Subject: [PATCH 24/49] Better Offline support + Minor fixes Added a better offline mode experience for the user. Minor UI improvemments to the patches and patchselection screens. Added some updates from other branch (but not all, might lack some functionality that are present in other branches) --- .../app/morphe/cli/command/MainCommand.kt | 1 + .../app/morphe/cli/command/OptionsCommand.kt | 84 ++++++++ .../app/morphe/cli/command/PatchCommand.kt | 165 ++++++++++++++-- .../cli/command/model/PatchOptionsFile.kt | 77 ++++++++ .../morphe/gui/ui/components/OfflineBanner.kt | 84 ++++++++ .../morphe/gui/ui/screens/home/HomeScreen.kt | 12 ++ .../gui/ui/screens/home/HomeViewModel.kt | 84 ++++++++ .../ui/screens/home/components/ApkInfoCard.kt | 34 ++-- .../screens/patches/PatchSelectionScreen.kt | 131 +++++++++---- .../gui/ui/screens/patches/PatchesScreen.kt | 179 +++++++++++++----- .../ui/screens/patches/PatchesViewModel.kt | 166 ++++++++++++++-- .../gui/ui/screens/quick/QuickPatchScreen.kt | 9 + .../ui/screens/quick/QuickPatchViewModel.kt | 80 +++++++- 13 files changed, 970 insertions(+), 136 deletions(-) create mode 100644 src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt create mode 100644 src/main/kotlin/app/morphe/cli/command/model/PatchOptionsFile.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt diff --git a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt index f6c8a83..b369ddb 100644 --- a/src/main/kotlin/app/morphe/cli/command/MainCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/MainCommand.kt @@ -38,6 +38,7 @@ private object CLIVersionProvider : IVersionProvider { PatchCommand::class, ListPatchesCommand::class, ListCompatibleVersions::class, + OptionsCommand::class, UtilityCommand::class, ] ) diff --git a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt new file mode 100644 index 0000000..09c885c --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt @@ -0,0 +1,84 @@ +package app.morphe.cli.command + +import app.morphe.cli.command.model.toPatchOptionsFile +import app.morphe.patcher.patch.loadPatchesFromJar +import kotlinx.serialization.json.Json +import picocli.CommandLine +import picocli.CommandLine.Command +import picocli.CommandLine.Model.CommandSpec +import picocli.CommandLine.Spec +import java.io.File +import java.util.concurrent.Callable +import java.util.logging.Logger + +@Command( + name = "options-create", + description = ["Create an options JSON file for the patches and options."], +) +internal object OptionsCommand : Callable { + + private const val EXIT_CODE_SUCCESS = 0 + private const val EXIT_CODE_ERROR = 1 + + private val logger = Logger.getLogger(this::class.java.name) + + @Spec + private lateinit var spec: CommandSpec + + @CommandLine.Option( + names = ["-p", "--patches"], + description = ["One or more paths to MPP files."], + required = true, + ) + @Suppress("unused") + private fun setPatchesFile(patchesFiles: Set) { + patchesFiles.firstOrNull { !it.exists() }?.let { + throw CommandLine.ParameterException(spec.commandLine(), "${it.name} can't be found") + } + this.patchesFiles = patchesFiles + } + + private var patchesFiles = emptySet() + + @CommandLine.Option( + names = ["-o", "--out"], + description = ["Path to the output JSON file."], + required = true, + ) + private lateinit var outputFile: File + + @CommandLine.Option( + names = ["-f", "--filter-package-name"], + description = ["Filter patches by compatible package name."], + ) + private var packageName: String? = null + + private val json = Json { prettyPrint = true } + + override fun call(): Int { + return try { + logger.info("Loading patches") + + val patches = loadPatchesFromJar(patchesFiles) + + val filtered = packageName?.let { pkg -> + patches.filter { patch -> + patch.compatiblePackages?.any { (name, _) -> name == pkg } ?: true + }.toSet() + } ?: patches + + val patchOptionsFile = filtered.toPatchOptionsFile() + val jsonString = json.encodeToString(patchOptionsFile) + + outputFile.absoluteFile.parentFile?.mkdirs() + outputFile.writeText(jsonString) + + logger.info("Exported ${patchOptionsFile.patches.size} patches to ${outputFile.path}") + + EXIT_CODE_SUCCESS + } catch (e: Exception) { + logger.severe("Failed to export options: ${e.message}") + EXIT_CODE_ERROR + } + } +} diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index d454435..a6c7da4 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -1,9 +1,14 @@ package app.morphe.cli.command import app.morphe.cli.command.model.FailedPatch +import app.morphe.cli.command.model.PatchEntry +import app.morphe.cli.command.model.PatchOptionsFile +import app.morphe.cli.command.model.PatchSerializer import app.morphe.cli.command.model.PatchingResult import app.morphe.cli.command.model.PatchingStep import app.morphe.cli.command.model.addStepResult +import app.morphe.cli.command.model.deserializeOptionValue +import app.morphe.cli.command.model.toPatchOptionsFile import app.morphe.cli.command.model.toSerializablePatch import app.morphe.engine.ApkLibraryStripper import app.morphe.library.ApkUtils @@ -276,6 +281,24 @@ internal object PatchCommand : Callable { ) private var continueOnError: Boolean = false + @CommandLine.Option( + names = ["--options-file"], + description = ["Path to an options JSON file to read patch enable/disable and option values from."], + ) + @Suppress("unused") + private fun setOptionsFilePath(optionsFilePath: File?) { + this.optionsFilePath = optionsFilePath + } + + private var optionsFilePath: File? = null + + @CommandLine.Option( + names = ["--options-update"], + description = ["Auto-update the options JSON file after patching to reflect the current .mpp contents. Without this flag, the file is left unchanged."], + showDefaultValue = ALWAYS, + ) + private var updateOptions: Boolean = false + override fun call(): Int { // region Setup @@ -326,12 +349,55 @@ internal object PatchCommand : Callable { val patchingResult = PatchingResult() var mergedApkToCleanup: File? = null + // Load patches before try block so they're accessible in finally for auto-update + logger.info("Loading patches") + val patches = loadPatchesFromJar(patchesFiles) + try { - // region Load patches + // region Parse options JSON - logger.info("Loading patches") + val patchOptionsFile = optionsFilePath?.let { file -> + if (file.exists()) { + logger.info("Reading options from ${file.path}") + Json.decodeFromString(file.readText()) + } else { + logger.info("Options file ${file.path} does not exist, generating with defaults") + val generated = patches.toPatchOptionsFile() + val json = Json { prettyPrint = true } + file.absoluteFile.parentFile?.mkdirs() + file.writeText(json.encodeToString(generated)) + logger.info("Generated options file at ${file.path}") + generated + } + } - val patches = loadPatchesFromJar(patchesFiles) + // Build enable/disable sets from JSON (lowercase for case-insensitive matching) + val jsonEnabledPatches = patchOptionsFile?.patches + ?.filter { (_, entry) -> entry.enabled } + ?.keys?.map { it.lowercase() }?.toSet() ?: emptySet() + val jsonDisabledPatches = patchOptionsFile?.patches + ?.filter { (_, entry) -> !entry.enabled } + ?.keys?.map { it.lowercase() }?.toSet() ?: emptySet() + + // Build options map from JSON, deserializing values using each patch's option types + val jsonOptionsMap: Map> = patchOptionsFile?.patches + ?.mapNotNull { (patchName, entry) -> + if (entry.options.isEmpty()) return@mapNotNull null + val patch = patches.firstOrNull { it.name.equals(patchName, ignoreCase = true) } + ?: return@mapNotNull null + val resolvedName = patch.name ?: return@mapNotNull null + val deserializedOptions = entry.options.mapNotNull { (key, element) -> + if (!patch.options.containsKey(key)) return@mapNotNull null + val option = patch.options[key] + try { + key to deserializeOptionValue(element, option.type) + } catch (e: Exception) { + logger.warning("Failed to deserialize option \"$key\" for \"$patchName\": ${e.message}") + null + } + }.toMap() + if (deserializedOptions.isEmpty()) null else resolvedName to deserializedOptions + }?.toMap() ?: emptyMap() // endregion @@ -372,12 +438,18 @@ internal object PatchCommand : Callable { patchingResult.packageName = packageName patchingResult.packageVersion = packageVersion - val filteredPatches = patches.filterPatchSelection(packageName, packageVersion) + val filteredPatches = patches.filterPatchSelection( + packageName, + packageVersion, + jsonEnabledPatches, + jsonDisabledPatches, + ) logger.info("Setting patch options") + // Build CLI options map (CLI flags take precedence over JSON) val patchesList = patches.toList() - selection.filter { it.enabled != null }.associate { + val cliOptionsMap = selection.filter { it.enabled != null }.associate { val enabledSelection = it.enabled!! val resolvedName = enabledSelection.selector.name?.let { userInput -> @@ -385,7 +457,16 @@ internal object PatchCommand : Callable { } ?: patchesList[enabledSelection.selector.index!!].name!! resolvedName to enabledSelection.options - }.let(filteredPatches::setOptions) + } + + // Merge: JSON options as base, CLI options override + val mergedOptionsMap = (jsonOptionsMap.keys + cliOptionsMap.keys).associateWith { patchName -> + val jsonOpts = jsonOptionsMap[patchName] ?: emptyMap() + val cliOpts = cliOptionsMap[patchName] ?: emptyMap() + jsonOpts + cliOpts // CLI entries override JSON entries for same key + } + + mergedOptionsMap.let(filteredPatches::setOptions) patcher += filteredPatches @@ -519,6 +600,47 @@ internal object PatchCommand : Callable { logger.info("Patching result saved to $outputFile") } + // Auto-update options JSON file + if (optionsFilePath != null && updateOptions) { + try { + val currentOptionsFile = optionsFilePath!!.let { file -> + if (file.exists()) { + Json.decodeFromString(file.readText()) + } else { + null + } + } + + val updatedEntries = patches + .filter { it.name != null } + .associate { patch -> + val patchName = patch.name!! + val existingEntry = currentOptionsFile?.patches?.entries + ?.firstOrNull { it.key.equals(patchName, ignoreCase = true) }?.value + + val validOptionKeys = patch.options.keys + + val updatedOptions = validOptionKeys.associateWith { key -> + val option = patch.options[key] + existingEntry?.options?.get(key) + ?: PatchSerializer.serializeValue(option.default) + } + + patchName to PatchEntry( + enabled = existingEntry?.enabled ?: patch.use, + options = updatedOptions, + ) + } + + val updatedFile = PatchOptionsFile(patches = updatedEntries) + val json = Json { prettyPrint = true } + optionsFilePath!!.writeText(json.encodeToString(updatedFile)) + logger.info("Updated options file ${optionsFilePath!!.path}") + } catch (e: Exception) { + logger.warning("Failed to update options file: ${e.message}") + } + } + if (purge) { logger.info("Purging temporary files") purge(temporaryFilesPath) @@ -540,24 +662,29 @@ internal object PatchCommand : Callable { * * @param packageName The package name of the APK file to be patched. * @param packageVersion The version of the APK file to be patched. + * @param jsonEnabledPatches Patch names enabled via JSON options file (lowercase). + * @param jsonDisabledPatches Patch names disabled via JSON options file (lowercase). * @return The filtered patches. */ private fun Set>.filterPatchSelection( packageName: String, packageVersion: String, + jsonEnabledPatches: Set = emptySet(), + jsonDisabledPatches: Set = emptySet(), ): Set> = buildSet { - val enabledPatchesByName = + // CLI flags (take precedence over JSON) + val cliEnabledByName = selection.mapNotNull { it.enabled?.selector?.name?.lowercase() }.toSet() - val enabledPatchesByIndex = + val cliEnabledByIndex = selection.mapNotNull { it.enabled?.selector?.index }.toSet() - - val disabledPatches = + val cliDisabledByName = selection.mapNotNull { it.disable?.selector?.name?.lowercase() }.toSet() - val disabledPatchesByIndex = + val cliDisabledByIndex = selection.mapNotNull { it.disable?.selector?.index }.toSet() this@filterPatchSelection.withIndex().forEach patchLoop@{ (i, patch) -> val patchName = patch.name!! + val patchNameLower = patchName.lowercase() // Check package compatibility first to avoid duplicate logs for multi-app patches. patch.compatiblePackages?.let { packages -> @@ -587,13 +714,21 @@ internal object PatchCommand : Callable { return@let } ?: logger.fine("\"$patchName\" has no package constraints") - val isManuallyDisabled = patchName.lowercase() in disabledPatches || i in disabledPatchesByIndex - if (isManuallyDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually") + // CLI flags take precedence over JSON, JSON takes precedence over defaults + val isCliDisabled = patchNameLower in cliDisabledByName || i in cliDisabledByIndex + if (isCliDisabled) return@patchLoop logger.info("\"$patchName\" disabled manually") + + val isCliEnabled = patchNameLower in cliEnabledByName || i in cliEnabledByIndex + + // JSON-sourced enable/disable (only applies if no CLI flag for this patch) + val isJsonDisabled = !isCliEnabled && patchNameLower in jsonDisabledPatches + if (isJsonDisabled) return@patchLoop logger.info("\"$patchName\" disabled via options file") + + val isJsonEnabled = patchNameLower in jsonEnabledPatches val isEnabled = !exclusive && patch.use - val isManuallyEnabled = patchName.lowercase() in enabledPatchesByName || i in enabledPatchesByIndex - if (!(isEnabled || isManuallyEnabled)) { + if (!(isEnabled || isCliEnabled || isJsonEnabled)) { return@patchLoop logger.info("\"$patchName\" disabled") } diff --git a/src/main/kotlin/app/morphe/cli/command/model/PatchOptionsFile.kt b/src/main/kotlin/app/morphe/cli/command/model/PatchOptionsFile.kt new file mode 100644 index 0000000..0eaa789 --- /dev/null +++ b/src/main/kotlin/app/morphe/cli/command/model/PatchOptionsFile.kt @@ -0,0 +1,77 @@ +package app.morphe.cli.command.model + +import app.morphe.patcher.patch.Patch +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.booleanOrNull +import kotlinx.serialization.json.doubleOrNull +import kotlinx.serialization.json.floatOrNull +import kotlinx.serialization.json.intOrNull +import kotlinx.serialization.json.longOrNull +import kotlin.reflect.KType +import kotlin.reflect.typeOf + +@Serializable +data class PatchOptionsFile( + val patches: Map, +) + +@Serializable +data class PatchEntry( + val enabled: Boolean, + val options: Map = emptyMap(), +) + +/** + * Converts a set of loaded patches to a [PatchOptionsFile] for JSON export. + */ +@OptIn(ExperimentalSerializationApi::class) +fun Set>.toPatchOptionsFile(): PatchOptionsFile { + val entries = this + .filter { it.name != null } + .associate { patch -> + patch.name!! to PatchEntry( + enabled = patch.use, + options = patch.options.mapValues { (_, option) -> + PatchSerializer.serializeValue(option.default) + }, + ) + } + return PatchOptionsFile(patches = entries) +} + +/** + * Deserializes a [JsonElement] to a typed value based on the option's [KType]. + */ +fun deserializeOptionValue(element: JsonElement, type: KType): Any? { + if (element is JsonNull) return null + + if (element is JsonPrimitive) { + val classifier = type.classifier + return when (classifier) { + Boolean::class -> element.booleanOrNull + ?: throw IllegalArgumentException("Expected Boolean, got: $element") + Int::class -> element.intOrNull + ?: throw IllegalArgumentException("Expected Int, got: $element") + Long::class -> element.longOrNull + ?: throw IllegalArgumentException("Expected Long, got: $element") + Float::class -> element.floatOrNull + ?: throw IllegalArgumentException("Expected Float, got: $element") + Double::class -> element.doubleOrNull + ?: throw IllegalArgumentException("Expected Double, got: $element") + String::class -> element.content + else -> element.content + } + } + + if (element is JsonArray) { + val elementType = type.arguments.firstOrNull()?.type ?: typeOf() + return element.map { deserializeOptionValue(it, elementType) } + } + + return element.toString() +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt b/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt new file mode 100644 index 0000000..f0ca9e9 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt @@ -0,0 +1,84 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Refresh +import androidx.compose.material.icons.filled.WifiOff +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun OfflineBanner( + onRetry: () -> Unit, + modifier: Modifier = Modifier +) { + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + + val buttonColor = if (isHovered) { + MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f) + } else { + MaterialTheme.colorScheme.onErrorContainer + } + + Surface( + modifier = modifier.fillMaxWidth(), + color = MaterialTheme.colorScheme.errorContainer, + shape = RoundedCornerShape(12.dp) + ) { + Row( + modifier = Modifier.padding(start = 16.dp, top = 10.dp, bottom = 10.dp, end = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Default.WifiOff, + contentDescription = null, + tint = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.size(18.dp) + ) + Text( + text = "Offline — showing cached patches", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onErrorContainer, + modifier = Modifier.weight(1f) + ) + Surface( + onClick = onRetry, + modifier = Modifier.hoverable(interactionSource), + color = buttonColor, + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + tint = MaterialTheme.colorScheme.errorContainer, + modifier = Modifier.size(14.dp) + ) + Text( + text = "Retry", + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.errorContainer + ) + } + } + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 8d6fe8b..8b6dc0c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -37,6 +37,7 @@ import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.screens.home.components.ApkInfoCard import app.morphe.gui.ui.screens.home.components.FullScreenDropZone +import app.morphe.gui.ui.components.OfflineBanner import app.morphe.gui.ui.screens.patches.PatchesScreen import app.morphe.gui.ui.screens.patches.PatchSelectionScreen import app.morphe.gui.ui.theme.MorpheColors @@ -179,6 +180,17 @@ fun HomeScreenContent( } } + // Offline banner + if (uiState.isOffline && !uiState.isLoadingPatches) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + OfflineBanner( + onRetry = { viewModel.retryLoadPatches() }, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + ) + } + Spacer(modifier = Modifier.height(if (isSmall) 16.dp else 32.dp)) MiddleContent( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 244844b..165643f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -58,6 +58,12 @@ class HomeViewModel( val releases = releasesResult.getOrNull() if (releases.isNullOrEmpty()) { + // Try to fall back to cached .mpp file when offline + val offlinePatchFile = findCachedPatchFile(savedVersion) + if (offlinePatchFile != null) { + loadPatchesFromFile(offlinePatchFile, versionFromFilename(offlinePatchFile), latestVersion = null) + return@launch + } _uiState.value = _uiState.value.copy( isLoadingPatches = false, patchLoadError = "Could not fetch patches: ${releasesResult.exceptionOrNull()?.message}" @@ -129,6 +135,7 @@ class HomeViewModel( _uiState.value = _uiState.value.copy( isLoadingPatches = false, + isOffline = false, supportedApps = supportedApps, patchesVersion = release.tagName, latestPatchesVersion = latestVersion, @@ -136,6 +143,17 @@ class HomeViewModel( ) } catch (e: Exception) { Logger.error("Failed to load patches and supported apps", e) + // Try to fall back to cached .mpp file + val config = configRepository.loadConfig() + val offlinePatchFile = findCachedPatchFile(config.lastPatchesVersion) + if (offlinePatchFile != null) { + try { + loadPatchesFromFile(offlinePatchFile, versionFromFilename(offlinePatchFile), latestVersion = null) + return@launch + } catch (inner: Exception) { + Logger.error("Failed to load cached patches fallback", inner) + } + } _uiState.value = _uiState.value.copy( isLoadingPatches = false, patchLoadError = e.message ?: "Unknown error" @@ -144,6 +162,71 @@ class HomeViewModel( } } + /** + * Find any cached .mpp file when offline. + * Prefers the file matching savedVersion from config. + */ + private fun findCachedPatchFile(savedVersion: String?): File? { + val patchesDir = FileUtils.getPatchesDir() + val mppFiles = patchesDir.listFiles { file -> file.extension.equals("mpp", ignoreCase = true) } + ?.filter { it.length() > 0 } + ?: return null + + if (mppFiles.isEmpty()) return null + + return if (savedVersion != null) { + // Strip "v" prefix — savedVersion is "v1.13.0" but filenames are "patches-1.13.0.mpp" + val versionNumber = savedVersion.removePrefix("v") + mppFiles.firstOrNull { it.name.contains(versionNumber, ignoreCase = true) } + ?: mppFiles.maxByOrNull { it.lastModified() } + } else { + mppFiles.maxByOrNull { it.lastModified() } + } + } + + /** + * Extract a version string from an .mpp filename (e.g. "morphe-patches-1.3.0.mpp" -> "v1.3.0"). + */ + private fun versionFromFilename(file: File): String { + val name = file.nameWithoutExtension + // Try to find a version pattern like 1.2.3 or v1.2.3 + val match = Regex("""v?(\d+\.\d+\.\d+[^\s]*)""").find(name) + return match?.value ?: name + } + + /** + * Load patches from a local .mpp file and update UI state. + * Used as fallback when offline with cached patches. + */ + private suspend fun loadPatchesFromFile(patchFile: File, version: String, latestVersion: String?) { + cachedPatchesFile = patchFile + lastLoadedVersion = version + + val patchesResult = patchService.listPatches(patchFile.absolutePath) + val patches = patchesResult.getOrNull() + + if (patches == null || patches.isEmpty()) { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Could not load cached patches: ${patchesResult.exceptionOrNull()?.message}" + ) + return + } + + cachedPatches = patches + val supportedApps = SupportedAppExtractor.extractSupportedApps(patches) + Logger.info("Loaded ${supportedApps.size} supported apps from cached patches: ${patchFile.name}") + + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + isOffline = true, + supportedApps = supportedApps, + patchesVersion = version, + latestPatchesVersion = latestVersion, + patchLoadError = null + ) + } + /** * Retry loading patches. */ @@ -450,6 +533,7 @@ data class HomeUiState( val isAnalyzing: Boolean = false, // Dynamic patches data val isLoadingPatches: Boolean = true, + val isOffline: Boolean = false, val supportedApps: List = emptyList(), val patchesVersion: String? = null, val latestPatchesVersion: String? = null, // Track the latest available version diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index cdd794c..bfbeede 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -118,7 +118,7 @@ fun ApkInfoCard( // Architecture InfoColumn( label = "Architecture", - value = formatArchitectures(apkInfo.architectures), + value = if (apkInfo.architectures.isEmpty()) "Unknown" else apkInfo.architectures.joinToString(", "), modifier = Modifier.weight(1f) ) @@ -369,19 +369,19 @@ private fun VersionStatusBanner( } } -private fun formatArchitectures(archs: List): String { - if (archs.isEmpty()) return "Unknown" - - // Show full architecture names for clarity - val formatted = archs.map { arch -> - when (arch) { - "arm64-v8a" -> "arm64-v8a" - "armeabi-v7a" -> "armeabi-v7a" - "x86_64" -> "x86_64" - "x86" -> "x86" - else -> arch - } - } - - return formatted.joinToString(", ") -} +//private fun formatArchitectures(archs: List): String { +// if (archs.isEmpty()) return "Unknown" +// +// // Show full architecture names for clarity +// val formatted = archs.map { arch -> +// when (arch) { +// "arm64-v8a" -> "arm64-v8a" +// "armeabi-v7a" -> "armeabi-v7a" +// "x86_64" -> "x86_64" +// "x86" -> "x86" +// else -> arch +// } +// } +// +// return formatted.joinToString(", ") +//} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 3d6d725..36b32b6 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -7,11 +7,15 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -253,7 +257,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { onQueryChange = { viewModel.setSearchQuery(it) }, showOnlySelected = uiState.showOnlySelected, onShowOnlySelectedChange = { viewModel.setShowOnlySelected(it) }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) ) // Info card about default-disabled patches @@ -398,13 +402,14 @@ private fun SearchBar( OutlinedTextField( value = query, onValueChange = onQueryChange, - modifier = Modifier.weight(1f), - placeholder = { Text("Search patches...") }, + modifier = Modifier.weight(1f).height(48.dp), + placeholder = { Text("Search patches...", style = MaterialTheme.typography.bodySmall) }, leadingIcon = { Icon( imageVector = Icons.Default.Search, contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp) ) }, trailingIcon = { @@ -413,33 +418,58 @@ private fun SearchBar( Icon( imageVector = Icons.Default.Clear, contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) ) } } }, singleLine = true, shape = RoundedCornerShape(12.dp), + textStyle = MaterialTheme.typography.bodySmall, colors = OutlinedTextFieldDefaults.colors( focusedBorderColor = MorpheColors.Blue, unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) ) ) - FilterChip( - selected = showOnlySelected, - onClick = { onShowOnlySelectedChange(!showOnlySelected) }, - label = { Text("Selected") }, - leadingIcon = if (showOnlySelected) { - { + val chipInteractionSource = remember { MutableInteractionSource() } + val chipHovered by chipInteractionSource.collectIsHoveredAsState() + Surface( + modifier = Modifier + .hoverable(chipInteractionSource) + .clickable(interactionSource = chipInteractionSource, indication = null) { + onShowOnlySelectedChange(!showOnlySelected) + }, + shape = RoundedCornerShape(8.dp), + color = if (showOnlySelected) MorpheColors.Blue.copy(alpha = if (chipHovered) 0.22f else 0.12f) + else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (chipHovered) 0.7f else 0.4f), + border = BorderStroke( + width = 1.dp, + color = if (showOnlySelected) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + if (showOnlySelected) { Icon( imageVector = Icons.Default.Check, contentDescription = null, + tint = MorpheColors.Blue, modifier = Modifier.size(16.dp) ) } - } else null - ) + Text( + text = "Selected", + fontSize = 14.sp, + color = if (showOnlySelected) MorpheColors.Blue else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } } } @@ -449,17 +479,19 @@ private fun PatchListItem( isSelected: Boolean, onToggle: () -> Unit ) { + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() val backgroundColor = if (isSelected) { - MorpheColors.Blue.copy(alpha = 0.1f) + MorpheColors.Blue.copy(alpha = if (isHovered) 0.17f else 0.1f) } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) + MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (isHovered) 0.5f else 0.3f) } Card( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = onToggle), + .hoverable(interactionSource) + .clickable(interactionSource = interactionSource, indication = null, onClick = onToggle), colors = CardDefaults.cardColors(containerColor = backgroundColor), shape = RoundedCornerShape(12.dp) ) { @@ -500,17 +532,22 @@ private fun PatchListItem( // Show compatible packages if any if (patch.compatiblePackages.isNotEmpty()) { + val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") Spacer(modifier = Modifier.height(4.dp)) Row( horizontalArrangement = Arrangement.spacedBy(4.dp) ) { patch.compatiblePackages.take(2).forEach { pkg -> + val meaningful = pkg.name.split(".").filter { it !in genericSegments } + val displayName = meaningful.takeLast(2).joinToString(" ") + .replaceFirstChar { it.uppercase() } Surface( - color = MaterialTheme.colorScheme.surfaceVariant, + color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.18f) + else MaterialTheme.colorScheme.surfaceVariant, shape = RoundedCornerShape(4.dp) ) { Text( - text = pkg.name.substringAfterLast("."), + text = displayName, fontSize = 10.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) @@ -793,29 +830,45 @@ private fun ArchitectureSelectorCard( ) { architectures.forEach { arch -> val isSelected = selectedArchitectures.contains(arch) - FilterChip( - selected = isSelected, - onClick = { onToggleArchitecture(arch) }, - label = { + val archInteractionSource = remember { MutableInteractionSource() } + val archHovered by archInteractionSource.collectIsHoveredAsState() + Surface( + modifier = Modifier + .hoverable(archInteractionSource) + .clickable(interactionSource = archInteractionSource, indication = null) { + onToggleArchitecture(arch) + }, + shape = RoundedCornerShape(8.dp), + color = if (isSelected) MorpheColors.Teal.copy(alpha = if (archHovered) 0.28f else 0.2f) + else if (archHovered) MorpheColors.Teal.copy(alpha = 0.1f) + else Color.Transparent, + border = BorderStroke( + width = 0.5.dp, + color = if (isSelected) MorpheColors.Teal.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f) + ) + ) { + Row( + modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Box( + modifier = Modifier + .size(6.dp) + .clip(CircleShape) + .background( + if (isSelected) MorpheColors.Teal + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f) + ) + ) Text( text = arch, - fontSize = 12.sp + fontSize = 12.sp, + color = if (isSelected) MorpheColors.Teal else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) ) - }, - leadingIcon = if (isSelected) { - { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(14.dp) - ) - } - } else null, - colors = FilterChipDefaults.filterChipColors( - selectedContainerColor = MorpheColors.Teal.copy(alpha = 0.2f), - selectedLabelColor = MorpheColors.Teal - ) - ) + } + } } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 3289d04..60f4d41 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -2,6 +2,9 @@ package app.morphe.gui.ui.screens.patches import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -31,7 +34,10 @@ import app.morphe.gui.ui.components.DeviceIndicator import app.morphe.gui.ui.components.SettingsButton import app.morphe.gui.ui.components.getErrorType import app.morphe.gui.ui.components.getFriendlyErrorMessage +import app.morphe.gui.ui.components.OfflineBanner import app.morphe.gui.ui.theme.MorpheColors +import java.awt.FileDialog +import java.awt.Frame import java.io.File /** @@ -129,14 +135,24 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { .fillMaxSize() .padding(paddingValues) ) { - // Channel selector - ChannelSelector( - selectedChannel = uiState.selectedChannel, - onChannelSelected = { viewModel.setChannel(it) }, - stableCount = uiState.stableReleases.size, - devCount = uiState.devReleases.size, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) + // Channel selector (hidden when offline) + if (!uiState.isOffline) { + ChannelSelector( + selectedChannel = uiState.selectedChannel, + onChannelSelected = { viewModel.setChannel(it) }, + stableCount = uiState.stableReleases.size, + devCount = uiState.devReleases.size, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } + + // Offline banner + if (uiState.isOffline && uiState.currentReleases.isNotEmpty()) { + OfflineBanner( + onRetry = { viewModel.loadReleases() }, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 0.dp) + ) + } when { uiState.isLoading -> { @@ -159,7 +175,7 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { uiState.currentReleases.isEmpty() && !uiState.isLoading -> { Box( - modifier = Modifier.fillMaxSize(), + modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center ) { Column( @@ -187,10 +203,15 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { contentPadding = PaddingValues(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(uiState.currentReleases) { release -> + items( + items = uiState.currentReleases, + key = { it.tagName } + ) { release -> ReleaseCard( release = release, - isSelected = release == uiState.selectedRelease, + isSelected = release.tagName == uiState.selectedRelease?.tagName, + isDownloaded = release.tagName in uiState.cachedReleaseVersions, + isOffline = uiState.isOffline, onClick = { viewModel.selectRelease(release) } ) } @@ -205,6 +226,17 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { viewModel.confirmSelection() // Go back to HomeScreen - the new patches file is now cached navigator.pop() + }, + onExportJsonClick = { + val fileDialog = FileDialog(null as Frame?, "Export Options JSON", FileDialog.SAVE).apply { + file = "options.json" + isVisible = true + } + val directory = fileDialog.directory + val file = fileDialog.file + if (directory != null && file != null) { + viewModel.exportOptionsJson(File(directory, file)) + } } ) } @@ -296,26 +328,50 @@ private fun ChannelChip( private fun ReleaseCard( release: Release, isSelected: Boolean, + isDownloaded: Boolean, + isOffline: Boolean = false, onClick: () -> Unit ) { - val backgroundColor = if (isSelected) { - MorpheColors.Blue.copy(alpha = 0.1f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - } + val titleColor = MaterialTheme.colorScheme.onSurface + val subtitleColor = MaterialTheme.colorScheme.onSurfaceVariant + val dateColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + val accentColor = if (isSelected && isDownloaded) MorpheColors.Teal else MorpheColors.Blue + val devBadgeColor = MorpheColors.Teal var isExpanded by remember { mutableStateOf(false) } val hasNotes = !release.body.isNullOrBlank() + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + val cardBackground = when { + isSelected && isDownloaded -> MorpheColors.Teal.copy(alpha = if (isHovered) 0.22f else 0.15f) + isSelected -> MorpheColors.Blue.copy(alpha = if (isHovered) 0.22f else 0.15f) + else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (isHovered) 0.7f else 0.25f) + } + Card( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = onClick), - colors = CardDefaults.cardColors(containerColor = backgroundColor), + .hoverable(interactionSource) + .clickable(interactionSource = interactionSource, indication = null) { onClick() }, + colors = CardDefaults.cardColors(containerColor = cardBackground), shape = RoundedCornerShape(12.dp) ) { - Column(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)) { + // Green ribbon for downloaded (non-selected) cards + if (isDownloaded && !isSelected) { + Box( + modifier = Modifier + .width(4.dp) + .fillMaxHeight() + .background( + MorpheColors.Teal, + RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp) + ) + ) + } + + Column(modifier = Modifier.weight(1f)) { Row( modifier = Modifier .fillMaxWidth() @@ -332,18 +388,18 @@ private fun ReleaseCard( text = release.tagName, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + color = titleColor ) if (release.isDevRelease()) { Surface( - color = MorpheColors.Teal.copy(alpha = 0.2f), + color = devBadgeColor.copy(alpha = 0.2f), shape = RoundedCornerShape(4.dp) ) { Text( text = "DEV", fontSize = 10.sp, fontWeight = FontWeight.Bold, - color = MorpheColors.Teal, + color = devBadgeColor, modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) ) } @@ -357,24 +413,30 @@ private fun ReleaseCard( Text( text = "${mppAsset.name} (${mppAsset.getFormattedSize()})", fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = subtitleColor ) } - Text( - text = "Published: ${formatDate(release.publishedAt)}", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) + val formattedDate = formatDate(release.publishedAt) + if (formattedDate.isNotEmpty()) { + Text( + text = "${if (isOffline) "Cached:" else "Published:"} $formattedDate", + fontSize = 12.sp, + color = dateColor + ) + } if (hasNotes) { Spacer(modifier = Modifier.height(4.dp)) Surface( - color = MorpheColors.Blue.copy(alpha = 0.1f), + color = accentColor.copy(alpha = 0.1f), shape = RoundedCornerShape(6.dp), modifier = Modifier .clip(RoundedCornerShape(6.dp)) - .clickable { isExpanded = !isExpanded } + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { isExpanded = !isExpanded } ) { Row( modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), @@ -385,12 +447,12 @@ private fun ReleaseCard( text = if (isExpanded) "Hide patch notes" else "Patch notes", fontSize = 12.sp, fontWeight = FontWeight.Medium, - color = MorpheColors.Blue + color = accentColor ) Icon( imageVector = if (isExpanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, contentDescription = null, - tint = MorpheColors.Blue, + tint = accentColor, modifier = Modifier.size(16.dp) ) } @@ -398,14 +460,6 @@ private fun ReleaseCard( } } - if (isSelected) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = "Selected", - tint = MorpheColors.Blue, - modifier = Modifier.size(24.dp) - ) - } } // Expandable release notes @@ -418,6 +472,7 @@ private fun ReleaseCard( modifier = Modifier.padding(16.dp) ) } + } } } } @@ -518,7 +573,8 @@ private fun cleanMarkdown(text: String): String { private fun BottomActionBar( uiState: PatchesUiState, onDownloadClick: () -> Unit, - onSelectClick: () -> Unit + onSelectClick: () -> Unit, + onExportJsonClick: () -> Unit, ) { Surface( modifier = Modifier.fillMaxWidth(), @@ -586,18 +642,39 @@ private fun BottomActionBar( fontWeight = FontWeight.Medium ) } + + // Export JSON button / spinner + if (uiState.isExporting) { + Box( + modifier = Modifier.height(48.dp).width(48.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp), + color = MorpheColors.Blue, + strokeWidth = 2.dp + ) + } + } else { + OutlinedButton( + onClick = onExportJsonClick, + modifier = Modifier.height(48.dp), + shape = RoundedCornerShape(12.dp), + border = androidx.compose.foundation.BorderStroke( + 1.dp, + MorpheColors.Blue + ), + ) { + Text( + text = "Export JSON", + fontWeight = FontWeight.Medium, + color = MorpheColors.Blue + ) + } + } } } - // Downloaded file info - uiState.downloadedPatchFile?.let { file -> - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Downloaded: ${file.name}", - fontSize = 12.sp, - color = MorpheColors.Teal - ) - } } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt index b8c3365..3cc072c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt @@ -1,15 +1,21 @@ package app.morphe.gui.ui.screens.patches +import app.morphe.cli.command.model.toPatchOptionsFile +import app.morphe.patcher.patch.loadPatchesFromJar import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import app.morphe.gui.data.model.Release import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json import app.morphe.gui.util.Logger +import app.morphe.gui.data.model.ReleaseAsset import java.io.File class PatchesViewModel( @@ -61,21 +67,67 @@ class PatchesViewModel( // Check if patches for the initial release are already cached val cachedFile = initialRelease?.let { checkCachedPatches(it) } + // Build set of all cached release versions + val cachedVersions = releases + .filter { checkCachedPatches(it) != null } + .map { it.tagName } + .toSet() + _uiState.value = _uiState.value.copy( isLoading = false, + isOffline = false, + offlineReleases = emptyList(), stableReleases = stableReleases, devReleases = devReleases, selectedChannel = initialChannel, selectedRelease = initialRelease, - downloadedPatchFile = cachedFile + downloadedPatchFile = cachedFile, + cachedReleaseVersions = cachedVersions ) Logger.info("Loaded ${stableReleases.size} stable and ${devReleases.size} dev releases, saved=$savedVersion, selected=${initialRelease?.tagName}, cached: ${cachedFile != null}") }, onFailure = { e -> - _uiState.value = _uiState.value.copy( - isLoading = false, - error = e.message ?: "Failed to load releases" - ) + // Even when offline, check for cached .mpp files + val cachedFiles = findAllCachedPatchFiles() + if (cachedFiles.isNotEmpty()) { + val offlineReleases = cachedFiles.mapNotNull { buildOfflineRelease(it) } + .sortedByDescending { rel -> + val version = rel.tagName.removePrefix("v") + parseVersionParts(version) + .fold(0L) { acc, part -> acc * 10000 + part } + } + val config = configRepository.loadConfig() + val savedVersion = config.lastPatchesVersion + + // Pre-select the saved version, or fall back to the first (most recent) + val initialRelease = if (savedVersion != null) { + offlineReleases.find { it.tagName == savedVersion } + } else null + val selected = initialRelease ?: offlineReleases.firstOrNull() + + // Find the cached file for the selected release + val cachedFile = selected?.let { rel -> + val assetName = rel.assets.firstOrNull()?.name + cachedFiles.find { it.name == assetName } + } + + _uiState.value = _uiState.value.copy( + isLoading = false, + isOffline = true, + offlineReleases = offlineReleases, + selectedRelease = selected, + downloadedPatchFile = cachedFile, + cachedReleaseVersions = offlineReleases.map { it.tagName }.toSet(), + error = null + ) + Logger.info("Offline — found ${cachedFiles.size} cached patch file(s), selected=${selected?.tagName}") + } else { + _uiState.value = _uiState.value.copy( + isLoading = false, + isOffline = true, + error = e.message ?: "Failed to load releases" + ) + } Logger.error("Failed to load releases", e) } ) @@ -83,8 +135,17 @@ class PatchesViewModel( } fun selectRelease(release: Release) { - // Check if patches for this release are already cached - val cachedFile = checkCachedPatches(release) + val cachedFile = if (_uiState.value.isOffline) { + // In offline mode, find the cached file by matching the asset name + val assetName = release.assets.firstOrNull()?.name + if (assetName != null) { + val patchesDir = app.morphe.gui.util.FileUtils.getPatchesDir() + val file = File(patchesDir, assetName) + if (file.exists()) file else null + } else null + } else { + checkCachedPatches(release) + } _uiState.value = _uiState.value.copy( selectedRelease = release, @@ -93,6 +154,53 @@ class PatchesViewModel( Logger.info("Selected release: ${release.tagName}, cached: ${cachedFile != null}") } + /** + * Find all cached .mpp files in the patches directory. + */ + private fun findAllCachedPatchFiles(): List { + val patchesDir = app.morphe.gui.util.FileUtils.getPatchesDir() + return patchesDir.listFiles { file -> file.extension.equals("mpp", ignoreCase = true) } + ?.filter { it.length() > 0 } + ?: emptyList() + } + + private val versionRegex = Regex("""(\d+\.\d+\.\d+(?:-dev\.\d+)?)""") + + /** + * Parse semantic version parts for comparison. + * "1.13.0" -> [1, 13, 0], "1.4.0-dev.5" -> [1, 4, 0, 5] + */ + private fun parseVersionParts(version: String): List { + return version.replace("-dev.", ".").split(".").mapNotNull { it.toIntOrNull() } + } + + /** + * Build a synthetic Release from a cached .mpp file for offline display. + * Extracts version from the filename (e.g. "patches-1.13.0.mpp" -> "v1.13.0"). + * publishedAt is left empty since we don't know the actual release date offline. + */ + private fun buildOfflineRelease(file: File): Release? { + val match = versionRegex.find(file.name) ?: return null + val version = match.groupValues[1] + + return Release( + id = file.name.hashCode().toLong(), + tagName = "v$version", + name = "v$version", + isPrerelease = version.contains("dev"), + publishedAt = java.time.Instant.ofEpochMilli(file.lastModified()).toString(), + assets = listOf( + ReleaseAsset( + id = file.name.hashCode().toLong(), + name = file.name, + downloadUrl = "", + size = file.length(), + contentType = "application/octet-stream" + ) + ) + ) + } + /** * Check if patches for a release are already downloaded and valid. */ @@ -145,7 +253,8 @@ class PatchesViewModel( _uiState.value = _uiState.value.copy( isDownloading = false, downloadedPatchFile = patchFile, - downloadProgress = 1f + downloadProgress = 1f, + cachedReleaseVersions = _uiState.value.cachedReleaseVersions + release.tagName ) Logger.info("Patches downloaded: ${patchFile.absolutePath}") @@ -180,6 +289,34 @@ class PatchesViewModel( } } + /** + * Export patch options from the downloaded .mpp file to a JSON file. + */ + fun exportOptionsJson(outputFile: File) { + val patchFile = _uiState.value.downloadedPatchFile ?: return + + screenModelScope.launch { + _uiState.value = _uiState.value.copy(isExporting = true) + try { + withContext(Dispatchers.IO) { + val patches = loadPatchesFromJar(setOf(patchFile)) + val patchOptionsFile = patches.toPatchOptionsFile() + val json = Json { prettyPrint = true } + outputFile.parentFile?.mkdirs() + outputFile.writeText(json.encodeToString(patchOptionsFile)) + } + Logger.info("Exported ${_uiState.value.downloadedPatchFile?.name} options to ${outputFile.path}") + } catch (e: Exception) { + _uiState.value = _uiState.value.copy( + error = "Failed to export options: ${e.message}" + ) + Logger.error("Failed to export options JSON", e) + } finally { + _uiState.value = _uiState.value.copy(isExporting = false) + } + } + } + fun getApkPath(): String = apkPath fun getApkName(): String = apkName } @@ -191,6 +328,8 @@ enum class ReleaseChannel { data class PatchesUiState( val isLoading: Boolean = false, + val isOffline: Boolean = false, + val offlineReleases: List = emptyList(), val stableReleases: List = emptyList(), val devReleases: List = emptyList(), val selectedChannel: ReleaseChannel = ReleaseChannel.STABLE, @@ -198,13 +337,16 @@ data class PatchesUiState( val isDownloading: Boolean = false, val downloadProgress: Float = 0f, val downloadedPatchFile: File? = null, + val cachedReleaseVersions: Set = emptySet(), + val isExporting: Boolean = false, val error: String? = null ) { val currentReleases: List - get() = when (selectedChannel) { - ReleaseChannel.STABLE -> stableReleases - ReleaseChannel.DEV -> devReleases - } + get() = if (isOffline) offlineReleases + else when (selectedChannel) { + ReleaseChannel.STABLE -> stableReleases + ReleaseChannel.DEV -> devReleases + } val isReady: Boolean get() = downloadedPatchFile != null diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 45478be..f39c0a0 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -41,6 +41,7 @@ import app.morphe.gui.data.repository.PatchRepository import app.morphe.gui.util.PatchService import org.jetbrains.compose.resources.painterResource import org.koin.compose.koinInject +import app.morphe.gui.ui.components.OfflineBanner import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.theme.MorpheColors import androidx.compose.runtime.rememberCoroutineScope @@ -159,6 +160,14 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { Spacer(modifier = Modifier.height(16.dp)) + // Offline banner + if (uiState.isOffline && uiState.phase == QuickPatchPhase.IDLE) { + OfflineBanner( + onRetry = { viewModel.retryLoadPatches() }, + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) + ) + } + // Main content based on phase // Remember last valid data for safe animation transitions val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 4c00950..4104e45 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -58,6 +58,13 @@ class QuickPatchViewModel( val releases = releasesResult.getOrNull() if (releases.isNullOrEmpty()) { + // Try to fall back to cached .mpp file when offline + val config = configRepository.loadConfig() + val offlinePatchFile = findCachedPatchFile(config.lastPatchesVersion) + if (offlinePatchFile != null) { + loadPatchesFromFile(offlinePatchFile, versionFromFilename(offlinePatchFile)) + return@launch + } Logger.warn("Quick mode: Could not fetch releases") _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "Could not fetch releases. Check your internet connection.") return@launch @@ -106,15 +113,83 @@ class QuickPatchViewModel( isLoadingPatches = false, supportedApps = supportedApps, patchesVersion = release.tagName, - patchLoadError = null + patchLoadError = null, + isOffline = false ) } catch (e: Exception) { Logger.error("Quick mode: Failed to load patches", e) + // Try to fall back to cached .mpp file + val config = configRepository.loadConfig() + val offlinePatchFile = findCachedPatchFile(config.lastPatchesVersion) + if (offlinePatchFile != null) { + try { + loadPatchesFromFile(offlinePatchFile, versionFromFilename(offlinePatchFile)) + return@launch + } catch (inner: Exception) { + Logger.error("Quick mode: Failed to load cached patches fallback", inner) + } + } _uiState.value = _uiState.value.copy(isLoadingPatches = false, patchLoadError = "Failed to load patches: ${e.message}") } } } + /** + * Find any cached .mpp file when offline. + */ + private fun findCachedPatchFile(savedVersion: String?): File? { + val patchesDir = FileUtils.getPatchesDir() + val mppFiles = patchesDir.listFiles { file -> file.extension.equals("mpp", ignoreCase = true) } + ?.filter { it.length() > 0 } + ?: return null + + if (mppFiles.isEmpty()) return null + + return if (savedVersion != null) { + mppFiles.firstOrNull { it.name.contains(savedVersion, ignoreCase = true) } + ?: mppFiles.maxByOrNull { it.lastModified() } + } else { + mppFiles.maxByOrNull { it.lastModified() } + } + } + + private fun versionFromFilename(file: File): String { + val name = file.nameWithoutExtension + val match = Regex("""v?(\d+\.\d+\.\d+[^\s]*)""").find(name) + return match?.value ?: name + } + + /** + * Load patches from a local .mpp file (offline fallback). + */ + private suspend fun loadPatchesFromFile(patchFile: File, version: String) { + cachedPatchesFile = patchFile + + val patchesResult = patchService.listPatches(patchFile.absolutePath) + val patches = patchesResult.getOrNull() + + if (patches.isNullOrEmpty()) { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Could not load cached patches: ${patchesResult.exceptionOrNull()?.message}" + ) + return + } + + cachedPatches = patches + val supportedApps = SupportedAppExtractor.extractSupportedApps(patches) + cachedSupportedApps = supportedApps + Logger.info("Quick mode: Loaded ${supportedApps.size} supported apps from cached patches: ${patchFile.name}") + + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + supportedApps = supportedApps, + patchesVersion = version, + patchLoadError = null, + isOffline = true + ) + } + /** * Retry loading patches after a failure. */ @@ -462,5 +537,6 @@ data class QuickPatchUiState( val isLoadingPatches: Boolean = true, val supportedApps: List = emptyList(), val patchesVersion: String? = null, - val patchLoadError: String? = null + val patchLoadError: String? = null, + val isOffline: Boolean = false ) From 34eb844fa635ae39c87551cbd0e4ebb620fbdfc5 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:15:25 +0530 Subject: [PATCH 25/49] Update PatchesViewModel.kt --- .../app/morphe/gui/ui/screens/patches/PatchesViewModel.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt index 3cc072c..6acbbe0 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt @@ -1,6 +1,6 @@ package app.morphe.gui.ui.screens.patches -import app.morphe.cli.command.model.toPatchOptionsFile +import app.morphe.cli.command.model.toPatchBundle import app.morphe.patcher.patch.loadPatchesFromJar import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope @@ -300,10 +300,10 @@ class PatchesViewModel( try { withContext(Dispatchers.IO) { val patches = loadPatchesFromJar(setOf(patchFile)) - val patchOptionsFile = patches.toPatchOptionsFile() + val bundle = patches.toPatchBundle(sourceFiles = setOf(patchFile)) val json = Json { prettyPrint = true } outputFile.parentFile?.mkdirs() - outputFile.writeText(json.encodeToString(patchOptionsFile)) + outputFile.writeText(json.encodeToString(listOf(bundle))) } Logger.info("Exported ${_uiState.value.downloadedPatchFile?.name} options to ${outputFile.path}") } catch (e: Exception) { From f3f80fc93a4bcd0b65eb236125878e8313fc104b Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:08:58 +0530 Subject: [PATCH 26/49] conflict fix proper --- build.gradle.kts | 6 +++--- gradle/libs.versions.toml | 3 --- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 339e8fd..421907a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -105,9 +105,8 @@ dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.coroutines.swing) implementation(libs.kotlinx.serialization.json) - testImplementation(libs.kotlin.test) - testImplementation(libs.junit.params) -} +// testImplementation(libs.kotlin.test) +//} // -- Networking (GUI) -------------------------------------------------- implementation(libs.ktor.client.core) @@ -131,6 +130,7 @@ dependencies { // -- Testing ----------------------------------------------------------- testImplementation(libs.kotlin.test) + testImplementation(libs.junit.params) testImplementation(libs.mockk) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb1cf6f..9e6b09e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,9 +35,6 @@ mockk = "1.14.3" [libraries] # Morphe Core junit-params = { module = "org.junit.jupiter:junit-jupiter-params", version.ref = "junit" } -kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } -kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx" } -kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } morphe-patcher = { module = "app.morphe:morphe-patcher", version.ref = "morphe-patcher" } morphe-library = { module = "app.morphe:morphe-library-jvm", version.ref = "morphe-library" } From 2cbb805b4576052692a170c00d3c388947982d8b Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 22 Feb 2026 15:21:21 +0530 Subject: [PATCH 27/49] this for sure is the fix --- src/main/kotlin/app/morphe/engine/PatchEngine.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index 19b474e..b1136be 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -103,7 +103,7 @@ object PatchEngine { Patcher(patcherConfig).use { patcher -> val packageName = patcher.context.packageMetadata.packageName - val packageVersion = patcher.context.packageMetadata.packageVersion + val packageVersion = patcher.context.packageMetadata.versionCode coroutineContext.ensureActive() From 3100d85b57838b54d1dd733f0be4a70945234134 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 28 Feb 2026 17:24:24 +0530 Subject: [PATCH 28/49] partial fix --- .../app/morphe/cli/command/PatchCommand.kt | 2 +- .../kotlin/app/morphe/engine/PatchEngine.kt | 21 ++++++++++++------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt index 1e3ddb2..2e56f2a 100644 --- a/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/PatchCommand.kt @@ -19,7 +19,7 @@ import app.morphe.cli.command.model.mergeWith import app.morphe.cli.command.model.toPatchBundle import app.morphe.cli.command.model.toSerializablePatch import app.morphe.cli.command.model.withUpdatedBundle -import app.morphe.gui.util.ApkLibraryStripper +import app.morphe.engine.ApkLibraryStripper import app.morphe.patcher.apk.ApkUtils import app.morphe.patcher.apk.ApkUtils.applyTo import app.morphe.library.installation.installer.* diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index b1136be..476829f 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -8,7 +8,10 @@ import app.morphe.patcher.PatcherConfig import app.morphe.patcher.patch.Patch import com.reandroid.apkeditor.merge.Merger import com.reandroid.apkeditor.merge.MergerOptions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.withContext import java.io.File import java.io.PrintWriter import java.io.StringWriter @@ -64,7 +67,9 @@ object PatchEngine { * Only throws for init errors (e.g. Patcher can't open the APK). */ suspend fun patch(config: Config, onProgress: (String) -> Unit = {}): Result { - val tempDir = config.tempDir ?: Files.createTempDirectory("morphe-patching").toFile() + val tempDir = config.tempDir ?: withContext(Dispatchers.IO) { + Files.createTempDirectory("morphe-patching") + }.toFile() var mergedApkToCleanup: File? = null val stepResults = mutableListOf() val appliedPatches = mutableListOf() @@ -87,7 +92,7 @@ object PatchEngine { config.inputApk } - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() // 2. Initialize patcher val patcherTempDir = File(tempDir, "patcher") @@ -105,7 +110,7 @@ object PatchEngine { val packageName = patcher.context.packageMetadata.packageName val packageVersion = patcher.context.packageMetadata.versionCode - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() // 3. Filter patches onProgress("Filtering patches for $packageName v$packageVersion...") @@ -120,7 +125,7 @@ object PatchEngine { onProgress = onProgress, ) - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() // 4. Set options if (config.patchOptions.isNotEmpty()) { @@ -132,7 +137,7 @@ object PatchEngine { patcher += filteredPatches - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() fun earlyResult() = Result( success = false, @@ -174,7 +179,7 @@ object PatchEngine { return earlyResult() } - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() // 6. Rebuild APK onProgress("Rebuilding APK...") @@ -191,7 +196,7 @@ object PatchEngine { val rebuiltApk = File(tempDir, "rebuilt.apk") - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() // 7. Strip libs (if configured) if (config.architecturesToKeep.isNotEmpty()) { @@ -207,7 +212,7 @@ object PatchEngine { } } - coroutineContext.ensureActive() + currentCoroutineContext().ensureActive() // 8. Sign APK (unless unsigned) val tempOutput = File(tempDir, config.outputApk.name) From a445ce6271766b9b1ae0278a8ea52d5c994fadf4 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:20:04 +0530 Subject: [PATCH 29/49] final fix? --- src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt index 7320a4e..682f363 100644 --- a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt @@ -16,7 +16,7 @@ import java.util.logging.Logger @Command( name = "options-create", - description = ["Create an options JSON file for the patches and options."], + description = ["Create an options JSON file for the patches and options."] , ) internal object OptionsCommand : Callable { From c3b231176a642051891b2235fd04a37a07536894 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 28 Feb 2026 18:21:05 +0530 Subject: [PATCH 30/49] final fix actually? --- src/main/kotlin/app/morphe/engine/PatchEngine.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index 476829f..e79ab36 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -1,8 +1,8 @@ package app.morphe.engine -import app.morphe.library.ApkUtils -import app.morphe.library.ApkUtils.applyTo -import app.morphe.library.setOptions +import app.morphe.patcher.apk.ApkUtils +import app.morphe.patcher.apk.ApkUtils.applyTo +import app.morphe.patcher.patch.setOptions import app.morphe.patcher.Patcher import app.morphe.patcher.PatcherConfig import app.morphe.patcher.patch.Patch From bf8058b6aac4620b9747eb33cc8063d67ad6b7bb Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Mon, 9 Mar 2026 01:01:50 +0530 Subject: [PATCH 31/49] Extremely minor update Excluded 'aapt2' binaries built for Android architectures. Should marginally reduce size. --- build.gradle.kts | 3 ++- src/main/kotlin/app/morphe/engine/PatchEngine.kt | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 421907a..3b52022 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -77,7 +77,8 @@ val strippedApkEditorLib by tasks.registering(org.gradle.jvm.tasks.Jar::class) { "com/beust/jcommander/**", "javax/annotation/**", "smali.properties", - "baksmali.properties" + "baksmali.properties", + "/prebuilt/android/**" ) } diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index e79ab36..259d338 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -19,7 +19,7 @@ import java.nio.file.Files import kotlin.coroutines.coroutineContext /** - * Single patching pipeline shared by CLI and GUI. + * Single patching pipeline shared by CLI and GUI. (Eventually. Right now we are still having 2 pipelines) */ object PatchEngine { From 633fa97c47a7e444fe9262c3f9eaad405f4cc1a6 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Mon, 9 Mar 2026 10:21:18 +0530 Subject: [PATCH 32/49] revert the aapt2 libraries exclude change Reverting this change since it is used by cli users using termux. --- build.gradle.kts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 3b52022..421907a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -77,8 +77,7 @@ val strippedApkEditorLib by tasks.registering(org.gradle.jvm.tasks.Jar::class) { "com/beust/jcommander/**", "javax/annotation/**", "smali.properties", - "baksmali.properties", - "/prebuilt/android/**" + "baksmali.properties" ) } From 258ee860f33e2fd2cc8a8b7b288ea5726419bfea Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Wed, 11 Mar 2026 18:50:39 +0530 Subject: [PATCH 33/49] backup before remote reset --- CLAUDE.md | 89 ++ src/main/kotlin/app/morphe/gui/App.kt | 19 +- .../app/morphe/gui/data/model/AppConfig.kt | 28 +- .../app/morphe/gui/data/model/Release.kt | 13 +- .../app/morphe/gui/data/model/SupportedApp.kt | 61 +- .../gui/data/repository/ConfigRepository.kt | 60 + .../gui/data/repository/PatchRepository.kt | 61 +- .../gui/data/repository/PatchSourceManager.kt | 142 +++ .../kotlin/app/morphe/gui/di/AppModule.kt | 19 +- .../gui/ui/components/DeviceIndicator.kt | 170 +-- .../morphe/gui/ui/components/OfflineBanner.kt | 80 +- .../gui/ui/components/SettingsButton.kt | 103 +- .../gui/ui/components/SettingsDialog.kt | 1025 ++++++++++++++--- .../morphe/gui/ui/screens/home/HomeScreen.kt | 840 ++++++++------ .../gui/ui/screens/home/HomeViewModel.kt | 70 +- .../ui/screens/home/components/ApkInfoCard.kt | 583 +++++----- .../screens/patches/PatchSelectionScreen.kt | 295 +++-- .../patches/PatchSelectionViewModel.kt | 40 +- .../gui/ui/screens/patches/PatchesScreen.kt | 92 +- .../ui/screens/patches/PatchesViewModel.kt | 39 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 181 ++- .../ui/screens/quick/QuickPatchViewModel.kt | 71 +- .../morphe/gui/ui/theme/MorpheTypography.kt | 20 + .../morphe/gui/util/SupportedAppExtractor.kt | 1 + .../resources/fonts/JetBrainsMono-Bold.ttf | Bin 0 -> 277828 bytes .../resources/fonts/JetBrainsMono-Light.ttf | Bin 0 -> 276452 bytes .../resources/fonts/JetBrainsMono-Medium.ttf | Bin 0 -> 273860 bytes .../resources/fonts/JetBrainsMono-Regular.ttf | Bin 0 -> 273900 bytes .../fonts/JetBrainsMono-SemiBold.ttf | Bin 0 -> 277092 bytes 29 files changed, 2932 insertions(+), 1170 deletions(-) create mode 100644 CLAUDE.md create mode 100644 src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt create mode 100644 src/main/resources/fonts/JetBrainsMono-Bold.ttf create mode 100644 src/main/resources/fonts/JetBrainsMono-Light.ttf create mode 100644 src/main/resources/fonts/JetBrainsMono-Medium.ttf create mode 100644 src/main/resources/fonts/JetBrainsMono-Regular.ttf create mode 100644 src/main/resources/fonts/JetBrainsMono-SemiBold.ttf diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4f4e9e8 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,89 @@ +# Morphe/ MorpheCLI (will be changed to Morphe Desktop) - Developer Guide + +## Project Overview +Morphe Desktop is a command-line and a GUI application that uses Morphe Patcher to patch Android apps. It has 2 parts +- **CLI**: Opens when the user calls the .jar file from a terminal. +- **GUI**: Opens when the user double-clicks the jar + +## Design Thinking + +Before coding, understand the context and commit to a BOLD aesthetic direction: +- **Purpose**: What problem does this interface solve? Who uses it? +- **Tone**: Commit to a distinct direction: brutally minimal, maximalist chaos, luxury/refined, lo-fi/zine, dark/moody, soft/pastel, editorial/magazine, brutalist/raw, retro-futuristic, handcrafted/artisanal, organic/natural, art deco/geometric, playful/whimsical, industrial/utilitarian, etc. There are infinite varieties to start from and surpass. Use these as inspiration, but the final design should feel singular, with every detail working in service of one cohesive direction. +- **Constraints**: Technical requirements (framework, performance, accessibility). +- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? + +**CRITICAL**: Choose a clear conceptual direction and execute it vigorously. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. + +Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: +- Production-grade, functional, and responsive +- Visually striking and memorable +- Cohesive with a clear aesthetic point-of-view +- Meticulously refined in every detail + +**Morphe CLI Context (MANDATORY — root every decision here)**: +- **Purpose**: This is a desktop GUI wrapper for morphe-cli (Morphe Patcher). Users select an APK (or APKM bundle), choose from community patches, apply them, sign the output, and optionally push to device via ADB. Core flows: drag-and-drop APK, searchable patch list with descriptions/categories, live log console during patching (long-running process), progress visualization, output APK management. Users are power users/modders (ReVanced/Morphe veterans) — they want speed, transparency, and raw power, not hand-holding. +- **Who uses it**: Tech-savvy Android tinkerers who already run the CLI. Desktop advantage: easier file management, bigger screen for logs/patches, keyboard shortcuts, multi-window feel. +- **Differentiation (UNFORGETTABLE)**: Make the patching process feel like a cyber-hacker ritual. Visual disassembly animation, neon code-rain progress, glitch effects on success/failure, terminal-style logs with syntax coloring. Someone should remember "that one desktop patcher that looks like it belongs in a cyberdeck". Beat Vary (existing simple Gio GUI) by being visually addictive yet perfectly functional. + +**CRITICAL**: The UI must feel like a professional dev tool that secretly has underground soul. Never default to plain Material 3 mobile patterns. + +## Frontend Aesthetics Guidelines + +Focus on: +- **Typography**: Typography carries the design's singular voice. Choose fonts with interesting personality. Default fonts signal default thinking: skip Arial, Inter, Roboto, system stacks. Font choices should be inseparable from the aesthetic direction. Display type should be expressive, even risky. Body text should be legible, refined. Pair them like actors in a scene. Work the full typographic range: size, weight, case, spacing to establish hierarchy. +- **Color & Theme**: Commit to a cohesive aesthetic. Palettes should take a clear position: bold and saturated, moody and restrained, or high-contrast and minimal. Lead with a dominant color, punctuate with sharp accents. Avoid timid, non-committal distributions. Use CSS variables for consistency. +- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. +- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap and z-depth. Diagonal flow. Grid-breaking elements. Dramatic scale jumps. Full-bleed moments. Generous negative space OR controlled density. +- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise and grain overlays, geometric patterns, layered transparencies and glassmorphism, dramatic or soft shadows and glows, parallax depth, decorative borders and clip-path shapes, print-inspired textures (halftone, duotone, stipple), knockout typography, and custom cursors. + +**CRITICAL ADDITIONS FOR COMPOSE DESKTOP**: +- **Framework Reality**: You are writing **Kotlin + Jetpack Compose for Desktop** (not HTML/React). Use `@Composable`, `Modifier`, `MaterialTheme` (or fully custom), `Window`, `MenuBar`, `Tray` if needed. Target Windows/macOS/Linux with native window chrome or custom undecorated title bar. Output complete, copy-pasteable, production-ready code with `main()` + `application { Window(...) }`. Include previews where possible. +- **Theming Mastery** (use official custom design system patterns): + - Extend `MaterialTheme` or build a full custom system with `CompositionLocal` (e.g., `LocalMorpheColors`, `LocalMorpheTypography`). + - Default to **dark theme** (dev tool law). Create semantic colors: `neonPrimary`, `terminalGreen`, `patchAccent`, `errorGlitchRed`. + - Typography: Pair a bold display font (load via `FontFamily` from resources — no Inter/Roboto) with **JetBrains Mono** or VT323-style monospace for logs/console. Make hierarchy dramatic (huge patch titles, tiny hex metadata). + - Shapes: Sharp corners or subtle hexagonal cuts for "patcher" tech feel. Use `absoluteElevation` for dynamic surface tints. +- **Motion & Delight (Compose-native)**: + - Staggered reveals on window open with `AnimatedVisibility` + `spring()` + `delayMillis`. + - Patching progress: Custom `Canvas`-based animation (code rain, hex particles, or APK "decompiling" scanline effect — performant, not heavy). + - Hover/glitch: `graphicsLayer` + `scale` + subtle `colorMatrix` for neon glows and chromatic aberration on active elements. + - Never overdo micro-animations — one hero animation during patching > 50 tiny ones. +- **Desktop-Power Interactions**: + - Drag & drop APK anywhere (use `onDrag` modifiers). + - Keyboard shortcuts (Cmd/Ctrl+O, Enter to patch). + - Context menus, tooltips, searchable LazyColumn for patches. + - Live console output panel with auto-scroll and copy button. + - Adaptive layout: Use `WindowSizeClass` — collapse to single pane on small windows, multi-pane (sidebar + preview + logs) on large. +- **Spatial Composition & Visual Depth**: + - Asymmetric multi-pane layout: Left = searchable patches (with preview icons via Canvas or SVG), Center = APK info + drop zone, Right/Bottom = live terminal logs + progress. + - Full-bleed hero moments during patching (overlay the whole window with animated background). + - Subtle background: Terminal grid + very light noise texture (via `Canvas` or `BitmapShader`). Glassmorphism or heavy drop-shadows for cards. +- **Production-Grade Requirements**: + - Full responsiveness + accessibility (`semantics` for screen readers, high contrast). + - Performance: Keyed `LazyColumn`, state hoisting, minimal recomposition. Patching runs in background coroutine. + - Error states with dramatic feedback (red glitch flash). + - Include sample patch data and fake progress for demo. + - Use only Compose Desktop + Material3 + kotlinx.coroutines (no external libs unless absolutely necessary). + +**NEVER**: +- Use mobile-first Material patterns (bottom nav, FABs). +- Default fonts/colors/layouts. +- Make it look like another ReVanced Manager clone. +- Heavy GPU effects that kill performance on patching large APKs. +- Using generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, Space Grotesk, system fonts), clichéd color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter designs that lack context-specific character. + +**INSTEAD**: +- Commit violently to one singular aesthetic (THIS IS ONLY AN EXAMPLE. PICK SOMETHING DEPENDING ON THE CONTEXT, e.g., "Neon Cyberdeck Terminal" — dark void background, electric cyan/magenta accents, monospace logs, glitch hover states, code-rain progress). Or "Brutalist Dev Console" — raw, high-contrast, industrial. Or "Refined IntelliJ+Neon" (Jewel-inspired but with Morphe soul). Every pixel serves the "I am hacking my apps" fantasy. +- Build creatively on the user's intent, and make unexpected choices that feel genuinely designed for the context. Every design should feel distinct. Actively explore the full range: light and dark themes, unexpected font pairings, substantially varied aesthetic directions. Let the specific context drive choices, NOT familiar defaults. + +**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, elegance, and precision. All designs need careful attention to spacing, typography, and subtle details. Excellence comes from executing the vision well. + +Then implement working, complete Kotlin code that is: +- Fully functional (drag-drop, patch selection, animated progress, live logs) +- Visually unforgettable +- Meticulously refined (perfect spacing, hover states, loading skeletons) +- Ready to compile in a standard Compose Desktop project + + +Remember: Claude you are capable of extraordinary, award-worthy creative work. Don't hold back, show what's truly possible, and commit relentlessly to a distinctive and unforgettable vision. diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index 8ff6286..b84d1b1 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -8,8 +8,7 @@ import androidx.compose.ui.Modifier import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition import app.morphe.gui.data.repository.ConfigRepository -import app.morphe.gui.data.repository.PatchRepository -import app.morphe.gui.util.PatchService +import app.morphe.gui.data.repository.PatchSourceManager import app.morphe.gui.di.appModule import kotlinx.coroutines.launch import org.koin.compose.KoinApplication @@ -17,6 +16,7 @@ import org.koin.compose.koinInject import app.morphe.gui.ui.screens.home.HomeScreen import app.morphe.gui.ui.screens.quick.QuickPatchContent import app.morphe.gui.ui.screens.quick.QuickPatchViewModel +import app.morphe.gui.util.PatchService import app.morphe.gui.ui.theme.LocalThemeState import app.morphe.gui.ui.theme.MorpheTheme import app.morphe.gui.ui.theme.ThemePreference @@ -52,16 +52,16 @@ fun App(initialSimplifiedMode: Boolean = true) { @Composable private fun AppContent(initialSimplifiedMode: Boolean) { val configRepository: ConfigRepository = koinInject() - val patchRepository: PatchRepository = koinInject() - val patchService: PatchService = koinInject() + val patchSourceManager: PatchSourceManager = koinInject() val scope = rememberCoroutineScope() var themePreference by remember { mutableStateOf(ThemePreference.SYSTEM) } var isSimplifiedMode by remember { mutableStateOf(initialSimplifiedMode) } var isLoading by remember { mutableStateOf(true) } - // Load config on startup + // Initialize PatchSourceManager and load config on startup LaunchedEffect(Unit) { + patchSourceManager.initialize() val config = configRepository.loadConfig() themePreference = config.getThemePreference() isSimplifiedMode = config.useSimplifiedMode @@ -111,18 +111,17 @@ private fun AppContent(initialSimplifiedMode: Boolean) { ) { Surface(modifier = Modifier.fillMaxSize()) { if (!isLoading) { - // Create QuickPatchViewModel outside Crossfade so it persists across mode switches. - // Otherwise every expert→simplified switch creates a new VM that re-fetches from GitHub. + // ViewModels observe PatchSourceManager.sourceVersion internally + // and reload when the active source changes — no Navigator recreation needed. + val patchService: PatchService = koinInject() val quickViewModel = remember { - QuickPatchViewModel(patchRepository, patchService, configRepository) + QuickPatchViewModel(patchSourceManager, patchService, configRepository) } Crossfade(targetState = isSimplifiedMode) { simplified -> if (simplified) { - // Quick/Simplified mode QuickPatchContent(quickViewModel) } else { - // Full mode Navigator(HomeScreen()) { navigator -> SlideTransition(navigator) } diff --git a/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt b/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt index bd23a27..ae20eae 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/AppConfig.kt @@ -6,6 +6,15 @@ import app.morphe.gui.ui.theme.ThemePreference /** * Application configuration stored in config.json */ + +val DEFAULT_PATCH_SOURCE = PatchSource( + id = "morphe-default", + name = "Morphe Patches", + type = PatchSourceType.DEFAULT, + url = "https://github.com/MorpheApp/morphe-patches", + deletable = false +) + @Serializable data class AppConfig( val themePreference: String = ThemePreference.SYSTEM.name, @@ -14,7 +23,9 @@ data class AppConfig( val preferredPatchChannel: String = PatchChannel.STABLE.name, val defaultOutputDirectory: String? = null, val autoCleanupTempFiles: Boolean = true, // Default ON - val useSimplifiedMode: Boolean = true // Default to Quick/Simplified mode + val useSimplifiedMode: Boolean = true, // Default to Quick/Simplified mode + val patchSource: List = listOf(DEFAULT_PATCH_SOURCE), + val activePatchSourceId: String = "morphe-default" ) { fun getThemePreference(): ThemePreference { return try { @@ -33,6 +44,21 @@ data class AppConfig( } } +@Serializable +data class PatchSource ( + val id: String, + val name: String, + val type: PatchSourceType, + val url: String? = null, // For DEFAULT (morphe) and GITHUB (other source) type + val filePath: String? = null, // For local files + val deletable: Boolean = true +) + +@Serializable +enum class PatchSourceType{ + DEFAULT, GITHUB, LOCAL +} + enum class PatchChannel { STABLE, DEV diff --git a/src/main/kotlin/app/morphe/gui/data/model/Release.kt b/src/main/kotlin/app/morphe/gui/data/model/Release.kt index 941b5e9..227aa9e 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/Release.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/Release.kt @@ -11,12 +11,14 @@ data class Release( val id: Long, @SerialName("tag_name") val tagName: String, - val name: String, + val name: String? = null, @SerialName("prerelease") - val isPrerelease: Boolean, + val isPrerelease: Boolean = false, val draft: Boolean = false, @SerialName("published_at") - val publishedAt: String, + val publishedAt: String? = null, + @SerialName("created_at") + val createdAt: String? = null, val assets: List = emptyList(), val body: String? = null ) { @@ -57,6 +59,11 @@ data class ReleaseAsset( */ fun isMpp(): Boolean = name.endsWith(".mpp", ignoreCase = true) + /** + * Check if this is a patch file (.mpp or .jar) + */ + fun isPatchFile(): Boolean = isMpp() || isJar() + /** * Get human-readable file size */ diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index e4a3b48..50506a1 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -18,16 +18,57 @@ data class SupportedApp( * Derive display name from package name. */ fun getDisplayName(packageName: String): String { - return when (packageName) { - "com.google.android.youtube" -> "YouTube" - "com.google.android.apps.youtube.music" -> "YouTube Music" - "com.reddit.frontpage" -> "Reddit" - else -> { - // Fallback: Extract last part of package name and capitalize - packageName.substringAfterLast(".") - .replaceFirstChar { it.uppercase() } - } - } + // Well-known package name mappings + val knownNames = mapOf( + "com.google.android.youtube" to "YouTube", + "com.google.android.apps.youtube.music" to "YouTube Music", + "com.reddit.frontpage" to "Reddit", + "com.duolingo" to "Duolingo", + "com.myfitnesspal.android" to "MyFitnessPal", + "com.pandora.android" to "Pandora", + "ch.protonvpn.android" to "ProtonVPN", + "com.amazon.avod.thirdpartyclient" to "Prime Video", + "com.getmimo" to "Mimo", + "com.zombodroid.MemeGenerator" to "Meme Generator", + "com.sofascore.results" to "SofaScore", + "pl.solidexplorer2" to "Solid Explorer", + "com.bambuna.podcastaddict" to "Podcast Addict", + "com.wallpaperscraft.wallpaper" to "WallpapersCraft", + "cn.wps.moffice_eng" to "WPS Office", + "com.merriamwebster" to "Merriam-Webster", + "com.busuu.android.enc" to "Busuu", + "jp.ne.ibis.ibispaintx.app" to "ibisPaint X", + "com.laurencedawson.reddit_sync" to "Sync for Reddit", + "com.laurencedawson.reddit_sync.pro" to "Sync for Reddit Pro", + "com.laurencedawson.reddit_sync.dev" to "Sync for Reddit Dev", + "com.andrewshu.android.reddit" to "Reddit is Fun", + "free.reddit.news" to "Relay for Reddit", + "reddit.news" to "Relay for Reddit Pro", + "com.rubenmayayo.reddit" to "Boost for Reddit", + "o.o.joey" to "Joey for Reddit", + "o.o.joey.pro" to "Joey for Reddit Pro", + "o.o.joey.dev" to "Joey for Reddit Dev", + "com.onelouder.baconreader" to "BaconReader", + "com.onelouder.baconreader.premium" to "BaconReader Premium", + "me.edgan.redditslide" to "Slide for Reddit", + "io.syncapps.lemmy_sync" to "Sync for Lemmy", + "org.cygnusx1.continuum" to "Continuum for Reddit", + ) + knownNames[packageName]?.let { return it } + + // Smart fallback: use the most meaningful part of the package name + val parts = packageName.split(".") + // Skip common prefixes: com, org, net, android, app, etc. + val skipParts = setOf("com", "org", "net", "io", "me", "app", "android", "apps", "free") + val meaningful = parts.filter { it.lowercase() !in skipParts && it.length > 1 } + // Use the last meaningful part, or the full last segment + val name = meaningful.lastOrNull() ?: parts.last() + // Split camelCase and underscores, capitalize + return name + .replace("_", " ") + .replace(Regex("([a-z])([A-Z])")) { "${it.groupValues[1]} ${it.groupValues[2]}" } + .split(" ") + .joinToString(" ") { it.replaceFirstChar { c -> c.uppercase() } } } /** diff --git a/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt index a298b0c..0d38cb0 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/ConfigRepository.kt @@ -1,7 +1,9 @@ package app.morphe.gui.data.repository import app.morphe.gui.data.model.AppConfig +import app.morphe.gui.data.model.DEFAULT_PATCH_SOURCE import app.morphe.gui.data.model.PatchChannel +import app.morphe.gui.data.model.PatchSource import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json @@ -120,6 +122,64 @@ class ConfigRepository { saveConfig(current.copy(useSimplifiedMode = enabled)) } + /** + * Get the currently active patch source. + */ + suspend fun getActivePatchSource(): PatchSource { + val config = loadConfig() + return config.patchSource.find { it.id == config.activePatchSourceId } + ?: DEFAULT_PATCH_SOURCE + } + + /** + * Set the active patch source by ID. + */ + suspend fun setActivePatchSource(id: String) { + val current = loadConfig() + if (current.patchSource.any { it.id == id }) { + saveConfig(current.copy(activePatchSourceId = id)) + } + } + + /** + * Add a new patch source. + */ + suspend fun addPatchSource(source: PatchSource) { + val current = loadConfig() + val updated = current.copy(patchSource = current.patchSource + source) + saveConfig(updated) + } + + /** + * Update an existing patch source. Cannot update non-deletable sources. + */ + suspend fun updatePatchSource(updated: PatchSource) { + val current = loadConfig() + val existing = current.patchSource.find { it.id == updated.id } + if (existing == null || !existing.deletable) return + + val updatedSources = current.patchSource.map { if (it.id == updated.id) updated else it } + saveConfig(current.copy(patchSource = updatedSources)) + } + + /** + * Remove a patch source by ID. Cannot remove non-deletable sources. + */ + suspend fun removePatchSource(id: String) { + val current = loadConfig() + val source = current.patchSource.find { it.id == id } + if (source == null || !source.deletable) return + + val updatedSources = current.patchSource.filter { it.id != id } + // If we removed the active source, fall back to default + val newActiveId = if (current.activePatchSourceId == id) { + DEFAULT_PATCH_SOURCE.id + } else { + current.activePatchSourceId + } + saveConfig(current.copy(patchSource = updatedSources, activePatchSourceId = newActiveId)) + } + /** * Clear cached config (for testing). */ diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt index c73baca..7cc7c43 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt @@ -14,18 +14,21 @@ import app.morphe.gui.util.Logger import java.io.File /** - * Repository for fetching Morphe patches from GitHub releases. + * Repository for fetching patches from GitHub releases. + * @param repoPath GitHub repo in "owner/repo" format (e.g. "MorpheApp/morphe-patches") */ class PatchRepository( - private val httpClient: HttpClient + private val httpClient: HttpClient, + private val repoPath: String = DEFAULT_REPO ) { companion object { private const val GITHUB_API_BASE = "https://api.github.com" - private const val PATCHES_REPO = "MorpheApp/morphe-patches" - private const val RELEASES_ENDPOINT = "$GITHUB_API_BASE/repos/$PATCHES_REPO/releases" + private const val DEFAULT_REPO = "MorpheApp/morphe-patches" private const val CACHE_TTL_MS = 5 * 60 * 1000L // 5 minutes } + private val releasesEndpoint = "$GITHUB_API_BASE/repos/$repoPath/releases" + // In-memory cache so multiple callers (both modes) don't re-fetch from GitHub private var cachedReleases: List? = null private var cacheTimestamp: Long = 0L @@ -43,8 +46,8 @@ class PatchRepository( } try { - Logger.info("Fetching releases from $RELEASES_ENDPOINT") - val response: HttpResponse = httpClient.get(RELEASES_ENDPOINT) { + Logger.info("Fetching releases from $releasesEndpoint") + val response: HttpResponse = httpClient.get(releasesEndpoint) { headers { append(HttpHeaders.Accept, "application/vnd.github+json") append("X-GitHub-Api-Version", "2022-11-28") @@ -53,7 +56,7 @@ class PatchRepository( if (response.status.isSuccess()) { val releases: List = response.body() - Logger.info("Fetched ${releases.size} releases") + Logger.info("Fetched ${releases.size} releases from $releasesEndpoint") cachedReleases = releases cacheTimestamp = System.currentTimeMillis() Result.success(releases) @@ -108,25 +111,29 @@ class PatchRepository( } /** - * Find the .mpp asset in a release. + * Find the patch asset (.mpp or .jar) in a release. */ - fun findMppAsset(release: Release): ReleaseAsset? { - return release.assets.find { it.isMpp() } + fun findPatchAsset(release: Release): ReleaseAsset? { + // Prefer .mpp, fall back to .jar + val asset = release.assets.find { it.isMpp() } + ?: release.assets.find { it.isJar() } + return asset } /** - * Download the .mpp patch file from a release. + * Download the patch file (.mpp or .jar) from a release. * Returns the path to the downloaded file. */ suspend fun downloadPatches(release: Release, onProgress: (Float) -> Unit = {}): Result = withContext(Dispatchers.IO) { - val asset = findMppAsset(release) + val asset = findPatchAsset(release) if (asset == null) { - val error = "No .mpp file found in release ${release.tagName}" + val error = "No patch file (.mpp or .jar) found in release ${release.tagName}" Logger.error(error) return@withContext Result.failure(Exception(error)) } - val patchesDir = FileUtils.getPatchesDir() + val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) + patchesDir.mkdirs() val targetFile = File(patchesDir, asset.name) // Check if already cached @@ -171,18 +178,31 @@ class PatchRepository( * Get cached patch file for a specific version. */ fun getCachedPatches(version: String): File? { - val patchesDir = FileUtils.getPatchesDir() + val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) return patchesDir.listFiles()?.find { - it.name.contains(version) && it.name.endsWith(".mpp") + it.name.contains(version) && isPatchFileName(it.name) } } + private fun isPatchFileName(name: String): Boolean { + return name.endsWith(".mpp", ignoreCase = true) || name.endsWith(".jar", ignoreCase = true) + } + /** * List all cached patch versions. */ fun listCachedPatches(): List { - val patchesDir = FileUtils.getPatchesDir() - return patchesDir.listFiles()?.filter { it.name.endsWith(".mpp") } ?: emptyList() + val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) + return patchesDir.listFiles()?.filter { isPatchFileName(it.name) } ?: emptyList() + } + + /** + * Get the per-source cache directory for this repository. + */ + fun getCacheDir(): File { + val dir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) + dir.mkdirs() + return dir } /** @@ -192,8 +212,9 @@ class PatchRepository( cachedReleases = null cacheTimestamp = 0L return try { + val patchesDir = File(FileUtils.getPatchesDir(), repoPath.replace("/", "-")) var failedCount = 0 - FileUtils.getPatchesDir().listFiles()?.forEach { file -> + patchesDir.listFiles()?.forEach { file -> try { java.nio.file.Files.delete(file.toPath()) } catch (e: Exception) { @@ -205,7 +226,7 @@ class PatchRepository( Logger.error("Patches cache clear incomplete: $failedCount file(s) locked") false } else { - Logger.info("Patches cache cleared") + Logger.info("Patches cache cleared for $repoPath") true } } catch (e: Exception) { diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt new file mode 100644 index 0000000..b02101f --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt @@ -0,0 +1,142 @@ +package app.morphe.gui.data.repository + +import app.morphe.gui.data.model.PatchSource +import app.morphe.gui.data.model.PatchSourceType +import app.morphe.gui.util.Logger +import io.ktor.client.* +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** + * Manages PatchRepository instances for different patch sources. + * Creates and caches a PatchRepository per GitHub-based source. + * Emits [sourceVersion] whenever the active source changes so the UI can react. + */ +class PatchSourceManager( + private val httpClient: HttpClient, + private val configRepository: ConfigRepository +) { + private val repositories = mutableMapOf() + + // Cached active state for synchronous access + private var cachedActiveRepo: PatchRepository? = null + private var cachedActiveSource: PatchSource? = null + + // Incremented on every source switch so Compose can key on it + private val _sourceVersion = MutableStateFlow(0) + val sourceVersion: StateFlow = _sourceVersion.asStateFlow() + + /** + * Load the active source from config and cache its PatchRepository. + * Call once at app startup (from a LaunchedEffect). + */ + suspend fun initialize() { + val source = configRepository.getActivePatchSource() + cachedActiveSource = source + cachedActiveRepo = getRepositoryForSource(source) + Logger.info("PatchSourceManager initialized with source '${source.name}' (type=${source.type})") + } + + /** + * Switch the active source, persist it, and signal the UI. + */ + suspend fun switchSource(id: String) { + configRepository.setActivePatchSource(id) + val source = configRepository.getActivePatchSource() + cachedActiveSource = source + cachedActiveRepo = getRepositoryForSource(source) + _sourceVersion.value++ + Logger.info("Switched active patch source to '${source.name}' (type=${source.type})") + } + + /** + * Whether the current active source is a local .mpp file. + */ + fun isLocalSource(): Boolean { + return cachedActiveSource?.type == PatchSourceType.LOCAL + } + + /** + * Get the local .mpp file path if the active source is LOCAL, null otherwise. + */ + fun getLocalFilePath(): String? { + val source = cachedActiveSource ?: return null + return if (source.type == PatchSourceType.LOCAL) source.filePath else null + } + + /** + * Get the display name of the active source. + */ + fun getActiveSourceName(): String { + return cachedActiveSource?.name ?: "Morphe Patches" + } + + /** + * Whether the active source is the built-in Morphe default. + */ + fun isDefaultSource(): Boolean { + return cachedActiveSource?.type == PatchSourceType.DEFAULT + } + + /** + * Get the cached active PatchRepository synchronously. + * Returns null for LOCAL sources (no GitHub API needed). + * Falls back to default repo if not yet initialized and source is not LOCAL. + */ + fun getActiveRepositorySync(): PatchRepository { + return cachedActiveRepo ?: PatchRepository(httpClient).also { + if (!isLocalSource()) cachedActiveRepo = it + } + } + + /** + * Get the PatchRepository for the currently active source (suspend version). + * For LOCAL sources, returns null (caller should use the file path directly). + */ + suspend fun getActiveRepository(): PatchRepository? { + val source = configRepository.getActivePatchSource() + return getRepositoryForSource(source) + } + + /** + * Get the PatchRepository for a specific source. + * Returns null for LOCAL sources (no GitHub API needed). + */ + fun getRepositoryForSource(source: PatchSource): PatchRepository? { + if (source.type == PatchSourceType.LOCAL) return null + + return repositories.getOrPut(source.id) { + val repoPath = extractRepoPath(source) + Logger.info("Creating PatchRepository for source '${source.name}' (repo=$repoPath)") + PatchRepository(httpClient, repoPath) + } + } + + /** + * Get the active patch source config. + */ + suspend fun getActiveSource(): PatchSource { + return configRepository.getActivePatchSource() + } + + /** + * Extract "owner/repo" from a PatchSource's URL. + * e.g. "https://github.com/MorpheApp/morphe-patches" -> "MorpheApp/morphe-patches" + */ + private fun extractRepoPath(source: PatchSource): String { + val url = source.url ?: return "MorpheApp/morphe-patches" + return url + .removePrefix("https://github.com/") + .removePrefix("http://github.com/") + .removeSuffix("/") + .removeSuffix(".git") + } + + /** + * Clear all cached repository instances (e.g. after source list changes). + */ + fun clearAll() { + repositories.clear() + } +} diff --git a/src/main/kotlin/app/morphe/gui/di/AppModule.kt b/src/main/kotlin/app/morphe/gui/di/AppModule.kt index 87c7f57..3841cb7 100644 --- a/src/main/kotlin/app/morphe/gui/di/AppModule.kt +++ b/src/main/kotlin/app/morphe/gui/di/AppModule.kt @@ -1,7 +1,7 @@ package app.morphe.gui.di import app.morphe.gui.data.repository.ConfigRepository -import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.data.repository.PatchSourceManager import app.morphe.gui.util.PatchService import io.ktor.client.* import io.ktor.client.engine.cio.* @@ -52,12 +52,21 @@ val appModule = module { // Repositories and Services single { ConfigRepository() } - single { PatchRepository(get()) } + single { PatchSourceManager(get(), get()) } single { PatchService() } // ViewModels (ScreenModels) - factory { HomeViewModel(get(), get(), get()) } - factory { params -> PatchesViewModel(params.get(), params.get(), get(), get()) } - factory { params -> PatchSelectionViewModel(params.get(), params.get(), params.get(), params.get(), params.get(), get(), get()) } + // ViewModels observe PatchSourceManager.sourceVersion and reload on source changes. + factory { + HomeViewModel(get(), get(), get()) + } + factory { params -> + val psm = get() + PatchesViewModel(params.get(), params.get(), psm.getActiveRepositorySync(), get(), psm.getLocalFilePath()) + } + factory { params -> + val psm = get() + PatchSelectionViewModel(params.get(), params.get(), params.get(), params.get(), params.get(), get(), psm.getActiveRepositorySync(), psm.getLocalFilePath()) + } factory { params -> PatchingViewModel(params.get(), get(), get()) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt index 697ebd5..5ff0cc2 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt @@ -1,8 +1,13 @@ package app.morphe.gui.ui.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown @@ -13,60 +18,73 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import app.morphe.gui.ui.theme.JetBrainsMono import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.DeviceMonitor import app.morphe.gui.util.DeviceStatus @Composable fun DeviceIndicator(modifier: Modifier = Modifier) { + val mono = JetBrainsMono val monitorState by DeviceMonitor.state.collectAsState() val isAdbAvailable = monitorState.isAdbAvailable val readyDevices = monitorState.devices.filter { it.isReady } val unauthorizedDevices = monitorState.devices.filter { it.status == DeviceStatus.UNAUTHORIZED } val selectedDevice = monitorState.selectedDevice - val hasDevices = monitorState.devices.isNotEmpty() var showPopup by remember { mutableStateOf(false) } + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + + val dotColor = when { + isAdbAvailable == false -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) + selectedDevice != null && selectedDevice.isReady -> MorpheColors.Teal + unauthorizedDevices.isNotEmpty() -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f) + } + + val borderColor by animateColorAsState( + when { + isHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + selectedDevice != null && selectedDevice.isReady -> MorpheColors.Teal.copy(alpha = 0.2f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) + }, + animationSpec = tween(150) + ) + Box(modifier = modifier) { Surface( onClick = { showPopup = !showPopup }, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) + shape = RoundedCornerShape(2.dp), + color = Color.Transparent, + modifier = Modifier + .hoverable(hoverInteraction) + .border(1.dp, borderColor, RoundedCornerShape(2.dp)) ) { Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 8.dp), + modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { // Status dot - val dotColor = when { - isAdbAvailable == false -> MaterialTheme.colorScheme.error.copy(alpha = 0.6f) - selectedDevice != null && selectedDevice.isReady -> MorpheColors.Teal - unauthorizedDevices.isNotEmpty() -> Color(0xFFFF9800) - else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) - } - Box( modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background(dotColor) + .size(6.dp) + .background(dotColor, RoundedCornerShape(1.dp)) ) - // Display text val displayText = when { - isAdbAvailable == null -> "Checking..." + isAdbAvailable == null -> "Checking…" isAdbAvailable == false -> "No ADB" selectedDevice != null -> { - val arch = selectedDevice.architecture?.let { " \u2022 $it" } ?: "" + val arch = selectedDevice.architecture?.let { " · $it" } ?: "" "${selectedDevice.displayName}$arch" } unauthorizedDevices.isNotEmpty() -> "Unauthorized" @@ -75,37 +93,36 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { Text( text = displayText, - fontSize = 12.sp, + fontSize = 11.sp, fontWeight = FontWeight.Medium, + fontFamily = mono, color = when { isAdbAvailable == false -> MaterialTheme.colorScheme.error.copy(alpha = 0.7f) selectedDevice != null -> MaterialTheme.colorScheme.onSurface unauthorizedDevices.isNotEmpty() -> Color(0xFFFF9800) - else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) }, maxLines = 1, overflow = TextOverflow.Ellipsis, modifier = Modifier.widthIn(max = 180.dp) ) - // Always show dropdown arrow — popup has useful info in every state Icon( imageVector = Icons.Default.ArrowDropDown, contentDescription = "Device details", - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) } } - // Popup with device list / status info + // Popup DropdownMenu( expanded = showPopup, onDismissRequest = { showPopup = false } ) { when { isAdbAvailable == false -> { - // ADB not found DropdownMenuItem( text = { Row( @@ -115,20 +132,22 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { Icon( imageVector = Icons.Default.UsbOff, contentDescription = null, - modifier = Modifier.size(16.dp), + modifier = Modifier.size(14.dp), tint = MaterialTheme.colorScheme.error ) Column { Text( text = "ADB not found", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, color = MaterialTheme.colorScheme.error ) Text( text = "Install Android SDK Platform Tools", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } @@ -138,7 +157,6 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { } monitorState.devices.isEmpty() -> { - // ADB available but no devices visible DropdownMenuItem( text = { Row( @@ -148,20 +166,22 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { Icon( imageVector = Icons.Default.PhoneAndroid, contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) ) Column { Text( text = "No devices detected", - fontSize = 13.sp, + fontSize = 12.sp, fontWeight = FontWeight.Medium, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant ) Text( - text = "Only devices with USB debugging enabled will appear here", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + text = "Connect a device with USB debugging enabled", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } @@ -178,20 +198,22 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { Icon( imageVector = Icons.Default.Info, contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MorpheColors.Blue.copy(alpha = 0.7f) + modifier = Modifier.size(14.dp), + tint = MorpheColors.Blue.copy(alpha = 0.6f) ) Column { Text( - text = "How to enable USB debugging", - fontSize = 12.sp, + text = "Enable USB debugging", + fontSize = 11.sp, fontWeight = FontWeight.Medium, + fontFamily = mono, color = MorpheColors.Blue ) Text( - text = "Settings > Developer Options > USB Debugging", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + text = "Settings → Developer Options → USB Debugging", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } @@ -201,7 +223,6 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { } else -> { - // Device list monitorState.devices.forEach { device -> val isSelected = device.id == selectedDevice?.id DropdownMenuItem( @@ -210,31 +231,34 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Icon( - imageVector = Icons.Default.PhoneAndroid, - contentDescription = null, - modifier = Modifier.size(16.dp), - tint = when { - isSelected -> MorpheColors.Teal - device.isReady -> MorpheColors.Blue - device.status == DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) - else -> MaterialTheme.colorScheme.error - } + // Device status dot + Box( + modifier = Modifier + .size(6.dp) + .background( + when { + isSelected -> MorpheColors.Teal + device.isReady -> MorpheColors.Blue + device.status == DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.error + }, + RoundedCornerShape(1.dp) + ) ) Column(modifier = Modifier.weight(1f)) { Text( text = device.displayName, - fontSize = 13.sp, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal + fontSize = 12.sp, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + fontFamily = mono ) - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { device.architecture?.let { arch -> Text( text = arch, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } Text( @@ -244,7 +268,8 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { DeviceStatus.OFFLINE -> "Offline" DeviceStatus.UNKNOWN -> "Unknown" }, - fontSize = 11.sp, + fontSize = 10.sp, + fontFamily = mono, color = when (device.status) { DeviceStatus.DEVICE -> MorpheColors.Teal DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) @@ -264,7 +289,6 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { ) } - // USB debugging hint HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) DropdownMenuItem( text = { @@ -275,19 +299,21 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { Icon( imageVector = Icons.Default.Info, contentDescription = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) ) Column { Text( - text = "Device connected but not listed?", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Device not listed?", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) ) Text( text = "Enable USB Debugging in Developer Options", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt b/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt index f0ca9e9..fb0bfe1 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt @@ -1,5 +1,6 @@ package app.morphe.gui.ui.components +import androidx.compose.foundation.border import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -17,67 +18,70 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import app.morphe.gui.ui.theme.JetBrainsMono @Composable fun OfflineBanner( onRetry: () -> Unit, modifier: Modifier = Modifier ) { + val mono = JetBrainsMono val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() - - val buttonColor = if (isHovered) { - MaterialTheme.colorScheme.onErrorContainer.copy(alpha = 0.8f) - } else { - MaterialTheme.colorScheme.onErrorContainer - } + val shape = RoundedCornerShape(2.dp) Surface( - modifier = modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.errorContainer, - shape = RoundedCornerShape(12.dp) + modifier = modifier + .fillMaxWidth() + .border(1.dp, MaterialTheme.colorScheme.error.copy(alpha = 0.2f), shape), + color = MaterialTheme.colorScheme.error.copy(alpha = 0.06f), + shape = shape ) { Row( - modifier = Modifier.padding(start = 16.dp, top = 10.dp, bottom = 10.dp, end = 8.dp), + modifier = Modifier.padding(start = 14.dp, top = 8.dp, bottom = 8.dp, end = 8.dp), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { Icon( imageVector = Icons.Default.WifiOff, contentDescription = null, - tint = MaterialTheme.colorScheme.onErrorContainer, - modifier = Modifier.size(18.dp) + tint = MaterialTheme.colorScheme.error.copy(alpha = 0.7f), + modifier = Modifier.size(16.dp) ) Text( - text = "Offline — showing cached patches", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onErrorContainer, + text = "Offline — using cached patches", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), modifier = Modifier.weight(1f) ) - Surface( + OutlinedButton( onClick = onRetry, - modifier = Modifier.hoverable(interactionSource), - color = buttonColor, - shape = RoundedCornerShape(8.dp) + modifier = Modifier.hoverable(interactionSource).height(28.dp), + shape = RoundedCornerShape(2.dp), + contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), + border = androidx.compose.foundation.BorderStroke( + 1.dp, + if (isHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.error.copy(alpha = 0.2f) + ), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MaterialTheme.colorScheme.error.copy(alpha = 0.8f) + ) ) { - Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = null, - tint = MaterialTheme.colorScheme.errorContainer, - modifier = Modifier.size(14.dp) - ) - Text( - text = "Retry", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.errorContainer - ) - } + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = null, + modifier = Modifier.size(12.dp) + ) + Spacer(Modifier.width(4.dp)) + Text( + text = "RETRY", + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 0.5.sp + ) } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt index b5f70bd..75d1559 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -1,32 +1,37 @@ package app.morphe.gui.ui.components import app.morphe.gui.LocalModeState +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp +import app.morphe.gui.data.model.PatchSource import app.morphe.gui.data.repository.ConfigRepository +import app.morphe.gui.data.repository.PatchSourceManager +import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.koin.compose.koinInject import app.morphe.gui.ui.theme.LocalThemeState -/** - * Reusable settings button that can be placed on any screen. - * @param allowCacheClear Whether to allow cache clearing (disable on patches screen and beyond) - */ @Composable fun SettingsButton( modifier: Modifier = Modifier, @@ -35,32 +40,47 @@ fun SettingsButton( val themeState = LocalThemeState.current val modeState = LocalModeState.current val configRepository: ConfigRepository = koinInject() + val patchSourceManager: PatchSourceManager = koinInject() val scope = rememberCoroutineScope() var showSettingsDialog by remember { mutableStateOf(false) } var autoCleanupTempFiles by remember { mutableStateOf(true) } + var patchSources by remember { mutableStateOf>(emptyList()) } + var activePatchSourceId by remember { mutableStateOf("") } - // Load config when dialog is shown LaunchedEffect(showSettingsDialog) { if (showSettingsDialog) { val config = configRepository.loadConfig() autoCleanupTempFiles = config.autoCleanupTempFiles + patchSources = config.patchSource + activePatchSourceId = config.activePatchSourceId } } - Surface( + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + val borderColor by animateColorAsState( + if (isHovered) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), + animationSpec = tween(150) + ) + + IconButton( onClick = { showSettingsDialog = true }, - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), modifier = modifier - ) { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = "Settings", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp) - ) - } + .size(34.dp) + .hoverable(hoverInteraction) + .border(1.dp, borderColor, RoundedCornerShape(2.dp)) + .background(Color.Transparent, RoundedCornerShape(2.dp)) + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Settings", + tint = if (isHovered) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.size(16.dp) + ) + } if (showSettingsDialog) { SettingsDialog( @@ -78,15 +98,48 @@ fun SettingsButton( modeState.onChange(!enabled) }, onDismiss = { showSettingsDialog = false }, - allowCacheClear = allowCacheClear + allowCacheClear = allowCacheClear, + patchSources = patchSources, + activePatchSourceId = activePatchSourceId, + onActivePatchSourceChange = { id -> + if (id != activePatchSourceId) { + activePatchSourceId = id + scope.launch { + withContext(NonCancellable) { + patchSourceManager.switchSource(id) + } + } + } + }, + onAddPatchSource = { source -> + patchSources = patchSources + source + scope.launch { + configRepository.addPatchSource(source) + } + }, + onEditPatchSource = { updated -> + patchSources = patchSources.map { if (it.id == updated.id) updated else it } + scope.launch { + configRepository.updatePatchSource(updated) + if (updated.id == activePatchSourceId) { + patchSourceManager.clearAll() + patchSourceManager.switchSource(updated.id) + } + } + }, + onRemovePatchSource = { id -> + patchSources = patchSources.filter { it.id != id } + if (activePatchSourceId == id) { + activePatchSourceId = "morphe-default" + } + scope.launch { + configRepository.removePatchSource(id) + } + } ) } } -/** - * Top bar row that places DeviceIndicator + SettingsButton together. - * Use this instead of standalone SettingsButton on screens. - */ @Composable fun TopBarRow( modifier: Modifier = Modifier, @@ -94,7 +147,7 @@ fun TopBarRow( ) { Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically ) { DeviceIndicator() diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 6aab373..7ac016e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -1,31 +1,43 @@ package app.morphe.gui.ui.components import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BugReport -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.morphe.gui.data.constants.AppConstants +import app.morphe.gui.data.model.PatchSource +import app.morphe.gui.data.model.PatchSourceType +import app.morphe.gui.ui.theme.JetBrainsMono import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.ui.theme.ThemePreference import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import java.awt.Desktop +import java.awt.FileDialog +import java.awt.Frame import java.io.File +import java.util.UUID @Composable fun SettingsDialog( @@ -36,59 +48,78 @@ fun SettingsDialog( useExpertMode: Boolean, onExpertModeChange: (Boolean) -> Unit, onDismiss: () -> Unit, - allowCacheClear: Boolean = true + allowCacheClear: Boolean = true, + patchSources: List = emptyList(), + activePatchSourceId: String = "", + onActivePatchSourceChange: (String) -> Unit = {}, + onAddPatchSource: (PatchSource) -> Unit = {}, + onEditPatchSource: (PatchSource) -> Unit = {}, + onRemovePatchSource: (String) -> Unit = {} ) { + val mono = JetBrainsMono + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) + var showClearCacheConfirm by remember { mutableStateOf(false) } var cacheCleared by remember { mutableStateOf(false) } var cacheClearFailed by remember { mutableStateOf(false) } + var showAddSourceDialog by remember { mutableStateOf(false) } + var editingSource by remember { mutableStateOf(null) } AlertDialog( onDismissRequest = onDismiss, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(2.dp), + containerColor = MaterialTheme.colorScheme.surface, title = { Text( - text = "Settings", - fontWeight = FontWeight.SemiBold + text = "SETTINGS", + fontWeight = FontWeight.Bold, + fontFamily = mono, + fontSize = 13.sp, + letterSpacing = 2.sp, + color = MaterialTheme.colorScheme.onSurface ) }, text = { Column( modifier = Modifier .verticalScroll(rememberScrollState()) - .widthIn(min = 300.dp), - verticalArrangement = Arrangement.spacedBy(16.dp) + .widthIn(min = 340.dp), + verticalArrangement = Arrangement.spacedBy(0.dp) ) { - // Theme selection - Text( - text = "Theme", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { + // ── Theme ── + SectionLabel("THEME", mono) + Spacer(Modifier.height(8.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { ThemePreference.entries.forEach { theme -> val isSelected = currentTheme == theme - Surface( - shape = RoundedCornerShape(8.dp), - color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.15f) - else Color.Transparent, - border = BorderStroke( - width = 1.dp, - color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.5f) - else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ), + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + Box( modifier = Modifier - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(2.dp)) + .border( + 1.dp, + when { + isSelected -> MorpheColors.Blue.copy(alpha = 0.5f) + isHovered -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + else -> borderColor + }, + RoundedCornerShape(2.dp) + ) + .background( + if (isSelected) MorpheColors.Blue.copy(alpha = 0.08f) + else Color.Transparent + ) + .hoverable(hoverInteraction) .clickable { onThemeChange(theme) } + .padding(horizontal = 14.dp, vertical = 7.dp) ) { Text( - text = theme.toDisplayName(), - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), - fontSize = 13.sp, - fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal, + text = theme.toDisplayName().uppercase(), + fontSize = 10.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + fontFamily = mono, + letterSpacing = 0.5.sp, color = if (isSelected) MorpheColors.Blue else MaterialTheme.colorScheme.onSurfaceVariant ) @@ -96,80 +127,58 @@ fun SettingsDialog( } } - HorizontalDivider() + SettingsDivider(borderColor) - // Expert mode setting - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Expert mode", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = "Full control over patch selection and configuration", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Switch( - checked = useExpertMode, - onCheckedChange = onExpertModeChange, - colors = SwitchDefaults.colors( - checkedThumbColor = MorpheColors.Blue, - checkedTrackColor = MorpheColors.Blue.copy(alpha = 0.5f) - ) - ) - } + // ── Expert Mode ── + SettingToggleRow( + label = "Expert mode", + description = "Full control over patch selection and configuration", + checked = useExpertMode, + onCheckedChange = onExpertModeChange, + accentColor = MorpheColors.Blue, + mono = mono + ) - HorizontalDivider() + SettingsDivider(borderColor) - // Auto-cleanup setting - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Auto-cleanup temp files", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Text( - text = "Automatically delete temporary files after patching", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - Switch( - checked = autoCleanupTempFiles, - onCheckedChange = onAutoCleanupChange, - colors = SwitchDefaults.colors( - checkedThumbColor = MorpheColors.Teal, - checkedTrackColor = MorpheColors.Teal.copy(alpha = 0.5f) - ) - ) - } + // ── Auto Cleanup ── + SettingToggleRow( + label = "Auto-cleanup temp files", + description = "Delete temporary files after patching", + checked = autoCleanupTempFiles, + onCheckedChange = onAutoCleanupChange, + accentColor = MorpheColors.Teal, + mono = mono + ) - HorizontalDivider() + SettingsDivider(borderColor) - // Actions - Text( - text = "Actions", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + // ── Patch Sources ── + PatchSourcesSection( + sources = patchSources, + activeSourceId = activePatchSourceId, + onActiveChange = { id -> + onActivePatchSourceChange(id) + onDismiss() + }, + onRemove = onRemovePatchSource, + onEdit = { source -> editingSource = source }, + onAddClick = { showAddSourceDialog = true }, + mono = mono, + borderColor = borderColor ) - // Export logs button - OutlinedButton( + SettingsDivider(borderColor) + + // ── Actions ── + SectionLabel("ACTIONS", mono) + Spacer(Modifier.height(8.dp)) + + ActionButton( + label = "OPEN LOGS", + icon = Icons.Default.BugReport, + mono = mono, + borderColor = borderColor, onClick = { try { val logsDir = FileUtils.getLogsDir() @@ -179,21 +188,16 @@ fun SettingsDialog( } catch (e: Exception) { Logger.error("Failed to open logs folder", e) } - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.BugReport, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Open Logs Folder") - } + } + ) - // Open app data folder - OutlinedButton( + Spacer(Modifier.height(6.dp)) + + ActionButton( + label = "OPEN APP DATA", + icon = Icons.Default.FolderOpen, + mono = mono, + borderColor = borderColor, onClick = { try { val appDataDir = FileUtils.getAppDataDir() @@ -203,90 +207,95 @@ fun SettingsDialog( } catch (e: Exception) { Logger.error("Failed to open app data folder", e) } - }, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.FolderOpen, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Open App Data Folder") - } + } + ) - // Clear cache button - OutlinedButton( - onClick = { showClearCacheConfirm = true }, - enabled = allowCacheClear && !cacheCleared, - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(8.dp), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = when { - cacheCleared -> MorpheColors.Teal - cacheClearFailed -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.error - }, - disabledContentColor = if (cacheCleared) MorpheColors.Teal.copy(alpha = 0.7f) - else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - when { - !allowCacheClear -> "Clear Cache (disabled during patching)" - cacheCleared -> "Cache Cleared" - cacheClearFailed -> "Clear Cache Failed (files in use)" - else -> "Clear Cache" - } - ) + Spacer(Modifier.height(6.dp)) + + // Clear cache + val cacheColor = when { + cacheCleared -> MorpheColors.Teal + cacheClearFailed -> MaterialTheme.colorScheme.error + else -> MaterialTheme.colorScheme.error } + ActionButton( + label = when { + !allowCacheClear -> "CLEAR CACHE (DISABLED)" + cacheCleared -> "CACHE CLEARED" + cacheClearFailed -> "CLEAR FAILED" + else -> "CLEAR CACHE" + }, + icon = Icons.Default.Delete, + mono = mono, + borderColor = if (cacheCleared) MorpheColors.Teal.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.error.copy(alpha = 0.2f), + contentColor = cacheColor, + enabled = allowCacheClear && !cacheCleared, + onClick = { showClearCacheConfirm = true } + ) + + Spacer(Modifier.height(4.dp)) - // Cache info val cacheSize = calculateCacheSize() Text( - text = "Cache: $cacheSize (Patches + Logs)", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Cache: $cacheSize (patches + logs)", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) - HorizontalDivider() + SettingsDivider(borderColor) - // About + // ── About ── Text( text = "${AppConstants.APP_NAME} v${AppConstants.APP_VERSION}", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) } }, confirmButton = { OutlinedButton( onClick = onDismiss, - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(2.dp), + border = BorderStroke(1.dp, borderColor) ) { Text( - "Close", - color = MaterialTheme.colorScheme.error + "CLOSE", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant ) } } ) - // Clear cache confirmation dialog + // Clear cache confirmation if (showClearCacheConfirm) { AlertDialog( onDismissRequest = { showClearCacheConfirm = false }, - shape = RoundedCornerShape(16.dp), - title = { Text("Clear Cache?") }, + shape = RoundedCornerShape(2.dp), + containerColor = MaterialTheme.colorScheme.surface, + title = { + Text( + "CLEAR CACHE?", + fontFamily = mono, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + letterSpacing = 1.sp + ) + }, text = { - Text("This will delete downloaded patch files and log files. Patches will be re-downloaded when needed.") + Text( + "This will delete downloaded patches and log files. Patches will be re-downloaded when needed.", + fontFamily = mono, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 18.sp + ) }, confirmButton = { Button( @@ -298,18 +307,663 @@ fun SettingsDialog( }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error - ) + ), + shape = RoundedCornerShape(2.dp) ) { - Text("Clear") + Text( + "CLEAR", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) } }, dismissButton = { TextButton(onClick = { showClearCacheConfirm = false }) { - Text("Cancel") + Text( + "CANCEL", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) } } ) } + + if (showAddSourceDialog) { + AddPatchSourceDialog( + onDismiss = { showAddSourceDialog = false }, + onAdd = { source -> + onAddPatchSource(source) + showAddSourceDialog = false + } + ) + } + + editingSource?.let { source -> + EditPatchSourceDialog( + source = source, + onDismiss = { editingSource = null }, + onSave = { updated -> + onEditPatchSource(updated) + editingSource = null + } + ) + } +} + +// ── Shared building blocks ── + +@Composable +private fun SectionLabel( + text: String, + mono: androidx.compose.ui.text.font.FontFamily +) { + Text( + text = text, + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) +} + +@Composable +private fun SettingsDivider(borderColor: Color) { + Spacer(Modifier.height(14.dp)) + HorizontalDivider(color = borderColor) + Spacer(Modifier.height(14.dp)) +} + +@Composable +private fun SettingToggleRow( + label: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + accentColor: Color, + mono: androidx.compose.ui.text.font.FontFamily +) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = label, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + Spacer(Modifier.height(2.dp)) + Text( + text = description, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + Spacer(Modifier.width(12.dp)) + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + colors = SwitchDefaults.colors( + checkedThumbColor = accentColor, + checkedTrackColor = accentColor.copy(alpha = 0.3f) + ) + ) + } +} + +@Composable +private fun ActionButton( + label: String, + icon: androidx.compose.ui.graphics.vector.ImageVector, + mono: androidx.compose.ui.text.font.FontFamily, + borderColor: Color, + contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + enabled: Boolean = true, + onClick: () -> Unit +) { + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + + OutlinedButton( + onClick = onClick, + enabled = enabled, + modifier = Modifier.fillMaxWidth().hoverable(hoverInteraction), + shape = RoundedCornerShape(2.dp), + border = BorderStroke( + 1.dp, + if (isHovered && enabled) contentColor.copy(alpha = 0.3f) + else borderColor + ), + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = contentColor, + disabledContentColor = contentColor.copy(alpha = 0.4f) + ) + ) { + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + label, + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp, + letterSpacing = 0.5.sp, + modifier = Modifier.weight(1f) + ) + } +} + +// ── Patch Sources Section ── + +@Composable +private fun PatchSourcesSection( + sources: List, + activeSourceId: String, + onActiveChange: (String) -> Unit, + onRemove: (String) -> Unit, + onEdit: (PatchSource) -> Unit, + onAddClick: () -> Unit, + mono: androidx.compose.ui.text.font.FontFamily, + borderColor: Color +) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + SectionLabel("PATCH SOURCES", mono) + Spacer(Modifier.height(2.dp)) + Text( + text = "Select where patches are loaded from", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + + Spacer(modifier = Modifier.height(8.dp)) + + sources.forEach { source -> + val isActive = source.id == activeSourceId + val hoverInteraction = remember(source.id) { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(2.dp)) + .border( + 1.dp, + when { + isActive -> MorpheColors.Blue.copy(alpha = 0.4f) + isHovered -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + else -> borderColor + }, + RoundedCornerShape(2.dp) + ) + .background( + if (isActive) MorpheColors.Blue.copy(alpha = 0.05f) + else Color.Transparent + ) + .hoverable(hoverInteraction) + .clickable { onActiveChange(source.id) } + .padding(horizontal = 12.dp, vertical = 10.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + // Active indicator dot + Box( + modifier = Modifier + .size(6.dp) + .background( + if (isActive) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f), + RoundedCornerShape(1.dp) + ) + ) + Spacer(Modifier.width(10.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = source.name, + fontSize = 12.sp, + fontWeight = if (isActive) FontWeight.SemiBold else FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = when (source.type) { + PatchSourceType.DEFAULT -> "Default" + PatchSourceType.GITHUB -> source.url?.removePrefix("https://github.com/") ?: "GitHub" + PatchSourceType.LOCAL -> source.filePath?.let { File(it).name } ?: "Local file" + }, + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (source.deletable) { + IconButton( + onClick = { onEdit(source) }, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = "Edit", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier.size(14.dp) + ) + } + Spacer(Modifier.width(2.dp)) + IconButton( + onClick = { onRemove(source.id) }, + modifier = Modifier.size(24.dp) + ) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = "Remove", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier.size(14.dp) + ) + } + } + } + } + Spacer(modifier = Modifier.height(4.dp)) + } + + // Add source + OutlinedButton( + onClick = onAddClick, + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(2.dp), + border = BorderStroke(1.dp, borderColor), + contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(14.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "ADD SOURCE", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp, + letterSpacing = 0.5.sp + ) + } + } +} + +// ── Add / Edit Source Dialogs ── + +@Composable +private fun AddPatchSourceDialog( + onDismiss: () -> Unit, + onAdd: (PatchSource) -> Unit +) { + val mono = JetBrainsMono + var name by remember { mutableStateOf("") } + var sourceType by remember { mutableStateOf(PatchSourceType.GITHUB) } + var url by remember { mutableStateOf("") } + var filePath by remember { mutableStateOf("") } + var error by remember { mutableStateOf(null) } + + AlertDialog( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(2.dp), + containerColor = MaterialTheme.colorScheme.surface, + title = { + Text( + "ADD SOURCE", + fontFamily = mono, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + letterSpacing = 1.sp + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.widthIn(min = 300.dp) + ) { + // Type toggle + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + listOf(PatchSourceType.GITHUB, PatchSourceType.LOCAL).forEach { type -> + val isSelected = sourceType == type + Box( + modifier = Modifier + .clip(RoundedCornerShape(2.dp)) + .border( + 1.dp, + if (isSelected) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), + RoundedCornerShape(2.dp) + ) + .background( + if (isSelected) MorpheColors.Blue.copy(alpha = 0.08f) + else Color.Transparent + ) + .clickable { sourceType = type } + .padding(horizontal = 14.dp, vertical = 7.dp) + ) { + Text( + text = when (type) { + PatchSourceType.GITHUB -> "GITHUB" + PatchSourceType.LOCAL -> "LOCAL FILE" + else -> "" + }, + fontSize = 10.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + fontFamily = mono, + letterSpacing = 0.5.sp, + color = if (isSelected) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + + OutlinedTextField( + value = name, + onValueChange = { name = it; error = null }, + label = { Text("Name", fontFamily = mono, fontSize = 11.sp) }, + placeholder = { Text("My Custom Patches", fontFamily = mono, fontSize = 11.sp) }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(2.dp) + ) + + when (sourceType) { + PatchSourceType.GITHUB -> { + OutlinedTextField( + value = url, + onValueChange = { url = it; error = null }, + label = { Text("Repository URL", fontFamily = mono, fontSize = 11.sp) }, + placeholder = { Text("https://github.com/owner/repo", fontFamily = mono, fontSize = 11.sp) }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(2.dp) + ) + } + PatchSourceType.LOCAL -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = filePath, + onValueChange = { filePath = it; error = null }, + label = { Text(".mpp file", fontFamily = mono, fontSize = 11.sp) }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(2.dp), + readOnly = true + ) + OutlinedButton( + onClick = { + val dialog = FileDialog(null as Frame?, "Select .mpp file", FileDialog.LOAD).apply { + setFilenameFilter { _, n -> n.endsWith(".mpp", ignoreCase = true) } + isVisible = true + } + if (dialog.directory != null && dialog.file != null) { + filePath = File(dialog.directory, dialog.file).absolutePath + if (name.isBlank()) name = dialog.file.removeSuffix(".mpp") + error = null + } + }, + shape = RoundedCornerShape(2.dp) + ) { + Text( + "BROWSE", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp, + letterSpacing = 0.5.sp + ) + } + } + } + else -> {} + } + + error?.let { + Text( + text = it, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.error + ) + } + } + }, + confirmButton = { + Button( + onClick = { + if (name.isBlank()) { error = "Name is required"; return@Button } + when (sourceType) { + PatchSourceType.GITHUB -> { + if (url.isBlank() || !url.contains("github.com/")) { + error = "Enter a valid GitHub repository URL"; return@Button + } + } + PatchSourceType.LOCAL -> { + if (filePath.isBlank() || !File(filePath).exists()) { + error = "Select a valid .mpp file"; return@Button + } + } + else -> {} + } + onAdd(PatchSource( + id = UUID.randomUUID().toString(), + name = name.trim(), + type = sourceType, + url = if (sourceType == PatchSourceType.GITHUB) url.trim() else null, + filePath = if (sourceType == PatchSourceType.LOCAL) filePath.trim() else null, + deletable = true + )) + }, + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), + shape = RoundedCornerShape(2.dp) + ) { + Text( + "ADD", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text( + "CANCEL", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) + } + } + ) +} + +@Composable +private fun EditPatchSourceDialog( + source: PatchSource, + onDismiss: () -> Unit, + onSave: (PatchSource) -> Unit +) { + val mono = JetBrainsMono + var name by remember { mutableStateOf(source.name) } + var url by remember { mutableStateOf(source.url ?: "") } + var filePath by remember { mutableStateOf(source.filePath ?: "") } + var error by remember { mutableStateOf(null) } + + AlertDialog( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(2.dp), + containerColor = MaterialTheme.colorScheme.surface, + title = { + Text( + "EDIT SOURCE", + fontFamily = mono, + fontWeight = FontWeight.Bold, + fontSize = 13.sp, + letterSpacing = 1.sp + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.widthIn(min = 300.dp) + ) { + // Type indicator + Text( + text = when (source.type) { + PatchSourceType.GITHUB -> "GITHUB REPOSITORY" + PatchSourceType.LOCAL -> "LOCAL FILE" + else -> "" + }, + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue, + letterSpacing = 1.sp + ) + + OutlinedTextField( + value = name, + onValueChange = { name = it; error = null }, + label = { Text("Name", fontFamily = mono, fontSize = 11.sp) }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(2.dp) + ) + + when (source.type) { + PatchSourceType.GITHUB -> { + OutlinedTextField( + value = url, + onValueChange = { url = it; error = null }, + label = { Text("Repository URL", fontFamily = mono, fontSize = 11.sp) }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(2.dp) + ) + } + PatchSourceType.LOCAL -> { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + OutlinedTextField( + value = filePath, + onValueChange = { filePath = it; error = null }, + label = { Text(".mpp file", fontFamily = mono, fontSize = 11.sp) }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), + modifier = Modifier.weight(1f), + shape = RoundedCornerShape(2.dp), + readOnly = true + ) + OutlinedButton( + onClick = { + val dialog = FileDialog(null as Frame?, "Select .mpp file", FileDialog.LOAD).apply { + setFilenameFilter { _, n -> n.endsWith(".mpp", ignoreCase = true) } + isVisible = true + } + if (dialog.directory != null && dialog.file != null) { + filePath = File(dialog.directory, dialog.file).absolutePath + error = null + } + }, + shape = RoundedCornerShape(2.dp) + ) { + Text( + "BROWSE", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 10.sp, + letterSpacing = 0.5.sp + ) + } + } + } + else -> {} + } + + error?.let { + Text(text = it, fontSize = 11.sp, fontFamily = mono, color = MaterialTheme.colorScheme.error) + } + } + }, + confirmButton = { + Button( + onClick = { + if (name.isBlank()) { error = "Name is required"; return@Button } + when (source.type) { + PatchSourceType.GITHUB -> { + if (url.isBlank() || !url.contains("github.com/")) { + error = "Enter a valid GitHub repository URL"; return@Button + } + } + PatchSourceType.LOCAL -> { + if (filePath.isBlank() || !File(filePath).exists()) { + error = "Select a valid .mpp file"; return@Button + } + } + else -> {} + } + onSave(source.copy( + name = name.trim(), + url = if (source.type == PatchSourceType.GITHUB) url.trim() else source.url, + filePath = if (source.type == PatchSourceType.LOCAL) filePath.trim() else source.filePath + )) + }, + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), + shape = RoundedCornerShape(2.dp) + ) { + Text( + "SAVE", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text( + "CANCEL", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) + } + } + ) } private fun ThemePreference.toDisplayName(): String { @@ -336,27 +990,14 @@ private fun calculateCacheSize(): String { private fun clearAllCache(): Boolean { return try { var failedCount = 0 - - // Delete patch files FileUtils.getPatchesDir().listFiles()?.forEach { file -> - try { - java.nio.file.Files.delete(file.toPath()) - } catch (e: Exception) { - failedCount++ - Logger.error("Failed to delete ${file.name}: ${e.message}") - } + try { java.nio.file.Files.delete(file.toPath()) } + catch (e: Exception) { failedCount++; Logger.error("Failed to delete ${file.name}: ${e.message}") } } - - // Delete log files FileUtils.getLogsDir().listFiles()?.forEach { file -> - try { - java.nio.file.Files.delete(file.toPath()) - } catch (e: Exception) { - failedCount++ - Logger.error("Failed to delete log ${file.name}: ${e.message}") - } + try { java.nio.file.Files.delete(file.toPath()) } + catch (e: Exception) { failedCount++; Logger.error("Failed to delete log ${file.name}: ${e.message}") } } - FileUtils.cleanupAllTempDirs() if (failedCount > 0) { Logger.error("Cache clear incomplete: $failedCount file(s) could not be deleted (may be locked)") diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 8b6dc0c..697c039 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -1,20 +1,31 @@ package app.morphe.gui.ui.screens.home +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* +import androidx.compose.foundation.horizontalScroll +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Clear +import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Warning +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -173,9 +184,10 @@ fun HomeScreenContent( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Loading patches...", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Loading patches…", + fontSize = 11.sp, + fontFamily = app.morphe.gui.ui.theme.JetBrainsMono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } @@ -237,6 +249,7 @@ fun HomeScreenContent( isCompact = isCompact, maxWidth = this@BoxWithConstraints.maxWidth, isLoading = uiState.isLoadingPatches, + isDefaultSource = uiState.isDefaultSource, supportedApps = uiState.supportedApps, loadError = uiState.patchLoadError, onRetry = { viewModel.retryLoadPatches() } @@ -310,6 +323,7 @@ private fun ApkSelectedSection( onChangeClick: () -> Unit, onContinueClick: () -> Unit ) { + val mono = app.morphe.gui.ui.theme.JetBrainsMono val showWarning = apkInfo.versionStatus != VersionStatus.EXACT_MATCH && apkInfo.versionStatus != VersionStatus.UNKNOWN val warningColor = when (apkInfo.versionStatus) { @@ -317,6 +331,7 @@ private fun ApkSelectedSection( VersionStatus.OLDER_VERSION -> Color(0xFFFF9800) else -> MorpheColors.Blue } + val primaryColor = if (showWarning) warningColor else MorpheColors.Blue Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -328,133 +343,114 @@ private fun ApkSelectedSection( modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(if (isCompact) 16.dp else 24.dp)) + Spacer(modifier = Modifier.height(if (isCompact) 16.dp else 20.dp)) - // Action buttons - stack vertically on compact if (isCompact) { Column( - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() ) { + // Primary action Button( onClick = onContinueClick, enabled = patchesLoaded, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (showWarning) warningColor else MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) + modifier = Modifier.fillMaxWidth().height(44.dp), + colors = ButtonDefaults.buttonColors(containerColor = primaryColor), + shape = RoundedCornerShape(2.dp) ) { - if (!patchesLoaded) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - "Loading patches...", - fontSize = 15.sp, - fontWeight = FontWeight.Medium - ) - } else { - if (showWarning) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = "Warning", - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Text( - "Continue", - fontSize = 15.sp, - fontWeight = FontWeight.Medium - ) - } + ActionButtonContent(patchesLoaded, showWarning, mono) } + // Secondary action OutlinedButton( onClick = onChangeClick, - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - shape = RoundedCornerShape(12.dp), + modifier = Modifier.fillMaxWidth().height(44.dp), + shape = RoundedCornerShape(2.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) ) { Text( - "Change APK", - fontSize = 15.sp, - fontWeight = FontWeight.Medium + "CHANGE APK", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 1.sp ) } } } else { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp) - ) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { OutlinedButton( onClick = onChangeClick, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(12.dp), + modifier = Modifier.height(44.dp), + shape = RoundedCornerShape(2.dp), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) ) { Text( - "Change APK", - fontSize = 15.sp, - fontWeight = FontWeight.Medium + "CHANGE APK", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 1.sp ) } - Button( onClick = onContinueClick, enabled = patchesLoaded, - modifier = Modifier - .widthIn(min = 160.dp) - .height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = if (showWarning) warningColor else MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) + modifier = Modifier.widthIn(min = 160.dp).height(44.dp), + colors = ButtonDefaults.buttonColors(containerColor = primaryColor), + shape = RoundedCornerShape(2.dp) ) { - if (!patchesLoaded) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp, - color = MaterialTheme.colorScheme.onPrimary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - "Loading...", - fontSize = 15.sp, - fontWeight = FontWeight.Medium - ) - } else { - if (showWarning) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = "Warning", - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - } - Text( - "Continue", - fontSize = 15.sp, - fontWeight = FontWeight.Medium - ) - } + ActionButtonContent(patchesLoaded, showWarning, mono) } } } } } +@Composable +private fun ActionButtonContent( + patchesLoaded: Boolean, + showWarning: Boolean, + mono: androidx.compose.ui.text.font.FontFamily +) { + if (!patchesLoaded) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MaterialTheme.colorScheme.onPrimary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + "LOADING…", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 1.sp + ) + } else { + if (showWarning) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = "Warning", + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + } + Text( + "CONTINUE", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 1.sp + ) + } +} + @Composable private fun VersionWarningDialog( versionStatus: VersionStatus, @@ -463,64 +459,76 @@ private fun VersionWarningDialog( onConfirm: () -> Unit, onDismiss: () -> Unit ) { + val mono = app.morphe.gui.ui.theme.JetBrainsMono + val warnColor = if (versionStatus == VersionStatus.NEWER_VERSION) + MaterialTheme.colorScheme.error else Color(0xFFFF9800) + val (title, message) = when (versionStatus) { VersionStatus.NEWER_VERSION -> Pair( - "Version Too New", - "You're using v$currentVersion, but the recommended version is v$suggestedVersion.\n\n" + - "Patching newer versions may cause issues or some patches might not work correctly.\n\n" + - "Do you want to continue anyway?" + "VERSION MISMATCH", + "Current: v$currentVersion\nExpected: v$suggestedVersion\n\nPatching newer versions may cause failures or broken patches." ) VersionStatus.OLDER_VERSION -> Pair( - "Older Version Detected", - "You're using v$currentVersion, but newer patches are available for v$suggestedVersion.\n\n" + - "You may be missing out on new features and bug fixes.\n\n" + - "Do you want to continue with this version?" + "OUTDATED VERSION", + "Current: v$currentVersion\nLatest patches target: v$suggestedVersion\n\nYou may be missing new features and fixes." ) - else -> Pair("Version Notice", "Continue with v$currentVersion?") + else -> Pair("VERSION NOTICE", "Continue with v$currentVersion?") } AlertDialog( onDismissRequest = onDismiss, - shape = RoundedCornerShape(16.dp), + shape = RoundedCornerShape(2.dp), + containerColor = MaterialTheme.colorScheme.surface, icon = { Icon( imageVector = Icons.Default.Warning, contentDescription = null, - tint = if (versionStatus == VersionStatus.NEWER_VERSION) - MaterialTheme.colorScheme.error - else - Color(0xFFFF9800), - modifier = Modifier.size(32.dp) + tint = warnColor, + modifier = Modifier.size(28.dp) ) }, title = { Text( text = title, - fontWeight = FontWeight.SemiBold + fontWeight = FontWeight.Bold, + fontFamily = mono, + fontSize = 14.sp, + letterSpacing = 1.sp ) }, text = { Text( text = message, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontFamily = mono, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 18.sp ) }, confirmButton = { Button( onClick = onConfirm, - colors = ButtonDefaults.buttonColors( - containerColor = if (versionStatus == VersionStatus.NEWER_VERSION) - MaterialTheme.colorScheme.error - else - Color(0xFFFF9800) - ) + colors = ButtonDefaults.buttonColors(containerColor = warnColor), + shape = RoundedCornerShape(2.dp) ) { - Text("Continue Anyway") + Text( + "CONTINUE ANYWAY", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) } }, dismissButton = { TextButton(onClick = onDismiss) { - Text("Cancel") + Text( + "CANCEL", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) } } ) @@ -547,6 +555,8 @@ private fun DropPromptSection( isCompact: Boolean = false, onBrowseClick: () -> Unit ) { + val mono = app.morphe.gui.ui.theme.JetBrainsMono + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = if (isCompact) 16.dp else 32.dp) @@ -554,11 +564,9 @@ private fun DropPromptSection( Text( text = if (isDragHovering) "Release to drop" else "Drop your APK here", fontSize = if (isCompact) 18.sp else 22.sp, - fontWeight = FontWeight.Medium, - color = if (isDragHovering) - MorpheColors.Blue - else - MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + color = if (isDragHovering) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurface, textAlign = TextAlign.Center ) @@ -566,65 +574,72 @@ private fun DropPromptSection( Text( text = "or", - fontSize = if (isCompact) 12.sp else 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 12.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) OutlinedButton( onClick = onBrowseClick, - modifier = Modifier.height(if (isCompact) 44.dp else 48.dp), - shape = RoundedCornerShape(12.dp), - colors = ButtonDefaults.outlinedButtonColors( - contentColor = MorpheColors.Blue - ) + modifier = Modifier.height(if (isCompact) 40.dp else 44.dp), + shape = RoundedCornerShape(2.dp), + border = BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.4f)), + colors = ButtonDefaults.outlinedButtonColors(contentColor = MorpheColors.Blue) ) { Text( - "Browse Files", - fontSize = if (isCompact) 14.sp else 16.sp, - fontWeight = FontWeight.Medium + "BROWSE FILES", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 1.sp ) } Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) Text( - text = "Supported: .apk and .apkm files", - fontSize = if (isCompact) 11.sp else 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + text = ".apk · .apkm", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f) ) } } @Composable private fun AnalyzingSection(isCompact: Boolean = false) { + val mono = app.morphe.gui.ui.theme.JetBrainsMono + Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(horizontal = if (isCompact) 16.dp else 32.dp) ) { CircularProgressIndicator( - modifier = Modifier.size(if (isCompact) 36.dp else 44.dp), + modifier = Modifier.size(if (isCompact) 28.dp else 32.dp), color = MorpheColors.Blue, - strokeWidth = 3.dp + strokeWidth = 2.dp ) Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) Text( - text = "Analyzing APK...", - fontSize = if (isCompact) 16.sp else 18.sp, - fontWeight = FontWeight.Medium, + text = "ANALYZING", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center + letterSpacing = 2.sp ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(6.dp)) Text( - text = "Reading app information", - fontSize = if (isCompact) 12.sp else 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Reading app metadata…", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } @@ -634,32 +649,37 @@ private fun SupportedAppsSection( isCompact: Boolean = false, maxWidth: Dp = 800.dp, isLoading: Boolean = false, + isDefaultSource: Boolean = true, supportedApps: List = emptyList(), loadError: String? = null, onRetry: () -> Unit = {} ) { - // Stack vertically if very narrow + val mono = app.morphe.gui.ui.theme.JetBrainsMono val useVerticalLayout = maxWidth < 400.dp Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { + // Section header — monospace, tracked, accent-colored Text( text = "SUPPORTED APPS", - fontSize = if (isCompact) 11.sp else 12.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - letterSpacing = 2.sp + fontSize = if (isCompact) 10.sp else 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Cyan.copy(alpha = 0.7f), + letterSpacing = 3.sp ) - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) + Spacer(modifier = Modifier.height(6.dp)) - // Important notice about APK handling Text( - text = "Download the exact version from APKMirror and drop it here directly.", + text = if (isDefaultSource) "Download the exact version from APKMirror and drop it here." + else "Drop the APK for a supported app here.", fontSize = if (isCompact) 10.sp else 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + fontFamily = mono, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), textAlign = TextAlign.Center, modifier = Modifier .widthIn(max = if (useVerticalLayout) 280.dp else 500.dp) @@ -670,91 +690,191 @@ private fun SupportedAppsSection( when { isLoading -> { - // Loading state Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(32.dp) ) { CircularProgressIndicator( - modifier = Modifier.size(32.dp), - color = MorpheColors.Blue, - strokeWidth = 3.dp + modifier = Modifier.size(24.dp), + color = MorpheColors.Cyan, + strokeWidth = 2.dp ) Spacer(modifier = Modifier.height(12.dp)) Text( text = "Loading patches...", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } } loadError != null -> { - // Error state Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp) ) { Text( - text = "Could not load supported apps", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.error + text = "LOAD FAILED", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.error, + letterSpacing = 1.sp ) Spacer(modifier = Modifier.height(4.dp)) Text( text = loadError, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), textAlign = TextAlign.Center ) Spacer(modifier = Modifier.height(12.dp)) OutlinedButton( onClick = onRetry, - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(2.dp), + colors = ButtonDefaults.outlinedButtonColors( + contentColor = MorpheColors.Cyan + ), + border = BorderStroke(1.dp, MorpheColors.Cyan.copy(alpha = 0.4f)) ) { - Text("Retry") + Text( + "RETRY", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 1.sp + ) } } } supportedApps.isEmpty() -> { - // Empty state (shouldn't happen normally) Text( text = "No supported apps found", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } else -> { - // Display supported apps dynamically - if (useVerticalLayout) { + val focusManager = LocalFocusManager.current + var searchQuery by remember { mutableStateOf("") } + val filteredApps = if (searchQuery.isBlank()) supportedApps + else supportedApps.filter { + it.displayName.contains(searchQuery, ignoreCase = true) || + it.packageName.contains(searchQuery, ignoreCase = true) + } + + if (supportedApps.size > 4) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { + Text( + "Filter apps…", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) + ) + }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = null, + tint = MorpheColors.Cyan.copy(alpha = 0.5f), + modifier = Modifier.size(16.dp) + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(14.dp) + ) + } + } + }, + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall.copy( + fontFamily = mono, + fontSize = 11.sp + ), + shape = RoundedCornerShape(2.dp), + modifier = Modifier + .widthIn(max = 260.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MorpheColors.Cyan.copy(alpha = 0.5f), + unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f), + cursorColor = MorpheColors.Cyan + ) + ) + Spacer(modifier = Modifier.height(12.dp)) + } + + // Wrap cards in a Box with min height so the section doesn't collapse + // when search yields no results (prevents layout jumping) + val cardsMinHeight = if (useVerticalLayout) 120.dp else 80.dp + + if (filteredApps.isEmpty()) { + // Empty results — hold the space + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(min = cardsMinHeight), + contentAlignment = Alignment.Center + ) { + Text( + text = "No matching apps", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + } else if (useVerticalLayout) { Column( - verticalArrangement = Arrangement.spacedBy(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .padding(horizontal = 16.dp) .widthIn(max = 300.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { focusManager.clearFocus() } ) { - supportedApps.forEach { app -> + filteredApps.forEach { app -> SupportedAppCardDynamic( supportedApp = app, isCompact = isCompact, + showDownloadButton = isDefaultSource, + showPackageName = !isDefaultSource, modifier = Modifier.fillMaxWidth() ) } } } else { Row( - horizontalArrangement = Arrangement.spacedBy(if (isCompact) 12.dp else 16.dp), + horizontalArrangement = Arrangement.spacedBy(if (isCompact) 6.dp else 8.dp), verticalAlignment = Alignment.Top, modifier = Modifier .padding(horizontal = if (isCompact) 8.dp else 16.dp) - .widthIn(max = 700.dp) + .horizontalScroll(rememberScrollState()) + .height(IntrinsicSize.Max) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { focusManager.clearFocus() } ) { - supportedApps.forEach { app -> + filteredApps.forEach { app -> SupportedAppCardDynamic( supportedApp = app, isCompact = isCompact, - modifier = Modifier.weight(1f) + showDownloadButton = isDefaultSource, + showPackageName = !isDefaultSource, + modifier = Modifier.width(190.dp).fillMaxHeight() ) } } @@ -765,7 +885,7 @@ private fun SupportedAppsSection( } /** - * Card showing current patches version with option to change. + * Patches version indicator — sharp, monospace, clickable. */ @Composable private fun PatchesVersionCard( @@ -775,52 +895,62 @@ private fun PatchesVersionCard( isCompact: Boolean = false, modifier: Modifier = Modifier ) { - Card( + val mono = app.morphe.gui.ui.theme.JetBrainsMono + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + val borderColor by animateColorAsState( + if (isHovered) MorpheColors.Blue.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = tween(200) + ) + + Box( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = onChangePatchesClick), - colors = CardDefaults.cardColors( - containerColor = MorpheColors.Blue.copy(alpha = 0.1f) - ), - shape = RoundedCornerShape(12.dp) + .clip(RoundedCornerShape(2.dp)) + .border(1.dp, borderColor, RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.surface) + .hoverable(hoverInteraction) + .clickable(onClick = onChangePatchesClick) ) { Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = if (isCompact) 10.dp else 12.dp), + .padding(horizontal = 16.dp, vertical = if (isCompact) 8.dp else 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Using patches", + text = "PATCHES", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = patchesVersion, fontSize = if (isCompact) 12.sp else 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MorpheColors.Blue ) - Spacer(modifier = Modifier.width(8.dp)) - Surface( - color = MorpheColors.Blue.copy(alpha = 0.2f), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = patchesVersion, - fontSize = if (isCompact) 11.sp else 12.sp, - fontWeight = FontWeight.SemiBold, - color = MorpheColors.Blue, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) - } if (isLatest) { - Spacer(modifier = Modifier.width(6.dp)) - Surface( - color = MorpheColors.Teal.copy(alpha = 0.2f), - shape = RoundedCornerShape(4.dp) + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(2.dp)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(2.dp)) + .padding(horizontal = 6.dp, vertical = 2.dp) ) { Text( - text = "Latest", - fontSize = if (isCompact) 9.sp else 10.sp, + text = "LATEST", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, color = MorpheColors.Teal, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + letterSpacing = 1.sp ) } } @@ -829,136 +959,184 @@ private fun PatchesVersionCard( } /** - * Dynamic supported app card that uses SupportedApp data from patches. + * Redesigned supported app card — sharp, technical, cyberdeck aesthetic. */ @Composable private fun SupportedAppCardDynamic( supportedApp: SupportedApp, isCompact: Boolean = false, + showDownloadButton: Boolean = true, + showPackageName: Boolean = false, modifier: Modifier = Modifier ) { + val mono = app.morphe.gui.ui.theme.JetBrainsMono var showAllVersions by remember { mutableStateOf(false) } - val cardPadding = if (isCompact) 12.dp else 16.dp - val downloadUrl = supportedApp.apkDownloadUrl + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + val borderColor by androidx.compose.animation.animateColorAsState( + if (isHovered) MorpheColors.Cyan.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = androidx.compose.animation.core.tween(200) + ) - Card( - modifier = modifier, - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ), - shape = RoundedCornerShape(if (isCompact) 12.dp else 16.dp) + Box( + modifier = modifier + .clip(RoundedCornerShape(2.dp)) + .border(1.dp, borderColor, RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.surface) + .hoverable(hoverInteraction) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(cardPadding), + .padding(if (isCompact) 12.dp else 14.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // App name + // App name — bold, clean Text( text = supportedApp.displayName, - fontSize = if (isCompact) 14.sp else 16.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + fontSize = if (isCompact) 13.sp else 14.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) - Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) + // Package name (always shown as monospace technical data) + Text( + text = supportedApp.packageName, + fontSize = 9.sp, + fontFamily = mono, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + letterSpacing = 0.3.sp + ) - // Recommended version badge (dynamic from patches) + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 10.dp)) + + // Version block if (supportedApp.recommendedVersion != null) { - val cornerRadius = if (isCompact) 6.dp else 8.dp - Surface( - color = MorpheColors.Teal.copy(alpha = 0.15f), - shape = RoundedCornerShape(cornerRadius), + // Recommended version — monospace, accent-colored + Column( modifier = Modifier - .clip(RoundedCornerShape(cornerRadius)) + .fillMaxWidth() + .clip(RoundedCornerShape(2.dp)) + .background(MorpheColors.Teal.copy(alpha = 0.06f)) + .border( + 1.dp, + MorpheColors.Teal.copy(alpha = 0.15f), + RoundedCornerShape(2.dp) + ) .clickable { showAllVersions = !showAllVersions } + .padding(horizontal = 10.dp, vertical = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.padding( - horizontal = if (isCompact) 10.dp else 12.dp, - vertical = if (isCompact) 6.dp else 8.dp - ), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Recommended", - fontSize = if (isCompact) 9.sp else 10.sp, - color = MorpheColors.Teal.copy(alpha = 0.8f), - letterSpacing = 0.5.sp - ) + Text( + text = "RECOMMENDED", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal.copy(alpha = 0.6f), + letterSpacing = 1.5.sp + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "v${supportedApp.recommendedVersion}", + fontSize = if (isCompact) 13.sp else 14.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MorpheColors.Teal + ) + val otherVersionsCount = supportedApp.supportedVersions.count { it != supportedApp.recommendedVersion } + if (otherVersionsCount > 0) { + Spacer(modifier = Modifier.height(2.dp)) Text( - text = "v${supportedApp.recommendedVersion}", - fontSize = if (isCompact) 12.sp else 14.sp, - fontWeight = FontWeight.SemiBold, - color = MorpheColors.Teal + text = if (showAllVersions) "hide ${otherVersionsCount} more" else "+${otherVersionsCount} compatible", + fontSize = 9.sp, + fontFamily = mono, + color = MorpheColors.Teal.copy(alpha = 0.4f) ) - // Show version count if more than 1 (excluding recommended) - val otherVersionsCount = supportedApp.supportedVersions.count { it != supportedApp.recommendedVersion } - if (otherVersionsCount > 0) { - Text( - text = if (showAllVersions) "▲ Hide versions" else "▼ +$otherVersionsCount more", - fontSize = if (isCompact) 9.sp else 10.sp, - color = MorpheColors.Teal.copy(alpha = 0.6f) - ) - } } } - // Expandable versions list (excluding recommended version) + // Expandable other versions — shown as proper tags val otherVersions = supportedApp.supportedVersions.filter { it != supportedApp.recommendedVersion } if (showAllVersions && otherVersions.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Surface( - color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), - shape = RoundedCornerShape(6.dp) + Spacer(modifier = Modifier.height(6.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(2.dp)) + .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(2.dp)) + .padding(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.padding(8.dp), - horizontalAlignment = Alignment.CenterHorizontally + Text( + text = "ALSO SUPPORTED", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f), + letterSpacing = 1.sp + ) + // Version tags in a flow-like layout + @OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth() ) { - Text( - text = "Other supported versions:", - fontSize = 9.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(4.dp)) - // Show versions in a compact grid-like format - val versionsText = otherVersions.joinToString(", ") { "v$it" } - Text( - text = versionsText, - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center, - lineHeight = 14.sp - ) + otherVersions.forEach { version -> + Box( + modifier = Modifier + .border( + 1.dp, + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.12f), + RoundedCornerShape(2.dp) + ) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = "v$version", + fontSize = 10.sp, + fontFamily = mono, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } } } } } else { - // No specific version recommended - Surface( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp) + // Any version — muted + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(2.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) + .padding(horizontal = 10.dp, vertical = 8.dp), + contentAlignment = Alignment.Center ) { Text( - text = "Any version", - fontSize = if (isCompact) 11.sp else 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding( - horizontal = if (isCompact) 10.dp else 12.dp, - vertical = if (isCompact) 6.dp else 8.dp - ) + text = "ANY VERSION", + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 1.sp ) } } - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) - - // Download from APKMirror button (only if URL is configured) - if (downloadUrl != null) { + // Download button + if (showDownloadButton && downloadUrl != null) { + Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 10.dp)) val uriHandler = LocalUriHandler.current OutlinedButton( onClick = { @@ -967,71 +1145,51 @@ private fun SupportedAppCardDynamic( } }, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(if (isCompact) 6.dp else 8.dp), - contentPadding = PaddingValues( - horizontal = if (isCompact) 8.dp else 12.dp, - vertical = if (isCompact) 6.dp else 8.dp - ), + shape = RoundedCornerShape(2.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp), colors = ButtonDefaults.outlinedButtonColors( - contentColor = MorpheColors.Blue + contentColor = MorpheColors.Cyan + ), + border = BorderStroke( + 1.dp, + MorpheColors.Cyan.copy(alpha = 0.3f) ) ) { Text( - text = "Download original APK", - fontSize = if (isCompact) 11.sp else 12.sp, - fontWeight = FontWeight.Medium + text = "DOWNLOAD APK", + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 0.5.sp ) } - - Spacer(modifier = Modifier.height(if (isCompact) 6.dp else 8.dp)) } - - // Package name - Text( - text = supportedApp.packageName, - fontSize = if (isCompact) 9.sp else 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center, - maxLines = 1 - ) } } } @Composable private fun DragOverlay() { + val mono = app.morphe.gui.ui.theme.JetBrainsMono + Box( modifier = Modifier .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - MorpheColors.Blue.copy(alpha = 0.15f), - MorpheColors.Blue.copy(alpha = 0.05f) - ) - ) - ), + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.85f)) + .border(2.dp, MorpheColors.Blue.copy(alpha = 0.4f)), contentAlignment = Alignment.Center ) { - Card( - modifier = Modifier.padding(32.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surface - ), - elevation = CardDefaults.cardElevation(defaultElevation = 8.dp), - shape = RoundedCornerShape(24.dp) + Column( + horizontalAlignment = Alignment.CenterHorizontally ) { - Column( - modifier = Modifier.padding(48.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Drop APK here", - fontSize = 24.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Blue - ) - } + Text( + text = "DROP APK", + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue, + letterSpacing = 4.sp + ) } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 165643f..5fb0335 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -6,10 +6,13 @@ import app.morphe.gui.data.model.Patch import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.data.repository.PatchSourceManager import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import net.dongliu.apk.parser.ApkFile @@ -20,21 +23,40 @@ import app.morphe.gui.util.SupportedAppExtractor import java.io.File class HomeViewModel( - private val patchRepository: PatchRepository, + private val patchSourceManager: PatchSourceManager, private val patchService: PatchService, private val configRepository: ConfigRepository ) : ScreenModel { - private val _uiState = MutableStateFlow(HomeUiState()) + private var patchRepository: PatchRepository = patchSourceManager.getActiveRepositorySync() + private var localPatchFilePath: String? = patchSourceManager.getLocalFilePath() + private var isDefaultSource: Boolean = patchSourceManager.isDefaultSource() + + private val _uiState = MutableStateFlow(HomeUiState(isDefaultSource = isDefaultSource)) val uiState: StateFlow = _uiState.asStateFlow() // Cached patches and supported apps private var cachedPatches: List = emptyList() private var cachedPatchesFile: File? = null + private var loadJob: Job? = null init { // Auto-fetch patches on startup loadPatchesAndSupportedApps() + + // Observe source changes — drop(1) to skip the initial value + screenModelScope.launch { + patchSourceManager.sourceVersion.drop(1).collect { + Logger.info("HomeVM: Source changed, reloading patches...") + patchRepository = patchSourceManager.getActiveRepositorySync() + localPatchFilePath = patchSourceManager.getLocalFilePath() + isDefaultSource = patchSourceManager.isDefaultSource() + lastLoadedVersion = null + cachedPatchesFile = null + _uiState.value = HomeUiState(isDefaultSource = isDefaultSource) + loadPatchesAndSupportedApps(forceRefresh = true) + } + } } // Track the last loaded version to avoid reloading unnecessarily @@ -45,9 +67,24 @@ class HomeViewModel( * If a saved version exists in config, load that version instead of latest. */ private fun loadPatchesAndSupportedApps(forceRefresh: Boolean = false) { - screenModelScope.launch { + loadJob?.cancel() + loadJob = screenModelScope.launch { _uiState.value = _uiState.value.copy(isLoadingPatches = true, patchLoadError = null) + // LOCAL source: skip GitHub entirely, load directly from the .mpp file + if (localPatchFilePath != null) { + val localFile = File(localPatchFilePath) + if (localFile.exists()) { + loadPatchesFromFile(localFile, localFile.nameWithoutExtension, latestVersion = null, isOffline = false) + } else { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Local patch file not found: ${localFile.name}" + ) + } + return@launch + } + try { // Check if there's a saved patches version in config val config = configRepository.loadConfig() @@ -165,22 +202,24 @@ class HomeViewModel( /** * Find any cached .mpp file when offline. * Prefers the file matching savedVersion from config. + * Searches the per-source cache directory. */ private fun findCachedPatchFile(savedVersion: String?): File? { - val patchesDir = FileUtils.getPatchesDir() - val mppFiles = patchesDir.listFiles { file -> file.extension.equals("mpp", ignoreCase = true) } - ?.filter { it.length() > 0 } - ?: return null + val patchesDir = patchRepository.getCacheDir() + val patchFiles = patchesDir.listFiles { file -> + val ext = file.extension.lowercase() + ext == "mpp" || ext == "jar" + }?.filter { it.length() > 0 } ?: return null - if (mppFiles.isEmpty()) return null + if (patchFiles.isEmpty()) return null return if (savedVersion != null) { // Strip "v" prefix — savedVersion is "v1.13.0" but filenames are "patches-1.13.0.mpp" val versionNumber = savedVersion.removePrefix("v") - mppFiles.firstOrNull { it.name.contains(versionNumber, ignoreCase = true) } - ?: mppFiles.maxByOrNull { it.lastModified() } + patchFiles.firstOrNull { it.name.contains(versionNumber, ignoreCase = true) } + ?: patchFiles.maxByOrNull { it.lastModified() } } else { - mppFiles.maxByOrNull { it.lastModified() } + patchFiles.maxByOrNull { it.lastModified() } } } @@ -198,7 +237,7 @@ class HomeViewModel( * Load patches from a local .mpp file and update UI state. * Used as fallback when offline with cached patches. */ - private suspend fun loadPatchesFromFile(patchFile: File, version: String, latestVersion: String?) { + private suspend fun loadPatchesFromFile(patchFile: File, version: String, latestVersion: String?, isOffline: Boolean = true) { cachedPatchesFile = patchFile lastLoadedVersion = version @@ -208,18 +247,18 @@ class HomeViewModel( if (patches == null || patches.isEmpty()) { _uiState.value = _uiState.value.copy( isLoadingPatches = false, - patchLoadError = "Could not load cached patches: ${patchesResult.exceptionOrNull()?.message}" + patchLoadError = "Could not load patches: ${patchesResult.exceptionOrNull()?.message}" ) return } cachedPatches = patches val supportedApps = SupportedAppExtractor.extractSupportedApps(patches) - Logger.info("Loaded ${supportedApps.size} supported apps from cached patches: ${patchFile.name}") + Logger.info("Loaded ${supportedApps.size} supported apps from ${if (isOffline) "cached" else "local"} patches: ${patchFile.name}") _uiState.value = _uiState.value.copy( isLoadingPatches = false, - isOffline = true, + isOffline = isOffline, supportedApps = supportedApps, patchesVersion = version, latestPatchesVersion = latestVersion, @@ -534,6 +573,7 @@ data class HomeUiState( // Dynamic patches data val isLoadingPatches: Boolean = true, val isOffline: Boolean = false, + val isDefaultSource: Boolean = true, val supportedApps: List = emptyList(), val patchesVersion: String? = null, val latestPatchesVersion: String? = null, // Track the latest available version diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index bfbeede..2f6e3b8 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -1,24 +1,31 @@ package app.morphe.gui.ui.screens.home.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material3.* -import androidx.compose.runtime.Composable +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.morphe.gui.ui.screens.home.ApkInfo import app.morphe.gui.ui.screens.home.VersionStatus +import app.morphe.gui.ui.theme.JetBrainsMono import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.ChecksumStatus @@ -28,262 +35,203 @@ fun ApkInfoCard( onClearClick: () -> Unit, modifier: Modifier = Modifier ) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ), - shape = RoundedCornerShape(16.dp) + val mono = JetBrainsMono + val accentColor = statusAccentColor(apkInfo) + val cardShape = RoundedCornerShape(2.dp) + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f) + + Box( + modifier = modifier + .fillMaxWidth() + .clip(cardShape) + .border(1.dp, borderColor, cardShape) + .background(MaterialTheme.colorScheme.surface) ) { + // Left accent stripe + Box( + modifier = Modifier + .width(3.dp) + .fillMaxHeight() + .background(accentColor) + .align(Alignment.CenterStart) + ) + Column( - modifier = Modifier.padding(20.dp) + modifier = Modifier + .fillMaxWidth() + .padding(start = 3.dp) ) { - // Header with app icon and close button + // ── Header: app identity + dismiss ── Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.Top + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.weight(1f) + // App initial — monospace, bold, in accent + Box( + modifier = Modifier + .size(44.dp) + .border(1.dp, accentColor.copy(alpha = 0.5f), RoundedCornerShape(2.dp)) + .background(accentColor.copy(alpha = 0.08f)), + contentAlignment = Alignment.Center ) { - Box( - modifier = Modifier - .size(64.dp) - .clip(RoundedCornerShape(14.dp)) - .background(Color.White), - contentAlignment = Alignment.Center - ) { - Text( - text = apkInfo.appName.first().toString(), - fontSize = 24.sp, - fontWeight = FontWeight.Bold, - color = MorpheColors.Blue - ) - } - - Column { - // App name - Text( - text = apkInfo.appName, - fontSize = 22.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface - ) + Text( + text = apkInfo.appName.first().uppercase(), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = accentColor + ) + } - Spacer(modifier = Modifier.height(2.dp)) + Spacer(Modifier.width(14.dp)) - // Version - Text( - text = "v${apkInfo.versionName}", - fontSize = 15.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + Column(modifier = Modifier.weight(1f)) { + Text( + text = apkInfo.appName, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.height(2.dp)) + Text( + text = apkInfo.packageName, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + letterSpacing = 0.3.sp + ) } - // Close button + // Dismiss button + val closeHover = remember { MutableInteractionSource() } + val isCloseHovered by closeHover.collectIsHoveredAsState() + val closeBg by animateColorAsState( + if (isCloseHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.12f) + else Color.Transparent, + animationSpec = tween(150) + ) + val closeBorder by animateColorAsState( + if (isCloseHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f), + animationSpec = tween(150) + ) + IconButton( onClick = onClearClick, modifier = Modifier - .size(32.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.surface.copy(alpha = 0.8f)) + .size(30.dp) + .hoverable(closeHover) + .background(closeBg, RoundedCornerShape(2.dp)) + .border(1.dp, closeBorder, RoundedCornerShape(2.dp)) ) { Icon( imageVector = Icons.Default.Close, - contentDescription = "Remove", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp) + contentDescription = "Remove APK", + tint = if (isCloseHovered) MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(14.dp) ) } } - Spacer(modifier = Modifier.height(20.dp)) - - // Info grid + // ── Technical data grid ── Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .padding(horizontal = 20.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp) ) { - // Size - InfoColumn( - label = "Size", - value = apkInfo.formattedSize, + TechDataCell( + label = "VERSION", + value = apkInfo.versionName, + mono = mono, modifier = Modifier.weight(1f) ) - - // Architecture - InfoColumn( - label = "Architecture", - value = if (apkInfo.architectures.isEmpty()) "Unknown" else apkInfo.architectures.joinToString(", "), + TechDataCell( + label = "SIZE", + value = apkInfo.formattedSize, + mono = mono, modifier = Modifier.weight(1f) ) - - // Min SDK if (apkInfo.minSdk != null) { - InfoColumn( - label = "Min SDK", + TechDataCell( + label = "MIN SDK", value = "API ${apkInfo.minSdk}", + mono = mono, modifier = Modifier.weight(1f) ) } } - // Version and checksum status section - Spacer(modifier = Modifier.height(16.dp)) - - HorizontalDivider( - color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - - Spacer(modifier = Modifier.height(16.dp)) - - // Version status - if (apkInfo.suggestedVersion != null && apkInfo.versionStatus != VersionStatus.EXACT_MATCH) { - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - VersionStatusBanner( - versionStatus = apkInfo.versionStatus, - currentVersion = apkInfo.versionName, - suggestedVersion = apkInfo.suggestedVersion - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - // Checksum warning for non-recommended versions - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - Text( - text = "Checksum verification unavailable for non-recommended versions", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - textAlign = TextAlign.Center - ) - } - } else if (apkInfo.versionStatus == VersionStatus.EXACT_MATCH) { - // Show checksum status for recommended version - Box( - modifier = Modifier.fillMaxWidth(), - contentAlignment = Alignment.Center - ) { - ChecksumStatusBanner(checksumStatus = apkInfo.checksumStatus) - } - } - } - } -} - -@Composable -private fun ChecksumStatusBanner(checksumStatus: ChecksumStatus) { - when (checksumStatus) { - is ChecksumStatus.Verified -> { - Surface( - color = MorpheColors.Teal.copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Recommended version - Verified", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal - ) - Text( - text = "Checksum matches APKMirror", - fontSize = 10.sp, - color = MorpheColors.Teal.copy(alpha = 0.8f) - ) - } - } - } - - is ChecksumStatus.Mismatch -> { - Surface( - color = MaterialTheme.colorScheme.error.copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Checksum Mismatch", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.error - ) - Text( - text = "File may be corrupted or modified. Re-download from APKMirror.", - fontSize = 10.sp, - color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f), - textAlign = TextAlign.Center - ) - } - } - } - - is ChecksumStatus.NotConfigured -> { - Surface( - color = MorpheColors.Teal.copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = "Using recommended version", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) - ) - } - } - - is ChecksumStatus.Error -> { - Surface( - color = Color(0xFFFF9800).copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), - horizontalAlignment = Alignment.CenterHorizontally + // ── Architectures — shown as individual tags, never truncated ── + if (apkInfo.architectures.isNotEmpty()) { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( - text = "Using recommended version", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = Color(0xFFFF9800) - ) - Text( - text = "Could not verify checksum", - fontSize = 10.sp, - color = Color(0xFFFF9800).copy(alpha = 0.8f) + text = "ARCH", + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp ) + Spacer(Modifier.width(4.dp)) + apkInfo.architectures.forEach { arch -> + Box( + modifier = Modifier + .border( + 1.dp, + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f), + RoundedCornerShape(2.dp) + ) + .padding(horizontal = 8.dp, vertical = 3.dp) + ) { + Text( + text = arch, + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface + ) + } + } } } - } - is ChecksumStatus.NonRecommendedVersion -> { - // This shouldn't happen in this branch, but handle it gracefully - Surface( - color = MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = "Using non-recommended version", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp) + // ── Status bar ── + val statusInfo = resolveStatus(apkInfo) + if (statusInfo != null) { + StatusBar( + statusInfo = statusInfo, + mono = mono, + borderColor = borderColor ) } } @@ -291,97 +239,160 @@ private fun ChecksumStatusBanner(checksumStatus: ChecksumStatus) { } @Composable -private fun InfoColumn( +private fun TechDataCell( label: String, value: String, + mono: androidx.compose.ui.text.font.FontFamily, modifier: Modifier = Modifier ) { - Column( - modifier = modifier, - horizontalAlignment = Alignment.Start - ) { + Column(modifier = modifier) { Text( text = label, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(Modifier.height(3.dp)) Text( text = value, fontSize = 14.sp, fontWeight = FontWeight.Medium, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurface, - maxLines = 2, + maxLines = 1, overflow = TextOverflow.Ellipsis ) } } +// ── Status ── + +private data class StatusInfo( + val color: Color, + val label: String, + val detail: String? = null +) + +@Composable +private fun resolveStatus(apkInfo: ApkInfo): StatusInfo? { + if (apkInfo.suggestedVersion != null && apkInfo.versionStatus != VersionStatus.EXACT_MATCH) { + return when (apkInfo.versionStatus) { + VersionStatus.OLDER_VERSION -> StatusInfo( + color = Color(0xFFFF9800), + label = "OUTDATED", + detail = "Patches target v${apkInfo.suggestedVersion}" + ) + VersionStatus.NEWER_VERSION -> StatusInfo( + color = MaterialTheme.colorScheme.error, + label = "VERSION MISMATCH", + detail = "Expected v${apkInfo.suggestedVersion} — patching may fail" + ) + else -> StatusInfo( + color = MaterialTheme.colorScheme.onSurfaceVariant, + label = "UNVERIFIED", + detail = "Suggested: v${apkInfo.suggestedVersion}" + ) + } + } + + if (apkInfo.versionStatus == VersionStatus.EXACT_MATCH) { + return when (apkInfo.checksumStatus) { + is ChecksumStatus.Verified -> StatusInfo( + color = MorpheColors.Teal, + label = "VERIFIED", + detail = "Checksum matches APKMirror" + ) + is ChecksumStatus.Mismatch -> StatusInfo( + color = MaterialTheme.colorScheme.error, + label = "CHECKSUM MISMATCH", + detail = "File may be corrupted — re-download from APKMirror" + ) + is ChecksumStatus.Error -> StatusInfo( + color = Color(0xFFFF9800), + label = "RECOMMENDED VERSION", + detail = "Checksum verification failed" + ) + is ChecksumStatus.NotConfigured -> StatusInfo( + color = MorpheColors.Teal, + label = "RECOMMENDED VERSION" + ) + is ChecksumStatus.NonRecommendedVersion -> null + } + } + + return null +} + @Composable -private fun VersionStatusBanner( - versionStatus: VersionStatus, - currentVersion: String, - suggestedVersion: String +private fun StatusBar( + statusInfo: StatusInfo, + mono: androidx.compose.ui.text.font.FontFamily, + borderColor: Color ) { - val (backgroundColor, textColor, message) = when (versionStatus) { - VersionStatus.OLDER_VERSION -> Triple( - Color(0xFFFF9800).copy(alpha = 0.15f), - Color(0xFFFF9800), - "Newer patches available for v$suggestedVersion" - ) - VersionStatus.NEWER_VERSION -> Triple( - MaterialTheme.colorScheme.error.copy(alpha = 0.15f), - MaterialTheme.colorScheme.error, - "Version too new. Recommended: v$suggestedVersion" + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .background(statusInfo.color.copy(alpha = 0.04f)) + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Status dot + Box( + modifier = Modifier + .size(6.dp) + .background(statusInfo.color, RoundedCornerShape(1.dp)) ) - else -> Triple( - MaterialTheme.colorScheme.surfaceVariant, - MaterialTheme.colorScheme.onSurfaceVariant, - "Suggested version: v$suggestedVersion" + + Spacer(Modifier.width(10.dp)) + + Text( + text = statusInfo.label, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = statusInfo.color, + letterSpacing = 1.sp ) - } - Surface( - color = backgroundColor, - shape = RoundedCornerShape(8.dp) - ) { - Column( - modifier = Modifier.padding(12.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { + if (statusInfo.detail != null) { + Spacer(Modifier.width(12.dp)) Text( - text = message, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = textColor, - textAlign = TextAlign.Center + text = statusInfo.detail, + fontSize = 11.sp, + fontFamily = mono, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis ) - if (versionStatus == VersionStatus.NEWER_VERSION) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "Patching may not work correctly with newer versions", - fontSize = 11.sp, - color = textColor.copy(alpha = 0.8f), - textAlign = TextAlign.Center - ) - } } } } -//private fun formatArchitectures(archs: List): String { -// if (archs.isEmpty()) return "Unknown" -// -// // Show full architecture names for clarity -// val formatted = archs.map { arch -> -// when (arch) { -// "arm64-v8a" -> "arm64-v8a" -// "armeabi-v7a" -> "armeabi-v7a" -// "x86_64" -> "x86_64" -// "x86" -> "x86" -// else -> arch -// } -// } -// -// return formatted.joinToString(", ") -//} +@Composable +private fun statusAccentColor(apkInfo: ApkInfo): Color { + if (apkInfo.suggestedVersion != null && apkInfo.versionStatus != VersionStatus.EXACT_MATCH) { + return when (apkInfo.versionStatus) { + VersionStatus.NEWER_VERSION -> MaterialTheme.colorScheme.error + VersionStatus.OLDER_VERSION -> Color(0xFFFF9800) + else -> MorpheColors.Blue + } + } + if (apkInfo.checksumStatus is ChecksumStatus.Mismatch) { + return MaterialTheme.colorScheme.error + } + if (apkInfo.versionStatus == VersionStatus.EXACT_MATCH) { + return MorpheColors.Teal + } + return MorpheColors.Blue +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 36b32b6..f8aa2ac 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -342,7 +342,13 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { PatchListItem( patch = patch, isSelected = uiState.selectedPatches.contains(patch.uniqueId), - onToggle = { viewModel.togglePatch(patch.uniqueId) } + onToggle = { viewModel.togglePatch(patch.uniqueId) }, + getOptionValue = { optionKey, default -> + viewModel.getOptionValue(patch.name, optionKey, default) + }, + onOptionValueChange = { optionKey, value -> + viewModel.setOptionValue(patch.name, optionKey, value) + } ) } } @@ -402,7 +408,7 @@ private fun SearchBar( OutlinedTextField( value = query, onValueChange = onQueryChange, - modifier = Modifier.weight(1f).height(48.dp), + modifier = Modifier.weight(1f), placeholder = { Text("Search patches...", style = MaterialTheme.typography.bodySmall) }, leadingIcon = { Icon( @@ -477,7 +483,9 @@ private fun SearchBar( private fun PatchListItem( patch: Patch, isSelected: Boolean, - onToggle: () -> Unit + onToggle: () -> Unit, + getOptionValue: (optionKey: String, default: String?) -> String = { _, d -> d ?: "" }, + onOptionValueChange: (optionKey: String, value: String) -> Unit = { _, _ -> } ) { val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() @@ -487,94 +495,140 @@ private fun PatchListItem( MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (isHovered) 0.5f else 0.3f) } + var showOptions by remember { mutableStateOf(false) } + Card( modifier = Modifier .fillMaxWidth() - .hoverable(interactionSource) - .clickable(interactionSource = interactionSource, indication = null, onClick = onToggle), + .hoverable(interactionSource), colors = CardDefaults.cardColors(containerColor = backgroundColor), shape = RoundedCornerShape(12.dp) ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Checkbox( - checked = isSelected, - onCheckedChange = null, - colors = CheckboxDefaults.colors( - checkedColor = MorpheColors.Blue, - uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant - ) - ) - - Column(modifier = Modifier.weight(1f)) { - Text( - text = patch.name, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + Column { + // Header area — clicking here toggles the patch + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(interactionSource = interactionSource, indication = null, onClick = onToggle) + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = isSelected, + onCheckedChange = null, + colors = CheckboxDefaults.colors( + checkedColor = MorpheColors.Blue, + uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant + ) ) - if (patch.description.isNotBlank()) { - Spacer(modifier = Modifier.height(4.dp)) + Column(modifier = Modifier.weight(1f)) { Text( - text = patch.description, - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis + text = patch.name, + fontSize = 15.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface ) - } - // Show compatible packages if any - if (patch.compatiblePackages.isNotEmpty()) { - val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") - Spacer(modifier = Modifier.height(4.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - patch.compatiblePackages.take(2).forEach { pkg -> - val meaningful = pkg.name.split(".").filter { it !in genericSegments } - val displayName = meaningful.takeLast(2).joinToString(" ") - .replaceFirstChar { it.uppercase() } - Surface( - color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.18f) - else MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = displayName, - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) + if (patch.description.isNotBlank()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = patch.description, + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + + // Show compatible packages if any + if (patch.compatiblePackages.isNotEmpty()) { + val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") + Spacer(modifier = Modifier.height(4.dp)) + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + patch.compatiblePackages.take(2).forEach { pkg -> + val meaningful = pkg.name.split(".").filter { it !in genericSegments } + val displayName = meaningful.takeLast(2).joinToString(" ") + .replaceFirstChar { it.uppercase() } + Surface( + color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.18f) + else MaterialTheme.colorScheme.surfaceVariant, + shape = RoundedCornerShape(4.dp) + ) { + Text( + text = displayName, + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + ) + } } } } + + // Options chip + if (patch.options.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${patch.options.size} option${if (patch.options.size > 1) "s" else ""} ${if (showOptions) "▲" else "▼"}", + fontSize = 10.sp, + color = MorpheColors.Teal + ) + } } + } - // Show options if patch has any - if (patch.options.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) + // Options editor — completely outside the toggle-clickable area + if (patch.options.isNotEmpty()) { + // Toggle button for options + if (!showOptions) { + Surface( + onClick = { showOptions = true }, + color = MorpheColors.Teal.copy(alpha = 0.06f), + modifier = Modifier.fillMaxWidth() ) { + Text( + text = "Configure options", + fontSize = 10.sp, + color = MorpheColors.Teal.copy(alpha = 0.7f), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) + ) + } + } + + AnimatedVisibility( + visible = showOptions, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Column( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Collapse button + Surface( + onClick = { showOptions = false }, + color = MorpheColors.Teal.copy(alpha = 0.06f), + shape = RoundedCornerShape(6.dp), + modifier = Modifier.fillMaxWidth() + ) { + Text( + text = "Hide options ▲", + fontSize = 10.sp, + color = MorpheColors.Teal.copy(alpha = 0.7f), + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + patch.options.forEach { option -> - Surface( - color = MorpheColors.Teal.copy(alpha = 0.1f), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = option.title.ifBlank { option.key }, - fontSize = 10.sp, - color = MorpheColors.Teal, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) - } + PatchOptionEditor( + option = option, + value = getOptionValue(option.key, option.default), + onValueChange = { onOptionValueChange(option.key, it) } + ) } } } @@ -583,6 +637,101 @@ private fun PatchListItem( } } +@Composable +private fun PatchOptionEditor( + option: app.morphe.gui.data.model.PatchOption, + value: String, + onValueChange: (String) -> Unit +) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = option.title.ifBlank { option.key }, + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal + ) + if (option.required) { + Text( + text = "*", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.error + ) + } + } + if (option.description.isNotBlank()) { + Text( + text = option.description, + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + when (option.type) { + app.morphe.gui.data.model.PatchOptionType.BOOLEAN -> { + var localChecked by remember(option.key) { mutableStateOf(value.equals("true", ignoreCase = true)) } + LaunchedEffect(value) { + val v = value.equals("true", ignoreCase = true) + if (localChecked != v) localChecked = v + } + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + Switch( + checked = localChecked, + onCheckedChange = { newChecked -> + localChecked = newChecked + onValueChange(newChecked.toString()) + }, + colors = SwitchDefaults.colors( + checkedTrackColor = MorpheColors.Teal + ) + ) + Text( + text = if (localChecked) "Enabled" else "Disabled", + fontSize = 10.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + else -> { + // Use local state to ensure text field is responsive, sync back to ViewModel + var localText by remember(option.key) { mutableStateOf(value) } + LaunchedEffect(value) { + if (localText != value) localText = value + } + + OutlinedTextField( + value = localText, + onValueChange = { newText -> + localText = newText + onValueChange(newText) + }, + placeholder = { + Text( + text = option.default ?: option.type.name.lowercase(), + fontSize = 11.sp + ) + }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontSize = 11.sp), + shape = RoundedCornerShape(6.dp), + modifier = Modifier.fillMaxWidth(), + colors = OutlinedTextFieldDefaults.colors( + unfocusedBorderColor = MorpheColors.Teal.copy(alpha = 0.3f), + focusedBorderColor = MorpheColors.Teal + ) + ) + } + } + } +} + @Composable private fun DefaultDisabledInfoCard( count: Int, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt index 74b710b..24017e3 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionViewModel.kt @@ -20,7 +20,8 @@ class PatchSelectionViewModel( private val packageName: String, private val apkArchitectures: List, private val patchService: PatchService, - private val patchRepository: PatchRepository + private val patchRepository: PatchRepository, + private val localPatchFilePath: String? = null ) : ScreenModel { // Actual path to use - may differ from patchesFilePath if we had to re-download @@ -162,6 +163,28 @@ class PatchSelectionViewModel( _uiState.value = _uiState.value.copy(selectedArchitectures = newSelection) } + /** + * Set a patch option value. Key format: "patchName.optionKey" + */ + fun setOptionValue(patchName: String, optionKey: String, value: String) { + val key = "$patchName.$optionKey" + val current = _uiState.value.patchOptionValues.toMutableMap() + if (value.isBlank()) { + current.remove(key) + } else { + current[key] = value + } + _uiState.value = _uiState.value.copy(patchOptionValues = current) + } + + /** + * Get a patch option value. Returns the user-set value, or the default if not set. + */ + fun getOptionValue(patchName: String, optionKey: String, default: String?): String { + val key = "$patchName.$optionKey" + return _uiState.value.patchOptionValues[key] ?: default ?: "" + } + /** * Count of patches that are disabled by default (from .mpp metadata). */ @@ -205,6 +228,7 @@ class PatchSelectionViewModel( patchesFilePath = actualPatchesFilePath, enabledPatches = selectedPatchNames, disabledPatches = disabledPatchNames, + patchOptions = _uiState.value.patchOptionValues, useExclusiveMode = true, striplibs = striplibs, continueOnError = continueOnError @@ -306,9 +330,20 @@ class PatchSelectionViewModel( /** * Download patches file if it's missing (e.g., after cache clear). + * For LOCAL sources, uses the local file directly. * Tries to find a release matching the expected filename, or falls back to latest stable. */ private suspend fun downloadMissingPatches(expectedFilename: String): Result { + // LOCAL source: use the local file directly instead of downloading + if (localPatchFilePath != null) { + val localFile = File(localPatchFilePath) + return if (localFile.exists()) { + Result.success(localFile) + } else { + Result.failure(Exception("Local patch file not found: ${localFile.name}")) + } + } + // Try to extract version from filename (e.g., "morphe-patches-1.9.0.mpp" -> "1.9.0") val versionRegex = Regex("""(\d+\.\d+\.\d+(?:-dev\.\d+)?)""") val versionMatch = versionRegex.find(expectedFilename) @@ -357,7 +392,8 @@ data class PatchSelectionUiState( val showOnlySelected: Boolean = false, val error: String? = null, val apkArchitectures: List = emptyList(), - val selectedArchitectures: Set = emptySet() + val selectedArchitectures: Set = emptySet(), + val patchOptionValues: Map = emptyMap() ) { val selectedCount: Int get() = selectedPatches.size val totalCount: Int get() = allPatches.size diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 60f4d41..548015e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropUp import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* @@ -135,26 +136,38 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { .fillMaxSize() .padding(paddingValues) ) { - // Channel selector (hidden when offline) - if (!uiState.isOffline) { - ChannelSelector( - selectedChannel = uiState.selectedChannel, - onChannelSelected = { viewModel.setChannel(it) }, - stableCount = uiState.stableReleases.size, - devCount = uiState.devReleases.size, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + // Local source: show simple file info, no release list + if (uiState.isLocalSource) { + LocalSourceBanner( + patchFile = uiState.downloadedPatchFile, + modifier = Modifier.padding(16.dp) ) - } + } else { + // Channel selector (hidden when offline) + if (!uiState.isOffline) { + ChannelSelector( + selectedChannel = uiState.selectedChannel, + onChannelSelected = { viewModel.setChannel(it) }, + stableCount = uiState.stableReleases.size, + devCount = uiState.devReleases.size, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + ) + } - // Offline banner - if (uiState.isOffline && uiState.currentReleases.isNotEmpty()) { - OfflineBanner( - onRetry = { viewModel.loadReleases() }, - modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 0.dp) - ) + // Offline banner + if (uiState.isOffline && uiState.currentReleases.isNotEmpty()) { + OfflineBanner( + onRetry = { viewModel.loadReleases() }, + modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 8.dp, bottom = 0.dp) + ) + } } when { + uiState.isLocalSource -> { + // Local source: ready, no release list needed + Spacer(modifier = Modifier.weight(1f)) + } uiState.isLoading -> { Box( modifier = Modifier.fillMaxSize(), @@ -408,16 +421,16 @@ private fun ReleaseCard( Spacer(modifier = Modifier.height(4.dp)) - // Show .mpp file info if available - release.assets.find { it.isMpp() }?.let { mppAsset -> + // Show patch file info if available (.mpp or .jar) + release.assets.find { it.isPatchFile() }?.let { patchAsset -> Text( - text = "${mppAsset.name} (${mppAsset.getFormattedSize()})", + text = "${patchAsset.name} (${patchAsset.getFormattedSize()})", fontSize = 13.sp, color = subtitleColor ) } - val formattedDate = formatDate(release.publishedAt) + val formattedDate = release.publishedAt?.let { formatDate(it) } ?: "" if (formattedDate.isNotEmpty()) { Text( text = "${if (isOffline) "Cached:" else "Published:"} $formattedDate", @@ -679,6 +692,47 @@ private fun BottomActionBar( } } +@Composable +private fun LocalSourceBanner( + patchFile: File?, + modifier: Modifier = Modifier +) { + Surface( + color = MorpheColors.Blue.copy(alpha = 0.08f), + shape = RoundedCornerShape(12.dp), + border = androidx.compose.foundation.BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.2f)), + modifier = modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Default.FolderOpen, + contentDescription = null, + tint = MorpheColors.Blue, + modifier = Modifier.size(24.dp) + ) + Column { + Text( + text = "Local Patch File", + fontSize = 14.sp, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurface + ) + if (patchFile != null) { + Text( + text = patchFile.name, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + private fun formatDate(isoDate: String): String { return try { // Takes "2024-01-15T10:30:00Z" and returns "Jan 15, 2024 at 10:30 AM" diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt index 6acbbe0..da9728c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt @@ -22,7 +22,8 @@ class PatchesViewModel( private val apkPath: String, private val apkName: String, private val patchRepository: PatchRepository, - private val configRepository: ConfigRepository + private val configRepository: ConfigRepository, + private val localPatchFilePath: String? = null ) : ScreenModel { private val _uiState = MutableStateFlow(PatchesUiState()) @@ -36,6 +37,24 @@ class PatchesViewModel( screenModelScope.launch { _uiState.value = _uiState.value.copy(isLoading = true, error = null) + // LOCAL source: skip GitHub, use the file directly + if (localPatchFilePath != null) { + val localFile = File(localPatchFilePath) + if (localFile.exists()) { + _uiState.value = _uiState.value.copy( + isLoading = false, + isLocalSource = true, + downloadedPatchFile = localFile + ) + } else { + _uiState.value = _uiState.value.copy( + isLoading = false, + error = "Local patch file not found: ${localFile.name}" + ) + } + return@launch + } + val result = patchRepository.fetchReleases() result.fold( @@ -139,7 +158,7 @@ class PatchesViewModel( // In offline mode, find the cached file by matching the asset name val assetName = release.assets.firstOrNull()?.name if (assetName != null) { - val patchesDir = app.morphe.gui.util.FileUtils.getPatchesDir() + val patchesDir = patchRepository.getCacheDir() val file = File(patchesDir, assetName) if (file.exists()) file else null } else null @@ -155,13 +174,14 @@ class PatchesViewModel( } /** - * Find all cached .mpp files in the patches directory. + * Find all cached .mpp files in the per-source cache directory. */ private fun findAllCachedPatchFiles(): List { - val patchesDir = app.morphe.gui.util.FileUtils.getPatchesDir() - return patchesDir.listFiles { file -> file.extension.equals("mpp", ignoreCase = true) } - ?.filter { it.length() > 0 } - ?: emptyList() + val patchesDir = patchRepository.getCacheDir() + return patchesDir.listFiles { file -> + val ext = file.extension.lowercase() + ext == "mpp" || ext == "jar" + }?.filter { it.length() > 0 } ?: emptyList() } private val versionRegex = Regex("""(\d+\.\d+\.\d+(?:-dev\.\d+)?)""") @@ -205,8 +225,8 @@ class PatchesViewModel( * Check if patches for a release are already downloaded and valid. */ private fun checkCachedPatches(release: Release): File? { - val asset = patchRepository.findMppAsset(release) ?: return null - val patchesDir = app.morphe.gui.util.FileUtils.getPatchesDir() + val asset = patchRepository.findPatchAsset(release) ?: return null + val patchesDir = patchRepository.getCacheDir() val cachedFile = File(patchesDir, asset.name) // Verify file exists and size matches (size check acts as basic integrity verification) @@ -329,6 +349,7 @@ enum class ReleaseChannel { data class PatchesUiState( val isLoading: Boolean = false, val isOffline: Boolean = false, + val isLocalSource: Boolean = false, val offlineReleases: List = emptyList(), val stableReleases: List = emptyList(), val devReleases: List = emptyList(), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index f39c0a0..228be08 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.draganddrop.dragAndDropTarget import androidx.compose.foundation.layout.* +import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll @@ -37,7 +38,7 @@ import app.morphe.morphe_cli.generated.resources.morphe_light import app.morphe.gui.ui.theme.LocalThemeState import app.morphe.gui.ui.theme.ThemePreference import app.morphe.gui.data.repository.ConfigRepository -import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.data.repository.PatchSourceManager import app.morphe.gui.util.PatchService import org.jetbrains.compose.resources.painterResource import org.koin.compose.koinInject @@ -62,12 +63,12 @@ import java.awt.Frame class QuickPatchScreen : Screen { @Composable override fun Content() { - val patchRepository: PatchRepository = koinInject() + val patchSourceManager: PatchSourceManager = koinInject() val patchService: PatchService = koinInject() val configRepository: ConfigRepository = koinInject() val viewModel = remember { - QuickPatchViewModel(patchRepository, patchService, configRepository) + QuickPatchViewModel(patchSourceManager, patchService, configRepository) } QuickPatchContent(viewModel) @@ -230,6 +231,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { isLoading = uiState.isLoadingPatches, loadError = uiState.patchLoadError, patchesVersion = uiState.patchesVersion, + isDefaultSource = uiState.isDefaultSource, onOpenUrl = { url -> openUrlAndFollowRedirects(url) { urlResolved -> uriHandler.openUri(urlResolved) @@ -708,6 +710,7 @@ private fun SupportedAppsRow( isLoading: Boolean, loadError: String? = null, patchesVersion: String?, + isDefaultSource: Boolean = true, onOpenUrl: (String) -> Unit, onRetry: () -> Unit = {} ) { @@ -720,7 +723,7 @@ private fun SupportedAppsRow( verticalAlignment = Alignment.CenterVertically ) { Text( - text = "Download original APK", + text = if (isDefaultSource) "Download original APK" else "Supported apps", fontSize = 12.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -776,45 +779,155 @@ private fun SupportedAppsRow( } } } else { - // Show supported apps dynamically - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - supportedApps.forEach { app -> - val url = app.apkDownloadUrl - if (url != null) { + // Search bar for many apps + val focusManager = androidx.compose.ui.platform.LocalFocusManager.current + var searchQuery by remember { mutableStateOf("") } + val filteredApps = if (searchQuery.isBlank()) supportedApps + else supportedApps.filter { + it.displayName.contains(searchQuery, ignoreCase = true) || + it.packageName.contains(searchQuery, ignoreCase = true) + } + + if (supportedApps.size > 4) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { + Text( + "Search apps...", + style = MaterialTheme.typography.bodySmall + ) + }, + leadingIcon = { + Icon( + Icons.Default.Search, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { + Icon( + Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(14.dp) + ) + } + } + }, + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall, + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(44.dp), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MorpheColors.Blue, + unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + ) + ) + Spacer(modifier = Modifier.height(8.dp)) + } + + if (isDefaultSource) { + // Default source: show clickable download cards with fixed width + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .height(IntrinsicSize.Max) + .clickable( + interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, + indication = null + ) { focusManager.clearFocus() }, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + filteredApps.forEach { app -> + val url = app.apkDownloadUrl + if (url != null) { + OutlinedCard( + onClick = { onOpenUrl(url) }, + modifier = Modifier.width(180.dp).fillMaxHeight(), + shape = RoundedCornerShape(8.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = app.displayName, + fontSize = 13.sp, + fontWeight = FontWeight.Medium + ) + app.recommendedVersion?.let { version -> + Text( + text = "v$version", + fontSize = 10.sp, + color = MorpheColors.Teal + ) + } + } + Icon( + imageVector = Icons.AutoMirrored.Filled.OpenInNew, + contentDescription = "Open", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } + } + } + } + } + } else { + // Custom source: show app names and versions in a scrollable row + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .height(IntrinsicSize.Max) + .clickable( + interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, + indication = null + ) { focusManager.clearFocus() }, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + filteredApps.forEach { app -> OutlinedCard( - onClick = { onOpenUrl(url) }, - modifier = Modifier.weight(1f), + modifier = Modifier.width(160.dp).fillMaxHeight(), shape = RoundedCornerShape(8.dp) ) { - Row( + Column( modifier = Modifier .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically + .padding(12.dp) ) { - Column(modifier = Modifier.weight(1f)) { + Text( + text = app.displayName, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Text( + text = app.packageName, + fontSize = 9.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + app.recommendedVersion?.let { version -> Text( - text = app.displayName, - fontSize = 13.sp, - fontWeight = FontWeight.Medium + text = "v$version", + fontSize = 10.sp, + color = MorpheColors.Teal ) - app.recommendedVersion?.let { version -> - Text( - text = "v$version", - fontSize = 10.sp, - color = MorpheColors.Teal - ) - } } - Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = "Open", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp) - ) } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 4104e45..7d53546 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -8,10 +8,12 @@ import app.morphe.gui.data.model.PatchConfig import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.data.repository.PatchSourceManager import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import net.dongliu.apk.parser.ApkFile import app.morphe.gui.util.ChecksumStatus @@ -25,15 +27,20 @@ import java.io.File * ViewModel for Quick Patch mode - handles the entire flow in one screen. */ class QuickPatchViewModel( - private val patchRepository: PatchRepository, + private val patchSourceManager: PatchSourceManager, private val patchService: PatchService, private val configRepository: ConfigRepository ) : ScreenModel { - private val _uiState = MutableStateFlow(QuickPatchUiState()) + private var patchRepository: PatchRepository = patchSourceManager.getActiveRepositorySync() + private var localPatchFilePath: String? = patchSourceManager.getLocalFilePath() + private var isDefaultSource: Boolean = patchSourceManager.isDefaultSource() + + private val _uiState = MutableStateFlow(QuickPatchUiState(isDefaultSource = isDefaultSource)) val uiState: StateFlow = _uiState.asStateFlow() private var patchingJob: Job? = null + private var loadJob: Job? = null // Cached dynamic data from patches private var cachedPatches: List = emptyList() @@ -43,15 +50,45 @@ class QuickPatchViewModel( init { // Load patches on startup to get dynamic app info loadPatchesAndSupportedApps() + + // Observe source changes + screenModelScope.launch { + patchSourceManager.sourceVersion.drop(1).collect { + Logger.info("QuickVM: Source changed, reloading patches...") + patchRepository = patchSourceManager.getActiveRepositorySync() + localPatchFilePath = patchSourceManager.getLocalFilePath() + isDefaultSource = patchSourceManager.isDefaultSource() + cachedPatchesFile = null + cachedPatches = emptyList() + cachedSupportedApps = emptyList() + _uiState.value = QuickPatchUiState(isDefaultSource = isDefaultSource) + loadPatchesAndSupportedApps() + } + } } /** * Load patches from GitHub and extract supported apps dynamically. */ private fun loadPatchesAndSupportedApps() { - screenModelScope.launch { + loadJob?.cancel() + loadJob = screenModelScope.launch { _uiState.value = _uiState.value.copy(isLoadingPatches = true, patchLoadError = null) + // LOCAL source: skip GitHub entirely, load directly from the .mpp file + if (localPatchFilePath != null) { + val localFile = File(localPatchFilePath) + if (localFile.exists()) { + loadPatchesFromFile(localFile, localFile.nameWithoutExtension, isOffline = false) + } else { + _uiState.value = _uiState.value.copy( + isLoadingPatches = false, + patchLoadError = "Local patch file not found: ${localFile.name}" + ) + } + return@launch + } + try { // Fetch releases val releasesResult = patchRepository.fetchReleases() @@ -136,20 +173,22 @@ class QuickPatchViewModel( /** * Find any cached .mpp file when offline. + * Searches the per-source cache directory. */ private fun findCachedPatchFile(savedVersion: String?): File? { - val patchesDir = FileUtils.getPatchesDir() - val mppFiles = patchesDir.listFiles { file -> file.extension.equals("mpp", ignoreCase = true) } - ?.filter { it.length() > 0 } - ?: return null + val patchesDir = patchRepository.getCacheDir() + val patchFiles = patchesDir.listFiles { file -> + val ext = file.extension.lowercase() + ext == "mpp" || ext == "jar" + }?.filter { it.length() > 0 } ?: return null - if (mppFiles.isEmpty()) return null + if (patchFiles.isEmpty()) return null return if (savedVersion != null) { - mppFiles.firstOrNull { it.name.contains(savedVersion, ignoreCase = true) } - ?: mppFiles.maxByOrNull { it.lastModified() } + patchFiles.firstOrNull { it.name.contains(savedVersion, ignoreCase = true) } + ?: patchFiles.maxByOrNull { it.lastModified() } } else { - mppFiles.maxByOrNull { it.lastModified() } + patchFiles.maxByOrNull { it.lastModified() } } } @@ -162,7 +201,7 @@ class QuickPatchViewModel( /** * Load patches from a local .mpp file (offline fallback). */ - private suspend fun loadPatchesFromFile(patchFile: File, version: String) { + private suspend fun loadPatchesFromFile(patchFile: File, version: String, isOffline: Boolean = true) { cachedPatchesFile = patchFile val patchesResult = patchService.listPatches(patchFile.absolutePath) @@ -171,7 +210,7 @@ class QuickPatchViewModel( if (patches.isNullOrEmpty()) { _uiState.value = _uiState.value.copy( isLoadingPatches = false, - patchLoadError = "Could not load cached patches: ${patchesResult.exceptionOrNull()?.message}" + patchLoadError = "Could not load patches: ${patchesResult.exceptionOrNull()?.message}" ) return } @@ -179,14 +218,14 @@ class QuickPatchViewModel( cachedPatches = patches val supportedApps = SupportedAppExtractor.extractSupportedApps(patches) cachedSupportedApps = supportedApps - Logger.info("Quick mode: Loaded ${supportedApps.size} supported apps from cached patches: ${patchFile.name}") + Logger.info("Quick mode: Loaded ${supportedApps.size} supported apps from ${if (isOffline) "cached" else "local"} patches: ${patchFile.name}") _uiState.value = _uiState.value.copy( isLoadingPatches = false, supportedApps = supportedApps, patchesVersion = version, patchLoadError = null, - isOffline = true + isOffline = isOffline ) } @@ -467,6 +506,7 @@ class QuickPatchViewModel( patchingJob = null _uiState.value = QuickPatchUiState( // Preserve already-loaded patches data + isDefaultSource = isDefaultSource, isLoadingPatches = false, supportedApps = cachedSupportedApps, patchesVersion = _uiState.value.patchesVersion @@ -526,6 +566,7 @@ data class QuickApkInfo( */ data class QuickPatchUiState( val phase: QuickPatchPhase = QuickPatchPhase.IDLE, + val isDefaultSource: Boolean = true, val apkFile: File? = null, val apkInfo: QuickApkInfo? = null, val error: String? = null, diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt b/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt new file mode 100644 index 0000000..738e65c --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt @@ -0,0 +1,20 @@ +package app.morphe.gui.ui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.platform.Font + +/** + * JetBrains Mono — the monospace face for all technical data: + * versions, package names, architectures, checksums, console output. + */ +val JetBrainsMono: FontFamily + @Composable + get() = FontFamily( + Font(resource = "fonts/JetBrainsMono-Light.ttf", weight = FontWeight.Light), + Font(resource = "fonts/JetBrainsMono-Regular.ttf", weight = FontWeight.Normal), + Font(resource = "fonts/JetBrainsMono-Medium.ttf", weight = FontWeight.Medium), + Font(resource = "fonts/JetBrainsMono-SemiBold.ttf", weight = FontWeight.SemiBold), + Font(resource = "fonts/JetBrainsMono-Bold.ttf", weight = FontWeight.Bold), + ) diff --git a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt index dc713a4..ff9037f 100644 --- a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt +++ b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt @@ -3,6 +3,7 @@ package app.morphe.gui.util import app.morphe.gui.data.model.Patch import app.morphe.gui.data.model.SupportedApp + /** * Extracts supported apps from parsed patch data. * This allows the app to dynamically determine which apps are supported diff --git a/src/main/resources/fonts/JetBrainsMono-Bold.ttf b/src/main/resources/fonts/JetBrainsMono-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..8c93043de6454ad2d5575f0751150c6551d9c588 GIT binary patch literal 277828 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

^Q2@p_nJptvCyhdNtfUDv9XvP^J$Tq^Yf}I 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 s3n!(`lCJDArN|NmBd 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

Zh7cd=34FaGWy`SDvi)66*nbC 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 RXt+-ctVzYaTEQjd{{RH8A$|Y= literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/JetBrainsMono-Light.ttf b/src/main/resources/fonts/JetBrainsMono-Light.ttf new file mode 100644 index 0000000000000000000000000000000000000000..15f15a2a121f3544286a7ea5ba886c56552e99b5 GIT binary patch literal 276452 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�gzxH>XR@G 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 JS1YjO{15&)U={!X literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/JetBrainsMono-Medium.ttf b/src/main/resources/fonts/JetBrainsMono-Medium.ttf new file mode 100644 index 0000000000000000000000000000000000000000..97671156df256e850498054fdebcd41d74a65d6b GIT binary patch literal 273860 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%0AmA2ptU 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 literal 0 HcmV?d00001 diff --git a/src/main/resources/fonts/JetBrainsMono-Regular.ttf b/src/main/resources/fonts/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000000000000000000000000000000000000..dff66cc50702c75abd025dcf49f62a4dcc2d72de GIT binary patch literal 273900 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≦fXo=WUuIA1w6wc ze~u{Wb9+aAvsl~X_C7Z%y?9g7;~Om*Y2NJVomZXf_|uD${+1m5eV$(Co5AnzN_)iO zk$!cK{^^8XB#Pf|ReI_g>7U8bAN2I%hmHA_(u)Ts{h=KF;e=lO6#e&@)F53$FBT-#1w4#e0(;y$~(rx=ejI`&Up%eY&ep_joO?K6-ez zZ;d?Mke@c%aGy@gbI z$L}*w?_iv6ru7>UdX)z)CFBucK4MkIU7z>Mf*0FI{6)F)w4$V?KO%liwtvCJ4iDcg z_;DVu;}uA3opm|*k{o|O|GA?ff6V`+eY%R!`k5wvrmK&BobZjlK387X$m+d`=PH@I z1;!toyoHn8@fYEq2fJvuUK2j-C3r;2RiWTX`D8x+2T$P{G9~{PKo*R{csXgi_yk`1;L;*n zu4#}j9#7NcqBqxpaA`dSl2aGmRf-4W=y;mTgpbg>zPP&F$jOQ>C&$L>91@1kp|t3N z?%3F$zRbhesr_e;#?JhVV~%+OK(AA~5aQvF4JLDA#UKi02W>~F;z{`Rx8t1_?57GGZHnfb25qGv4sLJr1xj<;J* zqciQp$t?S)+RpMgw@K(#%d_;qaP;19yxs5vskO00GBWE{AL;y5%O`g0SgJmL6Tsx` z@K9HueoMdv!y5FQL1)mnE4|<-U#ym#UG;WW`YSra{^E>6@syzG5%C3u1Wx3UnrAxa z$RF!G(Ca)SKR=%2V$2)T8gFTS=EhQSe^Q+4g*ojYT|MH4inCsg9fIx`{H#%kbBo-R z+L&t;a=P?sl?V5yls5UPU&L5=U7$ROk=Al7vpB+94p9T9WKJlZ zuSe#nXszbEdW1f|PiRlHz^~sij&6Q4rbXG}om=?y^OQMN#611nq^(VBlh?nGJ)cAa zk18M9O=+DmpCtYEwgMzlm+BDf!cNA?MkAMA0v-Ylb}7=wWKS?7)jl1Ol>aizg<7@I zm=zga`JTh64cO+!&i9eE`2NA3s|9!3tkNf?$lr}iddz)nk8vr#H)VPs>Xh~)O|8=! zc&mYFMh0El*?-B`(|Dy34fgc3Zl0bidhI`aTQXwmS+dq Date: Fri, 13 Mar 2026 16:40:31 +0530 Subject: [PATCH 34/49] Major Update Major Theme Changes Non- Morphe Repo patches are now addable. Major UI Improvements --- src/main/kotlin/app/morphe/gui/App.kt | 73 +- .../gui/ui/components/DeviceIndicator.kt | 10 +- .../gui/ui/components/LottieAnimation.kt | 88 ++ .../morphe/gui/ui/components/OfflineBanner.kt | 10 +- .../morphe/gui/ui/components/SakuraPetals.kt | 340 +++++++ .../gui/ui/components/SettingsButton.kt | 10 +- .../gui/ui/components/SettingsDialog.kt | 185 +++- .../morphe/gui/ui/screens/home/HomeScreen.kt | 846 +++++++++++------ .../ui/screens/home/components/ApkInfoCard.kt | 16 +- .../gui/ui/screens/patches/PatchesScreen.kt | 859 +++++++++++------- .../gui/ui/screens/quick/QuickPatchScreen.kt | 4 +- .../gui/ui/screens/result/ResultScreen.kt | 1 - .../morphe/gui/ui/theme/MorpheTypography.kt | 21 + .../kotlin/app/morphe/gui/ui/theme/Theme.kt | 155 +++- src/main/resources/cat2333s.json | 1 + src/main/resources/fonts/Nunito-Bold.ttf | Bin 0 -> 125464 bytes src/main/resources/fonts/Nunito-Light.ttf | Bin 0 -> 125564 bytes src/main/resources/fonts/Nunito-Medium.ttf | Bin 0 -> 125652 bytes src/main/resources/fonts/Nunito-Regular.ttf | Bin 0 -> 125528 bytes src/main/resources/fonts/Nunito-SemiBold.ttf | Bin 0 -> 125536 bytes 20 files changed, 1918 insertions(+), 701 deletions(-) create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt create mode 100644 src/main/resources/cat2333s.json create mode 100644 src/main/resources/fonts/Nunito-Bold.ttf create mode 100644 src/main/resources/fonts/Nunito-Light.ttf create mode 100644 src/main/resources/fonts/Nunito-Medium.ttf create mode 100644 src/main/resources/fonts/Nunito-Regular.ttf create mode 100644 src/main/resources/fonts/Nunito-SemiBold.ttf diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index b84d1b1..7745e89 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -1,10 +1,15 @@ package app.morphe.gui import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.* import androidx.compose.material3.Surface import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clipToBounds +import androidx.compose.ui.unit.dp +import app.morphe.gui.ui.components.LottieAnimation +import app.morphe.gui.ui.components.SakuraPetals import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition import app.morphe.gui.data.repository.ConfigRepository @@ -110,20 +115,62 @@ private fun AppContent(initialSimplifiedMode: Boolean) { LocalModeState provides modeState ) { Surface(modifier = Modifier.fillMaxSize()) { - if (!isLoading) { - // ViewModels observe PatchSourceManager.sourceVersion internally - // and reload when the active source changes — no Navigator recreation needed. - val patchService: PatchService = koinInject() - val quickViewModel = remember { - QuickPatchViewModel(patchSourceManager, patchService, configRepository) + Box(modifier = Modifier.fillMaxSize()) { + if (!isLoading) { + val patchService: PatchService = koinInject() + val quickViewModel = remember { + QuickPatchViewModel(patchSourceManager, patchService, configRepository) + } + + Crossfade(targetState = isSimplifiedMode) { simplified -> + if (simplified) { + QuickPatchContent(quickViewModel) + } else { + Navigator(HomeScreen()) { navigator -> + SlideTransition(navigator) + } + } + } } - Crossfade(targetState = isSimplifiedMode) { simplified -> - if (simplified) { - QuickPatchContent(quickViewModel) - } else { - Navigator(HomeScreen()) { navigator -> - SlideTransition(navigator) + // Falling petals — on top of everything (Sakura) + SakuraPetals( + enabled = themePreference == ThemePreference.SAKURA + ) + + // Matcha cat — top-right corner + if (themePreference == ThemePreference.MATCHA) { + val catJson = remember { + try { + object {}.javaClass.getResourceAsStream("/cat2333s.json") + ?.bufferedReader()?.readText() + } catch (e: Exception) { + null + } + } + catJson?.let { json -> + // 1080px canvas, rendered at 350dp (1dp ≈ 3.086 canvas px). + // Ears ~y385 → 125dp, bar bottom ~y576 → 187dp. + // Body shrunk to 85% so it hides behind bar. + // Clip from 120dp to 192dp (72dp visible) — ears to just past bar. + val renderSize = 350.dp + val clipTop = 120.dp // just above ears + val clipHeight = 72.dp // ears → just past bar bottom + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(top = 24.dp, end = 16.dp) + .requiredWidth(renderSize) + .requiredHeight(clipHeight) + .clipToBounds() + ) { + LottieAnimation( + jsonString = json, + modifier = Modifier + .requiredSize(renderSize) + .offset(y = -clipTop), + alpha = 0.28f + ) } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt index 5ff0cc2..474d52e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt @@ -23,14 +23,16 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.morphe.gui.ui.theme.JetBrainsMono +import app.morphe.gui.ui.theme.LocalMorpheFont +import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.DeviceMonitor import app.morphe.gui.util.DeviceStatus @Composable fun DeviceIndicator(modifier: Modifier = Modifier) { - val mono = JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val monitorState by DeviceMonitor.state.collectAsState() val isAdbAvailable = monitorState.isAdbAvailable @@ -62,11 +64,11 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { Box(modifier = modifier) { Surface( onClick = { showPopup = !showPopup }, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), color = Color.Transparent, modifier = Modifier .hoverable(hoverInteraction) - .border(1.dp, borderColor, RoundedCornerShape(2.dp)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) ) { Row( modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), diff --git a/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt b/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt new file mode 100644 index 0000000..44e624c --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt @@ -0,0 +1,88 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.unit.dp +import org.jetbrains.skia.Rect as SkiaRect +import org.jetbrains.skia.skottie.Animation + +/** + * Plays a Lottie JSON animation using Skia's built-in Skottie renderer. + * No extra dependencies needed — Compose Desktop includes Skottie via Skiko. + * + * @param jsonString The raw Lottie JSON content + * @param modifier Layout modifier + * @param alpha Opacity of the animation (0f–1f) + * @param iterations Number of loops (0 = infinite) + */ +@Composable +fun LottieAnimation( + jsonString: String, + modifier: Modifier = Modifier, + alpha: Float = 1f, + iterations: Int = 0 +) { + val animation = remember(jsonString) { + try { + Animation.makeFromString(jsonString) + } catch (e: Exception) { + null + } + } ?: return + + val duration = animation.duration + var progress by remember { mutableFloatStateOf(0f) } + var loopCount by remember { mutableIntStateOf(0) } + + LaunchedEffect(animation) { + val startTime = withFrameNanos { it } + var lastNanos = startTime + + while (true) { + withFrameNanos { nanos -> + val elapsed = (nanos - lastNanos) / 1_000_000_000.0 + lastNanos = nanos + + progress += (elapsed / duration).toFloat() + if (progress >= 1f) { + loopCount++ + if (iterations > 0 && loopCount >= iterations) { + progress = 1f + } else { + progress %= 1f + } + } + } + } + } + + Canvas(modifier = modifier) { + drawIntoCanvas { canvas -> + animation.seekFrameTime((progress * duration)) + canvas.save() + if (alpha < 1f) { + canvas.nativeCanvas.save() + // Apply alpha via layer + val paint = org.jetbrains.skia.Paint().apply { + this.alpha = (alpha * 255).toInt() + } + canvas.nativeCanvas.saveLayer( + SkiaRect.makeWH(size.width, size.height), + paint + ) + } + animation.render( + canvas.nativeCanvas, + SkiaRect.makeWH(size.width, size.height) + ) + if (alpha < 1f) { + canvas.nativeCanvas.restore() + } + canvas.restore() + } + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt b/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt index fb0bfe1..c5f6a88 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/OfflineBanner.kt @@ -18,17 +18,19 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import app.morphe.gui.ui.theme.JetBrainsMono +import app.morphe.gui.ui.theme.LocalMorpheFont +import app.morphe.gui.ui.theme.LocalMorpheCorners @Composable fun OfflineBanner( onRetry: () -> Unit, modifier: Modifier = Modifier ) { - val mono = JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() - val shape = RoundedCornerShape(2.dp) + val shape = RoundedCornerShape(corners.medium) Surface( modifier = modifier @@ -58,7 +60,7 @@ fun OfflineBanner( OutlinedButton( onClick = onRetry, modifier = Modifier.hoverable(interactionSource).height(28.dp), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), contentPadding = PaddingValues(horizontal = 10.dp, vertical = 0.dp), border = androidx.compose.foundation.BorderStroke( 1.dp, diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt b/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt new file mode 100644 index 0000000..906169f --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt @@ -0,0 +1,340 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.StrokeJoin +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.graphics.drawscope.rotate +import androidx.compose.ui.graphics.drawscope.translate +import androidx.compose.ui.graphics.drawscope.Stroke +import kotlin.math.cos +import kotlin.math.sin +import kotlin.random.Random + +private data class Petal( + var x: Float, + var y: Float, + val size: Float, // 6–14px + var rotation: Float, // degrees + val rotationSpeed: Float, // degrees per frame + val fallSpeed: Float, // px per frame + val driftAmplitude: Float, // horizontal sway amplitude + val driftFrequency: Float, // sway frequency + val alpha: Float, // 0.15–0.5 + val color: Color, + var age: Float = 0f // accumulator for drift sin wave +) + +/** + * Subtle falling sakura petals overlay. + * Draws 12–18 petals drifting down with gentle rotation and horizontal sway. + * Designed to be layered behind interactive content (no pointer input). + */ +@Composable +fun SakuraPetals( + modifier: Modifier = Modifier, + petalCount: Int = 15, + enabled: Boolean = true +) { + if (!enabled) return + + val petalColors = remember { + listOf( + Color(0xFFE8729A), // primary pink + Color(0xFFF2A0BA), // lighter pink + Color(0xFFD4607E), // deeper rose + Color(0xFFF7C4D4), // pale blush + ) + } + + var petals by remember { + mutableStateOf>(emptyList()) + } + + var canvasWidth by remember { mutableFloatStateOf(0f) } + var canvasHeight by remember { mutableFloatStateOf(0f) } + + // Animate frame-by-frame + LaunchedEffect(enabled) { + if (!enabled) return@LaunchedEffect + while (true) { + withFrameNanos { _ -> + if (canvasWidth <= 0f || canvasHeight <= 0f) return@withFrameNanos + + // Initialize petals if empty + if (petals.isEmpty()) { + petals = List(petalCount) { + createPetal(canvasWidth, canvasHeight, petalColors, scattered = true) + } + } + + // Update each petal + petals = petals.map { petal -> + val newAge = petal.age + 0.02f + val newY = petal.y + petal.fallSpeed + val drift = sin(newAge * petal.driftFrequency) * petal.driftAmplitude + val newX = petal.x + drift * 0.3f + val newRotation = petal.rotation + petal.rotationSpeed + + // Recycle if off-screen + if (newY > canvasHeight + 30f) { + createPetal(canvasWidth, canvasHeight, petalColors, scattered = false) + } else { + petal.copy( + x = newX, + y = newY, + rotation = newRotation, + age = newAge + ) + } + } + } + } + } + + Canvas( + modifier = modifier.fillMaxSize() + ) { + canvasWidth = size.width + canvasHeight = size.height + + petals.forEach { petal -> + drawPetal(petal) + } + } +} + +private fun createPetal( + width: Float, + height: Float, + colors: List, + scattered: Boolean +): Petal { + return Petal( + x = Random.nextFloat() * width, + y = if (scattered) Random.nextFloat() * height else Random.nextFloat() * -200f - 20f, + size = Random.nextFloat() * 8f + 6f, + rotation = Random.nextFloat() * 360f, + rotationSpeed = (Random.nextFloat() - 0.5f) * 1.5f, + fallSpeed = Random.nextFloat() * 0.4f + 0.2f, + driftAmplitude = Random.nextFloat() * 1.2f + 0.3f, + driftFrequency = Random.nextFloat() * 2f + 1f, + alpha = Random.nextFloat() * 0.3f + 0.15f, + color = colors.random() + ) +} + +private fun DrawScope.drawPetal(petal: Petal) { + translate(left = petal.x, top = petal.y) { + rotate(degrees = petal.rotation, pivot = Offset.Zero) { + val s = petal.size + val path = Path().apply { + moveTo(0f, -s) + cubicTo(s * 0.8f, -s * 0.6f, s * 0.6f, s * 0.3f, 0f, s * 0.5f) + cubicTo(-s * 0.6f, s * 0.3f, -s * 0.8f, -s * 0.6f, 0f, -s) + close() + } + drawPath( + path = path, + color = petal.color.copy(alpha = petal.alpha) + ) + } + } +} + +// ════════════════════════════════════════════════════════════════════ +// CHERRY BLOSSOM TREE — decorative background branch +// ════════════════════════════════════════════════════════════════════ + +private val BranchBrown = Color(0xFF8B6F5E) +private val BlossomPink = Color(0xFFE8729A) +private val BlossomLight = Color(0xFFF2A0BA) +private val BlossomPale = Color(0xFFF7C4D4) +private val BlossomCenter = Color(0xFFFFE0B2) + +/** + * Decorative cherry blossom branch growing from the bottom-right corner. + * Very low opacity — atmospheric, not distracting. + */ +@Composable +fun SakuraTree( + modifier: Modifier = Modifier, + enabled: Boolean = true +) { + if (!enabled) return + + Canvas(modifier = modifier.fillMaxSize()) { + val w = size.width + val h = size.height + + // All coordinates are relative to canvas size so it scales with the window + val branchAlpha = 0.10f + val blossomAlpha = 0.13f + + // ── Main trunk: curves up from bottom-right ── + val trunk = Path().apply { + moveTo(w + 10f, h + 20f) + cubicTo( + w - 40f, h - 80f, + w - 60f, h - 200f, + w - 90f, h - 320f + ) + cubicTo( + w - 110f, h - 400f, + w - 100f, h - 480f, + w - 130f, h - 540f + ) + } + drawPath( + path = trunk, + color = BranchBrown.copy(alpha = branchAlpha), + style = Stroke(width = 6f, cap = StrokeCap.Round, join = StrokeJoin.Round) + ) + + // ── Branch 1: sweeps left from mid-trunk ── + val branch1 = Path().apply { + moveTo(w - 80f, h - 280f) + cubicTo( + w - 140f, h - 310f, + w - 200f, h - 300f, + w - 260f, h - 330f + ) + } + drawPath( + path = branch1, + color = BranchBrown.copy(alpha = branchAlpha), + style = Stroke(width = 3.5f, cap = StrokeCap.Round, join = StrokeJoin.Round) + ) + + // ── Branch 1 twig ── + val twig1a = Path().apply { + moveTo(w - 200f, h - 300f) + cubicTo( + w - 220f, h - 330f, + w - 240f, h - 340f, + w - 270f, h - 350f + ) + } + drawPath( + path = twig1a, + color = BranchBrown.copy(alpha = branchAlpha * 0.8f), + style = Stroke(width = 2f, cap = StrokeCap.Round) + ) + + // ── Branch 2: sweeps right-upward from upper trunk ── + val branch2 = Path().apply { + moveTo(w - 110f, h - 420f) + cubicTo( + w - 70f, h - 460f, + w - 50f, h - 500f, + w - 80f, h - 560f + ) + } + drawPath( + path = branch2, + color = BranchBrown.copy(alpha = branchAlpha), + style = Stroke(width = 3f, cap = StrokeCap.Round, join = StrokeJoin.Round) + ) + + // ── Branch 3: small twig from lower trunk ── + val branch3 = Path().apply { + moveTo(w - 55f, h - 160f) + cubicTo( + w - 90f, h - 180f, + w - 120f, h - 170f, + w - 150f, h - 200f + ) + } + drawPath( + path = branch3, + color = BranchBrown.copy(alpha = branchAlpha * 0.8f), + style = Stroke(width = 2.5f, cap = StrokeCap.Round) + ) + + // ── Branch 4: top crown ── + val branch4 = Path().apply { + moveTo(w - 125f, h - 520f) + cubicTo( + w - 170f, h - 540f, + w - 210f, h - 530f, + w - 240f, h - 560f + ) + } + drawPath( + path = branch4, + color = BranchBrown.copy(alpha = branchAlpha * 0.7f), + style = Stroke(width = 2f, cap = StrokeCap.Round) + ) + + // ── Blossom clusters ── + // Each cluster: a few overlapping petals + a center dot + + // Cluster positions along the branches + val clusters = listOf( + // branch 1 clusters + Triple(w - 180f, h - 305f, 12f), + Triple(w - 240f, h - 325f, 10f), + Triple(w - 260f, h - 335f, 14f), + Triple(w - 270f, h - 350f, 9f), + // branch 1 twig + Triple(w - 255f, h - 345f, 11f), + // branch 2 clusters + Triple(w - 75f, h - 470f, 11f), + Triple(w - 65f, h - 510f, 13f), + Triple(w - 80f, h - 550f, 10f), + // branch 3 clusters + Triple(w - 120f, h - 175f, 10f), + Triple(w - 145f, h - 195f, 12f), + // branch 4 clusters + Triple(w - 190f, h - 535f, 11f), + Triple(w - 230f, h - 555f, 13f), + // trunk clusters + Triple(w - 95f, h - 340f, 10f), + Triple(w - 115f, h - 450f, 12f), + Triple(w - 130f, h - 530f, 9f), + ) + + clusters.forEach { (cx, cy, r) -> + drawBlossom(cx, cy, r, blossomAlpha) + } + } +} + +/** + * Draws a single cherry blossom: 5 petals arranged radially + center dot. + */ +private fun DrawScope.drawBlossom(cx: Float, cy: Float, radius: Float, alpha: Float) { + val petalColors = listOf(BlossomPink, BlossomLight, BlossomPale, BlossomLight, BlossomPink) + + // 5 petals at 72° intervals + for (i in 0 until 5) { + val angle = Math.toRadians((i * 72.0 + 18.0)) // offset 18° so it's not axis-aligned + val px = cx + cos(angle).toFloat() * radius * 0.5f + val py = cy + sin(angle).toFloat() * radius * 0.5f + + val petalPath = Path().apply { + val s = radius * 0.55f + moveTo(px, py - s) + cubicTo(px + s * 0.7f, py - s * 0.5f, px + s * 0.5f, py + s * 0.2f, px, py + s * 0.3f) + cubicTo(px - s * 0.5f, py + s * 0.2f, px - s * 0.7f, py - s * 0.5f, px, py - s) + close() + } + drawPath( + path = petalPath, + color = petalColors[i].copy(alpha = alpha) + ) + } + + // Center dot + drawCircle( + color = BlossomCenter.copy(alpha = alpha * 1.2f), + radius = radius * 0.15f, + center = Offset(cx, cy) + ) +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt index 75d1559..e7ce8bb 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -30,6 +30,7 @@ import kotlinx.coroutines.NonCancellable import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.compose.koinInject +import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.LocalThemeState @Composable @@ -37,6 +38,7 @@ fun SettingsButton( modifier: Modifier = Modifier, allowCacheClear: Boolean = true ) { + val corners = LocalMorpheCorners.current val themeState = LocalThemeState.current val modeState = LocalModeState.current val configRepository: ConfigRepository = koinInject() @@ -70,8 +72,8 @@ fun SettingsButton( modifier = modifier .size(34.dp) .hoverable(hoverInteraction) - .border(1.dp, borderColor, RoundedCornerShape(2.dp)) - .background(Color.Transparent, RoundedCornerShape(2.dp)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .background(Color.Transparent, RoundedCornerShape(corners.small)) ) { Icon( imageVector = Icons.Default.Settings, @@ -145,9 +147,11 @@ fun TopBarRow( modifier: Modifier = Modifier, allowCacheClear: Boolean = true, ) { + val corners = LocalMorpheCorners.current + val isSoft = corners.small >= 8.dp Row( modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(6.dp), + horizontalArrangement = Arrangement.spacedBy(if (isSoft) 12.dp else 6.dp), verticalAlignment = Alignment.CenterVertically ) { DeviceIndicator() diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 7ac016e..5f71c24 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -28,7 +28,8 @@ import androidx.compose.ui.unit.sp import app.morphe.gui.data.constants.AppConstants import app.morphe.gui.data.model.PatchSource import app.morphe.gui.data.model.PatchSourceType -import app.morphe.gui.ui.theme.JetBrainsMono +import app.morphe.gui.ui.theme.LocalMorpheFont +import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.ui.theme.ThemePreference import app.morphe.gui.util.FileUtils @@ -56,7 +57,8 @@ fun SettingsDialog( onEditPatchSource: (PatchSource) -> Unit = {}, onRemovePatchSource: (String) -> Unit = {} ) { - val mono = JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) var showClearCacheConfirm by remember { mutableStateOf(false) } @@ -67,7 +69,7 @@ fun SettingsDialog( AlertDialog( onDismissRequest = onDismiss, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.medium), containerColor = MaterialTheme.colorScheme.surface, title = { Text( @@ -89,38 +91,51 @@ fun SettingsDialog( // ── Theme ── SectionLabel("THEME", mono) Spacer(Modifier.height(8.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + @OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class) + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { ThemePreference.entries.forEach { theme -> val isSelected = currentTheme == theme + val themeAccent = theme.accentColor() val hoverInteraction = remember { MutableInteractionSource() } val isHovered by hoverInteraction.collectIsHoveredAsState() - Box( + Row( modifier = Modifier - .clip(RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.small)) .border( 1.dp, when { - isSelected -> MorpheColors.Blue.copy(alpha = 0.5f) + isSelected -> themeAccent.copy(alpha = 0.5f) isHovered -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) else -> borderColor }, - RoundedCornerShape(2.dp) + RoundedCornerShape(corners.small) ) .background( - if (isSelected) MorpheColors.Blue.copy(alpha = 0.08f) + if (isSelected) themeAccent.copy(alpha = 0.08f) else Color.Transparent ) .hoverable(hoverInteraction) .clickable { onThemeChange(theme) } - .padding(horizontal = 14.dp, vertical = 7.dp) + .padding(horizontal = 10.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { + // Themed icon + Text( + text = theme.iconSymbol(), + fontSize = 11.sp, + color = themeAccent + ) Text( text = theme.toDisplayName().uppercase(), fontSize = 10.sp, fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, fontFamily = mono, letterSpacing = 0.5.sp, - color = if (isSelected) MorpheColors.Blue + color = if (isSelected) themeAccent else MaterialTheme.colorScheme.onSurfaceVariant ) } @@ -258,7 +273,7 @@ fun SettingsDialog( confirmButton = { OutlinedButton( onClick = onDismiss, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), border = BorderStroke(1.dp, borderColor) ) { Text( @@ -277,7 +292,7 @@ fun SettingsDialog( if (showClearCacheConfirm) { AlertDialog( onDismissRequest = { showClearCacheConfirm = false }, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.medium), containerColor = MaterialTheme.colorScheme.surface, title = { Text( @@ -308,7 +323,7 @@ fun SettingsDialog( colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error ), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { Text( "CLEAR", @@ -430,6 +445,7 @@ private fun ActionButton( enabled: Boolean = true, onClick: () -> Unit ) { + val corners = LocalMorpheCorners.current val hoverInteraction = remember { MutableInteractionSource() } val isHovered by hoverInteraction.collectIsHoveredAsState() @@ -437,7 +453,7 @@ private fun ActionButton( onClick = onClick, enabled = enabled, modifier = Modifier.fillMaxWidth().hoverable(hoverInteraction), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), border = BorderStroke( 1.dp, if (isHovered && enabled) contentColor.copy(alpha = 0.3f) @@ -479,6 +495,7 @@ private fun PatchSourcesSection( mono: androidx.compose.ui.text.font.FontFamily, borderColor: Color ) { + val corners = LocalMorpheCorners.current Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { SectionLabel("PATCH SOURCES", mono) Spacer(Modifier.height(2.dp)) @@ -499,7 +516,7 @@ private fun PatchSourcesSection( Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.medium)) .border( 1.dp, when { @@ -507,7 +524,7 @@ private fun PatchSourcesSection( isHovered -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) else -> borderColor }, - RoundedCornerShape(2.dp) + RoundedCornerShape(corners.medium) ) .background( if (isActive) MorpheColors.Blue.copy(alpha = 0.05f) @@ -585,7 +602,7 @@ private fun PatchSourcesSection( OutlinedButton( onClick = onAddClick, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), border = BorderStroke(1.dp, borderColor), contentPadding = PaddingValues(horizontal = 14.dp, vertical = 8.dp) ) { @@ -613,7 +630,8 @@ private fun AddPatchSourceDialog( onDismiss: () -> Unit, onAdd: (PatchSource) -> Unit ) { - val mono = JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current var name by remember { mutableStateOf("") } var sourceType by remember { mutableStateOf(PatchSourceType.GITHUB) } var url by remember { mutableStateOf("") } @@ -622,7 +640,7 @@ private fun AddPatchSourceDialog( AlertDialog( onDismissRequest = onDismiss, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.medium), containerColor = MaterialTheme.colorScheme.surface, title = { Text( @@ -644,12 +662,12 @@ private fun AddPatchSourceDialog( val isSelected = sourceType == type Box( modifier = Modifier - .clip(RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.small)) .border( 1.dp, if (isSelected) MorpheColors.Blue.copy(alpha = 0.5f) else MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), - RoundedCornerShape(2.dp) + RoundedCornerShape(corners.small) ) .background( if (isSelected) MorpheColors.Blue.copy(alpha = 0.08f) @@ -683,7 +701,7 @@ private fun AddPatchSourceDialog( singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) when (sourceType) { @@ -692,11 +710,11 @@ private fun AddPatchSourceDialog( value = url, onValueChange = { url = it; error = null }, label = { Text("Repository URL", fontFamily = mono, fontSize = 11.sp) }, - placeholder = { Text("https://github.com/owner/repo", fontFamily = mono, fontSize = 11.sp) }, + placeholder = { Text("github.com/owner/repo or morphe.software link", fontFamily = mono, fontSize = 10.sp) }, singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) } PatchSourceType.LOCAL -> { @@ -712,7 +730,7 @@ private fun AddPatchSourceDialog( singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.weight(1f), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), readOnly = true ) OutlinedButton( @@ -727,7 +745,7 @@ private fun AddPatchSourceDialog( error = null } }, - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { Text( "BROWSE", @@ -758,9 +776,19 @@ private fun AddPatchSourceDialog( if (name.isBlank()) { error = "Name is required"; return@Button } when (sourceType) { PatchSourceType.GITHUB -> { - if (url.isBlank() || !url.contains("github.com/")) { - error = "Enter a valid GitHub repository URL"; return@Button + val trimmedUrl = url.trim() + val resolvedUrl = resolveGitHubUrl(trimmedUrl) + if (resolvedUrl == null) { + error = "Enter a valid GitHub URL or Morphe source link"; return@Button } + onAdd(PatchSource( + id = UUID.randomUUID().toString(), + name = name.trim(), + type = sourceType, + url = resolvedUrl, + deletable = true + )) + return@Button } PatchSourceType.LOCAL -> { if (filePath.isBlank() || !File(filePath).exists()) { @@ -773,13 +801,13 @@ private fun AddPatchSourceDialog( id = UUID.randomUUID().toString(), name = name.trim(), type = sourceType, - url = if (sourceType == PatchSourceType.GITHUB) url.trim() else null, + url = null, filePath = if (sourceType == PatchSourceType.LOCAL) filePath.trim() else null, deletable = true )) }, colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { Text( "ADD", @@ -810,7 +838,8 @@ private fun EditPatchSourceDialog( onDismiss: () -> Unit, onSave: (PatchSource) -> Unit ) { - val mono = JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current var name by remember { mutableStateOf(source.name) } var url by remember { mutableStateOf(source.url ?: "") } var filePath by remember { mutableStateOf(source.filePath ?: "") } @@ -818,7 +847,7 @@ private fun EditPatchSourceDialog( AlertDialog( onDismissRequest = onDismiss, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.medium), containerColor = MaterialTheme.colorScheme.surface, title = { Text( @@ -855,7 +884,7 @@ private fun EditPatchSourceDialog( singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) when (source.type) { @@ -867,7 +896,7 @@ private fun EditPatchSourceDialog( singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) } PatchSourceType.LOCAL -> { @@ -883,7 +912,7 @@ private fun EditPatchSourceDialog( singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.weight(1f), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), readOnly = true ) OutlinedButton( @@ -897,7 +926,7 @@ private fun EditPatchSourceDialog( error = null } }, - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { Text( "BROWSE", @@ -923,9 +952,15 @@ private fun EditPatchSourceDialog( if (name.isBlank()) { error = "Name is required"; return@Button } when (source.type) { PatchSourceType.GITHUB -> { - if (url.isBlank() || !url.contains("github.com/")) { - error = "Enter a valid GitHub repository URL"; return@Button + val resolvedUrl = resolveGitHubUrl(url.trim()) + if (resolvedUrl == null) { + error = "Enter a valid GitHub URL or Morphe source link"; return@Button } + onSave(source.copy( + name = name.trim(), + url = resolvedUrl + )) + return@Button } PatchSourceType.LOCAL -> { if (filePath.isBlank() || !File(filePath).exists()) { @@ -936,12 +971,11 @@ private fun EditPatchSourceDialog( } onSave(source.copy( name = name.trim(), - url = if (source.type == PatchSourceType.GITHUB) url.trim() else source.url, filePath = if (source.type == PatchSourceType.LOCAL) filePath.trim() else source.filePath )) }, colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { Text( "SAVE", @@ -971,10 +1005,40 @@ private fun ThemePreference.toDisplayName(): String { ThemePreference.LIGHT -> "Light" ThemePreference.DARK -> "Dark" ThemePreference.AMOLED -> "AMOLED" + ThemePreference.NORD -> "Nord" + ThemePreference.CATPPUCCIN -> "Catppuccin" + ThemePreference.SAKURA -> "Sakura" + ThemePreference.MATCHA -> "Matcha" ThemePreference.SYSTEM -> "System" } } +private fun ThemePreference.iconSymbol(): String { + return when (this) { + ThemePreference.LIGHT -> "☀" + ThemePreference.DARK -> "☾" + ThemePreference.AMOLED -> "◆" + ThemePreference.NORD -> "❄" + ThemePreference.CATPPUCCIN -> "🐱" + ThemePreference.SAKURA -> "🌸" + ThemePreference.MATCHA -> "🍵" + ThemePreference.SYSTEM -> "⚙" + } +} + +private fun ThemePreference.accentColor(): Color { + return when (this) { + ThemePreference.LIGHT -> MorpheColors.Blue + ThemePreference.DARK -> MorpheColors.Blue + ThemePreference.AMOLED -> MorpheColors.Cyan + ThemePreference.NORD -> Color(0xFF88C0D0) + ThemePreference.CATPPUCCIN -> Color(0xFFCBA6F7) + ThemePreference.SAKURA -> Color(0xFFE8729A) + ThemePreference.MATCHA -> Color(0xFF6DAF5C) + ThemePreference.SYSTEM -> MorpheColors.Blue + } +} + private fun calculateCacheSize(): String { val patchesSize = FileUtils.getPatchesDir().walkTopDown().filter { it.isFile }.sumOf { it.length() } val logsSize = FileUtils.getLogsDir().walkTopDown().filter { it.isFile }.sumOf { it.length() } @@ -1011,3 +1075,42 @@ private fun clearAllCache(): Boolean { false } } + +/** + * Resolves a URL to a GitHub repository URL. + * Supports: + * - Direct GitHub URLs: https://github.com/owner/repo + * - Morphe source links: https://morphe.software/add-source?github=owner/repo + * - Short form: owner/repo (assumed GitHub) + * Returns a normalized https://github.com/owner/repo URL, or null if invalid. + */ +private fun resolveGitHubUrl(input: String): String? { + val trimmed = input.trim() + if (trimmed.isBlank()) return null + + // Morphe source link: morphe.software/add-source?github=owner/repo + if (trimmed.contains("morphe.software/add-source")) { + val match = Regex("[?&]github=([^&]+)").find(trimmed) + val repoPath = match?.groupValues?.get(1) ?: return null + val clean = repoPath.trimEnd('/') + return if (clean.contains('/') && clean.split('/').size == 2) { + "https://github.com/$clean" + } else null + } + + // Direct GitHub URL: https://github.com/owner/repo + if (trimmed.contains("github.com/")) { + // Extract owner/repo from full URL + val match = Regex("github\\.com/([^/]+/[^/]+)").find(trimmed) + return if (match != null) { + "https://github.com/${match.groupValues[1].trimEnd('/')}" + } else null + } + + // Short form: owner/repo + if (trimmed.matches(Regex("[\\w.-]+/[\\w.-]+"))) { + return "https://github.com/$trimmed" + } + + return null +} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 697c039..601b688 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -26,6 +26,8 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -37,6 +39,8 @@ import androidx.compose.ui.platform.LocalUriHandler import app.morphe.morphe_cli.generated.resources.Res import app.morphe.morphe_cli.generated.resources.morphe_dark import app.morphe.morphe_cli.generated.resources.morphe_light +import app.morphe.gui.ui.theme.LocalMorpheCorners +import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.LocalThemeState import app.morphe.gui.ui.theme.ThemePreference import org.jetbrains.compose.resources.painterResource @@ -73,14 +77,11 @@ fun HomeScreenContent( val navigator = LocalNavigator.currentOrThrow val uiState by viewModel.uiState.collectAsState() - // Refresh patches when returning from PatchesScreen (in case user selected a different version) - // Use navigator.items.size as key so this triggers when navigation stack changes (e.g., pop back) val navStackSize = navigator.items.size LaunchedEffect(navStackSize) { viewModel.refreshPatchesIfNeeded() } - // Show error snackbar val snackbarHostState = remember { SnackbarHostState() } LaunchedEffect(uiState.error) { uiState.error?.let { error -> @@ -102,8 +103,8 @@ fun HomeScreenContent( BoxWithConstraints( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) ) { + val useSplitLayout = maxWidth >= 720.dp val isCompact = maxWidth < 500.dp val isSmall = maxHeight < 600.dp val padding = if (isCompact) 16.dp else 24.dp @@ -111,7 +112,6 @@ fun HomeScreenContent( // Version warning dialog state var showVersionWarningDialog by remember { mutableStateOf(false) } - // Version warning dialog if (showVersionWarningDialog && uiState.apkInfo != null) { VersionWarningDialog( versionStatus = uiState.apkInfo!!.versionStatus, @@ -134,38 +134,60 @@ fun HomeScreenContent( ) } - val scrollState = rememberScrollState() + val useHorizontalHeader = maxWidth >= 600.dp + val patchesLoaded = !uiState.isLoadingPatches && viewModel.getCachedPatchesFile() != null + val onChangePatchesClick: () -> Unit = { + navigator.push(PatchesScreen( + apkPath = uiState.apkInfo?.filePath ?: "", + apkName = uiState.apkInfo?.appName ?: "Select APK first" + )) + } + val onRetry: () -> Unit = { viewModel.retryLoadPatches() } + val onClearClick: () -> Unit = { viewModel.clearSelection() } + val onChangeClick: () -> Unit = { + openFilePicker()?.let { file -> + viewModel.onFileSelected(file) + } + } + val onContinueClick: () -> Unit = { + handleContinue(uiState, viewModel, navigator) { + showVersionWarningDialog = true + } + } Box(modifier = Modifier.fillMaxSize()) { - // SpaceBetween + fillMaxSize pushes supported apps to the bottom - // when there's room; verticalScroll kicks in when content overflows. + val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() - .verticalScroll(scrollState) - .padding(padding), - verticalArrangement = Arrangement.SpaceBetween, + .verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally ) { - // Top group: branding + patches version + middle content - Column(horizontalAlignment = Alignment.CenterHorizontally) { + // ── Header ── + if (useHorizontalHeader) { + HeaderBar( + uiState = uiState, + isSmall = isSmall, + padding = padding, + onChangePatchesClick = onChangePatchesClick, + onRetry = onRetry + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.08f)) + ) + } else { Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) BrandingSection(isCompact = isCompact) - // Patches version selector card - right under logo if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) PatchesVersionCard( patchesVersion = uiState.patchesVersion!!, isLatest = uiState.isUsingLatestPatches, - onChangePatchesClick = { - // Navigate to patches version selection screen - // Pass empty apk info since user hasn't selected an APK yet - navigator.push(PatchesScreen( - apkPath = uiState.apkInfo?.filePath ?: "", - apkName = uiState.apkInfo?.appName ?: "Select APK first" - )) - }, + onChangePatchesClick = onChangePatchesClick, isCompact = isCompact, modifier = Modifier .widthIn(max = 400.dp) @@ -173,77 +195,45 @@ fun HomeScreenContent( ) } else if (uiState.isLoadingPatches) { Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(14.dp), - strokeWidth = 2.dp, - color = MorpheColors.Blue - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Loading patches…", - fontSize = 11.sp, - fontFamily = app.morphe.gui.ui.theme.JetBrainsMono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) - } + PatchesLoadingIndicator() } - // Offline banner if (uiState.isOffline && !uiState.isLoadingPatches) { Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) OfflineBanner( - onRetry = { viewModel.retryLoadPatches() }, + onRetry = onRetry, modifier = Modifier .widthIn(max = 400.dp) .padding(horizontal = if (isCompact) 8.dp else 16.dp) ) } + } - Spacer(modifier = Modifier.height(if (isSmall) 16.dp else 32.dp)) - + // ── Main workspace area ── + Box( + modifier = Modifier + .fillMaxWidth() + .padding(padding), + contentAlignment = Alignment.Center + ) { MiddleContent( uiState = uiState, isCompact = isCompact, - patchesLoaded = !uiState.isLoadingPatches && viewModel.getCachedPatchesFile() != null, - onClearClick = { viewModel.clearSelection() }, - onChangeClick = { - openFilePicker()?.let { file -> - viewModel.onFileSelected(file) - } - }, - onContinueClick = { - val patchesFile = viewModel.getCachedPatchesFile() - if (patchesFile == null) { - // Patches not ready yet - return@MiddleContent - } - - val versionStatus = uiState.apkInfo?.versionStatus - if (versionStatus != null && versionStatus != VersionStatus.EXACT_MATCH && versionStatus != VersionStatus.UNKNOWN) { - showVersionWarningDialog = true - } else { - uiState.apkInfo?.let { info -> - navigator.push(PatchSelectionScreen( - apkPath = info.filePath, - apkName = info.appName, - patchesFilePath = patchesFile.absolutePath, - packageName = info.packageName, - apkArchitectures = info.architectures - )) - } - } - } + patchesLoaded = patchesLoaded, + onClearClick = onClearClick, + onChangeClick = onChangeClick, + onContinueClick = onContinueClick ) } - // Bottom group: supported apps section + // ── Supported apps ── Column( horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(top = if (isSmall) 16.dp else 24.dp) + modifier = Modifier.padding( + start = padding, + end = padding, + bottom = if (isSmall) 8.dp else 16.dp + ) ) { SupportedAppsSection( isCompact = isCompact, @@ -252,19 +242,20 @@ fun HomeScreenContent( isDefaultSource = uiState.isDefaultSource, supportedApps = uiState.supportedApps, loadError = uiState.patchLoadError, - onRetry = { viewModel.retryLoadPatches() } + onRetry = onRetry ) - Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) } } - // Top bar (device indicator + settings) in top-right corner - TopBarRow( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(padding), - allowCacheClear = true - ) + // Top bar — only floated when not using horizontal header + if (!useHorizontalHeader) { + TopBarRow( + modifier = Modifier + .align(Alignment.TopEnd) + .padding(padding), + allowCacheClear = true + ) + } // Snackbar host SnackbarHost( @@ -281,6 +272,205 @@ fun HomeScreenContent( } } +private fun handleContinue( + uiState: HomeUiState, + viewModel: HomeViewModel, + navigator: cafe.adriel.voyager.navigator.Navigator, + showWarning: () -> Unit +) { + val patchesFile = viewModel.getCachedPatchesFile() ?: return + val versionStatus = uiState.apkInfo?.versionStatus + if (versionStatus != null && versionStatus != VersionStatus.EXACT_MATCH && versionStatus != VersionStatus.UNKNOWN) { + showWarning() + } else { + uiState.apkInfo?.let { info -> + navigator.push(PatchSelectionScreen( + apkPath = info.filePath, + apkName = info.appName, + patchesFilePath = patchesFile.absolutePath, + packageName = info.packageName, + apkArchitectures = info.architectures + )) + } + } +} + +// ════════════════════════════════════════════════════════════════════ +// HEADER BAR — Logo + patches version + status, horizontal +// ════════════════════════════════════════════════════════════════════ + +@Composable +private fun HeaderBar( + uiState: HomeUiState, + isSmall: Boolean, + padding: Dp, + onChangePatchesClick: () -> Unit, + onRetry: () -> Unit +) { + val mono = LocalMorpheFont.current + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = padding, vertical = if (isSmall) 12.dp else 16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Logo — left-aligned, compact + BrandingSection(isCompact = true) + + Spacer(modifier = Modifier.width(16.dp)) + + // Patches version inline + if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { + PatchesVersionInline( + patchesVersion = uiState.patchesVersion!!, + isLatest = uiState.isUsingLatestPatches, + onChangePatchesClick = onChangePatchesClick + ) + } else if (uiState.isLoadingPatches) { + PatchesLoadingIndicator() + } + + // Offline badge + if (uiState.isOffline && !uiState.isLoadingPatches) { + Spacer(modifier = Modifier.width(12.dp)) + OfflineBadge(onRetry = onRetry) + } + + Spacer(modifier = Modifier.weight(1f)) + + // Device indicator + settings — inline in the header + TopBarRow(allowCacheClear = true) + } +} + +/** + * Inline patches version for the header bar — compact, horizontal. + */ +@Composable +private fun PatchesVersionInline( + patchesVersion: String, + isLatest: Boolean, + onChangePatchesClick: () -> Unit +) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + val borderColor by animateColorAsState( + if (isHovered) MorpheColors.Blue.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = tween(200) + ) + + Row( + modifier = Modifier + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surface) + .hoverable(hoverInteraction) + .clickable(onClick = onChangePatchesClick) + .padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "PATCHES", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = patchesVersion, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MorpheColors.Blue + ) + if (isLatest) { + Spacer(modifier = Modifier.width(6.dp)) + Box( + modifier = Modifier + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) + .padding(horizontal = 5.dp, vertical = 1.dp) + ) { + Text( + text = "LATEST", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp + ) + } + } + } +} + +@Composable +private fun PatchesLoadingIndicator() { + val mono = LocalMorpheFont.current + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Loading patches…", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } +} + +@Composable +private fun OfflineBadge(onRetry: () -> Unit) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + val borderColor by animateColorAsState( + if (isHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.error.copy(alpha = 0.2f), + animationSpec = tween(200) + ) + + Row( + modifier = Modifier + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .hoverable(hoverInteraction) + .clickable(onClick = onRetry) + .padding(horizontal = 10.dp, vertical = 5.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(6.dp) + .background(MaterialTheme.colorScheme.error, RoundedCornerShape(1.dp)) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "OFFLINE", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.error, + letterSpacing = 1.sp + ) + } +} + +// ════════════════════════════════════════════════════════════════════ +// MIDDLE CONTENT — Drop zone / APK info / Analyzing +// ════════════════════════════════════════════════════════════════════ + @Composable private fun MiddleContent( uiState: HomeUiState, @@ -314,6 +504,109 @@ private fun MiddleContent( } } +// ════════════════════════════════════════════════════════════════════ +// DROP ZONE — Corner brackets, scanner/targeting aesthetic +// ════════════════════════════════════════════════════════════════════ + +@Composable +private fun DropPromptSection( + isDragHovering: Boolean, + isCompact: Boolean = false, + onBrowseClick: () -> Unit +) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val bracketColor = if (isDragHovering) MorpheColors.Blue.copy(alpha = 0.7f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f) + val bracketLen = if (isCompact) 24f else 32f + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .widthIn(max = 440.dp) + .fillMaxWidth() + ) { + // Drop zone with corner brackets + Box( + modifier = Modifier + .fillMaxWidth() + .aspectRatio(if (isCompact) 1.6f else 1.4f) + .drawBehind { + val strokeWidth = 2f + val len = bracketLen.dp.toPx() + val inset = 0f + + // Top-left corner + drawLine(bracketColor, Offset(inset, inset), Offset(inset + len, inset), strokeWidth) + drawLine(bracketColor, Offset(inset, inset), Offset(inset, inset + len), strokeWidth) + // Top-right corner + drawLine(bracketColor, Offset(size.width - inset, inset), Offset(size.width - inset - len, inset), strokeWidth) + drawLine(bracketColor, Offset(size.width - inset, inset), Offset(size.width - inset, inset + len), strokeWidth) + // Bottom-left corner + drawLine(bracketColor, Offset(inset, size.height - inset), Offset(inset + len, size.height - inset), strokeWidth) + drawLine(bracketColor, Offset(inset, size.height - inset), Offset(inset, size.height - inset - len), strokeWidth) + // Bottom-right corner + drawLine(bracketColor, Offset(size.width - inset, size.height - inset), Offset(size.width - inset - len, size.height - inset), strokeWidth) + drawLine(bracketColor, Offset(size.width - inset, size.height - inset), Offset(size.width - inset, size.height - inset - len), strokeWidth) + }, + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = if (isDragHovering) "RELEASE TO DROP" else "DROP APK HERE", + fontSize = if (isCompact) 16.sp else 20.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = if (isDragHovering) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurface, + letterSpacing = 3.sp + ) + + Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) + + Text( + text = "or", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + + Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) + + OutlinedButton( + onClick = onBrowseClick, + modifier = Modifier.height(if (isCompact) 38.dp else 42.dp), + shape = RoundedCornerShape(corners.small), + border = BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.4f)), + colors = ButtonDefaults.outlinedButtonColors(contentColor = MorpheColors.Blue) + ) { + Text( + "BROWSE FILES", + fontSize = 11.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + letterSpacing = 1.5.sp + ) + } + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = ".apk · .apkm", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f), + letterSpacing = 0.5.sp + ) + } + } + } +} + +// ════════════════════════════════════════════════════════════════════ +// APK SELECTED — Info card + action buttons +// ════════════════════════════════════════════════════════════════════ + @Composable private fun ApkSelectedSection( patchesLoaded: Boolean, @@ -323,7 +616,8 @@ private fun ApkSelectedSection( onChangeClick: () -> Unit, onContinueClick: () -> Unit ) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val showWarning = apkInfo.versionStatus != VersionStatus.EXACT_MATCH && apkInfo.versionStatus != VersionStatus.UNKNOWN val warningColor = when (apkInfo.versionStatus) { @@ -350,21 +644,19 @@ private fun ApkSelectedSection( verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxWidth() ) { - // Primary action Button( onClick = onContinueClick, enabled = patchesLoaded, modifier = Modifier.fillMaxWidth().height(44.dp), colors = ButtonDefaults.buttonColors(containerColor = primaryColor), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { ActionButtonContent(patchesLoaded, showWarning, mono) } - // Secondary action OutlinedButton( onClick = onChangeClick, modifier = Modifier.fillMaxWidth().height(44.dp), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.onSurfaceVariant @@ -384,7 +676,7 @@ private fun ApkSelectedSection( OutlinedButton( onClick = onChangeClick, modifier = Modifier.height(44.dp), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)), colors = ButtonDefaults.outlinedButtonColors( contentColor = MaterialTheme.colorScheme.onSurfaceVariant @@ -403,7 +695,7 @@ private fun ApkSelectedSection( enabled = patchesLoaded, modifier = Modifier.widthIn(min = 160.dp).height(44.dp), colors = ButtonDefaults.buttonColors(containerColor = primaryColor), - shape = RoundedCornerShape(2.dp) + shape = RoundedCornerShape(corners.small) ) { ActionButtonContent(patchesLoaded, showWarning, mono) } @@ -451,166 +743,13 @@ private fun ActionButtonContent( } } -@Composable -private fun VersionWarningDialog( - versionStatus: VersionStatus, - currentVersion: String, - suggestedVersion: String, - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono - val warnColor = if (versionStatus == VersionStatus.NEWER_VERSION) - MaterialTheme.colorScheme.error else Color(0xFFFF9800) - - val (title, message) = when (versionStatus) { - VersionStatus.NEWER_VERSION -> Pair( - "VERSION MISMATCH", - "Current: v$currentVersion\nExpected: v$suggestedVersion\n\nPatching newer versions may cause failures or broken patches." - ) - VersionStatus.OLDER_VERSION -> Pair( - "OUTDATED VERSION", - "Current: v$currentVersion\nLatest patches target: v$suggestedVersion\n\nYou may be missing new features and fixes." - ) - else -> Pair("VERSION NOTICE", "Continue with v$currentVersion?") - } - - AlertDialog( - onDismissRequest = onDismiss, - shape = RoundedCornerShape(2.dp), - containerColor = MaterialTheme.colorScheme.surface, - icon = { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = warnColor, - modifier = Modifier.size(28.dp) - ) - }, - title = { - Text( - text = title, - fontWeight = FontWeight.Bold, - fontFamily = mono, - fontSize = 14.sp, - letterSpacing = 1.sp - ) - }, - text = { - Text( - text = message, - fontFamily = mono, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 18.sp - ) - }, - confirmButton = { - Button( - onClick = onConfirm, - colors = ButtonDefaults.buttonColors(containerColor = warnColor), - shape = RoundedCornerShape(2.dp) - ) { - Text( - "CONTINUE ANYWAY", - fontFamily = mono, - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - letterSpacing = 0.5.sp - ) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text( - "CANCEL", - fontFamily = mono, - fontWeight = FontWeight.SemiBold, - fontSize = 11.sp, - letterSpacing = 0.5.sp - ) - } - } - ) -} - -@Composable -private fun BrandingSection(isCompact: Boolean = false) { - val themeState = LocalThemeState.current - val isDark = when (themeState.current) { - ThemePreference.DARK, ThemePreference.AMOLED -> true - ThemePreference.LIGHT -> false - ThemePreference.SYSTEM -> isSystemInDarkTheme() - } - Image( - painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), - contentDescription = "Morphe Logo", - modifier = Modifier.height(if (isCompact) 48.dp else 60.dp) - ) -} - -@Composable -private fun DropPromptSection( - isDragHovering: Boolean, - isCompact: Boolean = false, - onBrowseClick: () -> Unit -) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.padding(horizontal = if (isCompact) 16.dp else 32.dp) - ) { - Text( - text = if (isDragHovering) "Release to drop" else "Drop your APK here", - fontSize = if (isCompact) 18.sp else 22.sp, - fontWeight = FontWeight.Bold, - color = if (isDragHovering) MorpheColors.Blue - else MaterialTheme.colorScheme.onSurface, - textAlign = TextAlign.Center - ) - - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) - - Text( - text = "or", - fontSize = 12.sp, - fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) - ) - - Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 12.dp)) - - OutlinedButton( - onClick = onBrowseClick, - modifier = Modifier.height(if (isCompact) 40.dp else 44.dp), - shape = RoundedCornerShape(2.dp), - border = BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.4f)), - colors = ButtonDefaults.outlinedButtonColors(contentColor = MorpheColors.Blue) - ) { - Text( - "BROWSE FILES", - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - fontFamily = mono, - letterSpacing = 1.sp - ) - } - - Spacer(modifier = Modifier.height(if (isCompact) 12.dp else 16.dp)) - - Text( - text = ".apk · .apkm", - fontSize = 11.sp, - fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f) - ) - } -} +// ════════════════════════════════════════════════════════════════════ +// ANALYZING STATE +// ════════════════════════════════════════════════════════════════════ @Composable private fun AnalyzingSection(isCompact: Boolean = false) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono + val mono = LocalMorpheFont.current Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -644,6 +783,13 @@ private fun AnalyzingSection(isCompact: Boolean = false) { } } +// ════════════════════════════════════════════════════════════════════ +// SUPPORTED APPS — Bottom section, horizontal scrolling cards +// ════════════════════════════════════════════════════════════════════ + +/** + * Bottom section — horizontal scrolling cards. + */ @Composable private fun SupportedAppsSection( isCompact: Boolean = false, @@ -654,14 +800,14 @@ private fun SupportedAppsSection( loadError: String? = null, onRetry: () -> Unit = {} ) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val useVerticalLayout = maxWidth < 400.dp Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth() ) { - // Section header — monospace, tracked, accent-colored Text( text = "SUPPORTED APPS", fontSize = if (isCompact) 10.sp else 11.sp, @@ -732,7 +878,7 @@ private fun SupportedAppsSection( Spacer(modifier = Modifier.height(12.dp)) OutlinedButton( onClick = onRetry, - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), colors = ButtonDefaults.outlinedButtonColors( contentColor = MorpheColors.Cyan ), @@ -802,7 +948,7 @@ private fun SupportedAppsSection( fontFamily = mono, fontSize = 11.sp ), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), modifier = Modifier .widthIn(max = 260.dp), colors = OutlinedTextFieldDefaults.colors( @@ -814,12 +960,9 @@ private fun SupportedAppsSection( Spacer(modifier = Modifier.height(12.dp)) } - // Wrap cards in a Box with min height so the section doesn't collapse - // when search yields no results (prevents layout jumping) val cardsMinHeight = if (useVerticalLayout) 120.dp else 80.dp if (filteredApps.isEmpty()) { - // Empty results — hold the space Box( modifier = Modifier .fillMaxWidth() @@ -884,9 +1027,24 @@ private fun SupportedAppsSection( } } -/** - * Patches version indicator — sharp, monospace, clickable. - */ +// ════════════════════════════════════════════════════════════════════ +// SHARED COMPONENTS +// ════════════════════════════════════════════════════════════════════ + +@Composable +private fun BrandingSection(isCompact: Boolean = false) { + val themeState = LocalThemeState.current + val isDark = when (themeState.current) { + ThemePreference.SYSTEM -> isSystemInDarkTheme() + else -> themeState.current.isDark() + } + Image( + painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), + contentDescription = "Morphe Logo", + modifier = Modifier.height(if (isCompact) 36.dp else 60.dp) + ) +} + @Composable private fun PatchesVersionCard( patchesVersion: String, @@ -895,7 +1053,8 @@ private fun PatchesVersionCard( isCompact: Boolean = false, modifier: Modifier = Modifier ) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val hoverInteraction = remember { MutableInteractionSource() } val isHovered by hoverInteraction.collectIsHoveredAsState() val borderColor by animateColorAsState( @@ -907,8 +1066,8 @@ private fun PatchesVersionCard( Box( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(2.dp)) - .border(1.dp, borderColor, RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) .background(MaterialTheme.colorScheme.surface) .hoverable(hoverInteraction) .clickable(onClick = onChangePatchesClick) @@ -940,8 +1099,8 @@ private fun PatchesVersionCard( Spacer(modifier = Modifier.width(8.dp)) Box( modifier = Modifier - .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(2.dp)) - .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(2.dp)) + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) .padding(horizontal = 6.dp, vertical = 2.dp) ) { Text( @@ -958,8 +1117,92 @@ private fun PatchesVersionCard( } } +@Composable +private fun VersionWarningDialog( + versionStatus: VersionStatus, + currentVersion: String, + suggestedVersion: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val warnColor = if (versionStatus == VersionStatus.NEWER_VERSION) + MaterialTheme.colorScheme.error else Color(0xFFFF9800) + + val (title, message) = when (versionStatus) { + VersionStatus.NEWER_VERSION -> Pair( + "VERSION MISMATCH", + "Current: v$currentVersion\nExpected: v$suggestedVersion\n\nPatching newer versions may cause failures or broken patches." + ) + VersionStatus.OLDER_VERSION -> Pair( + "OUTDATED VERSION", + "Current: v$currentVersion\nLatest patches target: v$suggestedVersion\n\nYou may be missing new features and fixes." + ) + else -> Pair("VERSION NOTICE", "Continue with v$currentVersion?") + } + + AlertDialog( + onDismissRequest = onDismiss, + shape = RoundedCornerShape(corners.medium), + containerColor = MaterialTheme.colorScheme.surface, + icon = { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = warnColor, + modifier = Modifier.size(28.dp) + ) + }, + title = { + Text( + text = title, + fontWeight = FontWeight.Bold, + fontFamily = mono, + fontSize = 14.sp, + letterSpacing = 1.sp + ) + }, + text = { + Text( + text = message, + fontFamily = mono, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 18.sp + ) + }, + confirmButton = { + Button( + onClick = onConfirm, + colors = ButtonDefaults.buttonColors(containerColor = warnColor), + shape = RoundedCornerShape(corners.small) + ) { + Text( + "CONTINUE ANYWAY", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text( + "CANCEL", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 0.5.sp + ) + } + } + ) +} + /** - * Redesigned supported app card — sharp, technical, cyberdeck aesthetic. + * Supported app card — sharp, technical, cyberdeck aesthetic. */ @Composable private fun SupportedAppCardDynamic( @@ -969,22 +1212,23 @@ private fun SupportedAppCardDynamic( showPackageName: Boolean = false, modifier: Modifier = Modifier ) { - val mono = app.morphe.gui.ui.theme.JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current var showAllVersions by remember { mutableStateOf(false) } val downloadUrl = supportedApp.apkDownloadUrl val hoverInteraction = remember { MutableInteractionSource() } val isHovered by hoverInteraction.collectIsHoveredAsState() - val borderColor by androidx.compose.animation.animateColorAsState( + val borderColor by animateColorAsState( if (isHovered) MorpheColors.Cyan.copy(alpha = 0.4f) else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), - animationSpec = androidx.compose.animation.core.tween(200) + animationSpec = tween(200) ) Box( modifier = modifier - .clip(RoundedCornerShape(2.dp)) - .border(1.dp, borderColor, RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) .background(MaterialTheme.colorScheme.surface) .hoverable(hoverInteraction) ) { @@ -994,7 +1238,6 @@ private fun SupportedAppCardDynamic( .padding(if (isCompact) 12.dp else 14.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // App name — bold, clean Text( text = supportedApp.displayName, fontSize = if (isCompact) 13.sp else 14.sp, @@ -1004,7 +1247,6 @@ private fun SupportedAppCardDynamic( overflow = TextOverflow.Ellipsis ) - // Package name (always shown as monospace technical data) Text( text = supportedApp.packageName, fontSize = 9.sp, @@ -1018,18 +1260,16 @@ private fun SupportedAppCardDynamic( Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 10.dp)) - // Version block if (supportedApp.recommendedVersion != null) { - // Recommended version — monospace, accent-colored Column( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.medium)) .background(MorpheColors.Teal.copy(alpha = 0.06f)) .border( 1.dp, MorpheColors.Teal.copy(alpha = 0.15f), - RoundedCornerShape(2.dp) + RoundedCornerShape(corners.medium) ) .clickable { showAllVersions = !showAllVersions } .padding(horizontal = 10.dp, vertical = 8.dp), @@ -1063,15 +1303,14 @@ private fun SupportedAppCardDynamic( } } - // Expandable other versions — shown as proper tags val otherVersions = supportedApp.supportedVersions.filter { it != supportedApp.recommendedVersion } if (showAllVersions && otherVersions.isNotEmpty()) { Spacer(modifier = Modifier.height(6.dp)) Column( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(2.dp)) - .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.medium)) .padding(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp), horizontalAlignment = Alignment.CenterHorizontally @@ -1084,7 +1323,6 @@ private fun SupportedAppCardDynamic( color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f), letterSpacing = 1.sp ) - // Version tags in a flow-like layout @OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class) FlowRow( horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), @@ -1097,7 +1335,7 @@ private fun SupportedAppCardDynamic( .border( 1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.12f), - RoundedCornerShape(2.dp) + RoundedCornerShape(corners.small) ) .padding(horizontal = 6.dp, vertical = 2.dp) ) { @@ -1114,11 +1352,10 @@ private fun SupportedAppCardDynamic( } } } else { - // Any version — muted Box( modifier = Modifier .fillMaxWidth() - .clip(RoundedCornerShape(2.dp)) + .clip(RoundedCornerShape(corners.medium)) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) .padding(horizontal = 10.dp, vertical = 8.dp), contentAlignment = Alignment.Center @@ -1134,7 +1371,6 @@ private fun SupportedAppCardDynamic( } } - // Download button if (showDownloadButton && downloadUrl != null) { Spacer(modifier = Modifier.height(if (isCompact) 8.dp else 10.dp)) val uriHandler = LocalUriHandler.current @@ -1145,7 +1381,7 @@ private fun SupportedAppCardDynamic( } }, modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(2.dp), + shape = RoundedCornerShape(corners.small), contentPadding = PaddingValues(horizontal = 8.dp, vertical = 6.dp), colors = ButtonDefaults.outlinedButtonColors( contentColor = MorpheColors.Cyan @@ -1168,27 +1404,55 @@ private fun SupportedAppCardDynamic( } } +// ════════════════════════════════════════════════════════════════════ +// DRAG OVERLAY +// ════════════════════════════════════════════════════════════════════ + @Composable private fun DragOverlay() { - val mono = app.morphe.gui.ui.theme.JetBrainsMono + val mono = LocalMorpheFont.current + val bracketColor = MorpheColors.Blue.copy(alpha = 0.6f) Box( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background.copy(alpha = 0.85f)) - .border(2.dp, MorpheColors.Blue.copy(alpha = 0.4f)), + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.9f)) + .drawBehind { + val strokeWidth = 3f + val len = 48.dp.toPx() + val inset = 24.dp.toPx() + + // Top-left + drawLine(bracketColor, Offset(inset, inset), Offset(inset + len, inset), strokeWidth) + drawLine(bracketColor, Offset(inset, inset), Offset(inset, inset + len), strokeWidth) + // Top-right + drawLine(bracketColor, Offset(size.width - inset, inset), Offset(size.width - inset - len, inset), strokeWidth) + drawLine(bracketColor, Offset(size.width - inset, inset), Offset(size.width - inset, inset + len), strokeWidth) + // Bottom-left + drawLine(bracketColor, Offset(inset, size.height - inset), Offset(inset + len, size.height - inset), strokeWidth) + drawLine(bracketColor, Offset(inset, size.height - inset), Offset(inset, size.height - inset - len), strokeWidth) + // Bottom-right + drawLine(bracketColor, Offset(size.width - inset, size.height - inset), Offset(size.width - inset - len, size.height - inset), strokeWidth) + drawLine(bracketColor, Offset(size.width - inset, size.height - inset), Offset(size.width - inset, size.height - inset - len), strokeWidth) + }, contentAlignment = Alignment.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( text = "DROP APK", - fontSize = 20.sp, + fontSize = 24.sp, fontWeight = FontWeight.Bold, fontFamily = mono, color = MorpheColors.Blue, - letterSpacing = 4.sp + letterSpacing = 6.sp + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = ".apk · .apkm", + fontSize = 11.sp, + fontFamily = mono, + color = MorpheColors.Blue.copy(alpha = 0.4f), + letterSpacing = 1.sp ) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index 2f6e3b8..46eaab2 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -25,7 +25,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.morphe.gui.ui.screens.home.ApkInfo import app.morphe.gui.ui.screens.home.VersionStatus -import app.morphe.gui.ui.theme.JetBrainsMono +import app.morphe.gui.ui.theme.LocalMorpheFont +import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.ChecksumStatus @@ -35,9 +36,10 @@ fun ApkInfoCard( onClearClick: () -> Unit, modifier: Modifier = Modifier ) { - val mono = JetBrainsMono + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val accentColor = statusAccentColor(apkInfo) - val cardShape = RoundedCornerShape(2.dp) + val cardShape = RoundedCornerShape(corners.medium) val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f) Box( @@ -72,7 +74,7 @@ fun ApkInfoCard( Box( modifier = Modifier .size(44.dp) - .border(1.dp, accentColor.copy(alpha = 0.5f), RoundedCornerShape(2.dp)) + .border(1.dp, accentColor.copy(alpha = 0.5f), RoundedCornerShape(corners.small)) .background(accentColor.copy(alpha = 0.08f)), contentAlignment = Alignment.Center ) { @@ -127,8 +129,8 @@ fun ApkInfoCard( modifier = Modifier .size(30.dp) .hoverable(closeHover) - .background(closeBg, RoundedCornerShape(2.dp)) - .border(1.dp, closeBorder, RoundedCornerShape(2.dp)) + .background(closeBg, RoundedCornerShape(corners.small)) + .border(1.dp, closeBorder, RoundedCornerShape(corners.small)) ) { Icon( imageVector = Icons.Default.Close, @@ -209,7 +211,7 @@ fun ApkInfoCard( .border( 1.dp, MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f), - RoundedCornerShape(2.dp) + RoundedCornerShape(corners.small) ) .padding(horizontal = 8.dp, vertical = 3.dp) ) { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 548015e..44b632f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -1,6 +1,10 @@ package app.morphe.gui.ui.screens.patches +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource @@ -13,7 +17,6 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material.icons.filled.ArrowDropUp -import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.Refresh import androidx.compose.material3.* @@ -21,7 +24,11 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen @@ -36,14 +43,15 @@ import app.morphe.gui.ui.components.SettingsButton import app.morphe.gui.ui.components.getErrorType import app.morphe.gui.ui.components.getFriendlyErrorMessage import app.morphe.gui.ui.components.OfflineBanner +import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors +import app.morphe.gui.ui.theme.LocalMorpheFont import java.awt.FileDialog import java.awt.Frame import java.io.File /** * Screen for selecting patch version to apply. - * This is the screen that selects the patches.mpp file */ data class PatchesScreen( val apkPath: String, @@ -57,11 +65,12 @@ data class PatchesScreen( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PatchesScreenContent(viewModel: PatchesViewModel) { + val corners = LocalMorpheCorners.current val navigator = LocalNavigator.currentOrThrow val uiState by viewModel.uiState.collectAsState() + val mono = LocalMorpheFont.current var showErrorDialog by remember { mutableStateOf(false) } var currentError by remember { mutableStateOf(null) } @@ -73,7 +82,6 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { } } - // Error dialog if (showErrorDialog && currentError != null) { ErrorDialog( title = "Error", @@ -91,66 +99,127 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { ) } - Scaffold( - topBar = { - TopAppBar( - title = { - Column { - Text("Select Patches", fontWeight = FontWeight.SemiBold) - Text( - text = viewModel.getApkName(), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - navigationIcon = { - IconButton(onClick = { navigator.pop() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - actions = { - DeviceIndicator() - IconButton( - onClick = { viewModel.loadReleases() }, - enabled = !uiState.isLoading - ) { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Refresh" - ) - } - SettingsButton(allowCacheClear = true) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + val dividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.08f) + + Column( + modifier = Modifier + .fillMaxSize() + ) { + // ── Header bar ── + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Back button + val backHover = remember { MutableInteractionSource() } + val isBackHovered by backHover.collectIsHoveredAsState() + val backBorder by animateColorAsState( + if (isBackHovered) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = tween(150) + ) + + IconButton( + onClick = { navigator.pop() }, + modifier = Modifier + .size(34.dp) + .hoverable(backHover) + .border(1.dp, backBorder, RoundedCornerShape(corners.small)) + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) ) + } + + Spacer(modifier = Modifier.width(14.dp)) + + // Title block + Column(modifier = Modifier.weight(1f)) { + Text( + text = "SELECT PATCHES", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 1.5.sp + ) + if (viewModel.getApkName().isNotBlank()) { + Text( + text = viewModel.getApkName(), + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + letterSpacing = 0.3.sp + ) + } + } + + // Actions + val refreshHover = remember { MutableInteractionSource() } + val isRefreshHovered by refreshHover.collectIsHoveredAsState() + val refreshBorder by animateColorAsState( + if (isRefreshHovered) MorpheColors.Blue.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = tween(150) ) - }, - ) { paddingValues -> - Column( + + if (!uiState.isLocalSource) { + IconButton( + onClick = { viewModel.loadReleases() }, + enabled = !uiState.isLoading, + modifier = Modifier + .size(34.dp) + .hoverable(refreshHover) + .border(1.dp, refreshBorder, RoundedCornerShape(corners.small)) + ) { + Icon( + imageVector = Icons.Default.Refresh, + contentDescription = "Refresh", + tint = if (uiState.isLoading) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } + Spacer(modifier = Modifier.width(6.dp)) + } + + DeviceIndicator() + Spacer(modifier = Modifier.width(6.dp)) + SettingsButton(allowCacheClear = true) + } + + // Divider + Box( modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - // Local source: show simple file info, no release list + .fillMaxWidth() + .height(1.dp) + .background(dividerColor) + ) + + // ── Content area ── + Column(modifier = Modifier.fillMaxSize()) { + // Local source banner if (uiState.isLocalSource) { LocalSourceBanner( patchFile = uiState.downloadedPatchFile, modifier = Modifier.padding(16.dp) ) } else { - // Channel selector (hidden when offline) + // Channel selector if (!uiState.isOffline) { ChannelSelector( selectedChannel = uiState.selectedChannel, onChannelSelected = { viewModel.setChannel(it) }, stableCount = uiState.stableReleases.size, devCount = uiState.devReleases.size, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp) ) } @@ -165,7 +234,6 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { when { uiState.isLocalSource -> { - // Local source: ready, no release list needed Spacer(modifier = Modifier.weight(1f)) } uiState.isLoading -> { @@ -173,40 +241,56 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - CircularProgressIndicator(color = MorpheColors.Blue) + Column(horizontalAlignment = Alignment.CenterHorizontally) { + CircularProgressIndicator( + modifier = Modifier.size(28.dp), + color = MorpheColors.Blue, + strokeWidth = 2.dp + ) + Spacer(modifier = Modifier.height(14.dp)) Text( - text = "Fetching releases...", - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "FETCHING RELEASES", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 2.sp ) } } } - uiState.currentReleases.isEmpty() && !uiState.isLoading -> { Box( modifier = Modifier.weight(1f).fillMaxWidth(), contentAlignment = Alignment.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { Text( - text = "No releases found", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "NO RELEASES FOUND", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 1.5.sp ) - OutlinedButton(onClick = { viewModel.loadReleases() }) { - Text("Retry") + Spacer(modifier = Modifier.height(12.dp)) + OutlinedButton( + onClick = { viewModel.loadReleases() }, + shape = RoundedCornerShape(corners.small), + border = BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.4f)), + colors = ButtonDefaults.outlinedButtonColors(contentColor = MorpheColors.Blue) + ) { + Text( + "RETRY", + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + fontSize = 11.sp, + letterSpacing = 1.sp + ) } } } } - else -> { // Releases list LazyColumn( @@ -214,7 +298,7 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { .weight(1f) .fillMaxWidth(), contentPadding = PaddingValues(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(6.dp) ) { items( items = uiState.currentReleases, @@ -235,9 +319,7 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { uiState = uiState, onDownloadClick = { viewModel.downloadPatches() }, onSelectClick = { - // Save the selected version to config before navigating back viewModel.confirmSelection() - // Go back to HomeScreen - the new patches file is now cached navigator.pop() }, onExportJsonClick = { @@ -258,6 +340,10 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { } } +// ═══════════════════════════════════════════════════════════════════ +// CHANNEL SELECTOR +// ═══════════════════════════════════════════════════════════════════ + @Composable private fun ChannelSelector( selectedChannel: ReleaseChannel, @@ -266,22 +352,26 @@ private fun ChannelSelector( devCount: Int, modifier: Modifier = Modifier ) { + val mono = LocalMorpheFont.current + Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { ChannelChip( - label = "Stable", + label = "STABLE", count = stableCount, isSelected = selectedChannel == ReleaseChannel.STABLE, onClick = { onChannelSelected(ReleaseChannel.STABLE) }, + accentColor = MorpheColors.Blue, modifier = Modifier.weight(1f) ) ChannelChip( - label = "Dev", + label = "DEV", count = devCount, isSelected = selectedChannel == ReleaseChannel.DEV, onClick = { onChannelSelected(ReleaseChannel.DEV) }, + accentColor = MorpheColors.Teal, modifier = Modifier.weight(1f) ) } @@ -293,50 +383,74 @@ private fun ChannelChip( count: Int, isSelected: Boolean, onClick: () -> Unit, + accentColor: Color, modifier: Modifier = Modifier ) { - val backgroundColor = if (isSelected) { - MorpheColors.Blue.copy(alpha = 0.15f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - } - - val borderColor = if (isSelected) { - MorpheColors.Blue - } else { - MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - } + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + + val borderColor by animateColorAsState( + when { + isSelected -> accentColor.copy(alpha = 0.5f) + isHovered -> accentColor.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) + }, + animationSpec = tween(150) + ) + val bgColor = if (isSelected) accentColor.copy(alpha = 0.08f) else Color.Transparent - Surface( + Box( modifier = modifier - .clip(RoundedCornerShape(12.dp)) - .clickable(onClick = onClick), - color = backgroundColor, - shape = RoundedCornerShape(12.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, borderColor) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .background(bgColor) + .hoverable(hoverInteraction) + .clickable(onClick = onClick) ) { Row( - modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 14.dp, vertical = 10.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { + // Selection dot + if (isSelected) { + Box( + modifier = Modifier + .size(6.dp) + .background(accentColor, RoundedCornerShape(1.dp)) + ) + Spacer(modifier = Modifier.width(8.dp)) + } Text( text = label, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, - color = if (isSelected) MorpheColors.Blue else MaterialTheme.colorScheme.onSurface + fontSize = 11.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + fontFamily = mono, + color = if (isSelected) accentColor else MaterialTheme.colorScheme.onSurface, + letterSpacing = 1.5.sp ) if (count > 0) { Spacer(modifier = Modifier.width(8.dp)) Text( - text = "($count)", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "$count", + fontSize = 10.sp, + fontFamily = mono, + color = if (isSelected) accentColor.copy(alpha = 0.6f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) } } } } +// ════════════════════════════════════════════════════════════════════ +// RELEASE CARD +// ════════════════════════════════════════════════════════════════════ + @Composable private fun ReleaseCard( release: Release, @@ -345,156 +459,203 @@ private fun ReleaseCard( isOffline: Boolean = false, onClick: () -> Unit ) { - val titleColor = MaterialTheme.colorScheme.onSurface - val subtitleColor = MaterialTheme.colorScheme.onSurfaceVariant - val dateColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - val accentColor = if (isSelected && isDownloaded) MorpheColors.Teal else MorpheColors.Blue - val devBadgeColor = MorpheColors.Teal + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val accentColor = when { + isSelected && isDownloaded -> MorpheColors.Teal + isSelected -> MorpheColors.Blue + isDownloaded -> MorpheColors.Teal + else -> MaterialTheme.colorScheme.onSurfaceVariant + } var isExpanded by remember { mutableStateOf(false) } val hasNotes = !release.body.isNullOrBlank() val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() - val cardBackground = when { - isSelected && isDownloaded -> MorpheColors.Teal.copy(alpha = if (isHovered) 0.22f else 0.15f) - isSelected -> MorpheColors.Blue.copy(alpha = if (isHovered) 0.22f else 0.15f) - else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (isHovered) 0.7f else 0.25f) + + val borderColor by animateColorAsState( + when { + isSelected -> accentColor.copy(alpha = 0.5f) + isHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f) + isDownloaded -> MorpheColors.Teal.copy(alpha = 0.2f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.08f) + }, + animationSpec = tween(150) + ) + + val bgColor = when { + isSelected -> accentColor.copy(alpha = 0.06f) + else -> MaterialTheme.colorScheme.surface } - Card( + Box( modifier = Modifier .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) + .background(bgColor) .hoverable(interactionSource) - .clickable(interactionSource = interactionSource, indication = null) { onClick() }, - colors = CardDefaults.cardColors(containerColor = cardBackground), - shape = RoundedCornerShape(12.dp) + .clickable(interactionSource = interactionSource, indication = null) { onClick() } ) { Row(modifier = Modifier.fillMaxWidth().height(IntrinsicSize.Min)) { - // Green ribbon for downloaded (non-selected) cards - if (isDownloaded && !isSelected) { + // Left accent stripe + if (isSelected || isDownloaded) { Box( modifier = Modifier - .width(4.dp) + .width(3.dp) .fillMaxHeight() - .background( - MorpheColors.Teal, - RoundedCornerShape(topStart = 12.dp, bottomStart = 12.dp) - ) + .background(accentColor) ) } Column(modifier = Modifier.weight(1f)) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text( - text = release.tagName, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = titleColor - ) - if (release.isDevRelease()) { - Surface( - color = devBadgeColor.copy(alpha = 0.2f), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = "DEV", - fontSize = 10.sp, - fontWeight = FontWeight.Bold, - color = devBadgeColor, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = release.tagName, + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = if (isSelected) accentColor else MaterialTheme.colorScheme.onSurface + ) + if (release.isDevRelease()) { + Box( + modifier = Modifier + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) + .padding(horizontal = 5.dp, vertical = 1.dp) + ) { + Text( + text = "DEV", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp + ) + } + } + if (isDownloaded) { + Box( + modifier = Modifier + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) + .padding(horizontal = 5.dp, vertical = 1.dp) + ) { + Text( + text = "CACHED", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp + ) + } } } - } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) - // Show patch file info if available (.mpp or .jar) - release.assets.find { it.isPatchFile() }?.let { patchAsset -> - Text( - text = "${patchAsset.name} (${patchAsset.getFormattedSize()})", - fontSize = 13.sp, - color = subtitleColor - ) - } + // Patch file info + release.assets.find { it.isPatchFile() }?.let { patchAsset -> + Text( + text = "${patchAsset.name} (${patchAsset.getFormattedSize()})", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + letterSpacing = 0.3.sp + ) + } - val formattedDate = release.publishedAt?.let { formatDate(it) } ?: "" - if (formattedDate.isNotEmpty()) { - Text( - text = "${if (isOffline) "Cached:" else "Published:"} $formattedDate", - fontSize = 12.sp, - color = dateColor - ) - } + val formattedDate = release.publishedAt?.let { formatDate(it) } ?: "" + if (formattedDate.isNotEmpty()) { + Text( + text = "${if (isOffline) "Cached:" else "Published:"} $formattedDate", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) + ) + } + + if (hasNotes) { + Spacer(modifier = Modifier.height(6.dp)) + val noteHover = remember { MutableInteractionSource() } + val isNoteHovered by noteHover.collectIsHoveredAsState() + val noteBorder by animateColorAsState( + if (isNoteHovered) accentColor.copy(alpha = 0.3f) + else accentColor.copy(alpha = 0.15f), + animationSpec = tween(150) + ) - if (hasNotes) { - Spacer(modifier = Modifier.height(4.dp)) - Surface( - color = accentColor.copy(alpha = 0.1f), - shape = RoundedCornerShape(6.dp), - modifier = Modifier - .clip(RoundedCornerShape(6.dp)) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null - ) { isExpanded = !isExpanded } - ) { Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), + modifier = Modifier + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, noteBorder, RoundedCornerShape(corners.small)) + .hoverable(noteHover) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { isExpanded = !isExpanded } + .padding(horizontal = 8.dp, vertical = 4.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Text( - text = if (isExpanded) "Hide patch notes" else "Patch notes", - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = accentColor + text = if (isExpanded) "HIDE NOTES" else "PATCH NOTES", + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = accentColor, + letterSpacing = 0.5.sp ) Icon( imageVector = if (isExpanded) Icons.Default.ArrowDropUp else Icons.Default.ArrowDropDown, contentDescription = null, tint = accentColor, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(14.dp) ) } } } } - } - - // Expandable release notes - if (isExpanded && hasNotes) { - HorizontalDivider( - color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f) - ) - FormattedReleaseNotes( - markdown = release.body.orEmpty(), - modifier = Modifier.padding(16.dp) - ) - } + // Expandable release notes + if (isExpanded && hasNotes) { + val notesDividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.06f) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(1.dp) + .background(notesDividerColor) + ) + FormattedReleaseNotes( + markdown = release.body.orEmpty(), + modifier = Modifier.padding(16.dp) + ) + } } } } } -/** - * Renders GitHub release notes markdown as formatted Compose text. - */ +// ════════════════════════════════════════════════════════════════════ +// RELEASE NOTES +// ════════════════════════════════════════════════════════════════════ + @Composable private fun FormattedReleaseNotes(markdown: String, modifier: Modifier = Modifier) { + val mono = LocalMorpheFont.current val lines = parseMarkdown(markdown) Column( modifier = modifier, @@ -504,36 +665,42 @@ private fun FormattedReleaseNotes(markdown: String, modifier: Modifier = Modifie when (line) { is MdLine.Header -> Text( text = line.text, - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 0.5.sp ) is MdLine.SubHeader -> Text( text = line.text, - fontSize = 13.sp, + fontSize = 11.sp, fontWeight = FontWeight.SemiBold, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurface ) is MdLine.Bullet -> { Row { Text( - text = "\u2022 ", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "· ", + fontSize = 11.sp, + fontFamily = mono, + color = MorpheColors.Blue.copy(alpha = 0.5f) ) Text( text = line.text, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 18.sp + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f), + lineHeight = 17.sp ) } } is MdLine.Plain -> Text( text = line.text, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 18.sp + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + lineHeight = 17.sp ) } } @@ -563,25 +730,19 @@ private fun parseMarkdown(markdown: String): List { } } -/** - * Strip markdown syntax to plain readable text: - * - **bold** → bold - * - [text](url) → text - * - ([hash](url)) → remove entirely (commit refs) - */ private fun cleanMarkdown(text: String): String { var result = text - // Remove commit refs like ([abc1234](https://...)) result = result.replace(Regex("""\(\[[\da-f]{7,}]\([^)]*\)\)"""), "") - // [text](url) → text result = result.replace(Regex("""\[([^\]]*?)]\([^)]*\)"""), "$1") - // **bold** → bold result = result.replace(Regex("""\*\*(.+?)\*\*"""), "$1") - // Clean up extra whitespace result = result.replace(Regex("""\s+"""), " ").trim() return result } +// ════════════════════════════════════════════════════════════════════ +// BOTTOM ACTION BAR +// ════════════════════════════════════════════════════════════════════ + @Composable private fun BottomActionBar( uiState: PatchesUiState, @@ -589,144 +750,185 @@ private fun BottomActionBar( onSelectClick: () -> Unit, onExportJsonClick: () -> Unit, ) { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp - ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - // Download progress - if (uiState.isDownloading) { - LinearProgressIndicator( - progress = { uiState.downloadProgress }, - modifier = Modifier - .fillMaxWidth() - .height(4.dp) - .clip(RoundedCornerShape(2.dp)), - color = MorpheColors.Blue, - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Downloading patches...", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val dividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.08f) + + Column( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = dividerColor, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1f ) - Spacer(modifier = Modifier.height(12.dp)) } + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) + ) { + // Download progress + if (uiState.isDownloading) { + LinearProgressIndicator( + progress = { uiState.downloadProgress }, + modifier = Modifier + .fillMaxWidth() + .height(3.dp) + .clip(RoundedCornerShape(1.dp)), + color = MorpheColors.Blue, + trackColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f) + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "DOWNLOADING…", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue.copy(alpha = 0.7f), + letterSpacing = 1.5.sp + ) + Spacer(modifier = Modifier.height(12.dp)) + } - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + if (uiState.downloadedPatchFile == null) { // Download button - if (uiState.downloadedPatchFile == null) { - Button( - onClick = onDownloadClick, - enabled = uiState.selectedRelease != null && !uiState.isDownloading, - modifier = Modifier - .weight(1f) - .height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) + Button( + onClick = onDownloadClick, + enabled = uiState.selectedRelease != null && !uiState.isDownloading, + modifier = Modifier + .weight(1f) + .height(44.dp), + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), + shape = RoundedCornerShape(corners.small) + ) { + Text( + text = if (uiState.isDownloading) "DOWNLOADING…" else "DOWNLOAD", + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + fontSize = 12.sp, + letterSpacing = 1.sp + ) + } + } else { + // Select button + Button( + onClick = onSelectClick, + modifier = Modifier + .weight(1f) + .height(44.dp), + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Teal), + shape = RoundedCornerShape(corners.small) + ) { + Text( + text = "SELECT", + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + fontSize = 12.sp, + letterSpacing = 1.sp + ) + } + + // Export JSON + if (uiState.isExporting) { + Box( + modifier = Modifier.height(44.dp).width(44.dp), + contentAlignment = Alignment.Center ) { - Text( - text = if (uiState.isDownloading) "Downloading..." else "Download Patches", - fontWeight = FontWeight.Medium + CircularProgressIndicator( + modifier = Modifier.size(20.dp), + color = MorpheColors.Blue, + strokeWidth = 2.dp ) } } else { - // Select button (patches downloaded) - Button( - onClick = onSelectClick, - modifier = Modifier - .weight(1f) - .height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Teal - ), - shape = RoundedCornerShape(12.dp) + OutlinedButton( + onClick = onExportJsonClick, + modifier = Modifier.height(44.dp), + shape = RoundedCornerShape(corners.small), + border = BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.3f)), + colors = ButtonDefaults.outlinedButtonColors(contentColor = MorpheColors.Blue) ) { Text( - text = "Select", - fontWeight = FontWeight.Medium + text = "EXPORT JSON", + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + fontSize = 11.sp, + letterSpacing = 0.5.sp ) } - - // Export JSON button / spinner - if (uiState.isExporting) { - Box( - modifier = Modifier.height(48.dp).width(48.dp), - contentAlignment = Alignment.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(24.dp), - color = MorpheColors.Blue, - strokeWidth = 2.dp - ) - } - } else { - OutlinedButton( - onClick = onExportJsonClick, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(12.dp), - border = androidx.compose.foundation.BorderStroke( - 1.dp, - MorpheColors.Blue - ), - ) { - Text( - text = "Export JSON", - fontWeight = FontWeight.Medium, - color = MorpheColors.Blue - ) - } - } } } - } } } +// ════════════════════════════════════════════════════════════════════ +// LOCAL SOURCE BANNER +// ════════════════════════════════════════════════════════════════════ + @Composable private fun LocalSourceBanner( patchFile: File?, modifier: Modifier = Modifier ) { - Surface( - color = MorpheColors.Blue.copy(alpha = 0.08f), - shape = RoundedCornerShape(12.dp), - border = androidx.compose.foundation.BorderStroke(1.dp, MorpheColors.Blue.copy(alpha = 0.2f)), - modifier = modifier.fillMaxWidth() + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, MorpheColors.Blue.copy(alpha = 0.2f), RoundedCornerShape(corners.medium)) + .background(MorpheColors.Blue.copy(alpha = 0.04f)) ) { Row( - modifier = Modifier.padding(16.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier + .fillMaxWidth() + .height(IntrinsicSize.Min) ) { - Icon( - imageVector = Icons.Default.FolderOpen, - contentDescription = null, - tint = MorpheColors.Blue, - modifier = Modifier.size(24.dp) + // Left accent stripe + Box( + modifier = Modifier + .width(3.dp) + .fillMaxHeight() + .background(MorpheColors.Blue) ) - Column { - Text( - text = "Local Patch File", - fontSize = 14.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + + Row( + modifier = Modifier.padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + Icon( + imageVector = Icons.Default.FolderOpen, + contentDescription = null, + tint = MorpheColors.Blue, + modifier = Modifier.size(20.dp) ) - if (patchFile != null) { + Column { Text( - text = patchFile.name, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "LOCAL PATCH FILE", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue, + letterSpacing = 1.5.sp ) + if (patchFile != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = patchFile.name, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + letterSpacing = 0.3.sp + ) + } } } } @@ -735,7 +937,6 @@ private fun LocalSourceBanner( private fun formatDate(isoDate: String): String { return try { - // Takes "2024-01-15T10:30:00Z" and returns "Jan 15, 2024 at 10:30 AM" val datePart = isoDate.substringBefore("T") val timePart = isoDate.substringAfter("T").substringBefore("Z").substringBefore("+") val parts = datePart.split("-") diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 228be08..cdd32de 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -127,7 +127,6 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { Box( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) .dragAndDropTarget( shouldStartDragAndDrop = { true }, target = dragAndDropTarget @@ -144,9 +143,8 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { Spacer(modifier = Modifier.height(8.dp)) val themeState = LocalThemeState.current val isDark = when (themeState.current) { - ThemePreference.DARK, ThemePreference.AMOLED -> true - ThemePreference.LIGHT -> false ThemePreference.SYSTEM -> isSystemInDarkTheme() + else -> themeState.current.isDark() } Image( painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt index 0409c25..124331e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt @@ -121,7 +121,6 @@ fun ResultScreenContent(outputPath: String) { Box( modifier = Modifier .fillMaxSize() - .background(MaterialTheme.colorScheme.background) ) { BoxWithConstraints( modifier = Modifier.fillMaxSize() diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt b/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt index 738e65c..f3c3a4e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt +++ b/src/main/kotlin/app/morphe/gui/ui/theme/MorpheTypography.kt @@ -1,6 +1,7 @@ package app.morphe.gui.ui.theme import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.platform.Font @@ -18,3 +19,23 @@ val JetBrainsMono: FontFamily Font(resource = "fonts/JetBrainsMono-SemiBold.ttf", weight = FontWeight.SemiBold), Font(resource = "fonts/JetBrainsMono-Bold.ttf", weight = FontWeight.Bold), ) + +/** + * Nunito — soft, rounded sans-serif for cute themes (Sakura, Matcha). + * Generous x-height, fully rounded terminals, pillowy feel. + */ +val Nunito: FontFamily + @Composable + get() = FontFamily( + Font(resource = "fonts/Nunito-Light.ttf", weight = FontWeight.Light), + Font(resource = "fonts/Nunito-Regular.ttf", weight = FontWeight.Normal), + Font(resource = "fonts/Nunito-Medium.ttf", weight = FontWeight.Medium), + Font(resource = "fonts/Nunito-SemiBold.ttf", weight = FontWeight.SemiBold), + Font(resource = "fonts/Nunito-Bold.ttf", weight = FontWeight.Bold), + ) + +/** + * Theme-aware font provider. Sharp themes get JetBrains Mono, + * soft/cute themes (Sakura, Matcha) get Nunito. + */ +val LocalMorpheFont = compositionLocalOf { FontFamily.Default } diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt index 3109a73..d01c21f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt +++ b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt @@ -1,11 +1,17 @@ package app.morphe.gui.ui.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Shapes import androidx.compose.material3.darkColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp // Morphe Brand Colors object MorpheColors { @@ -19,6 +25,32 @@ object MorpheColors { val TextDark = Color(0xFF1C1C1C) } +// ════════════════════════════════════════════════════════════════════ +// CORNER / SHAPE STYLE SYSTEM +// ════════════════════════════════════════════════════════════════════ + +/** + * Defines the corner radius style for the current theme. + * Sharp themes use 2dp, soft/cute themes use larger radii. + */ +data class MorpheCornerStyle( + val small: Dp = 2.dp, + val medium: Dp = 2.dp, + val large: Dp = 2.dp, +) + +val LocalMorpheCorners = compositionLocalOf { MorpheCornerStyle() } + +/** Sharp corners for cyberdeck/dev themes. */ +private val SharpCorners = MorpheCornerStyle(small = 2.dp, medium = 2.dp, large = 2.dp) + +/** Soft rounded corners for cute/warm themes. */ +private val SoftCorners = MorpheCornerStyle(small = 10.dp, medium = 14.dp, large = 18.dp) + +// ════════════════════════════════════════════════════════════════════ +// COLOR SCHEMES +// ════════════════════════════════════════════════════════════════════ + private val MorpheDarkColorScheme = darkColorScheme( primary = MorpheColors.Blue, secondary = MorpheColors.Teal, @@ -70,13 +102,114 @@ private val MorpheLightColorScheme = lightColorScheme( onError = Color.White ) +// ── Nord ── +// Arctic, cool-toned dark theme inspired by nordtheme.com +private val NordColorScheme = darkColorScheme( + primary = Color(0xFF88C0D0), // Frost + secondary = Color(0xFFA3BE8C), // Aurora Green + tertiary = Color(0xFF81A1C1), // Frost Blue + background = Color(0xFF2E3440), // Polar Night + surface = Color(0xFF3B4252), // Polar Night lighter + surfaceVariant = Color(0xFF434C5E), + onPrimary = Color(0xFF2E3440), + onSecondary = Color(0xFF2E3440), + onTertiary = Color(0xFF2E3440), + onBackground = Color(0xFFECEFF4), // Snow Storm + onSurface = Color(0xFFECEFF4), + onSurfaceVariant = Color(0xFFD8DEE9), + error = Color(0xFFBF616A), // Aurora Red + onError = Color(0xFFECEFF4) +) + +// ── Catppuccin Mocha ── +// Warm, soothing pastel dark theme +private val CatppuccinMochaColorScheme = darkColorScheme( + primary = Color(0xFFCBA6F7), // Mauve + secondary = Color(0xFFF5C2E7), // Pink + tertiary = Color(0xFF89B4FA), // Blue + background = Color(0xFF1E1E2E), // Base + surface = Color(0xFF313244), // Surface0 + surfaceVariant = Color(0xFF45475A), // Surface1 + onPrimary = Color(0xFF1E1E2E), + onSecondary = Color(0xFF1E1E2E), + onTertiary = Color(0xFF1E1E2E), + onBackground = Color(0xFFCDD6F4), // Text + onSurface = Color(0xFFCDD6F4), + onSurfaceVariant = Color(0xFFBAC2DE), // Subtext1 + error = Color(0xFFF38BA8), // Red + onError = Color(0xFF1E1E2E) +) + +// ── Sakura ── +// Soft pink, cute aesthetic — light theme with warm blush tones +private val SakuraColorScheme = lightColorScheme( + primary = Color(0xFFE8729A), // Rose pink + secondary = Color(0xFFC75088), // Deeper rose + tertiary = Color(0xFFF5A0C0), // Soft pink + background = Color(0xFFFFF5F7), // Blush white + surface = Color(0xFFFFE8EE), // Petal + surfaceVariant = Color(0xFFFFD6E0), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color(0xFF5A1A30), + onBackground = Color(0xFF4A2030), // Deep plum + onSurface = Color(0xFF4A2030), + onSurfaceVariant = Color(0xFF8A506A), + error = Color(0xFFD03050), + onError = Color.White +) + +// ── Matcha ── +// Pista green, cute aesthetic — light theme with fresh green tones +private val MatchaColorScheme = lightColorScheme( + primary = Color(0xFF6DAF5C), // Pista green + secondary = Color(0xFF8BC77E), // Fresh green + tertiary = Color(0xFFA3D99B), // Light mint + background = Color(0xFFF4F9F0), // Green-tinted white + surface = Color(0xFFE5F0DC), // Soft green + surfaceVariant = Color(0xFFD4E5C8), + onPrimary = Color.White, + onSecondary = Color(0xFF1E3318), + onTertiary = Color(0xFF1E3318), + onBackground = Color(0xFF1E3318), // Deep forest + onSurface = Color(0xFF1E3318), + onSurfaceVariant = Color(0xFF4A6B3D), + error = Color(0xFFC04040), + onError = Color.White +) + +// ════════════════════════════════════════════════════════════════════ +// THEME PREFERENCE +// ════════════════════════════════════════════════════════════════════ + enum class ThemePreference { LIGHT, DARK, AMOLED, - SYSTEM + NORD, + CATPPUCCIN, + SAKURA, + MATCHA, + SYSTEM; + + /** Whether this theme uses dark color scheme (for resource qualifiers). */ + fun isDark(): Boolean = when (this) { + DARK, AMOLED, NORD, CATPPUCCIN -> true + LIGHT, SAKURA, MATCHA -> false + SYSTEM -> false // caller should check isSystemInDarkTheme() + } + + /** Whether this theme uses soft/rounded corners. */ + fun isSoft(): Boolean = when (this) { + SAKURA, MATCHA -> true + else -> false + } } +// ════════════════════════════════════════════════════════════════════ +// THEME COMPOSABLE +// ════════════════════════════════════════════════════════════════════ + @Composable fun MorpheTheme( themePreference: ThemePreference = ThemePreference.SYSTEM, @@ -86,13 +219,25 @@ fun MorpheTheme( ThemePreference.DARK -> MorpheDarkColorScheme ThemePreference.AMOLED -> MorpheAmoledColorScheme ThemePreference.LIGHT -> MorpheLightColorScheme + ThemePreference.NORD -> NordColorScheme + ThemePreference.CATPPUCCIN -> CatppuccinMochaColorScheme + ThemePreference.SAKURA -> SakuraColorScheme + ThemePreference.MATCHA -> MatchaColorScheme ThemePreference.SYSTEM -> { if (isSystemInDarkTheme()) MorpheDarkColorScheme else MorpheLightColorScheme } } - MaterialTheme( - colorScheme = colorScheme, - content = content - ) + val corners = if (themePreference.isSoft()) SoftCorners else SharpCorners + val font = if (themePreference.isSoft()) Nunito else JetBrainsMono + + CompositionLocalProvider( + LocalMorpheCorners provides corners, + LocalMorpheFont provides font + ) { + MaterialTheme( + colorScheme = colorScheme, + content = content + ) + } } diff --git a/src/main/resources/cat2333s.json b/src/main/resources/cat2333s.json new file mode 100644 index 0000000..add2952 --- /dev/null +++ b/src/main/resources/cat2333s.json @@ -0,0 +1 @@ +{"nm": "cat", "ddd": 0, "h": 1080, "w": 1080, "meta": {"g": "@lottiefiles/toolkit-js 0.25.4"}, "layers": [{"ty": 4, "nm": "Layer 7 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [5.25, 5.25, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [531, 418.5, 0], "t": 25, "ti": [0, -1.125, 0], "to": [0, 1.125, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [531, 425.25, 0], "t": 30, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [531, 425.25, 0], "t": 59, "ti": [-1.375, 1.25, 0], "to": [1.375, -1.25, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [539.25, 417.75, 0], "t": 64, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [539.25, 417.75, 0], "t": 105, "ti": [1.5, 0, 0], "to": [-1.5, 0, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [530.25, 417.75, 0], "t": 110, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [530.25, 417.75, 0], "t": 129, "ti": [0, -1.25, 0], "to": [0, 1.25, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [530.25, 425.25, 0], "t": 135, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [530.25, 425.25, 0], "t": 159, "ti": [-0.125, 1.125, 0], "to": [0.125, -1.125, 0]}, {"s": [531, 418.5, 0], "t": 165.000006720588}], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": true, "i": [[0, -2.761], [2.761, 0], [0, 2.761], [-2.761, 0]], "o": [[0, 2.761], [-2.761, 0], [0, -2.761], [2.761, 0]], "v": [[5, 0], [0, 5], [-5, 0], [0, -5]]}, "ix": 2}}, {"ty": "fl", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Fill", "nm": "Fill 1", "c": {"a": 0, "k": [1, 1, 1], "ix": 4}, "r": 1, "o": {"a": 0, "k": 100, "ix": 5}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [5.25, 5.25], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 1}, {"ty": 4, "nm": "eye 1 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [5.25, 5.25, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [501, 429, 0], "t": 25, "ti": [0.062, -1.312, 0], "to": [-0.062, 1.312, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [500.625, 436.875, 0], "t": 30, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [500.625, 436.875, 0], "t": 59, "ti": [-1.25, 1.375, 0], "to": [1.25, -1.375, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [508.125, 428.625, 0], "t": 64, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [508.125, 428.625, 0], "t": 105, "ti": [1.25, 0, 0], "to": [-1.25, 0, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [500.625, 428.625, 0], "t": 110, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [500.625, 428.625, 0], "t": 129, "ti": [0, -1.25, 0], "to": [0, 1.25, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [500.625, 436.125, 0], "t": 135, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [500.625, 436.125, 0], "t": 159, "ti": [-0.062, 1.188, 0], "to": [0.062, -1.188, 0]}, {"s": [501, 429, 0], "t": 165.000006720588}], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": true, "i": [[0, -2.761], [2.761, 0], [0, 2.761], [-2.761, 0]], "o": [[0, 2.761], [-2.761, 0], [0, -2.761], [2.761, 0]], "v": [[5, 0], [0, 5], [-5, 0], [0, -5]]}, "ix": 2}}, {"ty": "fl", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Fill", "nm": "Fill 1", "c": {"a": 0, "k": [1, 1, 1], "ix": 4}, "r": 1, "o": {"a": 0, "k": 100, "ix": 5}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [5.25, 5.25], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 2}, {"ty": 4, "nm": "Layer 8 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [34.25, 68.25, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 0, "k": [532.5, 486, 0], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": true, "i": [[0, -18.778], [18.777, 0], [0, 18.778], [-18.778, 0]], "o": [[0, 18.778], [-18.778, 0], [0, -18.778], [18.777, 0]], "v": [[34, 0], [0, 34], [-34, 0], [0, -34]]}, "ix": 2}}, {"ty": "fl", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Fill", "nm": "Fill 1", "c": {"a": 0, "k": [0, 0, 0], "ix": 4}, "r": 1, "o": {"a": 0, "k": 100, "ix": 5}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [34.25, 34.25], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 3}, {"ty": 4, "nm": "ear2 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [7.25, 28.369, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [537, 400.678, 0], "t": 25, "ti": [1.625, -0.375, 0], "to": [-1.625, 0.375, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [527.25, 402.928, 0], "t": 30, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [527.25, 402.928, 0], "t": 59, "ti": [-1.531, 0.469, 0], "to": [1.531, -0.469, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [536.437, 400.116, 0], "t": 64, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [536.437, 400.116, 0], "t": 129, "ti": [1, 0, 0], "to": [-1, 0, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [530.437, 400.116, 0], "t": 135, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [530.437, 400.116, 0], "t": 159, "ti": [-1.094, -0.094, 0], "to": [1.094, 0.094, 0]}, {"s": [537, 400.678, 0], "t": 165.000006720588}], "ix": 2}, "r": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [0], "t": 25}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-9], "t": 30}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-9], "t": 59}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-7], "t": 64}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-7], "t": 72}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-3], "t": 74}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-12], "t": 75}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [5], "t": 77}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-12], "t": 81}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-5], "t": 82}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-18], "t": 85}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [5], "t": 88}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-8], "t": 91}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-8], "t": 105}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [1], "t": 110}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [1], "t": 129}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-19], "t": 135}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-19], "t": 159}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [0], "t": 165}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-7], "t": 177}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-3], "t": 179}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-12], "t": 180}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [5], "t": 182}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-12], "t": 186}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-5], "t": 187}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-18], "t": 190}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [5], "t": 193}, {"s": [0], "t": 196.000007983244}], "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": false, "i": [[0, 0], [0, 0], [0, 0]], "o": [[0, 0], [0, 0], [0, 0]], "v": [[-6.508, 14.059], [-7, -14.059], [7, 4.941]]}, "ix": 2}}, {"ty": "fl", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Fill", "nm": "Fill 1", "c": {"a": 0, "k": [0, 0, 0], "ix": 4}, "r": 1, "o": {"a": 0, "k": 100, "ix": 5}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [7.25, 14.31], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 4}, {"ty": 4, "nm": "ear 1 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [7.25, 28.369, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [498, 409.678, 0], "t": 25, "ti": [0.562, -2.188, 0], "to": [-0.562, 2.188, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [494.625, 422.803, 0], "t": 30, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [494.625, 422.803, 0], "t": 59, "ti": [-0.594, 1.156, 0], "to": [0.594, -1.156, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [498.187, 415.866, 0], "t": 64, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [498.187, 415.866, 0], "t": 129, "ti": [0.281, -0.594, 0], "to": [-0.281, 0.594, 0]}, {"o": {"x": 0.333, "y": 0.333}, "i": {"x": 0.667, "y": 0.667}, "s": [496.5, 419.428, 0], "t": 135, "ti": [0, 0, 0], "to": [0, 0, 0]}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [496.5, 419.428, 0], "t": 159, "ti": [-0.25, 1.625, 0], "to": [0.25, -1.625, 0]}, {"s": [498, 409.678, 0], "t": 165.000006720588}], "ix": 2}, "r": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [0], "t": 25}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-12], "t": 30}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-12], "t": 59}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-5], "t": 64}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-5], "t": 105}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [2], "t": 110}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [2], "t": 129}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-20], "t": 135}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [-20], "t": 159}, {"s": [0], "t": 165.000006720588}], "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": false, "i": [[0, 0], [0, 0], [0, 0]], "o": [[0, 0], [0, 0], [0, 0]], "v": [[-6.508, 14.059], [-7, -14.059], [7, 4.941]]}, "ix": 2}}, {"ty": "fl", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Fill", "nm": "Fill 1", "c": {"a": 0, "k": [0, 0, 0], "ix": 4}, "r": 1, "o": {"a": 0, "k": 100, "ix": 5}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [7.25, 14.31], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 5}, {"ty": 4, "nm": "foot Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [77, 40, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 0, "k": [540, 492, 0], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.792, -1.935], [1.377, -4.256], [0.347, -4.168]], "o": [[0, 0], [-4.388, 0.866], [-0.777, 2.406], [0, 0]], "v": [[18.5, -10], [-7.311, -8.065], [-16.666, 0.223], [-18.5, 10]]}], "t": 30}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.792, -1.935], [10.583, -4.223], [2.167, -4.5]], "o": [[0, 0], [-4.388, 0.866], [-2.348, 0.937], [0, 0]], "v": [[18.5, -10], [-8.061, -13.815], [-28.166, -11.277], [-40.583, -1.083]]}], "t": 33}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.911, 1.186], [8.892, -7.125], [0.25, -3.917]], "o": [[0, 0], [-2.939, -0.352], [-7.834, 6.277], [0, 0]], "v": [[18.5, -10], [-8.061, -15.648], [-28.833, -12.61], [-35.25, -0.083]]}], "t": 36}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.911, 1.186], [1.666, -1.89], [0.25, -3.917]], "o": [[0, 0], [-2.939, -0.352], [-3.491, 3.96], [0, 0]], "v": [[18.5, -10], [-8.061, -15.648], [-22.499, -10.777], [-28.25, 1.417]]}], "t": 39}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.792, -1.935], [1.377, -4.256], [0.347, -4.168]], "o": [[0, 0], [-4.388, 0.866], [-0.777, 2.406], [0, 0]], "v": [[18.5, -10], [-7.311, -8.065], [-16.666, 0.223], [-18.5, 10]]}], "t": 42}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.792, -1.935], [1.377, -4.256], [0.347, -4.168]], "o": [[0, 0], [-4.388, 0.866], [-0.777, 2.406], [0, 0]], "v": [[18.5, -10], [-7.311, -8.065], [-16.666, 0.223], [-18.5, 10]]}], "t": 135}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.792, -1.935], [10.583, -4.223], [2.167, -4.5]], "o": [[0, 0], [-4.388, 0.866], [-2.348, 0.937], [0, 0]], "v": [[18.5, -10], [-8.061, -13.815], [-28.166, -11.277], [-40.583, -1.083]]}], "t": 138}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.911, 1.186], [8.892, -7.125], [0.25, -3.917]], "o": [[0, 0], [-2.939, -0.352], [-7.834, 6.277], [0, 0]], "v": [[18.5, -10], [-8.061, -15.648], [-28.833, -12.61], [-35.25, -0.083]]}], "t": 141}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [9.911, 1.186], [1.666, -1.89], [0.25, -3.917]], "o": [[0, 0], [-2.939, -0.352], [-3.491, 3.96], [0, 0]], "v": [[18.5, -10], [-8.061, -15.648], [-22.499, -10.777], [-28.25, 1.417]]}], "t": 144}, {"s": [{"c": false, "i": [[0, 0], [9.792, -1.935], [1.377, -4.256], [0.347, -4.168]], "o": [[0, 0], [-4.388, 0.866], [-0.777, 2.406], [0, 0]], "v": [[18.5, -10], [-7.311, -8.065], [-16.666, 0.223], [-18.5, 10]]}], "t": 147.000005987433}], "ix": 2}}, {"ty": "st", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Stroke", "nm": "Stroke 1", "lc": 2, "lj": 1, "ml": 10, "o": {"a": 0, "k": 100, "ix": 4}, "w": {"a": 0, "k": 16, "ix": 5}, "c": {"a": 0, "k": [0, 0, 0], "ix": 3}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [58.5, 50], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 6}, {"ty": 4, "nm": "tail Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [104.011, 30, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 0, "k": [714.017, 497.25, 0], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 1, "k": [{"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [36.624, -19.156], [-16, 13]], "o": [[0, 0], [-7.025, 3.675], [0, 0]], "v": [[36.172, -45.78], [1.204, 42.105], [-21.828, 11.22]]}], "t": 0}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.407, -37.51], [7.125, 38.75]], "o": [[0, 0], [4.093, 27.74], [0, 0]], "v": [[36.172, -45.78], [35.079, 37.98], [-16.703, 49.72]]}], "t": 9}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.032, -32.385], [-9, -2.5]], "o": [[0, 0], [5.137, 33.062], [0, 0]], "v": [[36.172, -45.78], [22.704, 53.605], [64.672, 94.22]]}], "t": 12}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-16.771, -28.108], [29.5, 20]], "o": [[0, 0], [17.968, 30.115], [0, 0]], "v": [[36.172, -45.78], [34.204, 34.605], [72.672, 14.22]]}], "t": 25}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [30.638, -59.753], [-18, 0.5]], "o": [[0, 0], [-9.032, 17.615], [0, 0]], "v": [[36.172, -45.78], [25.204, 41.105], [45.672, 78.22]]}], "t": 37}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [29.718, -25.385], [-14.083, -12]], "o": [[0, 0], [-12.782, 14.115], [0, 0]], "v": [[36.172, -45.78], [22.704, 37.355], [-3.495, 67.22]]}], "t": 39}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [27.118, -32.385], [-1.592, 32.8]], "o": [[0, 0], [-12.24, 16.493], [0, 0]], "v": [[36.172, -45.78], [18.554, 35.105], [-27.611, 23.795]]}], "t": 41}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [28.793, -31.635], [-37.858, 26.212]], "o": [[0, 0], [-14.207, 23.365], [0, 0]], "v": [[36.172, -45.78], [11.879, 34.105], [-19.97, 12.758]]}], "t": 45}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [36.624, -19.156], [-16, 13]], "o": [[0, 0], [-7.025, 3.675], [0, 0]], "v": [[36.172, -45.78], [1.204, 42.105], [-21.828, 11.22]]}], "t": 49}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.407, -37.51], [7.125, 38.75]], "o": [[0, 0], [4.093, 27.74], [0, 0]], "v": [[36.172, -45.78], [35.079, 37.98], [-16.703, 49.72]]}], "t": 58}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.032, -32.385], [-9, -2.5]], "o": [[0, 0], [5.137, 33.062], [0, 0]], "v": [[36.172, -45.78], [22.704, 53.605], [64.672, 94.22]]}], "t": 61}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-16.771, -28.108], [29.5, 20]], "o": [[0, 0], [17.968, 30.115], [0, 0]], "v": [[36.172, -45.78], [34.204, 34.605], [72.672, 14.22]]}], "t": 74}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [30.638, -59.753], [-18, 0.5]], "o": [[0, 0], [-9.032, 17.615], [0, 0]], "v": [[36.172, -45.78], [25.204, 41.105], [45.672, 78.22]]}], "t": 86}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [29.718, -25.385], [-14.083, -12]], "o": [[0, 0], [-12.782, 14.115], [0, 0]], "v": [[36.172, -45.78], [22.704, 37.355], [-3.495, 67.22]]}], "t": 88}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [27.118, -32.385], [-1.592, 32.8]], "o": [[0, 0], [-12.24, 16.493], [0, 0]], "v": [[36.172, -45.78], [18.554, 35.105], [-27.611, 23.795]]}], "t": 90}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [28.793, -31.635], [-37.858, 26.212]], "o": [[0, 0], [-14.207, 23.365], [0, 0]], "v": [[36.172, -45.78], [11.879, 34.105], [-19.97, 12.758]]}], "t": 94}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [36.624, -19.156], [-16, 13]], "o": [[0, 0], [-7.025, 3.675], [0, 0]], "v": [[36.172, -45.78], [1.204, 42.105], [-21.828, 11.22]]}], "t": 98}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.407, -37.51], [7.125, 38.75]], "o": [[0, 0], [4.093, 27.74], [0, 0]], "v": [[36.172, -45.78], [35.079, 37.98], [-16.703, 49.72]]}], "t": 107}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.032, -32.385], [-9, -2.5]], "o": [[0, 0], [5.137, 33.062], [0, 0]], "v": [[36.172, -45.78], [22.704, 53.605], [64.672, 94.22]]}], "t": 110}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-16.771, -28.108], [29.5, 20]], "o": [[0, 0], [17.968, 30.115], [0, 0]], "v": [[36.172, -45.78], [34.204, 34.605], [72.672, 14.22]]}], "t": 123}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [30.638, -59.753], [-18, 0.5]], "o": [[0, 0], [-9.032, 17.615], [0, 0]], "v": [[36.172, -45.78], [25.204, 41.105], [45.672, 78.22]]}], "t": 135}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [29.718, -25.385], [-14.083, -12]], "o": [[0, 0], [-12.782, 14.115], [0, 0]], "v": [[36.172, -45.78], [22.704, 37.355], [-3.495, 67.22]]}], "t": 137}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [27.118, -32.385], [-1.592, 32.8]], "o": [[0, 0], [-12.24, 16.493], [0, 0]], "v": [[36.172, -45.78], [18.554, 35.105], [-27.611, 23.795]]}], "t": 139}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [28.793, -31.635], [-37.858, 26.212]], "o": [[0, 0], [-14.207, 23.365], [0, 0]], "v": [[36.172, -45.78], [11.879, 34.105], [-19.97, 12.758]]}], "t": 143}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [36.624, -19.156], [-16, 13]], "o": [[0, 0], [-7.025, 3.675], [0, 0]], "v": [[36.172, -45.78], [1.204, 42.105], [-21.828, 11.22]]}], "t": 147}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.407, -37.51], [7.125, 38.75]], "o": [[0, 0], [4.093, 27.74], [0, 0]], "v": [[36.172, -45.78], [35.079, 37.98], [-16.703, 49.72]]}], "t": 156}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-5.032, -32.385], [-9, -2.5]], "o": [[0, 0], [5.137, 33.062], [0, 0]], "v": [[36.172, -45.78], [22.704, 53.605], [64.672, 94.22]]}], "t": 159}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [-16.771, -28.108], [29.5, 20]], "o": [[0, 0], [17.968, 30.115], [0, 0]], "v": [[36.172, -45.78], [34.204, 34.605], [72.672, 14.22]]}], "t": 172}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [30.638, -59.753], [-18, 0.5]], "o": [[0, 0], [-9.032, 17.615], [0, 0]], "v": [[36.172, -45.78], [25.204, 41.105], [45.672, 78.22]]}], "t": 184}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [29.718, -25.385], [-14.083, -12]], "o": [[0, 0], [-12.782, 14.115], [0, 0]], "v": [[36.172, -45.78], [22.704, 37.355], [-3.495, 67.22]]}], "t": 186}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [27.118, -32.385], [-1.592, 32.8]], "o": [[0, 0], [-12.24, 16.493], [0, 0]], "v": [[36.172, -45.78], [18.554, 35.105], [-27.611, 23.795]]}], "t": 188}, {"o": {"x": 0.333, "y": 0}, "i": {"x": 0.667, "y": 1}, "s": [{"c": false, "i": [[0, 0], [28.793, -31.635], [-37.858, 26.212]], "o": [[0, 0], [-14.207, 23.365], [0, 0]], "v": [[36.172, -45.78], [11.879, 34.105], [-19.97, 12.758]]}], "t": 192}, {"s": [{"c": false, "i": [[0, 0], [36.624, -19.156], [-16, 13]], "o": [[0, 0], [-7.025, 3.675], [0, 0]], "v": [[36.172, -45.78], [1.204, 42.105], [-21.828, 11.22]]}], "t": 196.000007983244}], "ix": 2}}, {"ty": "st", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Stroke", "nm": "Stroke 1", "lc": 2, "lj": 1, "ml": 10, "o": {"a": 0, "k": 100, "ix": 4}, "w": {"a": 0, "k": 12, "ix": 5}, "c": {"a": 0, "k": [0, 0, 0], "ix": 3}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [67.828, 75.78], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 7}, {"ty": 4, "nm": "Layer 1 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [275, 33, 0], "ix": 1}, "s": {"a": 0, "k": [150, 150, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 0, "k": [583.5, 564, 0], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": false, "i": [[0, 0], [0, 0]], "o": [[0, 0], [0, 0]], "v": [[-200, 33], [1280, 33]]}, "ix": 2}}, {"ty": "st", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Stroke", "nm": "Stroke 1", "lc": 2, "lj": 1, "ml": 10, "o": {"a": 0, "k": 100, "ix": 4}, "w": {"a": 0, "k": 24, "ix": 5}, "c": {"a": 0, "k": [0.149, 0.2039, 0.1569], "ix": 3}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [0, 0], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 8}, {"ty": 4, "nm": "Layer 9 Outlines", "sr": 1, "st": 0, "op": 300.00001221925, "ip": 0, "hd": false, "ddd": 0, "bm": 0, "hasMask": false, "ao": 0, "ks": {"a": {"a": 0, "k": [70.25, 68.25, 0], "ix": 1}, "s": {"a": 0, "k": [85, 85, 100], "ix": 6}, "sk": {"a": 0, "k": 0}, "p": {"a": 0, "k": [618, 518, 0], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 10}, "sa": {"a": 0, "k": 0}, "o": {"a": 0, "k": 100, "ix": 11}}, "ef": [], "shapes": [{"ty": "gr", "bm": 0, "hd": false, "mn": "ADBE Vector Group", "nm": "Group 1", "ix": 1, "cix": 2, "np": 2, "it": [{"ty": "sh", "bm": 0, "hd": false, "mn": "ADBE Vector Shape - Group", "nm": "Path 1", "ix": 1, "d": 1, "ks": {"a": 0, "k": {"c": true, "i": [[0, -39.212], [39.212, 0], [0, 39.212], [-39.212, 0]], "o": [[0, 39.212], [-39.212, 0], [0, -39.212], [39.212, 0]], "v": [[70, -3], [-1, 68], [-70, 5], [7, -68]]}, "ix": 2}}, {"ty": "fl", "bm": 0, "hd": false, "mn": "ADBE Vector Graphic - Fill", "nm": "Fill 1", "c": {"a": 0, "k": [0, 0, 0], "ix": 4}, "r": 1, "o": {"a": 0, "k": 100, "ix": 5}}, {"ty": "tr", "a": {"a": 0, "k": [0, 0], "ix": 1}, "s": {"a": 0, "k": [100, 100], "ix": 3}, "sk": {"a": 0, "k": 0, "ix": 4}, "p": {"a": 0, "k": [70.25, 68.25], "ix": 2}, "r": {"a": 0, "k": 0, "ix": 6}, "sa": {"a": 0, "k": 0, "ix": 5}, "o": {"a": 0, "k": 100, "ix": 7}}]}], "ind": 9}], "v": "5.7.13", "fr": 29.9700012207031, "op": 197.000008023974, "ip": 0, "assets": []} \ No newline at end of file diff --git a/src/main/resources/fonts/Nunito-Bold.ttf b/src/main/resources/fonts/Nunito-Bold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..063f39a76a495691d71ce52bd1ad65576b804a90 GIT binary patch literal 125464 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¬ObjEEgjHN%5jd5t%vg$W_b4$N~CLY|3GaYbrGgyzDd`?8M5-9>qMDjg3< z+S$?~_*SBxq@Hc%3&o9!2qsKN%ec4Zlxm_%47t}VWh%*d@TggCg-GcM*xRgzRfNxH z>Zw=}mA}C~y6JKxSfB^8LIYVs^JL;x@?Rg|Gg`v)FX+i$3Y|oNPIH_sjdPnVA{UWG zvR-!K;kmhIDg{L!5k>G>5v5znVW;r8Ok*Nozu3$w^OJQ zkfYid3#mR93M5*R9h*loRl^Mpb1O3=n>~G}FPgJ{Fzs;K)o>(Wci0n+gSG3nR?KT| zYH4YjHMhd;LK<0#6?i-)-OJjW=4UeVo7$Imq4Y1EikyMW!aM)WeM){+lwxE3=Zo-k~dgW&g`V&$$=4??ai%FRCL z4;ItuCg#=h3OAX2d_NZRCs42Hy3#Lr=@qIh_??@AObtae&sC*YA^nLGPQjZVuBs6h zjD8Pmap9^@l#t&ru)>>athh7?pIM zKpvwf5i1Kv5~u7U^WER4!C)$dbLatLAWeBRs2@la1Ull|}2DrC`O&J)nlAGO@v0I|s&*_aelC z6fR6uSykoVjc6h7jUpc!8k0Y1Djf$c(`g$LI#a)o+193W0B4SLl>!G=ypqZPtL{s{ z*8}~$_w&|;WtUYl^ZovBRrmBH6JU40=X;)yhfG&hSHH*qec%6m zfEr}W%VcSbyqJi;FJ-xRZ9G0E$gz%rqy@1dl@E&I*!kn#PRXiI_`EwdR%=x??3qPU z-?s6B&(HNpgU8nS8p;j6fPnNTQLEB)yO;QNb8(19 z4~vGAYw;`GSK-m@Sh7FBWY=Fattzu+G0Vp*z}J)IQOKMJ`mX8W|}@iuGSjXmL2`zC;?Y}i}IKqcvv)LL7ZHs?%x0G$z$mn#+($d4=92DC;PRBG)e zjzZ^5JHbt`n=rm|f@$et16uN1IXf3Zg6t;bITH+&&sffaG;OIktaSJP{7e>vifOR^ zU}eJjB>Y@6>$CUh%o~=Zs3pOY)OI@OqhrE>nhCGK{ai&;Mc=?nOy69=OT_hwFNBwf z86p;%I}k$fN{@L4UW$dsEV6qag<<6#E8wM*(OBpbn81lAx}1MI=1uv8YUXV&v%k2T z@H9(}1A+5Ep{#(f&EE*c@sdTxKEq_Aw?lDN0}yU=tz~8<1I{%BADkoRsX^8V=BYvF z1{Rg-bJ&dC=MODC7lu&})z7Ky+%X$8vt~H9<@dWb=XU6D{sgS!=gRCocF#0YxX1Z? z$OHc*^y8dcWcK5obNdjTTRdkLU3jh(`K#D@e#rA2G{0P>b{Nx;LV9+qOu4pQz!FX+ zj-w^%9w@CeL6V%r{J$JJTtuoCa7mfj_RX7S*KN^NpZtS`pM}CDZr-r`V(=TWk*Nbc zgU23&nluuHUzlyrOpQ#KapkuWP(BT&?4Kvvwhm97%QAsE)>dSocdf0Tt+lrDG$+T} z`jGqF@q!^cJlU`S17?RO%MZL`04u7&0B;G<>k41Hf@fukd@obbt!2h`_Rkz0&W`Qq zr>d9}l#%-ccmCP)%D-)NlpyPl0;Bty16g;)m|l~O`c4<(PqCG7>{TDA+N(ZG_A0dF zJg3<>cC3e%J%@*%`}||>b7uKr?ceJ53*Sq$!?gMA`EBm=OYlh?%zEZOGC;a;tTgkj zWVFkP6QPq#6pXPmWSn4}9bM3pgP#GxI$=b3E)+7F8~COJ;pkWh(R^c7R}aU*Bn62* zJKp(mPD83jju$4z?P!>}oIK#jId3MHllSJM;b?hFn-6=icX2KEJkB+vDXt$`ZxODQ zFT(LTu9ZhZi2n+e^6C7+%xz%!h`am1{VIo?oe}xwG#EbQ?Buoku52xB`~+_#qTWCu z9q(&<<);+YsyDxVdgjhIm)AB9t#ww`v9%SMt&MytwZ->fyi|?yJJ?@s+BT^B(B^O6mG9`ue)x_4Jylo)UKA0Up*xy=c2>MW0HYeE~bra{9O6&nEuLzB@ zHx!ebB`dm_RltQt;tK3{mcy;4RQ zC+i>Jw@OxeK%4o(^g{!(#0XXSV#XgaBJB?v1a4M6%T+!woxn0kv-(-fO!q$|C}aQG z;DM)Hu7~(>SE^ju2cmL#`6R^V@w0F|W{v8@HD8^{?bK)u2vd{qU1Dj~&O{r_obc`- zrT85+u6S>UG01vbC+(Weu(o^v)#dygtSh*8b)MKI?`0UxmFsc;H|)Lv+0fygRHbEK z_WZ?tZ;Z+RsGZE{!IwGmYUe!w8vhA+jjFJkl6AApk{U;?h(E^(K>x!2OxRcq)Y&Ct z(Tt@Qe*XvV$A4@u@@rA%@6@LnTlt-*zM0EWiD1stytt9?MO%&+9*`)9G*^yh9vc(Y zXe~P6XcZ_E_j^MTDS&7KL<$SBIQJkcuXoGSswAmTbC=WIBSx}8=Z)^N+k-_&>mhd(5%T5~8lUaD%xf#O#RSK-P@b^}`JYnZ+F`1IGeF z7aPrbQa^p=vSZutJa5C*d%8Lf&whoBcR$jspihwo)-((q-GEZs4Krn3iH5`E<~aW0 ziw@tqv1R_U^=n^sv?XCG zHRst}DPg=*xU$rPY897J@;j>KIJ|)*ktZt(cr)vp?S`TUkmvvXTEZIT}S@Rj$@fAXW3-@k41 z&9~k5y4T%y+s(A&h)(=hjk}QCo>AzwO)8J~V<%lDvsh4VgR) zVa;>f+MZ(jo90G5la1cV9=MF_WJ7RQZb|9r%(`YomBf#^q03s3{wI{sTei1%P3Jak zSc7oL@M!C3Zp~!N#*&&a{Q<4%*x>5Jy_w6j zX#tLLa#hsuVPPQW*W+<*T?&#>4E^L}=TAdQZDsnH`+#H2K zE&L>r+*_VH*bjJQ8zh83@lstfKi)FAt-c|kghSj;yo;T(@N8r2;Q3RN7YqRkBN$f+ zt5$R~x>mUA8lDH7$(fGlOxZzbh+DnwiqX=BW7F;D7K5VSjA;#Pa$~#NWwCEZ@92T@ z(2=z*xeTocU(oj(QJ=((EZj>l;BK+wfxyBy198zvhWLX8>;#;CBdVYJ zxS^8=8DuQJ8S_m;(sp<~I)=5}9v#1G8q(TxZq+Kxxz;TwN1E1lOwJA;UAyk+P;RVc z&90G?TjsZIo1fdheXe6Nzj@6XV1o8tV_mbIenUGjF}%ICb^CD3`cBD^53U1@HjIyr zkB;IGx2}GmI5<8&I5dGZ>B1P3l{Eq8Tee&JdNgFsBI&1cb~N6tt6W`^KeruRz83!e z?R{)5CbwTsiDVnh!d^8#@yrAnFxriy=j?T%>EP=4L^Nb^uWoDR!P&Fru5 z;*0y|`jW>k>YpzsS6QWDlo}0{h8pK)vEu81O15wx@YE9yt0G7eARGkAFeD+!<5|;~ z%5F81ir<)*!@-~~b|aTaRyVm^w+zn@xl0U4dT`;>8N(#}RKTB|+(V@8Jrg@R5=w@>nb>9S<3A#1mx&WqyH64MRMt1{`_$%oK=NLOs>np9^jozYbZuGf-f*Z1Wc z#tT=<)G7H~KhMpg&oYA)*0H}I2Wv?ZXA^uupKJf8OVbG?S&bTzj(D!MrPxXIs!(hx z<}fBBPGef#Ft)xs*V0#-@Y*3c9u{RyO=r^y)6h-iG*4@a><`B=xK-UvWl8q;w-@{C zu#zc^?kGmLgaI8a?SdNCQ->BY4A=*i5a$os?+Dq!zym(W`gVruM`!GE=r*rqcG_An ztUM!HVe4slMHro?_0}i96C%jBp`Sh6`$!gac5^KhC;^967-f|K~U!s?jn z3!>;uce03_=8=*i8UaPkdWy|GX#Dw-ef{9VQ{4D(vk z&J4G-juFe{DPS!7Fy}1k7uoJ*#|W$9+QzEbj?h5u@&D519c^<1^{Z@@p0gFJ(du6n z5VKu%Lu(5Yt@-AbT)wT%d(q*U!~LcKuXIsVmCYOKK zuoLh=oFqm-QHD8ZtF!5oAH}P5*jGOAM1yJm8ihan_luTSj~U!tDr+FVF&I-8s0fAN zNu#B6Fq`XHsr(8P`NLp?Th#h%+-HyKIV&~SWsjVHQ=Kh`QTDrPg=wGq5 zwTOS-v4Q3xQIg7qzE!LG3S~(Whnfe*%K1z>pHFAu#Q0Q-B~xhwlLYUc}MGf32@ElT3WoOAw}%~fptB=YOft?8qFU& zZ_W9A-)k-cwxvQd_kH{qc#;2iY`p}Ytm6{g7XI(?>b2FvnM4yL8cYFbIN^^#*=f{ zJMfHT?_}6J)cYbm0iS!x;_tap{@YZEyW|I>Rt5(Gr-k)_unm;}cfI4`#;XCJ4sH8g z_%xy_&1~`a!p-|ZcG}zabm?c{)~7xM=q;%~TX`LB8qK~0gzLLa18pde9z{K_4Z4DTf;ydg;z zg%c;d0r-5$++3X7q9A}HBrSYw;UkD-0UoJoB}!-nw&HR>H11(wkl#xI{8$J`a!5FN zi4d}cP!P*DD}@jaqAYwQzVKmLLK}mN&wD-sxg=P@h7Q`eJkijQs7t544T(f1lSm*5 z6utXK_AUfigr=fLNEZ$Yb$;Z(4yrmni0qAZxlA&dmFz>TOlPFkie;0DOd%ic=mef% zeR97=pAKI~RFI)Q!I6pBb!7x~r;miYDw{}T=^x=7HYT(cCW(6GQPsA3#t2hx^DB&$ zd!o`2b{WRZ-vBt3gf`D_aK`Y-eG8v{+&kj{93f3GS+NYB4U2(E5i)mDV!CiuO%93E zlZiS$!WpXD`QM`0!bg*tOmbm!$b75{a+=+0&qpvj)+#{lXgMNbY?InqU7n=oOjR|* zd=iVrmhRC^l6xbo&U9%q!*^EZ0+8GSlrHmn-S*R?i9 z8atA)pb<+3Qw@zXotc55T&AzHPSGE)nQwG)iu)|)TXa2#X=oUkGa4cq7zu<9Fi;gI zk6JK1HN2~<)SD@U1YXxPL(gZ6$-(Z{*&O^5e2Uq4eiOkMyZSK*7%dyS>O0$_sva`y z$^+SAe|@1HLF*hwDA#5A?rfZ<5YN)S_k10zu$s<@11)zgT|j>wprtc{4amW7aToU} z_b9kyPhlCC4jInhHQ(Ja-`lyhyK8<|$CmE)xgPxAwS^)z)7<&oyODtk{&g1lLqwr< z#N?w<(uvZkW_}S-;0UpVTfm}JDpO5Rpbw^0zfSW8r`7ANXw|`ex5ohBYwSoHJM04VPWBT_lP<&H`fS?xuqIaM?BE0L$eZ zmbctf&Ru0{-uleEg22AeIl0dG33hc&J(3|Aa6n4lQI89}Qy1T$n(%(vRqwoGTi4WQ z1RL}5*58p5?D0EV-zG-vJ4I0o>W27?6wqWt58M?p;DhsK`uk?^6L3zD{tZ!-vSEn# zvWIx#&p*d%EK+4u^x@;za%pWKo%i7>AuhV1!!o{*MEBc zfcWmln%}#Mhqx9BY21@)D5U5~7_74=U-O!&sn@)E%C3_| zwAs8k?fI$OCVpRZRkq~Y6v5dg40b6s7f2$nXD%L463M_|U##;%RSYKWsaIDT3yRv2 z@1I!zmcK%qjf)$(3!wu6W&i~sSDj*sjkgEn7sIYfO>pF+4sv~jxCu+T`{B>oj(ED6 zfY6*Sch8s+TW^PAwkeTlf?~E^wXZa+$OrMz`r z=^vpP7n>F;^O?LFO9@2&4jxxLdM8^L1hT9_*}VT>5fMHtOgRYSxqH14!> z6;6iUwc1i9naDcqALcgT*@~pkpTk%|KQM=_RBZ&$d-42@?D;nK ze4AT^1n+yki|1f^%S7&qy=4#-oF`GlF?~Xfi*vHPIS^MhSsED$h`W4Z@FqQFyjI0* z>&)c@XY~N#H-3Yf2aDR)$dAhByncqXy zp>=Q)H@Y2MnCzb=!x~~MP5xzkFK{E49x!Ln>H!(=jz%#Q2KNr$?&}L6 z22AnsVO94I+@*@fW5Yh%H~P5gSKmJ9)3tZBYnrbU(Od3Eog6eCxl9f{qU(=@ zRr%uey1v%CA)s9Beqt=ci~EFE06rT$yUDVEw3Lp*kzrG8;xeVlVSpo$6Pv_FSYZ@N z*HKlBGRD!d*1I}W^4SBf)Etp;LvFQ>Yy6* z<;{?{CZegwPRoi(Mtl;&A$aRrwTYY5wNOYdw?4*iwytMJPF|w%T?i#LA}Hh(Jnv)d?C|d zfz^sSRLydHy`z+OqqoR4%v?|o70UUn+EHqq>tDO4esYleq^%*IPO!<$08CG?rO9PEm^L; z^8*?7WulqB960<$>(IemyPB}9)}ccU?MUijJ%(yOVcYNqLg9EvF4q_7i}$X|x6CFI zT3cptODl_eM3MAR7*sLe6YIs-g_hZPLTk@Jh0m+Ga$Ot1#D`jA`GF)Z4c-KFnFU1P z01FG6@b$GPil$i6;UnORMG2(%s>D+Rl8OlC`n}vad*h*U`OuBCvo{_rmk-`JJ9WwS zj*jh@Oif)%pO?-ZJbCh9b}9*dhH5F%j>y>BwPTU?xE6tlIyo)X{*;<(@7!@I{kF4n zJALlxJnzcGhcB@$(Xv%R)U{1BYvxr$3PfTO?4Dk-=~QR5)`=XeV2PFIRBEVY6w2gv z4!;39F+dgM*$6p>RyLR8UTkhHg=qMO>2>QSwRB0#1R}O-`ifF6KBIB_8k-uM_PzM* z4wtyy;ppVx;D{VA?u^6@^=QnL-%|Q@HlKsu1@bx<5w`PR#CicOxWU>C*HwyeAr62r zAeoZggKyeP&g@I>Uz9H3pG;deWFr)~4Z;uDtuV4Mn6L3G@LurW5L)=69eEk6eT#&> zaNG`a5j(`42PsR@7XHHdGzI?e?TMm55-8B8%E$QnbAWFpAS=nTb{cI zI@gy_SM<@A>Bh!sx2|ZhtE*V*>f-KcoNjKOYDB%!=GCR)p2kvdZ>h0|FvAF_?0cD( zh2$fb%DP0BGlf(nS}!mf8!|y-Q?aj_p%zk-A~cd? z!t>v2$DH;+7vsDgcce2S)xf}ETLA2 zyys$guRRHPm#Y5#&>v6*NjJX)q8*fALG zZB5qK>2;`0WO_NQ(0)yQt-FTt~3{Rvr zV>g3D_u^lKjnJc%sP@7Vw?KFxmj#Og%+1g~xCCMokT5<=+zUmRlI{yHa8De#{YYQ` z1-I=#dDq@1`PA!tg17OSO>VoH|Hg}L;L^KE#a7|^WCR@9FB3a z0eu?%yK`B|-)MWbyEod_fTF)tiRvbPKo}+*MO2$55GQU0@g&YUDWTZKaoHEUys*1OziFayJrRX3h#rp1v5di4hs+4! zliiCqanJEbDwT3p7&B-?bNdC|L>- zClco(2c6Lpg!=&DoovsP%N4Z+lMsk2FTpEp*-drmjLBUBw?ih%4!K|rmFVVT!|;`T zrPk3)H-<8T5j1UI!k-+m^_+nG;Fd41C<#sWiY8c`nU=|Y?O2Vr>3St*NZw%j!c{6V zL!|LLyjmy52SZw$V7)SCeKFH|)6NU;*cC9ZkWr0opTPxX={mZ6jV(gih-DJ0%M_1G zJhJu5=_VwnJV7s=ulwpv@je(lf$16-f6I69Kqa0TK!GGdb{rsi^fKv$9gHXtTa@HF zc0?ed*wAX1O3th?Pgp?d%oIiE&_udrc_^BQ$A)BCN(2W}8=IQer^|Ak6buf=kXMSH zC#AvE`sSvM=|MUE$F_AXE$iA^>2FX@$itc0=9by?DmgC8!Qpr^5gQD0au7{rW?Ncj zGeg0+EXhN$WLgb&9;M!+9J2nw5Xvey?U|nHZb50KU$>j_x<4ow(*PJG- z_0sY(9@Rkz&aUi-l4Z>$MOF5iF(b2ob$M+r-rsg)&+fqsrZw88M#qH(1STQlYj$&a z?e@M^7oNLs@O=0TzcOMR3~~9S6px3zawJ>do2_fClOp!s4MY3-1??PCFP@`ug)A0M zVeiCHEQo%H@KtQ7dixWqktT>C;1uBw{GYGH?etDuvRn&65wF8CDh=EGS$TI zeURx~vx%$3TaM;9-{{`X&b_0nwujU!b$CwS8ctev+#Dt4Oj{h6!O-^a>Jgm(!o zY~yfdW>wR0W(|^<%x*k6Ix_rWOVdM{t&|&XYbK;?)~Qo=6q8VfMsLDbmW4-OYudbO z*M8*2*bT9P^JwtPSqIF$-IO>M9&Xw?9l}-%MkKuL>dDEgw}t5tgi1%6WXdX{ zrU{>QcJ7Tt<$G+KDJqIqy8?KJ$N(s6ub=YdqwJ3y!6s=6Dz zzo3hS@8r^{sj;^Cq82qoQI5^*kaAP)>EVjd8rSQ9Qjs{$= z9c&Gb$oAiDCqi>{%;!Q0+m7>`EQNx4)TjRIPwDAT{-pULdN3?6JjchetGK*^L0Jly zw-X6_;a6M->VKm6+;7?A;KJ`we@;`lwuN6|lVO*U%BvUaGh6Wtqwu$-Vb)$cj8`q< zbs}l#I4`+Nxo2Xt5seyKOmiy@a;s@>r}Mns>^iir^I%>Li13w6t!^A2m)}9gn%iz; zf4Ma2mZLZb#uorv-7+uN2>P+xFx&cmkb<*Bl_>3my zz2ENWOTkzf&h7>}-FAl@iynn|k zBW#*sV`N8PEDHNZa;Q_M2YSa)9SVHK?wfnV1*3lZd^+3b+y2oD-Y|C`IqTnd@W!=% z|H5bJIXCV1uf6dgp3|6U=WfQBTpqo4AJps)2F|Ee;oO(@17g}M+nIP<3>f8(F!DR# zhC5Hv^}JpAL=n|w;FrlLdGl^18^u<;@(Glgkbap&YKibu8f6($FQE@p{Z1^7oOXZW zW%9HCz6xi*kamem8Hu#-(pS8G>OaCTZt-j$RJD^yhfLb25MWwvj4@I}lb}*2e?M!f zboSlR=oqO4#$u7H)}BAWho|XoIxGwvS+gM;-ID9gTbZ$rEr2mcabV_Kqp{8dYfXBXOeh3MeQIji;N8)WVe}p3 zJOqs<=z~E$6w5 zqjKeCt4~;$kT5C4{M`4UuJqr4TBE6#lNw{8{Gg?G^qxkGwj%i52z%ctq@Lq@U1 zXYHcP?IC{P*hZ^yr2a%uKi$ji1vk<=ft` zr)?J_4*?@XhIf2*fsv`Q`x4toBIQ}o@N(>HY(-i+ZL0yS+jBvKDoUZ4mYFF5>%NQ7 z^6fSFmJ6dg2*!0YFm1LT)E(Sy9GLc2c=~Gg(+tV-9*JRv&%t{Ws%561J`JKc;;zW< zj)ZHC#JSx`?$HgFMoU*+?( z&a2qGg55C~bYHyHh{}?F-4P=y1@!B_L17)-0=Omn>+U1>gGy(8!df4YqX}{Poc@4> zD3htLQr#2^9k|DRz`jU`Q;)NJqJ$^c15ZMm9EF%wS*GfR=vc#mBVmH1b*o%JV#O`t z#++Ea0HS1N;XaY_re+9Eqro2fm~Y4msOMqxQ}qW012%mAVEr_28=;VpZP-LevqKrC zk171H-gh{Co=?m^MX=mSJFV$i;CK7fr}#Pe2I;3hj7TsTCZ27ozVK8w!_S`jScae; z!HlLbBX~+Ms8^;`#ZYHYo3i}K6YTGz(ci`XUiW!Pz!3<`UyA;tjOeNeAN;#(m%hFW zo8i^f0U2CKtG85R;b&aRKv=7>@F%1C<*VUL&3XRS^L6e*$1|zm91aFfu8kZ<;OMmE znk0D6U;I1nY5tGUJd(FC`Lk3#O@-Ru)?7dC4@W}418Mf@E4i&Me9gTmXmBTrX>feO zCt@s)!94{YR`CYjS9LeTDE|sxG+$cDi{=N+ea81&=i_uQ8u*K65t|aFI0Kqp75Eh< zrLtwPfw)#P^l?%_e;ofz=gQ1l=BGl4OMWiMh+ujM>_i^(i~+;G&RP zAHv$z+D2pLK0<&0S0s)CcyUywb3Usx_!gDspn`SWt#RiH+zp~kJa6Yq+tmqI3h@-P8Xa`uNvYB`7FiWjpnIBaPx>L|ZGix{a=H zbVt(SY2|Z#i2N03)%weYaDi!6kT(fZ)jln(a>sNWA~GpE_G>1WXQ08XYuMap2(sYMzfB#bDUq#-V+A_u^Dt19>Lg$=3- zc?^l3blqH-C4e{^;I%$j18whJZFhRDh#BzS>4*9^A};)bphEv>)gjF<^^2LDIhEy$ z&po%WnXZTY++rL?w_tlp92I}bz#8FkQuCD2OmFH6Ap87I8 zgr|Ts!G<%f40Hl;191qFYZ8D!M4EyZeS>>fVyLmP*$C%#36UNFd$bTAEalrn;esBB z<3V^d|Dtbj!*Iz&6rtu-V_Ffq5zEGnpvKf<@gL6q753sP&v!AO3&`Ucc=1JM^Ag7T z-52bw%X2HAL9~kLV&Uu(asUr;Dim6{nytlyrgf`jevII5g}#}`I0rqi<;zVP zmsx~1lZ4ONPW7=Ow;fAHe@-dY!WEgaTg7HPyy(8)i}1CV1l>8!4^NDqJKnlAXA4r1-?>h2ssWh2O=?euY<~>U{C#;{!*wv`518 zDvHOebA_X0Lq|5XhRw~~5#1a3Ex}`qIfZdNi#+EpF7M{e`jEP9KaqhsYt|vOC&&(|#u0VzU+(zP6uimgR zGk(tK))Rx-sh(?yhrKl#_XPvFW;?&8x45blZtGPQ_~0V!(~b>&==i$Jx3+D1)4}!E z9Vx{q%pJ7X3dzm;kCf*xDyK*Gjm+OZYQ#cHNX9T}pYqe-`ZTIz0J+2Uwr5+mKsg0+ zvkejL2Vx`yAUvf-8y5C)w|t&&Eb==o!>~@hzu4G>oB~K0!FdMY!;%5t4z&!ExLtBu zQCwtkalQ@h1NS;q%!$~Eg|EdPjfI6Yl_q`urzC#~!t}qXVXFljH_LM0yfkX2K#kPsh8$l5x9kFo^ncx1BmI5+vPl1-&8J8Df0^=y z^PWCvNE@(Aimsdks^lE%TsvPv6>x`4(V3}r=%^m=2v=Bh@kqnyww|eT!>X|A<+A}( z0x{6kNO1nz?X3+H=ah2;rAXs6a#EH`tEG{P*7WTf&kqfsd-a$Xr3JBi$oA29S?A5hAz+Y zT~8ny(UD!{RRJ6%Z(yNwZg10U|CW9DlWLB1x7N2rdw64ohnyz)pg!d9M5H`E7hZkI zk?C`LE;)btV9x~3#)b{qsqT#%7Vcj0F?=p*M&47)X0+9F(2;f=9p{-dK^@ep#PHvp zv3K5?%-+V?{`wZoFx4FGKBZN_Ai?PSUjc)(;{3}0BjC_(>_Q%eQ}B?&>GeSxg?Wcb zuD}HBXq8}h;abz$lcM-!AZ*YD7oe+$AAb0s(%gr1V8suaT6$qCt`@c$`abSv?&gIn z;5&VYFL>?|`r%m#84R9nt`};B0Rg%L+ATS>jq%B&^9mmfDoAl(ZfPmQgDeQ*qcv&yBwia}m=PaXGPG2VRvK2h417kK_V|q4;WT$?;)c&kiP7Y3h7bQj|MFn7TZei z9hiIn3(cJr))k)yXgL^!D(7d#@rcb6F)5^Q5>-DBSV6@Qgn()^SS=lq+*t;9(#Ol1 zUyJLSD)53|k~N>tTbHtP5c2rNgIJ~egf6T<^e!u}63%&reT*=#ntob4P%7x(m<`9@ zzuELlSUFMcEfjlHQCAd6G&g+~5R^I_P~gipqdu<|R1~k&l|{m73-^M~7di_XHkcTU zCX>;iCw?Gk~D(0DQ>`yib%WOCR)gZJB3J7jSKLtK_O@<$4$hYE5{X-&JG?C zC0??W%PICWthjwX`oewIY@-$X{HyH}6=@Ks{az!qVxK2L%2Cz#a>bJ15X$EHGocyt zgfwxMd9pQexTn+i&WeNhYbAnk?x-Dg_6<_R7!9G8&Uj&^0r~v_E1)NIo%W1RmUXYs zTb~LS-91y!_KZb7Si%{L(80gRcG<7SBzZ=yH;`&vS|jp`CGUUzi|v)hpg*i^(-og0 zZ<9zmEyG zQJcbw*bI!w$hmP5nO8yqYrLa#JQN72NmhuX%IsTNIU3MC*zS3axeJi z$DR04S2N+L2_4acUGI)160uk!{-szliO&ST7q?)j*kksIILFH|<6D4j72q6bYpt4N zP^(dKbzv7@bhltce$PMcS$%I!Ix2~+^G7`U_jabl~43|gdc%kWuc+0f(WRl-g2Z-Q5D0ui~BcruC%ea=ta%BawXO4mTJ^`!~MkPg{Afe`{=gvbJ z3gm{I^NV(bZ@UvIh5V9nk7@_K^sC?ESAXsNs&+NP>Q8Y)n z-_VgL>)y&W3e*t)??DwEN{eE zU@#}cH)K~fI~R>dZxWGKL)^Jj3|peOMNXP#oy4zGZP^>V<(8mVw$~>m8pryZ1 zOLWFTJRLZiBRY*N*Eg(Z3$9Xc}s{iMT^JzjOWdOxkd6}r7gsN zz|G2mQ7!DvI%VGP)vdtc!;+=X-omFnpAp7@>)>iy(<(qh`|-@iStC|fTN|Ww$g>8TtaOr2 z_hvG^DOs8d3P|fb0lj%|x(yvsXK+a17T59XJfGx$On3{Y6*Uk^_^Aa8rRjG#jdv-k z;5Cx@Sge@ISdp}k*AzVRC0jDFVm1?EPk6-EL>tpqv_6upLq-8zPxYiD^^r`S4Cz-` zJeSXbe)+N5EyVQ*FG8+d{WQZjwfOV(LDgGfZzBFP*_%QwhKt7&s4HlI03<4$P4)Us z5u0TGrGHofXN>(ABZV@&*a&N|{fmw2?Ex3-Bg;zgtV6nj_-W-rhW#1m#m2+1eH&Y) z8l38TkX;kg0u3guq@h+oLw1N{A@8FBET=1b|OU6DOx-*2{`q3NWd6v^1pr@*& zvDE%s=TKudR*@}+DvcjF4@r>xlJ+t%M#K#s;m_V~&B`yeyEouUOfj7g@=NW){$eRa z(v2v^VBgPYJr{zj0WXa+PKt@;L{QE=R7^?mr^0OqqX%#eg!(OOhIJG`!KApi(Z80?#NRf=mj^LNZm) zcc)TTOOwthx}MVwqRNauKJ0mvzX-ZEk5%epdBYsurL!~L6qB2*@OtjybVQ34Vhx5l zJx9HNd2s}?`MYU{JJrpWaXU{Y;i?NK-Xx%VIU+l5S$sd<1bHR>TZJ>q)m)#>L4Jmj zD>PF1WHVlP)>B^m1kKnHq9B>pHt08NCL71>&F7t|<~5@(8co!vtrp~WUwqHvnCCJ` zE>I5Q=U4=od6>{&*<1~HRxYC^SVc1|SS3S2#y|L68_9IY4(L)K==1WNQdSd6OfGbe zeGX9>wk7$zzEDWfIDz*Hio9uKK=9_X1$Y)OUXNZ!p=p4U4NDIwl*waiSr~}bJ>1up zGE!DH9xwyCfz7I?KG)yYIAa&YSk_L~2?97L3*}qb&RT5pymOI7&0`IiY#xa?Gr?BC z1@^+ooFZt7q{uwzbz_g{1-(`UW$wEZ!ywk2Hz?6{-)`@>P`I`$Eu(abD<6Ey(iY&1BKNIS?j`L)RE6_B`Ad`MC~ z3vK4T?1OfkkcOo38fV85$M;fpoRCh$qKQP5qzmVh$t;VWTfD}9MP{id%tkT`sF(c3 zS^^6JNw`$zALZ*6*3CGur>@?a@JFX=hUy)f>7z6cv6gjL6;gQp2 zmf{hxL-1?efgIQofxkkDngPFh9b^#bt+>TM;a7J!zpBa!VE9T>G~^R^N--+JB|r%W z8Rt6HA22~iF!p=-XFYccf5AL)`dqrlxTUHx2HcI<=E^BUfhgppopL;?%fkCAi}V0& z$YJGr9aS=7q;>fq|8Jhpv5^pFXL}Q;4r8gdiR)azGW@KUf&v#^vpC! z1eD6Jg?<##=+{UB*zy}Sq!o+eh>*e5Wd zwrs}%cbqBR)&;i*d{#u>e-8X=e1UDjdj8)u~FC^V=10VT8;RCL83u@T|Zpo#V2tbFS&hKBfU-ue8 zSq<>=M4>n?^8rl?8eV-r|J<4xR9g^(VsCSEFFrWim|26dA4045cqQPK7qTJL9GI%s zTbfrgvB{22y0f2q{N3ILkX&^&sYcpVte%4l*k30Wua;Kw4oyN-rXN{dYC< z&vg{^5Ey+Wl1xSv(JO@XLdU%%lbxxVXI#FsQao1be{S)pLeE*d7qED&ypSjTj>W^! z6rT6`Wf^K3Z+*R;ucPzudYp$>2tWDnX-uBY^kp55Nw)7ajmfi~&v@B>Ppxc86OEA{GeY(yC4bHpcIsWqh>59GDT8 z`9IT@yfDa?7(ZCdR)iICbi0tX@%_Su#DhAz5=V-7J#;0U31x}U22BZs4txzBGzsqb zM=Nv3r5aBEC2+^kmI%p;LStoZiC}x)&Q^`IB{ewQ<#!!MJeQioA%Ko#O!OP};i~yS zxYSjWTu4^C9hIH{{UdI>D`{bBaP<-=y{BRafwn|OPCYjO`~Vq1m@wgJOWqCM_(I_$ zFHKt#ia}cv2+MlND+DyXDcs(R^JVb=t_1B^$RMgDczr=R1XqKGx=0+kqyyxcvp5b; zb6bV)wQ8VJt3k?bLtt7o>YwuY4eJfNC18n2V71Q&l+C{qRj+-L_F6d<@CL4-n}BvB zw9!na-G)rI9~2&}XiJy??kGz@UYIdg-24X#4EQ|;FfXe>|XX|TM zuU@O$oWDY=kzAKs{1Rx@fbh33q%ElzLu!ozfqyRTXiETO)gWz2EJ|^Z9{yHnp4U-U zNz#^(b=(!3{GFXm?G}6o!`U(Z)=W6kjwI;1)jowZ=iK5dZqoB4_XZ{bJ3H0&>Ai=X z&N);RwoPslUiHWs0M}^zqU<^4`4aDAzhE-6v1%K zJhF@8;x|11$^VJw3ZAANl@&OM+Yn83o)bS7K(r+ojs%wpw}@fnUkfxR7k&~>DgnO{ zQ)k+^KNQx{byS;aTd)dfuf%nGe!yMMB(=)Op>fN7lB{Ihab(8kx-C>>&g$^!UR)1d z`3IpZht7_4<+Oqo%ugl+vdUL<jdSR%j1KY=HI{z1kDbJz{kp^x%Y)~RFzzXA0PKeIijG zF!*(dIk16IG$R>}B~wf(iQW7p-_3QskUsY>FG-(^nXH%>56|6Ai*?LV!R>AcUrXfHW0Ru^~28uovuD zP(1Srn+?`7(AtVBpjgW4+ zdHJus{W3hPD~z*S|9*qo$KP=?Aw#1Hxw@j;pds0(rbTuq#C{_oK^}y6ew#Xk ze0d|G5${eeE}Rg3b;~e>k3)FsWFUg-+fTsX5&ne9(<` z^_mk>dpp>rPAi-{-PW3X272U|ILivB6}!d_{S<`*Kh|z~`OM0G<0tPY#2o~7k<%-R zr%$%8S_XQgNdh-hW7#I#_3#pE7$h7nu{9>+NgydF0m5f_Y=Vjq{`19q2?=O#gc$aK z`c-I%O=0-Dm~oG1+%z`4<2l0lalz%Qh8sT@?tb^i6eHx~_W|{74dJyDCa`z}_aNzu zly(APb0$r$6qzWgEG(rWJQA(O;W8Sh;~+zT7`0Kf5PKD{jdcSY0N=H2IQ+eoOUICC z8ZTpMYdv8`f8y+A;`;XMn@6@1@;g&yw58BCPC{=N;q(Z51CCRrPTB#k4eTMA<0!PS z$kPe>JzUfi&D3r~l%zZ0hA@qY1Gba2f!&S_fIWy5!7e5*!G48&3;Q@Z1N%q(82O0? zfl~-YtI#?$3U*`K7r*m-m)?BR4c>=CpQ_H6nj>`n9o*dNk; zu)m>5g&wEJVV|HUk>}I&G~gfU&#*6`DJWyC7VL0_GO-4%0qn*s7It&i9Cl0A61JPU zVS88#>`aEzvW~0^?0nV-c0X1CdlVZ5dn_9VyO>RdUB=2`&tmgoFJ#xizK*Sey@suU zeH*(C_FA?M_WkUB*c;hK*iSN)f^A}(V86g#fW4W$0(%>K5B3M_bJ%;>9@t;8uVH_~ zzJYy+p``pi{xJIAWBggz&-1;ozvdSRx7lrhgxZ2_!LaMt>cEZ^J=YdxYlQkoTcUkM z8;1c3kX(r55(F-9lRErU@M;QP=VWWz`Mdl*{yzVJf5<&*lVp313WPhbpBLsk=xL@gP5gQRZT}))-FGAJpY!3POrdDksT8%A$5e zxo5)P;*y%0B+EY;bmLLNB2Z34{Y-*e4!abbCITvx_1GS+uOyf$(AzLo&>*^*@Z9{~ zgURutc@?E(Q)yvk8RXryBsgnCo|Ej!$r|h=vvUT*-qR~<1nfTjdOOMZzP)o%f`R?| z0E6jB*aLex(S`&PJy7!JkVyd~6#b(rE~FY>BiBx>C@dn2N=qgc zk}EI+mXbei5Qa+U1oA-n zOdMnteM;g)n@}-#(9>)Rq~9_2ouWm!O{OK%6Y<3i8%FGysTgv20(K8UDQ7G*U!9Hf zGGB}|R}uyFBIYa+j-?GaPWeT=xTKz1+2I$wPqK5GpUQ7-%;_bl-J?RL18#}=XQP-EG#^n{L6~DEvPo<*I|$i-m>ofA z5IxUgBy|P5!!HJvwQJVB%3p27pga{h5H(LB0Yp6un-I*1be!-@TS#5e8&tehz-=Ir zqabsUJIt+&V*Udp=2r=!Wf)|DSR06$T|q<1aN2|R0#89y4ik02rZTf+DWehl_c{0QtX_)*w9CWuqCG#k;9YZ z5ZOaMBHJxl%u|oYmS~FLVken6i44h2x=E0 zBBk%V&>=ur4*>V27upTz6lxv4p6?Pc{|vT|=Ns^C;#*-q#<#(K+MC*I@V(Aol_6VX zh~WIP7kUwp=!G`^j2C(e(Ca$2Cj`tNhy66DYJ>li{AvCIe+jkvI!f?4-@^~`<7lT- z2-m%UZXhlBQGSFU=o+zX9!Md>80_L8rA4cr&5dV9AlK+5` zo#towk7)6q`B{FBpXa~uU-<>OsxkXIqq&eYZWyt~5ppI{_IAVbp`#7L7X zHPMdGVg!hM)|2^cF7w$^$}}6+S5qKoYfITB{g6D8hMj|aytAab@@9_5^G(P|(grubLAXMZ+&|_YbePk1Q z$+O7+3m8GFue`~(^S7|_MA|5C0jTR^h8rbUB1tgF= z8)hrAmD{eeU1z)9_K@va+v~OuY+u+8+fLhl4+spX7Z4lJIv_0|C!k-z$bh>8-nNI^ zo7fZW$@VVxUiM-374|RehwZ2BzXwJJIs@ATrU&K*_75BtI61H~a8cl@z_o!J0yhPo z2s|GY9n>PID5yN>`k*_4HU>Q%v?b`hpwEL22K^9pAviraH+XjNlHk?B>w+H)ekS;} z;P-=f2OkQF4e1{;Dr9m`kgXv*LiU6l2{{w;M`%!JWN1RDCp0&7 zaOi~4>7nyOSBBmax<2&r(9NOmhVBYI5PCB7*Rb@k@nJWHeNn4%t>(4dwK~`8S!-ym z@wLioZLPJV)}C5NYMrU|N9~~6k+q$*+tlt@drj>ZYHzFkQSH68zpedK?Tg`U!qdZZ z!~2Ji3ZEQa8NMidRruQQhr>6AzZ^>waDL zc-^!0Z1p1QMb~Rl&r`2!z25ak)tg+evfiS4tLm+-x1rvqdau@dx8AOL2O|4K7DN_D z&WJn_dA@#H{j2N0QUAmGU)Dcb|6BvwpjLyv4W4VTwP94lgoe`_&TqJ?;f{uT8XjqQ zCW=IbMMXumi1I{rjp`jWJZeJJ^r-n!E2D0SS|9az)aIz|Q6EQr-Ka^Urj6P+n$l=X zqxTwp-soVX9~xa~9ME`PWp@=TL0 zP2O$tS(E)uzKc$d?i@WM`j+T>qd$xOCi?rB*qClH17pU-Oo^Etvm|D9%(|G3F+asz zbR;=CIl4OrJH|Om9djK^9XC4ea_o%_jcpj)G`4+ghuB`R!(xkK%VV#MT@ib8?7gv% z#l9H(X6(+`Z(>i!8qRQMth2Q<&6(ru=N##r+1~;wWG_GmerWsB1nht6DOw-d%e@_TZsFx6% z&^jS4AtzyW!jgp53F{IbOn4^YwS@N*b|)Na7T2t8vy5hW%?30Z-K?b9tY(XwUEl0* zv(wFfZywk@x%s^2%bMTR{O;zDG(VQuFtKT3`@{~3-4X{Sj!nEHabx1siCYrCPyD3? zZxP<2NsAdRmbSRF#iZNvg)N_G`FzV=Ef2Kft=hLLYPG4=$yR4ulh$Fa zD_eiihPJ8Ircs-vZML`hxUH>iSlh_9(QUKa_G~+_?TEJ5wY|OV`L-9^x!a9zx2E0B zc1HVF?I*Xtqx}I_Q&(%(9M_w!4_sfkzDbHn8lUuF(l2h0`zH4t?oX2QlOIVw@9FNj z*K;%_K4o~ybtz|4JEu-dot3&cb!S>gTCcQQ)80!DNuQcNC;h7Q>(al@aAu6ixHIGH z%r=?nnYo$0Glyr6%Ph^Do4GXelgxdY-({Zb(6&QHhrA90I*jg6(qUGI#T`EC=;+w0 zla-QXZ6XtF6;KJ_p?K@GqSU?CuNss&&j?$``+xwavJ0~ za{A^h%y}Z`cy5E-uDSDbAJ09V*DfzLZ&BX+d7tL(%R8F)Lw-_zMgI0~tlQXble*pA zJ)--R?l1HR?lG#z`W|2R4DXrK^O2rE^@{7&r`PqpzUUp^yHD?%d*9i6U!RVBy7%ed zXH=gD`h43rw(pp}clABh_qTrbezp5G?ib&$UBAqJ`TbV)Tib6#zfJv)_4~P>(Lc0* z!~RYCx9{Jf|H}Tm`X3mOG@x`q<$(DEmJT>JFm~X$fhz{yF!1()_YQn);EMzI4~iO8 zHt5koCkEFWoIQB);4gh`#u_3#LHXk}`=!T)63=17LV%X|oyM{Ly-eLHX;oFCQ zIDGf;Z-yTq{^Ri93V18z23T727C|Fi-eZj2-cNc6dc%tC>f-MDa z7wjz9Q*f~0M8VGme~btiQF}zgYJQm`MvbT#aodPjN1Pc+Mz$U~Y~+TKr$+^hiW@b1 z)c2zUM>idvF?zu0g3&WZUo-l?(JzgDWAv9}_?Wh1W{uf0=HS?Xu`S1@jLjO`XKcmT z+s8gI_NlR-jy*Syj&qEgJnqTD)`brhems8Y_}7ct6|E?Gw&{yh4o=CR^1zfIrVg08cIq#s*`<$`{yMGw zw8hg-lyxo}Ty|606J=kP{Z!tpe02HB@(;_;PLG~GeEKcZKb?^|W5$ftGoG&quNYHt zW5u^K!)Cf?X3m^6^MRQ^SB|PYF)M1;uvxdw`hIry?D?}_m=iE(=$u7!9-XsuZqVE= zbBD}bFn8_TXXoyl7dEfyyzcX+&AV^jj(NXa(e8?gS8TqLT$z96$Sc=e`P!8y=EuzM zGk@;V8+>e)Ybqe_q;Z>CmN1 zmOj7q$g+rKDa!^do3d>AvL~0lwd}X$b(gnTK6?3r<;$1fuzc<`smLFRF z{R;bv@D))j;#Ra;k-VbgiozAkSFBmFZpF40Csr6MBUg4@IRUE%u?`WtHiC7DSnCMa z7IGUFA2xUbnbD(p&bDr}W=Y7r%&WD_jIG=Pr<9y!v zvU5vZy|@N(@o~-L+QqYYdwh6&-T3e36J}7SXe&Tn(8ox8>FvL@fII*M3 zBWXltQB)vYT+gJ8he}V!Sc?ITBw6sXv&-GT1d0h z02y%ouA|25Vrz649%F)h|>d16tJ9;}t zIL63%agk%SV~wnZ_0B-lLS1K+Gur8twcvJU=vt_BEUb{RbMb+TFVVM@589h!0y28SBr2TI^x~F_ zZ(rPh@j;VDy3b=>`Z(E0HjsPC-DDlP!x&`b8C{KbMr%U;{0^M+{OJFY=dk0DIQJlj zlg(&{Ll2i9ZhyG(p|=kw9eNnQ)rUSHULg0=v+l;1L#d?i<*#459P!82n?iBJ{>mBXZSN%t?RlHbXd8+ z#<%hv*sxJz)a+!uo)|0fPfZ1WX8+7%(}YETA%AcEH?#D+1;R zEDTr_uq5EdfO`Vg2fPsQYQVOD9RWh$OagY;7vd;osr_2}YWte%QnN1+Sot%6>*hOc)`tlcp`W6Av}YZ@q73Plsu2;V>~ZIpYFul@=V^IyZ9>XAFksbF0>sE zjGRP_ogSnoPC14`+c6nCu1YeSEJiPQkUT`5!fD4g@+H|zz99$5_q+%1zI zdW4J94(f#NtOHIqu-l=duzx9`Q*oYg2fdTtMK?eiZKfa7Pv~y?1wF*O@gm-x-vnus z%%}1Nd>UWOC-6}?DjH3H-~;#?-kFcF)#Bs$4ZJaLV&nWSK9Z-}YV$_)6dwqA70PSz z6v#pbeN+?Z{u)6`(v)E3m#{N? zjqaiA=w8wgCqA5E>8ErtC(}pCT)+4QGW9*`vkR0kJZW>QI)3zj^rjTUX zl=PvUNiW)w^rKx#U)qHXrrpRu?DLCp7F2{&p#s{QOrXQa7}^sW|4DQtDWPNNSW-%H z9zcu8G&-J?(+OlIolNFp$9n}WC-Z0-okkYXS>z77np{oilH2Jj@&H{+9;SDbN9n!f z5qb~VL^qP>>7(R1`UrW3J_LQ$vt$c>hP+CjCSTBx$Y=C@vYoz6cGC~ZC-hzNImHf@ zent+_FUetYk{%|f=uz?mJwk4x%gG0H8#zYzL&M&l#*(RI2AxRekOi~?THD`fHt9_} z(b42CdJ}0(LrDy+P41yL<1822JQ_$k($*xKCXtb}FBwmVkZb8xWG%g(JVw`(C+Pj; zZMubgO5Yxmyqx10b4y=U7QF-*y>{^A7BfzH56w> zI4Npi3$amKs4d(UX0t(G^fdP8LW{E*I-SR$AA1Qq_7~`H^mo>j{*3vtF*J`3%nwnl z5pyz*&3`Qxi8c>LY$_{d z({O55i8HggY#zG;JNJb+&Ag6X&sMS3>;`r-=G~icW_CBbhs|bl*uAWr-HwyAJJ>pQ zC%c8+%I;!UvH9#8b|Xt*>sd2)A7-rkSt5IYQMP~;vVCkk+fUE3>Clx{&|lb0%J;d6uhcVMX!rHM%S$p;vOJYwz|N126{-=QPWeTo&x zXKXmzMbEJrSc`nlMzGy%B>MtqkbBr@_9Yv`_TnV+D>jaOjoJEf)`7jqif~dmfgK0~~gfmPB> ztPpM`_tD$Poj7$~M{gjH)BDJi^a1iBeVlBjPmrhSgXCHIFnNhSNw(4#$Q$%UvWM;@ zyKpYMpMFlhqMwp|bQd{7zapn`K3k94NPTL@>BJvcg$SJ~PUbNSg^@ooRh)ws>ldul zeuXCZ0!}E-lPvzAstM{(t%_(y_(!emyu=k z3bKOECoAa!at&QbmeVWA8oGkqLa!mW(rd|W^g41gT}j@huao!b8)OH4lYE4kewR|04&X@2z`D!j+NjOW~>`M~=-!jk} z+k{~o$T052%I7}PsYdfH85WR0hOuvrUl?H1K|His){z5*NvcN5Ju&I-e3fR8Y- zNaH%?3fO9RsOdLO@L{At7@W)uvn2u#x2oYgtKdW#{|Ah%0rD{qdAkBevd5KBOh9YS70-KVx-C z>VM1d(IizEjJ@8%K+3kO$soKPbHNC=ia~p~W0C(+(+cn1)Rv|X;jAap`YV%;@}wK5 zm~;iK40QcnV0tTK>?OyI6EL5_oKlA5C>re7j$>7OT+-|Z90_YepXMq3ABar10PQM3S;I6c|7BTRUUbQFTXOZ4a)jIVEAg}L-!NW{#a}FwwNry zDKI%O9qB}4KRt+5>17&>{oZtp@#kL|mPC5|M-3;9cn{LNs=?b`Z5ShsYBY%?${L38 zdN^;%w<9b{|r~iWHC<*`MH$T7l!wNU&yZqNG5NHxh4jtE9eA0 z^2CS2G&KISm5{o&>wyz;TFv#X^qly+nBVvS<4=Ab;rEb^yeIhYGLG_BNE4a%F;a_v ztn+&fXzqo15aWR(P8|3@1k(hj74jl@=NrFyW$7ByjIV?LeUd5i2r~@uFn%YA6h`Dh z)`gf~&Xc-g-j?&|V<=Av=1;vQl4}*zAE%^~G-mxRf-E-YR_ok(A8BRQk#t4RFUg%n9s$fu zF)p%1(x3H5-8n#`4X=Y$?vJD?>y9wf6aucTO=q;{&47Es2;PEb0C<0iJ@2b%k1*m8 z2KAiG8lnwbAP+-He;x~y4q8Fq2xc$LM6^pWX$VbCf4uqPfk~z(@W#eZkdHqZKa+FD z2Q1t88THs7W$6s~G@E2xL>{AL+Rc$p2c6#((6ogKhddsMd=vpb1~U$3G5CJ2^JhnS zg=s-t>{--7SL81ZCL46UVJcCdFnA-Rzo-iq4csueqP&8qD9__?Mcu;iNpMx&wTIi@ ztUob_c;L2q$lf*c*98w%q*B}4Y!Be52hKZW=V6_xWJEN>&=O97MsEW2 zl0`6u7(3(X9+J^3eeHxr=EZUpUZBDA;P(P$cjclTqV-;RTxvzW%yrg%@M z8MH0UX(DYwThdl2_508ow}n2m9dyVpnuN2sWSm2#kd~ws`IV;9G`#trfp<1rLr2~b zTG7tXV0D3}rYp2FSLK?hAc%e>wnq zp+k_pgD@8irbFmZIt>3HD8T;*Mnckz!h2#LL*x31&=x}nUr5J8TRe$Qpv71fe?}+K z$d*44m)0#r(Li2qEy^_wS3(!KT z^eVatTCc^>nJ=MNLo>dNE{7f}jjp8E;N0;#x(eFcbm+-eLvMTonGF5fZh8~6<~Kus zek;9=-cHxzL|~mf6S$k+L+>RS(0r}OY32RUzjc7@{T^@Ay+SwA2kArfVZ2NED0IJ_ zpqKlWK0%*^#(opYg)aPQ`V7wXo}M=`ZwGoCI9Jc<4)i7usd$aD;vtq8vJA z&TQl`X7&IwfZ16f3u3`6goUy&><8vz%{7qBW8vgV76DDP&_hSE`eZ(I(G6J?xdJ-u z#?Wa;Yn`@JYNwk*quh)@pG+1&pWK4ABu|q;C@y7KJ>=ZlA&afZZPwZ!QmYu^J-oLP4*#-6+``$&&l~WDJc>6GCvJGtForvLEO+ua z9*>jd1n7#J<7BY~w8pJ?Yu*MYiCB5yBryr6jmbDkOyQ|K4d;v*IAQF-JL0Udv(zkS zLARWPcNWDdWH+4p^}xwtFX)(srdjBm2jDz!5Kd@^;G}XGPH78pW;v3N;-mQ(J{Bi{ zh2&28j(>f;C*6=lK_}7}Cw|dpQ+4)Pc@`VZjE=lgMvxB}XKJN5%z_$?%Y-%6e#&+^;&?R+iCCgQ&v zW5_uCJD@AsjuXe{$n$tp{6+Ezd6c|HUdK5=2zij-fiufsvXU$(*WrI7tH{;RsNYG( zLff;1-$l0ZyK#bhFImdhlPx3=Z+qTPe!*KA8~8^4Ab*HIjQ2Ah#aY(l{0XuQr&>?( zP2@J5b3KE%X={_ycrSJ{apL?lj>KXHy@lLN9wLQg1Igmgku~Hl@xCg-`99gmUnFZu z5P6%tB;Q|s8RulL@+~+Ie;p^{Z}4q6Et7Av;%!*-e^WceKd0~p?Z^C+YTkAJ3hx!~ z!@JPm;9Y3-*7Fg(OZ+X~kiLw!pZRb6cm4^R zi?KOuu{NhI&K7TLYR?*9Q9Qdia8_AKQeIY`a(Z`UyB^`Xl5%pDKTG-%FGu@z zysUKX&yw_Rm*nGeyUlRTH%sz$xn1tStZ9Ws73F1tS>=<;%ZjIlWL1=uO)4y!RaqRE zm8V>pbb69v?Me^LDJrQbnl)`=Y4O~UoC)QXg+)chWtB3;q#Tbux2O;d;Z&3tN^DY| zOXky+lrHlHzbr4xlVZ=4)nLz)kyNhTx)e#eblJLga&#$jbSZLl?c}K1al1Srd4AbR z%FEWQvds!f4b0O8mKC3*StfY`^Hc?>Qf8YO&bN1yIScLPE3_)9+a1!aN@{MGt^4@G z3dO>m724fDPEwxR!+P|z_wcqnT0m#eZI)5jP>#eSJS(t=&TtQvL3@wNlF|vqp*{VX zyONS+Sx~8I_TIAL?7e-Jnv|Sv>y7NHym?7=>d87)kLnjmIa&6;ibY=w3r~uQ?UD7J zl;<%M%+~mHo>w->-p^aSq`X|+RJl5#T-{{3nnkW+;np3*?bhv-Y&Nm(0y%jB{U(=J zl&OSrb$0ViZkj4rS7M&4R=>%!(2Xl*O)D*&B@2zJ%nR&iZTl1*J4L4D!e|QZH?y>G z=45ZLM-eq=kLs{)caD9as+fV6ib>I>NYc4Z(WOW-Q^>OqvM^26g-=Zl9aNw1lpC9ovecMb?qnXs>)BdkC4fPjHog`Gu#n9 zWXRZ_?ZiI-@sG&R9zNJ!=&dbuhJ3|6J59S<{+X#}%fW!rrZbnN2S=7EqIJBi3>801 z4ifZ2$p<3Y4A*?KWN8qd99XDpv`~xWLZ3)3EcW&zmpesQfGZ=k$X90}Mcz`|CwO}a zWUkB@V(ElkvVbT+s=Zj2+g@y;PSVB5*42=sW98`V=IFF@R5iF=DIvw)bajpCl4qMG zPYW!z_LU^fGASjn*iZJF8I|!}?hM(k7@zs}Niv6_lYFHWNx72T$sv=fr0!ProRpVh zo9vUxIx9)}p_Bc2KsKk?OT6umcF?(Z=LeU_&VoP7s&eIMahId>=gJN&(d8=9BD%!e z>s{{Tw9qO3X`v`8YEp7q*wjfC#l>Z%g=G^;itMGbb?v3zhJaMhv6Z4|D(PfdWoR>< zxn!LcPqMvCMhz`16jwP^luw^rY%jC0QL-8W&6MFLCsV*>@L~+ta&HYlcI&3j)rsZm zCePI@auo}=*|FTZa+A$2qPtK|en7cT&gAMW=b79zRj%%6c}ca(FO@UstmW3eV|M)% zvz4<$%P*1LnzKiBbhkU#j{gT^VPmhbRE!!9kd8XnDY_I%I)5qo_L<%>mz0;P3!j=6 zIAHFIQ|$AsGF;C^`MNJ9>nh97uwNmQ3Av)mxXwrppHy5i4dZ2e z>CATGpWqQCl5*@6Q>NQX+s&Nma)*}qP*O~I)cox7sJ4cyB#TG2waWw9E5wN>L#+rr zdS>xx3FOI;xkWtFm92nynqJFxk6KlNPL;}|CUyALu=nT@<5A0D;MID;qgO#5)hU3N zgA=Z7Ke%cY;z?3H+T+&gYFX!T>+-mDIrK8y-?&@$K_Gm0=RPShim4~=qF?gFlgpJLXh-aUBK#>M4P8y2`qT6xsu3BRs4HMzpC>tD?<@auY1t7w-;t)AiP zeCXA)N3E`b*X797^`O?nE>E5=hgvVgujA+GdQ@va;C1{w9Y0UkgIW{1Jo&mF@^$=t zUH|zyUcQc(ui~ZX@s^^;S&HuGDQ3S;*Ys+Ehjvl?QrvmrY8<-dkjB*!%I`|`_h+R0 z`?IqA{W=_%PN?`g9Ddc$)Gov2Q8N}?T|Om+Tpl&cKpyCJ(^ATlVaf^Je#yE%lC@v& zC_H+0^62fBM^E3LWLe5+FCLv=y-)J! zIoFe-)6sLTNAI#cddBvs9h%FdCreMNPB%@LCrziLCQr1J&PRr(%h2g&==^8tdeLib zk6N>%-)TN-t&Z`pwKuiG{&{&rxY0Q z>v~cO4)}Gw;W~bv&bL}$KpyCJ$k*}nb@}sky7@X@zKWNk=iL-N-=^q!F2$VR(lxzW zETCN!zZ7@AmIulH<2$9QJjn15&&u}qXQ*(WJW%1LJV?&)1Pm&fG_6odhh*IwlCyJy zuPCl4Zzt}U34)GH0hMzUCZuvQ7Vr`mJh6OMg@#IIYr2^wb0sQdCbWHJ3M?+cb!CdM ztVAX$U~4AwZRefo{(9+i?>Rby)|OOl_SpKqU5QYLl^GmDGL%O*&wq@?8F;+d7$ z>Qxq-7+sR=lv+jQrR8PQ&^sy%E9TiIl$TAi<;|)nSNv6txLs=f0G(d0t+b>zU!?FM;1AayD5nKa=&eldP9h$$FWctd}{- zdYO`}mm|r^>0!DNWQ#;-kH}k9ghwJYd73p;HjG5n_O_1ldMn6EF55{WG_!1LsBAH7 zsM{JUJ6lzRUsh$et4fh!Wtd|P%|TIBVRU9vt)XIExT+%jk`olkRVnbekBpL zz1_$fE_;|nM0vw~-K;95Uy~@0Zb8}WtmLxmRYjzQvQIh*e0EY835J z?Wa7+DV8|N)|yRDmmwjtiwUxMq2404E3c(`o@)KdV3Vja!bc-SM!SBsWl&OfyMB_L z95ZfHAE#GMZoP|g>)n{!)I*wQL3+M&>s6Us@7COUwdKxH!vf*H(tt>M@FpGZ;=@g6 zxNA|~#TO~>;)~RG@!7;{LU{9v!=ZS8h)*r9C?g5e3M;0P=xI}@O~qUF;=LkqKb~*} zjiAIIbX$e-^SB-EIL>kH1UG9U6nybzoSdjhuO zuWu2OH&2Osq|_}^;tna?KxN(N>=MXTqdO(|}M z3&ZJrnD>V^GDMpP=(BHeW2l{w#I4mD-1z1_BNvnbvOnPtI64;`8PbRZFGhY8_xa&H zHalT)v*V_thoOJ5KtWC4U~drCbWGFX3M>D^Pw94G@y=o5;Qtvz6T6e@3`xZMh5sW4 z_l?Or#{6%CeV2cL+h9feB+7fxTH<|$b9i@7NCA0AV6&g%HxqlpI1v=DT?zh6M6JP{Z-MbfZ9Hyaka)cL z8jrV7B|N4E_;P5haq-@F{6NVOtZUc{%h}nwrwJ;;r;}J2$?8uB`~bcw<;R@fclk7Zn`$ z>)*qY{-?j8KWU~F_iY^DxC4S7@ZK8myAb+O+`Edaq?K^oHXr^U!xo-?aRvTWm6jj= zpQiN!_-s-+j(gMsZvcFc#tV3@hIhrSiCZPhwj5Y}yGY#fxNSasoP|chixmC8g}(x+ zRq8wl7;mDBM~tty$r{GEs}>eCu;;nqFcq>l}P28xsp>h4=dV+IqT<6PxtJ0_I zxK?~ka83Mcq8E6Re@$`SmOQtyzyfc^wc=}nYvOOk*OX6d9;`IhxK`LoQxn`AE#lN| zEoI21a9m@QF;drmZS)~MU&IB~0D~ruDL#HQmw|Wwfw+1cInP_*%fbH@&03Y8OL$uG zCVYkv=LtdYJmR#%`)j}pnmrc!>fleEJDhJjw@Rv){|#91dG2rc)Rflc#Qmpvv!-Q* zYvN<2c@p*Zu&5EZ_ru(!;oJTpd?Vi7XC9>#-01G#dYFeK1$9b&C<(& z1^ptGma^5{vJ{7P_!>ww7FNek&H^J#}MuAsdXPC1pY`5Sw42(nMK+?mF{Vn#a@W-C| z*I{Q>KF1YLE50W9a?+J?HP6eTx6;&kk3B@ZPwdxTki_mr{d}zavF}^(+y9T? z*J3xvK7%wKk9{y!!0R=<&VskX78;R**jr*(OLwIOUh;3iHSm#X%-3oCJN#!OZ_@=g zxKk|fYk((c{5lIP@MAQ7xCK8jws&m49){$5OBPPqXDDs z#1q@V0{<0$y(GVs-o#tu3fRP3_&ZZ$Bb=_-5QN&Ctu6zuLhrb!<67}G!8P%(i5_?_ z{~F>teg(}>Di4m67FgiFtp;zUvB3Yh?3O%>JeYOp_*J!us80v>kLsyXx0bkm^>4vD z4iMtlt71E_?{j=q4g4!6;nn`7g$; zjkrNNF1rjE^f7-ZdS$CQE9N}*jHdo}wr~F!b4F3coIt!IKKOF4^Pf%pT=jzPNWiGIzmjF$YE6o|Ds`kCm*g+Kbi==BO8 zy-vBpRxrN0u{!!=>0jwrhn9AUM!z=O$Wexv^A#6syErx}oUk1md~oz^3p^cs%yzNV ze~bl=p5m)>Yk8xmn0%sNi=JS@$jcaC{nrS4H$(jaJ05q+Qt~+Nl%nKqiI0~yH_jon z3GPl2^x{2(TLm|Q8)IPa5jnv>;{|+C7=iOv2a7oFq!SiAa$S z)v(B6KS|R>+VL{y1pm9IG(o}%0uNpyt#k5=rq@Wy5{Wsf^9Fj{FfG#EA~}mM`l0L+ z^EgMX($3O8BJFw7E)_NtJ(1G3v@>iMO%#-}268FBVv1W!MBHwYK3Bmqw6lyQz7tDn zeG!TqfyWD2VoHTgPRq0Z(=vyr1wC&36**ic@VKT!wBs_#N1fdV%FvAxzfs~hN(_@Q zlW-#$+DO5YPb1trMQI~Rvq9l;%Qhvea90{yO6FlBS_814A-~GJtzuUKJ|jaf$Q+)L zp}36%_%9^p3(2jiqx48Y5GWk)z-tF$$;UjldU`CBCDi z6f+g29R-GL7ir#0q+i?SM3zhhGbzf3U zX5)U5x#T4>Lw>>fS==>p2KQx%Z&x3O)DI_y{F+06_;xjJ%)+;;X>IZCYTTQJZ&%~K zD}1{ew_D-c)wtmb->wFPZ&%~aDtx;d_f6s3)wtgY->$~ZO!#&+?peaOt8u3izFLiY zmGISSd;$}u zNa7}-Slj}1HSR$Y_W;Gqdw>$;JwVNI|IZrShJ>$KBPD#z8h0V#Yu3063173ueMtD4 zHSR>h*Q{|5J-%j*o00G}YvcxBv&Owh_?k6tLc-Uqao>@+wWp`}jy3Bo?kZse#P_P% zAo0CwHdx$H!iI=1JL1kGv@pKS@V{axzU08@B0s||ejDze55WCk0TexrPX3=U6nP22 zj2(b`&jPR-p~#sS{{%Ob2^$g(?^nJdY$3IQ5$h*{yNm?}Uc(SF7r+sW99xz5#P;F~ zk>cx^X$ZqNczZ3x8+xqsjp7}#yyz|`aI<88$>sm6h422#N8Akf*N@@d@hCcDmyce- z#u?*lfvb*}A=Ym4F@7-)3c$DsD_FkYDDe{atpbe;Cdv@=Ix)`Uw#>_Ze6i(MpNa81 zI4C|Rs$(L<_nIMQjiA*+5bt{vZ@LE7NXB8W?*|1L*!NX^uzLRQJoWxN$^2z15q}wK z4HT=D%YLvwA;zDA+}8t)U*J1$Trm5T@jLGG6%-dli&cFLeB8y^5o78wan=y)0Ajo+-)2YMTDbs^(P+GD!9_~yGorQ(th0=DW|)Dsi}4c7 zlU|IH5)SjmQ4=pYTr#$-p;ckQ*Y_BQ#67G3{$rN4I+;}y_bUCVCNe*Kb?qYv46!Hl zdqis)S`GrzG6c8$|L;9&Y+Po(E~nNmuT703H6G@-)kI00;TSyBjQ%sz2F&m0;B*%&mkA4;opI1)l5qLz&I3JyWYRmKZgI#z)A79;YS;lUnp>lbf-} z#Qt|(Yn}|TxAZ>7HZKUkxF|~U?>v8BLf;Ib`x@36Vg`_@%hj22PSuy$;%0tCOso9z z8wGy-QpA@$d|07%!oQVFUR7P=X%eAVcdr}IsXCGp{y1UAv*JE#aQqrH&jJWCwhF)h zgIGeEd3!u@;%gRx4wfE=G;XTl&vKo`_vM zF^6QBa#@Th(cW^Ed=#!|>$^lO;|p*Q6c|~?4pR;xN1y;kN($p!H6rC~ z$0RRrO$#dZnEfB?zAsH&&8}(%zuKxLsEz%WTBs7;M{Ul#m(J7R;J>caJVfriIeZ83 zaSQVh-1cfdfUt_eY%d00%Z8n1%`_HU}}qOYO;#s41FgIpm8znCS1Pt9*;xX%?M-8%>SF;&_a$FVno|9YT-#ak zz7b#Fj|IVf#nn7=A0+Cp%Bsh^X8c|4XDs2~cCoTUJ%42xNwMH4dZO;#);XCkp)8D&DsLCzY5Xy=Csf$Fv<@j z%T3(Xt^a(Z$Qnbojkzm8|MT`iF{Z0+Ulki8>IYNm3XE6Yt6=P}eXB=}5{_EG))Vc6 z*1q5isY$;0DEuKgn#zIm<`I}Mf0}>aXRdR-Qgk2kEqj&Ro635VE6=k+-l|z&NHRHJ zol$x7@3RQiquk7$ss&wts{M2jcvkJfmijZ}o9K zbP2{c##@qFtUoNh04oIH6aREpk6fQ%b>P;1izD+^UA}E5(Iutw_C1pVIZ?Sa(=+=g z({kIFZ(kJ5E>;gBdT9O-(e_<3*m$-UJ?p{HY+z_4>pg@AD!2C$G;}+Zv^61okB% z-Z*~)_}8QHs^kcbyCs?xwxIt7u$q2UEf@42Q<|t%uu0`xfhdf*|3EF7eM9R1OnRRy z_vcv0s7F(CwKp)a|Eg=1IW_YTlKj8$h*g~VI3-}yDJz+3hAG#FYb16V-p9YCL=OtF!=y*w>uHZuc#rWdVGagh6*IpdG^Ye~fqJ z{Nc-oL(0FS(7%eEk4WDehCS3#>8tD2CkbgNbST!^5GXvx`!Hg6rnCcMe9FEf^Y@$Z zsrtgq{->CiFf({lQXC`~)#H3)BU06k@xHew`iMoUV*l$~2dH?_)yij)w-YdGwTKpi zlsJhwawqn=A;wl>6T1Q9YwUuy!+pzJd){0c`yi|S5T!s^B;x!oX9uUu_aRXl5l2W& z*q@@UOw2Zc5s%gy=Stoo#3_-=5_fc%PlN=_dBa;5T4I@a?^rgYX;<{UE?!)`f^;3p zy{0!z00UuqshygH1n$xqMPdJeZQNre_SU5rDI~Pl^DhJcCJ%4RUXlTCo$Eeo&cBxs zN`z%*mHz7I-|FKEtS>d8Tau4=K9qTce&cQQ9y4sa`87Aoj#x#zO#71dsJSMt!GABz1?YplCy>C{fWh~VjKfl!B-Tl)-a;-JWhzP8$9101Bc9N7 zS+zM0Bw1K>vm>c1%lnvOBx0HmQkUn_rvL9;;{_|lKkdENxc>we{nK2vt9YV+NQ~3M zN31TK-Z{y)mhzDZ$)lu?btO{UQfCfuFy)hI6D3(1$<;&)yg&929#dy3S2rhAM0uv{ z%^5VvqGud3`;};2N$u5+R@F}$+vFJqG2R3vcCec3F6qkiX^bbyU$2_5f~xKjqt0Ay zi}}HCSMU1tdlmUJ{)0EGV;)|7_SNZ!!#4z_2Rgm!xh6iQ0w~MxDsElxtzRem*MNN583b2Jk6R{HRxq z)QqiM$-`SCSRptB9_Jlm?1|BK(xjId)kEuOoXWq{-Z>k}x1>~Mc>@-C$+K#@s z9do0X+QP>i!52Wu{aepMMQ}m~F5q?64|`V1lj)l8uPxKS<=uxj=$Sv=;oUsP1 z@ZSc3Mww#{r;_m*lLX{>adMB|H6mT1Ki|v8f&>dUm8<{h6x`slP(+cvp__J$RGg z6<>}-3RT*7mr7N(?-m8iJ-}NUzSIf9+bDlp+V?w1lkI+F=9z-XUxYV*QaWMocOwsI zUtbKBBe5^U*+1qt<8Ab5b$*0?jM?^g$UJk6NaZ?|sk$^rkK(208J~m=Q=V&7X{i{Q1i$$y%;EP9QJ83a{}J@@M8p`2WCpAEYgwFhJ;UzsCA6`>L1y=Yf12T~`V(+C z>-NR;LEctVD6LyI_Q*6*Th^5Zd}`-u&J9XB$o0LCqk=@N3uU7 zm4X%5$m+4e)um<)zZBL|8aXlF&&9f4p4pLbwDC5?2*KV(VLN^r|6ez#~Fg1yLZ=)a5YDT$oY4TZau^_A4pf9JU5a55!a{j`seJWw3R*9 zd}kUM(aQqyPTh}ky%K_NF^l>OgDyhUr&aqdd>8fHxmm#Qu^My>kwSuA@`H@}fVO~9hUzpjV{#+p2fm#lqe;4F>%dY7z$)9zXYw|Kz`j|l=ZIDYTZzKe1UiAT$n5{8C=_gsb z3~%9x5s+`9l?#sXfZBT7+KkuHpHBGv<+Cr`l+?(llpU6pSoQJ`{a3vSD!KZ}UmsT! z((erDz9l3!9+{3wVY;IK={2Lx#0l5j|D$FU7vFxo+Wa9(utgSV=_hMX@HP9EU@e|t ztPsOAeV~?H@%R_y|Bt6?-Y&_5*_SQ}xGY4~b)xZbjnRB9r1x{^_hP5f07l7YGpeBU z&P>qNiWO9i=lU{qH)(H1mha-6#N2tt*oqM+_Esn1ZpGSetFcv4Rh3JkCDl9+_fNE* z%nz^uB*)YbB1Fd*YYg!Ypjvxl4Oivg zi14)lT?^Q^$Q;&H`Bb)Eg~nkOfSo1MX(3lBC-q)O^hP=VA%1i3`XUf(-t)w+F*&y@dl0cLe=x_K*~VTMpJdRQ2!5tvZau(nq=MI zWEp+gTvJ=;lj>p#ok(?f6NeTRa$L;UzCF4L>2dzg*Iy=g<^GGM^k?!HKL3?$^@KuV z>wSlOpIz@S6md=RS*%?C>iuWL#(4Z$Fsb&WPQCwp)Vi|x>()@&`S06URmt7j<)>`+ z3F8@*U1?B6Eo@hN6O)g(#=R6~NL5$Ps@|Sz*3#d#tVt)@0p_&gYK{%*!z}F8B~(om zy8yh&aSEkBr*{WU5T3kPuf zLO4Cj>aaSvgP|U)M~~t5g!;INp&^ULO$@Ot4mUA0Wr?_bLEOji3v10>xP8Hm`xq{= zG?tG095PuaW@BAg77JoI_=U1O)))6N3}C}>tHTI3n#JIThVi(AVFK=GXvQY73fx>$ z$>y+RHjga;mqqLv-0g53ZehsBJq+vEV0IU~hYext@f(gi86Lo058_sak?c|SG;Vx& zmc0aCTi83e@!>t(yD*!b!0#4Lcrd$_NAWmzFK^4!*&{rYcVN%+PCSpj$h+|#Y%A}@ zhq5<$0UybB^3i+(`;?dP61JD$#qVNYq5tlaHEWkOYe&t-;D3S+{1}PFFB$8U=Gbc_ zVkTu+ueSxJ9q9(RJARz>z>krhc$=K#_Kl%{hvV)IMkbO;q&1n0)?uWQEP}n5JOG*v zWFz1Q@#Ewn@+9D=a7RZi@-lu5H+8%Lo!mCu{u4~TB>V9`+c)^7lLO=gc9-9i)A)zM z8S*12f5IjfwNqdq&+KUbc zTtG(<7ad8*03J)n0zVG-EZArXEd^XgXThFL*COs6^bWx5=sJYnN$&*aF8Tm48|afH zoIXXL0)7*Hk<_D`=}r=a`#(M>p>#LhP5RL<=t01Ta2Hg4dJO%N(G&P0Qd4@8o96!x(gS_=Ptpjz%SNIZdM&=o6T(7Zhq6%80DY_$i4i@RB(ew=K^*9{ z4S|nhQJ`4~1*6fo`^0Zk%q)kzh-94WSC zZGlf>Zs3zyGGN?5Leg;aM=H`vV`;#ovkat|#j=oAF3Sa+&$^T5xC^8gV)bEt!Dj#) z1b8qTjMRs)VaV-pHXM{A*(mTC!^R-)ST+_kH4 zDh$1SDrvz=St;qorm>kM0Hdgq^u}!`b4Uor(mc|bUBRvbei2(lV%cK01TgMDLFiJp z4DfQc9PkRZf;h!pD5Ne%-F2iTTg6u4LkickHK4hf-Hgy%*e&32E4vjK+>t{1vfJ71 zh`W}pB`NF zJ&e#t*ds{gQT8}6Pv9P`5cU-AKXc-~m8U`f40{H-dX_y4%yaB{Ua~5PFau0(_Vq2InK} z2q=%Tqkz9<-vWjtBKbUohmbCiLG4HsZ_iz%El=V}fZg0pI`L$lOgeK9_mC`}!c#~# zPvvPOf~WIzk_nlWNpg7y-hp(02i$}A0KO;h z3Ah*U4az>eKWWAX@ByS9AIJxi_IwZ@1b8qX40s41N)jLe3kc^U_z050NAi)RD<8#2 zksQdxF~E=IV@Vz#$H$S5ypWFveG#8Pn2?pQAuI8Ht$X;r&~GqE8!;CJN{JZ=iP;2j zG~{Lgq-HGXASGrjBxWM$u-n2rB`qN{DRz(TNquO@yMaP*;`xZ#MCIrt^wo}>AI7edZ*$l3eB`2qX_AZ<5* z`$qhFNtsKf%xxrPZiJM%U8Ky7gUo#&Zzp{~K1O~&f&2}X^0%dwzrCgWts~`cD=B{y zrToQgi}^NK%3r&bzYOxXA!KnBZWOc8M!4yXiTjmEXWE2D0~3QA%SPaqB?sVG8Vi}| zq^ZPC(;&5(MQRU%yzUOpJ*3p`MMr{u6l8QVWON~EOvmFkoFTLb_uMq56LJ5IjZT8> zZVcHy1Fq-eeSW56{x{ba=+DHi= zCM9^0l;B|&3El-Oi|>(^Pl9Jl37$^R&@*H#?tc1_B+#Gec}V$R=r1HyO7{Op-FpYd zQC<7vduLYLv^zVqGqbBMt$MX(b;;_@vMo!NdzEdlO*NPvdJCNZ>E9&eeFKjcNN52A zacH4D2M9?Bgys+&Nb;yjfW#Q%)$eoeotfR0C0_FV>-{iSIy9?XPp~zmaSI7Wg7@m(I0+i&QF=;`uVE4C9g|(77d8z%4;3 z{2j6cbEG-wf37swRf2j@_4pZesi4itHVkpwFhbrC;2ly2^!R*fK7Q_$y6{Yo)QcX- zPAKFb;p>nzgs;QW0#}9F5egwM`hF$qFoGv&m$3Ap^bqVf*_ANt$|Lyeqp&VvZe4ubx`bg}p1|`@ zN>Aa*r(t74+{Tn~8{>nG`86P9U1G2(U{;D!mF!$j11%jX8qbm>9P) zb75oNz-RveOXK5~CJaln51;)b?2V7xn=)>1=5Tvc#_i1Ec$WhFhHpZgrM%tFxM0oi*I*7~JZNajUbITb*%kbqsEGYPi*D<@TnP+Z%)1n^taV zYPh9o=9Z?HTbe#@X?nS(>Eo8BiCY@REzJ^cX=H9=7`HJ4+{P^BHl~T&7@6Cc0qE>! z__^lIp-V%Zp@vXx>c8NJ;#P=oIs*SoL|um-RK<*2gP z0Uf`Xjj|qF-s$+og>(D%^w(D9eD%h=`Y z3U(#CihZBm3!VKN;P+ehB6|rs`xW*odyT!$_M$@H8>rCtCff(S{XX>a$LtgK5B5*! zwso=>e1P;oYMLM+gP427Onjy@*lJgZt#z$Olu*Ksf@l7pu-FnHTLBq&F?kzu%JjP* zzx!OW^x4#la(L?Ras+z}Or||VCjA0uIkOOj)U&%?0d|k8klpK=%YKa>e}nb{+HcWb zM0*MCcd$_}f%XTqH_`T?y^DU|gFhjs_U@`cZ9_j|AwL^H z8$=sI8%A4z#;q#8FGd?hTY@%*HjcIwZ31l>+H$lNXe-fHp{+(+gSH-R1KLKkNwm#q zThNX`I}&Xx+BUT9Xh)$PjrLu%yU^Z8R42omtAQ6+1MjT{@0x9zIwZGEeF`W8lmSX_ zpmm^ApUWd?i;!KAY^W;;j%I=89B?!T9L-@rLAwv_ezaX^51_q^XWoNv*omBzo4#U< zg&1QY#z4%9`SZ)b5M$C4_28NcuBqUf3a+W(nhLI|;F=1qsqjQpa7_i*RB%lN*Hmy# z1=mz?O$FCfa7_i*RB%lN*Hmy#1=mz?O$FCfa7_i*RB%lN*Hmy#1=mz?O$FCfa7_i* zRB%lN*Hmy#1=m#g%}wx>8$f{u3N%ol$xB>Oc?@kFZ7F1tD2RiCI4FpNf;cFMgMv6H zh=YPSD2RiCI4FpNf;cFMgMv6Hh=YPSD2RiCI4FpNf;cFMgMv6Hh=YPSD2RiCI4FpN zf;cFMgMv6Hh=YPSD2RiCI4FpNf;cFMgMv6Hh=YP0P|yhq#z4UsC>R3;4WOU_6f}T> z22Oz&6nH^_7Zi9wffp2bL4g+(ctHW8ceGKoC1_)4<7i9KCeW6lEk|2{wi0a>+G?~l zXzS57plw8(MB9wE1?>p5Bhj{^ZA06Rb`;vtXx~M<3+;V)|4pEx4RbM!h-CzA5!y@? zn4rJ}1tuskL4gShOi*Bg0uvONpuhwLCMYmLfe8vsP+)=r6BL-Bzyt**C@?{R2?|V5 zV1fb@6qumE1O+B2FhPL{3QSO7f&vp1n4rJ}1tuskL4gShOi*Bg0_2GJ9F%~95>QYA z3hFTj^_YWt%t5`Y8dCJXcO4A7Lclx(%tKty{jZf4j-3mV;DRK$APFu=f(w%1f+V;g z2`)&23zFc1B)A|6E=Yn4lHh_QxGY(AK@wb$1Q#U11xav05?qi37bL+2NpL|DT#y78 zB*6tqa6uAWkOUVbfogzKWl$F+LDVw+F23J|_P)!-S6TzGYXEi)zz%h8(T33$ppBp{ zL|cqDiUuhMb`8L;0oXMFy9UT<1F&lVb`8L;0oXMFy9Qv_0PGroT?4Rd0Co+)t^wFJ z0J{cY*8uDqfL#NyYXEi)kkbapX#=op0Co+KQ}}4mc*BV4N6;3b&9u_opui0Z+@Qb> z3f!Q;4GP?#zzquApui0Z+@Qb>3f!Q;4GP?#zzquApui0Z+@Qb>3f!Q;4GP?#0P!Z; zMl{L~ZbsXJb_CjyXj{>?q4CHQ-zjfM*+I$#BClsrpo4-sP*4X7>Oes?R$4VyTD82? zRsFwS+xamEnV`S}3Ot~|0}6tmAP5SApdiR8_}{B5gvBA783hGVP!I(LMWCPv6cm91 zRE>tV`(NuTSyB`Q1&+mW)c|7!a<4${70A5;xmO_f3glja+$)fK1#+)I?iI+r0=ZWp z_X^}*f!r&Qdj)c@K<*XDy#l#cAomL7UV+>zkb4DkuR!h<$h`u&S0MKag7)8N@1VVlM!O!Bt~xY2?b9$d%6^U< ztBeevE2hP@vbYv+VAmyXWVMMG?~_ma{Hg{SS7ant@zeqA(d=>MK&o7^;_~uV*3#10 z5RI{@$5T>VUQrQ?w|YGuCRfD0@`$R-UZ(rA!{KZnGkkJHjYPeRDih197Wxfl+>pgi zj^5x6XRFf5hNc;Zu2FLoJ#vR>-l0dNqZ60tIy-x6N{lN1L@EbIutzVyLY1o>_zxZU zTd8nV?6#l(lLPOQ9+K_nE95-{M}M<@?rrCDpc>O^_xG1Hxa7e9oC=rkb>RDEf$vR) zyFJeHZ#Zzm*X}2Y?`)s@z4JNXD<_=(-b{tNWe5I;RCo%1KTd<6?>zrP8vG&${*nU+ zzS15C&iU)iQ*!=ZcAh6Z?{c31C=GtA1OFrqzRiJuNN_Hdz0%(xl?~WKb7d5lH#fDk z#>yRGgj}Y|sTT=FBih)|(%KS@dK_VuZkXE<&yzepw=JM5#Mi?NM||DEK#pFV<=6a5 z*zL~@7bG4Q68u;qret@OLfxaRV1p{kUr`^aicUCcY};4N=qZl*C*0iWz<^PG73k5yQtW^hLw$ znTW1O5*xIhe72GQc(2i0#OClHUx@ok3le7)9q%*Pm-ZpNRwlgRtdMkeC>T+-NPgng zsHSO=XyVs-^n3h{tmv+gA{~{u#E9a(3{!MhSpFfGG&*HPEBP<@F1z6IN!vj|l%`wZzZCyH|H#aJ#zdCb)#7r+Y0Q#hkBI?E zm6bO`j2gkYFUEkmqOqdZ7O$EaRDByav3av1v$Cnh@T-A<=8sLRm<287>YCawosQit zXl6(b1~Pm0#15J!1p=uUc%LZKZ{EJq?fy<0C)PSkU9Jd`rGUD2ij%QD`K3N2#)nl z(s4UxN~OwsDY9W2ibO)uEpe&f!tnSVKb^bb%d4*c*6>k7#aCYWEg=Q>{b<$2>*frX z4j(uCm9GFp^q67F_qxilt3>DO-jiO&cdxw9P3JYH=#&qQ2u$ejUU|Rk0Zth{C*969 zvViZy9?Ta$Kh>Ak&mMLPrwCuT&xS+lfTaU} zHWlvlL(+);F{iWjx7Yq0=DMC+WiiehHXL)B%VmJzWR*pKsh`VD`P}dA&!N8}Zk0uU zZ>GR=xm6bMKcvD_`0{ZYoUF2V{)IF+tsVh?$%YfYDSYAh&cOGV?dO4KF1N~}pO4bu zWR(T{lQcM4WdZ*%748l@@W0Oj|CN3s*7EWMsN5)3{0sLj4bk;CmSw^`E{L|ij2HesdMaeOWOl+Dw0T}%ARi{)@aG)9ZypY$d1CtPn3 zR9yn9a8n>%|0_~b{gd64j z?H7N!enVB{+6eypi53Y2t55Tmej~iS!X(d$195T~1S^O!_}%baV8EIiCcBlsQcJ8gyfH&M(}Xl6-e(4V zKXjQ9iy5pS@kb*D(MKh*19A-gH{&!Oc~i|=tfZJT+Dl(Cgo)tB`LVHqysVN|4Cjoi z$D89{H=?ms#SwK}jbh*sdn`oXBNHVNZA~_Ci6+(=2D>G(4hXTo2IFBZc08eTe9Zn{ z6pko~Fh?#s@fRb;uDY17hp8%}AAFhM^YKe|u2?6;7p|8Cd@no4g7>98zlWX|e02Q~ zbmGK4r_y1c#kC}-h(luJ1MEY5q8umSpg~>R8M8;m8~4b+=WzKI*98Q}y@3O)noEv= zzh%Kc1YGBs3iw_N{xr^S{wAfLk1hB?JnxZ7 z`1n+x>t4<|0pDZwLzM=ES1RByThEsR{%5WU1^grHc|jLxLIMB8g1-(t%Vm;S0soNT z;BjU5t1uh;QDM34?;NtP->1J3h zY;2*yE63YCEUE?#Cb@fB*&j{(m&k8lKF3XCpf9$H&m%Ihu&8@oPjY%`g|5LECFzpY zC~^fJFAl=MpT%maU?s^_DzV{UmZEF!VB!wOwuU8L51XOH?GhKnv-r?&vHDlD~vQOBgGsn)mcJ92%2FIch{8u$hkF%mJ1 z2voyPt`dH%Yno0r61t$7OZ>6ar)#^kKuA3G$t9J;ZHece5_AV}5b)P=9*D0T;07EV z_%jUV!?MAt@J_s&i#+kwu-U2bJ@)gkk*VLu+4I>mj^tt2?hYaJ;g}Mn?L!u^>8N@-OL%-Bb!gejF!|33~seGpPupfz; z0#;W8t2E6Htdc8*%M2lNo^BYb8V<1gvJ5?;FiblT8$Y6uAo7{pGkZE zA_xAG1Mf?Fevbq1m9ItM7Vd9VGyn~R>>_Ms=O6La@wydfEZX|j@i{BbSUh@6+u0{CI;QO$|JoZ*r08_b z4JWR+bh2^b;6>}N`1at1>o5Nf$qOHW&kf>B=$Tj8`Fzds0cMcqMa1TrL!|9lTe=j@FgD!60#Pc7e!52B>PO+y`_)T`6p0AKOoX^L3 z*58Sb_UDp+pBazj-(P0-=Tq@X{{5r0zmx2zkDo~UyN#b$el3k2ve$ynPtxFIuLb$BTe};PiihU66!3a!?_2_+fHw)9IDpb;cn%B!5TX z^?oW`-s`~MOM@rn=L3S%ibeL}J?U{*oMI=+dr^|K(E^!uTG=bt}x^ic5?yY4`;r7k_rAC{U+792bL6}bG!=RnV|#rP4* z=h*g#9BZhm5`59^^d3CXsPVx`XPtG@Q--m7V)^ok-Q>QXVwxvz+q&}~aEZ4q9A1De zX6o}G&Qm_kGl))~=+aE-w>7O_!+4r{e%dg8HnD8k#LqI&^J!+(j7*-eZQF^b8nL$) zE?6)^nZmBAE}CE5%_wrsfsTt}3MwcXYeiMCrU1?>oYa^iO<0AlZS=|hJ?p) zK-$4&_*SkfU!mQ5?zs@0bg6*vv*ECpz}kU7W5Xc}sr`KH!28mk-{ZjPd0NY&|4$tF z*0krJCphflJVcQ_n7dYCn=otL60)g65@-^W8P?PY8$9H*$>z^px1=!W&j?3Db=!N_ zt?pTL^yJ3!!O8ZX9krQ0PdKJju4-ytQZs+x=xtj{2RHlGf|x%RQz@n%Xsv4uo5uRl zxl39LN=p1OQ<3}yxkX)-^=+Z3xpHCcqUJpCjN{08iIWRl4|H%|3itugD&Vi;L>Tc{ zz=_8KzAqK-jydo@I`CfNfdhY*JmVSq*=s+Kxk`PWd(zJHCI4#%%Y+41q;U~j#3R>S>8XsV*H40& zcYj1vwf^p&z6~3Z9iU9W50{_RK4*DH-(*pqhf(kcU~9jr0^(Cs`*vHJlJcXM9sXIRCErFj2*-3a(T2i{BL zI?q3A!!bvx@Mml|<{}lo$ANE6gFjDj%)v6Ok4xcy77$&?qB*`cUn0(LVDgUPV@67e zncI&%a%^Om4%zr*}<3)Xt7v49Q>z`Y4y@Si0fRo=Q z;BT>1ILWnu?@fb~T^I0=)8OPc3HS>RyiWp64t$RT2dydiyqpH7eMr&IM=9`w+@BHf zf8u;UtsBDspwmx<%;9|9+H~~c@8k#3=aPR<#bc(=r{Z&v`(^2$PsfY=gY>^AQt(ct z^R+ZM=}tku(bPlVleUL7B}+YYRV0GGk<)Qkp>MIjRw0-3uJsaKjA8%abs}HVL^OwBZWZy@fCn7&^`pYvK8>9_7{yTJ& zv`LHPMGsv;zexj!9@Z(Zz_H*G^qN>165=)oPAfpb_ol%~(gpm5G&rpc0e_j`pt1}c z+y*M~8~m&CR^0@lX_Q92arx zh0M&?i(U^zy^)*{8!lu`;hab?ocIgN4hOZ|aN@bb#7{#xk&wb_5-%ws!XXSChJgco zJld1jVl$wCtSPHTR`j5s8LATXzhu6YiD4rw@R{a~AuH3))`fD>ZQ?anE=IbwJ@H^I z@rw7)dHAN_A*<#*6ma69fbUI%6AuOag)}%>DFJ`Uf%g&q4t$RT2VE)SzD#h7-XBQ6 zKyD_T-e^oOY2~}PWUETVwuGFdT+-EGIOu=D_@Q3cTu@lz4f-;)jI4U&Ij|tg76=w( zgo26Rf0SFQDKUS@!&odgv9~?(P+gJ}q`w3OAEjYR>sr7+OoP*U6!3Q~3a}R`b;HKx z^GsQJODi*ZbRf3Dk+*BM%-t&0@Ogvm2Z>XVGtk++s_FG+CN{J0(XXLcepp&mgbZ3F z6fFoJdY~`@6G(XS{yFE~1IH+i$1aBhrx_FQeQ9u-83BLEhQr1IPY0gNjv?-i_t)lO_IYoAxyRnpL2GF+h;xYr!4T~*PutP-gz!|$u>_g4&7w{+(; zhfCvCP38SHCwI3-3~z=C35gn2%li_4$2OKRR^C!T_;S3EF)PP?MB>{$OyzV4_-SdjNO+vjd{J_o+Ybxwc(nFgQZz;8;0OP@LLU1{)y1HV57 z{<%EQf#2-F319p9B)+Mi+mrISTb$4BXYb2ZPJg$i!1u^Wd~ZvIJL7oeC)41&oqp~} zgFo!R?{wh6m;Kd&bNy!r)A=BBjays9=^JpkqS51dw`pQ-+XXx7>ag@X8mhXBo7+kj zRD{eR3xsM`S1w$k#Ild-tsia4)3n6BtRr01RX1m-Y{jy|p`sABo&&-7n%b48VT*dJ zr5jol(hU_uwa0h2C3Qn**Zgi4?^@G_ylqRUX;EeEM7wvx5fKyjll7=CD`#wU!E#Gy z$c>~k7>qI389%^x$u$b!H5*pBKhC>ev|~eGaWvNp(JE^O`jT}JJIP_(b zoJ~smV7jz`1GqZ=UyzjnE-f|&K%oUI&r5q9t^BknZF(SU6RQyCI=!uw9KhVA??4zs zN4K|cADB1Vm>FYBhoawBV}8GOqGVLZB0*0yQaMlIO^4{T)XS4;|GWA80!An|1vjpAqxcX9hHHMl>4C2oywE3t&-iRj~Z1a~3VwvADdy za&hD6n#zTZ9~C(Fwt&yeE*T!6r4inPMyN@h6`peX z0)xORmYmqtdCbtfvDSQ?qq&JKRExUiG%hGzyR3MiNQo&K{!o0Kzia2%=t;ft?lrAy zxH;6axN`3DR?mjbNkwk0{`dH5F5-CILwGuH_;F0YA54Rjwh-`L4!oDWM}8lNlU5M& zm;O1@3gUD3JD=MxWsrYm!Ebipedy1Ap8PV=U&`m$dnup0#rfQR_6hlAR)4pqz~3Xk z%!1#R3U|gKzbuu0_+{4fccj5dixs(OeckE6fiKsV0?zSu_((QV!0&dR-w*oWms#*9 z(%|sREcjDtaQI~w{Lxf6`QaA)=d-{cci@<(6g;29O_bz(!>h6S*-g(wKC5A|{tWp< z{3-Tb#R<3qoEV-tR-s%e8{FI&!5!ViE}a$j%&i|RU9&tsRHWeix-S@CU$bOqXU9?g zwjGPZ;TEN3w6bZ@)HNbww{w#Tr!6hvD@8tE<@8JF} z+=3@8Wkg$(*qO1<8j!O=iCt1@tz1`oyySYCILT|Pwrg~=SJicORdnOJd_~J}D}h*7 zYw$~yOFiBjHQ1N6a9B0VH*NLFEEv%hCS~+?SBKSbSWBE!&2`ZLOYpUeQ(WX_q$K{E z;jjtMSeSTQxk4++>DHp8d0SP`>sItY?b3<`3j^UTeFH}{7qm|_^lw%I*(z` zdZ1xNcf7ZKoDO~@Cbhy4jxGh&!0-?&sa!m7@mg5o(KRHKqmaqJ!s1Rlp=3LpmU+^6 zv=8UxRN?c6%uU@LOUlWFHg^n+Lk=xZ_w|nX)hqiFpOF1w-oc*bLmyRR&w$Rk(J1h9 z`7S4Cpb&f#bxYu}<@p8UcB+x$G3+Pk(a29!Eq=ocICxd1Aa@m|URLGe$Gc}j2J3Tl zs*13|oWqmC`uruT6p;*aUeL~i1>eW87cc{SuLJK(>t_%31KH_8)x;m7x=M)NqH6Ez zf=pUfLUQ=>ws>h8*?D={Ik~wRNb`M%pT|7Pj7ghwV_DhRS+QI(O9x%wM|A^4FzNl- zeWZO>;CzrVZZQ`DCy#v3sen$sHg%9Ge3c34gB*BB1wk=x+i2MGEvu4*d|& z&m^)q;@LZrP()ca^hX3mmE(i1o4GG7p1qSpdvNv;+`+R8Ic9r!4}Zb4-%r8!ZvJdG zps#ZKBYJp(KP#vp>m#5~ap)VMEfclZZ0MsnQYE>Jsb1G}n2R#kJi)vF`OIPJ9GzF+ zQBmy4@Q~OA1Gxn$m!9&ota(FpU)&SbJzAzcec@ zI$lPUkF%eZg8u2Np0Xs3Eo9MQ-QOG;C1I+m_AA~@zZ&sH{A-r61fMNmOS1$c9*M|U zx}=ZLA7zQzX3phT_?qBQP9;GRJE4vc=L)FD=+@I-`5D(wE$y|RElq)UGJK5p-k0|L z9_RU9_9Cbdqk2)12mT74FP+-TCQ|x&UObO?xNxc+EPst7Ow2Wg(}f|}&$cvKFe}8~ z%WP`i%D;%DhjF?AP9a#D|Y3&@M?{ix^hnzWCqqHF<)Yg}@ zKfZyUP0TeLw|LiChtc)y-Pgp8pT|ril_sB#)k7= z$jcGW-|4_%sZyTjmWub^i&|TcV6OS|ICF{l;_$6$&p+?4VKYsB&t?6Ti?=BFdZBFOOOHR*f3C%ns@;>DEE}$b9TBl zXLjz1;1o>?_=71qBwhvIM6nGRntE`5a56eC`(Ka~PjT zk)pp_)8G_E3ixfQaAzEfA~B9bAK7&A{2ggBvQ92)Q*W?FymOZi;#?|YoT@1ODWcc=Z` z#wYptC(`~-(ogs%|9&v}_mp|`%0Efc1339Bg3hPX;N-Cg_@k+C9xV&_&u4)@?!bG~ z`2HNKF3@ScIa4oSTmu!f3zL}}9_YaqP0t6yG>OaMQJgB6ZJl&@qq^6}YJAa%&qsO7 znHSBfpgR*s1KmM6Ga3>zN0`veeT_?Ya>hskY>s;6-3~=0+2l_O4m_L&XAb;lsc;@8 zisv68xX=wUG#TzMA)h5O10vA14bZ=`qcJ^iI($@nQP;|*#mC}k+>S-P8|Pso<2E*g zbm}Lgt3UO4lYc3j}!BIB^Hioh9r3w_kCnU zq1!0-+{#zT^~%&lopKacnX)r?^U?*;d9QUUUpnnh~jXMp)uA~r;# zSo!!X?OWmEAAW`3`?tFF6#<_d(t{NXOWQljtJe2)&0W=AJ6;svhZPID3)(tL%h&hy z9qY&b5DdTU$eH33rv5STjbp|6c9X@o1DtOh zF86^y0)DS61dCW`d^UtI`Po9~0 z{!aUO%uMR@dz|M1=dm+A51)YGuyy=%+~e9WEku5?FFiZO%R+$rG#TWBiM)-I#j!Uu zMdc4MHy-x&nl;6laKwzv_(;LLw)*)?dzIpX!tA*C#N6RJX%>?9m@&K=~~iX zx6GfN*U7yk%1KykV{IjT&&Ec5i)PA0^;ncs}?)3A5>*}eW@_zc#p5NmK} z_N*BEi2t~$I%=#}=66Ga`6x1K`OqB=?nPx|b}YE`z5{;{RlSq&gAROG8k}sq=;!`f z;7_K(f95=YhXd~;svY<+q zF70`uF4+(HqM{$7E(s@(Nx%;#;owGc7JdSFa@O9#^I~?MN`vF%u?2rL4W8`hF@kfh z_DU*bveV@%gUmtpX5cI_6}kqlqGY8u8wgd#jfY(aWB@ys%;h`$jlNRU(NK*;gZqnjKCYXXxN1&b-Pc=ois4 zS<>nK;*_K~gNu`?!l~oOQa{mheBbnB7~!z)xjO4Quc@g~&sSpCcyUZrlRV$_=4x7w z_Zu}|_vUyb2K%O#qbPb}C%X=H3^aWIS|p_A00laJ%f6L3R@X_MI4)dneq(8Yo4K3` zIN3J=zn`VTDOMElJJR4JHv)b)!9i>7)Mu!r|ABMvPQZzG0)9VBg%j@t{Ejp@`33@h zrvvXJ{2llnc1cSA#B1^V-Sj-@%|eW@6dqdXO!V@&N1SkiYeteqbbALM^SUYj3fa9% zDQhV!&kKbz!hwRS$m?EXx0VzBZV7udoD=3!JQlBuX~+;uB{d08L?QVRw340@xIE$D z4&L)@ynsKN1}FU`;JYo{>p{Rncxz7H7o4~y>{o_Vk0-!#68}&&O=XQ(3>p5!K9(KV za5+CKlbvaj4-nL2nTd;Ig<%ai^1k^z{ZY&l-YZ173pnLB1bkN-oaRHo@3i6Y)qtY| z-$QWleKk(4{Qu(i4rm^QPxFCav8I963=UQ5Uq%CmT)QP0Y|>cbD*>zg?u;X1#{)%%^}6`upuSZ#tT0IEi7B| z05qMtvT~ZHaLGzMp3CQUFCXdbXq;DAoLwF*Ej6m6kXv77c2jmuYhErY!D(~Y_nOM* zRF-CC8~M@d{PuX<@6YgQT1L>H-B5mGo*FDH#A1+KG2qb&Ji;#MF<`=dd(xX=868Zt ze0(E%drf~iPEc3V*Xqjp`pT;MdiK5Y!8zD5E~BsIb6Oi4@qAMYNxxOWQu088X`I#g zvK?pMW3>O&XwABF7nV#rmY-92PE@zLm|uVW!S7By%PBdRtZntt5l3v+FbzB&?Sj|K6d>#im{kxW(ML0SDF^@bZ2yy^t|h~^CE z=aB)ldR65kc})+S0^#cs@>U@n^gK`&LA8s-lXll8sE{e-Nsxwz14*fqyZqZRd6!RVHVUe!0RqYbw}&N8#&>y2m`ZWP@+ z!M>V!t_}?UPLY3;vK2=j2m9Dq>n0pd1y7;zw`SS_#N_- zUh(UEoSGJJUQtZUoO~zF^9^#2^6H3`>X{?q*bZvg$hb<2%PmO0i4az6^umKHki2pQ z-q;p%Zb|dYk$*DGRc)jBYrRKT%NG z*KBO^dkqBKYx752R+GH4yyni`;h?N!t3U4@@#eA;1vjSsO5!P%#rUPecT_fC@n>M; zH1Ye~jFQfppR3u591QR7YD&Bf5n-+h=)!*JlrCFBa22>f9F~HB*xH-x7A`MowZm}R zR45b~Wb;}h4=smb%kmzFPXZ$7d*6jL*N!TNK1m+xp@(!Xd` z$7%k`QWV<4?O!ubDVSf|*M-~hT6bqNJC1dKN320 z5d{=d^P%0LAeRiR(W~WaVdE2AGHy?+HbQ%`0{$|uHbS@UH%R}Guf=WnVenFs$j#*E zgET^>WM*_p$h|OqaD{o^kd`I{32PKWB|VrCiE6%#+VDcGIKo>!jHKv9w&_b;(z!X? z>kkAquijrjuP@@$GI0?*d&`xfAj>qumxOggswcu`qWjutc9!&ZOKVtnyM3YZ%F6PP z&+XR3tt}SEw?p3TlWK>FHBJ|&i#0ShIRsZicC)fn+5bdg*Q%mTJ`We&g2k;Tb5$#9 z7p2Lkccgckcv|{>JFQPivh+KhM`fFAmCSvo68Jnk2B4zD8G$-sRT{(ZR;3sGej&$;?9umu7u2uqFOgog)vRTBF#h`(pQLdUXU9%^Ou8R! z-HYb74K*RqH!^?LSX5ioLw1>Gllc8>{JxuyZDQ=x;nyUi9Q&GRGLD2OCQuy9zajBQ zzifnY>R?KH!5dLg+RmTY?a{r6Z+=q5p24@UlGx6^qo^puaOm=oMi}rBXiZ4smelRM z?jTizcuQ8Lw{V-S73bvmj5L$%!$U!|!H+eP+rUqE!F#|ZoD#ZYQ9)(o_#J|1V`B_! zfmbbs+G%CCB4eC=ekU?S|}>8Zia%7f8c!V03VltkJ$g`vc)4(SRo<%Rzky8@DQ+4BU-Sje(l*kB=_5n ztX)}#tKd;?nmwA{cfsb<=`7dIlaJR;R?cf_QSmsk&g*f!V-L+HjnhmQ3+-;5(tVa0 zl=i~RJs*wH5bOr(zkIV1z1h&ugPXOJcY3?DihsYPEUI5mZy6Eb8Z7xS(0%IliB)p^}FH|Ve+nKSy^EfhC(3o7%9NP|& zIDB|dG`5QzyuD`D3yVS9&KI7gQJ=+NOQ?QBESmUdDs>Vx@_sk@w3T$ej*npJ0_d?>6~36^tD&=BhizYRM=_gkTpKw-UMEkQFR(y6Q_xX zjRcn+(#vJ!7L(IsSujx|^S6tR0rj5(zqN)GdUj%beBxQM8)@}2E!IP=n^ZD$ltLZS zm%J(o*Vt4YL{u{U61*+!oMFWa?U=SvhLgg!8;-j=+!>f&(fY(Wiq{j#QYu$8EjhkJ z&SjgpBmRH$rH&m-npRdRS>X(Su=Yf!x^;OVE2^+SVP5Z-R*)yI>1)sK%P-87!dWPC zjSz&Nn#cPTZM?8+WEX^KX^qiTl27iPgR7Is=uRsho85h49csa-F{NsCsFqPAp==iVHSwz^`5;NKA#rJS#@S#VP2-B z#A5#PQdH$$PPJsgKg=Rdh?2Ek#Vy`!RWGdyj(Bc6@tG=zpkC?Y*3Oa>UsaUL?|c!Il#!#yR?w<02-7JES|N_R9}} zmvPjd=d)Zv&VH(fYUCL>ZU@shr=|1OUq0@=o&qdVD&O~wU$$<`SC-UGoHl|I>Syd2 z+1~Oc|HL=8lZ+)raO<^WRDSo8^cr!%q;u+1X#u#vt0Hkrh6&_5(;{4fVdis-9XDDT zIFn3ettewJ6&U8}MRUu-JAFA>5x0LbHoqdrbK*HI$1Z*ss`Q@^QCTF4Ks>?1{9lEG zZY@W}3DCdWyWLEE0>LF47R6jR9E$9>}%;iwX!3Ik+IT6vjV9*dSX%pQw6wP(_z(WyT{JKh3q!8yhCGDl^a=Yp(G zi5->0nh9m`bTsx=EmCr`K^t%8U-07#FYG+)tewBLZ2@~P5-ut~mQ3@p<*FLL#z}^d zrOAb){s6pbq5Twlj_in%`%W_QN|bOwrj|_Tbaz{B)XjJ^`@J3=YhReZPQ;R%qw`mH zcDO$d;;ODR7cKa4Mn~7GE_1W+!`J06Y#&2v;;)H#dK~#=bziB;Vpy^oHYwgQOZ%#y zM4DSd$k^FTAtSq%Wf$-p_>Si*E#tZk6-s0uhIrR?gH0`6EOoO9kJ!zsW0* zIm?aI6~d^0F*djT6ZyG_UbZkZ+q;U#*_hRTD@fm7(qlvo_!l>pg_Qe5oc;e4s{e+v z7jQ=;#OND%jP7$$WJwCEk(_7y=mobELKuar7NYq@;d)m@U#w^S`!V}Xrv8H;=;rtS z?a1;tHO)F-Gsc$ov`BdKO{6)Pu%HlCf+Nq1xc5oQ*_d|D21w_G3kw&d z2ESJqYueR8)~fhDNXt-$rns{fAuwNVUCsWl7Z+cd5Scujtv-Nv4*!DkG~Ao9RAqW- zMfMCDYW9@V4-NAq7+mcf+&{)SsB>xO#>9_+mH03 zR&*qVQ%4rj&)+au53)+|K@zVPS~#a=mB{`|?*Q96G1qowFm7g&=pClZm~2G3*ZHO% zy&{?W>RerDvk^=3WNw95>u^F*c=>Os8Lq=K2tw{;4|Ka7rEp73{vQ9qn(M=&6bWU^ z`gw75OVWflNO@fzJeW}yOE<$-<@Ae_SzgGMd^@))hkKy5jkEEv5(6|X>{et~{p8gB zk$#<4pFJe9sgxtY+*lRbNrpEdmVDfW%ox`Yhfzrb+BsDZMUx1Kk$eDkA{jy9q(zxD zE_#>$l;f9l`W~$hPXVTF5>n1NAEE_zcYkYb6g63tX_xk-F zFG|j51Tzt(FB-;e_oIgwpS`9Yy0r_smEJAmqo^fSx26#1ne{a@=jza--hbs?Gq&dZ zpI$jb-s@+;X*uT7&I|Clq%Y)#V;X6d+f&J|jOg9btXD}~q!4YB>?0Yq;#(f7@lt!Y zl^FW>D#)j{YEZ#fev@aF?BDImN1w5}sdun$%7DcWAiW}1!TBO!P{Ge1;D=euXF2Nn zY?ME(3dI7iQHiWzN+B&h@T+7YtSU!<)5=s5e@dyQbytC~h{U0d z@`b2?P-V?oYC!97X!@F#H}Vup)&s;V_89SM7rTL$7;)@BjR>VumQal*D$aE1IcX(N zr(9ZQ>erW($I$_f+yjmvqU0G}EP$DFLN*g<&iI}Y`)q~;5npi#m9P+5^~`x9OPTO} zB!Pj*Oo=KX%^JLUZiO&R4C(MBHZdH4C2eaWz-Ue85qPRjiSQm)c zrf72R+H-o4*E6;sQMa;;>vR!Pf5yYA(@DMUJoQ+FRj(qfviyhV1^3Gi+q)v=W{-Fn z_oXZ`2B;;9#n4=nN4S#zhCPfEQwbc-5{l2_KHVVPFHb^OwApnxDnyP`uo^uhsA=?e z%h)}Wu}JAXV`(wGOwkwKlYy!^((uVQu9m{39pe?%(TF^6UVfO>tvYkGcl+qP?0}&j z`Zy%@p0mDrQ(ZU~b_as>r*2zvLOTnxLq_va{+0i+Bd>C#p{Uwa@#?I`n*8Noo@^-T zTYFp=^11oz&h5)B3`i=1>3E5B_25Mti;6a1FhFY`cOZ!ZSz<#10(u$3q z!8w~oueGDy>vDf%IZnYue zFR(%SCv+!y9;tgUv+WO%uz01PX6R#oaOJlkGD)-vp~SlcO=Y1mF#KFhwrcVpRE zoKw9Ly^*C1czDFg0P+46(h2s0bTkxk=0kKE#-2TO zzzXDNEofzdTeJdiXUFpuDB!f;EZ{F#aO*vWu-3>#Q$`F)gIxZ3u`kVcU;wwGahJz6 z!k^>6%jcu+AAZL;FR(K?T&!%2!^FydfmSv?&-a1peKHK0V*WgxjbW}TzCZl93-9K@ zjHYG=q0+J7F|jQurD-P#k)QBbc?!PV+r}@dL9qxGm7uS_O4Jt9$H;?nPkIs95!G{-*$#9 z-43Zb@!zx5EN4Ug{fT>LuT^f(6mgp8O=xC-2jHk&B5mbx%ru_?L@a=dJvY#4?YY_S z5)*LhN9@Jf?>rN5+A$UI*JzIp{qS8a-oI+~-_7B~$0-h{n^J_&kMN!HD~tIVl~zQY zB888QX>kIEbQmpdTEmvR6}J;(j%Ny6cZO;(2EV;+PXBBNSjx9xSubwCk105#%2-Bw z8}8p^9e$wY53t2*_wpJUn3YW}?>AIbSc`l%=M+gt2qk+&$E)SSYGZX_ z;n$`+r)gDQZ!3>4z7J2Th8(D`jSf~nJ{(0+r<}IxAD9W1t`K#*HqHKTE z45L7B-e6~EH*x@aXJ7w7VG)j9m@$7T?Hxw&Rvp@3$G*RKcM47Z7woco*sG{R;6@#S z7TRnd#Adrc;M0x6|7A~W_HZb1@ zcnz{CVOPKoIGtr zx2^P&0eZzQ3?6@H1NPN$CWwwXBWqj1Q*>~D`Rg0H+gS}xr(_43O|{abS2={TGBc|< zs8L(Ke1i6v_)8rYY{?4ug|mYh%DC5^ffqbTUry|kSZ0@!t+>Nu?t-oKwg)!#9A?b& zIB*VvpGBO>(cu0EWg9#}97`vMgM4C<-5|#!u{OowkbXoV&|zrDjz|P}Q7YDTPtdNOFB;&9Yr-sygPUG=IZg* z$t|#B$cVFl-~{!*`{v-Y=(^Su9g9l)nz|QkSaI9U^O~FI-F$srV`JU*IB5GdBbwh{ zJ*U?blKPvwM_OwtE2~?p%gUf481bI{2xDr=-Bu?MH_kM0*oFD^NJRfLm9YLgUIwA! zD#8V|V%FG?=uEbB0_XBQuJ`d>^mwzhCG`YnBlyarTAl$B-R?trBa#*Uoz?G3a@3BF z#n3nJ;|z{ffxwFAF!-WyaI=HOXHZ(JU*~9&2K{pcPK}NN<)=kJ`85{4S42!5c@0`^ z3DEl(=(TD@q+MOJJ9kW?X?(>n2dVDhAn}0@a5~lUIvrTwii0-lU1Egm!#K%#tH68_ zJAnQvE5)~5aWqB*(X@#8B2F6LWVIyx&BG>Dyjw9dR9&Msb`MP;#7>f>=tuU>AFIe6 zyeGqhmz8BGwf%FuM*5Zw4@I!AXHU#)ZQa#thq^mA_VOL7Go&V9Qc7_JKLtxCH90!g z#-YVSLW-Z%#4aN6z=2g9J7=ad_fBabY9hHcxp_$peJSXI1|gohPnIKT7r>i8Y2@ouXU1%FQ3jdSb*euq_w z!o=?9LH53L9tnoj*$%>v6x6cN%Z^Yr!o-pL>P-l@r5hX#t6pDq4CMp+pH`lKw`CC%c*wR}u z5|3mBec_79rVSf=o4Xbs>(7e$aII7J8%9lKMWu$qmz5=DHJa}CMss|z=tv*e0(~ug zWA;k@6gPX~9J^$AJL9R$(=s)`p+)p;R^p3lL6vKT?8IKL>Ba3H_5+Bkk+_^)ni=-# zYGM-|(PU>P&J3s|1Egg>m2hKFI*aRyA2J+YaNwjv1pID-Lw1)=oh}VZKSMTP2< z9<(!fb{v{SmPf@10cJjv8P159IFniDthd%J9px9&|6@i$)ZC6Baz0~huB*=zx|(6X0rH8p z#y*6OfCNz7PwSsfW71KP^uw6!DcyWBT-00K;2|Rfplh zn}woYwSHtv{}|T~$B9*d*_Q^K+j6AG`8GmK#||?IyB*o5#rmgex{oT}1>Lvb?hdM% zsHqgu^w=G4B{LYxxFd#FG)CBssAlL5`4hikbNwMN&e~35Zz;l#NxbOG40>5@;@c5i zD-bs6ATYolcPj7WmR+(CASLLrn9lrE@PMuLJ_%Jx7|ixvHgM>dw|xs(07td zUIjZH`b~`WDR_$Hr#%)K7Mn|5ENsfm<{OU*EX7Zh6<&uWIk^ZokTA*HsgX<}E1inAgxamwb5Wx+|?d z!KsvaIJ{TbDksu2W*P#UT`;2tB2O_hDMKYb1CVcmB{ty(A3rDB4Nbt$i6&1%2~Y3v zq4yZkbI6^&(Tsj=a#QV!uCHCy*4@R4IvcOZ)1t=3DP)0Hv8hkxPV`?+r{iKt&sHRo zoB{asa4X`R${fQx0o;D2tV)B$a7VMgz7gYfK4?bCp0F=n)z;I~c9rCThZXfk7o4_+ zn~wAO=pgZIGA;9~ICse}I3OkIN=U&8NV2O9YxZq8EDLD)4C_~2wb zKRD;)f{GU%*Aeeo*Iu(GC)4Xu^vt}0g0=;va^!uobMHsoE0+}XrkB0xK?YRkjLub51=qq?!qYSO$#;GX{?@X;gRaV9~Z zo$AFK&FQJN)>8-AC$KplHi)Na?)OX2gWD(XIT>4g zom+aX0&-3fC4cKtL$K(ysz`U=*1xG?hFWsK1KB|KELK8EO8&$NP~h1#nm~zM9Lu$- zXhe|7?cB|}v7n+&YsYpOVv%~SEkCquUB%M*cjH>#1LvPVzpV{E&37vEw}u0rNVYOj zQ0X5XMCBY9kgXB(;6w9jY9VTD>aSCaIA?IC=#v0$jW zR&AQ!zZB-H7X^BkMM%>{eu^J&3z?g{J4VY35OsQ0Gm;Ui8<_KvUCK|Iu~zRVy^}yo zno8hHH)fBY<4)5@=_?hA+g`F*E8;X9l+$^1+UYz};+NA|r^|)XNNU~-sHp#g~PbG@M+u*GTTa_L|f-w|Cio=d&ThN#h$zMc3r*66(m9WM;w1z54r zN!1NeV^_!w;s0HR*-&#*C=2)MLKh-bycZr&!Cl$eDZV5!T+eg=ya3i@K4czw5LmzT zIujyIg{To5H<(OF3{|nvQ3=3zQ|`rC{T&fgVxF)Puh1^><)G{m`-Yk=YFTu4wzwJI zIIjAojH=4Sv;1y`Id!I7jre53;u$|TfwhmWzr4sazN8@%kPwld$|G{0t5^8U&1q*c#qs8U zH8}rR)dnN_zZ0Qft!vj_tLYd28-Y4`US~=bm>KKzh;?E%Wo9e~Z(1yA^Iwv+h%Yyw zdl##P<$0szP|y-j3o9q`iv^F53uI=Vn_CzXs&Izb^0GpUmxR}+QVe*_x1tZ*1PdS8 z!shV23{JjqwcQLq!49`t3qq>z8v(pkmSS3}5nr-Zw!#z_1u!|a1XTrPCugPK-+%ldEn z5tRp!HsVpaTZ_ef<7!bE7>u<)T{$NHoPW*kPgi^;Ys1k**c23+jfr?M|T%aYWLO;Gw#+a^ec?_Skp)@VvlmSQ2jNGa*4j83j;UtUvHT2fQHtRq@yC`N#Vf>{kYEtA6) zrTw+FePtEHlm6~g#@pBTp{j)&m4-59k2`1XV9n&#@{xx6rl$JFkut9@!=oCwgYGYy zzr1PgaAoE2+@=*B2vlj8j;C90KHqPYYl(5R7Qqk2D9ni zQdI`MTPi1Ly<5swjQ{g1rf994!LuHA98H#r?h4U@i8qjTNp5DddK~(}f#75sL~DVj zvv&y2w$dTV9Rl3!7-H{365&qs6)?TH6KXmk)|B<=L&Md%5Sl|r5zSE2NN(mLkElV6q#lnN44WTLA5U|8AKn~^lpY_(sHaCF;dqQa zFm33?2EdWi`S|s)B2mzlb}p2cYXbYIv=Z%iV%%6-Stolbu#Y2=%$%?p_-5XBee_yY z7Q81};OW|cRp9Ao#g)R2>17X9G@>JF^mlHOK7wTEB8JV(o=J-8SX+88ztE*pV zP3x3qQx{ zTYDRp;6+wy;;syN?Rb2!P@&8O-e9$?u$zkOxV@~$-*Ma`cTsC`hTk1Vp)9vAqcrXr z*xD-IZslIFE?MK3=_smMwP3DHo_l1fld#85hNwXI9Q&`S&jqJlH}EL-9iCz3yIXuG zfrE6j(11i0Nj(J z`Z1I%8oW2dt5E<~J1|#YJecO(g}mMfcCZSw@g6y|YeO$#V`BfM6?Ss^X-uBkIedSB zR*6@Va0E}oVNCD5S@sIlIWfcc|5f)MP;y*lzIazvcUM;qoqJ|_I!^cGJUvOH37Sz3 zBWa|OB`Zrd4w7uy*kFTk0D}Psz}OfYSeszuhhag4#cMLl5_ZW8?Be%`yYOswVF4$s zp8xN=Ro&Agjb!hC-+AXe9lN{gPW7#OzkBbOL#-W!DR-8!8@vj36b6%VWDEWst3ph+ zF;-3E+`I_EVqK-E#+i&r^CqVdhPQ9%w`cPqRAj?h1=yYN#xp!I&;3qk=CCbAP&$vz z{LB}d?XW7{$vB zZeOjnW;vq{nbqiwG5-y+PB8xsIy$u?Cd!fXR>ZM7UxG? zOEhHyrrLOZuEd^W_e?*9dz{fn-AupE*+uq0-Z{IE(%HrPnU$9^{qpeDWY7N@d=pox zD#kRXOLTFr^>+Y7mndEzX(E^bn*d8^`d7n;np9MW390E#om*x%uiK`hD&tSfPl6BN zwxNiaDF0J%#j3r%L$7!u9t(uRx)2D5*UhY4Wki)fpr(6D4M*gs;>iC!xt~R36UKjz zviZ4|*3U1oz|x!?3+$tAJ8`fAg8^B!00R!$Nth)F?SK_!q<8XcRJdCV{LGB>1soi% zjx=p6c2LEJ^oqIuYQpX-xy#O-fBwBgLj+lO6c`=pL)=lIooP9#h7o_S3-KSbm2m7= z4_ED1pC|hjo_E^G*TAuBJ-Vd5Ty6h^+s-UOtpB&T@0D3UB&FQO+TZ53Uyg6$W7aXR zl0issRi8^nzZ~x>I>|)E7*j*W3M@1s_N-GkiRo#A#; zzcCz6t!zKVYsdh@ao#D2T%iw=BKUte($2dKQ#T(=h3)X*<~A?(U?1qppDT35@h`yc zgUcn8GS1ESv3^&?>Wg1Qls?yLvf2k%c_d+4h6;`&eW zdMxA(G-VQ{&g(x%={$`eHP>YKOqk!y-&tB)99lywQn9@?ab$ZVUrS}-A&i$Qb$kc= z3y>s^u&>tBvPQGcH~|-ii;k9bh(H897hke&w12#9$5{6T1yvHXh@PG)jE(fRZKN2X zo!xz7Yno?MR$K{~=|j>c_(SlTf4rk@G!5p9QkMsUR%mKv3C)s##uh0On_Wd^Ss|KKN)t4+r!AOer!P&)9cozR)lOc~+&Tq{80c5R z8m|ucB5S%T^_97WuL0IB2f(~zYU^oLZ9cVX0l!&c02Xx+j%J`1(%&aO^hL|ple?1T@?H^D*&v1-ia3bN`bSs;HU4Pl;&&>F1B`(1H2LEt7FX?T zq_M;??|$-6yT8Ug?;+p->%DDs%C^AL@>i%j2gZo)8dv*?UGf2j&s^Ca_m8prEZ_}) zCE!(ul6}+3qYgYg&U65%smxC6HyJhH$rj9i9G;{aY^Y@GEO8`4M!AST%LzdK!mfJw z1_@%^&gqprR69ElZu%~n@_w#bvQG)XIdk;b#tp|tbL)nYD;rqI zvg1>KefgocZfM;LP z&OG7^YEHDZTvB<*Iit{v6MiLhSCEhUhE{GO;Q;y21Ln5=wnnQOng z**1a8`oW82u|}*KqeB&l1tqFkn?03ld(~?Pk8gSPYX*9ZeuHCA1NR0e;b%BmydBg5X99`y|uaJH=PCco61TJM4p z+aJ(w(_*R~yq4M!Z))+S{UQ(7`@rsi3_k~WejvpKNAfFAd1tXsPVXgjN<810X?Or6 zv9gFMI1U(A`QKXF2ailm z9vuc0#xbrOR;|g+`C8$sYj_@%6+YV?y^4moRoku}ZCZE9RL6n5;`193HM1r^vAa|9 zmv#({?H?FCy1H#uQ`4$8_9@NYcln;T9O%iV1I*^q5Ffwsz=cEG+6IqIOkO-PQ&`>6 zL0aIBj@3jLQD=tb({+5>faYrg$FM9SoH8HQL%skvQNEWB2lrMRi7?CG5+kCHv>3;8 zPIR^&)=qy&*GZ0`3|j@vm+D$MydELnT5fO2Zmd1$!CJLB*S7hJk>Z-piH$>-tX}hq z;p}+p>YXE3Y@XY>b8h?m{PynYrfqB20uyx1k9BYCf<1KK#OSv6_HCoBYr7;}KCpId zzGKzo)Z_#rX(qW1sCY6uH8ncA3Tpy?4?b2|6JWk2yQRNZM_#SPC?Mhy+=|Q9HTg^X z!DZKC=*XnASowBZlJ>T#LSahEWdao4gTpg^)BXzwx3>)*9iO^r#9b+FuDG}+R5dTV z@FFTZK8mc;u2-XDWN@S~MxtCZ!{=6S4>u3x)@-BCNIIglw$o>u_p-|dX8Y?d#pgiX zK&ZH)dF$p-VYFp-7Aw99XmSJhVNV0$Fh&*$L6QLBAV_vDiSAvADD$9@0 z$XcMU(KKMsh+@Ey%O8ibl5j8hW^NAShQj4+?C3}+z`eBkK#sjkoj+&T za^RYVcrFv^2x$>yLru(NiX*91_Ue!0U8bg5(v6KhiN=g!1bo4m6bv`?>*GTkI z4wWxoh(Wfn2u{cC8|fd6WPwrLJuM#%tE1~kPCN)j3~xbH=SLv zraRNx*EHb`2c;-7bHe(bP1i$=FjDFIl#b*p!6*hd+TAiB1^ff;O#}5<$@Ic$?qa}t z5d%6{+68rZQCb8B?1xH-^PBcxnYI-8un#%?tQ=LA&RIR?y@F*7*qR(vo(q60d=7G< zK44gPed|e!Am7Dy%!f$F=@Ju4Gj$<_ygDrTlaZMd89~xTmhz+0qm( zSbj60Dh=Nx!u^PD_BA$*WSgdF>JeL%bhD+}{!CB($eP?l zTT^>SKHu5tz3kxXBmK68DtCTS)3oTAIFCQ{vs=JMBF^?S+o^VzIr$yRy_e z6p*A+zJGMIKVOohz))*xB{q3FpHF9UpKoex%;g#zakzNID!<6>sDUl;R&}LawB>^P zOxQk{9EmJ~HQ>*7Hw>@MO}1>>(Kg!zxaRX69p0cBK&@l~>qY|WT7Wfs>7kh;13zdh z7TW+{iVn(h5!536DlhWSVCyB&vWZJ@8~CrFjtDc6Kkc@p@XQ|eOxbP8pk)VZS)dm1 zxmPayjvMBGKsCAxEd3Zb5TpQ~rMhK;$lW4_Yy?!9 z$KvIW$Rcpj=)#MhM?9B!lnOR<(8ft5?Tg178oZ6MSR?(9MaQ#uvS%TjA!HSAgmmGc zQ0GVf>*WZX(-1n5Xl#fjl7Y}Z&8tSVaH}1rKCdk63Ydnfn5@lJ=YBQno0zu-v1 z?7A|7_?aW&UYCkeTa@8*HYW5HChz&>M^)SFStCri-7hmz?(Zru!Y;#@`ICTCL1_2< z8fOgO+&A#;$35XbHYZ3EOjay`XTxG(QlbEMqIOoQ!G*YZEE+>WBGrBOCrp$3Lad=7 zRvtIaPcK58dY$$>g4wZN0qPR=33`oUo7B$gWGs$e4OP{-AQxlM!P-6E5aaHLY-^Se z#~S!jWim8c0hszBaF|7sM=)AK%Z_@1$%}ZX&bl26%dL$+{<_fF38i$7S zsZwvfYJ8?TW%8k%;vUD0o7{M58X89CjE2YtMnXzhif_m8=(ZLTKOWlAUFc8cX#}dK z8~Jpx0b|^PFS~v70yE_vqQkFLk4rhu(4aLrQoorx)^*Mg1 zIx6gZ&-bwo6Ld-(XgT|}vPZvPeH5o;)-#V8JP!WYlV8H8LyGfvZ|&*a+Sj$Mr+e#s z=a!z1*AkOMw@AkpC)q+XYEK9(DFjGzzCtcCML!L=;#;EZ~>}7TE$tkb^sR z)atL(1ZhUOnIe&HR;FdIs>TCD@{kl)RY9I!{DYus^oQXL!#L0 zEFT8$ZYh`#ms~RruvE%oHA}sv?6s!mO*YIa7WCo!Q{6Gd0OIO~x+JO$7@D{4z4dS? zGBlwfwna4qw!OQqD}FV*x}h#5;2B+Lcu!IYu6$3&d*RyrfQVQvaOls=q7K$re79|c z?c3i$pLb)zvTn-H2SiQQ4e=preCPeP@!Y=w<1PH>!dJN>|1?o%;11@jEVIveWey$@R^S7{<1cR`i z5S?_N=ludSG<38PDT= zzlWFRGX3w`JpXNey0{o6Htl~nz zfibMDBTi$NVriX}gv>iN+tf=qD%EzwP#E%`e4+legTm zYSk?_PX*%=RYPi7SmpVV+b5%tj>?uioFX{6gugBe#A32om$if0gs4Q~fl^znj_!^Pylk+X+kPjLW>~3L^|DN2j*se8QFe9+ck!& zXHk1C-9oZjOS-Rn+6Y;CPdE}Q*3}hbk#LW0g^W|tj`S~Enou`Shk385`IkL?wyp&u z(O)II>%H~e$zMk!K~1AbcI<==+(30a&H&gz1}Fl7jc+l;*cpj4b?9bkfY}@yK_nb& zP9&OR;RqYSOo|2xY@@fNd%I83%OcTVyKny$ySjqEQj9#a*u|UZ zkg;L%f2Og8j2FLaz%Aha5SPR_jyt){;5eFu_1M)SK0nC#-BI`qY(~wrCSfyc-&}2f z8tq?2JL1dm{1$4b@xTuA2%ZPm2IPeej%6q~g0@5WUm-(vDw~>+NF_lU#V~(Lqw?!u z+d>UMJr3K(dU_f?eU8v^+$L4>Wmw2 zGGH1N)mzcq8x_<7_Dpae;8uhBH3>V|734=CO1`<@8Ga^5-2eF-V` zG+SRcyOV6{ZKE@~jUy+Qhx*q;K}Hrfi+=^*2kh9zV9ub`3qsx#jpBGQxOeiQ-k$z` zECGG>Wi#P0V2ZDgYKC|4U7BbRi_M1zI>fwtnsc7sF%Cn7|RM|@4OoDS?}3Jwgn`vbX1NEpMvu!fi~j{fFqC- zo5V&~VH8Q*fl;wrj1+j&;3ni7+rbw`wm?7*nzIOq0trIV0=&S#<{_R zV*@jYPVga@u~6jsWU?@n-u8~e#~%n7duSEd`UC=t-d`tUuv8S(Xrg`9@6<7Y% z;e!ueJ$~T_kMDfl%=CzFbmf7$wI?SBx~Eb>`d>_yb6SA60V7OY5zafje|fdC$LL%8LY6GOYw+z(QXt-X;Ahgt`;OfuXx80(VSxAbR*db{Fbt8Zg7 zl=mAab1PS7!{3XIt!=jKP_XBMjj7n>S{%f8N8j*NS4aFeLa4sGF9qg)wx>CgZ_jd# zoo(>~#3|0x4vXWTfs0{{CcB%(=ItH=r|`0cFmo{_%v zqNs9)BPW&|Dvrd{C}RaU0tGrn>w(@xGCK%Z6A~YfF`8TaheusG-ej zvN@|GksGRSxAHb>N7Al>uM2WNe*?Djf@fXriJ~czFBNSLg!!;iOG&;e@zj8%B7!+5 ziMw~hEr&{_LpN{Sc=N$h>EO*9r%ujwbL96(vv%q~Avp zdldi9$ZXUDbs}{4}|~7QsN$aE41j+zGyF}^Z{Ro{@#EzB;FHx zX8_VvFa?A1tv)Rf(0p&S%l}~8$0-JqD^e`e@gNsPyy2y)A^4Jc2BvEGtW6aN^4x>mM(nVctUUVJRx&}M@@S!e%A?)g#X@f{_h50d zrDdwng!ZXIslV9V*VkMG`^$O8L1U?o6J(x68tal+&JYseP=aR^)-*w3>muJX&GS&~ zJCz|* z!?Vq&e@r(l0O{Fg%trIPlOs7A9*%Uz+(?q%N*J3PQ4)Gkw|0Q`?0~^*C#+68-)R}# z-g;fT5RDhsX-4_M6k)h4;lcII8@ryFpzIAMSI)o8tSe3pc;jXRD5uqFCgUeP4PNuYV_^ugrtV#-JHL9Wo}@& zuwi~M`_ZO?Vk%xct{_GHoBfJ^U{~u%bGl}xtbH}7+A+G3lOkho!qu5C znSKQZ7uMJET6vBCwPn3Z<>8om!!2IoT+)|iSlQ;r+so}em*;VL%-x3 zJows~nVT z^nhC_9x{MMs170~c zIf+n^SBH$Fws-!4OSjy(r&q9#l6ZDBXr)o|8Q}=Y_|$85&EGH;hd+pl950Eq$=OF7 zd2RQ?8^CKEsnpY9iP6s-^CIOtGt`XLCFI8RkiKGXUqcD$5oo}{;w*9+_~CaD8&2j& zTOvEwC5P&8>kY^aTZ>^D?nM!C^0Mikxrt_9;z}Csaj1nwq&DDC%Q?|C)x7O6LaI4{ z0!g$&Q0lnw?E`#wvK<4xo3jr}OhO>8yg)pO%LuZYrCf}&>#+vd=4^A7oNv}sfo_gL z-&gV#+E!k*$xaCf)wX>Jv0+6>%kj7g`kG{=PM3v%5mHj?+@EiG`>km7&n*wEItp`&ACd;11ON=T!rO)agP8dpd$SyD#g^>wi!g_EU(Je=Cp z+PWzT zVn?aMq%t1WVNRVxG++ennX8J5gzQ*)dhZNe0TY9rhj-5p9bKhDDHzf^Uy+x6q7l{7 zvqS554X!+LaL@4JNjgjVVT;Q*Aafh)u|_k=fn0q{QVxf9uN&UoFX#tJw|G$JnzG24 zgzRK>4Qoa^N?Ig5w`ORr+h-loL*O!3E?mX00(I*_#5-(;j_l$}Xh8f9K3hZ#E-O~a z2;UEx&NZC4YP_XLj+4fAcX#bxxnj;%uh-!_eY+jCt*ALpW9x!bT2!&;o}@o;$LaI) zEo14m=DK07Ri2zhpLFqEYU`q{G#V>b!u(@3*7Y6oG!ifG_S9 zq=P7bj_Y9G-+P??@DFxWjM+CtXlN?;Gc$x;8Gf3!=FNyUw&OS1lo`JlXQLHoW2SO8 zn4|_#tx8kQK~lu(E(5edLvie#ld&BLdi!-#Q`f<74lgrd6_UM3G@=5dP4DaK+Bcn< z&4#a}K!|$~Zy;soI~2m$m&b{~Tzdb!w?bdIbh8u*X>tS6mt8kZS$4-CgN6oeVEK8; zgrB-Eak)cfC~J5eJ7-VzB-HGe>SfxI%NZL|t(yo~)@+~ZWf}r!g_&a*9ehl3yAE8Bfb^V+9r|6>molGV* zGub&;pr?FtZ1qklH`$&ZO&YX`)RPLWlb&eGMZg0lK$(JJF_W2InVZNNv|jQ_jgE{~ zSAutFeS@ThLV<9W=|m;$qh{cOlFR8V9+e8O)9ZL$qPR6AFXwRd!=2mVfCwqvXGe`W zI^1&x9dw?H2TWNDd6n|Q?`in|!Ff~xOA(aHzvXFPacwGwgN@Z@N27N6XI#G=FiKo(sb znZ+x{kbxs}v|(cPo8;cg$-;d80lrSw7DRUijH4P|$RaMHwi=XgH)Cl1FunlTN3LEm zfF!-q3lAzm^+~ZUAckXsq1bELRa_d3{m=$RR`1zFHR%KU_H4!j@xb0EyN4Qqbc30B zsJEc!#Xf1o$RX@8e6y4fOA5?>K!}J4VhOu~iM>(Em>lvSw4?F<9ixVAnzn(6uXw}` zg&W3tfY<=S?v)KxH0Fcb-gMEJFL)7L!>x;gzOjqmwC#iBvj4$Dx2^Mu<&RN2w?*`= zf72nf)0k-QZpN5gF1>ai)a(uh&Sm{h+qGiYUyZ( zDHH)j`p@W3eBt!}3}RRC?mWKe$cIecs1RXSvWV2sC8(9j?>V?wV*8#@c#0GQQ<2bh zYmN-^!5O-n2?~QpXSRhxJM%+L!OT?8P6`NlH|0ipcR1X&ceP2E_VoUqe+oyrwZW#* z!oYO8W;7#=i^GyuZ7o%-?GCGERN&$kfeGAjt|3+jy8U=0GEa(()w<8G&d}A)VED={ zXAXE1r4idhP}>v^Pt4#<>84aSl29nbO`n=tGI)3NW1x$jCoJX@WLqR1m&1U$l$+AT)xsIDB{WP* zF+cZHEvWilZ{^xkSJLTtY<>r)mES@8lDo_ZY+LskcKIBAe$pzm`>Z{5xyuwvmu@th zRy4d);)7y9m%d(?)@|4(R?k>$Q43F>am2&ML*Xt>sfiQ9j7#UJL z@EF?;B2|E2WF1MUtQ9Anit*UEfVexg3i_Q#u``XK-QJ@a)gQ1TYHBS*yM&kTt3kM2 z1B!>}mU%l+Z6*nSS}jl?ogBXtM@qF%GbGD!kDKu1S?21IehU7_6_2-g-0 z|A-*Nd+6l+C{%uG@s3W-*DByB4~3)rTPk<7vZ(69!>^F&C+WkhXuRJ=?3FW9S{DQeb41{Ci%;DjH6pr~< ze~;>T!zvJb*ll2ch>ENKo@E&&Jh>it5*pX3TVo2&#)i$WP8{Zu>qF-c$7gsO+D9SPxS5{lQ;|;2HrFJ* z_4U5P4M%)p>SqK?c3yJN*+?dR`h)yIUDNc_@8K`Ne1gyBst^1umEz}5KMXw*Mddxu zlLP~Q2?q7blq%3ZPYRVKj6jHB|2`Vj``G6jzA3>;MGs2f4}VWWW}G0eeEs!{e|{e} z!)?_8>0ClfSgKzBF;~=8E1{SF4H+2S>D<;v<9x>ReeRIsos@SD2LmV9PHrP`!CG`r z59>uL0k{_K>c?1AI`j{NDWI_JON9b04>`~-6mw5S6$j#lJR?OF~tPu3vHDhx{ zVz$1u>*bD_M*j0~G#U|As28{g@jmFGm%fd5{aJ9K zca+yTFk+vFD=_*RF7VGJ@0xB;1hXjO8?a))s+^uu^@4dhFCO{ILjJ;M1x=qX{~GH3 zox|Uuuitk1Aj$ZB`P=C3{XE=XPr zv}yyTd^pc^D?Y+G;=CGCcv<1TP##xgUJ0q(hmpjm+vkIqivRD6e~+>UhTs21z0ru0 zzw7{dYj=8sDjd)$<+1pF(bw-IRU7Xq?@U`J_Xubv7tqQR+~>6brv|&xff*6M==axj z>toQzF61qTj+{9@Ho&D+WDkt=oT_r4DzC$*4E4)nh;cB=s|g_7r|9!W$g&~Z`#|vl zFT_C6_Z}Zfx&HFcEif|#8R7=9E{L{u-;0^JQ$bQOcP=m%EM0!?#qw5q75A8i)bwh3 z9rq~}>FUdC*g^Rud-Hqzr2WJjMJwozAY_WKp_hWgva0iqG0pc&p68_sAUfTjr9=E# zdZT-~pIV&vwYGVO1wQUhdge~#BQWnU?Q5%(hBvL}YQ(001D?WDIL<7tib>iKjvSRK z4h{1UfI5bh1uyvo_uj-vbFtNozc%io=YO{klLr^fX zanS~L)NWWxEFV++LW;pF3JB(QO$|{;?Tf&T$OTf=_Q_)%L+x8G8*iD-tdyg+R~~DD zPwu5F3hUD<7f+03CNEi0_|nQt#8DN(C`yn~qHk9^KLr32#+-q_UmOU$S!e z=%%)?wSl|TutqML=-Ih@15OitiVv_&p*i34qIW-fFO{bj8=~#3oX=by!p3Rylrp(2`c4bU> zsTrBMff#HjZDoV(an2 z%&PudDDd;naMY)WO&y`!x>EC4bGUO@Q4qdPJ}i-uk6yXvmD}1k-+o}j>n<)vs4Zy8 zmMOI!xMXPCiJ{c^-mx9`PN0a2VoHP?A@;MZWIEM1faF0=zaUO*%jPM&Kz3a+tUc)u zSzz-2ry5R{w{q|L7N5)V2dLcn>Gx)H8RQm#Xvld6kq;vU&K-R+ncF3&71af$fZT6W zDV@8Jj5%mW%b!JTd{Ahnf&wr8h=h8>H1y(q zL6)6hAN+49{{@lp~0w}$X<*@%B17pjf;U$OPfi2ol@#SR|OdKS>4Xk!=>@2-h@o>59zQ_TE(oBC0TUVm4q&poqE}k+;5Pt~E2h zr<5CMjyA99>|9$cu9a3?KGQcpksBR7^twqNr3aw^;CZh9qPq`lyk=X=M1Ak-_Ri^I z+ot}$&G1X|VSK+8F5&-)@k-4YBgAfB4F~OzYT7HbSI;n5RL}e}e`^z=*hHw;P0n|@ zZ_5SE8~Qiz#z$j&Y@h=l{k(?zjrdD(!$7Jn*5BFK9_{BeGVf^o`|u4#V)?o&kFMI^ zdj&rFMshQ~n>J;q`!;PZKUir#&^wZy>0`~CVU!VRM&4(a%xIhEfFtiXTF$da`+s7_ z-i^!5@U&h517&y@#Fq~SY1#Sp{@()s}K|f zuB~4Ey3hA@M!vr8_kZ0VG(Pj0&wRU{`)dP6&5s*eeR-1(gTQ88pX2W0?kXS0PG2~} z=RNle&(+$B@;54E3%>qfl^yrDIn;$5ya;4kO*d3 zTm>^XQW*33Rl|nGak5@$Z!hRF$Js{HQ1S|h7M5}hg&$!-fBA1Ozyj&$fSTy-vaBFO zk^6QyMAF;Gwo^^32;T357@MCOHsC+L_;$-^8ctquF|5Pzt39SzGQartcFcX?d~+w2 zHM7z(w#I=_aX z)R~r|5W>pm>tNec*s8vYPkNpfxmY4 zzlpNGNxSY4E#2#%M%H$?A@Hq`P;KR;0d7==`3HBGC;?uwlxvWn10Dc?5WVx%`QKTy zjh21qZ97B`%DKQaJSXjC-+7f1GLW_Y8dT1LHynHCPlaa86H?4M=E>H?;hsdY!qQf9 z@$#~Rc(WP;{~Ze>h9Aje1s>r}8M~!3mS1i_vOi!4j5-S;NIdNs9W}=rlHn$I&or<- z6C^(@;jBgIXD>{Vi-yjF`Fifsng26)Am_TKtZd!-=zLdv`$`($|@b}>$zTXU|B z!Y%B?u6_jh41v|owWs?Ri;)$Z2W`=oRnZ^C50@=6hM16PME^c6+;P4>NB775tiV}t zva5R{C|W3$B_{Us&pCYtw5DRQNej4z%dpO`=J$HufLwqqPSn**Fw*L35ZLv6G#-ye zVzIAA;_*l{7UTEEB6Nu+_Z!iUujX@}HweA(^ox7k=4*)4Z;$#x0eXRKCFpE_|E6(9^>|L2+YP(drzci9|G(NbnaXA~bNS z)WR zsx1lhtEcg+w>rP7U5zwi-2?vZVkBw`0lrNZBwKw*M>e1c`Ti_?5Wl)pm|*MZ5pbkQ z??#JRsF}6C1IcepuXGex3>`V!gP(D`xu5#{6y;{4Dj(?cQDCqK5nHZO=52Q@pnDJ@ zon6ANcX>^y>)O4L&IA50<3)HGDqwwEg_%G=Yn^)`}{kPEfZa~sBjKr6_{q)c=N;RqnN_@P(v@7m>u zTF1XlikW6i;%C%gK#=da15Q~%^4rts896l1TKuYJ7xKY_YDaCkA zzyf)`>MDtot-xyx0Y;dKx(pgxnv?YRBurq4x8v7& zPVr9?=E8YJg+vm6Tt1P3h0^vryv75vCV0(6HX6^TQ+Bk`$7{%pBlzlCl1@v~j|61v zbv;&0TG57Bwm}91r#JQ_qe(b=NitrsaEQ-%-Xm~W?^fb_gqI*#jyA(VZM7KmR$28{ z7#uJ(U|4bQGB^<|WByDWm*q@7pGz=)#^G(~T2}7>sw2q~C)WAK3OHlz#Ta1%dC3vh z;QRBBO3>a3{@um9xbzL0q zSyXBYRyFbw)-S10r^>fOc}6_&wNMK`>HsF{UOM{lG7e)B0+iKoFt=(^zbfjzVQB2r4mlhH)j;oA3s z{>)YB4@1;dex0y(rOgE9Bw;eVmlka$lMHU(#kWnd3s=cAza1^Kc zK4^@9R~h!8(x(xMb|JRd6;WOXEb1?ex3-QKeZa4>!HWaM;(&;-#jLIOt}7PT_10il z^!g8cucwdy4;Bl`XfH9?Ae!(=bAn5`cwN9!r697E`-}KIJ zm7jwz;wNdwju-{mw6;UPj>&RhaA~qJ%-(zf!A-ViV5*5o1UV;yEk*S6p@nhJNysk9 zn}eTY30NR2xm~%L8Z&4sl~60JBC!Z|!Bil-t7@Hy? zLazKV93H&^jgyg76F!ZvWnTdr&H9BOU~fj)X#8L^$x@C@{#xYs5(8YT2nH}vj|su@ z=T#|Yns%I!fvj-@euEuHJm1UNaY6?B6%q^Ex9poqEeoDEc=i8^)RF*Z^Ey%sFMaI{ z0xO)5P)uIAoU&_%m4BwZ%C-L!JrEMW&_7(B9yt5vY&yS)SI?ve!h0(808+~tF9USH zEo432!c7EShF@LDi=E3u`sRcKV)Iv)Gu_dnPlz)~pdG23y?J>B{1Rsk7F2^v#w+kO9 zX`?z8{DS$9LHO7&g-x$GFDDGh2Rl(AEF3r>ctCA!`8tXyx(*k-bQQtsRhZW)um5~H ztQuBfSyj#weG*C@w(L7y zy%}W1u#9;g=KttzPm)+*ZsuHUq_Yw~70Um2v~0_POa_kw zEt{j6^^<1S=dEuH=b>ez9rq{C+ngN-+;NtCYb4}((QAgK{ReQIyy8wJ&i_f%ua|>R zVxBySkDz?L4(lhsa3%8E-R_OPT=`bs0o|+>I70HR;H#BypHYnKB~#%8S6v0Iohe-p z`L@dblGg1x&m+Pku6*l=w-tJcNFSUpzkg)62{|dCiJUUoRG5SwPYXclZtUToTfG(x zm|qr4?d>H|_Cvs3yBecEf?n_Uioh)Avm?|TnX2Yo8fa@9kOX~MP5yJMhR)LD4>dZP ze9>jRU-sK;4o}sJQF;5aR*dJCv0@B1x>gJwc4r5#9>&}s@RGe2QY!6&vu!9ZLp#6G zI$Jw$ZFouT{BvD--4c+Rk7ByOIS7`X?}B!|s*$giU}>K@E9JpT{r9aN73jHZcOI+9 z@(X#)%S($^56oW!=UAT%=jv&sZ^DVVMtJi7pU&i&bL&i=@jUJ&TiSn7XR=m7c4B2? zt=5){vOh4(N^2y`aIO4sU4$Uali znS4gkczI=8T~~W^s4j!kO?EDMMSER$TT3XBMFTR!YyHEdA*oknj$JpzGRR&6atBO) z8v6d9{`a*d=Q9T_qb(^r3Vv5xQqi0+ZOIv|7Ev3Fk#UmHmeg`C;!EfzYPI0fGcRF< z5KcH+apEr{%0HiDe6+(Hm=T}(-_w?y4`f)jD*Fj-30cj~f)(wQ-LUU{P&h(dXssOa zMrcbo6Y3J71A*lTv!}JbSV)3D{_*ns@qb-kA|%NRnf`~qU} z)ZlQJ<8@f^Y~7s1!v-6eLJ61Pk*WznIB^LMj$hI)hXsk7t#INs{Vu16slnBYy!75h zTL@UkNTsU_TyaA38$ie$_}phaM}&{QEPaU)j6+`{MP$QbM%qHC9r_Z;QO_u9C~ljo zqVirZgjf^q2ATSB!h+@<+DRUkrayURE1Ykz1}e2Wq}&`RTR7~WMn-z;mR$s(kT~P> z>ZZWGRt5JbX>XJfCKiOpz>VvIC1NMp_QzF331zsBNxHC*{gb{^sVEE_g>-!SqW0j`|1} z>CD2EWOYtK)xa%Ga5J9Iac{uht;is*zw>*@Uy_4Bj?Wo#V4@$%$h&9K`KY@!Mwr^OSDRM7lSL0fb;JWP3puaY*6 zPm!^MdqoPsE>SR4qh{ghjnI}q3T-*GcBJn*ALhSb)_AGE5Y4BOb|lF#KXPdH7Gt?o zqZNUwT<6yt+Q1ML<>>~7`Ei|$rW+(jN`eMryjDtbgd}F%F-*9ME4;2KH^aJfvn*p| z$2~vw+{FcH9xkqMGF_acD42gun)*)-RWnF4%PkCpem(K>=+{SS7Asm4&AD7Kp7Dcz zQNw6kyd|Hd23{ASH0>K<^b4NOhKB13{UVw64$lkNPjLJS7MS`5nyAW_%4WI~gec;kmc{KbtdU=XHBuBF^W4o^4)Kkg z0BfHpe9ZG7tmOb&c(nM1zrLD!ImjoGx4VodKH>Q^d*UFTcqME3q~~eo@3xP>g1g(% zYX@s|L$3jn{tK{mRj-8R3vP9uCp3^QwvfM1JP@?cRo_}#?w~qS;cBv>l&cXGK!SM?2 zF3e68KJR&fwH!kAqYlVlqVP@6YU*Ypql&(Y|f$=b5^{Vv)SPHE0Bk2&VNLI q`z$lQ^iFW;Gr=Ns`{P+D0sr`d~V+Y{0%iI9-{XOzsFBT_?nbKHYxR4kr8^Ou!p z2>Ey?p^?|lEGwB7b7lJogik?u#!MiBU5+pCjQo(KnU%GR*8j4yH6iiufz70f>e7+| z4-F$E{3O_ARF*87W9vvh0DTPTT~#HOWuAT|52A1rP@V~Ms^`@XnL6_zAs#o_HJ?*c zHfN^e@-?7u4Ems_sj+5@?Fu{+Y8WH}cVcTprjj61P3+>ya@hnGA^eB4w-aK|Fl=Yf z+LPghs({L)*ZQK|53Gq#5wDXfFJGnh(E_j)p&$j)gyt*1}&vABMk$z6JkndI0{X z6sgb?^aT7b=}F}I6g>s_JNg6sU(gJcF%||tf}u>TC2I-44U2=H%+S)TJ!=o&!#wc4 zEFFFh%YomW^@3l-2EiY~is4US6W~u~Q{b1e+3>4aHT?N(Dg5Q^68M*~%i&+mu7-a- zyB_|H>}L3PvOD2#Vw>PU%uoupg>8Yqm2HLpH2Wv~m)V=}-(nxa-^=#G-_JgQ|0(+v z{tagj8{&meA; z(+_n6k0Ee{L_q%hoXc%ExXl1X1qN<0tG?aryA8M5wXf^I5GRtm~W)XWUrtKnCG({w;p zvIe{2?k^oi9$GAf2GgerFDM!~lAI`ATvI`|RFu?KK`vfO8s&~FbdkOJxg%XgFk4{GLQA_SGEv=kG zZj=7{X;sygpvIH|_1*w&OmGoyyf2#B+NPl$I{K^`%q2P@?!4H!FU#3ssZxx+F`_demO#709AlK+45+~Y(ig|{fVzVGAkFzfnEy8UwEs>sxFJ|J# z#DQ6jA%|bW?2v=b(L>MPr z)iUKFGKMOx!*~x*lv`734*vdh9jJT3`y~5O^Hcc^$NXQ8+WlO}f57cAS8WnAh@=uF zYDt1IA5CX7*i3d9^Tbj18A5~U85S$4YuHTzF{rFvv+h;?!V!b=49U#_81<-qLNI&M zDdJiBLK2JKpyFi!?gS|v1^JKMVIF1_b0r`#7fT2&!yo~K9wD><1&ts~{4;)>AL9r40e*;o3fd3&ZqV;Rz3k;5^L?n< z!~6(8ir8Nu{#X1Y{~9Gb#lPj>p~ZjTr}>Zk4F8G$%zu#@klEK6Er6u)z?pKUIeL}o z=_&_G>Zxaev4Ku$OH^u06z%#t#>u;U7qlU%VS$1phx>pPnwVeXGW+!e(vkM3t479&k+b)p>~#|RMlY$o%WEc4l3$}}6f5@*+;gZmpqRAZ^Z~wedQItoxch#6=|cq#h`A18E%5qLsnxPZNTjE1ldM* zl25SSI1N^fX%tPOZrUBQ&`3!7=ja=BH$BACSuVSPy~b@klE?6NJc}3bp?opFgkOi+ zdkE>Bwh>#Tt(R?pZH%qdR&85hyUccj?OxmCwij)0**>xzwVks4W)HGAv&Y#x+B5C> z_96E1_FL_*Il>)n9LWxkqoqj+$ghAexvz~ zE^Kr~qnjFSYV>HM=NrA*=)*>bL*hb)hD->V8B!avGGu+ohLFu6PlRj>*%`7oKrl4m}ilGW6%hS&gSQUf=km zu-0L1!&1X~g!Ky>6;={f8TMk>TVWrC9Su7b_FH&Rc(d@h@Q&f#!mkQ{GW@0Rx5GaU z|2+J=@IN9tMr20hM+}J=A2B0hUc`!s%OY-wxHsadh}R=NiZ~weLxd3-8rdo`F|uo9 zx5z$`wUJLnz7qLv`VoNXD>azM*RTRz{aMXUH$ zm8}-Hx}?>ctv+mZxYgIK{)lQ6)gr2GRBBX@sD4qSqDrDFqZUW4iMlH4)~NfUo{V}a z>g}kHTSv8aweHk=5q)FyUC|FmKNGz@`rYV#(Vxe3jme3rin%)G)|g!}2V%a6b;kCI9UeO=c2?|y z*j2Gt#@-xzPwe-xXPv3e9?pKwQO*))rE{@!jq@t!tk&67t~jnN zZf@MNxJ%-$i@PK4p}1$_UX9xocR21l7j-pp#k)GYvR#F)VXld;a@WJIXI-zk-gg~z zeHlL_etrC|gr*6-5(XrUNtl{Ym9RKrO~U$w{Rzhten>D9LlaviCMI@G?3UOkaZKV9 ziQgsuk<=)uMN)iH=cMeU!lXq>tCQ9zZAjXj^hDCOq@787lRj&k*tToiZf*Ot9np4j z+u3aww!NtBKiYoY_Pe%!BsWS(ZM$3BecSH$_QCB3wqMkKW&20kKh=Iu`y(Cf9a1}# zb$G19HyzG&WF5mhF6j7nC*CQdQ*@`bonGtoerHGL@XjqeJ3AM29@u$g=Lwy!=zLS> zpSzGQ-Y(O+T-#+=SK75x*I8X}?t0jrE7!;l2mFFhU zdufGfo6~;u_VM24J(})HAC-P_`ga-KGG=AeWGv6vnHiMXH}mSuSF`L{Wm$8x7H6%= z+LPTTdt~-?*&pV#%}LGakyDs6ET=f9EN5=cvYa<_KFm3s^L4kj-BP>t=+>{>sBR_Q zD!VQ2_FDIr-4nVm>i$&s7rVdS!`5S1k4t;(=<$A!gFTM*_@>9NJsmy6dyek8zUMjwnfv6 zo-R7mXGEXzeXi=u`%dh8U*F&Q4eob!zqk7t{nPs2)c?qUCIfm5ST^AG0cQtxAGm7Z zr32p@)L~G@pq_*J54vj5?!k=*_aA)m;C+LS4gP-cuR|O|!iTgOl03vSWYLh-L)H)3 zFywfx6U zzj658!*`6ZjTkrL`Vo6Z7$duktQ`63$lpix9d*N~ZKIoxE*ZUk^z);C8dEf8$(U_p z+1Qk^Rb#h|eP-;-W8WIPd+dR+$Htx-`!lA$kmBUx%;LVq#loz zJuuBSt>d)C)3%h+vdXg8r#q)_oDn)>{)|s&j-Gkr%-_nFl<%0;b=Kvx_RP+iec|jw z6&)+CsW@C2RXMBjgQ}FO9#u=KuCIEzYF~9&bz$|K>L;r|nPZ#Nd(P51&(2MlTReBc z+*{}VR8v&5sOFuT)AQQQOPDug-evRl*A~>iKcCF+HGld1_ZPHZP`cpO1t%BwSU6+h zRSUN+Jhdo!QTIjD7Ohxx^P=sGepno`xa;DPi!WaM_~L_0S}Yl~f*7lz!2&i(a|t zKL?LXGu zxb~s7Z>-(5_T<_h*PdM$v@UX8>vak1`mC#1w`kqUb(`0{x9;Z+EFx% zW9?XHmc}yK2sVZl!`}BCdy(yA2cTIr<`KLpkLF3xFS_tlo(?VJT7DP5k3Yw^+1l6= zZ8^4l+hk}Lvu*Qjn`{PjiCBAkdsk;8XN0qrGu9dJZ0qdk?CQ*PcEfxy!a3eq?yPoR zfEnQ*&TE`EIB#?Q;tF*|x>~r}xSXy8SF+3PN^|A73S7fob6ksEm$>e5-R-*9bsy${ z$6QZh9(X>!S$xa*g!tt6E(t8bkr08ophZHP1ZP6WguV$=63W`{NcrMt<2MEyhPc9r z3oEJ|vXOdywXl&sLLaBwQ462bFJNW;6}6DeIxx4T7M@`*u-DjLsCN$3LKD?d-v`_n0*D{v`C$2gxRKH@ThMN^T}M86%8Bqqot;=t#&PUl4M(Fo6DV zJsfo&5&I+LXqp-AXz0=Eqg{`-Ir7@k)FYemcjb|{2s!fpk&jFYJdMC#k0W!BtLILo-hr4VWWMpeWZPyeVTo`eWtz2UTa@q zUu0imUus`&Uuj=uUvIz7zR|wb{+#_~`%b&CO%wZ0$8v1Btae=LxYBWTeW^KC39Mr! zwDOAum6gV^TEtaO;RQGI;wjw2NAYZ4#c#uuF`5_hB8=xX=+ixTXP(2mayP#m>xav@ zmkVo$6C)=DW2YbKj~$WGuy)MEimR3^AQz$++(Ygqk6>ryW%4oEM?NKo$XC1{@5cM{ zYj}Z8?2tr=oe}DS?W`MiL9p7P6R>_Mr?av5aTC3TZlHHV8a++lqr2!H`Vl?C`|wiU zmtO^Glg4NBWxSGK$fxlM*ixEEzvjdE)x0O4WDDa{_&<0X9&O`%10T;bY~j2${e};R zyb9%EJRP!-!5$S2+h1!~NfJqKScx-857;Nhl5tpVwI?gcKgcSw8h!aPz7GBQP4WtP zmAp<4lTDBfcJc%H4J$8(9iA}SoOY%0v@PvKN6-Ou5FJ8W(^|TkE~Ja;wR9!F7;<$R zeFiJD7wBGkGu=m8VRwj=(`2a~CE6uFeHAUD!0$OCjE zd5GRgUZc;G_vxEtD}4YqwkJqa7%IKwa(W^5b8aA4&{gCMddSwy))c!(k+v3C$=hwg zwpLCf~R`aAst)?x#DbK%&TYr-N~Q`U^N!v0(f7Q^CM z0-Mb$SS5DYYO%kzh%IJIuyS9HozKhI73^|$CHn`v2J`M!*k8Mq-NqKMh3s}#&2GT1 z+fD3db_=_fUB@=C6>KTHgso>uY$I#S?!b(7Cre>>G0K*)5_W)1We4eLHV3xS8u}BP zhk5;O*i1KJUb}~NWcRX8Y%^y1`&butKkLdKV5#gO*k2#U-2Vu63%9_c@F>e*k6{jc zoMo{mST=hS`-fYxd-yc#!Jc6~Sugfa)*JdkE_Ls_bdGn~NcJ8Z#dfjL z?0sk;AF#1(H~o>#g)Z_T8^`vr@$4h)SMFsK*~e@W+lO7t{cH;R1he&ntQ&iZm10+M z8aqUiXbaMb#*i-5NeZc#6wox1PLs$`?8AHFjW-Az8D`^hQn(tI9v=d$(f~e)59R}T z8K1#3`CLAqSMxc%mapJTcnzP&XYu8HDPO=B@(Z9D-NNICdQMlMOuXbsco20Gue}|AMncqmEZ0 zZ>v#OQSX}o|Cy^IZg#%zJ@6_u{%Fu0AcNR{$~{NIY$Hh9x{h5-lF>&Z8+0@4gpDNa zSY3xcCi)p`O;Z0`j!!13!m%Nwk8sd5IyB@E-hpMqiTe=JQQzgWEfKiZ|52Ahe{^GV;sXh3V1;Hh` z>yZ!LPej{8)9qt%y#RaQa^ZT=`Nl!$s2$JOu|?ojiDmSkIP|@O|ELQlt@vOPU)Qmg zqzgk>gU(G_TEn23bgt_dWETAb{qB3TZ4vtKm4xFt2kVU1*qv%2=hd4r_A!^TQ;tYudsM1f8Hqo_GbE)A++Sn>4Xq0i2N2YOZgm=fpE&e&b_7V@LQsq$eK( z{s)ZDv40%PgTem?5{6x8mER4Zc?RwU<0Nlx{K3V1D0GMfxNOi0-h+&veX?{TN#>hi zX!?j`G2{_$IN;&@A(RhJIh|RBFozx@Y5*IJd|JSL{e9Qjv?|Romqd&Ns?KI za_-dO$VWS~j^tg`59$JK&|21)kon6=HWT%X`W15m`@r}`)G>D$JJDWEgua3N4VLXB z$|c%M+}n~&mWz7qgf-gpb=(K2I}aJiCXhBP2{hXAM67pzCkbpA!c12II99uUq$#@{ z@JKkpThNTdTK7}prf(W&Su@l>9Pfg#1d@g{vRDZji06U4A7}-=3+@zLwQ-iuLf=@3 zynRbj>5t%X8Zz)K*7#?k|1jfiHo*9nw*+ll)WJaGYc>aQ5=pd7I~DCdROc6OyRiXq zPU9r2KtARG7HL$%-3-3_b^aQWC?;Gs^w?KHHwgLb2R8uoIm!XuDN;D(VUVZ`)a^hv z4P_SP6+A_Gp2c0%EgbTnDeJB+>5S*jX8nmd#8-c!p0pF?GwVp+1wZ7Iet^6rkaRc? z{S7iu)blvvhCM4qlo>GU+;~IeMYN^JpJ*ppE^$Zwv%{brOH$A$Tqs{58;3R?3;bA8 z1=jaAbgS46DUn(w#Pb;U_Tm#*$v*u5cs_tI#sIjbwnx3?u%H7vpfuDf;u7Wol8+riC%bHO!( z6L}YDiucS%!+H39#ADBp`Adg&d$Pq%0zA*+#Pe{t-f*x3OIM0JdW9kGm2j|d8#lq- zf!>14M^^!O3fx*aSfGs}xQP~ro+fz+KEmPMd}9=xi1V7@gu6&br28ry_#4aMrof4~ z;{8u?FMz8rZ!>^D1t;>hS+XNWIbgGVW?2PJlw%m2Sr;Nqz-Hdt0~R>biF}&*7Wa*C zB0qwMnfENfSY;bFxOlieIuA1ed*Ob-dvw!iHe3~~c~!7typ8)2+)tBna7Q&f13K$e z`W%_YvdC0yZcU@N;Q12Vhr+IT8WtDb=K)^>z4s&hi8}_gPH_utz4cu3R}4wyO;~d| zcE{}0L4z>nU#E>|2n~gmH;jfOr3j3xhhVX8N}J)gAf>P+Jq#;LOW1B(;qA#kV7r+H zYj$f`Z&P5s{Q^4)v3Sov4)*PM*g4B+0!_qwI&EQXNv0{Z9c@oLz>c{CR^!gFhjxJt z*-cZi7ng>8sB~DMJCL7g2F=8;1hQ!kBtbX4;nRcmgaxY?EH%Ahoymo@CJ*+~d{}G> z@C$+>+K2Xq4%ZJBuKtj}1L#0lqX)wtJ(LcEUFZnrs}YzJN77MrG#!Iq8x-TW2II+e zIsxyAy$6fyE@3T(4Zeg7lbQ$bU8FU3*39HwIu$ix-7r`>VhOVXSAjK}G zmtf!VGI}|zw^^{0T?xDKKgdkj*Y?n>U^Tx6_VerL_4EdMBX$ICmU{xX(%b0m*tg7~ z8?jq?C+u(C@J``Zc$@B@bQ8UY-b***UCR4m`|Sa{93(A$7#902qyVl_Hye8k&(LS-KVg}F9`f}C*uiGOQvVX(?0p6H^6hxHdn4>&g|LSek+ZPORnyn; zcGa8EhrgsdD5Y=Fo%n6WJESjchVP1f3fK+brypS7@I$d<0XzF%*uUq}eRMzlgdU&= z@f(gq^e{a_kIG$>U+8DpVLXPt))UaG2Z-OG(3A9Q`VD5|k4PFBgrV(jyu9`+!;3GuFMU&nhI^^bCyPa7r!rowKx-_^%Be(*{~FMgQd8K zuo7dJWjX5&{beP~BNwuKvI17(LfD7>{?Btzm1~I(9KE>%ty?IV|$R9={&e z_^V-+7nb+yVTr#{TjDoJ3;pe|z~2Gu`(3cGZ-RCGURc)egH`U`75xLzX~h)>#&f&3G4VLfA59ud!P6X4ea~}VedXC)gM4OZFAsxc-`b!%nep z*>~)F_5(Z3e#9HzKe3KnDBfD^xZzF1SnlL;+{NR00(O>@U@K0>&SE=QjXUs;yc2d3 zp?P2@F_n9;i|55V0~xRxXJOwl2fKsavDesBT9$KRTh7Nji((hD4|e_fVdroFY|O&a zENsrhu%9;qJG7&)Q#l5^w8hx79M324iF^{Dj2*xda*KS&zXi1aRwN2LeQmJg7sIDw z4{;hS)ze|Cp2^E$o1Tr`-Ae3w3Y+y@X|Jy3^T~7ApZk%Vkyh(PcyDqEY}ZS%=eQhJ z?UmSfT*WWqtFb$}mapR%V^{Q2?2BH`ui#iAWA|}Ab}6ss*N`S8g2WOhc>osuyYS}o zo#Yj=j)cLs-xM0(wIq^XM;;@O^XvHy{6><8y@1AK5}87JV!g5*JC09~C-J8EQ{+B! zKY4+?h<$(%au2@=8e1cBFiC#(5J@;nK` z+n#rlpYT@3-Fy?jhu_OL_MEud$CUw7xq8n zNgTP3T#H%sUi|*%Zj#HNz;Bv1i1$^+{yy2npCUJ6cj7hjjC_CfS?rTN$DhZ3_>0&P ze~G`0-7@($E8d1Re`B>%{K^V%(7wla)$^|Ne!N$F0PjM7ig%$8%eS6C!@I=C@P_nx zy#367<-hUY`5*i&hq8e^JZ9rI8}|0ZzFshP_Cm0$*VqSKDh$B)O)cH7)1`_mt_0N0l9O1kQ~CBEYY?YTdd7#i?g|G@wNn8q9b=|P1%C7 zp!rqhsfD?P>YkUO?{0nf=(|_k-KqHn>N!_FBVN9K*70()^mDGH_qZh=x5s0KYreUX zuiNAH1m#wil-5*N1?5)HsIDrT9gE1^XntvVP3io~ z=@n&*Lh`3o*Oru)mQ~ft6jSrPj)Kw>FvP8glmfGTC4nc*TwADOezKK??hl6pKLed?sE>`sob!QyFyht1YjXRuHROGr>HK&*FtuGaN&F#Y-(L z&`njK6DrV6R-joFC>9>wK|CJaPHAQn>n@OAXdg1Ox~57cRG_n4XmZn31-cRo-C;v! z&PO+{nO|8^GG7)NRaqD`#M<`hI(E8D%Z5M|hC|_G3SSMR(-ZSi@{B>HW6DZON7n*gfnH3g>jXJkZF@8o_>ohYr zXq4)v_EAERI7Z3Ns+uh`%P~e)L+BWPu^Ww8dKqa*UJ7gp8{*KC?ZM{bb1G{$0c`0P%~+(0+c{QR1sD zbcQ0uJug$=wfr+v&69%xqfKWnR}YR{Q$*`{x!Edyt{f!jg^~|MvKg-V=E~9_JT0h1 z*Jz0r$t8Y~TvF!iM{ZBLt^jv-XsN%>LP~w5c1-j263AScF~rgdyJZ1UfDA{OEVrY~ zLY=CMk*BL6U&qSV+0EB!=c{V)xYI+*eCg^M()Jff=>N&MA-8R!NlXX^7i$Z4x@_=kkca;0u zAMK!X?(q$tpve>CC0+ta#HLRWfR5Rf#zAp{9Dy z%rZxng^iNc5NM_hH#wOCE}NHOxK{gW0J2**b%9Q-KsR}TW>KJ6c+8IF(UqHKb`jl$ z@{8=%emPU1vs`F$(^LhzqZOuxRi7(o&{?akeaGzj>1HeEg;t*gXO%fdjt} zhGOHWu~dv24v>yI*Xg{?5*Dh$c$>d4lEAwy?6qsTGe*B0(nHD7{UCtqmZGwt*Jb*s7+28zx=p;_0OX<<>= z{B!G8j6-YPW+IlB;q#>opYQ9KKT3)TubQ9TUe(sPE6L(jZSD3#_6l+0%~l$LSI;b7ErGn*GPj6l-eoHwo~GBb z-K(@p(5X^+)ufJRHSE25#CVlD47}18yjlzLs!jpC9Gtkz_QPFiA>LHgqrD!Tu9kIP zk1mf#mqV-BUXRYNN9R|~J#MdB7T_-De%#Icnf!J7dSrXGcIH*G5#=`XZPtUCj?S-J z&CFi6t_P*;BAyy!UZwORU7a45BeMRrLgUTQ{4+H^Q}fT(e6w}Fvo&3|&TqD^r(9h> zdb#6O%O-caSs!Y--twS44!p8DlOXWRoXM|Iv-kl_A2cfcwLSHT@Okhc6$qT zIh4MPXC1##*Q3(?fYe-zU_?(>;_?(*?_^iWm=!A-|!||;8nObGI zy=un7U6)TuA-7k}GLQ$l-L#bQW}9+Cw_lpBk2L+PR}@}7J9+hT%d4kvZ~@#EF)?A7_z%Q>%C=Qmx`s|B3ftCRuU_4w2a zIU?^2yQiCU=CfH3x?a3Gzj~eI)pM>lU8kexT(4ebdG(C#RVy^N zS5KDS44rPKE>EUTM@^n+C!LRMO_#0H&DQzP)%BuvZLiYV(eE@LrK@B7>-b8S$Fq)~ zr{k+N0LHzJr`8B~*6C}FH{F!8c{(3TnZ~%*{FDI$&$^zJfdkLFUh;K)=IeS_YYmKZ zoe!mv;aTTHYh+%fu>r5kQJ~XTIu^#cj<0kuJnQ&{x*uru)T{IdgzNZ)I^RmafIQIc zP^9A*>GBupbc=MnA{8%P&%5b*zD?KjT)H{GWode)ETCN!zjRNLmIrBp<2${sJjf0V z&&>;b&Q{@md7#2gd61UvwT~#DQCT9TLz?alX?gjLmXy_0cM)gI1j9zAfZBx$6H+@9 z3cSQMnqED>MnmNbG~K-NMG_S<57xda1(ucLSTjXfRjwJTG{lKCG7SlcGz6g25THs! zK_U%FC(@8~Dh&y$G$f%+LqMWjusBI6fxGMlxJ#*ryKGI|WpBh?wkqy&mcU*14%}t$ z#9g+I$DOI9j$8MBx1KHCYLp;c^HZZ0&zfJR=9j7YWoEPd;kLn}1`bsDR9XXUm~s^0 zu11Ah&FXm8=__r*lc{Dg+_PKGDXS^3o+hl4FfB}zodBmI$!2xu7KBVMpD`bJn0i!7 zN~^}wT$ZFLtEk9PSzaYp3iHZJtE;9-s?^l9MrHGAvDB+AGcmd(dFf%L)fLrMmFOL{ zB{hp})2gdx*b3*@R4e|fMm%n%KftC}V5=yvk=+bPT|A{UATYx*r)(bh=(xI-lnep6 zF16Ag`TxU6zc?P73L#ZXF$$D@@qcV2p^m~7MB8E~l<*lmHc zEpVEKVXE-a3)rL=ut_iAv{1Q_DXp%YTDmyW0z-SBS0*wAqAblc3$U390n5IFd!`Q0 z6u3rm1tAzVlB&Oeba<+u)jabwF24&_2^yzn_%wIE8D^oEt%>Jc9hM6?wUJ!>SW-ZN z+*!?h*vWNsAi~XDdo?GlEPN#tuvt|CP7Rw`T|K*GYV`v2waTeFpGsn;X^EMpHK?@I zyvA4$O)aaaUg#&*>TH@;KhyNIlcv?FG_7W*X*DNJt0`$(9Z5^eYOEVUwn(IY5P7SM z@JfUx&$NchhLMPHU+btxUjLl9 zEhu}Pm0Whcx(HR0rU9K$(&)M-_EQgO>6SQY)|ySrk|80oiwUyD zp}rz^sSZ;;Pqlt+Ba^5$(oZ8qMwcPsGAK2#%Mi&kJDO{N3WthdNt-T?U3eP zke;tRS}XJD)tX0ZTb_J1ED-K54T$6e!OI-jN$r9+5!6|H&E;8q&GlJ)cvlH;CK>pR zCIKZQe0Et)6-lZrshLe;DrZ;DCgSWk;N?$xagv#Q$4KzBci6_y;B>eX*vA#;HBzL4 zLwfN3$!OfK2iA!+ofX|1<8W*D|v8hZI=ck%iV&g;$ zKGET)3BU^)8Aa1;*yMuH#tFX?l5jfV8-YtWaUSsb(AT9oqWN3#4Z-J|2b1Ud&|7I5 zO4o|F(tmVIZl{nfhPeTtMLNHo9*ICxH4g2 zLXm`e;|X(yf)ked@m3lM=V%&B+`ovUzhQ2;BV9T z8ykQH&DHhLoDcqT#9gc7uC&1Cga0#{B@OVj;!U^~=~fB)_?humcxnT9K{LTZUmrX= zerSCE_yS4Q^WT64pRB*&(@ zrnll7g8!R5t!at;nEABg|1NATmo@IQ;QxfGN!P;`Sk&`ojlUD{1`S`|0DL|)mbeK6 zUF%(!g4b%-@^j!t7QBXmSub)R=v_6qSGcCj=PCaNT$j%{&C`l+2tJ>5MO>5T`OsTw z8cNrSx6+R!zRNY(2NG9b-k5sAcoqtp6#_@4luBQnb*?h`-kas%F@@UF%dSm58&^jJffG;!PG zUWj{I;Nu>Pd(Z+~@dCa_)BHR5jY#EY!3Xz^aMwzZ#9fJcM%#%i?qUo4XLz)4z4RvD z8dtz3-pYSf+|sxOadVLBthj0C0oSFUq~lug4Z#iZZ-`#tP5uqUb=AZzbycW5xTaWO zfge{7-b!PE|906ec@}vv>rlu;b;ZqcP4UA4{4H_g8iL27MTTom0|6IlxHsS&4SNB1 z(QrG!2^x-R02VYY>!CRxd?ezA=(siud_MR;p>dwApQjaX!p@(a-#bqVdgrn8h7W1_ zeRblRc)`BYG>MSF?XA`N2gggJrOTU#c%>XAN!uuqv(b2>;#vro)h$#7Eb}N9>)UjbO~Qv z&Nj~G3KDPv`N>5_t-Ng zP3*U^Uz#xJMV~=m5trbfn55v?&j^V49 z`H$Tz)0+cfqy?-VtmXCN@Gj&nc75!nf~#w!;tkAdziu46I(9iQi(+d;Xlz9T@N_dS!l%HE zQ_rzJp1@h*!4`V7ouWJ6+pjX7;T z#C#KT!h{KlIRf{IdXCY3#0-sjFJ^~;W46O-Sk!~MVqOR+qa{Bk1!6rN^H|J-;yLD? zn2ib_bF;b&U%@fg##|Zmo_xMI)>4N7?Sj5xwvqE4#9XDgSlcBAeZah87MKudF;2}Z z2IJM&F4p?D!WEV}x0V;}uCACDVy2le_)qfJe}nK?lOFXy+=LO&2cnGz;`yLL#1w&V zZw==FHtTk8J=S<5fI_|8K!vEPO05q2yOC_Vs_2`}!+z{v5?wKw|&)AqjVu z{vr`dBcvZ9DGx~hSBCFS(W@o=i}EG?Cc%NMzVByo<#;@>GaK0=hkFoyn zKalto>D!Ip5gL!*zYBWtp26*kkMs`+&N!7;BPQsle+)2VNkrSLmB6yCKm<}Rcob~)8{0{1ZzLW10d?#DvI}r_hrm<2tbu<>ZjF^MLpMwOW(mu;3UI=Wh!rbi#mG=G ze8jhD1wKaO@x3#gR4(z--=souiZ>I?^ICQRFyG72-(+sT z7oj-)1^mepiT_06mq`34l5;!Bp`C`w9J3B=&LyY zT!cnTf4HKNe!QfKm+)gU)?-}lEy=)gT^B%JuB52_#U)?nV4mB>oYJc|>B;1qNq{Uj)wS65meZ^Ce}z#N-PM zc}s93I|YWkB{6Gd+_e&akHkwUMAk|UD+HddlzdjQ&H|R))Vy$w#9uCHu95i5Wh~iJ zw3CD{lK6`xTqNm>B>lG%U!;5)xTbR zIGqdM>Bb3N_)a$Bjj~_)a%YmcnY^dL@V3ZsX`*ShI3oWRtQo)BN_reEOX{$zSee65>)E56oE zzZ0h^(eK4~x^aR(zSB*A#@R^4IPnSJ>1LtgJKZ?n3E%0)7Y6X1ZhTh&-|5DQPU0k^ zIGkd15zcWE=NKi(bBvPYIY!Aizvyb5=7cYIBPD#f8zb>f{B_2OMVoM*6TaMyGoA3| zZk(f!FL&c)Cw#dZxxtsaajp}-+>Mi*@a1lt*Cb9I>My?8%?66IR@gA{ZEiL~e4Cq% z6ep~(QR2&#II{`o#fkp*KjK8+XE-C@>bT|EMr(93go~QoLIEuWed`-YzND3lk zjjUJrVzgjnK>7m%$p}ok#NZMCpJBAv>b%>w4=2HjFM4Jo3}4wDu$+Vt)^ojhudOhq z*OxdAb0|3ef4=aoVtI+v4FCKxguY?9jNSeR1skV~{Q_qQJ*v(H-B@47PsSku8h^kO z-x0N3#^>t6_{l^XzX-@UV+cujo|iv1&WXZh{067^oUD(94A<*Sk(Q>hp}mM9q-DTm zd=2bj6J>m*AmfOM@!yTpe)vBM#r#LPpC9wq=SK#rrsquwLadsIAyy{D-~UAWqDTB@ z{Hne-21xc7v?E%q?gc8ezvbd2NIccoI=)^iDP$an^u_n7&-+q+O_GZ>gLvm%mZKpC zGW@6+YUzI%8~#%J!Z8y3y;4&<>Kh-j?nK|Wj9l5*G0Kczj2Ge1mqoboxXi=r@(q0d z2+&E&`hML^ANS@0#%RZwHtvVJ(F_y0FrJ2c(1&?fLQZ3giSgf!Fa7XeLx#}$e3x;= zsCO*;2+k8QYx}R|pPyl!{^Sq%6G+wcxdi9pMQfRI5RjH3{uV!%$Nx8sf0Ud@7S5;E z&aX}JHHHQ*i{uZWFk3kQDIe;Mq2(!Lj2s`BrOf$5_2vLJ;4SLmoVvjH5HpD!EB**L zsk#ZCd~!;Uz+*ni<0n0599n`*5c8WUZH)T>`({bwZ9JJX8K@0CzoASfR?crGBmZ5F zVgLN5sm=M#7uz4>s0kjYXzGX5~Wlk)krsxPy}&HRX%R{0gsr=gwtFA*O-Q0lB2YnONB`w1l^ z(|m-q*4o{Rm?dN#NeO>~aN|+Y%fN9zX<|GIz!4~;B@=>JVs7*Gc#>dzfH?b%rw!Qd z<>}F(iW1nf$e|b?O>j!4x?;r*Bi^H*Ew}pS6iKc7tVt^8JgEaTEQ4eo%?OtJx#LpS zwl4)=53Xl6YB)7B0ds7KXHm<>Ar)d6BE;k^L(f2-9G8&Rjs5*a$2n!y$Q91?P5s$8 zq*AnSmKw6;t7IJ^5ysc(aVOEXs`o*gH+F%;F5_J@1>+TCgYk~FPaC_@M=jFc3{=W0aC*^WzIzM2+P>N5L3^gb=s%#ix- zsucoitC65X%JuM89Q@Sgyld+D))eY*Uy(a=1R8Gww$4@|_!XIX0fH8zbu-lXw<157 z4`i(aI%LUT7@p4a5Y79UoiFpR8Iwgf%vshvar0+Y0-x`;h?TcQq zLa;Y(#jp12xunLl%@WWKuwD0b;Ei==m>gAa0khSId0axBj8{yI|1POdgIdWBKp1r2mHDMv-P|I&TQDu>m$!q@LWB`n{*%hzW9UZTS4U0sP9l<8`TV6yS) zj94KFoa#3+)YmR1tqlLk_{bdDc<0N3mY`SU;UcXbAF=pq|h5 z{r$xfCVQz!L653vgsD3D(n6&{5nNPOa0Xe`HLD1kj*OJqJm9p6yZWUs89bBd8@BZe6yAgHOHEz?+LEzLT*L4za3Toj3AkZB3wB3jR4Eehz;Q z1=geSg5($y!0%t?eNM_g8Fo%cAOgL|oB@>N8xk%_<wz7s{4Ru&jM*4IRDH~C4Yi;NR>DhTriYSXMeUoMUPsJ-7Mj~&QW2+|exA$tQP5a>i4Au-|aLR*=bZ2}`M zVXMHECwYfhv`C@-$-0%{;w>iFFQjj&3#}0aYkZyA)WpjA^7o(i3a{(vT+MwX!iTDr z8lZlfi$5D_le9KS{Y+rZ@nG7Ctdufb+H3-`ay}4oe^Rr)tWqI#Y)FP&Nq3tZ%v59T?ZaiTP`@6l@8u#zOqJN&d29y{%i)vm%yGZcONzLm? zK}a4YeXN>D3rn3j!NHVIqD_=!iI%ftJG?vgH!gED3C+d_KL`fCoWX)Ddd3m6Uy0V0 z)V{TPUHhc5P3}<;scURCGov&(BKM~;o+N**HHpzx&m~5kscnn-!OWi|HOpULJf)=_ zH|wab{TaHSlmP)f)10ZA<2!3Y)~Ng)73|G*Mct%Ki6m)+HC~rPjwqpJNKM0MC(-kn ziluU(IAU~J^@V_W&o9??>B9r$3&;CTRvl3?thgxTM7ZDqtyo`n zZH3<>N+oL4FJ~o2^-!g8n)}Ames3}`@BW$8#8~gfK5#Z|e}@I^e`~->2N<;x(5qxG zk~9{r6SI-z7SQVE{FbHX%SLrLn~yOj@Gsx}oU{LehgEJ7()%x{8w}PnqYT$2|6Y$k zzn#P~(TldDFK)-&D5N2ELsE@tA!Z)%>UJO?rdjj&q!6&j~-RV9>AiF#f5$?cIf52 zBJ0<925U-lofHoJ={uBvr=HzTV!T>Ls_a>+$BC9~uC+f?+mQAv^p4l%2;XbGjT+SY zyi8lRsvj~r%l7@J!izCn-!82V4Z+(eXD#h}3ev>VzMwMOROGLTFMo0zVea>UJK9%s z!%gmJ$n_2MO3ZJ@8|c%gMYx>PzmhxlrjBS6`%I9GvM$ZhBWOX5md2&VyMV&gC_+sO zjZoB?Z=_0$plR&OpBfj(>c}WTqx25QKh@fJpXQ9#^`yjw-Rvvfw&B>}lDW_+s~kxl zh%F?jMXG_zHo+6NHz94s+Q^^3Bf$O1_yw)y@|8y70<{y=p0E=r{m-G^7sZNJ+79Zo zF8Eiqm}!($x1?l(vUdf{-dYlTAJ`@)J<5p|M9xq`HMfY_U2B5BWBxcMD5dQbb%FBu zThY&=?iyIzmW{nKP1Kg|0Xl}(#LPMswJB;J`hDF?&i?h;52Q|^a;@U%d_oJX=fVtL z=ZdcL?0fX-&pymw0ebx}^yjA)E$R^?6;~6q@iyE;P&QTC5Y=+$NJv?Bj8{3j!0Ao6 zGtgH=IU$v_?K=c3cTFL~)r=J)=ijS!>mjCj1xi;F`=7Y~xt{*#DGIr%G)nWGX<$Sz zv!nh_Nqr>*wI}K?9JYvUf}2lM6c1t+Q8Hqyfbql?l?=3c&2_t2afGxnUP8Rp5^1-_ z6k1*&Y2cIS-R7uLu=x~7cg~Z|XJ-fqLW|254depZCM0x_CUpix`O}90JqG>83NxH6nYBPSF{0U2x+EWsw=j5+G{oh zIp$_jUqvQb-N7*dP*;4VU0r6p?)uqsGqnH_Slhy{)A>vEPhgH7!QHS@c~=@kaxdh-Xo8bOMImVUDKtbI#`3fg4Y z1C+!H)xTCrP+Fr&@?X1~{l38j17$&jWDNwEHE9N0)=(zOy!%_oYzw>W`9eQvXA;S~ zpfqPULW1SGp}~E9@fHoVy*?%fNFD52V(vTzee;Zz?I&^H2HkF(DV@x|(&B)$#l5>s>6Eo3NcAizp+gD?Yv+@2;%Phde=+is z0Ev8wr*;Tw2mgBD>)7PA78(UD04qzR(@ttBClU7@(6)k8bALPRGMoL^4*d&h;qu5d zck8><%OzgqL&R5CCljdm?r)8HgC)|}0%W3vs6M{ENfjv4G*y9x`hO6VEronpWZ2)y zCHk_@ehO{fsL^9Wu#v#DREOci7wxAS<+WWKjpFxCCD0N}2aRzcw?>`^6&cA=wR)LZK zn@`3Mb!F~frEc~Kv47;3_89%vdiB@%U(CMXA!bz}zvN{?f7P-kor-JrGjnW6RyKqk z)ojnbg-9FWchKO~>Hw=MlZz&{S>$>|ed}MWXJ7X-_UFits|RXd5AOdEeP!ECG-%VohVAAg~&kPXHe48zzMoa8W$O=Ph+m0>E*U6_V*8QQWLtcInqTDFj- zvBhi|xU6KC;5>)RaPmSC&R)2gjbt0xZEO_Vh`+Hohv6=q?;uWM7|-r!kK&Yv$JsOB z^*nnWr#!rgvlbSxFY$LRHZL2o>v$B8XSegtJd54Ob9guQB=5ls*;Bj^@5i?B0em!j zg%|Vj>|H*QPh;=%a$e5%@eO#SS+fq*Y%EqXPW&G+gnkwfH5=xSe)Q+UVz zTk;(!zsFyKZ0`iLcR1h(8cA$8$0HM`gJjWc(t_sD9Fj=8(L7-CX+MPar~OF~9YDtd zE~ev%n~tZG08gfqfuDl29&EInRsgP|^WiU`HzMv$^d`VJ)0+``3%v!H4fHNx?xqiu z2>J+p1o$oVDbkESP2VNKIP>E}5{i?G_K+d;BYGI{5qb>yIgWnG=$H74QzAV{Pa@XW z^c#{Y&IuvO^k@1r>4!f12WgGoWg{^Ry%ygR3SlAeLs=+ki9QxaVnq)oDJ+sj;wZ;f ztQGK4_zF}E&JSrr`mksg4f+@sL%itCailMEF&AhOSOV#fo}CC7XN-U*g{6=T(aVuy zXPgJq66bt)fKOv-fN|;w$;4?N8AvOWWdf7MvXNpg%SBoRtN?Hk>r0Yx{>K2s8pH;J z&oDLu@JKcisgJ^`AF=4qV?jBdO#q)sY!c#5W|KiPg-t=~)7Ug%X0REEJCn@>zMPc< zp2cPXp3P>HcC3O`kO8cc%_DY41v4=tV2zvyWEjYieDSM1PhFm?)9tY+L_9XJQl|6-6&*HqB zChR%(9AZ7so<|N}U@rpR#e;L8b?|(X?LfM3v7Nxb&E5s(J@y{* zzl(i?E}yKkOUz($>kH!9AF2K;z9N)Xb!PM2tCY>06xl& zg7atWGf;lcJ_me^9RmzWM2dI_4fc?q8i`cgiPFd-}9LssIOV7KwxVc%eoHexOck`glr z5;Gcb4CJP;$i$IuQewtIV&X(NvD(5pMC~CnDXbk`NekkJ^t4OqNu~7cC#9!LN>47O zCzsMQ1=6z|@B7ZeUlf^*zZ6n|87~Slbsqd$oNC0$Lj0xRY@?-smqX5S$k{uQ)?N6s zL)zXA?wjy8K+0SyWo~OJb0ek9?ImSyJY?<;?DD)t-a~$OLH>qH`P*K~-+@y8Hj(nT zgOtB1QvPDL#eCaH%3p_+zYOxX6=ZP~je>M*OObL!ODs_NX7Z6hix z72|UWD}hc-vksesbwX=P7OlMo`g$pP9^+bjIjRE|uw$U3TcM-3+2U+Fs^F|+J5aGL z&UT~DO##~j-5rPSJ_X-8m7NO6Y0%(qXz$wI`a19>j8hkC+ z;7Rrf`-!cNYw#kj!FO;CUS!eWgYYcgz*spAKEgG47ki7nWjhx2p8jsDU~jX3K+7Lu zhin~Ov-{a$_CD;*2kZlz!nJ$_d=qB#!=I3#TkVqFwuNi_2>cYcZ4uY|DX#YybG_fp z^?o;bF8CHa7yN|hVylM@(D0MA|1n(qr?~cyaP7aDYyS@TBBizn*Zv(+g;as|l~N_f zB})+DmS73D1Qqai$PzS24d}m7YP6N322>M1qZSpkIoXEA+%_yFZwT-nsRw$zSL(&* zKB*sV2BjhNKz71U{tdMso+*5#jQvMw<0NS zMOwHO@ku92C)z4eC+k$$B(fy|ZcD1TEvbYpIoIZbhjt#|AA>D%aa-bpEx81rFNH<% zaf{NxEy^gjC@QxoJ=~(WU{OAa?_MokjrO0CK84TMNY~)=r=?HZ8ma0QK7STArjgs2 zt=z^ma2w;}*2M)+@JqH9Ze4uby12+IM0>I>K5ku9Ze1#2OTLZ%Z;@^REw{q1pf)J# z_4%lJm#u+Ymr-tARM?ff@z;A`S6o!j3;o{*>k^Q@Cw(9Go9s#ecI83*^&wc70JknK zZe0SfE{~%9W76Yj`D54^AGa};+{UngA@#OZe`~us1GlZz{RHY2fyzlG~dGZf`b`=WFwj z=Zl|Yb!xfQ>E~9bmRp@w-0H06R%eP^ooQ}$qTK3CaI3R{Tb-5M>O{HKspVFuliQn4 zZf~O8-gI(HQ_C$)JGV4L+|mqlOEbhR%`mq#ZQRl@ZfRCpA+o4DAW>@q70RbgjZL1NJeFJvmCELrkS8RW0W$RFDZwi(6;HASyht`MA-@$gWUC{T? z_0aK`v5&LM*%jMp|hW5&#~XI-?HaXmGAec%J(9B33~f4 z(97?zciI22e?Yfwv^(LqKzblGZIF;fn0v%be5MQ86s*SvSc;9dcDBd17#{yZSdR_1 zoq$Z+n0*&)F#Ybw?>?Jd`sd73_Q1?L_At(BGP?&SFqrgHocf3%3Ta|@+KSj+wo>*T zTRr=Qt&;sWo~Q8q63?&jJdNkqh$)`Izt7@%4$p7!{1(sicz%cH_jq2w^CF&?@Vt(G z-+(`1xAqQSX4F;&KdB7 zH4U88z&Xv}mttnq>>fP#;<*pc>uB?aEotw=>5BsTi7^&qjKvsZF?!o&^V|0zZezSf z6Y4B#h|x5*5YHk!i}8%&S%PONo-sVj@r>hHfd}yfTZv~9&ni5t@vOnK7SB37>+wwC znZ~mT&oOv5XD6Orcy{AKtO0L9gEycdcGD1vX`s9VzGw?5kU@bA z3S|2Vc$O1*R^pk&Y!d}7pr8d5w15Isgv2w7X9=FAc*gK7$1{#+1)d2!EAdR?S%qgc zo;7&Z;#r4hJ)S8%(|9)FIR?*WJX`Q=#j_31c04=q?8LJR&u%<>@EnKd3wZ9p^A}*= zW{ZG=K2R_Y3dTXfI4EcV1udYU1r)S!3cR4e3ktlTzzYhzpuh_Xyr94f3cR4e3ktlT zzzYhzpuh_Xyr94f3cR4e3ktlTzzYhzpuh_Xyr94f3cR4e3ktlTzzYhzpuh_Xyr94f z3cR4e3ktlTzzYhzpuh_Xyr3Wk3S>~w1q!-ALD!rVq(MO%6r@2x8Wf~KK^hdKK|vZ6 zq(MO%6r@2x8Wf~KK^hdKK|vZ6q(MO%6r@2x8Wf~KK^hdKK|vZ6q(MO%6r@2x8Wf~K zK^hdKK|vZ6q(MO%6r@2x8Wf~KK^hdKK|xxdgGx|P2?{DfK?CNX0dvrRIcTsgfE4}j zT?eB!H!ybtbGI!6O#jzP3&+j|Nw7f@Y>)&SB*6wrut5@RkOUhf!3IgNK@x0`1REs5 z21&3%5^TCG+aL)xNP-QLV1p#sAPF`|f(?>jgCy7>2{uTA4U%AkB#26&JMiyLJiGAh z#i~Zp z;I9Mxb%4JP@YezUI>28C`0D_F9aw1&@YezUI>28C`0D_F9pJA6{B?l84)E6j{yM;4 z2l(s2N^@YPIlx~B`0D_F9pJA6{B?l84)E6j{yMPIh=Ld>Xaog~pr8>H)M2I7VWrh! zrPckf*LFmK9~3x2ffE!sL4h9>_(6dm6!|9f?XusCEhHBg{|0u2Aw_;jksngzhZOlCMSe(;A5!Fp6!{@Ven^oYQsjpe`5{GqNRb~> zaXvH9UXD^Ew{dSE#nto)!p{}A6(U9$JUXAn@@)kgC4NOkzKQ?WmQ$3tfQl~MNygJaFmx-Wio1M zr_E=Ecr7LCS^ zd^J)MQq&uh$s1#+Se^YsEXH=jBj%&p@0F^+51j6@Ka2bl?RX3LEhhXGE4;vK!e2Gv z-O^)rqkYDHfZ*tFzVH3P{2r(-C@}l`Qy$!5!vAQ6+wV5vFUV}JoptR{Am*oe5C^>ob%V5r`-HK zW3~sL_B+k?Z|A{pHsSB)!S|Z*Hwg}@Ow9~Phar_M*rT!)mQ}U4b#$s#rZ7S-t#ax_ z!Y-<`wsdrMD2l@rR_Pn9ed#1~c^rm-LLv)YENF^vp}$BAm&RNnS1?rI@dZk<_X`Pr zq_ZL2E&H9pA|bjKftE|1PBk4#BEQn1`L?j)&P#6jfES$Hn?8 z*jiat{gRg8E16@;B@Jif`AYs1{(Pz z7_cxuX>@7(~q#`Xgc;@=`XANqaR{{l|(lV@5J~p47 zG?+tr_EN5A1)TJdfWKtI2MAXa{sg;3Zx6WHPsYySAJX6RW`CgFPI^U*^EAP+zDYXh z{XRje?Pie;IqH{4CB!E7 zmtF!&(PLqi{cc+oc5SFC+#Ax1_}6KFsX!qL=o|)(2u$ejPWvHPZ?ZTX6Rwd3{3Z6$ z4{txyo!8F+Y7dI=pM3xn6=P3oe#x6fU!JIKJ(fiUsh_!OA(479k(wuCc|>#UXSU{H z^Q6F@s4BQaI%05xy)4n$GpGGt`-}LOd=MM<;jdGBNF`~dC!r~E>a?(| z0V)96S}yGr+L7!8X-lW0L4u(t9~FAsFR?4voWE{CI3$&2-N}|+%NOrx>pgXK&#`sk zWTem=XufdS@i$C0tUtruw(as&a%A0tjpq&woxNfD%t0;bw!@BlN=v71JcU##Um5U+ zHLMJr$%Qw}ZPx4sQ5uR~5r>sxJNa7p8B0f1HJW|VnM_C<5-C~^FVTNWp6;Hyao<|dP*U!RC!z(Ogf?{6`b)121>62CM( z7(Bpk4iGBpiW)gORZ1VT`!$VSmE8}7*ekwFIGQ?^&^b0`{96@?2C@l`Tw(ToEy+H8 zw$R}PDL9ST#0kGHC$Ctz z`SHkSTl-2<%;6+4@q5*Bgd9P4Ak0G!gc>E?veOa4yQYNrhP6WPf`lc&Al}~rTsHGa zLE!Sbzy;@t4>KEZQGkns$4StQ7F-<83N?;sy{U!POYQ1@2a87oF(wssx3XWx@n5#S zZ(UOXjX{0w;PbeffR^#`n=|J`ut=S)iD%4+pt=c!YLOsWa`jXyU6L- zTQN8AbfbIer)? zc*z&ya6~@9&4iPc6Y!S|IA#>Mn(%`L99sBVu6soLr%iZwUi$+keAxcA96p#)1D~yV z?VmK;Q~%@(iT+yRhmi2>M&wBH1-6 zW;2haTOMS7MGdinhW3tHse|};6mLJW9osaL>4)*RFj#yR?r&vUfCxf%5x25Sc3!)x zVg1?TyFQ1I_3Y)NJK9g*iy!;l8@_fD`Bc+4p0wt&tt}ge&R&1v)q`iR|JXG&3*6J= zvvVPrm}l9=e0Bt!A_M_{B@a%sCg9Io;RP-e{(BSNEj`IK8MUW88~P`WX8s+XrsaFT z%Y##%O^owm9y~YB3s$&=j(76lWZ^_VPvya%H{rjU2mWgl4m!AxCHmoX%)!5B%=Vy* zTR74F?L7FHIqnSmsfFLPN~nFt&f(zGvkbrUafpw`_i}$fz%KgG?>}RH--=J}`)}v{ zouog1{A}Le4g8$;-{jFl7F*EyZXTR0wt&BBg>%nB!2doE{4EnckjHnN$*hElhZIo>y#8W^+y7v~-^hdK7_J2`S3e2ewgw(h6N(mnp|o8h#2WTc<@Q>Z>OGn?x{bF z$8TG{V#V^?$bsJyPn@!Q&nbU^Q+&e0k>Q1+&ku0^@Nu3&H2WlP-O_KTrQcLci$8GM zIp>`AKsQvk2L+ZG)@lK3XzLLavAYu zC(LFhm!+FS<4G46704we-jP6C^=g zrAeuv(oqf)DO0O&#sI}R`Pp^+a(BGCH@4oxR9Q)QKO%S3of41VGCnaeev8iG6Lt(8 z)8wzN`)ar}8i_oTN&FAkedf^Mz>sbiPv;W+O|B`QrQLcHPMTN1UozmZm%!PCAH0SUQOAqO;t zbLUsVK|YtPzhQc!B;YL!$Ak4dd)KY%S#M30$hoaio2m%0=2u-zM1i#sY8wD=TxdaQAIM+!XS zIC5T+A4J-8kn={skAPMIe=ZMBJQnbmtnh-k34hsy4-gMb_!A}^yx~?)^z*y{$6Q(4 zA2i^^OYYl<_D`Ge?!5K~tZ=hESyj*l`uO*_&vFRoZV%c9_}t`AO*uI&Jh!2H52jPv zX5@0PD^QG$9ArJd-#T6DcRPZruVkQXXyMrShDb~aEg4ukv261ea_>hk-n`=Efkl0- zV>RtN+^XW%;^9J9BsARF-G?M(w6D9nZ)!Sw91pg?u;`nA1-n}fl^#ucqY(~UNU z`=*C+u9@~HNRDWqfTO4DObTOIiTGQA9GhOJ83C9lZR$k?%BP25Bn(peEHUk zME!n=tk6Mn#i zgH{VZ&*Z^r7gF@|b`Ea0b8kk#{~-+{|w3Fz(A{ z``66&#CHQ<@@5P^0nWV{G45Xo4w>{}t=$2cj0wHK5Y3QLiTsd>Npb0%A?!_w3 z*sJn7Hb-!paW6iwK_%gS#;v6k&G~5M0XNPmsP4x~A9reUT=P8Zjw@>1^?>q_OLHnJ zax&#n<;WvapQ0%0k;kP0SuT$s`2zhWrH;I;D3EiG1(%=Sh;<^>bgsDVK&={{mJQE|UY{>_3<%9FR*R*4>0U)%rWjW)Ly6eX6dKM%;eKcV%y1gaLH1Yi!BLzj&gfU@rQ$+N7OIMja_Bs z8JFK(7%Ws9BL~5P1nUzla0Ua}w_hl(j0BV3pp!8*k$qt#`&efVBhnUvvbRkNFavg4 z)dK!z9-LOAfWNL&fHi@eK@a2p9PRN{b#yYBM+RaG99g?|-Pj!))!c4B4hMalGB51r zXdJ(0PiB|XufbG#L|RZzW-pkmh#dKKIaxo#llRYO3^iQ1wiG!q0jC)g@R#&lmw?mE z2>8=+V2AgLN;l{$>=_CNH z8aN-=S&{X;1^h*%Q+OI9Z$nAXuC%xMrB_;4HFREZLSw_ywdHLMm0hJxo$1Aup_rff zf_3Yw7EUdQsY=}KsvUAyFRJYrObqx-Q&o-SeVNm`TY?dnL)KyrIlN%~qU=m8k%$df zw)3nz$ID&-df(=JyNjV#6ZpyD(wipydseu;+l2p@3GZf4o9)RP3-JEt``(S__rMo> zi`n1T^5Cr|{OeY@L|&`F_g*X9&P@0{Ik@Drx0&#pOgQj0+vo7Le2@Lw^1W}E-#f(q zX|FZ=`=$l{v^|IKxANdQydJZ{aqG&U557wO zG}}L#2YBNFw2*{`+El)FuSGxwWsRyujfg zk*)cc!W{}IW&CJVZl9ujNQt^aMMDtee<|^@>hxA2_imn&GC3DCDgM5d6ZRn!{sSu< zx7LgqyWix{A$vDCgb4NDUD;|m8BBy1l##%0o>Ocl)7c;ex}dW|9K8XDGp!EC8=Y$# zyDm7fo-h5zs;+c%d+Fk;pt1CqOoig{-F?j~yNcx~&O}rMlidqy2g@c_rWTfj62Zcv zK-tEcwP&bGV5iUx@c|#{hKj-J6T4e;x}mG9qo;IWW0yS;81OYOs;pVj?wsBfjCiDA zO!KJf*s{eFf^YV2k~oIFJ!rnW*jMqdgz8p@qzc&NVzFahmFXnvD4Nl?PC3yO_&OZ3 z(zYvxwgeHnQVuwlC<<;F9NF65zHQ@zfwJX;HG^g2?%|H`TGCp`zGJ5~`&#?pz`m6$ zPV1kjp6Fgbotfxfx6#DlAha@cKcsaw22lr+DNwm^nuM$OVW@0cUsVze6#CpX4IhRP z`}4Z33Vb$6YH7ZHF0`|zpE0(A&_$5&2@}-x%2Tz;m**sJ($6RVGWOs!EDpu0!&Hj+9AOx>fjglVc zWAoVx>9eJA{m$W}N11@%i(A~H{Q%)=!k@6h&3@>V3)=J9H@*iNNuTQd-D7?aaGcsS z;J5SdNrV^bs^p|as1o?$`TH^0WH?G%*wD6)j%`Cjn;|oh7d&xqaC4?+pnQB_0e-mq zPg}8a-@sryLl_y?j(naZXIkkP&X*R#AOH;7ZAik2c@a{9)rocIq+t+*CqQ>PdUR-J$kgp9Vz24|?_Hd074LIY8>3p9UHFgFB^0B9JhG!?w;bImd#hzjJQ3qq^rT_2{;|`c%aZ*5@DS# zPb}`LDyrIj=ID}>mS7+i(I4K8z5;yW`R_>PC_?oN*je9_2i)|>6 zMz!2L-C;dzATp$e*KsuGC2+od+c0td%VAj#|6~D8K<)NOG#crQgWEI@xm9b-!?z;g zP^5N4G!~0)sEvfek*t%ZWWC&;HzizJyGbfcIu0FRH|ThyA`-klCg9)8 zgOj!p@Ow@8fTcZY1@r^?%ljVJ3gUbBnBO}jMajF;`@6}6ceA(6_T-g`{w&|a-*eyl zhWWii5+kon@9&#AINHN2)8XIBgOj#O+X&ystZ?$mboecKaMEI;{p}_k_;PJ2;2hsM z_{t);DuT1af(L6Z3G9CVS9voho4u8lBCof!wKQa&eVH1vdvf%kMcD!Z2;nV2- z+)wQxpBY%Jzd=3`@#~tApM5LFS>d@O70Q#^N4K}h5w!e$)tC@maL+xn5VyKtlN?y_B$fPkvQ_ju zX6s(tn;c6!PKcZ&{n$KbK+XmwbxEala$W6klH)1k46mWuU<|q z6zT2opDg3QZO#PFonzxanyA>Z%PmRRq7E}w*xi!}Mk8T4`JDrj9NFGc(!HvA=vcoe0bl5&XVt;~ z8LovzdqB}( zGFr*KoM+!-&^25#2~I0n!0)xf`N|XUd#rHjA7=X>=fP=3i1xSV!AW8T9A|VfK56iU z(j>UH*oM29kSUXEI7@&NaQeiNaLr<2xvt^CLZ>;8osOgVxuZ7VaRe1#<@&OQ{(yU9 zAQA~2he>q#w+s#M=q%|0Yqtp2+9gK={dtihpR|o!hQXSl@(IpmEnMhSBCMp?6ZHkc zibpB>SjS+`aU(5j`^yG9hRH?Eo)9YWCBgw;)H6EDTB{b-Em~ckM-g1pCx==zp|yjXGIm6(B`)OG036r>Aum`T{|@*LiYCud&Y-e zYsDz2Jt#&I_c8SS0wMS$>bk&VbM8aNZI47V%M~)<>YdSO_4P;Nzz#>k6_t~UrYVz^ zkx=DLMJn-jDcz0eu2M7+y=|PRk?@>h( zdhZ${EHj?Kni6uu7q`yIxK>uqmR7r(yGBXSP{3E%Yc)^e%D!m&O8UG#@CpDevd={45*v0BmupLL!ZQ$&sw05 z<=S$4Bii1QgQCt~6%)`~32GZg58vUQw1D2up*=I*#2vI99{^`IDuH!CkLI1--gVKnBv_Y!?yc@X%u_vHtx$Ktj;@%7_kVCGpxGpA%9CFcAm7F$t4_@bXq^8 zWN&I?KjDgYcwkkvJz$QGl}O{hmF^G5ksuBH_}QQo3#*Cn0V$_Y&i_F$tds;FYkVe{mc!vk zzS}C~`!M?>U#qwck2P$r)$BVA( z@%HA~h{Y0Oq(hAMRA#SzktZRm4h#Q>iIpT$H5s?xS>y>v-BI_Z_3W>F%Cwed##lg= zRXbyXR`f`jVz!lY`dPjXpLHOIANHC9qr1K2sy(ALgbZ@ zVwr7bcpgS1m|=9zfzeT8nm4+(w}0I4j~vtAwX`CEFuJz6bBGwZpt*CdpfQ}f1YY3ZN4NwXKs^~lQYe2oOHu^BM%s(qW?i`w4ENbvDSe zuM6NaQ9eS~VIqkYEzcQ4T>v;k9c(C~xC=u5_GWS>TJu<1v-Tn3);j9jS&Q%}j3sDr z_ZI`J5XOqAn>^ir=j3H#+3`qOZRi~1|8(S%F$1Tr=sjj(`M@a?JzGY~ttlAriE=Pr zK5yc@>0Jzq-hgw>3GC!e~T4vx0~?W=7HZ~!a)a@Gchiw!{C#J ze|MVgfj_tOqWz;5co;Ro4E%qBDh39>!)8AjJBRaigm{pT(=Fw`m;1XV-j(M1z9sI( z_bKird~)Bn#J~7E$$$Re-#6n|nvWcRziXk#E0OOa=zKg6PTq@vKV*gTxLLp-nFs!` z2_MMg`_EA2flll#nE5&Q8|N#d)`*dLRu>j&c4iO;N?Z_E6epW+o%CX--|b;_^pD#; z@1oh|FZ9O)Qh~p~7Y~S;0VaIrKEoy3lF0#v21lLt`%Q|7ljK_p4m^+t_n7b>=D~B4 z`XIrDZm{o#Zs@ix$)n#iLI2Lq#;go#Z0Yg6WrI^4V;_MLIAO`i);4Tt+{@~NNOLvx zr&q2h9x>8f8>-iwt|qjPj2@+EmJIbT>pfvPa$mld#XR>lwK13bTC#9>%&LPx$H zQUxgC`xch?!c?iWvraoP@HHZNLA)?lNO~f`+}7T{b!2c$uqg30GXZ*kT$2`&6EY=t z42unmq4syRIO@e3SwuK{ItK6!W@BJRjUS2u?6vPhFtDe%8}#ajVz2-_a2Y&6bZ&{l zg<~;)BI9YEeIF7z`C*Zhk0+io=LJl7w|z*Yf`>cgDZ=K+4`(q!c$BBIcTmg38sky? zibM{aT#?9$I?l%T;UIW|>lcP)CgA|Y@FLChRmS=T3K_2bYLes_?SCbffbJq&e=1)g zUlJ<>RZwW87^d=NeWFu_V7xWj&s--~PEy^sbHtsm1{56^VxwLZXG_oMZ> zTrY{%kBioL3+2|l)=!>IRvuY;c0afej}Y=_W=4210&$2`OhWFGK0+*UhSc$P-R6wX zV-Kjl%+m7qj*5z72ZmZU_SQ|5`dr8^`HOo~?d|EZV}}=>sM%jp-S`)VrsDxyi%7b_2|qwNg~QOg5wa5%*FOBd<;uUN!TTRb7E zr_d~C)p_{D^CqxPOzm4;-G|65+DAz3r%(IqTf+&r*YEWg)wma|?)?j;MfcF+-9xF+ zKeTr^SpY}33JRR?v-OcNXLoX0IbuVdS`$vujevi^3Tckfet)5hcf z^sdZMJk-?Iy(F&1;>~K@7oB$3O^q-cE-=QkGre6a24CYJ_q8Ts>y)lONH8D8UI=Wz zCsqziVs>=6^fwd!y*zlf>{@C+bW%_eDi}qAmx2+3e^292|Q{ zxmoxwal_!A^cRy$kLST}H{lOi;dYk^{}I7CR|g~wGTCRdRYK;<=j1Fgl``P7@A9HG zZFV>@8|)g$X5TD{oqcY`Pp58Nj=~Eesa%Y6tbdJv+=rtwo{aUVOlO0B(#D|K9x`e8 zzLhrI9W^BWek+{&+hQd=pwq0vKE4B*v0F`h0dt(*gjT}E7SA_b1#tz@?B|nTaI11y zE_B`OPU9pjyCj^Bgq7?k*y&0*QWDOdqblHKDY7H==oj%aS<>14nn!SPLRC0*{CMhN zxy1chmqUx<#^S}X%bl@EL~~rKT6+2B00+c;R!GG+W?4^oA^2BlB^7`Mpync(znShgh6YzVO1+2h_x?Ln^s|9AsD zw2HatHO}8+E#hzyNfy!VTKGd}p)R`@_^W$rYKsH@!k{Nr6aE#*kETP{f&2(+*O3&r zls9Q|G@?qSHCe^N1Fok8?vI+dgZDf$FW?X5!Cy9Mx?jf~x477k*lXlj8z*mt*5Jjb z#K~JYE9D!5$R`)EV3tmEu!e~0bQER(!o1ZoZd<2(Y#UvPjs{~%U-pZMs<2G+#QWy+ z^s<;IytT)Kb6-%v@6ChLda`nf?+DK5w+|3?*ic^s@6@K1liSPEQ{;$bq>qq4apd!~*Nh!*)WrA+_}s0x z*QaidJ#IQbZMdVjF~o%L$9=rqruWv(#bL$e2zV1+t$r5P;#xcwe~LL$>2#{Nq=W(~ zeFJ>kZtR`<;EbX;Lr+&Gn9{Nrpw0~5zJYCU=@@el1eDt`OILxWD%2ClYDFFQO8e23>G8!|wYuI^IvA8f_3Zlg>W1oS#D?)`Cec%pa=8mVQCymH z#apYsTpIN!O0W+oQO!-Z2zUf+&||=aOeyWFaNixd3Ejt!D!0`ws;*vCSGOpWSyWe5 zS65Y4U(dc!wYZ^iVI|IBH7v$WkEW)U=H})Wl7794rR0DF(>Tbn@#=G`EuE_pVcv%o z{!*^^oTzYh0l)fs+mrjwHOtQB>RbJG*Nz=~*7o%Fbz?NWcZ>$2_?kJ4stk1d)s!$} zEQkkPdBq%3_?Z1EuYG~ruErHI>J1XdYfmslW6-?MGS%!=weRMYJ#@J(6}}$n4!s-> zdTwb@H0-%PXLP;4NmKt%JjpMp(R!ZdxWPjs>E{6o#ZD%Ie1{)BzNN-`t(Z1$i^XH# z($q5(acN3N_=gQz?29Tkh9FVEdNIGVDrh!ViM4Mt`T-Yi(-IvN<#Yh!<5c{Y{ply^ z1jr}pS{?J?LTZFL#W1hpb2R%8+^^%jT}byp=@dWtEmc(z7{q!ac;MRKSb-LzWD+WI z*w?{sklQ1{kXH$WO2WNE+qW(lD@II3b8;Tt>&xB~UspeR{L;#{tq(W~3uR3%DkvzJ zT(RMVouP0RS7gQYWSX$2gQ1GC6PIn8IH?OV7650D0JC!4_dzg&^8oy?j$m&{XDzX& zy;P2Nq6RA%J)p)~77p}uqJY$CiDYJDOsiL8N1j;2PRstW4h;WdbFlA)TP~nFh)2Fw z4TfVr&pfl~14%pn3%}5%kE(w;Xf+$Z^LrTh9rBZ3^^0!E2>4TMl*?6e<~{q5um%@# zj`9kLlLR%$-l+oD>{}Y*1C&J zG?#Zn$)^6yeO`OCxWx0gx&v`sd}r)&Z*fYt7u{REV5CFd;Bji{lzUxbN&5a4o1(O#QQ!mw$GbpXN6nDNSb zr-prDFK*;$N_LMg>U8)cK4~v6iSn*g67oU7k9?NSwn*M1FNgeGGO$LkwOJ_nPo$c+C;I#Xluw?bqT)JS3&iFOi$cPYQ(ynew^OB_Vgh^uZP8c|%&75G1To z2$l4ZLyapQ$AaJxip>z-k)ue`mX&Dk>;*&HVlXPcs4G0!*f1D&NBlmwGq&yWV37H= z@CANVlN?0YTy)>0mZYQ?yV^raVSy)5URhZl@Dvm(q4qA$@%NxXc>PVQ{m+Rt&Kc*6 zwO(fIe3IQ_S@PbO8JJFQ@Vc~;V)xp_()K-;3PySI=~^~4OFX%L$N029C5g|%vs5=9 zc!g%(g{MU|8%VREz!~jhI31jk19qcUDLEL@PE;5BW2}B*UAWR;RTpOu8kl-;U!uTA!Ks9{=b9DuYtIgVOI{=AKWaczLc%@iX4AEv9{2 zQ_g{#CCYR?CiU^&|4)OaeHFL#$Mql~qY`0Xh65%1vTs;8M|SEc z&Veeb_QB3m)yFX$ri5|EB@>UUJ?K7h9}MQsD3FNGAN2VXm&8lt$dVQze=Ng zRjQ@zI^=Aw;j;Hbk?0lipvWY8r!gw;Jd6w$G$x-Y)3#$>9zDD-k-UW*yx%5LKV81J z^W1$ueQ?>>*fOH6g|4%v5{E3rNzliK3Wm{tHJ!BM{p%X}D3yHlU=p-n_8qlhVZBe2 z-9?^=uA+A(QnUHma^2Qz_e~~TcGQh73*^+b6&ad8+-%Zj^WgHmUFYpP4E*-TrJ@IykBu#VkgP^t%}jlsq0}WRo4JVs9n#l& zwH4@xPgU{#>YXqH%0@mmMz? z;-S%9rS`5pV~vxUP%4bm>$N8s1+GiVQF^8TckW>+WS@eTY|=mwg75j7+K ze^kwq;tlni&KstJrN?fro+!^RVe)h+ICRnWofP40IdMlIO2H17yhHXUz{^rppQq8Q zGUV!8^;0X)zi}&=f1W_PV$+qYT%V@!iWd8)J*%$RxZ|3Y4QtL=wrFe1$$J)VZQ18u z`RN@bV>uDr_KET78&6(y*|B+5;(*BjW_2yN!0RG${fr6Z`_dvDSsoGNiiQbZ8wa~Lx?W-;>JmtL3A+Ew9kR4zfeg> zzp!9||Aeh0$224Cg6G7}JTf#eIK<~-A?n9IY(u66IAkC`ZN9zf=*+9ojNgT>;C$jbnWHkx zYe7b*+=$3w$%Lx-MIw1sRIa&+?t#YRuj1nqKizlUdHW7Nv<&|}1cmS$Ih<+SPG)&~ zV>FWan3)J6E0aPjNp;*iXfH*VRC9v3-(*KliINS-(vta{?P}XwBXVgwdtFN`>0BIN z?@pyPr)QHgu(7Yj{<@#VVp1bs8RCDvptWzqki1ERkL%-$x|YP_(o5N1%%LTQ{C@p# z1uFlBa9JxAf{;}V*MC1+!o$U`777>HwalGP)5_$P2FVkZ;A238phgLHSq|Dt#4KCf z&H)K=4QgvR;hiJXoL{Kq{mQ!%eG^@%px}&z%9=CsgRZ=CCG3uoH4E%4pv5D0dr5C| zc97RCNzP0cxG>tJ;2QTddHpeSsgbJs_oH&dGqFFX#2Q!ni(RXEgpE0!BRrQ=^M&U- zYx-je4W7j}8v~)+M1-Bk;D0wr|9Oo_6iJBArzMKcT{em-Nmnd{+0hYXxF{9`#}ZncybyjGRU7VSQxU-`B9}YEz&2HSg46l3q_B* zdgSqCO3~xXowb9p1SQ-)5%q^|H+V0JI=Qo>OoPAVN4_V0378q#Y6A~FQ<%diKY4@L zrMwK;KqyV!TNtVeN9Dl7hVm$+F?E;my? zPAsORuz{sJ7oC_O- zz@ek)-pf)yFDgkz?B0i(7mTzU7*l>}8)3`-gdLPf)ikA&rmG@2M)C0&>kJpUlvFtT zYNn8Io|R96?R^h#5jjXZExJbI{sT>~1-+v*8S`if?r*-JCGuzXsLVk>dQwoZ+?v`t zI(5Kf1bYyA-H1+zZ@dmV|Deyb=?|L1_6pDB!rjy5O!$G6&*ecwBF+l)4X|D}{j<63 zF8TSlaf_1gdvZq#jG(zPoPxZ<>|nWrbo{S)W+1OR`}?`9H(H7E?1DxQEC}*pd}SR) zBhNjUv>}E>mKrGs&?Zt46h!KDN$r$(fMX$Y5ns;H18r|#iaA}*N74P(7RpiK%n+0j zbUVBT78>HOhjD*R8k~ZfR=Yy}WnR zP-#bVNxjy6(qy7lK`{YeATeCE^xQ4&jT?rBH#Bu@yU;!TUnfp|d`mNvv`8T0!hMN? zku%nAI(Mja=$M}6%gaVuR-V-3b~#;29ECJIjngQtdt7htaiinsOd%kkJ6hBHtuii( zR;+q;E~i}3)?rk+U>B(BF{wxX2@%O4>)bYTpHN`K9}@^+;2l_^r>Xm zsIksO^5+zxNiHrjG?RK%%L6p567|SKwTkxTCbHPP5)}8PU*x$Y<9DO((LY(s%xloq zRltI(jfofqNYT_j(m#7o+Ky=I&xoc(p654$ z_m%trBZXMx&?=GX} zMn+^c1)HNJG>8)h?AR}}L#@->#q#2IJLy8a9|QGrq;;p?v`GqA_phmLODP5QwW+Yw zIDO8Fk&mos)qGkcYxhh2XH2zh#;aFQ86nto`mUwNb^6$!)Rt}TbzeTQY{By8WJ?S? z0IvFqiq)5FX|5RAxVJm1xGUD}?^l!Ff@m`7DNal04V^PxU9;)zL9F`F%mCq!d@Ujj zUBFjAER;2}w((spzSr>qZ})AS(;>-Qja$$}et;+tFd1=pU^-MS5NTR}9gi2cL%C0iP@?{IS@e7LNf+sP^~ zTzKB5PTEJLeSj(4vv`>M8`eFS`St@yP@K}kg=#`^I~I6`=iM6^FOGZk?UM_G*gol* zXP@9ZT8YSo4)OP}RDx3uaT4G|ybU^Q`JnU zPmzU{1Fhz9xja@_-s!;Qz}`x%a2M9PJF&r*dv}9sdAUP@mtO7=AD_jSto1bxN4@Id z>m(Fdf8vdt*e?ODV!vb`Uw;BlyUPOp6ccd%K0v;sgS<1okLVijo}%x=6!HBQ#DRe8 z@wd%kTbldbekrQ`;diuqik-vZVqIe#Cf4;+w65`ezUxcxkYUIR^Y&Qbz@-8?qeWOv zxU`S?v}OX4CBu4$3xG-$`IQiPl>>5W9jtBqf;zv~5lRMYxAkBXxO7?HL?GgD6}_3g zj&9|%TY@rPzxR5idwg#AML3(moa6t-~joELQ{v3z)G>#m~{q zz6Y6Sb_MAQWz_ zOr}0L%OwrPtd(7Ft|}e>ZXu2m#_XQDjND)K@GP%1zryPu+E$m1tOiG`AepExixolDI;cD_p9Q6b z{@aK`?@DQD$^KDoSX76d^CFj=+gPt#`ylfn5|Hq^6lp(g*{n?ZoZ(2hKeuCZ+ zRV0)8+XRrk{|1*)y(%$&{|z`Q!BeqGH$wvcAM&$5P9#r^C^a7!`Pq--<4FhqgZwOM z9eOWAv3}Eud!1scoEa))K(Ew=vE%O)2+_1T=}0qXCByT>aC-Uc8v46frpOU1E^3YW zGZA`qgE#E)S9k&PwnUb$SWa#*e~H7A9a^X_R2(P_O}Om^c%g%|KYNHVe`lyTSQuVj znA${dcVII=n=!o&oO$5q4h@$$x5cG<%9LSn6sW2c3b!_qFDx<|lt0hzugf_b(hU^& z9Q7W!hleJ~N`+wQ-v=-BJr$uF<`WUa8<(CG35PjPU*x48=5em{B!tjh65cEbsSp}* zzPFm`8B=7Y5hZfz;*manM?{O?OYv4kWK>Pd_XNWV6PK^p zH!zVI?^v~=dTHzWjj&_mKuW@C>HqXi!57f&i68f^s2FM+T)buK#xK=3H`jmZvo-bg zHJ`acy~*UKxwvw@cfsr!`y2>hK^N3oInM{UxRT zQpv~kewR})zavqMzByKBaIA2ul=7rJbHNvdiJKvozmnDHKrBvZD){f&5OztFD^%sA za6)6}B#Pw-@VzOlsb?!V#>4j$*%P2wk9E21lSw2Djm}MzC`UgRi}z8B9TZtY8?1F7Vp zL~-K^Rl}=rvX}J#>8|7zG3A^lxngq9qIHSl;zagTF#DW_n6hb8!*gtG?c$4Q^&uB=TcmlTEK$)YPqQ+7b+@@q(b{D>J)qWwm?dDyMlLL zM@#OyyMr#5oC@BSwkvfN`$DNmVNvKjE#>{Ds2@`XPW(4GL7rHi2TlTE_+rLUZ$tO% zHc1@D{738dia?PR&_e0S%%a|=x@i)|6`97ify&Xeob-9ZHCx(`-8R_PzjU8lmR+hA zDsYEGm6c@`QHW=GNm*4e=4>=W#}kIUx{aMX~Tjb8Dx8J0EC#^*z=k+173*}2UVpl^z6Cv-# zT5>_9Y!T4-VqU-K>VGAOTvKCg&gb(v|LD(pPT6xDV z*}+)+u3%XQPX0z5MZrXQ)D3+oC5mZ{u;Y7|owl|oj2Cyt6YfC6@N~~4*AC~2RY0@6 z26Bcmb;^a;9Eot-YTg^ki}TM?R8KS*z=4FXe#POB`e7XKhDP;la~lmGivyfcw4%f9LM2V5+iy;q#jCk))c06W@wyw|Ogo>pilg`qS3 z9a6qjkLw+AzaNR!`1RM*&+I<>!#8P#3tcCLxUTysbQ`y(sPpsMd?!XJ$0ls*Qt%Gl zzs0-1*kBhiGka}}DhI5e}?FNQ&l zq|57KQMSKMl`rR`9A6Vws$mR8|4H1UI}DyU*fYF;{yW#k_?@p&4`jp+ZN6B_!eej5 zyNp%{ImuN?D@sG8&ud1(G)zP*j=!t))v?1QzRMHgJn%L@elNN7GryJs(_b1|Sw0EOe$;2rG zJ#8$=9p1OvdQj|V#i;|`?Qg^rGnth=Q>f70)3Y*zk3AC^e4d^LcUy6P@$a1aU-NlG zjLoQ~n7$H3*2|KNr+7f*iNRWS4|GU>+1}lRZ1+gIMQw$+;I$r~39~T6 z;OE#0{NG(Lzvsb;NB{5~Ul#r6okTR$q&WuS2zZMcl4AGM+eP^g1P`-&HbyuDV$QL* zH=%dvF47ZuXG6`N%?~;m_QI%&<3qE?nwJ)1L<>#enFFJWb11p{I^?;SgHfO@M_F}L zJzsF_ExvNK3+7}P$k_~jNyIPIw))#qXF_$)&yhP53U1LHT(xC;!}`9f&Ts4KX*=Iw z*LkZ};QeTe>l+(t$$N*E`I_D*IAxiKqkDy&GE4y6;W>t2577k%QZ-`EEkk8z0K7)% zBHYuPLAFd<22H?EgXYdW2_Nt1p`VN;_EE0kMlF8j)~yZe`ma2%y}P^pJaoEKj)s+l zw%C5R*GK}31xFE0=wnXuj$8N)TKhU@> zVdsi*nbQ$X#NE+_dndTr*vCgFY+zQ_b+>WuV#bi|A}f;jW{pOnSef{MlTY77!5Y zxMpc;Fj=Go3tj%?w0rQ>l|3l4kCU`9H6S@7;p)-4Al>|yS5Ac^u7Ut+cJR0cw}cO| z$AA+eW7q?0w%1bPcFuF3KdYTC1}*`$C^497UR*83UZIfVm6%j9+Li<|YOv7lE1s%d zadKbT@FrZNb|D>9;EIGZ%iCl^Ce}Pw9g4W@K}B^(RSR4migjO{3honD(GRrAU%rh4^ zJ*u4d6ejz}!rlE&+$-aYN!#QZ*fBhILT~BNbSE#AgdgMX_8p60%}EMKb6-DvO!FM| z;($l@%s<$3SO?{nyoni@z!J8~5Ijl=^4JO|yNn~4?AvN=u)HPMjuS=B&*7*3f34CA86U%pO0jou`eA9DuPhOw9p< zA&y`|HJwiJ&FOS+;bI(bj$C{dR~b3IdqN}>irz|xm~M@RLXoW&RVCRHINRdE40Y-6 zT_l@fjUB-lEDWxY>2co5j7@4%RI@XYw4;ji@xBM zqEU7|%uUht{*k)whWLP|kTzKReU^Oyk;y9Ex{DKiIzxmq&Ew*~jKFmVz+sQ5QFpv3<{B08G{oyah{ac? zOJXVxsVUptKK~gasQ%9a@+)fMvHc3ld`Pyknf*KxciD!7znp*eQXFFbcZ2fxqS4j( z#(xl*U!_K`zB;O2^dAK1nAHgDbCT4|` zE6d9Sk9QUM{U6nmMM4qI5mSB~wTL(`39t90HsIB(M;?X}7XC4>!;3qzvy(B#3wf+j zR$E0dT1fd9#MYOn$=XbW;@cD!R3KTd6-A@LP^8Tg<*vfH#Z|eSORTQUl5;8AMYSU~ zP+-RRIZG$BkbxtK@mzW_^da&0$Kc#Y^y{L1R9yv16m=u$bcpej1z5xDwO z<=QC6a+YGu9?9XV*5>j6nrZ>dqB<;Ng8*d@~x~9E}Ifo8%6Gw zVq_jH$Kw1$s8NM66A9R_SPkp-WL~o$(^IiD>S5_M?$I3`l^Zr7gs(yipM9TYsnQxF0BC(W!f z-$}R``Wid-;2>tv@Cc!$VRH0~k+5^z(D9u?g<{GMew?1dWt{!ew-YG|!+O`8IaEHh zu|1p#h69p49*8DAr5#n#jZ;073o@nYOwGz(r9lqHkQ@xen-iVemo3OFZfF>-TCi-p zd*HN{J)1_#3k&Qhwi7_^CssdLvtdiwa3k#uG!2(JT@Gh7hKfe+;-0nbO-pKOmNd1m z>xQ|ZU6K8aG7bO6c}jj(l+&SKZAy}AEYjZ~Rfal%`WvKT<@5%r(O5H%1;BMIN{<^V z@n644igF!i@LY!xJ#+cQO&}6TBjaHRCF>Cz0=eWzs0p{mjU9mtg<{&@#JFl+$tIJJhgI1h4{P45s4P)bzQFCM&?KkBziN351)F^SZ-3h_>NowVTo?~NlhSR#2}hnduX!FsC}+O; zg54oAwAV3_E<`%yOa!e_t)7i4O5G`k$vtcsSL1q&kFd2m8m-BG-%N{WD$*i!qUj7+ zs=pcM*LA(d>7P5dV-PR7xe#ebWqGW-OP9})?YYR*Zj@0vg0aQvoBVU0q+NBZiCUW0 zbDoBZjj|_j1)?XWl!wFdQ%)k|-QTBG1S3kK6MxW<(9^E*1zC796OBxUckkv|r{#Qx6+%NA#l3-; z{_YN|qI}1Q74qIB@^?dDN;tuMNID)mrqebeG^TzQv#m|%0CpMaD8>J+?mgh-xT?I- zuBuMeRn--{I_ES!Irk*ZjAoSMC`*>CEDKwbgAC3A+XS2EOa>bZFR*|C6THCM*qCU) zB`oZk;9b~V*za4j7#@6Udu@z0^Zw^ncTbOk<$e3T_xnBntm&%i>U+;U_uiB5IdE68 z^d->uvI{k7vsSjySI%-DUwhf45QudT$8iKPl+24>-^2xr`({v-(4766$FpfwY6{f| zS&gclD^MP&b-dvAaV_HTg)7|op^V4p*1;$RpF5lKO&zQ{^{YH6h;z76wi)D53VrG9NrETN8pjd;r6|&Kn00l)sfHxu=(`guKY!Ili8*7C#x)%x{l?}B* z7w)e9e>z1B_zbeo+i9o{U1HYe+PNAU zQ$5nMi7*gfb!2Lif>@3PZf^)i+b;byx)h5nvPkH&+?}@NDlaY2W(_7vG=O%J$BFRQMkwce8`3P zB)oU(zI}-HEuJ%fF8^KZ`>WZ0e#rHz%Z>T0)eK|y(FL}*nxD53W=RnSOs2;kV#dmU z8$D1`0|E}lq}Oz=S-WP%8a?Qdem47?kln^@&~1cUem^m`WN+W-vBzQRi$o2i=e3qB z9+@x_@^i@A*P=y}@;}5oHjPg2?&?E_CLioWwdc(8z;HkwEZ~4} z#r!^qjRCHx{sLkpnxt9)erjZ8nPM+9wekxqkFsJ6PH8I?P`#AfefFI4cPv>#P}iUU z)MWptL4oJYs>z`Kfb-n-OEwdZx9YQq9dNu=U+3Vh!uzLsu^wuEE+7K2_WqY@&zawc zwSRl9UzN4PZ29atQEY;TC^*S4E14uZ1dXi`TgiAA*_s^fjkfdYktB5n>h)b_L^#f{ zw3eLExjPaWC5zYSDd8pArO{|^y63NW%`j|^6UN68^zmL;ysQ&WNXxA#YiZqAj@r@c zhE5OG-~eJP-PqS;ldeaR^~11rE_u%ht%SVkSIuHBncG*mGY~Z?SLY`mQ28*7yVBe9 z?~st8Gt8?Wxw^fy?#sNMjClO{RJgD0>Mug#%NjrGna|j1WsPrjyk~HE%g8F4k=~l8 zt;CV1jbbYGxlaIIssT%}SimGng8hR{5tZS2OZu4uDrpme0eFsFx?)lPMEA~#?j4n& zEND?JGhG}T?rd30NZsDvJ+ibslL;lIfLYiluAdrMEGXWIuC_&4izOa+`!ysU&*TW1 z`COr_8wiz12P_2C5?@@*yyK-BMj1z95i;n!5!vI+H7K-~TUO^sUr3JkJD&0aau+g3 zc{5Yu9vnUJuI&-WUv4eEXszQeUq_6I?S-X$kWr}*Gb&Yv=c5lZ>*an-_srh|3oC4e zC$J{TKJIvm+bEA&vwyoxXIa?%I;EPh8k`)z^>PADA&xb@l$^3Rz*r7Hy5!Gv4rP&A zzrC21+@YA14_F;B6iY^x0X-`VEdiYHkSeHngbH@p=1m0$;_Yc2B?p!sNlUG2RCOyI zf*7b(AAsW7r$kjw9q@)GTkG|cxw)t3`9F(|V*Qu1P&4kJog{v&%dbzQw0W`aO%p)2thCXOniC zYzCk)EXFN1KWnFxjRhfi_fqKDy-k65_af}fx?_M=&04C^hPrZI4!#xK*DRh`B_C%L zt)|uE{&Va;hb-ubO=|G6@44VdWUFtSY5%C1%=YW|I2Z-6%Am3Fk0EAMfzOnDn^mO3 z>+21hwl~iSApgR?f^EDirtLQU&t@z)`^V?FA3fLG%8!Mt%E@oH_3&#?{%fJYCkglE zxix$hZ8)3FeCEjhJ65-Cc=M_i*Iigipqa2@2PIwex1M)saLt9)>V<0t z4xV3;Bf3S=0!wQdj^`t0u#XWZLnfoTfLKd$?i8#n_aR{QO2=c|-9NN~^u?Y?)Eive zT^WTZf{(G?Nj~E_*XZ!t$>(bB-WS0mB_oTJqy-3-|D`s5j|LCuY8MAcQ== zl*IoRLqkH)$V9rfj{fTSqibxFS)N|zTQEng+M>f1NCc(HK^#d-tyq)t?#_^u3K)o>&|!5tY3^-k3l0t3<3F~Xx4GC zjZtq5k72bonh%E}k*h65_UiV%jIaN=po_ zlf$bFcri~->s3-(oU@Ir57q^PAK9>LNW3MEQ_w%@XZP?LnB`3!om2T$Q>CetFC4`AF`HXYvo!mgM105K^!~vy;3F%t!%xvzZc$}o zCZ6^yDCD{c?_#CQmRg4nO)oh-LRg?vOBKvosg}nTi?VJjhEz?aVnjxrA?)1w0%V9= zy5-7=mNmzgcI+-IJ}*LsbJN-Jt?iO;U~?a$&qofgtc>S!d1NyZEX-=M-i$gA8p>}W@GJAs2B>6FKSuQwRr95(G~FF=O^2iZ(DSH!}^(-_3Jil zSl2O;TRlAuO3=AuymxK4*9h*J8r{;~zGbw1WshGE>{~UyqjTe;MT^G9@P}KP94HKq z;}p#V=7e@qsy-(e_kz_j&=*9utohSEBq7x5DL3ZimG(WS&&8s>n2YJHSI}hTTgLPG z@fK%F*j$*H3!CO*V5W8Cg2^Qpj&8V*%@nt{Fx86Lz|6Ex6#!jo?$6wA4mC}PijjQh zEEq=%t7U0mhWIB|ZVOjNi>tQUt)oRNWd_PE^tZ)x?AYLjf%uW5gBz>yNwYNEvTmhO z94@b4jTwi`$Zh66?E+)&V+M6bAn6#2Lm(;GiUk!OLs^W)g-v==@#!07Tb4C{SJ5=` zy1_+y17ai@=<)eQU7r1BUW16h(JtrC;~pZ(;dJUq9W-bWh@;cTcHNc&m&U`{jM-{v zQQ51;mgHI&GNs6(FB}gTW^%BQoh)7^Qm33NJ@*kfCwXbWkwt^6KEAwthXFVxzM z&<7s-fkBvWML)Z^za(AIS3q5!E9{f>w9D*xpjG%BoNI zD@yKZ;Z5zpd20dC8K^UFEic?W{R1J6j8_m142d>Yf~`jQB*c zS{xV~8z@#q(Kk{VnjFog(%Eb}mHS#QnaJf5$sFM}1!#6O;TCkOG1E1&1@O)!LY;8X zYRhH`X!Uz@U5Swu#p(8$Z5^9Sz-vC++6vtYBuey4GDEAmd<16mBZrnA8u-VSB5;d; z&=*pW-FL$#=HvepOD~2eOSl-f88xna{L8f`c|19f{|=tKQhSo-Y;J}r~S4F&Y9((6x?MV_(_Of@N&#-q=c(Q{%nWHC= zbC1vco?Fa6Lp8X|>|6;X5QHpDvkGM*cbz{H)&##hD0&gL!{3WfJsPBu&HY}u2fr&j zBSS680Cx+tBaDj&Z>zm=#Tnmtm3J&uX|&}7 zb=>NJ>4q9K`}xT1=Oq!es5npBnT~eJII%MOjWTB8u=5p>rB9YH05(P{;-o1^z z8^XI@81=w02OJXW{K$I?YUuE?p}12p-KFxPeJJP-Mpe7Zh~<-se5GV}_kvEaKDpna zPe-mJF32dq;0Sl@x(-3|R7kkX3h`t^sS)gie^)v`GwdT(NDfid) zj$upxUu=V2W*T^Qf*$r_idd1r09DC+iq{tdQbv zAZcOt=2U@isSk#r6|m<%2MM!<^btTyWSJE#BN6g6&5Yb`3r6uX)zAv5NL(K5XE* zT`@)bR?~<>bJN^6F`uPcB-$QUFmk=pL+~RiBT6_%hv8%X1PKOrf?b+DdwXwf@Q*Y14 z?Oijyog4b_f6q)eewXGBa34noD#X{B*jrW99HPPFXi@D58HruF}C^ zIx0?P^&3@>G_785MIzSq!D-2(29sjH+%G0oRgk8Y>&T*ToiZ&6s+RQk%L8J9J0hm^ zH(4Ie&>=Q^tqYB$KcE_i51YY&m^9vmvn7UZ=$yxTlb#es#dy=33@|Akxjg>F_hYrM zcWq*H1JLH3vBGn>;F_^}pO50&XgfGju1u(ewHHsA=_X$V_-LS7&EullSIu8*s_sO7 zy<&?+x9gr|{rGI@M z<#%GX=XtjniiI-#vwB$JRPAI7&W;7q-@kEL*CFl%tu(WVZj@-9np<&7$w?>Q`X++y}}~UEaZc6Jv+o1yoGKb-Y_;Y+8ykN%!VHNP{ozmF6L?)|5{|6gnW^zk55 z>^HdQFfy7e2Y0pcbg}WU{w54XF-=7=u;UJa))e{gQRO}GPeua=2eH4Ll!Gh@!%s#A zBRyp?jnfgatgh!HekGp%#Gz*|S#|H7)0$6Cg%`i|#_6fI-Z&Lb$$qriHaCq?J8d!^ z$u!~;#Wva4yF|gR71IgXAJ6DUHtJUrDRE>l+WiUDkH)5MeCyQojjS=BcJSFtSH1gA zvdJ_x0$4(lceMMBVwjO44z0q`qkWfhv_b)di zA+6mCMO$OBR`_h&wNS)-B-&eerc&Y}nhpnFspXl$VO!S#@iVEulsnm*eg+n2d`A?H z2Ry4A@HhZ4KnCf|0fTS;II#nXGxR*!lAus_7*RkFvZ8HK{BMN-LDX1Q=!s_F@-p;t zW@zw{TJPyz^xl`M^-e&xlsGP|s}ZhL0gwdG(HcB8X@Y`--t$y+YQ?` zHg2T9kHGkpHY3!@$#T=UT)%2qo7lMO`Ms$b^7gdOH*q^P$-v~Fr4}OQYrit01ke8# zIf=;}w{aIi<|qlPv8sLe`z0pvR=L09F5+Yv~!Z z^c~g`TixfB0J(odd3I{a5dmA62#B!_xi)Yg;l2e}!9g&FO|EaSvGwEmhuHJY?D=MT zj-K(p>z8;AgF=-UUa7x|)sJQ6^I@ZYI~ezG2n5!P@t_tEM@L0*tH-C@f_x=62jSt% z7}s2DWY7v)cRgEcg?Mb8v(jmOuXl|q`p3rnk{V3-*GbZhKd}`*z4jU-0&% z6jbtgy%s~nvjWVF@@Klp*|2Y zo{Hwwxc(Gs2Li4na^Vd_9)mPiqg*85n9K=CtJ#OKHr6!dpJwiRGR;(x+AqJ-L{cN8 zwNKh*9->uwnH4k+Yz!NgsT!SqvZ7vKMk3~0tld)o`oYJ;Ug6qKPa1V*gt{zps1i$k-$ojqd7Szintg%7CN% zvlsFH?xG+h6NSOl%smG#{`jtK4;)!`yiJX(Zng8B*H;!7ySgh=#omuCy8MBI2ma!U ziSs{k>9!k}Ar5k4YX8=imro7$E-KogzgMGDCR05)wfnYJ=YRV6<)7GI$jKf%E|=Q5 zT;Go2fr0K#gTr9bf@=)6;16NsRQm=y0-hBRe#paBpTn^*PU0yfbk(I~@GoDPxEBxu z$&fj#AZ&#lzGgbn-t;=K!WCCsIAj#7`E0PW+_9;;dRKC4n0v%lLaHFxR>&6Ib|%E_ zSGlldgq3BH)+;9lH)e$&S5l>}(DI!vJ#sb??d*?qh@Bg&xskpuI6ivTRk~NUtY2D~ z8qY?*6CPPoL}iF@&$iW>_+d3|DZw>8DD~Z%_-Db4w)dp^M){%6vXyU3Ct6!0#R~RA z1u+B?C~=I9ctB^Pu5AIw(?~({T*9!%clGt1Kk+Y-c-b_seGJ7qA0ODcXl!SHEb`hf z5qT;|uf0UBV(`#uSPcFKSlr3>Sb3p;icVaC;5T6vq-EX#LM7&zSJqc`c(~vcoexu0 zISOkjmC9AGcW&_foGf}&l$u{(C{@#it}FV+W9=m|XbvBk=sz!yFHwg+xw@DiOc%P} zGB6fzFZoqt_}~+5)kB$fC1DzELx<8GO2RN6w{3seMu3%Wg*&p4WCkPsW5xD$v4q;0 z9p2C$#@As3*)^<4M<&7 z0Qv!PCfByTZp{a?&j)}(hT(o<+L8%xfOv;>hX`>iT(&~S^#~RagWemA*(dGD0ao@l zM<*T+ggJyRal3=2C~JTElO~5kvkzAAM0i&8`^9HxPgWvfuG@k!9_Dy!_DA6e;LUUQ zbDOZj&RI?LYwb&0T9&rAQBBddVpmtO)ZNY9kL$MSmNNb>Esyt>OTE3NGLt`u!&1dq zNchl>Y8=*KupwRtgNf66jnhhJ}-TjKf}T9X~FTMsg~-bE*aBC)T< zqC$!S6JGl@&b7O?!4~7Z4Iz(oGZGL%eM6Sed{g^zuhRy1EEau<+IXq&k)l;MWRXaU`z#`V9<9-5(`%A-GSF((CNT|#{-v>+4Iw&Gr@Ni2Gc>}+Q za4_C=7}0A_1K;IFd_SxQREh;-Vn7m$@Gac5f)mpWrtJ-JOU9Qe15yZeP}XI~T6#yr z106`U6^!X>OcjE$v1yM_$Q6p=sJLSBTlLWZ7Ys{VId|KV((qV$&DP=E1I04-r8k_E z;1a&u6Y%tHt_+n@O(Sh{v{b|;GReB}xnRUNgv*Qrp~lFa;hQG=tm9xyPSd~KgoZC? zHDe3IMDN_6g*C9El&S8*yre)>AfJVJ0=mtD5+ES}(_o2QmLwNCa7&giyudxV=RFq; z3?9Ds{L4PLvl2M@PNb@7xo+KttGh*|71{18+b*4bl>410klVgJKXT)M=o#97!?IvW-Q^wlUX78y4Tst2EI3n5qis8RuV^(d3qJj7n20+rCmxNm9Xi5@(-KI;@3E)oz&aAhOVj zOzwOm65f36bZNz*i7>@@8i~adQ3*EiXfUy)b#g~X$BxO?sjLz){T?|yu{dFbuZbA@ zEbor{E?j@(u6~cXmuw$Nwz+q3 zuknZLHF8+W^Mx=sbG=iKmpLF$8WDYBcVDu)wo@aJqb(a!GF&wg)gDUZt1Y3KW$|iM zkEpP_CpWaj3E+cK|J2dxp3PIO-sB|&@I~=3aw)t_cxqbZ7HCC24Bs1iR zdgEi3mJ{g4o0p|nQ1bYVNH8^Bab}}^G7*d#qDM|09#!l>BqiuFewXk^hYYn{2wf8i zJ)P}%`<4sdw>@B9DJc=_JdF#8f$QkK3k6grnP4YDL~ZOR%=Y{Q6J^7#qYThi z8Z^SzvP(-r86`RO^!d|+OS9qrw!=Gj3?E*ql1WhSd{a^O`H*}evvy$F#@^8j_wE`w zG^JyK>U&Hs9~Bb`%N?*XiSA6S6i3FkU8_fT4Z4Fn$+EaJ$mKK0kEHRE9h(|x=~3*^ z_Lali`n={|m>UJ;r?{A>%nQA)N!L0yt~%Wc=Ry1(nl?We;(Mgf)?;d6ZLK*@YDT<; zgpT78#?^h^#ONkNzFfC${axXtZ70kzx~?RU%26^l{k?|FAlzm2=o{ss^pauxnHDE5 zU%TeA@zIe_g*447&4l9d&`b%LogJ zH5`=+S&TLYD}NWXFzElpqnwyzHd^9RwQ0$Q8W)XGVIoi1$4tM6vgUxO6EH2Zupa0!Y{9D<^__&#%L}9*P*V zbF$?|3`hL1!)I`Uh|gf%>}u?Trq$B8%u+VYQ^PfZ%cjNCxXeP`tjlxc2ZJD!n>@ce zl5}?)I4pWB|5o2mj8Mpc@DNYS)q$3!1vRE4D5mq$vF&@41rJIx+xk_B>)pSki`i!j z$;9NM*7XPh)xAC`x_q;kpYBYJqzqa?!J{%wlM%^h&0HR7bp-G^e>%N%vM`=EX}YAN zDs3Sn(woe;B8wWYN1~#YAzz3qz}C^zpxKVBGrv*l(oP@jk|fX|$hy9W5OnR(Yk^3@ z{dUq^t${(VHW6@|U__vw!4T z70FELvoEm6^6X1i+RzlPJo`s1F~CO#f88ltNKi}O0?FsoVmWkJGya%%DJzIa?rL`^SA(9KLntehSvVf6t9eecsup={Yy# z^)0<|51!Z8?RvmelhK>kLDTAB=nR{6$$fr3;6!_UMUZSu)&+s&`48FA@C@nJGvWGY zENmskpC+TI-GfIUJ?ty?8I739z)zDA2yZ5fJ36$`%eZAj_CAdvm zez4R#k*qbiwjlZzVu)jYqFuzIxTltANpc1lk6IibGz+zFm`x zcJ#8_eZcB9qvI1~5Q1FtC&*ggpp{A%Vkdu8O~isDVZPEXS+|_$HLjhnE|`6yxkQ4= z9OmVI3MIySlbUTw9HHIuv6&e`o&8G!k=vpB*Dm`kyZjv;1NvyN*y^!1(dCTE4kUxdzQehoU$o-E%+eyL?gxo=+}T8Lxgf+S z&9dGBN}GvG@hHq85p#U>R-BI1LeDWG%e(x*^$c)rBljO+yRm-j7{wN^sqD2UTzfSB zG(*)Z^!Mr5>~H7qXxBVUi;x!F?1or^zp;)Snprn`+k)P}JE^Y*86T*T^ZK5tciy1p zb?Oda(0$vxjA%gAui2wV1AhIQr)Yw?|D=%lry$J)MD5zz2i0bL$XX#vvABP7N1s0s zOZX?Brm`t?bl`oxq7=o0?Y;DcZ}FE{B`u;aD?leX9SoTZGLfPH*X*+f#02;?qPqgd^{jv8HJ@MBTq^=T@~Tw$F7+F z`W`HXs~Z3roTO&J`e&c#EF&1o=(8^yjp1B9NO1n%^?mNB6PHwUHU|?yxQ<%5$b4Xu z;Mz0yB=-vczhHT!Xkm(Gfq~G8G!>eE+jRXkZ#ZK5keLFXAZG2gR%f54z^GI%S5#%h zC2Rc8=`*UGJW`C)H}N8mxWRa#fIEWn|3D1Q56%!n^G^t#L4~B>Iv=OS&>*5Yi}Orj zI%hzmYaqWaq|}!TC@a@)nhzV6Wjt(RjXX@o=!ea0$oL}VgZwOg@mae1BHXPU^qE)r z7H$byk*m&VYrAJFy~urre*G%FPXF-!3fCB~@$k};AC#=BQ0fNB*gzp@XphHPZ*&Rk4mx}*Oa^zqLf9>~sKPxe|G;)nWwNFTpzS7ae*2iH@-HxLvTIS>2u44}0xr!|zT0CuWH7Juj&0Dw@wUY*^KpK~9fRwZ?I8TY6vq1C=G zYh^R$|BBaVL~6~3O)tYfoz*iOE>eY%PAw9E&@jSil0aO@MGmO85>&Y_&8}60imh@V zrJ%jpP4f%jR*z^#EYEv---F5!rs%oX3uQX$o&7h;yib^+S%)q^-!DdT`cM{5KKIh> z9-3V4qpB@)L3MU5_a!yR$@c6<*7HO3_tty8QRvd%djUZ>XZK{=5#aPHMhQn=cx1iR=Y#$yJ&-n&7 zY}%miunWV-QY`AI3<4uDoGogx^z_-k!de`2{RHE=kRqNz7tb-8HvlUff@cGS?daS1 z4EHIj$ik>_cAhxES@No7&c2P!#e=4GTL@GaGhvxOrgJXCuDA1P{#v#J>#@u1gf`QJ z5E;Ecs)=9qM~uAsRZ)!v4gX`kkNNetr)(^4H9NbJ{c4(u*LD!ur zUNk;(!Mb)!-^lF?>LUjyy0>f}IyBj_J~jKG4CW#rzWSYfH}?f_+%jpl4ozpGYr`!^ zkg^bs!7B4X!Kj0*^o|+G>r3&UpIDh))S6imiA-lEaoLucjzpFuv|L(I(m5?#C~$hF zSj^NOymQl3`*bPa+M3_Im@aU3W=Sxg&E}M@T&}A-o9j|?*=!ymN*ug~$mt3x+d0Pv zmWDuUc%q!h8tenGC>`+t9}b26UQ_MtRKs1X`a>`z&`l6GkE^g-a9p@8#pgSw+wIzI zYx?9b>6ZE+_ccW}P0UO3?5O2@_)haVchFS^1Pj+;?iMgtkV0>M9m%SfuUws3ymx%_ z@!{-L&ztGVJ7Nj1?9Y}A)MYLVm&{7Hf=FCgzC(6&=+hUkc*~}ab$9Jub={$26zxEY zQ7lt#Iq$&0`illqqvwsz+%;;%@K6Gnlu3YJ2B|MdRSckaG)3Fzi zxEo?|aOLFeuj7x!!h%g%(q8*5l`r96J}HJ{6t`Q;Pk{K9-?*r4@7yxT+b5mqn6l8& zi=Yvahz)*9#}2r7yLMaO4%kDj6;|APP2?9+DID@W?@c}0&3z<``aG6p&E6=CwIEjJ z(~&T>ycJMW))t%~C*8Jgo;$}$|684t{#^t0lm0PZaBWY^s3!WjX!A+`>grQZ`hP15 z;e$t3mY4Y=R!OO*=YT6Yn>ydgmvdx9Cyq;dra7FWvAu1su>9C~e$nQ>sohwxqeoWx z3_qBGs@T%T8@6`jr}k8{1I18jF;21~ZJM~~=(2%ri}S<7=U+MILG~L%524c4zymsH>I!@aBeHm34y~@yB@>uY|e<6~+2u zDt`|Z?Te|i@3{OBvN9b%G`+8Xd1kU_thCsZH$;lI& zWI>V@WGNkNYa8@?G+CD2zP2e%gk0ITOGRGQcfWkr_tQ+i*#bgjf|(Wrk6C;n~fT?;k$Zt2CL)RntX zdfMk#FF<~8cx>yv_hRf&Ue7*u$iNtS-4WtosJ?&B22WVTkrm6ZLVhEl=z?2R^t{>D zW9QET+vQ$9pn8L0RZ#?9@CE{^$Lmfetpd(P&+Wz>eN5=X?8EAEW;n6S>%ybWf*1yA z=D??5ePe+n-s%R;Ct}|GN?)Hy>|<$1&n@tFo-P>At;W;$Bes zQdd#61h-EP#bP1Z=N2rr*!2Ks1dF9;MA0M(l~5rrMsOWK;1WM~IiGU9D)fVAK^_F% zcUBH)QEdv#lx3;0oVG}|;SpCTNkg_x-wM&KM*>^#_gT8fH-&IRl@s}POt~!|F@xTq zazwUqchDW+1D0|*VyX48Tb`j+V{@kbqKmB91c#WuABY z0s+nKaVOGN8D;@mGYPh4Op3q~PMd@-{v69?S1=|9rq#5e*p@i$=Yy8C>s{wsD=o4w ztgP1+uOh7%5%eRjUw5{J!p&{Os{R}@>w%`7ZA}mFw7epUjrsK|&X%Frn4*Zj$W|U| z(iw_~;Y83R&isB|crUdnoROA644K>;K|+F{={F}jyT(n{s6V!ce`N?RNK__M^Ti@a z7OPA)1WaDaZ+G1dIN9k?inD_y!f~uMUGF5%O*9ez+h{Tw#WlYjw}`D{kJ%^AI9|#Z zU3UvZi0_M#t=4SX2F>=AnuVG5kr8d(=Nt9MKvm`M)*AnCtuy{U&{In}sEXjZ!N0E? zMeCpF@c=*Sx?d3LHXPuDps#^{wmz>S+wUJqQ~tYCJN%Xr)bL->xE1*p$O4Nae9kAN zwYjH(sbQS!fbfbLZQv=MOvV$*B)>fwBWW@YUlzXrGMcNiz%>sGN*p+tJsk$>`xvNB z7*!NOf`q26UOFfu4O`4VAKxSSJ-YG$(lJD|`|8(9SR*+a)tp>p5y#BX3T9EAXgDebi-yjdZtk_9d`A&h zE_BXeou;r(InqDK=?%@b0Rxs!_%_g;uz$-If7tT-)(4WNnF{c0gEkC{?|i3>8uG!r z9P19X6vf^l$%XB42p5i-l4-^;Z6yQin;TmfNbv|4ybEHoTe|gD{IQk0=tK%Xca+b# z-UFPQq#Y8yIJK>2+?y$7z0aeC#C`ikOY`{F%W0#&d@_o0JV3PR=uweV%)oI&`C(%o zxVgQcN+-@)fx*9e*ue@yLSxf2zs%jXV@fU2Dv%4CLe`6^Ch;NWs8ohq04 zT&9}M4yMzC*=#k_+mcF_%gIy=T74Ivc0D0Xg4*GrZ&Ryb#}0g2<4z-HCb&XEn3TL) zMU(YT(y4(=W*`+1aUNH<1C#MI?z&2;REhr4s#wM^cRkAgnkbh5=0V95Q^-C&h?N;; zXrUzij;wJ)4hn8P1_7az$(VN9!v}+SeIp5S>&6hz1K`Z->6NWckuPZx0V%`^_g2|3{# zvW*PcMq{##D#>@(>1zEGGvL5D1Q^@j0EA8S{#;Pq@6de_0UMljR*@7;~ONnNe*0qD_SoH`D3p8_|Fo}5u6Zf7D{~$a^Sp_ z)iwTDM%D2@<5H90Qob{Dspfs%Jg@WcF5pr$gOlJ;I3uiSm((|Q{nOz*!{}`oy&)K3 zA&x*)8k)v@`yW^*@yMa(M|vIT7Q?!#>h$lx9vF6z`R$M#k_4=U1N_QH6}!$}mU&nj{_ZCgPF8`I;F6a&=1kc+16m?bf(eG6#*o(BIR;#zaMJCwF@GvyQQ zxb3dEWy`#iZg6(IRlp73b04a0X!-++?(s`a*R&S)VlSWIpC_uyXb0#9)Bz_~KwUw( z8G4epWvZiNs^Sr}AgHUazgX<^^9I&`Nb6bA0{aK4b(%VWJou`HcE8K_xJHo&1}uX3 z8d>YWVMy_*9Vq1zX}_U}HUgQYfRN0j`JQyfY^!KcVnd3WSB(U!#4`^0uoLz$vaO$+<{OS zLyXDXrE^oRyXR;xKr$su)*F!_pCE}64aDo8)aQ3*U%=_ZuhNJeH42((bAw*9X|QpO zUY~=)grOFKMluowS;y$n&PV6QU2lQz0@9A3m&t+XaJl;8A|qF|ii%)$%P?H%EBQ^%TEjp2|4|R~{he+c?T@ z@LEwf3vfA6tH_`J5uAM{Q#|tk5})?^cB*m16HxzIiy7X4#_bC2eU1MYY~C_359XgIVyb;KM+Eo z(7!k{KXBHKGnO~N52(ZYL9R6oB>UDnAE`gCdf|V9nEmf)A+j9at0Wv@57*7hVbQ6wU*X{1#S7-pbuwN7=`~J|B~y&!byx1d!@$r zn2y;n$54^@?wY1DDMn4NxJ8P^G)ee)9bbO|AF{1nrz2`9reKwQl7G?lT?PqJ_9iaZ zXcKVdL<^qlNk4ouLZIId(~ftk523SsKoICCjdU~Yqe$_8RX_-s@=efYaf5N<|DU)v zKx+Z`w1OR?3C4xovo@0Ca)^QWiLQPMng8rhrGb|V0nG=5M@ZXfz=DOsV#mP7b|P`R zSKI(y0Q$j90H~FA3cSoKA^9>xRgKG#Mvs?|kc@CS{~xYzx&5za!)g*0&S=V6U{4Y_ zSwQO-F&oxJ=unUkxGzR(7L+law($rqvkAdx>~n$L?2LWZ?PeaNdBHe$BmU-N z==020qTK&GR-)G=Us#^eWsf4wh^8#gtUZgwU7v~PKlJ5JL{jkLT$^jeX?&YcL{^NKTantwslFM~NjkdGdf1sp!U zOoxWf!+!Ci>)r0yLj4w*4Upadq#k6ySq?Un&~M$ECGOuJFg3StU6X$MV$e7)T5{m% z(SU4;$Bp0*8zvfVZUgLN52KEUqunm#mMlFoIvPJ;bL+CC`uV_Qp*RuX{V?q6Zgn^R z%CeP1*+$CnYRM7Vony*F(hr+4@;7WRIM5918I*L)b3+~mUmUa*I}knEtP(kfsw$SER7 z`5f9;bw+or^KyT&)F(UEd42skt@E#($vS_cVUe#{=WCYwd0e?MQ~!OR$3pBaKD*E3 z448yJ20jl7HvKRR*r)qE-husajqvOLXBLwevxC{(Ksr5;!^J|2$&0SX-J$=w#bh~* zYMivdV)9~oAd~G&i{ey(*TR9x80;tMu9hTBHmM}lP?)O-Uf0irk1{_9y!Vc~c%k*8 zZZUZ@h@5Csor&)DiWSSk4$pl6fQ|ldi1}j=^w3H;|Zi!^ZY8 z;eY)1Z6&Yo3_8tLGJBSlc2`|ssDCnE0~nbc30q0C+~PaO-pKgFWCMpZ3AyIwbNC>H zD@0PCNxC#4U7iI#+6fNMNXq>0*-Bm?W%GjH180SWo7)0E(nG>UB!fD(5=V=;4Ym@_ zgt0_uOKVyth>$0ej-!x{e|2W*xS3150qGdJr4X$fG%iJ&dgSUOaoqVNEFBO`*ZpkP z$Xe1w!E5qvO{vH*8%b0g_4#yrU&DbQTtP+>uc#iBA@l60+hv;CJ(CrtiBiu?(y*4) zRspOff?03sKmdXjO}E#vmYjfWd_?&C8?u%l+#l8we@N6!x8PT`)=<}=ow9ZQKV=zT zX;MJqVJ?v+)9v+UlXlb$nSP|H<)QVx=-OVFd(9@SG+U5z8>w?cE1ZwH6}cME^A4k$y)MC zCT+o5;+PO%Es4h>R6W4O-{$&;u#$30ny{9@22nR?c&pWNhZVL%;p{knTRs$OXVrr` zr*Wu_n;YULTu*VgGY#0$Lu(PfClLE%aT63V$xWdCg@v$MrZe}rUUvNmxq%xxvlHfe zC)1fVZV7T$k<#p9N^{^*_>ZGe zZ%c8JaGO6IMW{<#a`rhp9)wOB3$E5=oDZ@&75$U}Gl%qNdYM+dO^c!k*1Z*G~MMxjbgv>lU4IK#^lEg)F?dXPJ za8PN2%60JVTu<-My~*{g>jPYr#!;gcPKJvsC?CV`a=QL3kpr^Ja&!IQUtfP?{OgNy zkQXQ0W0ict3}?OWpp0SpCp)5*V%`jAyn-emXT!i4@vpD~{-tRHix~fs2&xzO+c+NU z3E9-lGIO>xt*CsiD^fJd?Kn^#3Ku5Z`P;@g!ER5PI9J-bq)ZWM2l?&X+u)-`B{J8G zEN>g0AS(6Sc=9>deeB78J_RkzjVGUXy~LjE!xITlyuuf*qE_gQqp*ScgfF?CY`k$3 zd-7MV|HWe6&f|}9A9AePW|L*;I#BA@fW3xQn{)juTD!yXEut(08Piz&43}N#T$DTw z=OX3M-iHj8H-Ve`KwX?352GDKkT5N{iPAiCif2=a&wDo6SW4W3@MHOeuW}HQ8IE$? zcKD!t!e6`osfLl|+)uGeeZpfL%p2^TV;tG+eZtqVv=~lM2dN6}%O`xVmJ#nDo|Iw3 z^9kQ~J;!izj306R8riSO3d=@%s!j4S&~Dx_z@`#pljK)OLLd}W^UNrjfKjrYBqms3 zdyw7qb?mu+ena+LjO2_t@to(Bu?IOv_c7StjguD~EF-YXBFinvSap#6X4pY6OG1dp z^}nzfCL^ej5sAEh?S8oEhLAc+RsvpcOxI#wFM^UlqBz$JXm7vM9_Mx1qtKx;YujlL z#%wY`f`h@xt)X$g9Zks+n$onm*9${s(Cr_}=Z4VU&v4o%ImdkJjtztuZpeC7!vRlY z_IXpk2ZbN*q1q%rL%pt~P#_uN^f&p{rhYfl_uox#zMFp;HS@mxdSm`of#SAmV^(FI ze>E_OJXE4PC@GT16Gt8%A2Kva%ue$H{|dL?X%7L-411@JoI^Ub) Date: Mon, 16 Mar 2026 11:27:52 +0530 Subject: [PATCH 35/49] Minor UI Update Minor animation updates for a couple of themes. Better Window bar (on-top) handling. Minor UI adjustments. --- src/main/kotlin/app/morphe/gui/App.kt | 21 +- src/main/kotlin/app/morphe/gui/GuiMain.kt | 8 +- .../gui/ui/components/CustomTitleBar.kt | 157 ++ .../gui/ui/components/DeviceIndicator.kt | 14 +- .../gui/ui/components/SettingsButton.kt | 11 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 3 +- .../ui/screens/home/components/ApkInfoCard.kt | 11 +- .../screens/patches/PatchSelectionScreen.kt | 1528 ++++++++++------- .../gui/ui/screens/patches/PatchesScreen.kt | 16 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 1025 +++++------ 10 files changed, 1560 insertions(+), 1234 deletions(-) create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index 7745e89..d4bb6c4 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -8,6 +8,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.FrameWindowScope +import app.morphe.gui.ui.components.CustomTitleBar import app.morphe.gui.ui.components.LottieAnimation import app.morphe.gui.ui.components.SakuraPetals import cafe.adriel.voyager.navigator.Navigator @@ -42,7 +44,7 @@ val LocalModeState = staticCompositionLocalOf { } @Composable -fun App(initialSimplifiedMode: Boolean = true) { +fun App(initialSimplifiedMode: Boolean = true, frameWindowScope: FrameWindowScope? = null) { LaunchedEffect(Unit) { Logger.init() } @@ -50,12 +52,12 @@ fun App(initialSimplifiedMode: Boolean = true) { KoinApplication(application = { modules(appModule) }) { - AppContent(initialSimplifiedMode) + AppContent(initialSimplifiedMode, frameWindowScope) } } @Composable -private fun AppContent(initialSimplifiedMode: Boolean) { +private fun AppContent(initialSimplifiedMode: Boolean, frameWindowScope: FrameWindowScope? = null) { val configRepository: ConfigRepository = koinInject() val patchSourceManager: PatchSourceManager = koinInject() val scope = rememberCoroutineScope() @@ -115,8 +117,16 @@ private fun AppContent(initialSimplifiedMode: Boolean) { LocalModeState provides modeState ) { Surface(modifier = Modifier.fillMaxSize()) { - Box(modifier = Modifier.fillMaxSize()) { - if (!isLoading) { + Column(modifier = Modifier.fillMaxSize()) { + // Custom title bar (replaces native window chrome) + if (frameWindowScope != null) { + with(frameWindowScope) { + CustomTitleBar() + } + } + + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { + if (!isLoading) { val patchService: PatchService = koinInject() val quickViewModel = remember { QuickPatchViewModel(patchSourceManager, patchService, configRepository) @@ -175,6 +185,7 @@ private fun AppContent(initialSimplifiedMode: Boolean) { } } } + } } } } diff --git a/src/main/kotlin/app/morphe/gui/GuiMain.kt b/src/main/kotlin/app/morphe/gui/GuiMain.kt index 1891611..d0244cb 100644 --- a/src/main/kotlin/app/morphe/gui/GuiMain.kt +++ b/src/main/kotlin/app/morphe/gui/GuiMain.kt @@ -55,10 +55,14 @@ fun launchGui(args: Array) = application { onCloseRequest = ::exitApplication, title = "Morphe", state = windowState, - icon = appIcon + icon = appIcon, + undecorated = true ) { window.minimumSize = java.awt.Dimension(600, 400) - App(initialSimplifiedMode = initialSimplifiedMode) + App( + initialSimplifiedMode = initialSimplifiedMode, + frameWindowScope = this + ) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt b/src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt new file mode 100644 index 0000000..61a15df --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt @@ -0,0 +1,157 @@ +package app.morphe.gui.ui.components + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.window.FrameWindowScope +import app.morphe.gui.ui.theme.LocalMorpheFont +import java.awt.Frame +import java.awt.event.MouseAdapter +import java.awt.event.MouseEvent + +@Composable +fun FrameWindowScope.CustomTitleBar() { + val mono = LocalMorpheFont.current + + WindowDraggableArea( + modifier = Modifier + .fillMaxWidth() + .height(36.dp) + .background(MaterialTheme.colorScheme.surface) + ) { + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // App title + Text( + text = "Morphe", + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + letterSpacing = 0.5.sp, + modifier = Modifier.padding(start = 8.dp) + ) + + Spacer(modifier = Modifier.weight(1f)) + + // Window controls + WindowButton( + symbol = "─", + hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f), + symbolColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + onClick = { + window.extendedState = Frame.ICONIFIED + } + ) + + val isMaximized = remember { mutableStateOf(window.extendedState == Frame.MAXIMIZED_BOTH) } + + // Listen for external maximize state changes (e.g. OS double-click on title bar) + DisposableEffect(window) { + val listener = object : java.awt.event.WindowStateListener { + override fun windowStateChanged(e: java.awt.event.WindowEvent) { + isMaximized.value = (e.newState and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH + } + } + window.addWindowStateListener(listener) + onDispose { window.removeWindowStateListener(listener) } + } + + WindowButton( + symbol = if (isMaximized.value) "❐" else "□", + hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f), + symbolColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + onClick = { + if (isMaximized.value) { + window.extendedState = Frame.NORMAL + } else { + window.extendedState = Frame.MAXIMIZED_BOTH + } + isMaximized.value = !isMaximized.value + } + ) + + WindowButton( + symbol = "✕", + hoverColor = Color(0xFFE81123), + symbolColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + hoverSymbolColor = Color.White, + onClick = { + window.dispatchEvent( + java.awt.event.WindowEvent(window, java.awt.event.WindowEvent.WINDOW_CLOSING) + ) + } + ) + } + } + + // Double-click to maximize/restore + DisposableEffect(window) { + val listener = object : MouseAdapter() { + override fun mouseClicked(e: MouseEvent) { + if (e.clickCount == 2 && e.y <= 36) { + val isMax = (window.extendedState and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH + window.extendedState = if (isMax) Frame.NORMAL else Frame.MAXIMIZED_BOTH + } + } + } + window.addMouseListener(listener) + onDispose { window.removeMouseListener(listener) } + } +} + +@Composable +private fun WindowButton( + symbol: String, + hoverColor: Color, + symbolColor: Color, + hoverSymbolColor: Color? = null, + onClick: () -> Unit +) { + val hover = remember { MutableInteractionSource() } + val isHovered by hover.collectIsHoveredAsState() + val bg by animateColorAsState( + if (isHovered) hoverColor else Color.Transparent, + animationSpec = tween(100) + ) + val fg by animateColorAsState( + if (isHovered && hoverSymbolColor != null) hoverSymbolColor else symbolColor, + animationSpec = tween(100) + ) + + Box( + modifier = Modifier + .size(36.dp) + .hoverable(hover) + .clickable(onClick = onClick) + .background(bg), + contentAlignment = Alignment.Center + ) { + Text( + text = symbol, + fontSize = 11.sp, + color = fg + ) + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt index 474d52e..d96c2a5 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -18,6 +19,7 @@ import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow @@ -62,16 +64,18 @@ fun DeviceIndicator(modifier: Modifier = Modifier) { ) Box(modifier = modifier) { - Surface( - onClick = { showPopup = !showPopup }, - shape = RoundedCornerShape(corners.small), - color = Color.Transparent, + Box( modifier = Modifier + .height(34.dp) .hoverable(hoverInteraction) + .clip(RoundedCornerShape(corners.small)) .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .clickable { showPopup = !showPopup } ) { Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 7.dp), + modifier = Modifier + .fillMaxHeight() + .padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp) ) { diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt index e7ce8bb..55592e4 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -5,6 +5,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -13,10 +14,11 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.draw.clip import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings +import androidx.compose.foundation.layout.Box import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -67,13 +69,14 @@ fun SettingsButton( animationSpec = tween(150) ) - IconButton( - onClick = { showSettingsDialog = true }, + Box( modifier = modifier .size(34.dp) .hoverable(hoverInteraction) + .clip(RoundedCornerShape(corners.small)) .border(1.dp, borderColor, RoundedCornerShape(corners.small)) - .background(Color.Transparent, RoundedCornerShape(corners.small)) + .clickable { showSettingsDialog = true }, + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Settings, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 601b688..a7fbcdf 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -365,12 +365,13 @@ private fun PatchesVersionInline( Row( modifier = Modifier + .height(34.dp) .clip(RoundedCornerShape(corners.small)) .border(1.dp, borderColor, RoundedCornerShape(corners.small)) .background(MaterialTheme.colorScheme.surface) .hoverable(hoverInteraction) .clickable(onClick = onChangePatchesClick) - .padding(horizontal = 12.dp, vertical = 6.dp), + .padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index 46eaab2..58aa184 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.clickable import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState @@ -124,20 +125,22 @@ fun ApkInfoCard( animationSpec = tween(150) ) - IconButton( - onClick = onClearClick, + Box( modifier = Modifier - .size(30.dp) + .size(44.dp) .hoverable(closeHover) + .clip(RoundedCornerShape(corners.small)) .background(closeBg, RoundedCornerShape(corners.small)) .border(1.dp, closeBorder, RoundedCornerShape(corners.small)) + .clickable(onClick = onClearClick), + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Close, contentDescription = "Remove APK", tint = if (isCloseHovered) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - modifier = Modifier.size(14.dp) + modifier = Modifier.size(18.dp) ) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index f8aa2ac..65bd16e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -1,10 +1,12 @@ package app.morphe.gui.ui.screens.patches import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.hoverable @@ -15,7 +17,6 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -32,6 +33,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -45,10 +49,13 @@ import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.Patch import org.koin.core.parameter.parametersOf import app.morphe.gui.ui.components.ErrorDialog -import app.morphe.gui.ui.components.TopBarRow +import app.morphe.gui.ui.components.DeviceIndicator +import app.morphe.gui.ui.components.SettingsButton import app.morphe.gui.ui.components.getErrorType import app.morphe.gui.ui.components.getFriendlyErrorMessage import app.morphe.gui.ui.screens.patching.PatchingScreen +import app.morphe.gui.ui.theme.LocalMorpheCorners +import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.DeviceMonitor import java.awt.Toolkit @@ -78,6 +85,8 @@ data class PatchSelectionScreen( @OptIn(ExperimentalMaterial3Api::class) @Composable fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val navigator = LocalNavigator.currentOrThrow val uiState by viewModel.uiState.collectAsState() @@ -91,7 +100,6 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { } } - // Error dialog if (showErrorDialog && currentError != null) { ErrorDialog( title = "Error Loading Patches", @@ -109,282 +117,392 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { ) } - // State for command preview var cleanMode by remember { mutableStateOf(false) } var showCommandPreview by remember { mutableStateOf(false) } var continueOnError by remember { mutableStateOf(false) } - Scaffold( - topBar = { - TopAppBar( - title = { - Column { - Text("Select Patches", fontWeight = FontWeight.SemiBold) - Text( - text = "${uiState.selectedCount} of ${uiState.totalCount} selected", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - }, - navigationIcon = { - IconButton(onClick = { navigator.pop() }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" - ) - } - }, - actions = { - // Select all / Deselect all - TextButton( - onClick = { - if (uiState.selectedPatches.size == uiState.allPatches.size) { - viewModel.deselectAll() - } else { - viewModel.selectAll() - } - }, - shape = RoundedCornerShape(12.dp) - ) { - Text( - if (uiState.selectedPatches.size == uiState.allPatches.size) "Deselect All" else "Select All", - color = MorpheColors.Blue - ) - } + val dividerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.08f) - Spacer(Modifier.width(12.dp)) - - // Command preview toggle & continue-on-error toggle - if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { - val isActive = showCommandPreview - Surface( - onClick = { showCommandPreview = !showCommandPreview }, - shape = RoundedCornerShape(8.dp), - color = if (isActive) MorpheColors.Teal.copy(alpha = 0.15f) - else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - border = BorderStroke( - width = 1.dp, - color = if (isActive) MorpheColors.Teal.copy(alpha = 0.5f) - else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - ) { - Icon( - imageVector = Icons.Default.Terminal, - contentDescription = "Command Preview", - tint = if (isActive) MorpheColors.Teal else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp).size(20.dp) - ) - } + Column(modifier = Modifier.fillMaxSize()) { + // ── Header bar ── + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Back button + val backHover = remember { MutableInteractionSource() } + val isBackHovered by backHover.collectIsHoveredAsState() + val backBorder by animateColorAsState( + if (isBackHovered) MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = tween(150) + ) - Spacer(Modifier.width(6.dp)) + Box( + modifier = Modifier + .size(34.dp) + .hoverable(backHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, backBorder, RoundedCornerShape(corners.small)) + .clickable { navigator.pop() }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + } - // Continue on error toggle - TooltipBox( - positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), - tooltip = { - PlainTooltip { - Text("Continue patching even if a patch fails") - } - }, - state = rememberTooltipState() - ) { - Surface( - onClick = { continueOnError = !continueOnError }, - shape = RoundedCornerShape(8.dp), - color = if (continueOnError) MaterialTheme.colorScheme.error.copy(alpha = 0.15f) - else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - border = BorderStroke( - width = 1.dp, - color = if (continueOnError) MaterialTheme.colorScheme.error.copy(alpha = 0.5f) - else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) - ) { - Icon( - imageVector = Icons.Default.PlaylistRemove, - contentDescription = "Continue on error", - tint = if (continueOnError) MaterialTheme.colorScheme.error - else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp).size(20.dp) - ) - } - } - } + Spacer(modifier = Modifier.width(14.dp)) - Spacer(Modifier.width(12.dp)) + // Title block + Column(modifier = Modifier.weight(1f)) { + Text( + text = "SELECT PATCHES", + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 1.5.sp + ) + Text( + text = "${uiState.selectedCount} of ${uiState.totalCount} selected", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 0.3.sp + ) + } - TopBarRow(allowCacheClear = false) + // Select/Deselect all + val selectAllHover = remember { MutableInteractionSource() } + val isSelectAllHovered by selectAllHover.collectIsHoveredAsState() + val allSelected = uiState.selectedPatches.size == uiState.allPatches.size + val selectAllBorder by animateColorAsState( + if (isSelectAllHovered) MorpheColors.Blue.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + animationSpec = tween(150) + ) - Spacer(Modifier.width(12.dp)) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface + Box( + modifier = Modifier + .height(34.dp) + .hoverable(selectAllHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, selectAllBorder, RoundedCornerShape(corners.small)) + .clickable { + if (allSelected) viewModel.deselectAll() else viewModel.selectAll() + } + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = if (allSelected) "DESELECT ALL" else "SELECT ALL", + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = if (isSelectAllHovered) MorpheColors.Blue + else MorpheColors.Blue.copy(alpha = 0.7f), + letterSpacing = 1.sp ) - ) - }, - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - // Command preview - collapsible via top bar button + } + + Spacer(modifier = Modifier.width(6.dp)) + + // Command preview toggle if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { - val commandPreview = remember(uiState.selectedPatches, uiState.selectedArchitectures, cleanMode, continueOnError) { - viewModel.getCommandPreview(cleanMode, continueOnError) - } - AnimatedVisibility( - visible = showCommandPreview, - enter = expandVertically(), - exit = shrinkVertically() + val cmdHover = remember { MutableInteractionSource() } + val isCmdHovered by cmdHover.collectIsHoveredAsState() + val cmdActive = showCommandPreview + val cmdBorder by animateColorAsState( + when { + cmdActive -> MorpheColors.Teal.copy(alpha = 0.5f) + isCmdHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.1f) + }, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .size(34.dp) + .hoverable(cmdHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, cmdBorder, RoundedCornerShape(corners.small)) + .then( + if (cmdActive) Modifier.background( + MorpheColors.Teal.copy(alpha = 0.08f), + RoundedCornerShape(corners.small) + ) else Modifier + ) + .clickable { showCommandPreview = !showCommandPreview }, + contentAlignment = Alignment.Center ) { - CommandPreview( - command = commandPreview, - cleanMode = cleanMode, - onToggleMode = { cleanMode = !cleanMode }, - onCopy = { - val clipboard = Toolkit.getDefaultToolkit().systemClipboard - clipboard.setContents(StringSelection(commandPreview), null) - }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) + Icon( + imageVector = Icons.Default.Terminal, + contentDescription = "Command Preview", + tint = if (cmdActive) MorpheColors.Teal + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.size(16.dp) ) } - } - // Search bar - SearchBar( - query = uiState.searchQuery, - onQueryChange = { viewModel.setSearchQuery(it) }, - showOnlySelected = uiState.showOnlySelected, - onShowOnlySelectedChange = { viewModel.setShowOnlySelected(it) }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) - ) + Spacer(modifier = Modifier.width(6.dp)) - // Info card about default-disabled patches - val defaultDisabledCount = remember(uiState.allPatches) { - viewModel.getDefaultDisabledCount() + // Continue on error toggle + val errHover = remember { MutableInteractionSource() } + val isErrHovered by errHover.collectIsHoveredAsState() + val errBorder by animateColorAsState( + when { + continueOnError -> MaterialTheme.colorScheme.error.copy(alpha = 0.5f) + isErrHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.1f) + }, + animationSpec = tween(150) + ) + + TooltipBox( + positionProvider = TooltipDefaults.rememberTooltipPositionProvider(), + tooltip = { + PlainTooltip { + Text( + "Continue patching even if a patch fails", + fontFamily = mono, + fontSize = 11.sp + ) + } + }, + state = rememberTooltipState() + ) { + Box( + modifier = Modifier + .size(34.dp) + .hoverable(errHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, errBorder, RoundedCornerShape(corners.small)) + .then( + if (continueOnError) Modifier.background( + MaterialTheme.colorScheme.error.copy(alpha = 0.08f), + RoundedCornerShape(corners.small) + ) else Modifier + ) + .clickable { continueOnError = !continueOnError }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.PlaylistRemove, + contentDescription = "Continue on error", + tint = if (continueOnError) MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.size(16.dp) + ) + } + } + + Spacer(modifier = Modifier.width(6.dp)) } - var infoDismissed by remember { mutableStateOf(false) } + DeviceIndicator() + Spacer(modifier = Modifier.width(6.dp)) + SettingsButton(allowCacheClear = false) + } + + // Divider + Box( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(dividerColor) + ) + + // Command preview — collapsible + if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { + val commandPreview = remember(uiState.selectedPatches, uiState.selectedArchitectures, cleanMode, continueOnError) { + viewModel.getCommandPreview(cleanMode, continueOnError) + } AnimatedVisibility( - visible = defaultDisabledCount > 0 && !infoDismissed && !uiState.isLoading, + visible = showCommandPreview, enter = expandVertically(), exit = shrinkVertically() ) { - DefaultDisabledInfoCard( - count = defaultDisabledCount, - onDismiss = { infoDismissed = true }, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + CommandPreview( + command = commandPreview, + cleanMode = cleanMode, + onToggleMode = { cleanMode = !cleanMode }, + onCopy = { + val clipboard = Toolkit.getDefaultToolkit().systemClipboard + clipboard.setContents(StringSelection(commandPreview), null) + }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) ) } + } - when { - uiState.isLoading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - CircularProgressIndicator(color = MorpheColors.Blue) - Text( - text = "Loading patches...", - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } + // Search bar + PatchSearchBar( + query = uiState.searchQuery, + onQueryChange = { viewModel.setSearchQuery(it) }, + showOnlySelected = uiState.showOnlySelected, + onShowOnlySelectedChange = { viewModel.setShowOnlySelected(it) }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) + ) - uiState.filteredPatches.isEmpty() && !uiState.isLoading -> { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center + // Info card about default-disabled patches + val defaultDisabledCount = remember(uiState.allPatches) { + viewModel.getDefaultDisabledCount() + } + var infoDismissed by remember { mutableStateOf(false) } + + AnimatedVisibility( + visible = defaultDisabledCount > 0 && !infoDismissed && !uiState.isLoading, + enter = expandVertically(), + exit = shrinkVertically() + ) { + DefaultDisabledInfoCard( + count = defaultDisabledCount, + onDismiss = { infoDismissed = true }, + modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp) + ) + } + + when { + uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) ) { + CircularProgressIndicator( + color = MorpheColors.Blue, + strokeWidth = 2.dp, + modifier = Modifier.size(24.dp) + ) Text( - text = if (uiState.searchQuery.isNotBlank()) "No patches match your search" else "No patches found", - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "LOADING PATCHES", + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 1.5.sp ) } } + } - else -> { - // Patch list - LazyColumn( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Architecture selector at the top of the list - // Disabled for .apkm files until properly tested with merged APKs - val isApkm = viewModel.getApkPath().endsWith(".apkm", ignoreCase = true) - val showArchSelector = !isApkm && - uiState.apkArchitectures.size > 1 && - !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") - if (showArchSelector) { - item(key = "arch_selector") { - ArchitectureSelectorCard( - architectures = uiState.apkArchitectures, - selectedArchitectures = uiState.selectedArchitectures, - onToggleArchitecture = { viewModel.toggleArchitecture(it) } - ) - } - } + uiState.filteredPatches.isEmpty() && !uiState.isLoading -> { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = if (uiState.searchQuery.isNotBlank()) "No patches match your search" + else "No patches found", + fontSize = 12.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + } - items( - items = uiState.filteredPatches, - key = { it.uniqueId } - ) { patch -> - PatchListItem( - patch = patch, - isSelected = uiState.selectedPatches.contains(patch.uniqueId), - onToggle = { viewModel.togglePatch(patch.uniqueId) }, - getOptionValue = { optionKey, default -> - viewModel.getOptionValue(patch.name, optionKey, default) - }, - onOptionValueChange = { optionKey, value -> - viewModel.setOptionValue(patch.name, optionKey, value) - } + else -> { + // Patch list + LazyColumn( + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Architecture selector + val isApkm = viewModel.getApkPath().endsWith(".apkm", ignoreCase = true) + val showArchSelector = !isApkm && + uiState.apkArchitectures.size > 1 && + !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") + if (showArchSelector) { + item(key = "arch_selector") { + ArchitectureSelectorCard( + architectures = uiState.apkArchitectures, + selectedArchitectures = uiState.selectedArchitectures, + onToggleArchitecture = { viewModel.toggleArchitecture(it) } ) } } - // Bottom action bar - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Button( - onClick = { - val config = viewModel.createPatchConfig(continueOnError) - navigator.push(PatchingScreen(config)) - }, - enabled = uiState.selectedPatches.isNotEmpty(), - modifier = Modifier - .weight(1f) - .height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) - ) { - Text( - text = "Patch (${uiState.selectedCount})", - fontWeight = FontWeight.Medium - ) + items( + items = uiState.filteredPatches, + key = { it.uniqueId } + ) { patch -> + PatchListItem( + patch = patch, + isSelected = uiState.selectedPatches.contains(patch.uniqueId), + onToggle = { viewModel.togglePatch(patch.uniqueId) }, + getOptionValue = { optionKey, default -> + viewModel.getOptionValue(patch.name, optionKey, default) + }, + onOptionValueChange = { optionKey, value -> + viewModel.setOptionValue(patch.name, optionKey, value) } + ) + } + } + + // ── Bottom action bar ── + Box( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = dividerColor, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1f + ) } + .padding(16.dp) + ) { + val patchHover = remember { MutableInteractionSource() } + val isPatchHovered by patchHover.collectIsHoveredAsState() + val patchEnabled = uiState.selectedPatches.isNotEmpty() + val patchBg by animateColorAsState( + when { + !patchEnabled -> MorpheColors.Blue.copy(alpha = 0.1f) + isPatchHovered -> MorpheColors.Blue.copy(alpha = 0.9f) + else -> MorpheColors.Blue + }, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(42.dp) + .hoverable(patchHover) + .clip(RoundedCornerShape(corners.small)) + .background(patchBg, RoundedCornerShape(corners.small)) + .then( + if (patchEnabled) Modifier.clickable { + val config = viewModel.createPatchConfig(continueOnError) + navigator.push(PatchingScreen(config)) + } else Modifier + ), + contentAlignment = Alignment.Center + ) { + Text( + text = "PATCH (${uiState.selectedCount})", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = if (patchEnabled) Color.White + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), + letterSpacing = 1.5.sp + ) } } } @@ -392,72 +510,121 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { } } +// ── Search Bar ── + @Composable -private fun SearchBar( +private fun PatchSearchBar( query: String, onQueryChange: (String) -> Unit, showOnlySelected: Boolean, onShowOnlySelectedChange: (Boolean) -> Unit, modifier: Modifier = Modifier ) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + Row( modifier = modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { - OutlinedTextField( - value = query, - onValueChange = onQueryChange, - modifier = Modifier.weight(1f), - placeholder = { Text("Search patches...", style = MaterialTheme.typography.bodySmall) }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp) + // Custom compact search field + val searchFocused = remember { mutableStateOf(false) } + val searchBorderColor by animateColorAsState( + if (searchFocused.value) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), + animationSpec = tween(150) + ) + + Row( + modifier = Modifier + .weight(1f) + .height(38.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, searchBorderColor, RoundedCornerShape(corners.small)) + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = Icons.Default.Search, + contentDescription = "Search", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier.size(16.dp) + ) + + Box(modifier = Modifier.weight(1f)) { + if (query.isEmpty()) { + Text( + "Search patches…", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.35f) + ) + } + androidx.compose.foundation.text.BasicTextField( + value = query, + onValueChange = onQueryChange, + singleLine = true, + textStyle = LocalTextStyle.current.copy( + fontSize = 12.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface + ), + cursorBrush = androidx.compose.ui.graphics.SolidColor(MorpheColors.Blue), + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { searchFocused.value = it.isFocused } ) - }, - trailingIcon = { - if (query.isNotEmpty()) { - IconButton(onClick = { onQueryChange("") }) { - Icon( - imageVector = Icons.Default.Clear, - contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp) - ) - } + } + + if (query.isNotEmpty()) { + Box( + modifier = Modifier + .size(24.dp) + .clip(RoundedCornerShape(corners.small)) + .clickable { onQueryChange("") }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(14.dp) + ) } + } + } + + // "Selected" filter chip + val chipHover = remember { MutableInteractionSource() } + val isChipHovered by chipHover.collectIsHoveredAsState() + val chipBorder by animateColorAsState( + when { + showOnlySelected -> MorpheColors.Blue.copy(alpha = 0.5f) + isChipHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) }, - singleLine = true, - shape = RoundedCornerShape(12.dp), - textStyle = MaterialTheme.typography.bodySmall, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MorpheColors.Blue, - unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) - ) + animationSpec = tween(150) ) - val chipInteractionSource = remember { MutableInteractionSource() } - val chipHovered by chipInteractionSource.collectIsHoveredAsState() - Surface( + Box( modifier = Modifier - .hoverable(chipInteractionSource) - .clickable(interactionSource = chipInteractionSource, indication = null) { - onShowOnlySelectedChange(!showOnlySelected) - }, - shape = RoundedCornerShape(8.dp), - color = if (showOnlySelected) MorpheColors.Blue.copy(alpha = if (chipHovered) 0.22f else 0.12f) - else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (chipHovered) 0.7f else 0.4f), - border = BorderStroke( - width = 1.dp, - color = if (showOnlySelected) MorpheColors.Blue.copy(alpha = 0.5f) - else MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - ) + .height(38.dp) + .hoverable(chipHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, chipBorder, RoundedCornerShape(corners.small)) + .then( + if (showOnlySelected) Modifier.background( + MorpheColors.Blue.copy(alpha = 0.08f), + RoundedCornerShape(corners.small) + ) else Modifier + ) + .clickable { onShowOnlySelectedChange(!showOnlySelected) } + .padding(horizontal = 12.dp), + contentAlignment = Alignment.Center ) { Row( - modifier = Modifier.padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(4.dp) ) { @@ -466,19 +633,25 @@ private fun SearchBar( imageVector = Icons.Default.Check, contentDescription = null, tint = MorpheColors.Blue, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(14.dp) ) } Text( - text = "Selected", - fontSize = 14.sp, - color = if (showOnlySelected) MorpheColors.Blue else MaterialTheme.colorScheme.onSurfaceVariant + text = "SELECTED", + fontSize = 10.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = if (showOnlySelected) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 1.sp ) } } } } +// ── Patch List Item ── + @Composable private fun PatchListItem( patch: Patch, @@ -487,149 +660,212 @@ private fun PatchListItem( getOptionValue: (optionKey: String, default: String?) -> String = { _, d -> d ?: "" }, onOptionValueChange: (optionKey: String, value: String) -> Unit = { _, _ -> } ) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val interactionSource = remember { MutableInteractionSource() } val isHovered by interactionSource.collectIsHoveredAsState() - val backgroundColor = if (isSelected) { - MorpheColors.Blue.copy(alpha = if (isHovered) 0.17f else 0.1f) - } else { - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = if (isHovered) 0.5f else 0.3f) - } + + val borderColor by animateColorAsState( + when { + isSelected && isHovered -> MorpheColors.Blue.copy(alpha = 0.4f) + isSelected -> MorpheColors.Blue.copy(alpha = 0.2f) + isHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.08f) + }, + animationSpec = tween(150) + ) var showOptions by remember { mutableStateOf(false) } - Card( + Column( modifier = Modifier .fillMaxWidth() - .hoverable(interactionSource), - colors = CardDefaults.cardColors(containerColor = backgroundColor), - shape = RoundedCornerShape(12.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .then( + if (isSelected) Modifier.background( + MorpheColors.Blue.copy(alpha = 0.04f), + RoundedCornerShape(corners.small) + ) else Modifier + ) + .hoverable(interactionSource) ) { - Column { - // Header area — clicking here toggles the patch - Row( + // Header — clicking toggles patch + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(interactionSource = interactionSource, indication = null, onClick = onToggle) + .padding(14.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Custom checkbox + Box( modifier = Modifier - .fillMaxWidth() - .clickable(interactionSource = interactionSource, indication = null, onClick = onToggle) - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically + .size(18.dp) + .clip(RoundedCornerShape(corners.small)) + .border( + 1.5.dp, + if (isSelected) MorpheColors.Blue else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f), + RoundedCornerShape(corners.small) + ) + .then( + if (isSelected) Modifier.background(MorpheColors.Blue, RoundedCornerShape(corners.small)) + else Modifier + ), + contentAlignment = Alignment.Center ) { - Checkbox( - checked = isSelected, - onCheckedChange = null, - colors = CheckboxDefaults.colors( - checkedColor = MorpheColors.Blue, - uncheckedColor = MaterialTheme.colorScheme.onSurfaceVariant + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(12.dp) ) + } + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = patch.name, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface ) - Column(modifier = Modifier.weight(1f)) { + if (patch.description.isNotBlank()) { + Spacer(modifier = Modifier.height(3.dp)) Text( - text = patch.name, - fontSize = 15.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + text = patch.description, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + maxLines = 2, + overflow = TextOverflow.Ellipsis ) + } - if (patch.description.isNotBlank()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = patch.description, - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - } - - // Show compatible packages if any - if (patch.compatiblePackages.isNotEmpty()) { - val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") - Spacer(modifier = Modifier.height(4.dp)) - Row( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - patch.compatiblePackages.take(2).forEach { pkg -> - val meaningful = pkg.name.split(".").filter { it !in genericSegments } - val displayName = meaningful.takeLast(2).joinToString(" ") - .replaceFirstChar { it.uppercase() } - Surface( - color = if (isSelected) MorpheColors.Blue.copy(alpha = 0.18f) - else MaterialTheme.colorScheme.surfaceVariant, - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = displayName, - fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + // Compatible packages + if (patch.compatiblePackages.isNotEmpty()) { + val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") + Spacer(modifier = Modifier.height(6.dp)) + Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + patch.compatiblePackages.take(2).forEach { pkg -> + val meaningful = pkg.name.split(".").filter { it !in genericSegments } + val displayName = meaningful.takeLast(2).joinToString(" ") + .replaceFirstChar { it.uppercase() } + Box( + modifier = Modifier + .border( + 1.dp, + MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), + RoundedCornerShape(corners.small) ) - } + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = displayName, + fontSize = 9.sp, + fontFamily = mono, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 0.3.sp + ) } } } + } - // Options chip - if (patch.options.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "${patch.options.size} option${if (patch.options.size > 1) "s" else ""} ${if (showOptions) "▲" else "▼"}", - fontSize = 10.sp, - color = MorpheColors.Teal - ) - } + // Options indicator + if (patch.options.isNotEmpty()) { + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${patch.options.size} option${if (patch.options.size > 1) "s" else ""} ${if (showOptions) "▲" else "▼"}", + fontSize = 9.sp, + fontFamily = mono, + fontWeight = FontWeight.Medium, + color = MorpheColors.Teal.copy(alpha = 0.7f), + letterSpacing = 0.5.sp + ) } } + } - // Options editor — completely outside the toggle-clickable area - if (patch.options.isNotEmpty()) { - // Toggle button for options - if (!showOptions) { - Surface( - onClick = { showOptions = true }, - color = MorpheColors.Teal.copy(alpha = 0.06f), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "Configure options", - fontSize = 10.sp, - color = MorpheColors.Teal.copy(alpha = 0.7f), - modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp) - ) - } - } + // Options section + if (patch.options.isNotEmpty()) { + val optionDivider = MaterialTheme.colorScheme.outline.copy(alpha = 0.06f) - AnimatedVisibility( - visible = showOptions, - enter = expandVertically(), - exit = shrinkVertically() - ) { - Column( - modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 12.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Collapse button - Surface( - onClick = { showOptions = false }, - color = MorpheColors.Teal.copy(alpha = 0.06f), - shape = RoundedCornerShape(6.dp), - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = "Hide options ▲", - fontSize = 10.sp, - color = MorpheColors.Teal.copy(alpha = 0.7f), - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + if (!showOptions) { + Box( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = optionDivider, + start = Offset(14.dp.toPx(), 0f), + end = Offset(size.width - 14.dp.toPx(), 0f), + strokeWidth = 1f ) } + .clickable { showOptions = true } + .background(MorpheColors.Teal.copy(alpha = 0.03f)) + .padding(horizontal = 14.dp, vertical = 6.dp) + ) { + Text( + text = "CONFIGURE OPTIONS", + fontSize = 9.sp, + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + color = MorpheColors.Teal.copy(alpha = 0.5f), + letterSpacing = 1.sp + ) + } + } - patch.options.forEach { option -> - PatchOptionEditor( - option = option, - value = getOptionValue(option.key, option.default), - onValueChange = { onOptionValueChange(option.key, it) } + AnimatedVisibility( + visible = showOptions, + enter = expandVertically(), + exit = shrinkVertically() + ) { + Column( + modifier = Modifier + .drawBehind { + drawLine( + color = optionDivider, + start = Offset(14.dp.toPx(), 0f), + end = Offset(size.width - 14.dp.toPx(), 0f), + strokeWidth = 1f ) } + .padding(start = 14.dp, end = 14.dp, bottom = 12.dp, top = 8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Collapse button + Box( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .background(MorpheColors.Teal.copy(alpha = 0.04f)) + .clickable { showOptions = false } + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = "HIDE OPTIONS ▲", + fontSize = 9.sp, + fontFamily = mono, + fontWeight = FontWeight.SemiBold, + color = MorpheColors.Teal.copy(alpha = 0.5f), + letterSpacing = 1.sp + ) + } + + patch.options.forEach { option -> + PatchOptionEditor( + option = option, + value = getOptionValue(option.key, option.default), + onValueChange = { onOptionValueChange(option.key, it) } + ) } } } @@ -637,12 +873,17 @@ private fun PatchListItem( } } +// ── Patch Option Editor ── + @Composable private fun PatchOptionEditor( option: app.morphe.gui.data.model.PatchOption, value: String, onValueChange: (String) -> Unit ) { + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Row( horizontalArrangement = Arrangement.spacedBy(4.dp), @@ -652,12 +893,14 @@ private fun PatchOptionEditor( text = option.title.ifBlank { option.key }, fontSize = 11.sp, fontWeight = FontWeight.Medium, + fontFamily = mono, color = MorpheColors.Teal ) if (option.required) { Text( text = "*", fontSize = 11.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.error ) } @@ -666,7 +909,8 @@ private fun PatchOptionEditor( Text( text = option.description, fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), maxLines = 2, overflow = TextOverflow.Ellipsis ) @@ -695,12 +939,12 @@ private fun PatchOptionEditor( Text( text = if (localChecked) "Enabled" else "Disabled", fontSize = 10.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) ) } } else -> { - // Use local state to ensure text field is responsive, sync back to ViewModel var localText by remember(option.key) { mutableStateOf(value) } LaunchedEffect(value) { if (localText != value) localText = value @@ -715,16 +959,21 @@ private fun PatchOptionEditor( placeholder = { Text( text = option.default ?: option.type.name.lowercase(), - fontSize = 11.sp + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) ) }, singleLine = true, - textStyle = LocalTextStyle.current.copy(fontSize = 11.sp), - shape = RoundedCornerShape(6.dp), + textStyle = LocalTextStyle.current.copy( + fontSize = 11.sp, + fontFamily = mono + ), + shape = RoundedCornerShape(corners.small), modifier = Modifier.fillMaxWidth(), colors = OutlinedTextFieldDefaults.colors( - unfocusedBorderColor = MorpheColors.Teal.copy(alpha = 0.3f), - focusedBorderColor = MorpheColors.Teal + unfocusedBorderColor = MorpheColors.Teal.copy(alpha = 0.2f), + focusedBorderColor = MorpheColors.Teal.copy(alpha = 0.6f) ) ) } @@ -732,56 +981,63 @@ private fun PatchOptionEditor( } } +// ── Default Disabled Info Card ── + @Composable private fun DefaultDisabledInfoCard( count: Int, onDismiss: () -> Unit, modifier: Modifier = Modifier ) { - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MorpheColors.Blue.copy(alpha = 0.08f) - ), - shape = RoundedCornerShape(12.dp) + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + + Row( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .border( + 1.dp, + MorpheColors.Blue.copy(alpha = 0.15f), + RoundedCornerShape(corners.small) + ) + .background(MorpheColors.Blue.copy(alpha = 0.04f)) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Row( + Icon( + imageVector = Icons.Default.Info, + contentDescription = null, + tint = MorpheColors.Blue.copy(alpha = 0.6f), + modifier = Modifier.size(16.dp) + ) + Text( + text = "$count patch${if (count > 1) "es are" else " is"} unselected by default as they may cause issues.", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + modifier = Modifier.weight(1f) + ) + Box( modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) + .size(24.dp) + .clip(RoundedCornerShape(corners.small)) + .clickable(onClick = onDismiss), + contentAlignment = Alignment.Center ) { Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MorpheColors.Blue, - modifier = Modifier.size(18.dp) - ) - Text( - text = "$count patch${if (count > 1) "es are" else " is"} unselected by default as they may cause issues or are not recommended by the patches team.", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.weight(1f) + imageVector = Icons.Default.Close, + contentDescription = "Dismiss", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier.size(14.dp) ) - IconButton( - onClick = onDismiss, - modifier = Modifier.size(24.dp) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Dismiss", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp) - ) - } } } } -/** - * Terminal-style command preview showing the CLI command that will be executed. - */ +// ── Command Preview ── + @Composable private fun CommandPreview( command: String, @@ -790,14 +1046,15 @@ private fun CommandPreview( onCopy: () -> Unit, modifier: Modifier = Modifier ) { - val terminalBackground = Color(0xFF1E1E1E) - val terminalGreen = Color(0xFF6A9955) - val terminalText = Color(0xFFD4D4D4) - val terminalDim = Color(0xFF6A9955) + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + + val terminalGreen = MorpheColors.Teal + val terminalText = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.8f) + val terminalBg = MaterialTheme.colorScheme.surface var showCopied by remember { mutableStateOf(false) } - // Reset "Copied!" message after a delay LaunchedEffect(showCopied) { if (showCopied) { kotlinx.coroutines.delay(1500) @@ -805,111 +1062,130 @@ private fun CommandPreview( } } - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors(containerColor = terminalBackground), - shape = RoundedCornerShape(8.dp) + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .border( + 1.dp, + terminalGreen.copy(alpha = 0.15f), + RoundedCornerShape(corners.small) + ) + .background(terminalBg) + .padding(12.dp) ) { - Column( - modifier = Modifier.padding(12.dp) + // Header + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween ) { - // Header with terminal icon and controls Row( - modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - // Left side - icon and title - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Icon( - imageVector = Icons.Default.Terminal, - contentDescription = null, - tint = terminalGreen, - modifier = Modifier.size(14.dp) - ) - Text( - text = "Command Preview", - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = terminalGreen - ) - } + Icon( + imageVector = Icons.Default.Terminal, + contentDescription = null, + tint = terminalGreen.copy(alpha = 0.7f), + modifier = Modifier.size(14.dp) + ) + Text( + text = "COMMAND PREVIEW", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = terminalGreen.copy(alpha = 0.7f), + letterSpacing = 1.sp + ) + } - // Right side - controls - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - // Copy button - Surface( - onClick = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Copy button + val copyHover = remember { MutableInteractionSource() } + val isCopyHovered by copyHover.collectIsHoveredAsState() + + Box( + modifier = Modifier + .hoverable(copyHover) + .clip(RoundedCornerShape(corners.small)) + .clickable { onCopy() showCopied = true - }, - color = Color.Transparent, - shape = RoundedCornerShape(4.dp) - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp), - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) - ) { - Icon( - imageVector = Icons.Default.ContentCopy, - contentDescription = "Copy", - tint = if (showCopied) terminalGreen else terminalDim, - modifier = Modifier.size(12.dp) - ) - Text( - text = if (showCopied) "Copied!" else "Copy", - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - color = if (showCopied) terminalGreen else terminalDim - ) } - } - - // Mode toggle - Surface( - onClick = onToggleMode, - color = Color.Transparent, - shape = RoundedCornerShape(4.dp) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { + Icon( + imageVector = Icons.Default.ContentCopy, + contentDescription = "Copy", + tint = if (showCopied) terminalGreen + else terminalGreen.copy(alpha = if (isCopyHovered) 0.8f else 0.4f), + modifier = Modifier.size(12.dp) + ) Text( - text = if (cleanMode) "Compact" else "Expand", - fontSize = 12.sp, + text = if (showCopied) "COPIED" else "COPY", + fontSize = 9.sp, fontWeight = FontWeight.Bold, - color = terminalDim, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp) + fontFamily = mono, + color = if (showCopied) terminalGreen + else terminalGreen.copy(alpha = if (isCopyHovered) 0.8f else 0.4f), + letterSpacing = 0.5.sp ) } } - } - Spacer(modifier = Modifier.height(8.dp)) + // Mode toggle + val modeHover = remember { MutableInteractionSource() } + val isModeHovered by modeHover.collectIsHoveredAsState() - // Vertically scrollable command text with max height - Box( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 120.dp) - .verticalScroll(rememberScrollState()) - ) { - Text( - text = command, - fontSize = 11.sp, - fontFamily = FontFamily.Monospace, - color = terminalText, - lineHeight = 16.sp - ) + Box( + modifier = Modifier + .hoverable(modeHover) + .clip(RoundedCornerShape(corners.small)) + .clickable(onClick = onToggleMode) + .padding(horizontal = 6.dp, vertical = 2.dp) + ) { + Text( + text = if (cleanMode) "COMPACT" else "EXPAND", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = terminalGreen.copy(alpha = if (isModeHovered) 0.8f else 0.4f), + letterSpacing = 0.5.sp + ) + } } } + + Spacer(modifier = Modifier.height(8.dp)) + + // Command text + Box( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 120.dp) + .verticalScroll(rememberScrollState()) + ) { + Text( + text = command, + fontSize = 11.sp, + fontFamily = FontFamily.Monospace, + color = terminalText, + lineHeight = 16.sp + ) + } } } +// ── Architecture Selector ── + @Composable private fun ArchitectureSelectorCard( architectures: List, @@ -917,106 +1193,118 @@ private fun ArchitectureSelectorCard( onToggleArchitecture: (String) -> Unit, modifier: Modifier = Modifier ) { - // Get connected device architecture for hint + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val deviceState by DeviceMonitor.state.collectAsState() val deviceArch = deviceState.selectedDevice?.architecture - Card( - modifier = modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MorpheColors.Teal.copy(alpha = 0.08f) - ), - shape = RoundedCornerShape(12.dp) + Column( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .border( + 1.dp, + MorpheColors.Teal.copy(alpha = 0.15f), + RoundedCornerShape(corners.small) + ) + .background(MorpheColors.Teal.copy(alpha = 0.03f)) + .padding(12.dp) ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = MorpheColors.Teal, - modifier = Modifier.size(18.dp) - ) - Text( - text = "Strip native libraries", - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - } + Box( + modifier = Modifier + .size(6.dp) + .background(MorpheColors.Teal, RoundedCornerShape(1.dp)) + ) + Text( + text = "STRIP NATIVE LIBRARIES", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 1.sp + ) + } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "Uncheck architectures to remove from the output APK and reduce file size.", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + + if (deviceArch != null) { + Spacer(modifier = Modifier.height(2.dp)) Text( - text = "Uncheck architectures to remove from the output APK and reduce file size.", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Your device: $deviceArch", + fontSize = 10.sp, + fontWeight = FontWeight.Medium, + fontFamily = mono, + color = MorpheColors.Teal.copy(alpha = 0.8f) ) + } - if (deviceArch != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = "Your device: $deviceArch", - fontSize = 11.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal - ) - } + Spacer(modifier = Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(8.dp)) + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + architectures.forEach { arch -> + val isSelected = selectedArchitectures.contains(arch) + val archHover = remember { MutableInteractionSource() } + val isArchHovered by archHover.collectIsHoveredAsState() + val archBorder by animateColorAsState( + when { + isSelected -> MorpheColors.Teal.copy(alpha = 0.4f) + isArchHovered -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.1f) + }, + animationSpec = tween(150) + ) - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - architectures.forEach { arch -> - val isSelected = selectedArchitectures.contains(arch) - val archInteractionSource = remember { MutableInteractionSource() } - val archHovered by archInteractionSource.collectIsHoveredAsState() - Surface( - modifier = Modifier - .hoverable(archInteractionSource) - .clickable(interactionSource = archInteractionSource, indication = null) { - onToggleArchitecture(arch) - }, - shape = RoundedCornerShape(8.dp), - color = if (isSelected) MorpheColors.Teal.copy(alpha = if (archHovered) 0.28f else 0.2f) - else if (archHovered) MorpheColors.Teal.copy(alpha = 0.1f) - else Color.Transparent, - border = BorderStroke( - width = 0.5.dp, - color = if (isSelected) MorpheColors.Teal.copy(alpha = 0.5f) - else MaterialTheme.colorScheme.outline.copy(alpha = 0.1f) + Box( + modifier = Modifier + .hoverable(archHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, archBorder, RoundedCornerShape(corners.small)) + .then( + if (isSelected) Modifier.background( + MorpheColors.Teal.copy(alpha = 0.08f), + RoundedCornerShape(corners.small) + ) else Modifier ) + .clickable { onToggleArchitecture(arch) } + .padding(horizontal = 10.dp, vertical = 6.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Row( - modifier = Modifier.padding(horizontal = 10.dp, vertical = 6.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(6.dp) - ) { - Box( - modifier = Modifier - .size(6.dp) - .clip(CircleShape) - .background( - if (isSelected) MorpheColors.Teal - else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f) - ) - ) - Text( - text = arch, - fontSize = 12.sp, - color = if (isSelected) MorpheColors.Teal else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) - ) - } + Box( + modifier = Modifier + .size(6.dp) + .background( + if (isSelected) MorpheColors.Teal + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.2f), + RoundedCornerShape(1.dp) + ) + ) + Text( + text = arch, + fontSize = 11.sp, + fontFamily = mono, + fontWeight = FontWeight.Medium, + color = if (isSelected) MorpheColors.Teal + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 44b632f..5123088 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -121,12 +121,14 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { animationSpec = tween(150) ) - IconButton( - onClick = { navigator.pop() }, + Box( modifier = Modifier .size(34.dp) .hoverable(backHover) + .clip(RoundedCornerShape(corners.small)) .border(1.dp, backBorder, RoundedCornerShape(corners.small)) + .clickable { navigator.pop() }, + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, @@ -171,13 +173,17 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { ) if (!uiState.isLocalSource) { - IconButton( - onClick = { viewModel.loadReleases() }, - enabled = !uiState.isLoading, + Box( modifier = Modifier .size(34.dp) .hoverable(refreshHover) + .clip(RoundedCornerShape(corners.small)) .border(1.dp, refreshBorder, RoundedCornerShape(corners.small)) + .then( + if (!uiState.isLoading) Modifier.clickable { viewModel.loadReleases() } + else Modifier + ), + contentAlignment = Alignment.Center ) { Icon( imageVector = Icons.Default.Refresh, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index cdd32de..4a3ec53 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -1,29 +1,23 @@ package app.morphe.gui.ui.screens.quick import androidx.compose.animation.* -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable -import androidx.compose.foundation.draganddrop.dragAndDropTarget +import androidx.compose.animation.core.tween +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.OpenInNew import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier -import androidx.compose.ui.draganddrop.DragAndDropEvent -import androidx.compose.ui.draganddrop.DragAndDropTarget -import androidx.compose.ui.draganddrop.awtTransferable import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign @@ -31,106 +25,56 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen -import androidx.compose.foundation.isSystemInDarkTheme import app.morphe.morphe_cli.generated.resources.Res import app.morphe.morphe_cli.generated.resources.morphe_dark import app.morphe.morphe_cli.generated.resources.morphe_light -import app.morphe.gui.ui.theme.LocalThemeState -import app.morphe.gui.ui.theme.ThemePreference +import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchSourceManager -import app.morphe.gui.util.PatchService -import org.jetbrains.compose.resources.painterResource -import org.koin.compose.koinInject import app.morphe.gui.ui.components.OfflineBanner import app.morphe.gui.ui.components.TopBarRow -import app.morphe.gui.ui.theme.MorpheColors -import androidx.compose.runtime.rememberCoroutineScope +import app.morphe.gui.ui.screens.home.components.FullScreenDropZone +import app.morphe.gui.ui.theme.* +import app.morphe.gui.util.ChecksumStatus +import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects +import app.morphe.gui.util.PatchService import app.morphe.gui.util.AdbManager import app.morphe.gui.util.DeviceMonitor import kotlinx.coroutines.launch -import app.morphe.gui.util.ChecksumStatus -import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects +import org.jetbrains.compose.resources.painterResource +import org.koin.compose.koinInject import java.awt.Desktop -import java.awt.datatransfer.DataFlavor -import java.io.File import java.awt.FileDialog import java.awt.Frame +import java.io.File -/** - * Quick Patch Mode - Single screen simplified patching. - */ class QuickPatchScreen : Screen { @Composable override fun Content() { val patchSourceManager: PatchSourceManager = koinInject() val patchService: PatchService = koinInject() val configRepository: ConfigRepository = koinInject() - val viewModel = remember { QuickPatchViewModel(patchSourceManager, patchService, configRepository) } - QuickPatchContent(viewModel) } } -@OptIn(ExperimentalComposeUiApi::class) @Composable fun QuickPatchContent(viewModel: QuickPatchViewModel) { val uiState by viewModel.uiState.collectAsState() - val uriHandler = LocalUriHandler.current - - // Compose drag and drop target - val dragAndDropTarget = remember { - object : DragAndDropTarget { - override fun onStarted(event: DragAndDropEvent) { - viewModel.setDragHover(true) - } - - override fun onEnded(event: DragAndDropEvent) { - viewModel.setDragHover(false) - } - override fun onExited(event: DragAndDropEvent) { - viewModel.setDragHover(false) - } - - override fun onEntered(event: DragAndDropEvent) { - viewModel.setDragHover(true) - } - - override fun onDrop(event: DragAndDropEvent): Boolean { - viewModel.setDragHover(false) - val transferable = event.awtTransferable - return try { - if (transferable.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { - @Suppress("UNCHECKED_CAST") - val files = transferable.getTransferData(DataFlavor.javaFileListFlavor) as List - val apkFile = files.firstOrNull { it.name.endsWith(".apk", ignoreCase = true) || it.name.endsWith(".apkm", ignoreCase = true) } - if (apkFile != null) { - viewModel.onFileSelected(apkFile) - true - } else { - false - } - } else { - false - } - } catch (e: Exception) { - false - } - } - } - } - - Box( - modifier = Modifier - .fillMaxSize() - .dragAndDropTarget( - shouldStartDragAndDrop = { true }, - target = dragAndDropTarget - ) + FullScreenDropZone( + isDragHovering = uiState.isDragHovering, + onDragHoverChange = { viewModel.setDragHover(it) }, + onFilesDropped = { files -> + files.firstOrNull { + it.name.endsWith(".apk", ignoreCase = true) || + it.name.endsWith(".apkm", ignoreCase = true) + }?.let { viewModel.onFileSelected(it) } + }, + enabled = uiState.phase != QuickPatchPhase.ANALYZING ) { Box(modifier = Modifier.fillMaxSize()) { Column( @@ -139,23 +83,9 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { .padding(24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // Branding + // ── Branding ── Spacer(modifier = Modifier.height(8.dp)) - val themeState = LocalThemeState.current - val isDark = when (themeState.current) { - ThemePreference.SYSTEM -> isSystemInDarkTheme() - else -> themeState.current.isDark() - } - Image( - painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), - contentDescription = "Morphe Logo", - modifier = Modifier.height(48.dp) - ) - Text( - text = "Quick Patch", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + BrandingHeader(patchesVersion = uiState.patchesVersion, isLoading = uiState.isLoadingPatches) Spacer(modifier = Modifier.height(16.dp)) @@ -167,36 +97,32 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ) } - // Main content based on phase - // Remember last valid data for safe animation transitions + // ── Main content ── val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } val lastOutputPath = remember(uiState.outputPath) { uiState.outputPath } AnimatedContent( targetState = uiState.phase, - modifier = Modifier.weight(1f) + modifier = Modifier.weight(1f), + transitionSpec = { + fadeIn(tween(200)) togetherWith fadeOut(tween(200)) + } ) { phase -> when (phase) { QuickPatchPhase.IDLE, QuickPatchPhase.ANALYZING -> { IdleContent( isAnalyzing = phase == QuickPatchPhase.ANALYZING, isDragHovering = uiState.isDragHovering, - error = uiState.error, - onFileSelected = { viewModel.onFileSelected(it) }, - onDragHover = { viewModel.setDragHover(it) }, - onClearError = { viewModel.clearError() } + onBrowse = { openFilePicker()?.let { viewModel.onFileSelected(it) } } ) } QuickPatchPhase.READY -> { - // Use current or last known apkInfo to prevent crash during animation - val apkInfo = uiState.apkInfo ?: lastApkInfo - if (apkInfo != null) { + val info = uiState.apkInfo ?: lastApkInfo + if (info != null) { ReadyContent( - apkInfo = apkInfo, - error = uiState.error, + apkInfo = info, onPatch = { viewModel.startPatching() }, - onClear = { viewModel.reset() }, - onClearError = { viewModel.clearError() } + onClear = { viewModel.reset() } ) } } @@ -208,12 +134,12 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ) } QuickPatchPhase.COMPLETED -> { - val apkInfo = uiState.apkInfo ?: lastApkInfo - val outputPath = uiState.outputPath ?: lastOutputPath - if (apkInfo != null && outputPath != null) { + val info = uiState.apkInfo ?: lastApkInfo + val output = uiState.outputPath ?: lastOutputPath + if (info != null && output != null) { CompletedContent( - outputPath = outputPath, - apkInfo = apkInfo, + outputPath = output, + apkInfo = info, onPatchAnother = { viewModel.reset() } ) } @@ -221,32 +147,31 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { } } - // Bottom app cards (only show in IDLE phase) + // ── Supported apps (idle only) ── if (uiState.phase == QuickPatchPhase.IDLE) { Spacer(modifier = Modifier.height(16.dp)) SupportedAppsRow( supportedApps = uiState.supportedApps, isLoading = uiState.isLoadingPatches, loadError = uiState.patchLoadError, - patchesVersion = uiState.patchesVersion, isDefaultSource = uiState.isDefaultSource, - onOpenUrl = { url -> - openUrlAndFollowRedirects(url) { urlResolved -> - uriHandler.openUri(urlResolved) - } - }, onRetry = { viewModel.retryLoadPatches() } ) } } - // Top bar (device indicator + settings) in top-right corner + // Top-right: device indicator + settings TopBarRow( modifier = Modifier .align(Alignment.TopEnd) .padding(24.dp) ) + // Drag overlay + if (uiState.isDragHovering) { + DragOverlay() + } + // Error snackbar uiState.error?.let { error -> Snackbar( @@ -259,7 +184,8 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { } }, containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer + contentColor = MaterialTheme.colorScheme.onErrorContainer, + shape = RoundedCornerShape(LocalMorpheCorners.current.small) ) { Text(error) } @@ -268,95 +194,165 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { } } +// ════════════════════════════════════════════════════════════════════ +// BRANDING — Logo + patches version badge +// ════════════════════════════════════════════════════════════════════ + +@Composable +private fun BrandingHeader(patchesVersion: String?, isLoading: Boolean) { + val themeState = LocalThemeState.current + val isDark = when (themeState.current) { + ThemePreference.SYSTEM -> isSystemInDarkTheme() + else -> themeState.current.isDark() + } + + Image( + painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), + contentDescription = "Morphe Logo", + modifier = Modifier.height(48.dp) + ) + + Spacer(modifier = Modifier.height(4.dp)) + + if (isLoading) { + Row(verticalAlignment = Alignment.CenterVertically) { + CircularProgressIndicator( + modifier = Modifier.size(12.dp), + strokeWidth = 1.5.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Loading patches…", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + } else if (patchesVersion != null) { + Text( + text = "Patches $patchesVersion", + fontSize = 12.sp, + color = MorpheColors.Blue.copy(alpha = 0.8f), + fontWeight = FontWeight.Medium + ) + } else { + Text( + text = "Quick Patch", + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +} + +// ════════════════════════════════════════════════════════════════════ +// IDLE — Simple drop zone +// ════════════════════════════════════════════════════════════════════ + @Composable private fun IdleContent( isAnalyzing: Boolean, isDragHovering: Boolean, - error: String?, - onFileSelected: (File) -> Unit, - onDragHover: (Boolean) -> Unit, - onClearError: () -> Unit + onBrowse: () -> Unit ) { - val dropZoneColor = when { - isDragHovering -> MorpheColors.Blue.copy(alpha = 0.2f) - else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - } - - val borderColor = when { - isDragHovering -> MorpheColors.Blue - else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - } + val corners = LocalMorpheCorners.current + val bracketColor = if (isDragHovering) MorpheColors.Blue.copy(alpha = 0.7f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f) Box( modifier = Modifier .fillMaxSize() - .clip(RoundedCornerShape(16.dp)) - .background(dropZoneColor) - .border(2.dp, borderColor, RoundedCornerShape(16.dp)) - .clickable(enabled = !isAnalyzing) { - openFilePicker()?.let { onFileSelected(it) } + .clickable(enabled = !isAnalyzing) { onBrowse() } + .drawBehind { + val strokeWidth = 2f + val len = 32.dp.toPx() + val inset = 0f + + // Top-left + drawLine(bracketColor, Offset(inset, inset), Offset(inset + len, inset), strokeWidth) + drawLine(bracketColor, Offset(inset, inset), Offset(inset, inset + len), strokeWidth) + // Top-right + drawLine(bracketColor, Offset(size.width - inset, inset), Offset(size.width - inset - len, inset), strokeWidth) + drawLine(bracketColor, Offset(size.width - inset, inset), Offset(size.width - inset, inset + len), strokeWidth) + // Bottom-left + drawLine(bracketColor, Offset(inset, size.height - inset), Offset(inset + len, size.height - inset), strokeWidth) + drawLine(bracketColor, Offset(inset, size.height - inset), Offset(inset, size.height - inset - len), strokeWidth) + // Bottom-right + drawLine(bracketColor, Offset(size.width - inset, size.height - inset), Offset(size.width - inset - len, size.height - inset), strokeWidth) + drawLine(bracketColor, Offset(size.width - inset, size.height - inset), Offset(size.width - inset, size.height - inset - len), strokeWidth) }, contentAlignment = Alignment.Center ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally - ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { if (isAnalyzing) { CircularProgressIndicator( - modifier = Modifier.size(48.dp), + modifier = Modifier.size(40.dp), color = MorpheColors.Blue, strokeWidth = 3.dp ) Spacer(modifier = Modifier.height(16.dp)) Text( - text = "Analyzing APK...", - fontSize = 16.sp, + text = "Analyzing APK…", + fontSize = 15.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) } else { Icon( imageVector = Icons.Default.CloudUpload, contentDescription = null, - modifier = Modifier.size(48.dp), - tint = if (isDragHovering) MorpheColors.Blue else MaterialTheme.colorScheme.onSurfaceVariant + modifier = Modifier.size(44.dp), + tint = if (isDragHovering) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) Spacer(modifier = Modifier.height(16.dp)) Text( text = "Drop APK here", - fontSize = 18.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = if (isDragHovering) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurface ) Spacer(modifier = Modifier.height(4.dp)) Text( text = "or click to browse", - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + Spacer(modifier = Modifier.height(6.dp)) + Text( + text = ".apk · .apkm", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) ) } } } } +// ════════════════════════════════════════════════════════════════════ +// READY — Compact APK card + patch button +// ════════════════════════════════════════════════════════════════════ + @Composable private fun ReadyContent( apkInfo: QuickApkInfo, - error: String?, onPatch: () -> Unit, - onClear: () -> Unit, - onClearError: () -> Unit + onClear: () -> Unit ) { + val corners = LocalMorpheCorners.current + Column( modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - // APK Info Card - Card( + Spacer(modifier = Modifier.weight(1f)) + + // Simple APK info card + Surface( modifier = Modifier.fillMaxWidth(), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ), - shape = RoundedCornerShape(12.dp) + shape = RoundedCornerShape(corners.medium), + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.15f)) ) { Row( modifier = Modifier @@ -364,54 +360,56 @@ private fun ReadyContent( .padding(16.dp), verticalAlignment = Alignment.CenterVertically ) { - // App icon: first letter of display name + // App initial Box( modifier = Modifier - .size(48.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color.White), + .size(44.dp) + .clip(RoundedCornerShape(corners.small)) + .background(MorpheColors.Blue.copy(alpha = 0.1f)), contentAlignment = Alignment.Center ) { Text( - text = apkInfo.displayName.first().toString(), - fontSize = 20.sp, + text = apkInfo.displayName.first().uppercase(), + fontSize = 18.sp, fontWeight = FontWeight.Bold, color = MorpheColors.Blue ) } - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(14.dp)) Column(modifier = Modifier.weight(1f)) { Text( text = apkInfo.displayName, fontSize = 16.sp, fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis ) Text( - text = "v${apkInfo.versionName} • ${apkInfo.formattedSize}", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "v${apkInfo.versionName} · ${apkInfo.formattedSize}", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) } - // Checksum status + // Checksum badge when (apkInfo.checksumStatus) { is ChecksumStatus.Verified -> { Icon( imageVector = Icons.Default.VerifiedUser, contentDescription = "Verified", tint = MorpheColors.Teal, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(20.dp) ) } is ChecksumStatus.Mismatch -> { Icon( imageVector = Icons.Default.Warning, - contentDescription = "Checksum mismatch", + contentDescription = "Mismatch", tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(20.dp) ) } else -> {} @@ -419,38 +417,57 @@ private fun ReadyContent( Spacer(modifier = Modifier.width(8.dp)) - IconButton(onClick = onClear) { + IconButton(onClick = onClear, modifier = Modifier.size(32.dp)) { Icon( imageVector = Icons.Default.Close, contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(16.dp) ) } } } - Spacer(modifier = Modifier.height(8.dp)) + // Version status (only if noteworthy) + val statusText = when { + apkInfo.checksumStatus is ChecksumStatus.Verified -> + "Recommended version · Verified" + apkInfo.checksumStatus is ChecksumStatus.Mismatch -> + "Checksum mismatch — re-download from APKMirror" + !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> + "Recommended: v${apkInfo.recommendedVersion}" + apkInfo.isRecommendedVersion -> + "Recommended version" + else -> null + } + val statusColor = when { + apkInfo.checksumStatus is ChecksumStatus.Verified -> MorpheColors.Teal + apkInfo.checksumStatus is ChecksumStatus.Mismatch -> MaterialTheme.colorScheme.error + !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> Color(0xFFFF9800) + else -> MorpheColors.Teal + } - // Verification status banner - VerificationStatusBanner( - checksumStatus = apkInfo.checksumStatus, - isRecommendedVersion = apkInfo.isRecommendedVersion, - currentVersion = apkInfo.versionName, - suggestedVersion = apkInfo.recommendedVersion ?: "Unknown" - ) + if (statusText != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = statusText, + fontSize = 12.sp, + fontWeight = FontWeight.Medium, + color = statusColor, + textAlign = TextAlign.Center + ) + } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.height(20.dp)) // Patch button Button( onClick = onPatch, modifier = Modifier .fillMaxWidth() - .height(52.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) + .height(50.dp), + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), + shape = RoundedCornerShape(corners.medium) ) { Icon( imageVector = Icons.Default.AutoFixHigh, @@ -460,22 +477,28 @@ private fun ReadyContent( Spacer(modifier = Modifier.width(8.dp)) Text( text = "Patch with Defaults", - fontSize = 16.sp, - fontWeight = FontWeight.Medium + fontSize = 15.sp, + fontWeight = FontWeight.SemiBold ) } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(6.dp)) Text( text = "Uses latest patches with recommended settings", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), textAlign = TextAlign.Center ) + + Spacer(modifier = Modifier.weight(1f)) } } +// ════════════════════════════════════════════════════════════════════ +// PATCHING — Progress +// ════════════════════════════════════════════════════════════════════ + @Composable private fun PatchingContent( phase: QuickPatchPhase, @@ -488,8 +511,8 @@ private fun PatchingContent( verticalArrangement = Arrangement.Center ) { CircularProgressIndicator( - modifier = Modifier.size(64.dp), - strokeWidth = 4.dp, + modifier = Modifier.size(56.dp), + strokeWidth = 3.dp, color = MorpheColors.Teal ) @@ -497,12 +520,12 @@ private fun PatchingContent( Text( text = when (phase) { - QuickPatchPhase.DOWNLOADING -> "Preparing..." - QuickPatchPhase.PATCHING -> "Patching..." + QuickPatchPhase.DOWNLOADING -> "Preparing…" + QuickPatchPhase.PATCHING -> "Patching…" else -> "" }, - fontSize = 18.sp, - fontWeight = FontWeight.Medium, + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface ) @@ -510,11 +533,12 @@ private fun PatchingContent( Text( text = statusMessage, - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + maxLines = 2, overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 16.dp) + textAlign = TextAlign.Center, + modifier = Modifier.padding(horizontal = 24.dp) ) Spacer(modifier = Modifier.height(24.dp)) @@ -525,12 +549,17 @@ private fun PatchingContent( } } +// ════════════════════════════════════════════════════════════════════ +// COMPLETED — Success +// ════════════════════════════════════════════════════════════════════ + @Composable private fun CompletedContent( outputPath: String, apkInfo: QuickApkInfo, onPatchAnother: () -> Unit ) { + val corners = LocalMorpheCorners.current val outputFile = File(outputPath) val scope = rememberCoroutineScope() val adbManager = remember { AdbManager() } @@ -550,14 +579,14 @@ private fun CompletedContent( imageVector = Icons.Default.CheckCircle, contentDescription = "Success", tint = MorpheColors.Teal, - modifier = Modifier.size(64.dp) + modifier = Modifier.size(56.dp) ) Spacer(modifier = Modifier.height(16.dp)) Text( text = "Patching Complete!", - fontSize = 22.sp, + fontSize = 20.sp, fontWeight = FontWeight.Bold, color = MaterialTheme.colorScheme.onSurface ) @@ -566,15 +595,19 @@ private fun CompletedContent( Text( text = outputFile.name, - fontSize = 14.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center + fontSize = 13.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + textAlign = TextAlign.Center, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 16.dp) ) if (outputFile.exists()) { Text( text = formatFileSize(outputFile.length()), fontSize = 13.sp, + fontWeight = FontWeight.Medium, color = MorpheColors.Teal ) } @@ -582,9 +615,7 @@ private fun CompletedContent( Spacer(modifier = Modifier.height(24.dp)) // Action buttons - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { + Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { OutlinedButton( onClick = { try { @@ -592,30 +623,25 @@ private fun CompletedContent( if (folder != null && Desktop.isDesktopSupported()) { Desktop.getDesktop().open(folder) } - } catch (e: Exception) { } + } catch (_: Exception) {} }, - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(corners.small) ) { - Icon( - imageVector = Icons.Default.FolderOpen, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + Icon(Icons.Default.FolderOpen, null, modifier = Modifier.size(18.dp)) Spacer(modifier = Modifier.width(6.dp)) Text("Open Folder") } Button( onClick = onPatchAnother, - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Blue - ), - shape = RoundedCornerShape(8.dp) + colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), + shape = RoundedCornerShape(corners.small) ) { Text("Patch Another") } } + // ADB install if (monitorState.isAdbAvailable == true) { Spacer(modifier = Modifier.height(16.dp)) @@ -625,7 +651,7 @@ private fun CompletedContent( if (installSuccess) { Surface( color = MorpheColors.Teal.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(corners.small) ) { Text( text = "Installed successfully!", @@ -647,7 +673,7 @@ private fun CompletedContent( ) Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Installing...", + text = "Installing…", fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) @@ -671,13 +697,9 @@ private fun CompletedContent( } }, colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Teal), - shape = RoundedCornerShape(8.dp) + shape = RoundedCornerShape(corners.small) ) { - Icon( - imageVector = Icons.Default.PhoneAndroid, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) + Icon(Icons.Default.PhoneAndroid, null, modifier = Modifier.size(18.dp)) Spacer(modifier = Modifier.width(6.dp)) Text("Install on ${device.displayName}") } @@ -685,7 +707,7 @@ private fun CompletedContent( Text( text = "Connect your device via USB to install with ADB", fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) } @@ -702,19 +724,24 @@ private fun CompletedContent( } } +// ════════════════════════════════════════════════════════════════════ +// SUPPORTED APPS — Simple row at the bottom +// ════════════════════════════════════════════════════════════════════ + @Composable private fun SupportedAppsRow( - supportedApps: List, + supportedApps: List, isLoading: Boolean, loadError: String? = null, - patchesVersion: String?, isDefaultSource: Boolean = true, - onOpenUrl: (String) -> Unit, onRetry: () -> Unit = {} ) { - Column( - modifier = Modifier.fillMaxWidth() - ) { + val corners = LocalMorpheCorners.current + val uriHandler = LocalUriHandler.current + val focusManager = LocalFocusManager.current + + Column(modifier = Modifier.fillMaxWidth()) { + // Header Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -723,207 +750,174 @@ private fun SupportedAppsRow( Text( text = if (isDefaultSource) "Download original APK" else "Supported apps", fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) ) - if (patchesVersion != null) { - Text( - text = "Patches: $patchesVersion", - fontSize = 11.sp, - color = MorpheColors.Blue.copy(alpha = 0.8f) - ) - } } Spacer(modifier = Modifier.height(8.dp)) - if (isLoading) { - // Loading state - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MorpheColors.Blue - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Loading supported apps...", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } else if (loadError != null || supportedApps.isEmpty()) { - // Error or no apps loaded - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = loadError ?: "Could not load supported apps", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.width(8.dp)) - OutlinedButton( - onClick = onRetry, - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + when { + isLoading -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically ) { - Text("Retry", fontSize = 12.sp) + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "Loading supported apps…", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) } } - } else { - // Search bar for many apps - val focusManager = androidx.compose.ui.platform.LocalFocusManager.current - var searchQuery by remember { mutableStateOf("") } - val filteredApps = if (searchQuery.isBlank()) supportedApps - else supportedApps.filter { - it.displayName.contains(searchQuery, ignoreCase = true) || - it.packageName.contains(searchQuery, ignoreCase = true) - } - - if (supportedApps.size > 4) { - OutlinedTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, - placeholder = { - Text( - "Search apps...", - style = MaterialTheme.typography.bodySmall - ) - }, - leadingIcon = { - Icon( - Icons.Default.Search, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp) - ) - }, - trailingIcon = { - if (searchQuery.isNotEmpty()) { - IconButton(onClick = { searchQuery = "" }) { - Icon( - Icons.Default.Clear, - contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(14.dp) - ) - } - } - }, - singleLine = true, - textStyle = MaterialTheme.typography.bodySmall, - shape = RoundedCornerShape(8.dp), - modifier = Modifier - .fillMaxWidth() - .height(44.dp), - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MorpheColors.Blue, - unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + loadError != null || supportedApps.isEmpty() -> { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = loadError ?: "Could not load supported apps", + fontSize = 12.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant ) - ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.width(8.dp)) + OutlinedButton( + onClick = onRetry, + shape = RoundedCornerShape(corners.small), + contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + ) { + Text("Retry", fontSize = 12.sp) + } + } } + else -> { + // Search bar for many apps + var searchQuery by remember { mutableStateOf("") } + val filteredApps = if (searchQuery.isBlank()) supportedApps + else supportedApps.filter { + it.displayName.contains(searchQuery, ignoreCase = true) || + it.packageName.contains(searchQuery, ignoreCase = true) + } - if (isDefaultSource) { - // Default source: show clickable download cards with fixed width - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .height(IntrinsicSize.Max) - .clickable( - interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, - indication = null - ) { focusManager.clearFocus() }, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - filteredApps.forEach { app -> - val url = app.apkDownloadUrl - if (url != null) { - OutlinedCard( - onClick = { onOpenUrl(url) }, - modifier = Modifier.width(180.dp).fillMaxHeight(), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = app.displayName, - fontSize = 13.sp, - fontWeight = FontWeight.Medium - ) - app.recommendedVersion?.let { version -> - Text( - text = "v$version", - fontSize = 10.sp, - color = MorpheColors.Teal - ) - } - } + if (supportedApps.size > 4) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + placeholder = { + Text("Search apps…", style = MaterialTheme.typography.bodySmall) + }, + leadingIcon = { + Icon( + Icons.Default.Search, null, + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(16.dp) + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { searchQuery = "" }) { Icon( - imageVector = Icons.AutoMirrored.Filled.OpenInNew, - contentDescription = "Open", + Icons.Default.Clear, "Clear", tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(16.dp) + modifier = Modifier.size(14.dp) ) } } - } - } + }, + singleLine = true, + textStyle = MaterialTheme.typography.bodySmall, + shape = RoundedCornerShape(corners.small), + modifier = Modifier.fillMaxWidth(), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MorpheColors.Blue, + unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + ) + ) + Spacer(modifier = Modifier.height(8.dp)) } - } else { - // Custom source: show app names and versions in a scrollable row + + // Horizontal scrolling cards Row( modifier = Modifier .fillMaxWidth() .horizontalScroll(rememberScrollState()) .height(IntrinsicSize.Max) .clickable( - interactionSource = remember { androidx.compose.foundation.interaction.MutableInteractionSource() }, + interactionSource = remember { MutableInteractionSource() }, indication = null ) { focusManager.clearFocus() }, - horizontalArrangement = Arrangement.spacedBy(8.dp) + horizontalArrangement = Arrangement.spacedBy(10.dp) ) { filteredApps.forEach { app -> - OutlinedCard( - modifier = Modifier.width(160.dp).fillMaxHeight(), - shape = RoundedCornerShape(8.dp) + val url = app.apkDownloadUrl + val hoverInteraction = remember { MutableInteractionSource() } + val isHovered by hoverInteraction.collectIsHoveredAsState() + val borderColor by animateColorAsState( + if (isHovered) MorpheColors.Blue.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.outline.copy(alpha = 0.12f), + animationSpec = tween(200) + ) + + Surface( + modifier = Modifier + .width(170.dp) + .fillMaxHeight() + .hoverable(hoverInteraction) + .then( + if (isDefaultSource && url != null) { + Modifier.clickable { + openUrlAndFollowRedirects(url) { resolved -> + uriHandler.openUri(resolved) + } + } + } else Modifier + ), + shape = RoundedCornerShape(corners.small), + color = MaterialTheme.colorScheme.surface, + border = BorderStroke(1.dp, borderColor) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(12.dp) + .padding(12.dp), + horizontalAlignment = Alignment.CenterHorizontally ) { Text( text = app.displayName, fontSize = 13.sp, - fontWeight = FontWeight.Medium, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = app.packageName, - fontSize = 9.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + fontWeight = FontWeight.SemiBold, maxLines = 1, overflow = TextOverflow.Ellipsis ) - app.recommendedVersion?.let { version -> + if (app.recommendedVersion != null) { + Text( + text = "v${app.recommendedVersion}", + fontSize = 11.sp, + color = MorpheColors.Teal, + fontWeight = FontWeight.Medium + ) + } else { + Text( + text = "Any version", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + if (isDefaultSource && url != null) { + Spacer(modifier = Modifier.height(4.dp)) Text( - text = "v$version", + text = "Download ↗", fontSize = 10.sp, - color = MorpheColors.Teal + color = MorpheColors.Blue.copy(alpha = 0.7f), + fontWeight = FontWeight.Medium ) } } @@ -935,199 +929,54 @@ private fun SupportedAppsRow( } } -/** - * Shows verification status (version + checksum) in a compact banner. - */ -@Composable -private fun VerificationStatusBanner( - checksumStatus: ChecksumStatus, - isRecommendedVersion: Boolean, - currentVersion: String, - suggestedVersion: String -) { - when { - // Recommended version with verified checksum - checksumStatus is ChecksumStatus.Verified -> { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MorpheColors.Teal.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Default.VerifiedUser, - contentDescription = null, - tint = MorpheColors.Teal, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text( - text = "Recommended version • Verified", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal - ) - Text( - text = "Checksum matches APKMirror", - fontSize = 11.sp, - color = MorpheColors.Teal.copy(alpha = 0.8f) - ) - } - } - } - } - - // Checksum mismatch - warning - checksumStatus is ChecksumStatus.Mismatch -> { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.error.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Warning, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text( - text = "Checksum mismatch", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.error - ) - Text( - text = "File may be corrupted. Re-download from APKMirror.", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.error.copy(alpha = 0.8f) - ) - } - } - } - } - - // Recommended version but no checksum configured - isRecommendedVersion && checksumStatus is ChecksumStatus.NotConfigured -> { - Surface( - modifier = Modifier.fillMaxWidth(), - color = MorpheColors.Teal.copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = MorpheColors.Teal, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Using recommended version", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal - ) - } - } - } - - // Non-recommended version (older or newer) - !isRecommendedVersion -> { - Surface( - modifier = Modifier.fillMaxWidth(), - color = Color(0xFFFF9800).copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Icon( - imageVector = Icons.Default.Info, - contentDescription = null, - tint = Color(0xFFFF9800), - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Column { - Text( - text = "Version $currentVersion", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = Color(0xFFFF9800) - ) - Text( - text = "Recommended: v$suggestedVersion. Patching may have issues.", - fontSize = 11.sp, - color = Color(0xFFFF9800).copy(alpha = 0.8f) - ) - } - } - } - } +// ════════════════════════════════════════════════════════════════════ +// DRAG OVERLAY +// ════════════════════════════════════════════════════════════════════ - // Checksum error - checksumStatus is ChecksumStatus.Error -> { - Surface( - modifier = Modifier.fillMaxWidth(), - color = Color(0xFFFF9800).copy(alpha = 0.1f), - shape = RoundedCornerShape(8.dp) - ) { - Row( - modifier = Modifier.padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = Color(0xFFFF9800), - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = "Recommended version (checksum unavailable)", - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = Color(0xFFFF9800) - ) - } - } +@Composable +private fun DragOverlay() { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background.copy(alpha = 0.92f)) + .border( + width = 2.dp, + color = MorpheColors.Blue.copy(alpha = 0.5f), + shape = RoundedCornerShape(0.dp) + ), + contentAlignment = Alignment.Center + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Icon( + imageVector = Icons.Default.CloudUpload, + contentDescription = null, + modifier = Modifier.size(48.dp), + tint = MorpheColors.Blue + ) + Spacer(modifier = Modifier.height(12.dp)) + Text( + text = "Drop APK here", + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + color = MorpheColors.Blue + ) } } } -/** - * Open native file picker. - */ +// ════════════════════════════════════════════════════════════════════ +// UTILITIES +// ════════════════════════════════════════════════════════════════════ + private fun openFilePicker(): File? { val fileDialog = FileDialog(null as Frame?, "Select APK", FileDialog.LOAD).apply { isMultipleMode = false setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") } } isVisible = true } - val directory = fileDialog.directory val file = fileDialog.file - - return if (directory != null && file != null) { - File(directory, file) - } else null + return if (directory != null && file != null) File(directory, file) else null } private fun formatFileSize(bytes: Long): String { From bcea7dd6cf5b4192ddc8f31f3b615500d783a4f5 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:38:25 +0530 Subject: [PATCH 36/49] Major Update Changed vendor from adoptium to Jetbrains for better window handling. Major UI Improvements. --- build.gradle.kts | 5 +- gradle/libs.versions.toml | 6 + src/main/kotlin/app/morphe/gui/App.kt | 32 +- src/main/kotlin/app/morphe/gui/GuiMain.kt | 39 +- .../gui/ui/components/CustomTitleBar.kt | 157 --- .../gui/ui/components/DraggableHeaderArea.kt | 28 + .../gui/ui/components/TitleBarInsets.kt | 25 + .../morphe/gui/ui/screens/home/HomeScreen.kt | 70 +- .../ui/screens/home/components/ApkInfoCard.kt | 41 +- .../screens/patches/PatchSelectionScreen.kt | 11 +- .../gui/ui/screens/patches/PatchesScreen.kt | 9 +- .../gui/ui/screens/patching/PatchingScreen.kt | 745 +++++++----- .../gui/ui/screens/quick/QuickPatchScreen.kt | 7 +- .../gui/ui/screens/result/ResultScreen.kt | 1024 ++++++++++------- 14 files changed, 1259 insertions(+), 940 deletions(-) delete mode 100644 src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt create mode 100644 src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt diff --git a/build.gradle.kts b/build.gradle.kts index 421907a..b7984be 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,7 +19,7 @@ group = "app.morphe" kotlin { jvmToolchain { languageVersion.set(JavaLanguageVersion.of(17)) - vendor.set(JvmVendorSpec.ADOPTIUM) + vendor.set(JvmVendorSpec.JETBRAINS) } compilerOptions { jvmTarget.set(JvmTarget.JVM_17) @@ -125,6 +125,9 @@ dependencies { implementation(libs.voyager.koin) implementation(libs.voyager.transitions) + // -- JBR API (macOS title bar customization) ---------------------------- + implementation(libs.jbr.api) + // -- APK Parsing (GUI) ------------------------------------------------- implementation(libs.apk.parser) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 9ca5acf..440d239 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,9 @@ voyager = "1.1.0-beta03" coroutines = "1.10.2" kotlinx-serialization = "1.9.0" +# JBR +jbr-api = "1.5.0" + # APK apk-parser = "2.6.10" arsclib = "1.3.8" @@ -64,6 +67,9 @@ kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines- # Serialization kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +# JBR +jbr-api = { module = "org.jetbrains.runtime:jbr-api", version.ref = "jbr-api" } + # APK apk-parser = { module = "net.dongliu:apk-parser", version.ref = "apk-parser" } arsclib = { module = "io.github.reandroid:ARSCLib", version.ref = "arsclib" } diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index d4bb6c4..339be75 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -8,10 +8,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.FrameWindowScope -import app.morphe.gui.ui.components.CustomTitleBar +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.LottieAnimation import app.morphe.gui.ui.components.SakuraPetals +import app.morphe.gui.ui.components.TitleBarInsets import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.transitions.SlideTransition import app.morphe.gui.data.repository.ConfigRepository @@ -44,7 +44,7 @@ val LocalModeState = staticCompositionLocalOf { } @Composable -fun App(initialSimplifiedMode: Boolean = true, frameWindowScope: FrameWindowScope? = null) { +fun App(initialSimplifiedMode: Boolean = true) { LaunchedEffect(Unit) { Logger.init() } @@ -52,12 +52,12 @@ fun App(initialSimplifiedMode: Boolean = true, frameWindowScope: FrameWindowScop KoinApplication(application = { modules(appModule) }) { - AppContent(initialSimplifiedMode, frameWindowScope) + AppContent(initialSimplifiedMode) } } @Composable -private fun AppContent(initialSimplifiedMode: Boolean, frameWindowScope: FrameWindowScope? = null) { +private fun AppContent(initialSimplifiedMode: Boolean) { val configRepository: ConfigRepository = koinInject() val patchSourceManager: PatchSourceManager = koinInject() val scope = rememberCoroutineScope() @@ -111,22 +111,21 @@ private fun AppContent(initialSimplifiedMode: Boolean, frameWindowScope: FrameWi } } + val titleBarInsets = remember { + val isMac = System.getProperty("os.name")?.lowercase()?.contains("mac") == true + if (isMac) TitleBarInsets(start = 80.dp, top = 0.dp) + else TitleBarInsets() + } + MorpheTheme(themePreference = themePreference) { CompositionLocalProvider( LocalThemeState provides themeState, - LocalModeState provides modeState + LocalModeState provides modeState, + LocalTitleBarInsets provides titleBarInsets ) { Surface(modifier = Modifier.fillMaxSize()) { - Column(modifier = Modifier.fillMaxSize()) { - // Custom title bar (replaces native window chrome) - if (frameWindowScope != null) { - with(frameWindowScope) { - CustomTitleBar() - } - } - - Box(modifier = Modifier.weight(1f).fillMaxWidth()) { - if (!isLoading) { + Box(modifier = Modifier.fillMaxSize()) { + if (!isLoading) { val patchService: PatchService = koinInject() val quickViewModel = remember { QuickPatchViewModel(patchSourceManager, patchService, configRepository) @@ -185,7 +184,6 @@ private fun AppContent(initialSimplifiedMode: Boolean, frameWindowScope: FrameWi } } } - } } } } diff --git a/src/main/kotlin/app/morphe/gui/GuiMain.kt b/src/main/kotlin/app/morphe/gui/GuiMain.kt index d0244cb..66f8822 100644 --- a/src/main/kotlin/app/morphe/gui/GuiMain.kt +++ b/src/main/kotlin/app/morphe/gui/GuiMain.kt @@ -1,8 +1,10 @@ package app.morphe.gui +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.painter.BitmapPainter +import app.morphe.gui.ui.components.LocalFrameWindowScope import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp @@ -55,14 +57,39 @@ fun launchGui(args: Array) = application { onCloseRequest = ::exitApplication, title = "Morphe", state = windowState, - icon = appIcon, - undecorated = true + icon = appIcon ) { window.minimumSize = java.awt.Dimension(600, 400) - App( - initialSimplifiedMode = initialSimplifiedMode, - frameWindowScope = this - ) + + // macOS: transparent title bar with expanded height so traffic lights + // align with our header row content. Uses JetBrains Runtime custom title bar API. + // Other OS: standard decorated window (no-op). + remember { + val isMac = System.getProperty("os.name")?.lowercase()?.contains("mac") == true + if (isMac) { + window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) + window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) + window.rootPane.putClientProperty("apple.awt.windowTitleVisible", false) + + // JBR: expand the title bar so traffic lights center with our header row. + // Height ~= header top padding (26dp) + half content height (~20dp) + buffer + // → traffic lights center vertically with our header icons/text. + try { + val decorations = com.jetbrains.JBR.getWindowDecorations() + val titleBar = decorations.createCustomTitleBar() + titleBar.height = 56f + titleBar.putProperty("controls.visible", true) + decorations.setCustomTitleBar(window, titleBar) + } catch (_: Exception) { + // Not running on JBR — traffic lights stay at default position + } + } + true + } + + CompositionLocalProvider(LocalFrameWindowScope provides this) { + App(initialSimplifiedMode = initialSimplifiedMode) + } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt b/src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt deleted file mode 100644 index 61a15df..0000000 --- a/src/main/kotlin/app/morphe/gui/ui/components/CustomTitleBar.kt +++ /dev/null @@ -1,157 +0,0 @@ -package app.morphe.gui.ui.components - -import androidx.compose.animation.animateColorAsState -import androidx.compose.animation.core.tween -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.hoverable -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsHoveredAsState -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.window.WindowDraggableArea -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.FrameWindowScope -import app.morphe.gui.ui.theme.LocalMorpheFont -import java.awt.Frame -import java.awt.event.MouseAdapter -import java.awt.event.MouseEvent - -@Composable -fun FrameWindowScope.CustomTitleBar() { - val mono = LocalMorpheFont.current - - WindowDraggableArea( - modifier = Modifier - .fillMaxWidth() - .height(36.dp) - .background(MaterialTheme.colorScheme.surface) - ) { - Row( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // App title - Text( - text = "Morphe", - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - letterSpacing = 0.5.sp, - modifier = Modifier.padding(start = 8.dp) - ) - - Spacer(modifier = Modifier.weight(1f)) - - // Window controls - WindowButton( - symbol = "─", - hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f), - symbolColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - onClick = { - window.extendedState = Frame.ICONIFIED - } - ) - - val isMaximized = remember { mutableStateOf(window.extendedState == Frame.MAXIMIZED_BOTH) } - - // Listen for external maximize state changes (e.g. OS double-click on title bar) - DisposableEffect(window) { - val listener = object : java.awt.event.WindowStateListener { - override fun windowStateChanged(e: java.awt.event.WindowEvent) { - isMaximized.value = (e.newState and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH - } - } - window.addWindowStateListener(listener) - onDispose { window.removeWindowStateListener(listener) } - } - - WindowButton( - symbol = if (isMaximized.value) "❐" else "□", - hoverColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f), - symbolColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - onClick = { - if (isMaximized.value) { - window.extendedState = Frame.NORMAL - } else { - window.extendedState = Frame.MAXIMIZED_BOTH - } - isMaximized.value = !isMaximized.value - } - ) - - WindowButton( - symbol = "✕", - hoverColor = Color(0xFFE81123), - symbolColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - hoverSymbolColor = Color.White, - onClick = { - window.dispatchEvent( - java.awt.event.WindowEvent(window, java.awt.event.WindowEvent.WINDOW_CLOSING) - ) - } - ) - } - } - - // Double-click to maximize/restore - DisposableEffect(window) { - val listener = object : MouseAdapter() { - override fun mouseClicked(e: MouseEvent) { - if (e.clickCount == 2 && e.y <= 36) { - val isMax = (window.extendedState and Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH - window.extendedState = if (isMax) Frame.NORMAL else Frame.MAXIMIZED_BOTH - } - } - } - window.addMouseListener(listener) - onDispose { window.removeMouseListener(listener) } - } -} - -@Composable -private fun WindowButton( - symbol: String, - hoverColor: Color, - symbolColor: Color, - hoverSymbolColor: Color? = null, - onClick: () -> Unit -) { - val hover = remember { MutableInteractionSource() } - val isHovered by hover.collectIsHoveredAsState() - val bg by animateColorAsState( - if (isHovered) hoverColor else Color.Transparent, - animationSpec = tween(100) - ) - val fg by animateColorAsState( - if (isHovered && hoverSymbolColor != null) hoverSymbolColor else symbolColor, - animationSpec = tween(100) - ) - - Box( - modifier = Modifier - .size(36.dp) - .hoverable(hover) - .clickable(onClick = onClick) - .background(bg), - contentAlignment = Alignment.Center - ) { - Text( - text = symbol, - fontSize = 11.sp, - color = fg - ) - } -} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt b/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt new file mode 100644 index 0000000..e8c278a --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt @@ -0,0 +1,28 @@ +package app.morphe.gui.ui.components + +import androidx.compose.foundation.window.WindowDraggableArea +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +/** + * Wraps content in a WindowDraggableArea on macOS so the header row + * can be used to drag the window. Interactive children (buttons, etc.) + * still receive clicks normally — only drags on empty space move the window. + * On non-macOS or when FrameWindowScope is unavailable, renders content directly. + */ +@Composable +fun DraggableHeaderArea( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + val frameScope = LocalFrameWindowScope.current + if (frameScope != null) { + with(frameScope) { + WindowDraggableArea(modifier = modifier) { + content() + } + } + } else { + content() + } +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt b/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt new file mode 100644 index 0000000..707d3b6 --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt @@ -0,0 +1,25 @@ +package app.morphe.gui.ui.components + +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.FrameWindowScope + +/** + * Insets for the title bar region. On macOS with transparent title bar, + * the traffic lights occupy ~80dp on the left and some space from the top. + * Screens should apply these to their header rows so controls don't + * overlap with native window buttons. + */ +data class TitleBarInsets( + val start: androidx.compose.ui.unit.Dp = 0.dp, + val top: androidx.compose.ui.unit.Dp = 0.dp +) + +val LocalTitleBarInsets = compositionLocalOf { TitleBarInsets() } + +/** + * Provides FrameWindowScope so composables deep in the tree can use + * WindowDraggableArea for native window dragging. + */ +val LocalFrameWindowScope = staticCompositionLocalOf { null } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index a7fbcdf..ff7f94c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -49,6 +49,8 @@ import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.SupportedApp +import app.morphe.gui.ui.components.DraggableHeaderArea +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.screens.home.components.ApkInfoCard import app.morphe.gui.ui.screens.home.components.FullScreenDropZone @@ -249,10 +251,14 @@ fun HomeScreenContent( // Top bar — only floated when not using horizontal header if (!useHorizontalHeader) { + val titleInsets = LocalTitleBarInsets.current TopBarRow( modifier = Modifier .align(Alignment.TopEnd) - .padding(padding), + .padding( + top = padding + titleInsets.top, + end = padding + ), allowCacheClear = true ) } @@ -308,39 +314,47 @@ private fun HeaderBar( onRetry: () -> Unit ) { val mono = LocalMorpheFont.current + val titleInsets = LocalTitleBarInsets.current - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = padding, vertical = if (isSmall) 12.dp else 16.dp), - verticalAlignment = Alignment.CenterVertically - ) { - // Logo — left-aligned, compact - BrandingSection(isCompact = true) + DraggableHeaderArea { + Row( + modifier = Modifier + .fillMaxWidth() + .padding( + start = padding + titleInsets.start, + end = padding, + top = (if (isSmall) 8.dp else 10.dp) + titleInsets.top, + bottom = if (isSmall) 8.dp else 10.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + // Logo — left-aligned, compact + BrandingSection(isCompact = true) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.width(16.dp)) - // Patches version inline - if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { - PatchesVersionInline( - patchesVersion = uiState.patchesVersion!!, - isLatest = uiState.isUsingLatestPatches, - onChangePatchesClick = onChangePatchesClick - ) - } else if (uiState.isLoadingPatches) { - PatchesLoadingIndicator() - } + // Patches version inline + if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { + PatchesVersionInline( + patchesVersion = uiState.patchesVersion!!, + isLatest = uiState.isUsingLatestPatches, + onChangePatchesClick = onChangePatchesClick + ) + } else if (uiState.isLoadingPatches) { + PatchesLoadingIndicator() + } - // Offline badge - if (uiState.isOffline && !uiState.isLoadingPatches) { - Spacer(modifier = Modifier.width(12.dp)) - OfflineBadge(onRetry = onRetry) - } + // Offline badge + if (uiState.isOffline && !uiState.isLoadingPatches) { + Spacer(modifier = Modifier.width(12.dp)) + OfflineBadge(onRetry = onRetry) + } - Spacer(modifier = Modifier.weight(1f)) + Spacer(modifier = Modifier.weight(1f)) - // Device indicator + settings — inline in the header - TopBarRow(allowCacheClear = true) + // Device indicator + settings — inline in the header + TopBarRow(allowCacheClear = true) + } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index 58aa184..942df4b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -30,6 +30,7 @@ import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.ChecksumStatus +import app.morphe.gui.util.DeviceMonitor @Composable fun ApkInfoCard( @@ -182,8 +183,14 @@ fun ApkInfoCard( } } - // ── Architectures — shown as individual tags, never truncated ── + // ── Architectures — shown as individual tags, device arch highlighted ── if (apkInfo.architectures.isNotEmpty()) { + val deviceState by DeviceMonitor.state.collectAsState() + val deviceArch = deviceState.selectedDevice?.architecture + val hasMultipleArchs = apkInfo.architectures.size > 1 + // Highlight the device's arch when connected and APK has multiple archs + val highlightArch = if (hasMultipleArchs && deviceArch != null) deviceArch else null + Row( modifier = Modifier .fillMaxWidth() @@ -209,24 +216,42 @@ fun ApkInfoCard( ) Spacer(Modifier.width(4.dp)) apkInfo.architectures.forEach { arch -> + val isDeviceArch = highlightArch != null && arch == highlightArch + val tagBorder = if (isDeviceArch) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f) + val tagBg = if (isDeviceArch) MorpheColors.Blue.copy(alpha = 0.08f) + else Color.Transparent + val tagColor = if (isDeviceArch) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurface + val dimmed = highlightArch != null && !isDeviceArch + Box( modifier = Modifier - .border( - 1.dp, - MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f), - RoundedCornerShape(corners.small) - ) + .border(1.dp, tagBorder, RoundedCornerShape(corners.small)) + .background(tagBg, RoundedCornerShape(corners.small)) .padding(horizontal = 8.dp, vertical = 3.dp) ) { Text( text = arch, fontSize = 11.sp, - fontWeight = FontWeight.Medium, + fontWeight = if (isDeviceArch) FontWeight.Bold else FontWeight.Medium, fontFamily = mono, - color = MaterialTheme.colorScheme.onSurface + color = if (dimmed) tagColor.copy(alpha = 0.35f) else tagColor ) } } + // Hint text when device arch is highlighted + if (highlightArch != null) { + Spacer(Modifier.width(2.dp)) + Text( + text = "DEVICE", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue.copy(alpha = 0.5f), + letterSpacing = 1.sp + ) + } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 65bd16e..2a82027 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -48,6 +48,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.Patch import org.koin.core.parameter.parametersOf +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.ErrorDialog import app.morphe.gui.ui.components.DeviceIndicator import app.morphe.gui.ui.components.SettingsButton @@ -125,10 +126,16 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { Column(modifier = Modifier.fillMaxSize()) { // ── Header bar ── + val titleInsets = LocalTitleBarInsets.current Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding( + start = 16.dp + titleInsets.start, + end = 16.dp, + top = 12.dp + titleInsets.top, + bottom = 12.dp + ), verticalAlignment = Alignment.CenterVertically ) { // Back button @@ -1241,7 +1248,7 @@ private fun ArchitectureSelectorCard( if (deviceArch != null) { Spacer(modifier = Modifier.height(2.dp)) Text( - text = "Your device: $deviceArch", + text = "Your device's CPU architecture: $deviceArch", fontSize = 10.sp, fontWeight = FontWeight.Medium, fontFamily = mono, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index 5123088..a696c7d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -37,6 +37,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.Release import org.koin.core.parameter.parametersOf import cafe.adriel.voyager.koin.koinScreenModel +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.ErrorDialog import app.morphe.gui.ui.components.DeviceIndicator import app.morphe.gui.ui.components.SettingsButton @@ -106,10 +107,16 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { .fillMaxSize() ) { // ── Header bar ── + val titleInsets = LocalTitleBarInsets.current Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 12.dp), + .padding( + start = 16.dp + titleInsets.start, + end = 16.dp, + top = 12.dp + titleInsets.top, + bottom = 12.dp + ), verticalAlignment = Alignment.CenterVertically ) { // Back button diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt index be0d351..b6cbe9a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt @@ -1,6 +1,13 @@ package app.morphe.gui.ui.screens.patching +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items @@ -14,8 +21,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -25,10 +33,12 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.PatchConfig import org.koin.core.parameter.parametersOf -import app.morphe.gui.ui.components.DeviceIndicator -import app.morphe.gui.ui.components.SettingsButton +import app.morphe.gui.ui.components.DraggableHeaderArea +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.screens.result.ResultScreen +import app.morphe.gui.ui.theme.LocalMorpheCorners +import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger @@ -48,11 +58,14 @@ data class PatchingScreen( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PatchingScreenContent(viewModel: PatchingViewModel) { val navigator = LocalNavigator.currentOrThrow val uiState by viewModel.uiState.collectAsState() + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val titleInsets = LocalTitleBarInsets.current + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.10f) // Auto-start patching when screen loads LaunchedEffect(Unit) { @@ -70,173 +83,252 @@ fun PatchingScreenContent(viewModel: PatchingViewModel) { // Auto-navigate to result screen on successful completion LaunchedEffect(uiState.status) { if (uiState.status == PatchingStatus.COMPLETED && uiState.outputPath != null) { - // Small delay to let user see the success message kotlinx.coroutines.delay(1500) navigator.push(ResultScreen(outputPath = uiState.outputPath!!)) } } - Scaffold( - topBar = { - TopAppBar( - title = { - Column { - Text("Patching", fontWeight = FontWeight.SemiBold) - Text( - text = getStatusText(uiState.status), - style = MaterialTheme.typography.bodySmall, - color = getStatusColor(uiState.status) + Column( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + ) { + // ── Header row ── + DraggableHeaderArea { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f ) } - }, - navigationIcon = { - IconButton( - onClick = { navigator.pop() }, - enabled = !uiState.isInProgress + .padding( + start = 12.dp + titleInsets.start, + end = 12.dp, + top = 8.dp + titleInsets.top, + bottom = 8.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + // Back button + val backHover = remember { MutableInteractionSource() } + val isBackHovered by backHover.collectIsHoveredAsState() + val backBg by animateColorAsState( + if (isBackHovered) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) + else Color.Transparent, + animationSpec = tween(150) + ) + Box( + modifier = Modifier + .size(32.dp) + .hoverable(backHover) + .clip(RoundedCornerShape(corners.small)) + .background(backBg) + .clickable(enabled = !uiState.isInProgress) { navigator.pop() }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.size(18.dp), + tint = if (uiState.isInProgress) + MaterialTheme.colorScheme.onSurface.copy(alpha = 0.25f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } + + Spacer(Modifier.width(12.dp)) + + // Title + status + Column { + Text( + text = "PATCHING", + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 1.sp + ) + Text( + text = getStatusText(uiState.status).uppercase(), + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = getStatusColor(uiState.status), + letterSpacing = 1.sp + ) + } + + Spacer(Modifier.weight(1f)) + + // Cancel button + if (uiState.canCancel) { + val cancelHover = remember { MutableInteractionSource() } + val isCancelHovered by cancelHover.collectIsHoveredAsState() + val cancelBg by animateColorAsState( + if (isCancelHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.12f) + else Color.Transparent, + animationSpec = tween(150) + ) + val cancelBorder by animateColorAsState( + if (isCancelHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.4f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f), + animationSpec = tween(150) + ) + + Row( + modifier = Modifier + .hoverable(cancelHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, cancelBorder, RoundedCornerShape(corners.small)) + .background(cancelBg) + .clickable { viewModel.cancelPatching() } + .padding(horizontal = 12.dp, vertical = 6.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) ) { Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = "Back" + imageVector = Icons.Default.Close, + contentDescription = null, + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.error + ) + Text( + text = "CANCEL", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.error, + letterSpacing = 0.5.sp ) } - }, - actions = { - if (uiState.canCancel) { - TextButton( - onClick = { viewModel.cancelPatching() }, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.error - ) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = null, - modifier = Modifier.size(18.dp) - ) - Spacer(modifier = Modifier.width(4.dp)) - Text("Cancel") - } - } - TopBarRow(allowCacheClear = false) - Spacer(Modifier.width(12.dp)) - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surface - ) - ) + + Spacer(Modifier.width(8.dp)) + } + + TopBarRow(allowCacheClear = false) + } } - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - ) { - // Progress indicator - if (uiState.isInProgress) { - Column { - if (uiState.hasProgress) { - // Show determinate progress when we have progress info - LinearProgressIndicator( - progress = { uiState.progress }, - modifier = Modifier - .fillMaxWidth() - .height(4.dp), - color = MorpheColors.Blue, + + // ── Progress section ── + if (uiState.isInProgress) { + Column { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(3.dp), + color = MorpheColors.Blue, + trackColor = MorpheColors.Blue.copy(alpha = 0.08f), + progress = { if (uiState.hasProgress) uiState.progress else 0f }, + ) + if (!uiState.hasProgress) { + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(3.dp), + color = MorpheColors.Blue, + trackColor = Color.Transparent + ) + } + + if (uiState.hasProgress) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = uiState.currentPatch ?: "Applying patches...", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + maxLines = 1, + modifier = Modifier.weight(1f) ) - // Show progress text - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp, vertical = 4.dp), - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = uiState.currentPatch ?: "Applying patches...", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - modifier = Modifier.weight(1f) - ) - Text( - text = "${uiState.patchedCount}/${uiState.totalPatches}", - fontSize = 11.sp, - color = MorpheColors.Blue, - fontWeight = FontWeight.Medium - ) - } - } else { - // Show indeterminate progress when we don't have progress info - LinearProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .height(4.dp), - color = MorpheColors.Blue + Text( + text = "${uiState.patchedCount}/${uiState.totalPatches}", + fontSize = 10.sp, + fontFamily = mono, + color = MorpheColors.Blue, + fontWeight = FontWeight.Bold ) } } } + } - // Log output - LazyColumn( - state = listState, - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(12.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)), - contentPadding = PaddingValues(12.dp), - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(uiState.logs, key = { it.id }) { entry -> - LogEntryRow(entry) - } + // ── Log output ── + LazyColumn( + state = listState, + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.15f)), + contentPadding = PaddingValues(12.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + items(uiState.logs, key = { it.id }) { entry -> + LogEntryRow(entry, mono) } + } - // Bottom action bar (only for failed/cancelled - success auto-navigates) - when (uiState.status) { - PatchingStatus.COMPLETED -> { - // Show brief success message while auto-navigating - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically - ) { - CircularProgressIndicator( - modifier = Modifier.size(20.dp), - strokeWidth = 2.dp, - color = MorpheColors.Teal - ) - Spacer(modifier = Modifier.width(12.dp)) - Text( - text = "Patching completed! Loading result...", - color = MorpheColors.Teal, - fontWeight = FontWeight.Medium + // ── Bottom action bar ── + when (uiState.status) { + PatchingStatus.COMPLETED -> { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1f ) } - } - } - - PatchingStatus.FAILED, PatchingStatus.CANCELLED -> { - FailureBottomBar( - status = uiState.status, - error = uiState.error, - onStartOver = { navigator.popUntilRoot() }, - onGoBack = { navigator.pop() } + .background(MorpheColors.Teal.copy(alpha = 0.04f)) + .padding(14.dp), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(16.dp), + strokeWidth = 2.dp, + color = MorpheColors.Teal + ) + Spacer(modifier = Modifier.width(10.dp)) + Text( + text = "PATCHING COMPLETED — LOADING RESULT...", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 0.5.sp ) } + } - else -> { - // Show nothing for in-progress states - } + PatchingStatus.FAILED, PatchingStatus.CANCELLED -> { + FailureBottomBar( + status = uiState.status, + error = uiState.error, + corners = corners, + mono = mono, + borderColor = borderColor, + onStartOver = { navigator.popUntilRoot() }, + onGoBack = { navigator.pop() } + ) } + + else -> {} } } } @@ -245,6 +337,9 @@ fun PatchingScreenContent(viewModel: PatchingViewModel) { private fun FailureBottomBar( status: PatchingStatus, error: String?, + corners: app.morphe.gui.ui.theme.MorpheCornerStyle, + mono: androidx.compose.ui.text.font.FontFamily, + borderColor: Color, onStartOver: () -> Unit, onGoBack: () -> Unit ) { @@ -253,53 +348,84 @@ private fun FailureBottomBar( val tempFilesSize = remember { FileUtils.getTempDirSize() } val logFile = remember { Logger.getLogFile() } - Surface( - modifier = Modifier.fillMaxWidth(), - color = MaterialTheme.colorScheme.surface, - tonalElevation = 3.dp + Column( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, 0f), + end = Offset(size.width, 0f), + strokeWidth = 1f + ) + } + .background(MaterialTheme.colorScheme.surface) + .padding(16.dp) ) { - Column( - modifier = Modifier.padding(16.dp) - ) { - // Error message + // Error message + Text( + text = (if (status == PatchingStatus.CANCELLED) "PATCHING CANCELLED" else "PATCHING FAILED").uppercase(), + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.error, + letterSpacing = 1.sp + ) + if (error != null && status != PatchingStatus.CANCELLED) { + Spacer(modifier = Modifier.height(4.dp)) Text( - text = if (status == PatchingStatus.CANCELLED) - "Patching was cancelled" - else - error ?: "Patching failed", - color = MaterialTheme.colorScheme.error, - fontWeight = FontWeight.Medium + text = error, + fontSize = 12.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.error.copy(alpha = 0.7f) ) + } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Log file location - if (logFile != null && logFile.exists()) { - Row( + // Log file location + if (logFile != null && logFile.exists()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "LOG FILE", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.sp + ) + Spacer(Modifier.height(2.dp)) + Text( + text = logFile.absolutePath, + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + maxLines = 1 + ) + } + + val openHover = remember { MutableInteractionSource() } + val isOpenHovered by openHover.collectIsHoveredAsState() + val openBg by animateColorAsState( + if (isOpenHovered) MorpheColors.Blue.copy(alpha = 0.1f) else Color.Transparent, + animationSpec = tween(150) + ) + Box( modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Log file", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = logFile.absolutePath, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - fontFamily = FontFamily.Monospace, - maxLines = 1 - ) - } - TextButton( - onClick = { + .hoverable(openHover) + .clip(RoundedCornerShape(corners.small)) + .background(openBg) + .clickable { try { if (Desktop.isDesktopSupported()) { Desktop.getDesktop().open(logFile.parentFile) @@ -308,117 +434,181 @@ private fun FailureBottomBar( Logger.error("Failed to open logs folder", e) } } - ) { - Text("Open", fontSize = 12.sp) - } + .padding(horizontal = 10.dp, vertical = 4.dp) + ) { + Text( + text = "OPEN", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue, + letterSpacing = 0.5.sp + ) } - - Spacer(modifier = Modifier.height(12.dp)) } - // Cleanup option - if (hasTempFiles && !tempFilesCleared) { - Row( + Spacer(modifier = Modifier.height(8.dp)) + } + + // Cleanup option + if (hasTempFiles && !tempFilesCleared) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, borderColor, RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f)) + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "TEMPORARY FILES", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.sp + ) + Spacer(Modifier.height(2.dp)) + Text( + text = "${formatFileSize(tempFilesSize)} can be freed", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + + val cleanHover = remember { MutableInteractionSource() } + val isCleanHovered by cleanHover.collectIsHoveredAsState() + val cleanBg by animateColorAsState( + if (isCleanHovered) Color(0xFFFF9800).copy(alpha = 0.1f) else Color.Transparent, + animationSpec = tween(150) + ) + Box( modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = "Temporary files", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = "${formatFileSize(tempFilesSize)} can be freed", - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } - TextButton( - onClick = { + .hoverable(cleanHover) + .clip(RoundedCornerShape(corners.small)) + .background(cleanBg) + .clickable { FileUtils.cleanupAllTempDirs() tempFilesCleared = true Logger.info("Cleaned temp files after failed patching") } - ) { - Text("Clean up", fontSize = 12.sp) - } - } - - Spacer(modifier = Modifier.height(12.dp)) - } else if (tempFilesCleared) { - Row( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) - .background(MorpheColors.Teal.copy(alpha = 0.1f)) - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + .padding(horizontal = 10.dp, vertical = 4.dp) ) { Text( - text = "Temp files cleaned", - fontSize = 12.sp, - color = MorpheColors.Teal + text = "CLEAN UP", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color(0xFFFF9800), + letterSpacing = 0.5.sp ) } - - Spacer(modifier = Modifier.height(12.dp)) } - // Action buttons + Spacer(modifier = Modifier.height(8.dp)) + } else if (tempFilesCleared) { Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(corners.small)) + .background(MorpheColors.Teal.copy(alpha = 0.06f)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) + .padding(12.dp) ) { - OutlinedButton( - onClick = onStartOver, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(12.dp) - ) { - Text("Start Over") - } - Button( - onClick = onGoBack, - modifier = Modifier - .weight(1f) - .height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) - ) { - Text("Go Back", fontWeight = FontWeight.Medium) - } + Text( + text = "TEMP FILES CLEANED", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 0.5.sp + ) } + + Spacer(modifier = Modifier.height(8.dp)) } - } -} -private fun formatFileSize(bytes: Long): String { - return when { - bytes < 1024 -> "$bytes B" - bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) - bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) - else -> "%.2f GB".format(bytes / (1024.0 * 1024.0 * 1024.0)) + // Action buttons + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + // Start Over — outlined + val startOverHover = remember { MutableInteractionSource() } + val isStartOverHovered by startOverHover.collectIsHoveredAsState() + val startOverBorder by animateColorAsState( + if (isStartOverHovered) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f), + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .weight(1f) + .height(40.dp) + .hoverable(startOverHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, startOverBorder, RoundedCornerShape(corners.small)) + .clickable(onClick = onStartOver), + contentAlignment = Alignment.Center + ) { + Text( + text = "START OVER", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 0.5.sp + ) + } + + // Go Back — filled + val goBackHover = remember { MutableInteractionSource() } + val isGoBackHovered by goBackHover.collectIsHoveredAsState() + val goBackBg by animateColorAsState( + if (isGoBackHovered) MorpheColors.Blue.copy(alpha = 0.9f) + else MorpheColors.Blue, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .weight(1f) + .height(40.dp) + .hoverable(goBackHover) + .clip(RoundedCornerShape(corners.small)) + .background(goBackBg, RoundedCornerShape(corners.small)) + .clickable(onClick = onGoBack), + contentAlignment = Alignment.Center + ) { + Text( + text = "GO BACK", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 0.5.sp + ) + } + } } } @Composable -private fun LogEntryRow(entry: LogEntry) { +private fun LogEntryRow( + entry: LogEntry, + mono: androidx.compose.ui.text.font.FontFamily +) { val color = when (entry.level) { LogLevel.SUCCESS -> MorpheColors.Teal LogLevel.ERROR -> MaterialTheme.colorScheme.error LogLevel.WARNING -> Color(0xFFFF9800) LogLevel.PROGRESS -> MorpheColors.Blue - LogLevel.INFO -> MaterialTheme.colorScheme.onSurfaceVariant + LogLevel.INFO -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) } val prefix = when (entry.level) { @@ -431,13 +621,22 @@ private fun LogEntryRow(entry: LogEntry) { Text( text = "$prefix ${entry.message}", - fontFamily = FontFamily.Monospace, - fontSize = 12.sp, + fontFamily = mono, + fontSize = 11.sp, color = color, - lineHeight = 18.sp + lineHeight = 16.sp ) } +private fun formatFileSize(bytes: Long): String { + return when { + bytes < 1024 -> "$bytes B" + bytes < 1024 * 1024 -> "%.1f KB".format(bytes / 1024.0) + bytes < 1024 * 1024 * 1024 -> "%.1f MB".format(bytes / (1024.0 * 1024.0)) + else -> "%.2f GB".format(bytes / (1024.0 * 1024.0 * 1024.0)) + } +} + private fun getStatusText(status: PatchingStatus): String { return when (status) { PatchingStatus.IDLE -> "Ready" @@ -455,6 +654,6 @@ private fun getStatusColor(status: PatchingStatus): Color { PatchingStatus.COMPLETED -> MorpheColors.Teal PatchingStatus.FAILED -> MaterialTheme.colorScheme.error PatchingStatus.CANCELLED -> MaterialTheme.colorScheme.error - else -> MaterialTheme.colorScheme.onSurfaceVariant + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 4a3ec53..0ae510a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -32,6 +32,7 @@ import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchSourceManager import app.morphe.gui.ui.components.OfflineBanner +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.TopBarRow import app.morphe.gui.ui.screens.home.components.FullScreenDropZone import app.morphe.gui.ui.theme.* @@ -161,10 +162,14 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { } // Top-right: device indicator + settings + val titleInsets = LocalTitleBarInsets.current TopBarRow( modifier = Modifier .align(Alignment.TopEnd) - .padding(24.dp) + .padding( + top = 24.dp + titleInsets.top, + end = 24.dp + ) ) // Drag overlay diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt index 124331e..236f003 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt @@ -1,24 +1,32 @@ package app.morphe.gui.ui.screens.result -import androidx.compose.foundation.BorderStroke +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.tween import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.ui.graphics.Color +import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material.icons.filled.PhoneAndroid -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Usb import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import cafe.adriel.voyager.core.screen.Screen @@ -27,7 +35,11 @@ import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.repository.ConfigRepository import kotlinx.coroutines.launch import org.koin.compose.koinInject +import app.morphe.gui.ui.components.DraggableHeaderArea +import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.TopBarRow +import app.morphe.gui.ui.theme.LocalMorpheCorners +import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.AdbDevice import app.morphe.gui.util.AdbException @@ -52,10 +64,14 @@ data class ResultScreen( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun ResultScreenContent(outputPath: String) { val navigator = LocalNavigator.currentOrThrow + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val titleInsets = LocalTitleBarInsets.current + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.10f) + val outputFile = File(outputPath) val scope = rememberCoroutineScope() val adbManager = remember { AdbManager() } @@ -74,14 +90,12 @@ fun ResultScreenContent(outputPath: String) { var tempFilesCleared by remember { mutableStateOf(false) } var autoCleanupEnabled by remember { mutableStateOf(false) } - // Check for temp files and auto-cleanup setting LaunchedEffect(Unit) { val config = configRepository.loadConfig() autoCleanupEnabled = config.autoCleanupTempFiles hasTempFiles = FileUtils.hasTempFiles() tempFilesSize = FileUtils.getTempDirSize() - // Auto-cleanup if enabled if (autoCleanupEnabled && hasTempFiles) { FileUtils.cleanupAllTempDirs() hasTempFiles = false @@ -90,7 +104,6 @@ fun ResultScreenContent(outputPath: String) { } } - // Install function fun installViaAdb() { val device = monitorState.selectedDevice ?: return scope.launch { @@ -118,306 +131,370 @@ fun ResultScreenContent(outputPath: String) { } } - Box( + Column( modifier = Modifier .fillMaxSize() + .background(MaterialTheme.colorScheme.background) ) { - BoxWithConstraints( - modifier = Modifier.fillMaxSize() - ) { - val scrollState = rememberScrollState() - - // Estimate content height for dynamic spacing - val contentHeight = 600.dp // Approximate height of all content - val extraSpace = (maxHeight - contentHeight).coerceAtLeast(0.dp) - - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(32.dp) - ) { - // Add top spacing to center content on large screens - Spacer(modifier = Modifier.height(extraSpace / 2)) - // Success icon - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Success", - tint = MorpheColors.Teal, - modifier = Modifier.size(80.dp) + // ── Header row ── + DraggableHeaderArea { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f + ) + } + .padding( + start = 12.dp + titleInsets.start, + end = 12.dp, + top = 8.dp + titleInsets.top, + bottom = 8.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + // Back button + val backHover = remember { MutableInteractionSource() } + val isBackHovered by backHover.collectIsHoveredAsState() + val backBg by animateColorAsState( + if (isBackHovered) MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) + else Color.Transparent, + animationSpec = tween(150) ) + Box( + modifier = Modifier + .size(32.dp) + .hoverable(backHover) + .clip(RoundedCornerShape(corners.small)) + .background(backBg) + .clickable { navigator.popUntilRoot() }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = "Back", + modifier = Modifier.size(18.dp), + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f) + ) + } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(Modifier.width(12.dp)) + // Title + success indicator + Box( + modifier = Modifier + .size(8.dp) + .background(MorpheColors.Teal, RoundedCornerShape(2.dp)) + ) + Spacer(Modifier.width(8.dp)) Text( - text = "Patching Complete!", - fontSize = 28.sp, + text = "PATCHING COMPLETE", + fontSize = 13.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.weight(1f)) - Text( - text = "Your patched APK is ready", - fontSize = 16.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) + TopBarRow(allowCacheClear = false) + } + } - Spacer(modifier = Modifier.height(32.dp)) + // ── Content ── + val scrollState = rememberScrollState() + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(scrollState) + .padding(horizontal = 24.dp, vertical = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // ── Output file info ── + Box( + modifier = Modifier + .widthIn(max = 520.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) + .background(MaterialTheme.colorScheme.surface) + ) { + // Teal left stripe + Box( + modifier = Modifier + .width(3.dp) + .fillMaxHeight() + .background(MorpheColors.Teal) + .align(Alignment.CenterStart) + ) - // Output file info card - Card( - modifier = Modifier.widthIn(max = 500.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f) - ), - shape = RoundedCornerShape(16.dp) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 3.dp) ) { - Column( - modifier = Modifier.padding(20.dp) + // File name + size + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = "Output File", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = outputFile.name, - fontSize = 16.sp, - fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = outputFile.parent ?: "", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - + Column(modifier = Modifier.weight(1f)) { + Text( + text = "OUTPUT FILE", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(Modifier.height(4.dp)) + Text( + text = outputFile.name, + fontSize = 15.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(Modifier.height(2.dp)) + Text( + text = outputFile.parent ?: "", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } if (outputFile.exists()) { - Spacer(modifier = Modifier.height(8.dp)) Text( text = formatFileSize(outputFile.length()), fontSize = 13.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, color = MorpheColors.Teal ) } } - } - - Spacer(modifier = Modifier.height(24.dp)) - - // ADB Install Section - if (monitorState.isAdbAvailable == true) { - AdbInstallSection( - devices = monitorState.devices, - selectedDevice = monitorState.selectedDevice, - isLoadingDevices = false, - isInstalling = isInstalling, - installProgress = installProgress, - installError = installError, - installSuccess = installSuccess, - onDeviceSelected = { DeviceMonitor.selectDevice(it) }, - onRefreshDevices = { }, - onInstallClick = { installViaAdb() }, - onRetryClick = { - installError = null - installSuccess = false - installViaAdb() - }, - onDismissError = { installError = null } - ) - - Spacer(modifier = Modifier.height(16.dp)) - } - - // Cleanup section - if (hasTempFiles || tempFilesCleared) { - CleanupSection( - hasTempFiles = hasTempFiles, - tempFilesSize = tempFilesSize, - tempFilesCleared = tempFilesCleared, - autoCleanupEnabled = autoCleanupEnabled, - onCleanupClick = { - FileUtils.cleanupAllTempDirs() - hasTempFiles = false - tempFilesCleared = true - Logger.info("Manually cleaned temp files after patching") - } - ) - - Spacer(modifier = Modifier.height(16.dp)) - } - // Action buttons - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedButton( - onClick = { - try { - val folder = outputFile.parentFile - if (folder != null && Desktop.isDesktopSupported()) { - Desktop.getDesktop().open(folder) - } - } catch (e: Exception) { - // Ignore errors + // Open folder button row + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) } - }, - modifier = Modifier.height(48.dp), - shape = RoundedCornerShape(12.dp) + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically ) { - Icon( - imageVector = Icons.Default.FolderOpen, - contentDescription = null, - modifier = Modifier.size(18.dp) + val folderHover = remember { MutableInteractionSource() } + val isFolderHovered by folderHover.collectIsHoveredAsState() + val folderColor by animateColorAsState( + if (isFolderHovered) MorpheColors.Blue else MorpheColors.Blue.copy(alpha = 0.6f), + animationSpec = tween(150) ) - Spacer(modifier = Modifier.width(8.dp)) - Text("Open Folder") - } - Button( - onClick = { navigator.popUntilRoot() }, - modifier = Modifier.height(48.dp), - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Blue - ), - shape = RoundedCornerShape(12.dp) - ) { - Text("Patch Another", fontWeight = FontWeight.Medium) + Box( + modifier = Modifier + .hoverable(folderHover) + .clip(RoundedCornerShape(corners.small)) + .clickable { + try { + val folder = outputFile.parentFile + if (folder != null && Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(folder) + } + } catch (_: Exception) {} + } + .padding(vertical = 2.dp) + ) { + Text( + text = "OPEN FOLDER →", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = folderColor, + letterSpacing = 0.5.sp + ) + } } } + } - Spacer(modifier = Modifier.height(24.dp)) + // ── ADB Install section ── + if (monitorState.isAdbAvailable == true) { + AdbInstallSection( + devices = monitorState.devices, + selectedDevice = monitorState.selectedDevice, + isInstalling = isInstalling, + installProgress = installProgress, + installError = installError, + installSuccess = installSuccess, + corners = corners, + mono = mono, + borderColor = borderColor, + onDeviceSelected = { DeviceMonitor.selectDevice(it) }, + onInstallClick = { installViaAdb() }, + onRetryClick = { + installError = null + installSuccess = false + installViaAdb() + }, + onDismissError = { installError = null } + ) + } - // Help text (only show when ADB is not available) - if (monitorState.isAdbAvailable == false) { - Text( - text = "ADB not found. Install Android SDK Platform Tools to enable direct installation.", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center - ) - } else if (monitorState.isAdbAvailable == null) { - Text( - text = "Checking for ADB...", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - textAlign = TextAlign.Center - ) - } + // ── Cleanup section ── + if (hasTempFiles || tempFilesCleared) { + CleanupSection( + hasTempFiles = hasTempFiles, + tempFilesSize = tempFilesSize, + tempFilesCleared = tempFilesCleared, + autoCleanupEnabled = autoCleanupEnabled, + corners = corners, + mono = mono, + borderColor = borderColor, + onCleanupClick = { + FileUtils.cleanupAllTempDirs() + hasTempFiles = false + tempFilesCleared = true + Logger.info("Manually cleaned temp files after patching") + } + ) + } - // Bottom spacing to center content on large screens - Spacer(modifier = Modifier.height(extraSpace / 2)) + // ── ADB help text ── + if (monitorState.isAdbAvailable == false) { + Text( + text = "ADB not found. Install Android SDK Platform Tools to enable direct installation.", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + textAlign = TextAlign.Center, + modifier = Modifier.widthIn(max = 520.dp) + ) } - } - // Top bar (device indicator + settings) in top-right corner - TopBarRow( - modifier = Modifier - .align(Alignment.TopEnd) - .padding(24.dp), - allowCacheClear = false - ) + // ── Patch Another button ── + Spacer(Modifier.height(4.dp)) + + val patchAnotherHover = remember { MutableInteractionSource() } + val isPatchAnotherHovered by patchAnotherHover.collectIsHoveredAsState() + val patchAnotherBg by animateColorAsState( + if (isPatchAnotherHovered) MorpheColors.Blue.copy(alpha = 0.9f) else MorpheColors.Blue, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .widthIn(max = 520.dp) + .fillMaxWidth() + .height(42.dp) + .hoverable(patchAnotherHover) + .clip(RoundedCornerShape(corners.small)) + .background(patchAnotherBg, RoundedCornerShape(corners.small)) + .clickable { navigator.popUntilRoot() }, + contentAlignment = Alignment.Center + ) { + Text( + text = "PATCH ANOTHER", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 1.sp + ) + } + + Spacer(Modifier.height(8.dp)) + } } } +// ═══════════════════════════════════════════════════════════════════ +// ADB INSTALL SECTION +// ═══════════════════════════════════════════════════════════════════ + @Composable private fun AdbInstallSection( devices: List, selectedDevice: AdbDevice?, - isLoadingDevices: Boolean, isInstalling: Boolean, installProgress: String, installError: String?, installSuccess: Boolean, + corners: app.morphe.gui.ui.theme.MorpheCornerStyle, + mono: androidx.compose.ui.text.font.FontFamily, + borderColor: Color, onDeviceSelected: (AdbDevice) -> Unit, - onRefreshDevices: () -> Unit, onInstallClick: () -> Unit, onRetryClick: () -> Unit, onDismissError: () -> Unit ) { - Card( - modifier = Modifier.widthIn(max = 500.dp), - colors = CardDefaults.cardColors( - containerColor = when { - installSuccess -> MorpheColors.Teal.copy(alpha = 0.1f) - installError != null -> MaterialTheme.colorScheme.error.copy(alpha = 0.1f) - else -> MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - } - ), - shape = RoundedCornerShape(12.dp) + Box( + modifier = Modifier + .widthIn(max = 520.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) + .background(MaterialTheme.colorScheme.surface) ) { Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(20.dp) ) { // Header Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Icon( - imageVector = Icons.Default.Usb, - contentDescription = null, - tint = MorpheColors.Blue, - modifier = Modifier.size(20.dp) - ) - Text( - text = "Install via ADB", - fontWeight = FontWeight.SemiBold, - fontSize = 15.sp - ) - } - // Refresh button - IconButton( - onClick = onRefreshDevices, - enabled = !isLoadingDevices && !isInstalling - ) { - if (isLoadingDevices) { - CircularProgressIndicator( - modifier = Modifier.size(18.dp), - strokeWidth = 2.dp - ) - } else { - Icon( - imageVector = Icons.Default.Refresh, - contentDescription = "Refresh devices", - modifier = Modifier.size(20.dp) - ) - } - } + Text( + text = "ADB INSTALL", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) } - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(12.dp)) when { installSuccess -> { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + verticalAlignment = Alignment.CenterVertically ) { Icon( imageVector = Icons.Default.CheckCircle, contentDescription = null, tint = MorpheColors.Teal, - modifier = Modifier.size(24.dp) + modifier = Modifier.size(18.dp) ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(Modifier.width(8.dp)) Text( - text = "Installed successfully on ${selectedDevice?.displayName ?: "device"}!", - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal + text = "INSTALLED ON ${(selectedDevice?.displayName ?: "DEVICE").uppercase()}", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 0.5.sp ) } } @@ -425,27 +502,63 @@ private fun AdbInstallSection( installError != null -> { Text( text = installError, + fontSize = 11.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.error, - fontSize = 14.sp, - textAlign = TextAlign.Center, modifier = Modifier.fillMaxWidth() ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(Modifier.height(10.dp)) Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.Center + horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - TextButton(onClick = onDismissError) { - Text("Dismiss") - } - Spacer(modifier = Modifier.width(8.dp)) - Button( - onClick = onRetryClick, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.error + val dismissHover = remember { MutableInteractionSource() } + val isDismissHovered by dismissHover.collectIsHoveredAsState() + Box( + modifier = Modifier + .hoverable(dismissHover) + .clip(RoundedCornerShape(corners.small)) + .border( + 1.dp, + MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = if (isDismissHovered) 0.3f else 0.12f + ), + RoundedCornerShape(corners.small) + ) + .clickable(onClick = onDismissError) + .padding(horizontal = 12.dp, vertical = 6.dp) + ) { + Text( + text = "DISMISS", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 0.5.sp ) + } + + val retryHover = remember { MutableInteractionSource() } + val isRetryHovered by retryHover.collectIsHoveredAsState() + Box( + modifier = Modifier + .hoverable(retryHover) + .clip(RoundedCornerShape(corners.small)) + .background( + if (isRetryHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.9f) + else MaterialTheme.colorScheme.error, + RoundedCornerShape(corners.small) + ) + .clickable(onClick = onRetryClick) + .padding(horizontal = 12.dp, vertical = 6.dp) ) { - Text("Retry") + Text( + text = "RETRY", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 0.5.sp + ) } } } @@ -453,94 +566,175 @@ private fun AdbInstallSection( isInstalling -> { Row( modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center + verticalAlignment = Alignment.CenterVertically ) { CircularProgressIndicator( - modifier = Modifier.size(24.dp), + modifier = Modifier.size(16.dp), strokeWidth = 2.dp, color = MorpheColors.Blue ) - Spacer(modifier = Modifier.width(12.dp)) + Spacer(Modifier.width(10.dp)) Text( - text = installProgress.ifEmpty { "Installing..." }, - color = MaterialTheme.colorScheme.onSurface + text = installProgress.ifEmpty { "Installing..." }.uppercase(), + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue, + letterSpacing = 0.5.sp ) } } else -> { - // Device list val readyDevices = devices.filter { it.isReady } val notReadyDevices = devices.filter { !it.isReady } if (devices.isEmpty()) { - // No devices - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "No devices connected", - color = MaterialTheme.colorScheme.onSurfaceVariant, - fontSize = 14.sp - ) - Spacer(modifier = Modifier.height(4.dp)) - Text( - text = "Connect your Android device via USB with USB debugging enabled", - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - fontSize = 12.sp, - textAlign = TextAlign.Center - ) - } - } else { - // Show device list Text( - text = if (readyDevices.size == 1) "Connected device:" else "Select a device:", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "No devices connected", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) ) - Spacer(modifier = Modifier.height(8.dp)) - - // Ready devices - readyDevices.forEach { device -> - DeviceRow( - device = device, - isSelected = selectedDevice?.id == device.id, - onClick = { onDeviceSelected(device) } + Spacer(Modifier.height(2.dp)) + Text( + text = "Connect via USB with USB debugging enabled", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } else { + // Device list + (readyDevices + notReadyDevices).forEach { device -> + val isSelected = selectedDevice?.id == device.id + val enabled = device.isReady + val deviceHover = remember { MutableInteractionSource() } + val isDeviceHovered by deviceHover.collectIsHoveredAsState() + + val deviceBorder by animateColorAsState( + when { + isSelected -> MorpheColors.Teal.copy(alpha = 0.5f) + isDeviceHovered && enabled -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) + else -> borderColor + }, + animationSpec = tween(150) ) - Spacer(modifier = Modifier.height(6.dp)) - } - - // Not ready devices (unauthorized/offline) - notReadyDevices.forEach { device -> - DeviceRow( - device = device, - isSelected = false, - onClick = { }, - enabled = false + val deviceBg by animateColorAsState( + when { + isSelected -> MorpheColors.Teal.copy(alpha = 0.06f) + else -> Color.Transparent + }, + animationSpec = tween(150) ) - Spacer(modifier = Modifier.height(6.dp)) + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 6.dp) + .hoverable(deviceHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, deviceBorder, RoundedCornerShape(corners.small)) + .background(deviceBg, RoundedCornerShape(corners.small)) + .then( + if (enabled) Modifier.clickable { onDeviceSelected(device) } + else Modifier + ) + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp) + ) { + Icon( + imageVector = Icons.Default.PhoneAndroid, + contentDescription = null, + tint = when { + isSelected -> MorpheColors.Teal + enabled -> MorpheColors.Blue.copy(alpha = 0.6f) + else -> MaterialTheme.colorScheme.error.copy(alpha = 0.4f) + }, + modifier = Modifier.size(20.dp) + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = device.displayName, + fontSize = 12.sp, + fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Medium, + fontFamily = mono, + color = if (enabled) MaterialTheme.colorScheme.onSurface + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.4f) + ) + Text( + text = device.id, + fontSize = 9.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) + ) + } + // Status tag + val statusColor = when (device.status) { + DeviceStatus.DEVICE -> MorpheColors.Teal + DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) + else -> MaterialTheme.colorScheme.error + } + Box( + modifier = Modifier + .border(1.dp, statusColor.copy(alpha = 0.3f), RoundedCornerShape(corners.small)) + .background(statusColor.copy(alpha = 0.06f), RoundedCornerShape(corners.small)) + .padding(horizontal = 8.dp, vertical = 3.dp) + ) { + Text( + text = when (device.status) { + DeviceStatus.DEVICE -> "READY" + DeviceStatus.UNAUTHORIZED -> "UNAUTH" + DeviceStatus.OFFLINE -> "OFFLINE" + DeviceStatus.UNKNOWN -> "UNKNOWN" + }, + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = statusColor, + letterSpacing = 0.5.sp + ) + } + } } - Spacer(modifier = Modifier.height(8.dp)) + Spacer(Modifier.height(6.dp)) // Install button - Button( - onClick = onInstallClick, - modifier = Modifier.fillMaxWidth(), - enabled = selectedDevice != null, - colors = ButtonDefaults.buttonColors( - containerColor = MorpheColors.Teal - ), - shape = RoundedCornerShape(8.dp) + val installHover = remember { MutableInteractionSource() } + val isInstallHovered by installHover.collectIsHoveredAsState() + val installBg by animateColorAsState( + when { + selectedDevice == null -> MorpheColors.Teal.copy(alpha = 0.3f) + isInstallHovered -> MorpheColors.Teal.copy(alpha = 0.9f) + else -> MorpheColors.Teal + }, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .height(38.dp) + .hoverable(installHover) + .clip(RoundedCornerShape(corners.small)) + .background(installBg, RoundedCornerShape(corners.small)) + .then( + if (selectedDevice != null) Modifier.clickable(onClick = onInstallClick) + else Modifier + ), + contentAlignment = Alignment.Center ) { Text( text = if (selectedDevice != null) - "Install on ${selectedDevice.displayName}" + "INSTALL ON ${selectedDevice.displayName.uppercase()}" else - "Select a device to install", - fontWeight = FontWeight.Medium + "SELECT A DEVICE", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 0.5.sp ) } } @@ -550,158 +744,96 @@ private fun AdbInstallSection( } } +// ═══════════════════════════════════════════════════════════════════ +// CLEANUP SECTION +// ═══════════════════════════════════════════════════════════════════ + @Composable private fun CleanupSection( hasTempFiles: Boolean, tempFilesSize: Long, tempFilesCleared: Boolean, autoCleanupEnabled: Boolean, + corners: app.morphe.gui.ui.theme.MorpheCornerStyle, + mono: androidx.compose.ui.text.font.FontFamily, + borderColor: Color, onCleanupClick: () -> Unit ) { - Card( - modifier = Modifier.widthIn(max = 500.dp), - colors = CardDefaults.cardColors( - containerColor = if (tempFilesCleared) - MorpheColors.Teal.copy(alpha = 0.1f) - else - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.3f) - ), - shape = RoundedCornerShape(12.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column(modifier = Modifier.weight(1f)) { - Text( - text = if (tempFilesCleared) "Temp files cleaned" else "Temporary files", - fontWeight = FontWeight.Medium, - fontSize = 14.sp, - color = if (tempFilesCleared) - MorpheColors.Teal - else - MaterialTheme.colorScheme.onSurface - ) - Text( - text = when { - tempFilesCleared && autoCleanupEnabled -> "Auto-cleanup is enabled" - tempFilesCleared -> "Freed up ${formatFileSize(tempFilesSize)}" - else -> "${formatFileSize(tempFilesSize)} can be freed" - }, - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - - if (hasTempFiles && !tempFilesCleared) { - OutlinedButton( - onClick = onCleanupClick, - shape = RoundedCornerShape(8.dp), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) - ) { - Text("Clean up", fontSize = 13.sp) - } - } else if (tempFilesCleared) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = null, - tint = MorpheColors.Teal, - modifier = Modifier.size(24.dp) - ) - } - } - } -} + val accentColor = if (tempFilesCleared) MorpheColors.Teal else MaterialTheme.colorScheme.onSurfaceVariant -@Composable -private fun DeviceRow( - device: AdbDevice, - isSelected: Boolean, - onClick: () -> Unit, - enabled: Boolean = true -) { - OutlinedCard( - onClick = onClick, - modifier = Modifier.fillMaxWidth(), - enabled = enabled, - shape = RoundedCornerShape(8.dp), - border = BorderStroke( - width = if (isSelected) 2.dp else 1.dp, - color = when { - isSelected -> MorpheColors.Teal - !enabled -> MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) - else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) - } - ), - colors = CardDefaults.outlinedCardColors( - containerColor = if (isSelected) - MorpheColors.Teal.copy(alpha = 0.08f) - else - MaterialTheme.colorScheme.surface - ) + Row( + modifier = Modifier + .widthIn(max = 520.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border( + 1.dp, + if (tempFilesCleared) MorpheColors.Teal.copy(alpha = 0.2f) else borderColor, + RoundedCornerShape(corners.medium) + ) + .background( + if (tempFilesCleared) MorpheColors.Teal.copy(alpha = 0.04f) + else MaterialTheme.colorScheme.surface + ) + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(12.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - Icon( - imageVector = Icons.Default.PhoneAndroid, - contentDescription = null, - tint = when { - isSelected -> MorpheColors.Teal - device.isReady -> MorpheColors.Blue - else -> MaterialTheme.colorScheme.error.copy(alpha = 0.6f) - }, - modifier = Modifier.size(24.dp) + Column(modifier = Modifier.weight(1f)) { + Text( + text = if (tempFilesCleared) "TEMP FILES CLEANED" else "TEMPORARY FILES", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = if (tempFilesCleared) MorpheColors.Teal + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.sp ) - Column(modifier = Modifier.weight(1f)) { - Text( - text = device.displayName, - fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Medium, - color = if (enabled) - MaterialTheme.colorScheme.onSurface - else - MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f), - fontSize = 14.sp - ) - Text( - text = device.id, - fontSize = 11.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - ) - } - // Status badge - Surface( - color = when (device.status) { - DeviceStatus.DEVICE -> MorpheColors.Teal.copy(alpha = 0.15f) - DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800).copy(alpha = 0.15f) - else -> MaterialTheme.colorScheme.error.copy(alpha = 0.15f) + Spacer(Modifier.height(2.dp)) + Text( + text = when { + tempFilesCleared && autoCleanupEnabled -> "Auto-cleanup is enabled" + tempFilesCleared -> "Freed ${formatFileSize(tempFilesSize)}" + else -> "${formatFileSize(tempFilesSize)} can be freed" }, - shape = RoundedCornerShape(4.dp) + fontSize = 11.sp, + fontFamily = mono, + color = if (tempFilesCleared) MorpheColors.Teal.copy(alpha = 0.7f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + + if (hasTempFiles && !tempFilesCleared) { + val cleanHover = remember { MutableInteractionSource() } + val isCleanHovered by cleanHover.collectIsHoveredAsState() + val cleanBg by animateColorAsState( + if (isCleanHovered) Color(0xFFFF9800).copy(alpha = 0.1f) else Color.Transparent, + animationSpec = tween(150) + ) + Box( + modifier = Modifier + .hoverable(cleanHover) + .clip(RoundedCornerShape(corners.small)) + .background(cleanBg) + .clickable(onClick = onCleanupClick) + .padding(horizontal = 10.dp, vertical = 4.dp) ) { Text( - text = when (device.status) { - DeviceStatus.DEVICE -> "Ready" - DeviceStatus.UNAUTHORIZED -> "Unauthorized" - DeviceStatus.OFFLINE -> "Offline" - DeviceStatus.UNKNOWN -> "Unknown" - }, + text = "CLEAN UP", fontSize = 10.sp, - fontWeight = FontWeight.Medium, - color = when (device.status) { - DeviceStatus.DEVICE -> MorpheColors.Teal - DeviceStatus.UNAUTHORIZED -> Color(0xFFFF9800) - else -> MaterialTheme.colorScheme.error - }, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color(0xFFFF9800), + letterSpacing = 0.5.sp ) } + } else if (tempFilesCleared) { + Icon( + imageVector = Icons.Default.CheckCircle, + contentDescription = null, + tint = MorpheColors.Teal, + modifier = Modifier.size(18.dp) + ) } } } From bdc6d5b4973e00fb6bb943d59bce4744914d9b3f Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 21 Mar 2026 10:58:14 +0530 Subject: [PATCH 37/49] Major Fix Update Patch Screen gets better state awareness. Cache clear errors fixed. Minor UI tweaks and fixes. --- .../gui/data/repository/PatchRepository.kt | 2 +- .../gui/data/repository/PatchSourceManager.kt | 9 + .../kotlin/app/morphe/gui/di/AppModule.kt | 2 +- .../gui/ui/components/SettingsButton.kt | 10 +- .../gui/ui/components/SettingsDialog.kt | 40 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 42 +- .../gui/ui/screens/home/HomeViewModel.kt | 16 +- .../ui/screens/home/components/ApkInfoCard.kt | 12 - .../screens/patches/PatchSelectionScreen.kt | 38 +- .../gui/ui/screens/patches/PatchesScreen.kt | 33 +- .../ui/screens/patches/PatchesViewModel.kt | 16 +- .../gui/ui/screens/patching/PatchingScreen.kt | 2 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 937 +++++++++++------- 13 files changed, 752 insertions(+), 407 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt index 7cc7c43..2acc14f 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchRepository.kt @@ -216,7 +216,7 @@ class PatchRepository( var failedCount = 0 patchesDir.listFiles()?.forEach { file -> try { - java.nio.file.Files.delete(file.toPath()) + if (!file.deleteRecursively()) throw Exception("Could not delete") } catch (e: Exception) { failedCount++ Logger.error("Failed to delete ${file.name}: ${e.message}") diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt index b02101f..27b8fcb 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt @@ -139,4 +139,13 @@ class PatchSourceManager( fun clearAll() { repositories.clear() } + + /** + * Notify that cached patch files were deleted (e.g. via "Clear Cache" in settings). + * Clears cached repo state and bumps [sourceVersion] so ViewModels reload. + */ + fun notifyCacheCleared() { + cachedActiveRepo?.clearCache() + _sourceVersion.value++ + } } diff --git a/src/main/kotlin/app/morphe/gui/di/AppModule.kt b/src/main/kotlin/app/morphe/gui/di/AppModule.kt index 3841cb7..68dad92 100644 --- a/src/main/kotlin/app/morphe/gui/di/AppModule.kt +++ b/src/main/kotlin/app/morphe/gui/di/AppModule.kt @@ -62,7 +62,7 @@ val appModule = module { } factory { params -> val psm = get() - PatchesViewModel(params.get(), params.get(), psm.getActiveRepositorySync(), get(), psm.getLocalFilePath()) + PatchesViewModel(params.get(), params.get(), psm.getActiveRepositorySync(), get(), psm.getLocalFilePath(), psm) } factory { params -> val psm = get() diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt index 55592e4..bc781ab 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsButton.kt @@ -38,7 +38,8 @@ import app.morphe.gui.ui.theme.LocalThemeState @Composable fun SettingsButton( modifier: Modifier = Modifier, - allowCacheClear: Boolean = true + allowCacheClear: Boolean = true, + isPatching: Boolean = false ) { val corners = LocalMorpheCorners.current val themeState = LocalThemeState.current @@ -104,6 +105,7 @@ fun SettingsButton( }, onDismiss = { showSettingsDialog = false }, allowCacheClear = allowCacheClear, + isPatching = isPatching, patchSources = patchSources, activePatchSourceId = activePatchSourceId, onActivePatchSourceChange = { id -> @@ -140,6 +142,9 @@ fun SettingsButton( scope.launch { configRepository.removePatchSource(id) } + }, + onCacheCleared = { + patchSourceManager.notifyCacheCleared() } ) } @@ -149,6 +154,7 @@ fun SettingsButton( fun TopBarRow( modifier: Modifier = Modifier, allowCacheClear: Boolean = true, + isPatching: Boolean = false, ) { val corners = LocalMorpheCorners.current val isSoft = corners.small >= 8.dp @@ -158,6 +164,6 @@ fun TopBarRow( verticalAlignment = Alignment.CenterVertically ) { DeviceIndicator() - SettingsButton(allowCacheClear = allowCacheClear) + SettingsButton(allowCacheClear = allowCacheClear, isPatching = isPatching) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 5f71c24..851281b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -50,12 +50,14 @@ fun SettingsDialog( onExpertModeChange: (Boolean) -> Unit, onDismiss: () -> Unit, allowCacheClear: Boolean = true, + isPatching: Boolean = false, patchSources: List = emptyList(), activePatchSourceId: String = "", onActivePatchSourceChange: (String) -> Unit = {}, onAddPatchSource: (PatchSource) -> Unit = {}, onEditPatchSource: (PatchSource) -> Unit = {}, - onRemovePatchSource: (String) -> Unit = {} + onRemovePatchSource: (String) -> Unit = {}, + onCacheCleared: () -> Unit = {} ) { val corners = LocalMorpheCorners.current val mono = LocalMorpheFont.current @@ -151,7 +153,8 @@ fun SettingsDialog( checked = useExpertMode, onCheckedChange = onExpertModeChange, accentColor = MorpheColors.Blue, - mono = mono + mono = mono, + enabled = !isPatching ) SettingsDivider(borderColor) @@ -163,7 +166,8 @@ fun SettingsDialog( checked = autoCleanupTempFiles, onCheckedChange = onAutoCleanupChange, accentColor = MorpheColors.Teal, - mono = mono + mono = mono, + enabled = !isPatching ) SettingsDivider(borderColor) @@ -180,7 +184,8 @@ fun SettingsDialog( onEdit = { source -> editingSource = source }, onAddClick = { showAddSourceDialog = true }, mono = mono, - borderColor = borderColor + borderColor = borderColor, + enabled = !isPatching ) SettingsDivider(borderColor) @@ -319,6 +324,7 @@ fun SettingsDialog( cacheCleared = success cacheClearFailed = !success showClearCacheConfirm = false + if (success) onCacheCleared() }, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error @@ -401,8 +407,10 @@ private fun SettingToggleRow( checked: Boolean, onCheckedChange: (Boolean) -> Unit, accentColor: Color, - mono: androidx.compose.ui.text.font.FontFamily + mono: androidx.compose.ui.text.font.FontFamily, + enabled: Boolean = true ) { + val alpha = if (enabled) 1f else 0.4f Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -413,20 +421,21 @@ private fun SettingToggleRow( text = label, fontSize = 13.sp, fontWeight = FontWeight.Medium, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface.copy(alpha = alpha) ) Spacer(Modifier.height(2.dp)) Text( - text = description, + text = if (!enabled) "Disabled while patching" else description, fontSize = 11.sp, fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f * alpha) ) } Spacer(Modifier.width(12.dp)) Switch( checked = checked, onCheckedChange = onCheckedChange, + enabled = enabled, colors = SwitchDefaults.colors( checkedThumbColor = accentColor, checkedTrackColor = accentColor.copy(alpha = 0.3f) @@ -493,14 +502,16 @@ private fun PatchSourcesSection( onEdit: (PatchSource) -> Unit, onAddClick: () -> Unit, mono: androidx.compose.ui.text.font.FontFamily, - borderColor: Color + borderColor: Color, + enabled: Boolean = true ) { val corners = LocalMorpheCorners.current + val alpha = if (enabled) 1f else 0.4f Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { SectionLabel("PATCH SOURCES", mono) Spacer(Modifier.height(2.dp)) Text( - text = "Select where patches are loaded from", + text = if (!enabled) "Disabled while patching" else "Select where patches are loaded from", fontSize = 11.sp, fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) @@ -531,7 +542,7 @@ private fun PatchSourcesSection( else Color.Transparent ) .hoverable(hoverInteraction) - .clickable { onActiveChange(source.id) } + .then(if (enabled) Modifier.clickable { onActiveChange(source.id) } else Modifier) .padding(horizontal = 12.dp, vertical = 10.dp) ) { Row(verticalAlignment = Alignment.CenterVertically) { @@ -568,7 +579,7 @@ private fun PatchSourcesSection( overflow = TextOverflow.Ellipsis ) } - if (source.deletable) { + if (source.deletable && enabled) { IconButton( onClick = { onEdit(source) }, modifier = Modifier.size(24.dp) @@ -601,6 +612,7 @@ private fun PatchSourcesSection( // Add source OutlinedButton( onClick = onAddClick, + enabled = enabled, modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(corners.small), border = BorderStroke(1.dp, borderColor), @@ -1055,11 +1067,11 @@ private fun clearAllCache(): Boolean { return try { var failedCount = 0 FileUtils.getPatchesDir().listFiles()?.forEach { file -> - try { java.nio.file.Files.delete(file.toPath()) } + try { if (!file.deleteRecursively()) throw Exception("Could not delete") } catch (e: Exception) { failedCount++; Logger.error("Failed to delete ${file.name}: ${e.message}") } } FileUtils.getLogsDir().listFiles()?.forEach { file -> - try { java.nio.file.Files.delete(file.toPath()) } + try { if (!file.deleteRecursively()) throw Exception("Could not delete") } catch (e: Exception) { failedCount++; Logger.error("Failed to delete log ${file.name}: ${e.message}") } } FileUtils.cleanupAllTempDirs() diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index ff7f94c..b02a83b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -170,16 +170,9 @@ fun HomeScreenContent( HeaderBar( uiState = uiState, isSmall = isSmall, - padding = padding, onChangePatchesClick = onChangePatchesClick, onRetry = onRetry ) - Box( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(MaterialTheme.colorScheme.outline.copy(alpha = 0.08f)) - ) } else { Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) BrandingSection(isCompact = isCompact) @@ -198,6 +191,17 @@ fun HomeScreenContent( } else if (uiState.isLoadingPatches) { Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) PatchesLoadingIndicator() + } else if (uiState.patchLoadError != null) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + PatchesVersionCard( + patchesVersion = "NOT LOADED", + isLatest = false, + onChangePatchesClick = onChangePatchesClick, + isCompact = isCompact, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + ) } if (uiState.isOffline && !uiState.isLoadingPatches) { @@ -309,22 +313,30 @@ private fun handleContinue( private fun HeaderBar( uiState: HomeUiState, isSmall: Boolean, - padding: Dp, onChangePatchesClick: () -> Unit, onRetry: () -> Unit ) { val mono = LocalMorpheFont.current val titleInsets = LocalTitleBarInsets.current + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.10f) DraggableHeaderArea { Row( modifier = Modifier .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f + ) + } .padding( - start = padding + titleInsets.start, - end = padding, - top = (if (isSmall) 8.dp else 10.dp) + titleInsets.top, - bottom = if (isSmall) 8.dp else 10.dp + start = 12.dp + titleInsets.start, + end = 12.dp, + top = 8.dp + titleInsets.top, + bottom = 8.dp ), verticalAlignment = Alignment.CenterVertically ) { @@ -342,6 +354,12 @@ private fun HeaderBar( ) } else if (uiState.isLoadingPatches) { PatchesLoadingIndicator() + } else if (uiState.patchLoadError != null) { + PatchesVersionInline( + patchesVersion = "NOT LOADED", + isLatest = false, + onChangePatchesClick = onChangePatchesClick + ) } // Offline badge diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 5fb0335..10f9c56 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -157,9 +157,15 @@ class HomeViewModel( val patches = patchesResult.getOrNull() if (patches == null || patches.isEmpty()) { + val rawError = patchesResult.exceptionOrNull()?.message ?: "Unknown error" + val friendlyError = if (rawError.contains("zip", ignoreCase = true) || rawError.contains("END header", ignoreCase = true)) { + "Patch file is missing or corrupted. Clear cache and re-download." + } else { + "Could not load patches: $rawError" + } _uiState.value = _uiState.value.copy( isLoadingPatches = false, - patchLoadError = "Could not load patches: ${patchesResult.exceptionOrNull()?.message}" + patchLoadError = friendlyError ) return@launch } @@ -245,9 +251,15 @@ class HomeViewModel( val patches = patchesResult.getOrNull() if (patches == null || patches.isEmpty()) { + val rawError = patchesResult.exceptionOrNull()?.message ?: "Unknown error" + val friendlyError = if (rawError.contains("zip", ignoreCase = true) || rawError.contains("END header", ignoreCase = true)) { + "Patch file is missing or corrupted. Clear cache and re-download." + } else { + "Could not load patches: $rawError" + } _uiState.value = _uiState.value.copy( isLoadingPatches = false, - patchLoadError = "Could not load patches: ${patchesResult.exceptionOrNull()?.message}" + patchLoadError = friendlyError ) return } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index 942df4b..86abebd 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -240,18 +240,6 @@ fun ApkInfoCard( ) } } - // Hint text when device arch is highlighted - if (highlightArch != null) { - Spacer(Modifier.width(2.dp)) - Text( - text = "DEVICE", - fontSize = 8.sp, - fontWeight = FontWeight.Bold, - fontFamily = mono, - color = MorpheColors.Blue.copy(alpha = 0.5f), - letterSpacing = 1.sp - ) - } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 2a82027..1eea91d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -48,6 +48,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.Patch import org.koin.core.parameter.parametersOf +import app.morphe.gui.ui.components.DraggableHeaderArea import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.ErrorDialog import app.morphe.gui.ui.components.DeviceIndicator @@ -127,14 +128,23 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { Column(modifier = Modifier.fillMaxSize()) { // ── Header bar ── val titleInsets = LocalTitleBarInsets.current + DraggableHeaderArea { Row( modifier = Modifier .fillMaxWidth() + .drawBehind { + drawLine( + color = dividerColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f + ) + } .padding( - start = 16.dp + titleInsets.start, - end = 16.dp, - top = 12.dp + titleInsets.top, - bottom = 12.dp + start = 12.dp + titleInsets.start, + end = 12.dp, + top = 8.dp + titleInsets.top, + bottom = 8.dp ), verticalAlignment = Alignment.CenterVertically ) { @@ -167,21 +177,26 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { Spacer(modifier = Modifier.width(14.dp)) // Title block - Column(modifier = Modifier.weight(1f)) { + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.spacedBy(0.dp) + ) { Text( text = "SELECT PATCHES", fontSize = 14.sp, fontWeight = FontWeight.Bold, fontFamily = mono, color = MaterialTheme.colorScheme.onSurface, - letterSpacing = 1.5.sp + letterSpacing = 1.5.sp, + lineHeight = 14.sp ) Text( text = "${uiState.selectedCount} of ${uiState.totalCount} selected", fontSize = 10.sp, fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - letterSpacing = 0.3.sp + letterSpacing = 0.3.sp, + lineHeight = 8.sp ) } @@ -317,14 +332,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { Spacer(modifier = Modifier.width(6.dp)) SettingsButton(allowCacheClear = false) } - - // Divider - Box( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(dividerColor) - ) + } // Command preview — collapsible if (!uiState.isLoading && uiState.allPatches.isNotEmpty()) { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index a696c7d..8cbed4d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -37,6 +37,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import app.morphe.gui.data.model.Release import org.koin.core.parameter.parametersOf import cafe.adriel.voyager.koin.koinScreenModel +import app.morphe.gui.ui.components.DraggableHeaderArea import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.ErrorDialog import app.morphe.gui.ui.components.DeviceIndicator @@ -108,14 +109,23 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { ) { // ── Header bar ── val titleInsets = LocalTitleBarInsets.current + DraggableHeaderArea { Row( modifier = Modifier .fillMaxWidth() + .drawBehind { + drawLine( + color = dividerColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f + ) + } .padding( - start = 16.dp + titleInsets.start, - end = 16.dp, - top = 12.dp + titleInsets.top, - bottom = 12.dp + start = 12.dp + titleInsets.start, + end = 12.dp, + top = 8.dp + titleInsets.top, + bottom = 8.dp ), verticalAlignment = Alignment.CenterVertically ) { @@ -155,7 +165,8 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { fontWeight = FontWeight.Bold, fontFamily = mono, color = MaterialTheme.colorScheme.onSurface, - letterSpacing = 1.5.sp + letterSpacing = 1.5.sp, + lineHeight = 14.sp ) if (viewModel.getApkName().isNotBlank()) { Text( @@ -165,7 +176,8 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), maxLines = 1, overflow = TextOverflow.Ellipsis, - letterSpacing = 0.3.sp + letterSpacing = 0.3.sp, + lineHeight = 8.sp ) } } @@ -207,14 +219,7 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { Spacer(modifier = Modifier.width(6.dp)) SettingsButton(allowCacheClear = true) } - - // Divider - Box( - modifier = Modifier - .fillMaxWidth() - .height(1.dp) - .background(dividerColor) - ) + } // ── Content area ── Column(modifier = Modifier.fillMaxSize()) { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt index da9728c..a328358 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesViewModel.kt @@ -7,6 +7,8 @@ import cafe.adriel.voyager.core.model.screenModelScope import app.morphe.gui.data.model.Release import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchRepository +import app.morphe.gui.data.repository.PatchSourceManager +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,7 +25,8 @@ class PatchesViewModel( private val apkName: String, private val patchRepository: PatchRepository, private val configRepository: ConfigRepository, - private val localPatchFilePath: String? = null + private val localPatchFilePath: String? = null, + private val patchSourceManager: PatchSourceManager? = null ) : ScreenModel { private val _uiState = MutableStateFlow(PatchesUiState()) @@ -31,6 +34,17 @@ class PatchesViewModel( init { loadReleases() + + // Observe cache clears / source changes + patchSourceManager?.let { psm -> + screenModelScope.launch { + psm.sourceVersion.drop(1).collect { + Logger.info("PatchesVM: Source changed, reloading...") + _uiState.value = PatchesUiState() + loadReleases() + } + } + } } fun loadReleases() { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt index b6cbe9a..d0b39bd 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt @@ -210,7 +210,7 @@ fun PatchingScreenContent(viewModel: PatchingViewModel) { Spacer(Modifier.width(8.dp)) } - TopBarRow(allowCacheClear = false) + TopBarRow(allowCacheClear = false, isPatching = true) } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 0ae510a..5acbde8 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -31,6 +31,7 @@ import app.morphe.morphe_cli.generated.resources.morphe_light import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchSourceManager +import app.morphe.gui.ui.components.DraggableHeaderArea import app.morphe.gui.ui.components.OfflineBanner import app.morphe.gui.ui.components.LocalTitleBarInsets import app.morphe.gui.ui.components.TopBarRow @@ -66,6 +67,11 @@ class QuickPatchScreen : Screen { fun QuickPatchContent(viewModel: QuickPatchViewModel) { val uiState by viewModel.uiState.collectAsState() + val titleInsets = LocalTitleBarInsets.current + val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.10f) + FullScreenDropZone( isDragHovering = uiState.isDragHovering, onDragHoverChange = { viewModel.setDragHover(it) }, @@ -79,99 +85,122 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ) { Box(modifier = Modifier.fillMaxSize()) { Column( - modifier = Modifier - .fillMaxSize() - .padding(24.dp), - horizontalAlignment = Alignment.CenterHorizontally + modifier = Modifier.fillMaxSize() ) { - // ── Branding ── - Spacer(modifier = Modifier.height(8.dp)) - BrandingHeader(patchesVersion = uiState.patchesVersion, isLoading = uiState.isLoadingPatches) + // ── Header row — matches expert mode ── + DraggableHeaderArea { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(0f, size.height), + end = Offset(size.width, size.height), + strokeWidth = 1f + ) + } + .padding( + start = 12.dp + titleInsets.start, + end = 12.dp, + top = 8.dp + titleInsets.top, + bottom = 8.dp + ), + verticalAlignment = Alignment.CenterVertically + ) { + BrandingHeader( + patchesVersion = uiState.patchesVersion, + isLoading = uiState.isLoadingPatches + ) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.weight(1f)) - // Offline banner - if (uiState.isOffline && uiState.phase == QuickPatchPhase.IDLE) { - OfflineBanner( - onRetry = { viewModel.retryLoadPatches() }, - modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) - ) + TopBarRow( + allowCacheClear = false, + isPatching = uiState.phase == QuickPatchPhase.DOWNLOADING || uiState.phase == QuickPatchPhase.PATCHING + ) + } } - // ── Main content ── - val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } - val lastOutputPath = remember(uiState.outputPath) { uiState.outputPath } - - AnimatedContent( - targetState = uiState.phase, - modifier = Modifier.weight(1f), - transitionSpec = { - fadeIn(tween(200)) togetherWith fadeOut(tween(200)) + // ── Content ── + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Offline banner + if (uiState.isOffline && uiState.phase == QuickPatchPhase.IDLE) { + OfflineBanner( + onRetry = { viewModel.retryLoadPatches() }, + modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp) + ) } - ) { phase -> - when (phase) { - QuickPatchPhase.IDLE, QuickPatchPhase.ANALYZING -> { - IdleContent( - isAnalyzing = phase == QuickPatchPhase.ANALYZING, - isDragHovering = uiState.isDragHovering, - onBrowse = { openFilePicker()?.let { viewModel.onFileSelected(it) } } - ) + + // ── Main content ── + val lastApkInfo = remember(uiState.apkInfo) { uiState.apkInfo } + val lastOutputPath = remember(uiState.outputPath) { uiState.outputPath } + + AnimatedContent( + targetState = uiState.phase, + modifier = Modifier.weight(1f), + transitionSpec = { + fadeIn(tween(200)) togetherWith fadeOut(tween(200)) } - QuickPatchPhase.READY -> { - val info = uiState.apkInfo ?: lastApkInfo - if (info != null) { - ReadyContent( - apkInfo = info, - onPatch = { viewModel.startPatching() }, - onClear = { viewModel.reset() } + ) { phase -> + when (phase) { + QuickPatchPhase.IDLE, QuickPatchPhase.ANALYZING -> { + IdleContent( + isAnalyzing = phase == QuickPatchPhase.ANALYZING, + isDragHovering = uiState.isDragHovering, + onBrowse = { openFilePicker()?.let { viewModel.onFileSelected(it) } } ) } - } - QuickPatchPhase.DOWNLOADING, QuickPatchPhase.PATCHING -> { - PatchingContent( - phase = phase, - statusMessage = uiState.statusMessage, - onCancel = { viewModel.cancelPatching() } - ) - } - QuickPatchPhase.COMPLETED -> { - val info = uiState.apkInfo ?: lastApkInfo - val output = uiState.outputPath ?: lastOutputPath - if (info != null && output != null) { - CompletedContent( - outputPath = output, - apkInfo = info, - onPatchAnother = { viewModel.reset() } + QuickPatchPhase.READY -> { + val info = uiState.apkInfo ?: lastApkInfo + if (info != null) { + ReadyContent( + apkInfo = info, + onPatch = { viewModel.startPatching() }, + onClear = { viewModel.reset() } + ) + } + } + QuickPatchPhase.DOWNLOADING, QuickPatchPhase.PATCHING -> { + PatchingContent( + phase = phase, + statusMessage = uiState.statusMessage, + onCancel = { viewModel.cancelPatching() } ) } + QuickPatchPhase.COMPLETED -> { + val info = uiState.apkInfo ?: lastApkInfo + val output = uiState.outputPath ?: lastOutputPath + if (info != null && output != null) { + CompletedContent( + outputPath = output, + apkInfo = info, + onPatchAnother = { viewModel.reset() } + ) + } + } } } - } - // ── Supported apps (idle only) ── - if (uiState.phase == QuickPatchPhase.IDLE) { - Spacer(modifier = Modifier.height(16.dp)) - SupportedAppsRow( - supportedApps = uiState.supportedApps, - isLoading = uiState.isLoadingPatches, - loadError = uiState.patchLoadError, - isDefaultSource = uiState.isDefaultSource, - onRetry = { viewModel.retryLoadPatches() } - ) + // ── Supported apps (idle only) ── + if (uiState.phase == QuickPatchPhase.IDLE) { + Spacer(modifier = Modifier.height(16.dp)) + SupportedAppsRow( + supportedApps = uiState.supportedApps, + isLoading = uiState.isLoadingPatches, + loadError = uiState.patchLoadError, + isDefaultSource = uiState.isDefaultSource, + onRetry = { viewModel.retryLoadPatches() } + ) + } } } - // Top-right: device indicator + settings - val titleInsets = LocalTitleBarInsets.current - TopBarRow( - modifier = Modifier - .align(Alignment.TopEnd) - .padding( - top = 24.dp + titleInsets.top, - end = 24.dp - ) - ) - // Drag overlay if (uiState.isDragHovering) { DragOverlay() @@ -190,7 +219,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { }, containerColor = MaterialTheme.colorScheme.errorContainer, contentColor = MaterialTheme.colorScheme.onErrorContainer, - shape = RoundedCornerShape(LocalMorpheCorners.current.small) + shape = RoundedCornerShape(corners.small) ) { Text(error) } @@ -206,21 +235,22 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { @Composable private fun BrandingHeader(patchesVersion: String?, isLoading: Boolean) { val themeState = LocalThemeState.current + val mono = LocalMorpheFont.current val isDark = when (themeState.current) { ThemePreference.SYSTEM -> isSystemInDarkTheme() else -> themeState.current.isDark() } - Image( - painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), - contentDescription = "Morphe Logo", - modifier = Modifier.height(48.dp) - ) + Row(verticalAlignment = Alignment.CenterVertically) { + Image( + painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), + contentDescription = "Morphe Logo", + modifier = Modifier.height(28.dp) + ) - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.width(12.dp)) - if (isLoading) { - Row(verticalAlignment = Alignment.CenterVertically) { + if (isLoading) { CircularProgressIndicator( modifier = Modifier.size(12.dp), strokeWidth = 1.5.dp, @@ -229,23 +259,28 @@ private fun BrandingHeader(patchesVersion: String?, isLoading: Boolean) { Spacer(modifier = Modifier.width(6.dp)) Text( text = "Loading patches…", - fontSize = 12.sp, + fontSize = 11.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) ) + } else if (patchesVersion != null) { + Text( + text = "Patches $patchesVersion", + fontSize = 11.sp, + fontFamily = mono, + color = MorpheColors.Blue.copy(alpha = 0.8f), + fontWeight = FontWeight.Medium + ) + } else { + Text( + text = "QUICK PATCH", + fontSize = 11.sp, + fontFamily = mono, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurfaceVariant, + letterSpacing = 1.sp + ) } - } else if (patchesVersion != null) { - Text( - text = "Patches $patchesVersion", - fontSize = 12.sp, - color = MorpheColors.Blue.copy(alpha = 0.8f), - fontWeight = FontWeight.Medium - ) - } else { - Text( - text = "Quick Patch", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) } } @@ -344,6 +379,15 @@ private fun ReadyContent( onClear: () -> Unit ) { val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f) + + val accentColor = when { + apkInfo.checksumStatus is ChecksumStatus.Mismatch -> MaterialTheme.colorScheme.error + apkInfo.isRecommendedVersion -> MorpheColors.Teal + !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> Color(0xFFFF9800) + else -> MorpheColors.Blue + } Column( modifier = Modifier.fillMaxWidth(), @@ -352,138 +396,192 @@ private fun ReadyContent( ) { Spacer(modifier = Modifier.weight(1f)) - // Simple APK info card - Surface( - modifier = Modifier.fillMaxWidth(), - shape = RoundedCornerShape(corners.medium), - color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), - border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.15f)) + // APK info card — bordered box with accent stripe + Box( + modifier = Modifier + .widthIn(max = 480.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) + .background(MaterialTheme.colorScheme.surface) ) { - Row( + // Left accent stripe + Box( + modifier = Modifier + .width(3.dp) + .fillMaxHeight() + .background(accentColor) + .align(Alignment.CenterStart) + ) + + Column( modifier = Modifier .fillMaxWidth() - .padding(16.dp), - verticalAlignment = Alignment.CenterVertically + .padding(start = 3.dp) ) { - // App initial - Box( + // Header: app identity + dismiss + Row( modifier = Modifier - .size(44.dp) - .clip(RoundedCornerShape(corners.small)) - .background(MorpheColors.Blue.copy(alpha = 0.1f)), - contentAlignment = Alignment.Center + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalAlignment = Alignment.CenterVertically ) { - Text( - text = apkInfo.displayName.first().uppercase(), - fontSize = 18.sp, - fontWeight = FontWeight.Bold, - color = MorpheColors.Blue - ) - } - - Spacer(modifier = Modifier.width(14.dp)) + // App initial + Box( + modifier = Modifier + .size(44.dp) + .border(1.dp, accentColor.copy(alpha = 0.5f), RoundedCornerShape(corners.small)) + .background(accentColor.copy(alpha = 0.08f)), + contentAlignment = Alignment.Center + ) { + Text( + text = apkInfo.displayName.first().uppercase(), + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = accentColor + ) + } - Column(modifier = Modifier.weight(1f)) { - Text( - text = apkInfo.displayName, - fontSize = 16.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - Text( - text = "v${apkInfo.versionName} · ${apkInfo.formattedSize}", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) - ) - } + Spacer(modifier = Modifier.width(14.dp)) - // Checksum badge - when (apkInfo.checksumStatus) { - is ChecksumStatus.Verified -> { - Icon( - imageVector = Icons.Default.VerifiedUser, - contentDescription = "Verified", - tint = MorpheColors.Teal, - modifier = Modifier.size(20.dp) + Column(modifier = Modifier.weight(1f)) { + Text( + text = apkInfo.displayName, + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = "v${apkInfo.versionName} · ${apkInfo.formattedSize}", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + letterSpacing = 0.3.sp ) } - is ChecksumStatus.Mismatch -> { + + // Dismiss button + val closeHover = remember { MutableInteractionSource() } + val isCloseHovered by closeHover.collectIsHoveredAsState() + val closeBg by animateColorAsState( + if (isCloseHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.12f) + else Color.Transparent, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .size(36.dp) + .hoverable(closeHover) + .clip(RoundedCornerShape(corners.small)) + .background(closeBg) + .clickable(onClick = onClear), + contentAlignment = Alignment.Center + ) { Icon( - imageVector = Icons.Default.Warning, - contentDescription = "Mismatch", - tint = MaterialTheme.colorScheme.error, - modifier = Modifier.size(20.dp) + imageVector = Icons.Default.Close, + contentDescription = "Clear", + tint = if (isCloseHovered) MaterialTheme.colorScheme.error + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + modifier = Modifier.size(16.dp) ) } - else -> {} } - Spacer(modifier = Modifier.width(8.dp)) + // Status bar + val statusText = when { + apkInfo.checksumStatus is ChecksumStatus.Verified -> "VERIFIED" + apkInfo.checksumStatus is ChecksumStatus.Mismatch -> "CHECKSUM MISMATCH" + !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> "OUTDATED" + apkInfo.isRecommendedVersion -> "RECOMMENDED VERSION" + else -> null + } + val statusDetail = when { + apkInfo.checksumStatus is ChecksumStatus.Verified -> "Checksum matches APKMirror" + apkInfo.checksumStatus is ChecksumStatus.Mismatch -> "Re-download from APKMirror" + !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> + "Patches target v${apkInfo.recommendedVersion}" + else -> null + } - IconButton(onClick = onClear, modifier = Modifier.size(32.dp)) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = "Clear", - tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - modifier = Modifier.size(16.dp) - ) + if (statusText != null) { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .background(accentColor.copy(alpha = 0.04f)) + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(6.dp) + .background(accentColor, RoundedCornerShape(1.dp)) + ) + Spacer(Modifier.width(10.dp)) + Text( + text = statusText, + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = accentColor, + letterSpacing = 1.sp + ) + if (statusDetail != null) { + Spacer(Modifier.width(12.dp)) + Text( + text = statusDetail, + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } } } } - // Version status (only if noteworthy) - val statusText = when { - apkInfo.checksumStatus is ChecksumStatus.Verified -> - "Recommended version · Verified" - apkInfo.checksumStatus is ChecksumStatus.Mismatch -> - "Checksum mismatch — re-download from APKMirror" - !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> - "Recommended: v${apkInfo.recommendedVersion}" - apkInfo.isRecommendedVersion -> - "Recommended version" - else -> null - } - val statusColor = when { - apkInfo.checksumStatus is ChecksumStatus.Verified -> MorpheColors.Teal - apkInfo.checksumStatus is ChecksumStatus.Mismatch -> MaterialTheme.colorScheme.error - !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> Color(0xFFFF9800) - else -> MorpheColors.Teal - } - - if (statusText != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = statusText, - fontSize = 12.sp, - fontWeight = FontWeight.Medium, - color = statusColor, - textAlign = TextAlign.Center - ) - } - Spacer(modifier = Modifier.height(20.dp)) // Patch button - Button( - onClick = onPatch, + val patchHover = remember { MutableInteractionSource() } + val isPatchHovered by patchHover.collectIsHoveredAsState() + val patchBg by animateColorAsState( + if (isPatchHovered) MorpheColors.Blue.copy(alpha = 0.9f) else MorpheColors.Blue, + animationSpec = tween(150) + ) + + Box( modifier = Modifier + .widthIn(max = 480.dp) .fillMaxWidth() - .height(50.dp), - colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), - shape = RoundedCornerShape(corners.medium) + .height(46.dp) + .hoverable(patchHover) + .clip(RoundedCornerShape(corners.small)) + .background(patchBg, RoundedCornerShape(corners.small)) + .clickable(onClick = onPatch), + contentAlignment = Alignment.Center ) { - Icon( - imageVector = Icons.Default.AutoFixHigh, - contentDescription = null, - modifier = Modifier.size(20.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) Text( - text = "Patch with Defaults", - fontSize = 15.sp, - fontWeight = FontWeight.SemiBold + text = "PATCH WITH DEFAULTS", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 1.sp ) } @@ -492,6 +590,7 @@ private fun ReadyContent( Text( text = "Uses latest patches with recommended settings", fontSize = 11.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), textAlign = TextAlign.Center ) @@ -510,35 +609,41 @@ private fun PatchingContent( statusMessage: String, onCancel: () -> Unit ) { + val mono = LocalMorpheFont.current + val corners = LocalMorpheCorners.current + Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { CircularProgressIndicator( - modifier = Modifier.size(56.dp), + modifier = Modifier.size(48.dp), strokeWidth = 3.dp, color = MorpheColors.Teal ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(20.dp)) Text( text = when (phase) { - QuickPatchPhase.DOWNLOADING -> "Preparing…" - QuickPatchPhase.PATCHING -> "Patching…" + QuickPatchPhase.DOWNLOADING -> "PREPARING" + QuickPatchPhase.PATCHING -> "PATCHING" else -> "" }, - fontSize = 17.sp, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface + fontSize = 13.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp ) Spacer(modifier = Modifier.height(8.dp)) Text( text = statusMessage, - fontSize = 12.sp, + fontSize = 11.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -546,10 +651,31 @@ private fun PatchingContent( modifier = Modifier.padding(horizontal = 24.dp) ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(20.dp)) + + val cancelHover = remember { MutableInteractionSource() } + val isCancelHovered by cancelHover.collectIsHoveredAsState() + val cancelBg by animateColorAsState( + if (isCancelHovered) MaterialTheme.colorScheme.error.copy(alpha = 0.1f) else Color.Transparent, + animationSpec = tween(150) + ) - TextButton(onClick = onCancel) { - Text("Cancel", color = MaterialTheme.colorScheme.error) + Box( + modifier = Modifier + .hoverable(cancelHover) + .clip(RoundedCornerShape(corners.small)) + .background(cancelBg) + .clickable(onClick = onCancel) + .padding(horizontal = 16.dp, vertical = 6.dp) + ) { + Text( + text = "CANCEL", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.error, + letterSpacing = 0.5.sp + ) } } } @@ -565,6 +691,8 @@ private fun CompletedContent( onPatchAnother: () -> Unit ) { val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current + val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f) val outputFile = File(outputPath) val scope = rememberCoroutineScope() val adbManager = remember { AdbManager() } @@ -580,152 +708,277 @@ private fun CompletedContent( horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { - Icon( - imageVector = Icons.Default.CheckCircle, - contentDescription = "Success", - tint = MorpheColors.Teal, - modifier = Modifier.size(56.dp) + // Success indicator + Box( + modifier = Modifier + .size(8.dp) + .background(MorpheColors.Teal, RoundedCornerShape(2.dp)) ) - - Spacer(modifier = Modifier.height(16.dp)) - + Spacer(modifier = Modifier.height(12.dp)) Text( - text = "Patching Complete!", - fontSize = 20.sp, + text = "PATCHING COMPLETE", + fontSize = 13.sp, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp ) - Spacer(modifier = Modifier.height(8.dp)) - - Text( - text = outputFile.name, - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - textAlign = TextAlign.Center, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - modifier = Modifier.padding(horizontal = 16.dp) - ) + Spacer(modifier = Modifier.height(20.dp)) - if (outputFile.exists()) { - Text( - text = formatFileSize(outputFile.length()), - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal + // Output file card + Box( + modifier = Modifier + .widthIn(max = 480.dp) + .fillMaxWidth() + .clip(RoundedCornerShape(corners.medium)) + .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) + .background(MaterialTheme.colorScheme.surface) + ) { + Box( + modifier = Modifier + .width(3.dp) + .fillMaxHeight() + .background(MorpheColors.Teal) + .align(Alignment.CenterStart) ) - } - - Spacer(modifier = Modifier.height(24.dp)) - // Action buttons - Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { - OutlinedButton( - onClick = { - try { - val folder = outputFile.parentFile - if (folder != null && Desktop.isDesktopSupported()) { - Desktop.getDesktop().open(folder) - } - } catch (_: Exception) {} - }, - shape = RoundedCornerShape(corners.small) + Column( + modifier = Modifier + .fillMaxWidth() + .padding(start = 3.dp) ) { - Icon(Icons.Default.FolderOpen, null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(6.dp)) - Text("Open Folder") - } + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 14.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = "OUTPUT FILE", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(Modifier.height(4.dp)) + Text( + text = outputFile.name, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + if (outputFile.exists()) { + Text( + text = formatFileSize(outputFile.length()), + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal + ) + } + } - Button( - onClick = onPatchAnother, - colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Blue), - shape = RoundedCornerShape(corners.small) - ) { - Text("Patch Another") + // Open folder link + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val folderHover = remember { MutableInteractionSource() } + val isFolderHovered by folderHover.collectIsHoveredAsState() + val folderColor by animateColorAsState( + if (isFolderHovered) MorpheColors.Blue else MorpheColors.Blue.copy(alpha = 0.6f), + animationSpec = tween(150) + ) + Box( + modifier = Modifier + .hoverable(folderHover) + .clip(RoundedCornerShape(corners.small)) + .clickable { + try { + val folder = outputFile.parentFile + if (folder != null && Desktop.isDesktopSupported()) { + Desktop.getDesktop().open(folder) + } + } catch (_: Exception) {} + } + .padding(vertical = 2.dp) + ) { + Text( + text = "OPEN FOLDER →", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = folderColor, + letterSpacing = 0.5.sp + ) + } + } } } // ADB install if (monitorState.isAdbAvailable == true) { - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(12.dp)) val readyDevices = monitorState.devices.filter { it.isReady } val selectedDevice = monitorState.selectedDevice - if (installSuccess) { - Surface( - color = MorpheColors.Teal.copy(alpha = 0.1f), - shape = RoundedCornerShape(corners.small) - ) { - Text( - text = "Installed successfully!", - fontSize = 13.sp, - color = MorpheColors.Teal, - fontWeight = FontWeight.Medium, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) - ) + when { + installSuccess -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Box( + modifier = Modifier + .size(6.dp) + .background(MorpheColors.Teal, RoundedCornerShape(1.dp)) + ) + Spacer(Modifier.width(8.dp)) + Text( + text = "INSTALLED ON ${(selectedDevice?.displayName ?: "DEVICE").uppercase()}", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 0.5.sp + ) + } } - } else if (isInstalling) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.Center - ) { - CircularProgressIndicator( - modifier = Modifier.size(16.dp), - strokeWidth = 2.dp, - color = MorpheColors.Blue + isInstalling -> { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + CircularProgressIndicator( + modifier = Modifier.size(14.dp), + strokeWidth = 2.dp, + color = MorpheColors.Blue + ) + Spacer(Modifier.width(8.dp)) + Text( + text = "INSTALLING…", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Blue, + letterSpacing = 0.5.sp + ) + } + } + readyDevices.isNotEmpty() -> { + val device = selectedDevice ?: readyDevices.first() + val installHover = remember { MutableInteractionSource() } + val isInstallHovered by installHover.collectIsHoveredAsState() + val installBg by animateColorAsState( + if (isInstallHovered) MorpheColors.Teal.copy(alpha = 0.9f) else MorpheColors.Teal, + animationSpec = tween(150) ) - Spacer(modifier = Modifier.width(8.dp)) + + Box( + modifier = Modifier + .widthIn(max = 480.dp) + .fillMaxWidth() + .height(38.dp) + .hoverable(installHover) + .clip(RoundedCornerShape(corners.small)) + .background(installBg, RoundedCornerShape(corners.small)) + .clickable { + scope.launch { + isInstalling = true + installError = null + val result = adbManager.installApk( + apkPath = outputPath, + deviceId = device.id + ) + result.fold( + onSuccess = { installSuccess = true }, + onFailure = { installError = it.message } + ) + isInstalling = false + } + }, + contentAlignment = Alignment.Center + ) { + Text( + text = "INSTALL ON ${device.displayName.uppercase()}", + fontSize = 11.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 0.5.sp + ) + } + } + else -> { Text( - text = "Installing…", - fontSize = 13.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant + text = "Connect a device via USB to install with ADB", + fontSize = 10.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) ) } - } else if (readyDevices.isNotEmpty()) { - val device = selectedDevice ?: readyDevices.first() - Button( - onClick = { - scope.launch { - isInstalling = true - installError = null - val result = adbManager.installApk( - apkPath = outputPath, - deviceId = device.id - ) - result.fold( - onSuccess = { installSuccess = true }, - onFailure = { installError = it.message } - ) - isInstalling = false - } - }, - colors = ButtonDefaults.buttonColors(containerColor = MorpheColors.Teal), - shape = RoundedCornerShape(corners.small) - ) { - Icon(Icons.Default.PhoneAndroid, null, modifier = Modifier.size(18.dp)) - Spacer(modifier = Modifier.width(6.dp)) - Text("Install on ${device.displayName}") - } - } else { - Text( - text = "Connect your device via USB to install with ADB", - fontSize = 12.sp, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) - ) } installError?.let { error -> Spacer(modifier = Modifier.height(8.dp)) Text( text = error, - fontSize = 12.sp, + fontSize = 10.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.error, textAlign = TextAlign.Center ) } } + + Spacer(modifier = Modifier.height(16.dp)) + + // Patch another button + val patchAnotherHover = remember { MutableInteractionSource() } + val isPatchAnotherHovered by patchAnotherHover.collectIsHoveredAsState() + val patchAnotherBg by animateColorAsState( + if (isPatchAnotherHovered) MorpheColors.Blue.copy(alpha = 0.9f) else MorpheColors.Blue, + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .widthIn(max = 480.dp) + .fillMaxWidth() + .height(42.dp) + .hoverable(patchAnotherHover) + .clip(RoundedCornerShape(corners.small)) + .background(patchAnotherBg, RoundedCornerShape(corners.small)) + .clickable(onClick = onPatchAnother), + contentAlignment = Alignment.Center + ) { + Text( + text = "PATCH ANOTHER", + fontSize = 12.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + letterSpacing = 1.sp + ) + } } } @@ -742,6 +995,7 @@ private fun SupportedAppsRow( onRetry: () -> Unit = {} ) { val corners = LocalMorpheCorners.current + val mono = LocalMorpheFont.current val uriHandler = LocalUriHandler.current val focusManager = LocalFocusManager.current @@ -790,16 +1044,35 @@ private fun SupportedAppsRow( ) { Text( text = loadError ?: "Could not load supported apps", - fontSize = 12.sp, + fontSize = 11.sp, + fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(8.dp)) - OutlinedButton( - onClick = onRetry, - shape = RoundedCornerShape(corners.small), - contentPadding = PaddingValues(horizontal = 12.dp, vertical = 4.dp) + val retryHover = remember { MutableInteractionSource() } + val isRetryHovered by retryHover.collectIsHoveredAsState() + Box( + modifier = Modifier + .hoverable(retryHover) + .clip(RoundedCornerShape(corners.small)) + .border( + 1.dp, + MaterialTheme.colorScheme.onSurfaceVariant.copy( + alpha = if (isRetryHovered) 0.3f else 0.12f + ), + RoundedCornerShape(corners.small) + ) + .clickable(onClick = onRetry) + .padding(horizontal = 10.dp, vertical = 4.dp) ) { - Text("Retry", fontSize = 12.sp) + Text( + text = "RETRY", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface, + letterSpacing = 0.5.sp + ) } } } From 408395d0c7bbc3403c2828b741a4d0350f2a6072 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sun, 22 Mar 2026 11:04:20 +0530 Subject: [PATCH 38/49] Minor UI Fixes Added a scrollbar in the patch selection screen. UX improvements in the Settings panel. --- .../gui/ui/components/SettingsDialog.kt | 9 ++- .../screens/patches/PatchSelectionScreen.kt | 76 ++++++++++--------- 2 files changed, 50 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index 851281b..83df472 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -722,12 +722,19 @@ private fun AddPatchSourceDialog( value = url, onValueChange = { url = it; error = null }, label = { Text("Repository URL", fontFamily = mono, fontSize = 11.sp) }, - placeholder = { Text("github.com/owner/repo or morphe.software link", fontFamily = mono, fontSize = 10.sp) }, + placeholder = { Text("github.com/owner/repo", fontFamily = mono, fontSize = 10.sp) }, singleLine = true, textStyle = LocalTextStyle.current.copy(fontFamily = mono, fontSize = 12.sp), modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(corners.small) ) + Text( + "Accepts GitHub URL or morphe.software/add-source link", + fontFamily = mono, + fontSize = 9.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), + letterSpacing = 0.3.sp + ) } PatchSourceType.LOCAL -> { Row( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 1eea91d..c5beb4a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -428,44 +428,52 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { else -> { // Patch list - LazyColumn( - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(6.dp) - ) { - // Architecture selector - val isApkm = viewModel.getApkPath().endsWith(".apkm", ignoreCase = true) - val showArchSelector = !isApkm && - uiState.apkArchitectures.size > 1 && - !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") - if (showArchSelector) { - item(key = "arch_selector") { - ArchitectureSelectorCard( - architectures = uiState.apkArchitectures, - selectedArchitectures = uiState.selectedArchitectures, - onToggleArchitecture = { viewModel.toggleArchitecture(it) } + val lazyListState = androidx.compose.foundation.lazy.rememberLazyListState() + + Box(modifier = Modifier.weight(1f).fillMaxWidth()) { + LazyColumn( + state = lazyListState, + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Architecture selector + val isApkm = viewModel.getApkPath().endsWith(".apkm", ignoreCase = true) + val showArchSelector = !isApkm && + uiState.apkArchitectures.size > 1 && + !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") + if (showArchSelector) { + item(key = "arch_selector") { + ArchitectureSelectorCard( + architectures = uiState.apkArchitectures, + selectedArchitectures = uiState.selectedArchitectures, + onToggleArchitecture = { viewModel.toggleArchitecture(it) } + ) + } + } + + items( + items = uiState.filteredPatches, + key = { it.uniqueId } + ) { patch -> + PatchListItem( + patch = patch, + isSelected = uiState.selectedPatches.contains(patch.uniqueId), + onToggle = { viewModel.togglePatch(patch.uniqueId) }, + getOptionValue = { optionKey, default -> + viewModel.getOptionValue(patch.name, optionKey, default) + }, + onOptionValueChange = { optionKey, value -> + viewModel.setOptionValue(patch.name, optionKey, value) + } ) } } - items( - items = uiState.filteredPatches, - key = { it.uniqueId } - ) { patch -> - PatchListItem( - patch = patch, - isSelected = uiState.selectedPatches.contains(patch.uniqueId), - onToggle = { viewModel.togglePatch(patch.uniqueId) }, - getOptionValue = { optionKey, default -> - viewModel.getOptionValue(patch.name, optionKey, default) - }, - onOptionValueChange = { optionKey, value -> - viewModel.setOptionValue(patch.name, optionKey, value) - } - ) - } + androidx.compose.foundation.VerticalScrollbar( + modifier = Modifier.align(Alignment.CenterEnd).fillMaxHeight(), + adapter = androidx.compose.foundation.rememberScrollbarAdapter(lazyListState) + ) } // ── Bottom action bar ── From b73a3ddc36b26efa22e277f7d054b3b6fb5f19e1 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Mon, 23 Mar 2026 18:28:06 +0530 Subject: [PATCH 39/49] added ability to patch xapk --- .../kotlin/app/morphe/engine/PatchEngine.kt | 6 +-- .../morphe/gui/ui/screens/home/HomeScreen.kt | 6 +-- .../gui/ui/screens/home/HomeViewModel.kt | 32 +++++++------ .../ui/screens/home/components/ApkInfoCard.kt | 35 ++++++++++++++ .../screens/patches/PatchSelectionScreen.kt | 6 +-- .../gui/ui/screens/quick/QuickPatchScreen.kt | 23 +++++---- .../ui/screens/quick/QuickPatchViewModel.kt | 23 +++++---- .../kotlin/app/morphe/gui/util/FileUtils.kt | 47 ++++++++++++++++--- 8 files changed, 130 insertions(+), 48 deletions(-) diff --git a/src/main/kotlin/app/morphe/engine/PatchEngine.kt b/src/main/kotlin/app/morphe/engine/PatchEngine.kt index 259d338..28b7d22 100644 --- a/src/main/kotlin/app/morphe/engine/PatchEngine.kt +++ b/src/main/kotlin/app/morphe/engine/PatchEngine.kt @@ -76,9 +76,9 @@ object PatchEngine { val failedPatches = mutableListOf() try { - // 1. Handle APKM format (split APK bundle) - val actualInputApk = if (config.inputApk.extension.equals("apkm", ignoreCase = true)) { - onProgress("Converting APKM to APK...") + // 1. Handle split APK bundles (APKM, XAPK) + val actualInputApk = if (config.inputApk.extension.lowercase() in setOf("apkm", "xapk")) { + onProgress("Merging split APK bundle...") val mergedApk = File(tempDir, "${config.inputApk.nameWithoutExtension}-merged.apk") val mergerOptions = MergerOptions().apply { inputFile = config.inputApk diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index b02a83b..3f13379 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -625,7 +625,7 @@ private fun DropPromptSection( Spacer(modifier = Modifier.height(12.dp)) Text( - text = ".apk · .apkm", + text = ".apk · .apkm · .xapk", fontSize = 10.sp, fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f), @@ -1481,7 +1481,7 @@ private fun DragOverlay() { ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = ".apk · .apkm", + text = ".apk · .apkm · .xapk", fontSize = 11.sp, fontFamily = mono, color = MorpheColors.Blue.copy(alpha = 0.4f), @@ -1494,7 +1494,7 @@ private fun DragOverlay() { private fun openFilePicker(): File? { val fileDialog = FileDialog(null as Frame?, "Select APK File", FileDialog.LOAD).apply { isMultipleMode = false - setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") } } + setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") || it.endsWith(".xapk") } } isVisible = true } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 10f9c56..bd4f81b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -352,7 +352,7 @@ class HomeViewModel( onFileSelected(apkFile) } else { _uiState.value = _uiState.value.copy( - error = "Please drop a valid .apk or .apkm file", + error = "Please drop a valid .apk, .apkm, or .xapk file", isReady = false ) } @@ -389,7 +389,7 @@ class HomeViewModel( } if (!FileUtils.isApkFile(file)) { - return ApkValidationResult(false, errorMessage = "File must have .apk or .apkm extension") + return ApkValidationResult(false, errorMessage = "File must have .apk, .apkm, or .xapk extension") } if (file.length() < 1024) { @@ -411,11 +411,11 @@ class HomeViewModel( * This works with APKs from any source, not just APKMirror. */ private fun parseApkManifest(file: File): ApkInfo? { - // For .apkm files, extract base.apk first - val isApkm = file.extension.equals("apkm", ignoreCase = true) - val apkToParse = if (isApkm) { - FileUtils.extractBaseApkFromApkm(file) ?: run { - Logger.error("Failed to extract base.apk from APKM: ${file.name}") + // For split APK bundles (.apkm, .xapk), extract base.apk first + val isBundleFormat = FileUtils.isBundleFormat(file) + val apkToParse = if (isBundleFormat) { + FileUtils.extractBaseApkFromBundle(file) ?: run { + Logger.error("Failed to extract base APK from bundle: ${file.name}") return null } } else { @@ -439,13 +439,13 @@ class HomeViewModel( ) if (!isSupported) { - Logger.warn("Unsupported package: $packageName") - return null + Logger.warn("Unsupported package: $packageName — no compatible patches found") } - // Get app display name - prefer dynamic, fallback to hardcoded + // Get app display name - prefer dynamic, fallback to hardcoded, then package name val appName = dynamicSupportedApp?.displayName ?: SupportedApp.getDisplayName(packageName) + ?: packageName // Get recommended version from dynamic patches data (no hardcoded fallback) val suggestedVersion = dynamicSupportedApp?.recommendedVersion @@ -458,8 +458,8 @@ class HomeViewModel( } // Get supported architectures from native libraries - // For .apkm files, scan the original bundle (splits contain the native libs, not base.apk) - val architectures = extractArchitectures(if (isApkm) file else apkToParse) + // For split bundles, scan the original bundle (splits contain the native libs, not base.apk) + val architectures = extractArchitectures(if (isBundleFormat) file else apkToParse) // TODO: Re-enable when checksums are provided via .mpp files val checksumStatus = app.morphe.gui.util.ChecksumStatus.NotConfigured @@ -478,14 +478,15 @@ class HomeViewModel( minSdk = minSdk, suggestedVersion = suggestedVersion, versionStatus = versionStatus, - checksumStatus = checksumStatus + checksumStatus = checksumStatus, + isUnsupportedApp = !isSupported ) } } catch (e: Exception) { Logger.error("Failed to parse APK manifest", e) null } finally { - if (isApkm) apkToParse.delete() + if (isBundleFormat) apkToParse.delete() } } @@ -607,7 +608,8 @@ data class ApkInfo( val minSdk: Int? = null, val suggestedVersion: String? = null, val versionStatus: VersionStatus = VersionStatus.UNKNOWN, - val checksumStatus: app.morphe.gui.util.ChecksumStatus = app.morphe.gui.util.ChecksumStatus.NotConfigured + val checksumStatus: app.morphe.gui.util.ChecksumStatus = app.morphe.gui.util.ChecksumStatus.NotConfigured, + val isUnsupportedApp: Boolean = false ) enum class VersionStatus { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index 86abebd..3dedf1a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -146,6 +147,40 @@ fun ApkInfoCard( } } + // ── Unsupported app warning ── + if (apkInfo.isUnsupportedApp) { + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .background(Color(0xFFE65100).copy(alpha = 0.08f)) + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + val warningOrange = Color(0xFFE65100) + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = warningOrange, + modifier = Modifier.size(16.dp) + ) + Text( + text = "No compatible patches found for this app. You can still proceed, but patching may have no effect.", + fontSize = 11.sp, + color = warningOrange, + lineHeight = 14.sp + ) + } + } + // ── Technical data grid ── Row( modifier = Modifier diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index c5beb4a..e59ab91 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -437,9 +437,9 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(6.dp) ) { - // Architecture selector - val isApkm = viewModel.getApkPath().endsWith(".apkm", ignoreCase = true) - val showArchSelector = !isApkm && + // Architecture selector — disabled for split APK bundles + val isBundleFormat = viewModel.getApkPath().lowercase().let { it.endsWith(".apkm") || it.endsWith(".xapk") } + val showArchSelector = !isBundleFormat && uiState.apkArchitectures.size > 1 && !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") if (showArchSelector) { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 5acbde8..ec7a0ef 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -78,7 +78,8 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { onFilesDropped = { files -> files.firstOrNull { it.name.endsWith(".apk", ignoreCase = true) || - it.name.endsWith(".apkm", ignoreCase = true) + it.name.endsWith(".apkm", ignoreCase = true) || + it.name.endsWith(".xapk", ignoreCase = true) }?.let { viewModel.onFileSelected(it) } }, enabled = uiState.phase != QuickPatchPhase.ANALYZING @@ -206,22 +207,26 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { DragOverlay() } - // Error snackbar + // Error/warning snackbar uiState.error?.let { error -> + val mono = LocalMorpheFont.current + val isUnsupportedWarning = error.contains("not supported in Quick Patch") + val containerColor = if (isUnsupportedWarning) Color(0xFF4A3000) else MaterialTheme.colorScheme.errorContainer + val contentColor = if (isUnsupportedWarning) Color(0xFFFFB74D) else MaterialTheme.colorScheme.onErrorContainer Snackbar( modifier = Modifier .align(Alignment.BottomCenter) - .padding(16.dp), + .padding(horizontal = 24.dp, vertical = 20.dp), action = { TextButton(onClick = { viewModel.clearError() }) { - Text("Dismiss", color = MaterialTheme.colorScheme.inversePrimary) + Text("Dismiss", color = contentColor.copy(alpha = 0.8f), fontFamily = mono, fontSize = 12.sp) } }, - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, + containerColor = containerColor, + contentColor = contentColor, shape = RoundedCornerShape(corners.small) ) { - Text(error) + Text(error, fontFamily = mono, fontSize = 12.sp, lineHeight = 16.sp, modifier = Modifier.padding(vertical = 4.dp)) } } } @@ -359,7 +364,7 @@ private fun IdleContent( ) Spacer(modifier = Modifier.height(6.dp)) Text( - text = ".apk · .apkm", + text = ".apk · .apkm · .xapk", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) ) @@ -1249,7 +1254,7 @@ private fun DragOverlay() { private fun openFilePicker(): File? { val fileDialog = FileDialog(null as Frame?, "Select APK", FileDialog.LOAD).apply { isMultipleMode = false - setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") } } + setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") || it.endsWith(".xapk") } } isVisible = true } val directory = fileDialog.directory diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 7d53546..cf412f5 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -266,16 +266,16 @@ class QuickPatchViewModel( * Analyze the APK file using dynamic data from patches. */ private suspend fun analyzeApk(file: File): QuickApkInfo? { - if (!file.exists() || !(file.name.endsWith(".apk", ignoreCase = true) || file.name.endsWith(".apkm", ignoreCase = true))) { - _uiState.value = _uiState.value.copy(error = "Please drop a valid .apk or .apkm file") + if (!file.exists() || !FileUtils.isApkFile(file)) { + _uiState.value = _uiState.value.copy(error = "Please drop a valid .apk, .apkm, or .xapk file") return null } - // For .apkm files, extract base.apk first - val isApkm = file.extension.equals("apkm", ignoreCase = true) - val apkToParse = if (isApkm) { - FileUtils.extractBaseApkFromApkm(file) ?: run { - _uiState.value = _uiState.value.copy(error = "Failed to extract base.apk from APKM bundle") + // For split APK bundles (.apkm, .xapk), extract base.apk first + val isBundleFormat = FileUtils.isBundleFormat(file) + val apkToParse = if (isBundleFormat) { + FileUtils.extractBaseApkFromBundle(file) ?: run { + _uiState.value = _uiState.value.copy(error = "Failed to extract base APK from bundle") return null } } else { @@ -304,8 +304,13 @@ class QuickPatchViewModel( } if (packageName !in supportedPackages) { + val appName = SupportedApp.getDisplayName(packageName) + val supportedNames = cachedSupportedApps.map { it.displayName } + .ifEmpty { listOf("YouTube", "YouTube Music", "Reddit") } + .joinToString(", ") _uiState.value = _uiState.value.copy( - error = "Unsupported app: $packageName\n\nSupported apps: ${cachedSupportedApps.map { it.displayName }.ifEmpty { listOf("YouTube", "YouTube Music", "Reddit") }.joinToString(", ")}" + error = "$appName is not supported in Quick Patch mode. Supported apps: $supportedNames. Use Normal mode for unsupported apps.", + phase = QuickPatchPhase.IDLE ) return null } @@ -345,7 +350,7 @@ class QuickPatchViewModel( _uiState.value = _uiState.value.copy(error = "Failed to read APK: ${e.message}") null } finally { - if (isApkm) apkToParse.delete() + if (isBundleFormat) apkToParse.delete() } } diff --git a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt index 245b589..c57e9ee 100644 --- a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt +++ b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt @@ -142,22 +142,54 @@ object FileUtils { } /** - * Check if file is an APK or APKM. + * Check if file is an APK or split APK bundle (APKM, XAPK). */ fun isApkFile(file: File): Boolean { val ext = getExtension(file) - return file.isFile && (ext == "apk" || ext == "apkm") + return file.isFile && ext in setOf("apk", "apkm", "xapk") } /** - * Extract base.apk from an .apkm file to a temp directory. + * Check if file is a split APK bundle (.apkm or .xapk). + */ + fun isBundleFormat(file: File): Boolean { + return file.extension.lowercase() in setOf("apkm", "xapk") + } + + /** + * Extract base.apk from a split APK bundle (.apkm or .xapk) to a temp directory. + * For XAPK files, the base APK may not be named "base.apk" — falls back to the + * first non-split .apk entry or the largest by compressed size. * Returns the extracted base.apk file, or null if extraction fails. * Caller is responsible for cleaning up the returned temp file. */ - fun extractBaseApkFromApkm(apkmFile: File): File? { + fun extractBaseApkFromBundle(bundleFile: File): File? { return try { - ZipFile(apkmFile).use { zip -> - val baseEntry = zip.getEntry("base.apk") ?: return null + ZipFile(bundleFile).use { zip -> + val allEntries = zip.entries().asSequence().toList() + + // Try "base.apk" first (APKM format) + var baseEntry = zip.getEntry("base.apk") + + // For XAPK: find the base APK among all .apk entries. + // Splits are named like "config.arm64_v8a.apk", "split_config.en.apk", etc. + // The base APK is typically the package name (e.g., "com.google.android.youtube.apk"). + if (baseEntry == null) { + val apkEntries = allEntries + .filter { !it.isDirectory && it.name.endsWith(".apk", ignoreCase = true) } + + val splitPatterns = listOf("split_config", "config.", "split_") + baseEntry = apkEntries + .firstOrNull { entry -> + val name = entry.name.substringAfterLast('/').lowercase() + splitPatterns.none { name.startsWith(it) } + } + // Final fallback: largest .apk by compressed size + ?: apkEntries.maxByOrNull { it.compressedSize } + } + + if (baseEntry == null) return null + val tempFile = File(getTempDir(), "base-${System.currentTimeMillis()}.apk") zip.getInputStream(baseEntry).use { input -> tempFile.outputStream().use { output -> @@ -170,4 +202,7 @@ object FileUtils { null } } + + @Deprecated("Use extractBaseApkFromBundle instead", ReplaceWith("extractBaseApkFromBundle(apkmFile)")) + fun extractBaseApkFromApkm(apkmFile: File): File? = extractBaseApkFromBundle(apkmFile) } From 1b123a5260ff626da3fbd9d494fed06ec86b2764 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:20:53 +0530 Subject: [PATCH 40/49] apks support added Might need extra testing --- .../app/morphe/gui/ui/screens/home/HomeScreen.kt | 6 +++--- .../app/morphe/gui/ui/screens/home/HomeViewModel.kt | 6 +++--- .../gui/ui/screens/patches/PatchSelectionScreen.kt | 2 +- .../morphe/gui/ui/screens/quick/QuickPatchScreen.kt | 7 ++++--- .../morphe/gui/ui/screens/quick/QuickPatchViewModel.kt | 4 ++-- src/main/kotlin/app/morphe/gui/util/FileUtils.kt | 10 +++++----- 6 files changed, 18 insertions(+), 17 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 3f13379..3aeae41 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -625,7 +625,7 @@ private fun DropPromptSection( Spacer(modifier = Modifier.height(12.dp)) Text( - text = ".apk · .apkm · .xapk", + text = ".apk · .apkm · .xapk · .apks", fontSize = 10.sp, fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.25f), @@ -1481,7 +1481,7 @@ private fun DragOverlay() { ) Spacer(modifier = Modifier.height(8.dp)) Text( - text = ".apk · .apkm · .xapk", + text = ".apk · .apkm · .xapk · .apks", fontSize = 11.sp, fontFamily = mono, color = MorpheColors.Blue.copy(alpha = 0.4f), @@ -1494,7 +1494,7 @@ private fun DragOverlay() { private fun openFilePicker(): File? { val fileDialog = FileDialog(null as Frame?, "Select APK File", FileDialog.LOAD).apply { isMultipleMode = false - setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") || it.endsWith(".xapk") } } + setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") || it.endsWith(".xapk") || it.endsWith(".apks") } } isVisible = true } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index bd4f81b..6213eb5 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -352,7 +352,7 @@ class HomeViewModel( onFileSelected(apkFile) } else { _uiState.value = _uiState.value.copy( - error = "Please drop a valid .apk, .apkm, or .xapk file", + error = "Please drop a valid .apk, .apkm, .xapk, or .apks file", isReady = false ) } @@ -389,7 +389,7 @@ class HomeViewModel( } if (!FileUtils.isApkFile(file)) { - return ApkValidationResult(false, errorMessage = "File must have .apk, .apkm, or .xapk extension") + return ApkValidationResult(false, errorMessage = "File must have .apk, .apkm, .xapk, or .apks extension") } if (file.length() < 1024) { @@ -411,7 +411,7 @@ class HomeViewModel( * This works with APKs from any source, not just APKMirror. */ private fun parseApkManifest(file: File): ApkInfo? { - // For split APK bundles (.apkm, .xapk), extract base.apk first + // For split APK bundles (.apkm, .xapk, .apks), extract base.apk first val isBundleFormat = FileUtils.isBundleFormat(file) val apkToParse = if (isBundleFormat) { FileUtils.extractBaseApkFromBundle(file) ?: run { diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index e59ab91..db5295c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -438,7 +438,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { verticalArrangement = Arrangement.spacedBy(6.dp) ) { // Architecture selector — disabled for split APK bundles - val isBundleFormat = viewModel.getApkPath().lowercase().let { it.endsWith(".apkm") || it.endsWith(".xapk") } + val isBundleFormat = viewModel.getApkPath().lowercase().let { it.endsWith(".apkm") || it.endsWith(".xapk") || it.endsWith(".apks") } val showArchSelector = !isBundleFormat && uiState.apkArchitectures.size > 1 && !(uiState.apkArchitectures.size == 1 && uiState.apkArchitectures[0] == "universal") diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index ec7a0ef..cf7011d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -79,7 +79,8 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { files.firstOrNull { it.name.endsWith(".apk", ignoreCase = true) || it.name.endsWith(".apkm", ignoreCase = true) || - it.name.endsWith(".xapk", ignoreCase = true) + it.name.endsWith(".xapk", ignoreCase = true) || + it.name.endsWith(".apks", ignoreCase = true) }?.let { viewModel.onFileSelected(it) } }, enabled = uiState.phase != QuickPatchPhase.ANALYZING @@ -364,7 +365,7 @@ private fun IdleContent( ) Spacer(modifier = Modifier.height(6.dp)) Text( - text = ".apk · .apkm · .xapk", + text = ".apk · .apkm · .xapk · .apks", fontSize = 11.sp, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) ) @@ -1254,7 +1255,7 @@ private fun DragOverlay() { private fun openFilePicker(): File? { val fileDialog = FileDialog(null as Frame?, "Select APK", FileDialog.LOAD).apply { isMultipleMode = false - setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") || it.endsWith(".xapk") } } + setFilenameFilter { _, name -> name.lowercase().let { it.endsWith(".apk") || it.endsWith(".apkm") || it.endsWith(".xapk") || it.endsWith(".apks") } } isVisible = true } val directory = fileDialog.directory diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index cf412f5..b287bb7 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -267,11 +267,11 @@ class QuickPatchViewModel( */ private suspend fun analyzeApk(file: File): QuickApkInfo? { if (!file.exists() || !FileUtils.isApkFile(file)) { - _uiState.value = _uiState.value.copy(error = "Please drop a valid .apk, .apkm, or .xapk file") + _uiState.value = _uiState.value.copy(error = "Please drop a valid .apk, .apkm, .xapk, or .apks file") return null } - // For split APK bundles (.apkm, .xapk), extract base.apk first + // For split APK bundles (.apkm, .xapk, .apks), extract base.apk first val isBundleFormat = FileUtils.isBundleFormat(file) val apkToParse = if (isBundleFormat) { FileUtils.extractBaseApkFromBundle(file) ?: run { diff --git a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt index c57e9ee..4f53a08 100644 --- a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt +++ b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt @@ -142,22 +142,22 @@ object FileUtils { } /** - * Check if file is an APK or split APK bundle (APKM, XAPK). + * Check if file is an APK or split APK bundle (APKM, XAPK, APKS). */ fun isApkFile(file: File): Boolean { val ext = getExtension(file) - return file.isFile && ext in setOf("apk", "apkm", "xapk") + return file.isFile && ext in setOf("apk", "apkm", "xapk", "apks") } /** - * Check if file is a split APK bundle (.apkm or .xapk). + * Check if file is a split APK bundle (.apkm, .xapk, or .apks). */ fun isBundleFormat(file: File): Boolean { - return file.extension.lowercase() in setOf("apkm", "xapk") + return file.extension.lowercase() in setOf("apkm", "xapk", "apks") } /** - * Extract base.apk from a split APK bundle (.apkm or .xapk) to a temp directory. + * Extract base.apk from a split APK bundle (.apkm, .xapk, or .apks) to a temp directory. * For XAPK files, the base APK may not be named "base.apk" — falls back to the * first non-split .apk entry or the largest by compressed size. * Returns the extracted base.apk file, or null if extraction fails. From 9f56cb62a732b121ab4214525682d21aee9bcebd Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Sat, 28 Mar 2026 08:11:09 +0530 Subject: [PATCH 41/49] Minor UI fixes Fixed minor UI bugs in Simplified mode. Fixed appInfoCard loading issue and supported apps section UI issues --- .../morphe/gui/ui/screens/home/HomeScreen.kt | 1 + .../gui/ui/screens/home/HomeViewModel.kt | 35 +---- .../ui/screens/home/components/ApkInfoCard.kt | 2 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 131 +++++++++++++----- .../ui/screens/quick/QuickPatchViewModel.kt | 17 ++- .../app/morphe/gui/util/VersionUtils.kt | 33 +++++ 6 files changed, 153 insertions(+), 66 deletions(-) create mode 100644 src/main/kotlin/app/morphe/gui/util/VersionUtils.kt diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 3aeae41..4f3e89a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -59,6 +59,7 @@ import app.morphe.gui.ui.screens.patches.PatchesScreen import app.morphe.gui.ui.screens.patches.PatchSelectionScreen import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects +import app.morphe.gui.util.VersionStatus import java.awt.FileDialog import java.awt.Frame import java.io.File diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 6213eb5..150c86a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -20,6 +20,8 @@ import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import app.morphe.gui.util.PatchService import app.morphe.gui.util.SupportedAppExtractor +import app.morphe.gui.util.VersionStatus +import app.morphe.gui.util.compareVersions import java.io.File class HomeViewModel( @@ -549,31 +551,7 @@ class HomeViewModel( } } - /** - * Compares two version strings (e.g., "19.16.39" vs "20.40.45") - * Returns the version status of the current version relative to suggested. - */ - private fun compareVersions(current: String, suggested: String): VersionStatus { - return try { - val currentParts = current.split(".").map { it.toInt() } - val suggestedParts = suggested.split(".").map { it.toInt() } - - // Compare each part - for (i in 0 until maxOf(currentParts.size, suggestedParts.size)) { - val currentPart = currentParts.getOrElse(i) { 0 } - val suggestedPart = suggestedParts.getOrElse(i) { 0 } - - when { - currentPart > suggestedPart -> return VersionStatus.NEWER_VERSION - currentPart < suggestedPart -> return VersionStatus.OLDER_VERSION - } - } - VersionStatus.EXACT_MATCH - } catch (e: Exception) { - Logger.warn("Failed to compare versions: $current vs $suggested") - VersionStatus.UNKNOWN - } - } + // compareVersions and VersionStatus moved to app.morphe.gui.util.VersionUtils } data class HomeUiState( @@ -612,13 +590,6 @@ data class ApkInfo( val isUnsupportedApp: Boolean = false ) -enum class VersionStatus { - EXACT_MATCH, // Using the suggested version - OLDER_VERSION, // Using an older version (newer patches available) - NEWER_VERSION, // Using a newer version (might have issues) - UNKNOWN // Could not determine -} - data class ApkValidationResult( val isValid: Boolean, val apkInfo: ApkInfo? = null, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt index 3dedf1a..4a6b82f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/components/ApkInfoCard.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.morphe.gui.ui.screens.home.ApkInfo -import app.morphe.gui.ui.screens.home.VersionStatus +import app.morphe.gui.util.VersionStatus import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.MorpheColors diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index cf7011d..a0c0ad3 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -39,6 +39,7 @@ import app.morphe.gui.ui.screens.home.components.FullScreenDropZone import app.morphe.gui.ui.theme.* import app.morphe.gui.util.ChecksumStatus import app.morphe.gui.util.DownloadUrlResolver.openUrlAndFollowRedirects +import app.morphe.gui.util.VersionStatus import app.morphe.gui.util.PatchService import app.morphe.gui.util.AdbManager import app.morphe.gui.util.DeviceMonitor @@ -242,6 +243,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { private fun BrandingHeader(patchesVersion: String?, isLoading: Boolean) { val themeState = LocalThemeState.current val mono = LocalMorpheFont.current + val corners = LocalMorpheCorners.current val isDark = when (themeState.current) { ThemePreference.SYSTEM -> isSystemInDarkTheme() else -> themeState.current.isDark() @@ -257,26 +259,75 @@ private fun BrandingHeader(patchesVersion: String?, isLoading: Boolean) { Spacer(modifier = Modifier.width(12.dp)) if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(12.dp), - strokeWidth = 1.5.dp, - color = MorpheColors.Blue - ) - Spacer(modifier = Modifier.width(6.dp)) - Text( - text = "Loading patches…", - fontSize = 11.sp, - fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) - ) + Row( + modifier = Modifier + .height(34.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(12.dp), + strokeWidth = 1.5.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "LOADING…", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.sp + ) + } } else if (patchesVersion != null) { - Text( - text = "Patches $patchesVersion", - fontSize = 11.sp, - fontFamily = mono, - color = MorpheColors.Blue.copy(alpha = 0.8f), - fontWeight = FontWeight.Medium - ) + // Matches expert mode PatchesVersionInline — but not clickable + Row( + modifier = Modifier + .height(34.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "PATCHES", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = patchesVersion, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MorpheColors.Blue + ) + // Quick mode always uses latest, so show LATEST badge + Spacer(modifier = Modifier.width(6.dp)) + Box( + modifier = Modifier + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) + .padding(horizontal = 5.dp, vertical = 1.dp) + ) { + Text( + text = "LATEST", + fontSize = 8.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MorpheColors.Teal, + letterSpacing = 1.sp + ) + } + } } else { Text( text = "QUICK PATCH", @@ -391,12 +442,14 @@ private fun ReadyContent( val accentColor = when { apkInfo.checksumStatus is ChecksumStatus.Mismatch -> MaterialTheme.colorScheme.error apkInfo.isRecommendedVersion -> MorpheColors.Teal + apkInfo.versionStatus == VersionStatus.NEWER_VERSION -> MaterialTheme.colorScheme.error + apkInfo.versionStatus == VersionStatus.OLDER_VERSION -> Color(0xFFFF9800) !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> Color(0xFFFF9800) else -> MorpheColors.Blue } Column( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxHeight(), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center ) { @@ -410,15 +463,21 @@ private fun ReadyContent( .clip(RoundedCornerShape(corners.medium)) .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) .background(MaterialTheme.colorScheme.surface) + .drawBehind{ + drawRect( + color = accentColor, + size = androidx.compose.ui.geometry.Size(3.dp.toPx(), size.height) + ) + } ) { // Left accent stripe - Box( - modifier = Modifier - .width(3.dp) - .fillMaxHeight() - .background(accentColor) - .align(Alignment.CenterStart) - ) +// Box( +// modifier = Modifier +// .width(3.dp) +// .fillMaxHeight() +// .background(accentColor) +// .align(Alignment.CenterStart) +// ) Column( modifier = Modifier @@ -502,13 +561,19 @@ private fun ReadyContent( val statusText = when { apkInfo.checksumStatus is ChecksumStatus.Verified -> "VERIFIED" apkInfo.checksumStatus is ChecksumStatus.Mismatch -> "CHECKSUM MISMATCH" - !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> "OUTDATED" + apkInfo.versionStatus == VersionStatus.NEWER_VERSION -> "NEWER THAN RECOMMENDED" + apkInfo.versionStatus == VersionStatus.OLDER_VERSION -> "OLDER THAN RECOMMENDED" + !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> "VERSION MISMATCH" apkInfo.isRecommendedVersion -> "RECOMMENDED VERSION" else -> null } val statusDetail = when { apkInfo.checksumStatus is ChecksumStatus.Verified -> "Checksum matches APKMirror" apkInfo.checksumStatus is ChecksumStatus.Mismatch -> "Re-download from APKMirror" + apkInfo.versionStatus == VersionStatus.NEWER_VERSION -> + "Patches target v${apkInfo.recommendedVersion} — may not be compatible" + apkInfo.versionStatus == VersionStatus.OLDER_VERSION -> + "Patches target v${apkInfo.recommendedVersion}" !apkInfo.isRecommendedVersion && apkInfo.recommendedVersion != null -> "Patches target v${apkInfo.recommendedVersion}" else -> null @@ -551,7 +616,7 @@ private fun ReadyContent( fontSize = 11.sp, fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f), - maxLines = 1, + maxLines = 2, overflow = TextOverflow.Ellipsis ) } @@ -1129,10 +1194,11 @@ private fun SupportedAppsRow( } // Horizontal scrolling cards + val useScrolling = filteredApps.size > 4 Row( modifier = Modifier .fillMaxWidth() - .horizontalScroll(rememberScrollState()) + .then(if (useScrolling) Modifier.horizontalScroll(rememberScrollState()) else Modifier) .height(IntrinsicSize.Max) .clickable( interactionSource = remember { MutableInteractionSource() }, @@ -1152,7 +1218,10 @@ private fun SupportedAppsRow( Surface( modifier = Modifier - .width(170.dp) + .then( + if (useScrolling) Modifier.width(170.dp) + else Modifier.weight(1f) + ) .fillMaxHeight() .hoverable(hoverInteraction) .then( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index b287bb7..487b38a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -21,6 +21,8 @@ import app.morphe.gui.util.FileUtils import app.morphe.gui.util.Logger import app.morphe.gui.util.PatchService import app.morphe.gui.util.SupportedAppExtractor +import app.morphe.gui.util.VersionStatus +import app.morphe.gui.util.compareVersions import java.io.File /** @@ -324,14 +326,23 @@ class QuickPatchViewModel( // Version check val isRecommendedVersion = recommendedVersion != null && versionName == recommendedVersion + val versionStatus = if (recommendedVersion != null) { + compareVersions(versionName, recommendedVersion) + } else { + VersionStatus.UNKNOWN + } val versionWarning = if (!isRecommendedVersion && recommendedVersion != null) { - "Version $versionName may have compatibility issues. Recommended: $recommendedVersion" + when (versionStatus) { + VersionStatus.NEWER_VERSION -> "Version $versionName is newer than recommended $recommendedVersion — patches may not be compatible" + VersionStatus.OLDER_VERSION -> "Version $versionName is older than recommended $recommendedVersion" + else -> "Version $versionName may have compatibility issues. Recommended: $recommendedVersion" + } } else null // TODO: Re-enable when checksums are provided via .mpp files val checksumStatus = ChecksumStatus.NotConfigured - Logger.info("Quick mode: Analyzed $displayName v$versionName (recommended: $recommendedVersion)") + Logger.info("Quick mode: Analyzed $displayName v$versionName (recommended: $recommendedVersion, status: $versionStatus)") QuickApkInfo( fileName = file.name, @@ -341,6 +352,7 @@ class QuickPatchViewModel( displayName = displayName, recommendedVersion = recommendedVersion, isRecommendedVersion = isRecommendedVersion, + versionStatus = versionStatus, versionWarning = versionWarning, checksumStatus = checksumStatus ) @@ -554,6 +566,7 @@ data class QuickApkInfo( val displayName: String, val recommendedVersion: String?, val isRecommendedVersion: Boolean, + val versionStatus: VersionStatus = VersionStatus.UNKNOWN, val versionWarning: String?, val checksumStatus: ChecksumStatus ) { diff --git a/src/main/kotlin/app/morphe/gui/util/VersionUtils.kt b/src/main/kotlin/app/morphe/gui/util/VersionUtils.kt new file mode 100644 index 0000000..3b8144a --- /dev/null +++ b/src/main/kotlin/app/morphe/gui/util/VersionUtils.kt @@ -0,0 +1,33 @@ +package app.morphe.gui.util + +enum class VersionStatus { + EXACT_MATCH, // Using the suggested version + OLDER_VERSION, // Using an older version (newer patches available) + NEWER_VERSION, // Using a newer version (might have issues) + UNKNOWN // Could not determine +} + +/** + * Compares two version strings (e.g., "19.16.39" vs "20.40.45") + * Returns the version status of the current version relative to suggested. + */ +fun compareVersions(current: String, suggested: String): VersionStatus { + return try { + val currentParts = current.split(".").map { it.toInt() } + val suggestedParts = suggested.split(".").map { it.toInt() } + + for (i in 0 until maxOf(currentParts.size, suggestedParts.size)) { + val currentPart = currentParts.getOrElse(i) { 0 } + val suggestedPart = suggestedParts.getOrElse(i) { 0 } + + when { + currentPart > suggestedPart -> return VersionStatus.NEWER_VERSION + currentPart < suggestedPart -> return VersionStatus.OLDER_VERSION + } + } + VersionStatus.EXACT_MATCH + } catch (e: Exception) { + Logger.warn("Failed to compare versions: $current vs $suggested") + VersionStatus.UNKNOWN + } +} From 70a840e598dc47be8be2bb5db8dbf34086baf76f Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Mon, 30 Mar 2026 12:07:11 +0530 Subject: [PATCH 42/49] Options and minor fixes Added ability to add images in patch options. Fixed some patch selection screen UI. --- .../kotlin/app/morphe/gui/data/model/Patch.kt | 3 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 4 +- .../gui/ui/screens/home/HomeViewModel.kt | 46 +- .../screens/patches/PatchSelectionScreen.kt | 351 ++++++++++---- .../gui/ui/screens/quick/QuickPatchScreen.kt | 443 ++++++++++++++---- .../ui/screens/quick/QuickPatchViewModel.kt | 25 +- .../kotlin/app/morphe/gui/util/FileUtils.kt | 40 ++ .../app/morphe/gui/util/PatchService.kt | 14 +- 8 files changed, 672 insertions(+), 254 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt index ea413a7..c5f07df 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt @@ -65,7 +65,8 @@ enum class PatchOptionType { INT, LONG, FLOAT, - LIST + LIST, + FILE } /** diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index 4f3e89a..51751dd 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -344,9 +344,9 @@ private fun HeaderBar( // Logo — left-aligned, compact BrandingSection(isCompact = true) - Spacer(modifier = Modifier.width(16.dp)) + Spacer(modifier = Modifier.weight(1f)) - // Patches version inline + // Patches version inline — centered if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { PatchesVersionInline( patchesVersion = uiState.patchesVersion!!, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index 150c86a..85f29c9 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -461,7 +461,7 @@ class HomeViewModel( // Get supported architectures from native libraries // For split bundles, scan the original bundle (splits contain the native libs, not base.apk) - val architectures = extractArchitectures(if (isBundleFormat) file else apkToParse) + val architectures = FileUtils.extractArchitectures(if (isBundleFormat) file else apkToParse) // TODO: Re-enable when checksums are provided via .mpp files val checksumStatus = app.morphe.gui.util.ChecksumStatus.NotConfigured @@ -492,50 +492,6 @@ class HomeViewModel( } } - /** - * Extract supported CPU architectures from native libraries in the APK. - * Uses ZipFile to scan for lib// directories. - */ - private fun extractArchitectures(file: File): List { - return try { - java.util.zip.ZipFile(file).use { zip -> - val archDirs = mutableSetOf() - - // Scan for lib// entries directly (regular APK or merged APK) - zip.entries().asSequence() - .map { it.name } - .filter { it.startsWith("lib/") } - .mapNotNull { path -> - val parts = path.split("/") - if (parts.size >= 2) parts[1] else null - } - .forEach { archDirs.add(it) } - - // For .apkm bundles: also detect arch from split APK names - // e.g. split_config.arm64_v8a.apk -> arm64-v8a - if (archDirs.isEmpty()) { - val knownArchs = setOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") - zip.entries().asSequence() - .map { it.name } - .filter { it.endsWith(".apk") } - .forEach { name -> - // Convert split_config.arm64_v8a.apk format to arm64-v8a - val normalized = name.replace("_", "-") - knownArchs.filter { arch -> normalized.contains(arch) } - .forEach { archDirs.add(it) } - } - } - - archDirs.toList().ifEmpty { - listOf("universal") - } - } - } catch (e: Exception) { - Logger.warn("Could not extract architectures: ${e.message}") - emptyList() - } - } - // TODO: Re-enable checksum verification when checksums are provided via .mpp files // private fun verifyChecksum( // file: File, packageName: String, version: String, diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index db5295c..406fc13 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -25,6 +25,7 @@ import androidx.compose.material.icons.filled.Clear import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material.icons.filled.PlaylistRemove import androidx.compose.material.icons.filled.Terminal @@ -60,8 +61,10 @@ import app.morphe.gui.ui.theme.LocalMorpheCorners import app.morphe.gui.ui.theme.LocalMorpheFont import app.morphe.gui.ui.theme.MorpheColors import app.morphe.gui.util.DeviceMonitor +import java.awt.FileDialog import java.awt.Toolkit import java.awt.datatransfer.StringSelection +import java.io.File /** * Screen for selecting which patches to apply. @@ -699,6 +702,7 @@ private fun PatchListItem( ) var showOptions by remember { mutableStateOf(false) } + val hasOptions = patch.options.isNotEmpty() Column( modifier = Modifier @@ -749,31 +753,24 @@ private fun PatchListItem( } Column(modifier = Modifier.weight(1f)) { - Text( - text = patch.name, - fontSize = 13.sp, - fontWeight = FontWeight.Medium, - fontFamily = mono, - color = MaterialTheme.colorScheme.onSurface - ) - - if (patch.description.isNotBlank()) { - Spacer(modifier = Modifier.height(3.dp)) + // Name + app chips on same line + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { Text( - text = patch.description, - fontSize = 11.sp, + text = patch.name, + fontSize = 13.sp, + fontWeight = FontWeight.Medium, fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - maxLines = 2, - overflow = TextOverflow.Ellipsis + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f, fill = false) ) - } - // Compatible packages - if (patch.compatiblePackages.isNotEmpty()) { - val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") - Spacer(modifier = Modifier.height(6.dp)) - Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + if (patch.compatiblePackages.isNotEmpty()) { + val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") patch.compatiblePackages.take(2).forEach { pkg -> val meaningful = pkg.name.split(".").filter { it !in genericSegments } val displayName = meaningful.takeLast(2).joinToString(" ") @@ -800,51 +797,89 @@ private fun PatchListItem( } } - // Options indicator - if (patch.options.isNotEmpty()) { - Spacer(modifier = Modifier.height(4.dp)) + if (patch.description.isNotBlank()) { + Spacer(modifier = Modifier.height(3.dp)) Text( - text = "${patch.options.size} option${if (patch.options.size > 1) "s" else ""} ${if (showOptions) "▲" else "▼"}", - fontSize = 9.sp, + text = patch.description, + fontSize = 11.sp, fontFamily = mono, - fontWeight = FontWeight.Medium, - color = MorpheColors.Teal.copy(alpha = 0.7f), - letterSpacing = 0.5.sp + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), + maxLines = 2, + overflow = TextOverflow.Ellipsis ) } } - } - // Options section - if (patch.options.isNotEmpty()) { - val optionDivider = MaterialTheme.colorScheme.outline.copy(alpha = 0.06f) + // Gear button for options + if (hasOptions) { + val gearHover = remember { MutableInteractionSource() } + val isGearHovered by gearHover.collectIsHoveredAsState() + val gearBorder by animateColorAsState( + when { + showOptions -> MorpheColors.Teal.copy(alpha = 0.5f) + isGearHovered -> MorpheColors.Teal.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.outline.copy(alpha = 0.12f) + }, + animationSpec = tween(150) + ) + val gearBg by animateColorAsState( + if (showOptions) MorpheColors.Teal.copy(alpha = 0.08f) + else Color.Transparent, + animationSpec = tween(150) + ) - if (!showOptions) { + // Wrapper box — no clip, allows badge to overflow Box( - modifier = Modifier - .fillMaxWidth() - .drawBehind { - drawLine( - color = optionDivider, - start = Offset(14.dp.toPx(), 0f), - end = Offset(size.width - 14.dp.toPx(), 0f), - strokeWidth = 1f - ) - } - .clickable { showOptions = true } - .background(MorpheColors.Teal.copy(alpha = 0.03f)) - .padding(horizontal = 14.dp, vertical = 6.dp) + modifier = Modifier.size(48.dp), + contentAlignment = Alignment.Center ) { - Text( - text = "CONFIGURE OPTIONS", - fontSize = 9.sp, - fontFamily = mono, - fontWeight = FontWeight.SemiBold, - color = MorpheColors.Teal.copy(alpha = 0.5f), - letterSpacing = 1.sp - ) + // Gear button + Box( + modifier = Modifier + .size(44.dp) + .hoverable(gearHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, gearBorder, RoundedCornerShape(corners.small)) + .background(gearBg, RoundedCornerShape(corners.small)) + .clickable { showOptions = !showOptions }, + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Configure options", + tint = when { + showOptions -> MorpheColors.Teal + isGearHovered -> MorpheColors.Teal.copy(alpha = 0.7f) + else -> MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) + }, + modifier = Modifier.size(22.dp) + ) + } + // Options count badge — outside clip + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .offset(x = 3.dp, y = (-3).dp) + .size(18.dp) + .background(MorpheColors.Teal, RoundedCornerShape(9.dp)), + contentAlignment = Alignment.Center + ) { + Text( + text = "${patch.options.size}", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = Color.White, + lineHeight = 9.sp + ) + } } } + } + + // Expandable options section + if (hasOptions) { + val optionDivider = MaterialTheme.colorScheme.outline.copy(alpha = 0.06f) AnimatedVisibility( visible = showOptions, @@ -861,28 +896,9 @@ private fun PatchListItem( strokeWidth = 1f ) } - .padding(start = 14.dp, end = 14.dp, bottom = 12.dp, top = 8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) + .padding(start = 14.dp, end = 14.dp, bottom = 10.dp, top = 6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp) ) { - // Collapse button - Box( - modifier = Modifier - .fillMaxWidth() - .clip(RoundedCornerShape(corners.small)) - .background(MorpheColors.Teal.copy(alpha = 0.04f)) - .clickable { showOptions = false } - .padding(horizontal = 8.dp, vertical = 4.dp) - ) { - Text( - text = "HIDE OPTIONS ▲", - fontSize = 9.sp, - fontFamily = mono, - fontWeight = FontWeight.SemiBold, - color = MorpheColors.Teal.copy(alpha = 0.5f), - letterSpacing = 1.sp - ) - } - patch.options.forEach { option -> PatchOptionEditor( option = option, @@ -967,38 +983,171 @@ private fun PatchOptionEditor( ) } } - else -> { - var localText by remember(option.key) { mutableStateOf(value) } + app.morphe.gui.data.model.PatchOptionType.FILE -> { + var localPath by remember(option.key) { mutableStateOf(value) } LaunchedEffect(value) { - if (localText != value) localText = value + if (localPath != value) localPath = value } - OutlinedTextField( - value = localText, - onValueChange = { newText -> - localText = newText - onValueChange(newText) - }, - placeholder = { + // Detect if this is an image file option from key/title + val keyLower = option.key.lowercase() + " " + option.title.lowercase() + val isImage = keyLower.contains("icon") || keyLower.contains("image") || + keyLower.contains("logo") || keyLower.contains("banner") || + keyLower.contains("png") || keyLower.contains("jpg") + val fileFilterDesc = if (isImage) "Image files" else "All files" + val fileExtensions = if (isImage) "png,jpg,jpeg,webp" else "*" + + val fieldFocused = remember { mutableStateOf(false) } + val fieldBorder by animateColorAsState( + if (fieldFocused.value) MorpheColors.Teal.copy(alpha = 0.6f) + else MorpheColors.Teal.copy(alpha = 0.2f), + animationSpec = tween(150) + ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(32.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(6.dp) + ) { + // Path text field + Row( + modifier = Modifier + .weight(1f) + .fillMaxHeight() + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, fieldBorder, RoundedCornerShape(corners.small)) + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier.weight(1f)) { + if (localPath.isEmpty()) { + Text( + text = if (isImage) "Select image…" else "Select file…", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + androidx.compose.foundation.text.BasicTextField( + value = localPath, + onValueChange = { newPath -> + localPath = newPath + onValueChange(newPath) + }, + singleLine = true, + textStyle = LocalTextStyle.current.copy( + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface + ), + cursorBrush = androidx.compose.ui.graphics.SolidColor(MorpheColors.Teal), + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { fieldFocused.value = it.isFocused } + ) + } + } + + // Browse button + val browseHover = remember { MutableInteractionSource() } + val isBrowseHovered by browseHover.collectIsHoveredAsState() + val browseBorder by animateColorAsState( + if (isBrowseHovered) MorpheColors.Teal.copy(alpha = 0.5f) + else MorpheColors.Teal.copy(alpha = 0.2f), + animationSpec = tween(150) + ) + + Box( + modifier = Modifier + .fillMaxHeight() + .hoverable(browseHover) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, browseBorder, RoundedCornerShape(corners.small)) + .clickable { + val dialog = FileDialog(null as java.awt.Frame?, fileFilterDesc, FileDialog.LOAD) + if (isImage) { + // setFile pattern works on macOS; setFilenameFilter works on Linux/Windows + dialog.file = "*.png;*.jpg;*.jpeg;*.webp" + dialog.setFilenameFilter { _, name -> + val lower = name.lowercase() + lower.endsWith(".png") || lower.endsWith(".jpg") || + lower.endsWith(".jpeg") || lower.endsWith(".webp") + } + } + dialog.isVisible = true + val selected = dialog.file + if (selected != null) { + val fullPath = File(dialog.directory, selected).absolutePath + localPath = fullPath + onValueChange(fullPath) + } + } + .padding(horizontal = 10.dp), + contentAlignment = Alignment.Center + ) { Text( - text = option.default ?: option.type.name.lowercase(), - fontSize = 11.sp, + text = "BROWSE", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + color = if (isBrowseHovered) MorpheColors.Teal else MorpheColors.Teal.copy(alpha = 0.7f), + letterSpacing = 1.sp ) - }, - singleLine = true, - textStyle = LocalTextStyle.current.copy( - fontSize = 11.sp, - fontFamily = mono - ), - shape = RoundedCornerShape(corners.small), - modifier = Modifier.fillMaxWidth(), - colors = OutlinedTextFieldDefaults.colors( - unfocusedBorderColor = MorpheColors.Teal.copy(alpha = 0.2f), - focusedBorderColor = MorpheColors.Teal.copy(alpha = 0.6f) - ) + } + } + } + else -> { + var localText by remember(option.key) { mutableStateOf(value) } + LaunchedEffect(value) { + if (localText != value) localText = value + } + + val fieldFocused = remember { mutableStateOf(false) } + val fieldBorder by animateColorAsState( + if (fieldFocused.value) MorpheColors.Teal.copy(alpha = 0.6f) + else MorpheColors.Teal.copy(alpha = 0.2f), + animationSpec = tween(150) ) + + Row( + modifier = Modifier + .fillMaxWidth() + .height(32.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, fieldBorder, RoundedCornerShape(corners.small)) + .padding(horizontal = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box(modifier = Modifier.weight(1f)) { + if (localText.isEmpty()) { + Text( + text = option.default ?: option.type.name.lowercase(), + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + androidx.compose.foundation.text.BasicTextField( + value = localText, + onValueChange = { newText -> + localText = newText + onValueChange(newText) + }, + singleLine = true, + textStyle = LocalTextStyle.current.copy( + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface + ), + cursorBrush = androidx.compose.ui.graphics.SolidColor(MorpheColors.Teal), + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { fieldFocused.value = it.isFocused } + ) + } + } } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index a0c0ad3..716fd6b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -1,6 +1,7 @@ package app.morphe.gui.ui.screens.quick import androidx.compose.animation.* +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.interaction.MutableInteractionSource @@ -17,6 +18,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight @@ -28,6 +30,7 @@ import cafe.adriel.voyager.core.screen.Screen import app.morphe.morphe_cli.generated.resources.Res import app.morphe.morphe_cli.generated.resources.morphe_dark import app.morphe.morphe_cli.generated.resources.morphe_light +import app.morphe.gui.data.model.Patch import app.morphe.gui.data.model.SupportedApp import app.morphe.gui.data.repository.ConfigRepository import app.morphe.gui.data.repository.PatchSourceManager @@ -111,7 +114,13 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ), verticalAlignment = Alignment.CenterVertically ) { - BrandingHeader( + // Logo — left-aligned + BrandingLogo() + + Spacer(modifier = Modifier.weight(1f)) + + // Patches version badge — centered + PatchesVersionBadge( patchesVersion = uiState.patchesVersion, isLoading = uiState.isLoadingPatches ) @@ -164,6 +173,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { if (info != null) { ReadyContent( apkInfo = info, + compatiblePatches = uiState.compatiblePatches, onPatch = { viewModel.startPatching() }, onClear = { viewModel.reset() } ) @@ -240,103 +250,92 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { // ════════════════════════════════════════════════════════════════════ @Composable -private fun BrandingHeader(patchesVersion: String?, isLoading: Boolean) { +private fun BrandingLogo() { val themeState = LocalThemeState.current - val mono = LocalMorpheFont.current - val corners = LocalMorpheCorners.current val isDark = when (themeState.current) { ThemePreference.SYSTEM -> isSystemInDarkTheme() else -> themeState.current.isDark() } - Row(verticalAlignment = Alignment.CenterVertically) { - Image( - painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), - contentDescription = "Morphe Logo", - modifier = Modifier.height(28.dp) - ) + Image( + painter = painterResource(if (isDark) Res.drawable.morphe_dark else Res.drawable.morphe_light), + contentDescription = "Morphe Logo", + modifier = Modifier.height(28.dp) + ) +} - Spacer(modifier = Modifier.width(12.dp)) +@Composable +private fun PatchesVersionBadge(patchesVersion: String?, isLoading: Boolean) { + val mono = LocalMorpheFont.current + val corners = LocalMorpheCorners.current - if (isLoading) { - Row( + if (isLoading) { + Row( + modifier = Modifier + .height(34.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + CircularProgressIndicator( + modifier = Modifier.size(12.dp), + strokeWidth = 1.5.dp, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = "LOADING…", + fontSize = 10.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.sp + ) + } + } else if (patchesVersion != null) { + Row( + modifier = Modifier + .height(34.dp) + .clip(RoundedCornerShape(corners.small)) + .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .background(MaterialTheme.colorScheme.surface) + .padding(horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "PATCHES", + fontSize = 9.sp, + fontWeight = FontWeight.Bold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = patchesVersion, + fontSize = 12.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MorpheColors.Blue + ) + Spacer(modifier = Modifier.width(6.dp)) + Box( modifier = Modifier - .height(34.dp) - .clip(RoundedCornerShape(corners.small)) - .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically + .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) + .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) + .padding(horizontal = 5.dp, vertical = 1.dp) ) { - CircularProgressIndicator( - modifier = Modifier.size(12.dp), - strokeWidth = 1.5.dp, - color = MorpheColors.Blue - ) - Spacer(modifier = Modifier.width(8.dp)) Text( - text = "LOADING…", - fontSize = 10.sp, + text = "LATEST", + fontSize = 8.sp, fontWeight = FontWeight.Bold, fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + color = MorpheColors.Teal, letterSpacing = 1.sp ) } - } else if (patchesVersion != null) { - // Matches expert mode PatchesVersionInline — but not clickable - Row( - modifier = Modifier - .height(34.dp) - .clip(RoundedCornerShape(corners.small)) - .border(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) - .background(MaterialTheme.colorScheme.surface) - .padding(horizontal = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = "PATCHES", - fontSize = 9.sp, - fontWeight = FontWeight.Bold, - fontFamily = mono, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), - letterSpacing = 1.5.sp - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = patchesVersion, - fontSize = 12.sp, - fontWeight = FontWeight.SemiBold, - fontFamily = mono, - color = MorpheColors.Blue - ) - // Quick mode always uses latest, so show LATEST badge - Spacer(modifier = Modifier.width(6.dp)) - Box( - modifier = Modifier - .background(MorpheColors.Teal.copy(alpha = 0.1f), RoundedCornerShape(corners.small)) - .border(1.dp, MorpheColors.Teal.copy(alpha = 0.2f), RoundedCornerShape(corners.small)) - .padding(horizontal = 5.dp, vertical = 1.dp) - ) { - Text( - text = "LATEST", - fontSize = 8.sp, - fontWeight = FontWeight.Bold, - fontFamily = mono, - color = MorpheColors.Teal, - letterSpacing = 1.sp - ) - } - } - } else { - Text( - text = "QUICK PATCH", - fontSize = 11.sp, - fontFamily = mono, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurfaceVariant, - letterSpacing = 1.sp - ) } } } @@ -430,8 +429,10 @@ private fun IdleContent( // ════════════════════════════════════════════════════════════════════ @Composable +@OptIn(ExperimentalLayoutApi::class) private fun ReadyContent( apkInfo: QuickApkInfo, + compatiblePatches: List, onPatch: () -> Unit, onClear: () -> Unit ) { @@ -448,37 +449,34 @@ private fun ReadyContent( else -> MorpheColors.Blue } + val enabledPatches = compatiblePatches.filter { it.isEnabled } + val disabledPatches = compatiblePatches.filter { !it.isEnabled } + var isPatchListExpanded by remember { mutableStateOf(false) } + var patchSearchQuery by remember { mutableStateOf("") } + Column( - modifier = Modifier.fillMaxHeight(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()), + horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.weight(1f)) // APK info card — bordered box with accent stripe Box( modifier = Modifier - .widthIn(max = 480.dp) + .widthIn(max = 640.dp) .fillMaxWidth() .clip(RoundedCornerShape(corners.medium)) .border(1.dp, borderColor, RoundedCornerShape(corners.medium)) .background(MaterialTheme.colorScheme.surface) - .drawBehind{ + .drawBehind { drawRect( color = accentColor, size = androidx.compose.ui.geometry.Size(3.dp.toPx(), size.height) ) } ) { - // Left accent stripe -// Box( -// modifier = Modifier -// .width(3.dp) -// .fillMaxHeight() -// .background(accentColor) -// .align(Alignment.CenterStart) -// ) - Column( modifier = Modifier .fillMaxWidth() @@ -622,6 +620,256 @@ private fun ReadyContent( } } } + + // ── Info row: architectures, package, minSdk ── + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + // Architectures + if (apkInfo.architectures.isNotEmpty()) { + val deviceState by DeviceMonitor.state.collectAsState() + val deviceArch = deviceState.selectedDevice?.architecture + val hasMultipleArchs = apkInfo.architectures.size > 1 + val highlightArch = if (hasMultipleArchs && deviceArch != null) deviceArch else null + + Text( + text = "ARCH", + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + apkInfo.architectures.forEach { arch -> + val isDeviceArch = highlightArch != null && arch == highlightArch + val tagBorder = if (isDeviceArch) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f) + val tagBg = if (isDeviceArch) MorpheColors.Blue.copy(alpha = 0.08f) + else Color.Transparent + val tagColor = if (isDeviceArch) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurface + val dimmed = highlightArch != null && !isDeviceArch + + Box( + modifier = Modifier + .border(1.dp, tagBorder, RoundedCornerShape(corners.small)) + .background(tagBg, RoundedCornerShape(corners.small)) + .padding(horizontal = 8.dp, vertical = 3.dp) + ) { + Text( + text = arch, + fontSize = 11.sp, + fontWeight = if (isDeviceArch) FontWeight.Bold else FontWeight.Medium, + fontFamily = mono, + color = if (dimmed) tagColor.copy(alpha = 0.35f) else tagColor + ) + } + } + } + + // MinSdk + if (apkInfo.minSdk != null) { + Spacer(Modifier.width(4.dp)) + Text( + text = "MIN SDK", + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Text( + text = "${apkInfo.minSdk}", + fontSize = 11.sp, + fontWeight = FontWeight.Medium, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + + // ── Patches summary — collapsible ── + if (compatiblePatches.isNotEmpty()) { + val chevronRotation by animateFloatAsState( + if (isPatchListExpanded) 180f else 0f, + animationSpec = tween(200) + ) + + // Summary header — clickable to expand + Row( + modifier = Modifier + .fillMaxWidth() + .drawBehind { + drawLine( + color = borderColor, + start = Offset(20.dp.toPx(), 0f), + end = Offset(size.width - 20.dp.toPx(), 0f), + strokeWidth = 1f + ) + } + .clickable { isPatchListExpanded = !isPatchListExpanded } + .padding(horizontal = 20.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "PATCHES", + fontSize = 9.sp, + fontWeight = FontWeight.SemiBold, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + letterSpacing = 1.5.sp + ) + Spacer(Modifier.width(10.dp)) + Text( + text = "${enabledPatches.size} enabled", + fontSize = 11.sp, + fontFamily = mono, + fontWeight = FontWeight.Medium, + color = MorpheColors.Blue + ) + if (disabledPatches.isNotEmpty()) { + Spacer(Modifier.width(6.dp)) + Text( + text = "·", + fontSize = 11.sp, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + Spacer(Modifier.width(6.dp)) + Text( + text = "${disabledPatches.size} disabled", + fontSize = 11.sp, + fontFamily = mono, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f) + ) + } + Spacer(Modifier.weight(1f)) + Icon( + imageVector = Icons.Default.KeyboardArrowDown, + contentDescription = if (isPatchListExpanded) "Collapse" else "Expand", + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier + .size(18.dp) + .graphicsLayer { rotationZ = chevronRotation } + ) + } + + // Expanded patch list + AnimatedVisibility( + visible = isPatchListExpanded, + enter = expandVertically(tween(200)) + fadeIn(tween(200)), + exit = shrinkVertically(tween(200)) + fadeOut(tween(200)) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp) + .padding(bottom = 14.dp) + ) { + // Search bar + OutlinedTextField( + value = patchSearchQuery, + onValueChange = { patchSearchQuery = it }, + placeholder = { + Text("Search patches…", fontSize = 11.sp, fontFamily = mono) + }, + leadingIcon = { + Icon( + Icons.Default.Search, null, + tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + modifier = Modifier.size(14.dp) + ) + }, + trailingIcon = { + if (patchSearchQuery.isNotEmpty()) { + IconButton(onClick = { patchSearchQuery = "" }) { + Icon( + Icons.Default.Clear, "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(12.dp) + ) + } + } + }, + singleLine = true, + textStyle = LocalTextStyle.current.copy(fontSize = 11.sp, fontFamily = mono), + shape = RoundedCornerShape(corners.small), + modifier = Modifier.fillMaxWidth(), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MorpheColors.Blue, + unfocusedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.15f) + ) + ) + + Spacer(Modifier.height(10.dp)) + + // Filter patches by search + val filteredPatches = if (patchSearchQuery.isBlank()) { + compatiblePatches + } else { + compatiblePatches.filter { + it.name.contains(patchSearchQuery, ignoreCase = true) || + it.description.contains(patchSearchQuery, ignoreCase = true) + } + } + + // Chips in flow layout + FlowRow( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.fillMaxWidth() + ) { + filteredPatches.forEach { patch -> + val isEnabled = patch.isEnabled + val chipBorder = if (isEnabled) MorpheColors.Blue.copy(alpha = 0.5f) + else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.15f) + val chipBg = if (isEnabled) MorpheColors.Blue.copy(alpha = 0.08f) + else Color.Transparent + val chipTextColor = if (isEnabled) MorpheColors.Blue + else MaterialTheme.colorScheme.onSurface.copy(alpha = 0.35f) + + Box( + modifier = Modifier + .border(1.dp, chipBorder, RoundedCornerShape(corners.small)) + .background(chipBg, RoundedCornerShape(corners.small)) + .padding(horizontal = 8.dp, vertical = 4.dp) + ) { + Text( + text = patch.name, + fontSize = 10.sp, + fontWeight = if (isEnabled) FontWeight.Medium else FontWeight.Normal, + fontFamily = mono, + color = chipTextColor, + maxLines = 1 + ) + } + } + } + + if (filteredPatches.isEmpty() && patchSearchQuery.isNotBlank()) { + Spacer(Modifier.height(8.dp)) + Text( + text = "No patches matching \"$patchSearchQuery\"", + fontSize = 11.sp, + fontFamily = mono, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f) + ) + } + } + } + } } } @@ -659,7 +907,8 @@ private fun ReadyContent( Spacer(modifier = Modifier.height(6.dp)) Text( - text = "Uses latest patches with recommended settings", + text = "${enabledPatches.size} patches will be applied" + + if (disabledPatches.isNotEmpty()) " · ${disabledPatches.size} excluded" else "", fontSize = 11.sp, fontFamily = mono, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index 487b38a..e2abf53 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -250,10 +250,15 @@ class QuickPatchViewModel( val result = analyzeApk(file) if (result != null) { + // Filter patches compatible with this package (ignore version — patcher will attempt all) + val compatible = cachedPatches.filter { + it.isCompatibleWith(result.packageName) + } _uiState.value = _uiState.value.copy( phase = QuickPatchPhase.READY, apkFile = file, - apkInfo = result + apkInfo = result, + compatiblePatches = compatible ) } else { _uiState.value = _uiState.value.copy( @@ -342,7 +347,11 @@ class QuickPatchViewModel( // TODO: Re-enable when checksums are provided via .mpp files val checksumStatus = ChecksumStatus.NotConfigured - Logger.info("Quick mode: Analyzed $displayName v$versionName (recommended: $recommendedVersion, status: $versionStatus)") + // Extract architectures — scan the original file (bundles have splits with native libs) + val architectures = FileUtils.extractArchitectures(if (isBundleFormat) file else apkToParse) + val minSdk = meta.minSdkVersion?.toIntOrNull() + + Logger.info("Quick mode: Analyzed $displayName v$versionName (recommended: $recommendedVersion, status: $versionStatus, archs: $architectures)") QuickApkInfo( fileName = file.name, @@ -354,7 +363,9 @@ class QuickPatchViewModel( isRecommendedVersion = isRecommendedVersion, versionStatus = versionStatus, versionWarning = versionWarning, - checksumStatus = checksumStatus + checksumStatus = checksumStatus, + architectures = architectures, + minSdk = minSdk ) } } catch (e: Exception) { @@ -568,7 +579,9 @@ data class QuickApkInfo( val isRecommendedVersion: Boolean, val versionStatus: VersionStatus = VersionStatus.UNKNOWN, val versionWarning: String?, - val checksumStatus: ChecksumStatus + val checksumStatus: ChecksumStatus, + val architectures: List = emptyList(), + val minSdk: Int? = null ) { val formattedSize: String get() = when { @@ -597,5 +610,7 @@ data class QuickPatchUiState( val supportedApps: List = emptyList(), val patchesVersion: String? = null, val patchLoadError: String? = null, - val isOffline: Boolean = false + val isOffline: Boolean = false, + // Compatible patches for the loaded APK + val compatiblePatches: List = emptyList() ) diff --git a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt index 4f53a08..315e9d4 100644 --- a/src/main/kotlin/app/morphe/gui/util/FileUtils.kt +++ b/src/main/kotlin/app/morphe/gui/util/FileUtils.kt @@ -205,4 +205,44 @@ object FileUtils { @Deprecated("Use extractBaseApkFromBundle instead", ReplaceWith("extractBaseApkFromBundle(apkmFile)")) fun extractBaseApkFromApkm(apkmFile: File): File? = extractBaseApkFromBundle(apkmFile) + + /** + * Extract supported CPU architectures from native libraries in an APK or bundle. + * Scans for lib// directories, and for bundles also detects arch from split APK names. + */ + fun extractArchitectures(file: File): List { + return try { + ZipFile(file).use { zip -> + val archDirs = mutableSetOf() + + // Scan for lib// entries + zip.entries().asSequence() + .map { it.name } + .filter { it.startsWith("lib/") } + .mapNotNull { path -> + val parts = path.split("/") + if (parts.size >= 2) parts[1] else null + } + .forEach { archDirs.add(it) } + + // For bundles: detect arch from split APK names (e.g. split_config.arm64_v8a.apk) + if (archDirs.isEmpty()) { + val knownArchs = setOf("arm64-v8a", "armeabi-v7a", "x86", "x86_64") + zip.entries().asSequence() + .map { it.name } + .filter { it.endsWith(".apk") } + .forEach { name -> + val normalized = name.replace("_", "-") + knownArchs.filter { arch -> normalized.contains(arch) } + .forEach { archDirs.add(it) } + } + } + + archDirs.toList().ifEmpty { listOf("universal") } + } + } catch (e: Exception) { + Logger.warn("Could not extract architectures: ${e.message}") + emptyList() + } + } } diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 19d9830..fbd9aaa 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -152,11 +152,12 @@ class PatchService { ) } ?: emptyList(), options = this.options.values.map { opt -> + Logger.info("PatchService: option key='${opt.key}' title='${opt.title}' type=${opt.type}") PatchOption( key = opt.key, title = opt.title ?: opt.key, description = opt.description ?: "", - type = mapKTypeToOptionType(opt.type), + type = mapKTypeToOptionType(opt.type, opt.key, opt.title), default = opt.default?.toString(), required = opt.required ) @@ -168,7 +169,7 @@ class PatchService { /** * Map Kotlin KType to GUI PatchOptionType. */ - private fun mapKTypeToOptionType(kType: KType): PatchOptionType { + private fun mapKTypeToOptionType(kType: KType, key: String = "", title: String? = null): PatchOptionType { val typeName = kType.toString() return when { typeName.contains("Boolean") -> PatchOptionType.BOOLEAN @@ -176,7 +177,14 @@ class PatchService { typeName.contains("Long") -> PatchOptionType.LONG typeName.contains("Float") || typeName.contains("Double") -> PatchOptionType.FLOAT typeName.contains("List") || typeName.contains("Array") || typeName.contains("Set") -> PatchOptionType.LIST - else -> PatchOptionType.STRING + typeName.contains("File") || typeName.contains("Path") || typeName.contains("InputStream") -> PatchOptionType.FILE + else -> { + // Heuristic: detect file path options from key/title + val hint = (key + " " + (title ?: "")).lowercase() + val fileKeywords = listOf("icon", "image", "logo", "banner", "path", "file", "png", "jpg", "jpeg", "webp") + if (fileKeywords.any { hint.contains(it) }) PatchOptionType.FILE + else PatchOptionType.STRING + } } } } From c04a7a6d1e6cff9c4e3d0745a00b7dc823644b82 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Tue, 31 Mar 2026 09:33:32 +0530 Subject: [PATCH 43/49] Minor theme fixes --- .../kotlin/app/morphe/gui/ui/theme/Theme.kt | 75 ++++++++++++++++++- 1 file changed, 74 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt index d01c21f..1ee870b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt +++ b/src/main/kotlin/app/morphe/gui/ui/theme/Theme.kt @@ -25,6 +25,68 @@ object MorpheColors { val TextDark = Color(0xFF1C1C1C) } +// ════════════════════════════════════════════════════════════════════ +// ACCENT COLOR SYSTEM +// ════════════════════════════════════════════════════════════════════ + +/** + * Per-theme accent colors. Components should read from LocalMorpheAccents + * instead of using MorpheColors.Blue/Teal directly. + */ +data class MorpheAccentColors( + val primary: Color, // Buttons, selections, links (replaces MorpheColors.Blue) + val secondary: Color, // Badges, options, success states (replaces MorpheColors.Teal) + val warning: Color = Color(0xFFFF9800), // Warning states (was hardcoded everywhere) +) + +val LocalMorpheAccents = compositionLocalOf { MorpheAccentColors(MorpheColors.Blue, MorpheColors.Teal) } + +/** Morphe Dark — brand blue + teal on dark gray. */ +private val DarkAccents = MorpheAccentColors( + primary = MorpheColors.Blue, + secondary = MorpheColors.Teal, +) + +/** Amoled — slightly brighter accents to pop on pure black. */ +private val AmoledAccents = MorpheAccentColors( + primary = Color(0xFF4A7FFF), // Brighter blue for pure black + secondary = Color(0xFF00BFA5), // Brighter teal +) + +/** Morphe Light — brand colors work fine on light backgrounds. */ +private val LightAccents = MorpheAccentColors( + primary = MorpheColors.Blue, + secondary = MorpheColors.Teal, +) + +/** Nord — native Nord palette. Arctic frost + aurora. */ +private val NordAccents = MorpheAccentColors( + primary = Color(0xFF88C0D0), // Nord Frost + secondary = Color(0xFFA3BE8C), // Nord Aurora Green + warning = Color(0xFFEBCB8B), // Nord Aurora Yellow +) + +/** Catppuccin Mocha — native Catppuccin palette. Mauve + teal. */ +private val CatppuccinAccents = MorpheAccentColors( + primary = Color(0xFFCBA6F7), // Mauve + secondary = Color(0xFF94E2D5), // Teal + warning = Color(0xFFFAB387), // Peach +) + +/** Sakura — warm rose + dusty lavender. */ +private val SakuraAccents = MorpheAccentColors( + primary = Color(0xFFD4567A), // Deep rose + secondary = Color(0xFF9A6DAF), // Dusty lavender + warning = Color(0xFFE8874A), // Warm amber +) + +/** Matcha — forest green + sage. */ +private val MatchaAccents = MorpheAccentColors( + primary = Color(0xFF5A9A4E), // Forest green + secondary = Color(0xFF7AADAF), // Sage teal + warning = Color(0xFFD4944A), // Warm ochre +) + // ════════════════════════════════════════════════════════════════════ // CORNER / SHAPE STYLE SYSTEM // ════════════════════════════════════════════════════════════════════ @@ -230,10 +292,21 @@ fun MorpheTheme( val corners = if (themePreference.isSoft()) SoftCorners else SharpCorners val font = if (themePreference.isSoft()) Nunito else JetBrainsMono + val accents = when (themePreference) { + ThemePreference.DARK -> DarkAccents + ThemePreference.AMOLED -> AmoledAccents + ThemePreference.LIGHT -> LightAccents + ThemePreference.NORD -> NordAccents + ThemePreference.CATPPUCCIN -> CatppuccinAccents + ThemePreference.SAKURA -> SakuraAccents + ThemePreference.MATCHA -> MatchaAccents + ThemePreference.SYSTEM -> if (isSystemInDarkTheme()) DarkAccents else LightAccents + } CompositionLocalProvider( LocalMorpheCorners provides corners, - LocalMorpheFont provides font + LocalMorpheFont provides font, + LocalMorpheAccents provides accents ) { MaterialTheme( colorScheme = colorScheme, From 8cae648cf712956c120ccb1b626f00cf7d8b3d39 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:49:37 +0530 Subject: [PATCH 44/49] Delete CLAUDE.md --- CLAUDE.md | 89 ------------------------------------------------------- 1 file changed, 89 deletions(-) delete mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 4f4e9e8..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,89 +0,0 @@ -# Morphe/ MorpheCLI (will be changed to Morphe Desktop) - Developer Guide - -## Project Overview -Morphe Desktop is a command-line and a GUI application that uses Morphe Patcher to patch Android apps. It has 2 parts -- **CLI**: Opens when the user calls the .jar file from a terminal. -- **GUI**: Opens when the user double-clicks the jar - -## Design Thinking - -Before coding, understand the context and commit to a BOLD aesthetic direction: -- **Purpose**: What problem does this interface solve? Who uses it? -- **Tone**: Commit to a distinct direction: brutally minimal, maximalist chaos, luxury/refined, lo-fi/zine, dark/moody, soft/pastel, editorial/magazine, brutalist/raw, retro-futuristic, handcrafted/artisanal, organic/natural, art deco/geometric, playful/whimsical, industrial/utilitarian, etc. There are infinite varieties to start from and surpass. Use these as inspiration, but the final design should feel singular, with every detail working in service of one cohesive direction. -- **Constraints**: Technical requirements (framework, performance, accessibility). -- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember? - -**CRITICAL**: Choose a clear conceptual direction and execute it vigorously. Bold maximalism and refined minimalism both work - the key is intentionality, not intensity. - -Then implement working code (HTML/CSS/JS, React, Vue, etc.) that is: -- Production-grade, functional, and responsive -- Visually striking and memorable -- Cohesive with a clear aesthetic point-of-view -- Meticulously refined in every detail - -**Morphe CLI Context (MANDATORY — root every decision here)**: -- **Purpose**: This is a desktop GUI wrapper for morphe-cli (Morphe Patcher). Users select an APK (or APKM bundle), choose from community patches, apply them, sign the output, and optionally push to device via ADB. Core flows: drag-and-drop APK, searchable patch list with descriptions/categories, live log console during patching (long-running process), progress visualization, output APK management. Users are power users/modders (ReVanced/Morphe veterans) — they want speed, transparency, and raw power, not hand-holding. -- **Who uses it**: Tech-savvy Android tinkerers who already run the CLI. Desktop advantage: easier file management, bigger screen for logs/patches, keyboard shortcuts, multi-window feel. -- **Differentiation (UNFORGETTABLE)**: Make the patching process feel like a cyber-hacker ritual. Visual disassembly animation, neon code-rain progress, glitch effects on success/failure, terminal-style logs with syntax coloring. Someone should remember "that one desktop patcher that looks like it belongs in a cyberdeck". Beat Vary (existing simple Gio GUI) by being visually addictive yet perfectly functional. - -**CRITICAL**: The UI must feel like a professional dev tool that secretly has underground soul. Never default to plain Material 3 mobile patterns. - -## Frontend Aesthetics Guidelines - -Focus on: -- **Typography**: Typography carries the design's singular voice. Choose fonts with interesting personality. Default fonts signal default thinking: skip Arial, Inter, Roboto, system stacks. Font choices should be inseparable from the aesthetic direction. Display type should be expressive, even risky. Body text should be legible, refined. Pair them like actors in a scene. Work the full typographic range: size, weight, case, spacing to establish hierarchy. -- **Color & Theme**: Commit to a cohesive aesthetic. Palettes should take a clear position: bold and saturated, moody and restrained, or high-contrast and minimal. Lead with a dominant color, punctuate with sharp accents. Avoid timid, non-committal distributions. Use CSS variables for consistency. -- **Motion**: Use animations for effects and micro-interactions. Prioritize CSS-only solutions for HTML. Use Motion library for React when available. Focus on high-impact moments: one well-orchestrated page load with staggered reveals (animation-delay) creates more delight than scattered micro-interactions. Use scroll-triggering and hover states that surprise. -- **Spatial Composition**: Unexpected layouts. Asymmetry. Overlap and z-depth. Diagonal flow. Grid-breaking elements. Dramatic scale jumps. Full-bleed moments. Generous negative space OR controlled density. -- **Backgrounds & Visual Details**: Create atmosphere and depth rather than defaulting to solid colors. Add contextual effects and textures that match the overall aesthetic. Apply creative forms like gradient meshes, noise and grain overlays, geometric patterns, layered transparencies and glassmorphism, dramatic or soft shadows and glows, parallax depth, decorative borders and clip-path shapes, print-inspired textures (halftone, duotone, stipple), knockout typography, and custom cursors. - -**CRITICAL ADDITIONS FOR COMPOSE DESKTOP**: -- **Framework Reality**: You are writing **Kotlin + Jetpack Compose for Desktop** (not HTML/React). Use `@Composable`, `Modifier`, `MaterialTheme` (or fully custom), `Window`, `MenuBar`, `Tray` if needed. Target Windows/macOS/Linux with native window chrome or custom undecorated title bar. Output complete, copy-pasteable, production-ready code with `main()` + `application { Window(...) }`. Include previews where possible. -- **Theming Mastery** (use official custom design system patterns): - - Extend `MaterialTheme` or build a full custom system with `CompositionLocal` (e.g., `LocalMorpheColors`, `LocalMorpheTypography`). - - Default to **dark theme** (dev tool law). Create semantic colors: `neonPrimary`, `terminalGreen`, `patchAccent`, `errorGlitchRed`. - - Typography: Pair a bold display font (load via `FontFamily` from resources — no Inter/Roboto) with **JetBrains Mono** or VT323-style monospace for logs/console. Make hierarchy dramatic (huge patch titles, tiny hex metadata). - - Shapes: Sharp corners or subtle hexagonal cuts for "patcher" tech feel. Use `absoluteElevation` for dynamic surface tints. -- **Motion & Delight (Compose-native)**: - - Staggered reveals on window open with `AnimatedVisibility` + `spring()` + `delayMillis`. - - Patching progress: Custom `Canvas`-based animation (code rain, hex particles, or APK "decompiling" scanline effect — performant, not heavy). - - Hover/glitch: `graphicsLayer` + `scale` + subtle `colorMatrix` for neon glows and chromatic aberration on active elements. - - Never overdo micro-animations — one hero animation during patching > 50 tiny ones. -- **Desktop-Power Interactions**: - - Drag & drop APK anywhere (use `onDrag` modifiers). - - Keyboard shortcuts (Cmd/Ctrl+O, Enter to patch). - - Context menus, tooltips, searchable LazyColumn for patches. - - Live console output panel with auto-scroll and copy button. - - Adaptive layout: Use `WindowSizeClass` — collapse to single pane on small windows, multi-pane (sidebar + preview + logs) on large. -- **Spatial Composition & Visual Depth**: - - Asymmetric multi-pane layout: Left = searchable patches (with preview icons via Canvas or SVG), Center = APK info + drop zone, Right/Bottom = live terminal logs + progress. - - Full-bleed hero moments during patching (overlay the whole window with animated background). - - Subtle background: Terminal grid + very light noise texture (via `Canvas` or `BitmapShader`). Glassmorphism or heavy drop-shadows for cards. -- **Production-Grade Requirements**: - - Full responsiveness + accessibility (`semantics` for screen readers, high contrast). - - Performance: Keyed `LazyColumn`, state hoisting, minimal recomposition. Patching runs in background coroutine. - - Error states with dramatic feedback (red glitch flash). - - Include sample patch data and fake progress for demo. - - Use only Compose Desktop + Material3 + kotlinx.coroutines (no external libs unless absolutely necessary). - -**NEVER**: -- Use mobile-first Material patterns (bottom nav, FABs). -- Default fonts/colors/layouts. -- Make it look like another ReVanced Manager clone. -- Heavy GPU effects that kill performance on patching large APKs. -- Using generic AI-generated aesthetics like overused font families (Inter, Roboto, Arial, Space Grotesk, system fonts), clichéd color schemes (particularly purple gradients on white backgrounds), predictable layouts and component patterns, and cookie-cutter designs that lack context-specific character. - -**INSTEAD**: -- Commit violently to one singular aesthetic (THIS IS ONLY AN EXAMPLE. PICK SOMETHING DEPENDING ON THE CONTEXT, e.g., "Neon Cyberdeck Terminal" — dark void background, electric cyan/magenta accents, monospace logs, glitch hover states, code-rain progress). Or "Brutalist Dev Console" — raw, high-contrast, industrial. Or "Refined IntelliJ+Neon" (Jewel-inspired but with Morphe soul). Every pixel serves the "I am hacking my apps" fantasy. -- Build creatively on the user's intent, and make unexpected choices that feel genuinely designed for the context. Every design should feel distinct. Actively explore the full range: light and dark themes, unexpected font pairings, substantially varied aesthetic directions. Let the specific context drive choices, NOT familiar defaults. - -**IMPORTANT**: Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, elegance, and precision. All designs need careful attention to spacing, typography, and subtle details. Excellence comes from executing the vision well. - -Then implement working, complete Kotlin code that is: -- Fully functional (drag-drop, patch selection, animated progress, live logs) -- Visually unforgettable -- Meticulously refined (perfect spacing, hover states, loading skeletons) -- Ready to compile in a standard Compose Desktop project - - -Remember: Claude you are capable of extraordinary, award-worthy creative work. Don't hold back, show what's truly possible, and commit relentlessly to a distinctive and unforgettable vision. From 3616a97e9014630bc81a2fdcdeb2fa5e3e845098 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Wed, 1 Apr 2026 16:18:34 +0530 Subject: [PATCH 45/49] Final FIX HOPEFULLY --- build.gradle.kts | 3 + gradle/libs.versions.toml | 6 + .../app/morphe/engine/ApkLibraryStripper.kt | 0 .../gui/data/repository/PatchSourceManager.kt | 5 + .../gui/ui/components/DeviceIndicator.kt | 1 - .../gui/ui/components/DraggableHeaderArea.kt | 5 + .../gui/ui/components/LottieAnimation.kt | 6 + .../morphe/gui/ui/components/SakuraPetals.kt | 5 + .../gui/ui/components/SettingsDialog.kt | 2 - .../screens/patching/PatchingScreenModel.kt | 241 ------------------ .../ui/screens/patching/PatchingViewModel.kt | 7 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 5 + 12 files changed, 41 insertions(+), 245 deletions(-) delete mode 100644 src/main/kotlin/app/morphe/engine/ApkLibraryStripper.kt delete mode 100644 src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreenModel.kt diff --git a/build.gradle.kts b/build.gradle.kts index 09c0507..85a2caf 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -100,6 +100,9 @@ dependencies { implementation(libs.voyager.koin) implementation(libs.voyager.transitions) + // -- JBR API (macOS title bar customization) ---------------------------- + implementation(libs.jbr.api) + // -- APK Parsing (GUI) ------------------------------------------------- implementation(libs.apk.parser) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5f5e901..95f6397 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -26,6 +26,9 @@ voyager = "1.1.0-beta03" coroutines = "1.10.2" kotlinx-serialization = "1.9.0" +# JBR +jbr-api = "1.5.0" + # APK apk-parser = "2.6.10" @@ -65,6 +68,9 @@ kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines- # Serialization kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } +# JBR +jbr-api = { module = "org.jetbrains.runtime:jbr-api", version.ref = "jbr-api" } + # APK apk-parser = { module = "net.dongliu:apk-parser", version.ref = "apk-parser" } diff --git a/src/main/kotlin/app/morphe/engine/ApkLibraryStripper.kt b/src/main/kotlin/app/morphe/engine/ApkLibraryStripper.kt deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt b/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt index 27b8fcb..0a540b0 100644 --- a/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt +++ b/src/main/kotlin/app/morphe/gui/data/repository/PatchSourceManager.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + package app.morphe.gui.data.repository import app.morphe.gui.data.model.PatchSource diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt index 736cd53..26fc86a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/DeviceIndicator.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown diff --git a/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt b/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt index e8c278a..da83667 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/DraggableHeaderArea.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + package app.morphe.gui.ui.components import androidx.compose.foundation.window.WindowDraggableArea diff --git a/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt b/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt index 44e624c..8ffd9c2 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/LottieAnimation.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + package app.morphe.gui.ui.components import androidx.compose.foundation.Canvas @@ -11,6 +16,7 @@ import org.jetbrains.skia.Rect as SkiaRect import org.jetbrains.skia.skottie.Animation /** + * THIS IS STILL A WORK IN PROGRESS. THIS ANIMATION IS STILL NOT GOOD ENOUGH. NEEDS MUCH REWORK. * Plays a Lottie JSON animation using Skia's built-in Skottie renderer. * No extra dependencies needed — Compose Desktop includes Skottie via Skiko. * diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt b/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt index 906169f..8ba918a 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SakuraPetals.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + package app.morphe.gui.ui.components import androidx.compose.foundation.Canvas diff --git a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt index a02193e..71ba3af 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/SettingsDialog.kt @@ -23,8 +23,6 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreenModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreenModel.kt deleted file mode 100644 index ed85fb8..0000000 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreenModel.kt +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2026 Morphe. - * https://github.com/MorpheApp/morphe-cli - */ - -package app.morphe.gui.ui.screens.patching - -import cafe.adriel.voyager.core.model.ScreenModel -import cafe.adriel.voyager.core.model.screenModelScope -import app.morphe.gui.data.model.PatchConfig -import kotlinx.coroutines.Job -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.launch -import app.morphe.gui.util.Logger -import app.morphe.gui.util.PatchService -import java.io.File - -class PatchingScreenModel( - private val config: PatchConfig, - private val patchService: PatchService -) : ScreenModel { - - private val _uiState = MutableStateFlow(PatchingUiState()) - val uiState: StateFlow = _uiState.asStateFlow() - - private var patchingJob: Job? = null - - fun startPatching() { - if (_uiState.value.status != PatchingStatus.IDLE) return - - patchingJob = screenModelScope.launch { - _uiState.value = _uiState.value.copy( - status = PatchingStatus.PREPARING, - logs = listOf(LogEntry("Preparing to patch...", LogLevel.INFO)) - ) - - addLog("Initializing patcher...", LogLevel.INFO) - - // Start patching - _uiState.value = _uiState.value.copy( - status = PatchingStatus.PATCHING, - totalPatches = config.enabledPatches.size, - patchedCount = 0, - progress = 0f - ) - addLog("Starting patch process...", LogLevel.INFO) - addLog("Input: ${File(config.inputApkPath).name}", LogLevel.INFO) - addLog("Output: ${File(config.outputApkPath).name}", LogLevel.INFO) - addLog("Patches: ${config.enabledPatches.size} enabled", LogLevel.INFO) - - // Use PatchService for direct library patching - val result = patchService.patch( - patchesFilePath = config.patchesFilePath, - inputApkPath = config.inputApkPath, - outputApkPath = config.outputApkPath, - enabledPatches = config.enabledPatches, - disabledPatches = config.disabledPatches, - options = config.patchOptions, - exclusiveMode = config.useExclusiveMode, - keepArchitectures = config.keepArchitectures, - continueOnError = config.continueOnError, - onProgress = { message -> - parseAndAddLog(message) - } - ) - - result.fold( - onSuccess = { patchResult -> - if (patchResult.success) { - addLog("Patching completed successfully!", LogLevel.SUCCESS) - addLog("Applied ${patchResult.appliedPatches.size} patches", LogLevel.SUCCESS) - _uiState.value = _uiState.value.copy( - status = PatchingStatus.COMPLETED, - outputPath = config.outputApkPath, - progress = 1f - ) - Logger.info("Patching completed: ${config.outputApkPath}") - } else { - val failedMsg = if (patchResult.failedPatches.isNotEmpty()) { - "Failed patches: ${patchResult.failedPatches.joinToString(", ")}" - } else { - "Patching failed" - } - addLog(failedMsg, LogLevel.ERROR) - _uiState.value = _uiState.value.copy( - status = PatchingStatus.FAILED, - error = "Patching failed. Check logs for details." - ) - Logger.error("Patching failed: ${patchResult.failedPatches}") - } - }, - onFailure = { e -> - addLog("Error: ${e.message}", LogLevel.ERROR) - _uiState.value = _uiState.value.copy( - status = PatchingStatus.FAILED, - error = e.message ?: "Unknown error occurred" - ) - Logger.error("Patching error", e) - } - ) - } - } - - fun cancelPatching() { - patchingJob?.cancel() - patchingJob = null - addLog("Patching cancelled by user", LogLevel.WARNING) - _uiState.value = _uiState.value.copy( - status = PatchingStatus.CANCELLED - ) - Logger.info("Patching cancelled by user") - } - - private fun addLog(message: String, level: LogLevel) { - val entry = LogEntry(message, level) - _uiState.value = _uiState.value.copy( - logs = _uiState.value.logs + entry - ) - } - - private fun parseAndAddLog(line: String) { - val level = when { - line.contains("error", ignoreCase = true) -> LogLevel.ERROR - line.contains("warning", ignoreCase = true) -> LogLevel.WARNING - line.contains("success", ignoreCase = true) || - line.contains("completed", ignoreCase = true) || - line.contains("done", ignoreCase = true) -> LogLevel.SUCCESS - line.contains("patching", ignoreCase = true) || - line.contains("applying", ignoreCase = true) -> LogLevel.PROGRESS - else -> LogLevel.INFO - } - addLog(line, level) - - // Try to extract progress information - parseProgress(line) - } - - private fun parseProgress(line: String) { - // Pattern: "Executing patch X of Y: PatchName" or similar - val executingPattern = Regex("""(?:Executing|Applying)\s+patch\s+(\d+)\s+of\s+(\d+)(?::\s*(.+))?""", RegexOption.IGNORE_CASE) - val executingMatch = executingPattern.find(line) - if (executingMatch != null) { - val current = executingMatch.groupValues[1].toIntOrNull() ?: 0 - val total = executingMatch.groupValues[2].toIntOrNull() ?: 0 - val patchName = executingMatch.groupValues.getOrNull(3)?.trim() - - if (total > 0) { - val progress = current.toFloat() / total.toFloat() - _uiState.value = _uiState.value.copy( - progress = progress, - patchedCount = current, - totalPatches = total, - currentPatch = patchName, - hasReceivedProgressUpdate = true - ) - } - return - } - - // Pattern: "[X/Y]" or "(X/Y)" - val fractionPattern = Regex("""[\[\(](\d+)/(\d+)[\]\)]""") - val fractionMatch = fractionPattern.find(line) - if (fractionMatch != null) { - val current = fractionMatch.groupValues[1].toIntOrNull() ?: 0 - val total = fractionMatch.groupValues[2].toIntOrNull() ?: 0 - - if (total > 0) { - val progress = current.toFloat() / total.toFloat() - _uiState.value = _uiState.value.copy( - progress = progress, - patchedCount = current, - totalPatches = total, - hasReceivedProgressUpdate = true - ) - } - return - } - - // Pattern: "X%" percentage - val percentPattern = Regex("""(\d+(?:\.\d+)?)\s*%""") - val percentMatch = percentPattern.find(line) - if (percentMatch != null) { - val percent = percentMatch.groupValues[1].toFloatOrNull() ?: 0f - if (percent > 0) { - _uiState.value = _uiState.value.copy( - progress = percent / 100f, - hasReceivedProgressUpdate = true - ) - } - } - } - - fun getConfig(): PatchConfig = config -} - -enum class PatchingStatus { - IDLE, - PREPARING, - PATCHING, - COMPLETED, - FAILED, - CANCELLED -} - -enum class LogLevel { - INFO, - SUCCESS, - WARNING, - ERROR, - PROGRESS -} - -data class LogEntry( - val message: String, - val level: LogLevel, - val id: String = "${System.currentTimeMillis()}_${System.nanoTime()}" -) - -data class PatchingUiState( - val status: PatchingStatus = PatchingStatus.IDLE, - val logs: List = emptyList(), - val outputPath: String? = null, - val error: String? = null, - val progress: Float = 0f, - val currentPatch: String? = null, - val patchedCount: Int = 0, - val totalPatches: Int = 0, - val hasReceivedProgressUpdate: Boolean = false -) { - val isInProgress: Boolean - get() = status == PatchingStatus.PREPARING || status == PatchingStatus.PATCHING - - val canCancel: Boolean - get() = isInProgress - - // Only show determinate progress if we've actually received progress updates from CLI - val hasProgress: Boolean - get() = hasReceivedProgressUpdate && progress > 0f -} diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt index 8871a9b..dde8b5e 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingViewModel.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + package app.morphe.gui.ui.screens.patching import cafe.adriel.voyager.core.model.ScreenModel @@ -56,7 +61,7 @@ class PatchingViewModel( disabledPatches = config.disabledPatches, options = config.patchOptions, exclusiveMode = config.useExclusiveMode, - striplibs = config.striplibs, + keepArchitectures = config.keepArchitectures, continueOnError = config.continueOnError, onProgress = { message -> parseAndAddLog(message) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index 716fd6b..bf9ea41 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -1,3 +1,8 @@ +/* + * Copyright 2026 Morphe. + * https://github.com/MorpheApp/morphe-cli + */ + package app.morphe.gui.ui.screens.quick import androidx.compose.animation.* From 2b99b0183594bf944a86e4c399b6be6fd7106680 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:12:51 +0530 Subject: [PATCH 46/49] Fixed image selection fix for patch screen's patch options --- src/main/kotlin/app/morphe/gui/util/PatchService.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 47d832e..68e0e19 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -162,7 +162,7 @@ class PatchService { key = opt.key, title = opt.title ?: opt.key, description = opt.description ?: "", - type = mapKTypeToOptionType(opt.type), + type = mapKTypeToOptionType(opt.type, opt.key, opt.title ?: opt.key), default = opt.default?.toString(), required = opt.required ) @@ -174,7 +174,7 @@ class PatchService { /** * Map Kotlin KType to GUI PatchOptionType. */ - private fun mapKTypeToOptionType(kType: KType): PatchOptionType { + private fun mapKTypeToOptionType(kType: KType, key: String, title: String): PatchOptionType { val typeName = kType.toString() return when { typeName.contains("Boolean") -> PatchOptionType.BOOLEAN @@ -182,7 +182,12 @@ class PatchService { typeName.contains("Long") -> PatchOptionType.LONG typeName.contains("Float") || typeName.contains("Double") -> PatchOptionType.FLOAT typeName.contains("List") || typeName.contains("Array") || typeName.contains("Set") -> PatchOptionType.LIST - else -> PatchOptionType.STRING + typeName.contains("File") || typeName.contains("Path") || typeName.contains("InputStream") -> PatchOptionType.FILE + else -> { + val combined = "$key $title".lowercase() + val fileKeywords = listOf("icon", "image", "logo", "banner", "path", "file", "png", "jpg") + if (fileKeywords.any { it in combined }) PatchOptionType.FILE else PatchOptionType.STRING + } } } } From 02738c1d59acb8b58c03bbb4fc0e50a5ef770ae8 Mon Sep 17 00:00:00 2001 From: LisoUseInAIKyrios <118716522+LisoUseInAIKyrios@users.noreply.github.com> Date: Thu, 2 Apr 2026 12:34:46 +0200 Subject: [PATCH 47/49] refactor --- .../app/morphe/cli/command/OptionsCommand.kt | 2 +- .../app/morphe/gui/data/model/SupportedApp.kt | 30 ------------------- 2 files changed, 1 insertion(+), 31 deletions(-) diff --git a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt index 682f363..7320a4e 100644 --- a/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt +++ b/src/main/kotlin/app/morphe/cli/command/OptionsCommand.kt @@ -16,7 +16,7 @@ import java.util.logging.Logger @Command( name = "options-create", - description = ["Create an options JSON file for the patches and options."] , + description = ["Create an options JSON file for the patches and options."], ) internal object OptionsCommand : Callable { diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index 923bc3a..0b0c8c5 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -28,36 +28,6 @@ data class SupportedApp( "com.google.android.youtube" to "YouTube", "com.google.android.apps.youtube.music" to "YouTube Music", "com.reddit.frontpage" to "Reddit", - "com.duolingo" to "Duolingo", - "com.myfitnesspal.android" to "MyFitnessPal", - "com.pandora.android" to "Pandora", - "ch.protonvpn.android" to "ProtonVPN", - "com.amazon.avod.thirdpartyclient" to "Prime Video", - "com.getmimo" to "Mimo", - "com.zombodroid.MemeGenerator" to "Meme Generator", - "com.sofascore.results" to "SofaScore", - "pl.solidexplorer2" to "Solid Explorer", - "com.bambuna.podcastaddict" to "Podcast Addict", - "com.wallpaperscraft.wallpaper" to "WallpapersCraft", - "cn.wps.moffice_eng" to "WPS Office", - "com.merriamwebster" to "Merriam-Webster", - "com.busuu.android.enc" to "Busuu", - "jp.ne.ibis.ibispaintx.app" to "ibisPaint X", - "com.laurencedawson.reddit_sync" to "Sync for Reddit", - "com.laurencedawson.reddit_sync.pro" to "Sync for Reddit Pro", - "com.laurencedawson.reddit_sync.dev" to "Sync for Reddit Dev", - "com.andrewshu.android.reddit" to "Reddit is Fun", - "free.reddit.news" to "Relay for Reddit", - "reddit.news" to "Relay for Reddit Pro", - "com.rubenmayayo.reddit" to "Boost for Reddit", - "o.o.joey" to "Joey for Reddit", - "o.o.joey.pro" to "Joey for Reddit Pro", - "o.o.joey.dev" to "Joey for Reddit Dev", - "com.onelouder.baconreader" to "BaconReader", - "com.onelouder.baconreader.premium" to "BaconReader Premium", - "me.edgan.redditslide" to "Slide for Reddit", - "io.syncapps.lemmy_sync" to "Sync for Lemmy", - "org.cygnusx1.continuum" to "Continuum for Reddit", ) knownNames[packageName]?.let { return it } From 6acaba2d5e155146bd1a420240034810e1daf3e0 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:22:17 +0530 Subject: [PATCH 48/49] Windows fixes + Minor UI tweaks --- src/main/kotlin/app/morphe/gui/App.kt | 21 ++- src/main/kotlin/app/morphe/gui/GuiMain.kt | 39 +++- .../gui/ui/components/TitleBarInsets.kt | 3 +- .../morphe/gui/ui/screens/home/HomeScreen.kt | 178 +++++++++++++++--- .../screens/patches/PatchSelectionScreen.kt | 2 +- .../gui/ui/screens/patches/PatchesScreen.kt | 2 +- .../gui/ui/screens/patching/PatchingScreen.kt | 2 +- .../gui/ui/screens/quick/QuickPatchScreen.kt | 54 ++++-- .../gui/ui/screens/result/ResultScreen.kt | 2 +- 9 files changed, 237 insertions(+), 66 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/App.kt b/src/main/kotlin/app/morphe/gui/App.kt index 140000c..2d9c37a 100644 --- a/src/main/kotlin/app/morphe/gui/App.kt +++ b/src/main/kotlin/app/morphe/gui/App.kt @@ -49,7 +49,10 @@ val LocalModeState = staticCompositionLocalOf { } @Composable -fun App(initialSimplifiedMode: Boolean = true) { +fun App( + initialSimplifiedMode: Boolean = true, + titleBarInsets: TitleBarInsets = TitleBarInsets() +) { LaunchedEffect(Unit) { Logger.init() } @@ -57,12 +60,18 @@ fun App(initialSimplifiedMode: Boolean = true) { KoinApplication(application = { modules(appModule) }) { - AppContent(initialSimplifiedMode) + AppContent( + initialSimplifiedMode = initialSimplifiedMode, + titleBarInsets = titleBarInsets + ) } } @Composable -private fun AppContent(initialSimplifiedMode: Boolean) { +private fun AppContent( + initialSimplifiedMode: Boolean, + titleBarInsets: TitleBarInsets +) { val configRepository: ConfigRepository = koinInject() val patchSourceManager: PatchSourceManager = koinInject() val scope = rememberCoroutineScope() @@ -116,12 +125,6 @@ private fun AppContent(initialSimplifiedMode: Boolean) { } } - val titleBarInsets = remember { - val isMac = System.getProperty("os.name")?.lowercase()?.contains("mac") == true - if (isMac) TitleBarInsets(start = 80.dp, top = 0.dp) - else TitleBarInsets() - } - MorpheTheme(themePreference = themePreference) { CompositionLocalProvider( LocalThemeState provides themeState, diff --git a/src/main/kotlin/app/morphe/gui/GuiMain.kt b/src/main/kotlin/app/morphe/gui/GuiMain.kt index e334acf..473d84b 100644 --- a/src/main/kotlin/app/morphe/gui/GuiMain.kt +++ b/src/main/kotlin/app/morphe/gui/GuiMain.kt @@ -18,6 +18,7 @@ import androidx.compose.ui.window.WindowPosition import androidx.compose.ui.window.application import androidx.compose.ui.window.rememberWindowState import app.morphe.gui.data.model.AppConfig +import app.morphe.gui.ui.components.TitleBarInsets import kotlinx.serialization.json.Json import org.jetbrains.skia.Image import app.morphe.gui.util.FileUtils @@ -69,8 +70,10 @@ fun launchGui(args: Array) = application { // macOS: transparent title bar with expanded height so traffic lights // align with our header row content. Uses JetBrains Runtime custom title bar API. // Other OS: standard decorated window (no-op). - remember { + val titleBarInsets = remember { val isMac = System.getProperty("os.name")?.lowercase()?.contains("mac") == true + val isWindows = System.getProperty("os.name")?.lowercase()?.contains("win") == true + var insets = TitleBarInsets() if (isMac) { window.rootPane.putClientProperty("apple.awt.fullWindowContent", true) window.rootPane.putClientProperty("apple.awt.transparentTitleBar", true) @@ -85,15 +88,31 @@ fun launchGui(args: Array) = application { titleBar.height = 56f titleBar.putProperty("controls.visible", true) decorations.setCustomTitleBar(window, titleBar) + insets = titleBar.toInsets(window, fallbackStartDp = 80f) } catch (_: Exception) { // Not running on JBR — traffic lights stay at default position } } - true + if (isWindows) { + try { + val decorations = com.jetbrains.JBR.getWindowDecorations() + val titleBar = decorations.createCustomTitleBar() + titleBar.height = 50f + titleBar.putProperty("controls.visible", true) + decorations.setCustomTitleBar(window, titleBar) + insets = titleBar.toInsets(window, fallbackEndDp = 138f) + } catch (_: Exception) { + insets = TitleBarInsets(end = 138.dp) + } + } + insets } CompositionLocalProvider(LocalFrameWindowScope provides this) { - App(initialSimplifiedMode = initialSimplifiedMode) + App( + initialSimplifiedMode = initialSimplifiedMode, + titleBarInsets = titleBarInsets + ) } } } @@ -150,3 +169,17 @@ private fun loadAppIcon(): BitmapPainter? { } return null } + +private fun com.jetbrains.WindowDecorations.CustomTitleBar.toInsets( + window: java.awt.Window, + fallbackStartDp: Float = 0f, + fallbackEndDp: Float = 0f +): TitleBarInsets { + val scale = window.graphicsConfiguration?.defaultTransform?.scaleX?.toFloat()?.coerceAtLeast(1f) ?: 1f + val leftDp = (leftInset / scale).takeIf { it > 0f } ?: fallbackStartDp + val rightDp = (rightInset / scale).takeIf { it > 0f } ?: fallbackEndDp + return TitleBarInsets( + start = leftDp.dp, + end = rightDp.dp + ) +} diff --git a/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt b/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt index 707d3b6..7968d9d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt +++ b/src/main/kotlin/app/morphe/gui/ui/components/TitleBarInsets.kt @@ -13,7 +13,8 @@ import androidx.compose.ui.window.FrameWindowScope */ data class TitleBarInsets( val start: androidx.compose.ui.unit.Dp = 0.dp, - val top: androidx.compose.ui.unit.Dp = 0.dp + val top: androidx.compose.ui.unit.Dp = 0.dp, + val end: androidx.compose.ui.unit.Dp = 0.dp ) val LocalTitleBarInsets = compositionLocalOf { TitleBarInsets() } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt index c068436..438155c 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeScreen.kt @@ -34,6 +34,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp @@ -143,6 +144,7 @@ fun HomeScreenContent( } val useHorizontalHeader = maxWidth >= 600.dp + val pinSupportedAppsToBottom = useHorizontalHeader && maxHeight >= 760.dp val patchesLoaded = !uiState.isLoadingPatches && viewModel.getCachedPatchesFile() != null val onChangePatchesClick: () -> Unit = { navigator.push(PatchesScreen( @@ -163,13 +165,105 @@ fun HomeScreenContent( } } + val headerContent: @Composable ColumnScope.() -> Unit = { + if (useHorizontalHeader) { + HeaderBar( + uiState = uiState, + isSmall = isSmall, + onChangePatchesClick = onChangePatchesClick, + onRetry = onRetry + ) + } else { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 16.dp)) + BrandingSection(isCompact = isCompact) + + if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + PatchesVersionCard( + patchesVersion = uiState.patchesVersion!!, + isLatest = uiState.isUsingLatestPatches, + onChangePatchesClick = onChangePatchesClick, + isCompact = isCompact, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + ) + } else if (uiState.isLoadingPatches) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + PatchesLoadingIndicator() + } else if (uiState.patchLoadError != null) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + PatchesVersionCard( + patchesVersion = "NOT LOADED", + isLatest = false, + onChangePatchesClick = onChangePatchesClick, + isCompact = isCompact, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + ) + } + + if (uiState.isOffline && !uiState.isLoadingPatches) { + Spacer(modifier = Modifier.height(if (isSmall) 8.dp else 12.dp)) + OfflineBanner( + onRetry = onRetry, + modifier = Modifier + .widthIn(max = 400.dp) + .padding(horizontal = if (isCompact) 8.dp else 16.dp) + ) + } + } + } + + val workspaceContent: @Composable (Modifier) -> Unit = { modifier -> + Box( + modifier = modifier + .fillMaxWidth() + .padding(padding), + contentAlignment = Alignment.Center + ) { + MiddleContent( + uiState = uiState, + isCompact = isCompact, + patchesLoaded = patchesLoaded, + onClearClick = onClearClick, + onChangeClick = onChangeClick, + onContinueClick = onContinueClick + ) + } + } + + val supportedAppsContent: @Composable () -> Unit = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding( + start = padding, + end = padding, + bottom = if (isSmall) 8.dp else 16.dp + ) + ) { + SupportedAppsSection( + isCompact = isCompact, + maxWidth = this@BoxWithConstraints.maxWidth, + isLoading = uiState.isLoadingPatches, + isDefaultSource = uiState.isDefaultSource, + supportedApps = uiState.supportedApps, + loadError = uiState.patchLoadError, + onRetry = onRetry + ) + } + } + Box(modifier = Modifier.fillMaxSize()) { val scrollState = rememberScrollState() Column( modifier = Modifier .fillMaxSize() + .heightIn(min = this@BoxWithConstraints.maxHeight) .verticalScroll(scrollState), - horizontalAlignment = Alignment.CenterHorizontally + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = if (pinSupportedAppsToBottom) Arrangement.SpaceBetween else Arrangement.Top ) { // ── Header ── if (useHorizontalHeader) { @@ -268,7 +362,7 @@ fun HomeScreenContent( .align(Alignment.TopEnd) .padding( top = padding + titleInsets.top, - end = padding + end = padding + titleInsets.end ), allowCacheClear = true ) @@ -326,9 +420,13 @@ private fun HeaderBar( val mono = LocalMorpheFont.current val titleInsets = LocalTitleBarInsets.current val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.10f) + val density = androidx.compose.ui.platform.LocalDensity.current + var leadingWidthPx by remember { mutableIntStateOf(0) } + var trailingWidthPx by remember { mutableIntStateOf(0) } + val centerSidePadding = with(density) { maxOf(leadingWidthPx, trailingWidthPx).toDp() } + 16.dp DraggableHeaderArea { - Row( + Box( modifier = Modifier .fillMaxWidth() .drawBehind { @@ -340,45 +438,63 @@ private fun HeaderBar( ) } .padding( - start = 12.dp + titleInsets.start, - end = 12.dp, top = 8.dp + titleInsets.top, bottom = 8.dp - ), - verticalAlignment = Alignment.CenterVertically + ) ) { // Logo — left-aligned, compact - BrandingSection(isCompact = true) - - Spacer(modifier = Modifier.weight(1f)) + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 12.dp + titleInsets.start) + .onSizeChanged { leadingWidthPx = it.width } + ) { + BrandingSection(isCompact = true) + } // Patches version inline — centered - if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { - PatchesVersionInline( - patchesVersion = uiState.patchesVersion!!, - isLatest = uiState.isUsingLatestPatches, - onChangePatchesClick = onChangePatchesClick - ) - } else if (uiState.isLoadingPatches) { - PatchesLoadingIndicator() - } else if (uiState.patchLoadError != null) { - PatchesVersionInline( - patchesVersion = "NOT LOADED", - isLatest = false, - onChangePatchesClick = onChangePatchesClick - ) - } + Box( + modifier = Modifier + .align(Alignment.Center) + .padding(start = centerSidePadding, end = centerSidePadding) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + if (!uiState.isLoadingPatches && uiState.patchesVersion != null) { + PatchesVersionInline( + patchesVersion = uiState.patchesVersion!!, + isLatest = uiState.isUsingLatestPatches, + onChangePatchesClick = onChangePatchesClick + ) + } else if (uiState.isLoadingPatches) { + PatchesLoadingIndicator() + } else if (uiState.patchLoadError != null) { + PatchesVersionInline( + patchesVersion = "NOT LOADED", + isLatest = false, + onChangePatchesClick = onChangePatchesClick + ) + } - // Offline badge - if (uiState.isOffline && !uiState.isLoadingPatches) { - Spacer(modifier = Modifier.width(12.dp)) - OfflineBadge(onRetry = onRetry) + if (uiState.isOffline && !uiState.isLoadingPatches) { + Spacer(modifier = Modifier.width(12.dp)) + OfflineBadge(onRetry = onRetry) + } + } } - Spacer(modifier = Modifier.weight(1f)) // Device indicator + settings — inline in the header - TopBarRow(allowCacheClear = true) + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 12.dp + titleInsets.end) + .onSizeChanged { trailingWidthPx = it.width } + ) { + TopBarRow(allowCacheClear = true) + } } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 406fc13..33454b7 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -145,7 +145,7 @@ fun PatchSelectionScreenContent(viewModel: PatchSelectionViewModel) { } .padding( start = 12.dp + titleInsets.start, - end = 12.dp, + end = 12.dp + titleInsets.end, top = 8.dp + titleInsets.top, bottom = 8.dp ), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt index edbe117..7d7006b 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchesScreen.kt @@ -131,7 +131,7 @@ fun PatchesScreenContent(viewModel: PatchesViewModel) { } .padding( start = 12.dp + titleInsets.start, - end = 12.dp, + end = 12.dp + titleInsets.end, top = 8.dp + titleInsets.top, bottom = 8.dp ), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt index f720252..b59db3f 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patching/PatchingScreen.kt @@ -114,7 +114,7 @@ fun PatchingScreenContent(viewModel: PatchingViewModel) { } .padding( start = 12.dp + titleInsets.start, - end = 12.dp, + end = 12.dp + titleInsets.end, top = 8.dp + titleInsets.top, bottom = 8.dp ), diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt index bf9ea41..e42c7ff 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.font.FontWeight @@ -80,6 +81,10 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { val corners = LocalMorpheCorners.current val mono = LocalMorpheFont.current val borderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.10f) + val density = androidx.compose.ui.platform.LocalDensity.current + var leadingWidthPx by remember { mutableIntStateOf(0) } + var trailingWidthPx by remember { mutableIntStateOf(0) } + val centerSidePadding = with(density) { maxOf(leadingWidthPx, trailingWidthPx).toDp() } + 16.dp FullScreenDropZone( isDragHovering = uiState.isDragHovering, @@ -100,7 +105,7 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ) { // ── Header row — matches expert mode ── DraggableHeaderArea { - Row( + Box( modifier = Modifier .fillMaxWidth() .drawBehind { @@ -112,30 +117,43 @@ fun QuickPatchContent(viewModel: QuickPatchViewModel) { ) } .padding( - start = 12.dp + titleInsets.start, - end = 12.dp, top = 8.dp + titleInsets.top, bottom = 8.dp - ), - verticalAlignment = Alignment.CenterVertically + ) ) { // Logo — left-aligned - BrandingLogo() - - Spacer(modifier = Modifier.weight(1f)) + Box( + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 12.dp + titleInsets.start) + .onSizeChanged { leadingWidthPx = it.width } + ) { + BrandingLogo() + } // Patches version badge — centered - PatchesVersionBadge( - patchesVersion = uiState.patchesVersion, - isLoading = uiState.isLoadingPatches - ) - - Spacer(modifier = Modifier.weight(1f)) + Box( + modifier = Modifier + .align(Alignment.Center) + .padding(start = centerSidePadding, end = centerSidePadding) + ) { + PatchesVersionBadge( + patchesVersion = uiState.patchesVersion, + isLoading = uiState.isLoadingPatches + ) + } - TopBarRow( - allowCacheClear = false, - isPatching = uiState.phase == QuickPatchPhase.DOWNLOADING || uiState.phase == QuickPatchPhase.PATCHING - ) + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 12.dp + titleInsets.end) + .onSizeChanged { trailingWidthPx = it.width } + ) { + TopBarRow( + allowCacheClear = false, + isPatching = uiState.phase == QuickPatchPhase.DOWNLOADING || uiState.phase == QuickPatchPhase.PATCHING + ) + } } } diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt index 2fd00fa..5b35b2d 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/result/ResultScreen.kt @@ -156,7 +156,7 @@ fun ResultScreenContent(outputPath: String) { } .padding( start = 12.dp + titleInsets.start, - end = 12.dp, + end = 12.dp + titleInsets.end, top = 8.dp + titleInsets.top, bottom = 8.dp ), From 74a34d35a5147b7cd5d28658e1ffc82ec6170053 Mon Sep 17 00:00:00 2001 From: Prateek <129204458+prateek-who@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:29:44 +0530 Subject: [PATCH 49/49] App names are read from patch files + minor mac UI fixes --- src/main/kotlin/app/morphe/gui/GuiMain.kt | 8 +++++++- .../kotlin/app/morphe/gui/data/model/Patch.kt | 1 + .../app/morphe/gui/data/model/SupportedApp.kt | 4 ++++ .../morphe/gui/ui/screens/home/HomeViewModel.kt | 2 +- .../ui/screens/patches/PatchSelectionScreen.kt | 8 +++++--- .../gui/ui/screens/quick/QuickPatchViewModel.kt | 4 ++-- .../kotlin/app/morphe/gui/util/PatchService.kt | 16 ++++++++++------ .../app/morphe/gui/util/SupportedAppExtractor.kt | 9 ++++++++- 8 files changed, 38 insertions(+), 14 deletions(-) diff --git a/src/main/kotlin/app/morphe/gui/GuiMain.kt b/src/main/kotlin/app/morphe/gui/GuiMain.kt index 473d84b..026e4bb 100644 --- a/src/main/kotlin/app/morphe/gui/GuiMain.kt +++ b/src/main/kotlin/app/morphe/gui/GuiMain.kt @@ -88,9 +88,15 @@ fun launchGui(args: Array) = application { titleBar.height = 56f titleBar.putProperty("controls.visible", true) decorations.setCustomTitleBar(window, titleBar) - insets = titleBar.toInsets(window, fallbackStartDp = 80f) + val macInsets = titleBar.toInsets(window, fallbackStartDp = 80f) + insets = macInsets.copy( + // Keep header content clear of the traffic-light cluster. + // JBR's inset can still land a bit tight on macOS depending on scaling. + start = macInsets.start + 24.dp + ) } catch (_: Exception) { // Not running on JBR — traffic lights stay at default position + insets = TitleBarInsets(start = 104.dp) } } if (isWindows) { diff --git a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt index cb02d34..cc3c3f4 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/Patch.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/Patch.kt @@ -51,6 +51,7 @@ data class Patch( @Serializable data class CompatiblePackage( val name: String, + val displayName: String? = null, val versions: List = emptyList() ) diff --git a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt index 0b0c8c5..98be998 100644 --- a/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt +++ b/src/main/kotlin/app/morphe/gui/data/model/SupportedApp.kt @@ -19,6 +19,10 @@ data class SupportedApp( val apkDownloadUrl: String? = null ) { companion object { + fun resolveDisplayName(packageName: String, providedName: String?): String { + return providedName?.takeIf { it.isNotBlank() } ?: getDisplayName(packageName) + } + /** * Derive display name from package name. */ diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt index a2a8267..a58c1f2 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/home/HomeViewModel.kt @@ -451,7 +451,7 @@ class HomeViewModel( // Get app display name - prefer dynamic, fallback to hardcoded, then package name val appName = dynamicSupportedApp?.displayName - ?: SupportedApp.getDisplayName(packageName) + ?: SupportedApp.resolveDisplayName(packageName, meta.label) ?: packageName // Get recommended version from dynamic patches data (no hardcoded fallback) diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt index 33454b7..6718605 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/patches/PatchSelectionScreen.kt @@ -772,9 +772,11 @@ private fun PatchListItem( if (patch.compatiblePackages.isNotEmpty()) { val genericSegments = setOf("com", "org", "net", "android", "google", "apps", "app", "www") patch.compatiblePackages.take(2).forEach { pkg -> - val meaningful = pkg.name.split(".").filter { it !in genericSegments } - val displayName = meaningful.takeLast(2).joinToString(" ") - .replaceFirstChar { it.uppercase() } + val displayName = pkg.displayName?.takeIf { it.isNotBlank() } ?: run { + val meaningful = pkg.name.split(".").filter { it !in genericSegments } + meaningful.takeLast(2).joinToString(" ") + .replaceFirstChar { it.uppercase() } + } Box( modifier = Modifier .border( diff --git a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt index a197d98..b39afd6 100644 --- a/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt +++ b/src/main/kotlin/app/morphe/gui/ui/screens/quick/QuickPatchViewModel.kt @@ -316,7 +316,7 @@ class QuickPatchViewModel( } if (packageName !in supportedPackages) { - val appName = SupportedApp.getDisplayName(packageName) + val appName = SupportedApp.resolveDisplayName(packageName, meta.label) val supportedNames = cachedSupportedApps.map { it.displayName } .ifEmpty { listOf("YouTube", "YouTube Music", "Reddit") } .joinToString(", ") @@ -330,7 +330,7 @@ class QuickPatchViewModel( // Get display name and recommended version from dynamic data, fallback to constants val displayName = dynamicAppInfo?.displayName - ?: SupportedApp.getDisplayName(packageName) + ?: SupportedApp.resolveDisplayName(packageName, meta.label) val recommendedVersion = dynamicAppInfo?.recommendedVersion diff --git a/src/main/kotlin/app/morphe/gui/util/PatchService.kt b/src/main/kotlin/app/morphe/gui/util/PatchService.kt index 68e0e19..9a1f5c5 100644 --- a/src/main/kotlin/app/morphe/gui/util/PatchService.kt +++ b/src/main/kotlin/app/morphe/gui/util/PatchService.kt @@ -151,12 +151,16 @@ class PatchService { return Patch( name = this.name ?: "Unknown", description = this.description ?: "", - compatiblePackages = this.compatiblePackages?.map { (name, versions) -> - CompatiblePackage( - name = name, - versions = versions?.toList() ?: emptyList() - ) - } ?: emptyList(), + compatiblePackages = this.compatibility + ?.mapNotNull { compatibility -> + val packageName = compatibility.packageName ?: return@mapNotNull null + CompatiblePackage( + name = packageName, + displayName = compatibility.name, + versions = compatibility.targets.mapNotNull { it.version } + ) + } + ?: emptyList(), options = this.options.values.map { opt -> PatchOption( key = opt.key, diff --git a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt index 19b8982..bb1083c 100644 --- a/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt +++ b/src/main/kotlin/app/morphe/gui/util/SupportedAppExtractor.kt @@ -23,6 +23,7 @@ object SupportedAppExtractor { fun extractSupportedApps(patches: List): List { // Collect all package names and their versions from all patches val packageVersionsMap = mutableMapOf>() + val packageDisplayNames = mutableMapOf() for (patch in patches) { for (compatiblePackage in patch.compatiblePackages) { @@ -32,6 +33,9 @@ object SupportedAppExtractor { if (packageName.isNotBlank()) { val existingVersions = packageVersionsMap.getOrPut(packageName) { mutableSetOf() } existingVersions.addAll(versions) + compatiblePackage.displayName + ?.takeIf { it.isNotBlank() } + ?.let { packageDisplayNames.putIfAbsent(packageName, it) } } } } @@ -42,7 +46,10 @@ object SupportedAppExtractor { val recommendedVersion = SupportedApp.getRecommendedVersion(versionList) SupportedApp( packageName = packageName, - displayName = SupportedApp.getDisplayName(packageName), + displayName = SupportedApp.resolveDisplayName( + packageName = packageName, + providedName = packageDisplayNames[packageName] + ), supportedVersions = versionList, recommendedVersion = recommendedVersion, apkDownloadUrl = SupportedApp.getDownloadUrl(packageName, recommendedVersion)

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 z