groundwork

This commit is contained in:
izzy2lost
2025-11-14 11:20:49 -05:00
parent 0aa8d98778
commit b5355670c1
36 changed files with 1914 additions and 107 deletions
+51
View File
@@ -0,0 +1,51 @@
name: Build Native Bundletool ARM64
on:
workflow_dispatch:
jobs:
build-native:
runs-on: ubuntu-22.04
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install GraalVM (Java 21)
uses: graalvm/setup-graalvm@v1
with:
version: "21.0.2"
java-version: "21"
components: "native-image"
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y unzip zip curl clang build-essential
- name: Download bundletool source
run: |
curl -L -o bundletool.zip https://github.com/google/bundletool/archive/refs/heads/master.zip
unzip bundletool.zip
mv bundletool-master bundletool-src
- name: Build bundletool.jar
working-directory: bundletool-src
run: |
./gradlew build -x test
cp build/libs/bundletool-*.jar ../bundletool.jar
- name: Build Native Image (ARM64)
run: |
native-image \
--no-fallback \
--static \
-jar bundletool.jar \
bundletool
- name: Upload Native Binary
uses: actions/upload-artifact@v4
with:
name: bundletool-native-arm64
path: bundletool
+15 -38
View File
@@ -1,42 +1,19 @@
.gradle
# Dependencies
node_modules/
# Build outputs
dist/
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
*.log
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
# IDE
.vscode/
.idea/
### Mac OS ###
.DS_Store
# OS
.DS_Store
Thumbs.db
### Markdown files ###
*.md
!README.md
+42
View File
@@ -0,0 +1,42 @@
plugins {
id("com.android.application")
kotlin("android")
}
android {
namespace = "com.aab2apk"
compileSdk = 36
defaultConfig {
applicationId = "com.aab2apk"
minSdk = 23
targetSdk = 36
versionCode = 1
versionName = "1.0"
ndkVersion = "28.2.13676358"
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
}
dependencies {
implementation("androidx.core:core-ktx:1.10.1")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.compose.material3:material3:1.1.1")
}
+1
View File
@@ -0,0 +1 @@
# proguard rules
+10
View File
@@ -0,0 +1,10 @@
<manifest package="com.aab2apk" xmlns:android="http://schemas.android.com/apk/res/android">
<application android:label="aab2apk" android:allowBackup="true" android:icon="@mipmap/ic_launcher">
<activity android:name=".MainActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
+12
View File
@@ -0,0 +1,12 @@
This is a placeholder asset named 'bundletool'.
To enable on-device AAB -> APK conversion you must replace this file with a native bundletool executable
built for Android (ARM64 or ARMv7) using GraalVM native-image or a prebuilt binary.
Recommended approach:
1. Build bundletool native image with GraalVM (supports linux/arm64 target) or obtain a prebuilt binary.
2. Rename the binary to 'bundletool' and place it here: app/src/main/assets/bundletool
3. Rebuild the app. On first run the app will copy the asset to its internal files directory and mark it executable.
4. The UI will then be able to run bundletool to convert AAB -> APKS.
If you want, I can help produce a GraalVM build script for bundletool or attempt to include a tested ARM64 binary.
+2
View File
@@ -0,0 +1,2 @@
# Placeholder bundletool binary
# REPLACE THIS FILE with a native ARM64 bundletool executable built with GraalVM.
@@ -0,0 +1,103 @@
package com.aab2apk
import android.content.Context
import android.net.Uri
import android.util.Log
import android.widget.Toast
import androidx.documentfile.provider.DocumentFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.InputStream
import java.io.OutputStream
object AabToApkManager {
private const val TAG = "AabToApkManager"
/**
* Attempt to convert an AAB to APK(s).
*
* This is a best-effort adapter: real conversion normally requires bundletool (Java jar) or
* server-side conversion. On-device conversion is not generally supported unless you include
* an executable bundletool binary compiled for Android and place it in the app's files dir.
*
* This method will:
* - copy the selected AAB to the app's internal files directory
* - check for a self-contained 'bundletool' executable in filesDir and try to run it (if present)
* - otherwise, show a Toast explaining how to perform conversion externally (or how to provide bundletool)
*/
suspend fun convertAab(
context: Context,
aabUri: Uri,
outputDirUri: Uri?
): Result<String> = withContext(Dispatchers.IO) {
try {
// Copy AAB to internal storage
val aabFile = File(context.filesDir, "input.aab")
context.contentResolver.openInputStream(aabUri).use { input ->
if (input == null) return@withContext Result.failure(Exception("Cannot open AAB input"))
aabFile.outputStream().use { output ->
input.copyTo(output)
}
}
// Ensure a bundletool asset (if packaged) is installed to filesDir and set executable
val bundletoolFile = File(context.filesDir, "bundletool")
try {
if (!bundletoolFile.exists()) {
// Attempt to copy bundled asset (if present) into filesDir
context.assets.open("bundletool").use { input ->
bundletoolFile.outputStream().use { output -> input.copyTo(output) }
}
bundletoolFile.setExecutable(true)
}
} catch (e: Exception) {
// If asset not present or copy failed, we'll proceed to check for an existing executable.
}
if (bundletoolFile.exists() && bundletoolFile.canExecute()) {
// Build output path
val outFile = File(context.filesDir, "output.apks")
val cmd = arrayListOf(bundletoolFile.absolutePath, "build-apks", "--bundle=${aabFile.absolutePath}", "--output=${outFile.absolutePath}", "--mode=universal")
Log.i(TAG, "Running: ${cmd.joinToString(" ")}")
val procBuilder = ProcessBuilder(cmd)
procBuilder.redirectErrorStream(true)
val proc = procBuilder.start()
val stdout = StringBuilder()
proc.inputStream.bufferedReader().useLines { lines ->
lines.forEach { stdout.append(it).append("\n") }
}
val exit = proc.waitFor()
if (exit == 0) {
// If outputDirUri provided, copy the result there
if (outputDirUri != null) {
val doc = DocumentFile.fromTreeUri(context, outputDirUri)
if (doc != null && doc.canWrite()) {
val outDoc = doc.createFile("application/octet-stream", "output.apks")
if (outDoc != null) {
context.contentResolver.openOutputStream(outDoc.uri).use { os ->
outFile.inputStream().use { fis -> fis.copyTo(os!!) }
}
return@withContext Result.success("Conversion complete — output.apks saved to destination folder.")
}
}
}
return@withContext Result.success("Conversion complete. Output at ${outFile.absolutePath}")
} else {
return@withContext Result.failure(Exception("bundletool failed with exit $exit\n${stdout}"))
}
} else {
// No bundletool available — inform user what to do
val msg = """bundletool executable not found in app files.
To convert an AAB to APK on-device you must provide a bundletool binary built for Android and place it at: ${context.filesDir}/bundletool
Alternatively, convert the AAB on a desktop using bundletool.jar:
java -jar bundletool-all.jar build-apks --bundle=app.aab --output=app.apks --mode=universal
Then transfer resulting .apks/.apks.zip to the phone."""
Log.i(TAG, msg)
return@withContext Result.failure(Exception(msg))
}
} catch (e: Exception) {
Log.e(TAG, "Conversion failed", e)
return@withContext Result.failure(e)
}
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,79 @@
package com.aab2apk
import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class MainActivity: ComponentActivity() {
private var pickedAab: Uri? = null
private var pickedOutputFolder: Uri? = null
private val pickAabLauncher = registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri: Uri? ->
if (uri != null) {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
pickedAab = uri
Toast.makeText(this, "AAB selected", Toast.LENGTH_SHORT).show()
}
}
private val pickFolderLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri: Uri? ->
if (uri != null) {
contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
pickedOutputFolder = uri
Toast.makeText(this, "Output folder selected", Toast.LENGTH_SHORT).show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier.fillMaxSize()) {
Column(Modifier.padding(16.dp)) {
Text(text = "aab2apk", style = MaterialTheme.typography.headlineSmall)
Button(onClick = { pickAabLauncher.launch(arrayOf("application/octet-stream","*/*")) }, modifier = Modifier.padding(top = 12.dp)) {
Text("Pick .aab file")
}
Button(onClick = { pickFolderLauncher.launch(null) }, modifier = Modifier.padding(top = 12.dp)) {
Text("Select output folder")
}
Button(onClick = {
if (pickedAab == null) {
Toast.makeText(this, "Please pick an AAB first", Toast.LENGTH_SHORT).show()
return@Button
}
CoroutineScope(Dispatchers.Main).launch {
Toast.makeText(this@MainActivity, "Starting conversion (best-effort)...", Toast.LENGTH_SHORT).show()
val result = AabToApkManager.convertAab(this@MainActivity, pickedAab!!, pickedOutputFolder)
if (result.isSuccess) {
Toast.makeText(this@MainActivity, "Success: ${'$'}{result.getOrNull()}", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(this@MainActivity, "Failed: ${'$'}{result.exceptionOrNull()?.message}", Toast.LENGTH_LONG).show()
}
}
}, modifier = Modifier.padding(top = 12.dp)) {
Text("Convert AAB to APK (best-effort)")
}
}
}
}
}
}
@@ -0,0 +1,119 @@
package com.aab2apk.original
import utils.Log
import utils.SigningMode
import utils.Utils
class CommandBuilder {
private var bundletoolPath: String = ""
private var aabFilePath: Pair<String, String> = 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<String, String>) = 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<String, Boolean> {
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()
}
}
@@ -0,0 +1,55 @@
package com.aab2apk.original
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.BufferedReader
import java.io.InputStreamReader
class CommandExecutor {
fun executeCommand(
cmd: String,
coroutineScope: CoroutineScope,
onSuccess: (String) -> Unit,
onFailure: (Throwable) -> Unit
) {
coroutineScope.launch(Dispatchers.IO) {
try {
val runtime = Runtime.getRuntime()
val startTime = System.currentTimeMillis()
val process = runtime.exec(cmd)
val outputReader = BufferedReader(InputStreamReader(process.inputStream))
val errorReader = BufferedReader(InputStreamReader(process.errorStream))
var output = ""
var errorOutput = ""
// Read command output
var line: String?
while (outputReader.readLine().also { line = it } != null) {
output += line + "\n"
}
// Read error output
while (errorReader.readLine().also { line = it } != null) {
errorOutput += line + "\n"
}
process.waitFor()
val endTime = System.currentTimeMillis()
if (process.exitValue() == 0) {
val executionTime = ((endTime - startTime) / 1000).toString() + "s"
onSuccess("$cmd\n$output\nCommand Executed in $executionTime")
} else {
val exception = Exception("Command execution failed: $cmd$errorOutput")
onFailure(exception)
}
} catch (e: Exception) {
onFailure(e)
}
}
}
}
@@ -0,0 +1,76 @@
package com.aab2apk.original
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 <E> 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
}
}
}
@@ -0,0 +1,9 @@
package com.aab2apk.original
class KiteDbException : RuntimeException {
constructor(detailMessage: String?) : super(detailMessage)
constructor(detailMessage: String?, throwable: Throwable?) : super(detailMessage, throwable)
}
@@ -0,0 +1,12 @@
package com.aab2apk.original
class KiteTable<T> {
internal constructor()
internal constructor(content: T) {
mContent = content
}
// Serialized content
var mContent: T? = null
}
@@ -0,0 +1,37 @@
package com.aab2apk.original
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
)
}
@@ -0,0 +1,85 @@
package com.aab2apk.original
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
)
)
@@ -0,0 +1,85 @@
package com.aab2apk.original
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)
)
}
}
}
}
@@ -0,0 +1,78 @@
package com.aab2apk.original
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)
)
}
}
}
}
@@ -0,0 +1,75 @@
package com.aab2apk.original
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 = ""
)
}
}
}

Some files were not shown because too many files have changed in this diff Show More