kingkont 0.7.89 → 0.7.91

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.89",
3
+ "version": "0.7.91",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
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
- // Срабатывает только если фокус НЕ на input/textarea (там юзер пастит
842
- // текст) и НЕ на наших нодах (там Cmd+V обрабатывается отдельно через
843
- // keydown в settings.js он preventDefault'ит, и paste event сюда не
844
- // доходит). Поддерживает 2 источника:
845
- // - Файлы (Finder Cmd+C) clipboardData.files
846
- // - Картинки прямо из браузера (Edit→Copy Image) clipboardData.items
847
- // с kind='file' и type='image/*'
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
- if (!cd) return;
855
- // Собираем все источники файлов из буфера, дедупим по size+type.
856
- const all = [...(cd.files || [])];
857
- for (const it of (cd.items || [])) {
858
- if (it.kind === 'file') {
859
- const f = it.getAsFile();
860
- if (f && !all.some(x => x.size === f.size && x.type === f.type && x.name === f.name)) {
861
- all.push(f);
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
- if (!all.length) return;
900
+
901
+ // === 2. Fallback: внутренний state.clipboard (ноды/клипы) ===
902
+ if (!state.clipboard?.length) return;
866
903
  e.preventDefault();
867
- const cx = canvasWrap.scrollLeft / state.zoom + 200;
868
- const cy = canvasWrap.scrollTop / state.zoom + 100;
869
- let i = 0;
870
- for (const f of all) {
871
- const type = getFileType(f);
872
- if (!type) continue;
873
- // Картинка из браузера обычно даёт безымянный File типа 'image/png'.
874
- // Синтезируем читаемое имя со штампом времени.
875
- let file = f;
876
- if (!f.name || f.name === 'image.png' || f.name === 'unknown') {
877
- const ext = (f.type.split('/')[1] || 'png').replace('jpeg', 'jpg');
878
- file = new File([f], `pasted_${Date.now()}.${ext}`, { type: f.type });
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
- scheduleSave();
918
+ if (typeof pushHistory === 'function') pushHistory('Вставка нод');
919
+ await pasteClipboardNodes();
895
920
  });
896
921
 
897
922
  // =================== Текстовая нода ===================
@@ -1176,26 +1176,11 @@ document.addEventListener('keydown', async e => {
1176
1176
  await copySelectedNodes();
1177
1177
  }
1178
1178
  }
1179
- if (mod && e.key.toLowerCase() === 'v' && !e.shiftKey && !inText && state.clipboard?.length) {
1180
- e.preventDefault();
1181
- // Если буфер содержит клипы и фокус на таймлайне → вставить туда
1182
- const tl = getTimeline();
1183
- const cb = state.clipboard;
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
- }
1179
+ // Cmd+V: НЕ перехватываем здесь preventDefault'ом иначе paste-event
1180
+ // не сработает и мы не увидим что в системном буфере (картинка из
1181
+ // Telegram, browser «Copy image», файл из Finder). Всю paste-логику
1182
+ // (системный буфер → файлы; иначе fallback на state.clipboard → ноды)
1183
+ // делает document-level paste handler в renderer/media.js.
1199
1184
  // Cmd+X — вырезать (как copy + удалить)
1200
1185
  if (mod && e.key.toLowerCase() === 'x' && !e.shiftKey && !inText) {
1201
1186
  if (state.selectedClipIds.size) {
package/renderer/state.js CHANGED
@@ -86,7 +86,38 @@ async function fileExists(handle, name) {
86
86
  try { await handle.getFileHandle(name); return true; } catch { return false; }
87
87
  }
88
88
 
89
+ // Имена из внешних источников (drag-drop из Telegram/чатов, paste, импорт)
90
+ // часто содержат символы, недопустимые для File-System-Access-API: двоеточия
91
+ // (timestamp'ы вроде «IMAGE 16:48:03.jpg»), слэши, *, ? и control-символы.
92
+ // FS-Access-API строго следует правилам Windows-FS, поэтому даже на macOS
93
+ // getFileHandle('foo:bar.jpg') бросает 'Name is not allowed'.
94
+ function sanitizeFilename(name) {
95
+ if (!name) return 'file';
96
+ let safe = String(name)
97
+ .replace(/[\\/:*?"<>|\x00-\x1F]/g, '-') // зарезервированные + control-chars
98
+ .replace(/^\.+/, '') // имя не должно начинаться с точки
99
+ .replace(/[. ]+$/, '') // и не должно кончаться на точку/пробел (Windows)
100
+ .trim();
101
+ if (!safe) safe = 'file';
102
+ // Зарезервированные базовые имена Windows.
103
+ const reserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(\..*)?$/i;
104
+ if (reserved.test(safe)) safe = '_' + safe;
105
+ // Лимит длины (NTFS = 255, оставляем запас под суффикс _N).
106
+ if (safe.length > 200) {
107
+ const dot = safe.lastIndexOf('.');
108
+ if (dot > 0 && safe.length - dot < 20) {
109
+ safe = safe.slice(0, 200 - (safe.length - dot)) + safe.slice(dot);
110
+ } else {
111
+ safe = safe.slice(0, 200);
112
+ }
113
+ }
114
+ return safe;
115
+ }
116
+
89
117
  async function uniqueName(handle, name) {
118
+ // Always sanitize — uniqueName это «бутылочное горлышко» для всех импортов
119
+ // и записей с внешними именами. Если уже sanitized — операция идемпотентна.
120
+ name = sanitizeFilename(name);
90
121
  if (!(await fileExists(handle, name))) return name;
91
122
  const dot = name.lastIndexOf('.');
92
123
  const base = dot > 0 ? name.slice(0, dot) : name;