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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.20.50",
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).
@@ -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
- const text = (node.type === 'image' || node.type === 'video') ? (node.description || '').trim() : '';
595
- const sel = `.node-description-outside[data-desc-for="${node.id}"]`;
596
- let el = canvasEl.querySelector(sel);
597
- if (!text) {
598
- // Описания нет — удаляем sidecar.
599
- if (el) el.remove();
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
- // Высота описания (если есть) описание занимает нижнюю часть bbox'а
662
- // ноды (см. positionNodeDescriptionEl). Без этого описание выходило за
682
+ // Высота описания (если есть, включая placeholder «Добавить описание»)
683
+ // занимает нижнюю часть bbox'а ноды. Без этого описание выходило за
663
684
  // пределы node.height и «накладывалось» на соседнюю ноду снизу. Юзер:
664
685
  // «на ноде её описание показывается над нодой которая расположена выше».
665
- const descEl = (node.description || '').trim()
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 при суб-пиксельных колебаниях.
@@ -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