kingkont 0.7.44 → 0.7.46

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/main.js CHANGED
@@ -373,12 +373,12 @@ function openSettingsWindow() {
373
373
  settingsWin.focus();
374
374
  return;
375
375
  }
376
+ // Без parent: win — на macOS Sequoia 15.7 + Electron 32 child-windows
377
+ // могут крашить V8 (см. комментарий в openUpdatesWindow).
376
378
  settingsWin = new BrowserWindow({
377
379
  width: 560,
378
380
  height: 640,
379
381
  title: 'Настройки',
380
- parent: win || undefined,
381
- modal: false,
382
382
  resizable: false,
383
383
  minimizable: false,
384
384
  maximizable: false,
@@ -405,12 +405,13 @@ function openUpdatesWindow() {
405
405
  updatesWin.focus();
406
406
  return;
407
407
  }
408
+ // Не используем parent: win — на macOS Sequoia 15.7 + Electron 32 это
409
+ // приводит к V8 CHECK-крашу `v8::BackingStore::MaxByteLength` при создании
410
+ // child-window. Окно обновлений и так логически независимое.
408
411
  updatesWin = new BrowserWindow({
409
412
  width: 480,
410
413
  height: 380,
411
414
  title: 'Обновления',
412
- parent: win || undefined,
413
- modal: false,
414
415
  resizable: false,
415
416
  minimizable: false,
416
417
  maximizable: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.44",
3
+ "version": "0.7.46",
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
@@ -599,6 +599,7 @@ async function openFilm(handle) {
599
599
  // Закрыть текущий проект: выгрузить state, скрыть секции, восстановить
600
600
  // шапку, оставить запись в idb (чтобы Recent работал).
601
601
  async function closeProject() {
602
+ stopExternalWatcher();
602
603
  if (state.currentBoard?.urls) {
603
604
  for (const u of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(u);
604
605
  }
@@ -986,6 +987,8 @@ $('newLocation').addEventListener('click', async () => {
986
987
 
987
988
  // =================== Board (универсально для серии и персонажа) ===================
988
989
  async function selectBoard(board) {
990
+ // Останавливаем file-watcher предыдущей доски (если был).
991
+ stopExternalWatcher();
989
992
  if (state.currentBoard?.urls) {
990
993
  for (const url of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(url);
991
994
  }
@@ -1006,6 +1009,8 @@ async function selectBoard(board) {
1006
1009
  history: meta.history || { past: [], future: [] },
1007
1010
  },
1008
1011
  urls: {},
1012
+ dirty: false,
1013
+ lastDiskMtime: await _readSceneMtime(board.handle),
1009
1014
  };
1010
1015
  if (meta._migrated) await saveBoardMetadata(board.handle, state.currentBoard.metadata);
1011
1016
 
@@ -1076,6 +1081,104 @@ async function selectBoard(board) {
1076
1081
  resumeJob(n, state.currentBoard.key, state.currentBoard.handle);
1077
1082
  }
1078
1083
  }
1084
+
1085
+ // File-watcher: следим что scene.json не менялся снаружи (CLI / другой
1086
+ // редактор / git pull). Если поменялся — silent reload (если у нас нет
1087
+ // pending-изменений) или conflict-диалог.
1088
+ startExternalWatcher();
1089
+ }
1090
+
1091
+ // =============================================================================
1092
+ // External file watcher (FSAH polling).
1093
+ // FSAH не имеет нативного file-watch API — поллим scene.json's lastModified
1094
+ // раз в EXTERNAL_POLL_MS. Если mtime новее чем lastDiskMtime (который мы
1095
+ // обновляем при каждом своём write) — кто-то правил scene.json извне.
1096
+ // • dirty=false → тихо перечитать и перерендерить
1097
+ // • dirty=true → confirm() «перечитать с диска? текущие правки утеряются»
1098
+ // =============================================================================
1099
+
1100
+ const EXTERNAL_POLL_MS = 2000;
1101
+ let _externalWatchTimer = null;
1102
+ let _externalReloadInFlight = false;
1103
+
1104
+ async function _readSceneMtime(boardHandle) {
1105
+ try {
1106
+ const fh = await boardHandle.getFileHandle('scene.json');
1107
+ return (await fh.getFile()).lastModified;
1108
+ } catch { return null; }
1109
+ }
1110
+
1111
+ function stopExternalWatcher() {
1112
+ if (_externalWatchTimer) { clearInterval(_externalWatchTimer); _externalWatchTimer = null; }
1113
+ }
1114
+
1115
+ function startExternalWatcher() {
1116
+ stopExternalWatcher();
1117
+ _externalWatchTimer = setInterval(checkExternalChanges, EXTERNAL_POLL_MS);
1118
+ }
1119
+
1120
+ async function checkExternalChanges() {
1121
+ if (_externalReloadInFlight) return;
1122
+ if (!state.currentBoard?.handle) { stopExternalWatcher(); return; }
1123
+ // Если у нас сейчас pending save (saveTimer != null в media.js) — пропускаем
1124
+ // тик; иначе сравним свой grace-mtime с актуальным после нашего же flush.
1125
+ // Для простоты не лезем в saveTimer — dirty флаг покрывает этот сценарий.
1126
+ const mtime = await _readSceneMtime(state.currentBoard.handle);
1127
+ if (mtime == null || state.currentBoard.lastDiskMtime == null) return;
1128
+ if (mtime <= state.currentBoard.lastDiskMtime) return;
1129
+
1130
+ // Обнаружено внешнее изменение.
1131
+ _externalReloadInFlight = true;
1132
+ try {
1133
+ if (state.currentBoard.dirty) {
1134
+ // Conflict: спросить юзера.
1135
+ const ok = window.confirm(
1136
+ 'scene.json изменён извне (CLI или другой редактор).\n\n' +
1137
+ 'Перечитать с диска? — текущие НЕсохранённые изменения будут утеряны.\n' +
1138
+ 'Cancel — оставить свою версию (при следующем сохранении она затрёт внешнюю).'
1139
+ );
1140
+ if (!ok) {
1141
+ // Юзер выбрал «keep mine». Обновляем lastDiskMtime до текущего —
1142
+ // иначе диалог будет всплывать на каждом тике поллинга. Следующий
1143
+ // scheduleSave перезапишет внешнюю правку нашей версией.
1144
+ state.currentBoard.lastDiskMtime = mtime;
1145
+ return;
1146
+ }
1147
+ }
1148
+ await reloadCurrentBoardFromDisk();
1149
+ } catch (e) {
1150
+ console.error('external reload failed', e);
1151
+ } finally {
1152
+ _externalReloadInFlight = false;
1153
+ }
1154
+ }
1155
+
1156
+ async function reloadCurrentBoardFromDisk() {
1157
+ const board = state.currentBoard;
1158
+ if (!board?.handle) return;
1159
+ const meta = await loadBoardMetadata(board.handle);
1160
+ // Сохраняем view (чтобы юзер не терял scroll/zoom) — в DOM-state, не в meta.
1161
+ board.metadata = {
1162
+ nodes: meta.nodes,
1163
+ connections: meta.connections || [],
1164
+ view: meta.view || board.metadata.view || null,
1165
+ character: meta.character || null,
1166
+ location: meta.location || null,
1167
+ timeline: meta.timeline || [],
1168
+ history: meta.history || { past: [], future: [] },
1169
+ };
1170
+ board.dirty = false;
1171
+ board.lastDiskMtime = await _readSceneMtime(board.handle);
1172
+ // Очищаем кэш URL (некоторые файлы могли удалиться через CLI).
1173
+ for (const url of Object.values(board.urls || {})) URL.revokeObjectURL(url);
1174
+ board.urls = {};
1175
+ // Перерендерить канвас.
1176
+ await renderCanvas();
1177
+ if (!$('timelinePanel').classList.contains('hidden')) {
1178
+ renderTimeline();
1179
+ scheduleUpdatePreview();
1180
+ }
1181
+ console.log('[watcher] scene.json reloaded from disk (external change detected)');
1079
1182
  }
1080
1183
 
1081
1184
  function ensureConnectionsSvg() {
package/renderer/media.js CHANGED
@@ -291,11 +291,21 @@ const _mediaHydrationObserver = new IntersectionObserver((entries) => {
291
291
  let saveTimer = null;
292
292
  function scheduleSave() {
293
293
  if (!state.currentBoard) return;
294
+ // Mark dirty чтобы external file-watcher (pollExternalChanges в board.js)
295
+ // знал что у нас есть pending изменения; reload без подтверждения не делаем.
296
+ state.currentBoard.dirty = true;
294
297
  clearTimeout(saveTimer);
295
298
  saveTimer = setTimeout(async () => {
296
299
  saveTimer = null;
297
- try { await saveBoardMetadata(state.currentBoard.handle, state.currentBoard.metadata); }
298
- catch (e) { console.error('save failed', e); }
300
+ try {
301
+ const mtime = await saveBoardMetadata(state.currentBoard.handle, state.currentBoard.metadata);
302
+ // Обновляем lastDiskMtime — иначе pollExternalChanges подумает что это
303
+ // внешняя правка и попросит reload.
304
+ if (state.currentBoard && typeof mtime === 'number') {
305
+ state.currentBoard.lastDiskMtime = mtime;
306
+ }
307
+ if (state.currentBoard) state.currentBoard.dirty = false;
308
+ } catch (e) { console.error('save failed', e); }
299
309
  }, 300);
300
310
  }
301
311
 
package/renderer/state.js CHANGED
@@ -324,6 +324,13 @@ async function saveBoardMetadata(boardHandle, meta) {
324
324
  ? meta.history : null,
325
325
  };
326
326
  await writeFile(boardHandle, 'scene.json', JSON.stringify(payload, null, 2));
327
+ // Возвращаем свежий mtime — внешний file-watcher (poller в board.js) сравнит
328
+ // его с lastModified при следующем тике, чтобы не дёргать reload на наш же
329
+ // только что записанный файл.
330
+ try {
331
+ const fh = await boardHandle.getFileHandle('scene.json');
332
+ return (await fh.getFile()).lastModified;
333
+ } catch { return null; }
327
334
  }
328
335
 
329
336
  function getFileType(file) {