kingkont 0.8.8 → 0.9.0

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/renderer/board.js CHANGED
@@ -38,6 +38,27 @@ window.addEventListener('DOMContentLoaded', async () => {
38
38
  window.appMenu.on('open-settings', () => openSettings());
39
39
  window.appMenu.on('open-updates', () => openUpdatesOverlay());
40
40
  }
41
+ // Подписка на смену Chatium-auth-статуса (login/logout из настроек):
42
+ // - login → чистим localStorage-кэш и заново фетчим /api/projects.
43
+ // Welcome перерисуется со свежими облачными карточками.
44
+ // - logout → чистим кэш + перерисовываем (cloud-карточки исчезнут,
45
+ // fetchListCached не дёрнет /api/projects без логина).
46
+ // setCloudButtonsVisibility пересоберёт sidebar-кнопки (☁ Новый и пр.).
47
+ if (window.appChatium?.onAuthChanged) {
48
+ window.appChatium.onAuthChanged(async (state) => {
49
+ try {
50
+ // Сбрасываем кэш — он содержал данные старого юзера.
51
+ localStorage.removeItem('cloudProjectsCache');
52
+ localStorage.removeItem('cloudProjectsLastOpened');
53
+ // Если открыт welcome (нет проекта) — перерисовать. fetchListCached
54
+ // в renderWelcomeRecents подтянет свежий список с сервера.
55
+ if (!state || !window.cloudProjects) return;
56
+ if (window.cloudProjects.setVisibility) window.cloudProjects.setVisibility();
57
+ if (!document.body.classList.contains('no-project')) return;
58
+ await renderWelcomeRecents();
59
+ } catch (e) { vlog('err', 'auth-changed handler: ' + (e?.message || e)); }
60
+ });
61
+ }
41
62
  // Восстановить состояние панелей таймлайна/превью/реплик
42
63
  const tlOpen = localStorage.getItem('timelineOpen') === '1';
43
64
  const pvOpen = localStorage.getItem('previewOpen') === '1';
@@ -77,20 +98,30 @@ window.addEventListener('DOMContentLoaded', async () => {
77
98
  setInterval(() => { refreshBalance().catch(() => {}); }, 60_000);
78
99
 
79
100
  // Тихий autoload: первый recent с granted-permission (если есть).
80
- try {
81
- const recents = await getRecents();
82
- if (recents.length && recents[0].handle) {
83
- const first = recents[0];
84
- let q = 'prompt';
85
- try { q = await first.handle.queryPermission({ mode: 'readwrite' }); } catch {}
86
- vlog('info', `restore: handle=${first.name} queryPermission=${q}`);
87
- if (q === 'granted') {
88
- await openFilm(first.handle);
89
- return;
101
+ // НО — если юзер только что вышел из проекта (closeProject поставил
102
+ // флаг welcomeOnNextStart), пропускаем autoload и показываем welcome.
103
+ // Это нужно чтобы Cmd+R после явного выхода не реоткрывал проект.
104
+ // (При обычном перезапуске после crash'а флага нет → автоload работает.)
105
+ const skipAutoload = localStorage.getItem('welcomeOnNextStart') === '1';
106
+ if (skipAutoload) {
107
+ localStorage.removeItem('welcomeOnNextStart');
108
+ vlog('info', 'restore: skipped (user explicitly closed last project)');
109
+ } else {
110
+ try {
111
+ const recents = await getRecents();
112
+ if (recents.length && recents[0].handle) {
113
+ const first = recents[0];
114
+ let q = 'prompt';
115
+ try { q = await first.handle.queryPermission({ mode: 'readwrite' }); } catch {}
116
+ vlog('info', `restore: handle=${first.name} queryPermission=${q}`);
117
+ if (q === 'granted') {
118
+ await openFilm(first.handle);
119
+ return;
120
+ }
90
121
  }
122
+ } catch (e) {
123
+ vlog('err', 'restore failed: ' + (e?.message || e));
91
124
  }
92
- } catch (e) {
93
- vlog('err', 'restore failed: ' + (e?.message || e));
94
125
  }
95
126
  await renderWelcomeRecents();
96
127
 
@@ -318,6 +349,11 @@ async function getRecents() {
318
349
 
319
350
  async function touchRecent(handle, thumbBlob) {
320
351
  if (!handle) return;
352
+ // Облачные проекты НЕ попадают в recents — они уже показываются в
353
+ // welcome-grid'е через /api/projects (с ☁-бейджем). Иначе после первого
354
+ // открытия cloud-проект дублируется: один раз как «облачный», второй раз
355
+ // как «локальный недавний» (handle указывает на userData/cloud-projects/<id>).
356
+ if (window.cloudFsShim?.isCloudHandle?.(handle)) return;
321
357
  const list = await _readRecentsMeta();
322
358
  const i = list.findIndex(r => r.name === handle.name);
323
359
  let thumbDataUrl = (i >= 0 ? list[i].thumbDataUrl : null);
@@ -382,6 +418,37 @@ async function renderWelcomeRecents() {
382
418
  openCard.addEventListener('click', () => $('pickRoot').click());
383
419
  grid.appendChild(openCard);
384
420
 
421
+ // ☁ Облачные проекты — видна только если залогинен в Chatium.
422
+ // Создаёт серверную запись и открывает её как новый проект.
423
+ let isLoggedIn = false;
424
+ try {
425
+ const s = await window.appSettings?.get();
426
+ isLoggedIn = !!(s?.useChatium && s?.chatium?.token);
427
+ } catch {}
428
+ if (isLoggedIn) {
429
+ const cloudCard = document.createElement('div');
430
+ cloudCard.className = 'welcome-card open-card';
431
+ const cloudThumb = document.createElement('div');
432
+ cloudThumb.className = 'welcome-card-thumb';
433
+ cloudThumb.textContent = '☁';
434
+ const cloudMeta = document.createElement('div');
435
+ cloudMeta.className = 'welcome-card-meta';
436
+ const cloudName = document.createElement('div');
437
+ cloudName.className = 'welcome-card-name';
438
+ cloudName.textContent = 'Новый проект (облако)';
439
+ const cloudSub = document.createElement('div');
440
+ cloudSub.className = 'welcome-card-ts';
441
+ cloudSub.textContent = 'хранится на сервере';
442
+ cloudMeta.append(cloudName, cloudSub);
443
+ cloudCard.append(cloudThumb, cloudMeta);
444
+ cloudCard.addEventListener('click', () => {
445
+ if (window.cloudProjects?.createNew) window.cloudProjects.createNew();
446
+ });
447
+ grid.appendChild(cloudCard);
448
+ // (отдельная карточка «Мои облачные проекты» убрана — облачные теперь
449
+ // показываются inline в общем списке с ☁-бейджем.)
450
+ }
451
+
385
452
  // Сразу за «Открыть проект» — карточка «Шаблоны» (открывает библиотеку
386
453
  // шаблонов с серверa). Стиль такой же как у open-card — иконка + meta.
387
454
  const tplCard = document.createElement('div');
@@ -411,70 +478,194 @@ async function renderWelcomeRecents() {
411
478
  });
412
479
  grid.appendChild(tplCard);
413
480
 
414
- // Title меняем в зависимости от наличия recents.
415
- if (titleEl) titleEl.textContent = list.length ? 'Открыть проект · недавние' : 'Открыть проект';
481
+ // ---- Облачные проекты: добавляем в тот же grid с ☁ бейджем ----
482
+ // Cached рендерим сразу, fresh background merge через onListChange.
483
+ let cloudItems = [];
484
+ if (isLoggedIn && window.cloudProjects?.fetchListCached) {
485
+ const { cached, promise } = window.cloudProjects.fetchListCached();
486
+ cloudItems = cached;
487
+ // Когда фон-фетч обновит — перерендерим карточки in-place.
488
+ promise.then(items => {
489
+ if (state.filmHandle) return; // welcome больше не виден
490
+ if (JSON.stringify(items) === JSON.stringify(cloudItems)) return; // ничего не изменилось
491
+ renderWelcomeRecents().catch(() => {});
492
+ }).catch(() => {});
493
+ }
416
494
 
495
+ // ---- Объединённый список: cloud + folder, отсортированный по timestamp ----
496
+ // Каждый элемент: { type: 'cloud'|'recent', sortTs, item }.
497
+ // Для cloud берём максимум из (updatedAt | lastOpenedAt | createdAt) —
498
+ // юзер ожидает что недавно открытый проект (даже без сохранения) поднимется.
499
+ const merged = [];
500
+ for (const c of cloudItems) {
501
+ const ts = Math.max(c.lastOpenedAt || 0, c.updatedAt || 0, c.createdAt || 0);
502
+ merged.push({ type: 'cloud', sortTs: ts, item: c });
503
+ }
504
+ // Дедуп: cloud-проекты не должны показываться ещё и как «недавние папки».
505
+ // Источники дубля:
506
+ // 1) handle — cloud-shim (window.cloudFsShim.isCloudHandle) — фильтруем в touchRecent,
507
+ // но старые записи могли остаться от предыдущих версий.
508
+ // 2) имя совпадает с именем cloud-проекта — heuristic для legacy-записей.
509
+ const cloudNames = new Set(cloudItems.map(c => c.name).filter(Boolean));
417
510
  for (const r of list) {
418
- const card = document.createElement('div');
419
- card.className = 'welcome-card';
420
- const thumb = document.createElement('div');
421
- thumb.className = 'welcome-card-thumb';
422
- if (r.thumb) {
423
- const img = document.createElement('img');
424
- img.src = URL.createObjectURL(r.thumb);
425
- img.onload = () => setTimeout(() => URL.revokeObjectURL(img.src), 60_000);
426
- thumb.appendChild(img);
427
- } else {
428
- thumb.textContent = '🎬';
429
- }
430
- const meta = document.createElement('div');
431
- meta.className = 'welcome-card-meta';
432
- const nameEl = document.createElement('div');
433
- nameEl.className = 'welcome-card-name';
434
- nameEl.textContent = r.name;
435
- const tsEl = document.createElement('div');
436
- tsEl.className = 'welcome-card-ts';
437
- tsEl.textContent = fmtRelativeTime(r.ts);
438
- meta.append(nameEl, tsEl);
439
- card.append(thumb, meta);
440
- const del = document.createElement('div');
441
- del.className = 'welcome-card-del';
442
- del.textContent = '×';
443
- del.title = 'Удалить из недавних';
444
- del.addEventListener('click', async e => {
445
- e.stopPropagation();
446
- if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
447
- await removeRecent(r.name);
448
- await renderWelcomeRecents();
449
- });
450
- card.appendChild(del);
451
- card.addEventListener('click', async () => {
452
- try {
453
- if (r.handle) {
454
- let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
455
- if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
456
- vlog('info', `welcome card click ${r.name}: granted=${g}`);
457
- if (g) await openFilm(r.handle);
458
- else alert('Доступ к папке не подтверждён.');
459
- } else {
460
- // Handle потерялся (IDB сбросилась) — открываем picker прямо здесь,
461
- // чтобы сохранить user-gesture (через $('pickRoot').click() он
462
- // теряется и showDirectoryPicker валится SecurityError).
463
- try {
464
- const handle = await window.showDirectoryPicker({ mode: 'readwrite', id: 'video-editor-film' });
465
- await openFilm(handle);
466
- } catch (err) {
467
- if (err.name === 'AbortError') return;
468
- throw err;
469
- }
511
+ if (window.cloudFsShim?.isCloudHandle?.(r.handle)) continue; // shim в IDB
512
+ if (cloudNames.has(r.name)) continue; // имя дубля
513
+ merged.push({ type: 'recent', sortTs: r.ts || 0, item: r });
514
+ }
515
+ merged.sort((a, b) => b.sortTs - a.sortTs);
516
+
517
+ // Title меняем в зависимости от наличия проектов.
518
+ if (titleEl) titleEl.textContent = merged.length ? 'Открыть проект · недавние' : 'Открыть проект';
519
+
520
+ for (const m of merged) {
521
+ if (m.type === 'cloud') grid.appendChild(makeCloudWelcomeCard(m.item));
522
+ else grid.appendChild(makeRecentWelcomeCard(m.item));
523
+ }
524
+ }
525
+
526
+ // Карточка локального (папочного) проекта — извлечена из renderWelcomeRecents.
527
+ function makeRecentWelcomeCard(r) {
528
+ const card = document.createElement('div');
529
+ card.className = 'welcome-card';
530
+ const thumb = document.createElement('div');
531
+ thumb.className = 'welcome-card-thumb';
532
+ if (r.thumb) {
533
+ const img = document.createElement('img');
534
+ img.src = URL.createObjectURL(r.thumb);
535
+ img.onload = () => setTimeout(() => URL.revokeObjectURL(img.src), 60_000);
536
+ thumb.appendChild(img);
537
+ } else thumb.textContent = '🎬';
538
+ const meta = document.createElement('div');
539
+ meta.className = 'welcome-card-meta';
540
+ const nameEl = document.createElement('div');
541
+ nameEl.className = 'welcome-card-name';
542
+ nameEl.textContent = r.name;
543
+ const tsEl = document.createElement('div');
544
+ tsEl.className = 'welcome-card-ts';
545
+ tsEl.textContent = fmtRelativeTime(r.ts);
546
+ meta.append(nameEl, tsEl);
547
+ card.append(thumb, meta);
548
+ const del = document.createElement('div');
549
+ del.className = 'welcome-card-del';
550
+ del.textContent = '×';
551
+ del.title = 'Удалить из недавних';
552
+ del.addEventListener('click', async e => {
553
+ e.stopPropagation();
554
+ if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
555
+ await removeRecent(r.name);
556
+ await renderWelcomeRecents();
557
+ });
558
+ card.appendChild(del);
559
+ card.addEventListener('click', async () => {
560
+ try {
561
+ if (r.handle) {
562
+ let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
563
+ if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
564
+ vlog('info', `welcome card click ${r.name}: granted=${g}`);
565
+ if (g) await openFilm(r.handle);
566
+ else alert('Доступ к папке не подтверждён.');
567
+ } else {
568
+ try {
569
+ const handle = await window.showDirectoryPicker({ mode: 'readwrite', id: 'video-editor-film' });
570
+ await openFilm(handle);
571
+ } catch (err) {
572
+ if (err.name === 'AbortError') return;
573
+ throw err;
470
574
  }
471
- } catch (err) {
472
- vlog('err', 'welcome card failed: ' + (err?.message || err));
473
- alert('Ошибка: ' + (err?.message || err));
474
575
  }
475
- });
476
- grid.appendChild(card);
477
- }
576
+ } catch (err) {
577
+ vlog('err', 'welcome card failed: ' + (err?.message || err));
578
+ alert('Ошибка: ' + (err?.message || err));
579
+ }
580
+ });
581
+ return card;
582
+ }
583
+
584
+ // Карточка облачного проекта. Визуально такая же как у папочного, плюс
585
+ // ☁ бейдж в углу обложки. Клик → cloudProjects.open() (использует local
586
+ // cache если синхронизирован, иначе скачивает manifest+файлы).
587
+ // ПКМ → меню «Открыть в Finder», «Обновить из облака», «Удалить с сервера».
588
+ function makeCloudWelcomeCard(p) {
589
+ const card = document.createElement('div');
590
+ card.className = 'welcome-card cloud-card';
591
+ card.dataset.cloudId = p.id;
592
+
593
+ const thumb = document.createElement('div');
594
+ thumb.className = 'welcome-card-thumb';
595
+ if (p.coverUrl) {
596
+ const img = document.createElement('img');
597
+ img.src = '/api/proxy?url=' + encodeURIComponent(p.coverUrl);
598
+ img.draggable = false;
599
+ img.onerror = () => { thumb.textContent = '🎬'; img.remove(); };
600
+ thumb.appendChild(img);
601
+ } else thumb.textContent = '🎬';
602
+ // ☁-бейдж в нижнем правом углу обложки.
603
+ const cloudBadge = document.createElement('div');
604
+ cloudBadge.className = 'welcome-card-cloud-badge';
605
+ cloudBadge.textContent = '☁';
606
+ cloudBadge.title = 'Облачный проект';
607
+ thumb.appendChild(cloudBadge);
608
+
609
+ const meta = document.createElement('div');
610
+ meta.className = 'welcome-card-meta';
611
+ const nameEl = document.createElement('div');
612
+ nameEl.className = 'welcome-card-name';
613
+ nameEl.textContent = p.name || '(без названия)';
614
+ const tsEl = document.createElement('div');
615
+ tsEl.className = 'welcome-card-ts';
616
+ tsEl.textContent = p.updatedAt ? fmtRelativeTime(p.updatedAt) : 'облако';
617
+ meta.append(nameEl, tsEl);
618
+ card.append(thumb, meta);
619
+
620
+ card.addEventListener('click', () => {
621
+ if (window.cloudProjects?.open) window.cloudProjects.open(p.id, p.name);
622
+ });
623
+ card.addEventListener('contextmenu', e => {
624
+ e.preventDefault();
625
+ e.stopPropagation();
626
+ showCloudCardContextMenu(p, e.clientX, e.clientY);
627
+ });
628
+ return card;
629
+ }
630
+
631
+ // ПКМ-меню для облачной welcome-карточки.
632
+ function showCloudCardContextMenu(p, clientX, clientY) {
633
+ const menu = $('nodeMenu');
634
+ if (!menu) return;
635
+ menu.innerHTML = '';
636
+ const add = (label, fn, opts = {}) => {
637
+ const b = document.createElement('button');
638
+ b.textContent = label;
639
+ if (opts.danger) b.style.color = '#f88';
640
+ b.addEventListener('click', () => { menu.classList.add('hidden'); fn(); });
641
+ menu.appendChild(b);
642
+ };
643
+ add('📂 Открыть в Finder', async () => {
644
+ if (!window.cloudFs?.openInFinder) {
645
+ alert('Доступно только в Electron-приложении');
646
+ return;
647
+ }
648
+ const r = await window.cloudFs.openInFinder(p.id);
649
+ if (!r?.ok) alert('Не удалось открыть: ' + (r?.error || 'unknown'));
650
+ });
651
+ add('↻ Обновить из облака', () => {
652
+ if (window.cloudProjects?.open) window.cloudProjects.open(p.id, p.name, { forceRefresh: true });
653
+ });
654
+ add('🗑 Удалить с сервера', async () => {
655
+ if (!confirm(`Удалить «${p.name}» с сервера? Локальная копия в userData останется.`)) return;
656
+ try {
657
+ const r = await fetch('/api/projects/' + encodeURIComponent(p.id), { method: 'DELETE' });
658
+ if (!r.ok) throw new Error('HTTP ' + r.status);
659
+ // Очищаем кэш и перерендериваем grid.
660
+ try {
661
+ const cached = JSON.parse(localStorage.getItem('cloudProjectsCache') || '[]');
662
+ localStorage.setItem('cloudProjectsCache', JSON.stringify(cached.filter(c => c.id !== p.id)));
663
+ } catch {}
664
+ await renderWelcomeRecents();
665
+ } catch (e) { alert('Не удалось удалить: ' + (e?.message || e)); }
666
+ }, { danger: true });
667
+ positionFloatingMenu(menu, clientX, clientY);
668
+ setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
478
669
  }
479
670
 
480
671
  // Скопировать файл image-ноды в корень проекта как `.cover.<ext>`.
@@ -656,6 +847,8 @@ async function openFilm(handle) {
656
847
  loadUploadCache().catch(() => {});
657
848
  await refreshSidebar();
658
849
  showEmpty();
850
+ // Cloud-кнопки: показать «☁ Сохранить на сервер» когда проект открыт.
851
+ if (window.cloudProjects?.setVisibility) window.cloudProjects.setVisibility();
659
852
 
660
853
  const raw = localStorage.getItem(`lastBoard:${handle.name}`);
661
854
  if (raw) {
@@ -696,7 +889,13 @@ async function openFilm(handle) {
696
889
  // Закрыть текущий проект: выгрузить state, скрыть секции, восстановить
697
890
  // шапку, оставить запись в idb (чтобы Recent работал).
698
891
  async function closeProject() {
892
+ // Помечаем что юзер вышел явно — на следующем старте autoload пропускается.
893
+ // Cmd+R после close = welcome, а не реоткрытие.
894
+ try { localStorage.setItem('welcomeOnNextStart', '1'); } catch {}
699
895
  stopExternalWatcher();
896
+ // Сбрасываем UI таймлайна/превью — иначе при возврате через welcome
897
+ // в новый проект остаётся фрейм/дорожки прошлого.
898
+ if (typeof resetTimelineUI === 'function') resetTimelineUI();
700
899
  if (state.currentBoard?.urls) {
701
900
  for (const u of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(u);
702
901
  }
@@ -705,6 +904,9 @@ async function closeProject() {
705
904
  if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
706
905
  state.filmHandle = null;
707
906
  state.currentBoard = null;
907
+ // Сбрасываем cloud-маркеры (если был открыт облачный проект).
908
+ state.cloudProjectId = null;
909
+ state.cloudDirty = false;
708
910
  window.appProject?.notifyState(false);
709
911
  state.charactersInfo = [];
710
912
  state.locationsInfo = [];
@@ -712,6 +914,8 @@ async function closeProject() {
712
914
  state.selectedClipIds.clear();
713
915
  state.selectedTrackIds.clear();
714
916
  document.body.classList.add('no-project');
917
+ // Видимость cloud-кнопок зависит от наличия открытого проекта — переключаем.
918
+ if (window.cloudProjects?.setVisibility) window.cloudProjects.setVisibility();
715
919
  const sub = $('brandSub');
716
920
  if (sub) { sub.textContent = 'Видео-редактор'; sub.classList.remove('has-project'); }
717
921
  const boardEl = $('brandBoard');
@@ -794,21 +998,9 @@ function showBoardContextMenu(kind, item, clientX, clientY) {
794
998
  selectBoard({ kind, ...item }).then(() => openCharacterSettings()).catch(() => {});
795
999
  });
796
1000
  }
797
- // Сохранение этого board'а как шаблон. saveCurrentBoardAsTemplate
798
- // работает с state.currentBoard, поэтому сначала открываем нужный
799
- // board (если он не текущий), а затем запускаем сохранение.
800
- add('💾 Сохранить как шаблон', async () => {
801
- try {
802
- if (!state.currentBoard || state.currentBoard.kind !== kind || state.currentBoard.name !== item.name) {
803
- await selectBoard({ kind, ...item });
804
- }
805
- if (typeof saveCurrentBoardAsTemplate === 'function') {
806
- await saveCurrentBoardAsTemplate();
807
- }
808
- } catch (e) {
809
- alert('Не удалось сохранить как шаблон: ' + (e?.message || e));
810
- }
811
- });
1001
+ // «💾 Сохранить как шаблон» скрыт: scene-templates по упрощённой
1002
+ // модели больше не используются (только project-templates). Код
1003
+ // saveCurrentBoardAsTemplate сохранён может пригодиться позже.
812
1004
  add('🗑 Удалить', async () => {
813
1005
  if (!confirm(`Удалить «${item.name}»? Папка переедет в _deleted, Cmd+Z восстановит.`)) return;
814
1006
  try { await deleteBoard(kind, item.name); }
@@ -838,6 +1030,8 @@ async function promptBoardAspectRatio(kind, item) {
838
1030
  if (!state.currentBoard.metadata.settings) state.currentBoard.metadata.settings = {};
839
1031
  state.currentBoard.metadata.settings.aspectRatio = chosen;
840
1032
  scheduleSave();
1033
+ // Обновить aspect-label в заголовке таймлайна (если открыт).
1034
+ if (typeof updateTimelineAspectLabel === 'function') updateTimelineAspectLabel();
841
1035
  } else {
842
1036
  // Доска не активна — пишем напрямую.
843
1037
  const meta = await loadBoardMetadata(item.handle);
@@ -1366,6 +1560,10 @@ async function selectBoard(board) {
1366
1560
  if (state.currentBoard?.urls) {
1367
1561
  for (const url of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(url);
1368
1562
  }
1563
+ // Сбрасываем UI таймлайна/превью (иначе остаётся фрейм/контент прошлой сцены
1564
+ // пока юзер не нажмёт ▶ — особенно заметно при переключении на сцену
1565
+ // без таймлайна вовсе).
1566
+ if (typeof resetTimelineUI === 'function') resetTimelineUI();
1369
1567
  clearSelection();
1370
1568
  state.selectedClipIds.clear();
1371
1569
  state.selectedTrackIds.clear();