feat: WebSocket real-time + rAF render gate + guest board + screen picker
Classroom performance: - WebSocket server (ws-server.js) for low-latency cursor & stroke preview Replaces HTTP POST per event → eliminates per-message auth overhead Session member cache (30s TTL) avoids SQLite query per WS message Fallback to HTTP POST when WS not connected - Cursor throttle reduced 100ms → 33ms (~30fps) - Stroke preview throttle reduced 50ms → 20ms - whiteboard.js: render() is now rAF-gated (_doRender/_rafPending) Multiple render() calls within one frame collapse into one repaint document.hidden check — zero CPU when tab is in background visibilitychange listener restores canvas on tab focus Guest board: - guestClassroom.js route: public token-based read-only access - guest-board.html: name entry + read-only whiteboard + SSE - SSE: addGuestClient/removeGuestClient/emitToGuests Screen share picker: - Discord-style modal with tab switching (screen/window/tab) - Live video preview before confirming share - useExistingScreenStream() in ClassroomRTC Fullscreen exit overlay: - #cr-fs-exit-overlay button inside cr-board-wrap - Visible only via CSS :fullscreen selector (touchpad users) File sharing from library: - Teacher picks file from library, sends as styled card in chat - crDownloadLibraryFile() fetches with Bearer auth Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="/css/ls.css" />
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css" />
|
||||
<style>
|
||||
.sb-content { background: #f4f5f8; min-height: 100vh; }
|
||||
.fc-wrap { max-width: 1100px; margin: 0 auto; padding: 28px 28px 80px; }
|
||||
@@ -395,6 +396,8 @@
|
||||
<script src="https://cdn.jsdelivr.net/npm/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||||
<script src="/js/api.js"></script>
|
||||
<script src="/js/search.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/contrib/auto-render.min.js"></script>
|
||||
<script>
|
||||
(async () => {
|
||||
/* ── auth ── */
|
||||
@@ -664,13 +667,28 @@ async function startStudyForDeck(deckId) {
|
||||
bindSwipe();
|
||||
}
|
||||
|
||||
const _FC_DELIMS = [
|
||||
{ left: '\\(', right: '\\)', display: false },
|
||||
{ left: '\\[', right: '\\]', display: true },
|
||||
{ left: '$', right: '$', display: false },
|
||||
];
|
||||
function mathHtmlFC(text) {
|
||||
if (!text) return '';
|
||||
const tmp = document.createElement('span');
|
||||
tmp.textContent = text;
|
||||
if (window.renderMathInElement) {
|
||||
try { renderMathInElement(tmp, { delimiters: _FC_DELIMS, throwOnError: false }); } catch {}
|
||||
}
|
||||
return tmp.innerHTML;
|
||||
}
|
||||
|
||||
function showStudyCard() {
|
||||
const card = _studyCards[_studyIdx];
|
||||
if (!card) { finishStudy(); return; }
|
||||
const el = document.getElementById('study-card');
|
||||
el.className = 'study-card-inner';
|
||||
document.getElementById('study-front-text').textContent = card.front;
|
||||
document.getElementById('study-back-text').textContent = card.back;
|
||||
document.getElementById('study-front-text').innerHTML = mathHtmlFC(card.front);
|
||||
document.getElementById('study-back-text').innerHTML = mathHtmlFC(card.back);
|
||||
_studyFlipped = false;
|
||||
document.getElementById('study-btns').classList.remove('visible');
|
||||
document.getElementById('study-flip-hint').style.display = 'block';
|
||||
|
||||
Reference in New Issue
Block a user