mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-27 06:27:46 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b175890e9d | ||
|
|
4b1e9b5480 | ||
|
|
4531e6e259 | ||
|
|
526e424621 | ||
|
|
a7fa8f7d30 | ||
|
|
625d012e17 | ||
|
|
2cb0e1a67e | ||
|
|
b1f50d4098 | ||
|
|
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 | ||
|
|
bafd40a20c | ||
|
|
c89827c7b2 | ||
|
|
90b49cc571 | ||
|
|
8e86c0d2e1 | ||
|
|
8ac9fb0db5 | ||
|
|
6ad6cb2fe0 | ||
|
|
a9611a1667 | ||
|
|
276dd0be2e | ||
|
|
3758ba19f3 | ||
|
|
75450aad60 | ||
|
|
3788eb2008 | ||
|
|
95be099821 | ||
|
|
7a55787894 | ||
|
|
bd09c2798e | ||
|
|
b0b614dc6f | ||
|
|
6a7d5fe90f | ||
|
|
4791e69bec | ||
|
|
7f3618aeae | ||
|
|
ea9c6458c2 | ||
|
|
bdbcc4fc62 | ||
|
|
3b893c12ff | ||
|
|
9d60e3dc80 | ||
|
|
36382437f1 | ||
|
|
5dc9188772 | ||
|
|
7823a54d11 | ||
|
|
4d5fea843e | ||
|
|
d6bc889387 | ||
|
|
8501ccb0a9 | ||
|
|
ad6ebac6ce | ||
|
|
320148e774 | ||
|
|
d8e119eb4b | ||
|
|
10e5dcf365 | ||
|
|
1c65c5669a | ||
|
|
961291166c | ||
|
|
25852aaea1 | ||
|
|
c6b18d1aa9 | ||
|
|
ee8b25330d | ||
|
|
193d5ad90d | ||
|
|
942742e86e | ||
|
|
0b039110c9 | ||
|
|
045bbbab9a | ||
|
|
932f2a85dc | ||
|
|
fc002615f4 | ||
|
|
c89dd54fa6 | ||
|
|
53a4341d6b | ||
|
|
c81d2edba3 | ||
|
|
9180129afc | ||
|
|
d9de3207f5 | ||
|
|
59fe232ae4 | ||
|
|
e105a3eb35 | ||
|
|
e7b69a8deb | ||
|
|
324a6eb6c8 | ||
|
|
c5962ad1a8 | ||
|
|
77ab041b97 |
@@ -16,8 +16,12 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- id: set
|
- id: set
|
||||||
run: |
|
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:]')
|
BASE=$(cat .version | tr -d '[:space:]')
|
||||||
echo "version=$BASE.${{ github.run_number }}" >> $GITHUB_OUTPUT
|
echo "version=$BASE" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
Build:
|
Build:
|
||||||
needs: Version
|
needs: Version
|
||||||
@@ -48,6 +52,8 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
mv ./dist/app-release.apk ./dist/rl-manager.apk
|
mv ./dist/app-release.apk ./dist/rl-manager.apk
|
||||||
cp patches/data.json ./dist/data.json
|
cp patches/data.json ./dist/data.json
|
||||||
|
# Point each release's data.json at its own assets so historical releases stay self-contained.
|
||||||
|
sed -i "s|releases/download/latest/|releases/download/v${{ needs.Version.outputs.version }}/|g" ./dist/data.json
|
||||||
cd patches && zip -r ../dist/patches.zip . -x "data.json" && cd ..
|
cd patches && zip -r ../dist/patches.zip . -x "data.json" && cd ..
|
||||||
|
|
||||||
tidal_src=$(find tidal-apk -maxdepth 1 \( -name "*.apk" -o -name "*.apkm" \) | head -1)
|
tidal_src=$(find tidal-apk -maxdepth 1 \( -name "*.apk" -o -name "*.apkm" \) | head -1)
|
||||||
@@ -58,7 +64,7 @@ jobs:
|
|||||||
if [[ "$tidal_src" == *.apkm ]]; then
|
if [[ "$tidal_src" == *.apkm ]]; then
|
||||||
echo "Merging splits from $tidal_src via APKEditor"
|
echo "Merging splits from $tidal_src via APKEditor"
|
||||||
curl --fail --location --retry 3 --retry-delay 2 -sSo /tmp/APKEditor.jar \
|
curl --fail --location --retry 3 --retry-delay 2 -sSo /tmp/APKEditor.jar \
|
||||||
https://github.com/REAndroid/APKEditor/releases/download/V1.4.3/APKEditor-1.4.3.jar
|
https://github.com/REAndroid/APKEditor/releases/download/V1.4.9/APKEditor-1.4.9.jar
|
||||||
java -jar /tmp/APKEditor.jar m -i "$tidal_src" -o ./dist/tidal-stock.apk
|
java -jar /tmp/APKEditor.jar m -i "$tidal_src" -o ./dist/tidal-stock.apk
|
||||||
echo "Merged tidal-stock.apk:"
|
echo "Merged tidal-stock.apk:"
|
||||||
ls -la ./dist/tidal-stock.apk
|
ls -la ./dist/tidal-stock.apk
|
||||||
@@ -67,10 +73,10 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish release
|
- name: Publish release
|
||||||
uses: marvinpinto/action-automatic-releases@latest
|
uses: softprops/action-gh-release@v2
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
tag_name: v${{ needs.Version.outputs.version }}
|
||||||
automatic_release_tag: latest
|
name: v${{ needs.Version.outputs.version }}
|
||||||
prerelease: false
|
prerelease: false
|
||||||
title: v${{ needs.Version.outputs.version }}
|
make_latest: "true"
|
||||||
files: ./dist/**
|
files: ./dist/**
|
||||||
|
|||||||
@@ -27,3 +27,6 @@ Reference/
|
|||||||
!tidal-apk/*.apk
|
!tidal-apk/*.apk
|
||||||
!tidal-apk/*.apkm
|
!tidal-apk/*.apkm
|
||||||
tidal-apk/*/
|
tidal-apk/*/
|
||||||
|
|
||||||
|
# Kawarp-AGSL library (CLONE HERE! the build tooling in tools/kawarp-build expects it)
|
||||||
|
tools/kawarp-agsl
|
||||||
|
|||||||
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -23,13 +23,16 @@ val gitHasHasLocalChanges = providers.execIgnoreCode("git", "status", "-s").isNo
|
|||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.meowarex.rlmobile"
|
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 {
|
defaultConfig {
|
||||||
minSdk = 24
|
minSdk = 24
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 10_03_00
|
versionCode = 62
|
||||||
versionName = "1.3.0"
|
versionName = "1.7.7"
|
||||||
|
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
@@ -161,6 +164,7 @@ kotlin {
|
|||||||
"androidx.compose.foundation.ExperimentalFoundationApi",
|
"androidx.compose.foundation.ExperimentalFoundationApi",
|
||||||
"androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
"androidx.compose.foundation.layout.ExperimentalLayoutApi",
|
||||||
"androidx.compose.material3.ExperimentalMaterial3Api",
|
"androidx.compose.material3.ExperimentalMaterial3Api",
|
||||||
|
"androidx.compose.material3.ExperimentalMaterial3ExpressiveApi",
|
||||||
"kotlin.time.ExperimentalTime",
|
"kotlin.time.ExperimentalTime",
|
||||||
"kotlinx.serialization.ExperimentalSerializationApi",
|
"kotlinx.serialization.ExperimentalSerializationApi",
|
||||||
)
|
)
|
||||||
@@ -228,3 +232,13 @@ fun ProviderFactory.execIgnoreCode(vararg command: String): String = try {
|
|||||||
} catch (_: Exception) {
|
} 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,658 @@
|
|||||||
|
{
|
||||||
|
"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": [
|
||||||
|
"LyricsRlApi",
|
||||||
|
"PlayerBackdrop",
|
||||||
|
"QualityBadgeColors",
|
||||||
|
"PlayerMovePlayingFrom",
|
||||||
|
"PlayerOneHanded",
|
||||||
|
"LyricsProgressPill",
|
||||||
|
"LyricsDisableCover",
|
||||||
|
"LyricsReplaceLyricsButton",
|
||||||
|
"LyricsReplaceShareButton",
|
||||||
|
"LyricsKeepControlsVisible"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "Swap the player backdrop for the Kawarp shader, or fine-tune TIDAL's own blurred cover",
|
||||||
|
"default": true,
|
||||||
|
"defaultVariantIndex": 1,
|
||||||
|
"variants": [
|
||||||
|
{
|
||||||
|
"title": "Tidal",
|
||||||
|
"fileNames": [
|
||||||
|
"player-backdrop-playback.patch"
|
||||||
|
],
|
||||||
|
"extensionFiles": [
|
||||||
|
"radiant/BackdropFx.smali"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Radiant",
|
||||||
|
"fileNames": [
|
||||||
|
"player-backdrop-kawarp.patch"
|
||||||
|
],
|
||||||
|
"extensionFiles": [
|
||||||
|
"radiant/Kawarp.smali",
|
||||||
|
"dev/kawarp/KawarpEngine.smali"
|
||||||
|
],
|
||||||
|
"minSdk": 33
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"requiresVariant": 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
|
||||||
|
},
|
||||||
|
"requiresVariant": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"requiresVariant": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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
|
||||||
|
},
|
||||||
|
"requiresVariant": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "toggle",
|
||||||
|
"key": "kawarp_reactive",
|
||||||
|
"title": "Playback Reactive",
|
||||||
|
"description": "Cover shader reacts to playback state by freezing/resuming",
|
||||||
|
"default": true,
|
||||||
|
"token": "RL_KW_REACTIVE",
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_blur",
|
||||||
|
"title": "Blur Passes",
|
||||||
|
"description": "Kawase blur passes, higher is softer but costs more GPU",
|
||||||
|
"default": 5,
|
||||||
|
"min": 1,
|
||||||
|
"max": 40,
|
||||||
|
"steps": 38,
|
||||||
|
"displayAsPercent": false,
|
||||||
|
"token": "RL_KW_BLUR",
|
||||||
|
"encode": {
|
||||||
|
"kind": "int",
|
||||||
|
"scale": 1.0
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_warp",
|
||||||
|
"title": "Warp Intensity",
|
||||||
|
"description": "Strength of the fluidity effect",
|
||||||
|
"default": 100,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"steps": 19,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_WARP",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_speed",
|
||||||
|
"title": "Animation Speed",
|
||||||
|
"description": "How fast the backdrop flows",
|
||||||
|
"default": 175,
|
||||||
|
"min": 0,
|
||||||
|
"max": 500,
|
||||||
|
"steps": 19,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_SPEED",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_scale",
|
||||||
|
"title": "Backdrop Scale",
|
||||||
|
"description": "Zoom level of the effect",
|
||||||
|
"default": 100,
|
||||||
|
"min": 10,
|
||||||
|
"max": 400,
|
||||||
|
"steps": 12,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_SCALE",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_contrast",
|
||||||
|
"title": "Contrast",
|
||||||
|
"description": "Contrast of the backdrop",
|
||||||
|
"default": 125,
|
||||||
|
"min": 0,
|
||||||
|
"max": 300,
|
||||||
|
"steps": 11,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_CONTRAST",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_saturation",
|
||||||
|
"title": "Saturation",
|
||||||
|
"description": "Colour intensity of the backdrop",
|
||||||
|
"default": 125,
|
||||||
|
"min": 0,
|
||||||
|
"max": 400,
|
||||||
|
"steps": 15,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_SAT",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_brightness",
|
||||||
|
"title": "Brightness",
|
||||||
|
"description": "The value of Brightness applied to the backdrop",
|
||||||
|
"default": 100,
|
||||||
|
"min": 0,
|
||||||
|
"max": 200,
|
||||||
|
"steps": 19,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_BRIGHT",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_darken",
|
||||||
|
"title": "Auto Darken Bright Covers",
|
||||||
|
"description": "Prevents bright covers from making text unreadable",
|
||||||
|
"default": 80,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"steps": 19,
|
||||||
|
"displayAsPercent": true,
|
||||||
|
"token": "RL_KW_DARKEN",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.01
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "slider",
|
||||||
|
"key": "kawarp_dither",
|
||||||
|
"title": "Dithering",
|
||||||
|
"description": "Breaks up color banding in smooth gradients",
|
||||||
|
"default": 15,
|
||||||
|
"min": 0,
|
||||||
|
"max": 100,
|
||||||
|
"steps": 19,
|
||||||
|
"displayAsPercent": false,
|
||||||
|
"token": "RL_KW_DITHER",
|
||||||
|
"encode": {
|
||||||
|
"kind": "floatBits",
|
||||||
|
"scale": 0.001
|
||||||
|
},
|
||||||
|
"requiresVariant": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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.manager.*
|
||||||
import com.meowarex.rlmobile.patcher.InstallMetadata
|
import com.meowarex.rlmobile.patcher.InstallMetadata
|
||||||
import com.meowarex.rlmobile.ui.screens.home.HomeScreen
|
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.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.patchopts.PatchOptions
|
||||||
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
|
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
|
||||||
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsScreen
|
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.ManagerTheme
|
||||||
|
import com.meowarex.rlmobile.ui.theme.UiStyle
|
||||||
import com.meowarex.rlmobile.ui.widgets.updater.UpdaterDialog
|
import com.meowarex.rlmobile.ui.widgets.updater.UpdaterDialog
|
||||||
import com.meowarex.rlmobile.util.*
|
import com.meowarex.rlmobile.util.*
|
||||||
import com.github.diamondminer88.zip.ZipReader
|
import com.github.diamondminer88.zip.ZipReader
|
||||||
@@ -63,6 +67,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
ManagerTheme(
|
ManagerTheme(
|
||||||
theme = preferences.theme,
|
theme = preferences.theme,
|
||||||
|
uiStyle = preferences.uiStyle,
|
||||||
dynamicColor = preferences.dynamicColor,
|
dynamicColor = preferences.dynamicColor,
|
||||||
) {
|
) {
|
||||||
if (BuildConfig.RELEASE) {
|
if (BuildConfig.RELEASE) {
|
||||||
@@ -73,14 +78,20 @@ class MainActivity : ComponentActivity() {
|
|||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalNavigatorSaver provides parcelableNavigatorSaver(),
|
LocalNavigatorSaver provides parcelableNavigatorSaver(),
|
||||||
) {
|
) {
|
||||||
|
val legacyUi = preferences.uiStyle == UiStyle.Legacy
|
||||||
|
|
||||||
|
key(legacyUi) {
|
||||||
Navigator(
|
Navigator(
|
||||||
screen = HomeScreen(),
|
screen = if (legacyUi) LegacyHomeScreen() else HomeScreen(),
|
||||||
onBackPressed = null,
|
onBackPressed = null,
|
||||||
) { navigator ->
|
) { navigator ->
|
||||||
// Open the permissions screen whenever permissions are insufficient
|
// Open the permissions screen whenever permissions are insufficient
|
||||||
LaunchedEffect(permissions.requiredPermsGranted) {
|
LaunchedEffect(permissions.requiredPermsGranted) {
|
||||||
if (!permissions.requiredPermsGranted)
|
if (!permissions.requiredPermsGranted) {
|
||||||
navigator.pushOnce(PermissionsScreen())
|
navigator.pushOnce(
|
||||||
|
if (legacyUi) LegacyPermissionsScreen() else PermissionsScreen()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
@@ -104,6 +115,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
overlays.Overlays()
|
overlays.Overlays()
|
||||||
}
|
}
|
||||||
@@ -142,7 +154,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val targetDir = when (componentType) {
|
val targetDir = when (componentType) {
|
||||||
"injector" -> paths.customInjectorsDir
|
"tidal" -> paths.customTidalApksDir
|
||||||
"patches" -> paths.customPatchesDir
|
"patches" -> paths.customPatchesDir
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(BuildConfig.TAG, "Extra $EXTRA_COMPONENT_TYPE is not a valid value!")
|
Log.w(BuildConfig.TAG, "Extra $EXTRA_COMPONENT_TYPE is not a valid value!")
|
||||||
@@ -184,7 +196,11 @@ class MainActivity : ComponentActivity() {
|
|||||||
val patchOptions = metadata?.options
|
val patchOptions = metadata?.options
|
||||||
?: PatchOptions.Default.copy(packageName = packageName)
|
?: PatchOptions.Default.copy(packageName = packageName)
|
||||||
|
|
||||||
return PatchingScreen(patchOptions)
|
return if (preferences.uiStyle == UiStyle.Legacy) {
|
||||||
|
LegacyPatchingScreen(patchOptions)
|
||||||
|
} else {
|
||||||
|
PatchingScreen(patchOptions)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import com.meowarex.rlmobile.ui.screens.about.AboutModel
|
|||||||
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsModel
|
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsModel
|
||||||
import com.meowarex.rlmobile.ui.screens.home.HomeModel
|
import com.meowarex.rlmobile.ui.screens.home.HomeModel
|
||||||
import com.meowarex.rlmobile.ui.screens.log.LogScreenModel
|
import com.meowarex.rlmobile.ui.screens.log.LogScreenModel
|
||||||
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreenModel
|
|
||||||
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreenModel
|
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreenModel
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsModel
|
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsModel
|
||||||
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
|
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
|
||||||
@@ -64,7 +63,6 @@ class ManagerApplication : Application() {
|
|||||||
factoryOf(::PatchOptionsModel)
|
factoryOf(::PatchOptionsModel)
|
||||||
factoryOf(::ComponentOptionsModel)
|
factoryOf(::ComponentOptionsModel)
|
||||||
factoryOf(::LogScreenModel)
|
factoryOf(::LogScreenModel)
|
||||||
factoryOf(::LogsListScreenModel)
|
|
||||||
factoryOf(::PermissionsModel)
|
factoryOf(::PermissionsModel)
|
||||||
viewModelOf(::UpdaterViewModel)
|
viewModelOf(::UpdaterViewModel)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
|
|||||||
import com.meowarex.rlmobile.util.*
|
import com.meowarex.rlmobile.util.*
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.json.decodeFromStream
|
|
||||||
import java.io.IOException
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
@@ -21,52 +19,19 @@ import kotlin.time.Duration
|
|||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Central manager for storing all attempted installations and
|
* In-memory store for the most recent install attempts
|
||||||
* their associated logs/crashes (not including manager crashes themselves).
|
|
||||||
*/
|
*/
|
||||||
class InstallLogManager(
|
class InstallLogManager(
|
||||||
private val application: Application,
|
private val application: Application,
|
||||||
private val prefs: PreferencesManager,
|
private val prefs: PreferencesManager,
|
||||||
private val json: Json,
|
@Suppress("unused") private val json: Json,
|
||||||
) {
|
) {
|
||||||
val logsDir = application.filesDir.resolve("install-logs").apply { mkdir() }
|
private val entries = linkedMapOf<String, InstallLogData>()
|
||||||
|
|
||||||
/**
|
fun fetchInstallData(id: String): InstallLogData? = synchronized(entries) {
|
||||||
* Lists all the install data entries that exist on disk, sorted decreasing by
|
entries[id]
|
||||||
* the file creation date.
|
|
||||||
* @return List of installation ids, most recent installation first.
|
|
||||||
*/
|
|
||||||
fun fetchInstallDataEntries(): List<String> {
|
|
||||||
val files = logsDir.listFiles { it.extension == "json" } ?: emptyArray()
|
|
||||||
|
|
||||||
return files
|
|
||||||
.sortedByDescending { it.lastModified() }
|
|
||||||
.map { it.nameWithoutExtension }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Loads the install log from disk, if it exists.
|
|
||||||
*/
|
|
||||||
fun fetchInstallData(id: String): InstallLogData? {
|
|
||||||
val path = logsDir.resolve("$id.json")
|
|
||||||
if (!path.exists()) return null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
json.decodeFromStream(path.inputStream())
|
|
||||||
} catch (t: Throwable) {
|
|
||||||
Log.e(BuildConfig.TAG, "Failed to open install log $id", t)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun deleteAllEntries() {
|
|
||||||
logsDir.deleteRecursively()
|
|
||||||
logsDir.mkdir()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes an install log entry to disk.
|
|
||||||
*/
|
|
||||||
suspend fun storeInstallData(
|
suspend fun storeInstallData(
|
||||||
id: String,
|
id: String,
|
||||||
installDate: Instant,
|
installDate: Instant,
|
||||||
@@ -75,8 +40,6 @@ class InstallLogManager(
|
|||||||
log: String,
|
log: String,
|
||||||
error: Throwable?,
|
error: Throwable?,
|
||||||
) {
|
) {
|
||||||
val path = logsDir.resolve("$id.json")
|
|
||||||
|
|
||||||
val data = InstallLogData(
|
val data = InstallLogData(
|
||||||
id = id,
|
id = id,
|
||||||
installDate = installDate,
|
installDate = installDate,
|
||||||
@@ -87,16 +50,16 @@ class InstallLogManager(
|
|||||||
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
|
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
|
||||||
)
|
)
|
||||||
|
|
||||||
try {
|
synchronized(entries) {
|
||||||
path.writeText(json.encodeToString(data))
|
entries[id] = data
|
||||||
} catch (e: IOException) {
|
// Keep only the most recent few in memory.
|
||||||
Log.e(BuildConfig.TAG, "Failed to write log to disk", e)
|
while (entries.size > MAX_ENTRIES) {
|
||||||
|
val oldest = entries.keys.iterator().next()
|
||||||
|
entries.remove(oldest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a list of details about the current installation environment.
|
|
||||||
*/
|
|
||||||
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
@Suppress("KotlinConstantConditions", "SimplifyBooleanWithConstants")
|
||||||
@SuppressLint("UsableSpace")
|
@SuppressLint("UsableSpace")
|
||||||
suspend fun getEnvironmentInfo(): String {
|
suspend fun getEnvironmentInfo(): String {
|
||||||
@@ -139,6 +102,10 @@ class InstallLogManager(
|
|||||||
SOC: $soc
|
SOC: $soc
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
const val MAX_ENTRIES = 8
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
|
|||||||
@@ -22,11 +22,12 @@ class PathManager(
|
|||||||
|
|
||||||
val patchingDownloadDir = patchingDir.resolve("downloads")
|
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")
|
val customComponentsDir = patchingDir.resolve("custom")
|
||||||
|
|
||||||
val customInjectorsDir = customComponentsDir.resolve("injector")
|
val customTidalApksDir = customComponentsDir.resolve("tidal")
|
||||||
|
|
||||||
val customPatchesDir = customComponentsDir.resolve("patches")
|
val customPatchesDir = customComponentsDir.resolve("patches")
|
||||||
|
|
||||||
@@ -34,11 +35,15 @@ class PathManager(
|
|||||||
|
|
||||||
val patchedApk = patchingWorkingDir.resolve("patched.apk")
|
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() {
|
fun clearCache() {
|
||||||
val targets = arrayOf(
|
val targets = arrayOf(
|
||||||
patchingDownloadDir,
|
patchingDownloadDir,
|
||||||
patchingWorkingDir,
|
patchingWorkingDir,
|
||||||
cacheDownloadDir,
|
cacheDownloadDir,
|
||||||
|
cachedReleaseInfo,
|
||||||
context.cacheDir,
|
context.cacheDir,
|
||||||
)
|
)
|
||||||
for (dir in targets) {
|
for (dir in targets) {
|
||||||
@@ -54,7 +59,17 @@ class PathManager(
|
|||||||
.resolve("patches")
|
.resolve("patches")
|
||||||
.resolve("$version.zip")
|
.resolve("$version.zip")
|
||||||
|
|
||||||
fun customInjectors() = customInjectorsDir.listFiles()?.asList() ?: emptyList()
|
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()
|
fun customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,23 @@ import android.content.SharedPreferences
|
|||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.meowarex.rlmobile.manager.base.BasePreferenceManager
|
import com.meowarex.rlmobile.manager.base.BasePreferenceManager
|
||||||
import com.meowarex.rlmobile.ui.theme.Theme
|
import com.meowarex.rlmobile.ui.theme.Theme
|
||||||
|
import com.meowarex.rlmobile.ui.theme.UiStyle
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) {
|
class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) {
|
||||||
var theme by enumPreference("theme", Theme.System)
|
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 devMode by booleanPreference("dev_mode", false)
|
||||||
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
|
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
|
||||||
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
var keepPatchedApks by booleanPreference("keep_patched_apks", false)
|
||||||
var showNetworkWarning by booleanPreference("show_network_warning", true)
|
// 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 showPlayProtectWarning by booleanPreference("show_play_protect_warning", true)
|
||||||
var autoUpdateCheck by booleanPreference("auto_update_check", true)
|
var autoUpdateCheck by booleanPreference("auto_update_check", true)
|
||||||
|
|
||||||
|
var lastSeenManagerVersion by stringPreference("last_seen_manager_version", "")
|
||||||
|
var lastSeenPatchesVersion by stringPreference("last_seen_patches_version", "")
|
||||||
|
var lastSeenTidalVersionCode by intPreference("last_seen_tidal_version_code", -1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,7 @@ class TidalPatchRunner(
|
|||||||
RestoreDownloadsStep(),
|
RestoreDownloadsStep(),
|
||||||
|
|
||||||
// Download
|
// Download
|
||||||
DownloadTidalStep(),
|
DownloadTidalStep(options.customTidalApk),
|
||||||
DownloadPatchesStep(options.customPatches),
|
DownloadPatchesStep(options.customPatches),
|
||||||
CopyDependenciesStep(),
|
CopyDependenciesStep(),
|
||||||
|
|
||||||
|
|||||||
+25
-2
@@ -5,12 +5,17 @@ import com.meowarex.rlmobile.R
|
|||||||
import com.meowarex.rlmobile.manager.PathManager
|
import com.meowarex.rlmobile.manager.PathManager
|
||||||
import com.meowarex.rlmobile.patcher.StepRunner
|
import com.meowarex.rlmobile.patcher.StepRunner
|
||||||
import com.meowarex.rlmobile.patcher.steps.base.DownloadStep
|
import com.meowarex.rlmobile.patcher.steps.base.DownloadStep
|
||||||
|
import com.meowarex.rlmobile.patcher.steps.base.StepState
|
||||||
import com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep
|
import com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep
|
||||||
|
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class DownloadTidalStep : DownloadStep<Int>(), KoinComponent {
|
class DownloadTidalStep(
|
||||||
|
private val custom: PatchComponent?,
|
||||||
|
) : DownloadStep<Int>(), KoinComponent {
|
||||||
private val paths: PathManager by inject()
|
private val paths: PathManager by inject()
|
||||||
|
|
||||||
override val localizedName = R.string.patch_step_dl_tidal_apk
|
override val localizedName = R.string.patch_step_dl_tidal_apk
|
||||||
@@ -22,5 +27,23 @@ class DownloadTidalStep : DownloadStep<Int>(), KoinComponent {
|
|||||||
container.getStep<FetchInfoStep>().data.tidalApkUrl
|
container.getStep<FetchInfoStep>().data.tidalApkUrl
|
||||||
|
|
||||||
override fun getStoredFile(container: StepRunner) =
|
override fun getStoredFile(container: StepRunner) =
|
||||||
paths.cachedTidalApk(getVersion(container))
|
custom?.getFile(paths) ?: paths.cachedTidalApk(getVersion(container))
|
||||||
|
|
||||||
|
override suspend fun execute(container: StepRunner) {
|
||||||
|
if (custom != null) {
|
||||||
|
container.log("Using custom TIDAL APK with version ${custom.version} imported ${custom.timestamp}")
|
||||||
|
|
||||||
|
if (!custom.getFile(paths).exists()) {
|
||||||
|
throw FileNotFoundException(
|
||||||
|
"Selected custom TIDAL APK does not exist on disk! If this is an update, " +
|
||||||
|
"updates cannot occur when the originally selected custom component has been deleted."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state = StepState.Skipped
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
super.execute(container)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
@@ -26,11 +26,16 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
|
|||||||
?: throw IllegalArgumentException("No manifest found in APK")
|
?: throw IllegalArgumentException("No manifest found in APK")
|
||||||
|
|
||||||
container.log("Patching manifest")
|
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(
|
val patchedManifest = ManifestPatcher.patchManifest(
|
||||||
manifestBytes = manifest,
|
manifestBytes = manifest,
|
||||||
packageName = options.packageName,
|
packageName = options.packageName,
|
||||||
appName = options.appName,
|
appName = options.appName,
|
||||||
debuggable = options.debuggable,
|
debuggable = options.debuggable,
|
||||||
|
enableWazeIntegration = enableWazeIntegration,
|
||||||
)
|
)
|
||||||
|
|
||||||
container.log("Repacking apk with patched manifest")
|
container.log("Repacking apk with patched manifest")
|
||||||
|
|||||||
+119
-16
@@ -1,5 +1,6 @@
|
|||||||
package com.meowarex.rlmobile.patcher.steps.patch
|
package com.meowarex.rlmobile.patcher.steps.patch
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
import com.meowarex.rlmobile.manager.PathManager
|
import com.meowarex.rlmobile.manager.PathManager
|
||||||
import com.meowarex.rlmobile.patcher.StepRunner
|
import com.meowarex.rlmobile.patcher.StepRunner
|
||||||
@@ -8,7 +9,10 @@ import com.meowarex.rlmobile.patcher.steps.base.IDexProvider
|
|||||||
import com.meowarex.rlmobile.patcher.steps.base.Step
|
import com.meowarex.rlmobile.patcher.steps.base.Step
|
||||||
import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
|
import com.meowarex.rlmobile.patcher.steps.download.CopyDependenciesStep
|
||||||
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
|
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.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.Baksmali
|
||||||
import com.android.tools.smali.baksmali.BaksmaliOptions
|
import com.android.tools.smali.baksmali.BaksmaliOptions
|
||||||
import com.android.tools.smali.dexlib2.Opcodes
|
import com.android.tools.smali.dexlib2.Opcodes
|
||||||
@@ -19,6 +23,7 @@ import com.github.diamondminer88.zip.ZipReader
|
|||||||
import com.github.difflib.DiffUtils
|
import com.github.difflib.DiffUtils
|
||||||
import com.github.difflib.UnifiedDiffUtils
|
import com.github.difflib.UnifiedDiffUtils
|
||||||
import com.github.difflib.patch.Patch
|
import com.github.difflib.patch.Patch
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
import java.io.*
|
import java.io.*
|
||||||
@@ -27,6 +32,11 @@ class SmaliPatchStep(
|
|||||||
private val options: PatchOptions,
|
private val options: PatchOptions,
|
||||||
) : Step(), IDexProvider, KoinComponent {
|
) : Step(), IDexProvider, KoinComponent {
|
||||||
private val paths: PathManager by inject()
|
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 group = StepGroup.Patch
|
||||||
override val localizedName = R.string.patch_step_patch_smali
|
override val localizedName = R.string.patch_step_patch_smali
|
||||||
@@ -42,6 +52,29 @@ class SmaliPatchStep(
|
|||||||
val patches = mutableListOf<LoadedPatch>()
|
val patches = mutableListOf<LoadedPatch>()
|
||||||
val localsBumps = mutableMapOf<Pair<String, String>, Int>()
|
val localsBumps = mutableMapOf<Pair<String, String>, Int>()
|
||||||
|
|
||||||
|
// The patch list/options metadata lives in the zip's manifest.json
|
||||||
|
val manifestSpecs = try {
|
||||||
|
ZipReader(patchesZip).use { it.openEntry("manifest.json")?.read() }
|
||||||
|
?.let { json.decodeFromString(PatchManifest.serializer(), it.decodeToString()).patches }
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
container.log("Failed to parse manifest.json (${t.message}); using built-in patch specs")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
specs = manifestSpecs ?: builtinPatchSpecs(context, json)
|
||||||
|
container.log(
|
||||||
|
"Loaded ${specs.size} patch specs from " +
|
||||||
|
if (manifestSpecs != null) "manifest.json" else "built-in list"
|
||||||
|
)
|
||||||
|
|
||||||
|
val disabledFiles = options.disabledPatchFiles(specs)
|
||||||
|
val knownExtensionFiles = options.knownExtensionFiles(specs)
|
||||||
|
val enabledExtensionFiles = options.enabledExtensionFiles(specs)
|
||||||
|
val substitutions = options.smaliSubstitutions(specs)
|
||||||
|
if (substitutions.isNotEmpty()) {
|
||||||
|
container.log("Patch option substitutions: ${substitutions.entries.joinToString { "${it.key}=${it.value}" }}")
|
||||||
|
}
|
||||||
|
|
||||||
// Load and parse all the patches from the smali archive.
|
// Load and parse all the patches from the smali archive.
|
||||||
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
|
||||||
smaliDir.mkdirs()
|
smaliDir.mkdirs()
|
||||||
@@ -53,6 +86,11 @@ class SmaliPatchStep(
|
|||||||
|
|
||||||
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
|
if (patchFile.endsWith(".smali") && patchFile.startsWith("extension/")) {
|
||||||
val relative = patchFile.removePrefix("extension/")
|
val relative = patchFile.removePrefix("extension/")
|
||||||
|
// Only bundle helper smali patch/variant/sub-option is enabled
|
||||||
|
if (relative in knownExtensionFiles && relative !in enabledExtensionFiles) {
|
||||||
|
container.log("Skipping disabled extension smali: $relative")
|
||||||
|
continue
|
||||||
|
}
|
||||||
val out = smaliDir.resolve(relative)
|
val out = smaliDir.resolve(relative)
|
||||||
// Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
|
// Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
|
||||||
val baseCanonical = smaliDir.canonicalPath + File.separator
|
val baseCanonical = smaliDir.canonicalPath + File.separator
|
||||||
@@ -63,7 +101,19 @@ class SmaliPatchStep(
|
|||||||
val entry = zip.openEntry(patchFile)
|
val entry = zip.openEntry(patchFile)
|
||||||
?: throw FileNotFoundException("Missing zip entry: $patchFile")
|
?: throw FileNotFoundException("Missing zip entry: $patchFile")
|
||||||
out.canonicalFile.parentFile?.mkdirs()
|
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")
|
container.log("Extracted extension smali: $relative")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -71,16 +121,39 @@ class SmaliPatchStep(
|
|||||||
if (!patchFile.endsWith(".patch")) continue
|
if (!patchFile.endsWith(".patch")) continue
|
||||||
|
|
||||||
val basename = patchFile.substringAfterLast('/')
|
val basename = patchFile.substringAfterLast('/')
|
||||||
if (basename in options.disabledPatches) {
|
if (basename in disabledFiles) {
|
||||||
container.log("Skipping disabled patch $patchFile")
|
container.log("Skipping disabled patch $patchFile")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
val lines = zip.openEntry(patchFile)!!.read()
|
var patchText = zip.openEntry(patchFile)!!.read()
|
||||||
.decodeToString()
|
.decodeToString()
|
||||||
.replace("\r\n", "\n") // Replace CRLF endings with LF
|
.replace("\r\n", "\n") // Replace CRLF endings with LF
|
||||||
.trimEnd { it == '\n' } // Remove trailing new lines
|
.trimEnd { it == '\n' } // Remove trailing new lines
|
||||||
.split('\n')
|
|
||||||
|
// Bake advanced option values into the patch
|
||||||
|
for ((token, value) in substitutions) {
|
||||||
|
if (patchText.contains(token)) {
|
||||||
|
patchText = patchText.replace(token, value)
|
||||||
|
container.log("Applied substitution $token -> $value in $patchFile")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fail fast
|
||||||
|
UNRESOLVED_TOKEN.find(patchText)?.let { match ->
|
||||||
|
throw Error("Unresolved option placeholder ${match.value} in $patchFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip a patch that references a helper class which won't be bundled
|
||||||
|
val missingHelper = (knownExtensionFiles - enabledExtensionFiles).firstOrNull { rel ->
|
||||||
|
patchText.contains("L${rel.removeSuffix(".smali")};")
|
||||||
|
}
|
||||||
|
if (missingHelper != null) {
|
||||||
|
container.log("Skipping $patchFile: references gated-off helper class $missingHelper")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val lines = patchText.split('\n')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (directive in lines) {
|
for (directive in lines) {
|
||||||
@@ -92,18 +165,25 @@ class SmaliPatchStep(
|
|||||||
container.log("Recorded rl-locals bump: $smaliPath method≈\"$methodSubstring\" >= $newValue")
|
container.log("Recorded rl-locals bump: $smaliPath method≈\"$methodSubstring\" >= $newValue")
|
||||||
}
|
}
|
||||||
|
|
||||||
val targetLine = lines.firstOrNull { it.startsWith("--- a/") }
|
// Split into per-target sections — a single .patch may contain multiple
|
||||||
?: throw Error("Patch $patchFile is missing a '--- a/...' header")
|
// `--- a/...` blocks targeting different classes.
|
||||||
val fullClassName = targetLine
|
val sections = splitMultiTargetPatch(lines)
|
||||||
.removePrefix("--- a/")
|
if (sections.isEmpty()) {
|
||||||
.removeSuffix(".smali")
|
throw Error("Patch $patchFile is missing a '--- a/...' header")
|
||||||
.trim()
|
}
|
||||||
val patch = LoadedPatch(
|
for (section in sections) {
|
||||||
fullClassName = fullClassName,
|
val targetLine = section.first { it.startsWith("--- a/") }
|
||||||
patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
|
val fullClassName = targetLine
|
||||||
)
|
.removePrefix("--- a/")
|
||||||
patches.add(patch)
|
.removeSuffix(".smali")
|
||||||
container.log("Loaded patch file $patchFile for class ${patch.fullClassName}")
|
.trim()
|
||||||
|
val patch = LoadedPatch(
|
||||||
|
fullClassName = fullClassName,
|
||||||
|
patch = UnifiedDiffUtils.parseUnifiedDiff(section),
|
||||||
|
)
|
||||||
|
patches.add(patch)
|
||||||
|
container.log("Loaded patch file $patchFile for class ${patch.fullClassName}")
|
||||||
|
}
|
||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
throw Error("Failed to parse patch file $patchFile", t)
|
throw Error("Failed to parse patch file $patchFile", t)
|
||||||
}
|
}
|
||||||
@@ -313,6 +393,29 @@ class SmaliPatchStep(
|
|||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""")
|
val LOCALS_DIRECTIVE = Regex("""^#\s*rl-locals:\s+(\S+)\s+(\S+)\s+(\d+)\s*$""")
|
||||||
|
val UNRESOLVED_TOKEN = Regex("""__RL_[A-Z0-9_]+__""")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Splits a unified diff into per-target sections. A single `.patch` file may bundle
|
||||||
|
* multiple file diffs (each starting with `--- a/...`); each section becomes its own
|
||||||
|
* patch with its own target class. The header lines before the first `--- a/` (and
|
||||||
|
* any `# rl-locals:` directives) are preserved by being copied into every section so
|
||||||
|
* that `UnifiedDiffUtils.parseUnifiedDiff` can still parse the section in isolation.
|
||||||
|
*/
|
||||||
|
private fun splitMultiTargetPatch(lines: List<String>): List<List<String>> {
|
||||||
|
val headerEnd = lines.indexOfFirst { it.startsWith("--- a/") }
|
||||||
|
if (headerEnd < 0) return emptyList()
|
||||||
|
val header = lines.subList(0, headerEnd)
|
||||||
|
|
||||||
|
val sectionStarts = lines.withIndex()
|
||||||
|
.filter { (_, line) -> line.startsWith("--- a/") }
|
||||||
|
.map { it.index }
|
||||||
|
|
||||||
|
return sectionStarts.mapIndexed { i, start ->
|
||||||
|
val end = sectionStarts.getOrNull(i + 1) ?: lines.size
|
||||||
|
header + lines.subList(start, end)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+45
-2
@@ -1,19 +1,30 @@
|
|||||||
package com.meowarex.rlmobile.patcher.steps.prepare
|
package com.meowarex.rlmobile.patcher.steps.prepare
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.Stable
|
import androidx.compose.runtime.Stable
|
||||||
import com.meowarex.rlmobile.R
|
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.models.RLBuildInfo
|
||||||
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
||||||
import com.meowarex.rlmobile.network.utils.getOrThrow
|
import com.meowarex.rlmobile.network.utils.getOrThrow
|
||||||
import com.meowarex.rlmobile.patcher.StepRunner
|
import com.meowarex.rlmobile.patcher.StepRunner
|
||||||
import com.meowarex.rlmobile.patcher.steps.StepGroup
|
import com.meowarex.rlmobile.patcher.steps.StepGroup
|
||||||
import com.meowarex.rlmobile.patcher.steps.base.Step
|
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.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class FetchInfoStep : Step(), KoinComponent {
|
class FetchInfoStep : Step(), KoinComponent {
|
||||||
private val github: RadiantLyricsGithubService by inject()
|
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 group = StepGroup.Prepare
|
||||||
override val localizedName = R.string.patch_step_fetch_info
|
override val localizedName = R.string.patch_step_fetch_info
|
||||||
@@ -25,6 +36,33 @@ class FetchInfoStep : Step(), KoinComponent {
|
|||||||
private set
|
private set
|
||||||
|
|
||||||
override suspend fun execute(container: StepRunner) {
|
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}")
|
container.log("Fetching latest release from ${RadiantLyricsGithubService.REPO_OWNER}/${RadiantLyricsGithubService.REPO_NAME}")
|
||||||
val release = github.getLatestRelease(force = true).getOrThrow()
|
val release = github.getLatestRelease(force = true).getOrThrow()
|
||||||
|
|
||||||
@@ -33,14 +71,19 @@ class FetchInfoStep : Step(), KoinComponent {
|
|||||||
?.browserDownloadUrl
|
?.browserDownloadUrl
|
||||||
?: throw IllegalStateException("No ${RadiantLyricsGithubService.DATA_JSON_ASSET_NAME} asset found in latest release ${release.tagName}")
|
?: 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 }
|
.find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME }
|
||||||
?.browserDownloadUrl
|
?.browserDownloadUrl
|
||||||
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
|
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
|
||||||
|
|
||||||
container.log("Fetching build info from $dataJsonUrl")
|
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("Fetched build info: $data")
|
||||||
container.log("Patches asset URL: $patchesAssetUrl")
|
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 IS_SPLIT_REQUIRED = "isSplitRequired"
|
||||||
private const val REQUIRED_SPLIT_TYPES = "requiredSplitTypes"
|
private const val REQUIRED_SPLIT_TYPES = "requiredSplitTypes"
|
||||||
private const val SPLIT_TYPES = "splitTypes"
|
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(
|
fun patchManifest(
|
||||||
manifestBytes: ByteArray,
|
manifestBytes: ByteArray,
|
||||||
packageName: String,
|
packageName: String,
|
||||||
appName: String,
|
appName: String,
|
||||||
debuggable: Boolean,
|
debuggable: Boolean,
|
||||||
|
enableWazeIntegration: Boolean,
|
||||||
): ByteArray {
|
): ByteArray {
|
||||||
// Extract original package name to rewrite every reference to it
|
// Extract original package name to rewrite every reference to it
|
||||||
// (permissions, provider authorities) to the new packageName.
|
// (permissions, provider authorities) to the new packageName.
|
||||||
@@ -84,6 +88,49 @@ object ManifestPatcher {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return when (name) {
|
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) {
|
"uses-permission" -> object : NodeVisitor(nv) {
|
||||||
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) {
|
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) {
|
||||||
if (name != "maxSdkVersion") {
|
if (name != "maxSdkVersion") {
|
||||||
@@ -132,6 +179,7 @@ object ManifestPatcher {
|
|||||||
private var addUseEmbeddedDex = true
|
private var addUseEmbeddedDex = true
|
||||||
private var addExtractNativeLibs = true
|
private var addExtractNativeLibs = true
|
||||||
private var addMetadata = true
|
private var addMetadata = true
|
||||||
|
private var hasWazeInitReceiver = false
|
||||||
|
|
||||||
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
|
||||||
if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false
|
if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false
|
||||||
@@ -161,6 +209,22 @@ object ManifestPatcher {
|
|||||||
|
|
||||||
return when (name) {
|
return when (name) {
|
||||||
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName))
|
"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) {
|
"provider" -> object : NodeVisitor(visitor) {
|
||||||
override fun attr(
|
override fun attr(
|
||||||
ns: String?,
|
ns: String?,
|
||||||
@@ -219,6 +283,39 @@ object ManifestPatcher {
|
|||||||
0
|
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()
|
super.end()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
package com.meowarex.rlmobile.ui.components
|
package com.meowarex.rlmobile.ui.components
|
||||||
|
|
||||||
import androidx.activity.compose.LocalActivity
|
import androidx.activity.compose.LocalActivity
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton
|
||||||
import com.meowarex.rlmobile.util.back
|
import com.meowarex.rlmobile.util.back
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -18,14 +17,10 @@ fun BackButton() {
|
|||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
val activity = LocalActivity.current
|
val activity = LocalActivity.current
|
||||||
|
|
||||||
IconButton(
|
RadiantIconButton(
|
||||||
onClick = {
|
icon = painterResource(R.drawable.ic_back),
|
||||||
navigator?.back(activity)
|
contentDescription = stringResource(R.string.navigation_back),
|
||||||
},
|
subtle = true,
|
||||||
) {
|
onClick = { navigator?.back(activity) },
|
||||||
Icon(
|
)
|
||||||
painter = painterResource(R.drawable.ic_back),
|
|
||||||
contentDescription = stringResource(R.string.navigation_back),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+31
-35
@@ -1,13 +1,11 @@
|
|||||||
package com.meowarex.rlmobile.ui.components
|
package com.meowarex.rlmobile.ui.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantButton
|
||||||
import androidx.compose.ui.unit.dp
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonSize
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantButtonStyle
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MainActionButton(
|
fun MainActionButton(
|
||||||
@@ -15,35 +13,33 @@ fun MainActionButton(
|
|||||||
icon: Painter,
|
icon: Painter,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
colors: IconButtonColors = IconButtonDefaults.filledTonalIconButtonColors(
|
style: RadiantButtonStyle = RadiantButtonStyle.Accent,
|
||||||
containerColor = MaterialTheme.colorScheme.primary,
|
|
||||||
),
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) = RadiantButton(
|
||||||
FilledTonalIconButton(
|
text = text,
|
||||||
shape = MaterialTheme.shapes.medium,
|
icon = icon,
|
||||||
colors = colors,
|
onClick = onClick,
|
||||||
onClick = onClick,
|
enabled = enabled,
|
||||||
enabled = enabled,
|
style = style,
|
||||||
modifier = modifier
|
size = RadiantButtonSize.Large,
|
||||||
.fillMaxWidth()
|
fillWidth = true,
|
||||||
.height(46.dp),
|
modifier = modifier,
|
||||||
) {
|
)
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
painter = icon,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.size(20.dp),
|
|
||||||
)
|
|
||||||
|
|
||||||
Text(
|
@Composable
|
||||||
text = text,
|
fun DangerActionButton(
|
||||||
style = MaterialTheme.typography.labelLarge,
|
text: String,
|
||||||
textAlign = TextAlign.Center,
|
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
|
package com.meowarex.rlmobile.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.colorResource
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantButton
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProjectHeader(modifier: Modifier = Modifier) {
|
fun ProjectHeader(modifier: Modifier = Modifier) {
|
||||||
@@ -22,30 +26,37 @@ fun ProjectHeader(modifier: Modifier = Modifier) {
|
|||||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
modifier = modifier,
|
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(
|
||||||
text = stringResource(R.string.rlmobile),
|
text = stringResource(R.string.rlmobile),
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontSize = 26.sp)
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.app_description),
|
text = stringResource(R.string.app_description),
|
||||||
style = MaterialTheme.typography.titleSmall.copy(
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = .6f)
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
),
|
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.Center,
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier.padding(top = 6.dp),
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = { uriHandler.openUri("https://github.com/meowarex/rl-mobile") }) {
|
RadiantButton(
|
||||||
Icon(
|
text = stringResource(R.string.github),
|
||||||
painter = painterResource(R.drawable.ic_account_github_white_24dp),
|
icon = painterResource(R.drawable.ic_account_github_white_24dp),
|
||||||
contentDescription = null,
|
onClick = { uriHandler.openUri("https://github.com/meowarex/rl-mobile") },
|
||||||
modifier = Modifier.padding(end = ButtonDefaults.IconSpacing),
|
)
|
||||||
)
|
|
||||||
Text(text = stringResource(R.string.github))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-14
@@ -1,15 +1,13 @@
|
|||||||
package com.meowarex.rlmobile.ui.components
|
package com.meowarex.rlmobile.ui.components
|
||||||
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
import com.meowarex.rlmobile.ui.util.mirrorVertically
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantIconButton
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ResetToDefaultButton(
|
fun ResetToDefaultButton(
|
||||||
@@ -23,15 +21,12 @@ fun ResetToDefaultButton(
|
|||||||
exit = fadeOut() + slideOutHorizontally(),
|
exit = fadeOut() + slideOutHorizontally(),
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onClick) {
|
RadiantIconButton(
|
||||||
Icon(
|
icon = painterResource(R.drawable.ic_refresh),
|
||||||
painter = painterResource(R.drawable.ic_refresh),
|
contentDescription = stringResource(R.string.action_reset_default),
|
||||||
tint = MaterialTheme.colorScheme.secondary,
|
tint = MaterialTheme.colorScheme.secondary,
|
||||||
contentDescription = stringResource(R.string.action_reset_default),
|
subtle = true,
|
||||||
modifier = Modifier
|
onClick = onClick,
|
||||||
.mirrorVertically()
|
)
|
||||||
.size(26.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.meowarex.rlmobile.ui.components
|
||||||
|
|
||||||
|
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,
|
||||||
|
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 = tint,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,35 +1,47 @@
|
|||||||
package com.meowarex.rlmobile.ui.components
|
package com.meowarex.rlmobile.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Section header
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun TextDivider(
|
fun TextDivider(
|
||||||
text: String,
|
text: String,
|
||||||
style: TextStyle = MaterialTheme.typography.bodyMedium,
|
style: TextStyle = MaterialTheme.typography.labelLarge,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp, Alignment.CenterHorizontally),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxWidth(),
|
||||||
.fillMaxWidth(),
|
|
||||||
) {
|
) {
|
||||||
HorizontalDivider(Modifier.weight(1f))
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
Text(
|
.size(width = 14.dp, height = 3.dp)
|
||||||
text = text,
|
.clip(CircleShape)
|
||||||
style = style,
|
.background(MaterialTheme.colorScheme.primary),
|
||||||
color = MaterialTheme.colorScheme.outline,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
package com.meowarex.rlmobile.ui.components.dialogs
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.size
|
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.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantDialog
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InstallerAbortDialog(
|
fun InstallerAbortDialog(
|
||||||
onConfirm: () -> Unit,
|
onConfirm: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
AlertDialog(
|
RadiantDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
confirmButton = {
|
title = stringResource(R.string.installer_abort_title),
|
||||||
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 = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_warning),
|
painter = painterResource(R.drawable.ic_warning),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(26.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
confirmText = stringResource(R.string.action_exit_anyways),
|
||||||
iconContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
onConfirm = onConfirm,
|
||||||
titleContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
destructive = true,
|
||||||
textContentColor = MaterialTheme.colorScheme.onErrorContainer
|
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.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.*
|
||||||
import com.meowarex.rlmobile.ui.theme.customColors
|
import com.meowarex.rlmobile.ui.theme.customColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -29,70 +28,58 @@ fun PlayProtectDialog(
|
|||||||
var neverShow by rememberSaveable { mutableStateOf(false) }
|
var neverShow by rememberSaveable { mutableStateOf(false) }
|
||||||
val rememberedNeverShow by rememberUpdatedState(neverShow)
|
val rememberedNeverShow by rememberUpdatedState(neverShow)
|
||||||
|
|
||||||
AlertDialog(
|
RadiantDialog(
|
||||||
onDismissRequest = { onDismiss(rememberedNeverShow) },
|
onDismissRequest = { onDismiss(rememberedNeverShow) },
|
||||||
dismissButton = {
|
title = stringResource(R.string.play_protect_warning_title),
|
||||||
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 = {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_protect_warning),
|
painter = painterResource(R.drawable.ic_protect_warning),
|
||||||
tint = MaterialTheme.customColors.warning,
|
tint = MaterialTheme.customColors.warning,
|
||||||
contentDescription = null,
|
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(16.dp)) {
|
||||||
Column(
|
Text(stringResource(R.string.play_protect_warning_desc))
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier.clickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = null,
|
||||||
|
onClick = { neverShow = !rememberedNeverShow },
|
||||||
|
),
|
||||||
) {
|
) {
|
||||||
Text(
|
RadiantCheckbox(
|
||||||
text = stringResource(R.string.play_protect_warning_desc),
|
checked = neverShow,
|
||||||
textAlign = TextAlign.Center,
|
onCheckedChange = { neverShow = it },
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Text(stringResource(R.string.play_protect_warning_disable))
|
||||||
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(
|
// Both actions are equal, they share a row
|
||||||
dismissOnClickOutside = false,
|
Row(
|
||||||
usePlatformDefaultWidth = false,
|
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
|
||||||
),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
modifier = Modifier
|
) {
|
||||||
.padding(25.dp),
|
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() {
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
+237
@@ -0,0 +1,237 @@
|
|||||||
|
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. (i prefer the old on tbh so i reverted it back <3)
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun RadiantSlider(
|
||||||
|
value: Float,
|
||||||
|
onValueChange: (Float) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
valueRange: ClosedFloatingPointRange<Float> = 0f..1f,
|
||||||
|
steps: Int = 0,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
showStopIndicator: Boolean = true,
|
||||||
|
onValueChangeFinished: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
if (showStopIndicator) {
|
||||||
|
Slider(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
onValueChangeFinished = onValueChangeFinished,
|
||||||
|
valueRange = valueRange,
|
||||||
|
steps = steps,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Slider(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
onValueChangeFinished = onValueChangeFinished,
|
||||||
|
valueRange = valueRange,
|
||||||
|
steps = steps,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = modifier,
|
||||||
|
track = { SliderDefaults.Track(sliderState = it, drawStopIndicator = null) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.. */
|
||||||
+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(
|
TextDivider(
|
||||||
text = text,
|
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
|
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.foundation.layout.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.ProvideTextStyle
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.dp
|
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
|
@Composable
|
||||||
fun SettingsItem(
|
fun SettingsItem(
|
||||||
text: @Composable () -> Unit,
|
text: @Composable () -> Unit,
|
||||||
secondaryText: @Composable (() -> Unit) = { },
|
secondaryText: @Composable (() -> Unit) = { },
|
||||||
icon: @Composable (() -> Unit) = { },
|
icon: @Composable (() -> Unit) = { },
|
||||||
|
position: GroupPosition = GroupPosition.Single,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
trailing: @Composable (() -> Unit) = { },
|
trailing: @Composable (() -> Unit) = { },
|
||||||
) {
|
) {
|
||||||
|
val ui = radiantStyle
|
||||||
|
val iconShape = RoundedCornerShape(12.dp)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.heightIn(min = 64.dp)
|
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 32.dp, vertical = 12.dp),
|
// Legacy rows are flat and full bleed with no tile background or grouping. (because i'm lazy)
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
.then(
|
||||||
verticalAlignment = Alignment.CenterVertically
|
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(
|
if (ui.groupedSettings) {
|
||||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
Box(
|
||||||
modifier = Modifier.weight(2f, true)
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
modifier = Modifier
|
||||||
Row(
|
.glow(radius = 10.dp, shape = iconShape, alpha = 0.35f)
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
.size(36.dp)
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
.clip(iconShape)
|
||||||
|
.background(accentBrush, iconShape),
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.size(20.dp)) {
|
CompositionLocalProvider(
|
||||||
icon()
|
LocalContentColor provides MaterialTheme.colorScheme.onPrimary,
|
||||||
|
) {
|
||||||
|
Box(Modifier.size(20.dp)) { icon() }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(Modifier.size(20.dp)) { icon() }
|
||||||
|
}
|
||||||
|
|
||||||
ProvideTextStyle(MaterialTheme.typography.titleSmall) {
|
Column(
|
||||||
text()
|
verticalArrangement = Arrangement.spacedBy(3.dp),
|
||||||
}
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
ProvideTextStyle(MaterialTheme.typography.titleSmall) {
|
||||||
|
text()
|
||||||
}
|
}
|
||||||
|
|
||||||
ProvideTextStyle(
|
ProvideTextStyle(
|
||||||
MaterialTheme.typography.bodyMedium.copy(
|
MaterialTheme.typography.bodySmall.copy(
|
||||||
color = MaterialTheme.colorScheme.onSurface.copy(0.6f)
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
secondaryText()
|
secondaryText()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer(Modifier.weight(0.05f, true))
|
|
||||||
|
|
||||||
trailing()
|
trailing()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-4
@@ -1,10 +1,9 @@
|
|||||||
package com.meowarex.rlmobile.ui.components.settings
|
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.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantSwitch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsSwitch(
|
fun SettingsSwitch(
|
||||||
@@ -13,11 +12,14 @@ fun SettingsSwitch(
|
|||||||
disabled: Boolean = false,
|
disabled: Boolean = false,
|
||||||
icon: @Composable () -> Unit = {},
|
icon: @Composable () -> Unit = {},
|
||||||
pref: Boolean,
|
pref: Boolean,
|
||||||
|
position: GroupPosition = GroupPosition.Single,
|
||||||
onPrefChange: (Boolean) -> Unit,
|
onPrefChange: (Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
SettingsItem(
|
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) },
|
text = { Text(text = label, softWrap = true) },
|
||||||
icon = icon,
|
icon = icon,
|
||||||
secondaryText = {
|
secondaryText = {
|
||||||
@@ -26,7 +28,7 @@ fun SettingsSwitch(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Switch(
|
RadiantSwitch(
|
||||||
checked = pref,
|
checked = pref,
|
||||||
enabled = !disabled,
|
enabled = !disabled,
|
||||||
onCheckedChange = { onPrefChange(!pref) }
|
onCheckedChange = { onPrefChange(!pref) }
|
||||||
|
|||||||
+13
-6
@@ -1,11 +1,12 @@
|
|||||||
package com.meowarex.rlmobile.ui.components.settings
|
package com.meowarex.rlmobile.ui.components.settings
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.material3.OutlinedTextField
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.meowarex.rlmobile.ui.components.radiant.RadiantTextField
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsTextField(
|
fun SettingsTextField(
|
||||||
@@ -15,15 +16,21 @@ fun SettingsTextField(
|
|||||||
error: Boolean = false,
|
error: Boolean = false,
|
||||||
onPrefChange: (String) -> Unit,
|
onPrefChange: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(modifier = Modifier.padding(horizontal = 18.dp, vertical = 10.dp)) {
|
Column(
|
||||||
OutlinedTextField(
|
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||||
modifier = Modifier.fillMaxWidth(),
|
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,
|
value = pref,
|
||||||
onValueChange = onPrefChange,
|
onValueChange = onPrefChange,
|
||||||
enabled = !disabled,
|
enabled = !disabled,
|
||||||
label = { Text(label) },
|
|
||||||
isError = error,
|
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.*
|
||||||
import androidx.compose.foundation.layout.*
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
+43
-34
@@ -1,5 +1,10 @@
|
|||||||
package com.meowarex.rlmobile.ui.components.dialogs
|
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.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
@@ -13,51 +18,53 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
|
import com.meowarex.rlmobile.ui.theme.customColors
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NetworkWarningDialog(
|
fun PlayProtectDialog(
|
||||||
onConfirm: (neverShow: Boolean) -> Unit,
|
|
||||||
onDismiss: (neverShow: Boolean) -> Unit,
|
onDismiss: (neverShow: Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val activity = LocalActivity.currentOrThrow
|
||||||
val interactionSource = remember(::MutableInteractionSource)
|
val interactionSource = remember(::MutableInteractionSource)
|
||||||
var neverShow by rememberSaveable { mutableStateOf(false) }
|
var neverShow by rememberSaveable { mutableStateOf(false) }
|
||||||
val rememberedNeverShow by rememberUpdatedState(neverShow)
|
val rememberedNeverShow by rememberUpdatedState(neverShow)
|
||||||
|
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { onDismiss(rememberedNeverShow) },
|
onDismissRequest = { onDismiss(rememberedNeverShow) },
|
||||||
properties = DialogProperties(
|
dismissButton = {
|
||||||
dismissOnClickOutside = false,
|
FilledTonalButton(onClick = activity::launchPlayProtect) {
|
||||||
),
|
Text(stringResource(R.string.play_protect_warning_open_gpp))
|
||||||
|
}
|
||||||
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = { onConfirm(rememberedNeverShow) },
|
|
||||||
colors = ButtonDefaults.filledTonalButtonColors(
|
|
||||||
containerColor = MaterialTheme.colorScheme.error,
|
|
||||||
contentColor = MaterialTheme.colorScheme.onError,
|
|
||||||
),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.action_continue))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = { onDismiss(rememberedNeverShow) },
|
onClick = { onDismiss(rememberedNeverShow) },
|
||||||
colors = ButtonDefaults.textButtonColors(
|
colors = ButtonDefaults.filledTonalButtonColors(
|
||||||
contentColor = MaterialTheme.colorScheme.onErrorContainer
|
containerColor = MaterialTheme.colorScheme.primary,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
) {
|
) {
|
||||||
Text(stringResource(R.string.navigation_back))
|
Text(stringResource(R.string.play_protect_warning_ok))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = { Text(stringResource(R.string.network_warning_title)) },
|
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 = {
|
text = {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.network_warning_body),
|
text = stringResource(R.string.play_protect_warning_desc),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -77,20 +84,22 @@ fun NetworkWarningDialog(
|
|||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(stringResource(R.string.network_warning_disable))
|
Text(stringResource(R.string.play_protect_warning_disable))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
icon = {
|
properties = DialogProperties(
|
||||||
Icon(
|
dismissOnClickOutside = false,
|
||||||
painter = painterResource(R.drawable.ic_warning),
|
usePlatformDefaultWidth = false,
|
||||||
contentDescription = null,
|
),
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier
|
||||||
)
|
.padding(25.dp),
|
||||||
},
|
|
||||||
containerColor = MaterialTheme.colorScheme.errorContainer,
|
|
||||||
iconContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
|
||||||
titleContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
|
||||||
textContentColor = MaterialTheme.colorScheme.onErrorContainer,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+688
@@ -0,0 +1,688 @@
|
|||||||
|
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.hiddenForVariant
|
||||||
|
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
|
||||||
|
// Options belonging to another variant are hidden, not greyed out.
|
||||||
|
option.hiddenForVariant(selectedVariant) -> 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+9
-13
@@ -1,4 +1,7 @@
|
|||||||
package com.meowarex.rlmobile.ui.screens.logs.components
|
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.material3.*
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -6,26 +9,19 @@ import androidx.compose.ui.res.painterResource
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
import com.meowarex.rlmobile.ui.components.BackButton
|
import com.meowarex.rlmobile.ui.legacy.components.BackButton
|
||||||
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
|
import com.meowarex.rlmobile.ui.legacy.screens.settings.SettingsScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsListAppBar(
|
fun PatchOptionsAppBar(
|
||||||
onDeleteLogs: () -> Unit,
|
isUpdate: Boolean = false,
|
||||||
) {
|
) {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
navigationIcon = { BackButton() },
|
navigationIcon = { BackButton() },
|
||||||
title = { Text(stringResource(R.string.logs_title)) },
|
title = { Text(stringResource(if (!isUpdate) R.string.action_add_install else R.string.action_update_install)) },
|
||||||
actions = {
|
actions = {
|
||||||
val navigator = LocalNavigator.current
|
val navigator = LocalNavigator.current
|
||||||
|
|
||||||
IconButton(onClick = onDeleteLogs) {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.ic_delete_forever),
|
|
||||||
contentDescription = stringResource(R.string.logs_action_delete_all)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = { navigator?.push(SettingsScreen()) }) {
|
IconButton(onClick = { navigator?.push(SettingsScreen()) }) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(R.drawable.ic_settings),
|
painter = painterResource(R.drawable.ic_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,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package com.meowarex.rlmobile.ui.previews.dialogs
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.meowarex.rlmobile.ui.screens.logs.components.dialogs.DeleteLogsDialog
|
|
||||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
|
||||||
private fun DeleteLogsDialogPreview() {
|
|
||||||
ManagerTheme {
|
|
||||||
DeleteLogsDialog(
|
|
||||||
onConfirm = {},
|
|
||||||
onDismiss = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-19
@@ -1,19 +0,0 @@
|
|||||||
package com.meowarex.rlmobile.ui.previews.dialogs
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.meowarex.rlmobile.ui.components.dialogs.NetworkWarningDialog
|
|
||||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
|
||||||
private fun NetworkWarningDialogPreview() {
|
|
||||||
ManagerTheme {
|
|
||||||
NetworkWarningDialog(
|
|
||||||
onConfirm = {},
|
|
||||||
onDismiss = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+12
-6
@@ -30,6 +30,12 @@ private fun ComponentOptionsScreenPreview(
|
|||||||
selected = parameters.selected,
|
selected = parameters.selected,
|
||||||
onSelectComponent = {},
|
onSelectComponent = {},
|
||||||
onDeleteComponent = {},
|
onDeleteComponent = {},
|
||||||
|
onImportFromUri = {},
|
||||||
|
releasesExpanded = false,
|
||||||
|
releasesState = com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsModel.ReleasesState.Idle,
|
||||||
|
onToggleReleases = {},
|
||||||
|
onImportRelease = {},
|
||||||
|
importingReleaseTag = null,
|
||||||
onBackPressed = {},
|
onBackPressed = {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -44,27 +50,27 @@ private data class ComponentOptionsParameters(
|
|||||||
private class ComponentOptionsParametersProvider : PreviewParameterProvider<ComponentOptionsParameters> {
|
private class ComponentOptionsParametersProvider : PreviewParameterProvider<ComponentOptionsParameters> {
|
||||||
private val components = persistentListOf(
|
private val components = persistentListOf(
|
||||||
PatchComponent(
|
PatchComponent(
|
||||||
type = PatchComponent.Type.Injector,
|
type = PatchComponent.Type.TidalApk,
|
||||||
version = SemVer(1, 2, 3),
|
version = SemVer(1, 2, 3),
|
||||||
timestamp = Clock.System.now(),
|
timestamp = Clock.System.now(),
|
||||||
),
|
),
|
||||||
PatchComponent(
|
PatchComponent(
|
||||||
type = PatchComponent.Type.Injector,
|
type = PatchComponent.Type.TidalApk,
|
||||||
version = SemVer(2, 3, 1),
|
version = SemVer(2, 3, 1),
|
||||||
timestamp = Clock.System.now() - 10.minutes,
|
timestamp = Clock.System.now() - 10.minutes,
|
||||||
),
|
),
|
||||||
PatchComponent(
|
PatchComponent(
|
||||||
type = PatchComponent.Type.Injector,
|
type = PatchComponent.Type.TidalApk,
|
||||||
version = SemVer(2, 3, 1),
|
version = SemVer(2, 3, 1),
|
||||||
timestamp = Clock.System.now() - 1.days,
|
timestamp = Clock.System.now() - 1.days,
|
||||||
),
|
),
|
||||||
PatchComponent(
|
PatchComponent(
|
||||||
type = PatchComponent.Type.Injector,
|
type = PatchComponent.Type.TidalApk,
|
||||||
version = SemVer(0, 0, 1),
|
version = SemVer(0, 0, 1),
|
||||||
timestamp = Clock.System.now() - 10.hours,
|
timestamp = Clock.System.now() - 10.hours,
|
||||||
),
|
),
|
||||||
PatchComponent(
|
PatchComponent(
|
||||||
type = PatchComponent.Type.Injector,
|
type = PatchComponent.Type.TidalApk,
|
||||||
version = SemVer(3, 0, 2),
|
version = SemVer(3, 0, 2),
|
||||||
timestamp = Clock.System.now() - 7.days,
|
timestamp = Clock.System.now() - 7.days,
|
||||||
),
|
),
|
||||||
@@ -72,7 +78,7 @@ private class ComponentOptionsParametersProvider : PreviewParameterProvider<Comp
|
|||||||
|
|
||||||
override val values = sequenceOf(
|
override val values = sequenceOf(
|
||||||
ComponentOptionsParameters(
|
ComponentOptionsParameters(
|
||||||
componentType = PatchComponent.Type.Injector,
|
componentType = PatchComponent.Type.TidalApk,
|
||||||
components = components,
|
components = components,
|
||||||
selected = null,
|
selected = null,
|
||||||
),
|
),
|
||||||
|
|||||||
+23
-8
@@ -2,11 +2,14 @@ package com.meowarex.rlmobile.ui.previews.screens
|
|||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.*
|
import androidx.compose.ui.tooling.preview.*
|
||||||
import com.meowarex.rlmobile.network.utils.SemVer
|
import com.meowarex.rlmobile.network.utils.SemVer
|
||||||
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
|
||||||
import com.meowarex.rlmobile.ui.screens.patchopts.*
|
import com.meowarex.rlmobile.ui.screens.patchopts.*
|
||||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
|
|
||||||
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
|
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
|
||||||
@@ -18,25 +21,37 @@ private fun PatchOptionsScreenPreview(
|
|||||||
@PreviewParameter(PatchOptionsParametersProvider::class)
|
@PreviewParameter(PatchOptionsParametersProvider::class)
|
||||||
parameters: PatchOptionsParameters,
|
parameters: PatchOptionsParameters,
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val specs = remember { builtinPatchSpecs(context, Json { ignoreUnknownKeys = true }) }
|
||||||
|
|
||||||
ManagerTheme {
|
ManagerTheme {
|
||||||
PatchOptionsScreenContent(
|
PatchOptionsScreenContent(
|
||||||
isUpdate = parameters.isUpdate,
|
isUpdate = parameters.isUpdate,
|
||||||
isDevMode = parameters.isDevMode,
|
isDevMode = parameters.isDevMode,
|
||||||
debuggable = parameters.debuggable,
|
debuggable = parameters.debuggable,
|
||||||
setDebuggable = {},
|
setDebuggable = {},
|
||||||
|
bypassIncompatible = false,
|
||||||
|
setBypassIncompatible = {},
|
||||||
appName = parameters.appName,
|
appName = parameters.appName,
|
||||||
appNameIsError = parameters.appNameIsError,
|
appNameIsError = parameters.appNameIsError,
|
||||||
setAppName = {},
|
setAppName = {},
|
||||||
packageName = parameters.packageName,
|
packageName = parameters.packageName,
|
||||||
packageNameState = parameters.packageNameState,
|
packageNameState = parameters.packageNameState,
|
||||||
setPackageName = {},
|
setPackageName = {},
|
||||||
customInjector = parameters.customInjector,
|
packageNameLocked = parameters.packageName == PatchOptions.Default.packageName,
|
||||||
onSelectCustomInjector = {},
|
onUnlockPackageName = {},
|
||||||
|
onLockPackageName = {},
|
||||||
|
customTidalApk = parameters.customTidalApk,
|
||||||
|
onSelectCustomTidalApk = {},
|
||||||
customPatches = parameters.customPatches,
|
customPatches = parameters.customPatches,
|
||||||
onSelectCustomPatches = {},
|
onSelectCustomPatches = {},
|
||||||
enabledPatchCount = KnownPatch.All.size,
|
specs = specs,
|
||||||
isPatchEnabled = { true },
|
isPatchEnabled = { true },
|
||||||
onTogglePatch = { _, _ -> },
|
onTogglePatch = { _, _ -> },
|
||||||
|
patchLockState = { PatchLock.Free },
|
||||||
|
variantIndex = { 0 },
|
||||||
|
onSelectVariant = { _, _ -> },
|
||||||
|
optionState = PatchOptionState.Preview,
|
||||||
isConfigValid = parameters.isConfigValid,
|
isConfigValid = parameters.isConfigValid,
|
||||||
onInstall = {},
|
onInstall = {},
|
||||||
)
|
)
|
||||||
@@ -51,7 +66,7 @@ private data class PatchOptionsParameters(
|
|||||||
val appNameIsError: Boolean,
|
val appNameIsError: Boolean,
|
||||||
val packageName: String,
|
val packageName: String,
|
||||||
val packageNameState: PackageNameState,
|
val packageNameState: PackageNameState,
|
||||||
val customInjector: PatchComponent?,
|
val customTidalApk: PatchComponent?,
|
||||||
val customPatches: PatchComponent?,
|
val customPatches: PatchComponent?,
|
||||||
val isConfigValid: Boolean,
|
val isConfigValid: Boolean,
|
||||||
)
|
)
|
||||||
@@ -66,7 +81,7 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
|
|||||||
appNameIsError = false,
|
appNameIsError = false,
|
||||||
packageName = PatchOptions.Default.packageName,
|
packageName = PatchOptions.Default.packageName,
|
||||||
packageNameState = PackageNameState.Ok,
|
packageNameState = PackageNameState.Ok,
|
||||||
customInjector = null,
|
customTidalApk = null,
|
||||||
customPatches = null,
|
customPatches = null,
|
||||||
isConfigValid = true,
|
isConfigValid = true,
|
||||||
),
|
),
|
||||||
@@ -78,7 +93,7 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
|
|||||||
appNameIsError = true,
|
appNameIsError = true,
|
||||||
packageName = "a b",
|
packageName = "a b",
|
||||||
packageNameState = PackageNameState.Invalid,
|
packageNameState = PackageNameState.Invalid,
|
||||||
customInjector = null,
|
customTidalApk = null,
|
||||||
customPatches = null,
|
customPatches = null,
|
||||||
isConfigValid = false,
|
isConfigValid = false,
|
||||||
),
|
),
|
||||||
@@ -90,8 +105,8 @@ private class PatchOptionsParametersProvider : PreviewParameterProvider<PatchOpt
|
|||||||
appNameIsError = false,
|
appNameIsError = false,
|
||||||
packageName = PatchOptions.Default.packageName,
|
packageName = PatchOptions.Default.packageName,
|
||||||
packageNameState = PackageNameState.Taken,
|
packageNameState = PackageNameState.Taken,
|
||||||
customInjector = PatchComponent(
|
customTidalApk = PatchComponent(
|
||||||
type = PatchComponent.Type.Injector,
|
type = PatchComponent.Type.TidalApk,
|
||||||
version = SemVer(1, 2, 3),
|
version = SemVer(1, 2, 3),
|
||||||
timestamp = Clock.System.now(),
|
timestamp = Clock.System.now(),
|
||||||
),
|
),
|
||||||
|
|||||||
-78
@@ -1,78 +0,0 @@
|
|||||||
package com.meowarex.rlmobile.ui.previews.screens.logs
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.meowarex.rlmobile.ui.screens.logs.LogEntry
|
|
||||||
import com.meowarex.rlmobile.ui.screens.logs.LogsScreenContent
|
|
||||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
|
||||||
import java.util.UUID
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
|
||||||
private fun LogsListScreenNonePreview() {
|
|
||||||
ManagerTheme {
|
|
||||||
LogsScreenContent(
|
|
||||||
logs = logs,
|
|
||||||
onOpenLog = {},
|
|
||||||
onDeleteLogs = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val logs: SnapshotStateList<LogEntry> = mutableStateListOf(
|
|
||||||
LogEntry(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
isError = false,
|
|
||||||
installDate = "5 min. ago, 10:18 AM",
|
|
||||||
durationSecs = 18.555f,
|
|
||||||
stacktracePreview = null,
|
|
||||||
),
|
|
||||||
LogEntry(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
isError = true,
|
|
||||||
installDate = "7 min. ago, 10:17 AM",
|
|
||||||
durationSecs = 73.095f,
|
|
||||||
stacktracePreview = persistentListOf(
|
|
||||||
"kotlinx.coroutines.JobCancellationException: Job was cancelled; job=SupervisorJobImpl{Cancelling}@833e76f]",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
LogEntry(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
isError = false,
|
|
||||||
installDate = "Yesterday, 11:37 PM",
|
|
||||||
durationSecs = 58.439f,
|
|
||||||
stacktracePreview = null,
|
|
||||||
),
|
|
||||||
LogEntry(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
isError = true,
|
|
||||||
installDate = "Yesterday, 11:17 PM",
|
|
||||||
durationSecs = 24.405f,
|
|
||||||
stacktracePreview = persistentListOf(
|
|
||||||
"java.lang.Error: Installation was aborted or cancelled",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
LogEntry(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
isError = true,
|
|
||||||
installDate = "Yesterday, 11:17 PM",
|
|
||||||
durationSecs = 0.057f,
|
|
||||||
stacktracePreview = persistentListOf(
|
|
||||||
"java.lang.IllegalStateException: balls",
|
|
||||||
"\tat com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep.execute(FetchInfoStep.kt:31)",
|
|
||||||
"\tat com.meowarex.rlmobile.patcher.steps.prepare.FetchInfoStep\$execute\\$1.invokeSuspend(Unknown Source:15)",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
LogEntry(
|
|
||||||
id = UUID.randomUUID().toString(),
|
|
||||||
isError = false,
|
|
||||||
installDate = "Yesterday, 1:11 PM",
|
|
||||||
durationSecs = 210.539f,
|
|
||||||
stacktracePreview = null,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
package com.meowarex.rlmobile.ui.previews.screens.logs
|
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import com.meowarex.rlmobile.ui.screens.logs.LogsScreenContent
|
|
||||||
import com.meowarex.rlmobile.ui.theme.ManagerTheme
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
|
|
||||||
private fun LogsListScreenNonePreview() {
|
|
||||||
ManagerTheme {
|
|
||||||
LogsScreenContent(
|
|
||||||
logs = remember { mutableStateListOf() },
|
|
||||||
onOpenLog = {},
|
|
||||||
onDeleteLogs = {},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -85,7 +85,7 @@ fun AboutScreenContent(state: State<AboutScreenState>) {
|
|||||||
AboutScreenState.Loading -> item(key = "CONTRIBUTIONS_LOADING") {
|
AboutScreenState.Loading -> item(key = "CONTRIBUTIONS_LOADING") {
|
||||||
Box(
|
Box(
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
content = { CircularProgressIndicator() },
|
content = { LoadingIndicator() },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 38.dp),
|
.padding(vertical = 38.dp),
|
||||||
|
|||||||
+169
-1
@@ -1,26 +1,44 @@
|
|||||||
package com.meowarex.rlmobile.ui.screens.componentopts
|
package com.meowarex.rlmobile.ui.screens.componentopts
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.util.Log
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import com.meowarex.rlmobile.BuildConfig
|
||||||
import com.meowarex.rlmobile.R
|
import com.meowarex.rlmobile.R
|
||||||
import com.meowarex.rlmobile.manager.PathManager
|
import com.meowarex.rlmobile.manager.PathManager
|
||||||
|
import com.meowarex.rlmobile.manager.download.KtorDownloadManager
|
||||||
|
import com.meowarex.rlmobile.network.models.GithubRelease
|
||||||
|
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
|
||||||
import com.meowarex.rlmobile.network.utils.SemVer
|
import com.meowarex.rlmobile.network.utils.SemVer
|
||||||
|
import com.meowarex.rlmobile.network.utils.fold
|
||||||
import com.meowarex.rlmobile.ui.util.ScreenModelWithResult
|
import com.meowarex.rlmobile.ui.util.ScreenModelWithResult
|
||||||
import com.meowarex.rlmobile.ui.util.ScreenResultKey
|
import com.meowarex.rlmobile.ui.util.ScreenResultKey
|
||||||
import com.meowarex.rlmobile.util.*
|
import com.meowarex.rlmobile.util.*
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import java.io.File
|
||||||
import kotlin.time.Instant
|
import kotlin.time.Instant
|
||||||
|
|
||||||
class ComponentOptionsModel(
|
class ComponentOptionsModel(
|
||||||
screenResultKey: ScreenResultKey,
|
screenResultKey: ScreenResultKey,
|
||||||
private val paths: PathManager,
|
private val paths: PathManager,
|
||||||
private val context: Application,
|
private val context: Application,
|
||||||
|
private val github: RadiantLyricsGithubService,
|
||||||
|
private val downloader: KtorDownloadManager,
|
||||||
) : ScreenModelWithResult<PatchComponent?>(screenResultKey) {
|
) : ScreenModelWithResult<PatchComponent?>(screenResultKey) {
|
||||||
val components = mutableStateListOf<PatchComponent>()
|
val components = mutableStateListOf<PatchComponent>()
|
||||||
var selected by mutableStateOf<PatchComponent?>(null)
|
var selected by mutableStateOf<PatchComponent?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var releasesExpanded by mutableStateOf(false)
|
||||||
|
private set
|
||||||
|
var releasesState by mutableStateOf<ReleasesState>(ReleasesState.Idle)
|
||||||
|
private set
|
||||||
|
var importingReleaseTag by mutableStateOf<String?>(null)
|
||||||
|
private set
|
||||||
|
|
||||||
fun selectComponent(component: PatchComponent?) {
|
fun selectComponent(component: PatchComponent?) {
|
||||||
selected = component
|
selected = component
|
||||||
}
|
}
|
||||||
@@ -39,7 +57,7 @@ class ComponentOptionsModel(
|
|||||||
*/
|
*/
|
||||||
suspend fun refreshComponents(type: PatchComponent.Type) {
|
suspend fun refreshComponents(type: PatchComponent.Type) {
|
||||||
val files = when (type) {
|
val files = when (type) {
|
||||||
PatchComponent.Type.Injector -> paths.customInjectors()
|
PatchComponent.Type.TidalApk -> paths.customTidalApks()
|
||||||
PatchComponent.Type.Patches -> paths.customSmaliPatches()
|
PatchComponent.Type.Patches -> paths.customSmaliPatches()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +82,157 @@ class ComponentOptionsModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun importFromUri(uri: Uri, type: PatchComponent.Type) = screenModelScope.launchIO {
|
||||||
|
try {
|
||||||
|
val targetDir = when (type) {
|
||||||
|
PatchComponent.Type.TidalApk -> paths.customTidalApksDir
|
||||||
|
PatchComponent.Type.Patches -> paths.customPatchesDir
|
||||||
|
}
|
||||||
|
val ext = when (type) {
|
||||||
|
PatchComponent.Type.TidalApk -> "apk"
|
||||||
|
PatchComponent.Type.Patches -> "zip"
|
||||||
|
}
|
||||||
|
targetDir.mkdirs()
|
||||||
|
|
||||||
|
val tempFile = targetDir.resolve("import-${System.currentTimeMillis()}.tmp")
|
||||||
|
context.contentResolver.openInputStream(uri)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output -> input.copyTo(output) }
|
||||||
|
} ?: throw IllegalStateException("Could not open input stream for $uri")
|
||||||
|
|
||||||
|
val sourceDisplayName = queryDisplayName(uri)
|
||||||
|
val version = when (type) {
|
||||||
|
PatchComponent.Type.TidalApk -> readApkVersion(tempFile)
|
||||||
|
?: extractVersionFromName(sourceDisplayName)
|
||||||
|
?: FALLBACK_VERSION
|
||||||
|
PatchComponent.Type.Patches -> extractVersionFromName(sourceDisplayName)
|
||||||
|
?: FALLBACK_VERSION
|
||||||
|
}
|
||||||
|
|
||||||
|
val finalName = "${System.currentTimeMillis()}_$version.$ext"
|
||||||
|
val finalFile = targetDir.resolve(finalName)
|
||||||
|
if (!tempFile.renameTo(finalFile)) {
|
||||||
|
tempFile.copyTo(finalFile, overwrite = true)
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshComponents(type)
|
||||||
|
mainThread { context.showToast(R.string.intent_import_component_success, finalName) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(BuildConfig.TAG, "Failed to import custom component from $uri", t)
|
||||||
|
mainThread { context.showToast(R.string.intent_import_component_failure) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryDisplayName(uri: Uri): String? = try {
|
||||||
|
context.contentResolver.query(uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null)
|
||||||
|
?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) cursor.getString(0) else null
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readApkVersion(apkFile: File): String? = try {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
context.packageManager.getPackageArchiveInfo(apkFile.absolutePath, 0)
|
||||||
|
?.versionName
|
||||||
|
?.let { SemVer.parseOrNull(it) }
|
||||||
|
?.toString()
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun extractVersionFromName(name: String?): String? {
|
||||||
|
if (name == null) return null
|
||||||
|
return """(\d+\.\d+\.\d+)""".toRegex().find(name)?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleReleasesExpanded(type: PatchComponent.Type) {
|
||||||
|
releasesExpanded = !releasesExpanded
|
||||||
|
if (releasesExpanded && releasesState is ReleasesState.Idle) {
|
||||||
|
loadReleases(type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadReleases(type: PatchComponent.Type) = screenModelScope.launchIO {
|
||||||
|
releasesState = ReleasesState.Loading
|
||||||
|
github.getManagerReleases().fold(
|
||||||
|
success = { all ->
|
||||||
|
val assetName = assetNameFor(type)
|
||||||
|
val filtered = all.filter { release ->
|
||||||
|
release.assets.any { it.name == assetName }
|
||||||
|
}
|
||||||
|
releasesState = ReleasesState.Loaded(filtered)
|
||||||
|
},
|
||||||
|
fail = {
|
||||||
|
Log.w(BuildConfig.TAG, "Failed to load GitHub releases", it)
|
||||||
|
releasesState = ReleasesState.Failed
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importFromRelease(release: GithubRelease, type: PatchComponent.Type) = screenModelScope.launchIO {
|
||||||
|
val assetName = assetNameFor(type)
|
||||||
|
val asset = release.assets.find { it.name == assetName } ?: run {
|
||||||
|
mainThread { context.showToast(R.string.intent_import_component_failure) }
|
||||||
|
return@launchIO
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetDir = when (type) {
|
||||||
|
PatchComponent.Type.TidalApk -> paths.customTidalApksDir
|
||||||
|
PatchComponent.Type.Patches -> paths.customPatchesDir
|
||||||
|
}
|
||||||
|
targetDir.mkdirs()
|
||||||
|
|
||||||
|
importingReleaseTag = release.tagName
|
||||||
|
try {
|
||||||
|
val tempFile = targetDir.resolve("release-${System.currentTimeMillis()}.tmp")
|
||||||
|
val result = downloader.download(asset.browserDownloadUrl, tempFile)
|
||||||
|
if (result !is com.meowarex.rlmobile.manager.download.IDownloadManager.Result.Success) {
|
||||||
|
tempFile.delete()
|
||||||
|
mainThread { context.showToast(R.string.intent_import_component_failure) }
|
||||||
|
return@launchIO
|
||||||
|
}
|
||||||
|
|
||||||
|
val ext = when (type) {
|
||||||
|
PatchComponent.Type.TidalApk -> "apk"
|
||||||
|
PatchComponent.Type.Patches -> "zip"
|
||||||
|
}
|
||||||
|
val version = SemVer.parseOrNull(release.tagName.removePrefix("v"))?.toString()
|
||||||
|
?: FALLBACK_VERSION
|
||||||
|
val finalName = "${System.currentTimeMillis()}_$version.$ext"
|
||||||
|
val finalFile = targetDir.resolve(finalName)
|
||||||
|
if (!tempFile.renameTo(finalFile)) {
|
||||||
|
tempFile.copyTo(finalFile, overwrite = true)
|
||||||
|
tempFile.delete()
|
||||||
|
}
|
||||||
|
refreshComponents(type)
|
||||||
|
mainThread { context.showToast(R.string.intent_import_component_success, finalName) }
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
Log.e(BuildConfig.TAG, "Failed to import release ${release.tagName}", t)
|
||||||
|
mainThread { context.showToast(R.string.intent_import_component_failure) }
|
||||||
|
} finally {
|
||||||
|
importingReleaseTag = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assetNameFor(type: PatchComponent.Type) = when (type) {
|
||||||
|
PatchComponent.Type.TidalApk -> "tidal-stock.apk"
|
||||||
|
PatchComponent.Type.Patches -> "patches.zip"
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDispose() {
|
override fun onDispose() {
|
||||||
screenModelScope.launch { setResult(selected) }
|
screenModelScope.launch { setResult(selected) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface ReleasesState {
|
||||||
|
data object Idle : ReleasesState
|
||||||
|
data object Loading : ReleasesState
|
||||||
|
data class Loaded(val releases: List<GithubRelease>) : ReleasesState
|
||||||
|
data object Failed : ReleasesState
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val FALLBACK_VERSION = "0.0.0"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user