From 116876d8ece62394d50409d6da5dce4bbdc05e9c Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 4 Jun 2026 11:38:23 +0300 Subject: [PATCH] =?UTF-8?q?feat(materials):=20=D1=81=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=A7=D0=90=D0=A1=D0=A2?= =?UTF-8?q?=D0=98=20=D0=B4=D0=BE=D1=81=D0=BA=D0=B8=20(=D0=B2=D1=8B=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BE=D0=B1=D0=BB=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D0=B8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit На странице доски в «Мои уроки» кнопка «Область»: снимок страницы → модалка с выделением прямоугольника мышью → обрезка до выбранного фрагмента (таблица, рисунок и т.п.) → загрузка в /api/files → сохранение в «Мои материалы» (kind=image). Координаты выделения масштабируются к натуральному размеру снимка. Бэкенд не менялся. Co-Authored-By: Claude Opus 4.8 (1M context) --- frontend/my-lessons.html | 123 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/frontend/my-lessons.html b/frontend/my-lessons.html index a42687c..ecdcdcb 100644 --- a/frontend/my-lessons.html +++ b/frontend/my-lessons.html @@ -585,10 +585,14 @@ Загрузка доски...
- + + +
+ `; + document.body.appendChild(ov); + const img = ov.querySelector('#crop-img'); + const sel = ov.querySelector('#crop-sel'); + const saveBtn = ov.querySelector('#crop-save'); + img.src = url; + + let dragging = false, start = null, rect = null; + function close() { ov.remove(); URL.revokeObjectURL(url); } + ov.querySelector('#crop-cancel').onclick = close; + + function rel(e) { + const r = img.getBoundingClientRect(); + return { + x: Math.max(0, Math.min(r.width, e.clientX - r.left)), + y: Math.max(0, Math.min(r.height, e.clientY - r.top)), + }; + } + function onMove(e) { + if (!dragging) return; + const p = rel(e); + const x = Math.min(p.x, start.x), y = Math.min(p.y, start.y); + const w = Math.abs(p.x - start.x), h = Math.abs(p.y - start.y); + rect = { x, y, w, h }; + sel.style.display = 'block'; + sel.style.left = x + 'px'; sel.style.top = y + 'px'; + sel.style.width = w + 'px'; sel.style.height = h + 'px'; + saveBtn.disabled = (w < 6 || h < 6); + } + function onUp() { + dragging = false; + document.removeEventListener('pointermove', onMove); + document.removeEventListener('pointerup', onUp); + } + img.addEventListener('pointerdown', e => { + e.preventDefault(); + dragging = true; start = rel(e); rect = null; saveBtn.disabled = true; + sel.style.display = 'none'; + document.addEventListener('pointermove', onMove); + document.addEventListener('pointerup', onUp); + }); + + saveBtn.onclick = async () => { + if (!rect || rect.w < 6 || rect.h < 6) return; + saveBtn.disabled = true; + try { + const r = img.getBoundingClientRect(); + const sx = img.naturalWidth / r.width, sy = img.naturalHeight / r.height; + const cw = Math.max(1, Math.round(rect.w * sx)), ch = Math.max(1, Math.round(rect.h * sy)); + const off = document.createElement('canvas'); off.width = cw; off.height = ch; + 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(res => off.toBlob(res, 'image/png')); + if (!cblob) throw new Error('Не удалось обрезать область'); + const fd = new FormData(); fd.append('file', cblob, 'board-region.png'); + const up = await LS.uploadFile(fd); + const src = _matSource(); + await LS.saveMaterial({ + kind: 'image', + title: (src.sourceTitle || 'Доска') + ' · фрагмент', + url: LS.downloadFileUrl(up.id), + sourceSessionId: src.sourceSessionId, sourceTitle: src.sourceTitle, + }); + close(); + LS.toast('Фрагмент сохранён в «Мои материалы»', 'success'); + } catch (e) { + LS.toast(e.message || 'Ошибка сохранения', 'error'); + saveBtn.disabled = false; + } + }; +} + /* ─── Chat ─── */ async function loadChat() { _chatLoaded = true;