From edb5c06315a0da73e5cc97340e84e141dcf356a6 Mon Sep 17 00:00:00 2001 From: meowarex Date: Sun, 11 Oct 2026 21:59:04 +1100 Subject: [PATCH] AI Indicator <3 --- plugins/audio-visualizer-luna/src/audio.ts | 9 +- .../src/visualizers/spectrum-bars.ts | 2 +- plugins/copy-lyrics-luna/src/index.ts | 2 +- plugins/element-hider-luna/src/index.ts | 12 +- plugins/radiant-lyrics-luna/src/Settings.tsx | 2 +- plugins/radiant-lyrics-luna/src/api.ts | 36 ++---- plugins/radiant-lyrics-luna/src/artwork.ts | 82 +++++-------- .../radiant-lyrics-luna/src/hls-light.d.ts | 3 +- plugins/radiant-lyrics-luna/src/index.ts | 84 ++++++------- .../radiant-lyrics-luna/src/lyrics-glow.css | 2 +- plugins/radiant-lyrics-luna/src/styles.css | 113 ++++++++++++++++-- 11 files changed, 196 insertions(+), 151 deletions(-) diff --git a/plugins/audio-visualizer-luna/src/audio.ts b/plugins/audio-visualizer-luna/src/audio.ts index 08c5cba..1c02f6b 100644 --- a/plugins/audio-visualizer-luna/src/audio.ts +++ b/plugins/audio-visualizer-luna/src/audio.ts @@ -33,10 +33,7 @@ export interface AudioData { binCount: number; } -// Connection states -// disconnected - no audio source -// pending - wired but audio isn't detected (track muted / loading) -// live - audio is detected +// Connection states: disconnected, pending (no audio yet), live export type ConnectionState = "disconnected" | "pending" | "live"; let state: ConnectionState = "disconnected"; let onStateChange: ((state: ConnectionState) => void) | null = null; @@ -202,13 +199,13 @@ const captureFromEl = (el: HTMLMediaElement): boolean => { track.removeEventListener("ended", onEnded); }; - // Captured tracks start muted until samples flow; wait for "unmute" instead of guessing. + // Captured tracks start muted, wait for unmute loggedCaptureFailure = false; setState(track.muted ? "pending" : "live"); return true; }; -// An element is worth capturing from when it's actually advancing audio. +// Only capture elements that are playing const isPlayingAudio = (el: HTMLMediaElement): boolean => !el.paused && !el.ended && el.readyState >= 2; diff --git a/plugins/audio-visualizer-luna/src/visualizers/spectrum-bars.ts b/plugins/audio-visualizer-luna/src/visualizers/spectrum-bars.ts index 3b51282..ad0896f 100644 --- a/plugins/audio-visualizer-luna/src/visualizers/spectrum-bars.ts +++ b/plugins/audio-visualizer-luna/src/visualizers/spectrum-bars.ts @@ -36,7 +36,7 @@ void main() { float barMask = smoothstep(0.0, u_gap, cellPos) * smoothstep(0.0, u_gap, 1.0 - cellPos); - // Hard cut at bottom, soft feather only at the top edge + // Hard bottom, feathered top float feather = 1.5 / u_resolution.y; float heightMask = 1.0 - smoothstep(amp - feather, amp + feather, uv.y); diff --git a/plugins/copy-lyrics-luna/src/index.ts b/plugins/copy-lyrics-luna/src/index.ts index d472142..2025724 100644 --- a/plugins/copy-lyrics-luna/src/index.ts +++ b/plugins/copy-lyrics-luna/src/index.ts @@ -181,7 +181,7 @@ const onClickHooked = (event: MouseEvent): boolean | undefined => { return false; }; -// Add event listener with capture phase to intercept events before they reach other handlers +// Capture phase, runs before other handlers document.addEventListener("click", onClickHooked, true); diff --git a/plugins/element-hider-luna/src/index.ts b/plugins/element-hider-luna/src/index.ts index fbdba33..afc9204 100644 --- a/plugins/element-hider-luna/src/index.ts +++ b/plugins/element-hider-luna/src/index.ts @@ -20,9 +20,7 @@ new StyleTag("Element-Hider", unloads, styles); let targetElement: HTMLElement | null = null; let hiddenElements = new WeakSet(); -// Count of elements currently hidden in the live DOM. The `.element-hider-hidden` -// class is the source of truth — querying it avoids retaining detached nodes -// across SPA navigations. +// Count hidden elements in the live DOM function getHiddenCount(): number { return document.querySelectorAll(".element-hider-hidden").length; } @@ -371,7 +369,7 @@ let customMenu: HTMLElement | null = null; let contextMenuTimeout: number | null = null; let waitingForBuiltInMenu = false; -// Listen for right-click events to capture the target for context menu +// Grab the right-clicked element document.addEventListener( "contextmenu", (event: MouseEvent) => { @@ -403,18 +401,18 @@ document.addEventListener( const eventX = event.clientX; const eventY = event.clientY; - // Allow native context menu by default; we'll show our custom menu only if needed + // Native menu first, ours only if needed // Wait to see if the built-in context menu appears contextMenuTimeout = window.setTimeout(() => { - // If we're still waiting and no built-in menu appeared, show our custom menu + // No built-in menu appeared, show ours if (waitingForBuiltInMenu && currentContextElement) { showCustomMenu(eventX, eventY); } waitingForBuiltInMenu = false; }, 150); // Wait 150ms for built-in menu - // Don't prevent default initially - let Luna try to handle the context menu + // Let Luna handle the context menu first }, true, ); diff --git a/plugins/radiant-lyrics-luna/src/Settings.tsx b/plugins/radiant-lyrics-luna/src/Settings.tsx index 139195d..b15fc2a 100644 --- a/plugins/radiant-lyrics-luna/src/Settings.tsx +++ b/plugins/radiant-lyrics-luna/src/Settings.tsx @@ -210,7 +210,7 @@ export const Settings = () => { window.updateRadiantLyricsBackdrop?.(); }; - // Derive props and override onChange to accept a broader first param type + // Loosen onChange's first param type type BaseSwitchProps = React.ComponentProps; type AnySwitchProps = Omit & { onChange: (_: unknown, checked: boolean) => void; diff --git a/plugins/radiant-lyrics-luna/src/api.ts b/plugins/radiant-lyrics-luna/src/api.ts index eaa569a..f0b60d3 100644 --- a/plugins/radiant-lyrics-luna/src/api.ts +++ b/plugins/radiant-lyrics-luna/src/api.ts @@ -109,9 +109,9 @@ export interface WordLyricsResponse { songParts?: Array<{ name: string; time: number; duration: number }>; }; _cached?: boolean; - /** true when timings came from the hosted Radiant AI model (?synthesize=true) */ + // Timings came from Radiant AI _synthesized?: boolean; - /** original line-level data, kept so AI timings can be toggled off without a refetch */ + // Original lines, so AI can be turned off without refetch lines?: ApiLine[]; } @@ -209,24 +209,15 @@ export interface AnimatedArtwork { export interface ArtworkLookup { artwork: AnimatedArtwork | null; - /** - * Scope the result is valid for, as a dedupe key: the album key only when - * the answer was matched by album (by name or via the track's ISRC, which - * the server resolves to the same album). A title-only fallback (or a - * miss) is specific to this track, so its key scopes to the title — the - * album's other tracks must not inherit it. - */ + // Album key if album matched, else title key key: string; } -/** Title-scoped artwork key: a result only valid for this exact track. */ +// Key for this exact track only export const artworkTitleKey = (artist: string, title: string): string => `${artist}\u0000\u0000${title}`; -/** - * ISRC lookup: the server resolves the exact song by ISRC and answers - * from its album, immune to title/album name mismatches. - */ +// ISRC lookup, immune to name mismatches const artworkIsrcUrl = (isrc: string): string => `https://ama.trainswift.net/api/v1/artwork/search?isrc=${encodeURIComponent(isrc)}`; @@ -241,10 +232,7 @@ function artworkSearchUrl( return `https://ama.trainswift.net/api/v1/artwork/search?${q}`; } -// Cached including misses; most tracks have none and the answer never changes. -// Only album-matched results (by name or ISRC) go under the album key — a -// title-only fallback must not stand in for the album's other tracks (see -// ArtworkLookup). +// Cache hits and misses, album key only for album matches const animatedArtworkCache = new Map(); const ARTWORK_CACHE_MAX = 200; @@ -258,8 +246,7 @@ export async function fetchAnimatedArtwork( const albumName = album?.trim() || ""; const albumPresent = albumName !== ""; const isrcTrimmed = isrc?.trim() || ""; - // Album key, matching artworkKey() in index.ts; without an album the - // answer is only ever matched by title, so key by title directly. + // Album key, or title key without an album const cacheKey = albumPresent ? `${artist}\u0000${albumName}` : artworkTitleKey(artist, title); @@ -312,10 +299,7 @@ export async function fetchAnimatedArtwork( }; try { - // ISRC first: the exact match. A hit is resolved through the track's - // album, so it is album-scoped (cacheKey); a miss says nothing about - // the album (the ISRC may simply be unknown to the source), so fall - // through to the name flow without caching it. + // ISRC first, misses fall through uncached if (isrcTrimmed) { const byIsrc = await tryFetch(artworkIsrcUrl(isrcTrimmed)); if (byIsrc) return { artwork: remember(byIsrc), key: cacheKey }; @@ -327,9 +311,7 @@ export async function fetchAnimatedArtwork( // Album-matched: cacheable, and its scope is the whole album. if (withAlbum) return { artwork: remember(withAlbum), key: cacheKey }; if (albumPresent) { - // Album lookup missed: the title-only answer — hit or miss — is - // matched by title, so never cache it under the album key (that - // would poison the album's other tracks) and scope it to the title. + // Title-only result, scope it to this track const artwork = await tryFetch(artworkSearchUrl(title, artist)); return { artwork, key: artworkTitleKey(artist, title) }; } diff --git a/plugins/radiant-lyrics-luna/src/artwork.ts b/plugins/radiant-lyrics-luna/src/artwork.ts index 654c510..28cd91e 100644 --- a/plugins/radiant-lyrics-luna/src/artwork.ts +++ b/plugins/radiant-lyrics-luna/src/artwork.ts @@ -1,6 +1,5 @@ // MARKER: Animated Artwork (live artwork in the lyrics view) -// AnimatedArtworkLayer resolves the art for the playing track (bound to its -// track ID); each ArtworkSurface renders it over one of TIDAL's covers. +// Layer finds the art, surfaces draw it on covers. // Orchestration (track change, visibility, settings) lives in index.ts. import { Tracer } from "@luna/core"; @@ -17,27 +16,23 @@ import { settings } from "./Settings"; const { trace } = Tracer("[Radiant Lyrics]"); export const ART_TILE_SELECTOR = '[data-test="now-playing-artwork"]'; -/** Player-bar thumbnail (same element the stock backdrop samples). */ +// Player bar thumbnail export const MINI_ART_SELECTOR = '[data-test="footer-player"] [data-test="current-media-imagery"] img'; -/** Elements that cannot render child nodes, so the video must go beside them. */ +// Can't hold children, so the video goes beside them const REPLACED_TAGS = new Set(["IMG", "PICTURE", "VIDEO", "CANVAS", "SVG"]); -// Keeps ABR off the 2160x2160 rung (91MB a loop). Bandwidth only, not CPU. +// Skip the 2160px rung (91MB a loop) const ART_MAX_DPR = 1; const ART_NATIVE_FPS = 25; -/** Matches the .rl-animated-art opacity transition. */ +// Same as the CSS fade const ART_FADE_MS = 300; -/** - * One