From e6b7e914857472fd9a80c9fed3a27d6df01bf348 Mon Sep 17 00:00:00 2001 From: fatalcoder524 <11532648+fatalcoder524@users.noreply.github.com> Date: Sun, 12 Jul 2026 22:19:06 +0200 Subject: [PATCH] Modernize UI/UX with Material 3 refresh - New colored ActionTile components (icon badges + haptics) across main, slot, reboot, backups and flash screens; card relief + bolder ACTIVE badge - Light/dark theme toggle in the top bar (dark = AMOLED black), theme-aware system bars, themed Material 3 snackbars replacing Toasts - NavHost slide/fade transitions + predictive back; fix system back so it pops sub-pages and only confirms exit on the main screen - Flash confirmation dialog now shows the destination slot (A/B, active state) - Active slot shows full kernel uname directly (removed the check button) - Remove the Updates feature and the on-launch APK self-updater - Save dmesg/logcat/ramoops no longer trigger a full refresh - Fix CRLF-corrupted flash_ak3 scripts and add .gitattributes to keep LF - Re-enable R8 minify + resource shrinking (release ~5.9 MB), signing config --- .gitattributes | 12 ++ .gitignore | 5 +- app/build.gradle.kts | 32 +++- app/proguard-rules.pro | 12 +- app/src/main/AndroidManifest.xml | 1 + .../capntrips/kernelflasher/MainActivity.kt | 96 ++++++----- .../ui/components/ActionButton.kt | 109 +++++++++++++ .../kernelflasher/ui/components/ActionTile.kt | 110 +++++++++++++ .../kernelflasher/ui/components/Card.kt | 15 +- .../kernelflasher/ui/components/DataCard.kt | 63 ++++++-- .../ui/components/DialogButton.kt | 25 +-- .../ui/components/FlashButton.kt | 24 ++- .../kernelflasher/ui/components/FlashList.kt | 40 +++-- .../kernelflasher/ui/components/SlotCard.kt | 7 +- .../kernelflasher/ui/components/ViewButton.kt | 32 ++-- .../ui/screens/RefreshableScreen.kt | 153 +++++++++++------- .../ui/screens/backups/BackupsContent.kt | 31 ++-- .../ui/screens/backups/SlotBackupsContent.kt | 123 ++++++-------- .../ui/screens/error/ErrorScreen.kt | 29 ++-- .../ui/screens/main/MainContent.kt | 84 ++++------ .../ui/screens/main/MainViewModel.kt | 33 +++- .../ui/screens/reboot/RebootContent.kt | 78 ++++----- .../ui/screens/slot/SlotContent.kt | 118 ++++++-------- .../ui/screens/slot/SlotFlashContent.kt | 147 ++++++++--------- .../ui/screens/slot/SlotViewModel.kt | 16 +- .../ui/screens/updates/UpdatesAddContent.kt | 51 ------ .../updates/UpdatesChangelogContent.kt | 40 ----- .../ui/screens/updates/UpdatesContent.kt | 86 ---------- .../ui/screens/updates/UpdatesViewContent.kt | 95 ----------- .../capntrips/kernelflasher/ui/theme/Color.kt | 63 ++++++++ .../capntrips/kernelflasher/ui/theme/Theme.kt | 134 ++++++++++++++- app/src/main/res/values-it/strings.xml | 1 - app/src/main/res/values-ja/strings.xml | 1 - app/src/main/res/values-pl/strings.xml | 1 - app/src/main/res/values-pt-rBR/strings.xml | 143 ++++++++-------- app/src/main/res/values-ru/strings.xml | 1 - app/src/main/res/values-zh-rCN/strings.xml | 1 - app/src/main/res/values-zh-rTW/strings.xml | 1 - app/src/main/res/values/strings.xml | 2 +- gradle/libs.versions.toml | 1 + 40 files changed, 1110 insertions(+), 906 deletions(-) create mode 100644 .gitattributes create mode 100644 app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionButton.kt create mode 100644 app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionTile.kt delete mode 100644 app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt delete mode 100644 app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt delete mode 100644 app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt delete mode 100644 app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..2d0d0b15 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# This repo is worked on from Windows where core.autocrlf=true. Shell scripts in +# assets/ are executed on-device by Android's /system/bin/sh, which cannot tolerate +# CRLF line endings (a trailing \r breaks every line). Force LF for all shell scripts +# and treat the prebuilt ELF binaries as binary so they are never line-ending converted. + +*.sh text eol=lf + +app/src/main/assets/flash_ak3.sh text eol=lf +app/src/main/assets/flash_ak3_mkbootfs.sh text eol=lf + +app/src/main/assets/ksuinit binary +app/src/main/assets/mkbootfs binary diff --git a/.gitignore b/.gitignore index 010b2790..3852cd10 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,7 @@ local.properties .env .github/workflows/build_local.yml KernelFlasher.apk -/app/build/* \ No newline at end of file +/app/build/* +# Release signing +key.properties +keystore/ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7c8c2aa3..b90906ae 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,5 @@ +import java.util.Properties + plugins { alias(libs.plugins.android.application) alias(libs.plugins.devtools.ksp) @@ -6,6 +8,13 @@ plugins { alias(libs.plugins.kotlin.compose.compiler) } +val keystoreProperties = Properties().apply { + val file = rootProject.file("key.properties") + if (file.exists()) { + file.inputStream().use { load(it) } + } +} + android { compileSdk = 36 namespace = "com.github.capntrips.kernelflasher" @@ -36,13 +45,31 @@ android { } } + signingConfigs { + if (keystoreProperties.isNotEmpty()) { + create("release") { + storeFile = rootProject.file(keystoreProperties.getProperty("storeFile")) + storePassword = keystoreProperties.getProperty("storePassword") + keyAlias = keystoreProperties.getProperty("keyAlias") + keyPassword = keystoreProperties.getProperty("keyPassword") + } + } + } + buildTypes { release { - isMinifyEnabled = false - isShrinkResources = false + // R8 minify + resource shrinking are ON to keep the APK small (mainly to + // tree-shake material-icons-extended). Keep rules in proguard-rules.pro cover + // AIDL/serialization/libsu/retrofit/gson, and -dontobfuscate protects Room and + // reflection-by-name. Verify core flows on-device after changing keep rules. + isMinifyEnabled = true + isShrinkResources = true proguardFiles( getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) + if (keystoreProperties.isNotEmpty()) { + signingConfig = signingConfigs.getByName("release") + } } } @@ -95,6 +122,7 @@ android { implementation(libs.androidx.appcompat) implementation(libs.androidx.compose.material) implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.foundation) implementation(libs.androidx.compose.ui) implementation(libs.androidx.core.ktx) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 153e51df..bbf6235a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -52,4 +52,14 @@ -keepclassmembers class com.github.capntrips.kernelflasher.AppUpdater { *; } # Keep VectorDrawableCompat to avoid crashes or inflation errors --keep class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat { *; } \ No newline at end of file +-keep class androidx.vectordrawable.graphics.drawable.VectorDrawableCompat { *; } +# ============ KOTLINX.SERIALIZATION (app models: backups, partitions, updates) ============ +-keepattributes InnerClasses +-keep,includedescriptorclasses class com.github.capntrips.kernelflasher.**$$serializer { *; } +-keepclassmembers class com.github.capntrips.kernelflasher.** { + *** Companion; +} +-keepclasseswithmembers class com.github.capntrips.kernelflasher.** { + kotlinx.serialization.KSerializer serializer(...); +} +-keep class com.github.capntrips.kernelflasher.common.types.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1a391990..0f7d9fe3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ - viewModel!!.showUpdateDialog(title, lines, confirm) - } - } + // Self-update check disabled: this is a customised fork, and the upstream + // "Update APK" dialog would offer to replace this build with the stock one. val uri = viewModel?.pendingFlashUri @@ -334,11 +337,14 @@ class MainActivity : ComponentActivity() { val slotViewModelA = mainViewModel.slotA val slotViewModelB = mainViewModel.slotB val backupsViewModel = mainViewModel.backups - val updatesViewModel = mainViewModel.updates val rebootViewModel = mainViewModel.reboot - BackHandler(enabled = !mainViewModel.isRefreshing, onBack = {}) - // New back handler for exit - BackHandler(enabled = true) { + val navBackStackEntry by navController.currentBackStackEntryAsState() + val isMainScreen = navBackStackEntry?.destination?.route == "main" + // Block back entirely while a root operation is in progress. + BackHandler(enabled = mainViewModel.isRefreshing) {} + // Only confirm-exit on the main screen. On any sub-page this handler is + // disabled, so back falls through to the NavHost and pops to the previous page. + BackHandler(enabled = isMainScreen && !mainViewModel.isRefreshing) { showExitDialog = true } val slotContentA: @Composable AnimatedVisibilityScope.(NavBackStackEntry) -> Unit = { backStackEntry -> @@ -464,7 +470,26 @@ class MainActivity : ComponentActivity() { } } - NavHost(navController = navController, startDestination = "main") { + NavHost( + navController = navController, + startDestination = "main", + enterTransition = { + slideInHorizontally(initialOffsetX = { it / 4 }, animationSpec = tween(300)) + + fadeIn(tween(300)) + }, + exitTransition = { + slideOutHorizontally(targetOffsetX = { -it / 4 }, animationSpec = tween(300)) + + fadeOut(tween(300)) + }, + popEnterTransition = { + slideInHorizontally(initialOffsetX = { -it / 4 }, animationSpec = tween(300)) + + fadeIn(tween(300)) + }, + popExitTransition = { + slideOutHorizontally(targetOffsetX = { it / 4 }, animationSpec = tween(300)) + + fadeOut(tween(300)) + } + ) { composable("main") { RefreshableScreen(mainViewModel, navController, swipeEnabled = true) { MainContent(mainViewModel, navController) @@ -524,38 +549,6 @@ class MainActivity : ComponentActivity() { } } } - composable("updates") { - updatesViewModel.clearCurrent() - RefreshableScreen(mainViewModel, navController) { - UpdatesContent(updatesViewModel, navController) - } - } - composable("updates/add") { - RefreshableScreen(mainViewModel, navController) { - UpdatesAddContent(updatesViewModel, navController) - } - } - composable("updates/view/{updateId}") { backStackEntry -> - val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt() - val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId } - updatesViewModel.currentUpdate = currentUpdate - if (updatesViewModel.currentUpdate != null) { - // TODO: enable swipe refresh - RefreshableScreen(mainViewModel, navController) { - UpdatesViewContent(updatesViewModel, navController) - } - } - } - composable("updates/view/{updateId}/changelog") { backStackEntry -> - val updateId = backStackEntry.arguments?.getString("updateId")!!.toInt() - val currentUpdate = updatesViewModel.updates.firstOrNull { it.id == updateId } - updatesViewModel.currentUpdate = currentUpdate - if (updatesViewModel.currentUpdate != null) { - RefreshableScreen(mainViewModel, navController) { - UpdatesChangelogContent(updatesViewModel, navController) - } - } - } composable("reboot") { RefreshableScreen(mainViewModel, navController) { RebootContent(rebootViewModel, navController) @@ -573,6 +566,7 @@ class MainActivity : ComponentActivity() { if (dialogData != null) { AlertDialog( onDismissRequest = { viewModel!!.hideUpdateDialog() }, + icon = { Icon(Icons.Outlined.SystemUpdateAlt, contentDescription = null) }, title = { Text( dialogData.title, @@ -605,6 +599,7 @@ class MainActivity : ComponentActivity() { if (showExitDialog) { AlertDialog( onDismissRequest = { showExitDialog = false }, + icon = { Icon(Icons.AutoMirrored.Outlined.ExitToApp, contentDescription = null) }, title = { Text("Exit App") }, text = { Text("Are you sure you want to exit?") }, confirmButton = { @@ -628,6 +623,7 @@ class MainActivity : ComponentActivity() { if (viewModel?.showSlotIntentDialog?.value == true) { AlertDialog( onDismissRequest = { viewModel?.showSlotIntentDialog?.value = false }, + icon = { Icon(Icons.Outlined.Layers, contentDescription = null) }, title = { Text("Select Slot to Flash") }, text = { Text("Choose the slot where the zip should be flashed.") }, confirmButton = { diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionButton.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionButton.kt new file mode 100644 index 00000000..1f36a8ee --- /dev/null +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionButton.kt @@ -0,0 +1,109 @@ +package com.github.capntrips.kernelflasher.ui.components + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Checkbox +import androidx.compose.material3.FilledTonalButton +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import android.view.HapticFeedbackConstants + +/** + * Full-width tonal action button with an optional leading icon. + * Replaces the legacy stacks of sharp-cornered OutlinedButtons. + */ +@Composable +fun ActionButton( + text: String, + icon: ImageVector? = null, + modifier: Modifier = Modifier, + enabled: Boolean = true, + destructive: Boolean = false, + onClick: () -> Unit +) { + val colors = if (destructive) { + ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer + ) + } else { + ButtonDefaults.filledTonalButtonColors() + } + val view = LocalView.current + FilledTonalButton( + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .heightIn(min = 52.dp), + shape = RoundedCornerShape(16.dp), + colors = colors, + enabled = enabled, + onClick = { + view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + onClick() + } + ) { + if (icon != null) { + Icon(icon, contentDescription = null, modifier = Modifier.size(20.dp)) + Spacer(Modifier.width(10.dp)) + } + Text(text, style = MaterialTheme.typography.titleSmall) + } +} + +/** + * Selectable partition row with a proper checkbox, used for backup/restore + * partition pickers. + */ +@Composable +fun PartitionToggle( + name: String, + checked: Boolean, + enabled: Boolean = true, + onToggle: (Boolean) -> Unit +) { + val view = LocalView.current + Surface( + shape = RoundedCornerShape(16.dp), + color = if (checked) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + ) { + Row( + modifier = Modifier + .toggleable( + value = checked, + enabled = enabled, + role = Role.Checkbox, + onValueChange = { + view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + onToggle(it) + } + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox(checked, onCheckedChange = null, enabled = enabled) + Spacer(Modifier.width(10.dp)) + Text(name, style = MaterialTheme.typography.titleSmall) + } + } +} diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionTile.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionTile.kt new file mode 100644 index 00000000..0d867f8d --- /dev/null +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ActionTile.kt @@ -0,0 +1,110 @@ +package com.github.capntrips.kernelflasher.ui.components + +import android.view.HapticFeedbackConstants +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.KeyboardArrowRight +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.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.dp + +/** + * A small, fixed accent palette for the colored icon badges. These are intentionally + * hard-coded vivid tones so each action keeps a recognisable colour across light, dark + * and AMOLED-black themes (dynamic-colour surfaces would wash them out). + */ +object TileColors { + val Blue = Color(0xFF3B82F6) + val Green = Color(0xFF10B981) + val Amber = Color(0xFFF59E0B) + val Violet = Color(0xFF8B5CF6) + val Rose = Color(0xFFF43F5E) + val Cyan = Color(0xFF06B6D4) +} + +/** + * Modern list-item style action row: a rounded card holding a colored circular icon + * badge, a label, and (for navigation actions) a trailing chevron. Replaces the flat + * tonal [ActionButton] where a richer, more tactile look is wanted. + */ +@Composable +fun ActionTile( + text: String, + icon: ImageVector, + accent: Color, + modifier: Modifier = Modifier, + enabled: Boolean = true, + showChevron: Boolean = false, + onClick: () -> Unit +) { + val view = androidx.compose.ui.platform.LocalView.current + Surface( + onClick = { + view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + onClick() + }, + enabled = enabled, + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shadowElevation = 3.dp, + border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + modifier = modifier + .fillMaxWidth() + .padding(vertical = 4.dp) + .heightIn(min = 64.dp) + .alpha(if (enabled) 1f else 0.4f) + ) { + Row( + modifier = Modifier.padding(horizontal = 12.dp, vertical = 10.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Box( + modifier = Modifier + .size(44.dp) + .clip(CircleShape) + .background(accent.copy(alpha = 0.16f)), + contentAlignment = Alignment.Center + ) { + Icon( + icon, + contentDescription = null, + tint = accent, + modifier = Modifier.size(24.dp) + ) + } + Spacer(Modifier.width(16.dp)) + Text( + text = text, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f) + ) + if (showChevron) { + Icon( + Icons.AutoMirrored.Outlined.KeyboardArrowRight, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } +} diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt index e4a309cf..fd9aadcd 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/Card.kt @@ -15,15 +15,16 @@ import androidx.compose.ui.graphics.Shape import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -// TODO: Remove when card is supported in material3: https://m3.material.io/components/cards/implementation/android @Composable fun Card( - shape: Shape = RoundedCornerShape(4.dp), - backgroundColor: Color = MaterialTheme.colorScheme.surface, + shape: Shape = RoundedCornerShape(24.dp), + backgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, contentColor: Color = MaterialTheme.colorScheme.onSurface, - border: BorderStroke? = null, - tonalElevation: Dp = 2.dp, - shadowElevation: Dp = 1.dp, + // A hairline border gives the card an edge on AMOLED-black (where shadows are + // invisible); the shadow provides the relief in light/regular-dark themes. + border: BorderStroke? = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)), + tonalElevation: Dp = 0.dp, + shadowElevation: Dp = 4.dp, content: @Composable ColumnScope.() -> Unit ) { Surface( @@ -37,7 +38,7 @@ fun Card( Column( modifier = Modifier .fillMaxWidth() - .padding(18.dp, (13.788).dp, 18.dp, 18.dp), + .padding(20.dp, 16.dp, 20.dp, 20.dp), content = content ) } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt index 40ba89f3..a4570218 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DataCard.kt @@ -1,45 +1,88 @@ package com.github.capntrips.kernelflasher.ui.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.ColumnScope 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.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp @Composable fun DataCard( title: String, + badge: String? = null, button: @Composable (() -> Unit)? = null, content: @Composable (ColumnScope.() -> Unit)? = null ) { Card { Row( - modifier = Modifier - .fillMaxWidth() - .padding(0.dp), + modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Text( - modifier = Modifier.padding(0.dp, 9.dp, 8.dp, 9.dp).weight(1.0f), - text = title, - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.titleLarge - ) + Row( + modifier = Modifier + .padding(0.dp, 8.dp, 8.dp, 8.dp) + .weight(1.0f), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = title, + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold + ) + if (badge != null) { + Spacer(Modifier.width(10.dp)) + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + shadowElevation = 2.dp + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 8.dp, top = 5.dp, end = 12.dp, bottom = 5.dp) + ) { + Box( + Modifier + .size(7.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.onPrimary) + ) + Spacer(Modifier.width(6.dp)) + Text( + text = badge, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + letterSpacing = 0.8.sp + ) + } + } + } + } if (button != null) { button() } } if (content != null) { - Spacer(Modifier.height(10.dp)) + Spacer(Modifier.height(8.dp)) content() } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt index c2aecda6..98e13b20 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/DialogButton.kt @@ -1,34 +1,15 @@ package com.github.capntrips.kernelflasher.ui.components -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Text import androidx.compose.material3.TextButton -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.dp @Composable fun DialogButton( - buttonText: String, + buttonText: String, onClick: () -> Unit ) { - TextButton( - modifier = Modifier.padding(0.dp), - shape = RoundedCornerShape(4.0.dp), - contentPadding = PaddingValues( - horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, - vertical = ButtonDefaults.ContentPadding.calculateTopPadding() - ), - onClick = onClick - ) { - Text(buttonText, - maxLines = 1, - color = MaterialTheme.colorScheme.primary - ) + TextButton(onClick = onClick) { + Text(buttonText, maxLines = 1) } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt index de4b4384..466ddd69 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashButton.kt @@ -6,19 +6,17 @@ import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.UploadFile import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.ExperimentalUnitApi -import androidx.compose.ui.unit.dp import com.github.capntrips.kernelflasher.MainActivity import kotlinx.serialization.ExperimentalSerializationApi @@ -31,6 +29,8 @@ import kotlinx.serialization.ExperimentalSerializationApi fun FlashButton( buttonText: String, validExtension: String, + icon: ImageVector = Icons.Outlined.UploadFile, + accent: Color = TileColors.Blue, callback: (uri: Uri) -> Unit ) { val mainActivity = LocalContext.current as MainActivity @@ -41,17 +41,15 @@ fun FlashButton( mainActivity.isAwaitingResult = false } } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = buttonText, + icon = icon, + accent = accent, onClick = { mainActivity.isAwaitingResult = true launcher.launch("*/*") } - ) { - Text(buttonText) - } + ) result.value?.let {uri -> if (mainActivity.isAwaitingResult) { val contentResolver = mainActivity.contentResolver diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt index 00b8c055..4815c66e 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/FlashList.kt @@ -10,8 +10,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -59,24 +63,34 @@ fun ColumnScope.FlashList( listState.animateScrollToItem(output.size) } DataCard (cardTitle) - Spacer(Modifier.height(4.dp)) - LazyColumn( - Modifier + Spacer(Modifier.height(12.dp)) + Surface( + modifier = Modifier .weight(1.0f) - .fillMaxSize() - .scrollbar(listState), - listState + .fillMaxSize(), + shape = RoundedCornerShape(20.dp), + color = MaterialTheme.colorScheme.surfaceContainerLow, + contentColor = MaterialTheme.colorScheme.onSurfaceVariant ) { - items(output) { message -> - Text(message, - style = LocalTextStyle.current.copy( - fontFamily = FontFamily.Monospace, - fontSize = TextUnit(12.0f, TextUnitType.Sp), - lineHeight = TextUnit(18.0f, TextUnitType.Sp) + LazyColumn( + Modifier + .fillMaxSize() + .scrollbar(listState) + .padding(horizontal = 14.dp, vertical = 10.dp), + listState + ) { + items(output) { message -> + Text(message, + style = LocalTextStyle.current.copy( + fontFamily = FontFamily.Monospace, + fontSize = TextUnit(12.0f, TextUnitType.Sp), + lineHeight = TextUnit(18.0f, TextUnitType.Sp) + ) ) - ) + } } } + Spacer(Modifier.height(8.dp)) content() } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt index d0f74bb4..1132e2af 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/SlotCard.kt @@ -25,7 +25,8 @@ fun SlotCard( showDlkm: Boolean = true, ) { DataCard ( - title = "$title ${if(viewModel.isActive && viewModel.slotSuffix !="") "[${stringResource(R.string.active)}]" else ""}", + title = title, + badge = if (viewModel.isActive && viewModel.slotSuffix != "") stringResource(R.string.active).uppercase() else null, button = { if (!isSlotScreen) { AnimatedVisibility(!viewModel.isRefreshing.value) { @@ -85,13 +86,13 @@ fun SlotCard( label = stringResource(R.string.unbootable), value = viewModel.slotInfo.bootSlotInfo.unbootable ?: stringResource(R.string.not_found), mutableMaxWidth = cardWidth, - valueColor = if (viewModel.slotInfo.bootSlotInfo.unbootable == "Yes") Color.Red else Color.Unspecified + valueColor = if (viewModel.slotInfo.bootSlotInfo.unbootable == "Yes") MaterialTheme.colorScheme.error else Color.Unspecified ) DataRow( label = stringResource(R.string.successful), value = viewModel.slotInfo.bootSlotInfo.successful ?: stringResource(R.string.not_found), mutableMaxWidth = cardWidth, - valueColor = if (viewModel.slotInfo.bootSlotInfo.successful == "No") Color.Red else Color.Unspecified + valueColor = if (viewModel.slotInfo.bootSlotInfo.successful == "No") MaterialTheme.colorScheme.error else Color.Unspecified ) } if (!viewModel.isRefreshing.value && viewModel.hasError) { diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt index e3119519..1186133f 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/components/ViewButton.kt @@ -1,31 +1,35 @@ package com.github.capntrips.kernelflasher.ui.components import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ButtonDefaults +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import android.view.HapticFeedbackConstants import com.github.capntrips.kernelflasher.R @Composable fun ViewButton( onClick: () -> Unit ) { - TextButton( - modifier = Modifier.padding(0.dp), - shape = RoundedCornerShape(4.0.dp), - contentPadding = PaddingValues( - horizontal = ButtonDefaults.ContentPadding.calculateLeftPadding(LayoutDirection.Ltr) - (6.667).dp, - vertical = ButtonDefaults.ContentPadding.calculateTopPadding() - ), - onClick = onClick + val view = LocalView.current + FilledTonalButton( + modifier = Modifier.heightIn(min = 36.dp), + contentPadding = PaddingValues(horizontal = 16.dp, vertical = 6.dp), + onClick = { + view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK) + onClick() + } ) { - Text(stringResource(R.string.view), maxLines = 1) + Text( + stringResource(R.string.view), + maxLines = 1, + style = MaterialTheme.typography.labelLarge + ) } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt index 2d9af1d0..e162c75a 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/RefreshableScreen.kt @@ -7,37 +7,46 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.navigationBars -import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.statusBars +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack -import androidx.compose.material.pullrefresh.PullRefreshIndicator -import androidx.compose.material.pullrefresh.pullRefresh -import androidx.compose.material.pullrefresh.rememberPullRefreshState +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.outlined.DarkMode +import androidx.compose.material.icons.outlined.LightMode +import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Scaffold +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.ui.screens.main.MainViewModel +import com.github.capntrips.kernelflasher.ui.theme.ThemePrefs import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalMaterialApi @@ -50,55 +59,74 @@ fun RefreshableScreen( swipeEnabled: Boolean = false, content: @Composable ColumnScope.() -> Unit ) { - val statusBar = WindowInsets.statusBars.only(WindowInsetsSides.Top).asPaddingValues() val navigationBars = WindowInsets.navigationBars.asPaddingValues() val context = LocalContext.current - val state = rememberPullRefreshState(viewModel.isRefreshing, onRefresh = { - viewModel.refresh(context) - }) + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior() + val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(viewModel.snackbarMessage) { + viewModel.snackbarMessage?.let { message -> + snackbarHostState.showSnackbar(message) + viewModel.clearSnackbar() + } + } Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + snackbarHost = { + SnackbarHost(snackbarHostState) { data -> + Snackbar( + snackbarData = data, + modifier = Modifier.padding(12.dp), + shape = RoundedCornerShape(16.dp), + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + contentColor = MaterialTheme.colorScheme.onSurface + ) + } + }, topBar = { - Box( - Modifier - .fillMaxWidth() - .padding(statusBar)) { - if (navController.previousBackStackEntry != null) { - AnimatedVisibility( - !viewModel.isRefreshing, - enter = fadeIn(), - exit = fadeOut() - ) { - IconButton( - onClick = { navController.popBackStack() }, - modifier = Modifier.padding(16.dp, 8.dp, 0.dp, 8.dp) - ) { - Icon( - Icons.Filled.ArrowBack, - contentDescription = stringResource(R.string.back), - tint = MaterialTheme.colorScheme.onSurface - ) - } - } - } - Box( - Modifier - .fillMaxWidth() - .padding(16.dp)) { + CenterAlignedTopAppBar( + scrollBehavior = scrollBehavior, + title = { Text( - modifier = Modifier.align(Alignment.Center), text = stringResource(R.string.app_name), - style = MaterialTheme.typography.headlineSmall + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.SemiBold ) - } - } + }, + navigationIcon = { + if (navController.previousBackStackEntry != null) { + AnimatedVisibility( + !viewModel.isRefreshing, + enter = fadeIn(), + exit = fadeOut() + ) { + IconButton(onClick = { navController.popBackStack() }) { + Icon( + Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + } + } + }, + actions = { + val userDark by ThemePrefs.darkTheme + val isDark = userDark ?: isSystemInDarkTheme() + IconButton(onClick = { ThemePrefs.setDarkTheme(context, !isDark) }) { + Icon( + if (isDark) Icons.Outlined.LightMode else Icons.Outlined.DarkMode, + contentDescription = stringResource(R.string.toggle_theme), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = Color.Transparent, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer + ) + ) } ) { paddingValues -> - Box( - modifier = Modifier - .padding(paddingValues) - .pullRefresh(state, swipeEnabled) - .fillMaxSize(), - ) { + val screenContent: @Composable () -> Unit = { Column( modifier = Modifier .padding(16.dp, 0.dp, 16.dp, 16.dp + navigationBars.calculateBottomPadding()) @@ -106,14 +134,25 @@ fun RefreshableScreen( .verticalScroll(rememberScrollState()), content = content ) - PullRefreshIndicator( - viewModel.isRefreshing, - state = state, - modifier = Modifier.align(Alignment.TopCenter), - backgroundColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.primaryContainer, - scale = true - ) + } + if (swipeEnabled) { + PullToRefreshBox( + isRefreshing = viewModel.isRefreshing, + onRefresh = { viewModel.refresh(context) }, + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + screenContent() + } + } else { + Box( + modifier = Modifier + .padding(paddingValues) + .fillMaxSize() + ) { + screenContent() + } } } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt index f502b7fb..89152c9d 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/BackupsContent.kt @@ -6,10 +6,11 @@ import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.automirrored.outlined.DriveFileMove import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf @@ -25,6 +26,8 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.PartitionUtil +import com.github.capntrips.kernelflasher.ui.components.ActionTile +import com.github.capntrips.kernelflasher.ui.components.TileColors import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.DataSet @@ -81,14 +84,12 @@ fun ColumnScope.BackupsContent( AnimatedVisibility(!viewModel.isRefreshing) { Column { Spacer(Modifier.height(5.dp)) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.delete), + icon = Icons.Outlined.Delete, + accent = TileColors.Rose, onClick = { viewModel.delete(context) { navController.popBackStack() } } - ) { - Text(stringResource(R.string.delete)) - } + ) } } } else { @@ -96,14 +97,12 @@ fun ColumnScope.BackupsContent( AnimatedVisibility(viewModel.needsMigration) { Column { Spacer(Modifier.height(5.dp)) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.migrate), + icon = Icons.AutoMirrored.Outlined.DriveFileMove, + accent = TileColors.Cyan, onClick = { viewModel.migrate(context) } - ) { - Text(stringResource(R.string.migrate)) - } + ) } } if (viewModel.backups.isNotEmpty()) { diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt index ee58c22e..bc9da7b9 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/backups/SlotBackupsContent.kt @@ -1,27 +1,23 @@ package com.github.capntrips.kernelflasher.ui.screens.backups import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Checkbox +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material.icons.outlined.FlashOn +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material.icons.outlined.SettingsBackupRestore import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily @@ -33,10 +29,13 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.PartitionUtil +import com.github.capntrips.kernelflasher.ui.components.ActionTile +import com.github.capntrips.kernelflasher.ui.components.TileColors import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.DataSet import com.github.capntrips.kernelflasher.ui.components.FlashList +import com.github.capntrips.kernelflasher.ui.components.PartitionToggle import com.github.capntrips.kernelflasher.ui.components.SlotCard import com.github.capntrips.kernelflasher.ui.components.ViewButton import com.github.capntrips.kernelflasher.ui.screens.slot.SlotViewModel @@ -105,53 +104,46 @@ fun ColumnScope.SlotBackupsContent( Spacer(Modifier.height(5.dp)) if (slotViewModel.isActive) { if (currentBackup.type == "raw") { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.restore), + icon = Icons.Outlined.SettingsBackupRestore, + accent = TileColors.Violet, + showChevron = true, onClick = { navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore") } - ) { - Text(stringResource(R.string.restore)) - } + ) } else if (currentBackup.type == "ak3") { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.flash), + icon = Icons.Outlined.FlashOn, + accent = TileColors.Amber, onClick = { slotViewModel.flashAk3(context, backupsViewModel.currentBackup!!, currentBackup.filename!!) navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { popUpTo("slot$slotSuffix") } } - ) { - Text(stringResource(R.string.flash)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.flash_ak3_zip_mkbootfs), + icon = Icons.Outlined.FlashOn, + accent = TileColors.Amber, onClick = { slotViewModel.flashAk3_mkbootfs(context, backupsViewModel.currentBackup!!, currentBackup.filename!!) navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/flash/ak3") { popUpTo("slot$slotSuffix") } } - ) { - Text(stringResource(R.string.flash_ak3_zip_mkbootfs)) - } + ) } } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.delete), + icon = Icons.Outlined.Delete, + accent = TileColors.Rose, onClick = { backupsViewModel.delete(context) { navController.popBackStack() } } - ) { - Text(stringResource(R.string.delete)) - } + ) } } } else { @@ -186,34 +178,19 @@ fun ColumnScope.SlotBackupsContent( } else if (navController.currentDestination!!.route!!.endsWith("/backups/{backupId}/restore")) { DataCard (stringResource(R.string.restore)) Spacer(Modifier.height(5.dp)) - val disabledColor = ButtonDefaults.buttonColors( - Color.Transparent, - MaterialTheme.colorScheme.onSurface - ) val currentBackup = backupsViewModel.backups.getValue(backupsViewModel.currentBackup!!) if (currentBackup.hashes != null) { for (partitionName in PartitionUtil.PartitionNames) { val hash = currentBackup.hashes[partitionName] if (hash != null) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth() - .alpha(if (backupsViewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), - shape = RoundedCornerShape(4.dp), - colors = if (backupsViewModel.backupPartitions[partitionName] == true) ButtonDefaults.outlinedButtonColors() else disabledColor, + PartitionToggle( + name = partitionName, + checked = backupsViewModel.backupPartitions[partitionName] == true, enabled = backupsViewModel.backupPartitions[partitionName] != null, - onClick = { + onToggle = { backupsViewModel.backupPartitions[partitionName] = !backupsViewModel.backupPartitions[partitionName]!! - }, - ) { - Box(Modifier.fillMaxWidth()) { - Checkbox(backupsViewModel.backupPartitions[partitionName] == true, null, - Modifier - .align(Alignment.CenterStart) - .offset(x = -(16.dp))) - Text(partitionName, Modifier.align(Alignment.Center)) } - } + ) } } } else { @@ -225,22 +202,20 @@ fun ColumnScope.SlotBackupsContent( ) Spacer(Modifier.height(5.dp)) } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.restore), + icon = Icons.Outlined.SettingsBackupRestore, + accent = TileColors.Violet, + enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none { + currentBackup.hashes[it] != null && backupsViewModel.backupPartitions[it] == null + } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty()), onClick = { backupsViewModel.restore(context, slotSuffix) navController.navigate("slot$slotSuffix/backups/${backupsViewModel.currentBackup!!}/restore/restore") { popUpTo("slot$slotSuffix") } - }, - enabled = currentBackup.hashes == null || (PartitionUtil.PartitionNames.none { - currentBackup.hashes[it] != null && backupsViewModel.backupPartitions[it] == null - } && backupsViewModel.backupPartitions.filter { it.value }.isNotEmpty()) - ) { - Text(stringResource(R.string.restore)) - } + } + ) } else { FlashList( stringResource(R.string.restore), @@ -249,14 +224,12 @@ fun ColumnScope.SlotBackupsContent( AnimatedVisibility(!backupsViewModel.isRefreshing && backupsViewModel.wasRestored != null) { Column { if (backupsViewModel.wasRestored != false) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.reboot), + icon = Icons.Outlined.RestartAlt, + accent = TileColors.Rose, onClick = { navController.navigate("reboot") } - ) { - Text(stringResource(R.string.reboot)) - } + ) } } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt index 1a41b534..d16cea77 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/error/ErrorScreen.kt @@ -7,18 +7,20 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface 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.style.TextAlign import androidx.compose.ui.unit.dp -import com.github.capntrips.kernelflasher.ui.theme.Orange500 @ExperimentalMaterial3Api @Composable @@ -31,19 +33,26 @@ fun ErrorScreen(message: String) { .fillMaxSize() ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { - Icon( - Icons.Filled.Warning, - modifier = Modifier - .width(48.dp) - .height(48.dp), - tint = Orange500, - contentDescription = message - ) - Spacer(Modifier.height(8.dp)) + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.errorContainer + ) { + Icon( + Icons.Filled.Warning, + modifier = Modifier + .padding(20.dp) + .width(40.dp) + .height(40.dp), + tint = MaterialTheme.colorScheme.onErrorContainer, + contentDescription = message + ) + } + Spacer(Modifier.height(20.dp)) Text( message, modifier = Modifier.padding(32.dp, 0.dp, 32.dp, 32.dp), style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center ) } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt index 5a7ad237..8d82917e 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainContent.kt @@ -4,12 +4,14 @@ import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Archive +import androidx.compose.material.icons.outlined.BugReport +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material.icons.outlined.Terminal import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember @@ -19,9 +21,11 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R +import com.github.capntrips.kernelflasher.ui.components.ActionTile import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.DataRow import com.github.capntrips.kernelflasher.ui.components.SlotCard +import com.github.capntrips.kernelflasher.ui.components.TileColors import kotlinx.serialization.ExperimentalSerializationApi @ExperimentalMaterial3Api @@ -62,59 +66,41 @@ fun ColumnScope.MainContent( } Spacer(Modifier.height(16.dp)) AnimatedVisibility(!viewModel.isRefreshing) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.backups), + icon = Icons.Outlined.Archive, + accent = TileColors.Blue, + showChevron = true, onClick = { navController.navigate("backups") } - ) { - Text(stringResource(R.string.backups)) - } - } - AnimatedVisibility(!viewModel.isRefreshing) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { navController.navigate("updates") } - ) { - Text(stringResource(R.string.updates)) - } + ) } if (viewModel.hasRamoops) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.save_ramoops), + icon = Icons.Outlined.BugReport, + accent = TileColors.Cyan, onClick = { viewModel.saveRamoops(context) } - ) { - Text(stringResource(R.string.save_ramoops)) - } + ) } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.save_dmesg), + icon = Icons.Outlined.Terminal, + accent = TileColors.Amber, onClick = { viewModel.saveDmesg(context) } - ) { - Text(stringResource(R.string.save_dmesg)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.save_logcat), + icon = Icons.Outlined.Description, + accent = TileColors.Violet, onClick = { viewModel.saveLogcat(context) } - ) { - Text(stringResource(R.string.save_logcat)) - } + ) AnimatedVisibility(!viewModel.isRefreshing) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.reboot), + icon = Icons.Outlined.RestartAlt, + accent = TileColors.Rose, + showChevron = true, onClick = { navController.navigate("reboot") } - ) { - Text(stringResource(R.string.reboot)) - } + ) } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt index 676b78b0..d5ea71f5 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/main/MainViewModel.kt @@ -52,6 +52,7 @@ class MainViewModel( private val _isRefreshing: MutableState = mutableStateOf(true) private val _isRefreshRequired = mutableStateOf(true) + private val _snackbarMessage = mutableStateOf(null) private var _error: String? = null private var _backups: MutableMap = mutableMapOf() var showSlotIntentDialog: MutableState = mutableStateOf(false) @@ -63,6 +64,12 @@ class MainViewModel( get() = _isRefreshing.value val isRefreshRequired: Boolean get() = _isRefreshRequired.value + val snackbarMessage: String? + get() = _snackbarMessage.value + + fun clearSnackbar() { + _snackbarMessage.value = null + } val hasError: Boolean get() = _error != null val error: String @@ -162,12 +169,30 @@ class MainViewModel( } } + // Like [launch] but without toggling the shared refresh state. Lightweight one-shot + // actions (saving a log file) shouldn't trigger the pull-to-refresh spinner or + // collapse the navigation tiles — feedback is given via the Toast in [log]. + private fun launchQuiet(block: suspend () -> Unit) { + viewModelScope.launch(Dispatchers.IO) { + try { + block() + } catch (e: Exception) { + withContext (Dispatchers.Main) { + Log.e(TAG, e.message, e) + navController.navigate("error/${e.message}") { + popUpTo("main") + } + } + } + } + } + @Suppress("SameParameterValue") private fun log(context: Context, message: String, shouldThrow: Boolean = false) { Log.d(TAG, message) if (!shouldThrow) { viewModelScope.launch(Dispatchers.Main) { - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + _snackbarMessage.value = message } } else { throw Exception(message) @@ -175,7 +200,7 @@ class MainViewModel( } fun saveRamoops(context: Context) { - launch { + launchQuiet { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) @SuppressLint("SdCardPath") val ramoops = File("/sdcard/Download/console-ramoops--$now.log") @@ -189,7 +214,7 @@ class MainViewModel( } fun saveDmesg(context: Context) { - launch { + launchQuiet { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) @SuppressLint("SdCardPath") val dmesg = File("/sdcard/Download/dmesg--$now.log") @@ -203,7 +228,7 @@ class MainViewModel( } fun saveLogcat(context: Context) { - launch { + launchQuiet { val now = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd--HH-mm")) @SuppressLint("SdCardPath") val logcat = File("/sdcard/Download/logcat--$now.log") diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt index 2603140b..5d746933 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/reboot/RebootContent.kt @@ -1,19 +1,18 @@ package com.github.capntrips.kernelflasher.ui.screens.reboot -import android.os.Build -import android.os.PowerManager import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Build +import androidx.compose.material.icons.outlined.Code +import androidx.compose.material.icons.outlined.Download +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material.icons.outlined.Usb import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R +import com.github.capntrips.kernelflasher.ui.components.ActionTile +import com.github.capntrips.kernelflasher.ui.components.TileColors @Suppress("UnusedReceiverParameter") @Composable @@ -21,45 +20,34 @@ fun ColumnScope.RebootContent( viewModel: RebootViewModel, @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController ) { - val context = LocalContext.current - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.reboot), + icon = Icons.Outlined.RestartAlt, + accent = TileColors.Blue, onClick = { viewModel.rebootSystem() } - ) { - Text(stringResource(R.string.reboot)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.reboot_recovery), + icon = Icons.Outlined.Build, + accent = TileColors.Amber, onClick = { viewModel.rebootRecovery() } - ) { - Text(stringResource(R.string.reboot_recovery)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.reboot_bootloader), + icon = Icons.Outlined.Code, + accent = TileColors.Violet, onClick = { viewModel.rebootBootloader() } - ) { - Text(stringResource(R.string.reboot_bootloader)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.reboot_download), + icon = Icons.Outlined.Download, + accent = TileColors.Cyan, onClick = { viewModel.rebootDownload() } - ) { - Text(stringResource(R.string.reboot_download)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.reboot_edl), + icon = Icons.Outlined.Usb, + accent = TileColors.Rose, onClick = { viewModel.rebootEdl() } - ) { - Text(stringResource(R.string.reboot_edl)) - } + ) } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt index d7063476..cb59ff1c 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotContent.kt @@ -5,12 +5,15 @@ import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.FlashOn +import androidx.compose.material.icons.outlined.Link +import androidx.compose.material.icons.outlined.LinkOff +import androidx.compose.material.icons.outlined.Save +import androidx.compose.material.icons.outlined.SettingsBackupRestore +import androidx.compose.material.icons.outlined.Storage import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -19,7 +22,9 @@ import androidx.compose.ui.unit.ExperimentalUnitApi import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R +import com.github.capntrips.kernelflasher.ui.components.ActionTile import com.github.capntrips.kernelflasher.ui.components.SlotCard +import com.github.capntrips.kernelflasher.ui.components.TileColors @ExperimentalAnimationApi @ExperimentalMaterial3Api @@ -39,87 +44,64 @@ fun ColumnScope.SlotContent( ) AnimatedVisibility(!viewModel.isRefreshing.value) { Column { - Spacer(Modifier.height(5.dp)) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { - navController.navigate("slot$slotSuffix/flash") - } - ) { - Text(stringResource(R.string.flash)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + Spacer(Modifier.height(12.dp)) + ActionTile( + text = stringResource(R.string.flash), + icon = Icons.Outlined.FlashOn, + accent = TileColors.Amber, + showChevron = true, + onClick = { navController.navigate("slot$slotSuffix/flash") } + ) + ActionTile( + text = stringResource(R.string.backup), + icon = Icons.Outlined.Save, + accent = TileColors.Green, + showChevron = true, onClick = { viewModel.clearFlash(context) navController.navigate("slot$slotSuffix/backup") } - ) { - Text(stringResource(R.string.backup)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { - navController.navigate("slot$slotSuffix/backups") - } - ) { - Text(stringResource(R.string.restore)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { if (!viewModel.isRefreshing.value) viewModel.getKernel(context) } - ) { - Text(stringResource(R.string.check_kernel_version)) - } + ) + ActionTile( + text = stringResource(R.string.restore), + icon = Icons.Outlined.SettingsBackupRestore, + accent = TileColors.Violet, + showChevron = true, + onClick = { navController.navigate("slot$slotSuffix/backups") } + ) if (viewModel.hasVendorDlkm) { AnimatedVisibility(!viewModel.isRefreshing.value) { AnimatedVisibility(viewModel.isVendorDlkmMounted) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.unmount_vendor_dlkm), + icon = Icons.Outlined.LinkOff, + accent = TileColors.Rose, onClick = { viewModel.unmountVendorDlkm(context) } - ) { - Text(stringResource(R.string.unmount_vendor_dlkm)) - } + ) } AnimatedVisibility(!viewModel.isVendorDlkmMounted && viewModel.isVendorDlkmMapped) { Column { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.mount_vendor_dlkm), + icon = Icons.Outlined.Link, + accent = TileColors.Cyan, onClick = { viewModel.mountVendorDlkm(context) } - ) { - Text(stringResource(R.string.mount_vendor_dlkm)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ) + ActionTile( + text = stringResource(R.string.unmap_vendor_dlkm), + icon = Icons.Outlined.LinkOff, + accent = TileColors.Amber, onClick = { viewModel.unmapVendorDlkm(context) } - ) { - Text(stringResource(R.string.unmap_vendor_dlkm)) - } + ) } } AnimatedVisibility(!viewModel.isVendorDlkmMounted && !viewModel.isVendorDlkmMapped) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.map_vendor_dlkm), + icon = Icons.Outlined.Storage, + accent = TileColors.Cyan, onClick = { viewModel.mapVendorDlkm(context) } - ) { - Text(stringResource(R.string.map_vendor_dlkm)) - } + ) } } } diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt index 2246c046..00649f93 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotFlashContent.kt @@ -4,38 +4,33 @@ import android.provider.OpenableColumns import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ExperimentalAnimationApi -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.runtime.getValue import androidx.compose.runtime.setValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.TextButton -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Checkbox +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.outlined.Archive +import androidx.compose.material.icons.outlined.Description +import androidx.compose.material.icons.outlined.RestartAlt +import androidx.compose.material.icons.outlined.Save +import androidx.compose.material.icons.outlined.Storage import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.material3.AlertDialog import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.ExperimentalUnitApi @@ -44,10 +39,13 @@ import androidx.compose.ui.text.font.FontWeight import androidx.navigation.NavController import com.github.capntrips.kernelflasher.R import com.github.capntrips.kernelflasher.common.PartitionUtil +import com.github.capntrips.kernelflasher.ui.components.ActionTile import com.github.capntrips.kernelflasher.ui.components.DataCard import com.github.capntrips.kernelflasher.ui.components.FlashButton import com.github.capntrips.kernelflasher.ui.components.FlashList +import com.github.capntrips.kernelflasher.ui.components.PartitionToggle import com.github.capntrips.kernelflasher.ui.components.SlotCard +import com.github.capntrips.kernelflasher.ui.components.TileColors import com.github.capntrips.kernelflasher.ui.components.DialogButton import kotlinx.serialization.ExperimentalSerializationApi import java.io.File @@ -94,36 +92,35 @@ fun ColumnScope.SlotFlashContent( if (isFlashScreen) { DataCard (stringResource(R.string.flash)) Spacer(Modifier.height(5.dp)) - FlashButton(stringResource(R.string.flash_ak3_zip), "zip" ,callback = { uri -> + FlashButton(stringResource(R.string.flash_ak3_zip), "zip", accent = TileColors.Amber, callback = { uri -> viewModel.flashActionType = "flashAk3" viewModel.flashActionURI = uri viewModel.showConfirmDialog() }) - FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip" ,callback = { uri -> + FlashButton(stringResource(R.string.flash_ak3_zip_mkbootfs), "zip", accent = TileColors.Amber, callback = { uri -> viewModel.flashActionType = "flashAk3_mkbootfs" viewModel.flashActionURI = uri viewModel.showConfirmDialog() }) - FlashButton(stringResource(R.string.flash_ksu_lkm), "ko" ,callback = { uri -> + FlashButton(stringResource(R.string.flash_ksu_lkm), "ko", accent = TileColors.Cyan, callback = { uri -> viewModel.flashActionType = "flashKsuDriver" viewModel.flashActionURI = uri viewModel.showConfirmDialog() }) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.flash_partition_image), + icon = Icons.Outlined.Storage, + accent = TileColors.Blue, + showChevron = true, onClick = { navController.navigate("slot$slotSuffix/flash/image") } - ) { - Text(stringResource(R.string.flash_partition_image)) - } + ) } else if (isFlashImage) { DataCard (stringResource(R.string.flash_partition_image)) Spacer(Modifier.height(5.dp)) for (partitionName in PartitionUtil.AvailablePartitions) { - FlashButton(partitionName, "img" ,callback = { uri -> + FlashButton(partitionName, "img", accent = TileColors.Blue, callback = { uri -> viewModel.flashActionType = "flashImage" viewModel.flashActionURI = uri viewModel.flashActionPartName = partitionName @@ -133,41 +130,22 @@ fun ColumnScope.SlotFlashContent( } else if (isBackup) { DataCard (stringResource(R.string.backup)) Spacer(Modifier.height(5.dp)) - val disabledColor = ButtonDefaults.buttonColors( - Color.Transparent, - MaterialTheme.colorScheme.onSurface - ) for (partitionName in PartitionUtil.AvailablePartitions) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth() - .alpha(if (viewModel.backupPartitions[partitionName] == true) 1.0f else 0.5f), - shape = RoundedCornerShape(4.dp), - colors = if (viewModel.backupPartitions[partitionName]!!) ButtonDefaults.outlinedButtonColors() else disabledColor, - onClick = { + PartitionToggle( + name = partitionName, + checked = viewModel.backupPartitions[partitionName] == true, + onToggle = { viewModel.backupPartitions[partitionName] = !viewModel.backupPartitions[partitionName]!! - }, - ) { - Box(Modifier.fillMaxWidth()) { - Checkbox(viewModel.backupPartitions[partitionName]!!, null, - Modifier - .align(Alignment.CenterStart) - .offset(x = -(16.dp))) - Text(partitionName, Modifier.align(Alignment.Center)) } - } - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { - showBackupDialog = true - }, - enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty() - ) { - Text(stringResource(R.string.backup_now)) + ) } + ActionTile( + text = stringResource(R.string.backup_now), + icon = Icons.Outlined.Save, + accent = TileColors.Green, + enabled = viewModel.backupPartitions.filter { it.value }.isNotEmpty(), + onClick = { showBackupDialog = true } + ) } } else { Text("") @@ -180,21 +158,19 @@ fun ColumnScope.SlotFlashContent( AnimatedVisibility(!viewModel.isRefreshing.value && viewModel.wasFlashSuccess.value != null) { Column { if (isAk3) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.save_ak3_log), + icon = Icons.Outlined.Description, + accent = TileColors.Cyan, onClick = { viewModel.saveLog(context) } - ) { - Text(stringResource(R.string.save_ak3_log)) - } + ) } if (isAk3) { AnimatedVisibility(!currentRoute.endsWith("/backups/{backupId}/flash/ak3") && viewModel.wasFlashSuccess.value != false) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.save_ak3_zip_as_backup), + icon = Icons.Outlined.Archive, + accent = TileColors.Violet, onClick = { viewModel.backupZip(context) { navController.navigate("slot$slotSuffix/backups") { @@ -202,9 +178,7 @@ fun ColumnScope.SlotFlashContent( } } } - ) { - Text(stringResource(R.string.save_ak3_zip_as_backup)) - } + ) } } if (viewModel.wasFlashSuccess.value == true && viewModel.showCautionDialog == true){ @@ -238,23 +212,19 @@ fun ColumnScope.SlotFlashContent( ) } if (viewModel.wasFlashSuccess.value != false && isBackupResult) { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.back), + icon = Icons.AutoMirrored.Outlined.ArrowBack, + accent = TileColors.Blue, onClick = { navController.popBackStack() } - ) { - Text(stringResource(R.string.back)) - } + ) } else { - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), + ActionTile( + text = stringResource(R.string.reboot), + icon = Icons.Outlined.RestartAlt, + accent = TileColors.Rose, onClick = { navController.navigate("reboot") } - ) { - Text(stringResource(R.string.reboot)) - } + ) } } } @@ -283,14 +253,23 @@ fun ColumnScope.SlotFlashContent( text = { Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { Text("Are you sure you want to flash this file?", fontWeight = FontWeight.Bold) - + Text("Source: $filename") - + if (viewModel.flashActionType == "flashImage" && viewModel.flashActionPartName != null) { Text("Destination Partition: ${viewModel.flashActionPartName}", fontWeight = FontWeight.Bold) } else if (viewModel.flashActionType == "flashAk3" || viewModel.flashActionType == "flashAk3_mkbootfs") { Text("Destination: AnyKernel3 (Auto-detect)", fontWeight = FontWeight.Bold) } + + if (slotSuffix != "") { + Text( + "Destination Slot: ${if (slotSuffix == "_a") "A" else "B"} " + + "(${if (viewModel.isActive) "active" else "inactive"})", + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.primary + ) + } } }, confirmButton = { diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt index c2275ea4..d42b5fe7 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/slot/SlotViewModel.kt @@ -235,7 +235,16 @@ class SlotViewModel( _backupPartitions[partitionName] = true } - _slotInfo.value.bootImgInfo.kernelVersion = null + // Show the kernel version directly, without needing the old "check" button. The + // active slot is the running kernel, so report the full uname (release + version); + // an inactive slot reads its version from its own boot image. + if (isActive) { + _slotInfo.value.bootImgInfo.kernelVersion = + Shell.cmd("echo $(uname -r) $(uname -v)").exec().out.firstOrNull() + } else { + _slotInfo.value.bootImgInfo.kernelVersion = null + _getKernel(context) + } inInit = false } @@ -369,11 +378,6 @@ class SlotViewModel( Shell.cmd("$magiskboot cleanup").exec() } - fun getKernel(context: Context) { - launch { - _getKernel(context) - } - } private fun isPartitionMounted(partition: File): Boolean { @Suppress("LiftReturnOrAssignment") diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt deleted file mode 100644 index 060ed439..00000000 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesAddContent.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.capntrips.kernelflasher.ui.screens.updates - -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.OutlinedTextField -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.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import com.github.capntrips.kernelflasher.R -import kotlinx.serialization.ExperimentalSerializationApi - -@Suppress("UnusedReceiverParameter") -@ExperimentalMaterial3Api -@ExperimentalSerializationApi -@Composable -fun ColumnScope.UpdatesAddContent( - viewModel: UpdatesViewModel, - navController: NavController -) { - @Suppress("UNUSED_VARIABLE") val context = LocalContext.current - var url by remember { mutableStateOf("") } - OutlinedTextField( - value = url, - onValueChange = { url = it }, - label = { Text(stringResource(R.string.url)) }, - modifier = Modifier - .fillMaxWidth() - ) - Spacer(Modifier.height(5.dp)) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { viewModel.add(url) { navController.navigate("updates/view/$it") { popUpTo("updates") } } } - ) { - Text(stringResource(R.string.add)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt deleted file mode 100644 index 97e743ac..00000000 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesChangelogContent.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.github.capntrips.kernelflasher.ui.screens.updates - -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.unit.ExperimentalUnitApi -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.TextUnitType -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import com.github.capntrips.kernelflasher.ui.components.DataCard -import kotlinx.serialization.ExperimentalSerializationApi - -@Suppress("UnusedReceiverParameter") -@ExperimentalMaterial3Api -@ExperimentalSerializationApi -@ExperimentalUnitApi -@Composable -fun ColumnScope.UpdatesChangelogContent( - viewModel: UpdatesViewModel, - @Suppress("UNUSED_PARAMETER") ignoredNavController: NavController -) { - viewModel.currentUpdate?.let { currentUpdate -> - DataCard(currentUpdate.kernelName) - Spacer(Modifier.height(16.dp)) - Text(viewModel.changelog!!, - style = LocalTextStyle.current.copy( - fontFamily = FontFamily.Monospace, - fontSize = TextUnit(12.0f, TextUnitType.Sp), - lineHeight = TextUnit(18.0f, TextUnitType.Sp) - ) - ) - } -} diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt deleted file mode 100644 index d77ab7e6..00000000 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesContent.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.capntrips.kernelflasher.ui.screens.updates - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import com.github.capntrips.kernelflasher.R -import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer -import com.github.capntrips.kernelflasher.ui.components.DataCard -import com.github.capntrips.kernelflasher.ui.components.DataRow -import com.github.capntrips.kernelflasher.ui.components.ViewButton -import kotlinx.serialization.ExperimentalSerializationApi - -@ExperimentalMaterial3Api -@ExperimentalSerializationApi -@Composable -fun ColumnScope.UpdatesContent( - viewModel: UpdatesViewModel, - navController: NavController -) { - @Suppress("UNUSED_VARIABLE") val context = LocalContext.current - DataCard(stringResource(R.string.updates)) - if (viewModel.updates.isNotEmpty()) { - for (update in viewModel.updates.sortedByDescending { it.kernelDate }) { - Spacer(Modifier.height(16.dp)) - DataCard( - title = update.kernelName, - button = { - AnimatedVisibility(!viewModel.isRefreshing) { - Column { - ViewButton(onClick = { - navController.navigate("updates/view/${update.id}") - }) - } - } - } - ) { - val cardWidth = remember { mutableIntStateOf(0) } - DataRow(stringResource(R.string.version), update.kernelVersion, mutableMaxWidth = cardWidth) - DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(update.kernelDate), mutableMaxWidth = cardWidth) - DataRow( - label = stringResource(R.string.last_updated), - value = UpdatesViewModel.lastUpdatedFormatter.format(update.lastUpdated!!), - labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), - labelStyle = MaterialTheme.typography.labelMedium.copy( - fontStyle = FontStyle.Italic - ), - valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), - valueStyle = MaterialTheme.typography.titleSmall.copy( - fontStyle = FontStyle.Italic - ), - mutableMaxWidth = cardWidth - ) - } - } - } - AnimatedVisibility(!viewModel.isRefreshing) { - Column { - Spacer(Modifier.height(12.dp)) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { navController.navigate("updates/add") } - ) { - Text(stringResource(R.string.add)) - } - } - } -} diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt deleted file mode 100644 index 39a9a09b..00000000 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/screens/updates/UpdatesViewContent.kt +++ /dev/null @@ -1,95 +0,0 @@ -package com.github.capntrips.kernelflasher.ui.screens.updates - -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.unit.dp -import androidx.navigation.NavController -import com.github.capntrips.kernelflasher.R -import com.github.capntrips.kernelflasher.common.types.room.updates.DateSerializer -import com.github.capntrips.kernelflasher.ui.components.DataCard -import com.github.capntrips.kernelflasher.ui.components.DataRow -import kotlinx.serialization.ExperimentalSerializationApi - -@ExperimentalMaterial3Api -@ExperimentalSerializationApi -@Composable -fun ColumnScope.UpdatesViewContent( - viewModel: UpdatesViewModel, - navController: NavController -) { - val context = LocalContext.current - viewModel.currentUpdate?.let { currentUpdate -> - DataCard(currentUpdate.kernelName) { - val cardWidth = remember { mutableIntStateOf(0) } - DataRow(stringResource(R.string.version), currentUpdate.kernelVersion, mutableMaxWidth = cardWidth) - DataRow(stringResource(R.string.date_released), DateSerializer.formatter.format(currentUpdate.kernelDate), mutableMaxWidth = cardWidth) - DataRow( - label = stringResource(R.string.last_updated), - value = UpdatesViewModel.lastUpdatedFormatter.format(currentUpdate.lastUpdated!!), - labelColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), - labelStyle = MaterialTheme.typography.labelMedium.copy( - fontStyle = FontStyle.Italic - ), - valueColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.33f), - valueStyle = MaterialTheme.typography.titleSmall.copy( - fontStyle = FontStyle.Italic, - ), - mutableMaxWidth = cardWidth - ) - } - AnimatedVisibility(!viewModel.isRefreshing) { - Column { - Spacer(Modifier.height(5.dp)) - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { viewModel.downloadChangelog { navController.navigate("updates/view/${currentUpdate.id}/changelog") } } - ) { - Text(stringResource(R.string.changelog)) - } - // TODO: add download progress indicator - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { viewModel.downloadKernel(context) } - ) { - Text(stringResource(R.string.download)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { viewModel.update() } - ) { - Text(stringResource(R.string.check_for_updates)) - } - OutlinedButton( - modifier = Modifier - .fillMaxWidth(), - shape = RoundedCornerShape(4.dp), - onClick = { viewModel.delete { navController.popBackStack() } } - ) { - Text(stringResource(R.string.delete)) - } - } - } - } -} diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt index bee55f58..13181cd0 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Color.kt @@ -3,3 +3,66 @@ package com.github.capntrips.kernelflasher.ui.theme import androidx.compose.ui.graphics.Color val Orange500 = Color(0xFFFF9800) + +// Warm amber palette used when dynamic color is unavailable (Android < 12) +val LightPrimary = Color(0xFF8B5000) +val LightOnPrimary = Color(0xFFFFFFFF) +val LightPrimaryContainer = Color(0xFFFFDCBE) +val LightOnPrimaryContainer = Color(0xFF2C1600) +val LightSecondary = Color(0xFF725A42) +val LightOnSecondary = Color(0xFFFFFFFF) +val LightSecondaryContainer = Color(0xFFFFDCBE) +val LightOnSecondaryContainer = Color(0xFF291806) +val LightTertiary = Color(0xFF58633A) +val LightOnTertiary = Color(0xFFFFFFFF) +val LightTertiaryContainer = Color(0xFFDCE8B4) +val LightOnTertiaryContainer = Color(0xFF161E01) +val LightError = Color(0xFFBA1A1A) +val LightOnError = Color(0xFFFFFFFF) +val LightErrorContainer = Color(0xFFFFDAD6) +val LightOnErrorContainer = Color(0xFF410002) +val LightBackground = Color(0xFFFFF8F5) +val LightOnBackground = Color(0xFF221A14) +val LightSurfaceVariant = Color(0xFFF3DFD1) +val LightOnSurfaceVariant = Color(0xFF52443A) +val LightOutline = Color(0xFF847469) +val LightOutlineVariant = Color(0xFFD7C3B6) +val LightSurfaceContainerLowest = Color(0xFFFFFFFF) +val LightSurfaceContainerLow = Color(0xFFFFF1E8) +val LightSurfaceContainer = Color(0xFFF6EAE2) +val LightSurfaceContainerHigh = Color(0xFFF0E4DC) +val LightSurfaceContainerHighest = Color(0xFFEBDED6) +val LightInverseSurface = Color(0xFF382E28) +val LightInverseOnSurface = Color(0xFFFFEEE2) +val LightInversePrimary = Color(0xFFFFB870) + +val DarkPrimary = Color(0xFFFFB870) +val DarkOnPrimary = Color(0xFF4A2800) +val DarkPrimaryContainer = Color(0xFF693C00) +val DarkOnPrimaryContainer = Color(0xFFFFDCBE) +val DarkSecondary = Color(0xFFE1C1A4) +val DarkOnSecondary = Color(0xFF402C18) +val DarkSecondaryContainer = Color(0xFF59422C) +val DarkOnSecondaryContainer = Color(0xFFFFDCBE) +val DarkTertiary = Color(0xFFC0CC9A) +val DarkOnTertiary = Color(0xFF2A3310) +val DarkTertiaryContainer = Color(0xFF404A24) +val DarkOnTertiaryContainer = Color(0xFFDCE8B4) +val DarkError = Color(0xFFFFB4AB) +val DarkOnError = Color(0xFF690005) +val DarkErrorContainer = Color(0xFF93000A) +val DarkOnErrorContainer = Color(0xFFFFDAD6) +val DarkBackground = Color(0xFF19120C) +val DarkOnBackground = Color(0xFFEFE0D5) +val DarkSurfaceVariant = Color(0xFF52443A) +val DarkOnSurfaceVariant = Color(0xFFD7C3B6) +val DarkOutline = Color(0xFF9F8D81) +val DarkOutlineVariant = Color(0xFF52443A) +val DarkSurfaceContainerLowest = Color(0xFF130D07) +val DarkSurfaceContainerLow = Color(0xFF221A14) +val DarkSurfaceContainer = Color(0xFF261E18) +val DarkSurfaceContainerHigh = Color(0xFF312822) +val DarkSurfaceContainerHighest = Color(0xFF3C332C) +val DarkInverseSurface = Color(0xFFEFE0D5) +val DarkInverseOnSurface = Color(0xFF382E28) +val DarkInversePrimary = Color(0xFF8B5000) diff --git a/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt b/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt index 93033e30..28daf534 100644 --- a/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt +++ b/app/src/main/java/com/github/capntrips/kernelflasher/ui/theme/Theme.kt @@ -1,5 +1,7 @@ package com.github.capntrips.kernelflasher.ui.theme +import android.app.Activity +import android.content.Context import android.os.Build import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme @@ -8,25 +10,147 @@ import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.core.view.WindowCompat + +object ThemePrefs { + private const val PREFS_NAME = "theme" + private const val KEY_THEME_MODE = "theme_mode" // "system" | "light" | "dark" + + // null = follow system, true = forced dark, false = forced light + val darkTheme = mutableStateOf(null) + + fun load(context: Context) { + darkTheme.value = when ( + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString(KEY_THEME_MODE, "system") + ) { + "dark" -> true + "light" -> false + else -> null + } + } + + fun setDarkTheme(context: Context, dark: Boolean) { + darkTheme.value = dark + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .edit().putString(KEY_THEME_MODE, if (dark) "dark" else "light").apply() + } +} + +private val LightColorScheme = lightColorScheme( + primary = LightPrimary, + onPrimary = LightOnPrimary, + primaryContainer = LightPrimaryContainer, + onPrimaryContainer = LightOnPrimaryContainer, + secondary = LightSecondary, + onSecondary = LightOnSecondary, + secondaryContainer = LightSecondaryContainer, + onSecondaryContainer = LightOnSecondaryContainer, + tertiary = LightTertiary, + onTertiary = LightOnTertiary, + tertiaryContainer = LightTertiaryContainer, + onTertiaryContainer = LightOnTertiaryContainer, + error = LightError, + onError = LightOnError, + errorContainer = LightErrorContainer, + onErrorContainer = LightOnErrorContainer, + background = LightBackground, + onBackground = LightOnBackground, + surface = LightBackground, + onSurface = LightOnBackground, + surfaceVariant = LightSurfaceVariant, + onSurfaceVariant = LightOnSurfaceVariant, + outline = LightOutline, + outlineVariant = LightOutlineVariant, + surfaceContainerLowest = LightSurfaceContainerLowest, + surfaceContainerLow = LightSurfaceContainerLow, + surfaceContainer = LightSurfaceContainer, + surfaceContainerHigh = LightSurfaceContainerHigh, + surfaceContainerHighest = LightSurfaceContainerHighest, + inverseSurface = LightInverseSurface, + inverseOnSurface = LightInverseOnSurface, + inversePrimary = LightInversePrimary, +) + +private val DarkColorScheme = darkColorScheme( + primary = DarkPrimary, + onPrimary = DarkOnPrimary, + primaryContainer = DarkPrimaryContainer, + onPrimaryContainer = DarkOnPrimaryContainer, + secondary = DarkSecondary, + onSecondary = DarkOnSecondary, + secondaryContainer = DarkSecondaryContainer, + onSecondaryContainer = DarkOnSecondaryContainer, + tertiary = DarkTertiary, + onTertiary = DarkOnTertiary, + tertiaryContainer = DarkTertiaryContainer, + onTertiaryContainer = DarkOnTertiaryContainer, + error = DarkError, + onError = DarkOnError, + errorContainer = DarkErrorContainer, + onErrorContainer = DarkOnErrorContainer, + background = DarkBackground, + onBackground = DarkOnBackground, + surface = DarkBackground, + onSurface = DarkOnBackground, + surfaceVariant = DarkSurfaceVariant, + onSurfaceVariant = DarkOnSurfaceVariant, + outline = DarkOutline, + outlineVariant = DarkOutlineVariant, + surfaceContainerLowest = DarkSurfaceContainerLowest, + surfaceContainerLow = DarkSurfaceContainerLow, + surfaceContainer = DarkSurfaceContainer, + surfaceContainerHigh = DarkSurfaceContainerHigh, + surfaceContainerHighest = DarkSurfaceContainerHighest, + inverseSurface = DarkInverseSurface, + inverseOnSurface = DarkInverseOnSurface, + inversePrimary = DarkInversePrimary, +) @Composable fun KernelFlasherTheme( - darkTheme: Boolean = isSystemInDarkTheme(), + darkTheme: Boolean = ThemePrefs.darkTheme.value ?: isSystemInDarkTheme(), dynamicColor: Boolean = true, content: @Composable () -> Unit ) { - val colorScheme = when { + var colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { val context = LocalContext.current if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) } - darkTheme -> darkColorScheme() - else -> lightColorScheme() + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + // Dark mode always uses the AMOLED pure-black surfaces (true black on OLED screens). + if (darkTheme) { + colorScheme = colorScheme.copy( + background = Color.Black, + surface = Color.Black, + surfaceContainerLowest = Color.Black, + surfaceContainerLow = Color(0xFF0A0A0A), + surfaceContainer = Color(0xFF121212), + surfaceContainerHigh = Color(0xFF1C1C1C), + surfaceContainerHighest = Color(0xFF262626), + ) + } + // Keep the status/navigation bar icons legible against the current theme. + val view = LocalView.current + if (!view.isInEditMode) { + SideEffect { + val window = (view.context as Activity).window + val controller = WindowCompat.getInsetsController(window, view) + controller.isAppearanceLightStatusBars = !darkTheme + controller.isAppearanceLightNavigationBars = !darkTheme + } } MaterialTheme( colorScheme = colorScheme, typography = Typography, content = content ) -} \ No newline at end of file +} diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 5ef77ccd..71fb3e4d 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -32,7 +32,6 @@ Flash Partitione Image Flash KernelSU LKM Driver Ripristina - Controlla versione kernel Monta Vendor DLKM Smonta Vendor DLKM Mappa Vendor DLKM diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 99010dcf..92a9abf5 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -33,7 +33,6 @@ パーティションイメージをフラッシュ KernelSU LKM ドライバをフラッシュ 復元 - カーネルバージョンを確認 Vendor DLKM をマウント Vendor DLKM をアンマウント Vendor DLKM をマップ diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index d96c992c..49bddb88 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -33,7 +33,6 @@ Sflashuj obraz partycji Sflashuj KernelSU LKM Driver Przywróć - Sprawdź wersję jądra Zamontuj Vendor DLKM Odmontuj Vendor DLKM Migruj diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index da69494a..3c311c4c 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1,72 +1,71 @@ - - - Kernel Flasher - Root é necessário - Serviço root desconectado - Dispositivo - Modelo - Número da versão - Nome do kernel - Versão do kernel - Sufixo de slot - Slot A - Slot B - Boot SHA1 - Vendor DLKM - Existe - Não encontrado - Montado - Desmontado - Visualizar - Backups - Salvar ramoops - Salvar dmesg - Salvar logcat - Voltar - Backup - Atualizações - Flash - Flash using mkbootfs - Flash AK3 ZIP - Flash AK3 Zip using mkbootfs - Flashar imagem de partição - Flash KernelSU LKM Driver - Restaurar - Verificar versão do kernel - Montar Vendor DLKM - Desmontar Vendor DLKM - Mapear Vendor DLKM - Desmapear Vendor DLKM - Migrar - Nenhum backup encontrado - Excluir - Adicionar - URL - Versão - Data de lançamento - Ultima atualização - Registro de alterações - Verificar por atualizações - Baixar - Reiniciar - Reinicialização suave - Reiniciar em modo Recovery - Reiniciar em modo Bootloader - Reiniciar em modo Download - Reiniciar em modo EDL - Salvar registro do AK3 - Salvar registro do flash - Salvar registro do backup - Salvar registro da restauração - Salvar AK3 ZIP como backup - Tipo de backup - Hashes - Seleção de partição indisponível para backups antigos - Formato do boot.img - Formato do init_boot.img - Formato do Ramdisk - SUSFS Versão - Fazer backup das partições selecionadas - Formato do vendor_boot.img - Ativo - + + + Kernel Flasher + Root é necessário + Serviço root desconectado + Dispositivo + Modelo + Número da versão + Nome do kernel + Versão do kernel + Sufixo de slot + Slot A + Slot B + Boot SHA1 + Vendor DLKM + Existe + Não encontrado + Montado + Desmontado + Visualizar + Backups + Salvar ramoops + Salvar dmesg + Salvar logcat + Voltar + Backup + Atualizações + Flash + Flash using mkbootfs + Flash AK3 ZIP + Flash AK3 Zip using mkbootfs + Flashar imagem de partição + Flash KernelSU LKM Driver + Restaurar + Montar Vendor DLKM + Desmontar Vendor DLKM + Mapear Vendor DLKM + Desmapear Vendor DLKM + Migrar + Nenhum backup encontrado + Excluir + Adicionar + URL + Versão + Data de lançamento + Ultima atualização + Registro de alterações + Verificar por atualizações + Baixar + Reiniciar + Reinicialização suave + Reiniciar em modo Recovery + Reiniciar em modo Bootloader + Reiniciar em modo Download + Reiniciar em modo EDL + Salvar registro do AK3 + Salvar registro do flash + Salvar registro do backup + Salvar registro da restauração + Salvar AK3 ZIP como backup + Tipo de backup + Hashes + Seleção de partição indisponível para backups antigos + Formato do boot.img + Formato do init_boot.img + Formato do Ramdisk + SUSFS Versão + Fazer backup das partições selecionadas + Formato do vendor_boot.img + Ativo + diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d282bd0b..0e52a294 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -32,7 +32,6 @@ Прошить образ раздела Прошивка KernelSU LKM Driver Восстановить - Проверить версию ядра Подключить Vendor DLKM Отключить Vendor DLKM Сопоставить Vendor DLKM diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 93544946..1eb06bc5 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -32,7 +32,6 @@ 刷入分区镜像 刷入 KernelSU LKM Driver 恢复 - 检查内核版本 挂载 Vendor DLKM 卸载 Vendor DLKM 映射 Vendor DLKM diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index f60f77c0..821a8584 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -32,7 +32,6 @@ 刷入分割槽映象 刷入 KernelSU LKM Driver 還原 - 檢查核心版本 掛載 Vendor DLKM 解除安裝 Vendor DLKM 對映 Vendor DLKM diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f0b34343..4240cdd3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -23,6 +23,7 @@ Save dmesg Save logcat Back + Toggle light/dark theme Backup Updates Flash @@ -32,7 +33,6 @@ Flash Partition Image Flash KernelSU LKM Driver Restore - Check Kernel Version Mount Vendor DLKM Unmount Vendor DLKM Map Vendor DLKM diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1f91d686..3808c134 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -25,6 +25,7 @@ androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "androidx-compose" } androidx-compose-material = { group = "androidx.compose.material", name = "material", version.ref = "androidx-compose" } androidx-compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "androidx-compose-material3" } +androidx-compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended", version = "1.7.8" } androidx-compose-ui = { group = "androidx.compose.ui", name="ui", version.ref = "androidx-compose" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "androidx-core-ktx" } androidx-core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "androidx-core-splashscreen" }