diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..110272d --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +AabToApk \ No newline at end of file diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/artifacts/AabToApk_jvm_1_0_SNAPSHOT.xml b/.idea/artifacts/AabToApk_jvm_1_0_SNAPSHOT.xml index c831c1a..604dcae 100644 --- a/.idea/artifacts/AabToApk_jvm_1_0_SNAPSHOT.xml +++ b/.idea/artifacts/AabToApk_jvm_1_0_SNAPSHOT.xml @@ -1,8 +1,6 @@ $PROJECT_DIR$/build/libs - - - + \ No newline at end of file diff --git a/.idea/artifacts/composeApp_jvm.xml b/.idea/artifacts/composeApp_jvm.xml new file mode 100644 index 0000000..c26c841 --- /dev/null +++ b/.idea/artifacts/composeApp_jvm.xml @@ -0,0 +1,8 @@ + + + $PROJECT_DIR$/composeApp/build/libs + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.agent.xml b/.idea/copilot.data.migration.agent.xml new file mode 100644 index 0000000..4ea72a9 --- /dev/null +++ b/.idea/copilot.data.migration.agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask.xml b/.idea/copilot.data.migration.ask.xml new file mode 100644 index 0000000..7ef04e2 --- /dev/null +++ b/.idea/copilot.data.migration.ask.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.ask2agent.xml b/.idea/copilot.data.migration.ask2agent.xml new file mode 100644 index 0000000..1f2ea11 --- /dev/null +++ b/.idea/copilot.data.migration.ask2agent.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/copilot.data.migration.edit.xml b/.idea/copilot.data.migration.edit.xml new file mode 100644 index 0000000..8648f94 --- /dev/null +++ b/.idea/copilot.data.migration.edit.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 5404067..bc7ffee 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -9,6 +9,7 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 438bac6..fceb01e 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/build.gradle.kts b/build.gradle.kts index 0fcfeec..0979b05 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,58 +1,8 @@ -import org.jetbrains.compose.desktop.application.dsl.TargetFormat - plugins { - kotlin("multiplatform") - id("org.jetbrains.compose") -} - -group = "com.AabToApk" -version = "1.0-SNAPSHOT" - -repositories { - google() - mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") -} - -kotlin { - jvm { - jvmToolchain(11) - withJava() - } - sourceSets { - val jvmMain by getting { - dependencies { - implementation(compose.desktop.currentOs) - api("com.esotericsoftware:kryo:4.0.1") - } - } - val jvmTest by getting { - dependencies { - implementation(compose.desktop.uiTestJUnit4) - implementation(compose.desktop.currentOs) - } - } - val resourcesDir = "src/common/resources" - } -} - -compose.desktop { - application { - mainClass = "MainKt" - nativeDistributions { - targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) - packageName = "AabToApk" - packageVersion = "1.0.4" - val iconsRoot = project.file("desktop-icons") - macOS{ - iconFile.set(iconsRoot.resolve("launcher.icns")) - } - windows{ - iconFile.set(iconsRoot.resolve("launcher.ico")) - } - linux{ - iconFile.set(iconsRoot.resolve("launcher.png")) - } - } - } -} + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.composeHotReload) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + alias(libs.plugins.kotlinMultiplatform) apply false +} \ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts new file mode 100644 index 0000000..cd93c78 --- /dev/null +++ b/composeApp/build.gradle.kts @@ -0,0 +1,75 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) + alias(libs.plugins.kotlin.serialization) +} + +kotlin { + jvm() + sourceSets { + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.materialIconsExtended) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.androidx.lifecycle.viewmodelCompose) + implementation(libs.androidx.lifecycle.runtimeCompose) + // Ktor Client + implementation(libs.ktor.client.core) + implementation(libs.ktor.client.content.negotiation) + implementation(libs.ktor.serialization.kotlinx.json) + implementation(libs.ktor.client.logging) + // Koin + implementation(libs.koin.core) + implementation(libs.koin.compose) + implementation(libs.koin.viewmodel) + implementation(libs.koin.navigation) + // Navigation + implementation(libs.androidx.compose.navigation) + // Datastore + implementation(libs.datastore) + implementation(libs.datastore.prefs) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + implementation(libs.ktor.client.cio) + implementation(libs.kotlinx.coroutines.core) + implementation(libs.logback.logging) + } + val jvmMain by getting { + resources.srcDir("src/jvmMain/composeResources") + } + } +} + +compose.desktop { + application { + mainClass = "MainKt" + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "AabToApk" + packageVersion = "1.0.4" + val iconsRoot = project.file("desktop-icons") + macOS{ + iconFile.set(iconsRoot.resolve("launcher.icns")) + } + windows{ + iconFile.set(iconsRoot.resolve("launcher.ico")) + } + linux{ + iconFile.set(iconsRoot.resolve("launcher.png")) + } + } + } +} diff --git a/src/jvmMain/resources/launcher.png b/composeApp/src/jvmMain/composeResources/files/launcher.png similarity index 100% rename from src/jvmMain/resources/launcher.png rename to composeApp/src/jvmMain/composeResources/files/launcher.png diff --git a/src/jvmMain/resources/fonts/sans_bold.ttf b/composeApp/src/jvmMain/composeResources/font/sans_bold.ttf similarity index 100% rename from src/jvmMain/resources/fonts/sans_bold.ttf rename to composeApp/src/jvmMain/composeResources/font/sans_bold.ttf diff --git a/src/jvmMain/resources/fonts/sans_medium.ttf b/composeApp/src/jvmMain/composeResources/font/sans_medium.ttf similarity index 100% rename from src/jvmMain/resources/fonts/sans_medium.ttf rename to composeApp/src/jvmMain/composeResources/font/sans_medium.ttf diff --git a/src/jvmMain/resources/fonts/sans_regular.ttf b/composeApp/src/jvmMain/composeResources/font/sans_regular.ttf similarity index 100% rename from src/jvmMain/resources/fonts/sans_regular.ttf rename to composeApp/src/jvmMain/composeResources/font/sans_regular.ttf diff --git a/src/jvmMain/resources/fonts/sans_thin.ttf b/composeApp/src/jvmMain/composeResources/font/sans_thin.ttf similarity index 100% rename from src/jvmMain/resources/fonts/sans_thin.ttf rename to composeApp/src/jvmMain/composeResources/font/sans_thin.ttf diff --git a/composeApp/src/jvmMain/composeResources/values/strings.xml b/composeApp/src/jvmMain/composeResources/values/strings.xml new file mode 100644 index 0000000..1ce1a36 --- /dev/null +++ b/composeApp/src/jvmMain/composeResources/values/strings.xml @@ -0,0 +1,17 @@ + + + BundleTool Desktop + AAB to APK Converter + Convert Android App Bundles to various APK formats with ease. + 1. Select Android App Bundle (.aab) + No AAB file selected + 2. Choose BundleTool Executable + Bundle Tool not selected + Download BundleTool + 3. Choose Output Mode + Universal APK + APK Set (.apks) + Device-specific + 4. Output Directory (optional) + Leave empty for same directory as .aab + \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/Main.kt b/composeApp/src/jvmMain/kotlin/Main.kt new file mode 100644 index 0000000..b085ea5 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/Main.kt @@ -0,0 +1,25 @@ +import aabtoapk.composeapp.generated.resources.Res +import aabtoapk.composeapp.generated.resources.app_name +import androidx.compose.ui.Alignment +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 di.initKoin +import org.jetbrains.compose.resources.stringResource +import ui.App + +fun main() = application { + initKoin() + Window( + onCloseRequest = ::exitApplication, + state = rememberWindowState( + width = 800.dp, height = 1000.dp, + position = WindowPosition(Alignment.Center) + ), + title = stringResource(Res.string.app_name) + ) { + App {} + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/command/CommandBuilder.kt b/composeApp/src/jvmMain/kotlin/command/CommandBuilder.kt new file mode 100644 index 0000000..0d895ee --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/command/CommandBuilder.kt @@ -0,0 +1,86 @@ +package command + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class CommandBuilder( + private val config: Config +) : ICommandBuilder { + + val logger = LoggerFactory.getLogger("CommandBuilder") as Logger + data class Config( + val bundleToolPath: String, + val aabPath: String, + val outputDir: String? = null, + val isUniversal: Boolean = true, + val keystore: KeystoreConfig? = null, + val deviceId: String? = null, + val overwrite: Boolean = false + ) + + data class KeystoreConfig( + val path: String, + val password: String, + val alias: String, + val keyPassword: String + ) + + override fun build(): Result { + if (config.bundleToolPath.isBlank()) return Result.failure(IllegalArgumentException("BundleTool path missing")) + if (config.aabPath.isBlank()) return Result.failure(IllegalArgumentException("AAB file path missing")) + + val cmd = buildString { + append("java -jar \"${config.bundleToolPath}\" build-apks ") + append("--bundle=\"${config.aabPath}\" ") + append("--output=\"${if(config.outputDir.isNullOrEmpty()) getDefaultOutput() else config.outputDir}\" ") + if (config.isUniversal) append("--mode=universal ") + if (config.overwrite) append("--overwrite ") + config.keystore?.let { + append("--ks=${it.path} ") + append("--ks-pass=pass:${it.password} ") + append("--ks-key-alias=${it.alias} ") + append("--key-pass=pass:${it.keyPassword} ") + } + config.deviceId?.let { append("--device-id=$it ") } + append("--overwrite ") + } + logger.debug(cmd.trim()) + return Result.success(cmd.trim()) + } + + private fun getDefaultOutput(): String { + val base = config.aabPath.substringBeforeLast(".") + return "$base.apks" + } + + companion object { + fun create(block: Builder.() -> Unit): CommandBuilder { + val builder = Builder() + builder.block() + return CommandBuilder(builder.build()) + } + } + + class Builder { + private var bundleToolPath: String = "" + private var aabPath: String = "" + private var outputDir: String? = null + private var isUniversal: Boolean = true + private var keystore: KeystoreConfig? = null + private var deviceId: String? = null + private var overwrite: Boolean = false + + fun bundleTool(path: String) = apply { bundleToolPath = path } + fun aab(path: String) = apply { aabPath = path } + fun outputDir(dir: String?) = apply { outputDir = dir } + fun universal(enable: Boolean) = apply { isUniversal = enable } + fun overwrite(enable: Boolean) = apply { overwrite = enable } + fun keystore(block: KeystoreConfig.() -> Unit) = apply { + keystore = KeystoreConfig("", "", "", "").apply(block) + } + + fun device(id: String?) = apply { deviceId = id } + + fun build() = Config(bundleToolPath, aabPath, outputDir, isUniversal, keystore, deviceId, overwrite) + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/command/CommandExecutor.kt b/composeApp/src/jvmMain/kotlin/command/CommandExecutor.kt similarity index 100% rename from src/jvmMain/kotlin/command/CommandExecutor.kt rename to composeApp/src/jvmMain/kotlin/command/CommandExecutor.kt diff --git a/composeApp/src/jvmMain/kotlin/command/ICommandBuilder.kt b/composeApp/src/jvmMain/kotlin/command/ICommandBuilder.kt new file mode 100644 index 0000000..b2693a9 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/command/ICommandBuilder.kt @@ -0,0 +1,5 @@ +package command + +interface ICommandBuilder { + fun build(): Result // success or failure message +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/command/ICommandExecutor.kt b/composeApp/src/jvmMain/kotlin/command/ICommandExecutor.kt new file mode 100644 index 0000000..7765b97 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/command/ICommandExecutor.kt @@ -0,0 +1,7 @@ +package command + +import data.domain.CommandResult + +interface ICommandExecutor { + suspend fun execute(command: String): CommandResult +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/command/OSProcessExecutor.kt b/composeApp/src/jvmMain/kotlin/command/OSProcessExecutor.kt new file mode 100644 index 0000000..216ae02 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/command/OSProcessExecutor.kt @@ -0,0 +1,30 @@ +package command + +import data.domain.CommandResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader + +class OSProcessExecutor : ICommandExecutor { + + override suspend fun execute(command: String): CommandResult = withContext(Dispatchers.IO) { + try { + val start = System.currentTimeMillis() + val process = Runtime.getRuntime().exec(command) + + val output = process.inputStream.bufferedReader().use(BufferedReader::readText) + val errors = process.errorStream.bufferedReader().use(BufferedReader::readText) + + val exitCode = process.waitFor() + val duration = System.currentTimeMillis() - start + + if (exitCode == 0) { + CommandResult.Success(output.trim(), duration) + } else { + CommandResult.Failure(errors.ifEmpty { "Unknown error" }, exitCode) + } + } catch (e: Exception) { + CommandResult.Failure(e.localizedMessage ?: "Execution failed") + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/data/domain/CommandExecutionResult.kt b/composeApp/src/jvmMain/kotlin/data/domain/CommandExecutionResult.kt new file mode 100644 index 0000000..6f74035 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/data/domain/CommandExecutionResult.kt @@ -0,0 +1,6 @@ +package data.domain + +data class CommandExecutionResult( + val command: String, + val result: CommandResult +) \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/data/domain/CommandResult.kt b/composeApp/src/jvmMain/kotlin/data/domain/CommandResult.kt new file mode 100644 index 0000000..c7bca5c --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/data/domain/CommandResult.kt @@ -0,0 +1,6 @@ +package data.domain + +sealed class CommandResult { + data class Success(val output: String, val durationMs: Long) : CommandResult() + data class Failure(val error: String, val exitCode: Int? = null) : CommandResult() +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/data/model/ClickableText.kt b/composeApp/src/jvmMain/kotlin/data/model/ClickableText.kt new file mode 100644 index 0000000..5d68061 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/data/model/ClickableText.kt @@ -0,0 +1,6 @@ +package data.model + +data class ClickableText( + val text: String, + val url: String +) diff --git a/composeApp/src/jvmMain/kotlin/data/state/HomeState.kt b/composeApp/src/jvmMain/kotlin/data/state/HomeState.kt new file mode 100644 index 0000000..48688e3 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/data/state/HomeState.kt @@ -0,0 +1,51 @@ +package data.state + +import kotlinx.serialization.Serializable + +@Serializable +data class PersistedBundleToolConfig( + val bundleToolPath: String = "", + val aabPath: String = "", + val outputDir: String = "", + val mode: OutputMode = OutputMode.Universal, + val signingState: SigningState = SigningState() +) + +data class BundleToolState( + val aabPath: String = "", + val bundleToolPath: String = "", + val outputDir: String = "", + val signingState: SigningState = SigningState(), + val mode: OutputMode = OutputMode.Universal, + val isConverting: Boolean = false, + val log: String = "Waiting to start conversion..." +) { + val isReady get() = aabPath.isNotBlank() +} + +@Serializable +data class SigningState( + val signingMode: SigningMode = SigningMode.Debug, + val keystorePath: String = "", + val keystorePassword: String = "", + val keyAlias: String = "", + val keyPassword: String = "" +) + +sealed class BundleToolEvent { + data object Initialize : BundleToolEvent() + data class SelectAabFile(val path: String) : BundleToolEvent() + data class SelectBundleToolPath(val path: String) : BundleToolEvent() + data class SelectOutputDir(val path: String) : BundleToolEvent() + data class SelectSigning(val signingMode: SigningMode) : BundleToolEvent() + data class SelectKeyStore(val path: String) : BundleToolEvent() + data class SelectKeyStorePassword(val path: String) : BundleToolEvent() + data class SelectAlias(val alias: String) : BundleToolEvent() + data class SelectKeyPassword(val password: String) : BundleToolEvent() + data class SelectMode(val mode: OutputMode) : BundleToolEvent() + data object Convert : BundleToolEvent() +} + +enum class OutputMode { Universal, ApkSet, DeviceSpecific } + +enum class SigningMode { Debug, Release } diff --git a/composeApp/src/jvmMain/kotlin/di/Koin.kt b/composeApp/src/jvmMain/kotlin/di/Koin.kt new file mode 100644 index 0000000..5a8702a --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/di/Koin.kt @@ -0,0 +1,15 @@ +package di + +import org.koin.core.context.startKoin +import org.koin.dsl.KoinAppDeclaration + +fun initKoin(config: KoinAppDeclaration? = null) { + startKoin { + config?.invoke(this) + modules( + viewModelModules(), + storageModules(), + commandModules() + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/di/Modules.kt b/composeApp/src/jvmMain/kotlin/di/Modules.kt new file mode 100644 index 0000000..fea31f2 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/di/Modules.kt @@ -0,0 +1,34 @@ +package di + +import command.ICommandExecutor +import command.OSProcessExecutor +import local.AppDataStore +import local.AppPreferences +import local.StorageProvider +import manager.FileOperationManager +import org.koin.core.module.dsl.viewModel +import org.koin.dsl.module +import ui.windows.viewmodel.HomeViewModel +import usecase.CommandUseCase +import utils.files.FileUtilsImpl +import utils.files.IFileUtils + +fun viewModelModules() = module { + viewModel { HomeViewModel(get(), get()) } +} + +fun storageModules() = module { + single { StorageProvider.provideDataStore() } + single { AppDataStore(get()) } + single { AppPreferences(get()) } +} + +fun commandModules() = module { + // Low-level dependencies + single { OSProcessExecutor() } + single { FileUtilsImpl } + single { FileOperationManager(get()) } + + // Use Case layer + single { CommandUseCase(get(), get()) } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/local/AppDataStore.kt b/composeApp/src/jvmMain/kotlin/local/AppDataStore.kt new file mode 100644 index 0000000..0a838fe --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/local/AppDataStore.kt @@ -0,0 +1,54 @@ +package local + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.PreferenceDataStoreFactory +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.map +import kotlinx.serialization.json.Json +import okio.Path.Companion.toPath + +internal const val dataStoreFileName = "bundletool_ui.preferences_pb" + +fun createDataStore(producePath: () -> String): DataStore = + PreferenceDataStoreFactory.createWithPath( + produceFile = { producePath().toPath() } + ) + +class AppDataStore( + private val dataStore: DataStore, + val json: Json = Json { + ignoreUnknownKeys = true + encodeDefaults = true + isLenient = true + } +) { + + // Save primitive (String, Boolean, Int, etc.) + suspend fun save(key: Preferences.Key, value: T) { + dataStore.edit { prefs -> prefs[key] = value } + } + + // Get primitive with default value + suspend fun get(key: Preferences.Key, default: T): T { + return dataStore.data.map { prefs -> prefs[key] ?: default }.first() + } + + // Save object as JSON using kotlinx.serialization + suspend inline fun saveObject(key: Preferences.Key, obj: T) { + val jsonString = json.encodeToString(obj) + save(key, jsonString) + } + + // Get object from JSON + suspend inline fun getObject(key: Preferences.Key): T? { + val jsonString = get(key, "") + return if (jsonString.isEmpty()) null else json.decodeFromString(jsonString) + } + + // Clear all preferences + suspend fun clear() { + dataStore.edit { it.clear() } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/local/AppPreferences.kt b/composeApp/src/jvmMain/kotlin/local/AppPreferences.kt new file mode 100644 index 0000000..2668924 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/local/AppPreferences.kt @@ -0,0 +1,28 @@ +package local + +import androidx.datastore.preferences.core.stringPreferencesKey +import data.state.PersistedBundleToolConfig + +private const val TAG = "AppPreferences" + +class AppPreferences(private val appDataStore: AppDataStore) { + + companion object Keys { + val CONFIG_KEY = stringPreferencesKey("bundle_tool_config") + } + + // --- Save Config --- + suspend fun saveBundleToolConfig(config: PersistedBundleToolConfig) { + appDataStore.saveObject(CONFIG_KEY, config) + } + + // --- Load Config --- + suspend fun getBundleToolConfig(): PersistedBundleToolConfig? { + return appDataStore.getObject(CONFIG_KEY) + } + + // --- Clear All --- + suspend fun clearAll() { + appDataStore.clear() + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/local/StorageProvider.kt b/composeApp/src/jvmMain/kotlin/local/StorageProvider.kt new file mode 100644 index 0000000..c12952b --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/local/StorageProvider.kt @@ -0,0 +1,20 @@ +package local + +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import java.io.File + +interface Storage { + fun provideDataStore(): DataStore +} + +object StorageProvider : Storage { + override fun provideDataStore(): DataStore { + return createDataStore( + producePath = { + val file = File(System.getProperty("java.io.tmpdir"), dataStoreFileName) + file.absolutePath + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/manager/FileOperationManager.kt b/composeApp/src/jvmMain/kotlin/manager/FileOperationManager.kt new file mode 100644 index 0000000..b55e851 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/manager/FileOperationManager.kt @@ -0,0 +1,44 @@ +package manager + +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import utils.files.FileActionResult +import utils.files.FileUtilsImpl +import utils.files.IFileUtils +import java.io.File + +class FileOperationManager( + private val fileUtils: IFileUtils = FileUtilsImpl +) { + + suspend fun handleBundletoolOutput( + directory: String, + fileName: String, + isUniversal: Boolean + ): FileActionResult = withContext(Dispatchers.IO) { + if (!isUniversal) { + return@withContext FileActionResult.Success("Skipped file operations (not universal mode).") + } + + val baseName = fileName.substringBeforeLast(".") + val oldFile = File(directory, "$baseName.apks") + val newFile = File(directory, "$baseName.zip") + + if (!fileUtils.renameFile(oldFile, newFile)) { + return@withContext FileActionResult.Failure("Failed to rename ${oldFile.name}") + } + + return@withContext try { + fileUtils.unzip(newFile, directory) + val deleted = fileUtils.deleteFile(newFile) + val message = buildString { + append("Renamed and unzipped successfully.\n") + append("Output: $directory") + if (!deleted) append("\nWarning: Unable to delete temporary zip file.") + } + FileActionResult.Success(message) + } catch (e: Exception) { + FileActionResult.Failure("Unzipping failed: ${e.localizedMessage}") + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/App.kt b/composeApp/src/jvmMain/kotlin/ui/App.kt new file mode 100644 index 0000000..1c53111 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/App.kt @@ -0,0 +1,35 @@ +package ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.consumeWindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import org.jetbrains.compose.ui.tooling.preview.Preview +import org.koin.compose.KoinContext +import ui.navigation.AppNavHost +import ui.theme.AppTheme + +@Preview +@Composable +fun App(onCloseWindow: () -> Unit) { + KoinContext { + AppTheme { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background) + .consumeWindowInsets(WindowInsets.safeDrawing) // tell Compose we handle them + .padding(WindowInsets.safeDrawing.asPaddingValues()) // apply padding + ) { + AppNavHost() + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/ClickableLinkText.kt b/composeApp/src/jvmMain/kotlin/ui/components/ClickableLinkText.kt new file mode 100644 index 0000000..bf96153 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/ClickableLinkText.kt @@ -0,0 +1,56 @@ +package ui.components + +import androidx.compose.foundation.clickable +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.sp + +@Composable +fun ClickableLinkText( + text: String, + url: String, + modifier: Modifier = Modifier, + highlightColor: Color = MaterialTheme.colorScheme.primary, + textStyle: androidx.compose.ui.text.TextStyle = MaterialTheme.typography.bodyMedium.copy(fontSize = 14.sp) +) { + val uriHandler = LocalUriHandler.current + + val annotatedText = buildAnnotatedString { + append(text) + addStyle( + style = SpanStyle( + color = highlightColor, + fontWeight = FontWeight.Medium, + textDecoration = TextDecoration.Underline + ), + start = 0, + end = text.length + ) + addStringAnnotation( + tag = "URL", + annotation = url, + start = 0, + end = text.length + ) + } + + Text( + text = annotatedText, + style = textStyle, + modifier = modifier + .clickable { + annotatedText.getStringAnnotations("URL", 0, text.length) + .firstOrNull()?.let { + uriHandler.openUri(it.item) + } + } + ) +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/ConvertButton.kt b/composeApp/src/jvmMain/kotlin/ui/components/ConvertButton.kt new file mode 100644 index 0000000..59b02c4 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/ConvertButton.kt @@ -0,0 +1,82 @@ +package ui.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ButtonWithLoader( + enabled: Boolean, + isLoading: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + onClick = onClick, + enabled = enabled && !isLoading, + modifier = modifier + .fillMaxWidth() + .height(48.dp), + shape = MaterialTheme.shapes.medium, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + disabledContainerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), + disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f) + ), + contentPadding = PaddingValues(0.dp) + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + contentAlignment = Alignment.Center + ) { + if (isLoading) { + // โœ… Only show loader, perfectly centered and static + CircularProgressIndicator( + color = MaterialTheme.colorScheme.onPrimary, + strokeWidth = 3.dp, + modifier = Modifier.size(22.dp) + ) + } else { + // โœ… Regular text when not loading + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Center + ) { + Icon( + imageVector = Icons.Default.PlayArrow, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + text = "Convert", + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onPrimary + ) + ) + } + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/DottedBorderBox.kt b/composeApp/src/jvmMain/kotlin/ui/components/DottedBorderBox.kt new file mode 100644 index 0000000..d900709 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/DottedBorderBox.kt @@ -0,0 +1,56 @@ +package ui.components + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +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.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.dp + +@Composable +fun DottedBorderBox( + modifier: Modifier = Modifier, + color: Color = MaterialTheme.colorScheme.outlineVariant, + cornerRadius: Float = 16f, + dotSpacing: Float = 10f, + dotLength: Float = 10f, + content: @Composable BoxScope.() -> Unit +) { + Box( + modifier = modifier + .fillMaxWidth() + .clip(RoundedCornerShape(cornerRadius.dp)) + .padding(1.dp) + ) { + Canvas(modifier = Modifier.matchParentSize()) { + drawRoundRect( + color = color, + size = Size(size.width, size.height), + style = Stroke( + width = 2f, + pathEffect = PathEffect.dashPathEffect(floatArrayOf(dotLength, dotSpacing)) + ), + cornerRadius = androidx.compose.ui.geometry.CornerRadius(cornerRadius, cornerRadius) + ) + } + + Box( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(bottom = 16.dp) + .align(Alignment.CenterStart) + ) { + content() + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/FilePickerField.kt b/composeApp/src/jvmMain/kotlin/ui/components/FilePickerField.kt new file mode 100644 index 0000000..9f804ff --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/FilePickerField.kt @@ -0,0 +1,145 @@ +package ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Icon +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.FolderOpen +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import data.model.ClickableText +import java.awt.FileDialog +import java.awt.Frame + +@Composable +fun FilePickerField( + label: String, + value: String, + placeholder: String, + dialogTitle: String = "Select File", + clickableText: ClickableText? = null, + modifier: Modifier = Modifier.fillMaxWidth(), + fileExtensionFilter: String? = null, // e.g. ".aab" or ".jks", + onPick: (String) -> Unit, +) { + var showDialog by remember { mutableStateOf(false) } + + if (showDialog) { + LaunchedEffect(Unit) { + val file = pickFile(dialogTitle, fileExtensionFilter) + if (file != null) onPick(file) + showDialog = false + } + } + + Column(modifier = modifier) { + + // Label (above field) + Text( + text = label, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = Modifier.padding(bottom = 8.dp) + ) + + // Field + Icon (in one Row) + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .background( + color = MaterialTheme.colorScheme.surfaceContainerHighest, + shape = RoundedCornerShape(8.dp) + ), + verticalAlignment = Alignment.CenterVertically + ) { + // File path text + Text( + text = value.ifEmpty { placeholder }, + color = if (value.isNotEmpty()) + MaterialTheme.colorScheme.onSurface + else + MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .weight(1f) + .padding(horizontal = 16.dp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodyMedium + ) + + // Folder button + Box( + modifier = Modifier + .fillMaxHeight() + .width(48.dp) + .background( + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape( + topEnd = 8.dp, + bottomEnd = 8.dp + ) + ), + contentAlignment = Alignment.Center + ) { + IconButton( + onClick = { showDialog = true } + ) { + Icon( + Icons.Default.FolderOpen, + contentDescription = "Browse", + tint = Color.White + ) + } + } + } + clickableText?.let { + ClickableLinkText( + text = it.text, + url = it.url, + highlightColor = MaterialTheme.colorScheme.primary, + modifier = Modifier.padding(top = 4.dp) + ) + } + } +} + +/** + * Opens a native desktop file dialog and returns the selected path (or null if canceled). + */ +fun pickFile(title: String, extension: String?): String? { + return try { + val dialog = FileDialog(null as Frame?, title, FileDialog.LOAD).apply { + isMultipleMode = false + if (extension != null) file = "*$extension" + } + dialog.isVisible = true + dialog.file?.let { file -> + dialog.directory + file + } + } catch (e: Exception) { + e.printStackTrace() + null + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/LogBox.kt b/composeApp/src/jvmMain/kotlin/ui/components/LogBox.kt new file mode 100644 index 0000000..8ac5c80 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/LogBox.kt @@ -0,0 +1,168 @@ +package ui.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.DividerDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +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.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun LogBox( + log: String, + onClearLogs: (() -> Unit)? = null, + onRunCommand: ((String) -> Unit)? = null +) { + var command by remember { mutableStateOf("") } + val scrollState = rememberScrollState() + + Column( + modifier = Modifier + .fillMaxWidth() + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.4f), + shape = RoundedCornerShape(8.dp) + ) + .background(MaterialTheme.colorScheme.surfaceContainerLowest, RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(8.dp)) + ) { + // ๐Ÿ”น Header Row + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Progress Log", + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + Spacer(Modifier.weight(1f)) + if (onClearLogs != null) { + TextButton( + onClick = onClearLogs, + contentPadding = PaddingValues(0.dp) + ) { + Text( + "Clear Logs", + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } + } + + HorizontalDivider( + Modifier, + DividerDefaults.Thickness, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + ) + + // ๐Ÿงพ Log Area + Box( + modifier = Modifier + .fillMaxWidth() + .height(180.dp) + .background(MaterialTheme.colorScheme.surface, RoundedCornerShape(0.dp)) + .padding(12.dp) + .verticalScroll(scrollState) + ) { + Text( + text = log.ifBlank { "Waiting to start conversion..." }, + color = MaterialTheme.colorScheme.onSurfaceVariant, + style = MaterialTheme.typography.bodySmall.copy( + fontFamily = FontFamily.Monospace + ), + textAlign = TextAlign.Start + ) + } + + HorizontalDivider( + Modifier, + DividerDefaults.Thickness, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f) + ) + + // ๐Ÿ’ป Command Input Row + Row( + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.surfaceContainerLowest) + .padding(horizontal = 12.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "$", + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = Modifier.padding(end = 8.dp) + ) + + OutlinedTextField( + value = command, + onValueChange = { command = it }, + placeholder = { Text("Execute custom command...") }, + singleLine = true, + shape = RoundedCornerShape(6.dp), + modifier = Modifier.weight(1f), + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = Color.Transparent, + unfocusedBorderColor = Color.Transparent, + cursorColor = MaterialTheme.colorScheme.primary, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + focusedTextColor = MaterialTheme.colorScheme.onSurface, + unfocusedTextColor = MaterialTheme.colorScheme.onSurface + ) + ) + + Spacer(Modifier.width(8.dp)) + + Button( + onClick = { + if (command.isNotBlank()) { + onRunCommand?.invoke(command) + command = "" + } + }, + modifier = Modifier.height(40.dp), + shape = RoundedCornerShape(6.dp) + ) { + Text("Run") + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/OptionSelector.kt b/composeApp/src/jvmMain/kotlin/ui/components/OptionSelector.kt new file mode 100644 index 0000000..3fa3bdf --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/OptionSelector.kt @@ -0,0 +1,139 @@ +package ui.components + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.RadioButtonChecked +import androidx.compose.material.icons.outlined.RadioButtonUnchecked +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 + +/** + * Generic Material 3 Option Selector (like segmented buttons) + * + * @param title Title displayed above the group (optional) + * @param options List of selectable items + * @param selected Currently selected option + * @param onSelect Callback when an option is selected + * @param optionLabel Label renderer for each item + */ +@Composable +fun OptionSelector( + title: String? = null, + options: List, + selected: T, + onSelect: (T) -> Unit, + optionLabel: (T) -> String, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // Label above + if (!title.isNullOrEmpty()) { + Text( + text = title, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + + // Option cards + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + options.forEach { option -> + OptionCard( + title = optionLabel(option), + selected = option == selected, + onClick = { onSelect(option) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +@Composable +private fun OptionCard( + title: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier +) { + val borderColor = if (selected) + MaterialTheme.colorScheme.primary.copy(alpha = 0.4f) + else + MaterialTheme.colorScheme.outline.copy(alpha = 0.3f) + + val backgroundColor = if (selected) + MaterialTheme.colorScheme.primary.copy(alpha = 0.08f) + else + MaterialTheme.colorScheme.surfaceContainerHighest + + Surface( + modifier = modifier + .defaultMinSize(minWidth = 170.dp) + .height(56.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null + ) { onClick() }, + shape = RoundedCornerShape(8.dp), + color = MaterialTheme.colorScheme.surfaceContainerHighest, + border = BorderStroke(2.dp, borderColor) + ) { + Row( + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxHeight(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + imageVector = if (selected) + Icons.Filled.RadioButtonChecked + else + Icons.Outlined.RadioButtonUnchecked, + contentDescription = null, + tint = if (selected) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.outline + ) + + Text( + text = title, + style = MaterialTheme.typography.bodyMedium.copy( + fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, + color = if (selected) + MaterialTheme.colorScheme.onSurface + else + MaterialTheme.colorScheme.onSurfaceVariant + ) + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/SigningSection.kt b/composeApp/src/jvmMain/kotlin/ui/components/SigningSection.kt new file mode 100644 index 0000000..6ec66db --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/SigningSection.kt @@ -0,0 +1,111 @@ +package ui.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import data.state.SigningMode + +@Composable +fun SigningSection( + signingMode: SigningMode, + keystorePath: String, + keystorePassword: String, + keyAlias: String, + keyPassword: String, + onModeChange: (SigningMode) -> Unit, + onPickKeystore: (String) -> Unit, + onKeystorePasswordChange: (String) -> Unit, + onAliasChange: (String) -> Unit, + onKeyPasswordChange: (String) -> Unit +) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // ๐Ÿงพ Section title + Text( + text = "4. Signing Mode", + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + modifier = Modifier.padding(bottom = 8.dp) + ) + + // ๐ŸŸฃ Debug / Release selector + OptionSelector( + title = null, + options = listOf(SigningMode.Debug, SigningMode.Release), + selected = signingMode, + onSelect = onModeChange, + optionLabel = { it.name }, + modifier = Modifier.fillMaxWidth() + ) + val borderColor = MaterialTheme.colorScheme.outline + // ๐Ÿ”’ Release-only section with dotted border + AnimatedVisibility( + visible = signingMode == SigningMode.Release, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + DottedBorderBox { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + + // Keystore Picker + FilePickerField( + label = "", + value = keystorePath, + placeholder = "Select Keystore Path", + dialogTitle = "Select Keystore File", + fileExtensionFilter = ".jks", + onPick = onPickKeystore + ) + + // Key Alias + Key Password + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.fillMaxWidth() + ) { + StyledOutlinedTextField( + value = keyAlias, + onValueChange = onAliasChange, + placeholder = "Key Alias", + modifier = Modifier.weight(1f) + ) + + StyledOutlinedTextField( + value = keyPassword, + onValueChange = onKeyPasswordChange, + isPassword = true, + placeholder = "Key Password", + modifier = Modifier.weight(1f) + ) + } + + // Keystore Password + StyledOutlinedTextField( + value = keystorePassword, + onValueChange = onKeystorePasswordChange, + placeholder = "Keystore Password", + isPassword = true, + modifier = Modifier.fillMaxWidth() + ) + } + } + } + } +} + + + diff --git a/composeApp/src/jvmMain/kotlin/ui/components/StyledOutlinedTextField.kt b/composeApp/src/jvmMain/kotlin/ui/components/StyledOutlinedTextField.kt new file mode 100644 index 0000000..5b98524 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/StyledOutlinedTextField.kt @@ -0,0 +1,87 @@ +package ui.components + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.OutlinedTextFieldDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp + +@Composable +fun StyledOutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + placeholder: String, + modifier: Modifier = Modifier, + isPassword: Boolean = false +) { + var passwordVisible by remember { mutableStateOf(false) } + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(placeholder) }, + placeholder = { Text(placeholder) }, + singleLine = true, + modifier = modifier + .clip(RoundedCornerShape(8.dp)), + shape = RoundedCornerShape(8.dp), + textStyle = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Medium + ), + visualTransformation = if (isPassword && !passwordVisible) + PasswordVisualTransformation() + else + VisualTransformation.None, + keyboardOptions = if (isPassword) + KeyboardOptions(keyboardType = KeyboardType.Password) + else + KeyboardOptions.Default, + trailingIcon = { + if (isPassword) { + val visibilityIcon = if (passwordVisible) + Icons.Default.VisibilityOff + else + Icons.Default.Visibility + + val description = if (passwordVisible) + "Hide password" + else + "Show password" + + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = visibilityIcon, + contentDescription = description, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + }, + colors = OutlinedTextFieldDefaults.colors( + focusedBorderColor = MaterialTheme.colorScheme.primary, + unfocusedBorderColor = MaterialTheme.colorScheme.outlineVariant, + cursorColor = MaterialTheme.colorScheme.primary, + focusedContainerColor = MaterialTheme.colorScheme.surface, + unfocusedContainerColor = MaterialTheme.colorScheme.surface + ) + ) +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/components/WindowHeader.kt b/composeApp/src/jvmMain/kotlin/ui/components/WindowHeader.kt new file mode 100644 index 0000000..00cae0a --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/components/WindowHeader.kt @@ -0,0 +1,47 @@ +package ui.components + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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 + +@Composable +fun WindowHeader( + title: String, + subTitle: String +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, bottom = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall.copy( + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface + ), + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(4.dp)) + + Text( + text = subTitle, + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant + ), + textAlign = TextAlign.Center + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/navigation/AppNavHost.kt b/composeApp/src/jvmMain/kotlin/ui/navigation/AppNavHost.kt new file mode 100644 index 0000000..9f6da47 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/navigation/AppNavHost.kt @@ -0,0 +1,36 @@ +package ui.navigation + +import androidx.compose.runtime.Composable +import androidx.navigation.compose.NavHost +import androidx.navigation.compose.composable +import androidx.navigation.compose.rememberNavController +import org.koin.compose.viewmodel.koinViewModel +import ui.windows.HomeWindow +import ui.windows.SettingsWindow +import ui.windows.SplashWindow +import ui.windows.viewmodel.HomeViewModel + +@Composable +fun AppNavHost() { + val navController = rememberNavController() + NavHost( + navController = navController, + startDestination = Routes.SplashRoute + ) { + composable { + SplashWindow(onFinish = { + navController.navigate(Routes.HomeRoute) { + popUpTo(Routes.SplashRoute) { inclusive = true } + launchSingleTop = true + } + }) + } + composable { + val viewModel = koinViewModel() + HomeWindow(viewModel) + } + composable { + SettingsWindow() + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/navigation/Windows.kt b/composeApp/src/jvmMain/kotlin/ui/navigation/Windows.kt new file mode 100644 index 0000000..55a04b3 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/navigation/Windows.kt @@ -0,0 +1,14 @@ +package ui.navigation + +import kotlinx.serialization.Serializable + +sealed interface Routes { + @Serializable + object SplashRoute + + @Serializable + object HomeRoute + + @Serializable + object SettingsRoute +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/theme/Color.kt b/composeApp/src/jvmMain/kotlin/ui/theme/Color.kt new file mode 100644 index 0000000..ca5ab39 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/theme/Color.kt @@ -0,0 +1,80 @@ +package ui.theme + +import androidx.compose.ui.graphics.Color + +/** + * Created by Zaki on 28-07-2024. + */ +val primaryLight = Color(0xFF8F4C38) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFFFDBD1) +val onPrimaryContainerLight = Color(0xFF723523) +val secondaryLight = Color(0xFF77574E) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFFFDBD1) +val onSecondaryContainerLight = Color(0xFF5D4037) +val tertiaryLight = Color(0xFF6C5D2F) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFF5E1A7) +val onTertiaryContainerLight = Color(0xFF534619) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFFFF8F6) +val onBackgroundLight = Color(0xFF231917) +val surfaceLight = Color(0xFFFFF8F6) +val onSurfaceLight = Color(0xFF231917) +val surfaceVariantLight = Color(0xFFF5DED8) +val onSurfaceVariantLight = Color(0xFF53433F) +val outlineLight = Color(0xFF85736E) +val outlineVariantLight = Color(0xFFD8C2BC) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF392E2B) +val inverseOnSurfaceLight = Color(0xFFFFEDE8) +val inversePrimaryLight = Color(0xFFFFB5A0) +val surfaceDimLight = Color(0xFFE8D6D2) +val surfaceBrightLight = Color(0xFFFFF8F6) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFFFF1ED) +val surfaceContainerLight = Color(0xFFFCEAE5) +val surfaceContainerHighLight = Color(0xFFF7E4E0) +val surfaceContainerHighestLight = Color(0xFFF1DFDA) + +val primaryDark = Color(0xFFFFB5A0) +val onPrimaryDark = Color(0xFF561F0F) +val primaryContainerDark = Color(0xFF723523) +val onPrimaryContainerDark = Color(0xFFFFDBD1) +val secondaryDark = Color(0xFFE7BDB2) +val onSecondaryDark = Color(0xFF442A22) +val secondaryContainerDark = Color(0xFF5D4037) +val onSecondaryContainerDark = Color(0xFFFFDBD1) +val tertiaryDark = Color(0xFFD8C58D) +val onTertiaryDark = Color(0xFF3B2F05) +val tertiaryContainerDark = Color(0xFF534619) +val onTertiaryContainerDark = Color(0xFFF5E1A7) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF1A110F) +val onBackgroundDark = Color(0xFFF1DFDA) +val surfaceDark = Color(0xFF1A110F) +val onSurfaceDark = Color(0xFFF1DFDA) +val surfaceVariantDark = Color(0xFF53433F) +val onSurfaceVariantDark = Color(0xFFD8C2BC) +val outlineDark = Color(0xFFA08C87) +val outlineVariantDark = Color(0xFF53433F) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFF1DFDA) +val inverseOnSurfaceDark = Color(0xFF392E2B) +val inversePrimaryDark = Color(0xFF8F4C38) +val surfaceDimDark = Color(0xFF1A110F) +val surfaceBrightDark = Color(0xFF423734) +val surfaceContainerLowestDark = Color(0xFF140C0A) +val surfaceContainerLowDark = Color(0xFF231917) +val surfaceContainerDark = Color(0xFF271D1B) +val surfaceContainerHighDark = Color(0xFF322825) +val surfaceContainerHighestDark = Color(0xFF3D322F) + +val DarkGreen = Color(0xFF388E3C) \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/theme/Theme.kt b/composeApp/src/jvmMain/kotlin/ui/theme/Theme.kt new file mode 100644 index 0000000..0c9248a --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/theme/Theme.kt @@ -0,0 +1,101 @@ +package 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 + +private val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +private val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) + +@Composable +fun AppTheme( + useDarkTheme: Boolean = false, + content: @Composable() () -> Unit +) { + val colors = if (!useDarkTheme) { + lightScheme + } else { + darkScheme + } + + MaterialTheme( + colorScheme = colors, + typography = SansTypography(), + content = content + ) +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/theme/Typography.kt b/composeApp/src/jvmMain/kotlin/ui/theme/Typography.kt new file mode 100644 index 0000000..90393af --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/theme/Typography.kt @@ -0,0 +1,44 @@ +package ui.theme + +import aabtoapk.composeapp.generated.resources.Res +import aabtoapk.composeapp.generated.resources.sans_bold +import aabtoapk.composeapp.generated.resources.sans_medium +import aabtoapk.composeapp.generated.resources.sans_regular +import aabtoapk.composeapp.generated.resources.sans_thin +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight + +/** + * Created by Zaki on 21-07-2024. + */ +@Composable +fun SansFontFamily() = FontFamily( + org.jetbrains.compose.resources.Font(Res.font.sans_thin, FontWeight.Light), + org.jetbrains.compose.resources.Font(Res.font.sans_regular, FontWeight.Normal), + org.jetbrains.compose.resources.Font(Res.font.sans_medium, FontWeight.Medium), + org.jetbrains.compose.resources.Font(Res.font.sans_bold, FontWeight.Bold) +) + +@Composable +fun SansTypography() = Typography().run { + val fontFamily = SansFontFamily() + copy( + displayLarge = displayLarge.copy(fontFamily = fontFamily), + displayMedium = displayMedium.copy(fontFamily = fontFamily), + displaySmall = displaySmall.copy(fontFamily = fontFamily), + headlineLarge = headlineLarge.copy(fontFamily = fontFamily), + headlineMedium = headlineMedium.copy(fontFamily = fontFamily), + headlineSmall = headlineSmall.copy(fontFamily = fontFamily), + titleLarge = titleLarge.copy(fontFamily = fontFamily), + titleMedium = titleMedium.copy(fontFamily = fontFamily), + titleSmall = titleSmall.copy(fontFamily = fontFamily), + bodyLarge = bodyLarge.copy(fontFamily = fontFamily), + bodyMedium = bodyMedium.copy(fontFamily = fontFamily), + bodySmall = bodySmall.copy(fontFamily = fontFamily), + labelLarge = labelLarge.copy(fontFamily = fontFamily), + labelMedium = labelMedium.copy(fontFamily = fontFamily), + labelSmall = labelSmall.copy(fontFamily = fontFamily) + ) +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/windows/HomeWindow.kt b/composeApp/src/jvmMain/kotlin/ui/windows/HomeWindow.kt new file mode 100644 index 0000000..b3e385e --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/windows/HomeWindow.kt @@ -0,0 +1,138 @@ +package ui.windows + +import aabtoapk.composeapp.generated.resources.Res +import aabtoapk.composeapp.generated.resources.bundletool_not_selected +import aabtoapk.composeapp.generated.resources.download_bundletool +import aabtoapk.composeapp.generated.resources.no_aab_selected +import aabtoapk.composeapp.generated.resources.step_four_subtitle +import aabtoapk.composeapp.generated.resources.step_four_title +import aabtoapk.composeapp.generated.resources.step_one_title +import aabtoapk.composeapp.generated.resources.step_three_title +import aabtoapk.composeapp.generated.resources.step_two_title +import aabtoapk.composeapp.generated.resources.subtitle +import aabtoapk.composeapp.generated.resources.title +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import data.state.BundleToolEvent +import data.state.OutputMode +import org.jetbrains.compose.resources.stringResource +import ui.components.ButtonWithLoader +import ui.components.FilePickerField +import ui.components.LogBox +import ui.components.OptionSelector +import ui.components.SigningSection +import ui.components.WindowHeader +import ui.windows.viewmodel.HomeViewModel + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun HomeWindow(viewModel: HomeViewModel) { + val state by viewModel.uiState.collectAsStateWithLifecycle() + val scrollState = rememberScrollState() + + LaunchedEffect(Unit) { + viewModel.onEvent(BundleToolEvent.Initialize) + } + + Column( + modifier = Modifier + .background(MaterialTheme.colorScheme.background) + .padding(horizontal = 24.dp, vertical = 16.dp) + .fillMaxSize().verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Header + WindowHeader( + stringResource(Res.string.title), + stringResource(Res.string.subtitle) + ) + // 1. AAB File Picker + FilePickerField( + label = stringResource(Res.string.step_one_title), + value = state.aabPath, + placeholder = stringResource(Res.string.no_aab_selected), + fileExtensionFilter = ".aab", + onPick = { viewModel.onEvent(BundleToolEvent.SelectAabFile(it)) } + ) + + // 2. Choose BundleTool + FilePickerField( + label = stringResource(Res.string.step_two_title), + value = state.bundleToolPath, + placeholder = stringResource(Res.string.bundletool_not_selected), + fileExtensionFilter = ".jar", + clickableText = data.model.ClickableText( + text = stringResource(Res.string.download_bundletool), + url = "https://github.com/google/bundletool/releases" + ), + onPick = { viewModel.onEvent(BundleToolEvent.SelectBundleToolPath(it)) } + ) + + // 3. Output Mode Selector + OptionSelector( + title = stringResource(Res.string.step_three_title), + options = listOf(OutputMode.Universal, OutputMode.ApkSet), + selected = state.mode, + onSelect = { viewModel.onEvent(BundleToolEvent.SelectMode(it)) }, + optionLabel = { + when (it) { + OutputMode.Universal -> "Universal APK" + OutputMode.ApkSet -> "APK Set (.apks)" + OutputMode.DeviceSpecific -> "Device-specific" + } + } + ) + + // 4. Output Directory + FilePickerField( + label = stringResource(Res.string.step_four_title), + value = state.outputDir, + placeholder = stringResource(Res.string.step_four_subtitle), + onPick = { viewModel.onEvent(BundleToolEvent.SelectOutputDir(it)) } + ) + + // 5. Signing Keystore + SigningSection( + signingMode = state.signingState.signingMode, + keystorePath = state.signingState.keystorePath, + keystorePassword = state.signingState.keystorePassword, + keyAlias = state.signingState.keyAlias, + keyPassword = state.signingState.keyPassword, + onModeChange = { viewModel.onEvent(BundleToolEvent.SelectSigning(it)) }, + onPickKeystore = { viewModel.onEvent(BundleToolEvent.SelectKeyStore(it)) }, + onKeystorePasswordChange = { viewModel.onEvent(BundleToolEvent.SelectKeyStorePassword(it)) }, + onAliasChange = { viewModel.onEvent(BundleToolEvent.SelectAlias(it)) }, + onKeyPasswordChange = { viewModel.onEvent(BundleToolEvent.SelectKeyPassword(it)) } + ) + + // Convert Button + ButtonWithLoader( + enabled = state.isReady, + isLoading = state.isConverting, + onClick = { viewModel.onEvent(BundleToolEvent.Convert) } + ) + + // Log Box + LogBox(log = state.log, onRunCommand = { + viewModel.runCustomCommand(it) + }, onClearLogs = { + viewModel.clearLogs() + } + ) + } +} diff --git a/composeApp/src/jvmMain/kotlin/ui/windows/SettingsWindow.kt b/composeApp/src/jvmMain/kotlin/ui/windows/SettingsWindow.kt new file mode 100644 index 0000000..d8d281f --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/windows/SettingsWindow.kt @@ -0,0 +1,8 @@ +package ui.windows + +import androidx.compose.runtime.Composable + +@Composable +fun SettingsWindow() { + +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/windows/SplashWindow.kt b/composeApp/src/jvmMain/kotlin/ui/windows/SplashWindow.kt new file mode 100644 index 0000000..dcb75bd --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/windows/SplashWindow.kt @@ -0,0 +1,69 @@ +package ui.windows + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun SplashWindow( + appName: String = "BundleTool", + version: String = "v1.0.0", + onFinish: () -> Unit = {} +) { + + LaunchedEffect(Unit) { + // Simulate initialization delay + kotlinx.coroutines.delay(2000) + onFinish() + } + + // Background + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // App icon + Image( + painter = painterResource("files/launcher.png"), + contentDescription = null, + modifier = Modifier.size(96.dp) + ) + // App title + Text( + text = appName, + style = MaterialTheme.typography.displayMedium, + fontWeight = FontWeight.Companion.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + // Version + Text( + text = version, + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Companion.Medium, + style = MaterialTheme.typography.titleMedium + ) + // Loader + CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/ui/windows/viewmodel/HomeViewModel.kt b/composeApp/src/jvmMain/kotlin/ui/windows/viewmodel/HomeViewModel.kt new file mode 100644 index 0000000..9cbff36 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/ui/windows/viewmodel/HomeViewModel.kt @@ -0,0 +1,222 @@ +package ui.windows.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import command.CommandBuilder +import data.domain.CommandResult +import data.state.BundleToolEvent +import data.state.BundleToolState +import data.state.OutputMode +import data.state.PersistedBundleToolConfig +import data.state.SigningMode +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import local.AppPreferences +import usecase.CommandUseCase +import utils.logger + +@OptIn(FlowPreview::class) +class HomeViewModel( + private val appPreferences: AppPreferences, + private val commandUseCase: CommandUseCase +) : ViewModel() { + private val log = logger() + + private val _uiState = MutableStateFlow(BundleToolState()) + val uiState: StateFlow = _uiState + + init { + log.info("Initializing HomeViewModel...") + onEvent(BundleToolEvent.Initialize) + } + + fun onEvent(event: BundleToolEvent) { + log.debug("Received event: {}", event) + + when (event) { + is BundleToolEvent.Initialize -> loadPersistedState() + is BundleToolEvent.SelectAabFile -> { + _uiState.update { it.copy(aabPath = event.path) } + persistCurrentState() + } + + is BundleToolEvent.SelectBundleToolPath -> { + _uiState.update { it.copy(bundleToolPath = event.path) } + persistCurrentState() + } + + is BundleToolEvent.SelectOutputDir -> { + _uiState.update { it.copy(outputDir = event.path) } + persistCurrentState() + } + + is BundleToolEvent.SelectSigning -> { + _uiState.update { + it.copy(signingState = it.signingState.copy(signingMode = event.signingMode)) + } + persistCurrentState() + } + + is BundleToolEvent.SelectKeyStore -> { + _uiState.update { + it.copy(signingState = it.signingState.copy(keystorePath = event.path)) + } + persistCurrentState() + } + + is BundleToolEvent.SelectKeyStorePassword -> _uiState.update { + it.copy(signingState = it.signingState.copy(keystorePassword = event.path)) + } + + is BundleToolEvent.SelectAlias -> _uiState.update { + it.copy(signingState = it.signingState.copy(keyAlias = event.alias)) + } + + is BundleToolEvent.SelectKeyPassword -> _uiState.update { + it.copy(signingState = it.signingState.copy(keyPassword = event.password)) + } + + is BundleToolEvent.SelectMode -> _uiState.update { it.copy(mode = event.mode) } + is BundleToolEvent.Convert -> convert() + } + } + + private fun convert() { + val state = uiState.value + log.info("Starting conversion for AAB: {}", state.aabPath) + + _uiState.update { + it.copy(isConverting = true, log = "๐Ÿš€ Starting conversion...\n") + } + + viewModelScope.launch(Dispatchers.IO) { + try { + persistCurrentState() + val config = CommandBuilder.Config( + bundleToolPath = state.bundleToolPath, + aabPath = state.aabPath, + outputDir = state.outputDir, + isUniversal = state.mode == OutputMode.Universal, + keystore = if (state.signingState.signingMode == SigningMode.Release) + CommandBuilder.KeystoreConfig( + path = state.signingState.keystorePath, + password = state.signingState.keystorePassword, + alias = state.signingState.keyAlias, + keyPassword = state.signingState.keyPassword + ) else null + ) + + log.debug("Generated CommandBuilder config: {}", config) + + val (command, result) = commandUseCase.executeBundleTool(config) + + log.info("Executing command: {}", command) + _uiState.update { it.copy(log = it.log + "\n> $command\n") } + + when (result) { + is CommandResult.Success -> { + log.info("Command executed successfully in {} ms", result.durationMs) + _uiState.update { + it.copy( + isConverting = false, + log = it.log + "\nโœ… ${result.output}\nCompleted in ${result.durationMs}ms" + ) + } + } + + is CommandResult.Failure -> { + log.error("Command execution failed: {}", result.error) + _uiState.update { + it.copy( + isConverting = false, + log = it.log + "\nโŒ ${result.error}" + ) + } + } + } + } catch (e: Exception) { + log.error("Unexpected error during conversion", e) + _uiState.update { + it.copy( + isConverting = false, + log = it.log + "\nโŒ Unexpected error: ${e.message}" + ) + } + } + } + } + + fun runCustomCommand(command: String) { + log.info("Running custom command: {}", command) + + _uiState.update { + it.copy(log = it.log + "\n> $command\n") + } + + viewModelScope.launch(Dispatchers.IO) { + when (val result = commandUseCase.executeCommand(command)) { + is CommandResult.Success -> { + log.info("Custom command succeeded in {} ms", result.durationMs) + _uiState.update { + it.copy( + log = it.log + "${result.output}\nโœ… Done in ${result.durationMs}ms\n" + ) + } + } + + is CommandResult.Failure -> { + log.error("Custom command failed: {}", result.error) + _uiState.update { + it.copy(log = it.log + "โŒ ${result.error}\n") + } + } + } + } + } + + fun clearLogs() { + log.debug("Clearing UI logs") + _uiState.update { it.copy(log = "") } + } + + private fun loadPersistedState() { + viewModelScope.launch(Dispatchers.IO) { + log.info("Loading persisted bundle tool state from preferences...") + val savedConfig = appPreferences.getBundleToolConfig() + if (savedConfig != null) { + log.debug("Loaded persisted config: {}", savedConfig) + _uiState.update { + it.copy( + bundleToolPath = savedConfig.bundleToolPath, + aabPath = savedConfig.aabPath, + outputDir = savedConfig.outputDir, + mode = savedConfig.mode, + signingState = savedConfig.signingState + ) + } + } else { + log.warn("No saved bundle tool configuration found.") + } + } + } + + private fun persistCurrentState() { + viewModelScope.launch(Dispatchers.IO) { + val state = uiState.value + val config = PersistedBundleToolConfig( + bundleToolPath = state.bundleToolPath, + aabPath = state.aabPath, + outputDir = state.outputDir, + mode = state.mode, + signingState = state.signingState + ) + + log.debug("Persisting current config: {}", config) + appPreferences.saveBundleToolConfig(config) + } + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/usecase/CommandUseCase.kt b/composeApp/src/jvmMain/kotlin/usecase/CommandUseCase.kt new file mode 100644 index 0000000..45966b2 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/usecase/CommandUseCase.kt @@ -0,0 +1,51 @@ +package usecase + +import command.CommandBuilder +import command.ICommandExecutor +import data.domain.CommandExecutionResult +import data.domain.CommandResult +import manager.FileOperationManager +import utils.files.FileActionResult +import java.io.File + +class CommandUseCase( + private val executor: ICommandExecutor, + private val fileManager: FileOperationManager +) { + + suspend fun executeCommand(rawCommand: String): CommandResult { + return executor.execute(rawCommand) + } + + suspend fun executeBundleTool(config: CommandBuilder.Config): CommandExecutionResult { + val commandBuilder = CommandBuilder(config) + val result = commandBuilder.build() + + return result.fold( + onSuccess = { command -> + val execResult = when (val res = executor.execute(command)) { + is CommandResult.Success -> { + val fileResult = fileManager.handleBundletoolOutput( + directory = File(config.aabPath).parent ?: ".", + fileName = File(config.aabPath).name, + isUniversal = config.isUniversal + ) + when (fileResult) { + is FileActionResult.Success -> CommandResult.Success( + "${res.output}\n${fileResult.message}", + res.durationMs + ) + + is FileActionResult.Failure -> CommandResult.Failure(fileResult.error) + } + } + is CommandResult.Failure -> res + } + CommandExecutionResult(command, execResult) + }, + onFailure = { + CommandExecutionResult("Invalid command configuration", CommandResult.Failure(it.message ?: "Invalid config")) + } + ) + } +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/utils/Extensions.kt b/composeApp/src/jvmMain/kotlin/utils/Extensions.kt new file mode 100644 index 0000000..90bb773 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/utils/Extensions.kt @@ -0,0 +1,6 @@ +package utils + +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +inline fun T.logger(): Logger = LoggerFactory.getLogger(T::class.java) diff --git a/src/jvmMain/kotlin/utils/Utils.kt b/composeApp/src/jvmMain/kotlin/utils/Utils.kt similarity index 100% rename from src/jvmMain/kotlin/utils/Utils.kt rename to composeApp/src/jvmMain/kotlin/utils/Utils.kt diff --git a/composeApp/src/jvmMain/kotlin/utils/files/FileUtilsImpl.kt b/composeApp/src/jvmMain/kotlin/utils/files/FileUtilsImpl.kt new file mode 100644 index 0000000..81b15fe --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/utils/files/FileUtilsImpl.kt @@ -0,0 +1,36 @@ +package utils.files + +import java.io.File +import java.io.FileOutputStream + +object FileUtilsImpl : IFileUtils { + override fun renameFile(source: File, destination: File): Boolean { + if (!source.exists()) return false + return source.renameTo(destination) + } + + override fun unzip(zipFile: File, outputDir: String) { + val buffer = ByteArray(1024) + val zipInputStream = java.util.zip.ZipInputStream(zipFile.inputStream()) + var entry = zipInputStream.nextEntry + while (entry != null) { + val newFile = File(outputDir, entry.name) + if (entry.isDirectory) { + newFile.mkdirs() + } else { + newFile.parentFile?.mkdirs() + FileOutputStream(newFile).use { output -> + var len: Int + while (zipInputStream.read(buffer).also { len = it } > 0) { + output.write(buffer, 0, len) + } + } + } + zipInputStream.closeEntry() + entry = zipInputStream.nextEntry + } + zipInputStream.close() + } + + override fun deleteFile(file: File): Boolean = file.delete() +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/utils/files/FilesActionResult.kt b/composeApp/src/jvmMain/kotlin/utils/files/FilesActionResult.kt new file mode 100644 index 0000000..e2b6044 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/utils/files/FilesActionResult.kt @@ -0,0 +1,6 @@ +package utils.files + +sealed class FileActionResult { + data class Success(val message: String) : FileActionResult() + data class Failure(val error: String) : FileActionResult() +} \ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/utils/files/IFileUtils.kt b/composeApp/src/jvmMain/kotlin/utils/files/IFileUtils.kt new file mode 100644 index 0000000..465f035 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/utils/files/IFileUtils.kt @@ -0,0 +1,9 @@ +package utils.files + +import java.io.File + +interface IFileUtils { + fun renameFile(source: File, destination: File): Boolean + fun unzip(zipFile: File, outputDir: String) + fun deleteFile(file: File): Boolean +} \ No newline at end of file diff --git a/src/jvmTest/kotlin/command/CommandBuilderTest.kt b/composeApp/src/jvmTest/kotlin/command/CommandBuilderTest.kt similarity index 100% rename from src/jvmTest/kotlin/command/CommandBuilderTest.kt rename to composeApp/src/jvmTest/kotlin/command/CommandBuilderTest.kt diff --git a/src/jvmTest/kotlin/ui/components/CheckboxWithTextTest.kt b/composeApp/src/jvmTest/kotlin/ui/components/CheckboxWithTextTest.kt similarity index 100% rename from src/jvmTest/kotlin/ui/components/CheckboxWithTextTest.kt rename to composeApp/src/jvmTest/kotlin/ui/components/CheckboxWithTextTest.kt diff --git a/gradle.properties b/gradle.properties index 362b3b4..41d680b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,8 @@ +#Kotlin kotlin.code.style=official -kotlin.version=1.8.0 -agp.version=7.3.0 -compose.version=1.6.0 \ No newline at end of file +kotlin.daemon.jvmargs=-Xmx3072M + +#Gradle +org.gradle.jvmargs=-Xmx3072M -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true +org.gradle.caching=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..9775957 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,50 @@ +[versions] +androidx-lifecycle = "2.9.5" +composeHotReload = "1.0.0-rc02" +composeMultiplatform = "1.9.1" +junit = "4.13.2" +kotlin = "2.2.20" +kotlinx-coroutines = "1.10.2" +androidx-compose-navigation = "2.9.0-beta05" +slf4j-android = "1.7.36" +logback-jvm = "1.5.18" +koin_version = "4.0.3" +datastore = "1.1.7" +ktor-client = "3.1.3" +animationDesktop = "1.7.0" + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +#Compose Navigation +androidx-compose-navigation = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "androidx-compose-navigation" } +# Koin +koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin_version" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin_version" } +koin-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin_version" } +koin-navigation = { module = "io.insert-koin:koin-compose-viewmodel-navigation", version.ref = "koin_version" } +# DataStore +datastore = { module = "androidx.datastore:datastore", version.ref = "datastore" } +datastore-prefs = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" } +# Logging +sl4j-logging = { module = "org.slf4j:slf4j-android", version.ref = "slf4j-android" } +logback-logging = { module = "ch.qos.logback:logback-classic", version.ref = "logback-jvm" } +ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor-client" } +# Ktor Client +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor-client" } +ktor-client-cio = { module = "io.ktor:ktor-client-cio", version.ref = "ktor-client" } +ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor-client" } +ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor-client" } +androidx-animation-desktop = { group = "androidx.compose.animation", name = "animation-desktop", version.ref = "animationDesktop" } + +[plugins] +composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae04661..42f0071 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists +zipStorePath=wrapper/dists \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 8b8f1b2..23ff088 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,16 +1,36 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + pluginManagement { repositories { - google() - gradlePluginPortal() + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - } - - plugins { - kotlin("multiplatform").version(extra["kotlin.version"] as String) - id("org.jetbrains.compose").version(extra["compose.version"] as String) + gradlePluginPortal() } } +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +include(":composeApp") rootProject.name = "AabToApk" diff --git a/src/jvmMain/kotlin/Main.kt b/src/jvmMain/kotlin/Main.kt deleted file mode 100644 index 980a87b..0000000 --- a/src/jvmMain/kotlin/Main.kt +++ /dev/null @@ -1,573 +0,0 @@ -import androidx.compose.desktop.ui.tooling.preview.Preview -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentSize -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material.Button -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Icon -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.loadSvgPainter -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.useResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.AwtWindow -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 command.CommandBuilder -import command.CommandExecutor -import local.FileStorageHelper -import ui.Styles -import ui.components.ButtonWithToolTip -import ui.components.CheckboxWithText -import ui.components.ChooseFileTextField -import ui.components.CustomTextField -import ui.components.LoadingDialog -import utils.Constant -import utils.DBConstants -import utils.FileDialogType -import utils.FileHelper -import utils.Log -import utils.SigningMode -import utils.Strings -import java.awt.Desktop -import java.awt.FileDialog -import java.awt.Frame -import java.net.URI - -@Composable -@Preview -fun App(fileStorageHelper: FileStorageHelper, savedPath: String?, adbSavedPath: String?) { - val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) - val coroutineScope = rememberCoroutineScope() - var logs by remember { mutableStateOf("") } - var bundletoolPath by remember { mutableStateOf("") } - var aabFilePath by remember { mutableStateOf(Pair("", "")) } - var isLoading by remember { mutableStateOf(false) } - var isOpen by remember { mutableStateOf(false) } - var isExecute by remember { mutableStateOf(false) } - var fileDialogType by remember { mutableStateOf(0) } - var saveJarPath by remember { mutableStateOf(false) } - var isOverwrite by remember { mutableStateOf(false) } - var isAapt2PathEnabled by remember { mutableStateOf(false) } - var aapt2Path by remember { mutableStateOf("") } - var isUniversalMode by remember { mutableStateOf(true) } - var signingMode by remember { mutableStateOf(SigningMode.DEBUG) } - var keyStorePath by remember { mutableStateOf("") } - var keyStorePassword by remember { mutableStateOf("") } - var keyAlias by remember { mutableStateOf("") } - var keyPassword by remember { mutableStateOf("") } - var isAutoUnzip by remember { mutableStateOf(true) } - var savedJarPath by remember { mutableStateOf(savedPath) } - var isAdbSetupDone by remember { mutableStateOf(false) } - var adbPath by remember { mutableStateOf("") } - var showLoadingDialog by remember { mutableStateOf(Pair("", false)) } - var isDeviceIdEnabled by remember { mutableStateOf(false) } - var deviceSerialId by remember { mutableStateOf("") } - - // TODO: (Fixed this issue need to test more!) - Can't update file path once saved, For now Delete path.kb file inside storage directory. - savedJarPath?.let { - bundletoolPath = it - saveJarPath = true - } - - // Check if ADB Setup is Done or Not - adbSavedPath?.let { - adbPath = it - isAdbSetupDone = true - } - - if (isOpen && !isLoading) { - FileDialog { fileName, directory -> - isOpen = false - if (fileName.isNullOrEmpty() || directory.isNullOrEmpty()) { - return@FileDialog - } - when (fileDialogType) { - FileDialogType.BUNDLETOOL -> bundletoolPath = "$directory$fileName" - FileDialogType.AAPT2 -> aapt2Path = "$directory$fileName" - FileDialogType.KEY_STORE_PATH -> keyStorePath = "$directory$fileName" - FileDialogType.ADB_PATH -> { - adbPath = "$directory$fileName" - // Show Loading Here - showLoadingDialog = Pair(Strings.VERIFYING_ADB_PATH, true) - CommandExecutor().executeCommand( - CommandBuilder() - .verifyAdbPath(true, adbPath) - .getAdbVerifyCommand(), coroutineScope, - onSuccess = { - logs += it - isAdbSetupDone = true - Log.i("Saving Path in DB $adbPath") - fileStorageHelper.save(DBConstants.ADB_PATH, adbPath) - // Hide Loading - Thread.sleep(1000L) - showLoadingDialog = Pair(Strings.VERIFYING_ADB_PATH, false) - }, - onFailure = { - logs += it - isAdbSetupDone = false - // Hide Loading - showLoadingDialog = Pair(Strings.VERIFYING_ADB_PATH, false) - } - ) - } - - else -> { - aabFilePath = Pair(directory, fileName) - } - } - } - } - - if (showLoadingDialog.second) { - LoadingDialog(showLoadingDialog.first) - } - - if (isExecute) { - isExecute = false - // Get Command to Execute - val (cmd, isValid) = CommandBuilder() - .bundletoolPath(bundletoolPath) - .aabFilePath(aabFilePath) - .isOverwrite(isOverwrite) - .isUniversalMode(isUniversalMode) - .isAapt2PathEnabled(isAapt2PathEnabled) - .aapt2Path(aapt2Path) - .signingMode(signingMode) - .keyStorePath(keyStorePath) - .keyStorePassword(keyStorePassword) - .keyAlias(keyAlias) - .keyPassword(keyPassword) - .validateAndGetCommand() - if (isValid) { - Log.i("Command $cmd") - // logs += "Executing Command : \n$cmd\n" - CommandExecutor() - .executeCommand( - cmd, - coroutineScope, - onSuccess = { - logs += "$it\n" - // Do further file operation after new apks is generated - // From Auto Zip you can control further file operations. - if (isAutoUnzip) { - FileHelper.performFileOperations(aabFilePath.first, aabFilePath.second) { status, message -> - isLoading = false - Log.i("STATUS - $status\nMESSAGE - $message") - logs += "\nFiles Operations Starting...\n$message\n" - } - } else { - logs += "\nFile will be saved at ${aabFilePath.first.removeSuffix("\\")}.\n" - isLoading = false - } - - // Save Path in Storage - // If We don't have any saved path in file storage and save jar path option is checked.Then,we can save new value in storage. - if (savedJarPath == null && saveJarPath) { - fileStorageHelper.save("path", bundletoolPath) - } - // If we have saved path in file storage, and it is not changed and save jar path is checked. Then, we can update the value in file storage. - else if (savedJarPath != null && savedJarPath != bundletoolPath && saveJarPath) { - fileStorageHelper.save("path", bundletoolPath) - } else { - if (fileStorageHelper.delete("path")) - saveJarPath = false - } - }, - onFailure = { - isLoading = false - logs += "Failed -> ${it.printStackTrace()}" - } - ) - } else { - Log.i("Error $cmd") - logs += "\nError -> $cmd" - isLoading = false - } - } - - MaterialTheme { - Column( - modifier = Modifier.fillMaxWidth(), - horizontalAlignment = Alignment.Start - ) { - Spacer(modifier = Modifier.padding(12.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth() - ) { - Text( - text = Strings.APP_NAME, - style = Styles.TextStyleBold(28.sp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - ) - ButtonWithToolTip( - if (isAdbSetupDone) Strings.ABD_SETUP_DONE else Strings.SETUP_ADB, - onClick = { - fileDialogType = FileDialogType.ADB_PATH - isOpen = true - }, - Strings.SETUP_ADB_INFO, - icon = if (isAdbSetupDone) "done" else "info" - ) - } - Spacer(modifier = Modifier.padding(8.dp)) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.wrapContentSize(), - verticalAlignment = Alignment.CenterVertically - ) { - // Bundle tool select flow - ChooseFileTextField( - bundletoolPath, - Strings.SELECT_BUNDLETOOL_JAR, - onSelect = { - fileDialogType = FileDialogType.BUNDLETOOL - isOpen = true - } - ) - CheckboxWithText( - Strings.SAVE_JAR_PATH, - saveJarPath, - onCheckedChange = { - saveJarPath = it - }, - Strings.SAVE_JAR_PATH_INFO - ) - } - val downloadInfo = buildAnnotatedString { - withStyle(style = SpanStyle(color = Color.Blue, textDecoration = TextDecoration.Underline)) { - append(Strings.DOWNLOAD_BUNDLETOOL) - } - addStringAnnotation( - tag = Strings.URL, - annotation = Constant.BUNDLE_DOWNLOAD_LINK, - start = 0, - end = length - ) - } - ClickableText( - text = downloadInfo, - style = Styles.TextStyleMedium(14.sp), - modifier = Modifier.padding(start = 16.dp), - onClick = { offset -> - val annotations = downloadInfo.getStringAnnotations(Strings.URL, offset, offset) - if (annotations.isNotEmpty()) { - val uri = URI(annotations.first().item) - if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { - Desktop.getDesktop().browse(uri) - } else { - // Desktop not supported, handle as necessary - } - } - } - ) - Spacer(modifier = Modifier.padding(8.dp)) - ChooseFileTextField( - "${aabFilePath.first}${aabFilePath.second}", - Strings.SELECT_AAB_FILE, - onSelect = { - fileDialogType = FileDialogType.AAB - isOpen = true - } - ) - Spacer(modifier = Modifier.padding(8.dp)) - Text( - text = Strings.OPTIONS_FOR_BUILD_APKS, - style = Styles.TextStyleBold(20.sp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween - ) { - CheckboxWithText( - Strings.OVERWRITE, - isOverwrite, - onCheckedChange = { - isOverwrite = it - }, - Strings.OVERWRITE_INFO - ) - CheckboxWithText( - Strings.MODE_UNIVERSAL, - isUniversalMode, - onCheckedChange = { - isUniversalMode = it - }, - Strings.MODE_UNIVERSAL_INFO - ) - CheckboxWithText( - Strings.AAPT2_PATH, - isAapt2PathEnabled, - onCheckedChange = { - isAapt2PathEnabled = it - }, - Strings.AAPT2_PATH_INFO - ) - } - if (isAapt2PathEnabled) { - ChooseFileTextField( - aapt2Path, - Strings.SELECT_AAPT2_FILE, - onSelect = { - fileDialogType = FileDialogType.AAPT2 - isOpen = true - } - ) - } - Text( - text = Strings.SIGNING_MODE, - style = Styles.TextStyleBold(16.sp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween - ) { - CheckboxWithText( - Strings.DEBUG, - signingMode == SigningMode.DEBUG, - onCheckedChange = { - signingMode = if (it) SigningMode.DEBUG - else SigningMode.RELEASE - } - ) - CheckboxWithText( - Strings.RELEASE, - signingMode == SigningMode.RELEASE, - onCheckedChange = { - signingMode = if (it) SigningMode.RELEASE - else SigningMode.DEBUG - } - ) - } - if (signingMode == SigningMode.RELEASE) { - Row( - horizontalArrangement = Arrangement.SpaceBetween - ) { - ChooseFileTextField( - keyStorePath, - Strings.KEYSTORE_PATH, - onSelect = { - fileDialogType = FileDialogType.KEY_STORE_PATH - isOpen = true - } - ) - CustomTextField( - keyStorePassword, - Strings.KEYSTORE_PASSWORD, - onValueChange = { - keyStorePassword = it - } - ) - } - Row( - horizontalArrangement = Arrangement.SpaceBetween - ) { - CustomTextField( - keyAlias, - Strings.KEY_ALIAS, - forPassword = false, - onValueChange = { - keyAlias = it - } - ) - CustomTextField( - keyPassword, - Strings.KEY_PASSWORD, - onValueChange = { - keyPassword = it - } - ) - } - } - Text( - text = Strings.FILE_OPTIONS, - style = Styles.TextStyleBold(16.sp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - ) - CheckboxWithText( - Strings.AUTO_UNZIP, - isAutoUnzip, - onCheckedChange = { - isAutoUnzip = it - }, - Strings.AUTO_UNZIP - ) - Spacer(modifier = Modifier.padding(8.dp)) - if (isAdbSetupDone) { - Text( - text = Strings.DEVICE_OPTIONS, - style = Styles.TextStyleBold(16.sp), - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - ) - Row( - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - CheckboxWithText( - Strings.DEVICE_ID, - isDeviceIdEnabled, - onCheckedChange = { - isDeviceIdEnabled = it - }, - Strings.DEVICE_ID_INFO - ) - if (isDeviceIdEnabled) { - CustomTextField( - deviceSerialId, - Strings.SERIAL_ID, - forPassword = false, - onValueChange = { - deviceSerialId = it - } - ) - ButtonWithToolTip( - Strings.FETCH_DEVICES, - onClick = { - CommandExecutor() - .executeCommand( - CommandBuilder() - .getAdbFetchCommand(adbSavedPath!!), - coroutineScope, - onSuccess = { - logs += it - }, - onFailure = { - logs += it.printStackTrace() - } - ) - }, - Strings.FETCH_DEVICES_INFO, - icon = "device_fetch" - ) - } - } - Spacer(modifier = Modifier.padding(8.dp)) - } - if (isLoading) { - CircularProgressIndicator( - modifier = Modifier.size(size = 40.dp) - .padding(start = 16.dp, top = 0.dp, end = 0.dp, bottom = 0.dp), - strokeWidth = 4.dp - ) - } else { - Button( - onClick = { - isLoading = true - isExecute = true - }, - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - .wrapContentWidth() - ) { - Text( - text = Strings.EXECUTE, - style = Styles.TextStyleMedium(16.sp), - color = Color.White, - fontWeight = FontWeight.Medium - ) - } - } - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp, bottom = 8.dp), - verticalAlignment = Alignment.Bottom - ) { - Text( - text = Strings.LOGS_VIEW, - style = Styles.TextStyleBold(20.sp), - modifier = Modifier.padding(start = 0.dp, end = 0.dp, bottom = 8.dp) - ) - Button( - modifier = Modifier.padding(end = 0.dp, bottom = 8.dp), - onClick = { - logs = "" - } - ) { - Text( - text = Strings.CLEAR_LOGS, - style = Styles.TextStyleBold(13.sp) - ) - Spacer(modifier = Modifier.size(ButtonDefaults.IconSpacing)) - Icon( - painter = useResource("clear.svg") { loadSvgPainter(it, density) }, - contentDescription = "Clear", - modifier = Modifier.size(ButtonDefaults.IconSize) - ) - } - } - TextField( - modifier = Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 16.dp), - value = logs, - textStyle = Styles.TextStyleMedium(16.sp), - onValueChange = {} - ) - } - } -} - -@Composable -private fun FileDialog( - parent: Frame? = null, - onCloseRequest: (fileName: String?, directory: String?) -> Unit -) = AwtWindow( - create = { - object : FileDialog(parent, Strings.CHOOSE_FILE, LOAD) { - override fun setVisible(value: Boolean) { - super.setVisible(value) - if (value) { - onCloseRequest(file, directory) - } - } - } - }, - dispose = FileDialog::dispose -) - -fun main() = application { - val fileStorageHelper = FileStorageHelper() - // Check if path for bundletool exists in local storage - val path = fileStorageHelper.read(DBConstants.BUNDLETOOL_PATH) as String? - val adbPath = fileStorageHelper.read(DBConstants.ADB_PATH) as String? - val icon = painterResource("launcher.png") - Log.showLogs = true - Window( - icon = icon, - onCloseRequest = ::exitApplication, - state = rememberWindowState( - width = 1200.dp, height = 1000.dp, - position = WindowPosition(Alignment.Center) - ), - title = Strings.APP_NAME - ) { - App(fileStorageHelper, path, adbPath) - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/command/CommandBuilder.kt b/src/jvmMain/kotlin/command/CommandBuilder.kt deleted file mode 100644 index d5718bc..0000000 --- a/src/jvmMain/kotlin/command/CommandBuilder.kt +++ /dev/null @@ -1,118 +0,0 @@ -package command - -import utils.Log -import utils.SigningMode -import utils.Utils - -class CommandBuilder { - private var bundletoolPath: String = "" - private var aabFilePath: Pair = Pair("", "") - private var isOverwrite: Boolean = false - private var isAapt2PathEnabled: Boolean = false - private var aapt2Path: String = "" - private var isUniversalMode: Boolean = true - private var signingMode: Int = 1 - private var keyStorePath: String = "" - private var keyStorePassword: String = "" - private var keyAlias: String = "" - private var keyPassword: String = "" - private var adbVerifyCommandExecute = Pair(false, "") - private var isDeviceIdEnabled: Boolean = false - private var adbSerialId: String = "" - - fun bundletoolPath(path: String) = apply { this.bundletoolPath = path } - fun aabFilePath(path: Pair) = apply { this.aabFilePath = path } - fun isOverwrite(overwrite: Boolean) = apply { this.isOverwrite = overwrite } - fun isAapt2PathEnabled(enabled: Boolean) = apply { this.isAapt2PathEnabled = enabled } - fun aapt2Path(path: String) = apply { this.aapt2Path = path } - fun isUniversalMode(universalMode: Boolean) = apply { this.isUniversalMode = universalMode } - fun signingMode(mode: Int) = apply { this.signingMode = mode } - fun keyStorePath(path: String) = apply { this.keyStorePath = path } - fun keyStorePassword(password: String) = apply { this.keyStorePassword = password } - fun keyAlias(alias: String) = apply { this.keyAlias = alias } - fun keyPassword(password: String) = apply { this.keyPassword = password } - - fun verifyAdbPath(value: Boolean, path: String) = apply { this.adbVerifyCommandExecute = Pair(value, path) } - - fun isDeviceSerialIdEnabled(value: Boolean) = apply { this.isDeviceIdEnabled = value } - - fun adbSerialId(value: String) = apply { this.adbSerialId = value } - - fun getAdbVerifyCommand(): String { - val (forVerify, path) = adbVerifyCommandExecute - if (forVerify) { - return if (Utils.isWindowsOS()) "\"${path}\" version" else "$path version" - } - return "" - } - - fun getAdbFetchCommand(adbPath: String): String { - return if (Utils.isWindowsOS()) "\"${adbPath}\" devices" else "$adbPath devices" - } - - fun validateAndGetCommand(): Pair { - if (bundletoolPath.isEmpty()) { - return Pair("bundletoolPath", false) - } - if (aabFilePath.first.isEmpty() || aabFilePath.second.isEmpty()) { - return Pair("aabFilePath", false) - } - if (isAapt2PathEnabled && aapt2Path.isEmpty()) { - return Pair("aapt2Path", false) - } - if (signingMode == SigningMode.RELEASE && (keyStorePath.isEmpty() || keyStorePassword.isEmpty() || keyAlias.isEmpty() || keyPassword.isEmpty())) { - return Pair("Check Keystore Info!", false) - } - if (isDeviceIdEnabled && adbSerialId.isEmpty()) { - return Pair("Invalid Serial ID", false) - } - return Pair(getCommand(), true) - } - - private fun getCommand(): String { - val commandBuilder = StringBuilder() - if (Utils.isWindowsOS()) { - commandBuilder.append("java -jar \"$bundletoolPath\" build-apks ") - if (isAapt2PathEnabled) { - commandBuilder.append("--aapt2=\"$aapt2Path\" ") - } - commandBuilder.append( - "--bundle=\"${aabFilePath.first}${aabFilePath.second}\" --output=\"${aabFilePath.first}${ - aabFilePath.second.split( - "." - )[0] - }.apks\" " - ) - } else { - commandBuilder.append("java -jar $bundletoolPath build-apks ") - if (isAapt2PathEnabled) { - commandBuilder.append("--aapt2=$aapt2Path ") - } - commandBuilder.append( - "--bundle=${aabFilePath.first}${aabFilePath.second} --output=${aabFilePath.first}${ - aabFilePath.second.split( - "." - )[0] - }.apks " - ) - } - - if (isUniversalMode) { - commandBuilder.append("--mode=universal ") - } - if (isOverwrite) { - commandBuilder.append("--overwrite ") - } - - if (signingMode == SigningMode.RELEASE) { - commandBuilder.append("--ks=$keyStorePath --ks-pass=pass:$keyStorePassword --ks-key-alias=$keyAlias --key-pass=pass:$keyPassword ") - } - - if (isDeviceIdEnabled) { - commandBuilder.append("--device-id=$adbSerialId ") - } - - Log.i("COMMAND_BUILDER -> $commandBuilder") - return commandBuilder.toString() - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/local/FileStorageHelper.kt b/src/jvmMain/kotlin/local/FileStorageHelper.kt deleted file mode 100644 index c7cb24e..0000000 --- a/src/jvmMain/kotlin/local/FileStorageHelper.kt +++ /dev/null @@ -1,75 +0,0 @@ -package local - -import com.esotericsoftware.kryo.Kryo -import com.esotericsoftware.kryo.io.Input -import com.esotericsoftware.kryo.io.Output -import java.io.File -import java.io.FileInputStream -import java.io.FileOutputStream -import java.nio.file.Paths - -const val DB_PATH = "/storage" - -class FileStorageHelper { - - private lateinit var kryo: Kryo - - init { - initializeDir() - } - - private fun getKryo(): Kryo { - return if (this::kryo.isInitialized) { - kryo - } else { - val kryo = Kryo() - kryo.register(KiteTable::class.java) - kryo - } - } - - private fun initializeDir() { - val file = File(getCurrentDir()) - file.mkdir() - } - - private fun getCurrentDir(): String { - return Paths.get("").toAbsolutePath().toString() + DB_PATH - } - - private fun getPath(key: String): String { - return "${getCurrentDir()}/$key.kb" - } - - fun save(key: String, value: E) { - var kryoOutput: Output? - try { - val kiteTable = KiteTable(value) - val fileStream = FileOutputStream(File(getPath(key))) - kryoOutput = Output(fileStream) - getKryo().writeObject(kryoOutput, kiteTable) - kryoOutput.flush() - fileStream.flush() - kryoOutput.close() - } catch (e: Exception) { - throw KiteDbException(e.message) - } - } - - fun read(key: String): Any? { - val keyFile = File(getPath(key)) - if (!keyFile.exists()) return null - val kryoInput = Input(FileInputStream(keyFile)) - val kiteTable = getKryo().readObject(kryoInput, KiteTable::class.java) - return kiteTable.mContent - } - - fun delete(key: String): Boolean { - val keyFile = File(getPath(key)) - return if (keyFile.exists()) { - keyFile.delete() - } else { - false - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/local/KiteDbException.kt b/src/jvmMain/kotlin/local/KiteDbException.kt deleted file mode 100644 index a8237bb..0000000 --- a/src/jvmMain/kotlin/local/KiteDbException.kt +++ /dev/null @@ -1,8 +0,0 @@ -package local - -class KiteDbException : RuntimeException { - - constructor(detailMessage: String?) : super(detailMessage) - - constructor(detailMessage: String?, throwable: Throwable?) : super(detailMessage, throwable) -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/local/KiteTable.kt b/src/jvmMain/kotlin/local/KiteTable.kt deleted file mode 100644 index 246639d..0000000 --- a/src/jvmMain/kotlin/local/KiteTable.kt +++ /dev/null @@ -1,11 +0,0 @@ -package local - -class KiteTable { - internal constructor() - internal constructor(content: T) { - mContent = content - } - - // Serialized content - var mContent: T? = null -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/Styles.kt b/src/jvmMain/kotlin/ui/Styles.kt deleted file mode 100644 index 76bc587..0000000 --- a/src/jvmMain/kotlin/ui/Styles.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ui - -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.TextUnit - -object Styles { - - fun TextStyleNormal(size: TextUnit) = - TextStyle( - fontWeight = FontWeight.Normal, - fontFamily = codeFontFamily, - fontSize = size - ) - - fun TextStyleMedium(size: TextUnit) = - TextStyle( - fontWeight = FontWeight.Medium, - fontFamily = codeFontFamily, - fontSize = size - ) - - fun TextStyleSemiBold(size: TextUnit) = - TextStyle( - fontWeight = FontWeight.SemiBold, - fontFamily = codeFontFamily, - fontSize = size - ) - - fun TextStyleBold(size: TextUnit) = - TextStyle( - fontWeight = FontWeight.Bold, - fontFamily = codeFontFamily, - fontSize = size - ) -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/Typography.kt b/src/jvmMain/kotlin/ui/Typography.kt deleted file mode 100644 index 717a87f..0000000 --- a/src/jvmMain/kotlin/ui/Typography.kt +++ /dev/null @@ -1,84 +0,0 @@ -package ui - -import androidx.compose.material.Typography -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.platform.Font -import androidx.compose.ui.unit.sp - -val codeFontFamily = FontFamily( - Font(resource = "fonts/sans_regular.ttf", weight = FontWeight.Light), - Font(resource = "fonts/sans_thin.ttf", weight = FontWeight.Normal), - Font(resource = "fonts/sans_medium.ttf", weight = FontWeight.Medium), - Font(resource = "fonts/sans_bold.ttf", weight = FontWeight.Bold) -) - -val typography = Typography( - defaultFontFamily = codeFontFamily, - h1 = TextStyle( - fontWeight = FontWeight.Light, - fontSize = 96.sp, - letterSpacing = (-1.5).sp - ), - h2 = TextStyle( - fontWeight = FontWeight.Light, - fontSize = 60.sp, - letterSpacing = (-0.5).sp - ), - h3 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 48.sp, - letterSpacing = 0.sp - ), - h4 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 30.sp, - letterSpacing = 0.sp - ), - h5 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 24.sp, - letterSpacing = 0.sp - ), - h6 = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - letterSpacing = 0.sp - ), - subtitle1 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - letterSpacing = 0.15.sp - ), - subtitle2 = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - letterSpacing = 0.1.sp - ), - body1 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 16.sp, - letterSpacing = 0.5.sp - ), - body2 = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 14.sp, - letterSpacing = 0.25.sp - ), - button = TextStyle( - fontWeight = FontWeight.Bold, - fontSize = 14.sp, - letterSpacing = 0.25.sp - ), - caption = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 12.sp, - letterSpacing = 0.4.sp - ), - overline = TextStyle( - fontWeight = FontWeight.Normal, - fontSize = 10.sp, - letterSpacing = 1.sp - ) -) \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt b/src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt deleted file mode 100644 index cd7ffc7..0000000 --- a/src/jvmMain/kotlin/ui/components/ButtonWithToolTip.kt +++ /dev/null @@ -1,84 +0,0 @@ -package ui.components - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.TooltipArea -import androidx.compose.foundation.TooltipPlacement -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.ButtonDefaults -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Button -import androidx.compose.material.ButtonColors -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.loadSvgPainter -import androidx.compose.ui.res.useResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ui.Styles - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun ButtonWithToolTip( - label: String, - onClick: () -> Unit, - toolTipText: String = "", - icon: String = "info", - buttonColors: ButtonColors = ButtonDefaults.buttonColors() -) { - val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) - Button( - onClick = { - onClick.invoke() - }, - colors = buttonColors, - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 16.dp, bottom = 8.dp) - .wrapContentWidth() - ) { - Text( - text = label, - style = Styles.TextStyleMedium(16.sp), - color = Color.White, - fontWeight = FontWeight.Medium - ) - if (toolTipText.isNotEmpty()) { - TooltipArea( - tooltip = { - // composable tooltip content - Surface( - modifier = Modifier.shadow(4.dp), - color = Color(255, 255, 210), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = toolTipText, - color = Color.Black, - style = Styles.TextStyleMedium(12.sp), - modifier = Modifier.padding(10.dp) - ) - } - }, - delayMillis = 200, // in milliseconds - tooltipPlacement = TooltipPlacement.CursorPoint( - alignment = Alignment.BottomEnd, - offset = DpOffset.Zero // tooltip offset - ) - ) { - Icon( - painter = useResource("$icon.svg") { loadSvgPainter(it, density) }, - contentDescription = icon, - modifier = Modifier.padding(start = 8.dp) - ) - } - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt b/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt deleted file mode 100644 index 888785b..0000000 --- a/src/jvmMain/kotlin/ui/components/CheckboxWithText.kt +++ /dev/null @@ -1,77 +0,0 @@ -package ui.components - -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.TooltipArea -import androidx.compose.foundation.TooltipPlacement -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Checkbox -import androidx.compose.material.Icon -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.loadSvgPainter -import androidx.compose.ui.res.useResource -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ui.Styles -import utils.TestTags - -@OptIn(ExperimentalFoundationApi::class) -@Composable -fun CheckboxWithText(label: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, toolTipText: String = "") { - val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(end = 16.dp) - ) { - Checkbox( - modifier = Modifier.testTag(TestTags.CHECKBOX_TAG), - checked = isChecked, - onCheckedChange = { onCheckedChange.invoke(it) } - ) - Text( - modifier = Modifier.testTag(TestTags.TEXT_TAG), - text = label, - style = Styles.TextStyleMedium(14.sp) - ) - if (toolTipText.isNotEmpty()) { - TooltipArea( - tooltip = { - // composable tooltip content - Surface( - modifier = Modifier.shadow(4.dp), - color = Color(255, 255, 210), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = toolTipText, - - style = Styles.TextStyleMedium(12.sp), - modifier = Modifier.padding(10.dp) - ) - } - }, - delayMillis = 200, // in milliseconds - tooltipPlacement = TooltipPlacement.CursorPoint( - alignment = Alignment.BottomEnd, - offset = DpOffset.Zero // tooltip offset - ) - ) { - Icon( - painter = useResource("info.svg") { loadSvgPainter(it, density) }, - contentDescription = "Info", - modifier = Modifier.padding(start = 8.dp) - ) - } - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/ChooseFileTextField.kt b/src/jvmMain/kotlin/ui/components/ChooseFileTextField.kt deleted file mode 100644 index 1d988e0..0000000 --- a/src/jvmMain/kotlin/ui/components/ChooseFileTextField.kt +++ /dev/null @@ -1,74 +0,0 @@ -package ui.components - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Button -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.material.MaterialTheme -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.loadSvgPainter -import androidx.compose.ui.res.useResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ui.Styles - -@Composable -fun ChooseFileTextField(value: String, label: String, onSelect: () -> Unit) { - val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) - Row( - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 0.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - TextField( - modifier = Modifier.fillMaxWidth(0.4f).height(50.dp) - .border( - BorderStroke(width = 2.dp, color = MaterialTheme.colors.primary), - shape = RoundedCornerShape(topEnd = 0.dp, bottomEnd = 0.dp, topStart = 10.dp, bottomStart = 10.dp) - ), - value = value, - singleLine = true, - readOnly = true, - colors = TextFieldDefaults.textFieldColors( - backgroundColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ), - label = { - Text( - text = label, - style = Styles.TextStyleMedium(16.sp), - fontWeight = FontWeight.Medium - ) - }, - textStyle = Styles.TextStyleMedium(16.sp), - onValueChange = {} - ) - Button( - onClick = { - onSelect.invoke() - }, - modifier = Modifier - .height(50.dp) - .wrapContentWidth() - ) { - Icon( - painter = useResource("open_folder.svg") { loadSvgPainter(it, density) }, - contentDescription = "" - ) - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/CustomTextField.kt b/src/jvmMain/kotlin/ui/components/CustomTextField.kt deleted file mode 100644 index a173cec..0000000 --- a/src/jvmMain/kotlin/ui/components/CustomTextField.kt +++ /dev/null @@ -1,61 +0,0 @@ -package ui.components - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.MaterialTheme -import androidx.compose.material.Text -import androidx.compose.material.TextField -import androidx.compose.material.TextFieldDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ui.Styles - -@Composable -fun CustomTextField(value: String, label: String, forPassword: Boolean = true, onValueChange: (String) -> Unit) { - Row( - modifier = Modifier.padding(start = 16.dp, top = 8.dp, end = 8.dp, bottom = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - TextField( - modifier = Modifier.fillMaxWidth(0.4f).height(50.dp) - .border( - BorderStroke(width = 2.dp, color = MaterialTheme.colors.primary), - shape = RoundedCornerShape(topEnd = 10.dp, bottomEnd = 10.dp, topStart = 10.dp, bottomStart = 10.dp) - ), - value = value, - singleLine = true, - visualTransformation = if (forPassword) PasswordVisualTransformation() else VisualTransformation.None, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password), - colors = TextFieldDefaults.textFieldColors( - backgroundColor = Color.Transparent, - focusedIndicatorColor = Color.Transparent, - unfocusedIndicatorColor = Color.Transparent - ), - label = { - Text( - text = label, - style = Styles.TextStyleMedium(16.sp), - fontWeight = FontWeight.Medium - ) - }, - textStyle = Styles.TextStyleMedium(16.sp), - onValueChange = { - onValueChange.invoke(it) - } - ) - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/LoadingDialog.kt b/src/jvmMain/kotlin/ui/components/LoadingDialog.kt deleted file mode 100644 index 15bb87d..0000000 --- a/src/jvmMain/kotlin/ui/components/LoadingDialog.kt +++ /dev/null @@ -1,60 +0,0 @@ -package ui.components - -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.CircularProgressIndicator -import androidx.compose.material.Surface -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.compose.ui.window.Dialog -import ui.Styles - -@Composable -fun LoadingDialog(text: String) { - Surface( - shape = RoundedCornerShape(8.dp), - elevation = 8.dp - ) { - Box( - modifier = Modifier - .padding(4.dp) - .border( - BorderStroke(1.dp, Color.LightGray), - shape = RoundedCornerShape(8.dp) - ) - ) { - Dialog( - title = text, - onCloseRequest = { }, - undecorated = false, - resizable = false, - enabled = false - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = Modifier.padding(16.dp).fillMaxWidth().fillMaxHeight() - ) { - Text( - text = text, - style = Styles.TextStyleBold(20.sp), - modifier = Modifier.padding(start = 8.dp, end = 8.dp, bottom = 8.dp) - ) - CircularProgressIndicator() - } - } - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ui/components/TextWithIcon.kt b/src/jvmMain/kotlin/ui/components/TextWithIcon.kt deleted file mode 100644 index ca3b404..0000000 --- a/src/jvmMain/kotlin/ui/components/TextWithIcon.kt +++ /dev/null @@ -1,36 +0,0 @@ -package ui.components - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.loadSvgPainter -import androidx.compose.ui.res.useResource -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import ui.Styles - -@Composable -fun TextWithIcon(label: String, onIconClick: () -> Unit) { - val density = LocalDensity.current // to calculate the intrinsic size of vector images (SVG, XML) - Row { - Text( - text = label, - style = Styles.TextStyleMedium(16.sp), - color = Color.Black, - fontWeight = FontWeight.Medium - ) - Icon( - painter = useResource("info.svg") { loadSvgPainter(it, density) }, - contentDescription = "Info", - modifier = Modifier.padding(start = 8.dp) - .clickable { onIconClick.invoke() } - ) - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/Constant.kt b/src/jvmMain/kotlin/utils/Constant.kt deleted file mode 100644 index 6cbd0c7..0000000 --- a/src/jvmMain/kotlin/utils/Constant.kt +++ /dev/null @@ -1,25 +0,0 @@ -package utils - -object Constant { - const val SUCCESS = 0 - const val FAILURE = 1 - const val BUNDLE_DOWNLOAD_LINK = "https://github.com/google/bundletool/releases" -} - -object DBConstants { - const val BUNDLETOOL_PATH = "bundletool_path" - const val ADB_PATH = "adb_path" -} - -object FileDialogType { - const val AAB = -1 - const val BUNDLETOOL = 1 - const val AAPT2 = 2 - const val KEY_STORE_PATH = 3 - const val ADB_PATH = 4 -} - -object SigningMode { - const val DEBUG = 1 - const val RELEASE = 2 -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/FileHelper.kt b/src/jvmMain/kotlin/utils/FileHelper.kt deleted file mode 100644 index 7f1b7f4..0000000 --- a/src/jvmMain/kotlin/utils/FileHelper.kt +++ /dev/null @@ -1,31 +0,0 @@ -package utils - -import java.io.File -import java.io.IOException - -object FileHelper { - - fun performFileOperations(directory: String, fileName: String, fileStatus: (Int, String) -> Unit) { - val oldFile = File(directory, "${fileName.split(".")[0]}.apks") - val newFile = File(directory, "${fileName.split(".")[0]}.zip") - if (FileUtils.renameFile(oldFile, newFile)) { - Log.i("CHANGED NAME\n Unzipping...") - try { - FileUtils.unzip(newFile, directory) - fileStatus.invoke( - Constant.SUCCESS, - "Rename and Unzip Successful\nFile Saved at ${directory.removeSuffix("\\")}." - ) - Log.i("TRYING DELETING FILE") - val value = FileUtils.deleteFile(newFile) - Log.i("DELETE STATUS : $value") - } catch (exception: IOException) { - Log.i("Unzipping Failed: ${exception.printStackTrace()}") - fileStatus.invoke(Constant.FAILURE, "Unzipping Failed: ${exception.printStackTrace()}") - } - } else { - Log.i("FAILED RENAMING THE FILE!!") - fileStatus.invoke(Constant.FAILURE, "FAILED RENAMING THE FILE!!") - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/FileUtils.kt b/src/jvmMain/kotlin/utils/FileUtils.kt deleted file mode 100644 index e3fd1b0..0000000 --- a/src/jvmMain/kotlin/utils/FileUtils.kt +++ /dev/null @@ -1,80 +0,0 @@ -package utils - -import java.io.BufferedOutputStream -import java.io.File -import java.io.IOException -import java.io.InputStream -import java.io.FileOutputStream -import java.nio.file.Paths -import java.util.zip.ZipFile - -object FileUtils { - - /** - * Size of the buffer to read/write data - */ - private const val BUFFER_SIZE = 4096 - - fun deleteFile(file: File) = - file.delete() - - /** - * @param oldFile - * @param newFile - */ - fun renameFile(oldFile: File, newFile: File) = - oldFile.renameTo(newFile) - - /** - * @param zipFilePath - * @param destDirectory - * @throws IOException - */ - @Throws(IOException::class) - fun unzip(zipFilePath: File, destDirectory: String, skipFile: String = "toc.pb") { - - File(destDirectory).run { - if (!exists()) { - mkdirs() - } - } - - ZipFile(zipFilePath).use { zip -> - - zip.entries().asSequence().forEach { entry -> - if (entry.name == skipFile) return@forEach - - zip.getInputStream(entry).use { input -> - val safeEntryName = Paths.get(entry.name).normalize().toString() - val filePath = destDirectory + File.separator + safeEntryName - - if (!entry.isDirectory) { - // if the entry is a file, extracts it - extractFile(input, filePath) - } else { - // if the entry is a directory, make the directory - val dir = File(filePath) - dir.mkdir() - } - } - } - } - } - - /** - * Extracts a zip entry (file entry) - * @param inputStream - * @param destFilePath - * @throws IOException - */ - @Throws(IOException::class) - private fun extractFile(inputStream: InputStream, destFilePath: String) { - val bos = BufferedOutputStream(FileOutputStream(destFilePath)) - val bytesIn = ByteArray(BUFFER_SIZE) - var read: Int - while (inputStream.read(bytesIn).also { read = it } != -1) { - bos.write(bytesIn, 0, read) - } - bos.close() - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/Log.kt b/src/jvmMain/kotlin/utils/Log.kt deleted file mode 100644 index 2ab499e..0000000 --- a/src/jvmMain/kotlin/utils/Log.kt +++ /dev/null @@ -1,11 +0,0 @@ -package utils - -object Log { - - var showLogs: Boolean = true - fun i(message: String) { - if (showLogs) { - println(message) - } - } -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/Strings.kt b/src/jvmMain/kotlin/utils/Strings.kt deleted file mode 100644 index 9df72af..0000000 --- a/src/jvmMain/kotlin/utils/Strings.kt +++ /dev/null @@ -1,44 +0,0 @@ -package utils - -object Strings { - const val APP_NAME = "Android Bundletool UI" - const val URL = "URL" - const val CHOOSE_FILE = "Choose a file" - const val SELECT_BUNDLETOOL_JAR = "Select Bundletool Jar" - const val SAVE_JAR_PATH = "Save Jar Path" - const val SAVE_JAR_PATH_INFO = "Saves Path of Bundle Tool Jar for Future." - const val DOWNLOAD_BUNDLETOOL = "Download Bundletool from here" - const val SELECT_AAB_FILE = "Select Aab File" - const val OPTIONS_FOR_BUILD_APKS = "Options for the bundletool build-apks command" - const val OVERWRITE = "Overwrite" - const val OVERWRITE_INFO = "Overwrites any existing output file with the path you specify using the --output option. If you don't include this flag and the output file already exists, you get a build error." - const val AAPT2_PATH = "Aapt2 Path" - const val AAPT2_PATH_INFO = "Specifies a custom path to AAPT2. By default, bundletool includes its own version of AAPT2." - const val SELECT_AAPT2_FILE = "Select Aapt2 File" - const val MODE_UNIVERSAL = "Mode Universal" - const val MODE_UNIVERSAL_INFO = "Sets the mode to universal. Use this option if you want bundletool to build a single APK that includes all of your app's code and resources, so that the APK is compatible with all device configurations your app supports." + - "\nNote: bundletool includes only feature modules that specify in their manifest in a universal APK. To learn more, read about the feature module manifest.\n" + - "Keep in mind, these APKs are larger than those optimized for a particular device configuration. However, they're easier to share with internal testers who, for example, want to test your app on multiple device configurations." - const val SIGNING_MODE = "Signing Mode" - const val DEBUG = "Debug" - const val RELEASE = "Release" - const val KEYSTORE_PATH = "Select Keystore Path" - const val KEYSTORE_PASSWORD = "Keystore Password" - const val KEY_ALIAS = "Key Alias" - const val KEY_PASSWORD = "Key Password" - const val EXECUTE = "Execute" - const val FILE_OPTIONS = "File Options" - const val DEVICE_OPTIONS = "Device Options" - const val AUTO_UNZIP = "Automatically Unzip and Delete Apks File" - const val CLEAR_LOGS = "Clear Logs" - const val LOGS_VIEW = "Logs View" - const val SETUP_ADB = "Set up Adb" - const val ABD_SETUP_DONE = "Adb Connected" - const val SETUP_ADB_INFO = "Setup ADB path to create builds based on connected device" - const val DEVICE_ID = "Device Id" - const val DEVICE_ID_INFO = "Device Id based on the Serial Number. Use with Mode Universal to get one apk." - const val VERIFYING_ADB_PATH = "Verifying ADB Path.." - const val SERIAL_ID = "Serial Id" - const val FETCH_DEVICES = "Fetch Devices" - const val FETCH_DEVICES_INFO = "Fetch Connected Devices Info in LogView" -} \ No newline at end of file diff --git a/src/jvmMain/kotlin/utils/TestTags.kt b/src/jvmMain/kotlin/utils/TestTags.kt deleted file mode 100644 index e9000ee..0000000 --- a/src/jvmMain/kotlin/utils/TestTags.kt +++ /dev/null @@ -1,6 +0,0 @@ -package utils - -object TestTags { - const val CHECKBOX_TAG = "CHECKBOX_TAG" - const val TEXT_TAG = "TEXT_TAG" -} \ No newline at end of file diff --git a/src/jvmMain/resources/clear.svg b/src/jvmMain/resources/clear.svg deleted file mode 100644 index 4f42f39..0000000 --- a/src/jvmMain/resources/clear.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/jvmMain/resources/device_fetch.svg b/src/jvmMain/resources/device_fetch.svg deleted file mode 100644 index 2d8124b..0000000 --- a/src/jvmMain/resources/device_fetch.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/jvmMain/resources/done.svg b/src/jvmMain/resources/done.svg deleted file mode 100644 index 1655d12..0000000 --- a/src/jvmMain/resources/done.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/jvmMain/resources/info.svg b/src/jvmMain/resources/info.svg deleted file mode 100644 index e87bc17..0000000 --- a/src/jvmMain/resources/info.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/src/jvmMain/resources/open_folder.svg b/src/jvmMain/resources/open_folder.svg deleted file mode 100644 index 65b7a9f..0000000 --- a/src/jvmMain/resources/open_folder.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file