- 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.
143 lines
4.6 KiB
JavaScript
143 lines
4.6 KiB
JavaScript
// Room model: named multi-client listening sessions.
|
|
//
|
|
// Each user has an auto-provisioned "personal" room (slug = `u-<id>`) so the
|
|
// kiosk Just Works on first login. Shared rooms are explicit creations and
|
|
// have a member list.
|
|
|
|
import { getDb } from './db/index.js';
|
|
import { slugify } from './stations.js';
|
|
|
|
function rowToRoom(row) {
|
|
if (!row) return null;
|
|
return {
|
|
id: row.id,
|
|
slug: row.slug,
|
|
name: row.name,
|
|
created_by: row.created_by,
|
|
created_at: row.created_at
|
|
};
|
|
}
|
|
|
|
export function getRoomBySlug(slug) {
|
|
return rowToRoom(getDb().prepare('SELECT * FROM rooms WHERE slug = ?').get(slug));
|
|
}
|
|
|
|
export function getRoomById(id) {
|
|
return rowToRoom(getDb().prepare('SELECT * FROM rooms WHERE id = ?').get(id));
|
|
}
|
|
|
|
export function isMember(roomId, userId) {
|
|
if (!roomId || !userId) return false;
|
|
const row = getDb().prepare(
|
|
'SELECT 1 FROM room_members WHERE room_id = ? AND user_id = ?'
|
|
).get(roomId, userId);
|
|
return !!row;
|
|
}
|
|
|
|
export function listRoomsForUser(userId) {
|
|
return getDb().prepare(`
|
|
SELECT r.*, rm.role
|
|
FROM rooms r
|
|
JOIN room_members rm ON rm.room_id = r.id
|
|
WHERE rm.user_id = ?
|
|
ORDER BY r.name COLLATE NOCASE
|
|
`).all(userId).map((row) => ({ ...rowToRoom(row), role: row.role }));
|
|
}
|
|
|
|
export function listMembers(roomId) {
|
|
return getDb().prepare(`
|
|
SELECT u.id, u.username, rm.role, rm.created_at
|
|
FROM room_members rm JOIN users u ON u.id = rm.user_id
|
|
WHERE rm.room_id = ?
|
|
ORDER BY u.username COLLATE NOCASE
|
|
`).all(roomId);
|
|
}
|
|
|
|
function uniqueRoomSlug(base) {
|
|
const db = getDb();
|
|
let slug = base, n = 1;
|
|
while (db.prepare('SELECT 1 FROM rooms WHERE slug = ?').get(slug)) {
|
|
n += 1;
|
|
slug = `${base}-${n}`;
|
|
}
|
|
return slug;
|
|
}
|
|
|
|
export function createRoom({ name, slug, ownerId }) {
|
|
if (!name) throw new Error('name required');
|
|
const db = getDb();
|
|
const baseSlug = slug || slugify(name);
|
|
const finalSlug = uniqueRoomSlug(baseSlug);
|
|
const info = db.prepare(
|
|
'INSERT INTO rooms (slug, name, created_by) VALUES (?, ?, ?)'
|
|
).run(finalSlug, name, ownerId ?? null);
|
|
const id = info.lastInsertRowid;
|
|
if (ownerId) {
|
|
db.prepare(
|
|
"INSERT OR IGNORE INTO room_members (room_id, user_id, role) VALUES (?, ?, 'owner')"
|
|
).run(id, ownerId);
|
|
}
|
|
db.prepare(
|
|
'INSERT OR IGNORE INTO room_state (room_id, playing, volume) VALUES (?, 0, 0.7)'
|
|
).run(id);
|
|
return getRoomById(id);
|
|
}
|
|
|
|
export function addMember(roomId, userId, role = 'member') {
|
|
getDb().prepare(
|
|
'INSERT OR IGNORE INTO room_members (room_id, user_id, role) VALUES (?, ?, ?)'
|
|
).run(roomId, userId, role);
|
|
}
|
|
|
|
export function removeMember(roomId, userId) {
|
|
return getDb().prepare(
|
|
'DELETE FROM room_members WHERE room_id = ? AND user_id = ?'
|
|
).run(roomId, userId).changes > 0;
|
|
}
|
|
|
|
/** Idempotently ensure a personal room exists for the user, returning it. */
|
|
export function ensurePersonalRoom(user) {
|
|
if (!user) return null;
|
|
const slug = `u-${user.id}`;
|
|
const existing = getRoomBySlug(slug);
|
|
if (existing) return existing;
|
|
const db = getDb();
|
|
db.prepare(
|
|
'INSERT INTO rooms (slug, name, created_by) VALUES (?, ?, ?)'
|
|
).run(slug, `${user.username}'s room`, user.id);
|
|
const room = getRoomBySlug(slug);
|
|
db.prepare(
|
|
"INSERT OR IGNORE INTO room_members (room_id, user_id, role) VALUES (?, ?, 'owner')"
|
|
).run(room.id, user.id);
|
|
db.prepare(
|
|
'INSERT OR IGNORE INTO room_state (room_id, playing, volume) VALUES (?, 0, 0.7)'
|
|
).run(room.id);
|
|
return room;
|
|
}
|
|
|
|
export function getRoomState(roomId) {
|
|
const row = getDb().prepare('SELECT * FROM room_state WHERE room_id = ?').get(roomId);
|
|
if (!row) return { station_id: null, playing: false, volume: 0.7, updated_at: null };
|
|
return {
|
|
station_id: row.station_id,
|
|
playing: !!row.playing,
|
|
volume: row.volume,
|
|
updated_at: row.updated_at
|
|
};
|
|
}
|
|
|
|
export function setRoomState(roomId, patch) {
|
|
const cur = getRoomState(roomId);
|
|
const next = { ...cur, ...patch };
|
|
getDb().prepare(`
|
|
INSERT INTO room_state (room_id, station_id, playing, volume, updated_at)
|
|
VALUES (?, ?, ?, ?, datetime('now'))
|
|
ON CONFLICT(room_id) DO UPDATE SET
|
|
station_id = excluded.station_id,
|
|
playing = excluded.playing,
|
|
volume = excluded.volume,
|
|
updated_at = excluded.updated_at
|
|
`).run(roomId, next.station_id ?? null, next.playing ? 1 : 0, next.volume ?? 0.7);
|
|
return getRoomState(roomId);
|
|
}
|