From 5fadc4ebbcdaa42cabb28cf1405f5982ed54bae2 Mon Sep 17 00:00:00 2001 From: meowarex Date: Tue, 11 Aug 2026 17:29:31 +0000 Subject: [PATCH] UI Compose Rewrite --- Manager/app/build.gradle.kts | 10 +- .../com/meowarex/rlmobile/MainActivity.kt | 24 +- .../rlmobile/manager/PreferencesManager.kt | 5 +- .../rlmobile/ui/components/BackButton.kt | 19 +- .../ui/components/MainActionButton.kt | 66 +- .../rlmobile/ui/components/ProjectHeader.kt | 37 +- .../ui/components/ResetToDefaultButton.kt | 23 +- .../meowarex/rlmobile/ui/components/Tag.kt | 45 +- .../rlmobile/ui/components/TextDivider.kt | 38 +- .../dialogs/InstallerAbortDialog.kt | 53 +- .../components/dialogs/PlayProtectDialog.kt | 95 ++- .../ui/components/radiant/RadiantButton.kt | 321 ++++++++ .../components/radiant/RadiantContainers.kt | 181 +++++ .../ui/components/radiant/RadiantInputs.kt | 306 ++++++++ .../ui/components/radiant/RadiantToggles.kt | 245 +++++++ .../ui/components/settings/SettingsGroup.kt | 58 ++ .../ui/components/settings/SettingsHeader.kt | 2 +- .../ui/components/settings/SettingsItem.kt | 80 +- .../ui/components/settings/SettingsSwitch.kt | 10 +- .../components/settings/SettingsTextField.kt | 19 +- .../ui/legacy/components/BackButton.kt | 33 + .../ui/legacy/components/Contributors.kt | 74 ++ .../ui/legacy/components/InteractiveSlider.kt | 115 +++ .../rlmobile/ui/legacy/components/Label.kt | 44 ++ .../ui/legacy/components/LoadFailure.kt | 35 + .../ui/legacy/components/MainActionButton.kt | 51 ++ .../ui/legacy/components/ProjectHeader.kt | 53 ++ .../legacy/components/ResetToDefaultButton.kt | 39 + .../components/SegmentedButton.kt | 4 +- .../rlmobile/ui/legacy/components/Tag.kt | 27 + .../ui/legacy/components/TextDivider.kt | 37 + .../ui/legacy/components/VersionDisplay.kt | 32 + .../rlmobile/ui/legacy/components/Wakelock.kt | 28 + .../dialogs/InstallerAbortDialog.kt | 64 ++ .../components/dialogs/PlayProtectDialog.kt | 105 +++ .../components/settings/SettingsHeader.kt | 19 + .../components/settings/SettingsItem.kt | 59 ++ .../components/settings/SettingsSwitch.kt | 37 + .../components/settings/SettingsTextField.kt | 31 + .../ui/legacy/screens/about/AboutScreen.kt | 114 +++ .../about/components/LeadContributor.kt | 77 ++ .../componentopts/ComponentOptionsScreen.kt | 365 ++++++++++ .../components/ComponentOptionsAppBar.kt | 37 + .../components/PatchComponentCard.kt | 118 +++ .../ui/legacy/screens/home/HomeScreen.kt | 278 +++++++ .../screens/home/components/CommitList.kt | 118 +++ .../ui/legacy/screens/log/LogScreen.kt | 127 ++++ .../screens/log/components/LogAppBar.kt | 36 + .../screens/log/components/LogTextArea.kt | 45 ++ .../legacy/screens/patching/PatchingScreen.kt | 304 ++++++++ .../screens/patching/components/FunFact.kt | 53 ++ .../patching/components/PatchingAppBar.kt | 27 + .../patching/components/StepGroupCard.kt | 118 +++ .../screens/patching/components/StepItem.kt | 52 ++ .../patching/components/StepStateIcon.kt | 91 +++ .../screens/patching/components/TextBanner.kt | 61 ++ .../patching/components/TimeElapsed.kt | 33 + .../screens/patchopts/PatchOptionsScreen.kt | 313 ++++++++ .../components/PackageNameStateLabel.kt | 68 ++ .../components/PackageNameUnlockDialog.kt | 91 +++ .../components/PatchAdvancedOptionsSheet.kt | 685 ++++++++++++++++++ .../components/PatchOptionsAppBar.kt | 33 + .../components/PatchSelectionAccordion.kt | 433 +++++++++++ .../components/PatchVariantSelector.kt | 47 ++ .../components/options/IconPatchOption.kt | 52 ++ .../components/options/SwitchPatchOption.kt | 52 ++ .../components/options/TextPatchOption.kt | 84 +++ .../screens/permissions/PermissionsScreen.kt | 224 ++++++ .../components/PermissionButton.kt | 80 ++ .../components/PermissionsAppBar.kt | 48 ++ .../legacy/screens/settings/SettingsScreen.kt | 251 +++++++ .../settings/components/InstallersDialog.kt | 183 +++++ .../settings/components/ThemeDialog.kt | 92 +++ .../settings/components/UiStyleDialog.kt | 93 +++ .../managerupdate/ManagerUpdateDialog.kt | 135 ++++ .../legacy/widgets/updater/UpdaterDialog.kt | 105 +++ .../rlmobile/ui/screens/about/AboutScreen.kt | 2 +- .../componentopts/ComponentOptionsScreen.kt | 32 +- .../components/ComponentOptionsAppBar.kt | 4 +- .../components/PatchComponentCard.kt | 18 +- .../rlmobile/ui/screens/home/HomeModel.kt | 17 +- .../rlmobile/ui/screens/home/HomeScreen.kt | 351 +++++---- .../ui/screens/home/components/CommitList.kt | 8 +- .../ui/screens/log/components/LogAppBar.kt | 25 +- .../ui/screens/patching/PatchingScreen.kt | 20 +- .../screens/patching/PatchingScreenState.kt | 5 +- .../patching/components/OverallProgressBar.kt | 96 +++ .../patching/components/PatchingAppBar.kt | 13 +- .../patching/components/StepGroupCard.kt | 10 +- .../patching/components/StepStateIcon.kt | 18 +- .../screens/patching/components/TextBanner.kt | 4 +- .../ui/screens/patchopts/PatchOptionsModel.kt | 31 +- .../screens/patchopts/PatchOptionsScreen.kt | 41 +- .../components/PackageNameStateLabel.kt | 3 +- .../components/PackageNameUnlockDialog.kt | 86 +-- .../components/PatchAdvancedOptionsSheet.kt | 222 +++--- .../components/PatchOptionsAppBar.kt | 13 +- .../components/PatchSelectionAccordion.kt | 82 +-- .../components/PatchVariantSelector.kt | 41 +- .../components/options/IconPatchOption.kt | 22 +- .../components/options/SwitchPatchOption.kt | 4 +- .../components/options/TextPatchOption.kt | 45 +- .../screens/permissions/PermissionsScreen.kt | 30 +- .../components/PermissionButton.kt | 128 ++-- .../components/PermissionsAppBar.kt | 34 +- .../ui/screens/settings/SettingsModel.kt | 15 + .../ui/screens/settings/SettingsScreen.kt | 168 +++-- .../settings/components/InstallersDialog.kt | 102 +-- .../settings/components/ThemeDialog.kt | 108 +-- .../settings/components/UiStyleDialog.kt | 94 +++ .../rlmobile/ui/theme/LegacyTypography.kt | 54 ++ .../rlmobile/ui/theme/RadiantDesign.kt | 161 ++++ .../com/meowarex/rlmobile/ui/theme/Theme.kt | 74 +- .../meowarex/rlmobile/ui/theme/ThemeColors.kt | 188 +++++ .../rlmobile/ui/theme/ThemeCustomColors.kt | 19 + .../meowarex/rlmobile/ui/theme/ThemeShapes.kt | 19 + .../rlmobile/ui/theme/ThemeTypography.kt | 167 ++++- .../com/meowarex/rlmobile/ui/theme/UiStyle.kt | 90 +++ .../managerupdate/ManagerUpdateDialog.kt | 64 +- .../ui/widgets/updater/UpdaterDialog.kt | 126 ++-- Manager/app/src/main/res/values/strings.xml | 5 + Manager/gradle/libs.versions.toml | 12 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 123 files changed, 9295 insertions(+), 1225 deletions(-) create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantButton.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantContainers.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantInputs.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantToggles.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsGroup.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/BackButton.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Contributors.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/InteractiveSlider.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Label.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/LoadFailure.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/MainActionButton.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ProjectHeader.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ResetToDefaultButton.kt rename Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/{ => legacy}/components/SegmentedButton.kt (93%) create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Tag.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/TextDivider.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/VersionDisplay.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Wakelock.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/InstallerAbortDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/PlayProtectDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsHeader.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsItem.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsSwitch.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsTextField.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/AboutScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/components/LeadContributor.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/ComponentOptionsScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/ComponentOptionsAppBar.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/PatchComponentCard.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/HomeScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/components/CommitList.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/LogScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogAppBar.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogTextArea.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/PatchingScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/FunFact.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/PatchingAppBar.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepGroupCard.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepItem.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepStateIcon.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TextBanner.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TimeElapsed.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/PatchOptionsScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameStateLabel.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameUnlockDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchAdvancedOptionsSheet.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchOptionsAppBar.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchSelectionAccordion.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchVariantSelector.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/IconPatchOption.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/SwitchPatchOption.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/TextPatchOption.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/PermissionsScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionButton.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionsAppBar.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/SettingsScreen.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/InstallersDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/ThemeDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/UiStyleDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/managerupdate/ManagerUpdateDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/updater/UpdaterDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/OverallProgressBar.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/UiStyleDialog.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/LegacyTypography.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/RadiantDesign.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeColors.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeShapes.kt create mode 100644 Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/UiStyle.kt diff --git a/Manager/app/build.gradle.kts b/Manager/app/build.gradle.kts index 3d32ad9..2a835d1 100644 --- a/Manager/app/build.gradle.kts +++ b/Manager/app/build.gradle.kts @@ -23,13 +23,16 @@ val gitHasHasLocalChanges = providers.execIgnoreCode("git", "status", "-s").isNo android { namespace = "com.meowarex.rlmobile" - compileSdk = 36 + // 37 is required to compile against compose 1.12.x. targetSdk deliberately stays at 36 — + // this only widens the API surface available at compile time, it does not opt the app in to + // Android 17's runtime behaviour changes. + compileSdk = 37 defaultConfig { minSdk = 24 targetSdk = 36 - versionCode = 10_03_00 - versionName = "1.3.0" + versionCode = 10_05_02 + versionName = "1.5.2" vectorDrawables { useSupportLibrary = true @@ -161,6 +164,7 @@ kotlin { "androidx.compose.foundation.ExperimentalFoundationApi", "androidx.compose.foundation.layout.ExperimentalLayoutApi", "androidx.compose.material3.ExperimentalMaterial3Api", + "androidx.compose.material3.ExperimentalMaterial3ExpressiveApi", "kotlin.time.ExperimentalTime", "kotlinx.serialization.ExperimentalSerializationApi", ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/MainActivity.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/MainActivity.kt index ee5f305..c843d9f 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/MainActivity.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/MainActivity.kt @@ -22,11 +22,15 @@ import com.meowarex.rlmobile.MainActivity.Companion.EXTRA_PACKAGE_NAME import com.meowarex.rlmobile.manager.* import com.meowarex.rlmobile.patcher.InstallMetadata import com.meowarex.rlmobile.ui.screens.home.HomeScreen +import com.meowarex.rlmobile.ui.legacy.screens.home.HomeScreen as LegacyHomeScreen import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen +import com.meowarex.rlmobile.ui.legacy.screens.patching.PatchingScreen as LegacyPatchingScreen import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel import com.meowarex.rlmobile.ui.screens.permissions.PermissionsScreen +import com.meowarex.rlmobile.ui.legacy.screens.permissions.PermissionsScreen as LegacyPermissionsScreen import com.meowarex.rlmobile.ui.theme.ManagerTheme +import com.meowarex.rlmobile.ui.theme.UiStyle import com.meowarex.rlmobile.ui.widgets.updater.UpdaterDialog import com.meowarex.rlmobile.util.* import com.github.diamondminer88.zip.ZipReader @@ -63,6 +67,7 @@ class MainActivity : ComponentActivity() { ManagerTheme( theme = preferences.theme, + uiStyle = preferences.uiStyle, dynamicColor = preferences.dynamicColor, ) { if (BuildConfig.RELEASE) { @@ -73,14 +78,20 @@ class MainActivity : ComponentActivity() { CompositionLocalProvider( LocalNavigatorSaver provides parcelableNavigatorSaver(), ) { + val legacyUi = preferences.uiStyle == UiStyle.Legacy + + key(legacyUi) { Navigator( - screen = HomeScreen(), + screen = if (legacyUi) LegacyHomeScreen() else HomeScreen(), onBackPressed = null, ) { navigator -> // Open the permissions screen whenever permissions are insufficient LaunchedEffect(permissions.requiredPermsGranted) { - if (!permissions.requiredPermsGranted) - navigator.pushOnce(PermissionsScreen()) + if (!permissions.requiredPermsGranted) { + navigator.pushOnce( + if (legacyUi) LegacyPermissionsScreen() else PermissionsScreen() + ) + } } DisposableEffect(Unit) { @@ -104,6 +115,7 @@ class MainActivity : ComponentActivity() { ) ) } + } overlays.Overlays() } @@ -184,7 +196,11 @@ class MainActivity : ComponentActivity() { val patchOptions = metadata?.options ?: PatchOptions.Default.copy(packageName = packageName) - return PatchingScreen(patchOptions) + return if (preferences.uiStyle == UiStyle.Legacy) { + LegacyPatchingScreen(patchOptions) + } else { + PatchingScreen(patchOptions) + } } companion object { diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/manager/PreferencesManager.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/manager/PreferencesManager.kt index 3f94c65..381488e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/manager/PreferencesManager.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/manager/PreferencesManager.kt @@ -4,11 +4,14 @@ import android.content.SharedPreferences import androidx.compose.runtime.Stable import com.meowarex.rlmobile.manager.base.BasePreferenceManager import com.meowarex.rlmobile.ui.theme.Theme +import com.meowarex.rlmobile.ui.theme.UiStyle @Stable class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) { var theme by enumPreference("theme", Theme.System) - var dynamicColor by booleanPreference("dynamic_color", true) + var uiStyle by enumPreference("ui_style", UiStyle.Radiant) + // Off by default — Radiant ships its own palette. Opting in hands theming back to Monet. + var dynamicColor by booleanPreference("dynamic_color", false) var devMode by booleanPreference("dev_mode", false) var installer by enumPreference("installer", InstallerSetting.PackageInstaller) var keepPatchedApks by booleanPreference("keep_patched_apks", false) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/BackButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/BackButton.kt index 99a25b6..ab10791 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/BackButton.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/BackButton.kt @@ -1,13 +1,12 @@ package com.meowarex.rlmobile.ui.components import androidx.activity.compose.LocalActivity -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.navigator.LocalNavigator import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton import com.meowarex.rlmobile.util.back /** @@ -18,14 +17,10 @@ fun BackButton() { val navigator = LocalNavigator.current val activity = LocalActivity.current - IconButton( - onClick = { - navigator?.back(activity) - }, - ) { - Icon( - painter = painterResource(R.drawable.ic_back), - contentDescription = stringResource(R.string.navigation_back), - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.navigation_back), + subtle = true, + onClick = { navigator?.back(activity) }, + ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/MainActionButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/MainActionButton.kt index a6dc0c0..d45b697 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/MainActionButton.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/MainActionButton.kt @@ -1,13 +1,11 @@ package com.meowarex.rlmobile.ui.components -import androidx.compose.foundation.layout.* -import androidx.compose.material3.* import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle @Composable fun MainActionButton( @@ -15,35 +13,33 @@ fun MainActionButton( icon: Painter, onClick: () -> Unit, enabled: Boolean = true, - colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - ), + style: RadiantButtonStyle = RadiantButtonStyle.Accent, modifier: Modifier = Modifier, -) { - FilledTonalIconButton( - shape = MaterialTheme.shapes.medium, - colors = colors, - onClick = onClick, - enabled = enabled, - modifier = modifier - .fillMaxWidth() - .height(46.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - painter = icon, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) +) = RadiantButton( + text = text, + icon = icon, + onClick = onClick, + enabled = enabled, + style = style, + size = RadiantButtonSize.Large, + fillWidth = true, + modifier = modifier, +) - Text( - text = text, - style = MaterialTheme.typography.labelLarge, - textAlign = TextAlign.Center, - ) - } - } -} +@Composable +fun DangerActionButton( + text: String, + icon: Painter, + onClick: () -> Unit, + enabled: Boolean = true, + modifier: Modifier = Modifier, +) = RadiantButton( + text = text, + icon = icon, + onClick = onClick, + enabled = enabled, + style = RadiantButtonStyle.Danger, + size = RadiantButtonSize.Large, + fillWidth = true, + modifier = modifier, +) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ProjectHeader.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ProjectHeader.kt index 4d2564a..783859d 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ProjectHeader.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ProjectHeader.kt @@ -1,17 +1,21 @@ package com.meowarex.rlmobile.ui.components +import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton @Composable fun ProjectHeader(modifier: Modifier = Modifier) { @@ -22,30 +26,37 @@ fun ProjectHeader(modifier: Modifier = Modifier) { verticalArrangement = Arrangement.spacedBy(6.dp), modifier = modifier, ) { + Image( + painter = painterResource(R.drawable.ic_launcher_foreground), + contentDescription = null, + modifier = Modifier + .size(64.dp) + .clip(MaterialShapes.Cookie9Sided.toShape()) + .background(colorResource(R.color.ic_launcher_background)), + ) + Text( text = stringResource(R.string.rlmobile), - style = MaterialTheme.typography.titleMedium.copy(fontSize = 26.sp) + style = MaterialTheme.typography.headlineMedium, + textAlign = TextAlign.Center, ) Text( text = stringResource(R.string.app_description), - style = MaterialTheme.typography.titleSmall.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = .6f) - ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center, ) Row( horizontalArrangement = Arrangement.Center, + modifier = Modifier.padding(top = 6.dp), ) { - TextButton(onClick = { uriHandler.openUri("https://github.com/meowarex/rl-mobile") }) { - Icon( - painter = painterResource(R.drawable.ic_account_github_white_24dp), - contentDescription = null, - modifier = Modifier.padding(end = ButtonDefaults.IconSpacing), - ) - Text(text = stringResource(R.string.github)) - } + RadiantButton( + text = stringResource(R.string.github), + icon = painterResource(R.drawable.ic_account_github_white_24dp), + onClick = { uriHandler.openUri("https://github.com/meowarex/rl-mobile") }, + ) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ResetToDefaultButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ResetToDefaultButton.kt index 70f60e8..5aee773 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ResetToDefaultButton.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/ResetToDefaultButton.kt @@ -1,15 +1,13 @@ package com.meowarex.rlmobile.ui.components import androidx.compose.animation.* -import androidx.compose.foundation.layout.size -import androidx.compose.material3.* +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R -import com.meowarex.rlmobile.ui.util.mirrorVertically +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton @Composable fun ResetToDefaultButton( @@ -23,15 +21,12 @@ fun ResetToDefaultButton( exit = fadeOut() + slideOutHorizontally(), modifier = modifier, ) { - IconButton(onClick = onClick) { - Icon( - painter = painterResource(R.drawable.ic_refresh), - tint = MaterialTheme.colorScheme.secondary, - contentDescription = stringResource(R.string.action_reset_default), - modifier = Modifier - .mirrorVertically() - .size(26.dp), - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.action_reset_default), + tint = MaterialTheme.colorScheme.secondary, + subtle = true, + onClick = onClick, + ) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/Tag.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/Tag.kt index 2faffc8..c0abfca 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/Tag.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/Tag.kt @@ -1,25 +1,48 @@ package com.meowarex.rlmobile.ui.components -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.theme.glassSurface +/** + * Status pill + */ @Composable -fun Tag(text: String) { - Surface( - color = MaterialTheme.colorScheme.primaryContainer, - shape = RoundedCornerShape(6.dp), +fun Tag( + text: String, + icon: Painter? = null, + tint: Color = MaterialTheme.colorScheme.primary, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(5.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .glassSurface(CircleShape, tint = tint) + .clip(CircleShape) + .padding(horizontal = 9.dp, vertical = 4.dp), ) { + if (icon != null) { + Icon( + painter = icon, + contentDescription = null, + tint = tint, + modifier = Modifier.size(12.dp), + ) + } + Text( text = text, style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onPrimaryContainer, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + color = tint, ) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/TextDivider.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/TextDivider.kt index dfb183f..1d3f5e6 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/TextDivider.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/TextDivider.kt @@ -1,35 +1,47 @@ package com.meowarex.rlmobile.ui.components +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +/** + * Section header + */ @Composable fun TextDivider( text: String, - style: TextStyle = MaterialTheme.typography.bodyMedium, + style: TextStyle = MaterialTheme.typography.labelLarge, modifier: Modifier = Modifier, ) { Row( - horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically, - modifier = modifier - .fillMaxWidth(), + modifier = modifier.fillMaxWidth(), ) { - HorizontalDivider(Modifier.weight(1f)) - - Text( - text = text, - style = style, - color = MaterialTheme.colorScheme.outline, - fontWeight = FontWeight.SemiBold, + Box( + modifier = Modifier + .size(width = 14.dp, height = 3.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), ) - HorizontalDivider(Modifier.weight(1f)) + Text( + text = text.uppercase(), + style = style.copy(letterSpacing = 1.2.sp), + color = MaterialTheme.colorScheme.primary, + ) + + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant, + modifier = Modifier.weight(1f), + ) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/InstallerAbortDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/InstallerAbortDialog.kt index 2a9e7da..ed4f715 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/InstallerAbortDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/InstallerAbortDialog.kt @@ -1,62 +1,37 @@ package com.meowarex.rlmobile.ui.components.dialogs import androidx.compose.foundation.layout.size -import androidx.compose.material3.* +import androidx.compose.material3.Icon +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog @Composable fun InstallerAbortDialog( onConfirm: () -> Unit, onDismiss: () -> Unit, ) { - AlertDialog( + RadiantDialog( onDismissRequest = onDismiss, - confirmButton = { - FilledTonalButton( - onClick = onConfirm, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.error, - contentColor = MaterialTheme.colorScheme.onError, - ), - ) { - Text(stringResource(R.string.action_exit_anyways)) - } - }, - dismissButton = { - TextButton( - onClick = onDismiss, - colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.onErrorContainer - ), - ) { - Text(stringResource(R.string.action_cancel)) - } - }, - title = { - Text(stringResource(R.string.installer_abort_title)) - }, - text = { - Text( - text = stringResource(R.string.installer_abort_body), - textAlign = TextAlign.Center, - ) - }, + title = stringResource(R.string.installer_abort_title), icon = { Icon( painter = painterResource(R.drawable.ic_warning), contentDescription = null, - modifier = Modifier.size(32.dp), + modifier = Modifier.size(26.dp), ) }, - containerColor = MaterialTheme.colorScheme.errorContainer, - iconContentColor = MaterialTheme.colorScheme.onErrorContainer, - titleContentColor = MaterialTheme.colorScheme.onErrorContainer, - textContentColor = MaterialTheme.colorScheme.onErrorContainer - ) + confirmText = stringResource(R.string.action_exit_anyways), + onConfirm = onConfirm, + destructive = true, + dismissText = stringResource(R.string.action_cancel), + onDismissAction = onDismiss, + ) { + Text(stringResource(R.string.installer_abort_body)) + } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/PlayProtectDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/PlayProtectDialog.kt index 19bb1a0..45b64cf 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/PlayProtectDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/dialogs/PlayProtectDialog.kt @@ -13,11 +13,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.DialogProperties import cafe.adriel.voyager.navigator.currentOrThrow import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.* import com.meowarex.rlmobile.ui.theme.customColors @Composable @@ -29,70 +28,58 @@ fun PlayProtectDialog( var neverShow by rememberSaveable { mutableStateOf(false) } val rememberedNeverShow by rememberUpdatedState(neverShow) - AlertDialog( + RadiantDialog( onDismissRequest = { onDismiss(rememberedNeverShow) }, - dismissButton = { - FilledTonalButton(onClick = activity::launchPlayProtect) { - Text(stringResource(R.string.play_protect_warning_open_gpp)) - } - }, - confirmButton = { - FilledTonalButton( - onClick = { onDismiss(rememberedNeverShow) }, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Text(stringResource(R.string.play_protect_warning_ok)) - } - }, + title = stringResource(R.string.play_protect_warning_title), icon = { Icon( painter = painterResource(R.drawable.ic_protect_warning), tint = MaterialTheme.customColors.warning, contentDescription = null, - modifier = Modifier.size(36.dp), + modifier = Modifier.size(28.dp), ) }, - title = { Text(stringResource(R.string.play_protect_warning_title)) }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = Alignment.CenterHorizontally, + ) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text(stringResource(R.string.play_protect_warning_desc)) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier.clickable( + interactionSource = interactionSource, + indication = null, + onClick = { neverShow = !rememberedNeverShow }, + ), ) { - Text( - text = stringResource(R.string.play_protect_warning_desc), - textAlign = TextAlign.Center, + RadiantCheckbox( + checked = neverShow, + onCheckedChange = { neverShow = it }, ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable( - interactionSource = interactionSource, - indication = null, - onClick = { neverShow = !rememberedNeverShow }, - ) - .padding(end = 16.dp) - ) { - Checkbox( - checked = neverShow, - onCheckedChange = { neverShow = it }, - interactionSource = interactionSource, - ) - - Text(stringResource(R.string.play_protect_warning_disable)) - } + Text(stringResource(R.string.play_protect_warning_disable)) } - }, - properties = DialogProperties( - dismissOnClickOutside = false, - usePlatformDefaultWidth = false, - ), - modifier = Modifier - .padding(25.dp), - ) + + // Both actions are equal, they share a row + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End), + modifier = Modifier.fillMaxWidth(), + ) { + RadiantButton( + text = stringResource(R.string.play_protect_warning_open_gpp), + onClick = activity::launchPlayProtect, + style = RadiantButtonStyle.Glass, + size = RadiantButtonSize.Small, + ) + RadiantButton( + text = stringResource(R.string.play_protect_warning_ok), + onClick = { onDismiss(rememberedNeverShow) }, + style = RadiantButtonStyle.Accent, + size = RadiantButtonSize.Small, + ) + } + } + } } private fun Activity.launchPlayProtect() { diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantButton.kt new file mode 100644 index 0000000..a165a1c --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantButton.kt @@ -0,0 +1,321 @@ +package com.meowarex.rlmobile.ui.components.radiant + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.theme.* + +/** + * How much a button shrinks while held. + */ +private const val PRESS_SCALE = 0.965f + +enum class RadiantButtonStyle { + /** Gradient fill + glow. */ + Accent, + + /** Translucent accent wash + hairline. */ + Glass, + + /** Hairline only. */ + Ghost, + + /** Error-tinted glass. */ + Danger, +} + +/** + * Sizes are matched to the pre-redesign UI. + */ +enum class RadiantButtonSize( + val height: Dp, + val horizontalPadding: Dp, + val iconSize: Dp, + val corner: Dp, +) { + Small(36.dp, 14.dp, 16.dp, 12.dp), + Medium(40.dp, 18.dp, 18.dp, 14.dp), + Large(46.dp, 22.dp, 20.dp, 16.dp), +} + +/** + * The app's button. (forgot specifically what this means but.. claude did this part) + */ +@Composable +fun RadiantButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier, + icon: Painter? = null, + style: RadiantButtonStyle = RadiantButtonStyle.Glass, + size: RadiantButtonSize = RadiantButtonSize.Medium, + enabled: Boolean = true, + fillWidth: Boolean = false, +) { + val ui = radiantStyle + + // Legacy renders the old Material widget + if (ui.legacy) { + LegacyButton( + text = text, + onClick = onClick, + icon = icon, + variant = style, + enabled = enabled, + fillWidth = fillWidth, + modifier = modifier, + ) + return + } + + val interaction = remember(::MutableInteractionSource) + val pressed by interaction.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (pressed && enabled) ui.pressScale else 1f, + label = "RadiantButtonScale", + ) + + val shape: Shape = RoundedCornerShape(size.corner) + val scheme = MaterialTheme.colorScheme + + val contentColor = when { + !enabled -> scheme.onSurface.copy(alpha = 0.38f) + style == RadiantButtonStyle.Accent -> scheme.onPrimary + style == RadiantButtonStyle.Danger -> scheme.error + else -> scheme.primary + } + + // Glow only on the accent style + val glowModifier = if (style == RadiantButtonStyle.Accent && enabled) { + Modifier.glow( + color = scheme.primary, + radius = if (pressed) 22.dp else 14.dp, + shape = shape, + ) + } else { + Modifier + } + + val fillModifier = when (style) { + RadiantButtonStyle.Accent -> Modifier.background( + brush = if (enabled) accentBrush else accentBrushDisabled, + shape = shape, + ) + + RadiantButtonStyle.Glass -> Modifier.glassSurface( + shape = shape, + tint = scheme.primary, + fillAlpha = if (pressed) RadiantDesign.GLASS_ALPHA_PRESSED else RadiantDesign.GLASS_ALPHA, + ) + + RadiantButtonStyle.Danger -> Modifier.glassSurface( + shape = shape, + tint = scheme.error, + fillAlpha = if (pressed) RadiantDesign.GLASS_ALPHA_PRESSED else RadiantDesign.GLASS_ALPHA, + ) + + RadiantButtonStyle.Ghost -> Modifier.border( + width = RadiantDesign.Hairline, + brush = hairlineBrush(scheme.onSurface, RadiantDesign.BORDER_ALPHA * 2f), + shape = shape, + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .scale(scale) + .then(if (fillWidth) Modifier.fillMaxWidth() else Modifier) + .height(size.height) + .then(glowModifier) + .then(fillModifier) + .clip(shape) + .clickable( + interactionSource = interaction, + indication = null, + enabled = enabled, + role = Role.Button, + onClick = onClick, + ) + .padding(horizontal = size.horizontalPadding) + .alpha(if (enabled) 1f else 0.6f), + ) { + if (icon != null) { + Icon( + painter = icon, + contentDescription = null, + tint = contentColor, + modifier = Modifier.size(size.iconSize), + ) + } + + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + color = contentColor, + textAlign = TextAlign.Center, + maxLines = 1, + ) + } +} + +/** + * Icon-only sibling of RadiantButton. + */ +@Composable +fun RadiantIconButton( + icon: Painter, + contentDescription: String?, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + subtle: Boolean = false, + accent: Boolean = false, + size: Dp = 40.dp, + tint: Color? = null, +) { + val ui = radiantStyle + + if (ui.legacy) { + val content = @Composable { + Icon( + painter = icon, + contentDescription = contentDescription, + tint = tint ?: LocalContentColor.current, + ) + } + if (subtle) { + IconButton(onClick = onClick, enabled = enabled, modifier = modifier) { content() } + } else { + FilledTonalIconButton(onClick = onClick, enabled = enabled, modifier = modifier) { content() } + } + return + } + + val interaction = remember(::MutableInteractionSource) + val pressed by interaction.collectIsPressedAsState() + val scale by animateFloatAsState( + targetValue = if (pressed && enabled) 0.9f else 1f, + label = "RadiantIconButtonScale", + ) + + val shape = RoundedCornerShape(size / 3f) + val scheme = MaterialTheme.colorScheme + + val container = when { + subtle -> Modifier + accent -> Modifier + .glow(scheme.primary, if (pressed) 18.dp else 12.dp, shape) + .background(accentBrush, shape) + + else -> Modifier.glassSurface(shape = shape, tint = scheme.primary) + } + + val contentColor = tint ?: when { + !enabled -> scheme.onSurface.copy(alpha = 0.38f) + accent -> scheme.onPrimary + subtle -> LocalContentColor.current + else -> scheme.primary + } + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .scale(scale) + .size(size) + .then(container) + .clip(shape) + .clickable( + interactionSource = interaction, + indication = null, + enabled = enabled, + role = Role.Button, + onClick = onClick, + ), + ) { + Icon( + painter = icon, + contentDescription = contentDescription, + tint = contentColor, + modifier = Modifier.size(size * 0.48f), + ) + } +} + +/** Border stroke matching the hairline (technically a duplicate but i was lazy)*/ +@Composable +fun radiantHairline( + color: Color = MaterialTheme.colorScheme.onSurface, + alpha: Float = RadiantDesign.BORDER_ALPHA, +) = BorderStroke(RadiantDesign.Hairline, hairlineBrush(color, alpha)) + +/** + * Material equivalents for RadiantButtonStyle. + */ +@Composable +private fun LegacyButton( + text: String, + onClick: () -> Unit, + icon: Painter?, + variant: RadiantButtonStyle, + enabled: Boolean, + fillWidth: Boolean, + modifier: Modifier = Modifier, +) { + val m = if (fillWidth) modifier.fillMaxWidth() else modifier + + val content: @Composable RowScope.() -> Unit = { + if (icon != null) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(ButtonDefaults.IconSpacing)) + } + Text(text = text, maxLines = 1) + } + + when (variant) { + RadiantButtonStyle.Accent -> + Button(onClick = onClick, enabled = enabled, modifier = m, content = content) + + RadiantButtonStyle.Glass -> + FilledTonalButton(onClick = onClick, enabled = enabled, modifier = m, content = content) + + RadiantButtonStyle.Ghost -> + OutlinedButton(onClick = onClick, enabled = enabled, modifier = m, content = content) + + RadiantButtonStyle.Danger -> FilledTonalButton( + onClick = onClick, + enabled = enabled, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), + modifier = m, + content = content, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantContainers.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantContainers.kt new file mode 100644 index 0000000..b16f68e --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantContainers.kt @@ -0,0 +1,181 @@ +package com.meowarex.rlmobile.ui.components.radiant + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import com.meowarex.rlmobile.ui.theme.* + +/** + * The house card. (idk what a house card is, GPT 5.6 said it and i thought it was funny so i left it in everywhere) + */ +@Composable +fun RadiantCard( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(24.dp), + base: Color = MaterialTheme.colorScheme.surfaceContainerLow, + active: Boolean = false, + onClick: (() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + if (radiantStyle.legacy) { + // Material's own elevated card. + ElevatedCard( + shape = shape, + modifier = modifier + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), + content = content, + ) + return + } + + Column( + modifier = modifier + .radiantSurface(shape = shape, base = base, active = active) + .clip(shape) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), + content = content, + ) +} + +/** + * Dialog shell. + */ +@Composable +fun RadiantDialog( + onDismissRequest: () -> Unit, + title: String, + modifier: Modifier = Modifier, + icon: @Composable (() -> Unit)? = null, + confirmText: String? = null, + onConfirm: (() -> Unit)? = null, + confirmEnabled: Boolean = true, + dismissText: String? = null, + onDismissAction: (() -> Unit)? = null, + destructive: Boolean = false, + content: @Composable ColumnScope.() -> Unit, +) { + val shape = RoundedCornerShape(30.dp) + + if (radiantStyle.legacy) { + AlertDialog( + onDismissRequest = onDismissRequest, + icon = icon, + title = { Text(title) }, + text = { Column(content = content) }, + confirmButton = { + if (confirmText != null) { + TextButton( + onClick = onConfirm ?: onDismissRequest, + enabled = confirmEnabled, + ) { Text(confirmText) } + } + }, + dismissButton = dismissText?.let { + { + TextButton(onClick = onDismissAction ?: onDismissRequest) { Text(it) } + } + }, + modifier = modifier, + ) + return + } + + Dialog(onDismissRequest = onDismissRequest) { + Column( + modifier = modifier + .fillMaxWidth() + .radiantSurface( + shape = shape, + base = MaterialTheme.colorScheme.surfaceContainer, + ) + .clip(shape) + .padding(24.dp), + ) { + if (icon != null) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .padding(bottom = 14.dp) + .size(44.dp) + .clip(RoundedCornerShape(15.dp)) + .glassSurface(RoundedCornerShape(15.dp)), + ) { + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.primary, + ) { icon() } + } + } + + Text( + text = title, + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 10.dp), + ) + + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onSurfaceVariant, + ) { + ProvideTextStyle(MaterialTheme.typography.bodyMedium) { + content() + } + } + + if (confirmText != null || dismissText != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(top = 22.dp) + .fillMaxWidth(), + ) { + if (dismissText != null) { + RadiantButton( + text = dismissText, + onClick = onDismissAction ?: onDismissRequest, + style = RadiantButtonStyle.Ghost, + size = RadiantButtonSize.Small, + ) + } + if (confirmText != null) { + RadiantButton( + text = confirmText, + onClick = onConfirm ?: onDismissRequest, + enabled = confirmEnabled, + style = if (destructive) { + RadiantButtonStyle.Danger + } else { + RadiantButtonStyle.Accent + }, + size = RadiantButtonSize.Small, + ) + } + } + } + } + } +} + +/** + * Section rule. (short accent segment fading into a hairline) + */ +@Composable +fun RadiantDivider( + modifier: Modifier = Modifier, +) { + HorizontalDivider( + thickness = RadiantDesign.Hairline, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = RadiantDesign.BORDER_ALPHA * 1.4f), + modifier = modifier, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantInputs.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantInputs.kt new file mode 100644 index 0000000..b1afa5c --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantInputs.kt @@ -0,0 +1,306 @@ +package com.meowarex.rlmobile.ui.components.radiant + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.theme.* + +/** + * Text field. + */ +@Composable +fun RadiantTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + placeholder: String? = null, + isError: Boolean = false, + enabled: Boolean = true, + singleLine: Boolean = true, + visualTransformation: VisualTransformation = VisualTransformation.None, + trailing: @Composable (() -> Unit)? = null, +) { + if (radiantStyle.native) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + enabled = enabled, + isError = isError, + singleLine = singleLine, + visualTransformation = visualTransformation, + placeholder = placeholder?.let { { Text(it) } }, + trailingIcon = trailing, + modifier = modifier.fillMaxWidth(), + ) + return + } + + val scheme = MaterialTheme.colorScheme + val interaction = remember(::MutableInteractionSource) + val focused by interaction.collectIsFocusedAsState() + val shape = RoundedCornerShape(16.dp) + + val borderColor by animateColorAsState( + targetValue = when { + isError -> scheme.error + focused -> scheme.primary + else -> scheme.onSurface.copy(alpha = RadiantDesign.BORDER_ALPHA * 2f) + }, + label = "TextFieldBorder", + ) + val borderWidth by animateDpAsState( + targetValue = if (focused || isError) 2.dp else RadiantDesign.Hairline, + label = "TextFieldBorderWidth", + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxWidth() + .background(scheme.surfaceContainerLowest.copy(alpha = 0.7f), shape) + .border(borderWidth, borderColor, shape) + .clip(shape) + .padding(horizontal = 16.dp, vertical = 4.dp) + .heightIn(min = 52.dp), + ) { + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier.weight(1f), + ) { + if (value.isEmpty() && placeholder != null) { + Text( + text = placeholder, + style = MaterialTheme.typography.bodyLarge, + color = scheme.onSurfaceVariant.copy(alpha = 0.6f), + ) + } + + BasicTextField( + value = value, + onValueChange = onValueChange, + enabled = enabled, + singleLine = singleLine, + interactionSource = interaction, + visualTransformation = visualTransformation, + textStyle = MaterialTheme.typography.bodyLarge.copy(color = scheme.onSurface), + cursorBrush = SolidColor(scheme.primary), + modifier = Modifier.fillMaxWidth(), + ) + } + + trailing?.invoke() + } +} + +/** + * Slider with a gradient and glow. + */ +@Composable +fun RadiantSlider( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + valueRange: ClosedFloatingPointRange = 0f..1f, + steps: Int = 0, + enabled: Boolean = true, + onValueChangeFinished: (() -> Unit)? = null, +) { + val scheme = MaterialTheme.colorScheme + val interaction = remember(::MutableInteractionSource) + + if (radiantStyle.native) { + Slider( + value = value, + onValueChange = onValueChange, + onValueChangeFinished = onValueChangeFinished, + valueRange = valueRange, + steps = steps, + enabled = enabled, + modifier = modifier, + ) + return + } + + Slider( + value = value, + onValueChange = onValueChange, + onValueChangeFinished = onValueChangeFinished, + valueRange = valueRange, + steps = steps, + enabled = enabled, + interactionSource = interaction, + modifier = modifier, + track = { state -> + val fraction = if (valueRange.endInclusive > valueRange.start) { + ((state.value - valueRange.start) / (valueRange.endInclusive - valueRange.start)) + .coerceIn(0f, 1f) + } else 0f + + Box( + modifier = Modifier + .fillMaxWidth() + .height(6.dp) + .clip(CircleShape) + .background(scheme.surfaceContainerHighest), + ) { + Box( + modifier = Modifier + .fillMaxWidth(fraction) + .fillMaxHeight() + .clip(CircleShape) + .background(accentBrush), + ) + } + }, + thumb = { + Box( + modifier = Modifier + .glow(scheme.primary, 10.dp, CircleShape, alpha = 0.6f) + .size(20.dp) + .clip(CircleShape) + .background(scheme.onPrimary, CircleShape) + .border(3.dp, scheme.primary, CircleShape), + ) + }, + ) +} + +/** + * Pill segmented control. + */ +@Composable +fun RadiantSegmented( + options: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + if (radiantStyle.native) { + SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) { + options.forEachIndexed { index, label -> + SegmentedButton( + selected = index == selectedIndex, + onClick = { onSelect(index) }, + enabled = enabled, + shape = SegmentedButtonDefaults.itemShape(index = index, count = options.size), + icon = {}, + label = { Text(text = label, maxLines = 1) }, + ) + } + } + return + } + + val scheme = MaterialTheme.colorScheme + val trackShape = RoundedCornerShape(16.dp) + + Row( + modifier = modifier + .fillMaxWidth() + .background(scheme.surfaceContainerLowest.copy(alpha = 0.8f), trackShape) + .border( + RadiantDesign.Hairline, + hairlineBrush(scheme.onSurface, RadiantDesign.BORDER_ALPHA), + trackShape, + ) + .clip(trackShape) + .padding(3.dp), + horizontalArrangement = Arrangement.spacedBy(3.dp), + ) { + options.forEachIndexed { index, label -> + val selected = index == selectedIndex + val pillShape = RoundedCornerShape(13.dp) + val alpha by animateFloatAsState( + targetValue = if (selected) 1f else 0f, + label = "SegmentedPill", + ) + + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .weight(1f) + .height(34.dp) + .clip(pillShape) + .then( + if (alpha > 0.01f) { + Modifier.background( + accentBrush, + pillShape, + alpha = alpha, + ) + } else Modifier + ) + .clickable( + enabled = enabled, + role = Role.RadioButton, + onClick = { onSelect(index) }, + ), + ) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = if (selected) scheme.onPrimary else scheme.onSurfaceVariant, + maxLines = 1, + ) + } + } + } +} + +/** something.. */ +@Composable +fun RadiantLabeledSlider( + label: String, + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + valueRange: ClosedFloatingPointRange = 0f..1f, + steps: Int = 0, + trailingText: String? = null, +) { + Column(modifier = modifier.fillMaxWidth()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.weight(1f), + ) + if (trailingText != null) { + Text( + text = trailingText, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + } + RadiantSlider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = steps, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantToggles.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantToggles.kt new file mode 100644 index 0000000..dc162e7 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/radiant/RadiantToggles.kt @@ -0,0 +1,245 @@ +package com.meowarex.rlmobile.ui.components.radiant + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.selectable +import androidx.compose.foundation.selection.toggleable +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.theme.* + +private val TrackWidth = 52.dp +private val TrackHeight = 32.dp +private val ThumbSize = 24.dp +private val ThumbInset = 4.dp + +/** + * Capsule switch. + */ +@Composable +fun RadiantSwitch( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, +) { + if (radiantStyle.native) { + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + interactionSource = interactionSource, + modifier = modifier, + ) + return + } + + val scheme = MaterialTheme.colorScheme + val offset by animateDpAsState( + targetValue = if (checked) TrackWidth - ThumbSize - ThumbInset else ThumbInset, + animationSpec = spring(dampingRatio = 0.55f, stiffness = Spring.StiffnessMediumLow), + label = "SwitchThumbOffset", + ) + val trackColor by animateColorAsState( + targetValue = if (checked) Color.Transparent else scheme.surfaceContainerHighest, + label = "SwitchTrack", + ) + val thumbColor by animateColorAsState( + targetValue = if (checked) scheme.onPrimary else scheme.outline, + label = "SwitchThumb", + ) + + val trackShape = RoundedCornerShape(percent = 50) + + Box( + contentAlignment = Alignment.CenterStart, + modifier = modifier + .alpha(if (enabled) 1f else 0.45f) + .then( + if (checked && enabled) { + Modifier.glow(scheme.primary, 14.dp, trackShape, alpha = 0.5f) + } else Modifier + ) + .size(TrackWidth, TrackHeight) + .clip(trackShape) + .then(if (checked) Modifier.background(accentBrush, trackShape) else Modifier) + .background(trackColor, trackShape) + .border( + width = RadiantDesign.Hairline, + brush = hairlineBrush( + color = if (checked) scheme.primary else scheme.onSurface, + alpha = if (checked) RadiantDesign.BORDER_ALPHA_ACTIVE else RadiantDesign.BORDER_ALPHA * 2f, + ), + shape = trackShape, + ) + .then( + if (onCheckedChange != null) { + Modifier.toggleable( + value = checked, + enabled = enabled, + role = Role.Switch, + interactionSource = interactionSource, + indication = null, + onValueChange = onCheckedChange, + ) + } else Modifier + ), + ) { + Box( + modifier = Modifier + .padding(start = offset) + .size(ThumbSize) + .clip(CircleShape) + .background(thumbColor, CircleShape), + ) + } +} + +/** + * Ring & Core radio button. + */ +@Composable +fun RadiantRadio( + selected: Boolean, + onClick: (() -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource? = null, +) { + if (radiantStyle.native) { + RadioButton( + selected = selected, + onClick = onClick, + enabled = enabled, + interactionSource = interactionSource, + modifier = modifier, + ) + return + } + + val scheme = MaterialTheme.colorScheme + val coreScale by animateFloatAsState( + targetValue = if (selected) 1f else 0f, + animationSpec = spring(dampingRatio = 0.5f, stiffness = Spring.StiffnessMediumLow), + label = "RadioCore", + ) + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .alpha(if (enabled) 1f else 0.45f) + .size(20.dp) + .clip(CircleShape) + .border( + width = 2.dp, + color = if (selected) scheme.primary else scheme.outline.copy(alpha = 0.6f), + shape = CircleShape, + ) + .then( + if (onClick != null) { + Modifier.selectable( + selected = selected, + enabled = enabled, + role = Role.RadioButton, + interactionSource = interactionSource, + indication = null, + onClick = onClick, + ) + } else Modifier + ), + ) { + Box( + modifier = Modifier + .scale(coreScale) + .size(10.dp) + .clip(CircleShape) + .background(accentBrush, CircleShape), + ) + } +} + +/** + * Squircle checkbox that fills with the accent gradient when checked. + */ +@Composable +fun RadiantCheckbox( + checked: Boolean, + onCheckedChange: ((Boolean) -> Unit)?, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { + if (radiantStyle.native) { + Checkbox( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + modifier = modifier, + ) + return + } + + val scheme = MaterialTheme.colorScheme + val shape = RoundedCornerShape(7.dp) + val fillScale by animateFloatAsState( + targetValue = if (checked) 1f else 0f, + animationSpec = spring(dampingRatio = 0.5f, stiffness = Spring.StiffnessMediumLow), + label = "CheckboxFill", + ) + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .alpha(if (enabled) 1f else 0.45f) + .size(20.dp) + .clip(shape) + .border( + width = 2.dp, + color = if (checked) Color.Transparent else scheme.outline.copy(alpha = 0.6f), + shape = shape, + ) + .then( + if (onCheckedChange != null) { + Modifier.toggleable( + value = checked, + enabled = enabled, + role = Role.Checkbox, + onValueChange = onCheckedChange, + ) + } else Modifier + ), + ) { + Box( + modifier = Modifier + .scale(fillScale) + .matchParentSize() + .clip(shape) + .background(accentBrush, shape), + ) + + if (checked) { + Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = null, + tint = scheme.onPrimary, + modifier = Modifier.size(13.dp), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsGroup.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsGroup.kt new file mode 100644 index 0000000..610242a --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsGroup.kt @@ -0,0 +1,58 @@ +package com.meowarex.rlmobile.ui.components.settings + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** + * Where a row sits inside a settings group. (decides corner merging) + */ +enum class GroupPosition { + Top, + Middle, + Bottom, + Single; + + val shape: Shape + get() = when (this) { + Top -> RoundedCornerShape(OUTER, OUTER, INNER, INNER) + Middle -> RoundedCornerShape(INNER) + Bottom -> RoundedCornerShape(INNER, INNER, OUTER, OUTER) + Single -> RoundedCornerShape(OUTER) + } + + private companion object { + val OUTER = 24.dp + val INNER = 6.dp + } +} + +/** + * Resolves the position of index [index] in a group of [count] rows. + */ +fun groupPosition(index: Int, count: Int): GroupPosition = when { + count <= 1 -> GroupPosition.Single + index == 0 -> GroupPosition.Top + index == count - 1 -> GroupPosition.Bottom + else -> GroupPosition.Middle +} + +/** + * Container for settings rows. (3dp gap separates the tiles) + */ +@Composable +fun SettingsGroup( + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(3.dp), + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + content = content, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsHeader.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsHeader.kt index 2725d3b..fb92608 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsHeader.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsHeader.kt @@ -12,6 +12,6 @@ fun SettingsHeader( ) { TextDivider( text = text, - modifier = Modifier.padding(18.dp, 20.dp, 18.dp, 10.dp) + modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 26.dp, bottom = 12.dp), ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsItem.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsItem.kt index 1c5c1e6..8e69296 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsItem.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsItem.kt @@ -1,57 +1,93 @@ package com.meowarex.rlmobile.ui.components.settings +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ProvideTextStyle +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.theme.accentBrush +import com.meowarex.rlmobile.ui.theme.glow +import com.meowarex.rlmobile.ui.theme.radiantStyle +import com.meowarex.rlmobile.ui.theme.radiantSurface +/** + * A settings row rendered as a tile on the house surface. + */ @Composable fun SettingsItem( text: @Composable () -> Unit, secondaryText: @Composable (() -> Unit) = { }, icon: @Composable (() -> Unit) = { }, + position: GroupPosition = GroupPosition.Single, + onClick: (() -> Unit)? = null, modifier: Modifier = Modifier, trailing: @Composable (() -> Unit) = { }, ) { + val ui = radiantStyle + val iconShape = RoundedCornerShape(12.dp) + Row( modifier = modifier - .heightIn(min = 64.dp) .fillMaxWidth() - .padding(horizontal = 32.dp, vertical = 12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically + // Legacy rows are flat and full bleed with no tile background or grouping. (because i'm lazy) + .then( + if (ui.groupedSettings) { + Modifier + .radiantSurface(shape = position.shape) + .clip(position.shape) + } else Modifier + ) + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier) + .heightIn(min = 64.dp) + .padding( + horizontal = if (ui.groupedSettings) 16.dp else 32.dp, + vertical = if (ui.groupedSettings) 14.dp else 12.dp, + ), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, ) { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), - modifier = Modifier.weight(2f, true) - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, + if (ui.groupedSettings) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .glow(radius = 10.dp, shape = iconShape, alpha = 0.35f) + .size(36.dp) + .clip(iconShape) + .background(accentBrush, iconShape), ) { - Box(modifier = Modifier.size(20.dp)) { - icon() + CompositionLocalProvider( + LocalContentColor provides MaterialTheme.colorScheme.onPrimary, + ) { + Box(Modifier.size(20.dp)) { icon() } } + } + } else { + Box(Modifier.size(20.dp)) { icon() } + } - ProvideTextStyle(MaterialTheme.typography.titleSmall) { - text() - } + Column( + verticalArrangement = Arrangement.spacedBy(3.dp), + modifier = Modifier.weight(1f), + ) { + ProvideTextStyle(MaterialTheme.typography.titleSmall) { + text() } ProvideTextStyle( - MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(0.6f) + MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, ) ) { secondaryText() } } - Spacer(Modifier.weight(0.05f, true)) - trailing() } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsSwitch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsSwitch.kt index 62bbe41..d6d2f0a 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsSwitch.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsSwitch.kt @@ -1,10 +1,9 @@ package com.meowarex.rlmobile.ui.components.settings -import androidx.compose.foundation.clickable -import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.meowarex.rlmobile.ui.components.radiant.RadiantSwitch @Composable fun SettingsSwitch( @@ -13,11 +12,14 @@ fun SettingsSwitch( disabled: Boolean = false, icon: @Composable () -> Unit = {}, pref: Boolean, + position: GroupPosition = GroupPosition.Single, onPrefChange: (Boolean) -> Unit, modifier: Modifier = Modifier, ) { SettingsItem( - modifier = modifier.clickable(enabled = !disabled) { onPrefChange(!pref) }, + modifier = modifier, + position = position, + onClick = if (disabled) null else ({ onPrefChange(!pref) }), text = { Text(text = label, softWrap = true) }, icon = icon, secondaryText = { @@ -26,7 +28,7 @@ fun SettingsSwitch( } } ) { - Switch( + RadiantSwitch( checked = pref, enabled = !disabled, onCheckedChange = { onPrefChange(!pref) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsTextField.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsTextField.kt index 3adb17b..22198fb 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsTextField.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/settings/SettingsTextField.kt @@ -1,11 +1,12 @@ package com.meowarex.rlmobile.ui.components.settings import androidx.compose.foundation.layout.* -import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.components.radiant.RadiantTextField @Composable fun SettingsTextField( @@ -15,15 +16,21 @@ fun SettingsTextField( error: Boolean = false, onPrefChange: (String) -> Unit, ) { - Box(modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)) { - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp), + ) { + // Label sits above the field rather than notching into its border. + Text( + text = label, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + RadiantTextField( value = pref, onValueChange = onPrefChange, enabled = !disabled, - label = { Text(label) }, isError = error, - singleLine = true ) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/BackButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/BackButton.kt new file mode 100644 index 0000000..24e10bc --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/BackButton.kt @@ -0,0 +1,33 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.activity.compose.LocalActivity +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.LocalNavigator +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.util.back + +/** + * Standalone back button for interacting with th navigator. + */ +@Composable +fun BackButton() { + val navigator = LocalNavigator.current + val activity = LocalActivity.current + + IconButton( + onClick = { + navigator?.back(activity) + }, + ) { + Icon( + painter = painterResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.navigation_back), + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Contributors.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Contributors.kt new file mode 100644 index 0000000..7ecd6c3 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Contributors.kt @@ -0,0 +1,74 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontStyle +import androidx.compose.ui.unit.dp +import coil3.compose.SubcomposeAsyncImage +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.network.models.Contributor +import com.valentinilk.shimmer.shimmer + +@Composable +fun ContributorCommitsItem( + user: Contributor, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .clickable { uriHandler.openUri("https://github.com/${user.username}") } + .padding(horizontal = 16.dp, vertical = 8.dp) + .fillMaxWidth(), + ) { + SubcomposeAsyncImage( + model = user.avatarUrl, + contentDescription = user.username, + error = { + Surface( + content = {}, + tonalElevation = 2.dp, + modifier = Modifier + .fillMaxSize() + .shimmer(), + ) + }, + modifier = Modifier + .padding(top = 6.dp) + .size(45.dp) + .clip(CircleShape) + ) + + Column { + Text( + text = user.username, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = stringResource(R.string.contributors_contributions, user.commits), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + ) + + Text( + text = user.repositories.joinToString { it.name }, + fontStyle = FontStyle.Italic, + style = MaterialTheme.typography.bodySmall + .copy(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)), + modifier = Modifier.padding(top = 4.dp), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/InteractiveSlider.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/InteractiveSlider.kt new file mode 100644 index 0000000..79300f0 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/InteractiveSlider.kt @@ -0,0 +1,115 @@ +/** + * MIT License + * + * Copyright (c) 2025 zt64 + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// Adapted from compose-pipette: (use someone elses code because love not doing things ourselves) +// https://github.com/zt64/compose-pipette/blob/3e9fd958a315dceb142bf30250b4614ecde4e723/sample/src/commonMain/kotlin/dev/zt64/compose/pipette/sample/SampleSlider.kt + +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Slider +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.center +import androidx.compose.ui.graphics.* +import androidx.compose.ui.unit.dp + +@Composable +fun InteractiveSlider( + value: Float, + onValueChange: (Float) -> Unit, + valueRange: ClosedFloatingPointRange, + brush: Brush, + thumbColor: Color = MaterialTheme.colorScheme.primary, + modifier: Modifier = Modifier, +) { + val interactionSource = remember(::MutableInteractionSource) + + Slider( + value = value, + onValueChange = onValueChange, + thumb = { + val interactions = remember { mutableStateListOf() } + + LaunchedEffect(interactionSource) { + interactionSource.interactions.collect { interaction -> + when (interaction) { + is PressInteraction.Press -> interactions.add(interaction) + is PressInteraction.Release -> interactions.remove(interaction.press) + is PressInteraction.Cancel -> interactions.remove(interaction.press) + is DragInteraction.Start -> interactions.add(interaction) + is DragInteraction.Stop -> interactions.remove(interaction.start) + is DragInteraction.Cancel -> interactions.remove(interaction.start) + } + } + } + + Box( + modifier = Modifier + .size(28.dp) + .hoverable(interactionSource = interactionSource), + ) { + val visualSize by remember { + derivedStateOf { + if (interactions.isNotEmpty()) 28.dp else 24.dp + } + } + + Spacer( + modifier = Modifier + .size(visualSize) + .align(Alignment.Center) + .background(thumbColor, CircleShape), + ) + } + }, + track = { + Canvas( + modifier = Modifier + .widthIn(max = 700.dp) + .height(12.dp) + .fillMaxWidth(), + ) { + drawLine( + brush = brush, + start = Offset(0f, size.center.y), + end = Offset(size.width, size.center.y), + strokeWidth = size.height, + cap = StrokeCap.Round, + ) + } + }, + valueRange = valueRange, + interactionSource = interactionSource, + modifier = modifier, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Label.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Label.kt new file mode 100644 index 0000000..46efa21 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Label.kt @@ -0,0 +1,44 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.util.thenIf + +@Composable +fun Label( + name: String, + description: String?, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = modifier, + ) { + Text( + text = name, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier + .thenIf(description == null) { padding(bottom = 4.dp) }, + ) + + if (description != null) { + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .alpha(.7f) + .padding(bottom = 4.dp), + ) + } + + content() + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/LoadFailure.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/LoadFailure.kt new file mode 100644 index 0000000..91a3771 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/LoadFailure.kt @@ -0,0 +1,35 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R + +@Composable +fun LoadFailure(modifier: Modifier = Modifier) { + Column( + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + modifier = modifier, + ) { + Icon( + painter = painterResource(R.drawable.ic_warning), + tint = MaterialTheme.colorScheme.error, + contentDescription = null, + modifier = Modifier.size(34.dp), + ) + Text( + text = stringResource(R.string.network_load_fail), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.padding(top = 4.dp, bottom = 2.dp), + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/MainActionButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/MainActionButton.kt new file mode 100644 index 0000000..719b3c2 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/MainActionButton.kt @@ -0,0 +1,51 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp + +@Composable +fun MainActionButton( + text: String, + icon: Painter, + onClick: () -> Unit, + enabled: Boolean = true, + colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + ), + modifier: Modifier = Modifier, +) { + FilledTonalIconButton( + shape = MaterialTheme.shapes.medium, + colors = colors, + onClick = onClick, + enabled = enabled, + modifier = modifier + .fillMaxWidth() + .height(46.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + + Text( + text = text, + style = MaterialTheme.typography.labelLarge, + textAlign = TextAlign.Center, + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ProjectHeader.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ProjectHeader.kt new file mode 100644 index 0000000..17d5329 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ProjectHeader.kt @@ -0,0 +1,53 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.meowarex.rlmobile.R + +@Composable +fun ProjectHeader(modifier: Modifier = Modifier) { + val uriHandler = LocalUriHandler.current + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = modifier, + ) { + Text( + text = stringResource(R.string.rlmobile), + style = MaterialTheme.typography.titleMedium.copy(fontSize = 26.sp) + ) + + Text( + text = stringResource(R.string.app_description), + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = .6f) + ), + textAlign = TextAlign.Center, + ) + + Row( + horizontalArrangement = Arrangement.Center, + ) { + TextButton(onClick = { uriHandler.openUri("https://github.com/meowarex/rl-mobile") }) { + Icon( + painter = painterResource(R.drawable.ic_account_github_white_24dp), + contentDescription = null, + modifier = Modifier.padding(end = ButtonDefaults.IconSpacing), + ) + Text(text = stringResource(R.string.github)) + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ResetToDefaultButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ResetToDefaultButton.kt new file mode 100644 index 0000000..316ebbf --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/ResetToDefaultButton.kt @@ -0,0 +1,39 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.animation.* +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.util.mirrorVertically + +@Composable +fun ResetToDefaultButton( + enabled: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = enabled, + enter = fadeIn() + slideInHorizontally(), + exit = fadeOut() + slideOutHorizontally(), + modifier = modifier, + ) { + IconButton(onClick = onClick) { + Icon( + painter = painterResource(R.drawable.ic_refresh), + tint = MaterialTheme.colorScheme.secondary, + contentDescription = stringResource(R.string.action_reset_default), + modifier = Modifier + .mirrorVertically() + .size(26.dp), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/SegmentedButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/SegmentedButton.kt similarity index 93% rename from Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/SegmentedButton.kt rename to Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/SegmentedButton.kt index 9dc8879..83d373a 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/components/SegmentedButton.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/SegmentedButton.kt @@ -1,4 +1,6 @@ -package com.meowarex.rlmobile.ui.components +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* import androidx.compose.foundation.* import androidx.compose.foundation.layout.* diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Tag.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Tag.kt new file mode 100644 index 0000000..3c1138a --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Tag.kt @@ -0,0 +1,27 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun Tag(text: String) { + Surface( + color = MaterialTheme.colorScheme.primaryContainer, + shape = RoundedCornerShape(6.dp), + ) { + Text( + text = text, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 3.dp), + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/TextDivider.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/TextDivider.kt new file mode 100644 index 0000000..2287293 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/TextDivider.kt @@ -0,0 +1,37 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp + +@Composable +fun TextDivider( + text: String, + style: TextStyle = MaterialTheme.typography.bodyMedium, + modifier: Modifier = Modifier, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier + .fillMaxWidth(), + ) { + HorizontalDivider(Modifier.weight(1f)) + + Text( + text = text, + style = style, + color = MaterialTheme.colorScheme.outline, + fontWeight = FontWeight.SemiBold, + ) + + HorizontalDivider(Modifier.weight(1f)) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/VersionDisplay.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/VersionDisplay.kt new file mode 100644 index 0000000..cec4310 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/VersionDisplay.kt @@ -0,0 +1,32 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.* +import com.meowarex.rlmobile.ui.util.TidalVersion + +@Composable +fun VersionDisplay( + version: TidalVersion, + prefix: (@Composable AnnotatedString.Builder.() -> Unit)? = null, + style: TextStyle = MaterialTheme.typography.labelLarge, + modifier: Modifier = Modifier, +) { + Text( + text = buildAnnotatedString { + prefix?.invoke(this) + + if (version is TidalVersion.Existing) { + append(version.name) + append(" - ") + } + append(version.toDisplayName()) + }, + style = style, + modifier = modifier, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Wakelock.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Wakelock.kt new file mode 100644 index 0000000..84a73e5 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/Wakelock.kt @@ -0,0 +1,28 @@ +package com.meowarex.rlmobile.ui.legacy.components + +import com.meowarex.rlmobile.ui.components.* + +import android.view.WindowManager +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import cafe.adriel.voyager.navigator.currentOrThrow + +/** + * Maintain an active screen wakelock as long as [active] is true and this component is in scope. + */ +@Composable +fun Wakelock(active: Boolean = false) { + val window = LocalActivity.currentOrThrow.window + DisposableEffect(active) { + if (active) { + window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + + onDispose { + window?.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/InstallerAbortDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/InstallerAbortDialog.kt new file mode 100644 index 0000000..dd78185 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/InstallerAbortDialog.kt @@ -0,0 +1,64 @@ +package com.meowarex.rlmobile.ui.legacy.components.dialogs + +import com.meowarex.rlmobile.ui.components.dialogs.* + +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R + +@Composable +fun InstallerAbortDialog( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + FilledTonalButton( + onClick = onConfirm, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text(stringResource(R.string.action_exit_anyways)) + } + }, + dismissButton = { + TextButton( + onClick = onDismiss, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onErrorContainer + ), + ) { + Text(stringResource(R.string.action_cancel)) + } + }, + title = { + Text(stringResource(R.string.installer_abort_title)) + }, + text = { + Text( + text = stringResource(R.string.installer_abort_body), + textAlign = TextAlign.Center, + ) + }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_warning), + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + }, + containerColor = MaterialTheme.colorScheme.errorContainer, + iconContentColor = MaterialTheme.colorScheme.onErrorContainer, + titleContentColor = MaterialTheme.colorScheme.onErrorContainer, + textContentColor = MaterialTheme.colorScheme.onErrorContainer + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/PlayProtectDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/PlayProtectDialog.kt new file mode 100644 index 0000000..977cc98 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/dialogs/PlayProtectDialog.kt @@ -0,0 +1,105 @@ +package com.meowarex.rlmobile.ui.legacy.components.dialogs + +import com.meowarex.rlmobile.ui.components.dialogs.* + +import android.app.Activity +import android.content.Intent +import androidx.activity.compose.LocalActivity +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import cafe.adriel.voyager.navigator.currentOrThrow +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.theme.customColors + +@Composable +fun PlayProtectDialog( + onDismiss: (neverShow: Boolean) -> Unit, +) { + val activity = LocalActivity.currentOrThrow + val interactionSource = remember(::MutableInteractionSource) + var neverShow by rememberSaveable { mutableStateOf(false) } + val rememberedNeverShow by rememberUpdatedState(neverShow) + + AlertDialog( + onDismissRequest = { onDismiss(rememberedNeverShow) }, + dismissButton = { + FilledTonalButton(onClick = activity::launchPlayProtect) { + Text(stringResource(R.string.play_protect_warning_open_gpp)) + } + }, + confirmButton = { + FilledTonalButton( + onClick = { onDismiss(rememberedNeverShow) }, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Text(stringResource(R.string.play_protect_warning_ok)) + } + }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_protect_warning), + tint = MaterialTheme.customColors.warning, + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + }, + title = { Text(stringResource(R.string.play_protect_warning_title)) }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.play_protect_warning_desc), + textAlign = TextAlign.Center, + ) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + interactionSource = interactionSource, + indication = null, + onClick = { neverShow = !rememberedNeverShow }, + ) + .padding(end = 16.dp) + ) { + Checkbox( + checked = neverShow, + onCheckedChange = { neverShow = it }, + interactionSource = interactionSource, + ) + + Text(stringResource(R.string.play_protect_warning_disable)) + } + } + }, + properties = DialogProperties( + dismissOnClickOutside = false, + usePlatformDefaultWidth = false, + ), + modifier = Modifier + .padding(25.dp), + ) +} + +private fun Activity.launchPlayProtect() { + Intent("com.google.android.gms.settings.VERIFY_APPS_SETTINGS") + .setPackage("com.google.android.gms") + .addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS) + .also(::startActivity) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsHeader.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsHeader.kt new file mode 100644 index 0000000..29a5e44 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsHeader.kt @@ -0,0 +1,19 @@ +package com.meowarex.rlmobile.ui.legacy.components.settings + +import com.meowarex.rlmobile.ui.components.settings.* + +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.legacy.components.TextDivider + +@Composable +fun SettingsHeader( + text: String, +) { + TextDivider( + text = text, + modifier = Modifier.padding(18.dp, 20.dp, 18.dp, 10.dp) + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsItem.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsItem.kt new file mode 100644 index 0000000..c8c03f5 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsItem.kt @@ -0,0 +1,59 @@ +package com.meowarex.rlmobile.ui.legacy.components.settings + +import com.meowarex.rlmobile.ui.components.settings.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ProvideTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsItem( + text: @Composable () -> Unit, + secondaryText: @Composable (() -> Unit) = { }, + icon: @Composable (() -> Unit) = { }, + modifier: Modifier = Modifier, + trailing: @Composable (() -> Unit) = { }, +) { + Row( + modifier = modifier + .heightIn(min = 64.dp) + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 12.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.weight(2f, true) + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.size(20.dp)) { + icon() + } + + ProvideTextStyle(MaterialTheme.typography.titleSmall) { + text() + } + } + + ProvideTextStyle( + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(0.6f) + ) + ) { + secondaryText() + } + } + + Spacer(Modifier.weight(0.05f, true)) + + trailing() + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsSwitch.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsSwitch.kt new file mode 100644 index 0000000..cfbdd18 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsSwitch.kt @@ -0,0 +1,37 @@ +package com.meowarex.rlmobile.ui.legacy.components.settings + +import com.meowarex.rlmobile.ui.components.settings.* + +import androidx.compose.foundation.clickable +import androidx.compose.material3.Switch +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun SettingsSwitch( + label: String, + secondaryLabel: String? = null, + disabled: Boolean = false, + icon: @Composable () -> Unit = {}, + pref: Boolean, + onPrefChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, +) { + SettingsItem( + modifier = modifier.clickable(enabled = !disabled) { onPrefChange(!pref) }, + text = { Text(text = label, softWrap = true) }, + icon = icon, + secondaryText = { + secondaryLabel?.let { + Text(text = it) + } + } + ) { + Switch( + checked = pref, + enabled = !disabled, + onCheckedChange = { onPrefChange(!pref) } + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsTextField.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsTextField.kt new file mode 100644 index 0000000..c3d007b --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/components/settings/SettingsTextField.kt @@ -0,0 +1,31 @@ +package com.meowarex.rlmobile.ui.legacy.components.settings + +import com.meowarex.rlmobile.ui.components.settings.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun SettingsTextField( + label: String, + disabled: Boolean = false, + pref: String, + error: Boolean = false, + onPrefChange: (String) -> Unit, +) { + Box(modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)) { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + value = pref, + onValueChange = onPrefChange, + enabled = !disabled, + label = { Text(label) }, + isError = error, + singleLine = true + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/AboutScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/AboutScreen.kt new file mode 100644 index 0000000..89a0189 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/AboutScreen.kt @@ -0,0 +1,114 @@ +package com.meowarex.rlmobile.ui.legacy.screens.about + +import com.meowarex.rlmobile.ui.screens.about.* + +import android.os.Parcelable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import com.meowarex.rlmobile.BuildConfig +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.* +import com.meowarex.rlmobile.ui.legacy.screens.about.components.LeadContributor +import com.meowarex.rlmobile.ui.util.paddings.* +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +class AboutScreen : Screen, Parcelable { + @IgnoredOnParcel + override val key = "Legacy-About" + + @Composable + override fun Content() { + val model = koinScreenModel() + + AboutScreenContent(state = model.state.collectAsState()) + } +} + +@Composable +fun AboutScreenContent(state: State) { + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.navigation_about)) }, + navigationIcon = { BackButton() }, + ) + } + ) { paddingValues -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = paddingValues + .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) + .add(PaddingValues(vertical = 16.dp)), + modifier = Modifier + .padding(paddingValues.exclude(PaddingValuesSides.Bottom)) + .padding(horizontal = 14.dp), + ) { + item(key = "PROJECT_HEADER") { + ProjectHeader() + } + + item(key = "HEADER_DIVIDER") { + TextDivider( + text = stringResource(R.string.contributors_lead), + modifier = Modifier.padding(top = 18.dp, bottom = 20.dp), + ) + } + + item(key = "MAIN_CONTRIBUTORS") { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + LeadContributor(BuildConfig.PATCHES_REPO_OWNER, "Radiant Lyrics") + } + } + + item(key = "CONTRIBUTORS_DIVIDER") { + TextDivider( + text = stringResource(R.string.contributors), + modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) + ) + } + + when (val state = state.value) { + AboutScreenState.Loading -> item(key = "CONTRIBUTIONS_LOADING") { + Box( + contentAlignment = Alignment.Center, + content = { CircularProgressIndicator() }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 38.dp), + ) + } + + AboutScreenState.Failure -> item(key = "LOAD_FAILURE") { + LoadFailure( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 38.dp) + ) + } + + is AboutScreenState.Loaded -> { + val others = state.contributors.filter { it.username != BuildConfig.PATCHES_REPO_OWNER } + items(others, key = { it.username }) { user -> + ContributorCommitsItem(user) + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/components/LeadContributor.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/components/LeadContributor.kt new file mode 100644 index 0000000..5d9528c --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/about/components/LeadContributor.kt @@ -0,0 +1,77 @@ +package com.meowarex.rlmobile.ui.legacy.screens.about.components + +import com.meowarex.rlmobile.ui.screens.about.components.* +import com.meowarex.rlmobile.ui.screens.about.* + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import coil3.compose.SubcomposeAsyncImage +import com.valentinilk.shimmer.shimmer + +@Composable +fun LeadContributor( + name: String, + roles: String, + username: String = name, + modifier: Modifier = Modifier, +) { + val uriHandler = LocalUriHandler.current + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .clickable( + onClick = { uriHandler.openUri("https://github.com/$username") }, + indication = ripple(bounded = false, radius = 90.dp), + interactionSource = remember(::MutableInteractionSource) + ) + .widthIn(min = 100.dp) + ) { + SubcomposeAsyncImage( + model = "https://github.com/$username.png", + contentDescription = username, + error = { + Surface( + content = {}, + tonalElevation = 2.dp, + modifier = Modifier + .fillMaxSize() + .shimmer(), + ) + }, + modifier = Modifier + .size(80.dp) + .clip(CircleShape), + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = name, + style = MaterialTheme.typography.titleMedium.copy( + fontSize = 18.sp + ) + ) + + Text( + text = roles, + style = MaterialTheme.typography.titleSmall.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f) + ) + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/ComponentOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/ComponentOptionsScreen.kt new file mode 100644 index 0000000..1899206 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/ComponentOptionsScreen.kt @@ -0,0 +1,365 @@ +package com.meowarex.rlmobile.ui.legacy.screens.componentopts + +import com.meowarex.rlmobile.ui.screens.componentopts.* + +import android.net.Uri +import android.os.Parcelable +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.screens.componentopts.components.* +import com.meowarex.rlmobile.ui.util.ScreenWithResult +import com.meowarex.rlmobile.ui.util.paddings.* +import com.meowarex.rlmobile.util.back +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.koin.core.parameter.parametersOf + +@Parcelize +class ComponentOptionsScreen( + /** + * The type of custom component (patches or apk) + */ + private val componentType: PatchComponent.Type, + /** + * previously selected custom component that should be pre selected on this screen (i dont think this has ever worked) + */ + private val default: PatchComponent?, +) : ScreenWithResult(), Parcelable { + @IgnoredOnParcel + override val key = "Legacy-ComponentOptions-$componentType" + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val model = koinScreenModel { parametersOf(this.resultKey) } + + LaunchedEffect(Unit) { + model.components.clear() + withContext(Dispatchers.IO) { + model.refreshComponents(componentType) + } + if (default in model.components) { + model.selectComponent(default) + } + } + + ComponentOptionsScreenContent( + componentType = componentType, + components = model.components.toImmutableList(), + selected = model.selected, + onSelectComponent = model::selectComponent, + onDeleteComponent = model::deleteComponent, + onImportFromUri = { uri -> model.importFromUri(uri, componentType) }, + releasesExpanded = model.releasesExpanded, + releasesState = model.releasesState, + onToggleReleases = { model.toggleReleasesExpanded(componentType) }, + onImportRelease = { release -> model.importFromRelease(release, componentType) }, + importingReleaseTag = model.importingReleaseTag, + onBackPressed = { navigator.back(null) }, + ) + } +} + +@Composable +fun ComponentOptionsScreenContent( + componentType: PatchComponent.Type, + components: ImmutableList, + selected: PatchComponent?, + onSelectComponent: (PatchComponent?) -> Unit, + onDeleteComponent: (PatchComponent) -> Unit, + onImportFromUri: (Uri) -> Unit, + releasesExpanded: Boolean, + releasesState: ComponentOptionsModel.ReleasesState, + onToggleReleases: () -> Unit, + onImportRelease: (com.meowarex.rlmobile.network.models.GithubRelease) -> Unit, + importingReleaseTag: String?, + onBackPressed: () -> Unit, +) { + val mime = when (componentType) { + PatchComponent.Type.TidalApk -> "application/vnd.android.package-archive" + PatchComponent.Type.Patches -> "application/zip" + } + val filePicker = rememberLauncherForActivityResult( + ActivityResultContracts.OpenDocument(), + ) { uri -> uri?.let(onImportFromUri) } + + Scaffold( + topBar = { ComponentOptionsAppBar(componentType = componentType) }, + ) { paddingValues -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(12.dp), + contentPadding = paddingValues + .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) + .add(PaddingValues(16.dp)), + modifier = Modifier + .padding(paddingValues.exclude(PaddingValuesSides.Bottom)), + ) { + item(key = "NONE") { + PatchComponentCardBase( + selected = selected == null, + onSelect = { onSelectComponent(null) }, + ) { + Text( + text = stringResource(R.string.componentopts_selected_none), + style = MaterialTheme.typography.titleMedium, + ) + } + } + + item(key = "RELEASES_ACCORDION") { + ReleasesAccordion( + expanded = releasesExpanded, + state = releasesState, + importingTag = importingReleaseTag, + onToggle = onToggleReleases, + onImport = onImportRelease, + ) + } + + items( + items = components, + contentType = { "COMPONENT" }, + key = { it }, + ) { component -> + PatchComponentCard( + version = component.version, + timestamp = component.timestamp, + selected = selected == component, + onSelect = { onSelectComponent(component) }, + onDelete = { onDeleteComponent(component) }, + ) + } + + item(key = "BROWSE") { + BrowseImportCard(onClick = { filePicker.launch(arrayOf(mime)) }) + } + + item("EXIT_BTN") { + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(top = 10.dp), + ) { + FilledTonalButton( + onClick = onBackPressed, + ) { + Text(stringResource(R.string.action_confirm)) + } + } + } + } + } +} + +@Composable +private fun ReleasesAccordion( + expanded: Boolean, + state: ComponentOptionsModel.ReleasesState, + importingTag: String?, + onToggle: () -> Unit, + onImport: (com.meowarex.rlmobile.network.models.GithubRelease) -> Unit, +) { + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onToggle) + .padding(horizontal = 16.dp, vertical = 14.dp), + ) { + Icon( + painter = painterResource(R.drawable.ic_account_github_white_24dp), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.componentopts_releases_title), + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = stringResource(R.string.componentopts_releases_desc), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = 0.65f), + ) + } + Icon( + painter = painterResource( + if (expanded) R.drawable.ic_arrow_up_small else R.drawable.ic_arrow_down_small + ), + contentDescription = null, + tint = LocalContentColor.current.copy(alpha = 0.7f), + ) + } + + if (expanded) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.35f), + ) + ReleasesContent( + state = state, + importingTag = importingTag, + onImport = onImport, + ) + } + } + } +} + +@Composable +private fun ReleasesContent( + state: ComponentOptionsModel.ReleasesState, + importingTag: String?, + onImport: (com.meowarex.rlmobile.network.models.GithubRelease) -> Unit, +) { + when (state) { + is ComponentOptionsModel.ReleasesState.Idle, + ComponentOptionsModel.ReleasesState.Loading -> Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + ) { CircularProgressIndicator(modifier = Modifier.size(24.dp)) } + + ComponentOptionsModel.ReleasesState.Failed -> Text( + text = stringResource(R.string.network_load_fail), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + ) + + is ComponentOptionsModel.ReleasesState.Loaded -> { + if (state.releases.isEmpty()) { + Text( + text = stringResource(R.string.componentopts_releases_empty), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = 0.65f), + modifier = Modifier + .fillMaxWidth() + .padding(20.dp), + ) + } else { + Column { + state.releases.forEachIndexed { index, release -> + ReleaseRow( + release = release, + importing = importingTag == release.tagName, + anyImporting = importingTag != null, + onImport = { onImport(release) }, + ) + if (index != state.releases.lastIndex) { + HorizontalDivider( + color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.2f), + ) + } + } + } + } + } + } +} + +@Composable +private fun ReleaseRow( + release: com.meowarex.rlmobile.network.models.GithubRelease, + importing: Boolean, + anyImporting: Boolean, + onImport: () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = release.tagName, + style = MaterialTheme.typography.titleSmall, + ) + Text( + text = release.name ?: release.tagName, + style = MaterialTheme.typography.bodySmall, + color = LocalContentColor.current.copy(alpha = 0.55f), + ) + } + FilledTonalButton( + onClick = onImport, + enabled = !anyImporting, + ) { + if (importing) { + CircularProgressIndicator( + strokeWidth = 2.dp, + modifier = Modifier.size(16.dp), + ) + } else { + Text(stringResource(R.string.action_install)) + } + } + } +} + +@Composable +private fun BrowseImportCard(onClick: () -> Unit) { + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(16.dp), + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp), + ) { + Icon( + painter = painterResource(R.drawable.ic_add), + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + ) + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.componentopts_browse_title), + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = stringResource(R.string.componentopts_browse_desc), + style = MaterialTheme.typography.bodyMedium, + color = LocalContentColor.current.copy(alpha = 0.65f), + ) + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/ComponentOptionsAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/ComponentOptionsAppBar.kt new file mode 100644 index 0000000..e3ab2de --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/ComponentOptionsAppBar.kt @@ -0,0 +1,37 @@ +package com.meowarex.rlmobile.ui.legacy.screens.componentopts.components + +import com.meowarex.rlmobile.ui.screens.componentopts.components.* +import com.meowarex.rlmobile.ui.screens.componentopts.* + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.BackButton +import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent + +@Composable +fun ComponentOptionsAppBar( + componentType: PatchComponent.Type, +) { + TopAppBar( + navigationIcon = { BackButton() }, + title = { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text(stringResource(R.string.componentopts_screen_title, componentType.name)) + Text( + text = stringResource(R.string.componentopts_screen_desc), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } + }, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/PatchComponentCard.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/PatchComponentCard.kt new file mode 100644 index 0000000..46f909e --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/componentopts/components/PatchComponentCard.kt @@ -0,0 +1,118 @@ +package com.meowarex.rlmobile.ui.legacy.screens.componentopts.components + +import com.meowarex.rlmobile.ui.screens.componentopts.components.* +import com.meowarex.rlmobile.ui.screens.componentopts.* + +import android.text.format.DateUtils +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +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.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.network.utils.SemVer +import kotlin.time.Instant + +@Composable +fun PatchComponentCard( + version: SemVer, + timestamp: Instant, + selected: Boolean, + onSelect: () -> Unit, + onDelete: () -> Unit, + modifier: Modifier = Modifier, +) { + PatchComponentCardBase( + selected = selected, + onSelect = onSelect, + modifier = modifier, + ) { + Icon( + painter = painterResource(R.drawable.ic_page), + contentDescription = null, + modifier = Modifier + .alpha(.8f) + .padding(end = 4.dp), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + Text( + text = "v$version", + style = MaterialTheme.typography.titleMedium, + ) + + Text( + text = DateUtils.getRelativeDateTimeString( + /* c = */ LocalContext.current, + /* time = */ timestamp.toEpochMilliseconds(), + /* minResolution = */ DateUtils.SECOND_IN_MILLIS, + /* transitionResolution = */ DateUtils.WEEK_IN_MILLIS, + /* flags = */ DateUtils.FORMAT_ABBREV_ALL, + ).toString(), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.6f), + ) + } + + Spacer(Modifier.weight(1f, fill = true)) + + IconButton( + onClick = onDelete, + ) { + Icon( + painter = painterResource(R.drawable.ic_delete_forever), + tint = MaterialTheme.colorScheme.error, + contentDescription = stringResource(R.string.action_delete), + ) + } + } +} + +@Composable +fun PatchComponentCardBase( + selected: Boolean, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit, +) { + val interaction = remember(::MutableInteractionSource) + + Surface( + tonalElevation = 1.dp, + shadowElevation = 1.dp, + modifier = modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable( + interactionSource = interaction, + role = Role.RadioButton, + onClick = onSelect, + ), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(10.dp), + ) { + RadioButton( + selected = selected, + onClick = onSelect, + interactionSource = interaction, + ) + + content() + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/HomeScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/HomeScreen.kt new file mode 100644 index 0000000..7790b58 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/HomeScreen.kt @@ -0,0 +1,278 @@ +package com.meowarex.rlmobile.ui.legacy.screens.home + +import com.meowarex.rlmobile.ui.screens.home.* + +import android.os.Parcelable +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.Image +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.painter.BitmapPainter +import androidx.activity.ComponentActivity +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.core.content.ContextCompat +import androidx.core.graphics.drawable.toBitmap +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.paging.compose.collectAsLazyPagingItems +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.SegmentedButton +import com.meowarex.rlmobile.ui.legacy.components.Tag +import com.meowarex.rlmobile.ui.legacy.screens.about.AboutScreen +import com.meowarex.rlmobile.ui.legacy.screens.home.components.CommitList +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.PatchOptionsScreen +import com.meowarex.rlmobile.ui.legacy.screens.settings.SettingsScreen +import com.meowarex.rlmobile.ui.legacy.widgets.managerupdate.ManagerUpdateDialog +import com.meowarex.rlmobile.ui.widgets.updater.UpdaterViewModel +import com.meowarex.rlmobile.util.* +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.koin.androidx.compose.koinViewModel + +@Parcelize +class HomeScreen : Screen, Parcelable { + @IgnoredOnParcel + override val key = "Legacy-Home" + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val scope = rememberCoroutineScope() + val model = koinScreenModel() + val activity = LocalContext.current as ComponentActivity + val updater = koinViewModel(viewModelStoreOwner = activity) + val managerUpdateAvailable = updater.targetVersion != null + + LifecycleResumeEffect(Unit) { + model.refresh(delay = true) + onPauseOrDispose {} + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.navigation_home)) }, + actions = { + IconButton(onClick = { model.refresh() }) { + Icon( + painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.navigation_refresh), + ) + } + if (managerUpdateAvailable) { + IconButton(onClick = updater::reopenDialog) { + Icon( + painterResource(R.drawable.ic_update), + contentDescription = stringResource(R.string.action_update), + ) + } + } + IconButton(onClick = { navigator.push(AboutScreen()) }) { + Icon( + painterResource(R.drawable.ic_info), + contentDescription = stringResource(R.string.navigation_about), + ) + } + IconButton(onClick = { navigator.push(SettingsScreen()) }) { + Icon( + painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings), + ) + } + }, + ) + }, + ) { pv -> + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .padding(pv) + .padding(16.dp) + .fillMaxSize(), + ) { + val state = model.state + when (state) { + HomeState.Loading -> Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize(), + ) { CircularProgressIndicator() } + + is HomeState.Loaded -> HomeContent( + state = state, + commits = model.commits, + managerUpdateAvailable = managerUpdateAvailable, + onInstall = { navigator.pushOnce(PatchOptionsScreen()) }, + onRepatch = { + scope.launchIO { + val options = model.createRepatchOptions() ?: return@launchIO + val screen = PatchOptionsScreen(prefilledOptions = options) + mainThread { navigator.push(screen) } + } + }, + onLaunch = model::launchInstall, + onInfo = model::openCurrentAppInfo, + ) + } + + model.managerUpdateDeltas?.let { deltas -> + ManagerUpdateDialog( + deltas = deltas, + onDismiss = model::dismissManagerUpdate, + ) + } + } + } + } +} + +@Composable +private fun ColumnScope.HomeContent( + state: HomeState.Loaded, + commits: kotlinx.coroutines.flow.Flow>, + managerUpdateAvailable: Boolean, + onInstall: () -> Unit, + onRepatch: () -> Unit, + onLaunch: () -> Unit, + onInfo: () -> Unit, +) { + val install = state.install + val currentVersionName = (install?.version as? com.meowarex.rlmobile.ui.util.TidalVersion.Existing) + ?.let { "v${it.name} (build ${it.code})" } + val latestVersionName = state.latestTidalVersionCode?.let { "build $it" } + + val fallbackPainter = if (install?.icon == null) { + // R.mipmap.ic_launcher is an adaptive-icon XML on API 26+, which painterResource cannot decode. + val context = LocalContext.current + remember { + val drawable = ContextCompat.getDrawable(context, R.mipmap.ic_launcher) + drawable?.toBitmap()?.asImageBitmap()?.let(::BitmapPainter) + } + } else null + + val iconPainter = install?.icon ?: fallbackPainter + if (iconPainter != null) { + Image( + painter = iconPainter, + contentDescription = null, + modifier = Modifier + .size(60.dp) + .clip(CircleShape), + ) + } + + Text( + text = install?.name ?: stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + ) + + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth(), + ) { + AnimatedVisibility(visible = currentVersionName != null) { + Text( + text = "Current: ${currentVersionName ?: "-"}", + style = MaterialTheme.typography.labelLarge, + color = LocalContentColor.current.copy(alpha = 0.5f), + textAlign = TextAlign.Center, + ) + } + AnimatedVisibility(visible = latestVersionName != null) { + Text( + text = "Latest: ${latestVersionName ?: "-"}", + style = MaterialTheme.typography.labelLarge, + color = LocalContentColor.current.copy(alpha = 0.5f), + textAlign = TextAlign.Center, + ) + } + } + + val patchesBehind = install != null && install.patchesUpToDate == false + val tidalBehind = install != null && install.tidalUpToDate == false + AnimatedVisibility(visible = patchesBehind || tidalBehind) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { + if (patchesBehind) Tag(text = "New Patches!") + if (tidalBehind) Tag(text = "TIDAL Update!") + } + } + + val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind) + val canLocalRepatch = install != null && state.offlineRepatchReady + val onlineEnabled = state.latestTidalVersionCode != null || install != null + val buttonEnabled = !blockedByManagerUpdate && (if (state.offline) canLocalRepatch else onlineEnabled) + Button( + onClick = if (install == null) onInstall else onRepatch, + enabled = buttonEnabled, + modifier = Modifier.fillMaxWidth(), + ) { + val label = when { + blockedByManagerUpdate -> "Manager Update Required" + state.offline && install != null -> "Local Repatch" + state.offline -> "No Network" + install == null && state.latestTidalVersionCode == null -> "Loading…" + install == null -> "Install" + patchesBehind && tidalBehind -> "Update Patches & TIDAL" + patchesBehind -> "Update Patches" + tidalBehind -> "Update TIDAL" + else -> "Repatch" + } + Text( + text = label, + textAlign = TextAlign.Center, + maxLines = 1, + modifier = Modifier + .basicMarquee() + .fillMaxWidth(), + ) + } + + if (state.offline && install != null) { + Text( + text = "No Network Connection", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier + .fillMaxWidth() + .padding(top = 6.dp), + ) + } + + AnimatedVisibility(visible = install != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.clip(RoundedCornerShape(16.dp)), + ) { + SegmentedButton( + icon = painterResource(R.drawable.ic_launch), + text = stringResource(R.string.action_launch), + onClick = onLaunch, + ) + SegmentedButton( + icon = painterResource(R.drawable.ic_info), + text = stringResource(R.string.action_open_info), + onClick = onInfo, + ) + } + } + + ElevatedCard(modifier = Modifier.fillMaxSize()) { + CommitList(commits = commits.collectAsLazyPagingItems()) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/components/CommitList.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/components/CommitList.kt new file mode 100644 index 0000000..fcbe4c5 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/home/components/CommitList.kt @@ -0,0 +1,118 @@ +package com.meowarex.rlmobile.ui.legacy.screens.home.components + +import com.meowarex.rlmobile.ui.screens.home.components.* +import com.meowarex.rlmobile.ui.screens.home.* + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.itemKey +import coil3.compose.AsyncImage +import com.meowarex.rlmobile.network.models.GithubCommit +import java.text.SimpleDateFormat +import java.time.Instant +import java.util.Date + +@Composable +fun CommitList(commits: LazyPagingItems) { + val loading = commits.loadState.refresh is LoadState.Loading || commits.loadState.append is LoadState.Loading + val failed = commits.loadState.refresh is LoadState.Error || commits.loadState.append is LoadState.Error + + LazyColumn { + items( + count = commits.itemCount, + key = commits.itemKey { it.sha }, + ) { index -> + val commit = commits[index] ?: return@items + CommitRow(commit) + if (index < commits.itemCount - 1) { + HorizontalDivider( + thickness = 0.5.dp, + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + } + + if (loading) item { + Box( + contentAlignment = Alignment.TopCenter, + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) { + CircularProgressIndicator(strokeWidth = 3.dp, modifier = Modifier.size(30.dp)) + } + } + + if (failed) item { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth().padding(16.dp), + ) { + Text("Failed to load commits", style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center) + Button(onClick = { commits.retry() }) { Text("Retry") } + } + } + } +} + +@Composable +private fun CommitRow(commit: GithubCommit) { + val uriHandler = LocalUriHandler.current + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxWidth() + .clickable { uriHandler.openUri(commit.htmlUrl) } + .padding(16.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + AsyncImage( + model = commit.author?.avatarUrl ?: "https://github.com/ghost.png", + contentDescription = commit.author?.login ?: "ghost", + modifier = Modifier.size(20.dp).clip(CircleShape), + ) + Text( + text = commit.author?.login ?: commit.commit.author.name, + style = MaterialTheme.typography.labelMedium, + ) + Text("•", style = MaterialTheme.typography.labelLarge) + Text( + text = commit.sha.take(7), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + fontFamily = FontFamily.Monospace, + ) + + Text( + text = runCatching { + SimpleDateFormat.getDateInstance(SimpleDateFormat.SHORT) + .format(Date.from(Instant.parse(commit.commit.author.date))) + }.getOrDefault(""), + style = MaterialTheme.typography.labelMedium, + color = LocalContentColor.current.copy(alpha = 0.5f), + textAlign = TextAlign.End, + modifier = Modifier.weight(1f), + ) + } + Text( + text = commit.commit.message.lineSequence().first(), + style = MaterialTheme.typography.labelLarge, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/LogScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/LogScreen.kt new file mode 100644 index 0000000..25f0704 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/LogScreen.kt @@ -0,0 +1,127 @@ +package com.meowarex.rlmobile.ui.legacy.screens.log + +import com.meowarex.rlmobile.ui.screens.log.* + +import android.os.Parcelable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.manager.InstallLogData +import com.meowarex.rlmobile.ui.legacy.components.Label +import com.meowarex.rlmobile.ui.legacy.screens.log.components.LogAppBar +import com.meowarex.rlmobile.ui.legacy.screens.log.components.LogTextArea +import com.meowarex.rlmobile.util.back +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.koin.core.parameter.parametersOf + +@Parcelize +class LogScreen(private val installId: String) : Screen, Parcelable { + @IgnoredOnParcel + override val key: ScreenKey + get() = "Legacy-LogScreen-$installId" + + @Composable + override fun Content() { + val model = koinScreenModel { parametersOf(installId) } + val navigator = LocalNavigator.currentOrThrow + + if (model.shouldCloseScreen) { + navigator.back(currentActivity = null) + } + + model.data?.let { + LogScreenContent( + data = it, + onExportLog = model::saveLog, + onShareLog = model::shareLog, + ) + } + } +} + +@Composable +fun LogScreenContent( + data: InstallLogData, + onExportLog: () -> Unit, + onShareLog: () -> Unit, +) { + Scaffold( + topBar = { + LogAppBar( + onExportLog = onExportLog, + onShareLog = onShareLog, + ) + }, + ) { paddingValues -> + LazyColumn( + verticalArrangement = Arrangement.spacedBy(28.dp), + modifier = Modifier + .padding(paddingValues) + .padding(vertical = 10.dp, horizontal = 22.dp) + ) { + item("INSTALL_INFO") { + Label( + name = stringResource(R.string.log_section_install_info), + description = null, + ) { + LogTextArea( + text = """ + Installation ID: ${data.id} + Installation Date: ${data.installDate} + """.trimIndent(), + modifier = Modifier.fillMaxWidth(), + ) + } + } + + item("ENVIRONMENT_INFO") { + Label( + name = stringResource(R.string.log_section_env_info), + description = null, + ) { + LogTextArea( + text = data.environmentInfo, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + if (data.errorStacktrace != null) { + item("ERROR_STACKTRACE") { + Label( + name = stringResource(R.string.log_section_error), + description = null, + ) { + LogTextArea( + text = data.errorStacktrace, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + + item("LOG") { + Label( + name = stringResource(R.string.log_section_log), + description = null, + ) { + LogTextArea( + text = data.installationLog, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogAppBar.kt new file mode 100644 index 0000000..75f1231 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogAppBar.kt @@ -0,0 +1,36 @@ +package com.meowarex.rlmobile.ui.legacy.screens.log.components + +import com.meowarex.rlmobile.ui.screens.log.components.* +import com.meowarex.rlmobile.ui.screens.log.* + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.BackButton + +@Composable +fun LogAppBar( + onExportLog: () -> Unit, + onShareLog: () -> Unit, +) { + TopAppBar( + title = { Text(stringResource(R.string.log_title)) }, + navigationIcon = { BackButton() }, + actions = { + IconButton(onClick = onExportLog) { + Icon( + painter = painterResource(R.drawable.ic_save), + contentDescription = stringResource(R.string.log_action_export), + ) + } + IconButton(onClick = onShareLog) { + Icon( + painter = painterResource(R.drawable.ic_share), + contentDescription = stringResource(R.string.log_action_share), + ) + } + }, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogTextArea.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogTextArea.kt new file mode 100644 index 0000000..ef1bae7 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/log/components/LogTextArea.kt @@ -0,0 +1,45 @@ +package com.meowarex.rlmobile.ui.legacy.screens.log.components + +import com.meowarex.rlmobile.ui.screens.log.components.* +import com.meowarex.rlmobile.ui.screens.log.* + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.meowarex.rlmobile.ui.util.horizontalScrollbar +import com.meowarex.rlmobile.ui.util.thenIf + +@Composable +fun LogTextArea( + text: String, + modifier: Modifier = Modifier.Companion, +) { + val scrollState = rememberScrollState() + val scrollable by remember { derivedStateOf { scrollState.maxValue > 0 } } + + SelectionContainer { + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + lineHeight = 18.sp, + fontFamily = FontFamily.Companion.Monospace, + softWrap = false, + modifier = modifier + .clip(RoundedCornerShape(10.dp)) + .background(MaterialTheme.colorScheme.surfaceContainerHigh) + .padding(start = 18.dp, end = 18.dp, top = 14.dp, bottom = 14.dp) + .horizontalScroll(scrollState) + .horizontalScrollbar(scrollState) + .thenIf(scrollable) { padding(bottom = 10.dp) } + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/PatchingScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/PatchingScreen.kt new file mode 100644 index 0000000..e4e282f --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/PatchingScreen.kt @@ -0,0 +1,304 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching + +import com.meowarex.rlmobile.ui.screens.patching.* + +import android.os.Parcelable +import androidx.activity.compose.BackHandler +import androidx.compose.animation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +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.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.meowarex.rlmobile.BuildConfig +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.patcher.steps.StepGroup +import com.meowarex.rlmobile.ui.legacy.components.MainActionButton +import com.meowarex.rlmobile.ui.legacy.components.Wakelock +import com.meowarex.rlmobile.ui.legacy.components.dialogs.InstallerAbortDialog +import com.meowarex.rlmobile.ui.legacy.screens.log.LogScreen +import com.meowarex.rlmobile.ui.legacy.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.PatchOptionsScreen +import com.meowarex.rlmobile.ui.theme.customColors +import com.meowarex.rlmobile.ui.util.paddings.* +import com.meowarex.rlmobile.ui.util.spacedByLastAtBottom +import com.meowarex.rlmobile.ui.util.thenIf +import com.meowarex.rlmobile.util.back +import com.meowarex.rlmobile.util.isIgnoringBatteryOptimizations +import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.filter +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.koin.core.parameter.parametersOf + +val VERTICAL_PADDING: Dp = 18.dp + +@Parcelize +class PatchingScreen( + /** + * User-selected patching options. (allot of glorp) + * This may originate from [PatchOptionsScreen] or from an existing installation (to update it) + */ + private val options: PatchOptions, +) : Screen, Parcelable { + @IgnoredOnParcel + override val key = "Legacy-Patching" + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val context = LocalContext.current + val model = koinScreenModel { parametersOf(options) } + + val state by model.state.collectAsState() + val listState = rememberLazyListState() + val showMinimizationWarning = rememberSaveable { !context.isIgnoringBatteryOptimizations() } + + // Exit warning dialog (dismiss itself if install process state changes, esp. for Success) + var showAbortWarning by rememberSaveable(model.state.collectAsState().value) { mutableStateOf(false) } + + // The currently expanded step group on this screen + var expandedGroup by rememberSaveable { mutableStateOf(StepGroup.Prepare) } + + // Only show exit warning if currently working + val onTryExit: () -> Unit = remember { + { + // Show cancellation if currently running + if (state == PatchingScreenState.Working && !model.devMode) { + showAbortWarning = true + } + // Go home directly if install was successful + else if (state is PatchingScreenState.Success) { + navigator.popUntilRoot() + } + // Go back to the patch options screen + else { + navigator.back(currentActivity = null) + } + } + } + + // Prevent screen from turning off while working + Wakelock(active = state is PatchingScreenState.Working) + + LaunchedEffect(state) { + when (state) { + // Go home directly if screen model mandates so (usually caused by cancelled PackageInstaller dialog) + PatchingScreenState.CloseScreen -> navigator.popUntilRoot() + + // Close all groups when successfully finished everything + PatchingScreenState.Success -> { + expandedGroup = null + } + + else -> {} + } + + listState.animateScrollToItem(0) + } + + if (showAbortWarning) { + InstallerAbortDialog( + onDismiss = { showAbortWarning = false }, + onConfirm = { + navigator.back(currentActivity = null) + model.cancelInstall() + }, + ) + } else { + BackHandler(onBack = onTryExit) + } + + Scaffold( + topBar = { PatchingAppBar(onTryExit) }, + ) { paddingValues -> + Column( + verticalArrangement = Arrangement.spacedBy(VERTICAL_PADDING), + modifier = Modifier + .padding(paddingValues.exclude(PaddingValuesSides.Bottom)), + ) { + LazyColumn( + state = listState, + verticalArrangement = Arrangement.spacedByLastAtBottom(0.dp), + contentPadding = paddingValues + .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) + .add(PaddingValues(bottom = 25.dp)), + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxSize(), + ) { + item(key = "MINIMIZATION_WARNING") { + BannerSection(visible = showMinimizationWarning && !state.isFinished) { + TextBanner( + text = stringResource(R.string.installer_banner_minimization), + icon = painterResource(R.drawable.ic_warning), + iconColor = MaterialTheme.customColors.onWarningContainer, + outlineColor = MaterialTheme.customColors.warning, + containerColor = MaterialTheme.customColors.warningContainer, + modifier = Modifier + .padding(bottom = VERTICAL_PADDING) + .fillMaxWidth(), + ) + } + } + + item(key = "FAILED_BANNER") { + BannerSection(visible = state is PatchingScreenState.Failed) { + val handler = LocalUriHandler.current + + TextBanner( + text = stringResource(R.string.installer_banner_failure), + icon = painterResource(R.drawable.ic_warning), + iconColor = MaterialTheme.colorScheme.error, + outlineColor = null, + containerColor = MaterialTheme.colorScheme.errorContainer, + onClick = { handler.openUri("https://tidal.gg/${BuildConfig.SUPPORT_SERVER}") }, + modifier = Modifier + .padding(bottom = VERTICAL_PADDING) + .fillMaxWidth(), + ) + } + } + + item(key = "INSTALLED_BANNER") { + BannerSection(visible = state is PatchingScreenState.Success) { + TextBanner( + text = stringResource(R.string.installer_banner_success), + icon = painterResource(R.drawable.ic_check_circle), + iconColor = Color(0xFF59B463), + outlineColor = MaterialTheme.colorScheme.surfaceVariant, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier + .padding(bottom = VERTICAL_PADDING) + .fillMaxWidth(), + ) + } + } + + for ((group, steps) in model.steps?.entries ?: persistentListOf()) { + item(key = System.identityHashCode(group)) { + StepGroupCard( + name = stringResource(group.localizedName), + subSteps = steps, + isExpanded = expandedGroup == group, + onExpand = { expandedGroup = group }, + modifier = Modifier + .padding(bottom = VERTICAL_PADDING) + .fillMaxWidth() + .thenIf(state is PatchingScreenState.Success) { alpha(.5f) } + ) + } + } + + item(key = "BUTTONS") { + var cacheCleared by rememberSaveable { mutableStateOf(false) } + val filteredState by remember { model.state.filter { it.isProgressChange } } + .collectAsState(initial = PatchingScreenState.Working) + + AnimatedVisibility( + visible = filteredState != PatchingScreenState.Working, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically { it * -2 }, + ) { + Column( + verticalArrangement = Arrangement.spacedBy(VERTICAL_PADDING / 2), + ) { + HorizontalDivider( + thickness = 1.dp, + modifier = Modifier + .padding(bottom = VERTICAL_PADDING / 2) + ) + + when (filteredState) { + PatchingScreenState.Working -> {} + PatchingScreenState.CloseScreen -> error("unreachable") + + PatchingScreenState.Success -> { + MainActionButton( + text = stringResource(R.string.action_launch), + icon = painterResource(R.drawable.ic_launch), + onClick = model::launchApp, + ) + } + + is PatchingScreenState.Failed -> { + MainActionButton( + text = stringResource(R.string.action_retry_install), + icon = painterResource(R.drawable.ic_refresh), + onClick = model::install, + ) + + MainActionButton( + text = stringResource(R.string.action_open_error_log), + icon = painterResource(R.drawable.ic_launch), + onClick = { navigator.push(LogScreen(installId = model.getCurrentInstallId()!!)) }, + ) + } + } + + MainActionButton( + text = stringResource(R.string.settings_clear_cache), + icon = painterResource(R.drawable.ic_delete_forever), + enabled = !cacheCleared, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.error, + ), + onClick = { + cacheCleared = true + model.clearCache() + }, + ) + } + } + } + + item(key = "FUN_FACT") { + FunFact( + text = stringResource(model.funFact), + state = state, + ) + } + } + } + } + } +} + +@Composable +private fun BannerSection( + visible: Boolean, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + AnimatedVisibility( + visible = visible, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically(), + modifier = modifier + .padding(bottom = VERTICAL_PADDING), + ) { + Column { + content() + + HorizontalDivider( + thickness = 1.dp, + modifier = Modifier.padding(vertical = 4.dp), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/FunFact.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/FunFact.kt new file mode 100644 index 0000000..14a7412 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/FunFact.kt @@ -0,0 +1,53 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.animation.* +import androidx.compose.animation.core.tween +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.screens.patching.PatchingScreenState +import com.meowarex.rlmobile.ui.legacy.screens.patching.VERTICAL_PADDING + +@Composable +fun FunFact( + text: String, + state: PatchingScreenState, +) { + AnimatedVisibility( + visible = state !is PatchingScreenState.Failed, + enter = fadeIn() + slideInVertically { it * 2 }, + exit = fadeOut() + slideOutVertically { it * 2 }, + label = "fun fact visibility" + ) { + AnimatedContent( + targetState = text, + label = "fun fact change", + transitionSpec = { + val inSpec = fadeIn(tween(220, delayMillis = 90)) + slideInHorizontally { it * -2 } + val outSpec = fadeOut(tween(90)) + slideOutHorizontally { it * 2 } + inSpec togetherWith outSpec + } + ) { text -> + Text( + text = stringResource(R.string.fun_fact_prefix, text), + style = MaterialTheme.typography.bodySmall, + textAlign = TextAlign.Center, + modifier = Modifier + .padding(top = VERTICAL_PADDING, bottom = 25.dp, start = VERTICAL_PADDING, end = VERTICAL_PADDING) + .fillMaxWidth() + .alpha(.6f) + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/PatchingAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/PatchingAppBar.kt new file mode 100644 index 0000000..302cb78 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/PatchingAppBar.kt @@ -0,0 +1,27 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.meowarex.rlmobile.R + +@Composable +fun PatchingAppBar( + onBack: () -> Unit, +) { + TopAppBar( + title = { Text(stringResource(R.string.installer)) }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + painter = painterResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.navigation_back), + ) + } + } + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepGroupCard.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepGroupCard.kt new file mode 100644 index 0000000..0b57c59 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepGroupCard.kt @@ -0,0 +1,118 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.patcher.steps.base.Step +import com.meowarex.rlmobile.patcher.steps.base.StepState +import kotlinx.collections.immutable.ImmutableList + +@Composable +fun StepGroupCard( + name: String, + subSteps: ImmutableList, + isExpanded: Boolean, + onExpand: () -> Unit, + modifier: Modifier = Modifier, +) { + val groupState by remember(subSteps) { + derivedStateOf { + when { + // If all steps are pending then show pending + subSteps.all { it.state == StepState.Pending } -> StepState.Pending + // If any step has finished with an error then default to error + subSteps.any { it.state == StepState.Error } -> StepState.Error + // If all steps have finished as Skipped/Success then show success + subSteps.all { it.state.isFinished } -> StepState.Success + + else -> StepState.Running + } + } + } + + val totalSeconds = remember(groupState.isFinished) { + if (!groupState.isFinished) { + 0f + } else { + subSteps + .sumOf { step -> step.getDuration() } + .div(1000f) + } + } + + LaunchedEffect(groupState) { + if (groupState == StepState.Running) + onExpand() + } + + Column( + modifier = modifier + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerLow) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier + .clickable(true, onClick = onExpand) + .fillMaxWidth() + .padding(20.dp) + ) { + StepStateIcon( + state = groupState, + size = 24.dp, + ) + + Text( + text = name, + modifier = Modifier + .basicMarquee() + .weight(0.05f), + ) + + TimeElapsed( + enabled = groupState.isFinished, + seconds = totalSeconds, + ) + + if (isExpanded) { + Icon( + painter = painterResource(R.drawable.ic_arrow_up_small), + contentDescription = stringResource(R.string.action_collapse) + ) + } else { + Icon( + painter = painterResource(R.drawable.ic_arrow_down_small), + contentDescription = stringResource(R.string.action_expand) + ) + } + } + + AnimatedVisibility(visible = isExpanded) { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = Modifier + .background(MaterialTheme.colorScheme.background.copy(0.6f)) + .fillMaxWidth() + .padding(20.dp) + .padding(start = 4.dp) + ) { + for (step in subSteps) key(step) { + StepItem(step) + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepItem.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepItem.kt new file mode 100644 index 0000000..5e48ff5 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepItem.kt @@ -0,0 +1,52 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.patcher.steps.base.Step +import com.meowarex.rlmobile.patcher.steps.base.StepState +import com.meowarex.rlmobile.ui.util.thenIf + +@Composable +fun StepItem( + step: Step, + modifier: Modifier = Modifier, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier, + ) { + StepStateIcon( + size = 18.dp, + state = step.state, + stepProgress = step.progress, + ) + + Text( + text = stringResource(step.localizedName), + style = MaterialTheme.typography.labelLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .weight(1f, true) + .thenIf(step.state == StepState.Running) { basicMarquee() }, + ) + + TimeElapsed( + enabled = step.state != StepState.Pending, + seconds = step.collectDurationAsState().value, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepStateIcon.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepStateIcon.kt new file mode 100644 index 0000000..0dde6c7 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/StepStateIcon.kt @@ -0,0 +1,91 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.* +import androidx.compose.foundation.layout.size +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.unit.Dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.patcher.steps.base.StepState +import kotlin.math.floor +import kotlin.math.roundToInt + +@Composable +fun StepStateIcon( + state: StepState, + size: Dp, + stepProgress: Float = -1f, +) { + val animatedProgress by animateFloatAsState( + targetValue = stepProgress, + animationSpec = spring(stiffness = Spring.StiffnessVeryLow), + label = "Progress", + ) + + Crossfade(targetState = state, label = "State CrossFade") { animatedState -> + when (animatedState) { + StepState.Pending -> Icon( + painter = painterResource(R.drawable.ic_circle), + contentDescription = stringResource(R.string.status_queued), + tint = MaterialTheme.colorScheme.onSurface.copy(.2f), + modifier = Modifier.size(size) + ) + + StepState.Running -> { + val strokeWidth = Dp(floor(size.value / 10) + 1) + + if (stepProgress > .05f) { + CircularProgressIndicator( + progress = { animatedProgress }, + strokeWidth = strokeWidth, + modifier = Modifier + .size(size) + .semantics { contentDescription = "${(stepProgress * 100).roundToInt()}%" }, + ) + } else { + val description = stringResource(R.string.status_ongoing) + + // Infinite spinner because "Play has no limits" + CircularProgressIndicator( + strokeWidth = strokeWidth, + modifier = Modifier + .size(size) + .semantics { contentDescription = description }, + ) + } + } + + StepState.Success -> Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.status_success), + tint = Color(0xFF59B463), + modifier = Modifier.size(size) + ) + + StepState.Error -> Icon( + painter = painterResource(R.drawable.ic_canceled), + contentDescription = stringResource(R.string.status_failed), + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(size) + ) + + StepState.Skipped -> Icon( + painter = painterResource(R.drawable.ic_check_circle), + contentDescription = stringResource(R.string.status_skipped), + tint = Color(0xFFAEAEAE), + modifier = Modifier.size(size) + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TextBanner.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TextBanner.kt new file mode 100644 index 0000000..bc82d12 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TextBanner.kt @@ -0,0 +1,61 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.util.thenIf + +@Composable +fun TextBanner( + text: String, + icon: Painter, + iconColor: Color, + outlineColor: Color?, + containerColor: Color, + onClick: (() -> Unit)? = null, + modifier: Modifier = Modifier, +) { + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .thenIf(outlineColor) { color -> + border( + width = 2.dp, + color = color, + shape = MaterialTheme.shapes.medium, + ) + } + .clip(MaterialTheme.shapes.medium) + .background(containerColor) + .thenIf(onClick) { clickable(onClick = it) } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 20.dp, vertical = 14.dp) + ) { + Icon( + painter = icon, + tint = iconColor, + contentDescription = null, + modifier = Modifier.size(28.dp), + ) + + Text( + text = text, + style = MaterialTheme.typography.labelMedium, + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TimeElapsed.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TimeElapsed.kt new file mode 100644 index 0000000..265e8ff --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patching/components/TimeElapsed.kt @@ -0,0 +1,33 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patching.components + +import com.meowarex.rlmobile.ui.screens.patching.components.* +import com.meowarex.rlmobile.ui.screens.patching.* + +import androidx.compose.animation.* +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import com.meowarex.rlmobile.R + +@Composable +fun TimeElapsed( + seconds: Float, + enabled: Boolean = true, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = enabled, + enter = fadeIn(), + exit = ExitTransition.None, + label = "TimeElapsed Visibility" + ) { + Text( + text = stringResource(R.string.time_elapsed_seconds, seconds), + style = MaterialTheme.typography.labelMedium, + maxLines = 1, + modifier = modifier, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/PatchOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/PatchOptionsScreen.kt new file mode 100644 index 0000000..12668a8 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/PatchOptionsScreen.kt @@ -0,0 +1,313 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts + +import com.meowarex.rlmobile.ui.screens.patchopts.* + +import android.os.Parcelable +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.* +import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent +import com.meowarex.rlmobile.ui.legacy.screens.componentopts.ComponentOptionsScreen +import com.meowarex.rlmobile.ui.legacy.screens.patching.PatchingScreen +import androidx.compose.runtime.saveable.rememberSaveable +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.PackageNameStateLabel +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.PackageNameUnlockDialog +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.PatchOptionsAppBar +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.PatchSelectionAccordion +import com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.options.* +import com.meowarex.rlmobile.ui.util.spacedByLastAtBottom +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.koin.core.parameter.parametersOf + +@Parcelize +class PatchOptionsScreen( + private val prefilledOptions: PatchOptions? = null, +) : Screen, Parcelable { + @IgnoredOnParcel + override val key = "Legacy-PatchOptions" + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val model = koinScreenModel { parametersOf(prefilledOptions ?: PatchOptions.Default) } + + PatchOptionsScreenContent( + isUpdate = prefilledOptions != null, + isDevMode = model.isDevMode, + + debuggable = model.debuggable, + setDebuggable = model::changeDebuggable, + bypassIncompatible = model.bypassIncompatible, + setBypassIncompatible = model::changeBypassIncompatible, + + appName = model.appName, + appNameIsError = model.appNameIsError, + setAppName = model::changeAppName, + + packageName = model.packageName, + packageNameState = model.packageNameState, + setPackageName = model::changePackageName, + + packageNameLocked = model.packageNameLocked, + onUnlockPackageName = model::unlockPackageName, + onLockPackageName = model::lockPackageName, + + customTidalApk = model.customTidalApk, + customPatches = model.customPatches, + onSelectCustomTidalApk = { + model.selectCustomTidalApk(navigator) { default -> + ComponentOptionsScreen(default = default, componentType = PatchComponent.Type.TidalApk) + } + }, + onSelectCustomPatches = { + model.selectCustomPatches(navigator) { default -> + ComponentOptionsScreen(default = default, componentType = PatchComponent.Type.Patches) + } + }, + + specs = model.specs, + isPatchEnabled = model::isPatchEnabled, + onTogglePatch = model::setPatchEnabled, + patchLockState = model::lockState, + variantIndex = model::variantIndex, + onSelectVariant = model::selectVariant, + optionState = model.optionState, + + isConfigValid = model.isConfigValid, + onInstall = { + navigator.push(PatchingScreen(model.generateConfig())) + }, + ) + } +} + +@Composable +fun PatchOptionsScreenContent( + isUpdate: Boolean, + isDevMode: Boolean, + + debuggable: Boolean, + setDebuggable: (Boolean) -> Unit, + bypassIncompatible: Boolean, + setBypassIncompatible: (Boolean) -> Unit, + + appName: String, + appNameIsError: Boolean, + setAppName: (String) -> Unit, + + packageName: String, + packageNameState: PackageNameState, + setPackageName: (String) -> Unit, + + packageNameLocked: Boolean, + onUnlockPackageName: () -> Unit, + onLockPackageName: () -> Unit, + + customTidalApk: PatchComponent?, + onSelectCustomTidalApk: () -> Unit, + customPatches: PatchComponent?, + onSelectCustomPatches: () -> Unit, + + specs: List, + isPatchEnabled: (PatchSpec) -> Boolean, + onTogglePatch: (PatchSpec, Boolean) -> Unit, + patchLockState: (PatchSpec) -> PatchLock, + variantIndex: (PatchSpec) -> Int, + onSelectVariant: (PatchSpec, Int) -> Unit, + optionState: PatchOptionState, + + isConfigValid: Boolean, + onInstall: () -> Unit, +) { + var showPkgUnlockDialog by rememberSaveable { mutableStateOf(false) } + if (showPkgUnlockDialog) { + PackageNameUnlockDialog( + // With "Bypass Incompatible" on path-gated patches are force-applied (not disabled) + blockedTitles = if (bypassIncompatible) emptyList() else specs.filter { it.pathLocked }.map { it.title }, + onConfirm = { + showPkgUnlockDialog = false + onUnlockPackageName() + }, + onDismiss = { showPkgUnlockDialog = false }, + ) + } + + Scaffold( + topBar = { PatchOptionsAppBar(isUpdate = isUpdate) }, + ) { paddingValues -> + Column( + verticalArrangement = Arrangement.spacedByLastAtBottom(20.dp), + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(paddingValues) + .padding(horizontal = 20.dp, vertical = 10.dp) + ) { + Text( + text = stringResource(R.string.patchopts_title), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + + TextDivider(text = stringResource(R.string.patchopts_divider_basic)) + + val appNameIsDefault by remember { + derivedStateOf { + appName == PatchOptions.Default.appName + } + } + TextPatchOption( + name = stringResource(R.string.patchopts_appname_title), + description = stringResource(R.string.patchopts_appname_desc), + value = appName, + valueIsError = appNameIsError, + valueIsDefault = appNameIsDefault, + onValueChange = setAppName, + onValueReset = { setAppName(PatchOptions.Default.appName) }, + ) + + if (!isUpdate) { + val packageNameIsDefault by remember { + derivedStateOf { + packageName == PatchOptions.Default.packageName + } + } + TextPatchOption( + name = stringResource(R.string.patchopts_pkgname_title), + description = stringResource(R.string.patchopts_pkgname_desc), + value = packageName, + valueIsError = packageNameState == PackageNameState.Invalid, + valueIsDefault = packageNameIsDefault, + onValueChange = setPackageName, + onValueReset = { setPackageName(PatchOptions.Default.packageName) }, + locked = packageNameLocked, + onLockClick = { + if (packageNameLocked) showPkgUnlockDialog = true else onLockPackageName() + }, + ) { + PackageNameStateLabel( + state = packageNameState, + modifier = Modifier.padding(start = 4.dp), + ) + } + } + + val (integrations, patches) = remember(specs) { specs.partition { it.isIntegration } } + + if (integrations.isNotEmpty()) { + PatchSelectionAccordion( + iconRes = R.drawable.ic_extension, + titleRes = R.string.patchopts_integrations_title, + descriptionRes = R.string.patchopts_integrations_desc, + specs = integrations, + enabledCount = integrations.count(isPatchEnabled), + totalCount = integrations.size, + isEnabled = isPatchEnabled, + onToggle = onTogglePatch, + lockState = patchLockState, + variantIndex = variantIndex, + onSelectVariant = onSelectVariant, + optionState = optionState, + modifier = Modifier.padding(top = 4.dp), + ) + } + + PatchSelectionAccordion( + iconRes = R.drawable.ic_healing, + titleRes = R.string.patchopts_patches_title, + descriptionRes = R.string.patchopts_patches_desc, + specs = patches, + enabledCount = patches.count(isPatchEnabled), + totalCount = patches.size, + isEnabled = isPatchEnabled, + onToggle = onTogglePatch, + lockState = patchLockState, + variantIndex = variantIndex, + onSelectVariant = onSelectVariant, + optionState = optionState, + modifier = if (integrations.isEmpty()) Modifier.padding(top = 4.dp) else Modifier, + ) + + if (isDevMode) { + TextDivider( + text = stringResource(R.string.patchopts_divider_advanced), + modifier = Modifier.padding(top = 12.dp), + ) + + SwitchPatchOption( + icon = painterResource(R.drawable.ic_bug), + name = stringResource(R.string.patchopts_debuggable_title), + description = stringResource(R.string.patchopts_debuggable_desc), + value = debuggable, + onValueChange = setDebuggable, + ) + + IconPatchOption( + icon = painterResource(R.drawable.ic_music_note), + name = stringResource(R.string.patchopts_custom_tidal_apk_title), + description = stringResource(R.string.patchopts_custom_tidal_apk_desc), + modifier = Modifier.clickable(onClick = onSelectCustomTidalApk), + ) { + FilledTonalButton(onClick = onSelectCustomTidalApk) { + Text( + text = customTidalApk?.version?.toString() + ?: stringResource(R.string.componentopts_selected_none) + ) + } + } + + IconPatchOption( + icon = painterResource(R.drawable.ic_extension), + name = stringResource(R.string.patchopts_custom_patches_title), + description = stringResource(R.string.patchopts_custom_patches_desc), + modifier = Modifier.clickable(onClick = onSelectCustomPatches), + ) { + FilledTonalButton(onClick = onSelectCustomPatches) { + Text( + text = customPatches?.version?.toString() + ?: stringResource(R.string.componentopts_selected_none) + ) + } + } + + SwitchPatchOption( + icon = painterResource(R.drawable.ic_lock_open), + name = stringResource(R.string.patchopts_bypass_incompatible_title), + description = stringResource(R.string.patchopts_bypass_incompatible_desc), + value = bypassIncompatible, + onValueChange = setBypassIncompatible, + ) + } + + Spacer(Modifier.weight(1f)) + + FilledTonalButton( + enabled = isConfigValid, + onClick = onInstall, + colors = ButtonDefaults.filledTonalButtonColors( + contentColor = MaterialTheme.colorScheme.primary, + ), + modifier = Modifier + .padding(bottom = 10.dp) + .align(Alignment.End), + ) { + Text(stringResource(R.string.action_install)) + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameStateLabel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameStateLabel.kt new file mode 100644 index 0000000..cba4818 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameStateLabel.kt @@ -0,0 +1,68 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components + +import com.meowarex.rlmobile.ui.screens.patchopts.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.* + +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +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.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.screens.patchopts.PackageNameState + +@Composable +fun PackageNameStateLabel( + state: PackageNameState, + modifier: Modifier = Modifier, +) { + Crossfade( + targetState = state, + label = "PackageNameStateLabel CrossFade" + ) { animatedState -> + val (label, icon, tint) = when (animatedState) { + PackageNameState.Invalid -> Triple( + R.string.patchopts_pkgname_invalid, + R.drawable.ic_canceled, + MaterialTheme.colorScheme.error, + ) + + PackageNameState.Taken -> Triple( + R.string.patchopts_pkgname_taken, + R.drawable.ic_warning, + Color(0xFFFFCC00), + ) + + PackageNameState.Ok -> Triple( + R.string.patchopts_pkgname_ok, + R.drawable.ic_check_circle, + Color(0xFF59B463), + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Icon( + painter = painterResource(icon), + contentDescription = null, + tint = tint, + modifier = Modifier.size(20.dp), + ) + + Text( + text = stringResource(label), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameUnlockDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameUnlockDialog.kt new file mode 100644 index 0000000..27e75bc --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PackageNameUnlockDialog.kt @@ -0,0 +1,91 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components + +import com.meowarex.rlmobile.ui.screens.patchopts.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.* + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions + +/** + * Confirms unlocking the package name field + * (warning that a custom name blocks the listed patches/integrations from working) + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PackageNameUnlockDialog( + blockedTitles: List, + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + BasicAlertDialog(onDismissRequest = onDismiss) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + ) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(R.string.patchopts_pkgname_warning_title), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = AnnotatedString.fromHtml( + stringResource(R.string.patchopts_pkgname_warning_msg, PatchOptions.Default.packageName), + ), + style = MaterialTheme.typography.bodyLarge, + ) + if (blockedTitles.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(start = 4.dp), + ) { + for (title in blockedTitles) { + Text( + text = "• $title", + style = MaterialTheme.typography.bodyLarge, + fontWeight = FontWeight.Bold, + ) + } + } + } + Text( + text = stringResource(R.string.patchopts_pkgname_warning_permanent), + style = MaterialTheme.typography.bodyLarge, + ) + Box(modifier = Modifier.fillMaxWidth()) { + Row(modifier = Modifier.align(Alignment.CenterEnd)) { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + TextButton(onClick = onConfirm) { + Text(stringResource(R.string.action_continue)) + } + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchAdvancedOptionsSheet.kt new file mode 100644 index 0000000..bedc50b --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchAdvancedOptionsSheet.kt @@ -0,0 +1,685 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components + +import com.meowarex.rlmobile.ui.screens.patchopts.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.* + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +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.toArgb +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.text.input.KeyboardCapitalization +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.ResetToDefaultButton +import com.meowarex.rlmobile.ui.screens.patchopts.OptionLock +import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState +import com.meowarex.rlmobile.ui.screens.patchopts.optionLock +import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec +import kotlinx.coroutines.launch +import kotlin.math.roundToInt + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatchAdvancedOptionsSheet( + patch: PatchSpec, + state: PatchOptionState, + selectedVariant: Int, + onDismiss: () -> Unit, +) { + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scope = rememberCoroutineScope() + var lockDialog by remember { mutableStateOf(null) } + + fun dismiss() { + scope.launch { sheetState.hide() }.invokeOnCompletion { + if (!sheetState.isVisible) onDismiss() + } + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp) + .padding(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(R.string.patchopts_advanced_sheet_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary, + ) + Text( + text = patch.title, + style = MaterialTheme.typography.headlineSmall, + ) + } + + ResetToDefaultButton( + enabled = state.isModified(patch), + onClick = { state.reset(patch) }, + ) + } + + HorizontalDivider() + + val parentKeys = patch.advancedOptions + .filterIsInstance() + .filter { !it.inline } + .map { it.key } + .toSet() + val sheetOptions = patch.advancedOptions.filter { option -> + when { + option is OptionSpec.Toggle && option.inline -> false + option is OptionSpec.Toggle && option.requiresOption in parentKeys -> false + else -> true + } + } + for (option in sheetOptions) key(option.key) { + val lock = patch.optionLock(option, selectedVariant) { state.toggle(patch, it) } + when (option) { + is OptionSpec.Toggle -> { + val toggleOn = state.toggle(patch, option) + val gatedChoices = patch.advancedOptions.filterIsInstance() + .filter { it.requiresOption == option.key } + val gatedToggles = patch.advancedOptions.filterIsInstance() + .filter { it.requiresOption == option.key && !it.inline } + val hasSubOptions = gatedChoices.isNotEmpty() || gatedToggles.isNotEmpty() + val carded = hasSubOptions && toggleOn + + Column( + modifier = if (carded) { + Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f)) + .border( + width = 1.5.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f), + shape = MaterialTheme.shapes.medium, + ) + .padding(horizontal = 12.dp, vertical = 4.dp) + } else { + Modifier.fillMaxWidth() + }, + ) { + ToggleOptionRow( + title = option.title, + description = option.description, + checked = toggleOn, + onCheckedChange = { state.setToggle(patch, option, it) }, + locked = lock != OptionLock.Free, + onLockedTap = { lockDialog = lock }, + modifier = if (hasSubOptions) Modifier.padding(vertical = 6.dp) else Modifier, + ) + if (hasSubOptions) { + AnimatedVisibility(visible = toggleOn) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + gatedChoices.forEach { choice -> + key(choice.key) { + ChoiceOptionRow( + title = choice.title, + entries = choice.entries, + selectedIndex = state.choice(patch, choice), + onSelect = { state.setChoice(patch, choice, it) }, + ) + } + } + gatedToggles.forEach { sub -> + key(sub.key) { + val subLock = patch.optionLock(sub, selectedVariant) { state.toggle(patch, it) } + ToggleOptionRow( + title = sub.title, + description = sub.description, + checked = state.toggle(patch, sub), + onCheckedChange = { state.setToggle(patch, sub, it) }, + locked = subLock != OptionLock.Free, + onLockedTap = { lockDialog = subLock }, + ) + } + } + } + } + } + } + } + + is OptionSpec.Slider -> SliderOptionRow( + title = option.title, + description = option.description, + value = state.slider(patch, option), + valueLabel = formatSliderValue(option, state.slider(patch, option)), + valueRange = option.valueRange, + steps = option.steps, + onValueChange = { state.setSlider(patch, option, it) }, + ) + + is OptionSpec.Choice -> + if (option.requiresOption == null) { + ChoiceOptionRow( + title = option.title, + entries = option.entries, + selectedIndex = state.choice(patch, option), + onSelect = { state.setChoice(patch, option, it) }, + ) + } + + is OptionSpec.Color -> ColorOptionRow( + title = option.title, + color = state.color(patch, option), + onColorChange = { state.setColor(patch, option, it) }, + ) + } + } + + FilledTonalButton( + onClick = { dismiss() }, + colors = ButtonDefaults.filledTonalButtonColors( + contentColor = MaterialTheme.colorScheme.primary, + ), + modifier = Modifier + .align(Alignment.End) + .padding(top = 4.dp), + ) { + Text(stringResource(R.string.action_done)) + } + } + } + + lockDialog?.let { lock -> + OptionLockDialog(lock = lock, onDismiss = { lockDialog = null }) + } +} + +@Composable +private fun ToggleOptionRow( + title: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + locked: Boolean = false, + onLockedTap: () -> Unit = {}, + modifier: Modifier = Modifier, +) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Switch, + ) { if (locked) onLockedTap() else onCheckedChange(!checked) } + .alpha(if (locked) 0.45f else 1f), + ) { + OptionText( + title = title, + description = description, + modifier = Modifier.weight(1f), + ) + + Box { + Switch( + checked = checked, + enabled = !locked, + onCheckedChange = onCheckedChange, + interactionSource = interactionSource, + ) + if (locked) { + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = remember(::MutableInteractionSource), + indication = null, + role = Role.Switch, + ) { onLockedTap() } + ) + } + } + } +} + +@Composable +private fun SliderOptionRow( + title: String, + description: String, + value: Float, + valueLabel: String, + valueRange: ClosedFloatingPointRange, + steps: Int, + onValueChange: (Float) -> Unit, +) { + // Whether the slider snaps to the step dots + var snap by rememberSaveable { mutableStateOf(true) } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + modifier = Modifier.weight(1f), + ) + Text( + text = valueLabel, + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + if (steps > 0) { + IconButton( + onClick = { + snap = !snap + if (snap) onValueChange(snapToNearestStep(value, valueRange, steps)) + }, + modifier = Modifier.size(32.dp), + ) { + Icon( + painter = painterResource( + if (snap) R.drawable.ic_lock else R.drawable.ic_lock_open, + ), + contentDescription = stringResource(R.string.patchopts_toggle_snap), + tint = if (snap) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + } + } + + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + + // Locked -> native stepped slider (dots + snapping) + // Unlocked -> plain continuous slider with the trailing stop-indicator dot removed + if (snap) { + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + steps = steps, + ) + } else { + Slider( + value = value, + onValueChange = onValueChange, + valueRange = valueRange, + track = { SliderDefaults.Track(sliderState = it, drawStopIndicator = null) }, + ) + } + } +} + +private fun snapToNearestStep( + value: Float, + range: ClosedFloatingPointRange, + steps: Int, +): Float { + if (steps <= 0) return value + val stepSize = (range.endInclusive - range.start) / (steps + 1) + if (stepSize <= 0f) return value + val snapped = range.start + Math.round((value - range.start) / stepSize) * stepSize + return snapped.coerceIn(range.start, range.endInclusive) +} + +@Composable +private fun ChoiceOptionRow( + entries: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + title: String = "", + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (title.isNotEmpty()) { + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + if (entries.size > 3) { + var expanded by rememberSaveable { mutableStateOf(false) } + val selectedLabel = entries.getOrNull(selectedIndex).orEmpty() + + Box(modifier = Modifier.fillMaxWidth()) { + OutlinedButton( + onClick = { expanded = true }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = selectedLabel, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier.fillMaxWidth(), + ) { + entries.forEachIndexed { index, entry -> + DropdownMenuItem( + text = { Text(entry) }, + onClick = { + onSelect(index) + expanded = false + }, + ) + } + } + } + return@Column + } + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + entries.forEachIndexed { index, entry -> + SegmentedButton( + selected = index == selectedIndex, + onClick = { onSelect(index) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = entries.size), + icon = {}, + label = { + Text( + text = entry, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelMedium, + ) + }, + ) + } + } + } +} + +@Composable +private fun ColorOptionRow( + title: String, + color: Int, + onColorChange: (Int) -> Unit, +) { + var showPicker by rememberSaveable { mutableStateOf(false) } + val selectedColor = color.withOpaqueAlpha() + val materialYouColor = MaterialTheme.colorScheme.primary.toArgb().withOpaqueAlpha() + val presets = listOf( + stringResource(R.string.patch_waze_color_waze_default), + stringResource(R.string.patch_waze_color_tidal_cyan), + stringResource(R.string.patch_waze_color_material_you), + ) + val presetColors = listOf(WAZE_DEFAULT, TIDAL_CYAN, materialYouColor) + val selectedPreset = presetColors.indexOf(selectedColor) + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterHorizontally), + ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .clickable { showPicker = true } + .padding(vertical = 4.dp), + ) { + ColorSwatch(color = selectedColor) + Text( + text = selectedColor.toRgbHex(), + style = MaterialTheme.typography.labelLarge, + color = MaterialTheme.colorScheme.primary, + ) + } + SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { + presets.forEachIndexed { index, label -> + SegmentedButton( + selected = index == selectedPreset, + onClick = { onColorChange(presetColors[index]) }, + shape = SegmentedButtonDefaults.itemShape(index = index, count = presets.size), + icon = {}, + label = { + Text( + text = label, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelMedium, + ) + }, + ) + } + } + } + + if (showPicker) { + ColorPickerDialog( + initialColor = selectedColor, + onDismiss = { showPicker = false }, + onColorSelected = { + onColorChange(it) + showPicker = false + }, + ) + } +} + +@Composable +private fun ColorSwatch(color: Int, modifier: Modifier = Modifier) { + Box( + modifier = modifier + .size(32.dp) + .clip(CircleShape) + .background(Color(color)) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape), + ) +} + +@Composable +private fun ColorPickerDialog( + initialColor: Int, + onDismiss: () -> Unit, + onColorSelected: (Int) -> Unit, +) { + var color by rememberSaveable(initialColor) { mutableIntStateOf(initialColor.withOpaqueAlpha()) } + var hex by rememberSaveable(initialColor) { mutableStateOf(color.toRgbHex()) } + val parsedHex = parseRgbHex(hex) + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.patch_color_picker_title)) }, + text = { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ColorSwatch(color = parsedHex ?: color, modifier = Modifier.size(40.dp)) + OutlinedTextField( + value = hex, + onValueChange = { value -> + hex = value + parseRgbHex(value)?.let { color = it } + }, + label = { Text(stringResource(R.string.patch_color_picker_hex)) }, + singleLine = true, + isError = parsedHex == null, + keyboardOptions = KeyboardOptions( + capitalization = KeyboardCapitalization.Characters, + keyboardType = KeyboardType.Ascii, + ), + supportingText = if (parsedHex == null) { + { Text(stringResource(R.string.patch_color_picker_hex_error)) } + } else null, + modifier = Modifier.weight(1f), + ) + } + } + }, + confirmButton = { + TextButton( + enabled = parsedHex != null, + onClick = { parsedHex?.let(onColorSelected) }, + ) { + Text(stringResource(R.string.action_done)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.action_cancel)) + } + }, + ) +} + +@Composable +private fun OptionText( + title: String, + description: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } +} + +private fun formatSliderValue(option: OptionSpec.Slider, value: Float): String { + val rounded = value.roundToInt() + val unit = option.unit + return when { + option.displayAsPercent -> "$rounded%" + unit != null -> "$rounded $unit" + else -> rounded.toString() + } +} + +private const val OPAQUE_ALPHA = -0x1000000 +private const val WAZE_DEFAULT = -16747037 // #ff0075e3 +private const val TIDAL_CYAN = -14549268 // #ff21feec + +private fun Int.withOpaqueAlpha(): Int = (this and 0x00FFFFFF) or OPAQUE_ALPHA + +private fun Int.toRgbHex(): String = "#" + + (this and 0x00FFFFFF).toString(16).uppercase().padStart(6, '0') + +private fun parseRgbHex(value: String): Int? { + val clean = value.trim() + .removePrefix("#") + .removePrefix("0x") + .removePrefix("0X") + if (clean.length != 6 && clean.length != 8) return null + val parsed = clean.toLongOrNull(16) ?: return null + val rgb = if (clean.length == 8) parsed and 0x00FFFFFFL else parsed + return (0xFF000000L or rgb).toInt() +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun OptionLockDialog(lock: OptionLock, onDismiss: () -> Unit) { + val message = when (lock) { + is OptionLock.NeedsVariant -> stringResource(R.string.patch_opt_lock_needs_variant, lock.title) + is OptionLock.NeedsOption -> stringResource(R.string.patch_opt_lock_needs_option, lock.title) + OptionLock.Free -> return + } + + BasicAlertDialog(onDismissRequest = onDismiss) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + ) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(R.string.patch_opt_lock_title), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = AnnotatedString.fromHtml(message), + style = MaterialTheme.typography.titleMedium, + ) + Box(modifier = Modifier.fillMaxWidth()) { + TextButton( + onClick = onDismiss, + modifier = Modifier.align(Alignment.CenterEnd), + ) { + Text(stringResource(R.string.action_got_it)) + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchOptionsAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchOptionsAppBar.kt new file mode 100644 index 0000000..d5146c0 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchOptionsAppBar.kt @@ -0,0 +1,33 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components + +import com.meowarex.rlmobile.ui.screens.patchopts.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.* + +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import cafe.adriel.voyager.navigator.LocalNavigator +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.BackButton +import com.meowarex.rlmobile.ui.legacy.screens.settings.SettingsScreen + +@Composable +fun PatchOptionsAppBar( + isUpdate: Boolean = false, +) { + TopAppBar( + navigationIcon = { BackButton() }, + title = { Text(stringResource(if (!isUpdate) R.string.action_add_install else R.string.action_update_install)) }, + actions = { + val navigator = LocalNavigator.current + + IconButton(onClick = { navigator?.push(SettingsScreen()) }) { + Icon( + painter = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings) + ) + } + } + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchSelectionAccordion.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchSelectionAccordion.kt new file mode 100644 index 0000000..0704461 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchSelectionAccordion.kt @@ -0,0 +1,433 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components + +import com.meowarex.rlmobile.ui.screens.patchopts.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.* + + +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +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.draw.rotate +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.fromHtml +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec +import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState +import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions +import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec +import com.meowarex.rlmobile.ui.screens.patchopts.effectiveVariants +import com.meowarex.rlmobile.ui.screens.patchopts.resolveVariantIndex + +private data class LockInfo(val patch: PatchSpec, val lock: PatchLock) + +@Composable +fun PatchSelectionAccordion( + @DrawableRes iconRes: Int, + @StringRes titleRes: Int, + @StringRes descriptionRes: Int, + specs: List, + enabledCount: Int, + totalCount: Int, + isEnabled: (PatchSpec) -> Boolean, + onToggle: (PatchSpec, Boolean) -> Unit, + lockState: (PatchSpec) -> PatchLock, + variantIndex: (PatchSpec) -> Int, + onSelectVariant: (PatchSpec, Int) -> Unit, + optionState: PatchOptionState, + modifier: Modifier = Modifier, +) { + var expanded by rememberSaveable { mutableStateOf(false) } + val rotation by animateFloatAsState( + targetValue = if (expanded) 180f else 0f, + label = "patch-accordion-arrow", + ) + + var lockInfo by remember { mutableStateOf(null) } + var advancedFor by remember { mutableStateOf(null) } + + Column( + modifier = modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.large) + .background(MaterialTheme.colorScheme.surfaceContainerLow), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable(role = Role.Button) { expanded = !expanded } + .padding(horizontal = 16.dp, vertical = 14.dp), + ) { + Icon( + painter = painterResource(iconRes), + contentDescription = null, + ) + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.titleMedium, + ) + Text( + text = stringResource( + R.string.patchopts_patches_summary, + enabledCount, + totalCount, + ), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } + + Icon( + painter = painterResource(R.drawable.ic_arrow_down_small), + contentDescription = stringResource( + if (expanded) R.string.action_collapse else R.string.action_expand, + ), + modifier = Modifier.rotate(rotation), + ) + } + + AnimatedVisibility(visible = expanded) { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background.copy(0.4f)) + .padding(horizontal = 16.dp, vertical = 12.dp), + ) { + Text( + text = stringResource(descriptionRes), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .alpha(.7f) + .padding(bottom = 8.dp), + ) + + for (patch in specs) key(patch.id) { + val checked = isEnabled(patch) + val lock = lockState(patch) + + val inlineToggles = patch.advancedOptions + .filterIsInstance() + .filter { it.inline } + val hasSheetOptions = patch.advancedOptions.any { it !is OptionSpec.Toggle || !it.inline } + val hasSettings = patch.variants.isNotEmpty() || inlineToggles.isNotEmpty() || hasSheetOptions + + // When a patch is enabled and has settings show a carded view of the settings + val carded = checked && hasSettings + Column( + modifier = if (carded) { + Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f)) + .border( + width = 1.5.dp, + color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f), + shape = MaterialTheme.shapes.medium, + ) + .padding(horizontal = 12.dp, vertical = 4.dp) + } else { + Modifier.fillMaxWidth() + }, + ) { + PatchSwitchRow( + title = patch.title, + description = patch.description, + checked = checked, + lock = lock, + onCheckedChange = { onToggle(patch, it) }, + onLockedTap = { lockInfo = LockInfo(patch, lock) }, + ) + + if (hasSettings) { + AnimatedVisibility(visible = checked) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + ) { + val resolvedVariant = + if (patch.variants.isNotEmpty()) + patch.resolveVariantIndex(variantIndex(patch)) { + optionState.toggle(patch, it) + } + else -1 + if (patch.variants.isNotEmpty()) { + val effective = patch.effectiveVariants { optionState.toggle(patch, it) } + PatchVariantSelector( + variants = effective, + selectedIndex = effective + .indexOfFirst { it.originalIndex == resolvedVariant } + .coerceAtLeast(0), + onSelect = { pos -> + effective.getOrNull(pos)?.let { onSelectVariant(patch, it.originalIndex) } + }, + ) + } + + for (option in inlineToggles) key(option.key) { + val show = option.requiresVariant == null || + option.requiresVariant == resolvedVariant + if (show) { + InlineToggleRow( + title = option.title, + description = option.description, + checked = optionState.toggle(patch, option), + onCheckedChange = { optionState.setToggle(patch, option, it) }, + ) + } + } + + if (hasSheetOptions) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.Center, + ) { + AdvancedOptionsButton( + modified = optionState.isModified(patch), + onClick = { advancedFor = patch }, + ) + } + } + } + } + } + } + } + } + } + } + + lockInfo?.let { info -> + PatchLockDialog( + thisPatch = info.patch, + lock = info.lock, + onDismiss = { lockInfo = null }, + ) + } + + advancedFor?.let { patch -> + PatchAdvancedOptionsSheet( + patch = patch, + state = optionState, + selectedVariant = patch.resolveVariantIndex(variantIndex(patch)) { optionState.toggle(patch, it) }, + onDismiss = { advancedFor = null }, + ) + } +} + +@Composable +private fun AdvancedOptionsButton( + modified: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + FilledTonalButton( + onClick = onClick, + modifier = modifier, + ) { + Icon( + painter = painterResource(R.drawable.ic_tune), + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.patchopts_advanced_button)) + if (modified) { + Spacer(Modifier.width(8.dp)) + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primary), + ) + } + } +} + +@Composable +private fun InlineToggleRow( + title: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Switch, + ) { onCheckedChange(!checked) } + .padding(vertical = 4.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f), + ) { + Text(text = title, style = MaterialTheme.typography.titleSmall) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } + Switch( + checked = checked, + onCheckedChange = onCheckedChange, + interactionSource = interactionSource, + ) + } +} + +@Composable +private fun PatchSwitchRow( + title: String, + description: String, + checked: Boolean, + lock: PatchLock, + onCheckedChange: (Boolean) -> Unit, + onLockedTap: () -> Unit, +) { + val interactionSource = remember(::MutableInteractionSource) + val isLocked = lock !is PatchLock.Free + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .clickable( + interactionSource = interactionSource, + indication = null, + role = Role.Switch, + ) { + if (isLocked) onLockedTap() else onCheckedChange(!checked) + } + .alpha(if (isLocked) 0.45f else 1f) + .padding(vertical = 6.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f), + ) { + Text( + text = title, + style = MaterialTheme.typography.titleSmall, + ) + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.alpha(.7f), + ) + } + + Box { + Switch( + checked = checked, + enabled = !isLocked, + onCheckedChange = onCheckedChange, + interactionSource = interactionSource, + ) + if (isLocked) { + Box( + modifier = Modifier + .matchParentSize() + .clickable( + interactionSource = remember(::MutableInteractionSource), + indication = null, + role = Role.Switch, + ) { onLockedTap() } + ) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PatchLockDialog( + thisPatch: PatchSpec, + lock: PatchLock, + onDismiss: () -> Unit, +) { + val (titleRes, msgRes, blockerTitle) = when (lock) { + is PatchLock.LockedOn -> Triple( + R.string.patch_lock_required_title, + R.string.patch_lock_required_msg, + lock.by.title, + ) + is PatchLock.LockedOff -> Triple( + R.string.patch_lock_blocked_title, + R.string.patch_lock_blocked_msg, + lock.by.title, + ) + PatchLock.RequiresDefaultPackage -> Triple( + R.string.patch_lock_pkgname_title, + R.string.patch_lock_pkgname_msg, + PatchOptions.Default.packageName, + ) + PatchLock.Free -> return + } + + BasicAlertDialog(onDismissRequest = onDismiss) { + Surface( + shape = MaterialTheme.shapes.large, + color = MaterialTheme.colorScheme.surfaceContainerHigh, + tonalElevation = 6.dp, + ) { + Column( + modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + Text( + text = stringResource(titleRes), + style = MaterialTheme.typography.titleLarge, + ) + Text( + text = AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle)), + style = MaterialTheme.typography.bodyLarge, + ) + Box(modifier = Modifier.fillMaxWidth()) { + TextButton( + onClick = onDismiss, + modifier = Modifier.align(Alignment.CenterEnd), + ) { + Text(stringResource(R.string.action_got_it)) + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchVariantSelector.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchVariantSelector.kt new file mode 100644 index 0000000..5d4d43a --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/PatchVariantSelector.kt @@ -0,0 +1,47 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components + +import com.meowarex.rlmobile.ui.screens.patchopts.components.* +import com.meowarex.rlmobile.ui.screens.patchopts.* + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import com.meowarex.rlmobile.ui.screens.patchopts.EffectiveVariant + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun PatchVariantSelector( + variants: List, + selectedIndex: Int, + onSelect: (Int) -> Unit, + modifier: Modifier = Modifier, +) { + SingleChoiceSegmentedButtonRow( + modifier = modifier.fillMaxWidth(), + ) { + variants.forEachIndexed { index, variant -> + SegmentedButton( + selected = index == selectedIndex, + enabled = variant.enabled, + onClick = { if (variant.enabled) onSelect(index) }, + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = variants.size, + ), + icon = {}, + label = { + Text( + text = variant.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.labelMedium, + ) + }, + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/IconPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/IconPatchOption.kt new file mode 100644 index 0000000..1a188ac --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/IconPatchOption.kt @@ -0,0 +1,52 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.options + +import com.meowarex.rlmobile.ui.screens.patchopts.components.options.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +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.graphics.painter.Painter +import androidx.compose.ui.unit.dp + +@Composable +fun IconPatchOption( + icon: Painter, + name: String, + description: String, + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit, +) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = modifier, + ) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(26.dp), + ) + + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.weight(1f) + ) { + Text( + text = name, + style = MaterialTheme.typography.titleMedium, + ) + + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier + .alpha(.7f) + ) + } + + content() + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/SwitchPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/SwitchPatchOption.kt new file mode 100644 index 0000000..2f35014 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/SwitchPatchOption.kt @@ -0,0 +1,52 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.options + +import com.meowarex.rlmobile.ui.screens.patchopts.components.options.* + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Switch +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp + +@Composable +fun SwitchPatchOption( + icon: Painter, + name: String, + description: String, + value: Boolean, + onValueChange: (Boolean) -> Unit, + enabled: Boolean = true, + modifier: Modifier = Modifier, +) { + val interactionSource = remember(::MutableInteractionSource) + val onClick = remember(value) { { onValueChange(!value) } } + + IconPatchOption( + icon = icon, + name = name, + description = description, + modifier = modifier + .fillMaxWidth() + .clickable( + onClick = onClick, + enabled = enabled, + interactionSource = interactionSource, + indication = null, + role = Role.Switch, + ), + ) { + Switch( + checked = value, + enabled = enabled, + onCheckedChange = onValueChange, + interactionSource = interactionSource, + modifier = Modifier.padding(start = 6.dp), + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/TextPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/TextPatchOption.kt new file mode 100644 index 0000000..bf80193 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/patchopts/components/options/TextPatchOption.kt @@ -0,0 +1,84 @@ +package com.meowarex.rlmobile.ui.legacy.screens.patchopts.components.options + +import com.meowarex.rlmobile.ui.screens.patchopts.components.options.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.Label +import com.meowarex.rlmobile.ui.legacy.components.ResetToDefaultButton + +@Composable +fun TextPatchOption( + name: String, + description: String, + value: String, + valueIsError: Boolean, + valueIsDefault: Boolean, + onValueChange: (String) -> Unit, + onValueReset: () -> Unit, + modifier: Modifier = Modifier, + locked: Boolean? = null, + onLockClick: () -> Unit = {}, + extra: (@Composable ColumnScope.() -> Unit)? = null, +) { + Label( + name = name, + description = description, + modifier = modifier, + ) { + OutlinedTextField( + value = value, + onValueChange = onValueChange, + isError = valueIsError, + singleLine = true, + enabled = locked != true, + colors = OutlinedTextFieldDefaults.colors( + unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + errorContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, + ), + trailingIcon = { + Row(verticalAlignment = Alignment.CenterVertically) { + if (locked != true) { + ResetToDefaultButton( + enabled = !valueIsDefault, + onClick = onValueReset, + ) + } + if (locked != null) { + IconButton( + onClick = onLockClick, + modifier = Modifier.size(32.dp), + ) { + Icon( + painter = painterResource( + if (locked) R.drawable.ic_lock else R.drawable.ic_lock_open, + ), + contentDescription = stringResource( + if (locked) R.string.patchopts_field_unlock else R.string.patchopts_field_lock, + ), + tint = if (locked) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + Spacer(Modifier.width(8.dp)) + } + } + }, + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 4.dp), + ) + + extra?.invoke(this) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/PermissionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/PermissionsScreen.kt new file mode 100644 index 0000000..061f8db --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/PermissionsScreen.kt @@ -0,0 +1,224 @@ +package com.meowarex.rlmobile.ui.legacy.screens.permissions + +import com.meowarex.rlmobile.ui.screens.permissions.* + +import android.os.Build +import android.os.Parcelable +import androidx.compose.animation.EnterTransition +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.annotation.ExperimentalVoyagerApi +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.stack.StackEvent +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import cafe.adriel.voyager.transitions.ScreenTransition +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.manager.InstallerSetting +import com.meowarex.rlmobile.ui.legacy.components.TextDivider +import com.meowarex.rlmobile.ui.legacy.components.settings.SettingsItem +import com.meowarex.rlmobile.ui.legacy.screens.home.HomeScreen +import com.meowarex.rlmobile.ui.legacy.screens.permissions.components.PermissionButton +import com.meowarex.rlmobile.ui.legacy.screens.permissions.components.PermissionsAppBar +import com.meowarex.rlmobile.ui.legacy.screens.settings.components.InstallersDialog +import com.meowarex.rlmobile.ui.util.paddings.* +import com.meowarex.rlmobile.ui.util.spacedByLastAtBottom +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize +import org.koin.compose.viewmodel.koinActivityViewModel + +@Parcelize +@OptIn(ExperimentalVoyagerApi::class) +class PermissionsScreen : Screen, ScreenTransition, Parcelable { + @IgnoredOnParcel + override val key = "Legacy-Permissions" + + override fun enter(lastEvent: StackEvent): EnterTransition? = EnterTransition.None + + @Composable + override fun Content() { + val navigator = LocalNavigator.currentOrThrow + val model = koinActivityViewModel() + + // Go back (HomeScreen) when all permissions have been granted + LaunchedEffect(model.allPermsGranted) { + if (model.allPermsGranted) + navigator.pop() + } + + if (model.showInstallersDialog) { + InstallersDialog( + currentInstaller = model.installer, + onDismiss = model::hideInstallersDialog, + onConfirm = model::setInstaller, + ) + } + + PermissionsScreenContent( + installer = model.installer, + openInstallersDialog = model::showInstallersDialog, + storagePermsGranted = model.storagePermsGranted, + onGrantStoragePerms = if (Build.VERSION.SDK_INT >= 30) { + model::requestManageStoragePerms + } else { + model::requestStoragePerms + }, + unknownSourcesPermsGranted = model.unknownSourcesPermsGranted, + onGrantUnknownSourcesPerms = model::requestUnknownSourcesPerms, + notificationsPermsGranted = model.notificationsPermsGranted, + onGrantNotificationsPerms = model::requestNotificationsPerms, + batteryPermsGranted = model.batteryPermsGranted, + onGrantBatteryPerms = model::grantBatteryPerms, + canContinue = model.requiredPermsGranted, + onContinue = { navigator.replace(HomeScreen()) }, + ) + } +} + +@Composable +fun PermissionsScreenContent( + installer: InstallerSetting, + openInstallersDialog: () -> Unit, + storagePermsGranted: Boolean, + onGrantStoragePerms: () -> Unit, + unknownSourcesPermsGranted: Boolean, + onGrantUnknownSourcesPerms: () -> Unit, + notificationsPermsGranted: Boolean, + onGrantNotificationsPerms: () -> Unit, + batteryPermsGranted: Boolean, + onGrantBatteryPerms: () -> Unit, + canContinue: Boolean, + onContinue: () -> Unit, +) { + Scaffold( + topBar = { PermissionsAppBar() }, + ) { padding -> + LazyColumn( + verticalArrangement = Arrangement.spacedByLastAtBottom(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + contentPadding = padding + .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) + .add(PaddingValues(bottom = 12.dp, top = 24.dp)), + modifier = Modifier + .fillMaxSize() + .padding(padding.exclude(PaddingValuesSides.Bottom)) + ) { + item(key = "DIVIDER_OPTIONS", contentType = "DIVIDER") { + TextDivider( + text = stringResource(R.string.permissions_header_options), + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + + item(key = "INSTALLER") { + SettingsItem( + text = { Text(stringResource(R.string.setting_installer)) }, + secondaryText = { Text(stringResource(R.string.setting_installer_desc)) }, + icon = { Icon(painterResource(R.drawable.ic_apk_install), null) }, + modifier = Modifier.clickable(onClick = openInstallersDialog), + ) { + FilledTonalButton(onClick = openInstallersDialog) { + Icon( + painter = installer.icon(), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .padding(end = 6.dp), + ) + Text(installer.title()) + } + } + } + + item(key = "DIVIDER_PERMS", contentType = "DIVIDER") { + TextDivider( + text = stringResource(R.string.permissions_header_permissions), + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + + if (installer == InstallerSetting.PackageInstaller) { + item(key = "PERMS_UNKNOWN_SOURCES", contentType = "PERMISSION_BUTTON") { + PermissionButton( + name = stringResource(R.string.permissions_install_title), + description = stringResource(R.string.permissions_install_desc), + granted = unknownSourcesPermsGranted, + required = true, + icon = painterResource(R.drawable.ic_alt_route), + onClick = onGrantUnknownSourcesPerms, + modifier = Modifier.animateItem(), + ) + } + } + + item(key = "PERMS_STORAGE", contentType = "PERMISSION_BUTTON") { + PermissionButton( + name = stringResource(R.string.permissions_storage_title), + description = stringResource(R.string.permissions_storage_desc), + granted = storagePermsGranted, + required = true, + icon = painterResource(R.drawable.ic_save), + onClick = onGrantStoragePerms, + ) + } + + item(key = "PERMS_NOTIFICATIONS", contentType = "PERMISSION_BUTTON") { + PermissionButton( + name = stringResource(R.string.permissions_notifs_title), + description = stringResource(R.string.permissions_notifs_desc), + granted = notificationsPermsGranted, + required = false, + icon = painterResource(R.drawable.ic_bell), + onClick = onGrantNotificationsPerms, + ) + } + + item(key = "PERMS_BATTERY", contentType = "PERMISSION_BUTTON") { + PermissionButton( + name = stringResource(R.string.permissions_battery_title), + description = stringResource(R.string.permissions_battery_desc), + granted = batteryPermsGranted, + required = false, + icon = painterResource(R.drawable.ic_battery_settings), + onClick = onGrantBatteryPerms, + ) + } + + item(key = "LEGEND") { + Text( + text = stringResource(R.string.permissions_legend, "*"), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + modifier = Modifier + .padding(horizontal = 32.dp, vertical = 12.dp) + .fillMaxWidth(), + ) + } + + item(key = "CONTINUE") { + Row( + horizontalArrangement = Arrangement.End, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp, end = 32.dp), + ) { + FilledTonalButton( + onClick = onContinue, + enabled = canContinue, + ) { + Text(stringResource(R.string.action_continue)) + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionButton.kt new file mode 100644 index 0000000..d2de24c --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionButton.kt @@ -0,0 +1,80 @@ +package com.meowarex.rlmobile.ui.legacy.screens.permissions.components + +import com.meowarex.rlmobile.ui.screens.permissions.components.* +import com.meowarex.rlmobile.ui.screens.permissions.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.meowarex.rlmobile.R + +@Composable +fun PermissionButton( + name: String, + description: String, + granted: Boolean, + required: Boolean, + icon: Painter, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = modifier + .heightIn(min = 64.dp) + .fillMaxWidth() + .padding(horizontal = 32.dp, vertical = 8.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Companion.CenterVertically, + ) { + Icon( + painter = icon, + contentDescription = null, + modifier = Modifier.size(20.dp), + ) + + ProvideTextStyle(MaterialTheme.typography.titleSmall) { + Text(name) + + if (required) { + Text( + text = "*", + color = MaterialTheme.colorScheme.error, + fontSize = 10.sp, + ) + } + } + } + + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.Companion.CenterVertically, + ) { + ProvideTextStyle( + MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(0.6f), + ), + ) { + Text( + text = description, + modifier = Modifier.weight(1f), + ) + } + + OutlinedButton( + onClick = onClick, + enabled = !granted, + ) { + Text(stringResource(if (granted) R.string.permissions_granted else R.string.permissions_grant)) + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionsAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionsAppBar.kt new file mode 100644 index 0000000..5c1a19c --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/permissions/components/PermissionsAppBar.kt @@ -0,0 +1,48 @@ +package com.meowarex.rlmobile.ui.legacy.screens.permissions.components + +import com.meowarex.rlmobile.ui.screens.permissions.components.* +import com.meowarex.rlmobile.ui.screens.permissions.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.navigator.LocalNavigator +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.screens.settings.SettingsScreen + +@Composable +fun PermissionsAppBar() { + LargeTopAppBar( + title = { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.padding(start = 12.dp), + ) { + Text( + text = stringResource(R.string.permissions_title), + style = MaterialTheme.typography.displaySmall, + ) + Text( + text = stringResource(R.string.permissions_subtitle), + style = MaterialTheme.typography.bodyLarge.copy( + color = MaterialTheme.colorScheme.onSurface.copy(.6f), + ), + ) + } + }, + actions = { + val navigator = LocalNavigator.current + + IconButton(onClick = { navigator?.push(SettingsScreen()) }) { + Icon( + painter = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings) + ) + } + } + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/SettingsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/SettingsScreen.kt new file mode 100644 index 0000000..65a5d47 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/SettingsScreen.kt @@ -0,0 +1,251 @@ +package com.meowarex.rlmobile.ui.legacy.screens.settings + +import com.meowarex.rlmobile.ui.screens.settings.* + +import android.os.Build +import android.os.Parcelable +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.koin.koinScreenModel +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.BackButton +import com.meowarex.rlmobile.ui.legacy.components.MainActionButton +import com.meowarex.rlmobile.ui.legacy.components.settings.* +import com.meowarex.rlmobile.ui.legacy.screens.settings.components.InstallersDialog +import com.meowarex.rlmobile.ui.legacy.screens.settings.components.ThemeDialog +import com.meowarex.rlmobile.ui.legacy.screens.settings.components.UiStyleDialog +import com.meowarex.rlmobile.ui.util.paddings.* +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +class SettingsScreen : Screen, Parcelable { + @IgnoredOnParcel + override val key = "Legacy-Settings" + + @Composable + override fun Content() { + val model = koinScreenModel() + var clearedCache by rememberSaveable { mutableStateOf(false) } + val preferences = model.preferences + + if (model.showThemeDialog) { + ThemeDialog( + currentTheme = preferences.theme, + onDismiss = model::hideThemeDialog, + onConfirm = model::setTheme + ) + } + + if (model.showUiStyleDialog) { + UiStyleDialog( + current = preferences.uiStyle, + onDismiss = model::hideUiStyleDialog, + onConfirm = model::setUiStyle, + ) + } + + if (model.showInstallersDialog) { + InstallersDialog( + currentInstaller = preferences.installer, + onDismiss = model::hideInstallersDialog, + onConfirm = model::setInstaller, + ) + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.navigation_settings)) }, + navigationIcon = { BackButton() }, + ) + }, + ) { padding -> + LazyColumn( + contentPadding = padding + .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) + .add(PaddingValues(bottom = 12.dp)), + modifier = Modifier + .fillMaxSize() + .padding(padding.exclude(PaddingValuesSides.Bottom)) + ) { + item(key = "HEADER_APPEARANCE", contentType = "DIVIDER") { + SettingsHeader(stringResource(R.string.settings_header_appearance)) + } + + item(key = "SETTING_THEME") { + SettingsItem( + modifier = Modifier.clickable(onClick = model::showThemeDialog), + icon = { Icon(painterResource(R.drawable.ic_brush), null) }, + text = { Text(stringResource(R.string.setting_theme)) }, + secondaryText = { Text(stringResource(R.string.setting_theme_desc)) } + ) { + FilledTonalButton(onClick = model::showThemeDialog) { + Text(preferences.theme.toDisplayName()) + } + } + } + + item(key = "SETTING_UI_STYLE") { + SettingsItem( + modifier = Modifier.clickable(onClick = model::showUiStyleDialog), + icon = { Icon(painterResource(R.drawable.ic_sparkle), null) }, + text = { Text(stringResource(R.string.setting_ui_style)) }, + secondaryText = { Text(stringResource(R.string.setting_ui_style_desc)) } + ) { + FilledTonalButton(onClick = model::showUiStyleDialog) { + Text(preferences.uiStyle.toDisplayName()) + } + } + } + + // Material You theming on Android 12+ + if (Build.VERSION.SDK_INT >= 31) { + item(key = "SETTING_DYNAMIC_COLOR", contentType = "SETTING_SWITCH") { + SettingsSwitch( + label = stringResource(R.string.setting_dynamic_color), + secondaryLabel = stringResource(R.string.setting_dynamic_color_desc), + pref = preferences.dynamicColor, + icon = { Icon(painterResource(R.drawable.ic_palette), null) }, + onPrefChange = { preferences.dynamicColor = it }, + ) + } + } + + item(key = "SETTING_AUTO_UPDATE_CHECK", contentType = "SETTING_SWITCH") { + SettingsSwitch( + label = stringResource(R.string.setting_auto_update_check), + secondaryLabel = stringResource(R.string.setting_auto_update_check_desc), + pref = preferences.autoUpdateCheck, + icon = { Icon(painterResource(R.drawable.ic_update), null) }, + onPrefChange = { model.setAutoUpdateCheck(it) }, + ) + } + + item(key = "HEADER_INSTALL", contentType = "DIVIDER") { + SettingsHeader(stringResource(R.string.settings_header_installation)) + } + + item(key = "SETTING_INSTALLER") { + SettingsItem( + text = { Text(stringResource(R.string.setting_installer)) }, + secondaryText = { Text(stringResource(R.string.setting_installer_desc)) }, + icon = { Icon(painterResource(R.drawable.ic_apk_install), null) }, + modifier = Modifier.clickable(onClick = model::showInstallersDialog), + ) { + FilledTonalButton(onClick = model::showInstallersDialog) { + Icon( + painter = preferences.installer.icon(), + contentDescription = null, + modifier = Modifier + .size(20.dp) + .padding(end = 6.dp), + ) + Text(preferences.installer.title()) + } + } + } + + item(key = "SETTING_KEEP_APK", contentType = "SETTING_SWITCH") { + SettingsSwitch( + label = stringResource(R.string.setting_keep_patched_apks), + secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc), + icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) }, + pref = preferences.keepPatchedApks, + onPrefChange = { model.setKeepPatchedApks(it) }, + ) + } + + if (preferences.keepPatchedApks) { + item(key = "BUTTON_EXPORT_APK", contentType = "BUTTON") { + MainActionButton( + text = stringResource(R.string.settings_export_apk), + icon = painterResource(R.drawable.ic_save), + enabled = model.patchedApkExists, + onClick = model::shareApk, + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, top = 16.dp) + .fillMaxWidth() + .animateItem(), + ) + } + } + + item(key = "BUTTON_CACHE_CLEAR", contentType = "BUTTON") { + MainActionButton( + text = stringResource(R.string.settings_clear_cache), + icon = painterResource(R.drawable.ic_delete_forever), + enabled = !clearedCache, + colors = IconButtonDefaults.filledTonalIconButtonColors( + containerColor = MaterialTheme.colorScheme.error, + ), + onClick = { + clearedCache = true + model.clearCache() + }, + modifier = Modifier + .padding(start = 32.dp, end = 32.dp, top = 18.dp) + .fillMaxWidth() + ) + } + + item(key = "HEADER_ADVANCED", contentType = "DIVIDER") { + SettingsHeader(stringResource(R.string.settings_header_advanced)) + } + + item(key = "SETTING_DEVMODE", contentType = "SETTING_SWITCH") { + SettingsSwitch( + label = stringResource(R.string.setting_developer_options), + secondaryLabel = stringResource(R.string.setting_developer_options_desc), + pref = preferences.devMode, + icon = { Icon(painterResource(R.drawable.ic_code), null) }, + onPrefChange = { preferences.devMode = it }, + ) + } + + item(key = "HEADER_INFO", contentType = "DIVIDER") { + SettingsHeader(stringResource(R.string.settings_header_info)) + } + + item(key = "INFO") { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(horizontal = 36.dp, vertical = 12.dp), + ) { + Text( + text = model.installInfo, + style = MaterialTheme.typography.bodySmall.copy( + color = MaterialTheme.colorScheme.onSurface.copy(0.6f), + ), + ) + + Spacer(Modifier.weight(1f, fill = true)) + + IconButton(onClick = model::copyInstallInfo) { + Icon( + painter = painterResource(R.drawable.ic_copy), + contentDescription = stringResource(R.string.action_copy), + modifier = Modifier + .size(28.dp) + .alpha(.8f), + ) + } + } + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/InstallersDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/InstallersDialog.kt new file mode 100644 index 0000000..032b9b7 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/InstallersDialog.kt @@ -0,0 +1,183 @@ +package com.meowarex.rlmobile.ui.legacy.screens.settings.components + +import com.meowarex.rlmobile.ui.screens.settings.components.* +import com.meowarex.rlmobile.ui.screens.settings.* + +import androidx.compose.foundation.* +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.manager.* +import com.meowarex.rlmobile.util.showToast +import com.topjohnwu.superuser.Shell +import org.koin.compose.koinInject + +@Composable +fun InstallersDialog( + currentInstaller: InstallerSetting, + onDismiss: () -> Unit, + onConfirm: (InstallerSetting) -> Unit, +) { + val context = LocalContext.current + val shizuku = koinInject() + val dhizuku = koinInject() + + var shizukuAvailable by remember { mutableStateOf(false) } + var dhizukuAvailable by remember { mutableStateOf(false) } + var selectedInstaller by rememberSaveable { mutableStateOf(currentInstaller) } + + LaunchedEffect(Unit) { + shizukuAvailable = shizuku.shizukuAvailable() + dhizukuAvailable = dhizuku.dhizukuAvailable() + } + + // Check if selected installer is usable and ask for permissions when necessary + LaunchedEffect(selectedInstaller) { + when (selectedInstaller) { + InstallerSetting.PackageInstaller -> { + // Once the Google sideloading block is in place, + // check whether it is applicable to the device, and if so then it needs + // to be inaccessible. (Disable button) + } + + InstallerSetting.Root -> { + val shell = Shell.getShell() + if (!shell.isRoot) { + shell.waitAndClose() + Shell.getShell() + } + + if (Shell.isAppGrantedRoot() != true) { + context.showToast(R.string.permissions_root_denied) + selectedInstaller = InstallerSetting.PackageInstaller + } + } + + InstallerSetting.Intent -> { + // don't know whether this device supports this method + } + + InstallerSetting.Shizuku -> { + if (!shizuku.requestPermissions()) { + selectedInstaller = InstallerSetting.PackageInstaller + } + } + + InstallerSetting.Dhizuku -> { + if (!dhizuku.requestPermissions()) { + selectedInstaller = InstallerSetting.PackageInstaller + } + } + } + } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_apk_install), + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + }, + title = { Text(stringResource(R.string.setting_installer)) }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + for (installer in InstallerSetting.entries) key(installer) { + InstallerItem( + installer = installer, + selected = installer == selectedInstaller, + enabled = when (installer) { + InstallerSetting.PackageInstaller -> true + InstallerSetting.Root -> true + InstallerSetting.Intent -> true + InstallerSetting.Shizuku -> shizukuAvailable + InstallerSetting.Dhizuku -> dhizukuAvailable + }, + onClick = { selectedInstaller = installer }, + ) + } + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(selectedInstaller) + onDismiss() + }, + ) { + Text(stringResource(R.string.action_apply)) + } + }, + ) +} + +@Composable +private fun InstallerItem( + installer: InstallerSetting, + selected: Boolean, + enabled: Boolean, + onClick: () -> Unit, +) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.Companion.CenterVertically, + modifier = Modifier.Companion + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = onClick, + ) + .clip(MaterialTheme.shapes.medium) + .padding(horizontal = 6.dp, vertical = 8.dp), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.weight(1f), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.Companion.CenterVertically, + ) { + Icon( + painter = installer.icon(), + contentDescription = null, + modifier = Modifier + .size(20.dp) + ) + Text( + text = installer.title(), + style = MaterialTheme.typography.labelLarge + .copy(fontSize = 14.sp) + ) + } + Text( + text = installer.description(), + style = MaterialTheme.typography.labelMedium.copy( + color = MaterialTheme.colorScheme.onSurface.copy(alpha = .6f), + ), + ) + } + + Spacer(Modifier.Companion.weight(0.05f, true)) + + RadioButton( + selected = selected, + enabled = enabled, + onClick = onClick, + interactionSource = interactionSource, + ) + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/ThemeDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/ThemeDialog.kt new file mode 100644 index 0000000..9c66669 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/ThemeDialog.kt @@ -0,0 +1,92 @@ +package com.meowarex.rlmobile.ui.legacy.screens.settings.components + +import com.meowarex.rlmobile.ui.screens.settings.components.* +import com.meowarex.rlmobile.ui.screens.settings.* + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.theme.Theme + +@Composable +fun ThemeDialog( + currentTheme: Theme, + onDismiss: () -> Unit, + onConfirm: (Theme) -> Unit, +) { + var selectedTheme by rememberSaveable { mutableStateOf(currentTheme) } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_brush), + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + }, + title = { Text(stringResource(R.string.setting_theme)) }, + text = { + Column { + for (theme in Theme.entries) key(theme) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.Companion.CenterVertically, + modifier = Modifier.Companion + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = { selectedTheme = theme }, + ) + .clip(MaterialTheme.shapes.medium) + .padding(horizontal = 6.dp, vertical = 8.dp), + ) { + Icon( + painter = theme.toPainter(), + contentDescription = null, + modifier = Modifier + .padding(end = 14.dp) + .size(26.dp), + ) + + Text( + text = theme.toDisplayName(), + style = MaterialTheme.typography.labelLarge + .copy(fontSize = 14.sp) + ) + + Spacer(Modifier.Companion.weight(1f, true)) + + RadioButton( + selected = theme == selectedTheme, + onClick = { selectedTheme = theme }, + interactionSource = interactionSource, + ) + } + } + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(selectedTheme) + onDismiss() + }, + ) { + Text(stringResource(R.string.action_apply)) + } + }, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/UiStyleDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/UiStyleDialog.kt new file mode 100644 index 0000000..ee3c04b --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/screens/settings/components/UiStyleDialog.kt @@ -0,0 +1,93 @@ +package com.meowarex.rlmobile.ui.legacy.screens.settings.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.theme.UiStyle + +/** + * The Legacy counterpart of the UI style picker + * + * Without this you are stuck in legacy style for literally ever, like until the aliens find us. ever.. + */ +@Composable +fun UiStyleDialog( + current: UiStyle, + onDismiss: () -> Unit, + onConfirm: (UiStyle) -> Unit, +) { + var selected by rememberSaveable { mutableStateOf(current) } + + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_sparkle), + contentDescription = null, + modifier = Modifier.size(32.dp), + ) + }, + title = { Text(stringResource(R.string.setting_ui_style)) }, + text = { + Column { + for (style in UiStyle.entries) key(style) { + val interactionSource = remember(::MutableInteractionSource) + + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = { selected = style }, + ) + .clip(MaterialTheme.shapes.medium) + .padding(horizontal = 6.dp, vertical = 8.dp), + ) { + Icon( + painter = style.toPainter(), + contentDescription = null, + modifier = Modifier + .padding(end = 14.dp) + .size(26.dp), + ) + + Text( + text = style.toDisplayName(), + style = MaterialTheme.typography.labelLarge.copy(fontSize = 14.sp), + ) + + Spacer(Modifier.weight(1f, true)) + + RadioButton( + selected = style == selected, + onClick = { selected = style }, + interactionSource = interactionSource, + ) + } + } + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(selected) + onDismiss() + }, + ) { + Text(stringResource(R.string.action_apply)) + } + }, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/managerupdate/ManagerUpdateDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/managerupdate/ManagerUpdateDialog.kt new file mode 100644 index 0000000..84688c2 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/managerupdate/ManagerUpdateDialog.kt @@ -0,0 +1,135 @@ +package com.meowarex.rlmobile.ui.legacy.widgets.managerupdate + +import com.meowarex.rlmobile.ui.widgets.managerupdate.* + +import androidx.annotation.DrawableRes +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.legacy.components.Tag + + +@Composable +fun ManagerUpdateDialog( + deltas: List, + onDismiss: () -> Unit, +) { + AlertDialog( + onDismissRequest = onDismiss, + confirmButton = { + FilledTonalButton( + onClick = onDismiss, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + Text(stringResource(R.string.action_continue)) + } + }, + title = { + Text( + text = stringResource(R.string.manager_update_title), + textAlign = TextAlign.Center, + ) + }, + text = { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.manager_update_subtitle), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + deltas.forEach { delta -> + DeltaCard(delta = delta) + } + } + } + }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_update), + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + }, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = false, + ), + ) +} + +@Composable +private fun DeltaCard(delta: VersionDelta) { + val changed = delta.from != null && delta.from != delta.to + val subtitle = when { + delta.from == null || delta.from == delta.to -> delta.to + else -> "${delta.from} → ${delta.to}" + } + + Surface( + color = MaterialTheme.colorScheme.surfaceContainerHigh, + shape = RoundedCornerShape(16.dp), + modifier = Modifier.fillMaxWidth(), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier.padding(horizontal = 14.dp, vertical = 12.dp), + ) { + DeltaIcon(delta) + Column(modifier = Modifier.weight(1f)) { + Text( + text = delta.label, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = if (changed) MaterialTheme.colorScheme.primary + else LocalContentColor.current.copy(alpha = 0.65f), + fontWeight = if (changed) FontWeight.SemiBold else FontWeight.Normal, + ) + } + delta.tag?.let { Tag(text = it) } + } + } +} + +@Composable +private fun DeltaIcon(delta: VersionDelta) { + Surface( + shape = CircleShape, + color = MaterialTheme.colorScheme.secondaryContainer, + modifier = Modifier.size(40.dp), + ) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Icon( + painter = painterResource(delta.iconRes), + contentDescription = null, + tint = MaterialTheme.colorScheme.onSecondaryContainer, + modifier = Modifier.size(22.dp), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/updater/UpdaterDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/updater/UpdaterDialog.kt new file mode 100644 index 0000000..fb050ee --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/legacy/widgets/updater/UpdaterDialog.kt @@ -0,0 +1,105 @@ +package com.meowarex.rlmobile.ui.legacy.widgets.updater + +import com.meowarex.rlmobile.ui.widgets.updater.* + +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.DialogProperties +import com.meowarex.rlmobile.R +import org.koin.androidx.compose.koinViewModel + +@Composable +fun UpdaterDialog( + viewModel: UpdaterViewModel = koinViewModel(), +) { + if (!viewModel.showDialog) return + + val isWorking by viewModel.isWorking.collectAsState() + val downloadProgress by viewModel.progress.collectAsState() + val downloadInProgress by remember { derivedStateOf { downloadProgress != null } } + + AlertDialog( + confirmButton = { + FilledTonalButton( + onClick = viewModel::triggerUpdate, + colors = ButtonDefaults.filledTonalButtonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) { + if (!isWorking) { + Text(stringResource(R.string.action_update)) + } else if (!downloadInProgress) { + CircularProgressIndicator( + color = MaterialTheme.colorScheme.onSecondary, + strokeWidth = 2.5.dp, + modifier = Modifier.size(20.dp) + ) + } else { + CircularProgressIndicator( + progress = { downloadProgress ?: 1f }, + color = MaterialTheme.colorScheme.onSecondary, + trackColor = MaterialTheme.colorScheme.onSecondary.copy(alpha = 0.6f), + strokeWidth = 2.5.dp, + modifier = Modifier.size(20.dp) + ) + } + } + }, + dismissButton = { + TextButton( + onClick = viewModel::dismissDialog, + enabled = !isWorking, + ) { + Text(stringResource(R.string.action_dismiss)) + } + }, + onDismissRequest = {}, + title = { + Text(stringResource(R.string.updater_title, viewModel.targetVersion ?: "")) + }, + text = { + val uriHandler = LocalUriHandler.current + + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = stringResource(R.string.updater_body), + textAlign = TextAlign.Center, + ) + + TextButton( + onClick = { uriHandler.openUri(viewModel.targetReleaseUrl!!) } + ) { + Text( + text = stringResource(R.string.updater_open_github), + textAlign = TextAlign.Center, + textDecoration = TextDecoration.Underline, + ) + } + } + }, + icon = { + Icon( + painter = painterResource(R.drawable.ic_warning), + contentDescription = null, + modifier = Modifier.size(36.dp), + ) + }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/about/AboutScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/about/AboutScreen.kt index 0e0d477..e717779 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/about/AboutScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/about/AboutScreen.kt @@ -85,7 +85,7 @@ fun AboutScreenContent(state: State) { AboutScreenState.Loading -> item(key = "CONTRIBUTIONS_LOADING") { Box( contentAlignment = Alignment.Center, - content = { CircularProgressIndicator() }, + content = { LoadingIndicator() }, modifier = Modifier .fillMaxWidth() .padding(vertical = 38.dp), diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/ComponentOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/ComponentOptionsScreen.kt index e7714ea..d484502 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/ComponentOptionsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/ComponentOptionsScreen.kt @@ -21,6 +21,8 @@ import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize import com.meowarex.rlmobile.ui.screens.componentopts.components.* import com.meowarex.rlmobile.ui.util.ScreenWithResult import com.meowarex.rlmobile.ui.util.paddings.* @@ -160,11 +162,11 @@ fun ComponentOptionsScreenContent( .fillMaxWidth() .padding(top = 10.dp), ) { - FilledTonalButton( + RadiantButton( + text = stringResource(R.string.action_confirm), onClick = onBackPressed, - ) { - Text(stringResource(R.string.action_confirm)) - } + size = RadiantButtonSize.Small, + ) } } } @@ -312,18 +314,16 @@ private fun ReleaseRow( color = LocalContentColor.current.copy(alpha = 0.55f), ) } - FilledTonalButton( - onClick = onImport, - enabled = !anyImporting, - ) { - if (importing) { - CircularProgressIndicator( - strokeWidth = 2.dp, - modifier = Modifier.size(16.dp), - ) - } else { - Text(stringResource(R.string.action_install)) - } + // Swap the button out for the indicator while this specific release imports + if (importing) { + CircularWavyProgressIndicator(modifier = Modifier.size(30.dp)) + } else { + RadiantButton( + text = stringResource(R.string.action_install), + onClick = onImport, + enabled = !anyImporting, + size = RadiantButtonSize.Small, + ) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/ComponentOptionsAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/ComponentOptionsAppBar.kt index 84f4a82..680dde1 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/ComponentOptionsAppBar.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/ComponentOptionsAppBar.kt @@ -19,9 +19,7 @@ fun ComponentOptionsAppBar( TopAppBar( navigationIcon = { BackButton() }, title = { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - ) { + Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { Text(stringResource(R.string.componentopts_screen_title, componentType.name)) Text( text = stringResource(R.string.componentopts_screen_desc), diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/PatchComponentCard.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/PatchComponentCard.kt index 6167992..3b6241c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/PatchComponentCard.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/componentopts/components/PatchComponentCard.kt @@ -17,6 +17,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantRadio import com.meowarex.rlmobile.network.utils.SemVer import kotlin.time.Instant @@ -65,15 +67,13 @@ fun PatchComponentCard( Spacer(Modifier.weight(1f, fill = true)) - IconButton( + RadiantIconButton( + icon = painterResource(R.drawable.ic_delete_forever), + contentDescription = stringResource(R.string.action_delete), + tint = MaterialTheme.colorScheme.error, + subtle = true, onClick = onDelete, - ) { - Icon( - painter = painterResource(R.drawable.ic_delete_forever), - tint = MaterialTheme.colorScheme.error, - contentDescription = stringResource(R.string.action_delete), - ) - } + ) } } @@ -103,7 +103,7 @@ fun PatchComponentCardBase( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(10.dp), ) { - RadioButton( + RadiantRadio( selected = selected, onClick = onSelect, interactionSource = interaction, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt index 2eb722b..8e07c9c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeModel.kt @@ -211,6 +211,14 @@ class HomeModel( return createPrefilledPatchOptsScreen(current.packageName) } + /** + * The prefilled options behind [createRepatchScreen] without the Screen wrapper (needed for legacy ui) + */ + fun createRepatchOptions(): PatchOptions? { + val current = (state as? HomeState.Loaded)?.install ?: return null + return prefilledPatchOptions(current.packageName) + } + fun openApp(packageName: String) { val launchIntent = application.packageManager.getLaunchIntentForPackage(packageName) if (launchIntent != null) { @@ -228,11 +236,12 @@ class HomeModel( application.startActivity(launchIntent) } - fun createPrefilledPatchOptsScreen(packageName: String): PatchOptionsScreen { - val patchOptions = loadInstallMetadata(packageName)?.options + fun createPrefilledPatchOptsScreen(packageName: String): PatchOptionsScreen = + PatchOptionsScreen(prefilledOptions = prefilledPatchOptions(packageName)) + + fun prefilledPatchOptions(packageName: String): PatchOptions = + loadInstallMetadata(packageName)?.options ?: PatchOptions.Default.copy(packageName = packageName) - return PatchOptionsScreen(prefilledOptions = patchOptions) - } private fun loadInstallMetadata(packageName: String): InstallMetadata? = try { val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt index 2d75b89..43d14e5 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/HomeScreen.kt @@ -1,11 +1,12 @@ package com.meowarex.rlmobile.ui.screens.home import android.os.Parcelable +import androidx.activity.ComponentActivity import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.Image import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* @@ -14,7 +15,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.painter.BitmapPainter -import androidx.activity.ComponentActivity +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -29,8 +30,13 @@ import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.meowarex.rlmobile.R -import com.meowarex.rlmobile.ui.components.SegmentedButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantCard import com.meowarex.rlmobile.ui.components.Tag +import com.meowarex.rlmobile.ui.theme.radiantStyle import com.meowarex.rlmobile.ui.screens.about.AboutScreen import com.meowarex.rlmobile.ui.screens.home.components.CommitList import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsScreen @@ -42,6 +48,15 @@ import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import org.koin.androidx.compose.koinViewModel +// Connected group shapes (i'm pretty sure these are the segment control replacements, but i havent worked on this in months so fuck knows) +private val ConnectedLeadingShape = RoundedCornerShape( + topStart = 20.dp, bottomStart = 20.dp, topEnd = 6.dp, bottomEnd = 6.dp, +) +private val ConnectedTrailingShape = RoundedCornerShape( + topStart = 6.dp, bottomStart = 6.dp, topEnd = 20.dp, bottomEnd = 20.dp, +) +private val ConnectedPressedShape = RoundedCornerShape(8.dp) + @Parcelize class HomeScreen : Screen, Parcelable { @IgnoredOnParcel @@ -63,35 +78,37 @@ class HomeScreen : Screen, Parcelable { Scaffold( topBar = { + // Compact bar TopAppBar( - title = { Text(stringResource(R.string.navigation_home)) }, + title = { Text(stringResource(R.string.rlmobile)) }, actions = { - IconButton(onClick = { model.refresh() }) { - Icon( - painterResource(R.drawable.ic_refresh), - contentDescription = stringResource(R.string.navigation_refresh), - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_refresh), + contentDescription = stringResource(R.string.navigation_refresh), + subtle = true, + onClick = { model.refresh() }, + ) if (managerUpdateAvailable) { - IconButton(onClick = updater::reopenDialog) { - Icon( - painterResource(R.drawable.ic_update), - contentDescription = stringResource(R.string.action_update), - ) - } - } - IconButton(onClick = { navigator.push(AboutScreen()) }) { - Icon( - painterResource(R.drawable.ic_info), - contentDescription = stringResource(R.string.navigation_about), - ) - } - IconButton(onClick = { navigator.push(SettingsScreen()) }) { - Icon( - painterResource(R.drawable.ic_settings), - contentDescription = stringResource(R.string.navigation_settings), + RadiantIconButton( + icon = painterResource(R.drawable.ic_update), + contentDescription = stringResource(R.string.action_update), + accent = true, + size = 40.dp, + onClick = updater::reopenDialog, ) } + RadiantIconButton( + icon = painterResource(R.drawable.ic_info), + contentDescription = stringResource(R.string.navigation_about), + subtle = true, + onClick = { navigator.push(AboutScreen()) }, + ) + RadiantIconButton( + icon = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings), + subtle = true, + onClick = { navigator.push(SettingsScreen()) }, + ) }, ) }, @@ -101,15 +118,17 @@ class HomeScreen : Screen, Parcelable { verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier .padding(pv) - .padding(16.dp) + .padding(horizontal = 16.dp) .fillMaxSize(), ) { - val state = model.state - when (state) { + when (val state = model.state) { HomeState.Loading -> Box( contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize(), - ) { CircularProgressIndicator() } + ) { + // Expressive loader (a sequence of morphing polygons rather than a spinner.) [M3E is cool i guess] + ContainedLoadingIndicator() + } is HomeState.Loaded -> HomeContent( state = state, @@ -153,6 +172,83 @@ private fun ColumnScope.HomeContent( ?.let { "v${it.name} (build ${it.code})" } val latestVersionName = state.latestTidalVersionCode?.let { "build $it" } + val patchesBehind = install != null && install.patchesUpToDate == false + val tidalBehind = install != null && install.tidalUpToDate == false + + InstallHeroCard( + install = install, + currentVersionName = currentVersionName, + latestVersionName = latestVersionName, + patchesBehind = patchesBehind, + tidalBehind = tidalBehind, + onLaunch = onLaunch, + onInfo = onInfo, + ) + + val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind) + val canLocalRepatch = install != null && state.offlineRepatchReady + val onlineEnabled = state.latestTidalVersionCode != null || install != null + val buttonEnabled = !blockedByManagerUpdate && (if (state.offline) canLocalRepatch else onlineEnabled) + + val label = when { + blockedByManagerUpdate -> "Manager Update Required" + state.offline && install != null -> "Local Repatch" + state.offline -> "No Network" + install == null && state.latestTidalVersionCode == null -> "Loading…" + install == null -> "Install" + patchesBehind && tidalBehind -> "Update Patches & TIDAL" + patchesBehind -> "Update Patches" + tidalBehind -> "Update TIDAL" + else -> "Repatch" + } + + // The main button (repatch or update button) + RadiantButton( + text = label, + icon = painterResource( + if (install == null) R.drawable.ic_download else R.drawable.ic_sparkle + ), + onClick = if (install == null) onInstall else onRepatch, + enabled = buttonEnabled, + style = RadiantButtonStyle.Accent, + size = RadiantButtonSize.Large, + fillWidth = true, + ) + + if (state.offline && install != null) { + Text( + text = "No Network Connection", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth(), + ) + } + + RadiantCard( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + ) { + CommitList(commits = commits.collectAsLazyPagingItems()) + } + + Spacer(Modifier.height(4.dp)) +} + +/** + * The identity block: (what is installed & which build etc etc whatever) + */ +@Composable +private fun InstallHeroCard( + install: InstallData?, + currentVersionName: String?, + latestVersionName: String?, + patchesBehind: Boolean, + tidalBehind: Boolean, + onLaunch: () -> Unit, + onInfo: () -> Unit, +) { val fallbackPainter = if (install?.icon == null) { // R.mipmap.ic_launcher is an adaptive-icon XML on API 26+, which painterResource cannot decode. val context = LocalContext.current @@ -163,113 +259,108 @@ private fun ColumnScope.HomeContent( } else null val iconPainter = install?.icon ?: fallbackPainter - if (iconPainter != null) { - Image( - painter = iconPainter, - contentDescription = null, - modifier = Modifier - .size(60.dp) - .clip(CircleShape), - ) - } - Text( - text = install?.name ?: stringResource(R.string.app_name), - style = MaterialTheme.typography.titleLarge, - ) + val hero = radiantStyle.heroCard - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth(), - ) { - AnimatedVisibility(visible = currentVersionName != null) { - Text( - text = "Current: ${currentVersionName ?: "-"}", - style = MaterialTheme.typography.labelLarge, - color = LocalContentColor.current.copy(alpha = 0.5f), - textAlign = TextAlign.Center, - ) - } - AnimatedVisibility(visible = latestVersionName != null) { - Text( - text = "Latest: ${latestVersionName ?: "-"}", - style = MaterialTheme.typography.labelLarge, - color = LocalContentColor.current.copy(alpha = 0.5f), - textAlign = TextAlign.Center, - ) - } - } - - val patchesBehind = install != null && install.patchesUpToDate == false - val tidalBehind = install != null && install.tidalUpToDate == false - AnimatedVisibility(visible = patchesBehind || tidalBehind) { - Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) { - if (patchesBehind) Tag(text = "New Patches!") - if (tidalBehind) Tag(text = "TIDAL Update!") - } - } - - val blockedByManagerUpdate = managerUpdateAvailable && (patchesBehind || tidalBehind) - val canLocalRepatch = install != null && state.offlineRepatchReady - val onlineEnabled = state.latestTidalVersionCode != null || install != null - val buttonEnabled = !blockedByManagerUpdate && (if (state.offline) canLocalRepatch else onlineEnabled) - Button( - onClick = if (install == null) onInstall else onRepatch, - enabled = buttonEnabled, - modifier = Modifier.fillMaxWidth(), - ) { - val label = when { - blockedByManagerUpdate -> "Manager Update Required" - state.offline && install != null -> "Local Repatch" - state.offline -> "No Network" - install == null && state.latestTidalVersionCode == null -> "Loading…" - install == null -> "Install" - patchesBehind && tidalBehind -> "Update Patches & TIDAL" - patchesBehind -> "Update Patches" - tidalBehind -> "Update TIDAL" - else -> "Repatch" - } - Text( - text = label, - textAlign = TextAlign.Center, - maxLines = 1, - modifier = Modifier - .basicMarquee() - .fillMaxWidth(), - ) - } - - if (state.offline && install != null) { - Text( - text = "No Network Connection", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.Center, + // Legacy keeps the original flat centred stack rather than wrapping it in a raised card. (thx claude) + HeroContainer(hero) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(10.dp), modifier = Modifier .fillMaxWidth() - .padding(top = 6.dp), - ) - } - - AnimatedVisibility(visible = install != null) { - Row( - horizontalArrangement = Arrangement.spacedBy(2.dp), - modifier = Modifier.clip(RoundedCornerShape(16.dp)), + .padding(horizontal = 18.dp, vertical = 18.dp), ) { - SegmentedButton( - icon = painterResource(R.drawable.ic_launch), - text = stringResource(R.string.action_launch), - onClick = onLaunch, - ) - SegmentedButton( - icon = painterResource(R.drawable.ic_info), - text = stringResource(R.string.action_open_info), - onClick = onInfo, + if (iconPainter != null) { + // Clipped to a 9-sided cookie rather than a circle — the app icon is the one place + // a non-rectilinear silhouette reads as deliberate instead of noisy. (thx claude again) + Image( + painter = iconPainter, + contentDescription = null, + modifier = Modifier + .size(60.dp) + .clip(MaterialShapes.Cookie9Sided.toShape()), + ) + } + + Text( + text = install?.name ?: stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + textAlign = TextAlign.Center, ) + + if (currentVersionName != null || latestVersionName != null) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp), + ) { + currentVersionName?.let { + Text( + text = "Current: $it", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + latestVersionName?.let { + Text( + text = "Latest: $it", + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } + } + } + + AnimatedVisibility(visible = patchesBehind || tidalBehind) { + Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { + if (patchesBehind) Tag( + text = "New Patches!", + icon = painterResource(R.drawable.ic_sparkle), + ) + if (tidalBehind) Tag( + text = "TIDAL Update!", + icon = painterResource(R.drawable.ic_update), + tint = MaterialTheme.colorScheme.tertiary, + ) + } + } + + AnimatedVisibility(visible = install != null) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + RadiantButton( + text = stringResource(R.string.action_launch), + icon = painterResource(R.drawable.ic_launch), + onClick = onLaunch, + modifier = Modifier.weight(1f), + ) + RadiantButton( + text = stringResource(R.string.action_open_info), + icon = painterResource(R.drawable.ic_info), + onClick = onInfo, + modifier = Modifier.weight(1f), + ) + } + } } } +} - ElevatedCard(modifier = Modifier.fillMaxSize()) { - CommitList(commits = commits.collectAsLazyPagingItems()) +/** + * Wraps the home identity block in a raised card for Radiant (renders it inline for Legacy UI) + */ +@Composable +private fun HeroContainer( + carded: Boolean, + content: @Composable ColumnScope.() -> Unit, +) { + if (carded) { + RadiantCard(modifier = Modifier.fillMaxWidth(), content = content) + } else { + Column(modifier = Modifier.fillMaxWidth(), content = content) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/components/CommitList.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/components/CommitList.kt index 4443cbf..d2f6b07 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/components/CommitList.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/home/components/CommitList.kt @@ -1,6 +1,8 @@ package com.meowarex.rlmobile.ui.screens.home.components import androidx.compose.foundation.clickable +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.CircleShape @@ -59,7 +61,11 @@ fun CommitList(commits: LazyPagingItems) { modifier = Modifier.fillMaxWidth().padding(16.dp), ) { Text("Failed to load commits", style = MaterialTheme.typography.labelLarge, textAlign = TextAlign.Center) - Button(onClick = { commits.retry() }) { Text("Retry") } + RadiantButton( + text = "Retry", + onClick = { commits.retry() }, + size = RadiantButtonSize.Small, + ) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/log/components/LogAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/log/components/LogAppBar.kt index f41ef9e..e527aad 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/log/components/LogAppBar.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/log/components/LogAppBar.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton import com.meowarex.rlmobile.ui.components.BackButton @Composable @@ -16,18 +17,18 @@ fun LogAppBar( title = { Text(stringResource(R.string.log_title)) }, navigationIcon = { BackButton() }, actions = { - IconButton(onClick = onExportLog) { - Icon( - painter = painterResource(R.drawable.ic_save), - contentDescription = stringResource(R.string.log_action_export), - ) - } - IconButton(onClick = onShareLog) { - Icon( - painter = painterResource(R.drawable.ic_share), - contentDescription = stringResource(R.string.log_action_share), - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_save), + contentDescription = stringResource(R.string.log_action_export), + subtle = true, + onClick = onExportLog, + ) + RadiantIconButton( + icon = painterResource(R.drawable.ic_share), + contentDescription = stringResource(R.string.log_action_share), + subtle = true, + onClick = onShareLog, + ) }, ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreen.kt index 1c713fe..98cfec4 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreen.kt @@ -25,6 +25,7 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.meowarex.rlmobile.BuildConfig import com.meowarex.rlmobile.R import com.meowarex.rlmobile.patcher.steps.StepGroup +import com.meowarex.rlmobile.ui.components.DangerActionButton import com.meowarex.rlmobile.ui.components.MainActionButton import com.meowarex.rlmobile.ui.components.Wakelock import com.meowarex.rlmobile.ui.components.dialogs.InstallerAbortDialog @@ -130,6 +131,14 @@ class PatchingScreen( modifier = Modifier .padding(paddingValues.exclude(PaddingValuesSides.Bottom)), ) { + OverallProgressBar( + steps = model.steps, + state = state, + modifier = Modifier + .padding(horizontal = 16.dp) + .fillMaxWidth(), + ) + LazyColumn( state = listState, verticalArrangement = Arrangement.spacedByLastAtBottom(0.dp), @@ -178,9 +187,9 @@ class PatchingScreen( TextBanner( text = stringResource(R.string.installer_banner_success), icon = painterResource(R.drawable.ic_check_circle), - iconColor = Color(0xFF59B463), - outlineColor = MaterialTheme.colorScheme.surfaceVariant, - containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + iconColor = MaterialTheme.customColors.success, + outlineColor = null, + containerColor = MaterialTheme.customColors.successContainer, modifier = Modifier .padding(bottom = VERTICAL_PADDING) .fillMaxWidth(), @@ -249,13 +258,10 @@ class PatchingScreen( } } - MainActionButton( + DangerActionButton( text = stringResource(R.string.settings_clear_cache), icon = painterResource(R.drawable.ic_delete_forever), enabled = !cacheCleared, - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.error, - ), onClick = { cacheCleared = true model.clearCache() diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreenState.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreenState.kt index 0df2b60..d2d3160 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreenState.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/PatchingScreenState.kt @@ -13,4 +13,7 @@ val PatchingScreenState.isProgressChange: Boolean inline get() = this != CloseScreen val PatchingScreenState.isFinished: Boolean - inline get() = isProgressChange || this == CloseScreen + // Was `isProgressChange || this == CloseScreen`, which expands to `x != CloseScreen || + // x == CloseScreen` — a tautology, so this was unconditionally true and the battery + // optimisation banner it gates could never appear. (thx claude) + inline get() = this is PatchingScreenState.Success || this is PatchingScreenState.Failed diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/OverallProgressBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/OverallProgressBar.kt new file mode 100644 index 0000000..cec8922 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/OverallProgressBar.kt @@ -0,0 +1,96 @@ +package com.meowarex.rlmobile.ui.screens.patching.components + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.layout.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.patcher.steps.StepGroup +import com.meowarex.rlmobile.patcher.steps.base.Step +import com.meowarex.rlmobile.ui.screens.patching.PatchingScreenState +import com.meowarex.rlmobile.ui.screens.patching.isFinished +import com.meowarex.rlmobile.ui.theme.customColors +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +import kotlin.math.roundToInt + +/** + * Whole install progress + */ +@Composable +fun OverallProgressBar( + steps: ImmutableMap>?, + state: PatchingScreenState, + modifier: Modifier = Modifier, +) { + val allSteps = remember(steps) { steps?.values?.flatten().orEmpty() } + if (allSteps.isEmpty()) return + + val completed = allSteps.count { it.state.isFinished } + val fraction = completed.toFloat() / allSteps.size + val animatedFraction by animateFloatAsState( + targetValue = fraction, + label = "OverallProgress", + ) + + val failed = state is PatchingScreenState.Failed + val succeeded = state is PatchingScreenState.Success + + val trackColor = MaterialTheme.colorScheme.surfaceContainerHighest + val indicatorColor = when { + failed -> MaterialTheme.colorScheme.error + succeeded -> MaterialTheme.customColors.success + else -> MaterialTheme.colorScheme.primary + } + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier.padding(bottom = 4.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = stringResource( + R.string.installer_progress_summary, + completed, + allSteps.size, + ), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(Modifier.weight(1f)) + + Text( + text = "${(animatedFraction * 100).roundToInt()}%", + style = MaterialTheme.typography.labelLarge, + color = indicatorColor, + ) + } + + // finished run gets the flat determinate bar a live one keeps its wave (M3E) + if (state.isFinished) { + LinearProgressIndicator( + progress = { animatedFraction }, + color = indicatorColor, + trackColor = trackColor, + modifier = Modifier + .fillMaxWidth() + .height(10.dp), + ) + } else { + LinearWavyProgressIndicator( + progress = { animatedFraction }, + color = indicatorColor, + trackColor = trackColor, + modifier = Modifier.fillMaxWidth(), + ) + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/PatchingAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/PatchingAppBar.kt index f3bc755..e54e0e2 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/PatchingAppBar.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/PatchingAppBar.kt @@ -5,6 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton @Composable fun PatchingAppBar( @@ -13,12 +14,12 @@ fun PatchingAppBar( TopAppBar( title = { Text(stringResource(R.string.installer)) }, navigationIcon = { - IconButton(onClick = onBack) { - Icon( - painter = painterResource(R.drawable.ic_back), - contentDescription = stringResource(R.string.navigation_back), - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.navigation_back), + subtle = true, + onClick = onBack, + ) } ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepGroupCard.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepGroupCard.kt index d8a0392..f4225a3 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepGroupCard.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepGroupCard.kt @@ -14,6 +14,7 @@ import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R import com.meowarex.rlmobile.patcher.steps.base.Step import com.meowarex.rlmobile.patcher.steps.base.StepState +import com.meowarex.rlmobile.ui.theme.radiantSurface import kotlinx.collections.immutable.ImmutableList @Composable @@ -56,8 +57,12 @@ fun StepGroupCard( Column( modifier = modifier + .radiantSurface( + shape = MaterialTheme.shapes.large, + // running group lights its edge so the active stage is obvious + active = groupState == StepState.Running, + ) .clip(MaterialTheme.shapes.large) - .background(MaterialTheme.colorScheme.surfaceContainerLow) ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -101,7 +106,8 @@ fun StepGroupCard( Column( verticalArrangement = Arrangement.spacedBy(16.dp), modifier = Modifier - .background(MaterialTheme.colorScheme.background.copy(0.6f)) + // solid darker container cause opinions exist + .background(MaterialTheme.colorScheme.surfaceContainerLowest) .fillMaxWidth() .padding(20.dp) .padding(start = 4.dp) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepStateIcon.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepStateIcon.kt index d86809d..13f3fe9 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepStateIcon.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/StepStateIcon.kt @@ -7,7 +7,6 @@ import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription @@ -15,7 +14,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.Dp import com.meowarex.rlmobile.R import com.meowarex.rlmobile.patcher.steps.base.StepState -import kotlin.math.floor +import com.meowarex.rlmobile.ui.theme.customColors import kotlin.math.roundToInt @Composable @@ -40,12 +39,11 @@ fun StepStateIcon( ) StepState.Running -> { - val strokeWidth = Dp(floor(size.value / 10) + 1) - if (stepProgress > .05f) { - CircularProgressIndicator( + // Determinate work gets the wavy indicator — the amplitude of the wave is + // what distinguishes "actively moving" from a stalled determinate arc. (claudes idea) + CircularWavyProgressIndicator( progress = { animatedProgress }, - strokeWidth = strokeWidth, modifier = Modifier .size(size) .semantics { contentDescription = "${(stepProgress * 100).roundToInt()}%" }, @@ -53,9 +51,7 @@ fun StepStateIcon( } else { val description = stringResource(R.string.status_ongoing) - // Infinite spinner - CircularProgressIndicator( - strokeWidth = strokeWidth, + CircularWavyProgressIndicator( modifier = Modifier .size(size) .semantics { contentDescription = description }, @@ -66,7 +62,7 @@ fun StepStateIcon( StepState.Success -> Icon( painter = painterResource(R.drawable.ic_check_circle), contentDescription = stringResource(R.string.status_success), - tint = Color(0xFF59B463), + tint = MaterialTheme.customColors.success, modifier = Modifier.size(size) ) @@ -80,7 +76,7 @@ fun StepStateIcon( StepState.Skipped -> Icon( painter = painterResource(R.drawable.ic_check_circle), contentDescription = stringResource(R.string.status_skipped), - tint = Color(0xFFAEAEAE), + tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(size) ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/TextBanner.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/TextBanner.kt index a97ace9..1c8ea9e 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/TextBanner.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patching/components/TextBanner.kt @@ -29,10 +29,10 @@ fun TextBanner( border( width = 2.dp, color = color, - shape = MaterialTheme.shapes.medium, + shape = MaterialTheme.shapes.large, ) } - .clip(MaterialTheme.shapes.medium) + .clip(MaterialTheme.shapes.large) .background(containerColor) .thenIf(onClick) { clickable(onClick = it) } ) { diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt index ae1d19e..8231ba1 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsModel.kt @@ -7,6 +7,7 @@ import androidx.compose.runtime.* import cafe.adriel.voyager.core.model.ScreenModel import cafe.adriel.voyager.core.model.screenModelScope import cafe.adriel.voyager.navigator.Navigator +import com.meowarex.rlmobile.ui.util.ScreenWithResult import com.github.diamondminer88.zip.ZipReader import com.meowarex.rlmobile.BuildConfig import com.meowarex.rlmobile.manager.PathManager @@ -252,22 +253,24 @@ class PatchOptionsModel( var customPatches by mutableStateOf(null) private set - fun selectCustomTidalApk(navigator: Navigator) = screenModelScope.launch { - customTidalApk = navigator.pushForResult( - ComponentOptionsScreen( - default = customTidalApk, - componentType = PatchComponent.Type.TidalApk, - ) - ) + // The picker screen is supplied by the caller so the Legacy UI can push its own component + // screen instead of the Radiant one. Defaults keep every existing call site unchanged. (claudes bug fix) + fun selectCustomTidalApk( + navigator: Navigator, + screen: (PatchComponent?) -> ScreenWithResult = { default -> + ComponentOptionsScreen(default = default, componentType = PatchComponent.Type.TidalApk) + }, + ) = screenModelScope.launch { + customTidalApk = navigator.pushForResult(screen(customTidalApk)) } - fun selectCustomPatches(navigator: Navigator) = screenModelScope.launch { - customPatches = navigator.pushForResult( - ComponentOptionsScreen( - default = customPatches, - componentType = PatchComponent.Type.Patches, - ) - ) + fun selectCustomPatches( + navigator: Navigator, + screen: (PatchComponent?) -> ScreenWithResult = { default -> + ComponentOptionsScreen(default = default, componentType = PatchComponent.Type.Patches) + }, + ) = screenModelScope.launch { + customPatches = navigator.pushForResult(screen(customPatches)) reloadSpecs(customPatches) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt index 2153ea3..83037ef 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/PatchOptionsScreen.kt @@ -16,6 +16,8 @@ import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize import com.meowarex.rlmobile.ui.components.* import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen @@ -252,12 +254,12 @@ fun PatchOptionsScreenContent( description = stringResource(R.string.patchopts_custom_tidal_apk_desc), modifier = Modifier.clickable(onClick = onSelectCustomTidalApk), ) { - FilledTonalButton(onClick = onSelectCustomTidalApk) { - Text( - text = customTidalApk?.version?.toString() - ?: stringResource(R.string.componentopts_selected_none) - ) - } + RadiantButton( + text = customTidalApk?.version?.toString() + ?: stringResource(R.string.componentopts_selected_none), + onClick = onSelectCustomTidalApk, + size = RadiantButtonSize.Small, + ) } IconPatchOption( @@ -266,12 +268,12 @@ fun PatchOptionsScreenContent( description = stringResource(R.string.patchopts_custom_patches_desc), modifier = Modifier.clickable(onClick = onSelectCustomPatches), ) { - FilledTonalButton(onClick = onSelectCustomPatches) { - Text( - text = customPatches?.version?.toString() - ?: stringResource(R.string.componentopts_selected_none) - ) - } + RadiantButton( + text = customPatches?.version?.toString() + ?: stringResource(R.string.componentopts_selected_none), + onClick = onSelectCustomPatches, + size = RadiantButtonSize.Small, + ) } SwitchPatchOption( @@ -285,18 +287,13 @@ fun PatchOptionsScreenContent( Spacer(Modifier.weight(1f)) - FilledTonalButton( + MainActionButton( + text = stringResource(R.string.action_install), + icon = painterResource(R.drawable.ic_download), enabled = isConfigValid, onClick = onInstall, - colors = ButtonDefaults.filledTonalButtonColors( - contentColor = MaterialTheme.colorScheme.primary, - ), - modifier = Modifier - .padding(bottom = 10.dp) - .align(Alignment.End), - ) { - Text(stringResource(R.string.action_install)) - } + modifier = Modifier.padding(bottom = 10.dp), + ) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameStateLabel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameStateLabel.kt index 51a1491..9e40ece 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameStateLabel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameStateLabel.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R import com.meowarex.rlmobile.ui.screens.patchopts.PackageNameState +import com.meowarex.rlmobile.ui.theme.customColors @Composable fun PackageNameStateLabel( @@ -39,7 +40,7 @@ fun PackageNameStateLabel( PackageNameState.Ok -> Triple( R.string.patchopts_pkgname_ok, R.drawable.ic_check_circle, - Color(0xFF59B463), + MaterialTheme.customColors.success, ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt index 3b6afdc..2d9ca79 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PackageNameUnlockDialog.kt @@ -1,19 +1,11 @@ package com.meowarex.rlmobile.ui.screens.patchopts.components import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString @@ -21,68 +13,54 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.fromHtml import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions /** * Confirms unlocking the package-name field, warning that a custom name * blocks the listed patches/integrations from working. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PackageNameUnlockDialog( blockedTitles: List, onConfirm: () -> Unit, onDismiss: () -> Unit, ) { - BasicAlertDialog(onDismissRequest = onDismiss) { - Surface( - shape = MaterialTheme.shapes.large, - color = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 6.dp, - ) { - Column( - modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - Text( - text = stringResource(R.string.patchopts_pkgname_warning_title), - style = MaterialTheme.typography.titleLarge, - ) - Text( - text = AnnotatedString.fromHtml( - stringResource(R.string.patchopts_pkgname_warning_msg, PatchOptions.Default.packageName), + RadiantDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.patchopts_pkgname_warning_title), + confirmText = stringResource(R.string.action_continue), + onConfirm = onConfirm, + destructive = true, + dismissText = stringResource(R.string.action_cancel), + onDismissAction = onDismiss, + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + text = AnnotatedString.fromHtml( + stringResource( + R.string.patchopts_pkgname_warning_msg, + PatchOptions.Default.packageName, ), - style = MaterialTheme.typography.bodyLarge, - ) - if (blockedTitles.isNotEmpty()) { - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - modifier = Modifier.padding(start = 4.dp), - ) { - for (title in blockedTitles) { - Text( - text = "• $title", - style = MaterialTheme.typography.bodyLarge, - fontWeight = FontWeight.Bold, - ) - } - } - } - Text( - text = stringResource(R.string.patchopts_pkgname_warning_permanent), - style = MaterialTheme.typography.bodyLarge, - ) - Box(modifier = Modifier.fillMaxWidth()) { - Row(modifier = Modifier.align(Alignment.CenterEnd)) { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.action_cancel)) - } - TextButton(onClick = onConfirm) { - Text(stringResource(R.string.action_continue)) - } + ), + ) + + if (blockedTitles.isNotEmpty()) { + Column( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier.padding(start = 4.dp), + ) { + for (title in blockedTitles) { + Text( + text = "• $title", + color = MaterialTheme.colorScheme.onSurface, + fontWeight = FontWeight.Bold, + ) } } } + + Text(stringResource(R.string.patchopts_pkgname_warning_permanent)) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt index 20e1ee7..1c5a808 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchAdvancedOptionsSheet.kt @@ -30,6 +30,15 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantSegmented +import com.meowarex.rlmobile.ui.components.radiant.RadiantSlider +import com.meowarex.rlmobile.ui.components.radiant.RadiantSwitch +import com.meowarex.rlmobile.ui.components.radiant.RadiantTextField import com.meowarex.rlmobile.ui.components.ResetToDefaultButton import com.meowarex.rlmobile.ui.screens.patchopts.OptionLock import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec @@ -211,17 +220,14 @@ fun PatchAdvancedOptionsSheet( } } - FilledTonalButton( + RadiantButton( + text = stringResource(R.string.action_done), onClick = { dismiss() }, - colors = ButtonDefaults.filledTonalButtonColors( - contentColor = MaterialTheme.colorScheme.primary, - ), + style = RadiantButtonStyle.Accent, modifier = Modifier .align(Alignment.End) .padding(top = 4.dp), - ) { - Text(stringResource(R.string.action_done)) - } + ) } } @@ -261,7 +267,7 @@ private fun ToggleOptionRow( ) Box { - Switch( + RadiantSwitch( checked = checked, enabled = !locked, onCheckedChange = onCheckedChange, @@ -311,23 +317,20 @@ private fun SliderOptionRow( color = MaterialTheme.colorScheme.primary, ) if (steps > 0) { - IconButton( + RadiantIconButton( + icon = painterResource( + if (snap) R.drawable.ic_lock else R.drawable.ic_lock_open, + ), + contentDescription = stringResource(R.string.patchopts_toggle_snap), + tint = if (snap) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + subtle = true, + size = 32.dp, onClick = { snap = !snap if (snap) onValueChange(snapToNearestStep(value, valueRange, steps)) }, - modifier = Modifier.size(32.dp), - ) { - Icon( - painter = painterResource( - if (snap) R.drawable.ic_lock else R.drawable.ic_lock_open, - ), - contentDescription = stringResource(R.string.patchopts_toggle_snap), - tint = if (snap) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp), - ) - } + ) } } @@ -340,18 +343,17 @@ private fun SliderOptionRow( // Locked -> native stepped slider (dots + snapping), left exactly as-is // Unlocked -> plain continuous slider with the trailing stop-indicator dot removed if (snap) { - Slider( + RadiantSlider( value = value, onValueChange = onValueChange, valueRange = valueRange, steps = steps, ) } else { - Slider( + RadiantSlider( value = value, onValueChange = onValueChange, valueRange = valueRange, - track = { SliderDefaults.Track(sliderState = it, drawStopIndicator = null) }, ) } } @@ -395,16 +397,12 @@ private fun ChoiceOptionRow( val selectedLabel = entries.getOrNull(selectedIndex).orEmpty() Box(modifier = Modifier.fillMaxWidth()) { - OutlinedButton( + RadiantButton( + text = selectedLabel, onClick = { expanded = true }, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = selectedLabel, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) - } + style = RadiantButtonStyle.Ghost, + fillWidth = true, + ) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false }, @@ -423,25 +421,11 @@ private fun ChoiceOptionRow( } return@Column } - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - entries.forEachIndexed { index, entry -> - SegmentedButton( - selected = index == selectedIndex, - onClick = { onSelect(index) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = entries.size), - icon = {}, - label = { - Text( - text = entry, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelMedium, - ) - }, - ) - } - } + RadiantSegmented( + options = entries, + selectedIndex = selectedIndex, + onSelect = onSelect, + ) } } @@ -488,25 +472,11 @@ private fun ColorOptionRow( color = MaterialTheme.colorScheme.primary, ) } - SingleChoiceSegmentedButtonRow(modifier = Modifier.fillMaxWidth()) { - presets.forEachIndexed { index, label -> - SegmentedButton( - selected = index == selectedPreset, - onClick = { onColorChange(presetColors[index]) }, - shape = SegmentedButtonDefaults.itemShape(index = index, count = presets.size), - icon = {}, - label = { - Text( - text = label, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelMedium, - ) - }, - ) - } - } + RadiantSegmented( + options = presets, + selectedIndex = selectedPreset, + onSelect = { index -> onColorChange(presetColors[index]) }, + ) } if (showPicker) { @@ -542,53 +512,45 @@ private fun ColorPickerDialog( var hex by rememberSaveable(initialColor) { mutableStateOf(color.toRgbHex()) } val parsedHex = parseRgbHex(hex) - AlertDialog( + RadiantDialog( onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.patch_color_picker_title)) }, - text = { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - ColorSwatch(color = parsedHex ?: color, modifier = Modifier.size(40.dp)) - OutlinedTextField( - value = hex, - onValueChange = { value -> - hex = value - parseRgbHex(value)?.let { color = it } - }, - label = { Text(stringResource(R.string.patch_color_picker_hex)) }, - singleLine = true, - isError = parsedHex == null, - keyboardOptions = KeyboardOptions( - capitalization = KeyboardCapitalization.Characters, - keyboardType = KeyboardType.Ascii, - ), - supportingText = if (parsedHex == null) { - { Text(stringResource(R.string.patch_color_picker_hex_error)) } - } else null, - modifier = Modifier.weight(1f), - ) - } - } - }, - confirmButton = { - TextButton( - enabled = parsedHex != null, - onClick = { parsedHex?.let(onColorSelected) }, + title = stringResource(R.string.patch_color_picker_title), + confirmText = stringResource(R.string.action_done), + onConfirm = { parsedHex?.let(onColorSelected) }, + confirmEnabled = parsedHex != null, + dismissText = stringResource(R.string.action_cancel), + onDismissAction = onDismiss, + ) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), ) { - Text(stringResource(R.string.action_done)) + ColorSwatch(color = parsedHex ?: color, modifier = Modifier.size(44.dp)) + RadiantTextField( + value = hex, + onValueChange = { value -> + hex = value + parseRgbHex(value)?.let { color = it } + }, + placeholder = stringResource(R.string.patch_color_picker_hex), + isError = parsedHex == null, + modifier = Modifier.weight(1f), + ) } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.action_cancel)) + + if (parsedHex == null) { + Text( + text = stringResource(R.string.patch_color_picker_hex_error), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error, + ) } - }, - ) + } + } } + @Composable private fun OptionText( title: String, @@ -641,7 +603,6 @@ private fun parseRgbHex(value: String): Int? { return (0xFF000000L or rgb).toInt() } -@OptIn(ExperimentalMaterial3Api::class) @Composable private fun OptionLockDialog(lock: OptionLock, onDismiss: () -> Unit) { val message = when (lock) { @@ -650,33 +611,12 @@ private fun OptionLockDialog(lock: OptionLock, onDismiss: () -> Unit) { OptionLock.Free -> return } - BasicAlertDialog(onDismissRequest = onDismiss) { - Surface( - shape = MaterialTheme.shapes.large, - color = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 6.dp, - ) { - Column( - modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - Text( - text = stringResource(R.string.patch_opt_lock_title), - style = MaterialTheme.typography.titleLarge, - ) - Text( - text = AnnotatedString.fromHtml(message), - style = MaterialTheme.typography.titleMedium, - ) - Box(modifier = Modifier.fillMaxWidth()) { - TextButton( - onClick = onDismiss, - modifier = Modifier.align(Alignment.CenterEnd), - ) { - Text(stringResource(R.string.action_got_it)) - } - } - } - } + RadiantDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.patch_opt_lock_title), + confirmText = stringResource(R.string.action_got_it), + onConfirm = onDismiss, + ) { + Text(AnnotatedString.fromHtml(message)) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchOptionsAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchOptionsAppBar.kt index 229d117..60ea851 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchOptionsAppBar.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchOptionsAppBar.kt @@ -6,6 +6,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import cafe.adriel.voyager.navigator.LocalNavigator import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton import com.meowarex.rlmobile.ui.components.BackButton import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen @@ -19,12 +20,12 @@ fun PatchOptionsAppBar( actions = { val navigator = LocalNavigator.current - IconButton(onClick = { navigator?.push(SettingsScreen()) }) { - Icon( - painter = painterResource(R.drawable.ic_settings), - contentDescription = stringResource(R.string.navigation_settings) - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings), + subtle = true, + onClick = { navigator?.push(SettingsScreen()) }, + ) } ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt index 92d9132..c745918 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchSelectionAccordion.kt @@ -26,6 +26,12 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.fromHtml import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantSwitch +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog +import com.meowarex.rlmobile.ui.theme.radiantSurface +import com.meowarex.rlmobile.ui.theme.glassSurface import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec import com.meowarex.rlmobile.ui.screens.patchopts.PatchLock import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState @@ -64,8 +70,8 @@ fun PatchSelectionAccordion( Column( modifier = modifier .fillMaxWidth() - .clip(MaterialTheme.shapes.large) - .background(MaterialTheme.colorScheme.surfaceContainerLow), + .radiantSurface(shape = MaterialTheme.shapes.large) + .clip(MaterialTheme.shapes.large), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -113,7 +119,7 @@ fun PatchSelectionAccordion( verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier .fillMaxWidth() - .background(MaterialTheme.colorScheme.background.copy(0.4f)) + .background(MaterialTheme.colorScheme.surfaceContainerLowest) .padding(horizontal = 16.dp, vertical = 12.dp), ) { Text( @@ -140,13 +146,8 @@ fun PatchSelectionAccordion( modifier = if (carded) { Modifier .fillMaxWidth() + .glassSurface(MaterialTheme.shapes.medium) .clip(MaterialTheme.shapes.medium) - .background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f)) - .border( - width = 1.5.dp, - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f), - shape = MaterialTheme.shapes.medium, - ) .padding(horizontal = 12.dp, vertical = 4.dp) } else { Modifier.fillMaxWidth() @@ -245,27 +246,13 @@ private fun AdvancedOptionsButton( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - FilledTonalButton( + RadiantButton( + text = stringResource(R.string.patchopts_advanced_button) + if (modified) " •" else "", + icon = painterResource(R.drawable.ic_tune), onClick = onClick, + size = RadiantButtonSize.Small, modifier = modifier, - ) { - Icon( - painter = painterResource(R.drawable.ic_tune), - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Spacer(Modifier.width(8.dp)) - Text(stringResource(R.string.patchopts_advanced_button)) - if (modified) { - Spacer(Modifier.width(8.dp)) - Box( - modifier = Modifier - .size(8.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primary), - ) - } - } + ) } @Composable @@ -300,7 +287,7 @@ private fun InlineToggleRow( modifier = Modifier.alpha(.7f), ) } - Switch( + RadiantSwitch( checked = checked, onCheckedChange = onCheckedChange, interactionSource = interactionSource, @@ -351,7 +338,7 @@ private fun PatchSwitchRow( } Box { - Switch( + RadiantSwitch( checked = checked, enabled = !isLocked, onCheckedChange = onCheckedChange, @@ -398,33 +385,12 @@ private fun PatchLockDialog( PatchLock.Free -> return } - BasicAlertDialog(onDismissRequest = onDismiss) { - Surface( - shape = MaterialTheme.shapes.large, - color = MaterialTheme.colorScheme.surfaceContainerHigh, - tonalElevation = 6.dp, - ) { - Column( - modifier = Modifier.padding(start = 24.dp, end = 12.dp, top = 20.dp, bottom = 8.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - Text( - text = stringResource(titleRes), - style = MaterialTheme.typography.titleLarge, - ) - Text( - text = AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle)), - style = MaterialTheme.typography.bodyLarge, - ) - Box(modifier = Modifier.fillMaxWidth()) { - TextButton( - onClick = onDismiss, - modifier = Modifier.align(Alignment.CenterEnd), - ) { - Text(stringResource(R.string.action_got_it)) - } - } - } - } + RadiantDialog( + onDismissRequest = onDismiss, + title = stringResource(titleRes), + confirmText = stringResource(R.string.action_got_it), + onConfirm = onDismiss, + ) { + Text(AnnotatedString.fromHtml(stringResource(msgRes, blockerTitle))) } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt index 735203e..4968410 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/PatchVariantSelector.kt @@ -1,14 +1,10 @@ package com.meowarex.rlmobile.ui.screens.patchopts.components -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow +import com.meowarex.rlmobile.ui.components.radiant.RadiantSegmented import com.meowarex.rlmobile.ui.screens.patchopts.EffectiveVariant -@OptIn(ExperimentalMaterial3Api::class) @Composable fun PatchVariantSelector( variants: List, @@ -16,29 +12,14 @@ fun PatchVariantSelector( onSelect: (Int) -> Unit, modifier: Modifier = Modifier, ) { - SingleChoiceSegmentedButtonRow( - modifier = modifier.fillMaxWidth(), - ) { - variants.forEachIndexed { index, variant -> - SegmentedButton( - selected = index == selectedIndex, - enabled = variant.enabled, - onClick = { if (variant.enabled) onSelect(index) }, - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = variants.size, - ), - icon = {}, - label = { - Text( - text = variant.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.labelMedium, - ) - }, - ) - } - } + // Variants are mutually exclusive, so this reads as one recessed track with a floating accent + // pill rather than Material's row of connected outlined buttons. + RadiantSegmented( + options = variants.map { it.title }, + selectedIndex = selectedIndex, + onSelect = { index -> + if (variants.getOrNull(index)?.enabled == true) onSelect(index) + }, + modifier = modifier, + ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/IconPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/IconPatchOption.kt index 80ee3e2..079bf70 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/IconPatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/IconPatchOption.kt @@ -7,7 +7,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp +import androidx.compose.foundation.shape.RoundedCornerShape +import com.meowarex.rlmobile.ui.theme.glassSurface @Composable fun IconPatchOption( @@ -22,11 +25,20 @@ fun IconPatchOption( verticalAlignment = Alignment.CenterVertically, modifier = modifier, ) { - Icon( - painter = icon, - contentDescription = null, - modifier = Modifier.size(26.dp), - ) + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(40.dp) + .glassSurface(RoundedCornerShape(13.dp)) + .clip(RoundedCornerShape(13.dp)), + ) { + Icon( + painter = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + } Column( verticalArrangement = Arrangement.spacedBy(2.dp), diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/SwitchPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/SwitchPatchOption.kt index f6a8e8b..db685f1 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/SwitchPatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/SwitchPatchOption.kt @@ -4,13 +4,13 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Switch import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.ui.components.radiant.RadiantSwitch @Composable fun SwitchPatchOption( @@ -39,7 +39,7 @@ fun SwitchPatchOption( role = Role.Switch, ), ) { - Switch( + RadiantSwitch( checked = value, enabled = enabled, onCheckedChange = onValueChange, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt index 5b59819..afa6790 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/patchopts/components/options/TextPatchOption.kt @@ -11,6 +11,8 @@ import androidx.compose.ui.unit.dp import com.meowarex.rlmobile.R import com.meowarex.rlmobile.ui.components.Label import com.meowarex.rlmobile.ui.components.ResetToDefaultButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantTextField @Composable fun TextPatchOption( @@ -31,19 +33,13 @@ fun TextPatchOption( description = description, modifier = modifier, ) { - OutlinedTextField( + RadiantTextField( value = value, onValueChange = onValueChange, isError = valueIsError, - singleLine = true, enabled = locked != true, - colors = OutlinedTextFieldDefaults.colors( - unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, - focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, - errorContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, - disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow, - ), - trailingIcon = { + modifier = Modifier.padding(vertical = 4.dp), + trailing = { Row(verticalAlignment = Alignment.CenterVertically) { if (locked != true) { ResetToDefaultButton( @@ -52,29 +48,22 @@ fun TextPatchOption( ) } if (locked != null) { - IconButton( + RadiantIconButton( + icon = painterResource( + if (locked) R.drawable.ic_lock else R.drawable.ic_lock_open, + ), + contentDescription = stringResource( + if (locked) R.string.patchopts_field_unlock else R.string.patchopts_field_lock, + ), + tint = if (locked) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.onSurfaceVariant, + subtle = true, + size = 34.dp, onClick = onLockClick, - modifier = Modifier.size(32.dp), - ) { - Icon( - painter = painterResource( - if (locked) R.drawable.ic_lock else R.drawable.ic_lock_open, - ), - contentDescription = stringResource( - if (locked) R.string.patchopts_field_unlock else R.string.patchopts_field_lock, - ), - tint = if (locked) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp), - ) - } - Spacer(Modifier.width(8.dp)) + ) } } }, - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 4.dp), ) extra?.invoke(this) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/PermissionsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/PermissionsScreen.kt index deeb8c0..8453bec 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/PermissionsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/PermissionsScreen.kt @@ -21,6 +21,9 @@ import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow import cafe.adriel.voyager.transitions.ScreenTransition import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle import com.meowarex.rlmobile.manager.InstallerSetting import com.meowarex.rlmobile.ui.components.TextDivider import com.meowarex.rlmobile.ui.components.settings.SettingsItem @@ -122,18 +125,15 @@ fun PermissionsScreenContent( text = { Text(stringResource(R.string.setting_installer)) }, secondaryText = { Text(stringResource(R.string.setting_installer_desc)) }, icon = { Icon(painterResource(R.drawable.ic_apk_install), null) }, - modifier = Modifier.clickable(onClick = openInstallersDialog), + onClick = openInstallersDialog, + modifier = Modifier.padding(horizontal = 16.dp), ) { - FilledTonalButton(onClick = openInstallersDialog) { - Icon( - painter = installer.icon(), - contentDescription = null, - modifier = Modifier - .size(20.dp) - .padding(end = 6.dp), - ) - Text(installer.title()) - } + RadiantButton( + text = installer.title(), + icon = installer.icon(), + onClick = openInstallersDialog, + size = RadiantButtonSize.Small, + ) } } @@ -209,12 +209,12 @@ fun PermissionsScreenContent( .fillMaxWidth() .padding(top = 16.dp, end = 32.dp), ) { - FilledTonalButton( + RadiantButton( + text = stringResource(R.string.action_continue), onClick = onContinue, enabled = canContinue, - ) { - Text(stringResource(R.string.action_continue)) - } + style = RadiantButtonStyle.Accent, + ) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionButton.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionButton.kt index b4c5d41..d66359d 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionButton.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionButton.kt @@ -1,16 +1,30 @@ package com.meowarex.rlmobile.ui.screens.permissions.components +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.painter.Painter +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle +import com.meowarex.rlmobile.ui.theme.radiantSurface +import com.meowarex.rlmobile.ui.theme.customColors +/** + * permission tiles + */ @Composable fun PermissionButton( name: String, @@ -21,57 +35,89 @@ fun PermissionButton( onClick: () -> Unit, modifier: Modifier = Modifier, ) { - Column( - verticalArrangement = Arrangement.spacedBy(10.dp), + val containerColor by animateColorAsState( + targetValue = if (granted) { + MaterialTheme.customColors.successContainer + } else { + MaterialTheme.colorScheme.surfaceContainerHigh + }, + label = "PermissionIconContainer", + ) + val contentColor by animateColorAsState( + targetValue = if (granted) { + MaterialTheme.customColors.onSuccessContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + label = "PermissionIconContent", + ) + + Surface( + color = Color.Transparent, + shape = MaterialTheme.shapes.large, modifier = modifier - .heightIn(min = 64.dp) .fillMaxWidth() - .padding(horizontal = 32.dp, vertical = 8.dp), + .padding(horizontal = 16.dp, vertical = 4.dp) + .radiantSurface(shape = MaterialTheme.shapes.large, active = granted), ) { Row( - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Companion.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(14.dp), ) { - Icon( - painter = icon, - contentDescription = null, - modifier = Modifier.size(20.dp), - ) - - ProvideTextStyle(MaterialTheme.typography.titleSmall) { - Text(name) - - if (required) { - Text( - text = "*", - color = MaterialTheme.colorScheme.error, - fontSize = 10.sp, - ) - } - } - } - - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.Companion.CenterVertically, - ) { - ProvideTextStyle( - MaterialTheme.typography.bodyMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(0.6f), - ), + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .size(38.dp) + .clip(RoundedCornerShape(12.dp)) + .background(containerColor), ) { - Text( - text = description, - modifier = Modifier.weight(1f), + Icon( + painter = if (granted) painterResource(R.drawable.ic_check_circle) else icon, + contentDescription = null, + tint = contentColor, + modifier = Modifier.size(19.dp), ) } - OutlinedButton( + Column( + verticalArrangement = Arrangement.spacedBy(3.dp), + modifier = Modifier.weight(1f), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = name, + style = MaterialTheme.typography.titleSmall, + ) + + if (required) { + Text( + text = "*", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + ) + } + } + + Text( + text = description, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + + RadiantButton( + text = stringResource( + if (granted) R.string.permissions_granted else R.string.permissions_grant + ), onClick = onClick, enabled = !granted, - ) { - Text(stringResource(if (granted) R.string.permissions_granted else R.string.permissions_grant)) - } + style = if (granted) RadiantButtonStyle.Ghost else RadiantButtonStyle.Accent, + size = RadiantButtonSize.Small, + ) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionsAppBar.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionsAppBar.kt index de21f6b..a1a524c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionsAppBar.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/permissions/components/PermissionsAppBar.kt @@ -9,37 +9,23 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cafe.adriel.voyager.navigator.LocalNavigator import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen @Composable fun PermissionsAppBar() { - LargeTopAppBar( - title = { - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.padding(start = 12.dp), - ) { - Text( - text = stringResource(R.string.permissions_title), - style = MaterialTheme.typography.displaySmall, - ) - Text( - text = stringResource(R.string.permissions_subtitle), - style = MaterialTheme.typography.bodyLarge.copy( - color = MaterialTheme.colorScheme.onSurface.copy(.6f), - ), - ) - } - }, + LargeFlexibleTopAppBar( + title = { Text(stringResource(R.string.permissions_title)) }, + subtitle = { Text(stringResource(R.string.permissions_subtitle)) }, actions = { val navigator = LocalNavigator.current - IconButton(onClick = { navigator?.push(SettingsScreen()) }) { - Icon( - painter = painterResource(R.drawable.ic_settings), - contentDescription = stringResource(R.string.navigation_settings) - ) - } + RadiantIconButton( + icon = painterResource(R.drawable.ic_settings), + contentDescription = stringResource(R.string.navigation_settings), + subtle = true, + onClick = { navigator?.push(SettingsScreen()) }, + ) } ) } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsModel.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsModel.kt index 8163da7..13c670f 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsModel.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsModel.kt @@ -13,6 +13,7 @@ import com.meowarex.rlmobile.R import com.meowarex.rlmobile.di.ActivityProvider import com.meowarex.rlmobile.manager.* import com.meowarex.rlmobile.ui.theme.Theme +import com.meowarex.rlmobile.ui.theme.UiStyle import com.meowarex.rlmobile.updatechecker.UpdateCheckWorker import com.meowarex.rlmobile.util.* @@ -30,6 +31,8 @@ class SettingsModel( private set var showInstallersDialog by mutableStateOf(false) private set + var showUiStyleDialog by mutableStateOf(false) + private set fun showThemeDialog() { showThemeDialog = true @@ -47,10 +50,22 @@ class SettingsModel( showInstallersDialog = false } + fun showUiStyleDialog() { + showUiStyleDialog = true + } + + fun hideUiStyleDialog() { + showUiStyleDialog = false + } + fun setTheme(theme: Theme) { preferences.theme = theme } + fun setUiStyle(style: UiStyle) { + preferences.uiStyle = style + } + fun setInstaller(installer: InstallerSetting) { preferences.installer = installer } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsScreen.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsScreen.kt index d5ef690..fe51cc7 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsScreen.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/SettingsScreen.kt @@ -2,27 +2,31 @@ package com.meowarex.rlmobile.ui.screens.settings import android.os.Build import android.os.Parcelable -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.koin.koinScreenModel import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton import com.meowarex.rlmobile.ui.components.BackButton +import com.meowarex.rlmobile.ui.components.DangerActionButton import com.meowarex.rlmobile.ui.components.MainActionButton import com.meowarex.rlmobile.ui.components.settings.* import com.meowarex.rlmobile.ui.screens.settings.components.InstallersDialog import com.meowarex.rlmobile.ui.screens.settings.components.ThemeDialog -import com.meowarex.rlmobile.ui.util.paddings.* +import com.meowarex.rlmobile.ui.screens.settings.components.UiStyleDialog import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize @@ -45,6 +49,14 @@ class SettingsScreen : Screen, Parcelable { ) } + if (model.showUiStyleDialog) { + UiStyleDialog( + current = preferences.uiStyle, + onDismiss = model::hideUiStyleDialog, + onConfirm = model::setUiStyle, + ) + } + if (model.showInstallersDialog) { InstallersDialog( currentInstaller = preferences.installer, @@ -61,166 +73,152 @@ class SettingsScreen : Screen, Parcelable { ) }, ) { padding -> - LazyColumn( - contentPadding = padding - .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) - .add(PaddingValues(bottom = 12.dp)), + Column( modifier = Modifier .fillMaxSize() - .padding(padding.exclude(PaddingValuesSides.Bottom)) + .verticalScroll(rememberScrollState()) + .padding(padding), ) { - item(key = "HEADER_APPEARANCE", contentType = "DIVIDER") { - SettingsHeader(stringResource(R.string.settings_header_appearance)) - } + SettingsHeader(stringResource(R.string.settings_header_appearance)) + + SettingsGroup { + val dynamicColorAvailable = Build.VERSION.SDK_INT >= 31 + val appearanceCount = if (dynamicColorAvailable) 4 else 3 - item(key = "SETTING_THEME") { SettingsItem( - modifier = Modifier.clickable(onClick = model::showThemeDialog), + position = groupPosition(0, appearanceCount), + onClick = model::showThemeDialog, icon = { Icon(painterResource(R.drawable.ic_brush), null) }, text = { Text(stringResource(R.string.setting_theme)) }, secondaryText = { Text(stringResource(R.string.setting_theme_desc)) } ) { - FilledTonalButton(onClick = model::showThemeDialog) { - Text(preferences.theme.toDisplayName()) - } + RadiantButton( + text = preferences.theme.toDisplayName(), + onClick = model::showThemeDialog, + size = RadiantButtonSize.Small, + ) } - } - // Material You theming on Android 12+ - if (Build.VERSION.SDK_INT >= 31) { - item(key = "SETTING_DYNAMIC_COLOR", contentType = "SETTING_SWITCH") { + SettingsItem( + position = groupPosition(1, appearanceCount), + onClick = model::showUiStyleDialog, + icon = { Icon(painterResource(R.drawable.ic_sparkle), null) }, + text = { Text(stringResource(R.string.setting_ui_style)) }, + secondaryText = { Text(stringResource(R.string.setting_ui_style_desc)) }, + ) { + RadiantButton( + text = preferences.uiStyle.toDisplayName(), + onClick = model::showUiStyleDialog, + size = RadiantButtonSize.Small, + ) + } + + // Material You theming on Android 12+ + if (dynamicColorAvailable) { SettingsSwitch( label = stringResource(R.string.setting_dynamic_color), secondaryLabel = stringResource(R.string.setting_dynamic_color_desc), pref = preferences.dynamicColor, + position = groupPosition(2, appearanceCount), icon = { Icon(painterResource(R.drawable.ic_palette), null) }, onPrefChange = { preferences.dynamicColor = it }, ) } - } - item(key = "SETTING_AUTO_UPDATE_CHECK", contentType = "SETTING_SWITCH") { SettingsSwitch( label = stringResource(R.string.setting_auto_update_check), secondaryLabel = stringResource(R.string.setting_auto_update_check_desc), pref = preferences.autoUpdateCheck, + position = groupPosition(appearanceCount - 1, appearanceCount), icon = { Icon(painterResource(R.drawable.ic_update), null) }, onPrefChange = { model.setAutoUpdateCheck(it) }, ) } - item(key = "HEADER_INSTALL", contentType = "DIVIDER") { - SettingsHeader(stringResource(R.string.settings_header_installation)) - } + SettingsHeader(stringResource(R.string.settings_header_installation)) - item(key = "SETTING_INSTALLER") { + SettingsGroup { SettingsItem( + position = GroupPosition.Top, + onClick = model::showInstallersDialog, text = { Text(stringResource(R.string.setting_installer)) }, secondaryText = { Text(stringResource(R.string.setting_installer_desc)) }, icon = { Icon(painterResource(R.drawable.ic_apk_install), null) }, - modifier = Modifier.clickable(onClick = model::showInstallersDialog), ) { - FilledTonalButton(onClick = model::showInstallersDialog) { - Icon( - painter = preferences.installer.icon(), - contentDescription = null, - modifier = Modifier - .size(20.dp) - .padding(end = 6.dp), - ) - Text(preferences.installer.title()) - } + RadiantButton( + text = preferences.installer.title(), + icon = preferences.installer.icon(), + onClick = model::showInstallersDialog, + size = RadiantButtonSize.Small, + ) } - } - item(key = "SETTING_KEEP_APK", contentType = "SETTING_SWITCH") { SettingsSwitch( label = stringResource(R.string.setting_keep_patched_apks), secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc), icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) }, pref = preferences.keepPatchedApks, + position = GroupPosition.Bottom, onPrefChange = { model.setKeepPatchedApks(it) }, ) } - if (preferences.keepPatchedApks) { - item(key = "BUTTON_EXPORT_APK", contentType = "BUTTON") { + Column( + verticalArrangement = Arrangement.spacedBy(10.dp), + modifier = Modifier.padding(horizontal = 16.dp, vertical = 16.dp), + ) { + if (preferences.keepPatchedApks) { MainActionButton( text = stringResource(R.string.settings_export_apk), icon = painterResource(R.drawable.ic_save), enabled = model.patchedApkExists, onClick = model::shareApk, - modifier = Modifier - .padding(start = 32.dp, end = 32.dp, top = 16.dp) - .fillMaxWidth() - .animateItem(), ) } - } - item(key = "BUTTON_CACHE_CLEAR", contentType = "BUTTON") { - MainActionButton( + DangerActionButton( text = stringResource(R.string.settings_clear_cache), icon = painterResource(R.drawable.ic_delete_forever), enabled = !clearedCache, - colors = IconButtonDefaults.filledTonalIconButtonColors( - containerColor = MaterialTheme.colorScheme.error, - ), onClick = { clearedCache = true model.clearCache() }, - modifier = Modifier - .padding(start = 32.dp, end = 32.dp, top = 18.dp) - .fillMaxWidth() ) } - item(key = "HEADER_ADVANCED", contentType = "DIVIDER") { - SettingsHeader(stringResource(R.string.settings_header_advanced)) - } + SettingsHeader(stringResource(R.string.settings_header_advanced)) - item(key = "SETTING_DEVMODE", contentType = "SETTING_SWITCH") { + SettingsGroup { SettingsSwitch( label = stringResource(R.string.setting_developer_options), secondaryLabel = stringResource(R.string.setting_developer_options_desc), pref = preferences.devMode, + position = GroupPosition.Single, icon = { Icon(painterResource(R.drawable.ic_code), null) }, onPrefChange = { preferences.devMode = it }, ) } - item(key = "HEADER_INFO", contentType = "DIVIDER") { - SettingsHeader(stringResource(R.string.settings_header_info)) - } + SettingsHeader(stringResource(R.string.settings_header_info)) - item(key = "INFO") { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .padding(horizontal = 36.dp, vertical = 12.dp), + SettingsGroup { + SettingsItem( + position = GroupPosition.Single, + onClick = model::copyInstallInfo, + icon = { Icon(painterResource(R.drawable.ic_info), null) }, + text = { Text(stringResource(R.string.settings_header_info)) }, + secondaryText = { Text(model.installInfo) }, ) { - Text( - text = model.installInfo, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onSurface.copy(0.6f), - ), + RadiantIconButton( + icon = painterResource(R.drawable.ic_copy), + contentDescription = stringResource(R.string.action_copy), + onClick = model::copyInstallInfo, ) - - Spacer(Modifier.weight(1f, fill = true)) - - IconButton(onClick = model::copyInstallInfo) { - Icon( - painter = painterResource(R.drawable.ic_copy), - contentDescription = stringResource(R.string.action_copy), - modifier = Modifier - .size(28.dp) - .alpha(.8f), - ) - } } } + + Spacer(Modifier.height(28.dp)) } } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/InstallersDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/InstallersDialog.kt index ebb97e7..a0a14c1 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/InstallersDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/InstallersDialog.kt @@ -8,6 +8,7 @@ import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable 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.platform.LocalContext import androidx.compose.ui.res.painterResource @@ -17,6 +18,10 @@ import androidx.compose.ui.unit.sp import com.meowarex.rlmobile.R import com.meowarex.rlmobile.manager.* import com.meowarex.rlmobile.util.showToast +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog +import com.meowarex.rlmobile.ui.components.radiant.RadiantRadio +import com.meowarex.rlmobile.ui.theme.radiantSurface +import androidx.compose.foundation.shape.RoundedCornerShape import com.topjohnwu.superuser.Shell import org.koin.compose.koinInject @@ -79,45 +84,43 @@ fun InstallersDialog( } } - AlertDialog( + RadiantDialog( onDismissRequest = onDismiss, + title = stringResource(R.string.setting_installer), icon = { Icon( painter = painterResource(R.drawable.ic_apk_install), contentDescription = null, - modifier = Modifier.size(32.dp), + modifier = Modifier.size(26.dp), ) }, - title = { Text(stringResource(R.string.setting_installer)) }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - for (installer in InstallerSetting.entries) key(installer) { - InstallerItem( - installer = installer, - selected = installer == selectedInstaller, - enabled = when (installer) { - InstallerSetting.PackageInstaller -> true - InstallerSetting.Root -> true - InstallerSetting.Intent -> true - InstallerSetting.Shizuku -> shizukuAvailable - InstallerSetting.Dhizuku -> dhizukuAvailable - }, - onClick = { selectedInstaller = installer }, - ) - } - } + confirmText = stringResource(R.string.action_apply), + onConfirm = { + onConfirm(selectedInstaller) + onDismiss() }, - confirmButton = { - Button( - onClick = { - onConfirm(selectedInstaller) - onDismiss() - }, - ) { - Text(stringResource(R.string.action_apply)) + dismissText = stringResource(R.string.action_cancel), + ) { + Column( + verticalArrangement = Arrangement.spacedBy(6.dp), + modifier = Modifier.verticalScroll(rememberScrollState()), + ) { + for (installer in InstallerSetting.entries) key(installer) { + InstallerItem( + installer = installer, + selected = installer == selectedInstaller, + enabled = when (installer) { + InstallerSetting.PackageInstaller -> true + InstallerSetting.Root -> true + InstallerSetting.Intent -> true + InstallerSetting.Shizuku -> shizukuAvailable + InstallerSetting.Dhizuku -> dhizukuAvailable + }, + onClick = { selectedInstaller = installer }, + ) } - }, - ) + } + } } @Composable @@ -128,49 +131,56 @@ private fun InstallerItem( onClick: () -> Unit, ) { val interactionSource = remember(::MutableInteractionSource) + val shape = RoundedCornerShape(16.dp) Row( - verticalAlignment = Alignment.Companion.CenterVertically, - modifier = Modifier.Companion + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .fillMaxWidth() + .radiantSurface(shape = shape, active = selected) + .clip(shape) .clickable( indication = null, interactionSource = interactionSource, + enabled = enabled, onClick = onClick, ) - .clip(MaterialTheme.shapes.medium) - .padding(horizontal = 6.dp, vertical = 8.dp), + .alpha(if (enabled) 1f else 0.45f) + .padding(horizontal = 14.dp, vertical = 12.dp), ) { Column( - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), modifier = Modifier.weight(1f), ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.Companion.CenterVertically, + verticalAlignment = Alignment.CenterVertically, ) { Icon( painter = installer.icon(), contentDescription = null, - modifier = Modifier - .size(20.dp) + tint = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(20.dp), ) Text( text = installer.title(), - style = MaterialTheme.typography.labelLarge - .copy(fontSize = 14.sp) + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, ) } Text( text = installer.description(), - style = MaterialTheme.typography.labelMedium.copy( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = .6f), - ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) } - Spacer(Modifier.Companion.weight(0.05f, true)) - - RadioButton( + RadiantRadio( selected = selected, enabled = enabled, onClick = onClick, diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/ThemeDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/ThemeDialog.kt index b5fcf7a..e9dc22c 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/ThemeDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/ThemeDialog.kt @@ -3,6 +3,7 @@ package com.meowarex.rlmobile.ui.screens.settings.components import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -12,9 +13,11 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog +import com.meowarex.rlmobile.ui.components.radiant.RadiantRadio import com.meowarex.rlmobile.ui.theme.Theme +import com.meowarex.rlmobile.ui.theme.radiantSurface @Composable fun ThemeDialog( @@ -24,66 +27,69 @@ fun ThemeDialog( ) { var selectedTheme by rememberSaveable { mutableStateOf(currentTheme) } - AlertDialog( + RadiantDialog( onDismissRequest = onDismiss, + title = stringResource(R.string.setting_theme), icon = { Icon( painter = painterResource(R.drawable.ic_brush), contentDescription = null, - modifier = Modifier.size(32.dp), + modifier = Modifier.size(26.dp), ) }, - title = { Text(stringResource(R.string.setting_theme)) }, - text = { - Column { - for (theme in Theme.entries) key(theme) { - val interactionSource = remember(::MutableInteractionSource) + confirmText = stringResource(R.string.action_apply), + onConfirm = { + onConfirm(selectedTheme) + onDismiss() + }, + dismissText = stringResource(R.string.action_cancel), + ) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + for (theme in Theme.entries) key(theme) { + val interactionSource = remember(::MutableInteractionSource) + val selected = theme == selectedTheme + val shape = RoundedCornerShape(16.dp) - Row( - verticalAlignment = Alignment.Companion.CenterVertically, - modifier = Modifier.Companion - .clickable( - indication = null, - interactionSource = interactionSource, - onClick = { selectedTheme = theme }, - ) - .clip(MaterialTheme.shapes.medium) - .padding(horizontal = 6.dp, vertical = 8.dp), - ) { - Icon( - painter = theme.toPainter(), - contentDescription = null, - modifier = Modifier - .padding(end = 14.dp) - .size(26.dp), - ) - - Text( - text = theme.toDisplayName(), - style = MaterialTheme.typography.labelLarge - .copy(fontSize = 14.sp) - ) - - Spacer(Modifier.Companion.weight(1f, true)) - - RadioButton( - selected = theme == selectedTheme, - onClick = { selectedTheme = theme }, + // The whole row is the target and lights up when chosen (accessibility.. yayy) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier + .fillMaxWidth() + .radiantSurface(shape = shape, active = selected) + .clip(shape) + .clickable( + indication = null, interactionSource = interactionSource, + onClick = { selectedTheme = theme }, ) - } + .padding(horizontal = 14.dp, vertical = 12.dp), + ) { + Icon( + painter = theme.toPainter(), + contentDescription = null, + tint = if (selected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(24.dp), + ) + + Text( + text = theme.toDisplayName(), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + + RadiantRadio( + selected = selected, + onClick = { selectedTheme = theme }, + interactionSource = interactionSource, + ) } } - }, - confirmButton = { - Button( - onClick = { - onConfirm(selectedTheme) - onDismiss() - }, - ) { - Text(stringResource(R.string.action_apply)) - } - }, - ) + } + } } diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/UiStyleDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/UiStyleDialog.kt new file mode 100644 index 0000000..da521f5 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/screens/settings/components/UiStyleDialog.kt @@ -0,0 +1,94 @@ +package com.meowarex.rlmobile.ui.screens.settings.components + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog +import com.meowarex.rlmobile.ui.components.radiant.RadiantRadio +import com.meowarex.rlmobile.ui.theme.UiStyle +import com.meowarex.rlmobile.ui.theme.radiantSurface + +@Composable +fun UiStyleDialog( + current: UiStyle, + onDismiss: () -> Unit, + onConfirm: (UiStyle) -> Unit, +) { + var selected by rememberSaveable { mutableStateOf(current) } + + RadiantDialog( + onDismissRequest = onDismiss, + title = stringResource(R.string.setting_ui_style), + icon = { + Icon( + painter = painterResource(R.drawable.ic_sparkle), + contentDescription = null, + modifier = Modifier.size(26.dp), + ) + }, + confirmText = stringResource(R.string.action_apply), + onConfirm = { + onConfirm(selected) + onDismiss() + }, + dismissText = stringResource(R.string.action_cancel), + ) { + Column(verticalArrangement = Arrangement.spacedBy(6.dp)) { + for (style in UiStyle.entries) key(style) { + val interactionSource = remember(::MutableInteractionSource) + val isSelected = style == selected + val shape = RoundedCornerShape(16.dp) + + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(14.dp), + modifier = Modifier + .fillMaxWidth() + .radiantSurface(shape = shape, active = isSelected) + .clip(shape) + .clickable( + indication = null, + interactionSource = interactionSource, + onClick = { selected = style }, + ) + .padding(horizontal = 14.dp, vertical = 12.dp), + ) { + Icon( + painter = style.toPainter(), + contentDescription = null, + tint = if (isSelected) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(24.dp), + ) + + Text( + text = style.toDisplayName(), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f), + ) + + RadiantRadio( + selected = isSelected, + onClick = { selected = style }, + interactionSource = interactionSource, + ) + } + } + } + } +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/LegacyTypography.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/LegacyTypography.kt new file mode 100644 index 0000000..a2e7fa6 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/LegacyTypography.kt @@ -0,0 +1,54 @@ +package com.meowarex.rlmobile.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.font.* +import com.meowarex.rlmobile.R + +// This uses a variable font variant of Roboto. (a font of some unknown description) +// ref for later: https://medium.com/androiddevelopers/just-your-type-variable-fonts-in-compose-5bf63b357994 + +@OptIn(ExperimentalTextApi::class) +val LegacyTypography = run { + val baseline = Typography() + + val weights = arrayOf( + FontWeight.Thin, + FontWeight.ExtraLight, + FontWeight.Light, + FontWeight.Normal, + FontWeight.Medium, + FontWeight.SemiBold, + FontWeight.ExtraBold, + FontWeight.Bold, + FontWeight.Black, + ) + val fonts = weights.map { weight -> + Font( + resId = R.font.roboto_variable, + weight = weight, + variationSettings = FontVariation.Settings( + FontVariation.weight(weight.weight), + ) + ) + } + val fontFamily = FontFamily(fonts) + + Typography( + displayLarge = baseline.displayLarge.copy(fontFamily = fontFamily), + displayMedium = baseline.displayMedium.copy(fontFamily = fontFamily), + displaySmall = baseline.displaySmall.copy(fontFamily = fontFamily), + headlineLarge = baseline.headlineLarge.copy(fontFamily = fontFamily), + headlineMedium = baseline.headlineMedium.copy(fontFamily = fontFamily), + headlineSmall = baseline.headlineSmall.copy(fontFamily = fontFamily), + titleLarge = baseline.titleLarge.copy(fontFamily = fontFamily), + titleMedium = baseline.titleMedium.copy(fontFamily = fontFamily), + titleSmall = baseline.titleSmall.copy(fontFamily = fontFamily), + bodyLarge = baseline.bodyLarge.copy(fontFamily = fontFamily), + bodyMedium = baseline.bodyMedium.copy(fontFamily = fontFamily), + bodySmall = baseline.bodySmall.copy(fontFamily = fontFamily), + labelLarge = baseline.labelLarge.copy(fontFamily = fontFamily), + labelMedium = baseline.labelMedium.copy(fontFamily = fontFamily), + labelSmall = baseline.labelSmall.copy(fontFamily = fontFamily), + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/RadiantDesign.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/RadiantDesign.kt new file mode 100644 index 0000000..a603313 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/RadiantDesign.kt @@ -0,0 +1,161 @@ +package com.meowarex.rlmobile.ui.theme + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ReadOnlyComposable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +/** + * The primitives the Radiant control set is built from + */ +object RadiantDesign { + /** Hairline border width used on every raised surface. */ + val Hairline: Dp = 1.dp + + /** How far a container's top edge is lightened to fake a light source from above. */ + const val TOP_LIT_ALPHA = 0.06f + + /** Border opacity for inert containers vs. ones that are selected/active. */ + const val BORDER_ALPHA = 0.10f + const val BORDER_ALPHA_ACTIVE = 0.55f + + /** Fill opacity for "glass" (translucent tinted) controls. */ + const val GLASS_ALPHA = 0.10f + const val GLASS_ALPHA_PRESSED = 0.18f +} + +/** + * The accent - flat brand pink. (unless using M-YOU) + */ +val accentColor: Color + @Composable @ReadOnlyComposable + get() = MaterialTheme.colorScheme.primary + +/** [accentColor] as a Brush, for the `background(brush, shape)` call sites. */ +val accentBrush: Brush + @Composable @ReadOnlyComposable + get() = SolidColor(accentColor) + +/** Muted variant of [accentBrush] for disabled primary controls. */ +val accentBrushDisabled: Brush + @Composable @ReadOnlyComposable + get() = SolidColor(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.12f)) + +/** + * Vertical gradient for a raised container: a lit top edge falling to the flat container colour. + */ +@Composable +@ReadOnlyComposable +fun surfaceBrush(base: Color = MaterialTheme.colorScheme.surfaceContainerLow): Brush { + val lit = Color.White.copy(alpha = RadiantDesign.TOP_LIT_ALPHA).compositeOver(base) + return Brush.verticalGradient(listOf(lit, base)) +} + +/** Hairline border brush — brighter at the top, matching the surface's implied light source. */ +@Composable +@ReadOnlyComposable +fun hairlineBrush( + color: Color = MaterialTheme.colorScheme.onSurface, + alpha: Float = RadiantDesign.BORDER_ALPHA, +): Brush = Brush.verticalGradient( + listOf( + color.copy(alpha = alpha * 1.8f), + color.copy(alpha = alpha * 0.6f), + ) +) + +/** + * The house container treatment: gradient fill + hairline border, clipped to [shape]. + */ +@Composable +fun Modifier.radiantSurface( + shape: Shape, + base: Color = MaterialTheme.colorScheme.surfaceContainerLow, + active: Boolean = false, + borderColor: Color = if (active) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurface + }, +): Modifier { + val style = radiantStyle + return this + .background( + // Legacy drops the falloff and paints the flat container colour Material would use. (thx claude) + brush = if (style.surfaceGradient) surfaceBrush(base) else SolidColor(base), + shape = shape, + ) + .then( + if (style.hairline) { + Modifier.border( + width = RadiantDesign.Hairline, + brush = hairlineBrush( + color = borderColor, + alpha = if (active) { + RadiantDesign.BORDER_ALPHA_ACTIVE + } else { + RadiantDesign.BORDER_ALPHA + }, + ), + shape = shape, + ) + } else Modifier + ) +} + +/** + * Coloured outer glow + */ +@Composable +fun Modifier.glow( + color: Color = MaterialTheme.colorScheme.primary, + radius: Dp = 16.dp, + shape: Shape, + alpha: Float = 0.55f, +): Modifier { + if (!radiantStyle.glow) return this + return this.shadow( + elevation = radius, + shape = shape, + clip = false, + ambientColor = color.copy(alpha = alpha), + spotColor = color.copy(alpha = alpha), + ) +} + +/** + * Translucent tinted fill used by secondary "glass" controls + */ +@Composable +fun Modifier.glassSurface( + shape: Shape, + tint: Color = MaterialTheme.colorScheme.primary, + fillAlpha: Float = RadiantDesign.GLASS_ALPHA, + borderAlpha: Float = RadiantDesign.BORDER_ALPHA_ACTIVE, +): Modifier { + val style = radiantStyle + // In Legacy this becomes Material's secondaryContainer fill with no edge. + if (!style.hairline) { + return this.background( + color = MaterialTheme.colorScheme.secondaryContainer, + shape = shape, + ) + } + return this + .background(color = tint.copy(alpha = fillAlpha), shape = shape) + .border( + width = RadiantDesign.Hairline, + brush = hairlineBrush(color = tint, alpha = borderAlpha), + shape = shape, + ) +} diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/Theme.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/Theme.kt index c246eb6..c8dd9a4 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/Theme.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/Theme.kt @@ -11,14 +11,17 @@ import androidx.compose.ui.res.stringResource import com.meowarex.rlmobile.R import com.google.accompanist.systemuicontroller.rememberSystemUiController +@OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun ManagerTheme( theme: Theme = Theme.System, - dynamicColor: Boolean = true, + uiStyle: UiStyle = UiStyle.Radiant, + dynamicColor: Boolean = false, content: @Composable () -> Unit, ) { val context = LocalContext.current - val dynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + val useDynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S + val legacy = uiStyle == UiStyle.Legacy val isBlack = theme == Theme.Black val isDark = when (theme) { @@ -27,15 +30,21 @@ fun ManagerTheme( Theme.Dark, Theme.Black -> true } + // Dynamic colour is opt-in, not the default. Radiant ships its own palette so the app looks + // like itself on every device instead of inheriting the wallpaper's tonal scheme. + // Legacy falls back to Material's own baseline schemes, which is what the app used before. (this is just a note to self cause subject to change) val baseScheme = when { - dynamicColor && isDark -> dynamicDarkColorScheme(context) - dynamicColor -> dynamicLightColorScheme(context) - isDark -> darkColorScheme() - else -> lightColorScheme() + useDynamicColor && isDark -> dynamicDarkColorScheme(context) + useDynamicColor -> dynamicLightColorScheme(context) + legacy && isDark -> darkColorScheme() + legacy -> lightColorScheme() + isDark -> RadiantDarkColorScheme + else -> RadiantLightColorScheme } - val colorScheme = when (isBlack) { - true -> baseScheme.toPitchBlack() - false -> baseScheme + val colorScheme = when { + !isBlack -> baseScheme + legacy -> baseScheme.toLegacyPitchBlack() + else -> baseScheme.toPitchBlack() } val customColors = when (isDark) { true -> DarkCustomColors @@ -49,7 +58,7 @@ fun ManagerTheme( SideEffect { systemUiController.setSystemBarsColor( - color = colorScheme.background, + color = if (legacy) colorScheme.background else Color.Transparent, darkIcons = !isDark, ) systemUiController.setNavigationBarColor( @@ -57,12 +66,28 @@ fun ManagerTheme( ) } - CompositionLocalProvider(LocalCustomColors provides customColors) { - MaterialTheme( - colorScheme = colorScheme, - typography = ThemeTypography, - content = content, - ) + CompositionLocalProvider( + LocalCustomColors provides customColors, + LocalRadiantStyle provides if (legacy) RadiantStyle.Legacy else RadiantStyle.Radiant, + ) { + if (legacy) { + // Plain MaterialTheme with stock shapes and the original type scale + MaterialTheme( + colorScheme = colorScheme, + shapes = Shapes(), + typography = LegacyTypography, + content = content, + ) + } else { + MaterialExpressiveTheme( + colorScheme = colorScheme, + // Makes things springy and bouncy instead of shit + motionScheme = MotionScheme.expressive(), + shapes = ThemeShapes, + typography = ThemeTypography, + content = content, + ) + } } } @@ -93,12 +118,11 @@ enum class Theme { ) } -private fun ColorScheme.toPitchBlack(): ColorScheme { - return this.copy( - background = Color.Black, - surface = Color.Black, - surfaceVariant = Color.Black, - onBackground = Color.White, - onSurface = Color.White - ) -} +/** The original AMOLED treatment left alone cause [UiStyle.Legacy]. */ +private fun ColorScheme.toLegacyPitchBlack(): ColorScheme = copy( + background = Color.Black, + surface = Color.Black, + surfaceVariant = Color.Black, + onBackground = Color.White, + onSurface = Color.White, +) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeColors.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeColors.kt new file mode 100644 index 0000000..3fcadc8 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeColors.kt @@ -0,0 +1,188 @@ +package com.meowarex.rlmobile.ui.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.ui.graphics.Color + +/** (claude built this cause i'm literally colorblind) + * Radiant's own colour system. + * + * Deliberately *not* a Monet/Material-You tonal extraction: the ramps below are hand-tuned at a + * much higher chroma than a wallpaper-derived scheme ever produces, so the app keeps one identity + * on every device instead of turning into whatever pastel the launcher wallpaper happens to be. + * + * The hues come from the product itself — the launcher background (#B91D6F) is tone 40 of the + * magenta ramp, and the sparkle foreground (#FFE0EC) sits at tone 90. + * + * - primary magenta/rose — the brand + * - secondary violet — the cool half of the "radiant" spectrum + * - tertiary aqua — the pop accent, used sparingly for success/highlight states + * - neutrals rose-tinted ink, never flat grey + */ + +// ── Magenta / rose (primary) ───────────────────────────────────────────────── +private val Rose10 = Color(0xFF3F0022) +private val Rose20 = Color(0xFF650036) +private val Rose30 = Color(0xFF8D004C) +private val Rose40 = Color(0xFFB91D6F) // brand seed — matches ic_launcher_background +private val Rose50 = Color(0xFFDB3B88) +private val Rose60 = Color(0xFFF55A9F) +private val Rose70 = Color(0xFFFF83B6) +private val Rose80 = Color(0xFFFFAFCC) +private val Rose90 = Color(0xFFFFD9E4) +private val Rose95 = Color(0xFFFFECF1) + +// ── Violet (secondary) ─────────────────────────────────────────────────────── +private val Violet10 = Color(0xFF25004E) +private val Violet20 = Color(0xFF3D177A) +private val Violet30 = Color(0xFF553194) +private val Violet40 = Color(0xFF6E4BAF) +private val Violet80 = Color(0xFFD6BBFF) +private val Violet90 = Color(0xFFECDCFF) + +// ── Aqua (tertiary) ────────────────────────────────────────────────────────── +private val Aqua10 = Color(0xFF00201F) +private val Aqua20 = Color(0xFF003736) +private val Aqua30 = Color(0xFF00504E) +private val Aqua40 = Color(0xFF006A67) +private val Aqua80 = Color(0xFF4FDCD8) +private val Aqua90 = Color(0xFF70F9F4) + +// ── Neutrals — rose-tinted ink (chroma ~4, never a flat grey) ──────────────── +private val Neutral4 = Color(0xFF0F0B0D) +private val Neutral6 = Color(0xFF151012) +private val Neutral10 = Color(0xFF1D171A) +private val Neutral12 = Color(0xFF211B1E) +private val Neutral17 = Color(0xFF2C2529) +private val Neutral20 = Color(0xFF332C30) +private val Neutral22 = Color(0xFF373034) +private val Neutral24 = Color(0xFF3C3438) +private val Neutral80 = Color(0xFFCDC0C5) +private val Neutral87 = Color(0xFFE1D4D9) +private val Neutral90 = Color(0xFFEADCE1) +private val Neutral92 = Color(0xFFF0E2E7) +private val Neutral94 = Color(0xFFF5E8ED) +private val Neutral96 = Color(0xFFFBEEF3) +private val Neutral98 = Color(0xFFFFF7FA) +private val Neutral100 = Color(0xFFFFFFFF) + +// ── Neutral variant — a touch more chroma, for outlines & variant surfaces ─── +private val NeutralVariant30 = Color(0xFF514049) +private val NeutralVariant50 = Color(0xFF846E7A) +private val NeutralVariant60 = Color(0xFF9F8794) +private val NeutralVariant80 = Color(0xFFD8BFCC) +private val NeutralVariant90 = Color(0xFFF5DBE8) + +// ── Error ──────────────────────────────────────────────────────────────────── +private val Error10 = Color(0xFF410002) +private val Error20 = Color(0xFF690005) +private val Error30 = Color(0xFF93000A) +private val Error40 = Color(0xFFBA1A1A) +private val Error80 = Color(0xFFFFB4AB) +private val Error90 = Color(0xFFFFDAD6) + +val RadiantDarkColorScheme: ColorScheme = darkColorScheme( + primary = Rose80, + onPrimary = Rose20, + primaryContainer = Rose30, + onPrimaryContainer = Rose90, + inversePrimary = Rose40, + + secondary = Violet80, + onSecondary = Violet20, + secondaryContainer = Violet30, + onSecondaryContainer = Violet90, + + tertiary = Aqua80, + onTertiary = Aqua20, + tertiaryContainer = Aqua30, + onTertiaryContainer = Aqua90, + + error = Error80, + onError = Error20, + errorContainer = Error30, + onErrorContainer = Error90, + + background = Neutral6, + onBackground = Neutral90, + surface = Neutral6, + onSurface = Neutral90, + surfaceVariant = NeutralVariant30, + onSurfaceVariant = NeutralVariant80, + surfaceTint = Rose80, + inverseSurface = Neutral90, + inverseOnSurface = Neutral20, + + surfaceDim = Neutral6, + surfaceBright = Neutral24, + surfaceContainerLowest = Neutral4, + surfaceContainerLow = Neutral10, + surfaceContainer = Neutral12, + surfaceContainerHigh = Neutral17, + surfaceContainerHighest = Neutral22, + + outline = NeutralVariant60, + outlineVariant = NeutralVariant30, + scrim = Color.Black, +) + +val RadiantLightColorScheme: ColorScheme = lightColorScheme( + primary = Rose40, + onPrimary = Neutral100, + primaryContainer = Rose90, + onPrimaryContainer = Rose10, + inversePrimary = Rose80, + + secondary = Violet40, + onSecondary = Neutral100, + secondaryContainer = Violet90, + onSecondaryContainer = Violet10, + + tertiary = Aqua40, + onTertiary = Neutral100, + tertiaryContainer = Aqua90, + onTertiaryContainer = Aqua10, + + error = Error40, + onError = Neutral100, + errorContainer = Error90, + onErrorContainer = Error10, + + background = Neutral98, + onBackground = Neutral10, + surface = Neutral98, + onSurface = Neutral10, + surfaceVariant = NeutralVariant90, + onSurfaceVariant = NeutralVariant30, + surfaceTint = Rose40, + inverseSurface = Neutral20, + inverseOnSurface = Neutral96, + + surfaceDim = Neutral87, + surfaceBright = Neutral98, + surfaceContainerLowest = Neutral100, + surfaceContainerLow = Neutral96, + surfaceContainer = Neutral94, + surfaceContainerHigh = Neutral92, + surfaceContainerHighest = Neutral90, + + outline = NeutralVariant50, + outlineVariant = NeutralVariant80, + scrim = Color.Black, +) + +/** + * AMOLED variant. Only the true backdrop layers go black — the container ramp keeps its tint so + * cards still read as distinct planes instead of collapsing into one void. + */ +fun ColorScheme.toPitchBlack(): ColorScheme = copy( + background = Color.Black, + surface = Color.Black, + surfaceDim = Color.Black, + surfaceContainerLowest = Color.Black, + surfaceContainerLow = Neutral6, + surfaceContainer = Neutral10, + surfaceContainerHigh = Neutral12, + surfaceContainerHighest = Neutral17, +) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeCustomColors.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeCustomColors.kt index 7eaaf43..7154d47 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeCustomColors.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeCustomColors.kt @@ -19,6 +19,10 @@ data class CustomColors( val onWarning: Color, val warningContainer: Color, val onWarningContainer: Color, + val success: Color, + val onSuccess: Color, + val successContainer: Color, + val onSuccessContainer: Color, ) private val YellowAlt1 = Color(0xFFE9C414) @@ -27,11 +31,22 @@ private val DarkBrown = Color(0xFF3B2F00) private val DarkerBrown = Color(0xFF221B00) private val DarkBronze = Color(0xFF554600) +// Success leans mint rather than a stock green so it sits with the aqua tertiary ramp. (yet again claude cause colorblind) +private val Mint40 = Color(0xFF1E7D52) +private val Mint80 = Color(0xFF6EDBA4) +private val Mint90 = Color(0xFFB6F5D2) +private val MintDark10 = Color(0xFF00210F) +private val MintDark30 = Color(0xFF00623A) + val DarkCustomColors = CustomColors( warning = YellowAlt1, onWarning = DarkBrown, warningContainer = DarkBronze, onWarningContainer = Shandy, + success = Mint80, + onSuccess = MintDark10, + successContainer = MintDark30, + onSuccessContainer = Mint90, ) val LightCustomColors = CustomColors( @@ -39,4 +54,8 @@ val LightCustomColors = CustomColors( onWarning = Color.White, warningContainer = Shandy, onWarningContainer = DarkerBrown, + success = Mint40, + onSuccess = Color.White, + successContainer = Mint90, + onSuccessContainer = MintDark10, ) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeShapes.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeShapes.kt new file mode 100644 index 0000000..2d5b1b3 --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeShapes.kt @@ -0,0 +1,19 @@ +package com.meowarex.rlmobile.ui.theme + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Shapes +import androidx.compose.ui.unit.dp + +/** + * Expressive shape scale + */ +val ThemeShapes = Shapes( + extraSmall = RoundedCornerShape(8.dp), + small = RoundedCornerShape(14.dp), + medium = RoundedCornerShape(20.dp), + large = RoundedCornerShape(28.dp), + largeIncreased = RoundedCornerShape(32.dp), + extraLarge = RoundedCornerShape(36.dp), + extraLargeIncreased = RoundedCornerShape(44.dp), + extraExtraLarge = RoundedCornerShape(52.dp), +) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeTypography.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeTypography.kt index aa4d4cc..4c34794 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeTypography.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/ThemeTypography.kt @@ -2,16 +2,17 @@ package com.meowarex.rlmobile.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.ExperimentalTextApi +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.* +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.sp import com.meowarex.rlmobile.R // This uses a variable font variant of Roboto. // ref: https://medium.com/androiddevelopers/just-your-type-variable-fonts-in-compose-5bf63b357994 @OptIn(ExperimentalTextApi::class) -val ThemeTypography = run { - val baseline = Typography() - +private val RadiantFontFamily = run { val weights = arrayOf( FontWeight.Thin, FontWeight.ExtraLight, @@ -32,23 +33,145 @@ val ThemeTypography = run { ) ) } - val fontFamily = FontFamily(fonts) - - Typography( - displayLarge = baseline.displayLarge.copy(fontFamily = fontFamily), - displayMedium = baseline.displayMedium.copy(fontFamily = fontFamily), - displaySmall = baseline.displaySmall.copy(fontFamily = fontFamily), - headlineLarge = baseline.headlineLarge.copy(fontFamily = fontFamily), - headlineMedium = baseline.headlineMedium.copy(fontFamily = fontFamily), - headlineSmall = baseline.headlineSmall.copy(fontFamily = fontFamily), - titleLarge = baseline.titleLarge.copy(fontFamily = fontFamily), - titleMedium = baseline.titleMedium.copy(fontFamily = fontFamily), - titleSmall = baseline.titleSmall.copy(fontFamily = fontFamily), - bodyLarge = baseline.bodyLarge.copy(fontFamily = fontFamily), - bodyMedium = baseline.bodyMedium.copy(fontFamily = fontFamily), - bodySmall = baseline.bodySmall.copy(fontFamily = fontFamily), - labelLarge = baseline.labelLarge.copy(fontFamily = fontFamily), - labelMedium = baseline.labelMedium.copy(fontFamily = fontFamily), - labelSmall = baseline.labelSmall.copy(fontFamily = fontFamily), - ) + FontFamily(fonts) } + +/** + * Expressive type scale + */ +private val Emphasis = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, +) + +val ThemeTypography = Typography( + // Display — heavy and tight + displayLarge = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Black, + fontSize = 57.sp, + lineHeight = 60.sp, + letterSpacing = (-1.5).sp, + lineHeightStyle = Emphasis, + ), + displayMedium = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 45.sp, + lineHeight = 50.sp, + letterSpacing = (-1.0).sp, + lineHeightStyle = Emphasis, + ), + displaySmall = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.ExtraBold, + fontSize = 36.sp, + lineHeight = 42.sp, + letterSpacing = (-0.6).sp, + lineHeightStyle = Emphasis, + ), + + // Headline + headlineLarge = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 32.sp, + lineHeight = 38.sp, + letterSpacing = (-0.5).sp, + lineHeightStyle = Emphasis, + ), + headlineMedium = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 28.sp, + lineHeight = 34.sp, + letterSpacing = (-0.4).sp, + lineHeightStyle = Emphasis, + ), + headlineSmall = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + lineHeight = 30.sp, + letterSpacing = (-0.3).sp, + lineHeightStyle = Emphasis, + ), + + // Title — card and row headers + titleLarge = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Bold, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = (-0.2).sp, + lineHeightStyle = Emphasis, + ), + titleMedium = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.sp, + lineHeightStyle = Emphasis, + ), + titleSmall = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.sp, + lineHeightStyle = Emphasis, + ), + + // Body + bodyLarge = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.1.sp, + lineHeightStyle = Emphasis, + ), + bodyMedium = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.15.sp, + lineHeightStyle = Emphasis, + ), + bodySmall = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 17.sp, + letterSpacing = 0.2.sp, + lineHeightStyle = Emphasis, + ), + + // Label — buttons, tags, overlines + labelLarge = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.05.sp, + lineHeightStyle = Emphasis, + ), + labelMedium = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.SemiBold, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + lineHeightStyle = Emphasis, + ), + labelSmall = TextStyle( + fontFamily = RadiantFontFamily, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + lineHeightStyle = Emphasis, + ), +) diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/UiStyle.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/UiStyle.kt new file mode 100644 index 0000000..f51525e --- /dev/null +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/theme/UiStyle.kt @@ -0,0 +1,90 @@ +package com.meowarex.rlmobile.ui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import com.meowarex.rlmobile.R + +/** + * Which visual language the app renders in (UI Style not Theme) + */ +enum class UiStyle { + /** The current design */ + Radiant, + + /** Compose replica of the pre-redesign stock Material 3 look */ + Legacy; + + @Composable + fun toDisplayName() = stringResource( + when (this) { + Radiant -> R.string.ui_style_radiant + Legacy -> R.string.ui_style_legacy + } + ) + + @Composable + fun toPainter() = painterResource( + when (this) { + Radiant -> R.drawable.ic_sparkle + Legacy -> R.drawable.ic_history + } + ) +} + +/** + * controls that separate the styles (claude) + */ +@Immutable +data class RadiantStyle( + /** Raised surfaces get a vertical light-to-dark falloff. */ + val surfaceGradient: Boolean, + /** Raised surfaces get a hairline border. */ + val hairline: Boolean, + /** Accented controls cast a coloured outer glow. */ + val glow: Boolean, + /** Scale factor applied while a control is held. 1f disables the effect. */ + val pressScale: Float, + /** Use Material's ripple indication instead of the scale animation. */ + val ripple: Boolean, + /** Delegate switches/radios/checkboxes/fields to the stock Material widgets. */ + val native: Boolean, + /** Settings rows render as grouped tiles rather than flat full-bleed rows. */ + val groupedSettings: Boolean, + /** The home screen leads with a raised identity card rather than a flat centred stack. */ + val heroCard: Boolean, +) { + val legacy: Boolean get() = native + + companion object { + val Radiant = RadiantStyle( + surfaceGradient = true, + hairline = true, + glow = true, + pressScale = 0.965f, + ripple = false, + native = false, + groupedSettings = true, + heroCard = true, + ) + + val Legacy = RadiantStyle( + surfaceGradient = false, + hairline = false, + glow = false, + pressScale = 1f, + ripple = true, + native = true, + groupedSettings = false, + heroCard = false, + ) + } +} + +val LocalRadiantStyle = staticCompositionLocalOf { RadiantStyle.Radiant } + +/** Shorthand for `LocalRadiantStyle.current`. */ +val radiantStyle: RadiantStyle + @Composable get() = LocalRadiantStyle.current diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/managerupdate/ManagerUpdateDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/managerupdate/ManagerUpdateDialog.kt index 6ce8329..639a4ec 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/managerupdate/ManagerUpdateDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/managerupdate/ManagerUpdateDialog.kt @@ -15,6 +15,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog import com.meowarex.rlmobile.ui.components.Tag data class VersionDelta( @@ -30,57 +31,32 @@ fun ManagerUpdateDialog( deltas: List, onDismiss: () -> Unit, ) { - AlertDialog( + RadiantDialog( onDismissRequest = onDismiss, - confirmButton = { - FilledTonalButton( - onClick = onDismiss, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - Text(stringResource(R.string.action_continue)) - } - }, - title = { - Text( - text = stringResource(R.string.manager_update_title), - textAlign = TextAlign.Center, - ) - }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource(R.string.manager_update_subtitle), - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyMedium, - ) - Column( - verticalArrangement = Arrangement.spacedBy(8.dp), - modifier = Modifier.fillMaxWidth(), - ) { - deltas.forEach { delta -> - DeltaCard(delta = delta) - } - } - } - }, + title = stringResource(R.string.manager_update_title), icon = { Icon( painter = painterResource(R.drawable.ic_update), contentDescription = null, - modifier = Modifier.size(36.dp), + modifier = Modifier.size(26.dp), ) }, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = false, - ), - ) + confirmText = stringResource(R.string.action_continue), + onConfirm = onDismiss, + ) { + Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { + Text(stringResource(R.string.manager_update_subtitle)) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier.fillMaxWidth(), + ) { + deltas.forEach { delta -> + DeltaCard(delta = delta) + } + } + } + } } @Composable diff --git a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/updater/UpdaterDialog.kt b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/updater/UpdaterDialog.kt index c321083..54ff6f3 100644 --- a/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/updater/UpdaterDialog.kt +++ b/Manager/app/src/main/kotlin/com/meowarex/rlmobile/ui/widgets/updater/UpdaterDialog.kt @@ -13,6 +13,10 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.window.DialogProperties import com.meowarex.rlmobile.R +import com.meowarex.rlmobile.ui.components.radiant.RadiantButton +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize +import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle +import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog import org.koin.androidx.compose.koinViewModel @Composable @@ -25,79 +29,63 @@ fun UpdaterDialog( val downloadProgress by viewModel.progress.collectAsState() val downloadInProgress by remember { derivedStateOf { downloadProgress != null } } - AlertDialog( - confirmButton = { - FilledTonalButton( - onClick = viewModel::triggerUpdate, - colors = ButtonDefaults.filledTonalButtonColors( - containerColor = MaterialTheme.colorScheme.primary, - contentColor = MaterialTheme.colorScheme.onPrimary, - ), - ) { - if (!isWorking) { - Text(stringResource(R.string.action_update)) - } else if (!downloadInProgress) { - CircularProgressIndicator( - color = MaterialTheme.colorScheme.onSecondary, - strokeWidth = 2.5.dp, - modifier = Modifier.size(20.dp) - ) - } else { - CircularProgressIndicator( - progress = { downloadProgress ?: 1f }, - color = MaterialTheme.colorScheme.onSecondary, - trackColor = MaterialTheme.colorScheme.onSecondary.copy(alpha = 0.6f), - strokeWidth = 2.5.dp, - modifier = Modifier.size(20.dp) - ) - } - } - }, - dismissButton = { - TextButton( - onClick = viewModel::dismissDialog, - enabled = !isWorking, - ) { - Text(stringResource(R.string.action_dismiss)) - } - }, + RadiantDialog( onDismissRequest = {}, - title = { - Text(stringResource(R.string.updater_title, viewModel.targetVersion ?: "")) - }, - text = { - val uriHandler = LocalUriHandler.current - - Column( - verticalArrangement = Arrangement.spacedBy(6.dp), - horizontalAlignment = Alignment.CenterHorizontally, - ) { - Text( - text = stringResource(R.string.updater_body), - textAlign = TextAlign.Center, - ) - - TextButton( - onClick = { uriHandler.openUri(viewModel.targetReleaseUrl!!) } - ) { - Text( - text = stringResource(R.string.updater_open_github), - textAlign = TextAlign.Center, - textDecoration = TextDecoration.Underline, - ) - } - } - }, + title = stringResource(R.string.updater_title, viewModel.targetVersion ?: ""), icon = { Icon( - painter = painterResource(R.drawable.ic_warning), + painter = painterResource(R.drawable.ic_update), contentDescription = null, - modifier = Modifier.size(36.dp), + modifier = Modifier.size(26.dp), ) }, - properties = DialogProperties( - dismissOnBackPress = false, - dismissOnClickOutside = false, - ), - ) + ) { + val uriHandler = LocalUriHandler.current + + Column(verticalArrangement = Arrangement.spacedBy(10.dp)) { + Text(stringResource(R.string.updater_body)) + + RadiantButton( + text = stringResource(R.string.updater_open_github), + onClick = { uriHandler.openUri(viewModel.targetReleaseUrl!!) }, + style = RadiantButtonStyle.Ghost, + size = RadiantButtonSize.Small, + ) + + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End), + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .padding(top = 10.dp) + .fillMaxWidth(), + ) { + RadiantButton( + text = stringResource(R.string.action_dismiss), + onClick = viewModel::dismissDialog, + enabled = !isWorking, + style = RadiantButtonStyle.Ghost, + size = RadiantButtonSize.Small, + ) + + // update is running the confirm button is replaced by the progress readout + if (isWorking) { + if (downloadInProgress) { + CircularWavyProgressIndicator( + progress = { downloadProgress ?: 1f }, + modifier = Modifier.size(34.dp), + ) + } else { + CircularWavyProgressIndicator(modifier = Modifier.size(34.dp)) + } + } else { + RadiantButton( + text = stringResource(R.string.action_update), + onClick = viewModel::triggerUpdate, + style = RadiantButtonStyle.Accent, + size = RadiantButtonSize.Small, + ) + } + } + } + } } diff --git a/Manager/app/src/main/res/values/strings.xml b/Manager/app/src/main/res/values/strings.xml index ed6300e..d18b237 100644 --- a/Manager/app/src/main/res/values/strings.xml +++ b/Manager/app/src/main/res/values/strings.xml @@ -86,6 +86,10 @@ Appearance Info Theme + UI style + Want the old UI back? + Radiant + Legacy The theme to apply to this app Dynamic color Enables Material You theming on Android 12+ @@ -160,6 +164,7 @@ Cancelled uninstallation Failed to verify download Insufficient storage space! + %1$d of %2$d steps Since battery optimizations have not been disabled, minimizing this screen may abort the installation process! diff --git a/Manager/gradle/libs.versions.toml b/Manager/gradle/libs.versions.toml index dde2d0a..9c8ebc3 100644 --- a/Manager/gradle/libs.versions.toml +++ b/Manager/gradle/libs.versions.toml @@ -1,6 +1,7 @@ [versions] accompanist = "0.37.3" -agp = "9.0.0" +# 9.1.0+ is required by compose 1.12.x, which material3's expressive line depends on. +agp = "9.3.1" androidx-activity = "1.12.2" androidx-core = "1.17.0" androidx-lifecycle = "2.10.0" @@ -12,8 +13,11 @@ axml = "1.0.1" binary-resources = "2.1.0" bouncycastle = "1.83" coil = "3.3.0" -compose = "1.10.1" -compose-material3 = "1.4.0" +compose = "1.12.0-rc01" +# Material 3 Expressive (MaterialExpressiveTheme, MotionScheme, ButtonGroup, LoadingIndicator, +# wavy progress, MaterialShapes) only exists on the 1.5.0-alpha line — it is not in 1.4.0 stable. +compose-material3 = "1.5.0-alpha25" +graphics-shapes = "1.1.0" compose-pipette = "2.0.0-beta03" desugaring = "2.1.5" dhizuku = "2.5.4" @@ -59,6 +63,7 @@ compose-animations = { module = "androidx.compose.animation:animation-graphics", compose-ui = { module = "androidx.compose.ui:ui", version.ref = "compose" } compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } +compose-graphics-shapes = { module = "androidx.graphics:graphics-shapes", version.ref = "graphics-shapes" } # Compose debug tooling compose-runtime-tracing = { module = "androidx.compose.runtime:runtime-tracing", version.ref = "compose" } @@ -133,6 +138,7 @@ compose = [ "compose-foundation", "compose-material3", "compose-animations", + "compose-graphics-shapes", ] koin = [ "koin-android", diff --git a/Manager/gradle/wrapper/gradle-wrapper.properties b/Manager/gradle/wrapper/gradle-wrapper.properties index 19a6bde..1a70468 100644 --- a/Manager/gradle/wrapper/gradle-wrapper.properties +++ b/Manager/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME