kingkont 0.7.64 → 0.7.66

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
@@ -457,6 +457,38 @@
457
457
  </div>
458
458
 
459
459
  <!-- ===== Полноэкранный просмотр (image/video) ===== -->
460
+ <!-- ===== Updates overlay (вместо отдельного BrowserWindow — на Sequoia
461
+ child-windows крашат V8 в ValueSerializer). ===== -->
462
+ <div class="modal hidden" id="updatesOverlay">
463
+ <div class="modal-card" style="min-width:480px; max-width:560px;">
464
+ <h3>Обновления KingKont</h3>
465
+ <div class="hint" style="margin-bottom:14px;">Источник: <code>registry.npmjs.org/kingkont</code></div>
466
+ <div style="display:flex; gap:24px; margin-bottom:14px;">
467
+ <div style="flex:1;">
468
+ <div style="color:#888; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Установлено</div>
469
+ <div id="upCurrent" style="font-size:18px; font-family:ui-monospace,monospace;">—</div>
470
+ </div>
471
+ <div style="flex:1;">
472
+ <div style="color:#888; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; margin-bottom:4px;">Доступно</div>
473
+ <div id="upLatest" style="font-size:18px; font-family:ui-monospace,monospace;">…</div>
474
+ </div>
475
+ </div>
476
+ <div id="upUpdateBlock" style="display:none;">
477
+ <div style="display:flex; gap:8px; align-items:center; margin-bottom:8px; flex-wrap:wrap;">
478
+ <button id="upInstallBtn" class="primary">⬇ Установить и перезапустить</button>
479
+ <button id="upRelaunchBtn" class="primary" style="display:none; background:#16a34a;">↻ Перезапустить сейчас</button>
480
+ </div>
481
+ <pre id="upInstallLog" style="display:none; max-height:200px; overflow-y:auto; margin-top:10px; font-family:ui-monospace,monospace; font-size:11px; line-height:1.4; background:#0e0e0e; border:1px solid #2a2a2a; border-radius:4px; padding:8px 10px; color:#aaa; white-space:pre-wrap; word-break:break-all;"></pre>
482
+ </div>
483
+ <div id="upToDate" style="display:none; color:#16a34a; font-size:12px;">✓ У вас последняя версия.</div>
484
+ <div id="upError" style="display:none; color:#ef4444; font-size:12px; margin-top:12px;"></div>
485
+ <div class="modal-actions">
486
+ <button id="upRecheck">Проверить ещё раз</button>
487
+ <button id="upClose" class="primary">Закрыть</button>
488
+ </div>
489
+ </div>
490
+ </div>
491
+
460
492
  <!-- ===== Дефолтные промпты сцены (multi, с тегами kind) ===== -->
461
493
  <div class="modal hidden" id="defaultPromptsModal">
462
494
  <div class="modal-card" style="min-width:600px; max-width:760px;">
package/main.js CHANGED
@@ -398,37 +398,20 @@ ipcMain.handle('settings-window:close', () => {
398
398
  });
399
399
  ipcMain.handle('settings-window:open', () => openSettingsWindow());
400
400
 
