Files
server/doc/marketplace-scripts/playlist-now-playing/source.js
T
ges cc478f99dd 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
2026-05-01 10:54:47 +02:00

76 lines
2.4 KiB
JavaScript

/**
* 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();
};