kingkont 0.20.45 → 0.20.47

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.20.45",
3
+ "version": "0.20.47",
4
4
  "description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -2530,6 +2530,13 @@ function showBoardContextMenu(kind, item, clientX, clientY) {
2530
2530
  // юзер: «объедини дефолтные промпты и соотношение сторон в одну форму».
2531
2531
  // Доступно также по дабл-клику на названии сцены в сайдбаре.
2532
2532
  add('⚙ Настройки сцены…', () => openDefaultPromptsDialog(kind, item));
2533
+ // Скопировать / Вставить настройки между сценами.
2534
+ // Юзер: «сделай возможность скопировать настройки из одной сцены в другую».
2535
+ // state.clipboardSceneSettings — in-memory, живёт до закрытия проекта.
2536
+ add('📋 Скопировать настройки', () => copyBoardSettings(kind, item));
2537
+ if (state.clipboardSceneSettings) {
2538
+ add('📥 Вставить настройки', () => pasteBoardSettings(kind, item));
2539
+ }
2533
2540
  if (kind === 'character') {
2534
2541
  add('⚙ Свойства персонажа', () => {
2535
2542
  // Открываем панель этого персонажа и потом её character-modal
@@ -2582,6 +2589,81 @@ async function promptBoardAspectRatio(kind, item) {
2582
2589
  }
2583
2590
  console.log(`[board] ${kind}/${item.name} aspectRatio → ${chosen}`);
2584
2591
  }
2592
+ // Открыть редактор описания image/video-ноды.
2593
+ // Юзер: «дабл-клик на описании ноды должен открывать его редактирование».
2594
+ // Также используется из ПКМ ноды (📝 Изменить/Добавить описание).
2595
+ async function editNodeDescription(node) {
2596
+ if (!node || (node.type !== 'image' && node.type !== 'video')) return;
2597
+ const next = await askName(
2598
+ 'Описание ноды (Cmd+Enter — сохранить):',
2599
+ 'Например: лес ночью, луна за облаками, тишина',
2600
+ node.description || '',
2601
+ { multiline: true, okText: 'Сохранить' },
2602
+ );
2603
+ if (next == null) return;
2604
+ node.description = next;
2605
+ scheduleSave();
2606
+ if (typeof ensureNodeDescriptionEl === 'function') ensureNodeDescriptionEl(node);
2607
+ // Перерасчёт node.height — описание участвует в bbox'е (см.
2608
+ // fitNodeHeightToMedia в settings.js → расширяет node.height на descH).
2609
+ const el = canvas.querySelector(`.node[data-id="${node.id}"]`);
2610
+ const media = el?.querySelector('img, video');
2611
+ if (typeof fitNodeHeightToMedia === 'function' && el && media) {
2612
+ fitNodeHeightToMedia(node, el, media);
2613
+ }
2614
+ }
2615
+ window.editNodeDescription = editNodeDescription;
2616
+
2617
+ // Скопировать / вставить настройки сцены (aspectRatio + defaultPrompts).
2618
+ // Хранение — in-memory state.clipboardSceneSettings (живёт до closeProject).
2619
+ // Юзер: «сделай возможность скопировать настройки из одной сцены в другую».
2620
+ async function copyBoardSettings(kind, item) {
2621
+ const isActive = state.currentBoard?.kind === kind && state.currentBoard.name === item.name;
2622
+ const metaSettings = isActive
2623
+ ? (state.currentBoard.metadata.settings || {})
2624
+ : ((await loadBoardMetadata(item.handle)).settings || {});
2625
+ // Deep-copy чтобы дальнейшие правки исходной сцены не вмешивались.
2626
+ state.clipboardSceneSettings = JSON.parse(JSON.stringify({
2627
+ aspectRatio: metaSettings.aspectRatio ?? null,
2628
+ defaultPrompts: metaSettings.defaultPrompts || [],
2629
+ }));
2630
+ const fromName = item.name;
2631
+ if (typeof showToast === 'function') {
2632
+ showToast(`📋 Настройки сцены «${fromName}» скопированы`, 'info');
2633
+ }
2634
+ }
2635
+ async function pasteBoardSettings(kind, item) {
2636
+ const cb = state.clipboardSceneSettings;
2637
+ if (!cb) { alert('Сначала скопируй настройки другой сцены через ПКМ'); return; }
2638
+ const isActive = state.currentBoard?.kind === kind && state.currentBoard.name === item.name;
2639
+ if (isActive) {
2640
+ if (!state.currentBoard.metadata.settings) state.currentBoard.metadata.settings = {};
2641
+ const s = state.currentBoard.metadata.settings;
2642
+ if (cb.aspectRatio) s.aspectRatio = cb.aspectRatio;
2643
+ if (cb.defaultPrompts && cb.defaultPrompts.length) {
2644
+ // Глубокая копия + новые id чтобы не было коллизий id'ов промптов.
2645
+ s.defaultPrompts = cb.defaultPrompts.map(p => ({ ...p, id: crypto.randomUUID() }));
2646
+ } else {
2647
+ delete s.defaultPrompts;
2648
+ }
2649
+ scheduleSave();
2650
+ if (typeof updateTimelineAspectLabel === 'function') updateTimelineAspectLabel();
2651
+ } else {
2652
+ const meta = await loadBoardMetadata(item.handle);
2653
+ meta.settings = meta.settings || {};
2654
+ if (cb.aspectRatio) meta.settings.aspectRatio = cb.aspectRatio;
2655
+ if (cb.defaultPrompts && cb.defaultPrompts.length) {
2656
+ meta.settings.defaultPrompts = cb.defaultPrompts.map(p => ({ ...p, id: crypto.randomUUID() }));
2657
+ } else {
2658
+ delete meta.settings.defaultPrompts;
2659
+ }
2660
+ await saveBoardMetadata(item.handle, meta);
2661
+ }
2662
+ if (typeof showToast === 'function') {
2663
+ showToast(`📥 Настройки вставлены в «${item.name}»`, 'ok');
2664
+ }
2665
+ }
2666
+
2585
2667
  // Open «Дефолтные промпты сцены» modal — multi-prompt list, each with
2586
2668
  // kind-tags (image/video/audio/text) и default-enabled-чекбоксом.
2587
2669
  // Сохраняется в scene.json → settings.defaultPrompts как массив.
@@ -4185,21 +4267,7 @@ function showNodeContextMenu(node, clientX, clientY) {
4185
4267
  // (см. renderNodeBody), это удобный picker для крупного редактирования.
4186
4268
  if (node.type === 'image' || node.type === 'video') {
4187
4269
  const hasDesc = !!(node.description && node.description.trim());
4188
- add(hasDesc ? '📝 Изменить описание…' : '📝 Добавить описание…', async () => {
4189
- const next = await askName(
4190
- 'Описание ноды (Cmd+Enter — сохранить):',
4191
- 'Например: лес ночью, луна за облаками, тишина',
4192
- node.description || '',
4193
- { multiline: true, okText: 'Сохранить' },
4194
- );
4195
- if (next == null) return;
4196
- node.description = next;
4197
- scheduleSave();
4198
- // Sidecar-описание — отдельный элемент на canvas'е, см.
4199
- // ensureNodeDescriptionEl в settings.js. Идемпотентно: пересоздаёт/
4200
- // удаляет/обновляет в зависимости от того, есть ли текст.
4201
- if (typeof ensureNodeDescriptionEl === 'function') ensureNodeDescriptionEl(node);
4202
- });
4270
+ add(hasDesc ? '📝 Изменить описание…' : '📝 Добавить описание…', () => editNodeDescription(node));
4203
4271
  }
4204
4272
  // Image-нода как обложка проекта — копируется в `<project>/.cover.<ext>`,
4205
4273
  // оттуда подхватывается generateProjectThumb (для recents и шаблонов).
@@ -184,7 +184,15 @@
184
184
  list.innerHTML = '<div style="padding:24px; text-align:center; color:#555; font-size:12px;">Нет событий</div>';
185
185
  return;
186
186
  }
187
- for (let i = events.length - 1; i >= 0; i--) {
187
+ // В auto-режиме (panel всплыла сама после нового события) показываем
188
+ // ТОЛЬКО 3 последних события — иначе панель раскрывается на пол-экрана
189
+ // и мешает основному UI. Юзер: «всплывающее окно когда что-то готово —
190
+ // показывай 3 последних (сейчас слишком высокое)».
191
+ // В manual-режиме (юзер сам кликнул на 🔔) — полный список.
192
+ const limit = (openMode === 'auto') ? 3 : events.length;
193
+ const start = Math.max(0, events.length - limit);
194
+ const hidden = events.length - (events.length - start); // = events.length - count_shown
195
+ for (let i = events.length - 1; i >= start; i--) {
188
196
  const e = events[i];
189
197
  const row = document.createElement('div');
190
198
  const colors = {
@@ -238,6 +246,18 @@
238
246
  }
239
247
  list.appendChild(row);
240
248
  }
249
+ // Подсказка «ещё N» — в auto-режиме если есть скрытые события.
250
+ // Клик открывает панель в manual-режиме (= полный список + не закроется
251
+ // авто-таймером).
252
+ if (openMode === 'auto' && start > 0) {
253
+ const more = document.createElement('div');
254
+ more.style.cssText = 'padding:6px 10px; text-align:center; color:#888; font-size:11px; cursor:pointer; border-top:1px dashed #2a2a2a; margin-top:4px;';
255
+ more.textContent = `+ ещё ${start} событий — открыть всё`;
256
+ more.addEventListener('mouseenter', () => more.style.color = '#bbb');
257
+ more.addEventListener('mouseleave', () => more.style.color = '#888');
258
+ more.addEventListener('click', () => setOpen(true, 'manual'));
259
+ list.appendChild(more);
260
+ }
241
261
  }
242
262
 
243
263
  // Public API.
@@ -604,6 +604,17 @@ function ensureNodeDescriptionEl(node) {
604
604
  el.className = 'node-description-outside';
605
605
  el.dataset.descFor = node.id;
606
606
  canvasEl.appendChild(el);
607
+ // Дабл-клик по описанию → редактор (юзер: «дабл-клик на описании ноды
608
+ // должен открывать его редактирование»). editNodeDescription —
609
+ // глобал из board.js. e.stopPropagation — иначе дабл-клик пробрасывался
610
+ // в .node и срабатывало fullscreen-просмотр.
611
+ el.addEventListener('dblclick', e => {
612
+ e.stopPropagation();
613
+ e.preventDefault();
614
+ if (typeof window.editNodeDescription === 'function') {
615
+ window.editNodeDescription(node);
616
+ }
617
+ });
607
618
  }
608
619
  if (el.textContent !== text) el.textContent = text;
609
620
  positionNodeDescriptionEl(node, el);
@@ -614,9 +625,16 @@ function positionNodeDescriptionEl(node, el) {
614
625
  if (!el) return;
615
626
  const w = node.width || 280;
616
627
  const h = node.height || 200;
617
- const GAP = 4;
628
+ // Описание прикреплено к НИЖНЕЙ части bbox самой ноды (НЕ ниже неё).
629
+ // Раньше top = node.y + h + GAP — sidecar улетал вниз, и если сосед
630
+ // был близко, описание перекрывало его. Юзер: «сейчас на ноде её
631
+ // описание показывается над нодой которая расположена выше».
632
+ // Теперь node.height расширен на descH (см. fitNodeHeightToMedia), и
633
+ // sidecar помещён внутри bbox у самого низа. Соседняя нода физически
634
+ // не может попасть в зону описания.
635
+ const descH = el.offsetHeight || 0;
618
636
  el.style.left = node.x + 'px';
619
- el.style.top = (node.y + h + GAP) + 'px';
637
+ el.style.top = (node.y + h - descH - 2) + 'px';
620
638
  el.style.width = w + 'px';
621
639
  }
622
640
  function removeNodeDescriptionEl(nodeId) {
@@ -640,9 +658,22 @@ function fitNodeHeightToMedia(node, nodeEl, mediaEl) {
640
658
  // Берём актуальную width — node.width если задано, иначе текущий offsetWidth.
641
659
  const w = node.width || nodeEl.offsetWidth;
642
660
  if (!w) return;
643
- const newH = Math.round(w * (natH / natW) + chromeH);
661
+ // Высота описания (если есть) описание занимает нижнюю часть bbox'а
662
+ // ноды (см. positionNodeDescriptionEl). Без этого описание выходило за
663
+ // пределы node.height и «накладывалось» на соседнюю ноду снизу. Юзер:
664
+ // «на ноде её описание показывается над нодой которая расположена выше».
665
+ const descEl = (node.description || '').trim()
666
+ ? document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`)
667
+ : null;
668
+ const descH = descEl ? descEl.offsetHeight + 4 : 0; // +4 чтобы зрительно отделять
669
+ const newH = Math.round(w * (natH / natW) + chromeH + descH);
644
670
  // 2px tolerance — не шумим в save/connections при суб-пиксельных колебаниях.
645
- if (Math.abs(newH - (node.height || 0)) <= 2) return;
671
+ if (Math.abs(newH - (node.height || 0)) <= 2) {
672
+ // Высоту не меняем, но позицию sidecar'а всё равно обновим — а вдруг
673
+ // описание появилось/изменилось до того как media догрузилось.
674
+ positionNodeDescriptionEl(node);
675
+ return;
676
+ }
646
677
  node.height = newH;
647
678
  nodeEl.style.height = newH + 'px';
648
679
  if (typeof renderConnections === 'function') renderConnections();