mirror of
https://github.com/meowarex/TidaLuna-Plugins.git
synced 2026-08-26 14:27:44 +10:00
Backdrop Rewrite (Kawarp <3)
This commit is contained in:
@@ -7,5 +7,8 @@
|
||||
"avatarUrl": "https://avatars.githubusercontent.com/u/90243579"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"type": "module"
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@kawarp/core": "*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ declare global {
|
||||
updateStickyLyricsFeature?: () => void;
|
||||
updateStickyLyricsSetting?: (checked: boolean) => void;
|
||||
updateRadiantLyricsPlayerBarTint?: () => void;
|
||||
updateRadiantLyricsGlobalBackground?: () => void;
|
||||
updateRadiantLyricsNowPlayingBackground?: () => void;
|
||||
updateRadiantLyricsBackdrop?: () => void;
|
||||
updateQualityProgressColor?: () => void;
|
||||
updateIntegratedSeekBar?: () => void;
|
||||
updateLyricsStyle?: () => void;
|
||||
@@ -48,16 +47,22 @@ export const settings = await ReactiveStore.getPluginStorage("RadiantLyrics", {
|
||||
playerBarTint: 5,
|
||||
playerBarTintColor: "#000000" as string,
|
||||
playerBarTintCustomColors: [] as string[],
|
||||
// Master switch
|
||||
backdropEnabled: true,
|
||||
backdropPlaybackReactive: true,
|
||||
CoverEverywhere: true,
|
||||
performanceMode: false,
|
||||
spinningArt: true,
|
||||
backgroundScale: 15,
|
||||
backgroundRadius: 25,
|
||||
backgroundContrast: 120,
|
||||
backgroundBlur: 80,
|
||||
backgroundBrightness: 40,
|
||||
spinSpeed: 45,
|
||||
settingsAffectNowPlaying: true,
|
||||
// Backdrop is rendered entirely by kawarp (yay!)
|
||||
backdropOpacity: 75,
|
||||
backdropWarp: 100,
|
||||
backdropBlurPasses: 5,
|
||||
backdropSpeed: 175,
|
||||
backdropContrast: 125,
|
||||
backdropSaturation: 125,
|
||||
backdropDithering: 15,
|
||||
backdropScale: 100,
|
||||
// Readability on bright covers <3
|
||||
backdropDarken: 80,
|
||||
});
|
||||
|
||||
export const Settings = () => {
|
||||
@@ -74,27 +79,38 @@ export const Settings = () => {
|
||||
const [CoverEverywhere, setCoverEverywhere] = React.useState(
|
||||
settings.CoverEverywhere,
|
||||
);
|
||||
const [backdropEnabled, setBackdropEnabled] = React.useState(
|
||||
settings.backdropEnabled,
|
||||
);
|
||||
const [backdropPlaybackReactive, setBackdropPlaybackReactive] =
|
||||
React.useState(settings.backdropPlaybackReactive);
|
||||
const [performanceMode, setPerformanceMode] = React.useState(
|
||||
settings.performanceMode,
|
||||
);
|
||||
const [spinningArt, setspinningArt] = React.useState(settings.spinningArt);
|
||||
const [backgroundContrast, setBackgroundContrast] = React.useState(
|
||||
settings.backgroundContrast,
|
||||
const [backdropOpacity, setBackdropOpacity] = React.useState(
|
||||
settings.backdropOpacity,
|
||||
);
|
||||
const [backgroundBlur, setBackgroundBlur] = React.useState(
|
||||
settings.backgroundBlur,
|
||||
const [backdropWarp, setBackdropWarp] = React.useState(settings.backdropWarp);
|
||||
const [backdropBlurPasses, setBackdropBlurPasses] = React.useState(
|
||||
settings.backdropBlurPasses,
|
||||
);
|
||||
const [backgroundBrightness, setBackgroundBrightness] = React.useState(
|
||||
settings.backgroundBrightness,
|
||||
const [backdropSpeed, setBackdropSpeed] = React.useState(
|
||||
settings.backdropSpeed,
|
||||
);
|
||||
const [spinSpeed, setSpinSpeed] = React.useState(settings.spinSpeed);
|
||||
const [settingsAffectNowPlaying, setSettingsAffectNowPlaying] =
|
||||
React.useState(settings.settingsAffectNowPlaying);
|
||||
const [backgroundScale, setBackgroundScale] = React.useState(
|
||||
settings.backgroundScale,
|
||||
const [backdropContrast, setBackdropContrast] = React.useState(
|
||||
settings.backdropContrast,
|
||||
);
|
||||
const [backgroundRadius, setBackgroundRadius] = React.useState(
|
||||
settings.backgroundRadius,
|
||||
const [backdropSaturation, setBackdropSaturation] = React.useState(
|
||||
settings.backdropSaturation,
|
||||
);
|
||||
const [backdropDithering, setBackdropDithering] = React.useState(
|
||||
settings.backdropDithering,
|
||||
);
|
||||
const [backdropScale, setBackdropScale] = React.useState(
|
||||
settings.backdropScale,
|
||||
);
|
||||
const [backdropDarken, setBackdropDarken] = React.useState(
|
||||
settings.backdropDarken,
|
||||
);
|
||||
const [floatingPlayerBar, setFloatingPlayerBar] = React.useState(
|
||||
settings.floatingPlayerBar,
|
||||
@@ -185,6 +201,10 @@ export const Settings = () => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const refreshBackdrop = () => {
|
||||
window.updateRadiantLyricsBackdrop?.();
|
||||
};
|
||||
|
||||
// Derive props and override onChange to accept a broader first param type
|
||||
type BaseSwitchProps = React.ComponentProps<typeof LunaSwitchSetting>;
|
||||
type AnySwitchProps = Omit<BaseSwitchProps, "onChange"> & {
|
||||
@@ -893,225 +913,163 @@ export const Settings = () => {
|
||||
);
|
||||
})()}
|
||||
<AnySwitch
|
||||
title="Cover Everywhere"
|
||||
desc="Apply the spinning Cover Art background to the entire app, not just the Now Playing view, Heavily Inspired by Cover-Theme by @Inrixia"
|
||||
checked={CoverEverywhere}
|
||||
title="Custom Backdrop"
|
||||
desc="Render the cover-art shader backdrop, off = Tidals cover spin"
|
||||
checked={backdropEnabled}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
console.log(
|
||||
"Spinning Cover Everywhere:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
settings.CoverEverywhere = checked;
|
||||
setCoverEverywhere(checked);
|
||||
// Update styles immediately when setting changes
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
settings.backdropEnabled = checked;
|
||||
setBackdropEnabled(checked);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
{CoverEverywhere && (
|
||||
<AnySwitch
|
||||
title="Performance Mode | Experimental"
|
||||
desc="Performance mode: Reduces blur effects & uses smaller image sizes, to optimize GPU usage"
|
||||
checked={performanceMode}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
console.log("Performance Mode:", checked ? "enabled" : "disabled");
|
||||
settings.performanceMode = checked;
|
||||
setPerformanceMode(checked);
|
||||
// Update background animations immediately when setting changes
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (window.updateRadiantLyricsNowPlayingBackground) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<AnySwitch
|
||||
title="Background Cover Spin" // Cheers @Max/n0201 for the idea <3
|
||||
desc="Enable the spinning cover art background animation"
|
||||
checked={spinningArt}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
console.log(
|
||||
"Background Cover Spin:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
settings.spinningArt = checked;
|
||||
setspinningArt(checked);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<LunaNumberSetting
|
||||
title="Background Scale"
|
||||
desc="Adjust the scale of the background cover (1=10% - 50=500%, default: 15)"
|
||||
min={1}
|
||||
max={50}
|
||||
step={1}
|
||||
value={backgroundScale}
|
||||
onNumber={(value: number) => {
|
||||
settings.backgroundScale = value;
|
||||
setBackgroundScale(value);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<LunaNumberSetting
|
||||
title="Background Radius"
|
||||
desc="Adjust the cover art corner radius (0-100%, default: 25)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backgroundRadius}
|
||||
onNumber={(value: number) => {
|
||||
settings.backgroundRadius = value;
|
||||
setBackgroundRadius(value);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<LunaNumberSetting
|
||||
title="Background Contrast"
|
||||
desc="Adjust the contrast of the spinning background (0-200, default: 120)"
|
||||
min={0}
|
||||
max={200}
|
||||
step={1}
|
||||
value={backgroundContrast}
|
||||
onNumber={(value: number) => {
|
||||
settings.backgroundContrast = value;
|
||||
setBackgroundContrast(value);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<LunaNumberSetting
|
||||
title="Background Blur"
|
||||
desc="Adjust the blur amount of the spinning background (0-200, default: 80)"
|
||||
min={0}
|
||||
max={200}
|
||||
step={1}
|
||||
value={backgroundBlur}
|
||||
onNumber={(value: number) => {
|
||||
console.log("Background Blur:", value);
|
||||
settings.backgroundBlur = value;
|
||||
setBackgroundBlur(value);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<LunaNumberSetting
|
||||
title="Background Brightness"
|
||||
desc="Adjust the brightness of the spinning background (0-100, default: 40)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backgroundBrightness}
|
||||
onNumber={(value: number) => {
|
||||
console.log("Background Brightness:", value);
|
||||
settings.backgroundBrightness = value;
|
||||
setBackgroundBrightness(value);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && spinningArt && (
|
||||
<LunaNumberSetting
|
||||
title="Spin Speed"
|
||||
desc="Adjust the rotation speed in seconds (10-120, default: 45) - Lower values = Faster rotation"
|
||||
min={10}
|
||||
max={120}
|
||||
step={1}
|
||||
value={spinSpeed}
|
||||
onNumber={(value: number) => {
|
||||
console.log("Spin Speed:", value);
|
||||
settings.spinSpeed = value;
|
||||
setSpinSpeed(value);
|
||||
if (window.updateRadiantLyricsGlobalBackground) {
|
||||
window.updateRadiantLyricsGlobalBackground();
|
||||
}
|
||||
if (
|
||||
settings.settingsAffectNowPlaying &&
|
||||
window.updateRadiantLyricsNowPlayingBackground
|
||||
) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{CoverEverywhere && (
|
||||
<AnySwitch
|
||||
title="Settings Affect Now Playing"
|
||||
desc="Apply background settings to Now Playing view"
|
||||
checked={settingsAffectNowPlaying}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
console.log(
|
||||
"Settings Affect Now Playing:",
|
||||
checked ? "enabled" : "disabled",
|
||||
);
|
||||
settings.settingsAffectNowPlaying = checked;
|
||||
setSettingsAffectNowPlaying(checked);
|
||||
// Update Now Playing background immediately when setting changes
|
||||
if (window.updateRadiantLyricsNowPlayingBackground) {
|
||||
window.updateRadiantLyricsNowPlayingBackground();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<AnySwitch
|
||||
title="Playback Reactive"
|
||||
desc="Cover shader reacts to playback state by freezing/resuming"
|
||||
checked={backdropPlaybackReactive}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
settings.backdropPlaybackReactive = checked;
|
||||
setBackdropPlaybackReactive(checked);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<AnySwitch
|
||||
title="Cover Everywhere"
|
||||
desc="Apply the cover art backdrop to the entire app, not just the Now Playing view, Heavily Inspired by Cover-Theme by @Inrixia"
|
||||
checked={CoverEverywhere}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
settings.CoverEverywhere = checked;
|
||||
setCoverEverywhere(checked);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<AnySwitch
|
||||
title="Performance Mode | Experimental"
|
||||
desc="Caps the shader at 4 blur passes, disables dithering and renders at 1x pixel ratio to cut GPU load"
|
||||
checked={performanceMode}
|
||||
onChange={(_: unknown, checked: boolean) => {
|
||||
settings.performanceMode = checked;
|
||||
setPerformanceMode(checked);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Opacity"
|
||||
desc="How strongly the backdrop shows through (0-100% default = 75)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropOpacity}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropOpacity = value;
|
||||
setBackdropOpacity(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Warp Intensity"
|
||||
desc="Strength of the fluidity effect (0-100% default = 100)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropWarp}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropWarp = value;
|
||||
setBackdropWarp(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Blur Passes"
|
||||
desc="Kawase blur passes, higher is softer but costs more GPU (1-40 default = 5)"
|
||||
min={1}
|
||||
max={40}
|
||||
step={1}
|
||||
value={backdropBlurPasses}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropBlurPasses = value;
|
||||
setBackdropBlurPasses(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Animation Speed"
|
||||
desc="How fast the backdrop flows (0-500% default = 175)"
|
||||
min={0}
|
||||
max={500}
|
||||
step={5}
|
||||
value={backdropSpeed}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropSpeed = value;
|
||||
setBackdropSpeed(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Contrast"
|
||||
desc="Contrast of the backdrop (0-300%, 100 = stock default = 125)"
|
||||
min={0}
|
||||
max={300}
|
||||
step={5}
|
||||
value={backdropContrast}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropContrast = value;
|
||||
setBackdropContrast(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Auto Darken Bright Covers"
|
||||
desc="Prevents bright covers from making text unreadable (0-100% 0 = Off default = 80)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropDarken}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropDarken = value;
|
||||
setBackdropDarken(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Saturation"
|
||||
desc="Colour intensity of the backdrop (0-400% default = 125)"
|
||||
min={0}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropSaturation}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropSaturation = value;
|
||||
setBackdropSaturation(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Backdrop Scale"
|
||||
desc="Zoom level of the effect (10-400% default = 100)"
|
||||
min={10}
|
||||
max={400}
|
||||
step={5}
|
||||
value={backdropScale}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropScale = value;
|
||||
setBackdropScale(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
<LunaNumberSetting
|
||||
title="Dithering"
|
||||
desc="Breaks up color banding in smooth gradients (0-100 default = 15)"
|
||||
min={0}
|
||||
max={100}
|
||||
step={1}
|
||||
value={backdropDithering}
|
||||
onNumber={(value: number) => {
|
||||
settings.backdropDithering = value;
|
||||
setBackdropDithering(value);
|
||||
refreshBackdrop();
|
||||
}}
|
||||
/>
|
||||
|
||||
</LunaSettings>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -102,7 +102,6 @@ export interface WordLyricsResponse {
|
||||
type: "Word";
|
||||
data: WordLine[];
|
||||
metadata: {
|
||||
source: string;
|
||||
title: string;
|
||||
language: string;
|
||||
totalDuration: string;
|
||||
@@ -120,7 +119,6 @@ export interface LineLyricsResponse {
|
||||
type: "Line";
|
||||
data: ApiLine[];
|
||||
metadata: {
|
||||
source: string;
|
||||
title: string;
|
||||
language: string;
|
||||
totalDuration: string;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/* Kawarp backdrop canvas */
|
||||
|
||||
.rl-kawarp-canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
display: block;
|
||||
/* Z is important (breaks for some reason if changed) */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
transition: filter 800ms ease;
|
||||
}
|
||||
|
||||
/* engine animates on RAF loop*/
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.rl-kawarp-canvas {
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
// MARKER: Kawarp Backdrop
|
||||
// cover art URL -> fetch -> ImageBitmap -> kawarp -> animated backdrop <3
|
||||
|
||||
// Imported @ 1.2.0
|
||||
import { Kawarp, type KawarpOptions } from "@kawarp/core";
|
||||
import { settings } from "./Settings";
|
||||
|
||||
/** kawarp wants real units none if this fake luna slop (love you @inrixia <3) */
|
||||
const getLiveOptions = (): KawarpOptions => {
|
||||
const options: KawarpOptions = {
|
||||
warpIntensity: settings.backdropWarp / 100,
|
||||
blurPasses: settings.backdropBlurPasses,
|
||||
animationSpeed: settings.backdropSpeed / 100,
|
||||
saturation: settings.backdropSaturation / 100,
|
||||
dithering: settings.backdropDithering / 1000,
|
||||
scale: settings.backdropScale / 100,
|
||||
};
|
||||
if (!settings.performanceMode) return options;
|
||||
// Super WIP (probs also barely helps atleast rn)
|
||||
return {
|
||||
...options,
|
||||
blurPasses: Math.min(options.blurPasses ?? 8, 4),
|
||||
dithering: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const getDpr = (): number =>
|
||||
settings.performanceMode ? 1 : Math.min(window.devicePixelRatio || 1, 2);
|
||||
|
||||
// Claude did all the auto darken stuff cause i'm not bothered to make this stuff a second time.. (so ignore comment bloat)
|
||||
|
||||
// Auto-darken samples the album art at this size to judge how bright it is.
|
||||
// 16x16 is 256 pixels and the GPU does the downscale.
|
||||
const SAMPLE_SIZE = 16;
|
||||
// Never crush the backdrop to black, however blinding the cover art is
|
||||
const MIN_BRIGHTNESS = 0.15;
|
||||
// Darken strength maps onto a luminance ceiling: how bright the backdrop is
|
||||
// allowed to read before it gets pulled down. Strength 1 barely touches
|
||||
// anything, 100 keeps even white covers well below the lyrics.
|
||||
const CEILING_AT_MIN_STRENGTH = 0.9;
|
||||
const CEILING_AT_MAX_STRENGTH = 0.15;
|
||||
// Seconds to coast between full speed and a standstill. Linear, so the
|
||||
// deceleration is constant rather than dropping off a cliff and then crawling.
|
||||
const RAMP_SECONDS = 1.8;
|
||||
// kawarp's album-art crossfade defaults to 1000ms - keep rendering a little
|
||||
// past that so a track change still resolves while playback is paused
|
||||
const CROSSFADE_RENDER_MS = 1400;
|
||||
|
||||
/**
|
||||
* Mean Rec. 709 luma of an image, 0-1.
|
||||
*
|
||||
* Measured on the source art rather than the rendered canvas. The shader output
|
||||
* is in constant motion, so sampling it gives a brightness that drifts with the
|
||||
* animation; the cover art is fixed, so one reading per track is exact and
|
||||
* stays put. A blur preserves mean luminance, so this is also a faithful
|
||||
* predictor of how bright the finished backdrop lands.
|
||||
*/
|
||||
let sampleCtx: CanvasRenderingContext2D | null = null;
|
||||
const measureLuminance = (source: CanvasImageSource): number | null => {
|
||||
if (!sampleCtx) {
|
||||
const sampler = document.createElement("canvas");
|
||||
sampler.width = SAMPLE_SIZE;
|
||||
sampler.height = SAMPLE_SIZE;
|
||||
sampleCtx = sampler.getContext("2d", { willReadFrequently: true });
|
||||
}
|
||||
if (!sampleCtx) return null;
|
||||
try {
|
||||
sampleCtx.clearRect(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
sampleCtx.drawImage(source, 0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
const { data } = sampleCtx.getImageData(0, 0, SAMPLE_SIZE, SAMPLE_SIZE);
|
||||
let sum = 0;
|
||||
for (let i = 0; i < data.length; i += 4) {
|
||||
// Green dominates how bright a colour reads to the eye
|
||||
sum +=
|
||||
(0.2126 * data[i] + 0.7152 * data[i + 1] + 0.0722 * data[i + 2]) / 255;
|
||||
}
|
||||
return sum / (data.length / 4);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export class KawarpLayer {
|
||||
private host: HTMLElement;
|
||||
private canvas: HTMLCanvasElement;
|
||||
private kawarp: Kawarp | null = null;
|
||||
private resizeObserver: ResizeObserver | null = null;
|
||||
private currentSrc: string | null = null;
|
||||
private loadToken = 0;
|
||||
private appliedOptions = "";
|
||||
private running = false;
|
||||
private failed = false;
|
||||
// Luminance of the current cover art - measured once when it loads
|
||||
private coverLuminance: number | null = null;
|
||||
private brightness = 1;
|
||||
private playing = true;
|
||||
private rafId: number | null = null;
|
||||
private shaderTime = 0;
|
||||
private lastFrame = 0;
|
||||
// Current speed multiplier, eased between 0 and 1
|
||||
private speedFactor = 1;
|
||||
private transitionUntil = 0;
|
||||
|
||||
constructor(host: HTMLElement, id: string, zIndex: string) {
|
||||
this.host = host;
|
||||
this.canvas = document.createElement("canvas");
|
||||
// Same id shape BetterLyrics uses, so its own detection selector matches
|
||||
this.canvas.id = `better-lyrics-kawarp-${id}`;
|
||||
this.canvas.className = "rl-kawarp-canvas";
|
||||
this.canvas.style.zIndex = zIndex;
|
||||
host.appendChild(this.canvas);
|
||||
|
||||
this.canvas.addEventListener(
|
||||
"webglcontextlost",
|
||||
this.onContextLost as EventListener,
|
||||
false,
|
||||
);
|
||||
this.canvas.addEventListener(
|
||||
"webglcontextrestored",
|
||||
this.onContextRestored as EventListener,
|
||||
false,
|
||||
);
|
||||
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.syncSize();
|
||||
});
|
||||
this.resizeObserver.observe(host);
|
||||
}
|
||||
}
|
||||
|
||||
private onContextLost = (event: Event): void => {
|
||||
// Preventing default is what allows a restore to be delivered
|
||||
event.preventDefault();
|
||||
this.running = false;
|
||||
this.kawarp = null;
|
||||
this.appliedOptions = "";
|
||||
};
|
||||
|
||||
private onContextRestored = (): void => {
|
||||
this.kawarp = null;
|
||||
this.appliedOptions = "";
|
||||
this.failed = false;
|
||||
const src = this.currentSrc;
|
||||
this.currentSrc = null;
|
||||
this.apply(src);
|
||||
};
|
||||
|
||||
private ensureEngine(): Kawarp | null {
|
||||
if (this.kawarp || this.failed) return this.kawarp;
|
||||
try {
|
||||
this.syncSize();
|
||||
this.kawarp = new Kawarp(this.canvas, getLiveOptions());
|
||||
this.appliedOptions = JSON.stringify(getLiveOptions());
|
||||
} catch (_err) {
|
||||
// No WebGL/context refused
|
||||
this.failed = true;
|
||||
this.kawarp = null;
|
||||
}
|
||||
return this.kawarp;
|
||||
}
|
||||
|
||||
private syncSize(): void {
|
||||
const dpr = getDpr();
|
||||
const rect = this.host.getBoundingClientRect();
|
||||
const width = Math.max(1, Math.round(rect.width * dpr));
|
||||
const height = Math.max(1, Math.round(rect.height * dpr));
|
||||
if (this.canvas.width === width && this.canvas.height === height) return;
|
||||
this.canvas.width = width;
|
||||
this.canvas.height = height;
|
||||
this.kawarp?.resize();
|
||||
}
|
||||
|
||||
/** False once Tidal has rebuilt the container */
|
||||
isMountedIn(container: HTMLElement): boolean {
|
||||
return this.canvas.parentElement === container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this canvas is actually on screen.
|
||||
*/
|
||||
isVisible(): boolean {
|
||||
if (!this.canvas.isConnected) return false;
|
||||
if (typeof this.canvas.checkVisibility === "function") {
|
||||
return this.canvas.checkVisibility({ checkVisibilityCSS: true });
|
||||
}
|
||||
return getComputedStyle(this.canvas).visibility !== "hidden";
|
||||
}
|
||||
|
||||
/** Push current settings + cover art into engine. */
|
||||
apply(src: string | null): boolean {
|
||||
const engine = this.ensureEngine();
|
||||
if (!engine) return false;
|
||||
|
||||
const options = getLiveOptions();
|
||||
const serialized = JSON.stringify(options);
|
||||
if (serialized !== this.appliedOptions) {
|
||||
engine.setOptions(options);
|
||||
this.appliedOptions = serialized;
|
||||
}
|
||||
|
||||
const opacity = String(settings.backdropOpacity / 100);
|
||||
if (this.canvas.style.opacity !== opacity)
|
||||
this.canvas.style.opacity = opacity;
|
||||
|
||||
this.updateBrightness();
|
||||
this.applyFilter();
|
||||
|
||||
this.syncSize();
|
||||
|
||||
if (src && src !== this.currentSrc) {
|
||||
this.currentSrc = src;
|
||||
void this.loadCover(engine, src);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tidal's CDN sends no Access-Control-Allow-Origin (Drove me insane so using Fetch instead of kawarp)
|
||||
*/
|
||||
private async loadCover(engine: Kawarp, src: string): Promise<void> {
|
||||
const token = ++this.loadToken;
|
||||
try {
|
||||
const res = await fetch(src);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const blob = await res.blob();
|
||||
// A newer track won the race
|
||||
if (token !== this.loadToken || this.kawarp !== engine) return;
|
||||
// Decode once
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
if (token !== this.loadToken || this.kawarp !== engine) {
|
||||
bitmap.close();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.coverLuminance = measureLuminance(bitmap);
|
||||
engine.loadImageElement(bitmap);
|
||||
this.transitionUntil =
|
||||
performance.now() + CROSSFADE_RENDER_MS;
|
||||
this.ensureLoop();
|
||||
} finally {
|
||||
bitmap.close();
|
||||
}
|
||||
this.updateBrightness();
|
||||
} catch {
|
||||
try {
|
||||
if (token !== this.loadToken || this.kawarp !== engine) return;
|
||||
// Fallback for data:/blob: sources & i guess CORS
|
||||
this.coverLuminance = null;
|
||||
await engine.loadImage(src);
|
||||
this.updateBrightness();
|
||||
} catch {
|
||||
// Keep previous frame & later retry
|
||||
if (token === this.loadToken) this.currentSrc = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Contrast & auto-darken use canvas itself so for once no need for a million elements <3 */
|
||||
private applyFilter(): void {
|
||||
const parts: string[] = [];
|
||||
if (settings.backdropContrast !== 100) {
|
||||
// kawarp has no contrast uniform soo CSS time <3 (@aya would be proud)
|
||||
parts.push(`contrast(${settings.backdropContrast}%)`);
|
||||
}
|
||||
if (this.brightness < 0.999) {
|
||||
parts.push(`brightness(${this.brightness.toFixed(3)})`);
|
||||
}
|
||||
const filter = parts.join(" ");
|
||||
if (this.canvas.style.filter !== filter) this.canvas.style.filter = filter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decide the flat darkening for the current cover. (Luminance + Settings Value)
|
||||
*/
|
||||
private updateBrightness(): void {
|
||||
let brightness = 1;
|
||||
const strength = settings.backdropDarken;
|
||||
if (strength > 0 && this.coverLuminance !== null) {
|
||||
// container behind canvas is black so opacity scales brightness (cheap shortcut)
|
||||
const effective = this.coverLuminance * (settings.backdropOpacity / 100);
|
||||
const ceiling =
|
||||
CEILING_AT_MIN_STRENGTH -
|
||||
(strength / 100) * (CEILING_AT_MIN_STRENGTH - CEILING_AT_MAX_STRENGTH);
|
||||
if (effective > ceiling && effective > 0) {
|
||||
brightness = Math.max(ceiling / effective, MIN_BRIGHTNESS);
|
||||
}
|
||||
}
|
||||
if (brightness === this.brightness) return;
|
||||
this.brightness = brightness;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
/** Track play/pause (ramp render loop) */
|
||||
setPlaying(playing: boolean): void {
|
||||
if (this.playing === playing) return;
|
||||
this.playing = playing;
|
||||
this.ensureLoop();
|
||||
}
|
||||
|
||||
/** Where the speed multiplier is heading (1 = playing 0 = paused) */
|
||||
private get targetFactor(): number {
|
||||
if (!settings.backdropPlaybackReactive) return 1;
|
||||
return this.playing ? 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* render loop (replacing kawarp.start RIP)
|
||||
*/
|
||||
private tick = (): void => {
|
||||
this.rafId = null;
|
||||
if (!this.kawarp || !this.running) return;
|
||||
|
||||
const now = performance.now();
|
||||
// freeze so backgrounded tab doesn't jump shader
|
||||
const dt = Math.min((now - this.lastFrame) / 1000, 0.1);
|
||||
this.lastFrame = now;
|
||||
|
||||
const target = this.targetFactor;
|
||||
const step = dt / RAMP_SECONDS;
|
||||
if (this.speedFactor < target) {
|
||||
this.speedFactor = Math.min(target, this.speedFactor + step);
|
||||
} else if (this.speedFactor > target) {
|
||||
this.speedFactor = Math.max(target, this.speedFactor - step);
|
||||
}
|
||||
|
||||
this.shaderTime += dt * (settings.backdropSpeed / 100) * this.speedFactor;
|
||||
this.kawarp.renderFrame(this.shaderTime);
|
||||
|
||||
// Keep rendering while a track's crossfade is still resolving (NOT audio crossfade btw [it's the fading of cover arts])
|
||||
if (this.speedFactor === 0 && target === 0 && now >= this.transitionUntil) {
|
||||
return;
|
||||
}
|
||||
this.rafId = requestAnimationFrame(this.tick);
|
||||
};
|
||||
|
||||
private ensureLoop(): void {
|
||||
if (this.rafId !== null || !this.running || !this.kawarp) return;
|
||||
this.lastFrame = performance.now();
|
||||
this.rafId = requestAnimationFrame(this.tick);
|
||||
}
|
||||
|
||||
start(): void {
|
||||
if (this.running || !this.kawarp) return;
|
||||
this.running = true;
|
||||
this.ensureLoop();
|
||||
}
|
||||
|
||||
stop(): void {
|
||||
if (!this.running) return;
|
||||
this.running = false;
|
||||
if (this.rafId !== null) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
}
|
||||
|
||||
setPaused(paused: boolean): void {
|
||||
if (paused) this.stop();
|
||||
else this.start();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
this.resizeObserver?.disconnect();
|
||||
this.resizeObserver = null;
|
||||
this.canvas.removeEventListener(
|
||||
"webglcontextlost",
|
||||
this.onContextLost as EventListener,
|
||||
);
|
||||
this.canvas.removeEventListener(
|
||||
"webglcontextrestored",
|
||||
this.onContextRestored as EventListener,
|
||||
);
|
||||
this.kawarp?.dispose();
|
||||
this.kawarp = null;
|
||||
this.canvas.remove();
|
||||
this.currentSrc = null;
|
||||
this.appliedOptions = "";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
/* Global Spinning Background Styles - PERFORMANCE OPTIMIZED */
|
||||
/* Cover-art backdrop container styles
|
||||
*/
|
||||
|
||||
.global-background-container {
|
||||
position: fixed;
|
||||
@@ -9,37 +10,9 @@
|
||||
z-index: -3;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
/* Hardware acceleration */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
.global-spinning-black-bg {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* Dark base the shader composites over at its configured opacity */
|
||||
background: #000;
|
||||
z-index: -2;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.global-spinning-image {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 150vw;
|
||||
height: 150vh;
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
filter: blur(80px) brightness(0.4) contrast(1.2) saturate(1);
|
||||
opacity: 1;
|
||||
animation: spinGlobal 45s linear infinite;
|
||||
will-change: transform;
|
||||
/* Hardware acceleration */
|
||||
transform-origin: center center;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -55,7 +28,6 @@
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* Now Playing Background Container Optimization */
|
||||
.now-playing-background-container {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@@ -65,49 +37,18 @@
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
/* Hardware acceleration */
|
||||
background: #000;
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
||||
/* Ensure now-playing content renders above the dynamic background */
|
||||
/* now-playing renders above the backdrop */
|
||||
[data-test="new-now-playing"] > header,
|
||||
[data-test="new-now-playing"] > [class*="_content_"] {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/* Optimized keyframe animations with GPU acceleration */
|
||||
@keyframes spinGlobal {
|
||||
from {
|
||||
transform: translate(-50%, -50%) rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: translate(-50%, -50%) rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion for users who prefer it */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.global-spinning-image,
|
||||
.now-playing-background-image {
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
animation: none !important;
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
transform: translate(-50%, -50%) !important;
|
||||
/* biome-ignore lint: Accessibility override needs priority */
|
||||
will-change: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Performance mode: optimize effects but keep spinning */
|
||||
.performance-mode .global-spinning-image,
|
||||
.performance-mode .now-playing-background-image {
|
||||
/* Keep animations but optimize filter effects */
|
||||
/* biome-ignore lint: Intentional override of runtime styles */
|
||||
filter: blur(10px) brightness(0.4) contrast(1.1) !important;
|
||||
}
|
||||
|
||||
/* Make app chrome transparent for cover-everywhere background */
|
||||
body,
|
||||
#wimp,
|
||||
|
||||
@@ -19,14 +19,10 @@ import {
|
||||
flushLyrics as flushLyricsApi,
|
||||
romanizeLyrics as romanizeLyricsApi,
|
||||
} from "./api";
|
||||
import { KawarpLayer } from "./backdrop";
|
||||
import { Settings, settings } from "./Settings";
|
||||
|
||||
// Interpret integer backgroundScale (e.g., 10=1.0x, 20=2.0x)
|
||||
const getScaledMultiplier = (): number => {
|
||||
const value = settings.backgroundScale;
|
||||
return value / 10;
|
||||
};
|
||||
|
||||
import backdropStylesCss from "file://backdrop-styles.css?minify";
|
||||
import coverEverywhereCss from "file://cover-everywhere.css?minify";
|
||||
import floatingPlayerBarCss from "file://floating-player-bar.css?minify";
|
||||
import lyricsGlow from "file://lyrics-glow.css?minify";
|
||||
@@ -429,8 +425,8 @@ observe<HTMLElement>(unloads, '[data-test="footer-player"]', () => {
|
||||
applyIntegratedSeekBar();
|
||||
});
|
||||
|
||||
// Apply styles.css
|
||||
baseStyleTag.css = baseStyles;
|
||||
// Apply styles.css + backdrop keyframes (Now Playing needs these even with Cover Everywhere off)
|
||||
baseStyleTag.css = `${baseStyles}\n${backdropStylesCss}`;
|
||||
|
||||
// Lyrics glow vars & lyrics-glow.css (when enabled)
|
||||
const updateRadiantLyricsTextGlow = function (): void {
|
||||
@@ -681,48 +677,174 @@ const createHideUIButton = function (): void {
|
||||
buttonContainer.insertBefore(hideUIButton, closeButton);
|
||||
};
|
||||
|
||||
// MARKER: Background Rendering
|
||||
// Variable setup
|
||||
// MARKER: Background Rendering (Kawarp Shader)
|
||||
// cover art URL -> kawarp -> Animated Backdrop <3
|
||||
let globalSpinningBgStyleTag: StyleTag | null = null;
|
||||
let globalBackgroundContainer: HTMLElement | null = null;
|
||||
let globalBackgroundImage: HTMLImageElement | null = null;
|
||||
let globalBlackBg: HTMLElement | null = null;
|
||||
let globalGradientOverlay: HTMLElement | null = null;
|
||||
let currentGlobalCoverSrc: string | null = null;
|
||||
let lastUpdateTime = 0;
|
||||
const getUpdateThrottle = () => (settings.performanceMode ? 1500 : 500);
|
||||
|
||||
// Now Playing background caching
|
||||
let nowPlayingBackgroundContainer: HTMLElement | null = null;
|
||||
let nowPlayingBackgroundImage: HTMLImageElement | null = null;
|
||||
let nowPlayingBlackBg: HTMLElement | null = null;
|
||||
let nowPlayingGradientOverlay: HTMLElement | null = null;
|
||||
let spinAnimationAdded = false;
|
||||
let currentCoverSrc: string | null = null;
|
||||
|
||||
// apply scaled pixel sizes to cover art
|
||||
const applyScaledPixelSize = (img: HTMLImageElement | null): void => {
|
||||
if (!img) return;
|
||||
const scale = getScaledMultiplier();
|
||||
const apply = () => {
|
||||
const w = img.naturalWidth;
|
||||
const h = img.naturalHeight;
|
||||
if (w > 0 && h > 0) {
|
||||
const wPx = Math.round(w * scale);
|
||||
const hPx = Math.round(h * scale);
|
||||
const wStr = `${wPx}px`;
|
||||
const hStr = `${hPx}px`;
|
||||
if (img.style.width !== wStr) img.style.width = wStr;
|
||||
if (img.style.height !== hStr) img.style.height = hStr;
|
||||
}
|
||||
};
|
||||
if (img.complete && img.naturalWidth > 0) {
|
||||
apply();
|
||||
} else {
|
||||
img.addEventListener("load", apply, { once: true });
|
||||
}
|
||||
const kawarpLayers: {
|
||||
global: KawarpLayer | null;
|
||||
nowPlaying: KawarpLayer | null;
|
||||
} = { global: null, nowPlaying: null };
|
||||
|
||||
const disposeKawarpLayer = (slot: "global" | "nowPlaying"): void => {
|
||||
kawarpLayers[slot]?.dispose();
|
||||
kawarpLayers[slot] = null;
|
||||
};
|
||||
|
||||
// Update Cover Art background for Now Playing and Global
|
||||
/**
|
||||
* Only ever run one shader chain
|
||||
*/
|
||||
// Now Playing view slides = ~1 second (stops frozen backdrop on reveal) [Super WIP]
|
||||
const PAUSE_GRACE_MS = 1500;
|
||||
const pauseHiddenSince: { global: number; nowPlaying: number } = {
|
||||
global: 0,
|
||||
nowPlaying: 0,
|
||||
};
|
||||
|
||||
const setLayerRunning = (
|
||||
slot: "global" | "nowPlaying",
|
||||
layer: KawarpLayer | null,
|
||||
shouldRun: boolean,
|
||||
now: number,
|
||||
): void => {
|
||||
if (!layer) return;
|
||||
if (shouldRun) {
|
||||
pauseHiddenSince[slot] = 0;
|
||||
layer.setPaused(false);
|
||||
return;
|
||||
}
|
||||
// window hidden [Untested dk why it wouldn't work tho]
|
||||
if (document.hidden) {
|
||||
pauseHiddenSince[slot] = 0;
|
||||
layer.setPaused(true);
|
||||
return;
|
||||
}
|
||||
if (pauseHiddenSince[slot] === 0) pauseHiddenSince[slot] = now;
|
||||
if (now - pauseHiddenSince[slot] >= PAUSE_GRACE_MS) layer.setPaused(true);
|
||||
};
|
||||
|
||||
const syncBackdropActivity = (): void => {
|
||||
const nowPlaying = kawarpLayers.nowPlaying;
|
||||
const global = kawarpLayers.global;
|
||||
const nowPlayingVisible = nowPlaying?.isVisible() ?? false;
|
||||
const now = Date.now();
|
||||
|
||||
// Ramp shader down when playback pauses (maybe not the best way but it does work sooo)
|
||||
const playing = settings.backdropPlaybackReactive
|
||||
? reduxPlaybackIsPlaying()
|
||||
: true;
|
||||
nowPlaying?.setPlaying(playing);
|
||||
global?.setPlaying(playing);
|
||||
|
||||
setLayerRunning(
|
||||
"nowPlaying",
|
||||
nowPlaying,
|
||||
!document.hidden && nowPlayingVisible && playing,
|
||||
now,
|
||||
);
|
||||
// keep animating underneath for whole slide (stops freeze before hidden) [Super WIP]
|
||||
setLayerRunning(
|
||||
"global",
|
||||
global,
|
||||
!document.hidden &&
|
||||
!nowPlayingVisible &&
|
||||
playing &&
|
||||
(global?.isVisible() ?? false),
|
||||
now,
|
||||
);
|
||||
};
|
||||
|
||||
/** cover art @ resolution worth sampling */
|
||||
const getCoverArtSrc = (): string | null => {
|
||||
const targetRes = settings.performanceMode ? "640x640" : "1280x1280";
|
||||
const img = document.querySelector(
|
||||
'[data-test="current-media-imagery"] img',
|
||||
) as HTMLImageElement | null;
|
||||
if (img?.src) return img.src.replace(/\d+x\d+/, targetRes);
|
||||
|
||||
const video = document.querySelector(
|
||||
'[data-test="current-media-imagery"] video',
|
||||
) as HTMLVideoElement | null;
|
||||
const poster = video?.getAttribute("poster");
|
||||
return poster ? poster.replace(/\d+x\d+/, targetRes) : null;
|
||||
};
|
||||
|
||||
/** Mount/remount kawarp canvas */
|
||||
const ensureLayer = (
|
||||
slot: "global" | "nowPlaying",
|
||||
container: HTMLElement,
|
||||
zIndex: string,
|
||||
): KawarpLayer | null => {
|
||||
let layer = kawarpLayers[slot];
|
||||
if (layer && !layer.isMountedIn(container)) {
|
||||
// Tidal rebuilt the container
|
||||
disposeKawarpLayer(slot);
|
||||
layer = null;
|
||||
}
|
||||
if (!layer) {
|
||||
layer = new KawarpLayer(container, slot, zIndex);
|
||||
kawarpLayers[slot] = layer;
|
||||
}
|
||||
return layer;
|
||||
};
|
||||
|
||||
// Apply backdrop across whole app (cover everywhere)
|
||||
const applyGlobalBackdrop = (coverArtImageSrc: string): void => {
|
||||
if (!settings.CoverEverywhere) {
|
||||
cleanUpGlobalBackground();
|
||||
return;
|
||||
}
|
||||
|
||||
const appContainer = document.querySelector(
|
||||
'[data-test="main"]',
|
||||
) as HTMLElement | null;
|
||||
if (!appContainer) return;
|
||||
|
||||
if (!globalSpinningBgStyleTag) {
|
||||
globalSpinningBgStyleTag = new StyleTag(
|
||||
"RadiantLyrics-global-backdrop",
|
||||
unloads,
|
||||
coverEverywhereCss,
|
||||
);
|
||||
}
|
||||
|
||||
if (!globalBackgroundContainer?.isConnected) {
|
||||
globalBackgroundContainer = document.createElement("div");
|
||||
globalBackgroundContainer.className = "global-background-container";
|
||||
appContainer.appendChild(globalBackgroundContainer);
|
||||
}
|
||||
|
||||
ensureLayer("global", globalBackgroundContainer, "-1")?.apply(
|
||||
coverArtImageSrc,
|
||||
);
|
||||
};
|
||||
|
||||
// Apply backdrop in Now Playing
|
||||
const applyNowPlayingBackdrop = (coverArtImageSrc: string): void => {
|
||||
const nowPlayingContainerElement = document.querySelector(
|
||||
'[class*="_nowPlayingContainer"]',
|
||||
) as HTMLElement | null;
|
||||
if (!nowPlayingContainerElement) return;
|
||||
|
||||
if (
|
||||
!nowPlayingBackgroundContainer?.isConnected ||
|
||||
!nowPlayingContainerElement.contains(nowPlayingBackgroundContainer)
|
||||
) {
|
||||
nowPlayingBackgroundContainer = document.createElement("div");
|
||||
nowPlayingBackgroundContainer.className =
|
||||
"now-playing-background-container";
|
||||
nowPlayingContainerElement.appendChild(nowPlayingBackgroundContainer);
|
||||
}
|
||||
|
||||
ensureLayer("nowPlaying", nowPlayingBackgroundContainer, "0")?.apply(
|
||||
coverArtImageSrc,
|
||||
);
|
||||
};
|
||||
|
||||
// Feed cover art into both backdrops
|
||||
function updateCoverArtBackground(method: number = 0): void {
|
||||
if (method === 1) {
|
||||
safeTimeout(
|
||||
@@ -735,315 +857,30 @@ function updateCoverArtBackground(method: number = 0): void {
|
||||
return;
|
||||
}
|
||||
|
||||
const coverArtImageElement = document.querySelector(
|
||||
'[data-test="current-media-imagery"] img',
|
||||
) as HTMLImageElement;
|
||||
let coverArtImageSrc: string | null = null;
|
||||
|
||||
if (coverArtImageElement) {
|
||||
coverArtImageSrc = coverArtImageElement.src;
|
||||
// Use higher resolution for better quality, but consider performance mode
|
||||
const targetRes = settings.performanceMode ? "640x640" : "1280x1280";
|
||||
coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes);
|
||||
if (coverArtImageElement.src !== coverArtImageSrc) {
|
||||
coverArtImageElement.src = coverArtImageSrc;
|
||||
}
|
||||
} else {
|
||||
const videoElement = document.querySelector(
|
||||
'[data-test="current-media-imagery"] video',
|
||||
) as HTMLVideoElement;
|
||||
if (videoElement) {
|
||||
coverArtImageSrc = videoElement.getAttribute("poster");
|
||||
if (coverArtImageSrc) {
|
||||
const targetRes = settings.performanceMode ? "640x640" : "1280x1280";
|
||||
coverArtImageSrc = coverArtImageSrc.replace(/\d+x\d+/, targetRes);
|
||||
}
|
||||
} else {
|
||||
cleanUpDynamicArt();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Update backgrounds when we have a valid cover art source
|
||||
if (coverArtImageSrc) {
|
||||
// Apply global spinning background if enabled
|
||||
if (settings.CoverEverywhere) {
|
||||
applyGlobalSpinningBackground(coverArtImageSrc);
|
||||
}
|
||||
|
||||
// Apply spinning CoverArt background to the Now Playing container - OPTIMIZED
|
||||
const nowPlayingContainerElement = document.querySelector(
|
||||
'[class*="_nowPlayingContainer"]',
|
||||
) as HTMLElement;
|
||||
if (nowPlayingContainerElement) {
|
||||
// Create DOM structure if it doesn't exist (REUSE ELEMENTS)
|
||||
if (
|
||||
!nowPlayingBackgroundContainer ||
|
||||
!nowPlayingContainerElement.contains(nowPlayingBackgroundContainer)
|
||||
) {
|
||||
// Clean up any old elements first
|
||||
nowPlayingContainerElement
|
||||
.querySelectorAll(
|
||||
".now-playing-background-image, .now-playing-black-bg, .now-playing-gradient-overlay",
|
||||
)
|
||||
.forEach((el) => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
// Create container
|
||||
nowPlayingBackgroundContainer = document.createElement("div");
|
||||
nowPlayingBackgroundContainer.className =
|
||||
"now-playing-background-container";
|
||||
nowPlayingBackgroundContainer.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
`;
|
||||
nowPlayingContainerElement.appendChild(nowPlayingBackgroundContainer);
|
||||
|
||||
// Create black background layer
|
||||
nowPlayingBlackBg = document.createElement("div");
|
||||
nowPlayingBlackBg.className = "now-playing-black-bg";
|
||||
nowPlayingBlackBg.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #000;
|
||||
z-index: 0;
|
||||
`;
|
||||
nowPlayingBackgroundContainer.appendChild(nowPlayingBlackBg);
|
||||
|
||||
// Create image element
|
||||
nowPlayingBackgroundImage = document.createElement("img");
|
||||
nowPlayingBackgroundImage.className = "now-playing-background-image";
|
||||
nowPlayingBackgroundImage.style.cssText = `
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
object-fit: cover;
|
||||
z-index: 1;
|
||||
will-change: transform;
|
||||
transform-origin: center center;
|
||||
`;
|
||||
nowPlayingBackgroundContainer.appendChild(nowPlayingBackgroundImage);
|
||||
|
||||
// Create gradient overlay
|
||||
nowPlayingGradientOverlay = document.createElement("div");
|
||||
nowPlayingGradientOverlay.className = "now-playing-gradient-overlay";
|
||||
nowPlayingGradientOverlay.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, transparent 0%, rgba(0, 0, 0, 0.3) 60%, rgba(0, 0, 0, 0.8) 90%);
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
`;
|
||||
nowPlayingBackgroundContainer.appendChild(nowPlayingGradientOverlay);
|
||||
}
|
||||
|
||||
// Update image source efficiently
|
||||
if (
|
||||
nowPlayingBackgroundImage &&
|
||||
nowPlayingBackgroundImage.src !== coverArtImageSrc
|
||||
) {
|
||||
nowPlayingBackgroundImage.src = coverArtImageSrc;
|
||||
}
|
||||
|
||||
// Apply pixel-based size using intrinsic dimensions
|
||||
applyScaledPixelSize(nowPlayingBackgroundImage);
|
||||
|
||||
if (nowPlayingBackgroundImage) {
|
||||
const blur = settings.performanceMode
|
||||
? Math.min(settings.backgroundBlur, 20)
|
||||
: settings.backgroundBlur;
|
||||
const contrast = settings.performanceMode
|
||||
? Math.min(settings.backgroundContrast, 150)
|
||||
: settings.backgroundContrast;
|
||||
const radius = `${settings.backgroundRadius}%`;
|
||||
if (nowPlayingBackgroundImage.style.borderRadius !== radius)
|
||||
nowPlayingBackgroundImage.style.borderRadius = radius;
|
||||
const filt = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`;
|
||||
if (nowPlayingBackgroundImage.style.filter !== filt)
|
||||
nowPlayingBackgroundImage.style.filter = filt;
|
||||
const anim = settings.spinningArt
|
||||
? `spin ${settings.spinSpeed}s linear infinite`
|
||||
: "none";
|
||||
const wc = settings.spinningArt ? "transform" : "auto";
|
||||
if (nowPlayingBackgroundImage.style.animation !== anim)
|
||||
nowPlayingBackgroundImage.style.animation = anim;
|
||||
if (nowPlayingBackgroundImage.style.willChange !== wc)
|
||||
nowPlayingBackgroundImage.style.willChange = wc;
|
||||
}
|
||||
|
||||
// Add keyframe animation only once
|
||||
if (!spinAnimationAdded) {
|
||||
const styleSheet = document.createElement("style");
|
||||
styleSheet.id = "spinAnimation";
|
||||
styleSheet.textContent = `
|
||||
@keyframes spin {
|
||||
from { transform: translate(-50%, -50%) rotate(0deg); }
|
||||
to { transform: translate(-50%, -50%) rotate(360deg); }
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(styleSheet);
|
||||
spinAnimationAdded = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to apply spinning background to the entire app (cover everywhere)
|
||||
const applyGlobalSpinningBackground = (coverArtImageSrc: string): void => {
|
||||
const appContainer = document.querySelector(
|
||||
'[data-test="main"]',
|
||||
) as HTMLElement;
|
||||
|
||||
if (!settings.CoverEverywhere) {
|
||||
cleanUpGlobalSpinningBackground();
|
||||
// Teardown <3
|
||||
if (!settings.backdropEnabled) {
|
||||
cleanUpDynamicArt();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only throttle image src updates; style updates below always run for responsiveness
|
||||
const now = Date.now();
|
||||
const shouldUpdateImageSrc =
|
||||
now - lastUpdateTime >= getUpdateThrottle() ||
|
||||
currentGlobalCoverSrc !== coverArtImageSrc;
|
||||
if (shouldUpdateImageSrc) {
|
||||
lastUpdateTime = now;
|
||||
currentGlobalCoverSrc = coverArtImageSrc;
|
||||
const coverArtImageSrc = getCoverArtSrc();
|
||||
if (!coverArtImageSrc) {
|
||||
cleanUpDynamicArt();
|
||||
return;
|
||||
}
|
||||
currentCoverSrc = coverArtImageSrc;
|
||||
|
||||
// Add StyleTag if not present
|
||||
if (!globalSpinningBgStyleTag) {
|
||||
globalSpinningBgStyleTag = new StyleTag(
|
||||
"RadiantLyrics-global-spinning-bg",
|
||||
unloads,
|
||||
coverEverywhereCss,
|
||||
);
|
||||
}
|
||||
applyGlobalBackdrop(coverArtImageSrc);
|
||||
applyNowPlayingBackdrop(coverArtImageSrc);
|
||||
syncBackdropActivity();
|
||||
}
|
||||
|
||||
if (!appContainer) return;
|
||||
const cleanUpGlobalBackground = function (): void {
|
||||
// Release WebGL context before canvas is orphaned (otherwise death occurs)
|
||||
disposeKawarpLayer("global");
|
||||
|
||||
// Create container structure if it doesn't exist (REUSE DOM ELEMENTS)
|
||||
if (!globalBackgroundContainer) {
|
||||
globalBackgroundContainer = document.createElement("div");
|
||||
globalBackgroundContainer.className = "global-background-container";
|
||||
globalBackgroundContainer.style.cssText = `
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
z-index: -3;
|
||||
pointer-events: none;
|
||||
overflow: hidden;
|
||||
`;
|
||||
appContainer.appendChild(globalBackgroundContainer);
|
||||
|
||||
// Create black background layer
|
||||
globalBlackBg = document.createElement("div");
|
||||
globalBlackBg.className = "global-spinning-black-bg";
|
||||
globalBackgroundContainer.appendChild(globalBlackBg);
|
||||
|
||||
// Create image element
|
||||
globalBackgroundImage = document.createElement("img");
|
||||
globalBackgroundImage.className = "global-spinning-image";
|
||||
globalBackgroundImage.style.cssText = `
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
object-fit: cover;
|
||||
z-index: -1;
|
||||
will-change: transform;
|
||||
transform-origin: center center;
|
||||
`;
|
||||
globalBackgroundContainer.appendChild(globalBackgroundImage);
|
||||
|
||||
// Create gradient overlay
|
||||
globalGradientOverlay = document.createElement("div");
|
||||
globalGradientOverlay.className = "global-spinning-gradient-overlay";
|
||||
globalGradientOverlay.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, transparent 0%, rgba(0, 0, 0, 0.3) 60%, rgba(0, 0, 0, 0.8) 90%);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
`;
|
||||
globalBackgroundContainer.appendChild(globalGradientOverlay);
|
||||
}
|
||||
|
||||
// Ensure gradient overlay exists even if container was pre-existing
|
||||
if (!globalGradientOverlay && globalBackgroundContainer) {
|
||||
globalGradientOverlay = document.createElement("div");
|
||||
globalGradientOverlay.className = "global-spinning-gradient-overlay";
|
||||
globalGradientOverlay.style.cssText = `
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: radial-gradient(circle at center, transparent 0%, rgba(0, 0, 0, 0.3) 60%, rgba(0, 0, 0, 0.8) 90%);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
`;
|
||||
globalBackgroundContainer.appendChild(globalGradientOverlay);
|
||||
}
|
||||
|
||||
// Update image source efficiently (throttled)
|
||||
if (
|
||||
shouldUpdateImageSrc &&
|
||||
globalBackgroundImage &&
|
||||
globalBackgroundImage.src !== coverArtImageSrc
|
||||
) {
|
||||
globalBackgroundImage.src = coverArtImageSrc;
|
||||
}
|
||||
|
||||
if (globalBackgroundImage) {
|
||||
applyScaledPixelSize(globalBackgroundImage);
|
||||
const blur = settings.performanceMode
|
||||
? Math.min(settings.backgroundBlur, 20)
|
||||
: settings.backgroundBlur;
|
||||
const contrast = settings.performanceMode
|
||||
? Math.min(settings.backgroundContrast, 150)
|
||||
: settings.backgroundContrast;
|
||||
const radius = `${settings.backgroundRadius}%`;
|
||||
globalBackgroundImage.style.filter = `blur(${blur}px) brightness(${settings.backgroundBrightness / 100}) contrast(${contrast}%)`;
|
||||
if (globalBackgroundImage.style.borderRadius !== radius)
|
||||
globalBackgroundImage.style.borderRadius = radius;
|
||||
if (settings.spinningArt) {
|
||||
globalBackgroundImage.style.animation = `spinGlobal ${settings.spinSpeed}s linear infinite`;
|
||||
globalBackgroundImage.style.willChange = "transform";
|
||||
} else {
|
||||
globalBackgroundImage.style.animation = "none";
|
||||
globalBackgroundImage.style.willChange = "auto";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// cleanup function
|
||||
const cleanUpGlobalSpinningBackground = function (): void {
|
||||
if (globalBackgroundContainer && globalBackgroundContainer.parentNode) {
|
||||
globalBackgroundContainer.parentNode.removeChild(globalBackgroundContainer);
|
||||
}
|
||||
globalBackgroundContainer?.remove();
|
||||
globalBackgroundContainer = null;
|
||||
globalBackgroundImage = null;
|
||||
globalBlackBg = null;
|
||||
globalGradientOverlay = null;
|
||||
currentGlobalCoverSrc = null;
|
||||
|
||||
if (globalSpinningBgStyleTag) {
|
||||
globalSpinningBgStyleTag.remove();
|
||||
@@ -1051,138 +888,69 @@ const cleanUpGlobalSpinningBackground = function (): void {
|
||||
}
|
||||
};
|
||||
|
||||
// Function to update global background when settings change
|
||||
const updateRadiantLyricsGlobalBackground = function (): void {
|
||||
// Apply performance mode class to document body
|
||||
const cleanUpDynamicArt = function (): void {
|
||||
disposeKawarpLayer("nowPlaying");
|
||||
nowPlayingBackgroundContainer?.remove();
|
||||
nowPlayingBackgroundContainer = null;
|
||||
currentCoverSrc = null;
|
||||
|
||||
// more more teardown <3
|
||||
document
|
||||
.querySelectorAll(".now-playing-background-container")
|
||||
.forEach((el) => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
cleanUpGlobalBackground();
|
||||
};
|
||||
|
||||
// Re-render both backdrops when settings change
|
||||
const updateRadiantLyricsBackdrop = function (): void {
|
||||
if (settings.performanceMode) {
|
||||
document.body.classList.add("performance-mode");
|
||||
} else {
|
||||
document.body.classList.remove("performance-mode");
|
||||
}
|
||||
|
||||
if (settings.CoverEverywhere) {
|
||||
// Get current cover art and apply global background
|
||||
updateCoverArtBackground();
|
||||
} else {
|
||||
cleanUpGlobalSpinningBackground();
|
||||
if (!settings.backdropEnabled) {
|
||||
cleanUpDynamicArt();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to update Now Playing background when settings change
|
||||
const updateRadiantLyricsNowPlayingBackground = function (): void {
|
||||
const nowPlayingBackgroundImages = document.querySelectorAll(
|
||||
".now-playing-background-image",
|
||||
);
|
||||
nowPlayingBackgroundImages.forEach((img: Element) => {
|
||||
const imgElement = img as HTMLImageElement;
|
||||
if (!settings.CoverEverywhere) cleanUpGlobalBackground();
|
||||
|
||||
// Default values when settings don't affect Now Playing
|
||||
const defaultBlur = 80;
|
||||
const defaultBrightness = 40;
|
||||
const defaultContrast = 120;
|
||||
const defaultSpinSpeed = 45;
|
||||
|
||||
let blur: number, brightness: number, contrast: number, spinSpeed: number;
|
||||
|
||||
if (settings.settingsAffectNowPlaying) {
|
||||
blur = settings.backgroundBlur;
|
||||
brightness = settings.backgroundBrightness;
|
||||
contrast = settings.backgroundContrast;
|
||||
spinSpeed = settings.spinSpeed;
|
||||
} else {
|
||||
blur = defaultBlur;
|
||||
brightness = defaultBrightness;
|
||||
contrast = defaultContrast;
|
||||
spinSpeed = defaultSpinSpeed;
|
||||
}
|
||||
|
||||
// Apply pixel-based size using intrinsic dimensions and current scale
|
||||
applyScaledPixelSize(imgElement);
|
||||
const radius = `${settings.backgroundRadius}%`;
|
||||
if (imgElement.style.borderRadius !== radius)
|
||||
imgElement.style.borderRadius = radius;
|
||||
|
||||
if (settings.performanceMode) {
|
||||
blur = Math.min(blur, 20);
|
||||
contrast = Math.min(contrast, 150);
|
||||
}
|
||||
if (settings.spinningArt) {
|
||||
imgElement.style.animation = `spin ${spinSpeed}s linear infinite`;
|
||||
imgElement.style.willChange = "transform";
|
||||
} else {
|
||||
imgElement.style.animation = "none";
|
||||
imgElement.style.willChange = "auto";
|
||||
}
|
||||
imgElement.style.filter = `blur(${blur}px) brightness(${brightness / 100}) contrast(${contrast}%)`;
|
||||
});
|
||||
if (currentCoverSrc) {
|
||||
if (settings.CoverEverywhere) applyGlobalBackdrop(currentCoverSrc);
|
||||
applyNowPlayingBackdrop(currentCoverSrc);
|
||||
syncBackdropActivity();
|
||||
} else {
|
||||
updateCoverArtBackground();
|
||||
}
|
||||
};
|
||||
|
||||
// Make these functions available globally so Settings can call them
|
||||
(window as any).updateRadiantLyricsStyles = updateRadiantLyricsStyles;
|
||||
(window as any).updateRadiantLyricsGlobalBackground =
|
||||
updateRadiantLyricsGlobalBackground;
|
||||
(window as any).updateRadiantLyricsNowPlayingBackground =
|
||||
updateRadiantLyricsNowPlayingBackground;
|
||||
(window as any).updateRadiantLyricsBackdrop = updateRadiantLyricsBackdrop;
|
||||
(window as any).updateRadiantLyricsTextGlow = updateRadiantLyricsTextGlow;
|
||||
(window as any).updateRadiantLyricsPlayerBarTint =
|
||||
updateRadiantLyricsPlayerBarTint;
|
||||
(window as any).updateQualityProgressColor = applyQualityProgressColor;
|
||||
(window as any).updateIntegratedSeekBar = applyIntegratedSeekBar;
|
||||
|
||||
const cleanUpDynamicArt = function (): void {
|
||||
// Clean up cached Now Playing elements
|
||||
if (
|
||||
nowPlayingBackgroundContainer &&
|
||||
nowPlayingBackgroundContainer.parentNode
|
||||
) {
|
||||
nowPlayingBackgroundContainer.parentNode.removeChild(
|
||||
nowPlayingBackgroundContainer,
|
||||
);
|
||||
// halts the RAF loop (drives vissibility stuff)
|
||||
document.addEventListener("visibilitychange", syncBackdropActivity);
|
||||
|
||||
// Tidal toggles the Now Playing view with visibility (dk why they don't unmount but oki)
|
||||
safeInterval(unloads, syncBackdropActivity, 200);
|
||||
|
||||
// React the instant the slide starts/ends
|
||||
observe<HTMLElement>(unloads, '[class*="_nowPlayingContainer"]', (container) => {
|
||||
for (const event of ["transitionrun", "transitionstart", "transitionend"]) {
|
||||
container.addEventListener(event, syncBackdropActivity);
|
||||
}
|
||||
nowPlayingBackgroundContainer = null;
|
||||
nowPlayingBackgroundImage = null;
|
||||
nowPlayingBlackBg = null;
|
||||
nowPlayingGradientOverlay = null;
|
||||
|
||||
// Clean up any remaining elements (fallback)
|
||||
const nowPlayingBackgroundImages = document.getElementsByClassName(
|
||||
"now-playing-background-image",
|
||||
);
|
||||
Array.from(nowPlayingBackgroundImages).forEach((element) => {
|
||||
element.remove();
|
||||
});
|
||||
|
||||
// Clean up spinning background
|
||||
cleanUpGlobalSpinningBackground();
|
||||
};
|
||||
|
||||
// I may or may not have forgotten what this does..
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
const isHiddenDoc = document.hidden;
|
||||
const images = document.querySelectorAll(
|
||||
".global-spinning-image, .now-playing-background-image",
|
||||
);
|
||||
images.forEach((img) => {
|
||||
const el = img as HTMLElement;
|
||||
if (isHiddenDoc) {
|
||||
// Pause animation but keep state
|
||||
if (el.style.animationPlayState !== "paused")
|
||||
el.style.animationPlayState = "paused";
|
||||
if (el.style.willChange !== "auto") el.style.willChange = "auto";
|
||||
} else {
|
||||
if (el.style.animationPlayState !== "running")
|
||||
el.style.animationPlayState = "running";
|
||||
if (
|
||||
el.classList.contains("global-spinning-image") ||
|
||||
el.classList.contains("now-playing-background-image")
|
||||
) {
|
||||
if (el.style.willChange !== "transform")
|
||||
el.style.willChange = "transform";
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// Init performance mode
|
||||
if (settings.performanceMode) {
|
||||
document.body.classList.add("performance-mode");
|
||||
@@ -1240,14 +1008,8 @@ unloads.add(() => {
|
||||
el.remove();
|
||||
});
|
||||
|
||||
// Clean up spin animations
|
||||
const spinAnimationStyle = document.querySelector("#spinAnimation");
|
||||
if (spinAnimationStyle && spinAnimationStyle.parentNode) {
|
||||
spinAnimationStyle.parentNode.removeChild(spinAnimationStyle);
|
||||
}
|
||||
|
||||
// Clean up spinning background
|
||||
cleanUpGlobalSpinningBackground();
|
||||
// even more more teardown <3
|
||||
cleanUpDynamicArt();
|
||||
});
|
||||
|
||||
// MARKER: Sticky Lyrics Feature
|
||||
@@ -1917,7 +1679,10 @@ const registerSyntheticNativeLyrics = (
|
||||
lyricsId: `radiant-lyrics-${trackInfo.trackId}`,
|
||||
text: buildSyntheticLyricsText(response),
|
||||
lrcText: buildSyntheticLrcText(response),
|
||||
providerName: `Radiant Lyrics (${response.metadata.source})`,
|
||||
providerName:
|
||||
response.type === "Word" && response._synthesized
|
||||
? "Radiant Lyrics (AI)"
|
||||
: "Radiant Lyrics",
|
||||
direction: "LEFT_TO_RIGHT",
|
||||
response,
|
||||
};
|
||||
@@ -2470,10 +2235,7 @@ const fetchLyrics = async (
|
||||
const derived: LineLyricsResponse = {
|
||||
type: "Line",
|
||||
data: cachedLyricsData.lines,
|
||||
metadata: {
|
||||
...cachedLyricsData.metadata,
|
||||
source: cachedLyricsData.metadata.source.replace(/ • AI$/, ""),
|
||||
},
|
||||
metadata: cachedLyricsData.metadata,
|
||||
_cached: cachedLyricsData._cached,
|
||||
};
|
||||
cachedLyricsKey = cacheKey;
|
||||
@@ -4188,9 +3950,7 @@ const onTrackChange = async (): Promise<void> => {
|
||||
);
|
||||
}
|
||||
|
||||
sylTrace(
|
||||
`RL API: loaded ${response.data.length} lines (source: ${response.metadata.source})`,
|
||||
);
|
||||
sylTrace(`RL API: loaded ${response.data.length} lines`);
|
||||
sylLog(
|
||||
`[RL-Syllable] Loaded "${trackInfo.title}" by "${trackInfo.artist}" — ${response.data.length} lines`,
|
||||
);
|
||||
@@ -4201,7 +3961,7 @@ const onTrackChange = async (): Promise<void> => {
|
||||
if (response.type === "Word" && response._synthesized) {
|
||||
lyricsIsAiGenerated = true;
|
||||
sylLog(
|
||||
`[RL-Syllable] AI generated syllable timings for ${response.data.length} lines (source: ${response.metadata.source})`,
|
||||
`[RL-Syllable] AI generated syllable timings for ${response.data.length} lines`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user