kingkont 0.20.50 → 0.20.53
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/cloudProjects.js +91 -22
- package/renderer/settings.js +35 -16
- package/renderer/styles.css +19 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.53",
|
|
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": {
|
|
@@ -393,6 +393,66 @@
|
|
|
393
393
|
setCloudButtonsVisibility();
|
|
394
394
|
if (typeof window.renderTemplateOverlay === 'function') window.renderTemplateOverlay();
|
|
395
395
|
const bgBoards = Array.isArray(p.manifest?.boards) ? p.manifest.boards : [];
|
|
396
|
+
// Если в местной meta не было cdnByBoard (старая схема — meta
|
|
397
|
+
// сохранена до фичи), либо state.cloudCdnByBoard пустой — строим
|
|
398
|
+
// карту из свежего манифеста и применяем к текущей доске.
|
|
399
|
+
// Иначе картинки в web-view не находятся: state.currentBoard.urls
|
|
400
|
+
// остаётся {} → settings.js hydrate упирается в resolveBoardFile,
|
|
401
|
+
// который в web без локального файла бросает «Файл не найден».
|
|
402
|
+
// (a) Recovery cdnByBoard если в meta его не было.
|
|
403
|
+
const haveCdnMap = state.cloudCdnByBoard
|
|
404
|
+
&& Object.keys(state.cloudCdnByBoard).length > 0;
|
|
405
|
+
if (!haveCdnMap && bgBoards.length) {
|
|
406
|
+
state.cloudCdnByBoard = _buildCdnByBoard(bgBoards);
|
|
407
|
+
_applyCdnUrlsToCurrentBoard();
|
|
408
|
+
try {
|
|
409
|
+
const fresh = { ...meta, cdnByBoard: state.cloudCdnByBoard, syncedAt: Date.now() };
|
|
410
|
+
await writeCloudFile(projectId, '.kingkont-meta.json',
|
|
411
|
+
new TextEncoder().encode(JSON.stringify(fresh, null, 2)));
|
|
412
|
+
} catch {}
|
|
413
|
+
}
|
|
414
|
+
// (b) Recovery scene.json'ов независимо от (a). Старая сессия
|
|
415
|
+
// могла закешировать только часть досок в IDB (юзер не во все
|
|
416
|
+
// сцены кликал) — сайдбар тогда покажет 4 сцены вместо 5.
|
|
417
|
+
// Либо в IDB лежит «стартовый» scene.json на 0 нод. Если
|
|
418
|
+
// local-копия пустее серверной — перезаписываем.
|
|
419
|
+
let wroteAnyScene = false;
|
|
420
|
+
for (const board of bgBoards) {
|
|
421
|
+
if (!board.scene) continue;
|
|
422
|
+
const boardDir = boardKindToDir(board.kind, board.name);
|
|
423
|
+
const scenePath = joinPath(boardDir, 'scene.json');
|
|
424
|
+
let existing = null;
|
|
425
|
+
try {
|
|
426
|
+
existing = window.cloudFs
|
|
427
|
+
? await window.cloudFs.readText(projectId, scenePath).catch(() => null)
|
|
428
|
+
: await window.cloudFsShim?.readTextFile?.(projectId, scenePath).catch(() => null);
|
|
429
|
+
} catch {}
|
|
430
|
+
let needsWrite = !existing;
|
|
431
|
+
if (existing && !needsWrite) {
|
|
432
|
+
try {
|
|
433
|
+
const local = JSON.parse(existing);
|
|
434
|
+
const localN = Array.isArray(local?.nodes) ? local.nodes.length : 0;
|
|
435
|
+
const serverN = Array.isArray(board.scene?.nodes) ? board.scene.nodes.length : 0;
|
|
436
|
+
if (localN < serverN) needsWrite = true;
|
|
437
|
+
} catch { needsWrite = true; }
|
|
438
|
+
}
|
|
439
|
+
if (!needsWrite) continue;
|
|
440
|
+
try {
|
|
441
|
+
await writeCloudFile(projectId, scenePath,
|
|
442
|
+
new TextEncoder().encode(JSON.stringify(board.scene, null, 2)));
|
|
443
|
+
wroteAnyScene = true;
|
|
444
|
+
} catch {}
|
|
445
|
+
}
|
|
446
|
+
if (wroteAnyScene) {
|
|
447
|
+
try {
|
|
448
|
+
if (typeof refreshEpisodes === 'function') await refreshEpisodes();
|
|
449
|
+
if (typeof refreshLocations === 'function') await refreshLocations();
|
|
450
|
+
if (typeof refreshCharacters === 'function') await refreshCharacters();
|
|
451
|
+
} catch {}
|
|
452
|
+
}
|
|
453
|
+
if ((!haveCdnMap || wroteAnyScene) && state.currentBoard && typeof selectBoard === 'function') {
|
|
454
|
+
try { await selectBoard(state.currentBoard); } catch {}
|
|
455
|
+
}
|
|
396
456
|
await _downloadAllTexts(projectId, bgBoards).catch(() => {});
|
|
397
457
|
// Перечитываем тексты ТЕКУЩЕЙ доски (другие — при switchBoard).
|
|
398
458
|
await _refreshCurrentBoardTexts().catch(() => {});
|
|
@@ -451,28 +511,7 @@
|
|
|
451
511
|
// кешируется»). 1024px достаточно и для card-thumbnail и для
|
|
452
512
|
// полноэкранного просмотра. Маленькие thumbs (320x) — отдельной
|
|
453
513
|
// картой не требуются, scene-card сам ресайзит через CSS.
|
|
454
|
-
const cdnByBoard =
|
|
455
|
-
// R2 public bucket `pub-<id>.r2.dev` rate-limited Cloudflare'ом (503 при
|
|
456
|
-
// batch-загрузке из браузера, ~30 одновременных img-fetch'ей). Переписываем
|
|
457
|
-
// на Worker GET endpoint (Worker не лимитирован так агрессивно, +CORS).
|
|
458
|
-
// Старые manifests на чатиуме могут содержать pub-…r2.dev URL'ы — этот
|
|
459
|
-
// rewrite их чинит без серверной миграции.
|
|
460
|
-
const R2_PUB_PREFIX = 'https://pub-cd4114af9f7d44c9bf8c9442bc7dddc2.r2.dev/';
|
|
461
|
-
const R2_WORKER_GET = 'https://kingkont-r2-upload.timur-dd5.workers.dev/get/';
|
|
462
|
-
for (const board of boards) {
|
|
463
|
-
const map = {};
|
|
464
|
-
for (const [relPath, cdnUrlRaw] of Object.entries(board.files || {})) {
|
|
465
|
-
let cdnUrl = cdnUrlRaw;
|
|
466
|
-
if (cdnUrl && cdnUrl.startsWith(R2_PUB_PREFIX)) {
|
|
467
|
-
cdnUrl = R2_WORKER_GET + cdnUrl.slice(R2_PUB_PREFIX.length);
|
|
468
|
-
}
|
|
469
|
-
const thumbUrl = cdnUrl.includes('fs.chatium.ru/get/')
|
|
470
|
-
? cdnUrl.replace('/get/', '/thumbnail/') + '/1024x'
|
|
471
|
-
: cdnUrl;
|
|
472
|
-
map[relPath] = '/api/proxy?url=' + encodeURIComponent(thumbUrl);
|
|
473
|
-
}
|
|
474
|
-
cdnByBoard[board.name] = map;
|
|
475
|
-
}
|
|
514
|
+
const cdnByBoard = _buildCdnByBoard(boards);
|
|
476
515
|
// Skeleton meta + cdnByBoard (для fast cache-hit на reload).
|
|
477
516
|
await writeCloudFile(projectId, '.kingkont-meta.json',
|
|
478
517
|
new TextEncoder().encode(JSON.stringify({
|
|
@@ -634,6 +673,36 @@
|
|
|
634
673
|
if (changed) console.log('[cloudProjects] refreshed', changed, 'text-нод после фоновой загрузки');
|
|
635
674
|
}
|
|
636
675
|
|
|
676
|
+
// Построить CDN-карту {boardName: {relPath: '/api/proxy?url=...'}} из манифеста.
|
|
677
|
+
// Вынесено в helper — используется и в no-cache-ветке openCloudProject, и
|
|
678
|
+
// в фоновом fetch при cache-hit (когда местная meta была сохранена старой
|
|
679
|
+
// версией без cdnByBoard — иначе картинки в web-view не находятся).
|
|
680
|
+
// R2 public bucket `pub-<id>.r2.dev` rate-limited Cloudflare'ом (503 при
|
|
681
|
+
// batch-загрузке из браузера, ~30 одновременных img-fetch'ей). Переписываем
|
|
682
|
+
// на Worker GET endpoint (Worker не лимитирован так агрессивно, +CORS).
|
|
683
|
+
// Старые manifests на чатиуме могут содержать pub-…r2.dev URL'ы — этот
|
|
684
|
+
// rewrite их чинит без серверной миграции.
|
|
685
|
+
function _buildCdnByBoard(boards) {
|
|
686
|
+
const R2_PUB_PREFIX = 'https://pub-cd4114af9f7d44c9bf8c9442bc7dddc2.r2.dev/';
|
|
687
|
+
const R2_WORKER_GET = 'https://kingkont-r2-upload.timur-dd5.workers.dev/get/';
|
|
688
|
+
const cdnByBoard = {};
|
|
689
|
+
for (const board of boards || []) {
|
|
690
|
+
const map = {};
|
|
691
|
+
for (const [relPath, cdnUrlRaw] of Object.entries(board.files || {})) {
|
|
692
|
+
let cdnUrl = cdnUrlRaw;
|
|
693
|
+
if (cdnUrl && cdnUrl.startsWith(R2_PUB_PREFIX)) {
|
|
694
|
+
cdnUrl = R2_WORKER_GET + cdnUrl.slice(R2_PUB_PREFIX.length);
|
|
695
|
+
}
|
|
696
|
+
const thumbUrl = cdnUrl && cdnUrl.includes('fs.chatium.ru/get/')
|
|
697
|
+
? cdnUrl.replace('/get/', '/thumbnail/') + '/1024x'
|
|
698
|
+
: cdnUrl;
|
|
699
|
+
if (thumbUrl) map[relPath] = '/api/proxy?url=' + encodeURIComponent(thumbUrl);
|
|
700
|
+
}
|
|
701
|
+
cdnByBoard[board.name] = map;
|
|
702
|
+
}
|
|
703
|
+
return cdnByBoard;
|
|
704
|
+
}
|
|
705
|
+
|
|
637
706
|
// Подложить CDN-URL'ы под state.currentBoard.urls — рендеру медиа-нод
|
|
638
707
|
// в settings.js достаточно непустого `urls[node.file]`, чтобы не лезть
|
|
639
708
|
// в FS (см. settings.js:405).
|
package/renderer/settings.js
CHANGED
|
@@ -591,23 +591,30 @@ function ensureNodeDescriptionEl(node) {
|
|
|
591
591
|
if (!node) return null;
|
|
592
592
|
const canvasEl = document.getElementById('canvas');
|
|
593
593
|
if (!canvasEl) return null;
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
594
|
+
if (node.type !== 'image' && node.type !== 'video') {
|
|
595
|
+
const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
596
|
+
if (existing) existing.remove();
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const text = (node.description || '').trim();
|
|
600
|
+
// В view-only-режиме (share-link template) placeholder «Добавить описание»
|
|
601
|
+
// не показываем — он имеет смысл только если юзер может редактировать.
|
|
602
|
+
const canModify = (typeof state !== 'undefined' && state)
|
|
603
|
+
? (state.cloudCanModify !== false)
|
|
604
|
+
: true;
|
|
605
|
+
const isPlaceholder = !text;
|
|
606
|
+
if (isPlaceholder && !canModify) {
|
|
607
|
+
const existing = canvasEl.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
608
|
+
if (existing) existing.remove();
|
|
600
609
|
return null;
|
|
601
610
|
}
|
|
611
|
+
const sel = `.node-description-outside[data-desc-for="${node.id}"]`;
|
|
612
|
+
let el = canvasEl.querySelector(sel);
|
|
602
613
|
if (!el) {
|
|
603
614
|
el = document.createElement('div');
|
|
604
615
|
el.className = 'node-description-outside';
|
|
605
616
|
el.dataset.descFor = node.id;
|
|
606
617
|
canvasEl.appendChild(el);
|
|
607
|
-
// Дабл-клик по описанию → редактор (юзер: «дабл-клик на описании ноды
|
|
608
|
-
// должен открывать его редактирование»). editNodeDescription —
|
|
609
|
-
// глобал из board.js. e.stopPropagation — иначе дабл-клик пробрасывался
|
|
610
|
-
// в .node и срабатывало fullscreen-просмотр.
|
|
611
618
|
el.addEventListener('dblclick', e => {
|
|
612
619
|
e.stopPropagation();
|
|
613
620
|
e.preventDefault();
|
|
@@ -615,8 +622,22 @@ function ensureNodeDescriptionEl(node) {
|
|
|
615
622
|
window.editNodeDescription(node);
|
|
616
623
|
}
|
|
617
624
|
});
|
|
625
|
+
el.addEventListener('click', e => {
|
|
626
|
+
if (!el.classList.contains('empty')) return;
|
|
627
|
+
e.stopPropagation();
|
|
628
|
+
e.preventDefault();
|
|
629
|
+
if (typeof window.editNodeDescription === 'function') {
|
|
630
|
+
window.editNodeDescription(node);
|
|
631
|
+
}
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (isPlaceholder) {
|
|
635
|
+
el.classList.add('empty');
|
|
636
|
+
if (el.textContent !== 'Добавить описание') el.textContent = 'Добавить описание';
|
|
637
|
+
} else {
|
|
638
|
+
el.classList.remove('empty');
|
|
639
|
+
if (el.textContent !== text) el.textContent = text;
|
|
618
640
|
}
|
|
619
|
-
if (el.textContent !== text) el.textContent = text;
|
|
620
641
|
positionNodeDescriptionEl(node, el);
|
|
621
642
|
return el;
|
|
622
643
|
}
|
|
@@ -658,13 +679,11 @@ function fitNodeHeightToMedia(node, nodeEl, mediaEl) {
|
|
|
658
679
|
// Берём актуальную width — node.width если задано, иначе текущий offsetWidth.
|
|
659
680
|
const w = node.width || nodeEl.offsetWidth;
|
|
660
681
|
if (!w) return;
|
|
661
|
-
// Высота описания (если
|
|
662
|
-
//
|
|
682
|
+
// Высота описания (если есть, включая placeholder «Добавить описание»)
|
|
683
|
+
// — занимает нижнюю часть bbox'а ноды. Без этого описание выходило за
|
|
663
684
|
// пределы node.height и «накладывалось» на соседнюю ноду снизу. Юзер:
|
|
664
685
|
// «на ноде её описание показывается над нодой которая расположена выше».
|
|
665
|
-
const descEl = (node
|
|
666
|
-
? document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`)
|
|
667
|
-
: null;
|
|
686
|
+
const descEl = document.querySelector(`.node-description-outside[data-desc-for="${node.id}"]`);
|
|
668
687
|
const descH = descEl ? descEl.offsetHeight + 4 : 0; // +4 чтобы зрительно отделять
|
|
669
688
|
const newH = Math.round(w * (natH / natW) + chromeH + descH);
|
|
670
689
|
// 2px tolerance — не шумим в save/connections при суб-пиксельных колебаниях.
|
package/renderer/styles.css
CHANGED
|
@@ -130,6 +130,25 @@
|
|
|
130
130
|
z-index: 2;
|
|
131
131
|
pointer-events: auto;
|
|
132
132
|
}
|
|
133
|
+
/* Placeholder: описание отсутствует, подсказка «Добавить описание».
|
|
134
|
+
Юзер: «если описания в ноде нет — показывай блок с описанием с текстом
|
|
135
|
+
'добавить описание'». Стиль приглушённый, чтобы не отвлекать; клик/
|
|
136
|
+
дабл-клик открывает редактор. В view-only-режиме placeholder не
|
|
137
|
+
показываем (см. ensureNodeDescriptionEl). */
|
|
138
|
+
.node-description-outside.empty {
|
|
139
|
+
color: #6a6a6a;
|
|
140
|
+
font-style: italic;
|
|
141
|
+
border-left-color: #3a3a3a;
|
|
142
|
+
background: #1a1a1a;
|
|
143
|
+
box-shadow: none;
|
|
144
|
+
cursor: pointer;
|
|
145
|
+
transition: color 0.12s, border-left-color 0.12s, background 0.12s;
|
|
146
|
+
}
|
|
147
|
+
.node-description-outside.empty:hover {
|
|
148
|
+
color: #aac8e6;
|
|
149
|
+
border-left-color: #4a6a9a;
|
|
150
|
+
background: #1e1e1e;
|
|
151
|
+
}
|
|
133
152
|
/* Legacy: оставляем класс для совместимости (вдруг где-то ещё ссылается). */
|
|
134
153
|
.node .node-description { display: none; }
|
|
135
154
|
|