mirror of
https://github.com/screentinker/screentinker.git
synced 2026-05-15 07:32:23 -06:00
ScreenTinker - open source digital signage management software. MIT License, all features included, no license gates. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
185 lines
6.8 KiB
JavaScript
185 lines
6.8 KiB
JavaScript
const express = require('express');
|
|
const router = express.Router();
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { db } = require('../db/database');
|
|
|
|
// List walls
|
|
router.get('/', (req, res) => {
|
|
const isAdmin = req.user.role === 'superadmin';
|
|
const walls = db.prepare(
|
|
`SELECT * FROM video_walls ${isAdmin ? '' : 'WHERE user_id = ?'} ORDER BY created_at DESC`
|
|
).all(...(isAdmin ? [] : [req.user.id]));
|
|
|
|
// Attach devices to each wall
|
|
const devStmt = db.prepare(`
|
|
SELECT vwd.*, d.name as device_name, d.status as device_status
|
|
FROM video_wall_devices vwd
|
|
JOIN devices d ON vwd.device_id = d.id
|
|
WHERE vwd.wall_id = ?
|
|
ORDER BY vwd.grid_row, vwd.grid_col
|
|
`);
|
|
walls.forEach(w => { w.devices = devStmt.all(w.id); });
|
|
|
|
res.json(walls);
|
|
});
|
|
|
|
// Helper: check wall ownership
|
|
function checkWallAccess(req, res) {
|
|
const wall = db.prepare('SELECT * FROM video_walls WHERE id = ?').get(req.params.id);
|
|
if (!wall) { res.status(404).json({ error: 'Wall not found' }); return null; }
|
|
if (!['admin','superadmin'].includes(req.user.role) && wall.user_id !== req.user.id) { res.status(403).json({ error: 'Access denied' }); return null; }
|
|
return wall;
|
|
}
|
|
|
|
// Get wall with devices
|
|
router.get('/:id', (req, res) => {
|
|
const wall = checkWallAccess(req, res);
|
|
if (!wall) return;
|
|
|
|
wall.devices = db.prepare(`
|
|
SELECT vwd.*, d.name as device_name, d.status as device_status
|
|
FROM video_wall_devices vwd
|
|
JOIN devices d ON vwd.device_id = d.id
|
|
WHERE vwd.wall_id = ?
|
|
ORDER BY vwd.grid_row, vwd.grid_col
|
|
`).all(wall.id);
|
|
|
|
res.json(wall);
|
|
});
|
|
|
|
// Create wall
|
|
router.post('/', (req, res) => {
|
|
const { name, grid_cols, grid_rows, bezel_h_mm, bezel_v_mm, screen_w_mm, screen_h_mm } = req.body;
|
|
if (!name) return res.status(400).json({ error: 'name required' });
|
|
|
|
const id = uuidv4();
|
|
db.prepare(`
|
|
INSERT INTO video_walls (id, user_id, name, grid_cols, grid_rows, bezel_h_mm, bezel_v_mm, screen_w_mm, screen_h_mm)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`).run(id, req.user.id, name, grid_cols || 2, grid_rows || 2,
|
|
bezel_h_mm || 0, bezel_v_mm || 0, screen_w_mm || 400, screen_h_mm || 225);
|
|
|
|
const wall = db.prepare('SELECT * FROM video_walls WHERE id = ?').get(id);
|
|
wall.devices = [];
|
|
res.status(201).json(wall);
|
|
});
|
|
|
|
// Update wall
|
|
router.put('/:id', (req, res) => {
|
|
const wall = checkWallAccess(req, res);
|
|
if (!wall) return;
|
|
|
|
const fields = ['name', 'grid_cols', 'grid_rows', 'bezel_h_mm', 'bezel_v_mm',
|
|
'screen_w_mm', 'screen_h_mm', 'sync_mode', 'leader_device_id', 'content_id'];
|
|
const updates = [];
|
|
const values = [];
|
|
fields.forEach(f => {
|
|
if (req.body[f] !== undefined) { updates.push(`${f} = ?`); values.push(req.body[f]); }
|
|
});
|
|
|
|
if (updates.length > 0) {
|
|
updates.push("updated_at = strftime('%s','now')");
|
|
values.push(req.params.id);
|
|
db.prepare(`UPDATE video_walls SET ${updates.join(', ')} WHERE id = ?`).run(...values);
|
|
}
|
|
|
|
const updated = db.prepare('SELECT * FROM video_walls WHERE id = ?').get(req.params.id);
|
|
updated.devices = db.prepare(`
|
|
SELECT vwd.*, d.name as device_name, d.status as device_status
|
|
FROM video_wall_devices vwd JOIN devices d ON vwd.device_id = d.id
|
|
WHERE vwd.wall_id = ? ORDER BY vwd.grid_row, vwd.grid_col
|
|
`).all(req.params.id);
|
|
|
|
res.json(updated);
|
|
});
|
|
|
|
// Delete wall
|
|
router.delete('/:id', (req, res) => {
|
|
const wall = checkWallAccess(req, res);
|
|
if (!wall) return;
|
|
db.prepare("UPDATE devices SET wall_id = NULL WHERE wall_id = ?").run(req.params.id);
|
|
db.prepare('DELETE FROM video_walls WHERE id = ?').run(req.params.id);
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// Set device grid positions
|
|
router.put('/:id/devices', (req, res) => {
|
|
const { devices } = req.body;
|
|
if (!Array.isArray(devices)) return res.status(400).json({ error: 'devices array required' });
|
|
|
|
const wall = checkWallAccess(req, res);
|
|
if (!wall) return;
|
|
|
|
// Clear existing
|
|
db.prepare('DELETE FROM video_wall_devices WHERE wall_id = ?').run(req.params.id);
|
|
db.prepare("UPDATE devices SET wall_id = NULL WHERE wall_id = ?").run(req.params.id);
|
|
|
|
// Add new positions
|
|
const stmt = db.prepare('INSERT INTO video_wall_devices (wall_id, device_id, grid_col, grid_row, rotation) VALUES (?, ?, ?, ?, ?)');
|
|
const updateDevice = db.prepare("UPDATE devices SET wall_id = ? WHERE id = ?");
|
|
|
|
const transaction = db.transaction(() => {
|
|
devices.forEach(d => {
|
|
stmt.run(req.params.id, d.device_id, d.grid_col, d.grid_row, d.rotation || 0);
|
|
updateDevice.run(req.params.id, d.device_id);
|
|
});
|
|
// Set first device as leader if none set
|
|
if (!wall.leader_device_id && devices.length > 0) {
|
|
const leader = devices.find(d => d.grid_col === 0 && d.grid_row === 0) || devices[0];
|
|
db.prepare('UPDATE video_walls SET leader_device_id = ? WHERE id = ?').run(leader.device_id, req.params.id);
|
|
}
|
|
});
|
|
transaction();
|
|
|
|
const updated = db.prepare('SELECT * FROM video_walls WHERE id = ?').get(req.params.id);
|
|
updated.devices = db.prepare(`
|
|
SELECT vwd.*, d.name as device_name, d.status as device_status
|
|
FROM video_wall_devices vwd JOIN devices d ON vwd.device_id = d.id
|
|
WHERE vwd.wall_id = ? ORDER BY vwd.grid_row, vwd.grid_col
|
|
`).all(req.params.id);
|
|
|
|
res.json(updated);
|
|
});
|
|
|
|
// Set wall content
|
|
router.put('/:id/content', (req, res) => {
|
|
const { content_id } = req.body;
|
|
db.prepare("UPDATE video_walls SET content_id = ?, updated_at = strftime('%s','now') WHERE id = ?")
|
|
.run(content_id || null, req.params.id);
|
|
res.json({ success: true });
|
|
});
|
|
|
|
// Get wall config for a specific device (used by Android app)
|
|
router.get('/:id/device-config/:deviceId', (req, res) => {
|
|
const wall = db.prepare('SELECT * FROM video_walls WHERE id = ?').get(req.params.id);
|
|
if (!wall) return res.status(404).json({ error: 'Wall not found' });
|
|
|
|
const position = db.prepare('SELECT * FROM video_wall_devices WHERE wall_id = ? AND device_id = ?')
|
|
.get(req.params.id, req.params.deviceId);
|
|
if (!position) return res.status(404).json({ error: 'Device not in this wall' });
|
|
|
|
// Calculate crop region
|
|
const totalW = wall.grid_cols * wall.screen_w_mm + (wall.grid_cols - 1) * wall.bezel_h_mm;
|
|
const totalH = wall.grid_rows * wall.screen_h_mm + (wall.grid_rows - 1) * wall.bezel_v_mm;
|
|
|
|
const cropX = (position.grid_col * (wall.screen_w_mm + wall.bezel_h_mm)) / totalW;
|
|
const cropY = (position.grid_row * (wall.screen_h_mm + wall.bezel_v_mm)) / totalH;
|
|
const cropW = wall.screen_w_mm / totalW;
|
|
const cropH = wall.screen_h_mm / totalH;
|
|
|
|
res.json({
|
|
wall_id: wall.id,
|
|
grid_cols: wall.grid_cols,
|
|
grid_rows: wall.grid_rows,
|
|
grid_col: position.grid_col,
|
|
grid_row: position.grid_row,
|
|
rotation: position.rotation,
|
|
crop: { x: cropX, y: cropY, width: cropW, height: cropH },
|
|
content_id: wall.content_id,
|
|
sync_mode: wall.sync_mode,
|
|
is_leader: wall.leader_device_id === req.params.deviceId,
|
|
});
|
|
});
|
|
|
|
module.exports = router;
|