From 7f23cfdacde84f941060fc5df25f0b5552ffbd61 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Tue, 14 Apr 2026 21:18:44 +0300 Subject: [PATCH] =?UTF-8?q?fix:=20=D1=82=D0=B5=D0=BC=D0=B0=20=D0=B4=D0=BE?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=20=D1=81=D0=B8=D0=BD=D1=85=D1=80=D0=BE=D0=BD?= =?UTF-8?q?=D0=B8=D0=B7=D0=B8=D1=80=D1=83=D0=B5=D1=82=D1=81=D1=8F=20=D0=BC?= =?UTF-8?q?=D0=B5=D0=B6=D0=B4=D1=83=20=D1=83=D1=87=D0=B8=D1=82=D0=B5=D0=BB?= =?UTF-8?q?=D0=B5=D0=BC=20=D0=B8=20=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=BA?= =?UTF-8?q?=D0=B0=D0=BC=D0=B8=20=D0=B2=20=D1=80=D0=B5=D0=B0=D0=BB=D1=8C?= =?UTF-8?q?=D0=BD=D0=BE=D0=BC=20=D0=B2=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../src/controllers/classroomController.js | 17 ++++++++++++++ backend/src/db/migrate.js | 2 ++ backend/src/routes/classroom.js | 1 + frontend/classroom.html | 22 ++++++++++++++++--- 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/backend/src/controllers/classroomController.js b/backend/src/controllers/classroomController.js index 47445ec..6007a28 100644 --- a/backend/src/controllers/classroomController.js +++ b/backend/src/controllers/classroomController.js @@ -22,6 +22,7 @@ const GUEST_EVENTS = new Set([ 'classroom_stroke_updated', 'classroom_page_added', 'classroom_page_changed', 'classroom_template_changed', 'classroom_page_cleared', 'classroom_page_renamed', 'classroom_page_duplicated', 'classroom_page_deleted', 'classroom_ended', + 'classroom_board_theme', ]); /* ── Helper: broadcast to all session participants ─────────────────────── */ @@ -490,6 +491,21 @@ function updatePageTemplate(req, res) { res.json({ ok: true, template }); } +/* PATCH /api/classroom/:id/board-theme — change board theme and broadcast to all */ +function updateBoardTheme(req, res) { + const sessionId = Number(req.params.id); + const VALID_THEMES = new Set(['chalkboard', 'blackboard', 'whiteboard', 'dark', 'grid', 'dots']); + const { theme } = req.body; + if (!theme || !VALID_THEMES.has(theme)) return res.status(400).json({ error: 'invalid theme' }); + const session = db.prepare(`SELECT * FROM classroom_sessions WHERE id=? AND status='active'`).get(sessionId); + if (!session) return res.status(404).json({ error: 'Сессия не активна' }); + if (session.teacher_id !== req.user.id && req.user.role !== 'admin') + return res.status(403).json({ error: 'Нет доступа' }); + db.prepare('UPDATE classroom_sessions SET board_theme=? WHERE id=?').run(theme, sessionId); + emitToSession(sessionId, { type: 'classroom_board_theme', sessionId, theme }); + res.json({ ok: true, theme }); +} + /* POST /api/classroom/:id/hand — raise hand */ function raiseHand(req, res) { const sessionId = Number(req.params.id); @@ -1521,6 +1537,7 @@ module.exports = { addPage, changePage, updatePageTemplate, + updateBoardTheme, getPages, renamePage, duplicatePage, diff --git a/backend/src/db/migrate.js b/backend/src/db/migrate.js index f5476bb..0f53ff3 100644 --- a/backend/src/db/migrate.js +++ b/backend/src/db/migrate.js @@ -2858,6 +2858,8 @@ db.exec(` // Guest token for classroom sessions (public read-only whiteboard access) try { db.exec("ALTER TABLE classroom_sessions ADD COLUMN guest_token TEXT UNIQUE"); } catch {} +// Board theme per session (synced to all participants) +try { db.exec("ALTER TABLE classroom_sessions ADD COLUMN board_theme TEXT NOT NULL DEFAULT 'chalkboard'"); } catch {} // Persistent draw permissions (survives server restart) db.exec(` diff --git a/backend/src/routes/classroom.js b/backend/src/routes/classroom.js index f678ed1..4c4df8a 100644 --- a/backend/src/routes/classroom.js +++ b/backend/src/routes/classroom.js @@ -76,6 +76,7 @@ router.get('/:id/pages', ...auth, c.getPages); router.post('/:id/pages', ...teacher, c.addPage); router.put('/:id/page', ...teacher, c.changePage); router.patch('/:id/page-template', ...teacher, c.updatePageTemplate); +router.patch('/:id/board-theme', ...teacher, c.updateBoardTheme); router.patch('/:id/pages/:pageNum/name', ...teacher, c.renamePage); router.post('/:id/pages/:pageNum/duplicate', ...teacher, c.duplicatePage); router.delete('/:id/pages/:pageNum', ...teacher, c.deletePage); diff --git a/frontend/classroom.html b/frontend/classroom.html index e06f5a7..28a09d1 100644 --- a/frontend/classroom.html +++ b/frontend/classroom.html @@ -3160,6 +3160,7 @@ let _me = null; let _session = null; let _sessionId = null; + let _sessionBoardTheme = null; // pending theme from session data, applied in initWhiteboard let _sseHandle = null; let _participants = {}; // userId -> {name, micMuted} let _raisedHands = {}; // userId -> userName @@ -3554,6 +3555,12 @@ if (sel) sel.value = data.template; wbUpdateThumbnail(_wbCurrentPage); } + } else if (data.type === 'classroom_board_theme') { + if (_sessionId == data.sessionId && _wb) { + _wb.setBoardTheme(data.theme); + const sel = document.getElementById('wb-theme-select'); + if (sel) sel.value = data.theme; + } } else if (data.type === 'classroom_hand_raised') { if (_sessionId == data.sessionId) onHandRaised(data.userId, data.userName); } else if (data.type === 'classroom_hand_lowered') { @@ -3981,6 +3988,8 @@ // page state _wbCurrentPage = session.current_page || 1; _totalPages = session.pageCount || 1; + // Remember board theme from session — will be applied in initWhiteboard + if (session.board_theme) _sessionBoardTheme = session.board_theme; _raisedHands = {}; _handRaised = false; _followTeacher = true; @@ -4069,8 +4078,12 @@ stylusMultiplier: _prefs.stylusMultiplier, }); - // Sync board theme selector to whiteboard default - { const sel = document.getElementById('wb-theme-select'); if (sel) sel.value = 'chalkboard'; } + // Apply session board theme (set by teacher, synced for all participants) + if (_sessionBoardTheme && _sessionBoardTheme !== 'chalkboard') { + _wb.setBoardTheme(_sessionBoardTheme); + } + { const sel = document.getElementById('wb-theme-select'); if (sel) sel.value = _sessionBoardTheme || 'chalkboard'; } + _sessionBoardTheme = null; // consumed // Apply saved whiteboard defaults from user preferences if (canEdit && LS.prefs) { @@ -5508,10 +5521,13 @@ function wbSetBoardTheme(theme) { if (!_wb) return; _wb.setBoardTheme(theme); - // Update selector if called programmatically const sel = document.getElementById('wb-theme-select'); if (sel) sel.value = theme; if (LS.prefs) LS.prefs.set('wb.theme', theme); + // Broadcast to students if in active session + if (_sessionId) { + LS.patch(`/api/classroom/${_sessionId}/board-theme`, { theme }).catch(() => {}); + } } /* ── Page context menu ── */