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 +1 -1
- package/package.json +1 -1
- package/renderer/board.js +28 -2
- package/renderer/cloudProjects.js +2 -0
- package/renderer/generate.js +31 -0
- package/renderer/media.js +28 -3
package/index.html
CHANGED
package/package.json
CHANGED
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();
|
package/renderer/generate.js
CHANGED
|
@@ -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(
|
|
355
|
-
|
|
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
|
-
}
|
|
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;
|