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