bdc8075c3d
Клик по картинке/доске больше не открывает новую вкладку, а показывает материал в окне на странице (LS.modal size lg): изображение, кнопки «Скачать» и «В новой вкладке». Кнопка действия «Открыть» заменена на «Просмотр» (иконка eye). Ссылки по-прежнему открываются внешне. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
490 lines
28 KiB
HTML
490 lines
28 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8"/>
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||
<title>Мои материалы — LearnSpace</title>
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml"/>
|
||
<link href="https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;800&family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"/>
|
||
<link rel="stylesheet" href="/css/ls.css"/>
|
||
<script src="https://unpkg.com/lucide@0.469.0/dist/umd/lucide.min.js"></script>
|
||
<style>
|
||
.mm-main { padding: 28px 24px; max-width: 1100px; margin: 0 auto; width: 100%; }
|
||
.mm-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
|
||
.mm-title { font-family: 'Unbounded', sans-serif; font-weight: 800; font-size: 1.5rem; color: var(--text); }
|
||
.mm-sub { color: var(--text-3); font-size: 0.9rem; margin-bottom: 16px; }
|
||
.mm-toolbar { display: flex; gap: 10px; flex-wrap: wrap; align-items: center; margin-bottom: 14px; }
|
||
.mm-search { flex: 1; min-width: 180px; padding: 8px 12px; border: 1px solid var(--border); border-radius: 9px; font: inherit; background: var(--surface); color: var(--text); }
|
||
.mm-kind { padding: 8px 10px; border: 1px solid var(--border); border-radius: 9px; font: inherit; background: var(--surface); color: var(--text); }
|
||
.mm-cols { display: flex; gap: 8px; flex-wrap: wrap; margin-bottom: 18px; }
|
||
.mm-chip { display: inline-flex; align-items: center; gap: 6px; padding: 6px 12px; border: 1px solid var(--border); border-radius: 99px; background: var(--surface); cursor: pointer; font-size: .8rem; font-weight: 600; color: var(--text-2); }
|
||
.mm-chip:hover { border-color: var(--violet); color: var(--violet); }
|
||
.mm-chip.active { background: var(--violet); border-color: var(--violet); color: #fff; }
|
||
.mm-chip-count { font-size: .7rem; opacity: .7; }
|
||
.mm-chip-edit { display: inline-flex; opacity: .65; margin-left: 2px; }
|
||
.mm-chip-edit svg { width: 12px; height: 12px; }
|
||
.mm-chip-edit:hover { opacity: 1; }
|
||
.mm-chip-add { border-style: dashed; }
|
||
.mm-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); gap: 16px; }
|
||
.mm-card { background: var(--surface); border: 1px solid var(--border); border-radius: 14px; overflow: hidden; display: flex; flex-direction: column; position: relative; transition: border-color .14s, box-shadow .14s, transform .14s; }
|
||
.mm-card:hover { border-color: rgba(155,93,229,0.3); box-shadow: 0 8px 24px rgba(15,23,42,0.08); transform: translateY(-2px); }
|
||
.mm-card-media { background: #f1f5f9; aspect-ratio: 16/10; display: flex; align-items: center; justify-content: center; overflow: hidden; }
|
||
.mm-card-media img { width: 100%; height: 100%; object-fit: contain; background: #fff; }
|
||
.mm-card-note { padding: 14px 16px; font-size: 0.84rem; color: var(--text-2); white-space: pre-wrap; word-break: break-word; max-height: 180px; overflow: auto; line-height: 1.55; flex: 1; }
|
||
.mm-card-body { padding: 12px 14px; border-top: 1px solid var(--border); }
|
||
.mm-card-title { font-weight: 700; font-size: 0.86rem; color: var(--text); margin-bottom: 3px; }
|
||
.mm-card-meta { font-size: 0.74rem; color: var(--text-3); }
|
||
.mm-card-actions { display: flex; gap: 6px; margin-top: 10px; flex-wrap: wrap; align-items: center; }
|
||
.mm-btn { display: inline-flex; align-items: center; gap: 5px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 8px; background: var(--surface); cursor: pointer; font-size: 0.76rem; font-weight: 600; color: var(--text-2); text-decoration: none; transition: border-color .12s, color .12s; }
|
||
.mm-btn:hover { border-color: var(--violet); color: var(--violet); }
|
||
.mm-btn.danger:hover { border-color: #ef4444; color: #ef4444; }
|
||
.mm-btn svg { width: 13px; height: 13px; }
|
||
.mm-move { padding: 4px 6px; border: 1px solid var(--border); border-radius: 8px; font-size: .74rem; background: var(--surface); color: var(--text-2); max-width: 130px; margin-right: auto; }
|
||
.mm-kind-chip { display: inline-flex; align-items: center; gap: 4px; font-size: 0.66rem; font-weight: 700; padding: 3px 9px; border-radius: 99px; background: rgba(155,93,229,0.12); color: var(--violet); margin-bottom: 8px; }
|
||
.mm-kind-chip svg { width: 11px; height: 11px; }
|
||
.mm-card-link { display: flex; align-items: center; gap: 12px; padding: 18px 16px; background: linear-gradient(135deg, rgba(155,93,229,0.10), rgba(6,182,212,0.08)); text-decoration: none; }
|
||
.mm-card-link:hover { background: linear-gradient(135deg, rgba(155,93,229,0.16), rgba(6,182,212,0.12)); }
|
||
.mm-card-link-ic { width: 42px; height: 42px; border-radius: 12px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; background: #fff; color: var(--violet); box-shadow: 0 2px 6px rgba(155,93,229,0.18); }
|
||
.mm-card-link-ic svg { width: 20px; height: 20px; }
|
||
.mm-card-link-meta { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
|
||
.mm-card-link-label { font-size: 0.82rem; font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.mm-card-link-url { font-size: 0.7rem; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.mm-btn.primary { background: var(--violet); border-color: var(--violet); color: #fff; }
|
||
.mm-btn.primary:hover { background: #7c3aed; border-color: #7c3aed; color: #fff; }
|
||
.mm-viewer { display: flex; align-items: center; justify-content: center; background: #f1f5f9; border-radius: 10px; padding: 8px; }
|
||
.mm-viewer img { max-width: 100%; max-height: 68vh; object-fit: contain; border-radius: 6px; }
|
||
.mm-viewer-note { white-space: pre-wrap; word-break: break-word; line-height: 1.6; font-size: 0.9rem; color: var(--text-2); }
|
||
.mm-empty { padding: 60px 20px; text-align: center; color: var(--text-3); }
|
||
.mm-empty svg { width: 38px; height: 38px; opacity: 0.4; margin-bottom: 12px; }
|
||
@media (max-width: 640px) { .mm-grid { grid-template-columns: 1fr 1fr; } .mm-main { padding: 18px 14px; } }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="app-layout">
|
||
<aside class="sidebar" id="app-sidebar"></aside>
|
||
<main class="sb-content">
|
||
<div class="mm-main">
|
||
<div class="mm-head">
|
||
<span class="mm-title">Мои материалы</span>
|
||
<button class="mm-btn" style="margin-left:auto" onclick="createNote()"><i data-lucide="plus"></i> Заметка</button>
|
||
<button class="mm-btn" onclick="openDrawModal({})"><i data-lucide="pen-tool"></i> Рисунок</button>
|
||
</div>
|
||
<div class="mm-sub">Сохранённые с уроков: страницы доски, заметки и вложения. Хранятся у вас и не пропадают, даже если урок удалят.</div>
|
||
<div class="mm-toolbar">
|
||
<input class="mm-search" id="mm-search" placeholder="Поиск по материалам…" oninput="onSearch(this.value)" />
|
||
<select class="mm-kind" id="mm-kind" onchange="onKind(this.value)">
|
||
<option value="all">Все типы</option>
|
||
<option value="board">Доска</option>
|
||
<option value="image">Изображения</option>
|
||
<option value="note">Заметки</option>
|
||
<option value="link">Ссылки</option>
|
||
</select>
|
||
</div>
|
||
<div class="mm-cols" id="mm-cols"></div>
|
||
<div class="mm-grid" id="mm-grid"><div class="mm-empty">Загрузка…</div></div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
|
||
<script src="/js/api.js"></script>
|
||
<script src="/js/sidebar.js"></script>
|
||
<script src="/js/notifications.js"></script>
|
||
<script src="/js/mobile.js"></script>
|
||
<script src="/js/svg-sanitize.js"></script>
|
||
<script src="/js/svg-draw.js"></script>
|
||
<script>
|
||
(function () {
|
||
const _ip = LS.initPage() || {};
|
||
const _canShare = !!(_ip.isTeacher || _ip.isAdmin);
|
||
|
||
function esc(s) { return LS.escapeHtml(String(s || '')); }
|
||
function fmtDate(s) {
|
||
if (!s) return '';
|
||
try { const d = new Date(s.replace(' ', 'T') + (s.includes('Z') ? '' : 'Z'));
|
||
return d.toLocaleDateString('ru', { day: 'numeric', month: 'short', year: 'numeric' }); } catch (e) { return ''; }
|
||
}
|
||
|
||
const KIND_LABEL = { board: 'Доска', note: 'Заметка', link: 'Ссылка', image: 'Изображение' };
|
||
const KIND_ICON = { board: 'layout-template', note: 'sticky-note', link: 'link', image: 'image' };
|
||
const PENCIL = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z"/></svg>';
|
||
|
||
/* Human-readable label for a saved link (host for external, type for internal) */
|
||
function linkLabel(u) {
|
||
u = String(u || '');
|
||
if (/^https?:\/\//i.test(u)) { try { return new URL(u).host; } catch (e) { return u; } }
|
||
if (u.startsWith('/textbook')) return 'Раздел учебника';
|
||
if (u.startsWith('/course')) return 'Курс';
|
||
if (u.startsWith('/lesson') || u.startsWith('/online-lesson')) return 'Урок';
|
||
if (u.startsWith('/exam')) return 'Экзамен';
|
||
if (u.startsWith('/lab')) return 'Лаборатория';
|
||
return 'Ссылка';
|
||
}
|
||
let _mats = [];
|
||
let _cols = [];
|
||
const _filter = { col: 'all', kind: 'all', q: '' };
|
||
|
||
/* ── Move-to-collection select ── */
|
||
function moveSelect(m) {
|
||
const opts = ['<option value=""' + (!m.collection_id ? ' selected' : '') + '>Без папки</option>']
|
||
.concat(_cols.map(c => `<option value="${c.id}"${m.collection_id === c.id ? ' selected' : ''}>${esc(c.name)}</option>`));
|
||
return `<select class="mm-move" title="Папка" onchange="moveMaterial(${m.id}, this.value)">${opts.join('')}</select>`;
|
||
}
|
||
|
||
function card(m) {
|
||
const kind = KIND_LABEL[m.kind] || m.kind;
|
||
const meta = `${esc(m.source_title || '')}${m.source_title ? ' · ' : ''}${fmtDate(m.created_at)}`;
|
||
const chip = `<span class="mm-kind-chip"><i data-lucide="${KIND_ICON[m.kind] || 'tag'}"></i>${kind}</span>`;
|
||
const del = `<button class="mm-btn danger" onclick="delMaterial(${m.id})" title="Удалить"><i data-lucide="trash-2"></i></button>`;
|
||
const edit = `<button class="mm-btn" onclick="editMaterial(${m.id})" title="Изменить"><i data-lucide="pencil"></i></button>`;
|
||
const ann = (m.kind === 'board' || m.kind === 'image')
|
||
? `<button class="mm-btn" onclick="annotate(${m.id})" title="Аннотировать (рисовать поверх)"><i data-lucide="pencil-ruler"></i></button>` : '';
|
||
const fc = (m.kind === 'note')
|
||
? `<button class="mm-btn" onclick="toFlashcard(${m.id})" title="В флешкарты"><i data-lucide="copy"></i></button>` : '';
|
||
const sh = _canShare
|
||
? `<button class="mm-btn" onclick="openShareModal(${m.id})" title="Раздать ученикам"><i data-lucide="send"></i></button>` : '';
|
||
const mv = moveSelect(m);
|
||
|
||
if (m.kind === 'board' || m.kind === 'image') {
|
||
return `<div class="mm-card">
|
||
<a class="mm-card-media" href="${esc(m.url)}" onclick="openViewer(${m.id});return false;"><img src="${esc(m.url)}" alt="" loading="lazy"/></a>
|
||
<div class="mm-card-body">
|
||
${chip}
|
||
<div class="mm-card-title">${esc(m.title || kind)}</div>
|
||
<div class="mm-card-meta">${meta}</div>
|
||
<div class="mm-card-actions">
|
||
${mv}
|
||
<button class="mm-btn" onclick="openViewer(${m.id})" title="Просмотр"><i data-lucide="eye"></i></button>
|
||
<a class="mm-btn" href="${esc(m.url)}" download title="Скачать"><i data-lucide="download"></i></a>
|
||
${ann}${sh}${edit}${del}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
if (m.kind === 'link') {
|
||
return `<div class="mm-card">
|
||
<a class="mm-card-link" href="${esc(m.url)}" target="_blank" rel="noopener" title="${esc(m.url)}">
|
||
<span class="mm-card-link-ic"><i data-lucide="link"></i></span>
|
||
<span class="mm-card-link-meta">
|
||
<span class="mm-card-link-label">${esc(linkLabel(m.url))}</span>
|
||
<span class="mm-card-link-url">${esc(m.url)}</span>
|
||
</span>
|
||
</a>
|
||
<div class="mm-card-body">
|
||
${chip}
|
||
<div class="mm-card-title">${esc(m.title || kind)}</div>
|
||
<div class="mm-card-meta">${meta}</div>
|
||
<div class="mm-card-actions">
|
||
${mv}
|
||
<a class="mm-btn primary" href="${esc(m.url)}" target="_blank" rel="noopener"><i data-lucide="external-link"></i> Открыть</a>
|
||
${sh}${edit}${del}
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
// note
|
||
return `<div class="mm-card">
|
||
<div class="mm-card-note">${esc(m.body || '')}</div>
|
||
<div class="mm-card-body">
|
||
${chip}
|
||
<div class="mm-card-title">${esc(m.title || kind)}</div>
|
||
<div class="mm-card-meta">${meta}</div>
|
||
<div class="mm-card-actions">${mv}${fc}${sh}${edit}${del}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
/* ── Collections bar ── */
|
||
function chip(key, label, count, editId) {
|
||
const active = _filter.col === key ? ' active' : '';
|
||
const ed = editId ? `<span class="mm-chip-edit" onclick="event.stopPropagation();editCollection(${editId})" title="Изменить папку">${PENCIL}</span>` : '';
|
||
return `<button class="mm-chip${active}" onclick="setCol('${key}')">${esc(label)} <span class="mm-chip-count">${count}</span>${ed}</button>`;
|
||
}
|
||
function renderCols() {
|
||
const bar = document.getElementById('mm-cols');
|
||
const noneCount = _mats.filter(m => !m.collection_id).length;
|
||
let html = chip('all', 'Все', _mats.length);
|
||
_cols.forEach(c => { html += chip(String(c.id), c.name, c.count, c.id); });
|
||
if (noneCount) html += chip('none', 'Без папки', noneCount);
|
||
html += `<button class="mm-chip mm-chip-add" onclick="createCollection()">+ папка</button>`;
|
||
bar.innerHTML = html;
|
||
}
|
||
|
||
function filtered() {
|
||
return _mats.filter(m => {
|
||
if (_filter.col === 'none' && m.collection_id) return false;
|
||
if (_filter.col !== 'all' && _filter.col !== 'none' && String(m.collection_id) !== _filter.col) return false;
|
||
if (_filter.kind !== 'all' && m.kind !== _filter.kind) return false;
|
||
if (_filter.q) {
|
||
const hay = ((m.title || '') + ' ' + (m.body || '') + ' ' + (m.source_title || '') + ' ' + (m.url || '') + ' ' + (m.tags || '')).toLowerCase();
|
||
if (!hay.includes(_filter.q)) return false;
|
||
}
|
||
return true;
|
||
});
|
||
}
|
||
|
||
function renderGrid() {
|
||
const grid = document.getElementById('mm-grid');
|
||
if (!_mats.length) {
|
||
grid.innerHTML = `<div class="mm-empty" style="grid-column:1/-1">
|
||
<i data-lucide="folder-open"></i>
|
||
<p>Пока пусто. На уроке или в «Мои уроки» нажмите «К себе»/«Область», или создайте заметку.</p>
|
||
</div>`;
|
||
lucide.createIcons();
|
||
return;
|
||
}
|
||
const rows = filtered();
|
||
grid.innerHTML = rows.length
|
||
? rows.map(card).join('')
|
||
: `<div class="mm-empty" style="grid-column:1/-1"><i data-lucide="search-x"></i><p>Ничего не найдено</p></div>`;
|
||
lucide.createIcons();
|
||
}
|
||
|
||
async function load() {
|
||
try {
|
||
const data = await LS.listMaterials();
|
||
_mats = data.materials || [];
|
||
_cols = data.collections || [];
|
||
renderCols();
|
||
renderGrid();
|
||
} catch (e) {
|
||
document.getElementById('mm-grid').innerHTML = `<div class="mm-empty" style="grid-column:1/-1">Ошибка загрузки</div>`;
|
||
}
|
||
}
|
||
|
||
/* ── Filters ── */
|
||
function setCol(key) { _filter.col = key; renderCols(); renderGrid(); }
|
||
function onKind(v) { _filter.kind = v; renderGrid(); }
|
||
function onSearch(v) { _filter.q = (v || '').trim().toLowerCase(); renderGrid(); }
|
||
window.setCol = setCol; window.onKind = onKind; window.onSearch = onSearch;
|
||
|
||
/* ── Material actions ── */
|
||
async function moveMaterial(id, cid) {
|
||
try { await LS.updateMaterial(id, { collection_id: cid || null }); await load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
}
|
||
window.moveMaterial = moveMaterial;
|
||
|
||
async function delMaterial(id) {
|
||
if (!confirm('Удалить этот материал?')) return;
|
||
try { await LS.deleteMaterial(id); load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
}
|
||
window.delMaterial = delMaterial;
|
||
|
||
const FLD = 'padding:9px 12px;border:1px solid var(--border);border-radius:9px;font:inherit;width:100%;box-sizing:border-box';
|
||
function createNote() {
|
||
const content = `<div style="display:flex;flex-direction:column;gap:10px">
|
||
<input id="mm-nt-title" placeholder="Заголовок (необязательно)" style="${FLD}" />
|
||
<textarea id="mm-nt-body" rows="7" placeholder="Текст заметки…" style="${FLD};resize:vertical"></textarea>
|
||
</div>`;
|
||
const m = LS.modal({ title: 'Новая заметка', content, size: 'sm', actions: [
|
||
{ label: 'Отмена', onClick: () => m.close() },
|
||
{ label: 'Создать', primary: true, onClick: async () => {
|
||
const title = m.body.querySelector('#mm-nt-title').value.trim();
|
||
const text = m.body.querySelector('#mm-nt-body').value.trim();
|
||
if (!text && !title) { LS.toast('Введите текст заметки', 'warn'); return; }
|
||
const col = _filter.col !== 'all' && _filter.col !== 'none' ? Number(_filter.col) : null;
|
||
try { await LS.saveMaterial({ kind: 'note', title, body: text, collection_id: col }); m.close(); load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
] });
|
||
}
|
||
window.createNote = createNote;
|
||
|
||
function editMaterial(id) {
|
||
const mt = _mats.find(x => x.id === id);
|
||
if (!mt) return;
|
||
const isNote = mt.kind === 'note';
|
||
const content = `<div style="display:flex;flex-direction:column;gap:10px">
|
||
<input id="mm-ed-title" value="${esc(mt.title || '')}" placeholder="Заголовок" style="${FLD}" />
|
||
${isNote ? `<textarea id="mm-ed-body" rows="7" style="${FLD};resize:vertical">${esc(mt.body || '')}</textarea>` : ''}
|
||
</div>`;
|
||
const m = LS.modal({ title: 'Изменить', content, size: 'sm', actions: [
|
||
{ label: 'Отмена', onClick: () => m.close() },
|
||
{ label: 'Сохранить', primary: true, onClick: async () => {
|
||
const data = { title: m.body.querySelector('#mm-ed-title').value.trim() };
|
||
if (isNote) data.body = m.body.querySelector('#mm-ed-body').value;
|
||
try { await LS.updateMaterial(id, data); m.close(); load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
] });
|
||
}
|
||
window.editMaterial = editMaterial;
|
||
|
||
/* ── Просмотр материала в модалке (лайтбокс) ── */
|
||
function openViewer(id) {
|
||
const mt = _mats.find(x => x.id === id);
|
||
if (!mt) return false;
|
||
const kind = KIND_LABEL[mt.kind] || mt.kind;
|
||
let body;
|
||
if (mt.kind === 'image' || mt.kind === 'board') {
|
||
body = `<div class="mm-viewer"><img src="${esc(mt.url)}" alt="${esc(mt.title || '')}" /></div>`;
|
||
} else if (mt.kind === 'note') {
|
||
body = `<div class="mm-viewer-note">${esc(mt.body || '')}</div>`;
|
||
} else {
|
||
body = `<div class="mm-viewer-note"><a href="${esc(mt.url)}" target="_blank" rel="noopener" style="color:var(--violet)">${esc(mt.url)}</a></div>`;
|
||
}
|
||
const actions = [];
|
||
if (mt.url && (mt.kind === 'image' || mt.kind === 'board')) {
|
||
actions.push({ label: 'Скачать', onClick: () => {
|
||
const ext = (String(mt.url).match(/\.(png|jpe?g|gif|webp)(?:$|\?)/i) || [])[1] || 'png';
|
||
const name = (mt.title || 'material').slice(0, 60).replace(/[\\/:*?"<>|]/g, '_') + '.' + ext;
|
||
const a = document.createElement('a'); a.href = mt.url; a.download = name;
|
||
document.body.appendChild(a); a.click(); a.remove();
|
||
} });
|
||
}
|
||
if (mt.url) actions.push({ label: 'В новой вкладке', onClick: () => window.open(mt.url, '_blank', 'noopener') });
|
||
actions.push({ label: 'Закрыть', primary: true, onClick: () => m.close() });
|
||
const m = LS.modal({ title: mt.title || kind, content: body, size: 'lg', actions });
|
||
return false;
|
||
}
|
||
window.openViewer = openViewer;
|
||
|
||
/* ── Collection CRUD ── */
|
||
function createCollection() {
|
||
const content = `<input id="mm-col-name" placeholder="Название папки" style="${FLD}" />`;
|
||
const m = LS.modal({ title: 'Новая папка', content, size: 'sm', actions: [
|
||
{ label: 'Отмена', onClick: () => m.close() },
|
||
{ label: 'Создать', primary: true, onClick: async () => {
|
||
const name = m.body.querySelector('#mm-col-name').value.trim();
|
||
if (!name) { LS.toast('Введите название', 'warn'); return; }
|
||
try { await LS.createMaterialCollection({ name }); m.close(); load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
] });
|
||
}
|
||
window.createCollection = createCollection;
|
||
|
||
function editCollection(id) {
|
||
const col = _cols.find(c => c.id === id);
|
||
if (!col) return;
|
||
const content = `<input id="mm-col-name" value="${esc(col.name)}" placeholder="Название папки" style="${FLD}" />`;
|
||
const m = LS.modal({ title: 'Папка', content, size: 'sm', actions: [
|
||
{ label: 'Удалить', onClick: async () => {
|
||
if (!confirm('Удалить папку? Материалы останутся (станут «Без папки»).')) return;
|
||
try { await LS.deleteMaterialCollection(id); m.close(); if (_filter.col === String(id)) _filter.col = 'all'; load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
{ label: 'Отмена', onClick: () => m.close() },
|
||
{ label: 'Сохранить', primary: true, onClick: async () => {
|
||
const name = m.body.querySelector('#mm-col-name').value.trim();
|
||
if (!name) { LS.toast('Введите название', 'warn'); return; }
|
||
try { await LS.updateMaterialCollection(id, { name }); m.close(); load(); }
|
||
catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
] });
|
||
}
|
||
window.editCollection = editCollection;
|
||
|
||
/* ── Рисование / аннотации (SVG-рисовалка) ── */
|
||
function ensureDrawStyles() {
|
||
if (document.getElementById('mmdraw-style')) return;
|
||
const s = document.createElement('style');
|
||
s.id = 'mmdraw-style';
|
||
s.textContent = `
|
||
.mmdraw-ov { position:fixed; inset:0; z-index:99999; background:rgba(15,12,30,.72); display:flex; align-items:center; justify-content:center; padding:16px; }
|
||
.mmdraw-box { background:#fff; border-radius:14px; max-width:94vw; max-height:94vh; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 20px 60px rgba(0,0,0,.4); }
|
||
.mmdraw-host { padding:10px; overflow:auto; }
|
||
.mmdraw-actions { display:flex; justify-content:flex-end; gap:8px; padding:12px 16px; border-top:1px solid #eee; }
|
||
.mmdraw-actions button { padding:8px 16px; border-radius:9px; border:1px solid #e2e8f0; background:#fff; font-weight:700; font-size:.85rem; cursor:pointer; color:#475569; }
|
||
.mmdraw-actions button.primary { background:#8b5cf6; border-color:#8b5cf6; color:#fff; }
|
||
`;
|
||
document.head.appendChild(s);
|
||
}
|
||
function openDrawModal(o) {
|
||
o = o || {};
|
||
if (!window.SvgDraw) { LS.toast('Редактор не загружен', 'error'); return; }
|
||
ensureDrawStyles();
|
||
const ov = document.createElement('div');
|
||
ov.className = 'mmdraw-ov';
|
||
ov.innerHTML = '<div class="mmdraw-box"><div class="mmdraw-host"></div><div class="mmdraw-actions"><button data-a="cancel">Отмена</button><button data-a="save" class="primary">Сохранить</button></div></div>';
|
||
document.body.appendChild(ov);
|
||
const host = ov.querySelector('.mmdraw-host');
|
||
const ed = SvgDraw.mount(host, { bgImage: o.bgImage || null, width: 800, height: 500, onChange: function () {} });
|
||
function close() { try { ed.destroy(); } catch (e) {} ov.remove(); }
|
||
ov.querySelector('[data-a="cancel"]').onclick = close;
|
||
ov.querySelector('[data-a="save"]').onclick = function () {
|
||
const btn = this; btn.disabled = true;
|
||
ed.exportFlatBlob(async function (blob) {
|
||
try {
|
||
if (!blob) throw new Error('Не удалось сохранить рисунок');
|
||
const fd = new FormData(); fd.append('file', blob, 'drawing.png');
|
||
const up = await LS.uploadMaterialFile(fd);
|
||
await LS.saveMaterial({ kind: 'image', title: o.title || 'Рисунок', url: up.url, sourceTitle: o.sourceTitle || null });
|
||
close(); load(); LS.toast('Сохранено в «Мои материалы»', 'success');
|
||
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); btn.disabled = false; }
|
||
});
|
||
};
|
||
}
|
||
window.openDrawModal = openDrawModal;
|
||
|
||
function annotate(id) {
|
||
const mt = _mats.find(x => x.id === id);
|
||
if (!mt) return;
|
||
openDrawModal({ bgImage: mt.url, title: (mt.title || 'Рисунок') + ' (разметка)', sourceTitle: mt.source_title });
|
||
}
|
||
window.annotate = annotate;
|
||
|
||
/* ── Заметка → флешкарта ── */
|
||
async function toFlashcard(id) {
|
||
const mt = _mats.find(x => x.id === id);
|
||
if (!mt) return;
|
||
let decks = [];
|
||
try { const d = await LS.fcListDecks(); decks = d.decks || []; } catch (e) {}
|
||
const opts = ['<option value="__new">+ Новая колода «Из материалов»</option>']
|
||
.concat(decks.map(d => `<option value="${d.id}">${esc(d.title)}</option>`)).join('');
|
||
const front = (mt.title || '').trim() || (mt.body || '').slice(0, 80);
|
||
const back = (mt.body || '').trim() || (mt.title || '');
|
||
const content = `<div style="display:flex;flex-direction:column;gap:8px">
|
||
<label style="font-size:.8rem;color:var(--text-3)">Колода</label>
|
||
<select id="fc-deck" style="${FLD}">${opts}</select>
|
||
<label style="font-size:.8rem;color:var(--text-3)">Вопрос (лицевая сторона)</label>
|
||
<input id="fc-front" value="${esc(front)}" style="${FLD}" />
|
||
<label style="font-size:.8rem;color:var(--text-3)">Ответ (оборот)</label>
|
||
<textarea id="fc-back" rows="4" style="${FLD};resize:vertical">${esc(back)}</textarea>
|
||
</div>`;
|
||
const m = LS.modal({ title: 'В флешкарты', content, size: 'sm', actions: [
|
||
{ label: 'Отмена', onClick: () => m.close() },
|
||
{ label: 'Создать карточку', primary: true, onClick: async () => {
|
||
try {
|
||
let deckId = m.body.querySelector('#fc-deck').value;
|
||
if (deckId === '__new') { const nd = await LS.fcCreateDeck({ title: 'Из материалов' }); deckId = nd.id; }
|
||
await LS.fcAddCard(deckId, { front: m.body.querySelector('#fc-front').value, back: m.body.querySelector('#fc-back').value });
|
||
m.close(); LS.toast('Карточка добавлена в флешкарты', 'success');
|
||
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
] });
|
||
}
|
||
window.toFlashcard = toFlashcard;
|
||
|
||
/* ── Раздатка: учитель → класс (копия ученикам) ── */
|
||
async function openShareModal(id) {
|
||
let classes = [];
|
||
try { classes = await LS.getClasses(); } catch (e) {}
|
||
if (!Array.isArray(classes) || !classes.length) { LS.toast('Нет классов для раздачи', 'warn'); return; }
|
||
const opts = classes.map(c => `<option value="${c.id}">${esc(c.name)}</option>`).join('');
|
||
const content = `<div style="display:flex;flex-direction:column;gap:8px">
|
||
<label style="font-size:.8rem;color:var(--text-3)">Класс</label>
|
||
<select id="sh-class" style="${FLD}">${opts}</select>
|
||
<div style="font-size:.78rem;color:var(--text-3)">Копия материала появится у всех учеников класса (с уведомлением).</div>
|
||
</div>`;
|
||
const m = LS.modal({ title: 'Раздать материал', content, size: 'sm', actions: [
|
||
{ label: 'Отмена', onClick: () => m.close() },
|
||
{ label: 'Раздать', primary: true, onClick: async () => {
|
||
try {
|
||
const r = await LS.shareMaterial(id, { classId: Number(m.body.querySelector('#sh-class').value) });
|
||
m.close(); LS.toast('Отправлено ученикам: ' + (r.sent || 0), 'success');
|
||
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); }
|
||
} },
|
||
] });
|
||
}
|
||
window.openShareModal = openShareModal;
|
||
|
||
load();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|