kingkont 0.7.53 → 0.7.55

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/index.html CHANGED
@@ -352,6 +352,9 @@
352
352
  </label>
353
353
  <label id="imageOptionsRow">Соотношение сторон
354
354
  <div class="seg-control" id="imageAspectCtl">
355
+ <button class="seg" data-img-asp="source" type="button" id="imgAspSource"
356
+ title="Взять формат у исходной картинки-референса"
357
+ style="display:none;">↺ Как у исходной</button>
355
358
  <button class="seg active" data-img-asp="1:1" type="button">1:1</button>
356
359
  <button class="seg" data-img-asp="16:9" type="button">16:9</button>
357
360
  <button class="seg" data-img-asp="9:16" type="button">9:16</button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.53",
3
+ "version": "0.7.55",
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
@@ -704,32 +704,23 @@ async function promptBoardAspectRatio(kind, item) {
704
704
  const current = isActive
705
705
  ? (state.currentBoard.metadata.settings?.aspectRatio || '9:16')
706
706
  : (await loadBoardMetadata(item.handle)).settings?.aspectRatio || '9:16';
707
- const choices = ASPECT_OPTIONS.join(' / ');
708
- const ans = window.prompt(
709
- `Соотношение сторон для генераций image/video в «${item.name}».\n` +
710
- `Применяется когда нода или gen-модалка не указывают своё.\n\n` +
711
- `Доступные значения: ${choices}\n` +
712
- `Текущее: ${current}`,
707
+ const chosen = await askChoice(
708
+ `Соотношение сторон для «${item.name}» (image/video). Текущее: ${current}`,
709
+ ASPECT_OPTIONS,
713
710
  current,
714
711
  );
715
- if (ans == null) return;
716
- const trimmed = ans.trim();
717
- if (!trimmed) return;
718
- if (!ASPECT_OPTIONS.includes(trimmed)) {
719
- alert('Неподдерживаемое значение. Доступны: ' + choices);
720
- return;
721
- }
712
+ if (chosen == null || chosen === current) return;
722
713
  if (isActive) {
723
714
  if (!state.currentBoard.metadata.settings) state.currentBoard.metadata.settings = {};
724
- state.currentBoard.metadata.settings.aspectRatio = trimmed;
715
+ state.currentBoard.metadata.settings.aspectRatio = chosen;
725
716
  scheduleSave();
726
717
  } else {
727
718
  // Доска не активна — пишем напрямую.
728
719
  const meta = await loadBoardMetadata(item.handle);
729
- meta.settings = { ...(meta.settings || {}), aspectRatio: trimmed };
720
+ meta.settings = { ...(meta.settings || {}), aspectRatio: chosen };
730
721
  await saveBoardMetadata(item.handle, meta);
731
722
  }
732
- console.log(`[board] ${kind}/${item.name} aspectRatio → ${trimmed}`);
723
+ console.log(`[board] ${kind}/${item.name} aspectRatio → ${chosen}`);
733
724
  }
734
725
 
735
726
  // Переименовать board (папку): создать новую папку с тем же именем, перенести
@@ -913,7 +904,7 @@ async function undoBoardDelete() {
913
904
 
914
905
  // Собственный prompt — нативный prompt() в Electron подавлен (deprecated в Chromium).
915
906
  // Возвращает строку или null. Esc/Cancel = null. Enter = подтвердить.
916
- function askName(title, placeholder = '', initialValue = '') {
907
+ function askName(title, placeholder = '', initialValue = '', opts = {}) {
917
908
  return new Promise(resolve => {
918
909
  const overlay = document.createElement('div');
919
910
  overlay.className = 'modal';
@@ -933,9 +924,9 @@ function askName(title, placeholder = '', initialValue = '') {
933
924
  const cancel = document.createElement('button');
934
925
  cancel.textContent = 'Отмена';
935
926
  const ok = document.createElement('button');
936
- // Если поле уже заполнено это редактирование существующего объекта,
937
- // показываем «Сохранить». Иначе создание «Создать».
938
- ok.textContent = initialValue ? 'Сохранить' : 'Создать';
927
+ // Приоритет: явный opts.okText > '«Сохранить» если редактирование (есть
928
+ // initialValue)' > '«Создать» если создание нового объекта'.
929
+ ok.textContent = opts.okText || (initialValue ? 'Сохранить' : 'Создать');
939
930
  ok.className = 'primary';
940
931
  row.append(cancel, ok);
941
932
  box.append(h, inp, row); overlay.append(box);
@@ -952,6 +943,49 @@ function askName(title, placeholder = '', initialValue = '') {
952
943
  });
953
944
  }
954
945
 
946
+ // Похож на askName, но вместо input — список кнопок-опций.
947
+ // Возвращает выбранную строку или null если юзер закрыл.
948
+ // window.prompt() в Electron renderer молча возвращает null — поэтому
949
+ // нельзя использовать его для select-подобных диалогов.
950
+ function askChoice(title, options, currentValue) {
951
+ return new Promise(resolve => {
952
+ const overlay = document.createElement('div');
953
+ overlay.className = 'modal';
954
+ overlay.style.cssText = 'position:fixed; inset:0; background:rgba(0,0,0,0.55); display:flex; align-items:center; justify-content:center; z-index:9999;';
955
+ const box = document.createElement('div');
956
+ box.style.cssText = 'background:#222; border:1px solid #444; border-radius:8px; padding:18px 20px; min-width:360px; box-shadow:0 8px 32px rgba(0,0,0,0.6);';
957
+ const h = document.createElement('h3');
958
+ h.textContent = title;
959
+ h.style.cssText = 'margin:0 0 12px; font-size:14px; color:#e0e0e0;';
960
+ box.append(h);
961
+ const grid = document.createElement('div');
962
+ grid.style.cssText = 'display:flex; flex-wrap:wrap; gap:6px; margin-bottom:14px;';
963
+ const close = (val) => { overlay.remove(); resolve(val); };
964
+ for (const opt of options) {
965
+ const b = document.createElement('button');
966
+ b.textContent = opt;
967
+ b.style.cssText = 'padding:6px 12px; background:#1a1a1a; color:#e0e0e0; border:1px solid #444; border-radius:4px; font-size:13px; cursor:pointer;' +
968
+ (opt === currentValue ? 'border-color:#7c3aed; background:#2a1a3a;' : '');
969
+ b.addEventListener('click', () => close(opt));
970
+ grid.append(b);
971
+ }
972
+ box.append(grid);
973
+ const row = document.createElement('div');
974
+ row.style.cssText = 'display:flex; gap:8px; justify-content:flex-end;';
975
+ const cancel = document.createElement('button');
976
+ cancel.textContent = 'Отмена';
977
+ cancel.addEventListener('click', () => close(null));
978
+ row.append(cancel);
979
+ box.append(row);
980
+ overlay.append(box);
981
+ document.body.append(overlay);
982
+ overlay.addEventListener('mousedown', e => { if (e.target === overlay) close(null); });
983
+ document.addEventListener('keydown', function onEsc(e) {
984
+ if (e.key === 'Escape') { document.removeEventListener('keydown', onEsc); close(null); }
985
+ });
986
+ });
987
+ }
988
+
955
989
  // Лог-буфер: всё что пишем сюда — попадает в console и в window.veLog.
956
990
  // Достать из DevTools одной строкой:
957
991
  // copy(JSON.stringify(window.veLog, null, 2)) // в буфер
@@ -1433,7 +1467,10 @@ async function createNodeEl(node) {
1433
1467
  // =================== Контекстное меню ноды (ПКМ) ===================
1434
1468
  async function renameNode(node) {
1435
1469
  const current = node.name || '';
1436
- const newName = await askName('Имя ноды (для @-ссылок):', '', current);
1470
+ // okText='Сохранить' независимо от того, было имя или нет — мы НЕ создаём
1471
+ // ноду, а присваиваем/меняем имя существующей. Без явного okText askName
1472
+ // показал бы «Создать» для безымянной ноды (initialValue=='').
1473
+ const newName = await askName('Имя ноды (для @-ссылок):', '', current, { okText: 'Сохранить' });
1437
1474
  if (newName == null) return;
1438
1475
  const trimmed = newName.trim();
1439
1476
  // Текст-нода: переименовать соответствующий .md файл
@@ -739,10 +739,73 @@ document.querySelectorAll('#genModal [data-kind]').forEach(b => {
739
739
  });
740
740
  });
741
741
 
742
+ // Вычислить ближайший поддерживаемый aspect ratio из state.sourceRef.
743
+ // Возвращает строку '16:9' / '1:1' / etc, или null если не удалось.
744
+ async function computeSourceImageAspect() {
745
+ if (!state.sourceRef || state.sourceRef.type !== 'image' || !state.sourceRef.file) return null;
746
+ const handle = state.sourceRef.boardHandle || state.currentBoard?.handle;
747
+ if (!handle) return null;
748
+ let url = null;
749
+ try {
750
+ const fh = await resolveBoardFile(handle, state.sourceRef.file);
751
+ const file = await fh.getFile();
752
+ url = URL.createObjectURL(file);
753
+ const dim = await new Promise((res, rej) => {
754
+ const img = new Image();
755
+ img.onload = () => res({ w: img.naturalWidth, h: img.naturalHeight });
756
+ img.onerror = rej;
757
+ img.src = url;
758
+ });
759
+ if (!dim.w || !dim.h) return null;
760
+ return nearestSupportedAspect(dim.w / dim.h);
761
+ } catch { return null; }
762
+ finally { if (url) URL.revokeObjectURL(url); }
763
+ }
764
+
765
+ // Из произвольного ratio выбирает ближайший из поддерживаемых genModal'ом.
766
+ // Сравнение в логарифмической метрике — справедливо для пропорций
767
+ // (1.5 ↔ 0.667 равноудалены от 1.0).
768
+ function nearestSupportedAspect(ratio) {
769
+ const opts = [
770
+ { name: '1:1', v: 1.0 },
771
+ { name: '16:9', v: 16/9 },
772
+ { name: '9:16', v: 9/16 },
773
+ { name: '3:2', v: 3/2 },
774
+ { name: '2:3', v: 2/3 },
775
+ { name: '4:3', v: 4/3 },
776
+ { name: '3:4', v: 3/4 },
777
+ ];
778
+ let best = opts[0];
779
+ let bestDiff = Math.abs(Math.log(ratio / best.v));
780
+ for (const o of opts) {
781
+ const d = Math.abs(Math.log(ratio / o.v));
782
+ if (d < bestDiff) { best = o; bestDiff = d; }
783
+ }
784
+ return best.name;
785
+ }
786
+
742
787
  // === Source frame controls ===
743
788
  function syncSourceRefRow() {
744
789
  const showable = state.sourceRef && (state.genKind === 'image' || state.genKind === 'video');
745
790
  $('sourceRefRow').style.display = showable ? '' : 'none';
791
+ // Кнопка «↺ Как у исходной» в селекторе aspect ratio: показываем только
792
+ // когда есть image-source (для image-генерации). Если source выключен (use=false)
793
+ // — тоже скрываем; нет смысла предлагать aspect которого юзер не использует.
794
+ const srcBtn = $('imgAspSource');
795
+ if (srcBtn) {
796
+ const hasImgSource = state.sourceRef
797
+ && state.sourceRef.type === 'image'
798
+ && state.sourceRef.use
799
+ && state.genKind === 'image';
800
+ srcBtn.style.display = hasImgSource ? '' : 'none';
801
+ // Если кнопка скрылась а была активной — переключаем на дефолтное.
802
+ if (!hasImgSource && state.imageAspect === 'source') {
803
+ state.imageAspect = localStorage.getItem('imageAspect') || '1:1';
804
+ if (state.imageAspect === 'source') state.imageAspect = '1:1';
805
+ document.querySelectorAll('#genModal [data-img-asp]').forEach(b =>
806
+ b.classList.toggle('active', b.dataset.imgAsp === state.imageAspect));
807
+ }
808
+ }
746
809
  }
747
810
 
748
811
  // === Персонажи + Локация: чипы/дропдаун ===
@@ -1016,13 +1079,18 @@ document.querySelectorAll('#genModal [data-vid-model]').forEach(b => {
1016
1079
  localStorage.setItem('videoModel', state.videoModel);
1017
1080
  });
1018
1081
  });
1019
- // Переключатель aspect ratio для image
1082
+ // Переключатель aspect ratio для image. Особый кейс — 'source' (взять
1083
+ // у исходной картинки): не сохраняем в localStorage (это ad-hoc выбор
1084
+ // привязанный к конкретному запуску). Реальный aspectRatio будет
1085
+ // вычислен в момент submit через computeSourceImageAspect().
1020
1086
  document.querySelectorAll('#genModal [data-img-asp]').forEach(b => {
1021
1087
  b.addEventListener('click', () => {
1022
1088
  document.querySelectorAll('#genModal [data-img-asp]').forEach(x => x.classList.remove('active'));
1023
1089
  b.classList.add('active');
1024
1090
  state.imageAspect = b.dataset.imgAsp;
1025
- localStorage.setItem('imageAspect', state.imageAspect);
1091
+ if (state.imageAspect !== 'source') {
1092
+ localStorage.setItem('imageAspect', state.imageAspect);
1093
+ }
1026
1094
  });
1027
1095
  });
1028
1096
  function syncImageAspectActive() {
@@ -1283,6 +1351,17 @@ $('genSubmit').addEventListener('click', async () => {
1283
1351
  }
1284
1352
  let resolvedPrompt = resolveMentions(rawPrompt, mediaRefs);
1285
1353
  if (sourceMarker) resolvedPrompt = sourceMarker + resolvedPrompt;
1354
+
1355
+ // Если выбран aspect 'source' — резолвим в реальный ratio из исходной
1356
+ // картинки ДО того как обнулим state.sourceRef (ниже).
1357
+ let resolvedImageAspect = state.imageAspect;
1358
+ if (state.imageAspect === 'source') {
1359
+ const fromSource = await computeSourceImageAspect();
1360
+ resolvedImageAspect = fromSource || '1:1';
1361
+ if (!fromSource) console.warn('[gen] не смог получить aspect исходной → fallback 1:1');
1362
+ else console.log(`[gen] aspect "как у исходной" → ${fromSource}`);
1363
+ }
1364
+
1286
1365
  // Сбрасываем sourceRef и пикеры после использования
1287
1366
  state.sourceRef = null;
1288
1367
  resetPicks();
@@ -1337,7 +1416,7 @@ $('genSubmit').addEventListener('click', async () => {
1337
1416
  aspectRatio: state.videoAspect,
1338
1417
  } : {}),
1339
1418
  ...(kind === 'image' ? {
1340
- aspectRatio: state.imageAspect,
1419
+ aspectRatio: resolvedImageAspect,
1341
1420
  } : {}),
1342
1421
  },
1343
1422
  };