kingkont 0.7.90 → 0.7.92
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/renderer/generate.js +12 -12
- package/renderer/media.js +68 -43
- package/renderer/settings.js +9 -21
package/package.json
CHANGED
package/renderer/generate.js
CHANGED
|
@@ -210,7 +210,7 @@ async function addLabelAt(pos) {
|
|
|
210
210
|
const node = {
|
|
211
211
|
id: crypto.randomUUID(),
|
|
212
212
|
type: 'label',
|
|
213
|
-
text: '',
|
|
213
|
+
text: 'Подпись', // дефолтный текст — сразу видно ноду
|
|
214
214
|
textStyle: { fontSize: 32, italic: false, fontFamily: 'pencil' },
|
|
215
215
|
x, y,
|
|
216
216
|
// width/height не задаём — label всегда auto-размер по тексту
|
|
@@ -220,19 +220,19 @@ async function addLabelAt(pos) {
|
|
|
220
220
|
const el = await createNodeEl(node);
|
|
221
221
|
canvas.appendChild(el);
|
|
222
222
|
scheduleSave();
|
|
223
|
-
//
|
|
223
|
+
// Сразу входим в режим редактирования и выделяем весь текст —
|
|
224
|
+
// юзер начинает печатать, и «Подпись» заменяется на введённое.
|
|
224
225
|
setTimeout(() => {
|
|
225
226
|
const ed = el.querySelector('.label-text');
|
|
226
|
-
if (ed)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
}
|
|
227
|
+
if (!ed) return;
|
|
228
|
+
ed.contentEditable = 'plaintext-only';
|
|
229
|
+
ed.focus();
|
|
230
|
+
// Select all — type-to-replace UX (как на iOS notes / новой подписи).
|
|
231
|
+
const sel = window.getSelection();
|
|
232
|
+
const r = document.createRange();
|
|
233
|
+
r.selectNodeContents(ed);
|
|
234
|
+
sel?.removeAllRanges();
|
|
235
|
+
sel?.addRange(r);
|
|
236
236
|
}, 30);
|
|
237
237
|
}
|
|
238
238
|
|
package/renderer/media.js
CHANGED
|
@@ -837,61 +837,86 @@ canvasWrap.addEventListener('drop', async e => {
|
|
|
837
837
|
scheduleSave();
|
|
838
838
|
});
|
|
839
839
|
|
|
840
|
-
// Cmd+V
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
//
|
|
844
|
-
//
|
|
845
|
-
//
|
|
846
|
-
//
|
|
847
|
-
//
|
|
840
|
+
// Cmd+V — единый paste handler. Приоритет:
|
|
841
|
+
// 1. Если в системном буфере есть files/images (Telegram/browser
|
|
842
|
+
// «Copy image», Finder Cmd+C) → импортируем их.
|
|
843
|
+
// 2. Иначе — fallback на state.clipboard (внутренние ноды/клипы,
|
|
844
|
+
// положенные через Cmd+C в settings.js).
|
|
845
|
+
// Раньше Cmd+V в settings.js делал preventDefault на keydown и сразу
|
|
846
|
+
// пастил ноды — paste-event с системным буфером даже не успевал
|
|
847
|
+
// сработать, поэтому «Copy image» из Telegram игнорировался.
|
|
848
848
|
document.addEventListener('paste', async e => {
|
|
849
849
|
if (!state.currentBoard) return;
|
|
850
850
|
// Если фокус на текстовом инпуте — паст обработает он (юзер пастит текст).
|
|
851
851
|
const ae = document.activeElement;
|
|
852
852
|
if (ae && ae.matches?.('input, textarea, [contenteditable=""], [contenteditable="true"]')) return;
|
|
853
|
+
|
|
854
|
+
// === 1. Files/images из системного буфера ===
|
|
853
855
|
const cd = e.clipboardData;
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
856
|
+
const all = [];
|
|
857
|
+
if (cd) {
|
|
858
|
+
for (const f of (cd.files || [])) all.push(f);
|
|
859
|
+
for (const it of (cd.items || [])) {
|
|
860
|
+
if (it.kind === 'file') {
|
|
861
|
+
const f = it.getAsFile();
|
|
862
|
+
if (f && !all.some(x => x.size === f.size && x.type === f.type && x.name === f.name)) {
|
|
863
|
+
all.push(f);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (all.length) {
|
|
869
|
+
e.preventDefault();
|
|
870
|
+
const cx = canvasWrap.scrollLeft / state.zoom + 200;
|
|
871
|
+
const cy = canvasWrap.scrollTop / state.zoom + 100;
|
|
872
|
+
let i = 0;
|
|
873
|
+
for (const f of all) {
|
|
874
|
+
const type = getFileType(f);
|
|
875
|
+
if (!type) continue;
|
|
876
|
+
// Картинка из браузера обычно даёт безымянный File типа 'image/png'.
|
|
877
|
+
// Синтезируем читаемое имя со штампом времени.
|
|
878
|
+
let file = f;
|
|
879
|
+
if (!f.name || f.name === 'image.png' || f.name === 'unknown') {
|
|
880
|
+
const ext = (f.type.split('/')[1] || 'png').replace('jpeg', 'jpg');
|
|
881
|
+
file = new File([f], `pasted_${Date.now()}.${ext}`, { type: f.type });
|
|
862
882
|
}
|
|
883
|
+
try {
|
|
884
|
+
const filename = await importToBoard(state.currentBoard.handle, file, type);
|
|
885
|
+
const node = {
|
|
886
|
+
id: crypto.randomUUID(), type, file: filename,
|
|
887
|
+
x: cx + i*24, y: cy + i*24,
|
|
888
|
+
};
|
|
889
|
+
if (type === 'text') {
|
|
890
|
+
try { node.text = await file.text(); } catch { node.text = ''; }
|
|
891
|
+
}
|
|
892
|
+
state.currentBoard.metadata.nodes.push(node);
|
|
893
|
+
canvas.appendChild(await createNodeEl(node));
|
|
894
|
+
i++;
|
|
895
|
+
} catch (err) { console.error(err); alert(`Не удалось вставить: ${err.message}`); }
|
|
863
896
|
}
|
|
897
|
+
scheduleSave();
|
|
898
|
+
return;
|
|
864
899
|
}
|
|
865
|
-
|
|
900
|
+
|
|
901
|
+
// === 2. Fallback: внутренний state.clipboard (ноды/клипы) ===
|
|
902
|
+
if (!state.clipboard?.length) return;
|
|
866
903
|
e.preventDefault();
|
|
867
|
-
const
|
|
868
|
-
const
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
904
|
+
const tl = (typeof getTimeline === 'function') ? getTimeline() : null;
|
|
905
|
+
const cb = state.clipboard;
|
|
906
|
+
const cbHasMedia = cb.some(it => ['image','video','audio'].includes(it.node?.type));
|
|
907
|
+
const focusOnTimeline = !!document.activeElement?.closest?.('#timelinePanel');
|
|
908
|
+
if (focusOnTimeline && tl?.tracks && cbHasMedia) {
|
|
909
|
+
const isAudio = cb.every(it => it.node?.type === 'audio');
|
|
910
|
+
const wantedKind = isAudio ? 'audio' : 'video';
|
|
911
|
+
const target = tl.tracks.find(t => t.kind === wantedKind) || tl.tracks[0];
|
|
912
|
+
if (target) {
|
|
913
|
+
if (typeof pushHistory === 'function') pushHistory('Вставка на таймлайн');
|
|
914
|
+
await pasteClipboardToTimeline(target, state.playheadTime || 0);
|
|
915
|
+
return;
|
|
879
916
|
}
|
|
880
|
-
try {
|
|
881
|
-
const filename = await importToBoard(state.currentBoard.handle, file, type);
|
|
882
|
-
const node = {
|
|
883
|
-
id: crypto.randomUUID(), type, file: filename,
|
|
884
|
-
x: cx + i*24, y: cy + i*24,
|
|
885
|
-
};
|
|
886
|
-
if (type === 'text') {
|
|
887
|
-
try { node.text = await file.text(); } catch { node.text = ''; }
|
|
888
|
-
}
|
|
889
|
-
state.currentBoard.metadata.nodes.push(node);
|
|
890
|
-
canvas.appendChild(await createNodeEl(node));
|
|
891
|
-
i++;
|
|
892
|
-
} catch (err) { console.error(err); alert(`Не удалось вставить: ${err.message}`); }
|
|
893
917
|
}
|
|
894
|
-
|
|
918
|
+
if (typeof pushHistory === 'function') pushHistory('Вставка нод');
|
|
919
|
+
await pasteClipboardNodes();
|
|
895
920
|
});
|
|
896
921
|
|
|
897
922
|
// =================== Текстовая нода ===================
|
package/renderer/settings.js
CHANGED
|
@@ -1084,7 +1084,10 @@ async function deleteSelectedNodes() {
|
|
|
1084
1084
|
document.addEventListener('keydown', async e => {
|
|
1085
1085
|
const mod = e.metaKey || e.ctrlKey;
|
|
1086
1086
|
const tag = (e.target?.tagName || '').toLowerCase();
|
|
1087
|
-
|
|
1087
|
+
// contentEditable label-text — тоже текстовое поле: Backspace там должен
|
|
1088
|
+
// удалять символ, а не ноду. e.target.isContentEditable отлавливает любой
|
|
1089
|
+
// contenteditable-элемент (наша label-нода в edit-режиме).
|
|
1090
|
+
const inText = tag === 'textarea' || tag === 'input' || !!e.target?.isContentEditable;
|
|
1088
1091
|
|
|
1089
1092
|
// Zoom shortcuts (работают даже в инпутах — это естественно)
|
|
1090
1093
|
if (mod && (e.key === '=' || e.key === '+')) { e.preventDefault(); applyZoom(state.zoom * 1.25); return; }
|
|
@@ -1176,26 +1179,11 @@ document.addEventListener('keydown', async e => {
|
|
|
1176
1179
|
await copySelectedNodes();
|
|
1177
1180
|
}
|
|
1178
1181
|
}
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
const cbHasMedia = cb.some(it => ['image','video','audio'].includes(it.node?.type));
|
|
1185
|
-
const focusOnTimeline = !!document.activeElement?.closest?.('#timelinePanel');
|
|
1186
|
-
if (focusOnTimeline && tl?.tracks && cbHasMedia) {
|
|
1187
|
-
const isAudio = cb.every(it => it.node?.type === 'audio');
|
|
1188
|
-
const wantedKind = isAudio ? 'audio' : 'video';
|
|
1189
|
-
const target = tl.tracks.find(t => t.kind === wantedKind) || tl.tracks[0];
|
|
1190
|
-
if (target) {
|
|
1191
|
-
pushHistory('Вставка на таймлайн');
|
|
1192
|
-
await pasteClipboardToTimeline(target, state.playheadTime || 0);
|
|
1193
|
-
return;
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
pushHistory('Вставка нод');
|
|
1197
|
-
await pasteClipboardNodes();
|
|
1198
|
-
}
|
|
1182
|
+
// Cmd+V: НЕ перехватываем здесь preventDefault'ом — иначе paste-event
|
|
1183
|
+
// не сработает и мы не увидим что в системном буфере (картинка из
|
|
1184
|
+
// Telegram, browser «Copy image», файл из Finder). Всю paste-логику
|
|
1185
|
+
// (системный буфер → файлы; иначе fallback на state.clipboard → ноды)
|
|
1186
|
+
// делает document-level paste handler в renderer/media.js.
|
|
1199
1187
|
// Cmd+X — вырезать (как copy + удалить)
|
|
1200
1188
|
if (mod && e.key.toLowerCase() === 'x' && !e.shiftKey && !inText) {
|
|
1201
1189
|
if (state.selectedClipIds.size) {
|