kingkont 0.7.60 → 0.7.61

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 CHANGED
@@ -457,6 +457,8 @@
457
457
  <!-- ===== Полноэкранный просмотр (image/video) ===== -->
458
458
  <div class="fs-modal hidden" id="fsModal">
459
459
  <div class="fs-stage" id="fsStage"></div>
460
+ <button id="fsPrev" title="Предыдущая (←)" class="fs-nav fs-nav-prev">‹</button>
461
+ <button id="fsNext" title="Следующая (→)" class="fs-nav fs-nav-next">›</button>
460
462
  <button id="fsClose" title="Закрыть (Esc)">×</button>
461
463
  </div>
462
464
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.60",
3
+ "version": "0.7.61",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1638,33 +1638,94 @@ function showNodeLogs(node) {
1638
1638
  $('logsClose').addEventListener('click', () => $('logsModal').classList.add('hidden'));
1639
1639
 
1640
1640
  // =================== Полноэкранный просмотр ===================
1641
+ // Текущая нода в полноэкранном просмотре + список соседей того же type
1642
+ // для навигации стрелками. Список считаем один раз при openFullscreen
1643
+ // чтобы не пересортировывать на каждом prev/next.
1644
+ let _fsCurrent = null;
1645
+ let _fsSiblings = [];
1646
+
1647
+ // Сортировка нод «слева направо, сверху вниз» row-major.
1648
+ // Группируем по строкам через round(y/ROW_HEIGHT) bucket — sort transitive
1649
+ // (важно, иначе порядок непредсказуемый при больших спредах по y).
1650
+ const FS_ROW_HEIGHT = 100;
1651
+ function _fsSortRowMajor(nodes) {
1652
+ return nodes.slice().sort((a, b) => {
1653
+ const ra = Math.round((a.y || 0) / FS_ROW_HEIGHT);
1654
+ const rb = Math.round((b.y || 0) / FS_ROW_HEIGHT);
1655
+ if (ra !== rb) return ra - rb;
1656
+ return (a.x || 0) - (b.x || 0);
1657
+ });
1658
+ }
1659
+
1641
1660
  async function openFullscreen(node) {
1642
1661
  const stage = $('fsStage');
1643
1662
  stage.innerHTML = '';
1644
1663
  try {
1645
- let url = state.currentBoard?.urls?.[node.file];
1646
- if (!url && state.currentBoard?.handle) {
1647
- const fh = await resolveBoardFile(state.currentBoard.handle, node.file);
1648
- url = URL.createObjectURL(await fh.getFile());
1649
- state.currentBoard.urls[node.file] = url;
1650
- }
1651
- if (!url) return;
1652
- if (node.type === 'image') {
1653
- const img = document.createElement('img');
1654
- img.src = url;
1655
- stage.appendChild(img);
1656
- } else if (node.type === 'video') {
1657
- const v = document.createElement('video');
1658
- v.src = url;
1659
- v.controls = true;
1660
- v.autoplay = true;
1661
- stage.appendChild(v);
1664
+ // Собираем siblings того же type на текущей доске для навигации.
1665
+ // image-нода все остальные image; video → video. Без файла исключаем.
1666
+ if (state.currentBoard?.metadata?.nodes) {
1667
+ const sameType = state.currentBoard.metadata.nodes.filter(n =>
1668
+ n.type === node.type && n.file
1669
+ );
1670
+ _fsSiblings = _fsSortRowMajor(sameType);
1671
+ } else {
1672
+ _fsSiblings = [node];
1662
1673
  }
1674
+ _fsCurrent = node;
1675
+ await _fsLoadCurrent();
1676
+ _fsUpdateNavButtons();
1663
1677
  $('fsModal').classList.remove('hidden');
1664
1678
  } catch (e) {
1665
1679
  console.error('openFullscreen failed:', e);
1666
1680
  }
1667
1681
  }
1682
+
1683
+ async function _fsLoadCurrent() {
1684
+ const node = _fsCurrent;
1685
+ const stage = $('fsStage');
1686
+ stage.innerHTML = '';
1687
+ if (!node) return;
1688
+ let url = state.currentBoard?.urls?.[node.file];
1689
+ if (!url && state.currentBoard?.handle) {
1690
+ const fh = await resolveBoardFile(state.currentBoard.handle, node.file);
1691
+ url = URL.createObjectURL(await fh.getFile());
1692
+ state.currentBoard.urls[node.file] = url;
1693
+ }
1694
+ if (!url) return;
1695
+ if (node.type === 'image') {
1696
+ const img = document.createElement('img');
1697
+ img.src = url;
1698
+ stage.appendChild(img);
1699
+ } else if (node.type === 'video') {
1700
+ const v = document.createElement('video');
1701
+ v.src = url;
1702
+ v.controls = true;
1703
+ v.autoplay = true;
1704
+ stage.appendChild(v);
1705
+ }
1706
+ }
1707
+
1708
+ function _fsUpdateNavButtons() {
1709
+ const prev = $('fsPrev');
1710
+ const next = $('fsNext');
1711
+ const idx = _fsCurrent ? _fsSiblings.indexOf(_fsCurrent) : -1;
1712
+ if (prev) prev.disabled = idx <= 0;
1713
+ if (next) next.disabled = idx < 0 || idx >= _fsSiblings.length - 1;
1714
+ }
1715
+
1716
+ async function fsNavigate(direction /* -1 или +1 */) {
1717
+ if (!_fsCurrent) return;
1718
+ const idx = _fsSiblings.indexOf(_fsCurrent);
1719
+ const next = idx + direction;
1720
+ if (next < 0 || next >= _fsSiblings.length) return;
1721
+ _fsCurrent = _fsSiblings[next];
1722
+ // Останавливаем текущее video если было.
1723
+ const v = $('fsStage').querySelector('video');
1724
+ if (v) { try { v.pause(); } catch {} }
1725
+ await _fsLoadCurrent();
1726
+ _fsUpdateNavButtons();
1727
+ }
1728
+
1668
1729
  function closeFullscreen() {
1669
1730
  const modal = $('fsModal');
1670
1731
  if (modal.classList.contains('hidden')) return;
@@ -1672,16 +1733,20 @@ function closeFullscreen() {
1672
1733
  if (v) { try { v.pause(); } catch {} }
1673
1734
  $('fsStage').innerHTML = '';
1674
1735
  modal.classList.add('hidden');
1736
+ _fsCurrent = null;
1737
+ _fsSiblings = [];
1675
1738
  }
1676
1739
  $('fsClose').addEventListener('click', closeFullscreen);
1740
+ $('fsPrev')?.addEventListener('click', e => { e.stopPropagation(); fsNavigate(-1); });
1741
+ $('fsNext')?.addEventListener('click', e => { e.stopPropagation(); fsNavigate(+1); });
1677
1742
  $('fsModal').addEventListener('click', e => {
1678
1743
  if (e.target.id === 'fsModal' || e.target.id === 'fsStage') closeFullscreen();
1679
1744
  });
1680
1745
  window.addEventListener('keydown', e => {
1681
- if (e.key === 'Escape' && !$('fsModal').classList.contains('hidden')) {
1682
- e.stopPropagation();
1683
- closeFullscreen();
1684
- }
1746
+ if ($('fsModal').classList.contains('hidden')) return;
1747
+ if (e.key === 'Escape') { e.stopPropagation(); closeFullscreen(); }
1748
+ else if (e.key === 'ArrowLeft') { e.stopPropagation(); fsNavigate(-1); }
1749
+ else if (e.key === 'ArrowRight') { e.stopPropagation(); fsNavigate(+1); }
1685
1750
  }, true);
1686
1751
  // Универсальный copy-helper: clipboard API → fallback на execCommand.
1687
1752
  async function copyText(text) {
@@ -463,6 +463,19 @@
463
463
  }
464
464
  .fs-modal #fsClose:hover { background: rgba(255,68,68,0.7); }
465
465
 
466
+ /* Кнопки навигации prev/next в полноэкранном просмотре */
467
+ .fs-modal .fs-nav {
468
+ position: absolute; top: 50%; transform: translateY(-50%);
469
+ background: rgba(0,0,0,0.55); border: 1px solid #444; color: #fff;
470
+ font-size: 36px; line-height: 1; padding: 4px 14px; border-radius: 6px;
471
+ cursor: pointer; user-select: none;
472
+ transition: background 0.12s;
473
+ }
474
+ .fs-modal .fs-nav:hover { background: rgba(0,0,0,0.85); }
475
+ .fs-modal .fs-nav:disabled { opacity: 0.3; cursor: default; }
476
+ .fs-modal .fs-nav-prev { left: 16px; }
477
+ .fs-modal .fs-nav-next { right: 16px; }
478
+
466
479
  /* === Реплики (боковая панель справа) === */
467
480
  .repliques-panel {
468
481
  width: 380px; background: #1c1c1c; border-left: 1px solid #333;