401
- // ===== Updates window + npm version check =====
402
- let updatesWin = null;
401
+ // ===== Updates: показываем как overlay внутри основного окна =====
402
+ // Раньше открывалось отдельное BrowserWindow. На macOS Sequoia 15.7 это
403
+ // крашило V8 в ValueSerializer при IPC (даже после bump'а до Electron 33).
404
+ // Решение — рендерить overlay внутри main window'а, child window не создаём.
403
405
  function openUpdatesWindow() {
404
- if (updatesWin && !updatesWin.isDestroyed()) {
405
- updatesWin.focus();
406
- return;
406
+ if (win && !win.isDestroyed()) {
407
+ win.show();
408
+ win.focus();
409
+ win.webContents.send('menu:open-updates');
407
410
  }
408
- // Не используем parent: win — на macOS Sequoia 15.7 + Electron 32 это
409
- // приводит к V8 CHECK-крашу `v8::BackingStore::MaxByteLength` при создании
410
- // child-window. Окно обновлений и так логически независимое.
411
- updatesWin = new BrowserWindow({
412
- width: 480,
413
- height: 380,
414
- title: 'Обновления',
415
- resizable: false,
416
- minimizable: false,
417
- maximizable: false,
418
- backgroundColor: '#1a1a1a',
419
- webPreferences: {
420
- contextIsolation: true,
421
- nodeIntegration: false,
422
- preload: path.join(__dirname, 'preload.js'),
423
- },
424
- });
425
- updatesWin.removeMenu();
426
- updatesWin.loadFile(path.join(__dirname, 'updates.html'));
427
- updatesWin.on('closed', () => { updatesWin = null; });
428
411
  }
429
- ipcMain.handle('updates-window:close', () => {
430
- if (updatesWin && !updatesWin.isDestroyed()) updatesWin.close();
431
- });
412
+ // Старый close-window IPC — оставлен no-op для backward-compat (если old
413
+ // updates.html ещё где-то остался). Renderer теперь сам закрывает overlay.
414
+ ipcMain.handle('updates-window:close', () => { /* no-op */ });
432
415
 
433
416
  // Простой semver-сравнитель: возвращает true если b > a (есть более свежая).
434
417
  // Поддерживает X.Y.Z и pre-release suffix через '-' (rc/beta пропускаем как not-newer).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.64",
3
+ "version": "0.7.66",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/preload.js CHANGED
@@ -18,7 +18,7 @@ contextBridge.exposeInMainWorld('appMenu', {
18
18
  on: (channel, cb) => {
19
19
  const allowed = ['open-film', 'new-episode', 'new-character', 'new-location',
20
20
  'undo', 'redo', 'toggle-timeline', 'close-project',
21
- 'close-window-or-project', 'open-settings'];
21
+ 'close-window-or-project', 'open-settings', 'open-updates'];
22
22
  if (!allowed.includes(channel)) return;
23
23
  ipcRenderer.on(`menu:${channel}`, (_e, payload) => cb(payload));
24
24
  },
package/renderer/board.js CHANGED
@@ -36,6 +36,7 @@ window.addEventListener('DOMContentLoaded', async () => {
36
36
  else window.appWindow?.minimize();
37
37
  });
38
38
  window.appMenu.on('open-settings', () => openSettings());
39
+ window.appMenu.on('open-updates', () => openUpdatesOverlay());
39
40
  }
40
41
  // Восстановить состояние панелей таймлайна/превью/реплик
41
42
  const tlOpen = localStorage.getItem('timelineOpen') === '1';
@@ -603,6 +604,9 @@ async function closeProject() {
603
604
  if (state.currentBoard?.urls) {
604
605
  for (const u of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(u);
605
606
  }
607
+ // Освобождаем blob URL'ы для миниатюр @-popup'а — они грузились лазиво
608
+ // при открытии mention-popup, кэш накапливался.
609
+ if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
606
610
  state.filmHandle = null;
607
611
  state.currentBoard = null;
608
612
  window.appProject?.notifyState(false);
@@ -1312,6 +1316,91 @@ async function selectBoard(board) {
1312
1316
  startExternalWatcher();
1313
1317
  }
1314
1318
 
1319
+ // =============================================================================
1320
+ // Updates overlay — проверка/установка обновлений KingKont прямо в основном
1321
+ // окне (без создания child BrowserWindow, который на macOS Sequoia крашит
1322
+ // V8 в ValueSerializer при IPC).
1323
+ // =============================================================================
1324
+
1325
+ let _updatesUnsubLog = null;
1326
+
1327
+ async function openUpdatesOverlay() {
1328
+ const overlay = $('updatesOverlay');
1329
+ if (!overlay) return;
1330
+ overlay.classList.remove('hidden');
1331
+ // Сброс state.
1332
+ $('upError').style.display = 'none';
1333
+ $('upUpdateBlock').style.display = 'none';
1334
+ $('upToDate').style.display = 'none';
1335
+ $('upInstallLog').style.display = 'none';
1336
+ $('upRelaunchBtn').style.display = 'none';
1337
+ $('upInstallBtn').style.display = '';
1338
+ $('upInstallBtn').disabled = false;
1339
+ $('upInstallBtn').textContent = '⬇ Установить и перезапустить';
1340
+ $('upLatest').textContent = '…';
1341
+ await checkUpdates();
1342
+ }
1343
+
1344
+ async function checkUpdates() {
1345
+ $('upError').style.display = 'none';
1346
+ $('upUpdateBlock').style.display = 'none';
1347
+ $('upToDate').style.display = 'none';
1348
+ $('upLatest').textContent = '…';
1349
+ $('upRecheck').disabled = true;
1350
+ try {
1351
+ const r = await window.appUpdates.check();
1352
+ $('upCurrent').textContent = r.current;
1353
+ $('upLatest').textContent = r.latest;
1354
+ if (r.isNew) {
1355
+ $('upUpdateBlock').style.display = '';
1356
+ } else {
1357
+ $('upToDate').style.display = '';
1358
+ }
1359
+ $('upUpdateBlock').dataset.target = r.latest;
1360
+ } catch (e) {
1361
+ $('upLatest').textContent = '—';
1362
+ $('upError').style.display = '';
1363
+ $('upError').textContent = 'Не удалось проверить: ' + (e?.message || String(e));
1364
+ } finally {
1365
+ $('upRecheck').disabled = false;
1366
+ }
1367
+ }
1368
+
1369
+ document.getElementById('upClose')?.addEventListener('click', () => {
1370
+ $('updatesOverlay').classList.add('hidden');
1371
+ if (_updatesUnsubLog) { _updatesUnsubLog(); _updatesUnsubLog = null; }
1372
+ });
1373
+ document.getElementById('upRecheck')?.addEventListener('click', () => checkUpdates());
1374
+ document.getElementById('upInstallBtn')?.addEventListener('click', async () => {
1375
+ const btn = $('upInstallBtn');
1376
+ const log = $('upInstallLog');
1377
+ const target = $('upUpdateBlock').dataset.target || 'latest';
1378
+ btn.disabled = true;
1379
+ btn.textContent = 'Устанавливаю…';
1380
+ log.style.display = '';
1381
+ log.textContent = '';
1382
+ log.style.color = '#aaa';
1383
+ if (_updatesUnsubLog) _updatesUnsubLog();
1384
+ _updatesUnsubLog = window.appUpdates.onInstallOutput(({ stream, text }) => {
1385
+ log.textContent += text;
1386
+ log.scrollTop = log.scrollHeight;
1387
+ if (stream === 'stderr') log.style.color = '#f88';
1388
+ });
1389
+ try {
1390
+ await window.appUpdates.install(target);
1391
+ btn.style.display = 'none';
1392
+ $('upRelaunchBtn').style.display = '';
1393
+ } catch (e) {
1394
+ log.style.color = '#f88';
1395
+ log.textContent += `\n[error] ${e?.message || String(e)}`;
1396
+ btn.disabled = false;
1397
+ btn.textContent = '⬇ Попробовать снова';
1398
+ } finally {
1399
+ if (_updatesUnsubLog) { _updatesUnsubLog(); _updatesUnsubLog = null; }
1400
+ }
1401
+ });
1402
+ document.getElementById('upRelaunchBtn')?.addEventListener('click', () => window.appUpdates.relaunch());
1403
+
1315
1404
  // =============================================================================
1316
1405
  // External file watcher (FSAH polling).
1317
1406
  // FSAH не имеет нативного file-watch API — поллим scene.json's lastModified
package/renderer/media.js CHANGED
@@ -1132,6 +1132,8 @@ function updateMentionPopup() {
1132
1132
  type: 'image',
1133
1133
  scope: 'char-image',
1134
1134
  charName: c.name,
1135
+ file: img.file,
1136
+ boardHandle: c.handle,
1135
1137
  });
1136
1138
  }
1137
1139
  }
