feat(materials): серверные миниатюры (sharp) + пагинация рендера списка
Миниатюры: uploadPersonalFile генерирует webp ≤480px (sharp), возвращает {url, thumbUrl}; колонка thumb_url (мигр.074); грид рисует <img> на миниатюре, просмотр/скачивание/аннотация — полный url. Ссылочная чистка матчит url И thumb_url; share копирует thumb; квота учитывает файл+миниатюру. Сейверы board-clip/material-save/textbook-clip/draw пробрасывают thumbUrl.
Пагинация: клиент рендерит PAGE_SIZE=60 карточек + «Показать ещё» (сброс на смену фильтра), сохраняя клиентский поиск/сортировку над полным списком.
Тесты: materials.test.js 16→19. План V2 выполнен.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+23
-12
@@ -89,6 +89,7 @@
|
||||
.mm-swatch-none { background: repeating-linear-gradient(45deg,#fff,#fff 4px,#e2e8f0 4px,#e2e8f0 8px); }
|
||||
.mm-preview { min-height: 22px; padding: 8px 10px; border: 1px dashed var(--border); border-radius: 8px; font-size: .86rem; color: var(--text-2); background: rgba(148,163,184,0.06); white-space: pre-wrap; word-break: break-word; line-height: 1.5; }
|
||||
.mm-preview:empty::before { content: 'Превью формул появится здесь…'; color: var(--text-3); }
|
||||
.mm-more { display: flex; justify-content: center; padding: 8px 0 2px; }
|
||||
@media (max-width: 768px) { .mm-check { opacity: .85; } }
|
||||
@media (max-width: 768px) {
|
||||
.mm-body { flex-direction: column; }
|
||||
@@ -234,6 +235,8 @@
|
||||
let _cols = [];
|
||||
const _filter = { col: 'all', kind: 'all', q: '', sort: 'new', tag: '' };
|
||||
const _sel = new Set(); // ids selected for bulk actions
|
||||
const PAGE_SIZE = 60; // cards rendered to the DOM at once ("Показать ещё" adds more)
|
||||
let _shown = PAGE_SIZE;
|
||||
|
||||
/* ── Move-to-collection select ── */
|
||||
function moveSelect(m) {
|
||||
@@ -261,7 +264,7 @@
|
||||
|
||||
if (m.kind === 'board' || m.kind === 'image') {
|
||||
return `<div class="mm-card${selCls}" draggable="true" ondragstart="mmDragStart(event,${m.id})" ondragend="mmDragEnd(event)">${cb}
|
||||
<a class="mm-card-media" href="${esc(m.url)}" onclick="openViewer(${m.id});return false;"><img src="${esc(m.url)}" alt="" loading="lazy" draggable="false"/></a>
|
||||
<a class="mm-card-media" href="${esc(m.url)}" onclick="openViewer(${m.id});return false;"><img src="${esc(m.thumb_url || m.url)}" alt="" loading="lazy" decoding="async" draggable="false"/></a>
|
||||
<div class="mm-card-body">
|
||||
${chip}
|
||||
<div class="mm-card-title">${esc(m.title || kind)}</div>
|
||||
@@ -400,12 +403,20 @@
|
||||
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>`;
|
||||
if (!rows.length) {
|
||||
grid.innerHTML = `<div class="mm-empty" style="grid-column:1/-1"><i data-lucide="search-x"></i><p>Ничего не найдено</p></div>`;
|
||||
lucide.createIcons(); renderBulk(); return;
|
||||
}
|
||||
let html = rows.slice(0, _shown).map(card).join('');
|
||||
if (rows.length > _shown) {
|
||||
html += `<div class="mm-more" style="grid-column:1/-1"><button class="mm-btn" onclick="showMore()"><i data-lucide="chevron-down"></i> Показать ещё (${rows.length - _shown})</button></div>`;
|
||||
}
|
||||
grid.innerHTML = html;
|
||||
lucide.createIcons();
|
||||
renderBulk();
|
||||
}
|
||||
function showMore() { _shown += PAGE_SIZE; renderGrid(); }
|
||||
window.showMore = showMore;
|
||||
|
||||
async function load() {
|
||||
try {
|
||||
@@ -421,12 +432,12 @@
|
||||
}
|
||||
|
||||
/* ── 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(); }
|
||||
function onSort(v) { _filter.sort = v; renderGrid(); }
|
||||
function filterTag(t) { _filter.tag = String(t || ''); renderTagFilter(); renderGrid(); }
|
||||
function clearTag() { _filter.tag = ''; renderTagFilter(); renderGrid(); }
|
||||
function setCol(key) { _filter.col = key; _shown = PAGE_SIZE; renderCols(); renderGrid(); }
|
||||
function onKind(v) { _filter.kind = v; _shown = PAGE_SIZE; renderGrid(); }
|
||||
function onSearch(v) { _filter.q = (v || '').trim().toLowerCase(); _shown = PAGE_SIZE; renderGrid(); }
|
||||
function onSort(v) { _filter.sort = v; _shown = PAGE_SIZE; renderGrid(); }
|
||||
function filterTag(t) { _filter.tag = String(t || ''); _shown = PAGE_SIZE; renderTagFilter(); renderGrid(); }
|
||||
function clearTag() { _filter.tag = ''; _shown = PAGE_SIZE; renderTagFilter(); renderGrid(); }
|
||||
function renderTagFilter() {
|
||||
const el = document.getElementById('mm-tagfilter');
|
||||
if (!el) return;
|
||||
@@ -685,10 +696,10 @@
|
||||
const up = await LS.uploadMaterialFile(fd);
|
||||
if (o.materialId) {
|
||||
// Аннотация существующего материала — перезаписываем его, а не плодим копии
|
||||
await LS.updateMaterial(o.materialId, { url: up.url });
|
||||
await LS.updateMaterial(o.materialId, { url: up.url, thumbUrl: up.thumbUrl || null });
|
||||
close(); load(); LS.toast('Изменения сохранены', 'success');
|
||||
} else {
|
||||
await LS.saveMaterial({ kind: 'image', title: o.title || 'Рисунок', url: up.url, sourceTitle: o.sourceTitle || null });
|
||||
await LS.saveMaterial({ kind: 'image', title: o.title || 'Рисунок', url: up.url, thumbUrl: up.thumbUrl || null, sourceTitle: o.sourceTitle || null });
|
||||
close(); load(); LS.toast('Сохранено в «Мои материалы»', 'success');
|
||||
}
|
||||
} catch (e) { LS.toast(e.message || 'Ошибка', 'error'); btn.disabled = false; }
|
||||
|
||||
Reference in New Issue
Block a user