diff --git a/frontend/exam-prep-mock.html b/frontend/exam-prep-mock.html index f18ea2d..69d4c7e 100644 --- a/frontend/exam-prep-mock.html +++ b/frontend/exam-prep-mock.html @@ -56,6 +56,7 @@ + diff --git a/frontend/exam-prep-practice.html b/frontend/exam-prep-practice.html index dccf49e..5c0d30a 100644 --- a/frontend/exam-prep-practice.html +++ b/frontend/exam-prep-practice.html @@ -56,6 +56,7 @@ + diff --git a/frontend/exam-prep-topics.html b/frontend/exam-prep-topics.html index ce08349..426ad40 100644 --- a/frontend/exam-prep-topics.html +++ b/frontend/exam-prep-topics.html @@ -56,6 +56,7 @@ + diff --git a/frontend/exam-prep-variants.html b/frontend/exam-prep-variants.html index 1f764b5..69acbbd 100644 --- a/frontend/exam-prep-variants.html +++ b/frontend/exam-prep-variants.html @@ -72,6 +72,7 @@ + diff --git a/frontend/js/exam-prep/task-card.js b/frontend/js/exam-prep/task-card.js index dbf1a7e..7555d5b 100644 --- a/frontend/js/exam-prep/task-card.js +++ b/frontend/js/exam-prep/task-card.js @@ -33,6 +33,10 @@ function escapeHtml(s) { return String(s || '').replace(/[&<>"']/g, c => ({ '&':'&', '<':'<', '>':'>', '"':'"', "'":''' }[c])); } + /* HTML → readable plain text (keeps $…$ math source) for saving to materials. */ + function stripHtml(s) { + return String(s || '').replace(//gi, ' ').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); + } /* topic_ref → "Учить тему" deep-link to the textbook chapter/paragraph. ref = { slug, paragraph, title }. Paragraph is null for hub links. */ @@ -120,14 +124,18 @@ } const refLink = buildRefLink(task.topic_ref); - const solBlock = (showSol && task.solution) ? ` -
-
- - ${refLink} -
-
${task.solution}
-
` : (refLink ? `
${refLink}
` : ''); + const allowSave = mode !== 'mock'; + const saveMatBtn = allowSave + ? `` + : ''; + const solToggle = (showSol && task.solution) + ? `` : ''; + const solPanelHtml = (showSol && task.solution) + ? `
${task.solution}
` : ''; + const solBlock = (solToggle || refLink || saveMatBtn) + ? `
${solToggle}${refLink}${saveMatBtn}
${solPanelHtml}
` + : ''; card.innerHTML = `
@@ -147,6 +155,20 @@ // ── KaTeX render EP.katex?.run(card); + // ── Save task to «Мои материалы» (universal buffer) + const saveMatEl = card.querySelector('[data-tc-savemat]'); + if (saveMatEl) { + saveMatEl.addEventListener('click', () => { + if (!window.MaterialSave) return; + const examTitle = (window.EP && EP.info && EP.info.track && EP.info.track.title) || 'Экзамен'; + const loc = (task.variant != null ? 'Вариант ' + task.variant + ', ' : '') + '№' + (task.idx != null ? task.idx : numbering); + const parts = [stripHtml(task.text)]; + if (task.answer) parts.push('Ответ: ' + task.answer); + if (task.solution) parts.push('Решение: ' + stripHtml(task.solution)); + MaterialSave.note({ title: examTitle + ' · ' + loc, body: parts.join('\n\n'), sourceTitle: examTitle }, saveMatEl); + }); + } + // ── State let startedAt = Date.now(); let solutionLogged = false; diff --git a/frontend/js/material-save.js b/frontend/js/material-save.js new file mode 100644 index 0000000..1634b8c --- /dev/null +++ b/frontend/js/material-save.js @@ -0,0 +1,55 @@ +'use strict'; +/* material-save.js — универсальная кнопка «В мои материалы». + * Тонкий слой поверх LS.saveMaterial / LS.uploadFile: сохраняет заметку, + * ссылку или изображение в личную коллекцию ученика из любой части платформы + * (учебник, экзамен, лаборатория, чат и т.п.). + * + * MaterialSave.note({ title, body, sourceTitle }, btn?) + * MaterialSave.link({ title, url, sourceTitle }, btn?) + * MaterialSave.image({ title, url|blob, name?, sourceTitle }, btn?) + * + * Требует window.LS (api.js). Показывает тост; кнопку (если передана) блокирует на время. + */ +(function () { + function ok() { if (window.LS && LS.toast) LS.toast('Сохранено в «Мои материалы»', 'success'); } + function err(e) { if (window.LS && LS.toast) LS.toast((e && e.message) || 'Ошибка сохранения', 'error'); } + + async function note(o, btn) { + o = o || {}; + if (!String(o.body || '').trim() && !String(o.title || '').trim()) { err({ message: 'Пустая заметка' }); return; } + if (btn) btn.disabled = true; + try { + await LS.saveMaterial({ kind: 'note', title: o.title || '', body: o.body || '', sourceTitle: o.sourceTitle || null }); + ok(); + } catch (e) { err(e); } finally { if (btn) btn.disabled = false; } + } + + async function link(o, btn) { + o = o || {}; + if (!o.url) { err({ message: 'Нет ссылки' }); return; } + if (btn) btn.disabled = true; + try { + await LS.saveMaterial({ kind: 'link', title: o.title || '', url: o.url, sourceTitle: o.sourceTitle || null }); + ok(); + } catch (e) { err(e); } finally { if (btn) btn.disabled = false; } + } + + async function image(o, btn) { + o = o || {}; + if (btn) btn.disabled = true; + try { + let url = o.url; + if (o.blob) { + const fd = new FormData(); + fd.append('file', o.blob, o.name || 'image.png'); + const up = await LS.uploadFile(fd); + url = LS.downloadFileUrl(up.id); + } + if (!url) throw new Error('Нет изображения'); + await LS.saveMaterial({ kind: 'image', title: o.title || '', url: url, sourceTitle: o.sourceTitle || null }); + ok(); + } catch (e) { err(e); } finally { if (btn) btn.disabled = false; } + } + + window.MaterialSave = { note: note, link: link, image: image }; +})();