mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-10-15 07:53:12 +11:00
Crossfade & Animated Covers
This commit is contained in:
1 parent
d47693ccaa
commit
f5cd48be5a
158 files changed
+7211
-1855
No files matched your search
@@ -24,7 +24,7 @@ import androidx.compose.ui.platform.AndroidCompositionLocals_androidKt;
|
||||
|
||||
import coil.request.CachePolicy;
|
||||
|
||||
import com.tidal.android.feature.playerscreen.ui.composables.p3;
|
||||
import com.tidal.android.feature.playerscreen.ui.composables.e3;
|
||||
import com.tidal.android.feature.playerscreen.ui.model.PlayerBackgroundStyle;
|
||||
import com.tidal.android.image.coil.base.CoilImageLoader;
|
||||
import com.tidal.android.image.core.b; // MARKER: R8 core.b
|
||||
@@ -41,7 +41,7 @@ import java.util.concurrent.Executors;
|
||||
* (cloned at tools/kawarp-agsl) and is compiled into this bundle by tools/kawarp-build/build.sh;
|
||||
* this class only holds what is TIDAL/Compose-specific
|
||||
*/
|
||||
public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallback, b0.c { // MARKER: R8 am0.l b0.c
|
||||
public final class Kawarp implements yl0.l, Runnable, Choreographer.FrameCallback, c0.c { // MARKER: R8 yl0.l c0.c
|
||||
|
||||
/** Stop the frame loop once nothing has drawn for this long (player left / backgrounded). */
|
||||
private static final long IDLE_STOP_MS = 500L;
|
||||
@@ -61,7 +61,7 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
* would allocate an Integer per frame for nothing.
|
||||
*/
|
||||
private static final MutableIntState frameState = SnapshotIntStateKt.mutableIntStateOf(0);
|
||||
/** Flips true on any shader failure; read during composition so we recompose onto p3.a. */
|
||||
/** Flips true on any shader failure; read during composition so we recompose onto e3.a. */
|
||||
private static final MutableState fallbackState =
|
||||
SnapshotStateKt.mutableStateOf$default(Boolean.FALSE, null, 2, null);
|
||||
|
||||
@@ -70,7 +70,7 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
|
||||
private static KawarpEngine engine;
|
||||
/** TIDAL's application-scoped Coil loader and the context its requests are built with. */
|
||||
private static volatile coil.f loader; // MARKER: R8 coil.f
|
||||
private static volatile coil.e loader; // MARKER: R8 coil.e
|
||||
private static volatile Context appContext;
|
||||
private static volatile String requestedUuid;
|
||||
private static volatile int loadToken;
|
||||
@@ -95,11 +95,11 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
|
||||
|
||||
/**
|
||||
* Drop-in replacement for TIDAL's PlayerBackground composable (p3.b) - the patch redirects
|
||||
* Drop-in replacement for TIDAL's PlayerBackground composable (e3.b) - the patch redirects
|
||||
* the call site here so the whole backdrop works <3
|
||||
*/
|
||||
public static void b(PlayerBackgroundStyle style, long color, int albumId, String coverUuid,
|
||||
boolean isPlaying, boolean isSeeking, am0.a progress, Modifier modifier, // MARKER: R8 am0.a
|
||||
boolean isPlaying, boolean isSeeking, yl0.a progress, Modifier modifier, // MARKER: R8 yl0.a
|
||||
Composer composer, int changed) {
|
||||
render(albumId, coverUuid, isPlaying, modifier, composer);
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
boolean ok = !((Boolean) fallbackState.getValue()).booleanValue() && ensureEngine();
|
||||
if (!ok) {
|
||||
// No (working) AGSL here - fall back to TIDAL's own blurred backdrop.
|
||||
p3.a(albumId, coverUuid, isPlaying, false, null, modifier, composer, 0); // MARKER: R8 p3.a
|
||||
e3.a(albumId, coverUuid, isPlaying, false, null, modifier, composer, 0); // MARKER: R8 e3.a
|
||||
} else {
|
||||
engine.setPlaying(isPlaying);
|
||||
request(albumId, coverUuid);
|
||||
@@ -131,8 +131,8 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
Object local = composer.consume(AndroidCompositionLocals_androidKt.getLocalContext());
|
||||
if (!(local instanceof Context)) return;
|
||||
Context app = ((Context) local).getApplicationContext();
|
||||
if (!(app instanceof ce0.b.a)) return; // MARKER: R8 ce0.b.a
|
||||
Object tidalLoader = ((ce0.b.a) app).a().a(); // MARKER: R8 ce0.b.a
|
||||
if (!(app instanceof ud0.b.a)) return; // MARKER: R8 ud0.b.a
|
||||
Object tidalLoader = ((ud0.b.a) app).a().a(); // MARKER: R8 ud0.b.a
|
||||
if (!(tidalLoader instanceof CoilImageLoader)) return;
|
||||
appContext = app;
|
||||
loader = ((CoilImageLoader) tidalLoader).a;
|
||||
@@ -181,7 +181,7 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
*/
|
||||
public void run() {
|
||||
try {
|
||||
xd0.g tidal = new xd0.g(appContext, new b.a(String.valueOf(albumId), uuid), // MARKER: R8 xd0.g b.a
|
||||
pd0.g tidal = new pd0.g(appContext, new b.a(String.valueOf(albumId), uuid), // MARKER: R8 pd0.g b.a
|
||||
null, null, false, null, null, false);
|
||||
coil.request.h.a req = coil.request.h.a(com.tidal.android.image.coil.c.a(tidal, null)); // MARKER: R8 h.a coil.c.a
|
||||
coil.request.b d = req.b; // MARKER: R8 req.b
|
||||
@@ -207,7 +207,7 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
public void b(Drawable placeholder) {}
|
||||
|
||||
/** Target.onError - not in any of TIDAL's stores. */
|
||||
public void d(Drawable error) { fail(); }
|
||||
public void c(Drawable error) { fail(); }
|
||||
|
||||
private void fail() {
|
||||
if (token == loadToken) requestedUuid = null;
|
||||
@@ -258,7 +258,7 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
engine.draw(AndroidCanvas_androidKt.getNativeCanvas(scope.getDrawContext().getCanvas()),
|
||||
width, height);
|
||||
} catch (Throwable t) {
|
||||
fallbackState.setValue(Boolean.TRUE); // AGSL draw failed: recompose onto p3.a // MARKER: R8 p3.a
|
||||
fallbackState.setValue(Boolean.TRUE); // AGSL draw failed: recompose onto e3.a // MARKER: R8 e3.a
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package androidx.compose.ui.draw;
|
||||
import androidx.compose.ui.Modifier;
|
||||
public final class DrawModifierKt {
|
||||
public static Modifier drawBehind(Modifier modifier, am0.l onDraw) { return null; } // MARKER: R8 am0.l
|
||||
public static Modifier drawBehind(Modifier modifier, yl0.l onDraw) { return null; } // MARKER: R8 yl0.l
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package b0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `b0.c` - both package and name roll
|
||||
import android.graphics.drawable.Drawable;
|
||||
/** coil.target.Target (R8 name in TIDAL 2.200): a=onSuccess, b=onStart, d=onError. */
|
||||
public interface c {
|
||||
void a(Drawable success);
|
||||
void b(Drawable start);
|
||||
void d(Drawable error);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package c0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `c0.c` - both package and name roll
|
||||
import android.graphics.drawable.Drawable;
|
||||
/** coil.target.Target (R8 name in TIDAL 2.213): a=onSuccess, b=onStart, c=onError. */
|
||||
public interface c {
|
||||
void a(Drawable success);
|
||||
void b(Drawable start);
|
||||
void c(Drawable error);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package coil;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `coil.f` - both package and name roll
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `coil.e` - both package and name roll
|
||||
/** coil.ImageLoader; b() is enqueue(). */
|
||||
public interface f {
|
||||
public interface e {
|
||||
coil.request.d b(coil.request.h request);
|
||||
}
|
||||
@@ -9,12 +9,12 @@ import kotlinx.coroutines.CoroutineDispatcher;
|
||||
*/
|
||||
public final class b {
|
||||
public CoroutineDispatcher a, b, c, d;
|
||||
public d0.c.a e; // MARKER: R8 d0.c.a
|
||||
public e0.c.a e; // MARKER: R8 e0.c.a
|
||||
public coil.size.Precision f;
|
||||
public android.graphics.Bitmap.Config g;
|
||||
public boolean h;
|
||||
public CachePolicy i, j, k;
|
||||
public b(CoroutineDispatcher a, CoroutineDispatcher b, CoroutineDispatcher c, CoroutineDispatcher d,
|
||||
d0.c.a transition, coil.size.Precision precision, android.graphics.Bitmap.Config config, // MARKER: R8 d0.c.a
|
||||
e0.c.a transition, coil.size.Precision precision, android.graphics.Bitmap.Config config, // MARKER: R8 e0.c.a
|
||||
boolean h, CachePolicy memory, CachePolicy disk, CachePolicy network) {}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ public final class h {
|
||||
/** defaults block the built request copies its cache policies from */
|
||||
public b b;
|
||||
/** target */
|
||||
public b0.c d; // MARKER: R8 b0.c
|
||||
public c0.c d; // MARKER: R8 c0.c
|
||||
public h a() { return null; }
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
package com.tidal.android.feature.playerscreen.ui.composables;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `com.tidal.android.feature.playerscreen.ui.composables.p3` - both package and name roll
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `com.tidal.android.feature.playerscreen.ui.composables.e3` - both package and name roll
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.ui.Modifier;
|
||||
/** TIDAL's DarkBlurBackground composable (used as the pre-Android-13 fallback). */
|
||||
public final class p3 {
|
||||
public final class e3 {
|
||||
public static void a(int albumId, String coverUuid, boolean isPlaying, boolean isSeeking,
|
||||
am0.a progress, Modifier modifier, Composer composer, int changed) {} // MARKER: R8 am0.a
|
||||
yl0.a progress, Modifier modifier, Composer composer, int changed) {} // MARKER: R8 yl0.a
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
package com.tidal.android.image.coil.base;
|
||||
/** TIDAL's xd0.e implementation; a is the underlying Coil ImageLoader. */
|
||||
public final class CoilImageLoader implements xd0.e { // MARKER: R8 xd0.e
|
||||
public final coil.f a = null; // MARKER: R8 coil.f
|
||||
/** TIDAL's pd0.e implementation; a is the underlying Coil ImageLoader. */
|
||||
public final class CoilImageLoader implements pd0.e { // MARKER: R8 pd0.e
|
||||
public final coil.e a = null; // MARKER: R8 coil.e
|
||||
}
|
||||
@@ -2,5 +2,5 @@ package com.tidal.android.image.coil;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `com.tidal.android.image.coil.c` - both package and name roll
|
||||
/** TIDAL request -> Coil request; installs the AlbumCover data the interceptor resolves. */
|
||||
public final class c {
|
||||
public static coil.request.h a(xd0.g request, androidx.compose.ui.layout.ContentScale scale) { return null; } // MARKER: R8 xd0.g
|
||||
public static coil.request.h a(pd0.g request, androidx.compose.ui.layout.ContentScale scale) { return null; } // MARKER: R8 pd0.g
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package d0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `d0.c` - both package and name roll
|
||||
package e0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `e0.c` - both package and name roll
|
||||
/** coil.transition.Transition; c.a is Transition.Factory. */
|
||||
public interface c { interface a {} }
|
||||
@@ -1,4 +1,4 @@
|
||||
package xd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `xd0.e` - both package and name roll
|
||||
package pd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `pd0.e` - both package and name roll
|
||||
/** TIDAL's ImageLoader interface (implemented by CoilImageLoader). */
|
||||
public interface e {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package pd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `pd0.f` - both package and name roll
|
||||
/** Size spec wrapper accepted by pd0.g's constructor; we always pass null. */
|
||||
public final class f {}
|
||||
@@ -1,5 +1,5 @@
|
||||
package xd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `xd0.g` - both package and name roll
|
||||
package pd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `pd0.g` - both package and name roll
|
||||
import android.content.Context;
|
||||
import com.tidal.android.image.core.b; // MARKER: R8 core.b
|
||||
/** TIDAL's ImageRequest. */
|
||||
@@ -1,7 +1,7 @@
|
||||
package ce0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `ce0.b` - both package and name roll
|
||||
package ud0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `ud0.b` - both package and name roll
|
||||
/** Dagger image component; the Application implements b.a and hands out the loader. */
|
||||
public interface b {
|
||||
xd0.e a(); // MARKER: R8 xd0.e
|
||||
pd0.e a(); // MARKER: R8 pd0.e
|
||||
interface a { b a(); }
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
package xd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `xd0.f` - both package and name roll
|
||||
/** Size spec wrapper accepted by xd0.g's constructor; we always pass null. */
|
||||
public final class f {}
|
||||
@@ -1,4 +1,4 @@
|
||||
package am0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `am0.a` - both package and name roll
|
||||
package yl0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `yl0.a` - both package and name roll
|
||||
/** kotlin.jvm.functions.Function0 (obfuscated in the TIDAL dex). */
|
||||
public interface a { Object invoke(); }
|
||||
@@ -1,4 +1,4 @@
|
||||
package am0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `am0.l` - both package and name roll
|
||||
package yl0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `yl0.l` - both package and name roll
|
||||
/** kotlin.jvm.functions.Function1 (obfuscated in the TIDAL dex). */
|
||||
public interface l { Object invoke(Object p1); }
|
||||
Executable
+62
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# Builds the RL extension classes into smali
|
||||
#
|
||||
# ./build.sh # rebuild extension smali
|
||||
#
|
||||
# Java 8, no lambdas, no nested classes
|
||||
set -euo pipefail
|
||||
|
||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
repo="$(cd "$here/../.." && pwd)"
|
||||
|
||||
JAVA_HOME="${JAVA_HOME:-/home/meow/toolchain/jdk-21.0.12+8}"
|
||||
ANDROID_SDK="${ANDROID_SDK:-/home/meow/toolchain/android-sdk}"
|
||||
ANDROID_JAR="${ANDROID_JAR:-$ANDROID_SDK/platforms/android-36/android.jar}"
|
||||
D8="${D8:-$(ls -d "$ANDROID_SDK"/build-tools/*/d8 | sort | tail -1)}"
|
||||
BAKSMALI="${BAKSMALI:-/home/meow/tools/baksmali.jar}"
|
||||
|
||||
export PATH="$JAVA_HOME/bin:$PATH"
|
||||
javac="$JAVA_HOME/bin/javac"
|
||||
java="$JAVA_HOME/bin/java"
|
||||
out="$here/build"
|
||||
|
||||
rm -rf "$out"
|
||||
mkdir -p "$out/stubs" "$out/classes" "$out/dex" "$out/smali"
|
||||
|
||||
echo "==> javac stubs"
|
||||
find "$here/stubs" -name '*.java' > "$out/stubs.list"
|
||||
"$javac" -nowarn -g:none --release 8 -cp "$ANDROID_JAR" -d "$out/stubs" @"$out/stubs.list" 2>&1 |
|
||||
grep -v 'bootstrap class path\|source value 8\|target value 8\|deprecat\|^[0-9] warning' || true
|
||||
|
||||
echo "==> javac sources"
|
||||
find "$here/src" -name '*.java' > "$out/src.list"
|
||||
"$javac" -nowarn -g:none --release 8 -cp "$ANDROID_JAR:$out/stubs" -d "$out/classes" @"$out/src.list" 2>&1 |
|
||||
grep -v 'bootstrap class path\|source value 8\|target value 8\|deprecat\|^[0-9] warning' || true
|
||||
|
||||
echo "==> d8"
|
||||
find "$out/classes" -name '*.class' > "$out/class.list"
|
||||
"$D8" --min-api 24 --no-desugaring --lib "$ANDROID_JAR" --classpath "$out/stubs" \
|
||||
--output "$out/dex" @"$out/class.list"
|
||||
|
||||
echo "==> baksmali"
|
||||
"$java" -jar "$BAKSMALI" d "$out/dex/classes.dex" -o "$out/smali"
|
||||
|
||||
echo "==> post-process"
|
||||
find "$out/smali" -name '*.smali' -print0 | xargs -0 sed -i \
|
||||
-e 's/getSizeNHjbRc/getSize-NH-jbRc/g' \
|
||||
-e 's/getIntrinsicSizeNHjbRc/getIntrinsicSize-NH-jbRc/g' \
|
||||
-e 's/drawxKDEd0/draw-x_KDEd0/g' \
|
||||
-e 's/getWidthImpl/getWidth-impl/g' \
|
||||
-e 's/getHeightImpl/getHeight-impl/g'
|
||||
|
||||
if grep -rl 'invoke-custom\|access\$' "$out/smali" >/dev/null; then
|
||||
echo "!! invoke-custom / access\$ bridge in output - keep it to one class, no lambdas" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
dest="$repo/patches/extension/radiant"
|
||||
for c in AiSparkle AnimatedCovers AnimatedCoverView RoundOutline SparkleIcons; do
|
||||
rm -f "$dest/$c".smali
|
||||
cp "$out"/smali/radiant/"$c".smali "$dest/"
|
||||
echo " -> patches/extension/radiant/$c.smali"
|
||||
done
|
||||
@@ -0,0 +1,155 @@
|
||||
package radiant;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.LinearGradient;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.Shader;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.runtime.MutableState;
|
||||
import androidx.compose.runtime.SnapshotStateKt;
|
||||
import androidx.compose.ui.geometry.Size;
|
||||
import androidx.compose.ui.graphics.AndroidCanvas_androidKt;
|
||||
import androidx.compose.ui.graphics.ColorFilter;
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope;
|
||||
import androidx.compose.ui.graphics.painter.Painter;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
// Rainbow sparkle when syllables are AI generated
|
||||
public final class AiSparkle extends Painter implements Choreographer.FrameCallback, Runnable {
|
||||
private static final String TAG = "RLAiSparkle";
|
||||
|
||||
// Same colours as desktop
|
||||
private static final int[] PALETTE = {0xFFFF6B9D, 0xFFFFD166, 0xFF5EE7FF, 0xFFA78BFA};
|
||||
private static final long CYCLE_MS = 6000L;
|
||||
private static final float[] STOPS = {0f, 0.35f, 0.65f, 1f};
|
||||
|
||||
// Current lyrics are AI generated
|
||||
static final MutableState aiState = SnapshotStateKt.mutableStateOf$default(Boolean.FALSE, null, 2, null);
|
||||
// Frame tick for the animation
|
||||
static final MutableState tick = SnapshotStateKt.mutableStateOf$default(Long.valueOf(0L), null, 2, null);
|
||||
|
||||
private static final AiSparkle POSTER = new AiSparkle(null);
|
||||
private static volatile boolean pending;
|
||||
private static AiSparkle cached;
|
||||
private static boolean frameScheduled;
|
||||
private static long lastDrawMs;
|
||||
|
||||
private final Painter base;
|
||||
private final Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final int[] colors = new int[4];
|
||||
|
||||
private AiSparkle(Painter base) {
|
||||
this.base = base;
|
||||
fillPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
|
||||
}
|
||||
|
||||
// Check the API response for AI syllables
|
||||
public static void onResponse(String body) {
|
||||
boolean ai = false;
|
||||
try {
|
||||
JSONObject o = new JSONObject(body);
|
||||
ai = "Word".equals(o.optString("type", "")) && o.optBoolean("_synthesized", false);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
post(ai);
|
||||
}
|
||||
|
||||
// New lookup, clear it
|
||||
public static void reset() {
|
||||
post(false);
|
||||
}
|
||||
|
||||
// Use the rainbow painter while AI
|
||||
public static Painter wrap(Painter painter, Composer composer) {
|
||||
if (!((Boolean) aiState.getValue()).booleanValue() || painter == null) return painter;
|
||||
AiSparkle c = cached;
|
||||
if (c == null || c.base != painter) {
|
||||
c = new AiSparkle(painter);
|
||||
cached = c;
|
||||
}
|
||||
return c;
|
||||
}
|
||||
|
||||
private static void post(boolean ai) {
|
||||
pending = ai;
|
||||
new Handler(Looper.getMainLooper()).post(POSTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
Boolean v = Boolean.valueOf(pending);
|
||||
if (!v.equals(aiState.getValue())) aiState.setValue(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getIntrinsicSizeNHjbRc() {
|
||||
return base.getIntrinsicSizeNHjbRc();
|
||||
}
|
||||
|
||||
// Ignore the button tint
|
||||
@Override
|
||||
public boolean applyColorFilter(ColorFilter colorFilter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(DrawScope scope) {
|
||||
tick.getValue(); // redraw every frame
|
||||
long now = SystemClock.uptimeMillis();
|
||||
lastDrawMs = now;
|
||||
if (!frameScheduled) {
|
||||
frameScheduled = true;
|
||||
Choreographer.getInstance().postFrameCallback(POSTER);
|
||||
}
|
||||
|
||||
long size = scope.getSizeNHjbRc();
|
||||
float w = Size.getWidthImpl(size);
|
||||
float h = Size.getHeightImpl(size);
|
||||
if (w <= 0f || h <= 0f) return;
|
||||
Canvas canvas = AndroidCanvas_androidKt.getNativeCanvas(scope.getDrawContext().getCanvas());
|
||||
float phase = (now % CYCLE_MS) / (float) CYCLE_MS;
|
||||
|
||||
// Paint the gradient into the icon
|
||||
int save = canvas.saveLayer(0f, 0f, w, h, null);
|
||||
base.drawxKDEd0(scope, size, 1f, null);
|
||||
for (int i = 0; i < 4; i++) colors[i] = lerpPalette(phase + i * 0.25f);
|
||||
fillPaint.setShader(new LinearGradient(0f, h, w, 0f, colors, STOPS, Shader.TileMode.CLAMP));
|
||||
canvas.drawRect(0f, 0f, w, h, fillPaint);
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
if (((Boolean) aiState.getValue()).booleanValue() && now - lastDrawMs < 500L) {
|
||||
tick.setValue(Long.valueOf(now));
|
||||
Choreographer.getInstance().postFrameCallback(POSTER);
|
||||
} else {
|
||||
frameScheduled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Blend between palette colours
|
||||
private static int lerpPalette(float phase) {
|
||||
float p = phase - (float) Math.floor(phase);
|
||||
float x = p * 4f;
|
||||
int i = (int) x;
|
||||
float f = x - i;
|
||||
int a = PALETTE[i & 3];
|
||||
int b = PALETTE[(i + 1) & 3];
|
||||
int ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF;
|
||||
int br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF;
|
||||
int rr = (int) (ar + (br - ar) * f);
|
||||
int gg = (int) (ag + (bg - ag) * f);
|
||||
int bl = (int) (ab + (bb - ab) * f);
|
||||
return 0xFF000000 | (rr << 16) | (gg << 8) | bl;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
package radiant;
|
||||
|
||||
import android.graphics.SurfaceTexture;
|
||||
import android.media.MediaPlayer;
|
||||
import android.view.Surface;
|
||||
import android.view.TextureView;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.lang.ref.WeakReference;
|
||||
|
||||
// Animated cover over the lyrics pill cover
|
||||
public final class AnimatedCoverView extends TextureView implements TextureView.SurfaceTextureListener,
|
||||
MediaPlayer.OnPreparedListener, MediaPlayer.OnInfoListener, MediaPlayer.OnErrorListener,
|
||||
View.OnLayoutChangeListener, View.OnAttachStateChangeListener, Runnable {
|
||||
|
||||
private static final ArrayList<WeakReference<AnimatedCoverView>> live = new ArrayList<WeakReference<AnimatedCoverView>>();
|
||||
|
||||
private final ImageView target;
|
||||
private final ViewGroup host;
|
||||
private int albumId = -1;
|
||||
private String url;
|
||||
private MediaPlayer player;
|
||||
private boolean outlined;
|
||||
private Surface surface;
|
||||
|
||||
private AnimatedCoverView(ImageView target, ViewGroup host) {
|
||||
super(target.getContext());
|
||||
this.target = target;
|
||||
this.host = host;
|
||||
setOpaque(false);
|
||||
setAlpha(0f);
|
||||
setVisibility(View.INVISIBLE);
|
||||
setSurfaceTextureListener(this);
|
||||
setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
|
||||
// Pill cover was loaded
|
||||
public static void attach(ImageView image, int albumId) {
|
||||
try {
|
||||
if (image == null) return;
|
||||
AnimatedCoverView v = find(image);
|
||||
if (v == null) {
|
||||
// Use the CardView, ConstraintSet crashes otherwise
|
||||
ViewGroup host = frameAncestor(image);
|
||||
if (host == null) return;
|
||||
v = new AnimatedCoverView(image, host);
|
||||
v.setId(View.generateViewId());
|
||||
host.addView(v, new ViewGroup.LayoutParams(Math.max(1, image.getWidth()), Math.max(1, image.getHeight())));
|
||||
image.addOnLayoutChangeListener(v);
|
||||
image.addOnAttachStateChangeListener(v);
|
||||
if (image.getParent() instanceof View && image.getParent() != host) {
|
||||
((View) image.getParent()).addOnLayoutChangeListener(v);
|
||||
}
|
||||
live.add(new WeakReference<AnimatedCoverView>(v));
|
||||
v.sync();
|
||||
}
|
||||
v.albumId = albumId;
|
||||
v.refresh();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
// New cover URL, update every overlay
|
||||
static void refreshAll() {
|
||||
try {
|
||||
Iterator<WeakReference<AnimatedCoverView>> it = live.iterator();
|
||||
while (it.hasNext()) {
|
||||
AnimatedCoverView v = it.next().get();
|
||||
if (v == null) it.remove();
|
||||
else v.refresh();
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
private static ViewGroup frameAncestor(View view) {
|
||||
ViewParent p = view.getParent();
|
||||
while (p instanceof ViewGroup) {
|
||||
if (p instanceof android.widget.FrameLayout) return (ViewGroup) p;
|
||||
p = p.getParent();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static AnimatedCoverView find(ImageView image) {
|
||||
Iterator<WeakReference<AnimatedCoverView>> it = live.iterator();
|
||||
while (it.hasNext()) {
|
||||
AnimatedCoverView v = it.next().get();
|
||||
if (v == null) it.remove();
|
||||
else if (v.target == image) return v;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void refresh() {
|
||||
String u = AnimatedCovers.urlFor(albumId);
|
||||
if (u == null ? url == null : u.equals(url)) return;
|
||||
release();
|
||||
url = u;
|
||||
if (u == null) {
|
||||
setAlpha(0f);
|
||||
setVisibility(View.INVISIBLE);
|
||||
return;
|
||||
}
|
||||
setAlpha(0f);
|
||||
setVisibility(View.VISIBLE);
|
||||
start();
|
||||
}
|
||||
|
||||
private void start() {
|
||||
if (url == null || surface == null || player != null) return;
|
||||
try {
|
||||
MediaPlayer mp = new MediaPlayer();
|
||||
player = mp;
|
||||
mp.setSurface(surface);
|
||||
mp.setOnPreparedListener(this);
|
||||
mp.setOnInfoListener(this);
|
||||
mp.setOnErrorListener(this);
|
||||
mp.setLooping(true);
|
||||
mp.setVolume(0f, 0f);
|
||||
mp.setDataSource(url);
|
||||
mp.prepareAsync();
|
||||
} catch (Throwable t) {
|
||||
release();
|
||||
}
|
||||
}
|
||||
|
||||
private void release() {
|
||||
MediaPlayer mp = player;
|
||||
player = null;
|
||||
if (mp != null) {
|
||||
try {
|
||||
mp.release();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Line up with the cover
|
||||
private void sync() {
|
||||
ViewGroup.LayoutParams lp = getLayoutParams();
|
||||
int w = Math.max(1, target.getWidth());
|
||||
int h = Math.max(1, target.getHeight());
|
||||
if (lp != null && (lp.width != w || lp.height != h)) {
|
||||
lp.width = w;
|
||||
lp.height = h;
|
||||
setLayoutParams(lp);
|
||||
}
|
||||
// Cover position inside the card
|
||||
float x = 0f, y = 0f;
|
||||
View v = target;
|
||||
while (v != null && v != host) {
|
||||
x += v.getLeft() + v.getTranslationX();
|
||||
y += v.getTop() + v.getTranslationY();
|
||||
ViewParent p = v.getParent();
|
||||
v = p instanceof View ? (View) p : null;
|
||||
}
|
||||
if (v == null) return;
|
||||
setTranslationX(x - getLeft());
|
||||
setTranslationY(y - getTop());
|
||||
if (!outlined) {
|
||||
// Same corner radius as the cover
|
||||
outlined = true;
|
||||
float r = 0f;
|
||||
try {
|
||||
r = getResources().getDimension(com.aspiro.wamp.R.dimen.image_header_corner_radius);
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
setOutlineProvider(new RoundOutline(r));
|
||||
setClipToOutline(true);
|
||||
}
|
||||
if (target.getVisibility() != View.VISIBLE && getVisibility() == View.VISIBLE) setVisibility(View.INVISIBLE);
|
||||
}
|
||||
|
||||
// Re-align after layout
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
sync();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLayoutChange(View v, int l, int t, int r, int b, int ol, int ot, int or, int ob) {
|
||||
post(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(View v) {
|
||||
try {
|
||||
refresh();
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(View v) {
|
||||
release();
|
||||
url = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureAvailable(SurfaceTexture st, int w, int h) {
|
||||
surface = new Surface(st);
|
||||
start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureSizeChanged(SurfaceTexture st, int w, int h) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture st) {
|
||||
release();
|
||||
url = null;
|
||||
if (surface != null) surface.release();
|
||||
surface = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSurfaceTextureUpdated(SurfaceTexture st) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepared(MediaPlayer mp) {
|
||||
if (mp == player) mp.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInfo(MediaPlayer mp, int what, int extra) {
|
||||
if (mp == player && what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
|
||||
animate().alpha(1f).setDuration(300L).start();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onError(MediaPlayer mp, int what, int extra) {
|
||||
if (mp == player) {
|
||||
release();
|
||||
setAlpha(0f);
|
||||
setVisibility(View.INVISIBLE);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package radiant;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.compose.foundation.layout.SizeKt;
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.runtime.MutableState;
|
||||
import androidx.compose.runtime.SnapshotStateKt;
|
||||
import androidx.compose.ui.Modifier;
|
||||
import androidx.compose.ui.graphics.ColorFilter;
|
||||
import androidx.compose.ui.layout.ContentScale;
|
||||
|
||||
import com.tidal.android.core.compose.components.l;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
// Animated covers, played with TIDAL's own AnimatedCover
|
||||
public final class AnimatedCovers implements Runnable, yl0.p { // MARKER: R8 yl0.p
|
||||
private static final String TAG = "RLAnimatedCovers";
|
||||
private static final String SEARCH = "https://ama.trainswift.net/api/v1/artwork/search?";
|
||||
private static final int TARGET_PX = 700;
|
||||
private static final int CACHE_MAX = 200;
|
||||
|
||||
// Animated cover URL for the playing track
|
||||
static final MutableState urlState = SnapshotStateKt.mutableStateOf$default(null, null, 2, null);
|
||||
// Album the URL belongs to
|
||||
static final MutableState albumState = SnapshotStateKt.mutableStateOf$default(Integer.valueOf(-1), null, 2, null);
|
||||
|
||||
private static final LinkedHashMap<String, String> cache = new LinkedHashMap<String, String>(64, 0.75f, true);
|
||||
private static volatile String currentKey;
|
||||
// Play queue, for the album name
|
||||
private static volatile Object queue;
|
||||
|
||||
private static final int MODE_LOOKUP = 0;
|
||||
private static final int MODE_PUBLISH = 1;
|
||||
private static final int MODE_PLACEHOLDER = 2;
|
||||
private final int mode;
|
||||
|
||||
// lookup
|
||||
private String key, title, artist, album, url;
|
||||
private int albumId;
|
||||
|
||||
// normal cover under the video
|
||||
private yl0.l request; // MARKER: R8 yl0.l
|
||||
private String description;
|
||||
private ColorFilter colorFilter;
|
||||
private ContentScale contentScale;
|
||||
private Object placeholder;
|
||||
private yl0.a onError; // MARKER: R8 yl0.a
|
||||
private int defaults;
|
||||
|
||||
private AnimatedCovers(int mode) {
|
||||
this.mode = mode;
|
||||
}
|
||||
|
||||
// Keep the play queue
|
||||
public static void setQueue(Object playQueue) {
|
||||
queue = playQueue;
|
||||
}
|
||||
|
||||
// Track changed, look it up
|
||||
public static void onMediaState(String title, String artist, int albumId, String cover) {
|
||||
if (title == null || artist == null || title.isEmpty()) return;
|
||||
String key = artist + "\u0000" + title;
|
||||
if (key.equals(currentKey)) return;
|
||||
currentKey = key;
|
||||
publish(key, null, albumId);
|
||||
String hit;
|
||||
synchronized (cache) {
|
||||
hit = cache.get(key);
|
||||
}
|
||||
if (hit != null) {
|
||||
publish(key, hit.isEmpty() ? null : hit, albumId);
|
||||
return;
|
||||
}
|
||||
AnimatedCovers job = new AnimatedCovers(MODE_LOOKUP);
|
||||
job.key = key;
|
||||
job.title = title;
|
||||
job.artist = artist;
|
||||
job.album = currentAlbum(title);
|
||||
job.albumId = albumId;
|
||||
Thread t = new Thread(job, "RL-AnimatedCovers");
|
||||
t.setDaemon(true);
|
||||
t.start();
|
||||
}
|
||||
|
||||
// Animated cover if we have one, else TIDAL's image
|
||||
public static void cover(yl0.l request, String description, Modifier modifier, ColorFilter colorFilter, // MARKER: R8 yl0.l
|
||||
ContentScale contentScale, Object placeholder, yl0.a onError, // MARKER: R8 yl0.a
|
||||
Composer composer, int changed, int defaults, int albumId) {
|
||||
String u = (String) urlState.getValue();
|
||||
int playing = ((Integer) albumState.getValue()).intValue();
|
||||
if (u != null && playing == albumId) {
|
||||
composer.startReplaceGroup(0x52414143);
|
||||
AnimatedCovers ph = new AnimatedCovers(MODE_PLACEHOLDER);
|
||||
ph.request = request;
|
||||
ph.description = description;
|
||||
ph.colorFilter = colorFilter;
|
||||
ph.contentScale = contentScale;
|
||||
ph.placeholder = placeholder;
|
||||
ph.onError = onError;
|
||||
ph.defaults = defaults;
|
||||
l.a(u, modifier, ph, composer, 0);
|
||||
composer.endReplaceGroup();
|
||||
} else {
|
||||
composer.startReplaceGroup(0x52414144);
|
||||
sd0.f.a(request, description, modifier, colorFilter, contentScale, placeholder, onError, composer, changed, defaults);
|
||||
composer.endReplaceGroup();
|
||||
}
|
||||
}
|
||||
|
||||
// URL if it's for this album
|
||||
static String urlFor(int albumId) {
|
||||
String u = (String) urlState.getValue();
|
||||
return u != null && ((Integer) albumState.getValue()).intValue() == albumId ? u : null;
|
||||
}
|
||||
|
||||
// Normal cover as the placeholder
|
||||
@Override
|
||||
public Object invoke(Object composer, Object changed) {
|
||||
Modifier fill = SizeKt.fillMaxSize(Modifier.Companion, 1f);
|
||||
sd0.f.a(request, description, fill, colorFilter, contentScale, placeholder, onError, (Composer) composer, 0, defaults);
|
||||
return kotlin.u.a; // MARKER: R8 kotlin.u
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mode == MODE_PUBLISH) {
|
||||
// Newer track, drop it
|
||||
if (!key.equals(currentKey)) return;
|
||||
urlState.setValue(url);
|
||||
albumState.setValue(Integer.valueOf(albumId));
|
||||
AnimatedCoverView.refreshAll();
|
||||
return;
|
||||
}
|
||||
String found = null;
|
||||
try {
|
||||
// Queue can lag, try once more
|
||||
if (album == null) {
|
||||
Thread.sleep(800L);
|
||||
album = currentAlbum(title);
|
||||
}
|
||||
// With album first, then without
|
||||
if (album != null) found = lookup(title, artist, album);
|
||||
if (found == null) found = lookup(title, artist, null);
|
||||
} catch (Throwable t) {
|
||||
Log.w(TAG, "lookup failed for " + title + ": " + t);
|
||||
}
|
||||
synchronized (cache) {
|
||||
if (cache.size() >= CACHE_MAX) {
|
||||
String eldest = cache.keySet().iterator().next();
|
||||
cache.remove(eldest);
|
||||
}
|
||||
cache.put(key, found == null ? "" : found);
|
||||
}
|
||||
Log.i(TAG, (found == null ? "no animated cover for " : "animated cover for ") + title);
|
||||
publish(key, found, albumId);
|
||||
}
|
||||
|
||||
private static void publish(String key, String url, int albumId) {
|
||||
AnimatedCovers p = new AnimatedCovers(MODE_PUBLISH);
|
||||
p.key = key;
|
||||
p.url = url;
|
||||
p.albumId = albumId;
|
||||
new Handler(Looper.getMainLooper()).post(p);
|
||||
}
|
||||
|
||||
// Album name of the playing track
|
||||
private static String currentAlbum(String title) {
|
||||
try {
|
||||
Object q = queue;
|
||||
if (q == null) return null;
|
||||
com.aspiro.wamp.playqueue.q0 item = ((zh.e) q).getCurrentItem(); // MARKER: R8 zh.e q0
|
||||
if (item == null) return null;
|
||||
com.aspiro.wamp.model.MediaItem media = item.getMediaItem();
|
||||
if (media == null || !title.equals(media.getTitle()) || media.getAlbum() == null) return null;
|
||||
String a = media.getAlbum().getTitle();
|
||||
return a == null || a.trim().isEmpty() ? null : a.trim();
|
||||
} catch (Throwable t) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String lookup(String title, String artist, String album) throws Exception {
|
||||
String q = SEARCH + "title=" + enc(title) + "&artist=" + enc(artist);
|
||||
if (album != null) q = q + "&album=" + enc(album);
|
||||
String body = get(q);
|
||||
if (body == null) return null;
|
||||
JSONObject o = new JSONObject(body);
|
||||
String master = o.optString("url", "");
|
||||
if (master.isEmpty()) return null;
|
||||
return resolveMp4(master);
|
||||
}
|
||||
|
||||
// Find the mp4 behind the playlist
|
||||
private static String resolveMp4(String masterUrl) throws Exception {
|
||||
String master = get(masterUrl);
|
||||
if (master == null) return null;
|
||||
String[] lines = master.split("\n");
|
||||
String best = null;
|
||||
int bestW = -1;
|
||||
boolean bestBig = false;
|
||||
for (int i = 0; i < lines.length - 1; i++) {
|
||||
String line = lines[i].trim();
|
||||
if (!line.startsWith("#EXT-X-STREAM-INF:") || line.indexOf("avc1") < 0) continue;
|
||||
if (line.indexOf("VIDEO-RANGE=SDR") < 0 && line.indexOf("VIDEO-RANGE") >= 0) continue;
|
||||
int w = 0;
|
||||
int r = line.indexOf("RESOLUTION=");
|
||||
if (r >= 0) {
|
||||
int x = line.indexOf('x', r);
|
||||
try {
|
||||
w = Integer.parseInt(line.substring(r + 11, x));
|
||||
} catch (Throwable ignored) {
|
||||
}
|
||||
}
|
||||
String uri = lines[i + 1].trim();
|
||||
if (uri.isEmpty() || uri.startsWith("#")) continue;
|
||||
boolean big = w >= TARGET_PX;
|
||||
// Smallest one big enough
|
||||
if (best == null || (big && (!bestBig || w < bestW)) || (!big && !bestBig && w > bestW)) {
|
||||
best = new URL(new URL(masterUrl), uri).toString();
|
||||
bestW = w;
|
||||
bestBig = big;
|
||||
}
|
||||
}
|
||||
if (best == null) return null;
|
||||
String media = get(best);
|
||||
if (media == null) return null;
|
||||
int m = media.indexOf("#EXT-X-MAP:URI=\"");
|
||||
if (m < 0) return null;
|
||||
int s = m + 16;
|
||||
int e = media.indexOf('"', s);
|
||||
if (e < 0) return null;
|
||||
return new URL(new URL(best), media.substring(s, e)).toString();
|
||||
}
|
||||
|
||||
private static String get(String url) throws Exception {
|
||||
HttpURLConnection c = (HttpURLConnection) new URL(url).openConnection();
|
||||
c.setConnectTimeout(10000);
|
||||
c.setReadTimeout(10000);
|
||||
try {
|
||||
if (c.getResponseCode() != 200) return null;
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(c.getInputStream(), "UTF-8"));
|
||||
StringBuilder sb = new StringBuilder();
|
||||
char[] buf = new char[4096];
|
||||
int n;
|
||||
while ((n = r.read(buf)) > 0) sb.append(buf, 0, n);
|
||||
r.close();
|
||||
return sb.toString();
|
||||
} finally {
|
||||
c.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private static String enc(String s) throws Exception {
|
||||
return URLEncoder.encode(s, "UTF-8").replace("+", "%20");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package radiant;
|
||||
|
||||
import android.graphics.Outline;
|
||||
import android.view.View;
|
||||
import android.view.ViewOutlineProvider;
|
||||
|
||||
// Rounded corners for the cover overlay
|
||||
public final class RoundOutline extends ViewOutlineProvider {
|
||||
private final float radius;
|
||||
|
||||
public RoundOutline(float radius) {
|
||||
this.radius = radius;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getOutline(View view, Outline outline) {
|
||||
outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), radius);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package radiant;
|
||||
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffXfermode;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import androidx.compose.runtime.Composer;
|
||||
import androidx.compose.runtime.MutableState;
|
||||
import androidx.compose.runtime.SnapshotStateKt;
|
||||
import androidx.compose.ui.geometry.Size;
|
||||
import androidx.compose.ui.graphics.AndroidCanvas_androidKt;
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope;
|
||||
import androidx.compose.ui.graphics.painter.Painter;
|
||||
import androidx.compose.ui.res.PainterResources_androidKt;
|
||||
|
||||
import com.tidal.android.feature.playerscreen.ui.g;
|
||||
|
||||
// Sparkle button loading spinner and no lyrics icon
|
||||
public final class SparkleIcons extends Painter implements Choreographer.FrameCallback, Runnable {
|
||||
static final int MODE_SPARKLE = 0;
|
||||
static final int MODE_LOADING = 1;
|
||||
static final int MODE_NONE = 2;
|
||||
|
||||
private static final long SPIN_MS = 900L;
|
||||
// Same size as the lyrics icon
|
||||
private static final float NOTE_SCALE = 0.75f;
|
||||
// Desktop spinner, a bit smaller
|
||||
private static final float RING_R = 7.1f / 20f;
|
||||
private static final float RING_STROKE = 1.8f / 20f;
|
||||
private static final float ARC_SWEEP = 42f / (float) (2 * Math.PI * 8) * 360f; // same gap as desktop
|
||||
|
||||
// Mode for the sparkle button
|
||||
public static int currentMode = MODE_SPARKLE;
|
||||
|
||||
// RL API fetch running
|
||||
static final MutableState rlLoading = SnapshotStateKt.mutableStateOf$default(Boolean.FALSE, null, 2, null);
|
||||
static final MutableState tick = SnapshotStateKt.mutableStateOf$default(Long.valueOf(0L), null, 2, null);
|
||||
|
||||
private static final SparkleIcons POSTER = new SparkleIcons(MODE_SPARKLE, null, null);
|
||||
private static final long LOADING_TIMEOUT_MS = 60000L; // requests can take ~30s
|
||||
|
||||
private static final Object lock = new Object();
|
||||
// Newest fetch and if it's running
|
||||
private static Object latestWorker;
|
||||
private static boolean latestRunning;
|
||||
// Bumped by timers to recompose
|
||||
static final MutableState timeoutTick = SnapshotStateKt.mutableStateOf$default(Integer.valueOf(0), null, 2, null);
|
||||
private static long loadingSinceMs;
|
||||
private static boolean timeoutPosted;
|
||||
private static volatile boolean newFetch;
|
||||
private static Object lastMedia;
|
||||
private static final long NONE_GRACE_MS = 5000L;
|
||||
private static final String TAG = "RLSparkle";
|
||||
private static long noneSinceMs;
|
||||
private static int lastBranch = -1;
|
||||
private static long postedAtMs = Long.MAX_VALUE;
|
||||
private static final SparkleIcons TIMEOUT = new SparkleIcons(-1, null, null);
|
||||
private static SparkleIcons spinner;
|
||||
private static SparkleIcons noLyrics;
|
||||
private static boolean frameScheduled;
|
||||
private static long lastSpinDrawMs;
|
||||
|
||||
private final int mode;
|
||||
private final Painter inner;
|
||||
// Sparkle painter, for sizing
|
||||
private final Painter sizeRef;
|
||||
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint clear = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final RectF oval = new RectF();
|
||||
|
||||
private SparkleIcons(int mode, Painter inner, Painter sizeRef) {
|
||||
this.mode = mode;
|
||||
this.inner = inner;
|
||||
this.sizeRef = sizeRef;
|
||||
paint.setColor(0xFFFFFFFF);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setStrokeCap(Paint.Cap.ROUND);
|
||||
clear.setStyle(Paint.Style.STROKE);
|
||||
clear.setStrokeCap(Paint.Cap.ROUND);
|
||||
clear.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
|
||||
}
|
||||
|
||||
// Pick the button mode
|
||||
public static int modeFor(Object lyricsState, boolean hasLyrics, Object media) { // MARKER: R8 g$b g$c g$d
|
||||
timeoutTick.getValue(); // recompose on timers
|
||||
long now = SystemClock.uptimeMillis();
|
||||
// New track, reset timers
|
||||
if (media == null ? lastMedia != null : !media.equals(lastMedia)) {
|
||||
lastMedia = media;
|
||||
loadingSinceMs = 0L;
|
||||
lastBranch = -1;
|
||||
}
|
||||
boolean rl = ((Boolean) rlLoading.getValue()).booleanValue();
|
||||
int m;
|
||||
int branch;
|
||||
if (hasLyrics || lyricsState instanceof g.c || lyricsState instanceof g.d) {
|
||||
// Lyrics win
|
||||
branch = 0;
|
||||
m = MODE_SPARKLE;
|
||||
loadingSinceMs = 0L;
|
||||
} else if (rl || lyricsState instanceof g.b) {
|
||||
branch = 1;
|
||||
if (loadingSinceMs == 0L) loadingSinceMs = now;
|
||||
if (now - loadingSinceMs >= LOADING_TIMEOUT_MS) {
|
||||
m = MODE_NONE; // don't spin forever
|
||||
} else {
|
||||
m = MODE_LOADING;
|
||||
scheduleRecompose(loadingSinceMs + LOADING_TIMEOUT_MS);
|
||||
}
|
||||
} else {
|
||||
// Wait a bit before saying no lyrics
|
||||
branch = 2;
|
||||
if (lastBranch != 2) noneSinceMs = now;
|
||||
if (now - noneSinceMs < NONE_GRACE_MS) {
|
||||
m = MODE_LOADING;
|
||||
scheduleRecompose(noneSinceMs + NONE_GRACE_MS);
|
||||
} else {
|
||||
m = MODE_NONE;
|
||||
}
|
||||
loadingSinceMs = 0L;
|
||||
}
|
||||
lastBranch = branch;
|
||||
if (m != currentMode) {
|
||||
Log.i(TAG, "mode " + currentMode + " -> " + m + " (lyricsState="
|
||||
+ (lyricsState == null ? "null" : lyricsState.getClass().getName())
|
||||
+ " hasLyrics=" + hasLyrics + " rlLoading=" + rl + ")");
|
||||
}
|
||||
currentMode = m;
|
||||
return m;
|
||||
}
|
||||
|
||||
// Recompose later
|
||||
private static void scheduleRecompose(long atMs) {
|
||||
if (timeoutPosted && atMs >= postedAtMs) return;
|
||||
timeoutPosted = true;
|
||||
postedAtMs = atMs;
|
||||
long delay = Math.max(0L, atMs - SystemClock.uptimeMillis()) + 50L;
|
||||
new Handler(Looper.getMainLooper()).postDelayed(TIMEOUT, delay);
|
||||
}
|
||||
|
||||
// Icon for the mode
|
||||
public static Painter forMode(Painter sparkle, int mode, Composer composer) {
|
||||
composer.startReplaceGroup(0x52415300 + mode);
|
||||
Painter out = sparkle;
|
||||
if (mode == MODE_LOADING && sparkle != null) {
|
||||
SparkleIcons s = spinner;
|
||||
if (s == null || s.inner != sparkle) spinner = s = new SparkleIcons(MODE_LOADING, sparkle, sparkle);
|
||||
out = s;
|
||||
} else if (mode == MODE_NONE && sparkle != null) {
|
||||
Painter note = PainterResources_androidKt.painterResource(com.aspiro.wamp.R.drawable.ic_music_note, composer, 0);
|
||||
SparkleIcons s = noLyrics;
|
||||
if (s == null || s.inner != note || s.sizeRef != sparkle) noLyrics = s = new SparkleIcons(MODE_NONE, note, sparkle);
|
||||
out = s;
|
||||
}
|
||||
composer.endReplaceGroup();
|
||||
return out;
|
||||
}
|
||||
|
||||
// Fetch started
|
||||
public static void rlBegin(Object worker) {
|
||||
synchronized (lock) {
|
||||
latestWorker = worker;
|
||||
latestRunning = true;
|
||||
}
|
||||
newFetch = true;
|
||||
new Handler(Looper.getMainLooper()).post(POSTER);
|
||||
}
|
||||
|
||||
// Fetch done, only the newest counts
|
||||
public static void rlEnd(Object worker) {
|
||||
synchronized (lock) {
|
||||
if (worker == latestWorker) latestRunning = false;
|
||||
}
|
||||
new Handler(Looper.getMainLooper()).post(POSTER);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
if (mode == -1) {
|
||||
// Timer fired, recompose
|
||||
timeoutPosted = false;
|
||||
postedAtMs = Long.MAX_VALUE;
|
||||
timeoutTick.setValue(Integer.valueOf(((Integer) timeoutTick.getValue()).intValue() + 1));
|
||||
return;
|
||||
}
|
||||
boolean on;
|
||||
synchronized (lock) {
|
||||
on = latestRunning;
|
||||
}
|
||||
if (newFetch) {
|
||||
// New fetch, reset timeout
|
||||
newFetch = false;
|
||||
loadingSinceMs = 0L;
|
||||
timeoutTick.setValue(Integer.valueOf(((Integer) timeoutTick.getValue()).intValue() + 1));
|
||||
}
|
||||
Boolean v = Boolean.valueOf(on);
|
||||
if (!v.equals(rlLoading.getValue())) rlLoading.setValue(v);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getIntrinsicSizeNHjbRc() {
|
||||
return sizeRef.getIntrinsicSizeNHjbRc();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDraw(DrawScope scope) {
|
||||
long size = scope.getSizeNHjbRc();
|
||||
float w = Size.getWidthImpl(size);
|
||||
float h = Size.getHeightImpl(size);
|
||||
if (w <= 0f || h <= 0f) return;
|
||||
Canvas canvas = AndroidCanvas_androidKt.getNativeCanvas(scope.getDrawContext().getCanvas());
|
||||
float s = Math.min(w, h);
|
||||
|
||||
if (mode == MODE_LOADING) {
|
||||
tick.getValue(); // redraw every frame
|
||||
long now = SystemClock.uptimeMillis();
|
||||
lastSpinDrawMs = now;
|
||||
if (!frameScheduled) {
|
||||
frameScheduled = true;
|
||||
Choreographer.getInstance().postFrameCallback(POSTER);
|
||||
}
|
||||
float r = s * RING_R;
|
||||
paint.setStrokeWidth(s * RING_STROKE);
|
||||
oval.set(w / 2f - r, h / 2f - r, w / 2f + r, h / 2f + r);
|
||||
float angle = (now % SPIN_MS) / (float) SPIN_MS * 360f;
|
||||
canvas.drawArc(oval, angle - 90f, ARC_SWEEP, false, paint);
|
||||
return;
|
||||
}
|
||||
|
||||
// Note with a line through it
|
||||
int save = canvas.saveLayer(0f, 0f, w, h, null);
|
||||
canvas.save();
|
||||
canvas.scale(NOTE_SCALE, NOTE_SCALE, w / 2f, h / 2f);
|
||||
inner.drawxKDEd0(scope, size, 1f, null);
|
||||
canvas.restore();
|
||||
float stroke = s * 0.06f;
|
||||
float a = s * 0.2f, b = s * 0.8f;
|
||||
clear.setStrokeWidth(stroke * 2.6f);
|
||||
canvas.drawLine(a, a, b, b, clear);
|
||||
paint.setStrokeWidth(stroke);
|
||||
canvas.drawLine(a, a, b, b, paint);
|
||||
canvas.restoreToCount(save);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
if (now - lastSpinDrawMs < 500L) {
|
||||
tick.setValue(Long.valueOf(now));
|
||||
Choreographer.getInstance().postFrameCallback(POSTER);
|
||||
} else {
|
||||
frameScheduled = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package androidx.compose.foundation.layout;
|
||||
import androidx.compose.ui.Modifier;
|
||||
public final class SizeKt {
|
||||
public static Modifier fillMaxSize(Modifier modifier, float fraction) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface Composer {
|
||||
void startReplaceGroup(int key);
|
||||
void endReplaceGroup();
|
||||
Object consume(CompositionLocal key);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public abstract class CompositionLocal {}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface MutableState extends State { void setValue(Object value); }
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface SnapshotMutationPolicy {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package androidx.compose.runtime;
|
||||
public final class SnapshotStateKt {
|
||||
public static MutableState mutableStateOf$default(Object value, SnapshotMutationPolicy policy, int flags, Object marker) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public interface State { Object getValue(); }
|
||||
@@ -0,0 +1,6 @@
|
||||
package androidx.compose.ui;
|
||||
public interface Modifier {
|
||||
/** Non-constant initialiser so javac emits a real sget-object. */
|
||||
Companion Companion = new Companion();
|
||||
final class Companion implements Modifier {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package androidx.compose.ui.geometry;
|
||||
public final class Size {
|
||||
/** Renamed to getWidth-impl by build.sh (Kotlin value-class mangling). */
|
||||
public static float getWidthImpl(long packed) { return 0f; }
|
||||
/** Renamed to getHeight-impl by build.sh (Kotlin value-class mangling). */
|
||||
public static float getHeightImpl(long packed) { return 0f; }
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package androidx.compose.ui.graphics;
|
||||
public final class AndroidCanvas_androidKt {
|
||||
public static android.graphics.Canvas getNativeCanvas(Canvas canvas) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.ui.graphics;
|
||||
public interface Canvas {}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.ui.graphics;
|
||||
public class ColorFilter {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package androidx.compose.ui.graphics.drawscope;
|
||||
import androidx.compose.ui.graphics.Canvas;
|
||||
public interface DrawContext { Canvas getCanvas(); }
|
||||
@@ -0,0 +1,6 @@
|
||||
package androidx.compose.ui.graphics.drawscope;
|
||||
public interface DrawScope {
|
||||
DrawContext getDrawContext();
|
||||
/** Renamed to getSize-NH-jbRc by build.sh (Kotlin value-class mangling). */
|
||||
long getSizeNHjbRc();
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package androidx.compose.ui.graphics.painter;
|
||||
|
||||
import androidx.compose.ui.graphics.ColorFilter;
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope;
|
||||
|
||||
// build.sh fixes the mangled names
|
||||
public abstract class Painter {
|
||||
public Painter() {}
|
||||
public boolean applyAlpha(float alpha) { return false; }
|
||||
public boolean applyColorFilter(ColorFilter colorFilter) { return false; }
|
||||
public final void drawxKDEd0(DrawScope scope, long size, float alpha, ColorFilter colorFilter) {}
|
||||
public abstract long getIntrinsicSizeNHjbRc();
|
||||
public abstract void onDraw(DrawScope scope);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.ui.layout;
|
||||
public interface ContentScale {}
|
||||
@@ -0,0 +1,4 @@
|
||||
package androidx.compose.ui.res;
|
||||
public final class PainterResources_androidKt {
|
||||
public static androidx.compose.ui.graphics.painter.Painter painterResource(int id, androidx.compose.runtime.Composer composer, int changed) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.aspiro.wamp;
|
||||
// Not final so javac doesn't inline them
|
||||
public final class R {
|
||||
public static final class drawable { public static int ic_music_note; }
|
||||
public static final class dimen { public static int image_header_corner_radius; }
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package com.aspiro.wamp.model;
|
||||
public class Album { public String getTitle() { return null; } }
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.aspiro.wamp.model;
|
||||
public class MediaItem {
|
||||
public Album getAlbum() { return null; }
|
||||
public String getTitle() { return null; }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package com.aspiro.wamp.playqueue;
|
||||
// MARKER: R8 class name: TIDAL dex FQN is `com.aspiro.wamp.playqueue.q0` (play queue item)
|
||||
public interface q0 { com.aspiro.wamp.model.MediaItem getMediaItem(); }
|
||||
@@ -0,0 +1,6 @@
|
||||
package com.tidal.android.core.compose.components;
|
||||
// MARKER: R8 class name: TIDAL's AnimatedCover.kt (AnimatedCover(url, modifier, placeholder) + LoopingVideo)
|
||||
public final class l {
|
||||
public static void a(String url, androidx.compose.ui.Modifier modifier, yl0.p placeholder,
|
||||
androidx.compose.runtime.Composer composer, int changed) {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.tidal.android.feature.playerscreen.ui;
|
||||
// MARKER: R8 class names: player lyrics state - g$a Hidden, g$b Loading, g$c Synced, g$d Unsynced
|
||||
public abstract class g {
|
||||
public static final class b extends g {}
|
||||
public static final class c extends g {}
|
||||
public static final class d extends g {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package kotlin;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `kotlin.u` - both package and name roll
|
||||
/** kotlin.Unit (obfuscated in the TIDAL dex). */
|
||||
public final class u { public static final u a = null; }
|
||||
@@ -0,0 +1,7 @@
|
||||
package sd0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `sd0.f` (TIDAL's image composable used by AlbumCover)
|
||||
public final class f {
|
||||
public static void a(yl0.l request, String contentDescription, androidx.compose.ui.Modifier modifier,
|
||||
androidx.compose.ui.graphics.ColorFilter colorFilter, androidx.compose.ui.layout.ContentScale contentScale,
|
||||
Object placeholder, yl0.a onError, androidx.compose.runtime.Composer composer, int changed, int defaults) {}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package yl0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `yl0.a` - both package and name roll
|
||||
/** kotlin.jvm.functions.Function0 (obfuscated in the TIDAL dex). */
|
||||
public interface a { Object invoke(); }
|
||||
@@ -0,0 +1,4 @@
|
||||
package yl0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `yl0.l` - both package and name roll
|
||||
/** kotlin.jvm.functions.Function1 (obfuscated in the TIDAL dex). */
|
||||
public interface l { Object invoke(Object p1); }
|
||||
@@ -0,0 +1,4 @@
|
||||
package yl0;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `yl0.p` - both package and name roll
|
||||
/** kotlin.jvm.functions.Function2 (obfuscated in the TIDAL dex). */
|
||||
public interface p { Object invoke(Object p1, Object p2); }
|
||||
@@ -0,0 +1,3 @@
|
||||
package zh;
|
||||
// MARKER: R8 whole class: TIDAL dex FQN is `zh.e` (play queue provider held by Mini/PlayerViewModel.a)
|
||||
public interface e { com.aspiro.wamp.playqueue.q0 getCurrentItem(); }
|
||||
Reference in new issue
Block a user