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); +})();