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:
@@ -20,14 +20,14 @@
|
||||
async function uploadBlob(blob, name) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', blob, name);
|
||||
const up = await LS.uploadMaterialFile(fd);
|
||||
return up.url;
|
||||
return await LS.uploadMaterialFile(fd); // { url, thumbUrl }
|
||||
}
|
||||
|
||||
async function persist(meta, kind, url) {
|
||||
async function persist(meta, kind, url, thumbUrl) {
|
||||
await LS.saveMaterial({
|
||||
kind: kind,
|
||||
url: url,
|
||||
thumbUrl: thumbUrl || null,
|
||||
title: titleFor(meta, kind === 'image' ? ' · фрагмент' : ''),
|
||||
sourceSessionId: meta && meta.sourceSessionId,
|
||||
sourceTitle: meta && meta.sourceTitle,
|
||||
@@ -40,8 +40,8 @@
|
||||
wb.exportBlob(async function (blob) {
|
||||
try {
|
||||
if (!blob) throw new Error('Не удалось снять доску');
|
||||
const url = await uploadBlob(blob, 'board.png');
|
||||
await persist(meta, 'board', url);
|
||||
const up = await uploadBlob(blob, 'board.png');
|
||||
await persist(meta, 'board', up.url, up.thumbUrl);
|
||||
LS.toast('Страница сохранена в «Мои материалы»', 'success');
|
||||
} catch (e) {
|
||||
LS.toast(e.message || 'Ошибка сохранения', 'error');
|
||||
@@ -147,8 +147,8 @@
|
||||
off.getContext('2d').drawImage(img, Math.round(rect.x * sx), Math.round(rect.y * sy), cw, ch, 0, 0, cw, ch);
|
||||
const cblob = await new Promise(function (res) { off.toBlob(res, 'image/png'); });
|
||||
if (!cblob) throw new Error('Не удалось обрезать область');
|
||||
const cropUrl = await uploadBlob(cblob, 'board-region.png');
|
||||
await persist(meta, 'image', cropUrl);
|
||||
const up = await uploadBlob(cblob, 'board-region.png');
|
||||
await persist(meta, 'image', up.url, up.thumbUrl);
|
||||
close();
|
||||
LS.toast('Фрагмент сохранён в «Мои материалы»', 'success');
|
||||
} catch (e) {
|
||||
|
||||
@@ -39,14 +39,16 @@
|
||||
if (btn) btn.disabled = true;
|
||||
try {
|
||||
let url = o.url;
|
||||
let thumbUrl = o.thumbUrl || null;
|
||||
if (o.blob) {
|
||||
const fd = new FormData();
|
||||
fd.append('file', o.blob, o.name || 'image.png');
|
||||
const up = await LS.uploadMaterialFile(fd);
|
||||
url = up.url;
|
||||
thumbUrl = up.thumbUrl || null;
|
||||
}
|
||||
if (!url) throw new Error('Нет изображения');
|
||||
await LS.saveMaterial({ kind: 'image', title: o.title || '', url: url, sourceTitle: o.sourceTitle || null });
|
||||
await LS.saveMaterial({ kind: 'image', title: o.title || '', url: url, thumbUrl: thumbUrl, sourceTitle: o.sourceTitle || null });
|
||||
ok();
|
||||
} catch (e) { err(e); } finally { if (btn) btn.disabled = false; }
|
||||
}
|
||||
|
||||
@@ -185,6 +185,7 @@
|
||||
kind: 'image',
|
||||
title: input.value.trim() || sectionTitle(),
|
||||
url: up.url,
|
||||
thumbUrl: up.thumbUrl || null,
|
||||
sourceTitle: chapterTitle()
|
||||
});
|
||||
toast('Сохранено в «Мои материалы»', 'success');
|
||||
|
||||
Reference in New Issue
Block a user