mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-27 06:27:46 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
24c429e8b9 | ||
|
|
0f00c4dca6 | ||
|
|
56ed288355 | ||
|
|
5fadc4ebbc | ||
|
|
28cd296283 | ||
|
|
ba9bba2030 | ||
|
|
d44d833334 | ||
|
|
8d06cea14d | ||
|
|
b9baef4478 | ||
|
|
8fc0241611 | ||
|
|
a648e1a97c | ||
|
|
2150b87371 | ||
|
|
0d82c16168 | ||
|
|
4c257f7ad9 | ||
|
|
7458df8eba | ||
|
|
6a5771cf9e | ||
|
|
a8a576c994 | ||
|
|
585666c403 | ||
|
|
90c008082f | ||
|
|
c2a2586306 | ||
|
|
2291f4064e | ||
|
|
f6a4de0cc7 | ||
|
|
4911e7703d | ||
|
|
f9a174eafa | ||
|
|
49fe4c0d91 | ||
|
|
189e3d28a2 | ||
|
|
aae2f54fab | ||
|
|
191dcca239 | ||
|
|
08aee155c9 | ||
|
|
47c9c37236 | ||
|
|
50b99b6193 | ||
|
|
03271bf26d | ||
|
|
75f8187526 | ||
|
|
523f0a342b | ||
|
|
fb4c776c61 | ||
|
|
cbf04d9fa7 | ||
|
|
266cd184dd | ||
|
|
04ea66acdb | ||
|
|
22526f4e0b | ||
|
|
5a3ad2df7a | ||
|
|
fa99d10134 | ||
|
|
4e8af5960c | ||
|
|
09f6e8d34f | ||
|
|
160be11ac4 | ||
|
|
4486e52117 |
@@ -16,8 +16,12 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
- id: set
|
||||
run: |
|
||||
# .version now holds the full semver (e.g. 1.0.0) and is bumped per release.
|
||||
# It used to be a "0.3" prefix with github.run_number appended as the patch,
|
||||
# which cannot express an exact 1.0.0. versionCode still tracks run_number,
|
||||
# so it stays monotonic regardless of what versionName says.
|
||||
BASE=$(cat .version | tr -d '[:space:]')
|
||||
echo "version=$BASE.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
||||
echo "version=$BASE" >> $GITHUB_OUTPUT
|
||||
|
||||
Build:
|
||||
needs: Version
|
||||
|
||||
@@ -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",
|
||||
)
|
||||
@@ -228,3 +232,13 @@ fun ProviderFactory.execIgnoreCode(vararg command: String): String = try {
|
||||
} catch (_: Exception) {
|
||||
""
|
||||
}
|
||||
|
||||
// Sync Local Cached Assets with Repo Releases to keep them up to date
|
||||
val syncBundledManifest by tasks.registering(Copy::class) {
|
||||
val src = rootProject.rootDir.resolve("../patches/manifest.json")
|
||||
onlyIf { src.exists() }
|
||||
from(src)
|
||||
into(layout.projectDirectory.dir("src/main/assets"))
|
||||
rename { "patches-manifest.json" }
|
||||
}
|
||||
tasks.named("preBuild") { dependsOn(syncBundledManifest) }
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
{
|
||||
"version": 1,
|
||||
"patches": [
|
||||
{
|
||||
"id": "EnableLegacyUi",
|
||||
"order": 10,
|
||||
"fileNames": [
|
||||
"enable-legacy-ui.patch"
|
||||
],
|
||||
"title": "Enable Legacy UI",
|
||||
"description": "[This patch will stop working soon] - Replaces the New Compose based UI with the Legacy UI (disables player-market-ui feature flag)",
|
||||
"default": false,
|
||||
"requires": [
|
||||
"DebugMenuUnlock"
|
||||
],
|
||||
"disables": [
|
||||
"LyricsDisableCover",
|
||||
"LyricsReplaceLyricsButton",
|
||||
"LyricsReplaceShareButton",
|
||||
"LyricsRlApi",
|
||||
"LyricsKeepControlsVisible",
|
||||
"PlayerBackdrop",
|
||||
"QualityBadgeColors",
|
||||
"PlayerMovePlayingFrom",
|
||||
"PlayerOneHanded",
|
||||
"LyricsProgressPill",
|
||||
"MiniPlayerRedesign"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "LyricsRlApi",
|
||||
"order": 20,
|
||||
"fileNames": [
|
||||
"lyrics-rl-api.patch",
|
||||
"lyrics-rl-api-observer.patch",
|
||||
"lyrics-rl-api-isrc.patch"
|
||||
],
|
||||
"title": "Radiant Lyrics API - WIP",
|
||||
"description": "Use Radiant Lyrics API to fetch Lyrics (Syllable Level & Higher Quality)",
|
||||
"default": false,
|
||||
"defaultVariantIndex": 0,
|
||||
"variants": [
|
||||
{
|
||||
"title": "Line",
|
||||
"fileNames": []
|
||||
},
|
||||
{
|
||||
"title": "Word",
|
||||
"fileNames": [
|
||||
"lyrics-rl-api-word.patch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Syllable",
|
||||
"fileNames": [
|
||||
"lyrics-rl-api-syllable.patch"
|
||||
]
|
||||
}
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/RLAPILyricsHook.smali",
|
||||
"radiant/RLAPILyricsWorker.smali",
|
||||
"radiant/IsrcCache.smali",
|
||||
"radiant/WordLyrics.smali",
|
||||
"radiant/WordLyricsTick.smali",
|
||||
"radiant/SylBrush.smali",
|
||||
"radiant/SylLayout.smali"
|
||||
],
|
||||
"advancedOptions": [
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "romanize",
|
||||
"title": "Romanization",
|
||||
"description": "Return a romanized transcription and show that instead of the original script",
|
||||
"default": false,
|
||||
"token": "RL_ROMANIZE",
|
||||
"inline": true
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "synthesize",
|
||||
"title": "WIP | AI Generate Syllables",
|
||||
"description": "[Only works with API results] - Generates Syllable timings from Line timings using a self trained AI model ",
|
||||
"default": false,
|
||||
"inline": true,
|
||||
"token": "RL_SYNTHESIZE"
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "context_aware",
|
||||
"title": "Context Aware Lyrics",
|
||||
"description": "Display voice context cues (adlibs and duet singers get displayed seperately)",
|
||||
"default": true,
|
||||
"token": "RL_CTX_ON"
|
||||
},
|
||||
{
|
||||
"type": "choice",
|
||||
"key": "context_aware_mode",
|
||||
"title": "Show",
|
||||
"description": "",
|
||||
"entries": [
|
||||
"Adlibs",
|
||||
"Singers",
|
||||
"Both"
|
||||
],
|
||||
"values": [
|
||||
"0x1",
|
||||
"0x2",
|
||||
"0x3"
|
||||
],
|
||||
"defaultIndex": 0,
|
||||
"requiresOption": "context_aware",
|
||||
"token": "RL_CTX"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "PlayerBackdrop",
|
||||
"order": 30,
|
||||
"fileNames": [],
|
||||
"title": "Player Backdrop",
|
||||
"description": "Lets you fully adjust how the player backdrop blur looks and behaves",
|
||||
"default": true,
|
||||
"defaultVariantIndex": 2,
|
||||
"variants": [
|
||||
{
|
||||
"title": "Static",
|
||||
"fileNames": [
|
||||
"player-backdrop-static.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/BackdropFx.smali"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Animated",
|
||||
"fileNames": [
|
||||
"player-backdrop-animated.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/BackdropFx.smali"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Playback",
|
||||
"fileNames": [
|
||||
"player-backdrop-playback.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/BackdropFx.smali"
|
||||
]
|
||||
}
|
||||
],
|
||||
"advancedOptions": [
|
||||
{
|
||||
"type": "slider",
|
||||
"key": "blur_strength",
|
||||
"title": "Blur Strength",
|
||||
"description": "The value of Blur applied to the backdrop cover",
|
||||
"default": 50,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"steps": 19,
|
||||
"displayAsPercent": true,
|
||||
"token": "RL_BLUR",
|
||||
"encode": {
|
||||
"kind": "int",
|
||||
"scale": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "slider",
|
||||
"key": "contrast",
|
||||
"title": "Contrast",
|
||||
"description": "The value of Contrast applied to the backdrop cover",
|
||||
"default": 50,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"steps": 19,
|
||||
"displayAsPercent": true,
|
||||
"token": "RL_CONTRAST",
|
||||
"encode": {
|
||||
"kind": "floatBits",
|
||||
"scale": 0.02
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "slider",
|
||||
"key": "saturation",
|
||||
"title": "Saturation",
|
||||
"description": "The value of Saturation applied to the backdrop cover",
|
||||
"default": 50,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"steps": 19,
|
||||
"displayAsPercent": true,
|
||||
"token": "RL_SATURATION",
|
||||
"encode": {
|
||||
"kind": "floatBits",
|
||||
"scale": 0.02
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "slider",
|
||||
"key": "brightness",
|
||||
"title": "Brightness",
|
||||
"description": "The value of Brightness applied to the backdrop cover",
|
||||
"default": 50,
|
||||
"min": 0,
|
||||
"max": 100,
|
||||
"steps": 19,
|
||||
"displayAsPercent": true,
|
||||
"token": "RL_BRIGHTNESS",
|
||||
"encode": {
|
||||
"kind": "floatBits",
|
||||
"scale": 0.02
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "cover_everywhere",
|
||||
"title": "Cover Everywhere - WIP",
|
||||
"description": "Applies TIDAL's native blurred album-art backdrop to every Compose based page (Home & Collection)",
|
||||
"default": false,
|
||||
"inline": true,
|
||||
"fileNames": [
|
||||
"home-backdrop.patch",
|
||||
"collection-backdrop.patch",
|
||||
"cover-capture.patch"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "QualityBadgeColors",
|
||||
"order": 36,
|
||||
"fileNames": [
|
||||
"player-quality-badge-colors.patch"
|
||||
],
|
||||
"title": "Quality Badge Colors",
|
||||
"description": "Brings color back to the player quality badge.",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "PlayerMovePlayingFrom",
|
||||
"order": 37,
|
||||
"fileNames": [
|
||||
"player-move-playing-from.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/PlayingFrom.smali"
|
||||
],
|
||||
"title": "Playing From",
|
||||
"description": "Shows the source name (playlist/album/mix) above the track title",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "PlayerOneHanded",
|
||||
"order": 38,
|
||||
"fileNames": [
|
||||
"player-one-handed.patch"
|
||||
],
|
||||
"title": "One-Handed Mode",
|
||||
"description": "Mirrors the bottom player buttons left-to-right for easier one-handed reach.",
|
||||
"default": false
|
||||
},
|
||||
{
|
||||
"id": "LyricsProgressPill",
|
||||
"order": 40,
|
||||
"fileNames": [
|
||||
"lyrics-progress-pill.patch",
|
||||
"lyrics-fade-region.patch"
|
||||
],
|
||||
"title": "Lyrics Progress Pill",
|
||||
"description": "Restores the old Track Progress Pill in the top of the Lyrics screen!",
|
||||
"default": true,
|
||||
"requires": [
|
||||
"LyricsDisableCover",
|
||||
"LyricsReplaceLyricsButton",
|
||||
"LyricsReplaceShareButton"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "LyricsDisableCover",
|
||||
"order": 41,
|
||||
"fileNames": [
|
||||
"lyrics-disable-cover.patch"
|
||||
],
|
||||
"title": "Disable Lyrics Cover",
|
||||
"description": "Removes the mini track cover from the lyrics screen.",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "LyricsReplaceLyricsButton",
|
||||
"order": 42,
|
||||
"fileNames": [
|
||||
"lyrics-replace-lyrics-button.patch",
|
||||
"lyrics-sparkle-conditional-visibility.patch"
|
||||
],
|
||||
"title": "Replace Lyrics Button",
|
||||
"description": "Replaces the Lyrics button with the RL Sparkle!",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "LyricsReplaceShareButton",
|
||||
"order": 43,
|
||||
"fileNames": [
|
||||
"lyrics-replace-share-button.patch"
|
||||
],
|
||||
"title": "Replace Share Button",
|
||||
"description": "Replaces the share button with the Tidal Connect button from the top of the screen. (it's in the menu already)",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "MiniPlayerRedesign",
|
||||
"order": 50,
|
||||
"fileNames": [],
|
||||
"title": "Redesigned Mini-Player",
|
||||
"description": "Integrates the Seekbar into the control panel at the bottom of the screen.",
|
||||
"default": false,
|
||||
"defaultVariantIndex": 2,
|
||||
"variants": [
|
||||
{
|
||||
"title": "Floating",
|
||||
"fileNames": []
|
||||
},
|
||||
{
|
||||
"title": "Grey",
|
||||
"fileNames": [
|
||||
"mini-player-grey.patch"
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Black",
|
||||
"fileNames": [
|
||||
"mini-player-black.patch"
|
||||
]
|
||||
}
|
||||
],
|
||||
"advancedOptions": [
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "dynamic_bg",
|
||||
"title": "Mini-Player Dynamic Background",
|
||||
"description": "Colorizes the Mini-Player depending on the currently playing track.",
|
||||
"default": false,
|
||||
"inline": true,
|
||||
"fileNames": [
|
||||
"mini-player-dynamic-bg.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/MiniPlayerBackground.smali"
|
||||
],
|
||||
"hidesVariants": [
|
||||
2
|
||||
],
|
||||
"relabelVariants": {
|
||||
"1": "Legacy"
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "border",
|
||||
"title": "Progress Border",
|
||||
"description": "Draws an animated progress ring around the floating mini-player. Requires the Floating style.",
|
||||
"default": false,
|
||||
"requiresVariant": 0,
|
||||
"fileNames": [
|
||||
"mini-player-floating-border.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/MiniSeekerFloating.smali"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "gestures",
|
||||
"title": "Mini-Player Gestures",
|
||||
"description": "Adds swipe up gesture to the mini-player to reopen the player",
|
||||
"default": true,
|
||||
"fileNames": [
|
||||
"mini-player-up-gesture.patch",
|
||||
"player-swipe-gesture.patch",
|
||||
"lyrics-gesture-scroll-guard.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/gestures/MiniPlayerGestures.smali",
|
||||
"radiant/gestures/MiniPlayerGestures$Gesture.smali",
|
||||
"radiant/gestures/MiniPlayerGestures$FeedbackLayer.smali",
|
||||
"radiant/gestures/MiniPlayerGestures$FeedbackResetAnimator.smali",
|
||||
"radiant/gestures/MiniPlayerGestures$RootGesture.smali",
|
||||
"radiant/gestures/MiniPlayerGestures$ApplyPending.smali",
|
||||
"radiant/gestures/LyricsDrag.smali",
|
||||
"radiant/gestures/LyricsDrag$Guard.smali"
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "choice",
|
||||
"key": "swipe_up_drag",
|
||||
"title": "Swipe Up Style",
|
||||
"entries": [
|
||||
"Static",
|
||||
"Drag"
|
||||
],
|
||||
"values": [
|
||||
"0x0",
|
||||
"0x1"
|
||||
],
|
||||
"defaultIndex": 1,
|
||||
"requiresOption": "gestures",
|
||||
"token": "RL_MINI_PLAYER_SWIPE_UP_DRAG"
|
||||
},
|
||||
{
|
||||
"type": "toggle",
|
||||
"key": "next_prev",
|
||||
"title": "Next/Previous Gestures",
|
||||
"description": "Swipe left or right on the mini-player to skip tracks.",
|
||||
"default": false,
|
||||
"requiresOption": "gestures",
|
||||
"fileNames": [
|
||||
"mini-player-left-right-gesture.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/gestures/MiniPlayerTrackGestures.smali",
|
||||
"radiant/gestures/MiniPlayerTrackGestures$Gesture.smali",
|
||||
"radiant/gestures/MiniPlayerTrackGestures$OffsetLayer.smali",
|
||||
"radiant/gestures/MiniPlayerTrackGestures$ResetAnimator.smali",
|
||||
"radiant/gestures/MiniPlayerTrackGestures$TextDraw.smali",
|
||||
"com/tidal/android/feature/appscaffold/ui/r$c.smali"
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "LyricsKeepControlsVisible",
|
||||
"order": 60,
|
||||
"fileNames": [
|
||||
"lyrics-keep-controls-visible.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/LyricsReinitGuard.smali"
|
||||
],
|
||||
"title": "Keep Controls Visible",
|
||||
"description": "Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off. Also stops the open lyrics view from randomly closing when the player re-fetches the same track.",
|
||||
"default": true
|
||||
},
|
||||
{
|
||||
"id": "WazeIntegration",
|
||||
"order": 90,
|
||||
"category": "integration",
|
||||
"pathLocked": true,
|
||||
"fileNames": [
|
||||
"integration-waze-playback.patch"
|
||||
],
|
||||
"extensionFiles": [
|
||||
"radiant/WazeInitReceiver.smali",
|
||||
"radiant/WazeServiceConnection.smali"
|
||||
],
|
||||
"title": "Waze Integration",
|
||||
"description": "Control TIDAL playback from the Waze app",
|
||||
"default": true,
|
||||
"advancedOptions": [
|
||||
{
|
||||
"type": "choice",
|
||||
"key": "browse_root",
|
||||
"title": "Media Menu",
|
||||
"entries": [
|
||||
"Auto Menu",
|
||||
"Home",
|
||||
"Recents"
|
||||
],
|
||||
"values": [
|
||||
"ROOT_AUTO",
|
||||
"HOME_V2::home_page_v2_id",
|
||||
"RECENTLY_PLAYED::home/pages/CONTINUE_LISTEN_TO/view-all"
|
||||
],
|
||||
"defaultIndex": 0,
|
||||
"token": "RL_WAZE_ROOT_ID"
|
||||
},
|
||||
{
|
||||
"type": "color",
|
||||
"key": "accent_color",
|
||||
"title": "Accent Color",
|
||||
"default": -16747037,
|
||||
"token": "RL_WAZE_THEME_COLOR"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "DebugMenuUnlock",
|
||||
"order": 100,
|
||||
"fileNames": [
|
||||
"debug-menu-unlock.patch"
|
||||
],
|
||||
"title": "Unlock Debug Menu",
|
||||
"description": "Reveals TIDAL's hidden Debug Menu in Settings. (Unlocks Feature Flags & the legacy Debug Options)",
|
||||
"default": false
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,7 +22,8 @@ class PathManager(
|
||||
|
||||
val patchingDownloadDir = patchingDir.resolve("downloads")
|
||||
|
||||
val cacheDownloadDir = context.cacheDir.resolve("downloads")
|
||||
// Persistent Stash of assets for Offline Repatching
|
||||
val cacheDownloadDir = patchingDir.resolve("downloads-cache")
|
||||
|
||||
val customComponentsDir = patchingDir.resolve("custom")
|
||||
|
||||
@@ -34,11 +35,15 @@ class PathManager(
|
||||
|
||||
val patchedApk = patchingWorkingDir.resolve("patched.apk")
|
||||
|
||||
// Persistent snapshot of the last successful release fetch (build info + patches asset URL)
|
||||
val cachedReleaseInfo = patchingDir.resolve("release-info.json")
|
||||
|
||||
fun clearCache() {
|
||||
val targets = arrayOf(
|
||||
patchingDownloadDir,
|
||||
patchingWorkingDir,
|
||||
cacheDownloadDir,
|
||||
cachedReleaseInfo,
|
||||
context.cacheDir,
|
||||
)
|
||||
for (dir in targets) {
|
||||
@@ -54,6 +59,16 @@ class PathManager(
|
||||
.resolve("patches")
|
||||
.resolve("$version.zip")
|
||||
|
||||
fun hasCachedTidalApk(version: Int, split: String = "base"): Boolean {
|
||||
val rel = "tidal/$version/$split.apk"
|
||||
return patchingDownloadDir.resolve(rel).exists() || cacheDownloadDir.resolve(rel).exists()
|
||||
}
|
||||
|
||||
fun hasCachedSmaliPatches(version: SemVer): Boolean {
|
||||
val rel = "patches/$version.zip"
|
||||
return patchingDownloadDir.resolve(rel).exists() || cacheDownloadDir.resolve(rel).exists()
|
||||
}
|
||||
|
||||
fun customTidalApks() = customTidalApksDir.listFiles()?.asList() ?: emptyList()
|
||||
|
||||
fun customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
|
||||
|
||||
@@ -4,14 +4,19 @@ 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<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
|
||||
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
||||
// Dev: force-apply path-gated ("incompatible") patches even under a non-stock package name
|
||||
var bypassIncompatible by booleanPreference("bypass_incompatible", false)
|
||||
var showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
|
||||
var autoUpdateCheck by booleanPreference("auto_update_check", true)
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.meowarex.rlmobile.manager
|
||||
|
||||
import android.util.Log
|
||||
import com.meowarex.rlmobile.BuildConfig
|
||||
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
// Snapshot of the last successful release fetch (build info + patches asset URL)
|
||||
@Serializable
|
||||
data class CachedReleaseInfo(
|
||||
val data: RLBuildInfo,
|
||||
val patchesAssetUrl: String,
|
||||
)
|
||||
|
||||
object ReleaseInfoCache {
|
||||
fun load(paths: PathManager, json: Json): CachedReleaseInfo? = try {
|
||||
paths.cachedReleaseInfo
|
||||
.takeIf { it.exists() }
|
||||
?.let { json.decodeFromString<CachedReleaseInfo>(it.readText()) }
|
||||
} catch (t: Throwable) {
|
||||
Log.w(BuildConfig.TAG, "Failed to read cached release info", t)
|
||||
null
|
||||
}
|
||||
|
||||
fun save(paths: PathManager, json: Json, info: CachedReleaseInfo) = try {
|
||||
paths.cachedReleaseInfo.parentFile?.mkdirs()
|
||||
paths.cachedReleaseInfo.writeText(json.encodeToString(info))
|
||||
} catch (t: Throwable) {
|
||||
Log.w(BuildConfig.TAG, "Failed to cache release info", t)
|
||||
}
|
||||
}
|
||||
+5
@@ -26,11 +26,16 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
||||
?: throw IllegalArgumentException("No manifest found in APK")
|
||||
|
||||
container.log("Patching manifest")
|
||||
val wazeDefault = container.getStep<SmaliPatchStep>().specs
|
||||
.firstOrNull { it.id == "WazeIntegration" }?.defaultEnabled ?: false
|
||||
val enableWazeIntegration = options.patchStates["WazeIntegration"] ?: wazeDefault
|
||||
|
||||
val patchedManifest = ManifestPatcher.patchManifest(
|
||||
manifestBytes = manifest,
|
||||
packageName = options.packageName,
|
||||
appName = options.appName,
|
||||
debuggable = options.debuggable,
|
||||
enableWazeIntegration = enableWazeIntegration,
|
||||
)
|
||||
|
||||
container.log("Repacking apk with patched manifest")
|
||||
|
||||
+20
-2
@@ -1,5 +1,6 @@
|
||||
package com.meowarex.rlmobile.patcher.steps.patch
|
||||
|
||||
import android.content.Context
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.manager.PathManager
|
||||
import com.meowarex.rlmobile.patcher.StepRunner
|
||||
@@ -10,6 +11,7 @@ import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
|
||||
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchManifest
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs
|
||||
import com.android.tools.smali.baksmali.Baksmali
|
||||
import com.android.tools.smali.baksmali.BaksmaliOptions
|
||||
@@ -31,6 +33,10 @@ class SmaliPatchStep(
|
||||
) : Step(), IDexProvider, KoinComponent {
|
||||
private val paths: PathManager by inject()
|
||||
private val json: Json by inject()
|
||||
private val context: Context by inject()
|
||||
|
||||
var specs: List<PatchSpec> = emptyList()
|
||||
private set
|
||||
|
||||
override val group = StepGroup.Patch
|
||||
override val localizedName = R.string.patch_step_patch_smali
|
||||
@@ -55,7 +61,7 @@ class SmaliPatchStep(
|
||||
container.log("Failed to parse manifest.json (${t.message}); using built-in patch specs")
|
||||
null
|
||||
}
|
||||
val specs = manifestSpecs ?: builtinPatchSpecs { "" }
|
||||
specs = manifestSpecs ?: builtinPatchSpecs(context, json)
|
||||
container.log(
|
||||
"Loaded ${specs.size} patch specs from " +
|
||||
if (manifestSpecs != null) "manifest.json" else "built-in list"
|
||||
@@ -95,7 +101,19 @@ class SmaliPatchStep(
|
||||
val entry = zip.openEntry(patchFile)
|
||||
?: throw FileNotFoundException("Missing zip entry: $patchFile")
|
||||
out.canonicalFile.parentFile?.mkdirs()
|
||||
out.writeBytes(entry.read())
|
||||
var smaliText = entry.read()
|
||||
.decodeToString()
|
||||
.replace("\r\n", "\n")
|
||||
for ((token, value) in substitutions) {
|
||||
if (smaliText.contains(token)) {
|
||||
smaliText = smaliText.replace(token, value)
|
||||
container.log("Applied substitution $token -> $value in $patchFile")
|
||||
}
|
||||
}
|
||||
UNRESOLVED_TOKEN.find(smaliText)?.let { match ->
|
||||
throw Error("Unresolved option placeholder ${match.value} in $patchFile")
|
||||
}
|
||||
out.writeText(smaliText)
|
||||
container.log("Extracted extension smali: $relative")
|
||||
continue
|
||||
}
|
||||
|
||||
+45
-2
@@ -1,19 +1,30 @@
|
||||
package com.meowarex.rlmobile.patcher.steps.prepare
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.Stable
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.manager.CachedReleaseInfo
|
||||
import com.meowarex.rlmobile.manager.PathManager
|
||||
import com.meowarex.rlmobile.manager.ReleaseInfoCache
|
||||
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
||||
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
||||
import com.meowarex.rlmobile.network.utils.getOrThrow
|
||||
import com.meowarex.rlmobile.patcher.StepRunner
|
||||
import com.meowarex.rlmobile.patcher.steps.StepGroup
|
||||
import com.meowarex.rlmobile.patcher.steps.base.Step
|
||||
import com.meowarex.rlmobile.util.isOnline
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
@Stable
|
||||
class FetchInfoStep : Step(), KoinComponent {
|
||||
private val github: RadiantLyricsGithubService by inject()
|
||||
private val context: Application by inject()
|
||||
private val paths: PathManager by inject()
|
||||
private val json: Json by inject()
|
||||
|
||||
override val group = StepGroup.Prepare
|
||||
override val localizedName = R.string.patch_step_fetch_info
|
||||
@@ -25,6 +36,33 @@ class FetchInfoStep : Step(), KoinComponent {
|
||||
private set
|
||||
|
||||
override suspend fun execute(container: StepRunner) {
|
||||
// Fetch the latest release and refresh the cache (when online)
|
||||
if (context.isOnline()) {
|
||||
try {
|
||||
fetchFromRemote(container)
|
||||
return
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (t: Throwable) {
|
||||
container.log("Failed to fetch latest release: ${Log.getStackTraceString(t)}")
|
||||
container.log("Falling back to cached release info for a local re-patch")
|
||||
}
|
||||
} else {
|
||||
container.log("No network connection — using cached release info for a local re-patch")
|
||||
}
|
||||
|
||||
val cached = ReleaseInfoCache.load(paths, json)
|
||||
?: throw IllegalStateException(
|
||||
"Could not fetch the latest release and no cached release info is available. " +
|
||||
"Connect to the internet and try again."
|
||||
)
|
||||
data = cached.data
|
||||
patchesAssetUrl = cached.patchesAssetUrl
|
||||
container.log("Using cached build info: $data")
|
||||
container.log("Cached patches asset URL: $patchesAssetUrl")
|
||||
}
|
||||
|
||||
private suspend fun fetchFromRemote(container: StepRunner) {
|
||||
container.log("Fetching latest release from ${RadiantLyricsGithubService.REPO_OWNER}/${RadiantLyricsGithubService.REPO_NAME}")
|
||||
val release = github.getLatestRelease(force = true).getOrThrow()
|
||||
|
||||
@@ -33,14 +71,19 @@ class FetchInfoStep : Step(), KoinComponent {
|
||||
?.browserDownloadUrl
|
||||
?: throw IllegalStateException("No ${RadiantLyricsGithubService.DATA_JSON_ASSET_NAME} asset found in latest release ${release.tagName}")
|
||||
|
||||
patchesAssetUrl = release.assets
|
||||
val patchesUrl = release.assets
|
||||
.find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME }
|
||||
?.browserDownloadUrl
|
||||
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
|
||||
|
||||
container.log("Fetching build info from $dataJsonUrl")
|
||||
data = github.getBuildInfo(dataJsonUrl, force = true).getOrThrow()
|
||||
val buildInfo = github.getBuildInfo(dataJsonUrl, force = true).getOrThrow()
|
||||
|
||||
data = buildInfo
|
||||
patchesAssetUrl = patchesUrl
|
||||
container.log("Fetched build info: $data")
|
||||
container.log("Patches asset URL: $patchesAssetUrl")
|
||||
|
||||
ReleaseInfoCache.save(paths, json, CachedReleaseInfo(buildInfo, patchesUrl))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,12 +18,16 @@ object ManifestPatcher {
|
||||
private const val IS_SPLIT_REQUIRED = "isSplitRequired"
|
||||
private const val REQUIRED_SPLIT_TYPES = "requiredSplitTypes"
|
||||
private const val SPLIT_TYPES = "splitTypes"
|
||||
private const val WAZE_PACKAGE = "com.waze"
|
||||
private const val WAZE_INIT_RECEIVER = "radiant.WazeInitReceiver"
|
||||
private const val WAZE_ACTION_INIT = "com.waze.sdk.audio.ACTION_INIT"
|
||||
|
||||
fun patchManifest(
|
||||
manifestBytes: ByteArray,
|
||||
packageName: String,
|
||||
appName: String,
|
||||
debuggable: Boolean,
|
||||
enableWazeIntegration: Boolean,
|
||||
): ByteArray {
|
||||
// Extract original package name to rewrite every reference to it
|
||||
// (permissions, provider authorities) to the new packageName.
|
||||
@@ -84,6 +88,49 @@ object ManifestPatcher {
|
||||
}
|
||||
|
||||
return when (name) {
|
||||
"queries" -> object : NodeVisitor(nv) {
|
||||
private var hasWazePackage = false
|
||||
|
||||
override fun child(ns: String?, name: String): NodeVisitor {
|
||||
val visitor = super.child(ns, name)
|
||||
|
||||
return if (enableWazeIntegration && name == "package") {
|
||||
object : NodeVisitor(visitor) {
|
||||
override fun attr(
|
||||
ns: String?,
|
||||
name: String?,
|
||||
resourceId: Int,
|
||||
type: Int,
|
||||
value: Any?,
|
||||
) {
|
||||
if (name == "name" && value == WAZE_PACKAGE) {
|
||||
hasWazePackage = true
|
||||
}
|
||||
super.attr(ns, name, resourceId, type, value)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
visitor
|
||||
}
|
||||
}
|
||||
|
||||
override fun end() {
|
||||
if (enableWazeIntegration && !hasWazePackage) {
|
||||
super.child(null, "package").apply {
|
||||
attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"name",
|
||||
android.R.attr.name,
|
||||
TYPE_STRING,
|
||||
WAZE_PACKAGE,
|
||||
)
|
||||
end()
|
||||
}
|
||||
}
|
||||
super.end()
|
||||
}
|
||||
}
|
||||
|
||||
"uses-permission" -> object : NodeVisitor(nv) {
|
||||
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) {
|
||||
if (name != "maxSdkVersion") {
|
||||
@@ -132,6 +179,7 @@ object ManifestPatcher {
|
||||
private var addUseEmbeddedDex = true
|
||||
private var addExtractNativeLibs = true
|
||||
private var addMetadata = true
|
||||
private var hasWazeInitReceiver = false
|
||||
|
||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||
if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false
|
||||
@@ -161,6 +209,22 @@ object ManifestPatcher {
|
||||
|
||||
return when (name) {
|
||||
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName))
|
||||
|
||||
"receiver" -> object : NodeVisitor(visitor) {
|
||||
override fun attr(
|
||||
ns: String?,
|
||||
name: String,
|
||||
resourceId: Int,
|
||||
type: Int,
|
||||
value: Any?
|
||||
) {
|
||||
if (name == "name" && value == WAZE_INIT_RECEIVER) {
|
||||
hasWazeInitReceiver = true
|
||||
}
|
||||
super.attr(ns, name, resourceId, type, value)
|
||||
}
|
||||
}
|
||||
|
||||
"provider" -> object : NodeVisitor(visitor) {
|
||||
override fun attr(
|
||||
ns: String?,
|
||||
@@ -219,6 +283,39 @@ object ManifestPatcher {
|
||||
0
|
||||
)
|
||||
|
||||
if (enableWazeIntegration && !hasWazeInitReceiver) {
|
||||
super.child(null, "receiver").apply {
|
||||
attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"name",
|
||||
android.R.attr.name,
|
||||
TYPE_STRING,
|
||||
WAZE_INIT_RECEIVER,
|
||||
)
|
||||
attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"exported",
|
||||
android.R.attr.exported,
|
||||
TYPE_INT_BOOLEAN,
|
||||
1,
|
||||
)
|
||||
child(null, "intent-filter").apply {
|
||||
child(null, "action").apply {
|
||||
attr(
|
||||
ANDROID_NAMESPACE,
|
||||
"name",
|
||||
android.R.attr.name,
|
||||
TYPE_STRING,
|
||||
WAZE_ACTION_INIT,
|
||||
)
|
||||
end()
|
||||
}
|
||||
end()
|
||||
}
|
||||
end()
|
||||
}
|
||||
}
|
||||
|
||||
super.end()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
)
|
||||
}
|
||||
|
||||
+31
-35
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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") },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+9
-14
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+14
-39
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
+41
-54
@@ -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() {
|
||||
|
||||
+321
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+181
@@ -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,
|
||||
)
|
||||
}
|
||||
+306
@@ -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<Float> = 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<String>,
|
||||
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<Float> = 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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+245
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+58
@@ -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,
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -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),
|
||||
)
|
||||
}
|
||||
|
||||
+58
-22
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
+6
-4
@@ -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) }
|
||||
|
||||
+13
-6
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
+74
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+115
@@ -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<Float>,
|
||||
brush: Brush,
|
||||
thumbColor: Color = MaterialTheme.colorScheme.primary,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val interactionSource = remember(::MutableInteractionSource)
|
||||
|
||||
Slider(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
thumb = {
|
||||
val interactions = remember { mutableStateListOf<Interaction>() }
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
+51
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+39
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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.*
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
+32
@@ -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,
|
||||
)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+64
@@ -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
|
||||
)
|
||||
}
|
||||
+105
@@ -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)
|
||||
}
|
||||
+19
@@ -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)
|
||||
)
|
||||
}
|
||||
+59
@@ -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()
|
||||
}
|
||||
}
|
||||
+37
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+31
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
+114
@@ -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<AboutModel>()
|
||||
|
||||
AboutScreenContent(state = model.state.collectAsState())
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AboutScreenContent(state: State<AboutScreenState>) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+77
@@ -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)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+365
@@ -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<PatchComponent?>(), Parcelable {
|
||||
@IgnoredOnParcel
|
||||
override val key = "Legacy-ComponentOptions-$componentType"
|
||||
|
||||
@Composable
|
||||
override fun Content() {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val model = koinScreenModel<ComponentOptionsModel> { 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<PatchComponent>,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -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),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+118
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+278
@@ -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<HomeModel>()
|
||||
val activity = LocalContext.current as ComponentActivity
|
||||
val updater = koinViewModel<UpdaterViewModel>(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<androidx.paging.PagingData<com.meowarex.rlmobile.network.models.GithubCommit>>,
|
||||
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())
|
||||
}
|
||||
}
|
||||
+118
@@ -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<GithubCommit>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<LogScreenModel> { 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
@@ -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),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+45
@@ -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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
+304
@@ -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<PatchingScreenModel> { 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?>(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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+53
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+27
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+118
@@ -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<Step>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+91
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+61
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+313
@@ -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<PatchOptionsModel> { 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<PatchSpec>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+91
@@ -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<String>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+685
@@ -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<OptionLock?>(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<OptionSpec.Toggle>()
|
||||
.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<OptionSpec.Choice>()
|
||||
.filter { it.requiresOption == option.key }
|
||||
val gatedToggles = patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
|
||||
.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<Float>,
|
||||
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<Float>,
|
||||
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<String>,
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+33
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+433
@@ -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<PatchSpec>,
|
||||
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<LockInfo?>(null) }
|
||||
var advancedFor by remember { mutableStateOf<PatchSpec?>(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<OptionSpec.Toggle>()
|
||||
.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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+47
@@ -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<EffectiveVariant>,
|
||||
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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+52
@@ -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()
|
||||
}
|
||||
}
|
||||
+52
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
+84
@@ -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)
|
||||
}
|
||||
}
|
||||
+224
@@ -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<PermissionsModel>()
|
||||
|
||||
// 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+80
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+48
@@ -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)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
+251
@@ -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<SettingsModel>()
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+183
@@ -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<ShizukuManager>()
|
||||
val dhizuku = koinInject<DhizukuManager>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
+92
@@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+93
@@ -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))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
+135
@@ -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<VersionDelta>,
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+105
@@ -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,
|
||||
),
|
||||
)
|
||||
}
|
||||
+7
-2
@@ -9,6 +9,7 @@ import com.meowarex.rlmobile.network.utils.SemVer
|
||||
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
||||
import com.meowarex.rlmobile.ui.screens.patchopts.*
|
||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlin.time.Clock
|
||||
|
||||
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
|
||||
@@ -21,7 +22,7 @@ private fun PatchOptionsScreenPreview(
|
||||
parameters: PatchOptionsParameters,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val specs = remember { builtinPatchSpecs { context.getString(it) } }
|
||||
val specs = remember { builtinPatchSpecs(context, Json { ignoreUnknownKeys = true }) }
|
||||
|
||||
ManagerTheme {
|
||||
PatchOptionsScreenContent(
|
||||
@@ -29,18 +30,22 @@ private fun PatchOptionsScreenPreview(
|
||||
isDevMode = parameters.isDevMode,
|
||||
debuggable = parameters.debuggable,
|
||||
setDebuggable = {},
|
||||
bypassIncompatible = false,
|
||||
setBypassIncompatible = {},
|
||||
appName = parameters.appName,
|
||||
appNameIsError = parameters.appNameIsError,
|
||||
setAppName = {},
|
||||
packageName = parameters.packageName,
|
||||
packageNameState = parameters.packageNameState,
|
||||
setPackageName = {},
|
||||
packageNameLocked = parameters.packageName == PatchOptions.Default.packageName,
|
||||
onUnlockPackageName = {},
|
||||
onLockPackageName = {},
|
||||
customTidalApk = parameters.customTidalApk,
|
||||
onSelectCustomTidalApk = {},
|
||||
customPatches = parameters.customPatches,
|
||||
onSelectCustomPatches = {},
|
||||
specs = specs,
|
||||
enabledPatchCount = specs.size,
|
||||
isPatchEnabled = { true },
|
||||
onTogglePatch = { _, _ -> },
|
||||
patchLockState = { PatchLock.Free },
|
||||
|
||||
@@ -85,7 +85,7 @@ fun AboutScreenContent(state: State<AboutScreenState>) {
|
||||
AboutScreenState.Loading -> item(key = "CONTRIBUTIONS_LOADING") {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
content = { CircularProgressIndicator() },
|
||||
content = { LoadingIndicator() },
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 38.dp),
|
||||
|
||||
+16
-16
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+1
-3
@@ -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),
|
||||
|
||||
+9
-9
@@ -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,
|
||||
|
||||
@@ -19,7 +19,9 @@ import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import com.github.diamondminer88.zip.ZipReader
|
||||
import com.meowarex.rlmobile.BuildConfig
|
||||
import com.meowarex.rlmobile.R
|
||||
import com.meowarex.rlmobile.manager.PathManager
|
||||
import com.meowarex.rlmobile.manager.PreferencesManager
|
||||
import com.meowarex.rlmobile.manager.ReleaseInfoCache
|
||||
import com.meowarex.rlmobile.network.models.GithubCommit
|
||||
import com.meowarex.rlmobile.network.models.RLBuildInfo
|
||||
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
||||
@@ -42,6 +44,7 @@ class HomeModel(
|
||||
private val github: RadiantLyricsGithubService,
|
||||
private val json: Json,
|
||||
private val prefs: PreferencesManager,
|
||||
private val paths: PathManager,
|
||||
) : ScreenModel {
|
||||
|
||||
var state by mutableStateOf<HomeState>(HomeState.Loading)
|
||||
@@ -170,16 +173,23 @@ class HomeModel(
|
||||
|
||||
refreshingLock.withLock {
|
||||
val pkg = fetchInstalled()
|
||||
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
|
||||
remote.await()
|
||||
// Skip the remote fetch entirely when offline
|
||||
val online = application.isOnline()
|
||||
if (online) {
|
||||
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
|
||||
remote.await()
|
||||
}
|
||||
|
||||
val install = pkg?.toInstallData()
|
||||
val latest = remoteDataJson?.tidalVersionCode
|
||||
val offlineRepatchReady = hasOfflineRepatchAssets()
|
||||
|
||||
mainThread {
|
||||
state = HomeState.Loaded(
|
||||
install = install,
|
||||
latestTidalVersionCode = latest,
|
||||
offline = !online,
|
||||
offlineRepatchReady = offlineRepatchReady,
|
||||
)
|
||||
maybeCheckManagerUpdate(pkg)
|
||||
}
|
||||
@@ -201,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) {
|
||||
@@ -218,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)
|
||||
@@ -257,6 +276,13 @@ class HomeModel(
|
||||
)
|
||||
}
|
||||
|
||||
// check if an offline "Local Repatch" is possible
|
||||
private fun hasOfflineRepatchAssets(): Boolean {
|
||||
val info = ReleaseInfoCache.load(paths, json) ?: return false
|
||||
return paths.hasCachedTidalApk(info.data.tidalVersionCode) &&
|
||||
paths.hasCachedSmaliPatches(info.data.patchesVersion)
|
||||
}
|
||||
|
||||
private suspend fun fetchRemoteData() {
|
||||
val release = try {
|
||||
github.getLatestRelease().fold(
|
||||
|
||||
@@ -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,96 +259,108 @@ private fun ColumnScope.HomeContent(
|
||||
} else null
|
||||
|
||||
val iconPainter = install?.icon ?: fallbackPainter
|
||||
if (iconPainter != null) {
|
||||
Image(
|
||||
painter = iconPainter,
|
||||
contentDescription = null,
|
||||
|
||||
val hero = radiantStyle.heroCard
|
||||
|
||||
// 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
|
||||
.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)
|
||||
Button(
|
||||
onClick = if (install == null) onInstall else onRepatch,
|
||||
enabled = state.latestTidalVersionCode != null && !blockedByManagerUpdate,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
val label = when {
|
||||
blockedByManagerUpdate -> "Manager Update Required"
|
||||
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(),
|
||||
)
|
||||
}
|
||||
|
||||
AnimatedVisibility(visible = install != null) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier.clip(RoundedCornerShape(16.dp)),
|
||||
.fillMaxWidth()
|
||||
.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()),
|
||||
)
|
||||
}
|
||||
|
||||
ElevatedCard(modifier = Modifier.fillMaxSize()) {
|
||||
CommitList(commits = commits.collectAsLazyPagingItems())
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,5 +8,7 @@ sealed interface HomeState {
|
||||
data class Loaded(
|
||||
val install: InstallData?,
|
||||
val latestTidalVersionCode: Int?,
|
||||
val offline: Boolean = false,
|
||||
val offlineRepatchReady: Boolean = false,
|
||||
) : HomeState
|
||||
}
|
||||
|
||||
+7
-1
@@ -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<GithubCommit>) {
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -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,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
+13
-7
@@ -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()
|
||||
|
||||
+4
-1
@@ -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
|
||||
|
||||
+96
@@ -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<StepGroup, ImmutableList<Step>>?,
|
||||
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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-6
@@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
+8
-2
@@ -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)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user