kingkont 0.20.49 → 0.20.52

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.49",
3
+ "version": "0.20.52",
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
@@ -1671,14 +1671,7 @@ async function openShareModal(p) {
1671
1671
  // там приложение живёт на kingkont.ru. В web используем origin.
1672
1672
  const isLocal = /^https?:\/\/(localhost|127\.0\.0\.1)/.test(location.origin);
1673
1673
  const base = isLocal ? 'https://kingkont.ru' : location.origin;
1674
- // Раньше: /static/web.html но chatium-edge кэширует этот путь
1675
- // НАВЕЧНО, и юзеры получали старую версию HTML со старыми хешами
1676
- // скриптов. Старые cloudProjects.js не имеют R2 pub→worker rewrite
1677
- // → картинки 503 → «Файл не найден».
1678
- // Сейчас: /app/spaces/client/ — auth-aware stub, который JS'ом
1679
- // редиректит на свежий /static/web-<sha>.html (новое имя файла =
1680
- // свежий cache-entry на каждом deploy).
1681
- return base + '/app/spaces/client/#template=' + proj.id;
1674
+ return base + '/static/web.html#template=' + proj.id;
1682
1675
  }
1683
1676
  function render() {
1684
1677
  const isPub = !!proj.isPublic;
@@ -82,16 +82,23 @@
82
82
  // openShareModal живёт в board.js (window.openShareModal). В Electron
83
83
  // share-API'ы (/api/proj/setPublic|share|unshare|shares) проксируются
84
84
  // через server.js → kingkont.ru/proj_*; в web — через web-shim.
85
+ // ↗ Перейти — рядом, только когда проект публичный (state.cloudIsPublic).
86
+ // Юзер: «на проекте если он публичный сделай кнопку перейти (с иконкой,
87
+ // без надписи) справа от кнопки Расшарить».
85
88
  {
86
- let shareBtn = $('shareProjectBtn');
89
+ let row = $('shareProjectRow');
87
90
  const wantShow = logged && state.cloudProjectId && state.cloudMine !== false;
88
- if (wantShow && !shareBtn && saveBtn?.parentNode) {
89
- shareBtn = document.createElement('button');
91
+ if (wantShow && !row && saveBtn?.parentNode) {
92
+ row = document.createElement('div');
93
+ row.id = 'shareProjectRow';
94
+ row.style.cssText = 'display:flex; gap:6px; margin-top:6px; align-items:stretch;';
95
+
96
+ const shareBtn = document.createElement('button');
90
97
  shareBtn.id = 'shareProjectBtn';
91
98
  shareBtn.className = 'kk-edit-only';
92
99
  shareBtn.textContent = '🤝 Расшарить';
93
100
  shareBtn.title = 'Расшарить проект другому юзеру или сделать публичным';
94
- shareBtn.style.cssText = 'margin-top:6px; font-size:12px;';
101
+ shareBtn.style.cssText = 'flex:1; font-size:12px;';
95
102
  shareBtn.addEventListener('click', () => {
96
103
  if (typeof window.openShareModal === 'function') {
97
104
  window.openShareModal({
@@ -102,9 +109,31 @@
102
109
  });
103
110
  }
104
111
  });
105
- saveBtn.parentNode.insertBefore(shareBtn, saveBtn.nextSibling);
112
+ row.appendChild(shareBtn);
113
+
114
+ // ↗ Перейти — иконка, открывает share-URL в новой вкладке.
115
+ // Видна только если проект публичный (по private-проекту ссылка
116
+ // не работает у незалогиненных, и тут смысла нет).
117
+ const gotoBtn = document.createElement('button');
118
+ gotoBtn.id = 'gotoPublicBtn';
119
+ gotoBtn.className = 'kk-edit-only';
120
+ gotoBtn.textContent = '↗';
121
+ gotoBtn.title = 'Открыть публичную страницу проекта в новой вкладке';
122
+ gotoBtn.style.cssText = 'font-size:14px; padding:0 10px;';
123
+ gotoBtn.addEventListener('click', () => {
124
+ const isLocal = /^https?:\/\/(localhost|127\.0\.0\.1)/.test(location.origin);
125
+ const base = isLocal ? 'https://kingkont.ru' : location.origin;
126
+ const url = base + '/static/web.html#template=' + encodeURIComponent(state.cloudProjectId);
127
+ window.open(url, '_blank', 'noopener');
128
+ });
129
+ row.appendChild(gotoBtn);
130
+
131
+ saveBtn.parentNode.insertBefore(row, saveBtn.nextSibling);
106
132
  }
107
- if (shareBtn) shareBtn.style.display = wantShow ? '' : 'none';
133
+ if (row) row.style.display = wantShow ? 'flex' : 'none';
134
+ // Видимость ↗ Перейти зависит от isPublic — обновляем каждый тик.
135
+ const gotoBtn = $('gotoPublicBtn');
136
+ if (gotoBtn) gotoBtn.style.display = (wantShow && state.cloudIsPublic) ? '' : 'none';
108
137
  }
109
138
  }).catch(() => {});
