kingkont 0.20.5 → 0.20.7

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
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>Видео-редактор сериала</title>
6
+ <title>KingKont</title>
7
7
  <link rel="stylesheet" href="renderer/styles.css">
8
8
  </head>
9
9
  <body>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.20.5",
3
+ "version": "0.20.7",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1467,6 +1467,8 @@ async function openFilm(handle) {
1467
1467
  // Подзаголовок шапки = имя открытого проекта (вместо «Видео-редактор»).
1468
1468
  const sub = $('brandSub');
1469
1469
  if (sub) { sub.textContent = handle.name; sub.classList.add('has-project'); }
1470
+ // Tab title = «<имя> — KingKont».
1471
+ document.title = (handle.name || 'Project') + ' — KingKont';
1470
1472
  // Для cloud-проектов имя авторитетное на сервере (manifest.name). Если
1471
1473
  // юзер пришёл по уведомлению / прямой ссылке — handle.name мог быть
1472
1474
  // id или старое имя; подтягиваем актуальное и обновляем шапку +
@@ -1477,6 +1479,7 @@ async function openFilm(handle) {
1477
1479
  .then(proj => {
1478
1480
  if (!proj?.name || state.cloudProjectId !== proj.id) return;
1479
1481
  if (sub && proj.name !== sub.textContent) sub.textContent = proj.name;
1482
+ document.title = proj.name + ' — KingKont';
1480
1483
  // Patch handle.name (используется sidebar/recents и т.п.).
1481
1484
  try { handle.name = proj.name; } catch {}
1482
1485
  // Sync cache.
@@ -1518,6 +1521,7 @@ async function openFilm(handle) {
1518
1521
  }
1519
1522
 
1520
1523
  const raw = localStorage.getItem(`lastBoard:${handle.name}`);
1524
+ let restored = false;
1521
1525
  if (raw) {
1522
1526
  try {
1523
1527
  const last = JSON.parse(raw);
@@ -1525,8 +1529,18 @@ async function openFilm(handle) {
1525
1529
  : last.kind === 'location' ? await listLocations(handle)
1526
1530
  : await listEpisodes(handle);
1527
1531
  const found = list.find(x => x.name === last.name);
1528
- if (found) await selectBoard({ kind: last.kind, ...found });
1529
- } catch {}
1532
+ if (found) { await selectBoard({ kind: last.kind, ...found }); restored = true; }
1533
+ } catch (e) { console.warn('[openFilm] restore lastBoard failed:', e?.message || e); }
1534
+ }
1535
+ // Нет запомненной доски (первый open проекта) — авто-открываем первую сцену.
1536
+ // Без этого юзер видит пустой canvas с подсказкой «выбери сцену» и должен
1537
+ // лишний раз кликать в сайдбаре.
1538
+ if (!restored && !state.currentBoard) {
1539
+ try {
1540
+ const eps = await listEpisodes(handle);
1541
+ console.log('[openFilm] auto-select first scene:', eps.length, 'episodes');
1542
+ if (eps.length) await selectBoard({ kind: 'episode', ...eps[0] });
1543
+ } catch (e) { console.warn('[openFilm] auto-select failed:', e?.message || e); }
1530
1544
  }
1531
1545
 
1532
1546
  // Сканируем все доски на незавершённые задачи генерации (после перезагрузки)
@@ -1572,6 +1586,12 @@ async function closeProject() {
1572
1586
  catch (e) { console.warn(`[closeProject] ${label} failed:`, e?.message || e); }
1573
1587
  };
1574
1588
 
1589
+ // КРИТИЧНО первым шагом: слить pending debounced save (см. media.js
1590
+ // scheduleSave). Без этого мутации только что отработавшего chat-тула
1591
+ // теряются — saveTimer ещё ждёт 300мс, а мы уже обнулили state.currentBoard.
1592
+ await safeAwait('flush pending save', async () => {
1593
+ if (typeof window.flushScheduledSave === 'function') await window.flushScheduledSave();
1594
+ });
1575
1595
  safe('persist welcome flag', () => {
1576
1596
  localStorage.setItem('welcomeOnNextStart', '1');
1577
1597
  localStorage.setItem('lastLocation', 'welcome');
@@ -1599,6 +1619,12 @@ async function closeProject() {
1599
1619
  safe('dispatch project-changed', () => {
1600
1620
  window.dispatchEvent(new CustomEvent('project-changed'));
1601
1621
  });
1622
+ // Web: чистим hash (мы вернулись на welcome).
1623
+ if (window.__KINGKONT_WEB__ && location.hash.startsWith('#project=')) {
1624
+ history.replaceState(null, '', location.pathname + location.search);
1625
+ }
1626
+ // Reset tab title.
1627
+ document.title = 'KingKont';
1602
1628
  state.charactersInfo = [];
1603
1629
  state.locationsInfo = [];
1604
1630
  state.selectedNodeIds.clear();
@@ -279,6 +279,7 @@
279
279
  touchCloudOpened(projectId);
280
280
  await openFilm(handle);
281
281
  setCloudButtonsVisibility();
282
+ if (window.__KINGKONT_WEB__) location.hash = '#project=' + projectId;
282
283
  return;
283
284
  }
284
285
  }
@@ -398,6 +399,7 @@
398
399
  touchCloudOpened(projectId);
399
400
  await openFilm(handle);
400
401
  setCloudButtonsVisibility();
402
+ if (window.__KINGKONT_WEB__) location.hash = '#project=' + projectId;
401
403
  PROGRESS.hide();
402
404
  } catch (e) {
403
405
  PROGRESS.hide();
@@ -24,6 +24,14 @@ canvasWrap.addEventListener('contextmenu', e => {
24
24
  // Rubber-band селектор + сброс выделения по клику в пустое место
25
25
  canvasWrap.addEventListener('mousedown', e => {
26
26
  if (!state.currentBoard) return;
27
+ // Middle-button (button=1) → pan-режим. Дефолтное поведение браузера —
28
+ // auto-scroll режим со специальным курсором; перехватываем чтобы пана
29
+ // через drag, без активации auto-scroll.
30
+ if (e.button === 1) {
31
+ e.preventDefault();
32
+ startMiddleMousePan(e);
33
+ return;
34
+ }
27
35
  if (e.button !== 0) return;
28
36
  if (e.target.closest('.node, .conn, .anchor, .resize-handle, button, input, textarea, select, .modal, #addMenu, #nodeMenu')) return;
29
37
  // Если без модификатора — сбрасываем старое выделение
@@ -36,6 +44,29 @@ canvasWrap.addEventListener('mousedown', e => {
36
44
  startRubberBand(e, additive);
37
45
  });
38
46
 
47
+ // Pan через зажатую среднюю кнопку. Курсор меняем на grab во время дrag'а.
48
+ function startMiddleMousePan(e) {
49
+ const startX = e.clientX, startY = e.clientY;
50
+ const startScrollLeft = canvasWrap.scrollLeft;
51
+ const startScrollTop = canvasWrap.scrollTop;
52
+ const prevCursor = canvasWrap.style.cursor;
53
+ canvasWrap.style.cursor = 'grabbing';
54
+ document.body.style.userSelect = 'none';
55
+ const onMove = ev => {
56
+ canvasWrap.scrollLeft = startScrollLeft - (ev.clientX - startX);
57
+ canvasWrap.scrollTop = startScrollTop - (ev.clientY - startY);
58
+ };
59
+ const onUp = ev => {
60
+ if (ev.button !== 1) return;
61
+ document.removeEventListener('mousemove', onMove);
62
+ document.removeEventListener('mouseup', onUp);
63
+ canvasWrap.style.cursor = prevCursor;
64
+ document.body.style.userSelect = '';
65
+ };
66
+ document.addEventListener('mousemove', onMove);
67
+ document.addEventListener('mouseup', onUp);
68
+ }
69
+
39
70
  function startRubberBand(e, additive) {
40
71
  const rectStart = canvas.getBoundingClientRect();
41
72
  const startC = { x: (e.clientX - rectStart.left) / state.zoom, y: (e.clientY - rectStart.top) / state.zoom };
package/renderer/media.js CHANGED
@@ -345,14 +345,23 @@ const _mediaHydrationObserver = new IntersectionObserver((entries) => {
345
345
  }, { rootMargin: '400px' });
346
346
 
347
347
  let saveTimer = null;
348
+ let _saveInFlight = null; // promise of currently-running save, или null
348
349
  function scheduleSave() {
349
350
  if (!state.currentBoard) return;
350
351
  // Mark dirty чтобы external file-watcher (pollExternalChanges в board.js)
351
352
  // знал что у нас есть pending изменения; reload без подтверждения не делаем.
352
353
  state.currentBoard.dirty = true;
353
354
  clearTimeout(saveTimer);
354
- saveTimer = setTimeout(async () => {
355
- saveTimer = null;
355
+ saveTimer = setTimeout(() => { saveTimer = null; _runSaveNow(); }, 300);
356
+ }
357
+
358
+ // Сериализуем последовательные save'ы (если предыдущий ещё в полёте — ждём
359
+ // его прежде чем запускать новый). Без этого два scheduleSave подряд могли
360
+ // бы конкурентно дёргать FSAH-handle.
361
+ async function _runSaveNow() {
362
+ if (_saveInFlight) { try { await _saveInFlight; } catch {} }
363
+ if (!state.currentBoard?.handle) return;
364
+ _saveInFlight = (async () => {
356
365
  try {
357
366
  const mtime = await saveBoardMetadata(state.currentBoard.handle, state.currentBoard.metadata);
358
367
  // Обновляем lastDiskMtime — иначе pollExternalChanges подумает что это
@@ -362,8 +371,24 @@ function scheduleSave() {
362
371
  }
363
372
  if (state.currentBoard) state.currentBoard.dirty = false;
364
373
  } catch (e) { console.error('save failed', e); }
365
- }, 300);
374
+ })();
375
+ try { await _saveInFlight; } finally { _saveInFlight = null; }
376
+ }
377
+
378
+ // Принудительно слить pending debounced save (и дождаться уже идущего).
379
+ // Нужно перед closeProject — иначе тулы чата, которые только что
380
+ // scheduleSave'нули, теряют изменения когда state.currentBoard обнуляется
381
+ // до того как 300мс-таймер сработает.
382
+ async function flushScheduledSave() {
383
+ if (saveTimer) {
384
+ clearTimeout(saveTimer);
385
+ saveTimer = null;
386
+ await _runSaveNow();
387
+ } else if (_saveInFlight) {
388
+ try { await _saveInFlight; } catch {}
389
+ }
366
390
  }
391
+ window.flushScheduledSave = flushScheduledSave;
367
392
 
368
393
  async function refreshNodeDOM(nodeId) {
369
394
  if (!state.currentBoard) return;