const API = location.origin; const form = document.getElementById('form'); const preview = document.getElementById('preview'); const placeholderSrc = preview.src; const removePhotoBtn = document.getElementById('removePhotoBtn'); const groupInput = document.getElementById('groupInput'); const moduleInput = document.getElementById('moduleInput'); const moduleInputList = document.getElementById('moduleInputList'); const moduleField = document.getElementById('moduleField'); const moduleNames = new Map(); let moduleSearchTimer = null; const toast = document.getElementById('toast'); const camVideo = document.getElementById('camVideo'); const camCanvas = document.getElementById('camCanvas'); const camModal = document.getElementById('camModal'); let capturedBlob = null; let stream = null; let camOpening = false; const descInput = document.getElementById('descInput'); const filesInput = document.getElementById('filesInput'); const filesList = document.getElementById('filesList'); const filesClearBtn = document.getElementById('filesClearBtn'); let selectedFiles = []; const sentPanel = document.getElementById('sentPanel'); const sentTitle = document.getElementById('sentTitle'); const sentText = document.getElementById('sentText'); const sentTimer = document.getElementById('sentTimer'); const SENT_KEY = 'whatido_entry_sent_at'; let entryCooldownMin = 30; let captureWidth = 640; let captureHeight = 480; let captureQuality = 0.92; let sentTimerId = null; function unlockForm() { clearInterval(sentTimerId); sentTimerId = null; localStorage.removeItem(SENT_KEY); sentPanel.style.display = 'none'; form.style.display = 'grid'; toast.textContent = 'Можно отправить новую запись'; toast.classList.add('show'); setTimeout(() => toast.classList.remove('show'), 2500); } function tickSentTimer(unlockTime) { const remain = Math.max(0, unlockTime - Date.now()); if (remain <= 0) { sentTimer.textContent = ''; unlockForm(); return; } const m = Math.floor(remain / 60000); const s = Math.floor((remain % 60000) / 1000); sentTimer.textContent = 'Новую запись можно отправить через ' + m + ' мин ' + s + ' сек'; } function showSent(title, text, unlockTime) { form.style.display = 'none'; sentPanel.style.display = 'flex'; sentTitle.textContent = title; sentText.textContent = text; tickSentTimer(unlockTime); clearInterval(sentTimerId); sentTimerId = setInterval(() => tickSentTimer(unlockTime), 1000); } function cooldownMs() { return entryCooldownMin * 60 * 1000; } function checkPreviousSend() { const raw = localStorage.getItem(SENT_KEY); if (!raw) return; const sentAt = parseInt(raw, 10); if (!Number.isFinite(sentAt)) { localStorage.removeItem(SENT_KEY); return; } const unlockTime = sentAt + cooldownMs(); if (Date.now() < unlockTime) { showSent('Запись уже отправлена', 'Ты уже отправил(а) запись сегодня. Дождись окончания отсчёта, чтобы отправить новую.', unlockTime); } else { localStorage.removeItem(SENT_KEY); } } function humanSize(b) { if (b < 1024) return b + ' Б'; if (b < 1048576) return (b / 1024).toFixed(1) + ' КБ'; return (b / 1048576).toFixed(1) + ' МБ'; } function renderFiles() { filesList.innerHTML = ''; selectedFiles.forEach((f, i) => { const row = document.createElement('div'); row.className = 'file-row'; const name = document.createElement('span'); name.className = 'f-name'; name.textContent = f.name; name.title = f.name; const size = document.createElement('span'); size.className = 'f-size'; size.textContent = humanSize(f.size); const del = document.createElement('button'); del.type = 'button'; del.textContent = '✕'; del.addEventListener('click', () => removeFile(i)); row.append(name, size, del); filesList.appendChild(row); }); filesClearBtn.classList.toggle('show', selectedFiles.length > 0); } function removeFile(i) { selectedFiles.splice(i, 1); renderFiles(); } function clearFiles() { selectedFiles = []; filesInput.value = ''; renderFiles(); } const MAX_FILES = 10; const DEFAULT_FILE_LIMIT_MB = 50; const DEFAULT_TOTAL_LIMIT_MB = 200; let MAX_FILE_BYTES = DEFAULT_FILE_LIMIT_MB * 1024 * 1024; let MAX_TOTAL_BYTES = DEFAULT_TOTAL_LIMIT_MB * 1024 * 1024; const BLOCKED_FILE_EXT = /\.(?:html?|js|mjs|cjs|svg|xml|json|map|wasm|php\d?|phtml|asp|aspx|jsp|sh|bat|cmd|cgi|exe|dll|com|msi|scr|hta|vbs|py|r|rb|htaccess)$/i; const MIME_EXT = { 'image/png': '.png', 'image/jpeg': '.jpg', 'image/jpg': '.jpg', 'image/gif': '.gif', 'image/webp': '.webp', 'image/bmp': '.bmp', 'image/avif': '.avif', 'image/heic': '.heic', 'image/heif': '.heif', 'image/svg+xml': '.svg', 'application/pdf': '.pdf', 'application/zip': '.zip', 'application/x-zip-compressed': '.zip', 'application/msword': '.doc', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': '.docx', 'application/vnd.ms-excel': '.xls', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx', 'application/vnd.ms-powerpoint': '.ppt', 'application/vnd.openxmlformats-officedocument.presentationml.presentation': '.pptx', 'text/plain': '.txt', 'text/csv': '.csv', 'application/rtf': '.rtf', 'application/x-rar-compressed': '.rar', 'application/vnd.rar': '.rar', 'application/x-7z-compressed': '.7z' }; let notifyTimer = null; function notify(text, isError) { toast.textContent = text; toast.classList.add('show'); toast.classList.toggle('error', !!isError); clearTimeout(notifyTimer); notifyTimer = setTimeout(() => toast.classList.remove('show', 'error'), 3200); } function extOf(name) { const s = String(name || ''); const i = s.lastIndexOf('.'); return i > 0 ? s.slice(i).toLowerCase() : ''; } function extFromFile(file) { const named = extOf(file.name); if (/^\.[a-z0-9]{1,8}$/i.test(named)) return named; const byMime = MIME_EXT[String(file.type || '').toLowerCase()]; if (byMime) return byMime; const sub = String(file.type || '').split('/')[1] || ''; return /^[a-z0-9]{1,8}$/i.test(sub) ? '.' + sub : ''; } function pasteFileName(file) { const base = String(file.name || '').trim(); const stem = base.replace(/\.[a-z0-9]{1,8}$/i, ''); if (base && !/^(image|photo|picture|blob|file|screenshot|снимок|скриншот|изображение|файл)$/i.test(stem)) return base; const d = new Date(); const pad = n => String(n).padStart(2, '0'); const stamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}-${pad(d.getMinutes())}-${pad(d.getSeconds())}`; return `Вставка ${stamp}${extFromFile(file)}`; } function totalUploadBytes() { return (capturedBlob ? capturedBlob.size : 0) + selectedFiles.reduce((sum, f) => sum + f.size, 0); } function applyUploadLimits(fileMb, totalMb) { const f = parseInt(fileMb, 10); const t = parseInt(totalMb, 10); const fileLimit = Number.isFinite(f) && f >= 1 ? f : DEFAULT_FILE_LIMIT_MB; const totalLimit = Number.isFinite(t) && t >= fileLimit ? t : Math.max(DEFAULT_TOTAL_LIMIT_MB, fileLimit); MAX_FILE_BYTES = fileLimit * 1024 * 1024; MAX_TOTAL_BYTES = totalLimit * 1024 * 1024; } function addFiles(list) { const added = []; const skipped = []; const fileLimitMb = Math.round(MAX_FILE_BYTES / 1024 / 1024); const totalLimitMb = Math.round(MAX_TOTAL_BYTES / 1024 / 1024); for (const file of list) { if (selectedFiles.length >= MAX_FILES) { skipped.push(`Можно прикрепить не более ${MAX_FILES} файлов`); break; } if (file.size > MAX_FILE_BYTES) { skipped.push(`«${file.name}» больше ${fileLimitMb} МБ`); continue; } if (BLOCKED_FILE_EXT.test(extOf(file.name))) { skipped.push(`«${file.name}»: недопустимый тип файла`); continue; } if (totalUploadBytes() + file.size > MAX_TOTAL_BYTES) { skipped.push(`Суммарный размер файлов — не более ${totalLimitMb} МБ`); break; } selectedFiles.push(file); added.push(file.name); } if (added.length) renderFiles(); return { added, skipped }; } filesInput.addEventListener('change', () => { const res = addFiles([...filesInput.files]); filesInput.value = ''; if (res.skipped.length) alert(res.skipped[0]); }); document.addEventListener('paste', (e) => { if (form.style.display === 'none') return; const clip = e.clipboardData; if (!clip) return; const pasted = []; for (const item of Array.from(clip.items || [])) { if (item.kind !== 'file') continue; const file = item.getAsFile(); if (file) pasted.push(file); } if (!pasted.length && clip.files && clip.files.length) pasted.push(...Array.from(clip.files)); if (!pasted.length) return; e.preventDefault(); let filesAdded = 0; const skipped = []; for (const raw of pasted) { const name = pasteFileName(raw); const file = raw.name === name ? raw : new File([raw], name, { type: raw.type || 'application/octet-stream', lastModified: raw.lastModified }); const res = addFiles([file]); filesAdded += res.added.length; skipped.push(...res.skipped); } if (!filesAdded) { if (skipped.length) notify(skipped[0], true); return; } const summary = filesAdded === 1 ? 'Файл добавлен' : `Файлов добавлено: ${filesAdded}`; notify(summary + (skipped.length ? '. ' + skipped[0] : ''), skipped.length > 0); }); const params = new URLSearchParams(location.search); let studentName = ''; function escName(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; } if (params.get('nameInput')) { studentName = params.get('nameInput'); document.getElementById('nameInput').value = studentName; const field = document.getElementById('nameField'); field.innerHTML = `