Add master display UI with audio output management and styling
- Implement main.js for the master display functionality, including WebSocket connection, audio output management, and state handling. - Create style.css for the master display's visual design, ensuring a cohesive look and feel with a dark theme and responsive layout. - Integrate device management with a fallback for non-Electron environments, allowing users to select audio outputs. - Add features for managing favorites, including toggling favorites and filtering by genre. - Enhance user experience with a responsive favorites grid and drag-to-scroll functionality.
This commit is contained in:
@@ -6,7 +6,8 @@ import {
|
||||
import { resolveStream } from '../streams/resolver.js';
|
||||
import { requireAdmin, requireUser } from '../auth.js';
|
||||
import * as radiobrowser from '../sources/radiobrowser.js';
|
||||
import { castVote, getStationStats, getStatsMap, recordPlay, sortByMode } from '../stats.js';
|
||||
import { castVote, getStationStats, getStatsMap, recordPlay, endPlaySession, sortByMode } from '../stats.js';
|
||||
import { broadcastGlobal } from '../ws.js';
|
||||
|
||||
export const router = Router();
|
||||
|
||||
@@ -51,15 +52,41 @@ router.post('/:id/vote', requireUser, (req, res) => {
|
||||
: raw === 0 || raw === '0' || raw === null || raw === 'clear' ? 0
|
||||
: NaN;
|
||||
if (Number.isNaN(value)) return res.status(400).json({ error: 'value must be 1, -1 or 0' });
|
||||
res.json(castVote(req.user.id, id, value));
|
||||
const stats = castVote(req.user.id, id, value);
|
||||
// Tell every open client so other panels' vote counts update live.
|
||||
broadcastGlobal({ type: 'vote', stationId: id, stats: { up: stats.up, down: stats.down, score: stats.score }, by: req.user.username });
|
||||
res.json(stats);
|
||||
});
|
||||
|
||||
// Lightweight play-count ping (called when the kiosk actually starts a station).
|
||||
// Opens a listening session in play_history; the returned `sessionId` should be
|
||||
// echoed back to POST /api/stations/:id/play/end so we can credit total listen
|
||||
// time toward the leaderboard score.
|
||||
router.post('/:id/play', requireUser, (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!getStation(id)) return res.status(404).json({ error: 'not found' });
|
||||
recordPlay(id);
|
||||
res.json(getStationStats(id, req.user.id));
|
||||
const streamId = Number.isFinite(Number(req.body?.streamId)) ? Number(req.body.streamId) : null;
|
||||
const sessionId = recordPlay(id, req.user.id, streamId);
|
||||
const stats = getStationStats(id, req.user.id);
|
||||
broadcastGlobal({ type: 'plays', stationId: id, plays: stats.plays });
|
||||
res.json({ ...stats, sessionId });
|
||||
});
|
||||
|
||||
// Close a session opened by POST /:id/play. Idempotent — calling twice or with
|
||||
// an unknown id silently no-ops. Accepts an optional `duration_ms` so a client
|
||||
// that knows the real listened time (e.g. minus buffering stalls) can be honest.
|
||||
router.post('/:id/play/end', requireUser, (req, res) => {
|
||||
const id = Number(req.params.id);
|
||||
if (!getStation(id)) return res.status(404).json({ error: 'not found' });
|
||||
const sessionId = Number(req.body?.sessionId);
|
||||
if (!Number.isFinite(sessionId)) return res.status(400).json({ error: 'sessionId required' });
|
||||
const rawMs = req.body?.duration_ms;
|
||||
const ms = rawMs == null ? null : Number(rawMs);
|
||||
const closed = endPlaySession(sessionId, req.user.id, ms);
|
||||
if (closed == null) return res.json({ ok: false });
|
||||
const stats = getStationStats(closed, req.user.id);
|
||||
broadcastGlobal({ type: 'plays', stationId: closed, plays: stats.plays });
|
||||
res.json({ ok: true, ...stats });
|
||||
});
|
||||
|
||||
router.post('/:id/resolve', requireUser, async (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user