Compare commits

...
91 Commits
Author SHA1 Message Date
meoware.exe 28cd296283 Merge pull request #77 from meowarex/dev
WIP | Syllable AI [RL API]
2026-08-11 20:26:40 +10:00
meowarex ba9bba2030 WIP | Syllable AI [RL API] 2026-08-11 07:57:30 +00:00
meowarex d44d833334 Gestures Rewrite 2026-08-11 03:22:28 +00:00
meoware.exe 8d06cea14d Merge pull request #76 from meowarex/dev
1.0 Release <3
2026-08-10 17:00:52 +10:00
meoware.exe b9baef4478 1.0 Release <3 2026-08-10 17:00:22 +10:00
meoware.exe 8fc0241611 Merge pull request #75 from meowarex/dev
Syllable Level Lyrics <3
2026-08-10 03:20:25 +10:00
meowarex a648e1a97c Syllable Level Lyrics <3 2026-08-09 17:11:50 +00:00
meoware.exe 2150b87371 Merge pull request #71 from meowarex/dev
Word by Word Lyrics <3
2026-07-18 15:04:56 +10:00
meoware.exe 0d82c16168 Readme Update 2026-07-18 15:04:31 +10:00
meoware.exe 4c257f7ad9 Word by Word Lyrics <3 2026-07-18 15:02:07 +10:00
meoware.exe 7458df8eba Merge pull request #69 from meowarex/dev
Lyrics Button Hotfix <3
2026-07-17 00:01:18 +10:00
meoware.exe 6a5771cf9e Readme changes <3 2026-07-17 00:00:49 +10:00
meoware.exe a8a576c994 Lyrics Button Hotfix <3 2026-07-16 23:00:03 +10:00
meoware.exe 585666c403 Merge pull request #68 from meowarex/dev
Player Backdrop Image Settings <3
2026-07-14 01:54:31 +10:00
meoware.exe 90c008082f Player Backdrop Image Settings <3 2026-07-14 01:33:30 +10:00
meoware.exe c2a2586306 Merge pull request #67 from meowarex/dev
Upgrade Tidal Version + Native Blur <3
2026-07-14 00:35:34 +10:00
meoware.exe 2291f4064e Upgrade Tidal Version + Native Blur <3 2026-07-14 00:31:41 +10:00
meoware.exe f6a4de0cc7 Fix Missing Blocks on Patches <3 2026-07-06 16:56:56 +10:00
meoware.exe 4911e7703d Merge pull request #66 from meowarex/dev
Nav Bar Color Hotfix <3
2026-07-06 14:25:27 +10:00
meoware.exe f9a174eafa Nav Bar Color Hotfix <3 2026-07-06 14:24:51 +10:00
meoware.exe 49fe4c0d91 Merge pull request #65 from meowarex/dev
APK Bump + Offline Support + Playing From
2026-07-05 23:03:16 +10:00
meoware.exe 189e3d28a2 Offline Repatch + Force Incompatible (Dev Option) 2026-07-05 23:00:14 +10:00
meoware.exe aae2f54fab Update APK Version & Add Playing From 2026-07-05 20:58:22 +10:00
meoware.exe 191dcca239 Merge pull request #64 from meowarex/dev
Waze Integration + Integrations UI <3
2026-07-03 20:20:14 +10:00
meoware.exe 08aee155c9 Integrations UI + Package Name Warnings/Gate <3 2026-07-03 20:19:16 +10:00
meoware.exe 47c9c37236 Merge pull request #63 from Se7en-dev/feat/waze-support-patch
[PATCH] Waze Integration
2026-07-03 15:46:39 +10:00
Se7en-dev 50b99b6193 feat(waze patch): customizable themeColor 2026-07-02 22:56:09 +02:00
Se7en-dev 03271bf26d feat(waze patch): Tidal cyan accent color for waze 2026-07-02 20:44:24 +02:00
Se7en-dev 75f8187526 feat(waze patch): waze media menu browse source options 2026-07-02 20:26:12 +02:00
Se7en-dev 523f0a342b feat(patch): waze integration patch 2026-07-02 15:49:18 +02:00
meoware.exe fb4c776c61 Merge pull request #60 from meowarex/dev
Fold Cover Everywhere -> Player Backdrop
2026-06-26 23:02:05 +10:00
meoware.exe cbf04d9fa7 Fold Cover Everywhere -> Player Backdrop 2026-06-26 23:01:26 +10:00
meoware.exe 266cd184dd Merge pull request #59 from meowarex/dev
Improve Swipe Gstures <3
2026-06-26 22:38:36 +10:00
meoware.exe 04ea66acdb Improve Swipe Gstures <3 2026-06-26 22:38:04 +10:00
meoware.exe 22526f4e0b Merge pull request #58 from Se7en-dev/fix/mini-player-gestures-swipe-up-android-gestures-nav-fix
Fix : Mini Player Gestures - Android gesture navigation swipe up gesture fix + enable drag option + visual swipe up feedback
2026-06-26 19:02:36 +10:00
Se7en-dev 5a3ad2df7a fix(MP gestures): axis locking + swipe up feedback reset fixes 2026-06-26 10:59:11 +02:00
Se7en-dev fa99d10134 lint(MP gestures enable drag option): remove now useless text 2026-06-26 00:00:12 +02:00
Se7en-dev 4e8af5960c fix(mini player gestures): scope swipe up gesture to track area 2026-06-25 23:57:46 +02:00
Se7en-dev 09f6e8d34f feat(mini player gestures): enable drag option 2026-06-25 21:58:27 +02:00
meoware.exe 160be11ac4 Merge pull request #55 from meowarex/dev
Improve Patch Option Style <3
2026-06-25 01:28:34 +10:00
meoware.exe 4486e52117 Improve Patch Option Style <3 2026-06-25 01:27:32 +10:00
meoware.exe bafd40a20c Merge pull request #54 from meowarex/dev
Advanced Options + New Patch Options <3
2026-06-25 01:05:04 +10:00
meoware.exe c89827c7b2 Advanced Options + New Patch Options <3 2026-06-25 01:03:28 +10:00
meoware.exe 90b49cc571 Merge pull request #49 from Se7en-dev/feat/mini-player-dynamic-color-patch
[PATCH] mini player dynamic bg color
2026-06-24 18:31:38 +10:00
meoware.exe 8e86c0d2e1 Merge branch 'dev' into feat/mini-player-dynamic-color-patch 2026-06-24 18:20:49 +10:00
meoware.exe 8ac9fb0db5 Merge pull request #50 from Se7en-dev/feat/mini-player-gestures-patch
[PATCH] mini player gestures patch
2026-06-24 18:05:05 +10:00
Se7en-dev 6ad6cb2fe0 fix(patch): update order and patches version 2026-06-24 01:04:45 +02:00
Se7en-dev a9611a1667 feat(patch): mini player dynamic bg color 2026-06-24 01:01:57 +02:00
Se7en-dev 276dd0be2e fix(miniplayer gestures): clip text on right side 2026-06-24 00:30:14 +02:00
Se7en-dev 3758ba19f3 feat(mini player gestures): track swipe feedback + fix stuck offsets 2026-06-23 23:08:02 +02:00
Se7en-dev 75450aad60 fix(mini player gestures patch): support redeisgned mp floating seek bar variant 2026-06-23 14:51:12 +02:00
Se7en-dev 3788eb2008 feat(patch): mini player gestures patch + patch suboptions 2026-06-23 14:18:38 +02:00
meoware.exe 95be099821 Merge pull request #48 from meowarex/dev
Overhaul Patch Defaults <3
2026-06-22 00:17:01 +10:00
meoware.exe 7a55787894 Bump Patch Version.. 2026-06-22 00:15:37 +10:00
meoware.exe bd09c2798e Overhaul Patch Defaults <3 2026-06-22 00:15:04 +10:00
meoware.exe b0b614dc6f Merge pull request #47 from meowarex/dev
New Patch: One-Handed Mode <3
2026-06-21 23:16:23 +10:00
meoware.exe 6a7d5fe90f Support for Animated Covers <3 2026-06-21 17:21:07 +10:00
meoware.exe 4791e69bec New Patch: One-Handed Mode <3 2026-06-21 01:36:05 +10:00
meoware.exe 7f3618aeae Fix Nav Bleed <3 2026-06-21 00:37:24 +10:00
meoware.exe ea9c6458c2 Update "Add Track" Icon <3 2026-06-20 23:53:33 +10:00
meoware.exe bdbcc4fc62 Merge pull request #44 from meowarex/dev
New Patch: Quality Badge Color
2026-06-20 23:06:36 +10:00
meoware.exe 3b893c12ff Renamed Patch File <3 2026-06-20 23:05:28 +10:00
meoware.exe 9d60e3dc80 Merge pull request #43 from Se7en-dev/feat/quality-badge-colors-patch
[PATCH] Quality Badge Colors
2026-06-20 22:55:24 +10:00
Se7en-dev 36382437f1 feat(patches): added quality badge colors patch 2026-06-19 17:29:19 +02:00
meoware.exe 5dc9188772 Merge pull request #41 from meowarex/dev
Mark RL API as WIP
2026-06-16 02:44:23 +10:00
meoware.exe 7823a54d11 Mark RL API as WIP 2026-06-16 02:43:35 +10:00
meoware.exe 4d5fea843e Merge pull request #40 from meowarex/dev
Rework Update Notification
2026-06-16 02:31:19 +10:00
meoware.exe d6bc889387 Rework Update Notification 2026-06-16 02:19:33 +10:00
meoware.exe 8501ccb0a9 Merge pull request #39 from meowarex/dev
Dev
2026-06-16 02:13:17 +10:00
meoware.exe ad6ebac6ce Bump Patch Version 2026-06-16 02:13:02 +10:00
meoware.exe 320148e774 Update RL-API Platform (DX) <3 2026-06-16 02:11:23 +10:00
meoware.exe d8e119eb4b Merge pull request #34 from meowarex/dev
Forgot to Bump Patches Version <3
2026-06-02 11:02:34 +10:00
meoware.exe 10e5dcf365 Forgot to Bump Patches Version <3 2026-06-02 11:02:12 +10:00
meoware.exe 1c65c5669a Merge pull request #32 from meowarex/dev
Fix Tidal Connect Share Bug <3
2026-06-01 20:59:31 +10:00
meoware.exe 961291166c Fix Tidal Connect button triggering Share Dialog 2026-06-01 20:52:16 +10:00
meoware.exe 25852aaea1 Merge pull request #29 from meowarex/dev
New Patch: Mini-Player Redesign
2026-05-26 22:53:46 +10:00
meoware.exe c6b18d1aa9 New Patch: Mini-Player Redesign 2026-05-26 22:49:21 +10:00
meoware.exe ee8b25330d Merge pull request #28 from meowarex/dev
New Patch: Cover Everywhere - WIP
2026-05-25 21:13:34 +10:00
meoware.exe 193d5ad90d Overhaul Manager Updates & Logging 2026-05-25 21:13:00 +10:00
meoware.exe 942742e86e New Patch: Cover Everywhere - WIP 2026-05-25 20:27:03 +10:00
meoware.exe 0b039110c9 Merge pull request #26 from meowarex/dev
Fix Missing Dependency
2026-05-25 04:35:38 +10:00
meoware.exe 045bbbab9a Fix Missing Dependency 2026-05-25 04:35:06 +10:00
meoware.exe 932f2a85dc Merge pull request #25 from meowarex/dev
Update data.json
2026-05-25 04:27:15 +10:00
meoware.exe fc002615f4 Update data.json 2026-05-25 04:26:49 +10:00
meoware.exe c89dd54fa6 Merge pull request #24 from meowarex/dev
New Patches: RL API & Keep Controls Visible
2026-05-25 04:19:08 +10:00
meoware.exe 53a4341d6b New Patches: RL API & Keep Controls Visible 2026-05-25 04:17:44 +10:00
meoware.exe c81d2edba3 Merge pull request #23 from meowarex/dev
Fix Update Status & Dynamic Labels
2026-05-25 00:51:52 +10:00
meoware.exe 9180129afc Fix Update Status & Dynamic Labels 2026-05-25 00:51:05 +10:00
meoware.exe d9de3207f5 Merge pull request #22 from meowarex/dev
Update to TIDAL 2.192.1-9090
2026-05-25 00:14:30 +10:00
meoware.exe 59fe232ae4 Update to TIDAL 2.192.1-9090 2026-05-25 00:13:46 +10:00
meoware.exe e105a3eb35 Fix Lyrics Fade Region 2026-05-24 22:55:00 +10:00
129 changed files with 16976 additions and 1275 deletions
+1 -1
View File
@@ -60,7 +60,7 @@ jobs:
if [[ "$tidal_src" == *.apkm ]]; then
echo "Merging splits from $tidal_src via APKEditor"
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
echo "Merged tidal-stock.apk:"
ls -la ./dist/tidal-stock.apk
Binary file not shown.
File diff suppressed because it is too large Load Diff
+10
View File
@@ -228,3 +228,13 @@ fun ProviderFactory.execIgnoreCode(vararg command: String): String = try {
} catch (_: Exception) {
""
}
// Sync Local Cached Assets with Repo Releases to keep them up to date
val syncBundledManifest by tasks.registering(Copy::class) {
val src = rootProject.rootDir.resolve("../patches/manifest.json")
onlyIf { src.exists() }
from(src)
into(layout.projectDirectory.dir("src/main/assets"))
rename { "patches-manifest.json" }
}
tasks.named("preBuild") { dependsOn(syncBundledManifest) }
@@ -0,0 +1,500 @@
{
"version": 1,
"patches": [
{
"id": "EnableLegacyUi",
"order": 10,
"fileNames": [
"enable-legacy-ui.patch"
],
"title": "Enable Legacy UI",
"description": "[This patch will stop working soon] - Replaces the New Compose based UI with the Legacy UI (disables player-market-ui feature flag)",
"default": false,
"requires": [
"DebugMenuUnlock"
],
"disables": [
"LyricsDisableCover",
"LyricsReplaceLyricsButton",
"LyricsReplaceShareButton",
"LyricsRlApi",
"LyricsKeepControlsVisible",
"PlayerBackdrop",
"QualityBadgeColors",
"PlayerMovePlayingFrom",
"PlayerOneHanded",
"LyricsProgressPill",
"MiniPlayerRedesign"
]
},
{
"id": "LyricsRlApi",
"order": 20,
"fileNames": [
"lyrics-rl-api.patch",
"lyrics-rl-api-observer.patch",
"lyrics-rl-api-isrc.patch"
],
"title": "Radiant Lyrics API - WIP",
"description": "Use Radiant Lyrics API to fetch Lyrics (Syllable Level & Higher Quality)",
"default": false,
"defaultVariantIndex": 0,
"variants": [
{
"title": "Line",
"fileNames": []
},
{
"title": "Word",
"fileNames": [
"lyrics-rl-api-word.patch"
]
},
{
"title": "Syllable",
"fileNames": [
"lyrics-rl-api-syllable.patch"
]
}
],
"extensionFiles": [
"radiant/RLAPILyricsHook.smali",
"radiant/RLAPILyricsWorker.smali",
"radiant/IsrcCache.smali",
"radiant/WordLyrics.smali",
"radiant/WordLyricsTick.smali",
"radiant/SylBrush.smali",
"radiant/SylLayout.smali"
],
"advancedOptions": [
{
"type": "toggle",
"key": "romanize",
"title": "Romanization",
"description": "Return a romanized transcription and show that instead of the original script",
"default": false,
"token": "RL_ROMANIZE",
"inline": true
},
{
"type": "toggle",
"key": "synthesize",
"title": "WIP | AI Generate Syllables",
"description": "[Only works with API results] - Generates Syllable timings from Line timings using a self trained AI model ",
"default": false,
"inline": true,
"token": "RL_SYNTHESIZE"
},
{
"type": "toggle",
"key": "context_aware",
"title": "Context Aware Lyrics",
"description": "Display voice context cues (adlibs and duet singers get displayed seperately)",
"default": true,
"token": "RL_CTX_ON"
},
{
"type": "choice",
"key": "context_aware_mode",
"title": "Show",
"description": "",
"entries": [
"Adlibs",
"Singers",
"Both"
],
"values": [
"0x1",
"0x2",
"0x3"
],
"defaultIndex": 0,
"requiresOption": "context_aware",
"token": "RL_CTX"
}
]
},
{
"id": "PlayerBackdrop",
"order": 30,
"fileNames": [],
"title": "Player Backdrop",
"description": "Lets you fully adjust how the player backdrop blur looks and behaves",
"default": true,
"defaultVariantIndex": 2,
"variants": [
{
"title": "Static",
"fileNames": [
"player-backdrop-static.patch"
],
"extensionFiles": [
"radiant/BackdropFx.smali"
]
},
{
"title": "Animated",
"fileNames": [
"player-backdrop-animated.patch"
],
"extensionFiles": [
"radiant/BackdropFx.smali"
]
},
{
"title": "Playback",
"fileNames": [
"player-backdrop-playback.patch"
],
"extensionFiles": [
"radiant/BackdropFx.smali"
]
}
],
"advancedOptions": [
{
"type": "slider",
"key": "blur_strength",
"title": "Blur Strength",
"description": "The value of Blur applied to the backdrop cover",
"default": 50,
"min": 0,
"max": 100,
"steps": 19,
"displayAsPercent": true,
"token": "RL_BLUR",
"encode": {
"kind": "int",
"scale": 1.0
}
},
{
"type": "slider",
"key": "contrast",
"title": "Contrast",
"description": "The value of Contrast applied to the backdrop cover",
"default": 50,
"min": 0,
"max": 100,
"steps": 19,
"displayAsPercent": true,
"token": "RL_CONTRAST",
"encode": {
"kind": "floatBits",
"scale": 0.02
}
},
{
"type": "slider",
"key": "saturation",
"title": "Saturation",
"description": "The value of Saturation applied to the backdrop cover",
"default": 50,
"min": 0,
"max": 100,
"steps": 19,
"displayAsPercent": true,
"token": "RL_SATURATION",
"encode": {
"kind": "floatBits",
"scale": 0.02
}
},
{
"type": "slider",
"key": "brightness",
"title": "Brightness",
"description": "The value of Brightness applied to the backdrop cover",
"default": 50,
"min": 0,
"max": 100,
"steps": 19,
"displayAsPercent": true,
"token": "RL_BRIGHTNESS",
"encode": {
"kind": "floatBits",
"scale": 0.02
}
},
{
"type": "toggle",
"key": "cover_everywhere",
"title": "Cover Everywhere - WIP",
"description": "Applies TIDAL's native blurred album-art backdrop to every Compose based page (Home & Collection)",
"default": false,
"inline": true,
"fileNames": [
"home-backdrop.patch",
"collection-backdrop.patch",
"cover-capture.patch"
]
}
]
},
{
"id": "QualityBadgeColors",
"order": 36,
"fileNames": [
"player-quality-badge-colors.patch"
],
"title": "Quality Badge Colors",
"description": "Brings color back to the player quality badge.",
"default": true
},
{
"id": "PlayerMovePlayingFrom",
"order": 37,
"fileNames": [
"player-move-playing-from.patch"
],
"extensionFiles": [
"radiant/PlayingFrom.smali"
],
"title": "Playing From",
"description": "Shows the source name (playlist/album/mix) above the track title",
"default": true
},
{
"id": "PlayerOneHanded",
"order": 38,
"fileNames": [
"player-one-handed.patch"
],
"title": "One-Handed Mode",
"description": "Mirrors the bottom player buttons left-to-right for easier one-handed reach.",
"default": false
},
{
"id": "LyricsProgressPill",
"order": 40,
"fileNames": [
"lyrics-progress-pill.patch",
"lyrics-fade-region.patch"
],
"title": "Lyrics Progress Pill",
"description": "Restores the old Track Progress Pill in the top of the Lyrics screen!",
"default": true,
"requires": [
"LyricsDisableCover",
"LyricsReplaceLyricsButton",
"LyricsReplaceShareButton"
]
},
{
"id": "LyricsDisableCover",
"order": 41,
"fileNames": [
"lyrics-disable-cover.patch"
],
"title": "Disable Lyrics Cover",
"description": "Removes the mini track cover from the lyrics screen.",
"default": true
},
{
"id": "LyricsReplaceLyricsButton",
"order": 42,
"fileNames": [
"lyrics-replace-lyrics-button.patch",
"lyrics-sparkle-conditional-visibility.patch"
],
"title": "Replace Lyrics Button",
"description": "Replaces the Lyrics button with the RL Sparkle!",
"default": true
},
{
"id": "LyricsReplaceShareButton",
"order": 43,
"fileNames": [
"lyrics-replace-share-button.patch"
],
"title": "Replace Share Button",
"description": "Replaces the share button with the Tidal Connect button from the top of the screen. (it's in the menu already)",
"default": true
},
{
"id": "MiniPlayerRedesign",
"order": 50,
"fileNames": [],
"title": "Redesigned Mini-Player",
"description": "Integrates the Seekbar into the control panel at the bottom of the screen.",
"default": false,
"defaultVariantIndex": 2,
"variants": [
{
"title": "Floating",
"fileNames": []
},
{
"title": "Grey",
"fileNames": [
"mini-player-grey.patch"
]
},
{
"title": "Black",
"fileNames": [
"mini-player-black.patch"
]
}
],
"advancedOptions": [
{
"type": "toggle",
"key": "dynamic_bg",
"title": "Mini-Player Dynamic Background",
"description": "Colorizes the Mini-Player depending on the currently playing track.",
"default": false,
"inline": true,
"fileNames": [
"mini-player-dynamic-bg.patch"
],
"extensionFiles": [
"radiant/MiniPlayerBackground.smali"
],
"hidesVariants": [
2
],
"relabelVariants": {
"1": "Legacy"
}
},
{
"type": "toggle",
"key": "border",
"title": "Progress Border",
"description": "Draws an animated progress ring around the floating mini-player. Requires the Floating style.",
"default": false,
"requiresVariant": 0,
"fileNames": [
"mini-player-floating-border.patch"
],
"extensionFiles": [
"radiant/MiniSeekerFloating.smali"
]
},
{
"type": "toggle",
"key": "gestures",
"title": "Mini-Player Gestures",
"description": "Adds swipe up gesture to the mini-player to reopen the player",
"default": true,
"fileNames": [
"mini-player-up-gesture.patch",
"player-swipe-gesture.patch",
"lyrics-gesture-scroll-guard.patch"
],
"extensionFiles": [
"radiant/gestures/MiniPlayerGestures.smali",
"radiant/gestures/MiniPlayerGestures$Gesture.smali",
"radiant/gestures/MiniPlayerGestures$FeedbackLayer.smali",
"radiant/gestures/MiniPlayerGestures$FeedbackResetAnimator.smali",
"radiant/gestures/MiniPlayerGestures$RootGesture.smali",
"radiant/gestures/MiniPlayerGestures$ApplyPending.smali",
"radiant/gestures/LyricsDrag.smali",
"radiant/gestures/LyricsDrag$Guard.smali"
]
},
{
"type": "choice",
"key": "swipe_up_drag",
"title": "Swipe Up Style",
"entries": [
"Static",
"Drag"
],
"values": [
"0x0",
"0x1"
],
"defaultIndex": 1,
"requiresOption": "gestures",
"token": "RL_MINI_PLAYER_SWIPE_UP_DRAG"
},
{
"type": "toggle",
"key": "next_prev",
"title": "Next/Previous Gestures",
"description": "Swipe left or right on the mini-player to skip tracks.",
"default": false,
"requiresOption": "gestures",
"fileNames": [
"mini-player-left-right-gesture.patch"
],
"extensionFiles": [
"radiant/gestures/MiniPlayerTrackGestures.smali",
"radiant/gestures/MiniPlayerTrackGestures$Gesture.smali",
"radiant/gestures/MiniPlayerTrackGestures$OffsetLayer.smali",
"radiant/gestures/MiniPlayerTrackGestures$ResetAnimator.smali",
"radiant/gestures/MiniPlayerTrackGestures$TextDraw.smali",
"com/tidal/android/feature/appscaffold/ui/r$c.smali"
]
}
]
},
{
"id": "LyricsKeepControlsVisible",
"order": 60,
"fileNames": [
"lyrics-keep-controls-visible.patch"
],
"extensionFiles": [
"radiant/LyricsReinitGuard.smali"
],
"title": "Keep Controls Visible",
"description": "Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off. Also stops the open lyrics view from randomly closing when the player re-fetches the same track.",
"default": true
},
{
"id": "WazeIntegration",
"order": 90,
"category": "integration",
"pathLocked": true,
"fileNames": [
"integration-waze-playback.patch"
],
"extensionFiles": [
"radiant/WazeInitReceiver.smali",
"radiant/WazeServiceConnection.smali"
],
"title": "Waze Integration",
"description": "Control TIDAL playback from the Waze app",
"default": true,
"advancedOptions": [
{
"type": "choice",
"key": "browse_root",
"title": "Media Menu",
"entries": [
"Auto Menu",
"Home",
"Recents"
],
"values": [
"ROOT_AUTO",
"HOME_V2::home_page_v2_id",
"RECENTLY_PLAYED::home/pages/CONTINUE_LISTEN_TO/view-all"
],
"defaultIndex": 0,
"token": "RL_WAZE_ROOT_ID"
},
{
"type": "color",
"key": "accent_color",
"title": "Accent Color",
"default": -16747037,
"token": "RL_WAZE_THEME_COLOR"
}
]
},
{
"id": "DebugMenuUnlock",
"order": 100,
"fileNames": [
"debug-menu-unlock.patch"
],
"title": "Unlock Debug Menu",
"description": "Reveals TIDAL's hidden Debug Menu in Settings. (Unlocks Feature Flags & the legacy Debug Options)",
"default": false
}
]
}
@@ -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.home.HomeModel
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.patchopts.PatchOptionsModel
import com.meowarex.rlmobile.ui.screens.permissions.PermissionsModel
@@ -64,7 +63,6 @@ class ManagerApplication : Application() {
factoryOf(::PatchOptionsModel)
factoryOf(::ComponentOptionsModel)
factoryOf(::LogScreenModel)
factoryOf(::LogsListScreenModel)
factoryOf(::PermissionsModel)
viewModelOf(::UpdaterViewModel)
})
@@ -12,8 +12,6 @@ import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.util.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -21,52 +19,19 @@ import kotlin.time.Duration
import kotlin.time.Instant
/**
* Central manager for storing all attempted installations and
* their associated logs/crashes (not including manager crashes themselves).
* In-memory store for the most recent install attempts
*/
class InstallLogManager(
private val application: Application,
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>()
/**
* Lists all the install data entries that exist on disk, sorted decreasing by
* 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 }
fun fetchInstallData(id: String): InstallLogData? = synchronized(entries) {
entries[id]
}
/**
* 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(
id: String,
installDate: Instant,
@@ -75,8 +40,6 @@ class InstallLogManager(
log: String,
error: Throwable?,
) {
val path = logsDir.resolve("$id.json")
val data = InstallLogData(
id = id,
installDate = installDate,
@@ -87,16 +50,16 @@ class InstallLogManager(
errorStacktrace = error?.let { Log.getStackTraceString(it).trimEnd() },
)
try {
path.writeText(json.encodeToString(data))
} catch (e: IOException) {
Log.e(BuildConfig.TAG, "Failed to write log to disk", e)
synchronized(entries) {
entries[id] = data
// Keep only the most recent few in memory.
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")
@SuppressLint("UsableSpace")
suspend fun getEnvironmentInfo(): String {
@@ -139,6 +102,10 @@ class InstallLogManager(
SOC: $soc
""".trimIndent()
}
private companion object {
const val MAX_ENTRIES = 8
}
}
@Immutable
@@ -22,7 +22,8 @@ class PathManager(
val patchingDownloadDir = patchingDir.resolve("downloads")
val cacheDownloadDir = context.cacheDir.resolve("downloads")
// Persistent Stash of assets for Offline Repatching
val cacheDownloadDir = patchingDir.resolve("downloads-cache")
val customComponentsDir = patchingDir.resolve("custom")
@@ -34,11 +35,15 @@ class PathManager(
val patchedApk = patchingWorkingDir.resolve("patched.apk")
// Persistent snapshot of the last successful release fetch (build info + patches asset URL)
val cachedReleaseInfo = patchingDir.resolve("release-info.json")
fun clearCache() {
val targets = arrayOf(
patchingDownloadDir,
patchingWorkingDir,
cacheDownloadDir,
cachedReleaseInfo,
context.cacheDir,
)
for (dir in targets) {
@@ -54,6 +59,16 @@ class PathManager(
.resolve("patches")
.resolve("$version.zip")
fun hasCachedTidalApk(version: Int, split: String = "base"): Boolean {
val rel = "tidal/$version/$split.apk"
return patchingDownloadDir.resolve(rel).exists() || cacheDownloadDir.resolve(rel).exists()
}
fun hasCachedSmaliPatches(version: SemVer): Boolean {
val rel = "patches/$version.zip"
return patchingDownloadDir.resolve(rel).exists() || cacheDownloadDir.resolve(rel).exists()
}
fun customTidalApks() = customTidalApksDir.listFiles()?.asList() ?: emptyList()
fun customSmaliPatches() = customPatchesDir.listFiles()?.asList() ?: emptyList()
@@ -12,7 +12,8 @@ class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager
var devMode by booleanPreference("dev_mode", false)
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PackageInstaller)
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 autoUpdateCheck by booleanPreference("auto_update_check", true)
@@ -0,0 +1,32 @@
package com.meowarex.rlmobile.manager
import android.util.Log
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.network.models.RLBuildInfo
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
// Snapshot of the last successful release fetch (build info + patches asset URL)
@Serializable
data class CachedReleaseInfo(
val data: RLBuildInfo,
val patchesAssetUrl: String,
)
object ReleaseInfoCache {
fun load(paths: PathManager, json: Json): CachedReleaseInfo? = try {
paths.cachedReleaseInfo
.takeIf { it.exists() }
?.let { json.decodeFromString<CachedReleaseInfo>(it.readText()) }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to read cached release info", t)
null
}
fun save(paths: PathManager, json: Json, info: CachedReleaseInfo) = try {
paths.cachedReleaseInfo.parentFile?.mkdirs()
paths.cachedReleaseInfo.writeText(json.encodeToString(info))
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to cache release info", t)
}
}
@@ -26,11 +26,16 @@ class PatchManifestStep(private val options: PatchOptions) : Step() {
?: throw IllegalArgumentException("No manifest found in APK")
container.log("Patching manifest")
val wazeDefault = container.getStep<SmaliPatchStep>().specs
.firstOrNull { it.id == "WazeIntegration" }?.defaultEnabled ?: false
val enableWazeIntegration = options.patchStates["WazeIntegration"] ?: wazeDefault
val patchedManifest = ManifestPatcher.patchManifest(
manifestBytes = manifest,
packageName = options.packageName,
appName = options.appName,
debuggable = options.debuggable,
enableWazeIntegration = enableWazeIntegration,
)
container.log("Repacking apk with patched manifest")
@@ -1,5 +1,6 @@
package com.meowarex.rlmobile.patcher.steps.patch
import android.content.Context
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.patcher.StepRunner
@@ -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.download.CopyDependenciesStep
import com.meowarex.rlmobile.patcher.steps.download.DownloadPatchesStep
import com.meowarex.rlmobile.ui.screens.patchopts.PatchManifest
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
import com.meowarex.rlmobile.ui.screens.patchopts.builtinPatchSpecs
import com.android.tools.smali.baksmali.Baksmali
import com.android.tools.smali.baksmali.BaksmaliOptions
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.UnifiedDiffUtils
import com.github.difflib.patch.Patch
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import java.io.*
@@ -27,6 +32,11 @@ class SmaliPatchStep(
private val options: PatchOptions,
) : Step(), IDexProvider, KoinComponent {
private val paths: PathManager by inject()
private val json: Json by inject()
private val context: Context by inject()
var specs: List<PatchSpec> = emptyList()
private set
override val group = StepGroup.Patch
override val localizedName = R.string.patch_step_patch_smali
@@ -42,6 +52,29 @@ class SmaliPatchStep(
val patches = mutableListOf<LoadedPatch>()
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.
container.log("Loading patches from smali patch archive: ${patchesZip.absolutePath}")
smaliDir.mkdirs()
@@ -53,6 +86,11 @@ class SmaliPatchStep(
if (patchFile.endsWith(".smali") && patchFile.startsWith("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)
// Guard against zip-slip: a crafted entry could otherwise escape smaliDir.
val baseCanonical = smaliDir.canonicalPath + File.separator
@@ -63,7 +101,19 @@ class SmaliPatchStep(
val entry = zip.openEntry(patchFile)
?: throw FileNotFoundException("Missing zip entry: $patchFile")
out.canonicalFile.parentFile?.mkdirs()
out.writeBytes(entry.read())
var smaliText = entry.read()
.decodeToString()
.replace("\r\n", "\n")
for ((token, value) in substitutions) {
if (smaliText.contains(token)) {
smaliText = smaliText.replace(token, value)
container.log("Applied substitution $token -> $value in $patchFile")
}
}
UNRESOLVED_TOKEN.find(smaliText)?.let { match ->
throw Error("Unresolved option placeholder ${match.value} in $patchFile")
}
out.writeText(smaliText)
container.log("Extracted extension smali: $relative")
continue
}
@@ -71,16 +121,39 @@ class SmaliPatchStep(
if (!patchFile.endsWith(".patch")) continue
val basename = patchFile.substringAfterLast('/')
if (basename in options.disabledPatches) {
if (basename in disabledFiles) {
container.log("Skipping disabled patch $patchFile")
continue
}
val lines = zip.openEntry(patchFile)!!.read()
var patchText = zip.openEntry(patchFile)!!.read()
.decodeToString()
.replace("\r\n", "\n") // Replace CRLF endings with LF
.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 {
for (directive in lines) {
@@ -92,18 +165,25 @@ class SmaliPatchStep(
container.log("Recorded rl-locals bump: $smaliPath method≈\"$methodSubstring\" >= $newValue")
}
val targetLine = lines.firstOrNull { it.startsWith("--- a/") }
?: throw Error("Patch $patchFile is missing a '--- a/...' header")
val fullClassName = targetLine
.removePrefix("--- a/")
.removeSuffix(".smali")
.trim()
val patch = LoadedPatch(
fullClassName = fullClassName,
patch = UnifiedDiffUtils.parseUnifiedDiff(lines),
)
patches.add(patch)
container.log("Loaded patch file $patchFile for class ${patch.fullClassName}")
// Split into per-target sections — a single .patch may contain multiple
// `--- a/...` blocks targeting different classes.
val sections = splitMultiTargetPatch(lines)
if (sections.isEmpty()) {
throw Error("Patch $patchFile is missing a '--- a/...' header")
}
for (section in sections) {
val targetLine = section.first { it.startsWith("--- a/") }
val fullClassName = targetLine
.removePrefix("--- a/")
.removeSuffix(".smali")
.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) {
throw Error("Failed to parse patch file $patchFile", t)
}
@@ -313,6 +393,29 @@ class SmaliPatchStep(
private companion object {
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)
}
}
}
@@ -1,19 +1,30 @@
package com.meowarex.rlmobile.patcher.steps.prepare
import android.app.Application
import android.util.Log
import androidx.compose.runtime.Stable
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.CachedReleaseInfo
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.manager.ReleaseInfoCache
import com.meowarex.rlmobile.network.models.RLBuildInfo
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
import com.meowarex.rlmobile.network.utils.getOrThrow
import com.meowarex.rlmobile.patcher.StepRunner
import com.meowarex.rlmobile.patcher.steps.StepGroup
import com.meowarex.rlmobile.patcher.steps.base.Step
import com.meowarex.rlmobile.util.isOnline
import kotlinx.serialization.json.Json
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import kotlin.coroutines.cancellation.CancellationException
@Stable
class FetchInfoStep : Step(), KoinComponent {
private val github: RadiantLyricsGithubService by inject()
private val context: Application by inject()
private val paths: PathManager by inject()
private val json: Json by inject()
override val group = StepGroup.Prepare
override val localizedName = R.string.patch_step_fetch_info
@@ -25,6 +36,33 @@ class FetchInfoStep : Step(), KoinComponent {
private set
override suspend fun execute(container: StepRunner) {
// Fetch the latest release and refresh the cache (when online)
if (context.isOnline()) {
try {
fetchFromRemote(container)
return
} catch (e: CancellationException) {
throw e
} catch (t: Throwable) {
container.log("Failed to fetch latest release: ${Log.getStackTraceString(t)}")
container.log("Falling back to cached release info for a local re-patch")
}
} else {
container.log("No network connection — using cached release info for a local re-patch")
}
val cached = ReleaseInfoCache.load(paths, json)
?: throw IllegalStateException(
"Could not fetch the latest release and no cached release info is available. " +
"Connect to the internet and try again."
)
data = cached.data
patchesAssetUrl = cached.patchesAssetUrl
container.log("Using cached build info: $data")
container.log("Cached patches asset URL: $patchesAssetUrl")
}
private suspend fun fetchFromRemote(container: StepRunner) {
container.log("Fetching latest release from ${RadiantLyricsGithubService.REPO_OWNER}/${RadiantLyricsGithubService.REPO_NAME}")
val release = github.getLatestRelease(force = true).getOrThrow()
@@ -33,14 +71,19 @@ class FetchInfoStep : Step(), KoinComponent {
?.browserDownloadUrl
?: throw IllegalStateException("No ${RadiantLyricsGithubService.DATA_JSON_ASSET_NAME} asset found in latest release ${release.tagName}")
patchesAssetUrl = release.assets
val patchesUrl = release.assets
.find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME }
?.browserDownloadUrl
?: throw IllegalStateException("No ${RadiantLyricsGithubService.PATCHES_ASSET_NAME} asset found in latest release ${release.tagName}")
container.log("Fetching build info from $dataJsonUrl")
data = github.getBuildInfo(dataJsonUrl, force = true).getOrThrow()
val buildInfo = github.getBuildInfo(dataJsonUrl, force = true).getOrThrow()
data = buildInfo
patchesAssetUrl = patchesUrl
container.log("Fetched build info: $data")
container.log("Patches asset URL: $patchesAssetUrl")
ReleaseInfoCache.save(paths, json, CachedReleaseInfo(buildInfo, patchesUrl))
}
}
@@ -18,12 +18,16 @@ object ManifestPatcher {
private const val IS_SPLIT_REQUIRED = "isSplitRequired"
private const val REQUIRED_SPLIT_TYPES = "requiredSplitTypes"
private const val SPLIT_TYPES = "splitTypes"
private const val WAZE_PACKAGE = "com.waze"
private const val WAZE_INIT_RECEIVER = "radiant.WazeInitReceiver"
private const val WAZE_ACTION_INIT = "com.waze.sdk.audio.ACTION_INIT"
fun patchManifest(
manifestBytes: ByteArray,
packageName: String,
appName: String,
debuggable: Boolean,
enableWazeIntegration: Boolean,
): ByteArray {
// Extract original package name to rewrite every reference to it
// (permissions, provider authorities) to the new packageName.
@@ -84,6 +88,49 @@ object ManifestPatcher {
}
return when (name) {
"queries" -> object : NodeVisitor(nv) {
private var hasWazePackage = false
override fun child(ns: String?, name: String): NodeVisitor {
val visitor = super.child(ns, name)
return if (enableWazeIntegration && name == "package") {
object : NodeVisitor(visitor) {
override fun attr(
ns: String?,
name: String?,
resourceId: Int,
type: Int,
value: Any?,
) {
if (name == "name" && value == WAZE_PACKAGE) {
hasWazePackage = true
}
super.attr(ns, name, resourceId, type, value)
}
}
} else {
visitor
}
}
override fun end() {
if (enableWazeIntegration && !hasWazePackage) {
super.child(null, "package").apply {
attr(
ANDROID_NAMESPACE,
"name",
android.R.attr.name,
TYPE_STRING,
WAZE_PACKAGE,
)
end()
}
}
super.end()
}
}
"uses-permission" -> object : NodeVisitor(nv) {
override fun attr(ns: String?, name: String?, resourceId: Int, type: Int, value: Any?) {
if (name != "maxSdkVersion") {
@@ -132,6 +179,7 @@ object ManifestPatcher {
private var addUseEmbeddedDex = true
private var addExtractNativeLibs = true
private var addMetadata = true
private var hasWazeInitReceiver = false
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
if (name == REQUEST_LEGACY_EXTERNAL_STORAGE) addLegacyStorage = false
@@ -161,6 +209,22 @@ object ManifestPatcher {
return when (name) {
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName))
"receiver" -> object : NodeVisitor(visitor) {
override fun attr(
ns: String?,
name: String,
resourceId: Int,
type: Int,
value: Any?
) {
if (name == "name" && value == WAZE_INIT_RECEIVER) {
hasWazeInitReceiver = true
}
super.attr(ns, name, resourceId, type, value)
}
}
"provider" -> object : NodeVisitor(visitor) {
override fun attr(
ns: String?,
@@ -219,6 +283,39 @@ object ManifestPatcher {
0
)
if (enableWazeIntegration && !hasWazeInitReceiver) {
super.child(null, "receiver").apply {
attr(
ANDROID_NAMESPACE,
"name",
android.R.attr.name,
TYPE_STRING,
WAZE_INIT_RECEIVER,
)
attr(
ANDROID_NAMESPACE,
"exported",
android.R.attr.exported,
TYPE_INT_BOOLEAN,
1,
)
child(null, "intent-filter").apply {
child(null, "action").apply {
attr(
ANDROID_NAMESPACE,
"name",
android.R.attr.name,
TYPE_STRING,
WAZE_ACTION_INIT,
)
end()
}
end()
}
end()
}
}
super.end()
}
}
@@ -1,96 +0,0 @@
package com.meowarex.rlmobile.ui.components.dialogs
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.meowarex.rlmobile.R
@Composable
fun NetworkWarningDialog(
onConfirm: (neverShow: Boolean) -> Unit,
onDismiss: (neverShow: Boolean) -> Unit,
) {
val interactionSource = remember(::MutableInteractionSource)
var neverShow by rememberSaveable { mutableStateOf(false) }
val rememberedNeverShow by rememberUpdatedState(neverShow)
AlertDialog(
onDismissRequest = { onDismiss(rememberedNeverShow) },
properties = DialogProperties(
dismissOnClickOutside = false,
),
confirmButton = {
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) },
colors = ButtonDefaults.textButtonColors(
contentColor = MaterialTheme.colorScheme.onErrorContainer
),
) {
Text(stringResource(R.string.navigation_back))
}
},
title = { Text(stringResource(R.string.network_warning_title)) },
text = {
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = stringResource(R.string.network_warning_body),
textAlign = TextAlign.Center,
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable(
interactionSource = interactionSource,
indication = null,
onClick = { neverShow = !rememberedNeverShow },
)
.padding(end = 16.dp)
) {
Checkbox(
checked = neverShow,
onCheckedChange = { neverShow = it },
interactionSource = interactionSource,
)
Text(stringResource(R.string.network_warning_disable))
}
}
},
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,
)
}
@@ -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 = {},
)
}
}
@@ -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 = {},
)
}
}
@@ -2,11 +2,14 @@ package com.meowarex.rlmobile.ui.previews.screens
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.*
import com.meowarex.rlmobile.network.utils.SemVer
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.screens.patchopts.*
import com.meowarex.rlmobile.ui.theme.ManagerTheme
import kotlinx.serialization.json.Json
import kotlin.time.Clock
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
@@ -18,25 +21,37 @@ private fun PatchOptionsScreenPreview(
@PreviewParameter(PatchOptionsParametersProvider::class)
parameters: PatchOptionsParameters,
) {
val context = LocalContext.current
val specs = remember { builtinPatchSpecs(context, Json { ignoreUnknownKeys = true }) }
ManagerTheme {
PatchOptionsScreenContent(
isUpdate = parameters.isUpdate,
isDevMode = parameters.isDevMode,
debuggable = parameters.debuggable,
setDebuggable = {},
bypassIncompatible = false,
setBypassIncompatible = {},
appName = parameters.appName,
appNameIsError = parameters.appNameIsError,
setAppName = {},
packageName = parameters.packageName,
packageNameState = parameters.packageNameState,
setPackageName = {},
packageNameLocked = parameters.packageName == PatchOptions.Default.packageName,
onUnlockPackageName = {},
onLockPackageName = {},
customTidalApk = parameters.customTidalApk,
onSelectCustomTidalApk = {},
customPatches = parameters.customPatches,
onSelectCustomPatches = {},
enabledPatchCount = KnownPatch.All.size,
specs = specs,
isPatchEnabled = { true },
onTogglePatch = { _, _ -> },
patchLockState = { PatchLock.Free },
variantIndex = { 0 },
onSelectVariant = { _, _ -> },
optionState = PatchOptionState.Preview,
isConfigValid = parameters.isConfigValid,
onInstall = {},
)
@@ -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:18AM",
durationSecs = 18.555f,
stacktracePreview = null,
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = true,
installDate = "7 min. ago, 10:17AM",
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:37PM",
durationSecs = 58.439f,
stacktracePreview = null,
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = true,
installDate = "Yesterday, 11:17PM",
durationSecs = 24.405f,
stacktracePreview = persistentListOf(
"java.lang.Error: Installation was aborted or cancelled",
),
),
LogEntry(
id = UUID.randomUUID().toString(),
isError = true,
installDate = "Yesterday, 11:17PM",
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:11PM",
durationSecs = 210.539f,
stacktracePreview = null,
),
)
@@ -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 = {},
)
}
}
@@ -19,7 +19,9 @@ import cafe.adriel.voyager.core.model.screenModelScope
import com.github.diamondminer88.zip.ZipReader
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.manager.PreferencesManager
import com.meowarex.rlmobile.manager.ReleaseInfoCache
import com.meowarex.rlmobile.network.models.GithubCommit
import com.meowarex.rlmobile.network.models.RLBuildInfo
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
@@ -42,6 +44,7 @@ class HomeModel(
private val github: RadiantLyricsGithubService,
private val json: Json,
private val prefs: PreferencesManager,
private val paths: PathManager,
) : ScreenModel {
var state by mutableStateOf<HomeState>(HomeState.Loading)
@@ -117,18 +120,22 @@ class HomeModel(
val remote = remoteDataJson
val currentPatches = remote?.patchesVersion?.toString()
val installedPatches = installMetadata?.patchesVersion?.toString()
val previousPatches = initialPrefPatchesVersion.ifEmpty {
installMetadata?.patchesVersion?.toString().orEmpty()
installedPatches.orEmpty()
}
val patchesFrom = previousPatches.takeIf { it.isNotEmpty() }
val patchesTo = currentPatches ?: previousPatches.ifEmpty { "?" }
val patchesUpdateAvailable = installedPatches != null
&& currentPatches != null
&& installedPatches != currentPatches
add(
VersionDelta(
label = application.getString(R.string.manager_update_row_patches),
iconRes = R.drawable.ic_extension,
from = patchesFrom,
to = patchesTo,
tag = if (patchesFrom != null && patchesFrom != patchesTo)
tag = if (patchesUpdateAvailable)
application.getString(R.string.manager_update_tag_available) else null,
)
)
@@ -142,13 +149,16 @@ class HomeModel(
val tidalTo = currentTidal?.toString()
?: previousTidal.takeIf { it > 0 }?.toString()
?: "?"
val tidalUpdateAvailable = installedTidalVersionCode > 0
&& currentTidal != null
&& installedTidalVersionCode != currentTidal
add(
VersionDelta(
label = application.getString(R.string.manager_update_row_tidal),
iconRes = R.drawable.ic_music_note,
from = tidalFrom,
to = tidalTo,
tag = if (tidalFrom != null && tidalFrom != tidalTo)
tag = if (tidalUpdateAvailable)
application.getString(R.string.manager_update_tag_available) else null,
)
)
@@ -163,16 +173,23 @@ class HomeModel(
refreshingLock.withLock {
val pkg = fetchInstalled()
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
remote.await()
// Skip the remote fetch entirely when offline
val online = application.isOnline()
if (online) {
val remote = async(Dispatchers.IO) { if (remoteDataJson == null) fetchRemoteData() }
remote.await()
}
val install = pkg?.toInstallData()
val latest = remoteDataJson?.tidalVersionCode
val offlineRepatchReady = hasOfflineRepatchAssets()
mainThread {
state = HomeState.Loaded(
install = install,
latestTidalVersionCode = latest,
offline = !online,
offlineRepatchReady = offlineRepatchReady,
)
maybeCheckManagerUpdate(pkg)
}
@@ -250,6 +267,13 @@ class HomeModel(
)
}
// check if an offline "Local Repatch" is possible
private fun hasOfflineRepatchAssets(): Boolean {
val info = ReleaseInfoCache.load(paths, json) ?: return false
return paths.hasCachedTidalApk(info.data.tidalVersionCode) &&
paths.hasCachedSmaliPatches(info.data.patchesVersion)
}
private suspend fun fetchRemoteData() {
val release = try {
github.getLatestRelease().fold(
@@ -14,6 +14,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.activity.ComponentActivity
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -32,13 +33,14 @@ import com.meowarex.rlmobile.ui.components.SegmentedButton
import com.meowarex.rlmobile.ui.components.Tag
import com.meowarex.rlmobile.ui.screens.about.AboutScreen
import com.meowarex.rlmobile.ui.screens.home.components.CommitList
import com.meowarex.rlmobile.ui.screens.logs.LogsListScreen
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionsScreen
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
import com.meowarex.rlmobile.ui.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 {
@@ -50,6 +52,9 @@ class HomeScreen : Screen, Parcelable {
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)
@@ -67,18 +72,20 @@ class HomeScreen : Screen, Parcelable {
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(LogsListScreen()) }) {
Icon(
painterResource(R.drawable.ic_receipt),
contentDescription = stringResource(R.string.navigation_logs),
)
}
IconButton(onClick = { navigator.push(SettingsScreen()) }) {
Icon(
painterResource(R.drawable.ic_settings),
@@ -107,6 +114,7 @@ class HomeScreen : Screen, Parcelable {
is HomeState.Loaded -> HomeContent(
state = state,
commits = model.commits,
managerUpdateAvailable = managerUpdateAvailable,
onInstall = { navigator.pushOnce(PatchOptionsScreen()) },
onRepatch = {
scope.launchIO {
@@ -134,6 +142,7 @@ class HomeScreen : Screen, Parcelable {
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,
@@ -200,15 +209,24 @@ private fun ColumnScope.HomeContent(
}
}
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 = state.latestTidalVersionCode != null,
enabled = buttonEnabled,
modifier = Modifier.fillMaxWidth(),
) {
val label = when {
state.latestTidalVersionCode == null -> "Loading…"
blockedByManagerUpdate -> "Manager Update Required"
state.offline && install != null -> "Local Repatch"
state.offline -> "No Network"
install == null && state.latestTidalVersionCode == null -> "Loading…"
install == null -> "Install"
install.isUpToDate == false -> "Update"
patchesBehind && tidalBehind -> "Update Patches & TIDAL"
patchesBehind -> "Update Patches"
tidalBehind -> "Update TIDAL"
else -> "Repatch"
}
Text(
@@ -221,6 +239,18 @@ private fun ColumnScope.HomeContent(
)
}
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),
@@ -8,5 +8,7 @@ sealed interface HomeState {
data class Loaded(
val install: InstallData?,
val latestTidalVersionCode: Int?,
val offline: Boolean = false,
val offlineRepatchReady: Boolean = false,
) : HomeState
}
@@ -1,99 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs
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.Scaffold
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier
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.ui.screens.log.LogScreen
import com.meowarex.rlmobile.ui.screens.logs.components.*
import com.meowarex.rlmobile.ui.screens.logs.components.dialogs.DeleteLogsDialog
import com.meowarex.rlmobile.ui.util.paddings.PaddingValuesSides
import com.meowarex.rlmobile.ui.util.paddings.exclude
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class LogsListScreen : Screen, Parcelable {
@IgnoredOnParcel
override val key: ScreenKey
get() = "LogsScreen"
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val model = koinScreenModel<LogsListScreenModel>()
var showWipeConfirmDialog by remember { mutableStateOf(false) }
if (showWipeConfirmDialog) {
DeleteLogsDialog(
onConfirm = {
showWipeConfirmDialog = false
model.deleteLogs()
},
onDismiss = {
showWipeConfirmDialog = false
},
)
}
LogsScreenContent(
logs = model.logEntries,
onOpenLog = { navigator.push(LogScreen(installId = it)) },
onDeleteLogs = { showWipeConfirmDialog = true },
)
}
}
@Composable
fun LogsScreenContent(
logs: SnapshotStateList<LogEntry>,
onOpenLog: (id: String) -> Unit,
onDeleteLogs: () -> Unit,
) {
Scaffold(
topBar = {
LogsListAppBar(
onDeleteLogs = onDeleteLogs,
)
},
) { paddingValues ->
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = paddingValues.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top),
modifier = Modifier
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(vertical = 12.dp, horizontal = 22.dp)
) {
if (logs.isEmpty()) {
item(key = "EMPTY") {
LogsNone(
modifier = Modifier.fillParentMaxSize(),
)
}
}
items(
items = logs,
contentType = { "LOG" },
key = { it.id },
) { data ->
LogEntryCard(
data = data,
onClick = remember(onOpenLog, data.id) { { onOpenLog(data.id) } },
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
@@ -1,71 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs
import android.app.Application
import android.text.format.DateUtils
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.mutableStateListOf
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.manager.InstallLogManager
import com.meowarex.rlmobile.util.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
class LogsListScreenModel(
private val logsManager: InstallLogManager,
private val application: Application,
) : ScreenModel {
/**
* All the loaded log entries sorted descending by creation date.
*/
val logEntries = mutableStateListOf<LogEntry>()
init {
loadLogsList()
}
fun deleteLogs() = screenModelScope.launchIO {
logsManager.deleteAllEntries()
mainThread {
logEntries.clear()
application.showToast(R.string.logs_status_delete_success)
}
}
private fun loadLogsList() = screenModelScope.launchIO {
for (installId in logsManager.fetchInstallDataEntries()) {
val data = logsManager.fetchInstallData(id = installId)
?: continue
val entry = LogEntry(
id = data.id,
isError = data.isError,
installDate = DateUtils.getRelativeDateTimeString(
/* c = */ application,
/* time = */ data.installDate.toEpochMilliseconds(),
/* minResolution = */ DateUtils.SECOND_IN_MILLIS,
/* transitionResolution = */ DateUtils.WEEK_IN_MILLIS,
/* flags = */ DateUtils.FORMAT_ABBREV_ALL,
).toString(),
durationSecs = data.installDuration.inWholeMilliseconds / 1000f,
stacktracePreview = data.errorStacktrace
?.splitToSequence('\n')
?.take(3)
?.toImmutableList(),
)
mainThread { logEntries += entry }
}
}
}
@Immutable
data class LogEntry(
val id: String,
val isError: Boolean,
val installDate: String,
val durationSecs: Float,
val stacktracePreview: ImmutableList<String>?,
)
@@ -1,122 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.components
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.key
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.patcher.steps.base.StepState
import com.meowarex.rlmobile.ui.screens.logs.LogEntry
import com.meowarex.rlmobile.ui.screens.patching.components.StepStateIcon
import com.meowarex.rlmobile.ui.screens.patching.components.TimeElapsed
@Composable
fun LogEntryCard(
data: LogEntry,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
val errorColor = MaterialTheme.colorScheme.error
ElevatedCard(
shape = RectangleShape,
modifier = modifier
.clickable(onClick = onClick)
.clip(RoundedCornerShape(topStart = 6.dp, 12.0.dp, bottomStart = 6.dp, bottomEnd = 12.0.dp))
.drawWithCache {
val color = when (data.isError) {
true -> errorColor
false -> Color(0xFF59B463)
}
onDrawWithContent {
drawContent()
drawRect(
color = color,
alpha = .8f,
topLeft = Offset.Zero,
size = Size(4.dp.toPx(), size.height),
)
}
}
) {
Row(
horizontalArrangement = Arrangement.spacedBy(14.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.padding(vertical = 18.dp, horizontal = 22.dp),
) {
StepStateIcon(
state = if (data.isError) StepState.Error else StepState.Success,
size = 24.dp,
)
Column {
Text(
text = when (data.isError) {
true -> stringResource(R.string.status_failed)
false -> stringResource(R.string.status_success)
},
)
Text(
text = data.installDate,
style = MaterialTheme.typography.labelSmall,
modifier = Modifier.alpha(.6f),
)
}
Spacer(Modifier.weight(1f, fill = true))
TimeElapsed(
seconds = data.durationSecs,
modifier = Modifier.alpha(.9f),
)
}
if (data.stacktracePreview != null) {
Column(
modifier = Modifier
.padding(start = 26.dp, end = 20.dp, bottom = 18.dp)
// https://stackoverflow.com/a/76270310/13964629
.graphicsLayer(
alpha = .95f,
compositingStrategy = CompositingStrategy.Offscreen,
)
.drawWithContent {
val colors = listOf(Color.Black, Color.Black, Color.Transparent)
drawContent()
drawRect(
brush = Brush.verticalGradient(colors),
blendMode = BlendMode.DstIn,
)
}
) {
// The stacktrace is separated into multiple Text elements because ellipsis is not supported per-line
for (line in data.stacktracePreview) key(line) {
Text(
text = line,
softWrap = false,
overflow = TextOverflow.Ellipsis,
lineHeight = 18.sp,
style = MaterialTheme.typography.labelSmall,
fontFamily = FontFamily.Companion.Monospace,
)
}
}
}
}
}
@@ -1,37 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.components
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.BackButton
import com.meowarex.rlmobile.ui.screens.settings.SettingsScreen
@Composable
fun LogsListAppBar(
onDeleteLogs: () -> Unit,
) {
TopAppBar(
navigationIcon = { BackButton() },
title = { Text(stringResource(R.string.logs_title)) },
actions = {
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()) }) {
Icon(
painter = painterResource(R.drawable.ic_settings),
contentDescription = stringResource(R.string.navigation_settings)
)
}
}
)
}
@@ -1,33 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.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.draw.alpha
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 LogsNone(modifier: Modifier = Modifier) {
Box(modifier = modifier) {
Column(
modifier = Modifier.align(Alignment.Center),
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
painter = painterResource(R.drawable.ic_reciept_off),
contentDescription = null,
)
Text(
text = stringResource(R.string.logs_none),
style = MaterialTheme.typography.bodyLarge,
modifier = Modifier.alpha(.8f),
)
}
}
}
@@ -1,53 +0,0 @@
package com.meowarex.rlmobile.ui.screens.logs.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 DeleteLogsDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
},
title = { Text(stringResource(R.string.logs_wipe_title)) },
text = {
Text(
text = stringResource(R.string.logs_wipe_desc),
textAlign = TextAlign.Center,
)
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(R.string.action_cancel))
}
}
)
}
@@ -28,7 +28,6 @@ import com.meowarex.rlmobile.patcher.steps.StepGroup
import com.meowarex.rlmobile.ui.components.MainActionButton
import com.meowarex.rlmobile.ui.components.Wakelock
import com.meowarex.rlmobile.ui.components.dialogs.InstallerAbortDialog
import com.meowarex.rlmobile.ui.components.dialogs.NetworkWarningDialog
import com.meowarex.rlmobile.ui.screens.log.LogScreen
import com.meowarex.rlmobile.ui.screens.patching.components.*
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptions
@@ -111,19 +110,6 @@ class PatchingScreen(
listState.animateScrollToItem(0)
}
if (model.showNetworkWarningDialog) {
NetworkWarningDialog(
onConfirm = { neverShow ->
model.hideNetworkWarning(neverShow)
model.install()
},
onDismiss = { neverShow ->
model.hideNetworkWarning(neverShow)
navigator.pop()
},
)
}
if (showAbortWarning) {
InstallerAbortDialog(
onDismiss = { showAbortWarning = false },
@@ -41,9 +41,6 @@ class PatchingScreenModel(
val devMode get() = prefs.devMode
var showNetworkWarningDialog by mutableStateOf(!alreadyShownNetworkWarning && application.isNetworkDangerous())
private set
var steps by mutableStateOf<ImmutableMap<StepGroup, ImmutableList<Step>>?>(null)
private set
@@ -52,11 +49,7 @@ class PatchingScreenModel(
private set
init {
if (!prefs.showNetworkWarning)
showNetworkWarningDialog = false
if (!showNetworkWarningDialog)
install()
install()
// Rotate fun facts every so often
screenModelScope.launch {
@@ -67,12 +60,6 @@ class PatchingScreenModel(
}
}
fun hideNetworkWarning(neverShow: Boolean) {
showNetworkWarningDialog = false
alreadyShownNetworkWarning = true
prefs.showNetworkWarning = !neverShow
}
fun launchApp() {
if (state.value !is PatchingScreenState.Success)
return
@@ -194,9 +181,6 @@ class PatchingScreenModel(
}
companion object {
// Global state to avoid showing the warning more than once per launch
private var alreadyShownNetworkWarning = false
/**
* Random fun facts to show on the installation screen.
*/
@@ -1,71 +0,0 @@
package com.meowarex.rlmobile.ui.screens.patchopts
import androidx.annotation.StringRes
import com.meowarex.rlmobile.R
enum class KnownPatch(
val fileNames: List<String>,
@StringRes val titleRes: Int,
@StringRes val descRes: Int,
val requires: List<KnownPatch> = emptyList(),
val disables: List<KnownPatch> = emptyList(),
) {
// Dependency-first order (later refs need backward resolution)
LyricsDisableCover(
fileNames = listOf("lyrics-disable-cover.patch"),
titleRes = R.string.patch_lyrics_disable_cover_title,
descRes = R.string.patch_lyrics_disable_cover_desc,
),
LyricsReplaceLyricsButton(
fileNames = listOf(
"lyrics-replace-lyrics-button.patch",
"lyrics-sparkle-conditional-visibility.patch",
),
titleRes = R.string.patch_lyrics_replace_button_title,
descRes = R.string.patch_lyrics_replace_button_desc,
),
LyricsReplaceShareButton(
fileNames = listOf("lyrics-replace-share-button.patch"),
titleRes = R.string.patch_lyrics_replace_share_button_title,
descRes = R.string.patch_lyrics_replace_share_button_desc,
),
PlayerBackdrop(
fileNames = listOf("player-backdrop.patch"),
titleRes = R.string.patch_player_backdrop_title,
descRes = R.string.patch_player_backdrop_desc,
),
DebugMenuUnlock(
fileNames = listOf("debug-menu-unlock.patch"),
titleRes = R.string.patch_debug_menu_unlock_title,
descRes = R.string.patch_debug_menu_unlock_desc,
),
LyricsProgressPill(
fileNames = listOf(
"lyrics-progress-pill.patch",
"lyrics-fade-region.patch",
),
titleRes = R.string.patch_lyrics_progress_pill_title,
descRes = R.string.patch_lyrics_progress_pill_desc,
requires = listOf(LyricsDisableCover, LyricsReplaceShareButton),
),
EnableLegacyUi(
fileNames = listOf("enable-legacy-ui.patch"),
titleRes = R.string.patch_enable_legacy_ui_title,
descRes = R.string.patch_enable_legacy_ui_desc,
requires = listOf(DebugMenuUnlock),
disables = listOf(
LyricsDisableCover,
LyricsReplaceLyricsButton,
LyricsReplaceShareButton,
PlayerBackdrop,
LyricsProgressPill,
),
);
companion object {
// Alphabetical by first filename, but pin DebugMenuUnlock to the bottom
val All: List<KnownPatch> = entries.sortedWith(
compareBy({ it == DebugMenuUnlock }, { it.fileNames.first() })
)
}
}
@@ -32,16 +32,128 @@ data class PatchOptions(
*/
val customPatches: PatchComponent? = null,
val disabledPatches: Set<String> = emptySet(),
val patchStates: Map<String, Boolean> = emptyMap(),
val selectedVariants: Map<String, Int> = emptyMap(),
val optionFloats: Map<String, Float> = emptyMap(),
val optionBools: Map<String, Boolean> = emptyMap(),
val optionInts: Map<String, Int> = emptyMap(),
) : Parcelable {
fun isEnabled(spec: PatchSpec): Boolean =
patchStates[spec.id] ?: spec.defaultEnabled
fun variantIndex(spec: PatchSpec): Int {
val stored = (selectedVariants[spec.id] ?: spec.defaultVariantIndex)
.coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
return spec.resolveVariantIndex(stored) { isToggleOn(spec, it) }
}
fun sliderValue(spec: PatchSpec, option: OptionSpec.Slider): Float =
(optionFloats["${spec.id}/${option.key}"] ?: option.default).coerceIn(option.min, option.max)
fun choiceIndex(spec: PatchSpec, option: OptionSpec.Choice): Int =
(optionInts["${spec.id}/${option.key}"] ?: option.defaultIndex)
.coerceIn(0, option.entries.lastIndex.coerceAtLeast(0))
fun colorValue(spec: PatchSpec, option: OptionSpec.Color): Int =
optionInts["${spec.id}/${option.key}"] ?: option.default
fun isToggleOn(spec: PatchSpec, option: OptionSpec.Toggle): Boolean =
optionBools["${spec.id}/${option.key}"] ?: option.default
fun isToggleActive(spec: PatchSpec, option: OptionSpec.Toggle): Boolean {
if (!isToggleOn(spec, option)) return false
option.requiresVariant?.let { if (variantIndex(spec) != it) return false }
option.requiresOption?.let { key ->
val required = spec.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.firstOrNull { it.key == key }
if (required != null && !isToggleOn(spec, required)) return false
}
return true
}
fun disabledPatchFiles(specs: List<PatchSpec>): Set<String> = buildSet {
for (spec in specs) {
val enabled = isEnabled(spec)
if (spec.variants.isEmpty()) {
if (!enabled) addAll(spec.fileNames)
} else {
// base fileNames apply for every variant while enabled; disable them when off
if (!enabled) addAll(spec.fileNames)
val selected = variantIndex(spec)
spec.variants.forEachIndexed { index, variant ->
if (!enabled || index != selected) addAll(variant.fileNames)
}
}
// Toggle sub-options that gate patch files.
for (option in spec.advancedOptions) {
if (option is OptionSpec.Toggle && option.fileNames.isNotEmpty()) {
if (!enabled || !isToggleActive(spec, option)) addAll(option.fileNames)
}
}
}
}
fun knownExtensionFiles(specs: List<PatchSpec>): Set<String> = buildSet {
for (spec in specs) {
addAll(spec.extensionFiles)
spec.variants.forEach { addAll(it.extensionFiles) }
spec.advancedOptions.forEach { if (it is OptionSpec.Toggle) addAll(it.extensionFiles) }
}
}
fun enabledExtensionFiles(specs: List<PatchSpec>): Set<String> = buildSet {
for (spec in specs) {
if (!isEnabled(spec)) continue
addAll(spec.extensionFiles)
if (spec.variants.isNotEmpty()) {
spec.variants.getOrNull(variantIndex(spec))?.let { addAll(it.extensionFiles) }
}
for (option in spec.advancedOptions) {
if (option is OptionSpec.Toggle && isToggleActive(spec, option)) addAll(option.extensionFiles)
}
}
}
fun smaliSubstitutions(specs: List<PatchSpec>): Map<String, String> = buildMap {
for (spec in specs) {
if (!isEnabled(spec)) continue
for (option in spec.advancedOptions) {
when (option) {
is OptionSpec.Toggle -> {
val token = option.token ?: continue
put("__${token}__", if (isToggleActive(spec, option)) "0x1" else "0x0")
}
is OptionSpec.Slider -> {
val token = option.token ?: continue
val encode = option.encode ?: continue
put("__${token}__", encode.encode(sliderValue(spec, option)))
}
is OptionSpec.Choice -> {
val token = option.token ?: continue
val gatedOff = option.requiresOption?.let { key ->
spec.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.firstOrNull { it.key == key }
?.let { !isToggleOn(spec, it) }
} ?: false
val index = if (gatedOff) 0 else choiceIndex(spec, option)
put("__${token}__", option.values.getOrNull(index) ?: continue)
}
is OptionSpec.Color -> {
val token = option.token ?: continue
put("__${token}__", colorValue(spec, option).toString())
}
}
}
}
}
companion object {
val Default = PatchOptions(
val Default: PatchOptions = PatchOptions(
appName = "TIDAL",
packageName = "com.aspiro.tidal",
debuggable = false,
customTidalApk = null,
customPatches = null,
disabledPatches = (KnownPatch.DebugMenuUnlock.fileNames + KnownPatch.EnableLegacyUi.fileNames).toSet(),
)
}
}
@@ -2,21 +2,35 @@ package com.meowarex.rlmobile.ui.screens.patchopts
import android.content.Context
import android.content.pm.PackageManager.NameNotFoundException
import android.util.Log
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.navigator.Navigator
import com.github.diamondminer88.zip.ZipReader
import com.meowarex.rlmobile.BuildConfig
import com.meowarex.rlmobile.manager.PathManager
import com.meowarex.rlmobile.manager.PreferencesManager
import com.meowarex.rlmobile.ui.screens.componentopts.ComponentOptionsScreen
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.util.pushForResult
import com.meowarex.rlmobile.util.*
import com.meowarex.rlmobile.manager.download.IDownloadManager
import com.meowarex.rlmobile.manager.download.KtorDownloadManager
import com.meowarex.rlmobile.network.services.RadiantLyricsGithubService
import com.meowarex.rlmobile.network.utils.getOrThrow
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import java.io.File
class PatchOptionsModel(
prefilledOptions: PatchOptions,
private val context: Context,
private val prefs: PreferencesManager,
private val paths: PathManager,
private val json: Json,
private val github: RadiantLyricsGithubService,
private val downloader: KtorDownloadManager,
) : ScreenModel {
var packageName by mutableStateOf(prefilledOptions.packageName)
private set
@@ -29,6 +43,24 @@ class PatchOptionsModel(
fetchPkgNameStateDebounced()
}
/** Patches flagged [PatchSpec.pathLocked] only work under the stock package name. */
fun isBlockedByPackageName(spec: PatchSpec): Boolean =
spec.pathLocked && packageName != PatchOptions.Default.packageName && !bypassIncompatible
/** The package-name field starts locked & editing requires confirming the unlock dialog */
var packageNameLocked by mutableStateOf(prefilledOptions.packageName == PatchOptions.Default.packageName)
private set
fun unlockPackageName() {
packageNameLocked = false
}
/** Re-locking snaps the package name back to the stock default */
fun lockPackageName() {
packageNameLocked = true
changePackageName(PatchOptions.Default.packageName)
}
var appName by mutableStateOf(prefilledOptions.appName)
private set
@@ -47,39 +79,173 @@ class PatchOptionsModel(
debuggable = value
}
var disabledPatches by mutableStateOf(prefilledOptions.disabledPatches)
/** Dev override: force-allow path-gated ("incompatible") patches under a non-stock package name. */
var bypassIncompatible by mutableStateOf(prefs.bypassIncompatible)
private set
fun isPatchEnabled(patch: KnownPatch): Boolean =
patch.fileNames.none { it in disabledPatches }
fun changeBypassIncompatible(value: Boolean) {
bypassIncompatible = value
prefs.bypassIncompatible = value
validatePatchSelection()
}
fun setPatchEnabled(patch: KnownPatch, enabled: Boolean) {
fun closure(seed: KnownPatch, step: (KnownPatch) -> List<KnownPatch>): Set<KnownPatch> =
// The accordion renders from [specs]. It defaults to the compiled-in (localized) list and is
// rebuilt from a custom zip's manifest.json whenever a custom patch set is selected. (Rminder for others about the new custom patch selection flow)
private val builtinSpecs: List<PatchSpec> = builtinPatchSpecs(context, json)
var specs by mutableStateOf(builtinSpecs)
private set
var specsLoading by mutableStateOf(false)
private set
var patchStates by mutableStateOf(prefilledOptions.patchStates)
private set
var selectedVariants by mutableStateOf(prefilledOptions.selectedVariants)
private set
fun variantIndex(spec: PatchSpec): Int =
(selectedVariants[spec.id] ?: spec.defaultVariantIndex)
.coerceIn(0, spec.variants.lastIndex.coerceAtLeast(0))
fun isPatchEnabled(spec: PatchSpec): Boolean =
!isBlockedByPackageName(spec) && (patchStates[spec.id] ?: spec.defaultEnabled)
fun setPatchEnabled(spec: PatchSpec, enabled: Boolean) {
val byId = specs.associateBy { it.id }
fun closure(seed: PatchSpec, step: (PatchSpec) -> List<PatchSpec>): Set<PatchSpec> =
buildSet {
fun walk(p: KnownPatch) { if (add(p)) step(p).forEach(::walk) }
fun walk(p: PatchSpec) { if (add(p)) step(p).forEach(::walk) }
walk(seed)
}
val enableUnits: Set<KnownPatch>
val disableUnits: Set<KnownPatch>
val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) }
val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } }
val enableUnits: Set<PatchSpec>
val disableUnits: Set<PatchSpec>
if (enabled) {
enableUnits = closure(patch) { it.requires }
enableUnits = closure(spec, requiresOf)
disableUnits = enableUnits.flatMap { it.disables }
.flatMapTo(mutableSetOf()) { d ->
closure(d) { dep -> KnownPatch.All.filter { dep in it.requires } }
}
.mapNotNull(byId::get)
.flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) }
} else {
enableUnits = emptySet()
disableUnits = closure(patch) { p -> KnownPatch.All.filter { p in it.requires } }
disableUnits = closure(spec, dependentsOf)
}
val enableFiles = enableUnits.flatMap { it.fileNames }.toSet()
val disableFiles = disableUnits.flatMap { it.fileNames }.toSet()
disabledPatches = (disabledPatches - enableFiles) + disableFiles
patchStates = patchStates.toMutableMap().apply {
enableUnits.forEach { this[it.id] = true }
disableUnits.forEach { this[it.id] = false }
}
}
val enabledPatchCount: Int
get() = KnownPatch.All.count { isPatchEnabled(it) }
fun selectVariant(spec: PatchSpec, index: Int) {
if (spec.variants.isEmpty() || index !in spec.variants.indices) return
selectedVariants = selectedVariants + (spec.id to index)
}
// Advanced (per-patch) options
// Values are seeded from the prefilled config, edited here, and written back out in
// generateConfig(). At patch time PatchOptions.smaliSubstitutions() turns slider values into
// the literals baked into the matching `.patch` files (see SmaliPatchStep). ()
private var optionBools by mutableStateOf(prefilledOptions.optionBools)
private var optionFloats by mutableStateOf(prefilledOptions.optionFloats)
private var optionInts by mutableStateOf(prefilledOptions.optionInts)
private fun keyOf(spec: PatchSpec, option: OptionSpec): String = "${spec.id}/${option.key}"
fun toggleValue(spec: PatchSpec, option: OptionSpec.Toggle): Boolean =
optionBools[keyOf(spec, option)] ?: option.default
fun setToggleValue(spec: PatchSpec, option: OptionSpec.Toggle, value: Boolean) {
optionBools = optionBools + (keyOf(spec, option) to value)
}
fun sliderValue(spec: PatchSpec, option: OptionSpec.Slider): Float =
(optionFloats[keyOf(spec, option)] ?: option.default).coerceIn(option.min, option.max)
fun setSliderValue(spec: PatchSpec, option: OptionSpec.Slider, value: Float) {
optionFloats = optionFloats + (keyOf(spec, option) to value)
}
fun choiceValue(spec: PatchSpec, option: OptionSpec.Choice): Int =
(optionInts[keyOf(spec, option)] ?: option.defaultIndex)
.coerceIn(0, option.entries.lastIndex.coerceAtLeast(0))
fun setChoiceValue(spec: PatchSpec, option: OptionSpec.Choice, index: Int) {
if (index !in option.entries.indices) return
optionInts = optionInts + (keyOf(spec, option) to index)
}
fun colorValue(spec: PatchSpec, option: OptionSpec.Color): Int =
optionInts[keyOf(spec, option)] ?: option.default
fun setColorValue(spec: PatchSpec, option: OptionSpec.Color, value: Int) {
optionInts = optionInts + (keyOf(spec, option) to value)
}
fun isAdvancedModified(spec: PatchSpec): Boolean = spec.advancedOptions.any { option ->
when (option) {
// inline toggles sit next to the variant picker, not in the sheet, so they
// must not light up the sheet's "modified" dot
is OptionSpec.Toggle -> !option.inline && toggleValue(spec, option) != option.default
is OptionSpec.Slider -> sliderValue(spec, option) != option.default
is OptionSpec.Choice -> choiceValue(spec, option) != option.defaultIndex
is OptionSpec.Color -> colorValue(spec, option) != option.default
}
}
fun resetAdvanced(spec: PatchSpec) {
val prefix = "${spec.id}/"
optionBools = optionBools.filterKeys { !it.startsWith(prefix) }
optionFloats = optionFloats.filterKeys { !it.startsWith(prefix) }
optionInts = optionInts.filterKeys { !it.startsWith(prefix) }
}
val optionState: PatchOptionState = PatchOptionState(
toggle = ::toggleValue,
setToggle = ::setToggleValue,
slider = ::sliderValue,
setSlider = ::setSliderValue,
choice = ::choiceValue,
setChoice = ::setChoiceValue,
color = ::colorValue,
setColor = ::setColorValue,
isModified = ::isAdvancedModified,
reset = ::resetAdvanced,
)
fun lockState(spec: PatchSpec): PatchLock {
if (isBlockedByPackageName(spec)) return PatchLock.RequiresDefaultPackage
if (spec.variants.isNotEmpty()) return PatchLock.Free
val byId = specs.associateBy { it.id }
fun closure(seed: PatchSpec, step: (PatchSpec) -> List<PatchSpec>): Set<PatchSpec> =
buildSet {
fun walk(p: PatchSpec) { if (add(p)) step(p).forEach(::walk) }
walk(seed)
}
val requiresOf = { p: PatchSpec -> p.requires.mapNotNull(byId::get) }
val dependentsOf = { p: PatchSpec -> specs.filter { p.id in it.requires } }
for (other in specs) {
if (other.id == spec.id || !isPatchEnabled(other)) continue
val requiresClosure = closure(other, requiresOf)
if (spec in requiresClosure - other) return PatchLock.LockedOn(other)
val disablesClosure = requiresClosure.flatMap { it.disables }
.mapNotNull(byId::get)
.flatMapTo(mutableSetOf()) { d -> closure(d, dependentsOf) }
if (spec in disablesClosure) return PatchLock.LockedOff(other)
}
return PatchLock.Free
}
var customTidalApk by mutableStateOf<PatchComponent?>(null)
private set
@@ -102,6 +268,89 @@ class PatchOptionsModel(
componentType = PatchComponent.Type.Patches,
)
)
reloadSpecs(customPatches)
}
/**
* Rebuilds [specs] from the selected patch set
*/
private fun reloadSpecs(component: PatchComponent?) = screenModelScope.launchIO {
mainThread { specsLoading = true }
val loaded = if (component == null) builtinSpecs else loadManifestSpecs(component) ?: builtinSpecs
mainThread {
specs = loaded
validatePatchSelection()
specsLoading = false
}
}
private fun loadManifestSpecs(component: PatchComponent): List<PatchSpec>? =
loadManifestSpecs(component.getFile(paths))
private fun loadManifestSpecs(file: File): List<PatchSpec>? {
if (!file.exists()) return null
return try {
val bytes = ZipReader(file).use { it.openEntry(MANIFEST_NAME)?.read() } ?: return null
json.decodeFromString(PatchManifest.serializer(), bytes.decodeToString())
.patches
.sortedBy { it.order } // manifest is authoritative for UI order (not KnownPatch)
.takeIf { it.isNotEmpty() }
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to parse $MANIFEST_NAME; using built-in list", t)
null
}
}
/**
* Pulls the patch list from the latest GitHub release's patches.zip manifest
*/
private suspend fun loadLatestReleaseSpecs(): List<PatchSpec>? = try {
val release = github.getLatestRelease().getOrThrow()
val url = release.assets
.find { it.name == RadiantLyricsGithubService.PATCHES_ASSET_NAME }
?.browserDownloadUrl
if (url == null) {
Log.w(BuildConfig.TAG, "Latest release ${release.tagName} has no patches.zip asset; using built-in list")
null
} else {
// Cached per release tag so re-opening the screen doesn't re-download the same zip.
val dest = paths.cacheDownloadDir.resolve("manifest-${release.tagName}.zip")
.apply { parentFile?.mkdirs() }
if (dest.exists() || downloader.download(url, dest) is IDownloadManager.Result.Success) {
loadManifestSpecs(dest).also { loaded ->
if (loaded != null) {
Log.i(BuildConfig.TAG, "Loaded ${loaded.size} patches from latest release ${release.tagName} manifest")
} else {
Log.w(BuildConfig.TAG, "Latest release ${release.tagName} ships no manifest.json; using built-in list")
}
}
} else {
null
}
}
} catch (t: Throwable) {
Log.w(BuildConfig.TAG, "Failed to load latest release manifest; using built-in list", t)
null
}
private fun loadCachedReleaseManifestSpecs(): List<PatchSpec>? =
paths.cacheDownloadDir
.listFiles { f -> f.name.startsWith("manifest-") && f.name.endsWith(".zip") }
?.maxByOrNull { it.lastModified() }
?.let { loadManifestSpecs(it) }
?.also { Log.i(BuildConfig.TAG, "Loaded ${it.size} patches from cached release manifest (offline)") }
/** Default (non-custom) source: latest release manifest → last cached manifest → built-in list. */
private fun loadDefaultSpecs() = screenModelScope.launchIO {
mainThread { specsLoading = true }
// Only hit the network when actually online
val loaded = (if (context.isOnline()) loadLatestReleaseSpecs() else null)
?: loadCachedReleaseManifestSpecs() ?: builtinSpecs
mainThread {
specs = loaded
validatePatchSelection()
specsLoading = false
}
}
val isConfigValid by derivedStateOf {
@@ -122,7 +371,14 @@ class PatchOptionsModel(
debuggable = debuggable,
customTidalApk = customTidalApk,
customPatches = customPatches,
disabledPatches = disabledPatches,
// Hard-disable package-name-bound patches so the patcher can't apply
patchStates = patchStates.toMutableMap().apply {
specs.filter { isBlockedByPackageName(it) }.forEach { this[it.id] = false }
},
selectedVariants = selectedVariants,
optionFloats = optionFloats,
optionBools = optionBools,
optionInts = optionInts,
)
}
@@ -147,11 +403,24 @@ class PatchOptionsModel(
mainThread { packageNameState = state }
}
private fun validatePatchSelection() {
for (spec in specs) {
if (isPatchEnabled(spec)) {
setPatchEnabled(spec, true)
}
}
}
init {
validatePatchSelection()
screenModelScope.launchBlock { fetchPkgNameState() }
// Default source is the latest release's manifest; custom selections drive themselves.
if (customPatches == null) loadDefaultSpecs()
}
companion object {
private const val MANIFEST_NAME = "manifest.json"
private val PACKAGE_REGEX = """^[a-z]\w*(\.[a-z]\w*)+$"""
.toRegex(RegexOption.IGNORE_CASE)
}
@@ -162,3 +431,12 @@ enum class PackageNameState {
Invalid,
Taken,
}
sealed class PatchLock {
data object Free : PatchLock()
data class LockedOn(val by: PatchSpec) : PatchLock()
data class LockedOff(val by: PatchSpec) : PatchLock()
/** Locked off because the install uses a non-default package name */
data object RequiresDefaultPackage : PatchLock()
}
@@ -19,7 +19,9 @@ import com.meowarex.rlmobile.R
import com.meowarex.rlmobile.ui.components.*
import com.meowarex.rlmobile.ui.screens.componentopts.PatchComponent
import com.meowarex.rlmobile.ui.screens.patching.PatchingScreen
import androidx.compose.runtime.saveable.rememberSaveable
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameStateLabel
import com.meowarex.rlmobile.ui.screens.patchopts.components.PackageNameUnlockDialog
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchOptionsAppBar
import com.meowarex.rlmobile.ui.screens.patchopts.components.PatchSelectionAccordion
import com.meowarex.rlmobile.ui.screens.patchopts.components.options.*
@@ -46,6 +48,8 @@ class PatchOptionsScreen(
debuggable = model.debuggable,
setDebuggable = model::changeDebuggable,
bypassIncompatible = model.bypassIncompatible,
setBypassIncompatible = model::changeBypassIncompatible,
appName = model.appName,
appNameIsError = model.appNameIsError,
@@ -55,14 +59,22 @@ class PatchOptionsScreen(
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) },
onSelectCustomPatches = { model.selectCustomPatches(navigator) },
enabledPatchCount = model.enabledPatchCount,
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 = {
@@ -79,6 +91,8 @@ fun PatchOptionsScreenContent(
debuggable: Boolean,
setDebuggable: (Boolean) -> Unit,
bypassIncompatible: Boolean,
setBypassIncompatible: (Boolean) -> Unit,
appName: String,
appNameIsError: Boolean,
@@ -88,18 +102,39 @@ fun PatchOptionsScreenContent(
packageNameState: PackageNameState,
setPackageName: (String) -> Unit,
packageNameLocked: Boolean,
onUnlockPackageName: () -> Unit,
onLockPackageName: () -> Unit,
customTidalApk: PatchComponent?,
onSelectCustomTidalApk: () -> Unit,
customPatches: PatchComponent?,
onSelectCustomPatches: () -> Unit,
enabledPatchCount: Int,
isPatchEnabled: (KnownPatch) -> Boolean,
onTogglePatch: (KnownPatch, Boolean) -> 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 ->
@@ -149,6 +184,10 @@ fun PatchOptionsScreenContent(
valueIsDefault = packageNameIsDefault,
onValueChange = setPackageName,
onValueReset = { setPackageName(PatchOptions.Default.packageName) },
locked = packageNameLocked,
onLockClick = {
if (packageNameLocked) showPkgUnlockDialog = true else onLockPackageName()
},
) {
PackageNameStateLabel(
state = packageNameState,
@@ -157,12 +196,40 @@ fun PatchOptionsScreenContent(
}
}
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(
enabledCount = enabledPatchCount,
totalCount = KnownPatch.All.size,
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,
modifier = Modifier.padding(top = 4.dp),
lockState = patchLockState,
variantIndex = variantIndex,
onSelectVariant = onSelectVariant,
optionState = optionState,
modifier = if (integrations.isEmpty()) Modifier.padding(top = 4.dp) else Modifier,
)
if (isDevMode) {
@@ -206,6 +273,14 @@ fun PatchOptionsScreenContent(
)
}
}
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))
@@ -0,0 +1,261 @@
package com.meowarex.rlmobile.ui.screens.patchopts
import android.content.Context
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.Stable
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlin.math.roundToInt
@Serializable
data class PatchManifest(
val version: Int = 1,
val patches: List<PatchSpec> = emptyList(),
)
@Immutable
@Serializable
data class PatchSpec(
val id: String,
val order: Int = 0,
val fileNames: List<String> = emptyList(),
val extensionFiles: List<String> = emptyList(),
val title: String,
val description: String = "",
@SerialName("default") val defaultEnabled: Boolean = false,
/** ids of patches that get force-enabled when this one is enabled. */
val requires: List<String> = emptyList(),
/** ids of patches that get force-disabled when this one is enabled. */
val disables: List<String> = emptyList(),
val variants: List<VariantSpec> = emptyList(),
val defaultVariantIndex: Int = 0,
val advancedOptions: List<OptionSpec> = emptyList(),
val category: String = CATEGORY_PATCH,
val pathLocked: Boolean = false,
) {
val isIntegration: Boolean get() = category == CATEGORY_INTEGRATION
companion object {
const val CATEGORY_PATCH = "patch"
const val CATEGORY_INTEGRATION = "integration"
}
}
@Immutable
@Serializable
data class VariantSpec(
val title: String,
val fileNames: List<String> = emptyList(),
val extensionFiles: List<String> = emptyList(),
val enabled: Boolean = true,
)
/** A single advanced option. Discriminated by `"type"` in JSON. */
@Serializable
sealed interface OptionSpec {
val key: String
val title: String
val description: String
@Immutable
@Serializable
@SerialName("toggle")
data class Toggle(
override val key: String,
override val title: String,
override val description: String = "",
val default: Boolean = false,
/** Patch files applied only while this toggle is on (and its patch is enabled). */
val fileNames: List<String> = emptyList(),
/** Helper smali extracted only while this toggle is on. */
val extensionFiles: List<String> = emptyList(),
/** Render inline beneath the variant selector instead of inside the advanced sheet. */
val inline: Boolean = false,
/** Greyed out (with the lock dialog) unless this variant index is the selected one. */
val requiresVariant: Int? = null,
/** Greyed out unless the sibling option with this key is currently on. */
val requiresOption: String? = null,
/** Variant indices hidden from the picker while this toggle is on. */
val hidesVariants: List<Int> = emptyList(),
/** Variant title overrides (index -> title) applied while this toggle is on. */
val relabelVariants: Map<Int, String> = emptyMap(),
/** Placeholder name (without the surrounding `__`) baked into the `.patch` files. */
val token: String? = null,
) : OptionSpec
@Immutable
@Serializable
@SerialName("slider")
data class Slider(
override val key: String,
override val title: String,
override val description: String = "",
val default: Float = 0f,
val min: Float = 0f,
val max: Float = 100f,
val steps: Int = 0,
val displayAsPercent: Boolean = false,
val unit: String? = null,
/** Placeholder name (without the surrounding `__`) baked into the `.patch` files. */
val token: String? = null,
/** How the chosen value becomes the smali literal that replaces the token. */
val encode: SmaliEncode? = null,
) : OptionSpec {
val valueRange: ClosedFloatingPointRange<Float> get() = min..max
}
@Immutable
@Serializable
@SerialName("choice")
data class Choice(
override val key: String,
override val title: String = "",
override val description: String = "",
val entries: List<String> = emptyList(),
val defaultIndex: Int = 0,
val values: List<String> = emptyList(),
val requiresOption: String? = null,
val token: String? = null,
) : OptionSpec
@Immutable
@Serializable
@SerialName("color")
data class Color(
override val key: String,
override val title: String = "",
override val description: String = "",
val default: Int = 0,
/** Placeholder name (without the surrounding `__`) baked into the `.patch`/extension files. */
val token: String? = null,
) : OptionSpec
}
@Serializable
enum class EncodeKind {
@SerialName("floatBits")
FloatBits,
@SerialName("argbAlpha")
ArgbAlpha,
@SerialName("int")
IntValue,
}
@Immutable
@Serializable
data class SmaliEncode(
val kind: EncodeKind,
val scale: Float = 1f,
) {
fun encode(value: Float): String = when (kind) {
// Float dp bit-pattern: const vX, <bits>; Dp.constructor-impl(F)F
EncodeKind.FloatBits -> (value * scale).toRawBits().toString()
// ARGB int with black RGB and a percentage-derived alpha: const vX, <argb>
EncodeKind.ArgbAlpha -> ((value / 100f * 255f).roundToInt() shl 24).toString()
// Plain (optionally scaled) integer literal.
EncodeKind.IntValue -> (value * scale).roundToInt().toString()
}
}
/** Asset filename of the bundled fallback manifest snapshot (a copy of the release manifest.json). */
const val BUNDLED_MANIFEST_ASSET = "patches-manifest.json"
// Fallback patch list (snapshot of the release manifest bundled in the app's assets)
fun builtinPatchSpecs(context: Context, json: Json): List<PatchSpec> = try {
context.assets.open(BUNDLED_MANIFEST_ASSET).use { it.readBytes() }
.let { json.decodeFromString(PatchManifest.serializer(), it.decodeToString()) }
.patches
.sortedBy { it.order }
} catch (t: Throwable) {
emptyList()
}
@Immutable
data class EffectiveVariant(val originalIndex: Int, val title: String, val enabled: Boolean = true)
fun PatchSpec.effectiveVariants(isOptionOn: (OptionSpec.Toggle) -> Boolean): List<EffectiveVariant> {
if (variants.isEmpty()) return emptyList()
val hidden = mutableSetOf<Int>()
val relabel = mutableMapOf<Int, String>()
for (option in advancedOptions) {
if (option is OptionSpec.Toggle && isOptionOn(option)) {
hidden += option.hidesVariants
relabel += option.relabelVariants
}
}
return variants.mapIndexedNotNull { index, variant ->
if (index in hidden) null
else EffectiveVariant(index, relabel[index] ?: variant.title, variant.enabled)
}
}
fun PatchSpec.resolveVariantIndex(stored: Int, isOptionOn: (OptionSpec.Toggle) -> Boolean): Int {
val visible = effectiveVariants(isOptionOn)
// never resolve onto a greyed/disabled variant
if (visible.isEmpty() || visible.any { it.originalIndex == stored && it.enabled }) return stored
val relabelKeys = buildSet {
for (option in advancedOptions) {
if (option is OptionSpec.Toggle && isOptionOn(option)) addAll(option.relabelVariants.keys)
}
}
return visible.firstOrNull { it.originalIndex in relabelKeys && it.enabled }?.originalIndex
?: visible.firstOrNull { it.enabled }?.originalIndex
?: visible.first().originalIndex
}
sealed interface OptionLock {
data object Free : OptionLock
data class NeedsVariant(val title: String) : OptionLock
data class NeedsOption(val title: String) : OptionLock
}
fun PatchSpec.optionLock(
option: OptionSpec,
selectedVariant: Int,
isOptionOn: (OptionSpec.Toggle) -> Boolean,
): OptionLock {
if (option !is OptionSpec.Toggle) return OptionLock.Free
option.requiresVariant?.let { required ->
if (selectedVariant != required) {
variants.getOrNull(required)?.let { return OptionLock.NeedsVariant(it.title) }
}
}
option.requiresOption?.let { key ->
val required = advancedOptions.filterIsInstance<OptionSpec.Toggle>().firstOrNull { it.key == key }
if (required != null && !isOptionOn(required)) return OptionLock.NeedsOption(required.title)
}
return OptionLock.Free
}
@Stable
class PatchOptionState(
val toggle: (PatchSpec, OptionSpec.Toggle) -> Boolean,
val setToggle: (PatchSpec, OptionSpec.Toggle, Boolean) -> Unit,
val slider: (PatchSpec, OptionSpec.Slider) -> Float,
val setSlider: (PatchSpec, OptionSpec.Slider, Float) -> Unit,
val choice: (PatchSpec, OptionSpec.Choice) -> Int,
val setChoice: (PatchSpec, OptionSpec.Choice, Int) -> Unit,
val color: (PatchSpec, OptionSpec.Color) -> Int,
val setColor: (PatchSpec, OptionSpec.Color, Int) -> Unit,
val isModified: (PatchSpec) -> Boolean,
val reset: (PatchSpec) -> Unit,
) {
companion object {
/** Read-only stub returning defaults — for Compose previews. */
val Preview = PatchOptionState(
toggle = { _, option -> option.default },
setToggle = { _, _, _ -> },
slider = { _, option -> option.default },
setSlider = { _, _, _ -> },
choice = { _, option -> option.defaultIndex },
setChoice = { _, _, _ -> },
color = { _, option -> option.default },
setColor = { _, _, _ -> },
isModified = { false },
reset = {},
)
}
}
@@ -0,0 +1,88 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
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))
}
}
}
}
}
}
}
@@ -0,0 +1,682 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
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.components.ResetToDefaultButton
import com.meowarex.rlmobile.ui.screens.patchopts.OptionLock
import com.meowarex.rlmobile.ui.screens.patchopts.OptionSpec
import com.meowarex.rlmobile.ui.screens.patchopts.PatchOptionState
import com.meowarex.rlmobile.ui.screens.patchopts.optionLock
import com.meowarex.rlmobile.ui.screens.patchopts.PatchSpec
import kotlinx.coroutines.launch
import kotlin.math.roundToInt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PatchAdvancedOptionsSheet(
patch: PatchSpec,
state: PatchOptionState,
selectedVariant: Int,
onDismiss: () -> Unit,
) {
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
val scope = rememberCoroutineScope()
var lockDialog by remember { mutableStateOf<OptionLock?>(null) }
fun dismiss() {
scope.launch { sheetState.hide() }.invokeOnCompletion {
if (!sheetState.isVisible) onDismiss()
}
}
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
) {
Column(
modifier = Modifier
.fillMaxWidth()
.verticalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth(),
) {
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.weight(1f),
) {
Text(
text = stringResource(R.string.patchopts_advanced_sheet_label),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
Text(
text = patch.title,
style = MaterialTheme.typography.headlineSmall,
)
}
ResetToDefaultButton(
enabled = state.isModified(patch),
onClick = { state.reset(patch) },
)
}
HorizontalDivider()
val parentKeys = patch.advancedOptions
.filterIsInstance<OptionSpec.Toggle>()
.filter { !it.inline }
.map { it.key }
.toSet()
val sheetOptions = patch.advancedOptions.filter { option ->
when {
option is OptionSpec.Toggle && option.inline -> false
option is OptionSpec.Toggle && option.requiresOption in parentKeys -> false
else -> true
}
}
for (option in sheetOptions) key(option.key) {
val lock = patch.optionLock(option, selectedVariant) { state.toggle(patch, it) }
when (option) {
is OptionSpec.Toggle -> {
val toggleOn = state.toggle(patch, option)
val gatedChoices = patch.advancedOptions.filterIsInstance<OptionSpec.Choice>()
.filter { it.requiresOption == option.key }
val gatedToggles = patch.advancedOptions.filterIsInstance<OptionSpec.Toggle>()
.filter { it.requiresOption == option.key && !it.inline }
val hasSubOptions = gatedChoices.isNotEmpty() || gatedToggles.isNotEmpty()
val carded = hasSubOptions && toggleOn
Column(
modifier = if (carded) {
Modifier
.fillMaxWidth()
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.07f))
.border(
width = 1.5.dp,
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.55f),
shape = MaterialTheme.shapes.medium,
)
.padding(horizontal = 12.dp, vertical = 4.dp)
} else {
Modifier.fillMaxWidth()
},
) {
ToggleOptionRow(
title = option.title,
description = option.description,
checked = toggleOn,
onCheckedChange = { state.setToggle(patch, option, it) },
locked = lock != OptionLock.Free,
onLockedTap = { lockDialog = lock },
modifier = if (hasSubOptions) Modifier.padding(vertical = 6.dp) else Modifier,
)
if (hasSubOptions) {
AnimatedVisibility(visible = toggleOn) {
Column(
verticalArrangement = Arrangement.spacedBy(10.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 8.dp),
) {
gatedChoices.forEach { choice ->
key(choice.key) {
ChoiceOptionRow(
title = choice.title,
entries = choice.entries,
selectedIndex = state.choice(patch, choice),
onSelect = { state.setChoice(patch, choice, it) },
)
}
}
gatedToggles.forEach { sub ->
key(sub.key) {
val subLock = patch.optionLock(sub, selectedVariant) { state.toggle(patch, it) }
ToggleOptionRow(
title = sub.title,
description = sub.description,
checked = state.toggle(patch, sub),
onCheckedChange = { state.setToggle(patch, sub, it) },
locked = subLock != OptionLock.Free,
onLockedTap = { lockDialog = subLock },
)
}
}
}
}
}
}
}
is OptionSpec.Slider -> SliderOptionRow(
title = option.title,
description = option.description,
value = state.slider(patch, option),
valueLabel = formatSliderValue(option, state.slider(patch, option)),
valueRange = option.valueRange,
steps = option.steps,
onValueChange = { state.setSlider(patch, option, it) },
)
is OptionSpec.Choice ->
if (option.requiresOption == null) {
ChoiceOptionRow(
title = option.title,
entries = option.entries,
selectedIndex = state.choice(patch, option),
onSelect = { state.setChoice(patch, option, it) },
)
}
is OptionSpec.Color -> ColorOptionRow(
title = option.title,
color = state.color(patch, option),
onColorChange = { state.setColor(patch, option, it) },
)
}
}
FilledTonalButton(
onClick = { dismiss() },
colors = ButtonDefaults.filledTonalButtonColors(
contentColor = MaterialTheme.colorScheme.primary,
),
modifier = Modifier
.align(Alignment.End)
.padding(top = 4.dp),
) {
Text(stringResource(R.string.action_done))
}
}
}
lockDialog?.let { lock ->
OptionLockDialog(lock = lock, onDismiss = { lockDialog = null })
}
}
@Composable
private fun ToggleOptionRow(
title: String,
description: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
locked: Boolean = false,
onLockedTap: () -> Unit = {},
modifier: Modifier = Modifier,
) {
val interactionSource = remember(::MutableInteractionSource)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.fillMaxWidth()
.clickable(
interactionSource = interactionSource,
indication = null,
role = Role.Switch,
) { if (locked) onLockedTap() else onCheckedChange(!checked) }
.alpha(if (locked) 0.45f else 1f),
) {
OptionText(
title = title,
description = description,
modifier = Modifier.weight(1f),
)
Box {
Switch(
checked = checked,
enabled = !locked,
onCheckedChange = onCheckedChange,
interactionSource = interactionSource,
)
if (locked) {
Box(
modifier = Modifier
.matchParentSize()
.clickable(
interactionSource = remember(::MutableInteractionSource),
indication = null,
role = Role.Switch,
) { onLockedTap() }
)
}
}
}
}
@Composable
private fun SliderOptionRow(
title: String,
description: String,
value: Float,
valueLabel: String,
valueRange: ClosedFloatingPointRange<Float>,
steps: Int,
onValueChange: (Float) -> Unit,
) {
// Whether the slider snaps to the step dots
var snap by rememberSaveable { mutableStateOf(true) }
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(2.dp),
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f),
)
Text(
text = valueLabel,
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.primary,
)
if (steps > 0) {
IconButton(
onClick = {
snap = !snap
if (snap) onValueChange(snapToNearestStep(value, valueRange, steps))
},
modifier = Modifier.size(32.dp),
) {
Icon(
painter = painterResource(
if (snap) R.drawable.ic_lock else R.drawable.ic_lock_open,
),
contentDescription = stringResource(R.string.patchopts_toggle_snap),
tint = if (snap) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(18.dp),
)
}
}
}
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.alpha(.7f),
)
// Locked -> native stepped slider (dots + snapping), left exactly as-is
// 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))
}
}
}
}
}
}
@@ -1,12 +1,16 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
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
@@ -18,16 +22,34 @@ 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.KnownPatch
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: (KnownPatch) -> Boolean,
onToggle: (KnownPatch, Boolean) -> Unit,
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) }
@@ -36,6 +58,9 @@ fun PatchSelectionAccordion(
label = "patch-accordion-arrow",
)
var lockInfo by remember { mutableStateOf<LockInfo?>(null) }
var advancedFor by remember { mutableStateOf<PatchSpec?>(null) }
Column(
modifier = modifier
.fillMaxWidth()
@@ -50,12 +75,17 @@ fun PatchSelectionAccordion(
.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(R.string.patchopts_patches_title),
text = stringResource(titleRes),
style = MaterialTheme.typography.titleMedium,
)
Text(
@@ -80,34 +110,166 @@ fun PatchSelectionAccordion(
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(R.string.patchopts_patches_desc),
text = stringResource(descriptionRes),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier
.alpha(.7f)
.padding(bottom = 8.dp),
)
for (patch in KnownPatch.All) key(patch) {
PatchCheckboxRow(
title = stringResource(patch.titleRes),
description = stringResource(patch.descRes),
checked = isEnabled(patch),
onCheckedChange = { onToggle(patch, it) },
)
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 PatchCheckboxRow(
private fun InlineToggleRow(
title: String,
description: String,
checked: Boolean,
@@ -117,22 +279,62 @@ private fun PatchCheckboxRow(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.fillMaxWidth()
.clickable(
interactionSource = interactionSource,
indication = null,
role = Role.Checkbox,
role = Role.Switch,
) { onCheckedChange(!checked) }
.padding(vertical = 4.dp),
) {
Checkbox(
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),
@@ -147,5 +349,82 @@ private fun PatchCheckboxRow(
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))
}
}
}
}
}
}
@@ -0,0 +1,44 @@
package com.meowarex.rlmobile.ui.screens.patchopts.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import com.meowarex.rlmobile.ui.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,
)
},
)
}
}
}
@@ -3,8 +3,12 @@ package 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.components.Label
import com.meowarex.rlmobile.ui.components.ResetToDefaultButton
@@ -18,6 +22,8 @@ fun TextPatchOption(
onValueChange: (String) -> Unit,
onValueReset: () -> Unit,
modifier: Modifier = Modifier,
locked: Boolean? = null,
onLockClick: () -> Unit = {},
extra: (@Composable ColumnScope.() -> Unit)? = null,
) {
Label(
@@ -30,16 +36,41 @@ fun TextPatchOption(
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 = {
ResetToDefaultButton(
enabled = !valueIsDefault,
onClick = onValueReset,
)
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()
@@ -55,6 +55,10 @@ class UpdaterViewModel(
showDialog = false
}
fun reopenDialog() {
if (targetVersion != null) showDialog = true
}
fun triggerUpdate() = viewModelScope.launchIO {
if (!isWorking.compareAndSet(expect = false, update = true))
return@launchIO
@@ -7,7 +7,6 @@ import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.util.Log
import androidx.core.app.ActivityCompat
@@ -51,15 +50,15 @@ class UpdateCheckWorker(
.maxByOrNull { it.first }
?: return Result.success()
val (version, release) = latestVersion
val (version, _) = latestVersion
if (current >= version) return Result.success()
Log.i(BuildConfig.TAG, "Update available: $version (installed $current)")
postUpdateNotification(version.toString(), release.htmlUrl)
postUpdateNotification(version.toString())
return Result.success()
}
private fun postUpdateNotification(version: String, releaseUrl: String) {
private fun postUpdateNotification(version: String) {
val nm = applicationContext.getSystemService<NotificationManager>() ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -78,10 +77,15 @@ class UpdateCheckWorker(
return
}
val launchIntent = applicationContext.packageManager
.getLaunchIntentForPackage(applicationContext.packageName)
?.apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }
?: return
val pendingIntent = PendingIntent.getActivity(
applicationContext,
0,
Intent(Intent.ACTION_VIEW, Uri.parse(releaseUrl)).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) },
launchIntent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT,
)
@@ -6,10 +6,10 @@ import android.content.*
import android.content.pm.PackageManager
import android.content.res.Resources
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.net.Uri
import android.os.*
import android.provider.Settings
import android.telephony.TelephonyManager
import android.util.Log
import android.util.TypedValue
import android.widget.Toast
@@ -65,6 +65,15 @@ fun Context.getPackageVersion(pkg: String): Pair<String?, Int> {
.let { it.versionName to it.versionCode }
}
// Whether the device currently has a validated internet connection
fun Context.isOnline(): Boolean {
val cm = getSystemService<ConnectivityManager>() ?: return true
val network = cm.activeNetwork ?: return false
val caps = cm.getNetworkCapabilities(network) ?: return false
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
}
fun Context.isPackageInstalled(packageName: String): Boolean {
return try {
packageManager.getPackageInfo(packageName, 0)
@@ -152,37 +161,6 @@ suspend fun Context.isPlayProtectEnabled(): Boolean? {
}
}
/**
* Check whether the device is connected on a metered WIFI connection or through any type of mobile data,
* to avoid unknowingly downloading a lot of stuff through a potentially metered network.
*/
@Suppress("DEPRECATION")
fun Context.isNetworkDangerous(): Boolean {
val connectivity = this.getSystemService<ConnectivityManager>()
?: error("Unable to get system connectivity service")
if (connectivity.isActiveNetworkMetered) return true
when (val info = connectivity.activeNetworkInfo) {
null -> return false
else -> {
if (info.isRoaming) return true
if (info.type == ConnectivityManager.TYPE_WIFI) return false
}
}
val telephony = this.getSystemService<TelephonyManager>()
?: error("Unable to get system telephony service")
val dangerousMobileDataStates = arrayOf(
/* TelephonyManager.DATA_DISCONNECTING */ 4,
TelephonyManager.DATA_CONNECTED,
TelephonyManager.DATA_CONNECTING,
)
return dangerousMobileDataStates.contains(telephony.dataState)
}
/**
* Gets the user associated with this context.
*/
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M218,856L104,742Q81,719 81,686Q81,653 104,630L630,104Q653,81 686,81Q719,81 742,104L856,218Q879,241 879,274Q879,307 856,330L330,856Q307,879 274,879Q241,879 218,856ZM508,734L734,508L856,630Q879,653 879,686Q879,719 856,742L742,856Q719,879 686,879Q653,879 630,856L508,734ZM480,600Q497,600 508.5,588.5Q520,577 520,560Q520,543 508.5,531.5Q497,520 480,520Q463,520 451.5,531.5Q440,543 440,560Q440,577 451.5,588.5Q463,600 480,600ZM400,520Q417,520 428.5,508.5Q440,497 440,480Q440,463 428.5,451.5Q417,440 400,440Q383,440 371.5,451.5Q360,463 360,480Q360,497 371.5,508.5Q383,520 400,520ZM560,520Q577,520 588.5,508.5Q600,497 600,480Q600,463 588.5,451.5Q577,440 560,440Q543,440 531.5,451.5Q520,463 520,480Q520,497 531.5,508.5Q543,520 560,520ZM225,451L104,330Q81,307 81,274Q81,241 104,218L218,104Q241,81 274,81Q307,81 330,104L452,226L225,451ZM480,440Q497,440 508.5,428.5Q520,417 520,400Q520,383 508.5,371.5Q497,360 480,360Q463,360 451.5,371.5Q440,383 440,400Q440,417 451.5,428.5Q463,440 480,440Z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#e8eaed"
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8H8.9V6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#e8eaed"
android:pathData="M18,8h-1V6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2H6c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V10c0,-1.1 -0.9,-2 -2,-2zM18,20H6V10h12v10zM12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M120,880v-800l60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60v800l-60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60 -60,-60 -60,60ZM240,680h480v-80L240,600v80ZM240,520h480v-80L240,440v80ZM240,360h480v-80L240,280v80Z" />
</vector>
@@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="m819,932 l-59,-59q-10,3 -19.5,5T720,880L240,880q-50,0 -85,-35t-35,-85v-120h120v-287L27,140l57,-57L876,875l-57,57ZM840,727 L760,647v-447L313,200l-73,-73v-47l60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60 60,60 60,-60v647ZM320,640h207L320,433v207ZM475,360 L395,280h205v80L475,360ZM595,480 L515,400h85v80h-5ZM680,480q-17,0 -28.5,-11.5T640,440q0,-17 11.5,-28.5T680,400q17,0 28.5,11.5T720,440q0,17 -11.5,28.5T680,480ZM680,360q-17,0 -28.5,-11.5T640,320q0,-17 11.5,-28.5T680,280q17,0 28.5,11.5T720,320q0,17 -11.5,28.5T680,360Z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#e8eaed"
android:pathData="M3,17v2h6v-2H3zM3,5v2h10V5H3zM13,21v-2h8v-2h-8v-2h-2v6h2zM7,9v2H3v2h4v2h2V9H7zM21,13v-2H11v2h10zM15,7h2V5h4V3h-4V1h-2v6z" />
</vector>
@@ -5,5 +5,5 @@
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M480,840q-75,0 -140.5,-28.5t-114,-77q-48.5,-48.5 -77,-114T120,480q0,-75 28.5,-140.5t77,-114q48.5,-48.5 114,-77T480,120q82,0 155.5,35T760,254v-54q0,-17 11.5,-28.5T800,160q17,0 28.5,11.5T840,200v160q0,17 -11.5,28.5T800,400L640,400q-17,0 -28.5,-11.5T600,360q0,-17 11.5,-28.5T640,320h70q-41,-56 -101,-88t-129,-32q-117,0 -198.5,81.5T200,480q0,117 81.5,198.5T480,760q95,0 170,-57t99,-147q5,-16 18,-24t29,-6q17,2 27,14.5t6,27.5q-29,119 -126,195.5T480,840ZM520,464 L620,564q11,11 11,28t-11,28q-11,11 -28,11t-28,-11L452,508q-6,-6 -9,-13.5t-3,-15.5v-159q0,-17 11.5,-28.5T480,280q17,0 28.5,11.5T520,320v144Z" />
android:pathData="M480,640q17,0 28.5,-11.5T520,600v-184l64,64q11,11 28,11t28,-11q11,-11 11,-28t-11,-28L508,308q-12,-12 -28,-12t-28,12L324,424q-11,11 -11,28t11,28q11,11 28,11t28,-11l64,-64v184q0,17 11.5,28.5T480,640Zm0,240q-83,0 -156,-31.5T197,763q-54,-54 -85.5,-127T80,480q0,-83 31.5,-156T197,197q54,-54 127,-85.5T480,80q83,0 156,31.5T763,197q54,54 85.5,127T880,480q0,83 -31.5,156T763,763q-54,54 -127,85.5T480,880Z" />
</vector>
+78 -10
View File
@@ -22,6 +22,7 @@
<string name="action_retry_install">Retry installation</string>
<string name="action_open_error_log">Open error log</string>
<string name="action_apply">Apply</string>
<string name="action_done">Done</string>
<string name="action_confirm">Confirm</string>
<string name="action_dismiss">Dismiss</string>
<string name="action_install">Install</string>
@@ -129,7 +130,6 @@
<string name="navigation_back">Back</string>
<string name="navigation_about">About</string>
<string name="navigation_logs">Logs</string>
<string name="navigation_settings">Settings</string>
<string name="navigation_refresh">Refresh</string>
@@ -180,7 +180,7 @@
<string name="install_error_timeout">Installer timed out</string>
<string name="install_group_prepare">Prepare</string>
<string name="install_group_download">Download dependencies</string>
<string name="install_group_download">Download Batteries</string>
<string name="install_group_patch">Patch APK</string>
<string name="install_group_install">Install APK</string>
@@ -247,11 +247,23 @@
<string name="patchopts_custom_tidal_apk_desc">Provide your own Stock TIDAL APK to patch instead of the version from Github.</string>
<string name="patchopts_custom_patches_title">Custom Patches</string>
<string name="patchopts_custom_patches_desc">Provide your own Patches Zip to use instead of the ones from Github.</string>
<string name="patchopts_bypass_incompatible_title">Bypass Incompatible</string>
<string name="patchopts_bypass_incompatible_desc">Force-allow path-gated patches. This can be VERY DANGEROUS!</string>
<string name="patchopts_divider_basic">Basic</string>
<string name="patchopts_divider_advanced">Advanced</string>
<string name="patchopts_patches_title">Patches</string>
<string name="patchopts_patches_desc">Toggle which patches are applied during installation. Unchecked patches are skipped entirely.</string>
<string name="patchopts_patches_summary">%1$d of %2$d enabled</string>
<string name="patchopts_integrations_title">Integrations</string>
<string name="patchopts_integrations_desc">Connect TIDAL to other systems/apps</string>
<string name="patchopts_pkgname_warning_title">Custom Package Name</string>
<string name="patchopts_pkgname_warning_msg">Installing under a package name other than &lt;b>%s&lt;/b> will block the following from working:</string>
<string name="patchopts_pkgname_warning_permanent">The package name is permanent once installed (requires a fresh install)</string>
<string name="patchopts_advanced_button">Advanced Options</string>
<string name="patchopts_advanced_sheet_label">Advanced Options</string>
<string name="patchopts_toggle_snap">Toggle snapping to steps</string>
<string name="patchopts_field_unlock">Unlock editing</string>
<string name="patchopts_field_lock">Lock and reset to default</string>
<string name="patch_lyrics_disable_cover_title">Disable Lyrics Cover</string>
<string name="patch_lyrics_disable_cover_desc">Removes the mini track cover from the lyrics screen.</string>
@@ -259,8 +271,22 @@
<string name="patch_lyrics_progress_pill_desc">Restores the old Track Progress Pill in the top of the Lyrics screen!</string>
<string name="patch_lyrics_replace_button_title">Replace Lyrics Button</string>
<string name="patch_lyrics_replace_button_desc">Replaces the Lyrics button with the RL Sparkle!</string>
<string name="patch_lyrics_rl_api_title">Radiant Lyrics API - WIP</string>
<string name="patch_lyrics_rl_api_desc">Use Radiant Lyrics API to fetch Lyrics (Higher quality &amp; More providers)</string>
<string name="patch_sticky_lyrics_title">Sticky Lyrics</string>
<string name="patch_sticky_lyrics_desc">Always Forces the Lyrics page to be opened (aslong as the track has lyrics)</string>
<string name="patch_lyrics_keep_controls_title">Keep Controls Visible</string>
<string
name="patch_lyrics_keep_controls_desc"
>Inverts the auto-hide behavior on the lyrics screen, playback controls stay visible by default and only hide when you tap them off.</string>
<string name="patch_player_backdrop_title">Player Backdrop</string>
<string name="patch_player_backdrop_desc">Restores the legacy translucent backdrop blur behind the player.</string>
<string name="patch_quality_badge_colors_title">Quality Badge Colors</string>
<string name="patch_quality_badge_colors_desc">Brings color back to the player quality badge.</string>
<string name="patch_player_one_handed_title">One-Handed Mode</string>
<string name="patch_player_one_handed_desc">Mirrors the bottom player buttons left-to-right for easier one-handed reach.</string>
<string name="patch_cover_everywhere_title">Cover Everywhere - WIP</string>
<string name="patch_cover_everywhere_desc">Applies the blurred backdrop to every Compose based page (Home &amp; Collection)</string>
<string name="patch_lyrics_replace_share_button_title">Replace Share Button</string>
<string
name="patch_lyrics_replace_share_button_desc"
@@ -269,11 +295,60 @@
<string
name="patch_debug_menu_unlock_desc"
>Reveals TIDAL\'s hidden Debug Menu in Settings. (Unlocks Feature Flags &amp; the legacy Debug Options)</string>
<string name="patch_waze_integration_title">Waze Integration</string>
<string name="patch_waze_integration_desc">Control TIDAL playback from the Waze app</string>
<string name="patch_waze_browse_root_title">Media Menu</string>
<string name="patch_waze_accent_color_title">Accent Color</string>
<string name="patch_waze_color_waze_default">Waze Default</string>
<string name="patch_waze_color_tidal_cyan">TIDAL Cyan</string>
<string name="patch_waze_color_material_you">Material You</string>
<string name="patch_waze_root_auto">Auto Menu</string>
<string name="patch_waze_root_home">Home</string>
<string name="patch_waze_root_recents">Recents</string>
<string name="patch_color_picker_title">Custom color</string>
<string name="patch_color_picker_hex">Hex color</string>
<string name="patch_color_picker_hex_error">Use #RRGGBB or #AARRGGBB.</string>
<string name="patch_enable_legacy_ui_title">Enable Legacy UI</string>
<string
name="patch_enable_legacy_ui_desc"
>[This patch will stop working soon] - Replaces the New Compose based UI with the Legacy UI (disables player-market-ui feature flag)</string>
<string name="patch_mini_player_redesign_title">Redesigned Mini-Player</string>
<string name="patch_mini_player_redesign_desc">Integrates the Seekbar into the control panel at the bottom of the screen.</string>
<string name="patch_mini_player_gestures_title">Mini-Player Gestures</string>
<string name="patch_mini_player_gestures_desc">Swipe up on the mini-player to open the full player.</string>
<string name="patch_mini_player_drag_title">Swipe Up Style</string>
<string name="patch_mini_player_drag_desc">Whether the player follows your finger when swiping up to open it.</string>
<string name="patch_mini_player_drag_static">Static</string>
<string name="patch_mini_player_drag_drag">Drag</string>
<string name="patch_mini_player_left_right_gestures_title">Next/Previous Gestures</string>
<string name="patch_mini_player_left_right_gestures_desc">Swipe left or right on the mini-player to skip tracks.</string>
<string name="patch_mini_player_dynamic_bg_title">Mini-Player Dynamic Background</string>
<string name="patch_mini_player_dynamic_bg_desc">Colorizes the Mini-Player depending on the currently playing track.</string>
<string name="patch_mini_player_variant_floating_title">Floating</string>
<string name="patch_mini_player_variant_square_grey_title">Grey</string>
<string name="patch_mini_player_variant_square_black_title">Black</string>
<string name="patch_mini_player_variant_legacy_title">Legacy</string>
<string name="patch_mini_player_border_title">Progress Border</string>
<string name="patch_mini_player_border_desc">Draws an animated progress ring around the floating mini-player. Requires the Floating style.</string>
<!-- Advanced option labels -->
<string name="patch_opt_backdrop_blur_title">Blur Strength</string>
<string name="patch_opt_backdrop_blur_desc">How strongly the album art behind the player is blurred.</string>
<string name="patch_opt_backdrop_dimming_title">Backdrop Dimming</string>
<string name="patch_opt_backdrop_dimming_desc">Adjusts the level of darkening on the backdrop to keep foreground controls legible.</string>
<string name="patch_lock_required_title">Patch Required!</string>
<string name="patch_lock_blocked_title">Patch Blocked!</string>
<string name="patch_lock_required_msg">Patch Required by &lt;b>%s&lt;/b>.</string>
<string name="patch_lock_blocked_msg">Patch Blocked by &lt;b>%s&lt;/b>.</string>
<string name="patch_lock_pkgname_title">Original Package Name Invalid!</string>
<string name="patch_lock_pkgname_msg">This only works while the app keeps its original &lt;b>%s&lt;/b> package name. Reset the Package Name option to enable it.&lt;br/>&lt;br/>An existing install can\'t be renamed (requires a fresh install)</string>
<string name="patch_opt_lock_title">Option Locked</string>
<string name="patch_opt_lock_needs_variant">Requires the &lt;b>%s&lt;/b> style.</string>
<string name="patch_opt_lock_needs_option">Requires the &lt;b>%s&lt;/b> option.</string>
<string name="action_got_it">Got it</string>
<string name="componentopts_screen_title">Custom Component (%s)</string>
<string name="componentopts_screen_desc">Select a custom build that was imported by Manager.</string>
<string name="componentopts_selected_none">Latest</string>
@@ -293,13 +368,6 @@
<string name="log_action_export_apk">Export APK</string>
<string name="log_action_share">Share log</string>
<string name="logs_title">Logs</string>
<string name="logs_none">No logs present!</string>
<string name="logs_action_delete_all">Delete all logs</string>
<string name="logs_status_delete_success">Successfully deleted all logs</string>
<string name="logs_wipe_title">Clear Logs</string>
<string name="logs_wipe_desc">Are you sure you want to permanently delete all the logs?</string>
<string name="notif_group_install_title">Active installation</string>
<string name="notif_group_install_desc">Progress notifications sent during a minimized installation</string>
<string name="notif_install_ready_title">Radiant Lyrics installation ready!</string>
@@ -315,7 +383,7 @@
<string name="play_protect_warning_open_gpp">Open Play Protect</string>
<string name="play_protect_warning_disable">Don\'t show this again</string>
<string name="fun_fact_prefix">Fun Fact: %s</string>
<string name="fun_fact_prefix">%s</string>
<string name="fun_fact_1">Did you know that TIDAL no longer like blur!</string>
<string name="fun_fact_2">Radiant Lyrics is also available for DESKTOP!!!</string>
<string name="fun_fact_3">i am in your walls!</string>
+21 -6
View File
@@ -2,13 +2,25 @@
A Manager app for Android that patches TIDAL to bring the Radiant Lyrics experience to mobile.
## What it does
## What it does?
- Blurred album art background in the player
- Progress pill overlay in lyrics mode - restored from old UI
- Hides the ugly cover art when in lyrics mode
- Syllable Level lyrics (SoonTM)
This project adds ALLOT of completely Optional & Customisable Patches/Integrations to TIDAL on Android (iOS SoonTM).
Every Patch & Integration can be disabled/enabled & adjusted exactly how you want TIDAL to feel/look <3
### Patches:
- Insane Customisation of the Player Backdrop
- Higher quality lyrics from more providers (WIP)
- Progress pill overlay in lyrics mode (restored from old UI)
- Keeps lyrics controls visible
- One-handed player controls
- Redesigned mini-player with gestures
- Word Level Lyrics (Word by Word) <3
- Syllable Level Lyrics (Syllable by Syllable) <3
### Integrations
- Waze | Control TIDAL playback from Waze
### Disclaimer
@@ -18,7 +30,10 @@ This project is not affiliated with TIDAL in any way. Use at your own risk.
## Installation
### Batteries <3
- Android 10.0+ & >340 DPI
- Phone of some unknown description
- The smallest amount of brain damage
- Android 7.0+ & >340 DPI
- Internet
- Paid TIDAL Account
### Steps
+22
View File
@@ -0,0 +1,22 @@
--- a/com/tidal/wave2/foundation/WaveScaffoldKt.smali
+++ b/com/tidal/wave2/foundation/WaveScaffoldKt.smali
@@ -889,6 +889,10 @@
.line 386
.line 387
:cond_20
+ const/4 v0, 0x0 # render $changed flag
+
+ invoke-static {v1, v0}, Lradiant/HomeBackdrop;->render(Landroidx/compose/runtime/Composer;I)V # render album-art backdrop
+
new-instance v0, Lcom/tidal/wave2/foundation/WaveScaffoldKt$WaveScaffold$2;
.line 388
@@ -991,7 +995,7 @@
.line 436
.line 437
- move-wide v15, v10
+ const-wide/16 v15, 0x0 # transparent containerColor
.line 438
const/4 v11, 0x0
+22
View File
@@ -0,0 +1,22 @@
--- a/com/tidal/android/feature/appscaffold/ui/q.smali
+++ b/com/tidal/android/feature/appscaffold/ui/q.smali
@@ -55,6 +55,8 @@
.line 10
iput-object p4, p0, Lcom/tidal/android/feature/appscaffold/ui/q;->d:Ljava/lang/String;
+ invoke-static {p3, p4}, Lradiant/HomeBackdrop;->fromMiniPlayer(ILjava/lang/String;)V # capture cover state
+
.line 11
.line 12
iput-boolean p5, p0, Lcom/tidal/android/feature/appscaffold/ui/q;->e:Z
--- a/com/tidal/android/feature/appscaffold/ui/t.smali
+++ b/com/tidal/android/feature/appscaffold/ui/t.smali
@@ -23,6 +23,8 @@
.method public constructor <init>(Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType;Lcom/tidal/android/feature/appscaffold/ui/q;Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState;Z)V
.locals 0
+ invoke-static {p3}, Lradiant/HomeBackdrop;->onPlayState(Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState;)V # RL: publish live play state to Cover Everywhere
+
.line 1
invoke-virtual {p1}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
+2 -2
View File
@@ -1,5 +1,5 @@
{
"tidalVersionCode": 9089,
"tidalVersionCode": 10004,
"tidalApkUrl": "https://github.com/meowarex/rl-mobile/releases/download/latest/tidal-stock.apk",
"patchesVersion": "0.6.0"
"patchesVersion": "1.1.2"
}
+14 -20
View File
@@ -1,43 +1,37 @@
--- a/com/tidal/android/core/debug/DebugFeatureInteractorDefault.smali
+++ b/com/tidal/android/core/debug/DebugFeatureInteractorDefault.smali
@@ -53,6 +53,11 @@
@@ -54,6 +54,9 @@
.method public final a()Z
.locals 2
+ # rl-debug-unlock: force a() to always return true, bypassing the
+ # "debug-menu" feature flag check that normally hides the Settings entry.
+ const/4 v0, 0x1
+ return v0
+ const/4 v0, 0x1 # force true
+ return v0 # bypass debug flag
+
.line 1
iget-object v0, p0, Lcom/tidal/android/core/debug/DebugFeatureInteractorDefault;->a:Lcom/tidal/android/featureflags/l;
iget-object v0, p0, Lcom/tidal/android/core/debug/DebugFeatureInteractorDefault;->a:Lcom/tidal/android/featureflags/n;
@@ -97,6 +102,14 @@
@@ -99,6 +102,14 @@
}
.end annotation
+ # rl-debug-unlock: short-circuit b() to always emit MutableStateFlow(TRUE).
+ # Mirrors the original ":cond_1 :goto_0" path the app uses for internal/test builds.
+ sget-object v0, Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;
+ sget-object v0, Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean; # always-true value
+
+ invoke-static {v0}, Lkotlinx/coroutines/flow/StateFlowKt;->MutableStateFlow(Ljava/lang/Object;)Lkotlinx/coroutines/flow/MutableStateFlow;
+ invoke-static {v0}, Lkotlinx/coroutines/flow/StateFlowKt;->MutableStateFlow(Ljava/lang/Object;)Lkotlinx/coroutines/flow/MutableStateFlow; # wrap in flow
+
+ move-result-object v0
+ move-result-object v0 # the flow
+
+ return-object v0
+ return-object v0 # short-circuit b()
+
.line 1
sget-object v0, Lh50/a;->a:Ljava/lang/String;
sget-object v0, Lo50/a;->a:Ljava/lang/String;
@@ -261,6 +274,11 @@
@@ -261,6 +272,9 @@
.method public final c()Z
.locals 3
+ # rl-debug-unlock: force c() to always return true, bypassing the
+ # applicationId-substring / in-app-bug-reports / export-logs flag checks.
+ const/4 v0, 0x1
+ return v0
+ const/4 v0, 0x1 # force true
+ return v0 # bypass debug flag
+
.line 1
sget-object v0, Lh50/a;->a:Ljava/lang/String;
sget-object v0, Lo50/a;->a:Ljava/lang/String;
+3 -4
View File
@@ -1,7 +1,6 @@
--- a/ig/d.smali
+++ b/ig/d.smali
@@ -26,8 +26,8 @@
new-instance v0, Lig/d;
--- a/hg/d.smali
+++ b/hg/d.smali
@@ -27,7 +27,7 @@
.line 2
.line 3
@@ -0,0 +1,73 @@
.class public final Lcom/tidal/android/feature/appscaffold/ui/r$c;
.super Lcom/tidal/android/feature/appscaffold/ui/r;
.source "SourceFile"
# Synthetic mini-player event used by right swipe (previous/rewind).
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lcom/tidal/android/feature/appscaffold/ui/r;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "c"
.end annotation
# static fields
.field public static final a:Lcom/tidal/android/feature/appscaffold/ui/r$c;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lcom/tidal/android/feature/appscaffold/ui/r$c;
invoke-direct {v0}, Lcom/tidal/android/feature/appscaffold/ui/r;-><init>()V
sput-object v0, Lcom/tidal/android/feature/appscaffold/ui/r$c;->a:Lcom/tidal/android/feature/appscaffold/ui/r$c;
return-void
.end method
# virtual methods
.method public final equals(Ljava/lang/Object;)Z
.locals 1
const/4 v0, 0x1
if-ne p0, p1, :cond_0
return v0
:cond_0
instance-of p1, p1, Lcom/tidal/android/feature/appscaffold/ui/r$c;
if-nez p1, :cond_1
const/4 p1, 0x0
return p1
:cond_1
return v0
.end method
.method public final hashCode()I
.locals 1
const v0, -0x5d6a0f8
return v0
.end method
.method public final toString()Ljava/lang/String;
.locals 1
const-string v0, "PreviousButtonClicked"
return-object v0
.end method
+133
View File
@@ -0,0 +1,133 @@
.class public final Lradiant/BackdropFx;
.super Ljava/lang/Object;
.method public static apply(Landroid/graphics/Bitmap;)Landroid/graphics/Bitmap;
.locals 14
const v0, __RL_CONTRAST__
const v1, __RL_SATURATION__
const v2, __RL_BRIGHTNESS__
# cm = ColorMatrix(); cm.setSaturation(s)
new-instance v3, Landroid/graphics/ColorMatrix;
invoke-direct {v3}, Landroid/graphics/ColorMatrix;-><init>()V
invoke-virtual {v3, v1}, Landroid/graphics/ColorMatrix;->setSaturation(F)V
# o = 128 - 128*c (contrast pivot at mid-grey, 0..255 space)
const/high16 v4, 0x43000000
mul-float v5, v4, v0
sub-float v5, v4, v5
# contrast matrix: diag = c, offset column = o
const/16 v6, 0x14
new-array v7, v6, [F
const/4 v8, 0x0
aput v0, v7, v8
const/4 v8, 0x4
aput v5, v7, v8
const/4 v8, 0x6
aput v0, v7, v8
const/16 v8, 0x9
aput v5, v7, v8
const/16 v8, 0xc
aput v0, v7, v8
const/16 v8, 0xe
aput v5, v7, v8
const/high16 v9, 0x3f800000
const/16 v8, 0x12
aput v9, v7, v8
new-instance v8, Landroid/graphics/ColorMatrix;
invoke-direct {v8, v7}, Landroid/graphics/ColorMatrix;-><init>([F)V
invoke-virtual {v3, v8}, Landroid/graphics/ColorMatrix;->postConcat(Landroid/graphics/ColorMatrix;)V
# brightness matrix: diag = b
new-array v7, v6, [F
const/4 v8, 0x0
aput v2, v7, v8
const/4 v8, 0x6
aput v2, v7, v8
const/16 v8, 0xc
aput v2, v7, v8
const/16 v8, 0x12
aput v9, v7, v8
new-instance v8, Landroid/graphics/ColorMatrix;
invoke-direct {v8, v7}, Landroid/graphics/ColorMatrix;-><init>([F)V
invoke-virtual {v3, v8}, Landroid/graphics/ColorMatrix;->postConcat(Landroid/graphics/ColorMatrix;)V
# out = createBitmap(w, h, ARGB_8888)
invoke-virtual {p0}, Landroid/graphics/Bitmap;->getWidth()I
move-result v10
invoke-virtual {p0}, Landroid/graphics/Bitmap;->getHeight()I
move-result v11
sget-object v12, Landroid/graphics/Bitmap$Config;->ARGB_8888:Landroid/graphics/Bitmap$Config;
invoke-static {v10, v11, v12}, Landroid/graphics/Bitmap;->createBitmap(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;
move-result-object v10
# canvas over out
new-instance v11, Landroid/graphics/Canvas;
invoke-direct {v11, v10}, Landroid/graphics/Canvas;-><init>(Landroid/graphics/Bitmap;)V
# paint with the color-matrix filter
new-instance v12, Landroid/graphics/Paint;
invoke-direct {v12}, Landroid/graphics/Paint;-><init>()V
new-instance v13, Landroid/graphics/ColorMatrixColorFilter;
invoke-direct {v13, v3}, Landroid/graphics/ColorMatrixColorFilter;-><init>(Landroid/graphics/ColorMatrix;)V
invoke-virtual {v12, v13}, Landroid/graphics/Paint;->setColorFilter(Landroid/graphics/ColorFilter;)Landroid/graphics/ColorFilter;
# draw the blurred bitmap through the filter
const/4 v0, 0x0
invoke-virtual {v11, p0, v0, v0, v12}, Landroid/graphics/Canvas;->drawBitmap(Landroid/graphics/Bitmap;FFLandroid/graphics/Paint;)V
invoke-virtual {p0}, Landroid/graphics/Bitmap;->recycle()V
return-object v10
.end method
@@ -0,0 +1,415 @@
.class public final Lradiant/HomeBackdrop;
.super Ljava/lang/Object;
# static fields
.field public static volatile currentAlbumId:I
# 0 = Static, 1 = Animated (always rotate), 2 = Playback (follow live play state)
.field public static volatile mode:I
# Live play state from the mini-player (t.c PlayState = Playing)
.field public static isPlayingState:Landroidx/compose/runtime/MutableState;
.annotation system Ldalvik/annotation/Signature;
value = {
"Landroidx/compose/runtime/MutableState<",
"Ljava/lang/Boolean;",
">;"
}
.end annotation
.end field
.field public static coverUuidState:Landroidx/compose/runtime/MutableState;
.annotation system Ldalvik/annotation/Signature;
value = {
"Landroidx/compose/runtime/MutableState<",
"Ljava/lang/String;",
">;"
}
.end annotation
.end field
# direct methods
.method static constructor <clinit>()V
.locals 4
const/4 v0, 0x0
sput v0, Lradiant/HomeBackdrop;->currentAlbumId:I
const/4 v0, 0x2
sput v0, Lradiant/HomeBackdrop;->mode:I
sget-object v0, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;
const/4 v1, 0x0
const/4 v2, 0x2
const/4 v3, 0x0
invoke-static {v0, v1, v2, v3}, Landroidx/compose/runtime/SnapshotStateKt;->mutableStateOf$default(Ljava/lang/Object;Landroidx/compose/runtime/SnapshotMutationPolicy;ILjava/lang/Object;)Landroidx/compose/runtime/MutableState;
move-result-object v0
sput-object v0, Lradiant/HomeBackdrop;->isPlayingState:Landroidx/compose/runtime/MutableState;
const/4 v0, 0x0
const/4 v1, 0x0
const/4 v2, 0x2
const/4 v3, 0x0
invoke-static {v0, v1, v2, v3}, Landroidx/compose/runtime/SnapshotStateKt;->mutableStateOf$default(Ljava/lang/Object;Landroidx/compose/runtime/SnapshotMutationPolicy;ILjava/lang/Object;)Landroidx/compose/runtime/MutableState;
move-result-object v0
sput-object v0, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
return-void
.end method
.method public static setModeStatic()V
.locals 1
const/4 v0, 0x0
sput v0, Lradiant/HomeBackdrop;->mode:I
return-void
.end method
.method public static setModeAnimated()V
.locals 1
const/4 v0, 0x1
sput v0, Lradiant/HomeBackdrop;->mode:I
return-void
.end method
.method public static setModePlayback()V
.locals 1
const/4 v0, 0x2
sput v0, Lradiant/HomeBackdrop;->mode:I
return-void
.end method
# from the mini-player state ctor (t.<init>) whenever play state changes.
.method public static onPlayState(Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState;)V
.locals 2
sget-object v0, Lradiant/HomeBackdrop;->isPlayingState:Landroidx/compose/runtime/MutableState;
if-nez v0, :cond_ready
return-void
:cond_ready
sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState;->Playing:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState;
if-ne p0, v1, :cond_paused
sget-object v1, Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;
goto :goto_set
:cond_paused
sget-object v1, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;
:goto_set
invoke-interface {v0, v1}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static dlog(Ljava/lang/String;)V
.locals 1
const-string v0, "RLHomeBackdrop"
invoke-static {v0, p0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
.method private static isDebugMenuStack()Z
.locals 6
new-instance v0, Ljava/lang/Throwable;
invoke-direct {v0}, Ljava/lang/Throwable;-><init>()V
invoke-virtual {v0}, Ljava/lang/Throwable;->getStackTrace()[Ljava/lang/StackTraceElement;
move-result-object v0
array-length v1, v0
const/4 v2, 0x0
:loop_start
if-ge v2, v1, :loop_end
aget-object v3, v0, v2
invoke-virtual {v3}, Ljava/lang/StackTraceElement;->getClassName()Ljava/lang/String;
move-result-object v3
const-string v4, "debugmenu"
invoke-virtual {v3, v4}, Ljava/lang/String;->contains(Ljava/lang/CharSequence;)Z
move-result v5
if-eqz v5, :next
const/4 v0, 0x1
return v0
:next
add-int/lit8 v2, v2, 0x1
goto :loop_start
:loop_end
const/4 v0, 0x0
return v0
.end method
.method public static fromMiniPlayer(ILjava/lang/String;)V
.locals 2
new-instance v0, Ljava/lang/StringBuilder;
const-string v1, "fromMiniPlayer: id="
invoke-direct {v0, v1}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v0, p0}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v1, " cover="
invoke-virtual {v0, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0, p1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
sput p0, Lradiant/HomeBackdrop;->currentAlbumId:I
sget-object v0, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
invoke-interface {v0, p1}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
return-void
.end method
.method public static onTrack(Lcom/aspiro/wamp/model/Track;)V
.locals 3
const-string v0, "onTrack: entered"
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
if-eqz p0, :clear
:try_start
invoke-virtual {p0}, Lcom/aspiro/wamp/model/MediaItem;->getAlbum()Lcom/aspiro/wamp/model/Album;
move-result-object v0
if-eqz v0, :clear
invoke-virtual {v0}, Lcom/aspiro/wamp/model/Album;->getId()I
move-result v1
sput v1, Lradiant/HomeBackdrop;->currentAlbumId:I
invoke-virtual {v0}, Lcom/aspiro/wamp/model/Album;->getCover()Ljava/lang/String;
move-result-object v0
sget-object v1, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
invoke-interface {v1, v0}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
new-instance v1, Ljava/lang/StringBuilder;
const-string v2, "onTrack: set uuid="
invoke-direct {v1, v2}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v0
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :swallow
return-void
:clear
const-string v0, "onTrack: clearing (null track/album)"
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
const/4 v0, 0x0
sput v0, Lradiant/HomeBackdrop;->currentAlbumId:I
sget-object v1, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
const/4 v2, 0x0
invoke-interface {v1, v2}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
return-void
:swallow
const-string v0, "onTrack: caught throwable"
invoke-static {v0}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
return-void
.end method
.method public static render(Landroidx/compose/runtime/Composer;I)V
.locals 16
.annotation build Landroidx/compose/runtime/Composable;
.end annotation
invoke-static {}, Lradiant/HomeBackdrop;->isDebugMenuStack()Z
move-result v0
if-eqz v0, :proceed
return-void
:proceed
move-object/from16 v0, p0
const-string v1, "render: entered"
invoke-static {v1}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
const v1, 0x52414449
invoke-interface {v0, v1}, Landroidx/compose/runtime/Composer;->startReplaceGroup(I)V
sget-object v1, Lradiant/HomeBackdrop;->coverUuidState:Landroidx/compose/runtime/MutableState;
invoke-interface {v1}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/String;
new-instance v2, Ljava/lang/StringBuilder;
const-string v3, "render: uuid="
invoke-direct {v2, v3}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v2
invoke-static {v2}, Lradiant/HomeBackdrop;->dlog(Ljava/lang/String;)V
if-eqz v1, :skip
invoke-virtual {v1}, Ljava/lang/String;->length()I
move-result v2
if-eqz v2, :skip
# Render TIDAL's NATIVE DarkBlurBackground (p3.a)
sget-object v8, Landroidx/compose/ui/Modifier;->Companion:Landroidx/compose/ui/Modifier$Companion;
const/4 v9, 0x0
const/4 v10, 0x1
const/4 v11, 0x0
invoke-static {v8, v9, v10, v11}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxSize$default(Landroidx/compose/ui/Modifier;FILjava/lang/Object;)Landroidx/compose/ui/Modifier;
move-result-object v13 # v13 = fillMaxSize modifier
sget v8, Lradiant/HomeBackdrop;->currentAlbumId:I # v8 = album id
move-object v9, v1 # v9 = cover uuid
# v10 = isPlaying, mirrored from the Player Backdrop variant
sget v10, Lradiant/HomeBackdrop;->mode:I
const/4 v11, 0x2
if-ne v10, v11, :rl_mode_done
sget-object v10, Lradiant/HomeBackdrop;->isPlayingState:Landroidx/compose/runtime/MutableState;
invoke-interface {v10}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
move-result-object v10
check-cast v10, Ljava/lang/Boolean;
invoke-virtual {v10}, Ljava/lang/Boolean;->booleanValue()Z
move-result v10
:rl_mode_done
const/4 v11, 0x0 # v11 = isSeeking = false
const/4 v12, 0x0 # v12 = progress callback = null (unused while not seeking)
move-object v14, v0 # v14 = composer
const/4 v15, 0x0 # v15 = changed flags
invoke-static/range {v8 .. v15}, Lcom/tidal/android/feature/playerscreen/ui/composables/p3;->a(ILjava/lang/String;ZZLam0/a;Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V
:skip
invoke-interface {v0}, Landroidx/compose/runtime/Composer;->endReplaceGroup()V
return-void
.end method
+61
View File
@@ -0,0 +1,61 @@
.class public final Lradiant/IsrcCache;
.super Ljava/lang/Object;
# Caches trackId(String) -> ISRC(String), captured from TIDAL's own track mapper
.field public static final map:Ljava/util/concurrent/ConcurrentHashMap;
.method static constructor <clinit>()V
.locals 1
new-instance v0, Ljava/util/concurrent/ConcurrentHashMap;
invoke-direct {v0}, Ljava/util/concurrent/ConcurrentHashMap;-><init>()V
sput-object v0, Lradiant/IsrcCache;->map:Ljava/util/concurrent/ConcurrentHashMap;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static put(Ljava/lang/String;Ljava/lang/String;)V
.locals 1
if-eqz p0, :ret
if-eqz p1, :ret
sget-object v0, Lradiant/IsrcCache;->map:Ljava/util/concurrent/ConcurrentHashMap;
invoke-virtual {v0, p0, p1}, Ljava/util/concurrent/ConcurrentHashMap;->put(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;
:ret
return-void
.end method
.method public static get(Ljava/lang/String;)Ljava/lang/String;
.locals 2
const/4 v0, 0x0
if-eqz p0, :none
sget-object v1, Lradiant/IsrcCache;->map:Ljava/util/concurrent/ConcurrentHashMap;
invoke-virtual {v1, p0}, Ljava/util/concurrent/ConcurrentHashMap;->get(Ljava/lang/Object;)Ljava/lang/Object;
move-result-object v0
check-cast v0, Ljava/lang/String;
:none
return-object v0
.end method
@@ -0,0 +1,27 @@
.class public final Lradiant/LyricsReinitGuard;
.super Ljava/lang/Object;
.source "LyricsReinitGuard.smali"
# static fields
# stuff that makes lyrics view stays open.. i forgot what it actually does other than short circuit a call or some shit <3
.field public static lastMediaId:I
.field public static lastVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
# direct methods
.method static constructor <clinit>()V
.locals 1
const/4 v0, -0x1
sput v0, Lradiant/LyricsReinitGuard;->lastMediaId:I
const/4 v0, 0x0
sput-object v0, Lradiant/LyricsReinitGuard;->lastVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
return-void
.end method
@@ -0,0 +1,139 @@
.class public final Lradiant/MiniPlayerBackground;
.super Ljava/lang/Object;
# static fields
.field public static final colorState:Landroidx/compose/runtime/MutableState; # player bg color
# direct methods
.method static constructor <clinit>()V
.locals 2
const/4 v0, 0x0
const/4 v1, 0x2
invoke-static {v0, v0, v1, v0}, Landroidx/compose/runtime/SnapshotStateKt;->mutableStateOf$default(Ljava/lang/Object;Landroidx/compose/runtime/SnapshotMutationPolicy;ILjava/lang/Object;)Landroidx/compose/runtime/MutableState;
move-result-object v0
sput-object v0, Lradiant/MiniPlayerBackground;->colorState:Landroidx/compose/runtime/MutableState;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static color(JLandroidx/compose/runtime/Composer;I)J
.locals 8
.annotation build Landroidx/compose/runtime/Composable;
.end annotation
const v0, 0x4d504442 # compose group key
invoke-interface {p2, v0}, Landroidx/compose/runtime/Composer;->startReplaceGroup(I)V
sget-object v0, Lradiant/MiniPlayerBackground;->colorState:Landroidx/compose/runtime/MutableState;
invoke-interface {v0}, Landroidx/compose/runtime/MutableState;->getValue()Ljava/lang/Object; # read player color
move-result-object v0
check-cast v0, Landroidx/compose/ui/graphics/Color;
if-eqz v0, :fallback
invoke-virtual {v0}, Landroidx/compose/ui/graphics/Color;->unbox-impl()J
move-result-wide p0
:fallback
const/16 v0, 0x2ee # 750ms
const/4 v1, 0x0
const/4 v2, 0x0
const/4 v3, 0x6
const/4 v4, 0x0
invoke-static {v0, v1, v2, v3, v4}, Landroidx/compose/animation/core/AnimationSpecKt;->tween$default(IILandroidx/compose/animation/core/Easing;ILjava/lang/Object;)Landroidx/compose/animation/core/TweenSpec;
move-result-object v2
move-wide v0, p0
const-string v3, "miniPlayerBackgroundColorAnimation"
const/4 v4, 0x0
move-object v5, p2
const/16 v6, 0x1b0
const/16 v7, 0x8
invoke-static/range {v0 .. v7}, Landroidx/compose/animation/SingleValueAnimationKt;->animateColorAsState-euL9pac(JLandroidx/compose/animation/core/AnimationSpec;Ljava/lang/String;Lam0/l;Landroidx/compose/runtime/Composer;II)Landroidx/compose/runtime/State;
move-result-object v0
invoke-interface {v0}, Landroidx/compose/runtime/State;->getValue()Ljava/lang/Object;
move-result-object v0
check-cast v0, Landroidx/compose/ui/graphics/Color;
invoke-virtual {v0}, Landroidx/compose/ui/graphics/Color;->unbox-impl()J
move-result-wide p0
invoke-interface {p2}, Landroidx/compose/runtime/Composer;->endReplaceGroup()V
return-wide p0
.end method
.method public static setHex(Ljava/lang/String;)V
.locals 3
const/4 v0, 0x0
if-eqz p0, :set_color
:try_start
invoke-static {p0}, Landroid/graphics/Color;->parseColor(Ljava/lang/String;)I
move-result p0
invoke-static {p0}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack ARGB color
move-result-wide v1
invoke-static {v1, v2}, Landroidx/compose/ui/graphics/Color;->box-impl(J)Landroidx/compose/ui/graphics/Color;
move-result-object v0
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :catch_parse
goto :set_color
:catch_parse
move-exception p0
:set_color
sget-object p0, Lradiant/MiniPlayerBackground;->colorState:Landroidx/compose/runtime/MutableState;
invoke-interface {p0, v0}, Landroidx/compose/runtime/MutableState;->setValue(Ljava/lang/Object;)V
return-void
.end method
@@ -0,0 +1,436 @@
.class public final Lradiant/MiniSeekerFloating;
.super Ljava/lang/Object;
.implements Lam0/l;
# static fields
.field public static final INSTANCE:Lradiant/MiniSeekerFloating;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/MiniSeekerFloating; # singleton
invoke-direct {v0}, Lradiant/MiniSeekerFloating;-><init>()V # ctor
sput-object v0, Lradiant/MiniSeekerFloating;->INSTANCE:Lradiant/MiniSeekerFloating; # publish
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public final invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 2
check-cast p1, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope; # narrow type
invoke-interface {p1}, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;->drawContent()V # draw pill content
invoke-static {}, Lradiant/MiniSeekerLine;->start()V # lazy subscribe
:try_start
invoke-static {p1}, Lradiant/MiniSeekerFloating;->drawBorder(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V # draw progress
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :swallow
:swallow
sget-object v0, Lkotlin/u;->a:Lkotlin/u; # Unit
return-object v0
.end method
.method private static drawBorder(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)V
.locals 60
sget-object v0, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # state
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F # read tracked
move-result v0 # raw progress
const/4 v1, 0x0 # lower bound
invoke-static {v0, v1}, Ljava/lang/Math;->max(FF)F # clamp low
move-result v0 # clamped low
const/high16 v1, 0x3f800000 # 1.0f upper
invoke-static {v0, v1}, Ljava/lang/Math;->min(FF)F # clamp high
move-result v0 # final progress
const/4 v1, 0x0 # zero
cmpg-float v2, v0, v1 # compare to zero
if-lez v2, :done # skip if empty
move/from16 v16, v0 # stash progress
invoke-interface/range {p0 .. p0}, Landroidx/compose/ui/graphics/drawscope/DrawScope;->getSize-NH-jbRc()J # size long
move-result-wide v0 # size pair
invoke-static {v0, v1}, Landroidx/compose/ui/geometry/Size;->getWidth-impl(J)F # extract width
move-result v2 # width px
move/from16 v17, v2 # stash width
invoke-static {v0, v1}, Landroidx/compose/ui/geometry/Size;->getHeight-impl(J)F # extract height
move-result v2 # height px
move/from16 v18, v2 # stash height
invoke-interface/range {p0 .. p0}, Landroidx/compose/ui/unit/Density;->getDensity()F # density
move-result v2 # px per dp
move/from16 v19, v2 # stash density
const/high16 v0, 0x42100000 # 36.0f dp
move/from16 v1, v19 # density
mul-float v2, v0, v1 # to px
move/from16 v20, v2 # stash r
const/high16 v0, 0x40000000 # 2.0f
move/from16 v1, v20 # r
mul-float v2, v1, v0 # 2*r
move/from16 v21, v2 # stash 2r
move/from16 v0, v17 # w
move/from16 v1, v20 # r
sub-float v2, v0, v1 # w-r
move/from16 v22, v2 # stash w-r
move/from16 v0, v18 # h
move/from16 v1, v20 # r
sub-float v2, v0, v1 # h-r
move/from16 v23, v2 # stash h-r
move/from16 v0, v17 # w
move/from16 v1, v21 # 2r
sub-float v2, v0, v1 # w-2r
move/from16 v24, v2 # stash w-2r
move/from16 v0, v18 # h
move/from16 v1, v21 # 2r
sub-float v2, v0, v1 # h-2r
move/from16 v25, v2 # stash h-2r
move/from16 v0, v17 # w
const/high16 v1, 0x40000000 # 2.0f
div-float v2, v0, v1 # w/2
move/from16 v26, v2 # stash top-center x
const/high16 v0, 0x42b40000 # 90.0f
move/from16 v27, v0 # stash 90
const/4 v0, 0x0 # zero
move/from16 v1, v27 # 90
sub-float v2, v0, v1 # -90
move/from16 v28, v2 # stash -90
const/high16 v0, 0x43340000 # 180.0f
move/from16 v29, v0 # stash 180
invoke-static {}, Landroidx/compose/ui/graphics/AndroidPath_androidKt;->Path()Landroidx/compose/ui/graphics/Path; # new path
move-result-object v0 # path
move-object/from16 v30, v0 # stash path
move-object/from16 v0, v30 # path
move/from16 v1, v26 # w/2
const/4 v2, 0x0 # 0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->moveTo(FF)V # start top-center
move-object/from16 v0, v30 # path
move/from16 v1, v22 # w-r
const/4 v2, 0x0 # 0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # top edge right
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
move/from16 v4, v24 # w-2r
const/4 v5, 0x0 # 0
move/from16 v6, v17 # w
move/from16 v7, v21 # 2r
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # TR rect
move-object/from16 v0, v30 # path
move/from16 v4, v28 # -90
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # TR corner
move-object/from16 v0, v30 # path
move/from16 v1, v17 # w
move/from16 v2, v23 # h-r
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # right edge
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
move/from16 v4, v24 # w-2r
move/from16 v5, v25 # h-2r
move/from16 v6, v17 # w
move/from16 v7, v18 # h
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # BR rect
move-object/from16 v0, v30 # path
const/4 v4, 0x0 # 0 deg
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # BR corner
move-object/from16 v0, v30 # path
move/from16 v1, v20 # r
move/from16 v2, v18 # h
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # bottom edge
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
const/4 v4, 0x0 # 0
move/from16 v5, v25 # h-2r
move/from16 v6, v21 # 2r
move/from16 v7, v18 # h
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # BL rect
move-object/from16 v0, v30 # path
move/from16 v4, v27 # 90
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # BL corner
move-object/from16 v0, v30 # path
const/4 v1, 0x0 # 0
move/from16 v2, v20 # r
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # left edge
new-instance v3, Landroidx/compose/ui/geometry/Rect; # arc bounds
const/4 v4, 0x0 # 0
const/4 v5, 0x0 # 0
move/from16 v6, v21 # 2r
move/from16 v7, v21 # 2r
invoke-direct {v3, v4, v5, v6, v7}, Landroidx/compose/ui/geometry/Rect;-><init>(FFFF)V # TL rect
move-object/from16 v0, v30 # path
move/from16 v4, v29 # 180
move/from16 v5, v27 # 90
const/4 v6, 0x0 # false
invoke-interface {v0, v3, v4, v5, v6}, Landroidx/compose/ui/graphics/Path;->arcTo(Landroidx/compose/ui/geometry/Rect;FFZ)V # TL corner
move-object/from16 v0, v30 # path
move/from16 v1, v26 # w/2
const/4 v2, 0x0 # 0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/graphics/Path;->lineTo(FF)V # close to start
const/high16 v0, 0x40000000 # 2.0f
move/from16 v1, v24 # w-2r
mul-float v2, v1, v0 # 2*(w-2r)
move/from16 v1, v25 # h-2r
mul-float v3, v1, v0 # 2*(h-2r)
add-float v4, v2, v3 # straight edges
const v5, 0x40c90fdb # 2*pi
move/from16 v6, v20 # r
mul-float v7, v6, v5 # arc length
add-float v8, v4, v7 # total perim
move/from16 v31, v8 # stash perim
move/from16 v0, v16 # progress
move/from16 v1, v31 # perim
mul-float v2, v0, v1 # visible length
move/from16 v32, v2 # stash visible
sget-object v0, Landroidx/compose/ui/graphics/PathEffect;->Companion:Landroidx/compose/ui/graphics/PathEffect$Companion; # companion
const/4 v1, 0x2 # length 2
new-array v1, v1, [F # intervals
const/4 v2, 0x0 # index 0
move/from16 v3, v32 # visible
aput v3, v1, v2 # dash length
const/4 v2, 0x1 # index 1
move/from16 v3, v31 # perim
aput v3, v1, v2 # gap length
const/4 v2, 0x0 # phase int
int-to-float v2, v2 # phase float
invoke-virtual {v0, v1, v2}, Landroidx/compose/ui/graphics/PathEffect$Companion;->dashPathEffect([FF)Landroidx/compose/ui/graphics/PathEffect; # dash effect
move-result-object v0 # effect
move-object/from16 v33, v0 # stash effect
const/high16 v0, 0x40000000 # 2.0f
move/from16 v1, v19 # density
mul-float v2, v0, v1 # stroke px
move/from16 v34, v2 # stash stroke width
new-instance v40, Landroidx/compose/ui/graphics/drawscope/Stroke; # stroke obj
move/from16 v41, v34 # width
const/16 v42, 0x0 # miter default
const/16 v43, 0x0 # cap default
const/16 v44, 0x0 # join default
move-object/from16 v45, v33 # effect
const/16 v46, 0xe # default mask
const/16 v47, 0x0 # marker null
invoke-direct/range {v40 .. v47}, Landroidx/compose/ui/graphics/drawscope/Stroke;-><init>(FFIILandroidx/compose/ui/graphics/PathEffect;ILkotlin/jvm/internal/DefaultConstructorMarker;)V # stroke ctor
move-object/from16 v35, v40 # stash stroke
const v0, -0x1 # white ARGB
invoke-static {v0}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color
move-result-wide v0 # color long
move-wide/from16 v36, v0 # stash color
move-object/from16 v50, p0 # scope
move-object/from16 v51, v30 # path
move-wide/from16 v52, v36 # color
const/16 v54, 0x0 # alpha default
move-object/from16 v55, v35 # style
const/16 v56, 0x0 # filter null
const/16 v57, 0x0 # blend default
const/16 v58, 0x34 # default mask
const/16 v59, 0x0 # marker null
invoke-static/range {v50 .. v59}, Landroidx/compose/ui/graphics/drawscope/DrawScope;->drawPath-LG529CI$default(Landroidx/compose/ui/graphics/drawscope/DrawScope;Landroidx/compose/ui/graphics/Path;JFLandroidx/compose/ui/graphics/drawscope/DrawStyle;Landroidx/compose/ui/graphics/ColorFilter;IILjava/lang/Object;)V # draw stroke
:done
return-void
.end method
@@ -0,0 +1,200 @@
.class public final Lradiant/MiniSeekerLine;
.super Ljava/lang/Object;
# static fields
.field public static progressState:Landroidx/compose/runtime/MutableFloatState;
.field public static volatile vm:Lgh/j;
.field public static volatile sub:Lio/reactivex/disposables/Disposable;
# direct methods
.method static constructor <clinit>()V
.locals 1
const/4 v0, 0x0 # initial progress
invoke-static {v0}, Landroidx/compose/runtime/PrimitiveSnapshotStateKt;->mutableFloatStateOf(F)Landroidx/compose/runtime/MutableFloatState; # state holder
move-result-object v0 # state instance
sput-object v0, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # publish state
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method private static dlog(Ljava/lang/String;)V
.locals 1
const-string v0, "RLProgressLine" # log tag
invoke-static {v0, p0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I # info log
return-void
.end method
.method public static start()V
.locals 3
sget-object v0, Lradiant/MiniSeekerLine;->sub:Lio/reactivex/disposables/Disposable; # current sub
if-eqz v0, :start_block # only run once
return-void
:start_block
:try_start
sget-object v0, Lradiant/MiniSeekerLine;->vm:Lgh/j; # cached vm
if-nez v0, :got_vm # reuse if present
invoke-static {}, Lcom/aspiro/wamp/App$a;->a()Lcom/aspiro/wamp/App; # app singleton
move-result-object v0 # app
invoke-virtual {v0}, Lcom/aspiro/wamp/App;->e()Le5/c; # dagger component
move-result-object v0 # component
check-cast v0, Le5/z$n2; # narrow type
iget-object v0, v0, Le5/z$n2;->z0:Ldagger/internal/j; # playqueue provider
invoke-interface {v0}, Lql0/a;->get()Ljava/lang/Object; # resolve
move-result-object v0 # playqueue
check-cast v0, Lcom/aspiro/wamp/playqueue/d1; # narrow type
new-instance v1, Lgh/j; # build vm
invoke-direct {v1, v0}, Lgh/j;-><init>(Lcom/aspiro/wamp/playqueue/d1;)V # ctor
sput-object v1, Lradiant/MiniSeekerLine;->vm:Lgh/j; # cache
:got_vm
sget-object v0, Lradiant/MiniSeekerLine;->vm:Lgh/j; # vm ref
invoke-virtual {v0}, Lgh/j;->a()V # activate emission
iget-object v0, v0, Lgh/j;->d:Lio/reactivex/subjects/BehaviorSubject; # position subject
invoke-static {}, Lio/reactivex/android/schedulers/AndroidSchedulers;->mainThread()Lio/reactivex/Scheduler; # main scheduler
move-result-object v1 # scheduler
invoke-virtual {v0, v1}, Lio/reactivex/Observable;->observeOn(Lio/reactivex/Scheduler;)Lio/reactivex/Observable; # observe main
move-result-object v0 # observable
new-instance v1, Lradiant/SeekerConsumer; # consumer
invoke-direct {v1}, Lradiant/SeekerConsumer;-><init>()V # ctor
invoke-virtual {v0, v1}, Lio/reactivex/Observable;->subscribe(Lio/reactivex/functions/Consumer;)Lio/reactivex/disposables/Disposable; # subscribe
move-result-object v0 # disposable
sput-object v0, Lradiant/MiniSeekerLine;->sub:Lio/reactivex/disposables/Disposable; # cache
const-string v0, "started" # log msg
invoke-static {v0}, Lradiant/MiniSeekerLine;->dlog(Ljava/lang/String;)V # info log
return-void
:try_end
.catch Ljava/lang/Throwable; {:try_start .. :try_end} :swallow
:swallow
move-exception v0 # error
const-string v1, "start failed" # log msg
invoke-static {v1}, Lradiant/MiniSeekerLine;->dlog(Ljava/lang/String;)V # info log
return-void
.end method
.method public static render(Landroidx/compose/runtime/Composer;I)V
.locals 5
.annotation build Landroidx/compose/runtime/Composable;
.end annotation
invoke-static {}, Lradiant/MiniSeekerLine;->start()V # lazy subscribe
sget-object v0, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # state
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F # read tracked
move-result v0 # raw progress
const/4 v1, 0x0 # lower bound
invoke-static {v0, v1}, Ljava/lang/Math;->max(FF)F # clamp low
move-result v0 # clamped low
const/high16 v1, 0x3f800000 # 1.0f upper
invoke-static {v0, v1}, Ljava/lang/Math;->min(FF)F # clamp high
move-result v0 # final progress
const v1, 0x52414c49 # 'RALI' slot key
invoke-interface {p0, v1}, Landroidx/compose/runtime/Composer;->startReplaceGroup(I)V # open group
sget-object v1, Landroidx/compose/ui/Modifier;->Companion:Landroidx/compose/ui/Modifier$Companion; # base modifier
invoke-static {v1, v0}, Landroidx/compose/foundation/layout/SizeKt;->fillMaxWidth(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; # width fraction
move-result-object v0 # filled modifier
const/4 v1, 0x2 # 2 dp
int-to-float v1, v1 # to float
invoke-static {v1}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F # box as Dp
move-result v1 # dp value
invoke-static {v0, v1}, Landroidx/compose/foundation/layout/SizeKt;->height-3ABfNKs(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier; # 2dp tall
move-result-object v0 # sized modifier
const v1, -0x1 # 0xFFFFFFFF white
invoke-static {v1}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J # pack color
move-result-wide v1 # color long
invoke-static {}, Landroidx/compose/ui/graphics/RectangleShapeKt;->getRectangleShape()Landroidx/compose/ui/graphics/Shape; # rect shape
move-result-object v3 # shape
invoke-static {v0, v1, v2, v3}, Landroidx/compose/foundation/BackgroundKt;->background-bw27NRU(Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier; # white bg
move-result-object v0 # final modifier
const/4 v1, 0x0 # composer $changed
invoke-static {v0, p0, v1}, Landroidx/compose/foundation/layout/SpacerKt;->Spacer(Landroidx/compose/ui/Modifier;Landroidx/compose/runtime/Composer;I)V # draw line
invoke-interface {p0}, Landroidx/compose/runtime/Composer;->endReplaceGroup()V # close group
return-void
.end method
+1 -1
View File
@@ -1,6 +1,6 @@
.class public final Lradiant/NoOp;
.super Ljava/lang/Object;
.implements Ltl0/a;
.implements Lam0/a;
# static fields
@@ -0,0 +1,9 @@
.class public final Lradiant/PlayingFrom;
.super Ljava/lang/Object;
.source "PlayingFrom.smali"
# static fields
# puts the "Playing from" source above track title
.field public static a:Ljava/lang/String;
@@ -0,0 +1,124 @@
.class public final Lradiant/QualityBadge;
.super Ljava/lang/Object;
# direct methods
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# qualities : 0 = LOW / HIGH, 1 = LOSSLESS, 2 = HI_RES_LOSSLESS
.method private static backgroundArgb(I)I
.locals 0
packed-switch p0, :quality_switch
const p0, 0x66475569 # LOW / HIGH bg
return p0
:lossless
const p0, 0x3306b6d4 # LOSSLESS bg
return p0
:hires
const p0, 0x40f59e0b # HI_RES_LOSSLESS bg
return p0
:quality_switch
.packed-switch 0x1
:lossless
:hires
.end packed-switch
.end method
.method private static textArgb(I)I
.locals 0
packed-switch p0, :quality_switch
const p0, -0x1d1710 # LOW / HIGH text
return p0
:lossless
const p0, -0x5a0c04 # LOSSLESS text
return p0
:hires
const p0, -0x21976 # HI_RES_LOSSLESS text
return p0
:quality_switch
.packed-switch 0x1
:lossless
:hires
.end packed-switch
.end method
.method public static modifier(Landroidx/compose/ui/Modifier;I)Landroidx/compose/ui/Modifier;
.locals 3
invoke-static {p1}, Lradiant/QualityBadge;->backgroundArgb(I)I
move-result p1
invoke-static {p1}, Landroidx/compose/ui/graphics/ColorKt;->Color(I)J
move-result-wide v0
const/high16 p1, 0x40800000 # 4.0f
invoke-static {p1}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result p1
invoke-static {p1}, Landroidx/compose/foundation/shape/RoundedCornerShapeKt;->RoundedCornerShape-0680j_4(F)Landroidx/compose/foundation/shape/RoundedCornerShape;
move-result-object v2
invoke-static {p0, v0, v1, v2}, Landroidx/compose/foundation/BackgroundKt;->background-bw27NRU(Landroidx/compose/ui/Modifier;JLandroidx/compose/ui/graphics/Shape;)Landroidx/compose/ui/Modifier;
move-result-object p0
const/high16 p1, 0x40c00000 # 6.0f
invoke-static {p1}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result p1
const/high16 v0, 0x40000000 # 2.0f
invoke-static {v0}, Landroidx/compose/ui/unit/Dp;->constructor-impl(F)F
move-result v0
invoke-static {p0, p1, v0}, Landroidx/compose/foundation/layout/PaddingKt;->padding-VpY3zN4(Landroidx/compose/ui/Modifier;FF)Landroidx/compose/ui/Modifier;
move-result-object p0
return-object p0
.end method
.method public static style(ILq20/c2;)Lq20/c2;
.registers 16
invoke-static {p0}, Lradiant/QualityBadge;->textArgb(I)I
move-result v0
new-instance v1, Lf20/a;
invoke-direct {v1, v0}, Lf20/a;-><init>(I)V
new-instance v0, Lf20/d;
const/4 v2, 0x0
const/4 v3, 0x0
const/4 v4, 0x0
const/4 v5, 0x0
const/4 v6, 0x0
const/4 v7, 0x0
const/4 v8, 0x0
const/4 v9, 0x0
const/4 v10, 0x0
const/4 v11, 0x0
const/4 v12, 0x0
const/16 v13, 0xffe
const/4 v14, 0x0
invoke-direct/range {v0 .. v14}, Lf20/d;-><init>(Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;Lf20/a;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
move-object v9, v0
move-object/from16 v0, p1
const/4 v1, 0x0
move-object v2, v9
const/4 v3, 0x0
const/4 v4, 0x0
const/4 v5, 0x0
const/16 v6, 0x1d
const/4 v7, 0x0
invoke-static/range {v0 .. v7}, Lq20/c2;->g(Lq20/c2;Lq20/s4;Lf20/d;Lcom/squareup/ui/market/core/text/MarketTextAlignment;Lcom/squareup/ui/market/core/text/MarketTextTransform;Lq20/r4;ILjava/lang/Object;)Lq20/c2;
move-result-object p0
return-object p0
.end method
@@ -0,0 +1,334 @@
.class public final Lradiant/RLAPILyricsHook;
.super Ljava/lang/Object;
# static fields
.field public static volatile currentKey:Ljava/lang/String;
.field public static volatile currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
.field public static volatile isRlState:Z
.field public static volatile keepOpen:Z
# direct methods
.method static constructor <clinit>()V
.locals 1
const/4 v0, 0x0
sput-object v0, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-object v0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
sput-boolean v0, Lradiant/RLAPILyricsHook;->isRlState:Z
sput-boolean v0, Lradiant/RLAPILyricsHook;->keepOpen:Z
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static dlog(Ljava/lang/String;)V
.locals 1
const-string v0, "RLLyrics"
invoke-static {v0, p0}, Landroid/util/Log;->i(Ljava/lang/String;Ljava/lang/String;)I
return-void
.end method
.method private static isBlank(Ljava/lang/String;)Z
.locals 2
if-nez p0, :not_null
const/4 v0, 0x1
return v0
:not_null
invoke-virtual {p0}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v0
invoke-virtual {v0}, Ljava/lang/String;->isEmpty()Z
move-result v1
return v1
.end method
.method public static isOwner(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
.locals 2
sget-object v0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne p0, v0, :not_owner
sget-boolean v1, Lradiant/RLAPILyricsHook;->isRlState:Z
return v1
:not_owner
const/4 v1, 0x0
return v1
.end method
.method public static shouldKeepOpen(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;)Z
.locals 2
sget-object v0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne p0, v0, :do_not_keep
sget-boolean v1, Lradiant/RLAPILyricsHook;->keepOpen:Z
if-nez v1, :keep
sget-boolean v1, Lradiant/RLAPILyricsHook;->isRlState:Z
:keep
return v1
:do_not_keep
const/4 v1, 0x0
return v1
.end method
.method public static onWampTrack(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;Lcom/aspiro/wamp/model/Track;)V
.locals 11
invoke-static {p1}, Lradiant/HomeBackdrop;->onTrack(Lcom/aspiro/wamp/model/Track;)V
const-string v3, "onWampTrack: hook entered"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
if-eqz p1, :null_track
if-eqz p0, :done
goto :have_track
:null_track
const-string v3, "onWampTrack: wamp Track is null, skip"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :invalid_track
:have_track
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getTitle()Ljava/lang/String;
move-result-object v1
if-nez v1, :title_present
const-string v3, "bail: getTitle() returned null"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :invalid_track
:title_present
invoke-static {v1}, Lradiant/RLAPILyricsHook;->isBlank(Ljava/lang/String;)Z
move-result v2
if-eqz v2, :title_ok
const-string v3, "bail: title is blank"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :invalid_track
:title_ok
# match desktop: primary artist name (artist?.name), fall back to the joined list
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getArtist()Lcom/aspiro/wamp/model/Artist;
move-result-object v3
if-eqz v3, :use_artist_names
invoke-virtual {v3}, Lcom/aspiro/wamp/model/Artist;->getName()Ljava/lang/String;
move-result-object v2
if-nez v2, :artist_present
:use_artist_names
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getArtistNames()Ljava/lang/String;
move-result-object v2
if-nez v2, :artist_present
const-string v3, "bail: artist name null"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :invalid_track
:artist_present
invoke-static {v2}, Lradiant/RLAPILyricsHook;->isBlank(Ljava/lang/String;)Z
move-result v3
if-eqz v3, :artist_ok
const-string v3, "bail: artist is blank"
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :invalid_track
:artist_ok
invoke-virtual {p1}, Lcom/aspiro/wamp/model/MediaItem;->getId()I
move-result v3
invoke-static {v3}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v4
sget-object v5, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
invoke-virtual {v4, v5}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v5
if-eqz v5, :rl_new_track
sget-object v5, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne p0, v5, :rl_new_track
const-string v5, "onWampTrack: same track re-fired, skipping duplicate fetch"
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
:rl_new_track
iget-object v5, p0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-interface {v5}, Lkotlinx/coroutines/flow/MutableStateFlow;->getValue()Ljava/lang/Object;
move-result-object v5
check-cast v5, Ljava/lang/Boolean;
invoke-virtual {v5}, Ljava/lang/Boolean;->booleanValue()Z
move-result v5
if-nez v5, :store_keep_open
sget-object v6, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-eqz v6, :store_keep_open
if-eq p0, v6, :store_keep_open
iget-object v7, v6, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-interface {v7}, Lkotlinx/coroutines/flow/MutableStateFlow;->getValue()Ljava/lang/Object;
move-result-object v7
check-cast v7, Ljava/lang/Boolean;
invoke-virtual {v7}, Ljava/lang/Boolean;->booleanValue()Z
move-result v5
:store_keep_open
sput-boolean v5, Lradiant/RLAPILyricsHook;->keepOpen:Z
const/4 v5, 0x0
sput-boolean v5, Lradiant/RLAPILyricsHook;->isRlState:Z
sput-object v4, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-object p0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
move-object v3, v4
new-instance v5, Ljava/lang/StringBuilder;
const-string v6, "onWampTrack: fetching for title='"
invoke-direct {v5, v6}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v5, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v6, "' artist='"
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v6, "'"
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v5
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
new-instance v5, Lradiant/RLAPILyricsWorker;
move-object v6, p0
move-object v7, v1
move-object v8, v2
move-object v9, v4
move-object v10, v3
invoke-direct/range {v5 .. v10}, Lradiant/RLAPILyricsWorker;-><init>(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
new-instance v6, Ljava/lang/Thread;
invoke-direct {v6, v5}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
const/4 v7, 0x1
invoke-virtual {v6, v7}, Ljava/lang/Thread;->setDaemon(Z)V
invoke-virtual {v6}, Ljava/lang/Thread;->start()V
goto :done
:invalid_track
const/4 v3, 0x0
sput-boolean v3, Lradiant/RLAPILyricsHook;->isRlState:Z
sput-boolean v3, Lradiant/RLAPILyricsHook;->keepOpen:Z
sput-object v3, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-object p0, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
:done
return-void
.end method
@@ -0,0 +1,904 @@
.class public final Lradiant/RLAPILyricsWorker;
.super Ljava/lang/Object;
.implements Ljava/lang/Runnable;
# instance fields
.field public final vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
.field public final title:Ljava/lang/String;
.field public final artist:Ljava/lang/String;
.field public final key:Ljava/lang/String;
.field public final lyricsId:Ljava/lang/String;
.field public static volatile clientIp:Ljava/lang/String;
# direct methods
.method public constructor <init>(Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
iput-object p2, p0, Lradiant/RLAPILyricsWorker;->title:Ljava/lang/String;
iput-object p3, p0, Lradiant/RLAPILyricsWorker;->artist:Ljava/lang/String;
iput-object p4, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
iput-object p5, p0, Lradiant/RLAPILyricsWorker;->lyricsId:Ljava/lang/String;
return-void
.end method
.method private failCurrent()V
.locals 4
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isCurrent()Z
move-result v0
if-eqz v0, :done
invoke-static {}, Lradiant/WordLyrics;->clear()V
const/4 v0, 0x0
sput-object v0, Lradiant/RLAPILyricsHook;->currentKey:Ljava/lang/String;
sput-boolean v0, Lradiant/RLAPILyricsHook;->isRlState:Z
sget-boolean v0, Lradiant/StickyLyrics;->enabled:Z
if-nez v0, :done
sget-boolean v0, Lradiant/RLAPILyricsHook;->keepOpen:Z
if-nez v0, :done
iget-object v1, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
iget-object v2, v1, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
sget-object v3, Ljava/lang/Boolean;->FALSE:Ljava/lang/Boolean;
invoke-interface {v2, v3}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
:done
return-void
.end method
.method private isCurrent()Z
.locals 4
iget-object v0, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
sget-object v1, Lradiant/RLAPILyricsHook;->currentVm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
if-ne v0, v1, :not_current
iget-object v1, v0, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->a:Lbi/e;
invoke-interface {v1}, Lbi/e;->getCurrentItem()Lcom/aspiro/wamp/playqueue/u0;
move-result-object v1
if-eqz v1, :not_current
invoke-interface {v1}, Lcom/aspiro/wamp/playqueue/u0;->getMediaItem()Lcom/aspiro/wamp/model/MediaItem;
move-result-object v1
if-eqz v1, :not_current
invoke-virtual {v1}, Lcom/aspiro/wamp/model/MediaItem;->getId()I
move-result v2
invoke-static {v2}, Ljava/lang/String;->valueOf(I)Ljava/lang/String;
move-result-object v1
iget-object v2, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v3
return v3
:not_current
const/4 v3, 0x0
return v3
.end method
# Public client IP (needed for debugging with ratelimits)
.method static clientIp()Ljava/lang/String;
.locals 6
sget-object v0, Lradiant/RLAPILyricsWorker;->clientIp:Ljava/lang/String;
if-eqz v0, :fetch
return-object v0
:fetch
:try_start
new-instance v0, Ljava/net/URL;
const-string v1, "https://api.ipify.org?format=text"
invoke-direct {v0, v1}, Ljava/net/URL;-><init>(Ljava/lang/String;)V
invoke-virtual {v0}, Ljava/net/URL;->openConnection()Ljava/net/URLConnection;
move-result-object v0
check-cast v0, Ljava/net/HttpURLConnection;
const/16 v1, 0x5dc
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setConnectTimeout(I)V
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setReadTimeout(I)V
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getResponseCode()I
move-result v1
const/16 v2, 0xc8
if-eq v1, v2, :ok
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
goto :fail
:ok
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getInputStream()Ljava/io/InputStream;
move-result-object v1
new-instance v2, Ljava/io/InputStreamReader;
const-string v3, "UTF-8"
invoke-direct {v2, v1, v3}, Ljava/io/InputStreamReader;-><init>(Ljava/io/InputStream;Ljava/lang/String;)V
new-instance v3, Ljava/io/BufferedReader;
invoke-direct {v3, v2}, Ljava/io/BufferedReader;-><init>(Ljava/io/Reader;)V
invoke-virtual {v3}, Ljava/io/BufferedReader;->readLine()Ljava/lang/String;
move-result-object v4
invoke-virtual {v3}, Ljava/io/BufferedReader;->close()V
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
if-eqz v4, :fail
invoke-virtual {v4}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v4
invoke-virtual {v4}, Ljava/lang/String;->isEmpty()Z
move-result v5
if-nez v5, :fail
sput-object v4, Lradiant/RLAPILyricsWorker;->clientIp:Ljava/lang/String;
return-object v4
:try_end
.catchall {:try_start .. :try_end} :catch_all
:catch_all
move-exception v0
:fail
const-string v0, "null"
return-object v0
.end method
.method public static fetch(Ljava/lang/String;Z)Ljava/lang/String;
.locals 7
:try_start
new-instance v0, Ljava/net/URL;
invoke-direct {v0, p0}, Ljava/net/URL;-><init>(Ljava/lang/String;)V
invoke-virtual {v0}, Ljava/net/URL;->openConnection()Ljava/net/URLConnection;
move-result-object v0
check-cast v0, Ljava/net/HttpURLConnection;
const-string v1, "GET"
invoke-virtual {v0, v1}, Ljava/net/HttpURLConnection;->setRequestMethod(Ljava/lang/String;)V
const v1, 0x2710
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setConnectTimeout(I)V
const v1, 0xafc8
invoke-virtual {v0, v1}, Ljava/net/URLConnection;->setReadTimeout(I)V
if-eqz p1, :no_auth
const-string v1, "P-Access-Token-Id"
const-string v2, "58hy4s86"
invoke-virtual {v0, v1, v2}, Ljava/net/URLConnection;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V
const-string v1, "P-Access-Token"
const-string v2, "xjehy2lfg5h5mjwotoxrcqugam"
invoke-virtual {v0, v1, v2}, Ljava/net/URLConnection;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V
const-string v1, "x-client-ip"
invoke-static {}, Lradiant/RLAPILyricsWorker;->clientIp()Ljava/lang/String;
move-result-object v2
invoke-virtual {v0, v1, v2}, Ljava/net/URLConnection;->setRequestProperty(Ljava/lang/String;Ljava/lang/String;)V
:no_auth
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getResponseCode()I
move-result v1
new-instance v3, Ljava/lang/StringBuilder;
const-string v4, "fetch status="
invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v3, v1}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v4, " url="
invoke-virtual {v3, v4}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
invoke-static {v3}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
const/16 v2, 0xc8
if-eq v1, v2, :status_ok
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
const/4 v1, 0x0
return-object v1
:status_ok
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->getInputStream()Ljava/io/InputStream;
move-result-object v1
new-instance v2, Ljava/io/InputStreamReader;
const-string v3, "UTF-8"
invoke-direct {v2, v1, v3}, Ljava/io/InputStreamReader;-><init>(Ljava/io/InputStream;Ljava/lang/String;)V
new-instance v3, Ljava/io/BufferedReader;
invoke-direct {v3, v2}, Ljava/io/BufferedReader;-><init>(Ljava/io/Reader;)V
new-instance v4, Ljava/lang/StringBuilder;
invoke-direct {v4}, Ljava/lang/StringBuilder;-><init>()V
:read_loop
invoke-virtual {v3}, Ljava/io/BufferedReader;->readLine()Ljava/lang/String;
move-result-object v5
if-eqz v5, :read_done
invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const/16 v5, 0xa
invoke-virtual {v4, v5}, Ljava/lang/StringBuilder;->append(C)Ljava/lang/StringBuilder;
goto :read_loop
:read_done
invoke-virtual {v3}, Ljava/io/BufferedReader;->close()V
invoke-virtual {v0}, Ljava/net/HttpURLConnection;->disconnect()V
invoke-virtual {v4}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v6
return-object v6
:try_end
.catchall {:try_start .. :try_end} :catch_all
:catch_all
move-exception v0
new-instance v1, Ljava/lang/StringBuilder;
const-string v2, "fetch exception url="
invoke-direct {v1, v2}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v1, p0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v2, " err="
invoke-virtual {v1, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v0}, Ljava/lang/Throwable;->toString()Ljava/lang/String;
move-result-object v0
invoke-virtual {v1, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v1}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v1
invoke-static {v1}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
const/4 v1, 0x0
return-object v1
.end method
.method public static parseLines(Ljava/lang/String;)Ljava/util/ArrayList;
.locals 11
:try_start
new-instance v0, Lorg/json/JSONObject;
invoke-direct {v0, p0}, Lorg/json/JSONObject;-><init>(Ljava/lang/String;)V
const-string v1, "type"
const-string v2, ""
invoke-virtual {v0, v1, v2}, Lorg/json/JSONObject;->optString(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
const-string v2, "Line"
invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v2
if-nez v2, :type_ok
const-string v2, "Word"
invoke-virtual {v2, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v2
if-nez v2, :type_ok
const/4 v0, 0x0
return-object v0
:type_ok
const-string v1, "data"
invoke-virtual {v0, v1}, Lorg/json/JSONObject;->optJSONArray(Ljava/lang/String;)Lorg/json/JSONArray;
move-result-object v0
if-nez v0, :data_ok
const/4 v0, 0x0
return-object v0
:data_ok
invoke-virtual {v0}, Lorg/json/JSONArray;->length()I
move-result v1
if-nez v1, :nonempty
const/4 v0, 0x0
return-object v0
:nonempty
new-instance v2, Ljava/util/ArrayList;
invoke-direct {v2, v1}, Ljava/util/ArrayList;-><init>(I)V
const/4 v3, 0x0
:loop
if-ge v3, v1, :loop_done
invoke-virtual {v0, v3}, Lorg/json/JSONArray;->getJSONObject(I)Lorg/json/JSONObject;
move-result-object v4
invoke-static {v4}, Lradiant/WordLyrics;->lineText(Lorg/json/JSONObject;)Ljava/lang/String;
move-result-object v5
const-string v6, "startTime"
const-wide/16 v7, 0x0
invoke-virtual {v4, v6, v7, v8}, Lorg/json/JSONObject;->optDouble(Ljava/lang/String;D)D
move-result-wide v7
const-wide v9, 0x408f400000000000L
mul-double/2addr v7, v9
double-to-long v7, v7
new-instance v4, Lcom/tidal/android/feature/playerscreen/ui/f;
invoke-direct {v4, v5, v7, v8}, Lcom/tidal/android/feature/playerscreen/ui/f;-><init>(Ljava/lang/String;J)V
invoke-virtual {v2, v4}, Ljava/util/ArrayList;->add(Ljava/lang/Object;)Z
add-int/lit8 v3, v3, 0x1
goto :loop
:loop_done
return-object v2
:try_end
.catchall {:try_start .. :try_end} :catch_all
:catch_all
move-exception v0
const/4 v1, 0x0
return-object v1
.end method
# Returns "&isrc=<enc>" for the current track
.method private isrcParam()Ljava/lang/String;
.locals 6
iget-object v0, p0, Lradiant/RLAPILyricsWorker;->key:Ljava/lang/String;
const/4 v1, 0x0
:loop
invoke-static {v0}, Lradiant/IsrcCache;->get(Ljava/lang/String;)Ljava/lang/String;
move-result-object v2
if-eqz v2, :retry
invoke-virtual {v2}, Ljava/lang/String;->trim()Ljava/lang/String;
move-result-object v2
invoke-virtual {v2}, Ljava/lang/String;->isEmpty()Z
move-result v3
if-nez v3, :retry
:try_enc_start
invoke-static {v2}, Lradiant/RLAPILyricsWorker;->uric(Ljava/lang/String;)Ljava/lang/String;
move-result-object v2
:try_enc_end
.catchall {:try_enc_start .. :try_enc_end} :enc_fail
:enc_done
new-instance v3, Ljava/lang/StringBuilder;
const-string v4, "&isrc="
invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v2
return-object v2
:enc_fail
move-exception v3
goto :enc_done
:retry
# keep waiting for TIDAL's mapper to cache the ISRC while this track is still current
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isCurrent()Z
move-result v3
if-eqz v3, :giveup
const/16 v3, 0x28
if-ge v1, v3, :giveup
:try_sleep_start
const-wide/16 v3, 0x64
invoke-static {v3, v4}, Ljava/lang/Thread;->sleep(J)V
:try_sleep_end
.catchall {:try_sleep_start .. :try_sleep_end} :sleep_catch
:sleep_done
add-int/lit8 v1, v1, 0x1
goto :loop
:sleep_catch
move-exception v3
goto :sleep_done
:giveup
const-string v0, ""
return-object v0
.end method
# URL-encode like JS encodeURIComponent (so requests are byte identical to the desktop plugin)
.method static uric(Ljava/lang/String;)Ljava/lang/String;
.locals 3
const-string v0, "UTF-8"
invoke-static {p0, v0}, Ljava/net/URLEncoder;->encode(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
const-string v1, "+"
const-string v2, "%20"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%21"
const-string v2, "!"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%27"
const-string v2, "\'"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%28"
const-string v2, "("
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%29"
const-string v2, ")"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
const-string v1, "%7E"
const-string v2, "~"
invoke-virtual {v0, v1, v2}, Ljava/lang/String;->replace(Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Ljava/lang/String;
move-result-object v0
return-object v0
.end method
.method private runImpl()V
.locals 13
iget-object v0, p0, Lradiant/RLAPILyricsWorker;->title:Ljava/lang/String;
invoke-static {v0}, Lradiant/RLAPILyricsWorker;->uric(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
iget-object v2, p0, Lradiant/RLAPILyricsWorker;->artist:Ljava/lang/String;
invoke-static {v2}, Lradiant/RLAPILyricsWorker;->uric(Ljava/lang/String;)Ljava/lang/String;
move-result-object v1
new-instance v2, Ljava/lang/StringBuilder;
const-string v3, "?title="
invoke-direct {v2, v3}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
const-string v0, "&artist="
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v2, v1}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isrcParam()Ljava/lang/String;
move-result-object v0
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-static {}, Lradiant/WordLyrics;->romanizeOn()Z
move-result v0
if-eqz v0, :no_romanize
const-string v0, "&romanize=true"
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
:no_romanize
const-string v0, "&platform=rl-mobile"
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-static {}, Lradiant/WordLyrics;->synthesizeOn()Z
move-result v0
if-eqz v0, :no_synthesize
const-string v0, "&synthesize=true"
invoke-virtual {v2, v0}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
:no_synthesize
invoke-virtual {v2}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v2
new-instance v3, Ljava/lang/StringBuilder;
const-string v4, "https://api.atomix.one/rl-api"
invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
const/4 v4, 0x1
invoke-static {v3, v4}, Lradiant/RLAPILyricsWorker;->fetch(Ljava/lang/String;Z)Ljava/lang/String;
move-result-object v3
if-nez v3, :got_body
new-instance v3, Ljava/lang/StringBuilder;
const-string v4, "https://rl-api.kineticsand.net/lyrics"
invoke-direct {v3, v4}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v3, v2}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v3}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v3
const/4 v4, 0x0
invoke-static {v3, v4}, Lradiant/RLAPILyricsWorker;->fetch(Ljava/lang/String;Z)Ljava/lang/String;
move-result-object v3
if-nez v3, :got_body
const-string v4, "both primary and fallback fetches failed"
invoke-static {v4}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :request_failed
:got_body
move-object v11, v3
invoke-static {v3}, Lradiant/RLAPILyricsWorker;->parseLines(Ljava/lang/String;)Ljava/util/ArrayList;
move-result-object v3
if-nez v3, :parse_ok
const-string v4, "parse returned null (bad JSON / unsupported type)"
invoke-static {v4}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :request_failed
:parse_ok
invoke-virtual {v3}, Ljava/util/ArrayList;->size()I
move-result v4
if-nez v4, :nonempty
const-string v5, "parsed 0 lines"
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
goto :request_failed
:nonempty
new-instance v5, Ljava/lang/StringBuilder;
const-string v6, "parsed "
invoke-direct {v5, v6}, Ljava/lang/StringBuilder;-><init>(Ljava/lang/String;)V
invoke-virtual {v5, v4}, Ljava/lang/StringBuilder;->append(I)Ljava/lang/StringBuilder;
const-string v6, " lines"
invoke-virtual {v5, v6}, Ljava/lang/StringBuilder;->append(Ljava/lang/String;)Ljava/lang/StringBuilder;
invoke-virtual {v5}, Ljava/lang/StringBuilder;->toString()Ljava/lang/String;
move-result-object v5
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->isCurrent()Z
move-result v4
if-nez v4, :key_ok
const-string v5, "race-check failed (user skipped tracks)"
invoke-static {v5}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
return-void
:key_ok
invoke-static {v11}, Lradiant/WordLyrics;->ingest(Ljava/lang/String;)V
iget-object v4, p0, Lradiant/RLAPILyricsWorker;->vm:Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;
iget-object v5, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->O:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-static {v3}, Lvn0/a;->c(Ljava/lang/Iterable;)Lvn0/b;
move-result-object v6
iget-object v7, p0, Lradiant/RLAPILyricsWorker;->lyricsId:Ljava/lang/String;
if-nez v7, :have_id
const-string v7, ""
:have_id
new-instance v8, Lcom/tidal/android/feature/playerscreen/ui/g$c;
const/4 v9, -0x1
const/4 v10, 0x0
invoke-direct {v8, v7, v6, v9, v10}, Lcom/tidal/android/feature/playerscreen/ui/g$c;-><init>(Ljava/lang/String;Lvn0/b;IZ)V
const-string v6, "publishing g$c -> K=true (O=true if sticky) N=g$c"
invoke-static {v6}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
const/4 v6, 0x1
invoke-interface {v5, v8}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
sput-boolean v6, Lradiant/RLAPILyricsHook;->isRlState:Z
iget-object v6, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->K:Lkotlinx/coroutines/flow/MutableStateFlow;
sget-object v7, Ljava/lang/Boolean;->TRUE:Ljava/lang/Boolean;
invoke-interface {v6, v7}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
sget-boolean v9, Lradiant/RLAPILyricsHook;->keepOpen:Z
if-nez v9, :open_lyrics
sget-boolean v9, Lradiant/StickyLyrics;->enabled:Z
if-eqz v9, :skip_n
:open_lyrics
iget-object v6, v4, Lcom/tidal/android/feature/playerscreen/ui/PlayerViewModel;->P:Lkotlinx/coroutines/flow/MutableStateFlow;
invoke-interface {v6, v7}, Lkotlinx/coroutines/flow/MutableStateFlow;->setValue(Ljava/lang/Object;)V
:skip_n
:done
return-void
:request_failed
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->failCurrent()V
return-void
.end method
# virtual methods
.method public run()V
.locals 2
:try_start
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->runImpl()V
:try_end
.catchall {:try_start .. :try_end} :catch_all
return-void
:catch_all
move-exception v0
invoke-virtual {v0}, Ljava/lang/Throwable;->toString()Ljava/lang/String;
move-result-object v1
invoke-static {v1}, Lradiant/RLAPILyricsHook;->dlog(Ljava/lang/String;)V
invoke-direct {p0}, Lradiant/RLAPILyricsWorker;->failCurrent()V
return-void
.end method
@@ -0,0 +1,60 @@
.class public final Lradiant/SeekerConsumer;
.super Ljava/lang/Object;
.implements Lio/reactivex/functions/Consumer;
# direct methods
.method public constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final accept(Ljava/lang/Object;)V
.locals 3
if-eqz p1, :ret # null guard
instance-of v0, p1, Lgh/b; # type guard
if-eqz v0, :ret # skip if wrong type
check-cast p1, Lgh/b; # narrow type
iget v0, p1, Lgh/b;->b:F # raw progress
invoke-static {v0}, Ljava/lang/Float;->isNaN(F)Z # nan check
move-result v1 # nan flag
if-eqz v1, :not_nan # skip clamp if finite
const/4 v0, 0x0 # default to zero
:not_nan
const/4 v1, 0x0 # lower bound
invoke-static {v0, v1}, Ljava/lang/Math;->max(FF)F # clamp low
move-result v0 # clamped low
const/high16 v2, 0x3f800000 # 1.0f upper bound
invoke-static {v0, v2}, Ljava/lang/Math;->min(FF)F # clamp high
move-result v0 # final value
sget-object v1, Lradiant/MiniSeekerLine;->progressState:Landroidx/compose/runtime/MutableFloatState; # shared state
if-eqz v1, :ret # safety
invoke-interface {v1, v0}, Landroidx/compose/runtime/MutableFloatState;->setFloatValue(F)V # publish
:ret
return-void
.end method
@@ -4,7 +4,7 @@
# direct methods
.method public static final a(ILandroidx/compose/runtime/Composer;Landroidx/compose/ui/Modifier;Ltl0/a;)V
.method public static final a(ILandroidx/compose/runtime/Composer;Landroidx/compose/ui/Modifier;Lam0/a;)V
.locals 21
.annotation build Landroidx/compose/runtime/Composable;
.end annotation
@@ -98,7 +98,7 @@
const/4 v4, 0x6
invoke-static {v2, v11, v4}, Lcom/squareup/ui/market/core/theme/w;->t(Lcom/squareup/ui/market/core/theme/k$a;Landroidx/compose/runtime/Composer;I)Lcom/squareup/ui/market/core/theme/MarketStylesheet;
invoke-static {v2, v11, v4}, Lcom/squareup/ui/market/core/theme/z;->t(Lcom/squareup/ui/market/core/theme/k$a;Landroidx/compose/runtime/Composer;I)Lcom/squareup/ui/market/core/theme/MarketStylesheet;
move-result-object v15
@@ -131,7 +131,7 @@
const/16 v18, 0x0
invoke-static/range {v15 .. v20}, Lcom/squareup/ui/market/components/MarketIconButtonKt;->P(Lcom/squareup/ui/market/core/theme/MarketStylesheet;Lcom/squareup/ui/market/core/components/properties/IconButton$Size;Lcom/squareup/ui/market/core/components/properties/IconButton$Rank;Lcom/squareup/ui/market/core/components/properties/IconButton$Variant;ILjava/lang/Object;)Ll20/v1;
invoke-static/range {v15 .. v20}, Lcom/squareup/ui/market/components/MarketIconButtonKt;->P(Lcom/squareup/ui/market/core/theme/MarketStylesheet;Lcom/squareup/ui/market/core/components/properties/IconButton$Size;Lcom/squareup/ui/market/core/components/properties/IconButton$Rank;Lcom/squareup/ui/market/core/components/properties/IconButton$Variant;ILjava/lang/Object;)Lq20/u1;
move-result-object v4
@@ -140,7 +140,7 @@
:cond_5
move-object v9, v4
check-cast v9, Ll20/v1;
check-cast v9, Lq20/u1;
sget v2, Lcom/tidal/android/feature/playerscreen/ui/R$string;->lyrics:I
@@ -172,7 +172,7 @@
const/4 v8, 0x0
invoke-static/range {v1 .. v13}, Lcom/squareup/ui/market/components/MarketIconButtonKt;->c(Ltl0/a;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/interaction/MutableInteractionSource;ZLcom/squareup/ui/market/components/n;Ltl0/a;Ljava/lang/String;Ll20/v1;Ltl0/p;Landroidx/compose/runtime/Composer;II)V
invoke-static/range {v1 .. v13}, Lcom/squareup/ui/market/components/MarketIconButtonKt;->b(Lam0/a;Ljava/lang/String;Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/interaction/MutableInteractionSource;ZLcom/squareup/ui/market/components/n;Lam0/a;Ljava/lang/String;Lq20/u1;Lam0/p;Landroidx/compose/runtime/Composer;II)V
invoke-static {}, Landroidx/compose/runtime/ComposerKt;->isTraceInProgress()Z
@@ -3,7 +3,7 @@
.source "SourceFile"
# interfaces
.implements Ltl0/p;
.implements Lam0/p;
# virtual methods
@@ -35,7 +35,7 @@
invoke-static {v0, p2, v1, v2}, Landroidx/compose/runtime/ComposerKt;->traceEventStart(IIILjava/lang/String;)V
:cond_0
const p2, 0x7f08051d
const p2, 0x7f080522
const/4 v0, 0x0
+1 -1
View File
@@ -1,6 +1,6 @@
.class public final Lradiant/SpvFactory;
.super Ljava/lang/Object;
.implements Ltl0/l;
.implements Lam0/l;
# static fields
@@ -0,0 +1,26 @@
.class public final Lradiant/StickyLyrics;
.super Ljava/lang/Object;
# static fields
.field public static volatile enabled:Z
# direct methods
.method static constructor <clinit>()V
.locals 1
const/4 v0, 0x0
sput-boolean v0, Lradiant/StickyLyrics;->enabled:Z
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
+59
View File
@@ -0,0 +1,59 @@
.class public final Lradiant/SylBrush;
.super Landroidx/compose/ui/graphics/ShaderBrush;
# Paints the syllable wipe gradient
# x0 = wipe edge, x1 = end of the soft trail
# instance state
.field public final x0:F
.field public final x1:F
.field public final c0:I
.field public final c1:I
.method public constructor <init>(FFII)V
.locals 0
invoke-direct {p0}, Landroidx/compose/ui/graphics/ShaderBrush;-><init>()V
iput p1, p0, Lradiant/SylBrush;->x0:F
iput p2, p0, Lradiant/SylBrush;->x1:F
iput p3, p0, Lradiant/SylBrush;->c0:I
iput p4, p0, Lradiant/SylBrush;->c1:I
return-void
.end method
# Gradient across the whole line
.method public createShader-uvyYCjk(J)Landroid/graphics/Shader;
.locals 8
new-instance v0, Landroid/graphics/LinearGradient;
iget v1, p0, Lradiant/SylBrush;->x0:F
const/4 v2, 0x0
iget v3, p0, Lradiant/SylBrush;->x1:F
const/4 v4, 0x0
iget v5, p0, Lradiant/SylBrush;->c0:I
iget v6, p0, Lradiant/SylBrush;->c1:I
sget-object v7, Landroid/graphics/Shader$TileMode;->CLAMP:Landroid/graphics/Shader$TileMode;
invoke-direct/range {v0 .. v7}, Landroid/graphics/LinearGradient;-><init>(FFFFIILandroid/graphics/Shader$TileMode;)V
return-object v0
.end method
+264
View File
@@ -0,0 +1,264 @@
.class public final Lradiant/SylLayout;
.super Ljava/lang/Object;
# interfaces
.implements Lam0/l;
# Remembers where each line's glyphs landed
# static state
.field public static cbs:[Lradiant/SylLayout;
.field public static layouts:[Landroidx/compose/ui/text/TextLayoutResult;
# instance state
.field public final idx:I
.method public constructor <init>(I)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput p1, p0, Lradiant/SylLayout;->idx:I
return-void
.end method
# One reusable callback per line
.method public static cb(I)Lam0/l;
.locals 5
if-gez p0, :have_idx
const/4 p0, 0x0
:have_idx
sget-object v0, Lradiant/SylLayout;->cbs:[Lradiant/SylLayout;
if-eqz v0, :grow
array-length v1, v0
if-gt v1, p0, :have_arr
:grow
add-int/lit8 v1, p0, 0x1
const/16 v2, 0x40
if-ge v1, v2, :cap_ok
const/16 v1, 0x40
:cap_ok
if-nez v0, :copy
new-array v0, v1, [Lradiant/SylLayout;
goto :store_arr
:copy
invoke-static {v0, v1}, Ljava/util/Arrays;->copyOf([Ljava/lang/Object;I)[Ljava/lang/Object;
move-result-object v0
check-cast v0, [Lradiant/SylLayout;
:store_arr
sput-object v0, Lradiant/SylLayout;->cbs:[Lradiant/SylLayout;
:have_arr
aget-object v1, v0, p0
if-nez v1, :ret
new-instance v1, Lradiant/SylLayout;
invoke-direct {v1, p0}, Lradiant/SylLayout;-><init>(I)V
aput-object v1, v0, p0
:ret
return-object v1
.end method
.method public static get(I)Landroidx/compose/ui/text/TextLayoutResult;
.locals 2
if-ltz p0, :none
sget-object v0, Lradiant/SylLayout;->layouts:[Landroidx/compose/ui/text/TextLayoutResult;
if-eqz v0, :none
array-length v1, v0
if-ge p0, v1, :none
aget-object v0, v0, p0
return-object v0
:none
const/4 v0, 0x0
return-object v0
.end method
# Forget the layouts on track change
.method public static reset()V
.locals 1
const/4 v0, 0x0
sput-object v0, Lradiant/SylLayout;->layouts:[Landroidx/compose/ui/text/TextLayoutResult;
return-void
.end method
# Font size in pixels
.method public static em(Landroidx/compose/ui/text/TextLayoutResult;)F
.locals 8
invoke-virtual {p0}, Landroidx/compose/ui/text/TextLayoutResult;->getLayoutInput()Landroidx/compose/ui/text/TextLayoutInput;
move-result-object v0
invoke-virtual {v0}, Landroidx/compose/ui/text/TextLayoutInput;->getStyle()Landroidx/compose/ui/text/TextStyle;
move-result-object v1
invoke-virtual {v1}, Landroidx/compose/ui/text/TextStyle;->getFontSize-XSAIIZE()J
move-result-wide v1
# Only Sp converts to pixels
invoke-static {v1, v2}, Landroidx/compose/ui/unit/TextUnit;->getType-UIouoOA(J)J
move-result-wide v3
sget-object v5, Landroidx/compose/ui/unit/TextUnitType;->Companion:Landroidx/compose/ui/unit/TextUnitType$Companion;
invoke-virtual {v5}, Landroidx/compose/ui/unit/TextUnitType$Companion;->getSp-UIouoOA()J
move-result-wide v5
invoke-static {v3, v4, v5, v6}, Landroidx/compose/ui/unit/TextUnitType;->equals-impl0(JJ)Z
move-result v3
if-eqz v3, :fallback
invoke-virtual {v0}, Landroidx/compose/ui/text/TextLayoutInput;->getDensity()Landroidx/compose/ui/unit/Density;
move-result-object v0
invoke-interface {v0, v1, v2}, Landroidx/compose/ui/unit/Density;->toPx--R2X_6o(J)F
move-result v0
const/4 v1, 0x0
cmpg-float v1, v0, v1
if-lez v1, :fallback
return v0
:fallback
# No font size, guess from the line box
const/4 v0, 0x0
invoke-virtual {p0, v0}, Landroidx/compose/ui/text/TextLayoutResult;->getLineBottom(I)F
move-result v1
const/4 v0, 0x0
invoke-virtual {p0, v0}, Landroidx/compose/ui/text/TextLayoutResult;->getLineTop(I)F
move-result v2
sub-float/2addr v1, v2
const/4 v0, 0x0
cmpg-float v0, v1, v0
if-lez v0, :default_em
const v0, 0x3f4ccccd # 0.8f
mul-float/2addr v0, v1
return v0
:default_em
const/high16 v0, 0x42200000 # 40.0f
return v0
.end method
# virtual methods
.method public invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 4
iget v0, p0, Lradiant/SylLayout;->idx:I
if-ltz v0, :ret
instance-of v1, p1, Landroidx/compose/ui/text/TextLayoutResult;
if-eqz v1, :ret
sget-object v1, Lradiant/SylLayout;->layouts:[Landroidx/compose/ui/text/TextLayoutResult;
if-eqz v1, :grow
array-length v2, v1
if-gt v2, v0, :store
:grow
add-int/lit8 v2, v0, 0x1
const/16 v3, 0x40
if-ge v2, v3, :cap_ok
const/16 v2, 0x40
:cap_ok
if-nez v1, :copy
new-array v1, v2, [Landroidx/compose/ui/text/TextLayoutResult;
goto :store
:copy
invoke-static {v1, v2}, Ljava/util/Arrays;->copyOf([Ljava/lang/Object;I)[Ljava/lang/Object;
move-result-object v1
check-cast v1, [Landroidx/compose/ui/text/TextLayoutResult;
:store
sput-object v1, Lradiant/SylLayout;->layouts:[Landroidx/compose/ui/text/TextLayoutResult;
check-cast p1, Landroidx/compose/ui/text/TextLayoutResult;
aput-object p1, v1, v0
:ret
sget-object v0, Lkotlin/u;->a:Lkotlin/u;
return-object v0
.end method
@@ -0,0 +1,87 @@
.class public final Lradiant/WazeInitReceiver;
.super Landroid/content/BroadcastReceiver;
.source "WazeInitReceiver.smali"
# direct methods
.method public constructor <init>()V
.locals 0
invoke-direct {p0}, Landroid/content/BroadcastReceiver;-><init>()V
return-void
.end method
# virtual methods
.method public onReceive(Landroid/content/Context;Landroid/content/Intent;)V
.locals 5
if-eqz p1, :return
if-eqz p2, :return
const-string v0, "com.waze.sdk.audio.ACTION_INIT"
invoke-virtual {p2}, Landroid/content/Intent;->getAction()Ljava/lang/String;
move-result-object v1
invoke-virtual {v0, v1}, Ljava/lang/String;->equals(Ljava/lang/Object;)Z
move-result v0
if-eqz v0, :return
const-string v0, "token"
invoke-virtual {p2, v0}, Landroid/content/Intent;->getStringExtra(Ljava/lang/String;)Ljava/lang/String;
move-result-object v0
if-eqz v0, :return
invoke-virtual {v0}, Ljava/lang/String;->length()I
move-result v1
if-lez v1, :return
invoke-virtual {p1}, Landroid/content/Context;->getApplicationContext()Landroid/content/Context;
move-result-object v1
if-eqz v1, :context_ready
move-object p1, v1
:context_ready
new-instance v1, Landroid/content/Intent;
invoke-direct {v1}, Landroid/content/Intent;-><init>()V
const-string v2, "com.waze"
const-string v3, "com.waze.sdk.SdkService"
invoke-virtual {v1, v2, v3}, Landroid/content/Intent;->setClassName(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;
new-instance v2, Lradiant/WazeServiceConnection;
invoke-direct {v2, p1, v0}, Lradiant/WazeServiceConnection;-><init>(Landroid/content/Context;Ljava/lang/String;)V
:try_start_0
const/4 v3, 0x1
invoke-virtual {p1, v1, v2, v3}, Landroid/content/Context;->bindService(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z
:try_end_0
.catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0
goto :return
:catch_0
move-exception v4
:return
return-void
.end method
@@ -0,0 +1,228 @@
.class public final Lradiant/WazeServiceConnection;
.super Ljava/lang/Object;
.source "WazeServiceConnection.smali"
# interfaces
.implements Landroid/content/ServiceConnection;
# instance fields
.field private final context:Landroid/content/Context;
.field private final token:Ljava/lang/String;
# direct methods
.method public constructor <init>(Landroid/content/Context;Ljava/lang/String;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/WazeServiceConnection;->context:Landroid/content/Context;
iput-object p2, p0, Lradiant/WazeServiceConnection;->token:Ljava/lang/String;
return-void
.end method
.method private addOpenMeIntent(Landroid/os/Bundle;)V
.locals 4
iget-object v0, p0, Lradiant/WazeServiceConnection;->context:Landroid/content/Context;
invoke-virtual {v0}, Landroid/content/Context;->getPackageManager()Landroid/content/pm/PackageManager;
move-result-object v1
invoke-virtual {v0}, Landroid/content/Context;->getPackageName()Ljava/lang/String;
move-result-object v2
invoke-virtual {v1, v2}, Landroid/content/pm/PackageManager;->getLaunchIntentForPackage(Ljava/lang/String;)Landroid/content/Intent;
move-result-object v1
if-eqz v1, :return
const/high16 v2, 0x10000000
invoke-virtual {v1, v2}, Landroid/content/Intent;->addFlags(I)Landroid/content/Intent;
const v2, 0xc000000
const/4 v3, 0x0
invoke-static {v0, v3, v1, v2}, Landroid/app/PendingIntent;->getActivity(Landroid/content/Context;ILandroid/content/Intent;I)Landroid/app/PendingIntent;
move-result-object v1
const-string v2, "openMeIntent"
invoke-virtual {p1, v2, v1}, Landroid/os/Bundle;->putParcelable(Ljava/lang/String;Landroid/os/Parcelable;)V
:return
return-void
.end method
.method private createClientMessenger()Landroid/os/Messenger;
.locals 3
new-instance v0, Landroid/os/Handler;
invoke-static {}, Landroid/os/Looper;->getMainLooper()Landroid/os/Looper;
move-result-object v1
invoke-direct {v0, v1}, Landroid/os/Handler;-><init>(Landroid/os/Looper;)V
new-instance v2, Landroid/os/Messenger;
invoke-direct {v2, v0}, Landroid/os/Messenger;-><init>(Landroid/os/Handler;)V
return-object v2
.end method
.method private createConfigBundle()Landroid/os/Bundle;
.locals 3
new-instance v0, Landroid/os/Bundle;
invoke-direct {v0}, Landroid/os/Bundle;-><init>()V
const-string v1, "versionCode"
const/16 v2, 0xa
invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V
const-string v1, "themeColor"
const v2, __RL_WAZE_THEME_COLOR__ # Waze SDK themeColor
invoke-virtual {v0, v1, v2}, Landroid/os/Bundle;->putInt(Ljava/lang/String;I)V
invoke-direct {p0, v0}, Lradiant/WazeServiceConnection;->addOpenMeIntent(Landroid/os/Bundle;)V
return-object v0
.end method
.method private register(Landroid/os/IBinder;)V
.locals 5
if-eqz p1, :return
invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v0
invoke-static {}, Landroid/os/Parcel;->obtain()Landroid/os/Parcel;
move-result-object v1
const-string v2, "com.waze.sdk.ISdkService"
invoke-virtual {v0, v2}, Landroid/os/Parcel;->writeInterfaceToken(Ljava/lang/String;)V
iget-object v2, p0, Lradiant/WazeServiceConnection;->token:Ljava/lang/String;
invoke-virtual {v0, v2}, Landroid/os/Parcel;->writeString(Ljava/lang/String;)V
invoke-direct {p0}, Lradiant/WazeServiceConnection;->createConfigBundle()Landroid/os/Bundle;
move-result-object v2
invoke-static {v0, v2}, Lradiant/WazeServiceConnection;->writeParcelable(Landroid/os/Parcel;Landroid/os/Parcelable;)V
invoke-direct {p0}, Lradiant/WazeServiceConnection;->createClientMessenger()Landroid/os/Messenger;
move-result-object v2
invoke-static {v0, v2}, Lradiant/WazeServiceConnection;->writeParcelable(Landroid/os/Parcel;Landroid/os/Parcelable;)V
const/4 v2, 0x1
const/4 v3, 0x0
invoke-interface {p1, v2, v0, v1, v3}, Landroid/os/IBinder;->transact(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z
move-result v4
if-eqz v4, :cleanup
invoke-virtual {v1}, Landroid/os/Parcel;->readException()V
:cleanup
invoke-virtual {v1}, Landroid/os/Parcel;->recycle()V
invoke-virtual {v0}, Landroid/os/Parcel;->recycle()V
:return
return-void
.end method
.method private safeUnbind()V
.locals 1
:try_start_0
iget-object v0, p0, Lradiant/WazeServiceConnection;->context:Landroid/content/Context;
invoke-virtual {v0, p0}, Landroid/content/Context;->unbindService(Landroid/content/ServiceConnection;)V
:try_end_0
.catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0
goto :return
:catch_0
move-exception v0
:return
return-void
.end method
.method private static writeParcelable(Landroid/os/Parcel;Landroid/os/Parcelable;)V
.locals 2
const/4 v0, 0x0
if-eqz p1, :null_value
const/4 v1, 0x1
invoke-virtual {p0, v1}, Landroid/os/Parcel;->writeInt(I)V
invoke-interface {p1, p0, v0}, Landroid/os/Parcelable;->writeToParcel(Landroid/os/Parcel;I)V
return-void
:null_value
invoke-virtual {p0, v0}, Landroid/os/Parcel;->writeInt(I)V
return-void
.end method
# virtual methods
.method public onServiceConnected(Landroid/content/ComponentName;Landroid/os/IBinder;)V
.locals 1
:try_start_0
invoke-direct {p0, p2}, Lradiant/WazeServiceConnection;->register(Landroid/os/IBinder;)V
:try_end_0
.catch Ljava/lang/Throwable; {:try_start_0 .. :try_end_0} :catch_0
goto :unbind
:catch_0
move-exception v0
:unbind
invoke-direct {p0}, Lradiant/WazeServiceConnection;->safeUnbind()V
return-void
.end method
.method public onServiceDisconnected(Landroid/content/ComponentName;)V
.locals 0
return-void
.end method
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,21 @@
.class public final Lradiant/WordLyricsTick;
.super Ljava/lang/Object;
.implements Ljava/lang/Runnable;
# 50ms clock tick driver for word level lyrics highlighting
.method public constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public run()V
.locals 0
invoke-static {}, Lradiant/WordLyrics;->tick()V
return-void
.end method
@@ -0,0 +1,53 @@
.class public final Lradiant/gestures/LyricsDrag$Guard;
.super Ljava/lang/Object;
# interfaces
.implements Landroidx/compose/ui/input/nestedscroll/NestedScrollConnection;
# Blocks leftover scroll from the sheet
# static state
.field public static final INSTANCE:Lradiant/gestures/LyricsDrag$Guard;
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/gestures/LyricsDrag$Guard;
invoke-direct {v0}, Lradiant/gestures/LyricsDrag$Guard;-><init>()V
sput-object v0, Lradiant/gestures/LyricsDrag$Guard;->INSTANCE:Lradiant/gestures/LyricsDrag$Guard;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
# Consume what the list didn't use
.method public onPostScroll-DzOQY0M(JJI)J
.locals 0
return-wide p3
.end method
.method public onPostFling-RZ2iAVY(JJLkotlin/coroutines/e;)Ljava/lang/Object;
.locals 2
invoke-static {p3, p4}, Landroidx/compose/ui/unit/Velocity;->box-impl(J)Landroidx/compose/ui/unit/Velocity;
move-result-object v0
return-object v0
.end method
@@ -0,0 +1,28 @@
.class public final Lradiant/gestures/LyricsDrag;
.super Ljava/lang/Object;
# Stops lyrics scroll dismissing the player
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# Stop leftover scroll reaching the sheet
.method public static wrap(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
.locals 2
sget-object v0, Lradiant/gestures/LyricsDrag$Guard;->INSTANCE:Lradiant/gestures/LyricsDrag$Guard;
const/4 v1, 0x0
invoke-static {p0, v0, v1}, Landroidx/compose/ui/input/nestedscroll/NestedScrollModifierKt;->nestedScroll(Landroidx/compose/ui/Modifier;Landroidx/compose/ui/input/nestedscroll/NestedScrollConnection;Landroidx/compose/ui/input/nestedscroll/NestedScrollDispatcher;)Landroidx/compose/ui/Modifier;
move-result-object p0
return-object p0
.end method
@@ -0,0 +1,62 @@
.class public final Lradiant/gestures/MiniPlayerGestures$ApplyPending;
.super Ljava/lang/Object;
.implements Ljava/lang/Runnable;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "ApplyPending"
.end annotation
# instance fields
.field public final a:Landroidx/compose/material3/SheetState;
.field public final b:I
# direct methods
.method public constructor <init>(Landroidx/compose/material3/SheetState;I)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/gestures/MiniPlayerGestures$ApplyPending;->a:Landroidx/compose/material3/SheetState;
iput p2, p0, Lradiant/gestures/MiniPlayerGestures$ApplyPending;->b:I
return-void
.end method
# virtual methods
# Apply once, then re schedule if there is still pending movement
# -> Sheet anchors can appear one (or more) frames after opening the player, so this keeps applying the latest drag until no delta remains
.method public final run()V
.locals 3
iget-object v0, p0, Lradiant/gestures/MiniPlayerGestures$ApplyPending;->a:Landroidx/compose/material3/SheetState;
invoke-static {v0}, Lradiant/gestures/MiniPlayerGestures;->applyPendingDrag(Landroidx/compose/material3/SheetState;)V
iget v1, p0, Lradiant/gestures/MiniPlayerGestures$ApplyPending;->b:I
add-int/lit8 v1, v1, -0x1
if-lez v1, :done
invoke-static {v0}, Lradiant/gestures/MiniPlayerGestures;->needsApply(Landroidx/compose/material3/SheetState;)Z
move-result v2
if-eqz v2, :done
invoke-static {v0, v1}, Lradiant/gestures/MiniPlayerGestures;->scheduleApply(Landroidx/compose/material3/SheetState;I)V
:done
return-void
.end method
@@ -0,0 +1,70 @@
.class public final Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;
.super Ljava/lang/Object;
.implements Lam0/l;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "FeedbackLayer"
.end annotation
# static fields
.field public static final INSTANCE:Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;
invoke-direct {v0}, Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;-><init>()V
sput-object v0, Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;->INSTANCE:Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final invoke(Landroidx/compose/ui/graphics/GraphicsLayerScope;)Lkotlin/u;
.locals 1
sget-object v0, Lradiant/gestures/MiniPlayerGestures;->v:Landroidx/compose/runtime/MutableFloatState;
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F
move-result v0
invoke-interface {p1, v0}, Landroidx/compose/ui/graphics/GraphicsLayerScope;->setTranslationY(F)V
sget-object p1, Lkotlin/u;->a:Lkotlin/u;
return-object p1
.end method
.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 0
check-cast p1, Landroidx/compose/ui/graphics/GraphicsLayerScope;
invoke-virtual {p0, p1}, Lradiant/gestures/MiniPlayerGestures$FeedbackLayer;->invoke(Landroidx/compose/ui/graphics/GraphicsLayerScope;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
@@ -0,0 +1,60 @@
.class public final Lradiant/gestures/MiniPlayerGestures$FeedbackResetAnimator;
.super Ljava/lang/Object;
.implements Landroid/animation/ValueAnimator$AnimatorUpdateListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "FeedbackResetAnimator"
.end annotation
# direct methods
.method public constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final onAnimationUpdate(Landroid/animation/ValueAnimator;)V
.locals 3
move-object v0, p1
invoke-virtual {p1}, Landroid/animation/ValueAnimator;->getAnimatedValue()Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/Float;
invoke-virtual {v1}, Ljava/lang/Float;->floatValue()F
move-result v1
invoke-static {v1}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
invoke-virtual {v0}, Landroid/animation/ValueAnimator;->getAnimatedFraction()F
move-result v1
const/high16 v2, 0x3f800000 # 1.0f
cmpg-float v1, v1, v2
if-gez v1, :done
invoke-static {v0}, Lradiant/gestures/MiniPlayerGestures;->clearSwipeFeedbackAnimator(Landroid/animation/ValueAnimator;)V
:done
return-void
.end method
@@ -0,0 +1,521 @@
.class public final Lradiant/gestures/MiniPlayerGestures$Gesture;
.super Ljava/lang/Object;
.implements Lam0/l;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "Gesture"
.end annotation
# instance fields
.field public final a:Lam0/l;
# Y at ACTION_DOWN
.field public b:F
# Last Y seen (on this pointer stream)
.field public c:F
# True while this listener owns an active pointer stream
.field public d:Z
# True when sheet drag has started
.field public e:Z
.field public final f:Landroidx/compose/material3/SheetState;
.field public g:F
# X at ACTION_DOWN
.field public i:F
# True once horizontal movement wins (prevents later vertical start)
.field public j:Z
# Drag option enabled
.field public final k:Z
# direct methods
.method public constructor <init>(Lam0/l;Landroidx/compose/material3/SheetState;Z)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->a:Lam0/l;
iput-object p2, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState;
iput-boolean p3, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->k:Z
return-void
.end method
.method private static dp(F)F
.locals 1
invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources;
move-result-object v0
invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
move-result-object v0
iget v0, v0, Landroid/util/DisplayMetrics;->density:F
mul-float/2addr p0, v0
return p0
.end method
.method private dragSheet(FJ)V
.locals 1
iget-object v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState;
invoke-static {v0, p1, p2, p3}, Lradiant/gestures/MiniPlayerGestures;->updateDragTimed(Landroidx/compose/material3/SheetState;FJ)V
return-void
.end method
.method private finishSheet(F)V
.locals 2
iget-object v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState;
iget-object v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->a:Lam0/l;
invoke-static {v0, v1, p1}, Lradiant/gestures/MiniPlayerGestures;->finishDrag(Landroidx/compose/material3/SheetState;Lam0/l;F)V
return-void
.end method
.method private openPlayer()V
.locals 2
iget-object v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->a:Lam0/l;
sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/b$b;->a:Lcom/tidal/android/feature/appscaffold/ui/b$b;
invoke-interface {v0, v1}, Lam0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
return-void
.end method
.method private reset()V
.locals 1
const/4 v0, 0x0
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->d:Z
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->j:Z
const/4 v0, 0x0
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->i:F
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->g:F
return-void
.end method
.method private startSheet(FJ)V
.locals 2
# empty player guard
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->hasMiniPlayerMedia()Z
move-result v0
if-nez v0, :start_guard
return-void
:start_guard
# avoid starting twice
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
if-eqz v0, :start
return-void
:start
const/4 v0, 0x1
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->k:Z
if-eqz v0, :done
iget-object v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState;
iget v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->b:F
invoke-static {v1, p1, v0, p2, p3}, Lradiant/gestures/MiniPlayerGestures;->beginDrag(Landroidx/compose/material3/SheetState;FFJ)V
invoke-direct {p0}, Lradiant/gestures/MiniPlayerGestures$Gesture;->openPlayer()V
:done
return-void
.end method
# virtual methods
# Listens for MotionEvents
.method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u;
.locals 11
invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I
move-result v0
if-eqz v0, :down
const/4 v1, 0x1
if-eq v0, v1, :up
const/4 v1, 0x2
if-eq v0, v1, :move
const/4 v1, 0x3
if-eq v0, v1, :cancel
goto :done
# ACTION_DOWN
:down
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->resetGestureLock()V
invoke-static {p1}, Lradiant/gestures/MiniPlayerGestures;->beginMotion(Landroid/view/MotionEvent;)V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v0
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->b:F
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->c:F
const/4 v0, 0x1
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->d:Z
const/4 v0, 0x0
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
const/4 v0, 0x0
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->g:F
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v0
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->i:F
const/4 v0, 0x0
iput-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->j:Z
iget v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->b:F
invoke-static {v0}, Lradiant/gestures/MiniPlayerGestures;->beginSwipeGesture(F)V
:skip_feedback_down
goto :done
# ACTION_MOVE
:move
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->d:Z
if-eqz v0, :done
invoke-static {p1}, Lradiant/gestures/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v0
invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J
move-result-wide v5
iget v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->c:F
sub-float v1, v0, v1
iput v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->g:F
iget v2, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->b:F
sub-float v2, v0, v2
iget-boolean v3, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
if-eqz v3, :maybe_start
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->suppressHorizontalGestures()V
iget-boolean v3, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->k:Z
if-eqz v3, :feedback_active
invoke-direct {p0, v2, v5, v6}, Lradiant/gestures/MiniPlayerGestures$Gesture;->dragSheet(FJ)V
:feedback_active
invoke-static {v2}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackFromDrag(F)V
goto :store_last
:maybe_start
iget-boolean v3, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->j:Z
if-eqz v3, :axis_check
goto :store_last
:axis_check
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v7
iget v8, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->i:F
sub-float/2addr v7, v8
invoke-static {v7}, Ljava/lang/Math;->abs(F)F
move-result v7
invoke-static {v2}, Ljava/lang/Math;->abs(F)F
move-result v8
const/high16 v9, 0x40c00000 # 6.0f axis deadzone
invoke-static {v9}, Lradiant/gestures/MiniPlayerGestures$Gesture;->dp(F)F
move-result v9
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->isHorizontalGestureLocked()Z
move-result v10
if-eqz v10, :horizontal_check
const/4 v10, 0x0
invoke-static {v10}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
goto :store_last
:horizontal_check
cmpg-float v10, v7, v9
if-gez v10, :horizontal_dominance
goto :vertical_check
:horizontal_dominance
const v10, 0x3f8ccccd # 1.1f
mul-float/2addr v10, v8
cmpg-float v10, v7, v10
if-gez v10, :horizontal_wins
goto :vertical_check
:horizontal_wins
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->lockHorizontalGesture()Z
move-result v10
if-eqz v10, :store_last
const/4 v10, 0x1
iput-boolean v10, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->j:Z
const/4 v10, 0x0
invoke-static {v10}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
goto :store_last
# At least 16dp up to open the player
:vertical_check
neg-float v9, v9
cmpg-float v10, v2, v9
if-lez v10, :vertical_dominance
invoke-static {v2}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackFromDrag(F)V
goto :store_last
:vertical_dominance
const v10, 0x3f8ccccd # 1.1f -> early vertical axis lock
mul-float/2addr v7, v10
cmpl-float v10, v8, v7
if-gtz v10, :begin
goto :store_last
:begin
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->lockVerticalGesture()Z
move-result v10
if-eqz v10, :store_last
invoke-direct {p0, v2, v5, v6}, Lradiant/gestures/MiniPlayerGestures$Gesture;->startSheet(FJ)V
iget-boolean v10, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
if-eqz v10, :store_last
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->suppressHorizontalGestures()V
iget-boolean v10, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->k:Z
if-eqz v10, :feedback_begin
invoke-direct {p0, v2, v5, v6}, Lradiant/gestures/MiniPlayerGestures$Gesture;->dragSheet(FJ)V
:feedback_begin
invoke-static {v2}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackFromDrag(F)V
:store_last
iput v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->c:F
goto :done
# ACTION_UP
:up
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->d:Z
if-eqz v0, :done
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
if-eqz v0, :up_no_drag
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->k:Z
if-eqz v0, :open_simple
invoke-static {p1}, Lradiant/gestures/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v0
invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J
move-result-wide v5
iget-object v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState;
invoke-static {v1, v0, v5, v6}, Lradiant/gestures/MiniPlayerGestures;->updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V
invoke-static {v1}, Lradiant/gestures/MiniPlayerGestures;->releaseVelocity(Landroidx/compose/material3/SheetState;)F
move-result v0
invoke-direct {p0, v0}, Lradiant/gestures/MiniPlayerGestures$Gesture;->finishSheet(F)V
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :finish
:up_no_drag
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :finish
:open_simple
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->suppressHorizontalGestures()V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v0
iget-object v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->a:Lam0/l;
invoke-static {v1, v0}, Lradiant/gestures/MiniPlayerGestures;->completeSimpleSwipe(Lam0/l;F)Z
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
:finish
invoke-direct {p0}, Lradiant/gestures/MiniPlayerGestures$Gesture;->reset()V
goto :done
# ACTION_CANCEL
:cancel
iget-boolean v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->e:Z
if-eqz v0, :cancel_no_active
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
iget-object v0, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->f:Landroidx/compose/material3/SheetState;
iget-object v1, p0, Lradiant/gestures/MiniPlayerGestures$Gesture;->a:Lam0/l;
invoke-static {v0, v1}, Lradiant/gestures/MiniPlayerGestures;->cancelDrag(Landroidx/compose/material3/SheetState;Lam0/l;)V
goto :cancel_reset
:cancel_no_active
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
:cancel_reset
invoke-direct {p0}, Lradiant/gestures/MiniPlayerGestures$Gesture;->reset()V
:done
sget-object p1, Lkotlin/u;->a:Lkotlin/u;
return-object p1
.end method
.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 0
check-cast p1, Landroid/view/MotionEvent;
invoke-virtual {p0, p1}, Lradiant/gestures/MiniPlayerGestures$Gesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
@@ -0,0 +1,178 @@
.class public final Lradiant/gestures/MiniPlayerGestures$RootGesture;
.super Ljava/lang/Object;
.implements Lam0/l;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "RootGesture"
.end annotation
# instance fields
.field public final a:Lam0/l;
.field public final b:Landroidx/compose/material3/SheetState;
# direct methods
.method public constructor <init>(Lam0/l;Landroidx/compose/material3/SheetState;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/gestures/MiniPlayerGestures$RootGesture;->a:Lam0/l;
iput-object p2, p0, Lradiant/gestures/MiniPlayerGestures$RootGesture;->b:Landroidx/compose/material3/SheetState;
return-void
.end method
# virtual methods
.method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u;
.locals 6
iget-object v0, p0, Lradiant/gestures/MiniPlayerGestures$RootGesture;->b:Landroidx/compose/material3/SheetState;
invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I
move-result v1
invoke-static {v0}, Lradiant/gestures/MiniPlayerGestures;->isDragging(Landroidx/compose/material3/SheetState;)Z
move-result v2
if-eqz v2, :maybe_reset_only
const/4 v2, 0x1
if-eq v1, v2, :up
const/4 v2, 0x2
if-eq v1, v2, :move
const/4 v2, 0x3
if-eq v1, v2, :cancel
goto :done
:maybe_reset_only
const/4 v2, 0x1
if-eq v1, v2, :simple_up
const/4 v2, 0x3
if-eq v1, v2, :reset_only
const/4 v2, 0x2
if-eq v1, v2, :simple_move
goto :done
:reset_only
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->resetGestureLock()V
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :done
:simple_move
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-static {v1}, Lradiant/gestures/MiniPlayerGestures;->updateSimpleSwipe(F)Z
goto :done
:simple_up
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
iget-object v2, p0, Lradiant/gestures/MiniPlayerGestures$RootGesture;->a:Lam0/l;
invoke-static {v2, v1}, Lradiant/gestures/MiniPlayerGestures;->completeSimpleSwipe(Lam0/l;F)Z
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :done
# ACTION_MOVE
:move
invoke-static {p1}, Lradiant/gestures/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v2
invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J
move-result-wide v4
invoke-static {v0, v2, v4, v5}, Lradiant/gestures/MiniPlayerGestures;->updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V
goto :done
# ACTION_UP
:up
invoke-static {p1}, Lradiant/gestures/MiniPlayerGestures;->trackMotion(Landroid/view/MotionEvent;)V
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-virtual {p1}, Landroid/view/MotionEvent;->getEventTime()J
move-result-wide v4
invoke-static {v0, v1, v4, v5}, Lradiant/gestures/MiniPlayerGestures;->updateDragToYTimed(Landroidx/compose/material3/SheetState;FJ)V
invoke-static {v0}, Lradiant/gestures/MiniPlayerGestures;->releaseVelocity(Landroidx/compose/material3/SheetState;)F
move-result v1
iget-object v2, p0, Lradiant/gestures/MiniPlayerGestures$RootGesture;->a:Lam0/l;
invoke-static {v0, v2, v1}, Lradiant/gestures/MiniPlayerGestures;->finishDrag(Landroidx/compose/material3/SheetState;Lam0/l;F)V
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
goto :done
# ACTION_CANCEL
:cancel
iget-object v2, p0, Lradiant/gestures/MiniPlayerGestures$RootGesture;->a:Lam0/l;
invoke-static {v0, v2}, Lradiant/gestures/MiniPlayerGestures;->cancelDrag(Landroidx/compose/material3/SheetState;Lam0/l;)V
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->animateSwipeFeedbackReset()V
:done
sget-object p1, Lkotlin/u;->a:Lkotlin/u;
return-object p1
.end method
.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 0
check-cast p1, Landroid/view/MotionEvent;
invoke-virtual {p0, p1}, Lradiant/gestures/MiniPlayerGestures$RootGesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,150 @@
.class public final Lradiant/gestures/MiniPlayerTrackGestures$Gesture;
.super Ljava/lang/Object;
.implements Lam0/l;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerTrackGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "Gesture"
.end annotation
# instance fields
.field public final a:Lam0/l;
# direct methods
.method public constructor <init>(Lam0/l;)V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
iput-object p1, p0, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;->a:Lam0/l;
return-void
.end method
.method private nextTrack()V
.locals 2
iget-object v0, p0, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;->a:Lam0/l;
sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/r$a;->a:Lcom/tidal/android/feature/appscaffold/ui/r$a; # Tidal's built-in next track mini-player event
invoke-interface {v0, v1}, Lam0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
return-void
.end method
.method private previousTrack()V
.locals 2
iget-object v0, p0, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;->a:Lam0/l;
sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/r$c;->a:Lcom/tidal/android/feature/appscaffold/ui/r$c; # Our synthetic previous track event
invoke-interface {v0, v1}, Lam0/l;->invoke(Ljava/lang/Object;)Ljava/lang/Object;
return-void
.end method
# virtual methods
.method public final invoke(Landroid/view/MotionEvent;)Lkotlin/u;
.locals 3
invoke-virtual {p1}, Landroid/view/MotionEvent;->getActionMasked()I
move-result v0
if-eqz v0, :down
const/4 v1, 0x1
if-eq v0, v1, :up
const/4 v1, 0x2
if-eq v0, v1, :move
const/4 v1, 0x3
if-eq v0, v1, :cancel
goto :done
# ACTION_DOWN
:down
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v0
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-static {v0, v1}, Lradiant/gestures/MiniPlayerTrackGestures;->beginDrag(FF)V
goto :done
# ACTION_MOVE
:move
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v0
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-static {v0, v1}, Lradiant/gestures/MiniPlayerTrackGestures;->moveDrag(FF)V
goto :done
# ACTION_UP
:up
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawX()F
move-result v0
invoke-virtual {p1}, Landroid/view/MotionEvent;->getRawY()F
move-result v1
invoke-static {v0, v1}, Lradiant/gestures/MiniPlayerTrackGestures;->finishDrag(FF)I
move-result v0
if-eqz v0, :done
# Negative result = swipe left -> next. Positive result = swipe right -> previous
if-gez v0, :previous
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->suppressTapOpen()V
invoke-direct {p0}, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;->nextTrack()V
goto :done
:previous
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->suppressTapOpen()V
invoke-direct {p0}, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;->previousTrack()V
goto :done
# ACTION_CANCEL
:cancel
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->cancelDrag()V
:done
sget-object p1, Lkotlin/u;->a:Lkotlin/u;
return-object p1
.end method
.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 0
check-cast p1, Landroid/view/MotionEvent;
invoke-virtual {p0, p1}, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;->invoke(Landroid/view/MotionEvent;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
@@ -0,0 +1,70 @@
.class public final Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;
.super Ljava/lang/Object;
.implements Lam0/l;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerTrackGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "OffsetLayer"
.end annotation
# static fields
.field public static final INSTANCE:Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;
invoke-direct {v0}, Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;-><init>()V
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;->INSTANCE:Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final invoke(Landroidx/compose/ui/graphics/GraphicsLayerScope;)Lkotlin/u;
.locals 1
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState;
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F
move-result v0
invoke-interface {p1, v0}, Landroidx/compose/ui/graphics/GraphicsLayerScope;->setTranslationX(F)V
sget-object p1, Lkotlin/u;->a:Lkotlin/u;
return-object p1
.end method
.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 0
check-cast p1, Landroidx/compose/ui/graphics/GraphicsLayerScope;
invoke-virtual {p0, p1}, Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;->invoke(Landroidx/compose/ui/graphics/GraphicsLayerScope;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
@@ -0,0 +1,60 @@
.class public final Lradiant/gestures/MiniPlayerTrackGestures$ResetAnimator;
.super Ljava/lang/Object;
.implements Landroid/animation/ValueAnimator$AnimatorUpdateListener;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerTrackGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "ResetAnimator"
.end annotation
# direct methods
.method public constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final onAnimationUpdate(Landroid/animation/ValueAnimator;)V
.locals 3
move-object v0, p1
invoke-virtual {p1}, Landroid/animation/ValueAnimator;->getAnimatedValue()Ljava/lang/Object;
move-result-object v1
check-cast v1, Ljava/lang/Float;
invoke-virtual {v1}, Ljava/lang/Float;->floatValue()F
move-result v1
invoke-static {v1}, Lradiant/gestures/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V
invoke-virtual {v0}, Landroid/animation/ValueAnimator;->getAnimatedFraction()F
move-result v1
const/high16 v2, 0x3f800000 # 1.0f
cmpg-float v1, v1, v2
if-gez v1, :done
invoke-static {v0}, Lradiant/gestures/MiniPlayerTrackGestures;->clearResetAnimator(Landroid/animation/ValueAnimator;)V
:done
return-void
.end method
@@ -0,0 +1,140 @@
.class public final Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;
.super Ljava/lang/Object;
.implements Lam0/l;
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lradiant/gestures/MiniPlayerTrackGestures;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x19
name = "TextDraw"
.end annotation
# static fields
.field public static final INSTANCE:Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;
# direct methods
.method static constructor <clinit>()V
.locals 1
new-instance v0, Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;
invoke-direct {v0}, Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;-><init>()V
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;->INSTANCE:Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
# virtual methods
.method public final invoke(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)Lkotlin/u;
.locals 12
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState;
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F
move-result v10
invoke-interface {p1}, Landroidx/compose/ui/graphics/drawscope/DrawScope;->getDrawContext()Landroidx/compose/ui/graphics/drawscope/DrawContext;
move-result-object v7
invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getSize-NH-jbRc()J
move-result-wide v8
const/16 v0, 0x20
shr-long v1, v8, v0
long-to-int v1, v1
invoke-static {v1}, Ljava/lang/Float;->intBitsToFloat(I)F
move-result v4
invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getCanvas()Landroidx/compose/ui/graphics/Canvas;
move-result-object v0
invoke-interface {v0}, Landroidx/compose/ui/graphics/Canvas;->save()V
:try_start_0
sget-object v0, Landroidx/compose/ui/graphics/ClipOp;->Companion:Landroidx/compose/ui/graphics/ClipOp$Companion;
invoke-virtual {v0}, Landroidx/compose/ui/graphics/ClipOp$Companion;->getIntersect-rtfAjoo()I
move-result v6
invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getTransform()Landroidx/compose/ui/graphics/drawscope/DrawTransform;
move-result-object v1
const v2, -0x800001 # -Float.MAX_VALUE -> unbounded left
const v3, -0x800001 # -Float.MAX_VALUE -> unbounded top
const v5, 0x7f7fffff # Float.MAX_VALUE -> unbounded bottom
invoke-interface/range {v1 .. v6}, Landroidx/compose/ui/graphics/drawscope/DrawTransform;->clipRect-N_I0leg(FFFFI)V
const/4 v0, 0x0
invoke-interface {v1, v10, v0}, Landroidx/compose/ui/graphics/drawscope/DrawTransform;->translate(FF)V
invoke-interface {p1}, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;->drawContent()V
:try_end_0
.catchall {:try_start_0 .. :try_end_0} :catchall_0
invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getCanvas()Landroidx/compose/ui/graphics/Canvas;
move-result-object v0
invoke-interface {v0}, Landroidx/compose/ui/graphics/Canvas;->restore()V
invoke-interface {v7, v8, v9}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->setSize-uvyYCjk(J)V
sget-object p1, Lkotlin/u;->a:Lkotlin/u;
return-object p1
:catchall_0
move-exception v0
invoke-interface {v7}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->getCanvas()Landroidx/compose/ui/graphics/Canvas;
move-result-object v1
invoke-interface {v1}, Landroidx/compose/ui/graphics/Canvas;->restore()V
invoke-interface {v7, v8, v9}, Landroidx/compose/ui/graphics/drawscope/DrawContext;->setSize-uvyYCjk(J)V
throw v0
.end method
.method public bridge synthetic invoke(Ljava/lang/Object;)Ljava/lang/Object;
.locals 0
check-cast p1, Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;
invoke-virtual {p0, p1}, Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;->invoke(Landroidx/compose/ui/graphics/drawscope/ContentDrawScope;)Lkotlin/u;
move-result-object p1
return-object p1
.end method
@@ -0,0 +1,605 @@
.class public final Lradiant/gestures/MiniPlayerTrackGestures;
.super Ljava/lang/Object;
# static fields
.field private static a:J
.field public static b:Landroidx/compose/runtime/MutableFloatState;
.field private static c:Landroid/animation/ValueAnimator;
.field private static d:Ljava/lang/Object;
# X at ACTION_DOWN
.field private static e:F
# Y at ACTION_DOWN
.field private static f:F
.field private static g:Z
.field private static h:Z
# direct methods
.method static constructor <clinit>()V
.locals 1
const/4 v0, 0x0
invoke-static {v0}, Landroidx/compose/runtime/PrimitiveSnapshotStateKt;->mutableFloatStateOf(F)Landroidx/compose/runtime/MutableFloatState;
move-result-object v0
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState;
return-void
.end method
.method private constructor <init>()V
.locals 0
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
.method public static animateReset()V
.locals 7
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
if-eqz v0, :read_offset
invoke-virtual {v0}, Landroid/animation/ValueAnimator;->cancel()V
const/4 v0, 0x0
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
:read_offset
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState;
invoke-interface {v0}, Landroidx/compose/runtime/MutableFloatState;->getFloatValue()F
move-result v0
invoke-static {v0}, Ljava/lang/Math;->abs(F)F
move-result v1
const/4 v3, 0x0
const/high16 v2, 0x3f000000 # 0.5f
cmpg-float v1, v1, v2
if-lez v1, :snap_zero
const/4 v1, 0x2
new-array v1, v1, [F
const/4 v2, 0x0
aput v0, v1, v2
const/4 v0, 0x1
aput v3, v1, v0
invoke-static {v1}, Landroid/animation/ValueAnimator;->ofFloat([F)Landroid/animation/ValueAnimator;
move-result-object v1
const-wide/16 v4, 0xb4 # 180ms
invoke-virtual {v1, v4, v5}, Landroid/animation/ValueAnimator;->setDuration(J)Landroid/animation/ValueAnimator;
new-instance v0, Landroid/view/animation/DecelerateInterpolator;
const/high16 v4, 0x3fc00000 # 1.5f
invoke-direct {v0, v4}, Landroid/view/animation/DecelerateInterpolator;-><init>(F)V
invoke-virtual {v1, v0}, Landroid/animation/ValueAnimator;->setInterpolator(Landroid/animation/TimeInterpolator;)V
new-instance v0, Lradiant/gestures/MiniPlayerTrackGestures$ResetAnimator;
invoke-direct {v0}, Lradiant/gestures/MiniPlayerTrackGestures$ResetAnimator;-><init>()V
invoke-virtual {v1, v0}, Landroid/animation/ValueAnimator;->addUpdateListener(Landroid/animation/ValueAnimator$AnimatorUpdateListener;)V
sput-object v1, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
invoke-virtual {v1}, Landroid/animation/ValueAnimator;->start()V
return-void
:snap_zero
invoke-static {v3}, Lradiant/gestures/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V
return-void
.end method
.method public static beginDrag(FF)V
.locals 2
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->resetGestureLock()V
sput p0, Lradiant/gestures/MiniPlayerTrackGestures;->e:F
sput p1, Lradiant/gestures/MiniPlayerTrackGestures;->f:F
const/4 v0, 0x1
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
const/4 v0, 0x0
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
if-eqz v0, :zero
invoke-virtual {v0}, Landroid/animation/ValueAnimator;->cancel()V
const/4 v0, 0x0
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
:zero
const/4 v0, 0x0
invoke-static {v0}, Lradiant/gestures/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V
return-void
.end method
.method public static cancelDrag()V
.locals 1
const/4 v0, 0x0
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->animateReset()V
return-void
.end method
.method public static clearResetAnimator(Landroid/animation/ValueAnimator;)V
.locals 1
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
if-ne v0, p0, :done
const/4 v0, 0x0
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
:done
return-void
.end method
.method public static finishDrag(FF)I
.locals 5
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->isAnyDragging()Z
move-result v0
if-eqz v0, :check_active
const/4 v0, 0x0
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->animateReset()V
return v0
:check_active
sget-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
if-nez v0, :active
const/4 v0, 0x0
return v0
:active
const/4 v0, 0x0
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
sget-boolean v1, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
sget v2, Lradiant/gestures/MiniPlayerTrackGestures;->e:F
sub-float/2addr p0, v2
sget v2, Lradiant/gestures/MiniPlayerTrackGestures;->f:F
sub-float/2addr p1, v2
if-eqz v1, :reset
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->suppressTapOpen()V
:reset
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->animateReset()V
invoke-static {p0}, Ljava/lang/Math;->abs(F)F
move-result v1
const/high16 v2, 0x42400000 # 48.0f -> at least 48dp horizontal swipe
invoke-static {v2}, Lradiant/gestures/MiniPlayerTrackGestures;->dp(F)F
move-result v2
cmpg-float v3, v1, v2
if-gez v3, :check_axis
return v0
:check_axis
invoke-static {p1}, Ljava/lang/Math;->abs(F)F
move-result p1
const/high16 v2, 0x3fc00000 # 1.5f -> horizontal distance must dominate vertical mov. by 1.5x
mul-float/2addr p1, v2
cmpg-float v1, v1, p1
if-lez v1, :no_swipe
const/4 p1, 0x0
cmpg-float p0, p0, p1
if-ltz p0, :next
const/4 v0, 0x1
return v0
:next
const/4 v0, -0x1
return v0
:no_swipe
return v0
.end method
.method public static moveDrag(FF)V
.locals 6
sget-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
if-eqz v0, :done
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->isAnyDragging()Z
move-result v0
if-eqz v0, :move_continue
const/4 v0, 0x0
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->animateReset()V
goto :done
:move_continue
sget v0, Lradiant/gestures/MiniPlayerTrackGestures;->e:F
sub-float/2addr p0, v0
sget v0, Lradiant/gestures/MiniPlayerTrackGestures;->f:F
sub-float/2addr p1, v0
invoke-static {p0}, Ljava/lang/Math;->abs(F)F
move-result v0
invoke-static {p1}, Ljava/lang/Math;->abs(F)F
move-result v1
sget-boolean v2, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
if-nez v2, :publish
const/high16 v2, 0x40c00000 # 6.0f dp dead zone
invoke-static {v2}, Lradiant/gestures/MiniPlayerTrackGestures;->dp(F)F
move-result v2
cmpg-float v3, v0, v2
if-gez v3, :move_axis
goto :done
:move_axis
const v2, 0x3f8ccccd # 1.1f horizontal dominance while dragging
mul-float/2addr v1, v2
cmpg-float v2, v0, v1
if-gez v2, :accept
goto :done
:accept
invoke-static {}, Lradiant/gestures/MiniPlayerGestures;->lockHorizontalGesture()Z
move-result v2
if-eqz v2, :cancel_locked
const/4 v2, 0x1
sput-boolean v2, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
:publish
const/4 v2, 0x0
invoke-static {v2}, Lradiant/gestures/MiniPlayerGestures;->setSwipeFeedbackDirect(F)V
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->suppressTapOpen()V
invoke-static {p0}, Lradiant/gestures/MiniPlayerTrackGestures;->setDragOffset(F)V
:done
return-void
:cancel_locked
const/4 v0, 0x0
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->g:Z
sput-boolean v0, Lradiant/gestures/MiniPlayerTrackGestures;->h:Z
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->animateReset()V
goto :done
.end method
.method private static dp(F)F
.locals 1
invoke-static {}, Landroid/content/res/Resources;->getSystem()Landroid/content/res/Resources;
move-result-object v0
invoke-virtual {v0}, Landroid/content/res/Resources;->getDisplayMetrics()Landroid/util/DisplayMetrics;
move-result-object v0
iget v0, v0, Landroid/util/DisplayMetrics;->density:F
mul-float/2addr p0, v0
return p0
.end method
.method public static feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
.locals 2
const/high16 v0, 0x3f800000 # 1.0f
invoke-static {p0, v0}, Landroidx/compose/ui/ZIndexModifierKt;->zIndex(Landroidx/compose/ui/Modifier;F)Landroidx/compose/ui/Modifier;
move-result-object p0
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;->INSTANCE:Lradiant/gestures/MiniPlayerTrackGestures$OffsetLayer;
invoke-static {p0, v0}, Landroidx/compose/ui/graphics/GraphicsLayerModifierKt;->graphicsLayer(Landroidx/compose/ui/Modifier;Lam0/l;)Landroidx/compose/ui/Modifier;
move-result-object p0
return-object p0
.end method
.method public static textFeedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier;
.locals 1
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures$TextDraw;->INSTANCE:Lradiant/gestures/MiniPlayerTrackGestures$TextDraw; # text clip on right only
invoke-static {p0, v0}, Landroidx/compose/ui/draw/DrawModifierKt;->drawWithContent(Landroidx/compose/ui/Modifier;Lam0/l;)Landroidx/compose/ui/Modifier;
move-result-object p0
return-object p0
.end method
.method public static onRenderedItem(Ljava/lang/Object;)V
.locals 2
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->d:Ljava/lang/Object;
if-eqz v0, :store_initial
invoke-virtual {v0, p0}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z
move-result v1
if-nez v1, :done
sput-object p0, Lradiant/gestures/MiniPlayerTrackGestures;->d:Ljava/lang/Object;
invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->animateReset()V
return-void
:store_initial
sput-object p0, Lradiant/gestures/MiniPlayerTrackGestures;->d:Ljava/lang/Object;
:done
return-void
.end method
.method public static consumeTapOpenSuppression()Z
.locals 5
sget-wide v0, Lradiant/gestures/MiniPlayerTrackGestures;->a:J
const-wide/16 v2, 0x0
cmp-long v4, v0, v2
if-nez v4, :check_time
const/4 v0, 0x0
return v0
:check_time
sput-wide v2, Lradiant/gestures/MiniPlayerTrackGestures;->a:J
invoke-static {}, Landroid/os/SystemClock;->uptimeMillis()J
move-result-wide v2
cmp-long v4, v2, v0
if-lez v4, :yes
const/4 v0, 0x0
return v0
:yes
const/4 v0, 0x1
return v0
.end method
.method public static suppressTapOpen()V
.locals 4
invoke-static {}, Landroid/os/SystemClock;->uptimeMillis()J
move-result-wide v0
const-wide/16 v2, 0x258
add-long/2addr v0, v2
sput-wide v0, Lradiant/gestures/MiniPlayerTrackGestures;->a:J
return-void
.end method
.method public static setDragOffset(F)V
.locals 5
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
if-eqz v0, :rubber_band
invoke-virtual {v0}, Landroid/animation/ValueAnimator;->cancel()V
const/4 v0, 0x0
sput-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->c:Landroid/animation/ValueAnimator;
:rubber_band
invoke-static {p0}, Ljava/lang/Math;->abs(F)F
move-result v0
const/high16 v1, 0x42800000 # 64.0f dp free travel
invoke-static {v1}, Lradiant/gestures/MiniPlayerTrackGestures;->dp(F)F
move-result v1
cmpg-float v2, v0, v1
if-lez v2, :publish
sub-float/2addr v0, v1
const/high16 v2, 0x3e800000 # 0.25f resistance after 64dp
mul-float/2addr v0, v2
const v2, 0x3eb33333 # 0.35f max extra travel
mul-float/2addr v2, v1
invoke-static {v0, v2}, Ljava/lang/Math;->min(FF)F
move-result v0
add-float/2addr v0, v1
const/4 v1, 0x0
cmpg-float v2, p0, v1
if-gez v2, :positive
neg-float p0, v0
goto :publish
:positive
move p0, v0
:publish
invoke-static {p0}, Lradiant/gestures/MiniPlayerTrackGestures;->setDragOffsetDirect(F)V
return-void
.end method
.method public static setDragOffsetDirect(F)V
.locals 1
sget-object v0, Lradiant/gestures/MiniPlayerTrackGestures;->b:Landroidx/compose/runtime/MutableFloatState;
invoke-interface {v0, p0}, Landroidx/compose/runtime/MutableFloatState;->setFloatValue(F)V
return-void
.end method
.method public static trackModifier(Landroidx/compose/ui/Modifier;Lam0/l;)Landroidx/compose/ui/Modifier;
.locals 1
new-instance v0, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;
invoke-direct {v0, p1}, Lradiant/gestures/MiniPlayerTrackGestures$Gesture;-><init>(Lam0/l;)V
invoke-static {p0, v0}, Landroidx/compose/ui/input/pointer/PointerInteropFilter_androidKt;->motionEventSpy(Landroidx/compose/ui/Modifier;Lam0/l;)Landroidx/compose/ui/Modifier;
move-result-object p0
return-object p0
.end method
@@ -0,0 +1,13 @@
--- a/com/tidal/android/feature/playerscreen/ui/composables/LyricsKt.smali
+++ b/com/tidal/android/feature/playerscreen/ui/composables/LyricsKt.smali
@@ -6107,6 +6107,10 @@
move-object v15, v10
+ invoke-static {v15}, Lradiant/gestures/LyricsDrag;->wrap(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # lyrics own their scroll
+
+ move-result-object v15
+
.line 61
invoke-static/range {v15 .. v27}, Landroidx/compose/foundation/lazy/LazyDslKt;->LazyColumn(Landroidx/compose/ui/Modifier;Landroidx/compose/foundation/lazy/LazyListState;Landroidx/compose/foundation/layout/PaddingValues;ZLandroidx/compose/foundation/layout/Arrangement$Vertical;Landroidx/compose/ui/Alignment$Horizontal;Landroidx/compose/foundation/gestures/FlingBehavior;ZLandroidx/compose/foundation/OverscrollEffect;Lam0/l;Landroidx/compose/runtime/Composer;II)V
@@ -0,0 +1,126 @@
--- a/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali
@@ -1215,6 +1215,10 @@
move-result-object v6
+ invoke-static {v6, v1}, Lradiant/gestures/MiniPlayerTrackGestures;->trackModifier(Landroidx/compose/ui/Modifier;Lam0/l;)Landroidx/compose/ui/Modifier; # attach horizontal swipe listener
+
+ move-result-object v6
+
.line 38
sget-object v21, Landroidx/compose/ui/Alignment;->Companion:Landroidx/compose/ui/Alignment$Companion;
@@ -1352,6 +1356,8 @@
.line 56
iget-object v11, v0, Lcom/tidal/android/feature/appscaffold/ui/t;->b:Lcom/tidal/android/feature/appscaffold/ui/q;
+ invoke-static {v11}, Lradiant/gestures/MiniPlayerTrackGestures;->onRenderedItem(Ljava/lang/Object;)V # reset stale drag offset if track metadata changes mid-gesture
+
.line 57
invoke-virtual/range {v16 .. v16}, Lcom/squareup/ui/market/core/theme/MarketStylesheet;->getBorderRadii()Lcom/squareup/ui/market/core/theme/MarketStylesheet$b;
@@ -1390,6 +1396,10 @@
move-result-object v6
+ invoke-static {v6}, Lradiant/gestures/MiniPlayerTrackGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move the whole cover container with horizontal swipe
+
+ move-result-object v6
+
.line 63
invoke-virtual/range {v22 .. v22}, Lcom/squareup/ui/market/core/theme/MarketStylesheet$c;->p0()Lf20/a;
@@ -1501,6 +1511,10 @@
move-result-object v5
+ invoke-static {v5}, Lradiant/gestures/MiniPlayerTrackGestures;->textFeedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist
+
+ move-result-object v5
+
.line 72
invoke-virtual/range {v32 .. v32}, Landroidx/compose/foundation/layout/Arrangement;->getTop()Landroidx/compose/foundation/layout/Arrangement$Vertical;
--- a/com/tidal/android/feature/appscaffold/ui/MiniPlayerViewModel.smali
+++ b/com/tidal/android/feature/appscaffold/ui/MiniPlayerViewModel.smali
@@ -512,16 +512,16 @@
.line 110
invoke-virtual {p1, v0}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z
.line 111
.line 112
.line 113
- move-result p1
+ move-result v0
.line 114
- if-eqz p1, :cond_f
+ if-eqz v0, :cond_f
.line 115
.line 116
invoke-interface {v3}, Lbi/e;->i()Z
.line 117
@@ -695,12 +695,34 @@
.line 194
:cond_e
return-void
.line 195
:cond_f
+ sget-object v0, Lcom/tidal/android/feature/appscaffold/ui/r$c;->a:Lcom/tidal/android/feature/appscaffold/ui/r$c; # our synthetic previous-track event
+
+ invoke-virtual {p1, v0}, Ljava/lang/Object;->equals(Ljava/lang/Object;)Z # is this the swipe-previous event?
+
+ move-result p1
+
+ if-eqz p1, :unknown_event
+
+ invoke-interface {v3}, Lbi/e;->canSkipToPreviousOrRewind()Z
+
+ move-result p1
+
+ if-eqz p1, :previous_done
+
+ const/4 p1, 0x0
+
+ invoke-interface {v3, p1}, Lbi/e;->h(Z)V # false = previous/rewind
+
+ :previous_done
+ return-void
+
+ :unknown_event
invoke-static {}, Landroidx/navigation/f0;->a()V
.line 196
.line 197
.line 198
return-void
--- a/com/tidal/android/feature/appscaffold/ui/composable/g.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/g.smali
@@ -24,12 +24,23 @@
# virtual methods
.method public final invoke()Ljava/lang/Object;
.locals 2
+ invoke-static {}, Lradiant/gestures/MiniPlayerTrackGestures;->consumeTapOpenSuppression()Z # skip open-player if a horizontal skip-swipe just happened
+
+ move-result v0
+
+ if-eqz v0, :rl_continue_open
+
+ sget-object v0, Lkotlin/u;->a:Lkotlin/u;
+
+ return-object v0
+
+ :rl_continue_open
.line 1
iget-object v0, p0, Lcom/tidal/android/feature/appscaffold/ui/composable/g;->a:Lam0/l;
.line 2
.line 3
sget-object v1, Lcom/tidal/android/feature/appscaffold/ui/b$b;->a:Lcom/tidal/android/feature/appscaffold/ui/b$b;
@@ -0,0 +1,76 @@
# rl-locals: com/tidal/android/feature/appscaffold/ui/composable/i.smali b( 52
--- a/com/tidal/android/feature/appscaffold/ui/composable/i.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/i.smali
@@ -1691,6 +1691,14 @@
move-result-object v24
+ move-object/from16 v49, p5 # gesture expand lambda (AppScaffold Function1 param)
+
+ move-object/from16 v50, v24 # SheetState
+
+ const/16 v51, __RL_MINI_PLAYER_SWIPE_UP_DRAG__ # Enable Drag option
+
+ invoke-static/range {v49 .. v51}, Lradiant/gestures/MiniPlayerGestures;->configure(Lam0/l;Landroidx/compose/material3/SheetState;Z)V # wire swipe-up to expand the sheet
+
.line 69
invoke-static {}, Landroidx/compose/ui/platform/CompositionLocalsKt;->getLocalDensity()Landroidx/compose/runtime/ProvidableCompositionLocal;
--- a/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali
+++ b/com/tidal/android/feature/appscaffold/ui/composable/MiniPlayerKt.smali
@@ -712,6 +712,22 @@
iget-object v3, v0, Lcom/tidal/android/feature/appscaffold/ui/t;->c:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$PlayState;
+ iget-object v5, v0, Lcom/tidal/android/feature/appscaffold/ui/t;->a:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType; # currently rendered mini player media type
+
+ sget-object v6, Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType;->Unknown:Lcom/tidal/android/feature/appscaffold/ui/MiniPlayerContract$ItemType; # Unknown = no media playing
+
+ if-ne v5, v6, :rl_has_media # prevent swipe up when nothing playing
+
+ const/4 v5, 0x0
+
+ goto :rl_set_media
+
+ :rl_has_media
+ const/4 v5, 0x1
+
+ :rl_set_media
+ invoke-static {v5}, Lradiant/gestures/MiniPlayerGestures;->setHasMiniPlayerMedia(Z)V
+
invoke-virtual {v1}, Ljava/lang/Object;->getClass()Ljava/lang/Class;
const v4, -0x571456a1
@@ -1215,6 +1231,10 @@
move-result-object v6
+ invoke-static {v6}, Lradiant/gestures/MiniPlayerGestures;->trackAreaModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # attach swipe up listener
+
+ move-result-object v6
+
.line 38
sget-object v21, Landroidx/compose/ui/Alignment;->Companion:Landroidx/compose/ui/Alignment$Companion;
@@ -1390,6 +1410,10 @@
move-result-object v6
+ invoke-static {v6}, Lradiant/gestures/MiniPlayerGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move cover (swipe up feedback)
+
+ move-result-object v6
+
.line 63
invoke-virtual/range {v22 .. v22}, Lcom/squareup/ui/market/core/theme/MarketStylesheet$c;->p0()Lf20/a;
@@ -1501,6 +1525,10 @@
move-result-object v5
+ invoke-static {v5}, Lradiant/gestures/MiniPlayerGestures;->feedbackModifier(Landroidx/compose/ui/Modifier;)Landroidx/compose/ui/Modifier; # move title/artist (swipe up feedback)
+
+ move-result-object v5
+
.line 72
invoke-virtual/range {v32 .. v32}, Landroidx/compose/foundation/layout/Arrangement;->getTop()Landroidx/compose/foundation/layout/Arrangement$Vertical;
@@ -0,0 +1,55 @@
--- a/androidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1.smali
+++ b/androidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1.smali
@@ -212,6 +212,12 @@
.line 24
iget-object p1, p0, Landroidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1;->$sheetState:Landroidx/compose/material3/SheetState;
+ invoke-static {p1}, Lradiant/gestures/MiniPlayerGestures;->suppressAutoShow(Landroidx/compose/material3/SheetState;)Z # avoid Material3 show() taking over manual drag
+
+ move-result v1
+
+ if-nez v1, :skip_show
+
.line 25
.line 26
iput v2, p0, Landroidx/compose/material3/ModalBottomSheetKt$ModalBottomSheet$4$1;->label:I
@@ -232,6 +238,7 @@
.line 34
return-object v0
+ :skip_show
.line 35
:cond_2
:goto_0
--- a/androidx/compose/material3/ModalBottomSheetKt.smali
+++ b/androidx/compose/material3/ModalBottomSheetKt.smali
@@ -1288,12 +1288,18 @@
.line 33
invoke-interface {v5, v12}, Landroidx/compose/runtime/Composer;->updateRememberedValue(Ljava/lang/Object;)V
.line 34
:cond_47
+ move-object v14, v12 # settle callback
+
+ check-cast v14, Lam0/l; # to Function1
+
+ invoke-static {v13, v14}, Lradiant/gestures/MiniPlayerGestures;->setSettleCallback(Landroidx/compose/material3/SheetState;Lam0/l;)V # reuse native settle animation on release
+
move-object/from16 v32, v12
check-cast v32, Lam0/l;
.line 35
invoke-interface {v5}, Landroidx/compose/runtime/Composer;->rememberedValue()Ljava/lang/Object;
--- a/androidx/compose/material3/SheetDefaultsKt.smali
+++ b/androidx/compose/material3/SheetDefaultsKt.smali
@@ -98,7 +98,7 @@
const/4 v2, 0x0
.line 16
- const/16 v3, 0x12c
+ const/16 v3, 0x64 # 100ms
.line 17
.line 18
+44
View File
@@ -0,0 +1,44 @@
--- a/com/tidal/android/feature/home/ui/k.smali
+++ b/com/tidal/android/feature/home/ui/k.smali
@@ -424,7 +424,7 @@
const/4 v6, 0x0
.line 181
- move-wide v7, v8
+ const-wide/16 v7, 0x0 # transparent containerColor
.line 182
const-wide/16 v9, 0x0
--- a/com/tidal/android/feature/home/ui/o.smali
+++ b/com/tidal/android/feature/home/ui/o.smali
@@ -187,6 +187,10 @@
.line 61
.line 62
:cond_3
+ const/4 v0, 0x0 # render $changed flag
+
+ invoke-static {v4, v0}, Lradiant/HomeBackdrop;->render(Landroidx/compose/runtime/Composer;I)V # render album-art backdrop
+
invoke-interface {p1}, Landroidx/compose/foundation/layout/PaddingValues;->calculateTopPadding-D9Ej5fM()F
.line 63
--- a/com/tidal/android/feature/home/ui/composables/g.smali
+++ b/com/tidal/android/feature/home/ui/composables/g.smali
@@ -790,6 +790,8 @@
.line 104
move-result-wide v14
+ const-wide/16 v14, 0x0 # transparent topBar background
+
.line 105
const/16 v17, 0x2
@@ -850,6 +852,8 @@
.line 134
move-result-wide v14
+ const-wide/16 v14, 0x0 # transparent TopAppBar containerColor
+
.line 135
sget-object v3, Landroidx/compose/ui/graphics/Color;->Companion:Landroidx/compose/ui/graphics/Color$Companion;

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