110
139
  }
@@ -364,6 +393,66 @@
364
393
  setCloudButtonsVisibility();
365
394
  if (typeof window.renderTemplateOverlay === 'function') window.renderTemplateOverlay();
366
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
+ }
367
456
  await _downloadAllTexts(projectId, bgBoards).catch(() => {});
368
457
  // Перечитываем тексты ТЕКУЩЕЙ доски (другие — при switchBoard).
369
458
  await _refreshCurrentBoardTexts().catch(() => {});
@@ -422,28 +511,7 @@
422
511
  // кешируется»). 1024px достаточно и для card-thumbnail и для
423
512
  // полноэкранного просмотра. Маленькие thumbs (320x) — отдельной
424
513
  // картой не требуются, scene-card сам ресайзит через CSS.
425
- const cdnByBoard = {};
426
- // R2 public bucket `pub-<id>.r2.dev` rate-limited Cloudflare'ом (503 при
427
- // batch-загрузке из браузера, ~30 одновременных img-fetch'ей). Переписываем
428
- // на Worker GET endpoint (Worker не лимитирован так агрессивно, +CORS).
429
- // Старые manifests на чатиуме могут содержать pub-…r2.dev URL'ы — этот
430
- // rewrite их чинит без серверной миграции.
431
- const R2_PUB_PREFIX = 'https://pub-cd4114af9f7d44c9bf8c9442bc7dddc2.r2.dev/';
432
- const R2_WORKER_GET = 'https://kingkont-r2-upload.timur-dd5.workers.dev/get/';
433
- for (const board of boards) {
434
- const map = {};
435
- for (const [relPath, cdnUrlRaw] of Object.entries(board.files || {})) {
436
- let cdnUrl = cdnUrlRaw;
437
- if (cdnUrl && cdnUrl.startsWith(R2_PUB_PREFIX)) {
438
- cdnUrl = R2_WORKER_GET + cdnUrl.slice(R2_PUB_PREFIX.length);
439
- }
440
- const thumbUrl = cdnUrl.includes('fs.chatium.ru/get/')
441
- ? cdnUrl.replace('/get/', '/thumbnail/') + '/1024x'
442
- : cdnUrl;
443
- map[relPath] = '/api/proxy?url=' + encodeURIComponent(thumbUrl);
444
- }
445
- cdnByBoard[board.name] = map;
446
- }
514
+ const cdnByBoard = _buildCdnByBoard(boards);
447
515
  // Skeleton meta + cdnByBoard (для fast cache-hit на reload).
448
516
  await writeCloudFile(projectId, '.kingkont-meta.json',
449
517
  new TextEncoder().encode(JSON.stringify({
@@ -605,6 +673,36 @@
605
673
  if (changed) console.log('[cloudProjects] refreshed', changed, 'text-нод после фоновой загрузки');
606
674
  }
607
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
+
608
706
  // Подложить CDN-URL'ы под state.currentBoard.urls — рендеру медиа-нод
609
707
  // в settings.js достаточно непустого `urls[node.file]`, чтобы не лезть
610
708
  // в FS (см. settings.js:405).