@@ -1146,6 +1148,8 @@ function updateMentionPopup() {
1146
1148
  type: 'image',
1147
1149
  scope: 'loc-image',
1148
1150
  locName: l.name,
1151
+ file: img.file,
1152
+ boardHandle: l.handle,
1149
1153
  });
1150
1154
  }
1151
1155
  }
@@ -1162,6 +1166,9 @@ function updateMentionPopup() {
1162
1166
  type: 'image',
1163
1167
  scope: 'char',
1164
1168
  hasImages, hasSheet: !!c.characterSheet,
1169
+ // Превью: characterSheet если есть, иначе первая imageNodes.
1170
+ file: c.characterSheet || c.imageNodes?.[0]?.file || null,
1171
+ boardHandle: c.handle,
1165
1172
  });
1166
1173
  }
1167
1174
  items = items.filter(s => s.key.toLowerCase().includes(query)).slice(0, 16);
@@ -1174,7 +1181,11 @@ function updateMentionPopup() {
1174
1181
  for (const n of state.currentBoard.metadata.nodes) {
1175
1182
  if (!n.name || !n.id) continue;
1176
1183
  if (!allowed.includes(n.type)) continue;
1177
- items.push({ key: n.name, label: n.name, type: n.type, scope: 'board' });
1184
+ items.push({
1185
+ key: n.name, label: n.name, type: n.type, scope: 'board',
1186
+ file: n.file || null,
1187
+ boardHandle: state.currentBoard.handle,
1188
+ });
1178
1189
  }
1179
1190
  if (state.genKind === 'image' || state.genKind === 'video') {
1180
1191
  for (const c of state.charactersInfo) {
@@ -1187,6 +1198,8 @@ function updateMentionPopup() {
1187
1198
  type: 'image',
1188
1199
  scope: 'char',
1189
1200
  hasImages, hasSheet: !!c.characterSheet,
1201
+ file: c.characterSheet || c.imageNodes?.[0]?.file || null,
1202
+ boardHandle: c.handle,
1190
1203
  });
1191
1204
  }
1192
1205
  }
