forked from Keelrleek/SoulStream
mini player + dark glass: eight shapes run by the hidden main as the one brain, dissolving covers and accents, drag from anywhere, and the space between the panes turned to real acrylic - with the webview flags that keep rest frames honest
This commit is contained in:
1 parent
9b84b30aef
commit
dc7ea16b22
33 files changed
+2432
-5
No files matched your search
@@ -0,0 +1,175 @@
|
||||
// The mini player's lifeline. The mini window is a REMOTE CONTROL, not a
|
||||
// second brain: it never imports the player store (a second instance would
|
||||
// double-run the auto-advance and fight this window over the queue — the
|
||||
// same phantom-instance trap the probe rules exist for). Instead THIS window
|
||||
// — the only brain, alive while hidden — streams view-state out over three
|
||||
// channels and executes command intents that come back:
|
||||
//
|
||||
// mini:track { title, artist, album, artUrl, duration, waveform,
|
||||
// coverage, accent, hover } on track/waveform change
|
||||
// mini:tick { position, status, volume, muted, shuffle, repeat }
|
||||
// throttled ~5/s
|
||||
// mini:queue { rows: [{ title, dur }], index, next: { title, artUrl } }
|
||||
// on queue shape change
|
||||
// mini:cmd { op, value } mini → here
|
||||
// mini:hello {} mini → here, answered
|
||||
// with a full snapshot
|
||||
import { emit } from "@tauri-apps/api/event";
|
||||
import * as api from "./api.js";
|
||||
import {
|
||||
player,
|
||||
theme,
|
||||
togglePlayPause,
|
||||
nextTrack,
|
||||
prevTrack,
|
||||
toggleShuffle,
|
||||
cycleRepeat,
|
||||
jumpToQueueIndex,
|
||||
peekNextIndices,
|
||||
engineTime,
|
||||
getArt,
|
||||
getCatalogArt,
|
||||
playerArtSource,
|
||||
} from "./state.svelte.js";
|
||||
|
||||
let started = false;
|
||||
|
||||
/** Split the track's sub line ("Artist · Album") the way the bar renders it. */
|
||||
function meta() {
|
||||
const t = player.track;
|
||||
if (!t) return { title: null, artist: null, album: null };
|
||||
const parts = (t.sub ?? "").split(" · ");
|
||||
return { title: t.title ?? null, artist: parts[0] || null, album: parts[1] || null };
|
||||
}
|
||||
|
||||
async function artUrlForCurrent() {
|
||||
const item = player.queue?.[player.queueIndex];
|
||||
if (!item) return null;
|
||||
try {
|
||||
if (item.art) return await getArt(item.art);
|
||||
if (item.artUrl) return await getCatalogArt(playerArtSource(item));
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
let trackGen = 0;
|
||||
async function emitTrack() {
|
||||
const gen = ++trackGen;
|
||||
const m = meta();
|
||||
const artUrl = await artUrlForCurrent();
|
||||
if (gen !== trackGen) return; // a newer track landed while art resolved
|
||||
void emit("mini:track", {
|
||||
...m,
|
||||
artUrl,
|
||||
duration: player.duration,
|
||||
waveform: player.waveform,
|
||||
coverage: player.waveformCoverage,
|
||||
accent: theme.accentTarget,
|
||||
hover: theme.accentHoverTarget,
|
||||
});
|
||||
}
|
||||
|
||||
async function emitQueue() {
|
||||
const rows = (player.queue ?? []).map((it) => ({
|
||||
title: it.title ?? "—",
|
||||
dur: it.duration ?? it.duration_secs ?? null,
|
||||
}));
|
||||
let next = null;
|
||||
const [ni] = peekNextIndices(1);
|
||||
const nItem = ni != null ? player.queue?.[ni] : null;
|
||||
if (nItem) {
|
||||
let artUrl = null;
|
||||
try {
|
||||
if (nItem.art) artUrl = await getArt(nItem.art, 128);
|
||||
else if (nItem.artUrl) artUrl = await getCatalogArt(playerArtSource(nItem));
|
||||
} catch {}
|
||||
next = { title: nItem.title ?? "—", artUrl };
|
||||
}
|
||||
void emit("mini:queue", { rows, index: player.queueIndex, next });
|
||||
}
|
||||
|
||||
function emitTick() {
|
||||
void emit("mini:tick", {
|
||||
position: player.position,
|
||||
status: player.status,
|
||||
volume: player.muted ? 0 : player.volume,
|
||||
muted: player.muted,
|
||||
shuffle: player.shuffle,
|
||||
repeat: player.repeat,
|
||||
});
|
||||
}
|
||||
|
||||
export function initMiniBridge() {
|
||||
if (started) return;
|
||||
started = true;
|
||||
|
||||
// ---- outbound: watch the store, emit on the three channels ----------
|
||||
$effect.root(() => {
|
||||
// Track identity + waveform arrivals. The waveform array is replaced
|
||||
// wholesale by the store, so reference equality is the change signal.
|
||||
$effect(() => {
|
||||
void player.track?.title;
|
||||
void player.track?.sub;
|
||||
void player.duration;
|
||||
void player.waveform;
|
||||
void player.waveformCoverage;
|
||||
void emitTrack();
|
||||
});
|
||||
// Accent: stream the crossfade TARGET, set synchronously when the
|
||||
// fade starts, and let the mini run its own CSS fade. Never mirror
|
||||
// theme.accent from here — it lerps via rAF in THIS window, which is
|
||||
// hidden in mini mode, so it freezes mid-color (and a trailing
|
||||
// debounce on it turned the mini's fade into one late snap).
|
||||
$effect(() => {
|
||||
void theme.accentTarget;
|
||||
void theme.accentHoverTarget;
|
||||
void emitTrack();
|
||||
});
|
||||
$effect(() => {
|
||||
void player.queue;
|
||||
void player.queueIndex;
|
||||
void player.shuffle;
|
||||
void player.shuffleOrder;
|
||||
void player.repeat;
|
||||
void emitQueue();
|
||||
});
|
||||
let tickTimer = null;
|
||||
$effect(() => {
|
||||
void player.position;
|
||||
void player.status;
|
||||
void player.volume;
|
||||
void player.muted;
|
||||
void player.shuffle;
|
||||
void player.repeat;
|
||||
if (tickTimer) return;
|
||||
tickTimer = setTimeout(() => {
|
||||
tickTimer = null;
|
||||
emitTick();
|
||||
}, 180);
|
||||
});
|
||||
});
|
||||
|
||||
// ---- inbound: the mini's intents, executed by this brain ------------
|
||||
api.onEvent("mini:hello", () => {
|
||||
void emitTrack();
|
||||
void emitQueue();
|
||||
emitTick();
|
||||
});
|
||||
api.onEvent("mini:cmd", (p) => {
|
||||
const { op, value } = p ?? {};
|
||||
if (op === "toggle") togglePlayPause();
|
||||
else if (op === "next") nextTrack();
|
||||
else if (op === "prev") prevTrack();
|
||||
else if (op === "shuffle") toggleShuffle();
|
||||
else if (op === "repeat") cycleRepeat();
|
||||
else if (op === "jump") jumpToQueueIndex(value);
|
||||
else if (op === "seek") api.playerControl("seek", engineTime(value));
|
||||
else if (op === "volume") {
|
||||
const frac = Math.max(0, Math.min(1, value));
|
||||
player.volume = frac;
|
||||
player.muted = frac === 0;
|
||||
api.playerControl("volume", frac);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user