kingkont 0.7.44 → 0.7.45
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/package.json +1 -1
- package/renderer/board.js +103 -0
- package/renderer/media.js +12 -2
- package/renderer/state.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.45",
|
|
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 {
|
|
298
|
-
|
|
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) {
|