kingkont 0.7.65 → 0.7.67
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 +32 -0
- package/main.js +11 -28
- package/package.json +1 -1
- package/preload.js +1 -1
- package/renderer/board.js +86 -0
- package/renderer/media.js +85 -28
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
|
|
402
|
-
|
|
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 (
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
430
|
-
|
|
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
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';
|
|
@@ -1315,6 +1316,91 @@ async function selectBoard(board) {
|
|
|
1315
1316
|
startExternalWatcher();
|
|
1316
1317
|
}
|
|
1317
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
|
+
|
|
1318
1404
|
// =============================================================================
|
|
1319
1405
|
// External file watcher (FSAH polling).
|
|
1320
1406
|
// FSAH не имеет нативного file-watch API — поллим scene.json's lastModified
|
package/renderer/media.js
CHANGED
|
@@ -1119,40 +1119,81 @@ function updateMentionPopup() {
|
|
|
1119
1119
|
let items = [];
|
|
1120
1120
|
|
|
1121
1121
|
if (dotIdx >= 0) {
|
|
1122
|
-
// Уже выбран character
|
|
1122
|
+
// Уже выбран character / location / __scene (текущая доска) — drilldown.
|
|
1123
1123
|
const ownerName = query.slice(0, dotIdx);
|
|
1124
1124
|
const imgQuery = query.slice(dotIdx + 1);
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1125
|
+
if (ownerName === '__scene') {
|
|
1126
|
+
// Drilldown в ноды ТЕКУЩЕЙ доски (тип фильтруем по genKind).
|
|
1127
|
+
const allowed = state.genKind === 'image' ? ['image', 'text']
|
|
1128
|
+
: state.genKind === 'video' ? ['image', 'video', 'audio', 'text']
|
|
1129
|
+
: ['text', 'image', 'video', 'audio'];
|
|
1130
|
+
// Дедуп: если у двух нод одинаковый label (basename), второй
|
|
1131
|
+
// получает суффикс -<id4>. Это match'ит логику _scanBoardForRefs
|
|
1132
|
+
// для chars/locs.
|
|
1133
|
+
const seen = new Set();
|
|
1134
|
+
for (const n of state.currentBoard.metadata.nodes) {
|
|
1135
|
+
if (!n.id) continue;
|
|
1136
|
+
if (!allowed.includes(n.type)) continue;
|
|
1137
|
+
let label = (n.name || '').trim();
|
|
1138
|
+
if (!label) {
|
|
1139
|
+
label = n.file
|
|
1140
|
+
? (n.file.split('/').pop() || '').replace(/\.[^.]+$/, '')
|
|
1141
|
+
: n.id.slice(0, 8);
|
|
1138
1142
|
}
|
|
1143
|
+
if (!label) label = n.id.slice(0, 8);
|
|
1144
|
+
if (seen.has(label)) label = `${label}-${n.id.slice(0, 4)}`;
|
|
1145
|
+
seen.add(label);
|
|
1146
|
+
if (imgQuery && !label.toLowerCase().includes(imgQuery)) continue;
|
|
1147
|
+
items.push({
|
|
1148
|
+
// key = label: красивая ссылка типа [@kitchen] для безымянной
|
|
1149
|
+
// картинки kitchen.jpg. resolveMentions для board-нод сначала
|
|
1150
|
+
// ищет по name; если не нашёл — gatherMediaRefs упустит,
|
|
1151
|
+
// но мы переименуем после: для board-drill final key = label.
|
|
1152
|
+
// Чтобы он попал в gatherMediaRefs — добавим временный n.name
|
|
1153
|
+
// ниже. На самом деле проще: всегда insert = [@<label>], и в
|
|
1154
|
+
// gatherMediaRefs сравнение идёт по nodeRefKey = name||id.
|
|
1155
|
+
// Если label != name && != id — НЕ попадёт. Поэтому используем
|
|
1156
|
+
// n.name || n.id (id как fallback) — это работает в
|
|
1157
|
+
// resolveMentions.
|
|
1158
|
+
key: n.name || n.id,
|
|
1159
|
+
label, type: n.type, scope: 'board',
|
|
1160
|
+
file: n.file || null,
|
|
1161
|
+
boardHandle: state.currentBoard.handle,
|
|
1162
|
+
});
|
|
1139
1163
|
}
|
|
1140
1164
|
} else {
|
|
1141
|
-
const
|
|
1142
|
-
if (
|
|
1143
|
-
for (const img of (
|
|
1165
|
+
const c = state.charactersInfo.find(x => x.name.toLowerCase() === ownerName);
|
|
1166
|
+
if (c) {
|
|
1167
|
+
for (const img of (c.imageNodes || [])) {
|
|
1144
1168
|
if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
|
|
1145
1169
|
items.push({
|
|
1146
|
-
key: `${
|
|
1170
|
+
key: `${c.name}.${img.name}`,
|
|
1147
1171
|
label: img.name,
|
|
1148
1172
|
type: 'image',
|
|
1149
|
-
scope: '
|
|
1150
|
-
|
|
1173
|
+
scope: 'char-image',
|
|
1174
|
+
charName: c.name,
|
|
1151
1175
|
file: img.file,
|
|
1152
|
-
boardHandle:
|
|
1176
|
+
boardHandle: c.handle,
|
|
1153
1177
|
});
|
|
1154
1178
|
}
|
|
1155
1179
|
}
|
|
1180
|
+
} else {
|
|
1181
|
+
const l = state.locationsInfo.find(x => x.name.toLowerCase() === ownerName);
|
|
1182
|
+
if (l) {
|
|
1183
|
+
for (const img of (l.imageNodes || [])) {
|
|
1184
|
+
if (!imgQuery || img.name.toLowerCase().includes(imgQuery)) {
|
|
1185
|
+
items.push({
|
|
1186
|
+
key: `${l.name}.${img.name}`,
|
|
1187
|
+
label: img.name,
|
|
1188
|
+
type: 'image',
|
|
1189
|
+
scope: 'loc-image',
|
|
1190
|
+
locName: l.name,
|
|
1191
|
+
file: img.file,
|
|
1192
|
+
boardHandle: l.handle,
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1156
1197
|
}
|
|
1157
1198
|
}
|
|
1158
1199
|
} else if (isCharOnly) {
|
|
@@ -1173,17 +1214,26 @@ function updateMentionPopup() {
|
|
|
1173
1214
|
}
|
|
1174
1215
|
items = items.filter(s => s.key.toLowerCase().includes(query)).slice(0, 16);
|
|
1175
1216
|
} else {
|
|
1176
|
-
// Верхний уровень:
|
|
1177
|
-
//
|
|
1217
|
+
// Верхний уровень: вместо плоского списка нод текущей доски —
|
|
1218
|
+
// одна запись «Эта сцена» (drilldown в её ноды). Это убирает
|
|
1219
|
+
// лишний шум когда в проекте много персонажей/локаций. Click →
|
|
1220
|
+
// selectMention вставит [@__scene. и реоткроет popup в drilldown.
|
|
1221
|
+
// Если у текущей доски НЕТ нод вообще — пункт не показываем.
|
|
1178
1222
|
const allowed = state.genKind === 'image' ? ['image', 'text']
|
|
1179
1223
|
: state.genKind === 'video' ? ['image', 'video', 'audio', 'text']
|
|
1180
1224
|
: ['text', 'image', 'video', 'audio'];
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1225
|
+
const boardHasNodes = (state.currentBoard?.metadata?.nodes || []).some(n =>
|
|
1226
|
+
n.id && allowed.includes(n.type)
|
|
1227
|
+
);
|
|
1228
|
+
if (boardHasNodes) {
|
|
1229
|
+
// Превью первой image-ноды для миниатюры (если есть).
|
|
1230
|
+
const firstImg = state.currentBoard.metadata.nodes.find(n => n.type === 'image' && n.file);
|
|
1184
1231
|
items.push({
|
|
1185
|
-
key:
|
|
1186
|
-
|
|
1232
|
+
key: '__scene__top', // не для вставки — drillFromSceneFolder заменит
|
|
1233
|
+
label: 'Эта сцена',
|
|
1234
|
+
type: 'image', // для type-чипа
|
|
1235
|
+
scope: 'scene-folder',
|
|
1236
|
+
file: firstImg?.file || null,
|
|
1187
1237
|
boardHandle: state.currentBoard.handle,
|
|
1188
1238
|
});
|
|
1189
1239
|
}
|
|
@@ -1251,6 +1301,7 @@ function updateMentionPopup() {
|
|
|
1251
1301
|
: s.scope === 'char-image' ? `персонаж·${s.charName}`
|
|
1252
1302
|
: s.scope === 'loc' ? 'локация'
|
|
1253
1303
|
: s.scope === 'loc-image' ? `локация·${s.locName}`
|
|
1304
|
+
: s.scope === 'scene-folder' ? 'сцена'
|
|
1254
1305
|
: s.type;
|
|
1255
1306
|
const nm = document.createElement('span');
|
|
1256
1307
|
nm.textContent = s.label;
|
|
@@ -1301,11 +1352,17 @@ function selectMention(idx = mentionState.selected) {
|
|
|
1301
1352
|
const after = ta.value.slice(cursor);
|
|
1302
1353
|
// Если выбрали персонажа/локацию и у них есть картинки — авто-drill:
|
|
1303
1354
|
// оставляем @name. для дальнейшего ввода имени картинки.
|
|
1355
|
+
// Если выбрали «Эта сцена» (scope='scene-folder') — то же самое со
|
|
1356
|
+
// спец-префиксом __scene. Финальный select board-ноды стирает префикс
|
|
1357
|
+
// и вставляет [@nodename] (см. ветку drillFromScene ниже).
|
|
1304
1358
|
let insert;
|
|
1305
1359
|
let reopen = false;
|
|
1306
1360
|
if ((item.scope === 'char' || item.scope === 'loc') && item.hasImages) {
|
|
1307
1361
|
insert = '[@' + item.key + '.';
|
|
1308
1362
|
reopen = true;
|
|
1363
|
+
} else if (item.scope === 'scene-folder') {
|
|
1364
|
+
insert = '[@__scene.';
|
|
1365
|
+
reopen = true;
|
|
1309
1366
|
} else {
|
|
1310
1367
|
insert = '[@' + (item.key || item.name) + '] ';
|
|
1311
1368
|
}
|