From 43fe90d601206dd41edef295dde88a75e0aba9da Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 4 Jun 2026 12:17:08 +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=202)=20=E2=80=94=20=D0=B8?= =?UTF-8?q?=D1=81=D1=82=D0=BE=D1=87=D0=BD=D0=B8=D0=BA=20=C2=AB=D0=A3=D1=87?= =?UTF-8?q?=D0=B5=D0=B1=D0=BD=D0=B8=D0=BA=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Сервер инжектит в /textbook/ плавающую кнопку «В мои материалы» (js/textbook-clip.js + material-save.js рядом с deep-link). Сохраняет текущий § как ссылку /textbook/#sec- (заголовок = название §, источник = глава). Скрыта в classroom-embed и для неавторизованных. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/server.js | 6 ++++- frontend/js/textbook-clip.js | 49 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 frontend/js/textbook-clip.js diff --git a/backend/src/server.js b/backend/src/server.js index 3c932e0..2e26525 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -424,7 +424,11 @@ const EMBED_INJECT = ` // Always injected (plain + embed): deep-link helper so /textbook/#sec-pN // actually opens § N. Without it the page ignores the hash and shows §1. -const DEEPLINK_INJECT = `\n\n`; +const DEEPLINK_INJECT = ` + + + +`; function _renderTextbook(filePath, slug, embed) { let stat; try { stat = fs.statSync(filePath); } catch { return null; } diff --git a/frontend/js/textbook-clip.js b/frontend/js/textbook-clip.js new file mode 100644 index 0000000..12ca674 --- /dev/null +++ b/frontend/js/textbook-clip.js @@ -0,0 +1,49 @@ +'use strict'; +/* textbook-clip.js — floating «В мои материалы» button on textbook pages. + * Injected by the server into /textbook/. Saves the currently open + * paragraph as a link (/textbook/#sec-) into the student's + * personal materials. Reuses MaterialSave (material-save.js) + LS (api.js). + * Hidden inside the classroom embed (iframe) to avoid clutter. */ +(function () { + if (window.parent !== window) return; // skip in classroom embed + if (!window.LS || !LS.getToken || !LS.getToken()) return; // only for logged-in users + + function chapterTitle() { + return (document.title || 'Учебник').replace(/\s*[—|].*$/, '').replace(/\s*·\s*LearnSpace.*$/i, '').trim() || 'Учебник'; + } + function sectionTitle() { + const h = document.querySelector('.sec.active .sec-h'); + if (h && h.textContent.trim()) return h.textContent.trim(); + const n = document.querySelector('.psel-card.active .psel-name'); + if (n && n.textContent.trim()) return n.textContent.trim(); + return (document.title || 'Тема').split('·').pop().trim() || 'Тема'; + } + function activeId() { + const c = document.querySelector('.psel-card.active'); + return c && c.dataset ? c.dataset.id : null; + } + + function save(btn) { + if (!window.MaterialSave) { if (LS.toast) LS.toast('Модуль не загружен', 'error'); return; } + const slug = location.pathname.replace(/^\/textbook\//, '').replace(/\/+$/, ''); + if (!slug) return; + const id = activeId(); + const hash = id ? '#sec-' + id : (location.hash || ''); + MaterialSave.link({ title: sectionTitle(), url: '/textbook/' + slug + hash, sourceTitle: chapterTitle() }, btn); + } + + function build() { + if (document.getElementById('__ls_clip') || !document.body) return; + const btn = document.createElement('button'); + btn.id = '__ls_clip'; + btn.type = 'button'; + btn.title = 'Сохранить эту тему в «Мои материалы»'; + btn.innerHTML = 'В мои материалы'; + btn.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:9000;display:inline-flex;align-items:center;gap:7px;padding:10px 14px;border-radius:99px;border:none;background:#8b5cf6;color:#fff;font:600 13px/1 Inter,system-ui,sans-serif;cursor:pointer;box-shadow:0 6px 20px rgba(139,92,246,.4)'; + btn.addEventListener('click', function () { save(btn); }); + document.body.appendChild(btn); + } + + if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { setTimeout(build, 300); }); + else setTimeout(build, 300); +})();