@@ -1200,6 +1213,8 @@ function updateMentionPopup() {
1200
1213
  type: 'image',
1201
1214
  scope: 'loc',
1202
1215
  hasImages, hasSheet: !!l.sheet,
1216
+ file: l.sheet || l.imageNodes?.[0]?.file || null,
1217
+ boardHandle: l.handle,
1203
1218
  });
1204
1219
  }
1205
1220
  }
@@ -1222,6 +1237,14 @@ function updateMentionPopup() {
1222
1237
  const it = document.createElement('div');
1223
1238
  it.className = 'mit' + (i === 0 ? ' selected' : '');
1224
1239
  it.dataset.idx = i;
1240
+ // Превью миниатюра — только для image-items с file. Лазиво грузим
1241
+ // через blob URL (с кешем mentionThumbCache).
1242
+ if (s.type === 'image' && s.file && s.boardHandle) {
1243
+ const img = document.createElement('img');
1244
+ img.className = 'mit-thumb';
1245
+ loadMentionThumb(s.boardHandle, s.file).then(url => { if (url) img.src = url; }).catch(() => {});
1246
+ it.appendChild(img);
1247
+ }
1225
1248
  const t = document.createElement('span');
1226
1249
  t.className = `mtype ${s.type}`;
1227
1250
  t.textContent = s.scope === 'char' ? 'персонаж'
@@ -1239,6 +1262,30 @@ function updateMentionPopup() {
1239
1262
  popup.classList.remove('hidden');
1240
1263
  }
1241
1264
 
1265
+ // Кеш blob URL'ов для миниатюр @-popup'а. Ключ = boardHandle.name + ':' + file.
1266
+ // blobURL'ы НЕ revoke'аются автоматически — копят память пока юзер открыт
1267
+ // в проекте; при закрытии проекта чистятся (revokeMentionThumbCache).
1268
+ // Это OK потому что миниатюры маленькие, и одна и та же картинка часто
1269
+ // показывается в нескольких popup'ах без релоада.
1270
+ const _mentionThumbCache = new Map();
1271
+ async function loadMentionThumb(boardHandle, file) {
1272
+ if (!boardHandle || !file) return null;
1273
+ const key = (boardHandle.name || '?') + ':' + file;
1274
+ if (_mentionThumbCache.has(key)) return _mentionThumbCache.get(key);
1275
+ try {
1276
+ const fh = await resolveBoardFile(boardHandle, file);
1277
+ const f = await fh.getFile();
1278
+ if (!f.type?.startsWith('image/') && !/\.(jpe?g|png|gif|webp|avif)$/i.test(file)) return null;
1279
+ const url = URL.createObjectURL(f);
1280
+ _mentionThumbCache.set(key, url);
1281
+ return url;
1282
+ } catch { return null; }
1283
+ }
1284
+ function revokeMentionThumbCache() {
1285
+ for (const url of _mentionThumbCache.values()) { try { URL.revokeObjectURL(url); } catch {} }
1286
+ _mentionThumbCache.clear();
1287
+ }
1288
+
1242
1289
  function closeMentionPopup() {
1243
1290
  mentionState.open = false;
1244
1291
  $('mentionPopup').classList.add('hidden');
@@ -1297,20 +1344,59 @@ $('genPrompt').addEventListener('keydown', e => {
1297
1344
  });
1298
1345
 
1299
1346
  // =================== Character settings ===================
1347
+ // Хелпер: пути к именованным image-нодам доски. Если эта доска сейчас
1348
+ // открыта в state.currentBoard — берём свежие in-memory нодии (юзер мог
1349
+ // только что добавить ноду, scheduleSave ещё не flush'нул на диск).
1350
+ // Иначе грузим с диска через loadBoardMetadata.
1351
+ // Включаем nameless-ноды по basename файла — без этого только что
1352
+ // созданные ноды не появляются в @-popup'е.
1353
+ async function _scanBoardForRefs(itemKind, item) {
1354
+ let nodes, character, location;
1355
+ const isActive = state.currentBoard?.kind === itemKind && state.currentBoard.name === item.name;
1356
+ if (isActive) {
1357
+ nodes = state.currentBoard.metadata.nodes || [];
1358
+ character = state.currentBoard.metadata.character;
1359
+ location = state.currentBoard.metadata.location;
1360
+ } else {
1361
+ const meta = await loadBoardMetadata(item.handle);
1362
+ nodes = meta.nodes || [];
1363
+ character = meta.character;
1364
+ location = meta.location;
1365
+ }
1366
+ // Image-ноды с file. Имя берём из n.name если есть, иначе basename файла
1367
+ // без расширения. Дедупим по итоговому labelKey.
1368
+ const seen = new Set();
1369
+ const imageNodes = [];
1370
+ for (const n of nodes) {
1371
+ if (n.type !== 'image' || !n.file) continue;
1372
+ let label = (n.name || '').trim();
1373
+ if (!label) {
1374
+ // basename без расширения
1375
+ const base = n.file.split('/').pop() || '';
1376
+ label = base.replace(/\.[^.]+$/, '') || base;
1377
+ }
1378
+ // Дедуп: если 2 ноды с одинаковым label → к второй припишем -<short id>
1379
+ let key = label;
1380
+ if (seen.has(key)) {
1381
+ key = `${label}-${(n.id || '').slice(0, 4)}`;
1382
+ }
1383
+ seen.add(key);
1384
+ imageNodes.push({ name: key, file: n.file, id: n.id });
1385
+ }
1386
+ return { imageNodes, character, location };
1387
+ }
1388
+
1300
1389
  async function loadAllCharactersInfo() {
1301
1390
  if (!state.filmHandle) return;
1302
1391
  const chars = await listCharacters(state.filmHandle);
1303
1392
  const info = [];
1304
1393
  for (const c of chars) {
1305
1394
  try {
1306
- const meta = await loadBoardMetadata(c.handle);
1307
- const imageNodes = meta.nodes
1308
- .filter(n => n.type === 'image' && n.file && n.name)
1309
- .map(n => ({ name: n.name, file: n.file, id: n.id }));
1395
+ const { imageNodes, character } = await _scanBoardForRefs('character', c);
1310
1396
  info.push({
1311
1397
  name: c.name,
1312
1398
  handle: c.handle,
1313
- ...(meta.character || {}),
1399
+ ...(character || {}),
1314
1400
  imageNodes,
1315
1401
  });
1316
1402
  } catch {}
@@ -1354,14 +1440,11 @@ async function loadAllLocationsInfo() {
1354
1440
  const info = [];
1355
1441
  for (const l of locs) {
1356
1442
  try {
1357
- const meta = await loadBoardMetadata(l.handle);
1358
- const imageNodes = meta.nodes
1359
- .filter(n => n.type === 'image' && n.file && n.name)
1360
- .map(n => ({ name: n.name, file: n.file, id: n.id }));
1443
+ const { imageNodes, location } = await _scanBoardForRefs('location', l);
1361
1444
  info.push({
1362
1445
  name: l.name,
1363
1446
  handle: l.handle,
1364
- sheet: meta.location?.sheet || null,
1447
+ sheet: location?.sheet || null,
1365
1448
  imageNodes,
1366
1449
  });
1367
1450
  } catch {}
@@ -1036,6 +1036,10 @@
1036
1036
  .mention-popup .mit .mtype.audio { background: #6a4a2a; color: #e6c8aa; }
1037
1037
  .mention-popup .mit .mtype.text { background: #2a6a2a; color: #c8e6aa; }
1038
1038
  .mention-popup .mit .mtype.image { background: #5a2a6a; color: #e6aac8; }
1039
+ .mention-popup .mit .mit-thumb {
1040
+ width: 28px; height: 28px; border-radius: 3px; object-fit: cover;
1041
+ flex-shrink: 0; background: #1a1a1a; border: 1px solid #333;
1042
+ }
1039
1043
  .mention-popup .empty-msg { padding: 8px 12px; color: #666; font-size: 12px; }
1040
1044
 
1041
1045
  .settings-row { display: flex; flex-direction: column; gap: 4px; }