From 61e30bedf9395c2414b32145b958c94bd9648547 Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 4 Jun 2026 12:13:44 +0300 Subject: [PATCH] =?UTF-8?q?feat(materials):=20=D0=A4=D0=B0=D0=B7=D0=B0=203?= =?UTF-8?q?=20(=D1=87=D0=B0=D1=81=D1=82=D1=8C=201)=20=E2=80=94=20=D1=83?= =?UTF-8?q?=D0=BD=D0=B8=D0=B2=D0=B5=D1=80=D1=81=D0=B0=D0=BB=D1=8C=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B1=D1=83=D1=84=D0=B5=D1=80=20+=20=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=20=C2=AB=D0=AD=D0=BA?= =?UTF-8?q?=D0=B7=D0=B0=D0=BC=D0=B5=D0=BD=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /js/material-save.js — общий модуль: MaterialSave.note/link/image поверх LS.saveMaterial/uploadFile. - exam-prep/task-card.js: кнопка «В мои материалы» на карточке задачи (вариант/тренажёр/тема) — сохраняет условие+ответ+решение как заметку (sourceTitle = название экзамена). В пробнике скрыта. - Подключён material-save.js на 4 страницах экзамена. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/exam-prep-mock.html | 1 + frontend/exam-prep-practice.html | 1 + frontend/exam-prep-topics.html | 1 + frontend/exam-prep-variants.html | 1 + frontend/js/exam-prep/task-card.js | 38 ++++++++++++++++----- frontend/js/material-save.js | 55 ++++++++++++++++++++++++++++++ 6 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 frontend/js/material-save.js 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 }; +})();