mirror of
https://github.com/imgfloat/server.git
synced 2026-06-22 21:01:23 +00:00
feat: add playlist admin panel UI, now-playing pill, CSS, and marketplace script
- Admin panel in rail with collapsible playlist section (create, rename,
delete, expand, drag-reorder tracks, playback controls, active selection)
- playlist.js IIFE wired to REST API with CSRF meta-tag injection
- Live events forwarded from console.js via window CustomEvent
- Admin-only now-playing pill overlaid on canvas surface
- playlist-now-playing marketplace script draws rounded pill on canvas
- Fix: add data-asset-type/id/name to asset list items so playlist.js can
populate the track-add dropdown
- Fix: renderer.js _onPlaylistTrackEnded reads XSRF-TOKEN cookie for CSRF
- Fix: playlist.js commandPause no longer passes headers:{} that stripped CSRF
- Fix: PLAYLIST_PREV restarts current track when currentTime >= 3 s
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
/**
|
||||
* Now Playing — minimal playlist pill overlay.
|
||||
*
|
||||
* Displays a pill in the bottom-right corner of the broadcast canvas
|
||||
* showing the current track name when a playlist is active and playing.
|
||||
* Fades in when playback starts and fades out when paused or stopped.
|
||||
*
|
||||
* Context used: context.playlist, context.width, context.height
|
||||
*/
|
||||
|
||||
exports.init = function (context, state) {
|
||||
state.opacity = 0;
|
||||
state.targetOpacity = 0;
|
||||
state.lastTrackName = null;
|
||||
};
|
||||
|
||||
exports.tick = function (context, state) {
|
||||
const { ctx, width, height, deltaMs, playlist } = context;
|
||||
|
||||
const isPlaying = playlist && playlist.active && !playlist.paused && playlist.trackName;
|
||||
state.targetOpacity = isPlaying ? 1 : 0;
|
||||
|
||||
// Smooth fade
|
||||
const speed = (deltaMs / 1000) * 3; // ~333ms transition
|
||||
if (state.opacity < state.targetOpacity) {
|
||||
state.opacity = Math.min(state.targetOpacity, state.opacity + speed);
|
||||
} else if (state.opacity > state.targetOpacity) {
|
||||
state.opacity = Math.max(state.targetOpacity, state.opacity - speed);
|
||||
}
|
||||
|
||||
if (state.opacity <= 0.01) {
|
||||
return;
|
||||
}
|
||||
|
||||
const trackName = playlist?.trackName ?? "";
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = state.opacity;
|
||||
|
||||
const fontSize = Math.round(height * 0.022);
|
||||
ctx.font = `600 ${fontSize}px system-ui, sans-serif`;
|
||||
|
||||
const iconGlyph = "\u266A"; // ♪
|
||||
const text = `${iconGlyph} ${trackName}`;
|
||||
const paddingH = fontSize * 0.9;
|
||||
const paddingV = fontSize * 0.55;
|
||||
const pillWidth = Math.min(ctx.measureText(text).width + paddingH * 2, width * 0.38);
|
||||
const pillHeight = fontSize + paddingV * 2;
|
||||
const margin = Math.round(height * 0.025);
|
||||
const x = width - pillWidth - margin;
|
||||
const y = height - pillHeight - margin;
|
||||
const radius = pillHeight / 2;
|
||||
|
||||
// Pill background
|
||||
ctx.fillStyle = "rgba(0, 0, 0, 0.60)";
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, pillWidth, pillHeight, radius);
|
||||
ctx.fill();
|
||||
|
||||
// Clip text to pill
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x + paddingH, y, pillWidth - paddingH * 2, pillHeight, 0);
|
||||
ctx.clip();
|
||||
|
||||
// Draw icon in accent colour
|
||||
ctx.fillStyle = "#a78bfa"; // purple accent
|
||||
ctx.fillText(iconGlyph, x + paddingH, y + paddingV + fontSize * 0.82);
|
||||
|
||||
// Draw track name in white
|
||||
const iconWidth = ctx.measureText(iconGlyph + " ").width;
|
||||
ctx.fillStyle = "#ffffff";
|
||||
ctx.fillText(trackName, x + paddingH + iconWidth, y + paddingV + fontSize * 0.82);
|
||||
|
||||
ctx.restore();
|
||||
};
|
||||
Reference in New Issue
Block a user