kingkont 0.8.8 → 0.9.1

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
 
@@ -143,6 +174,7 @@ async function refreshBalance() {
143
174
  // API не дал баланс) — pill не рендерим.
144
175
  const pills = [
145
176
  { key: 'kingkont', label: 'KingKont', onClick: () => window.openTxLog?.(), low: 100, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b>&nbsp;credits` },
177
+ { key: 'kie', label: 'KIE', low: 5, fmt: (a) => `<b>${a.toLocaleString('ru-RU', { maximumFractionDigits: 2 })}</b>&nbsp;credits` },
146
178
  { key: 'openrouter', label: 'OpenRouter', low: 0.5, fmt: (a) => `<b>$${a.toFixed(2)}</b>` },
147
179
  { key: 'elevenlabs', label: 'ElevenLabs', low: 1000, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b>&nbsp;chars` },
148
180
  ];
@@ -318,6 +350,11 @@ async function getRecents() {
318
350
 
319
351
  async function touchRecent(handle, thumbBlob) {
320
352
  if (!handle) return;
353
+ // Облачные проекты НЕ попадают в recents — они уже показываются в
354
+ // welcome-grid'е через /api/projects (с ☁-бейджем). Иначе после первого
355
+ // открытия cloud-проект дублируется: один раз как «облачный», второй раз
356
+ // как «локальный недавний» (handle указывает на userData/cloud-projects/<id>).
357
+ if (window.cloudFsShim?.isCloudHandle?.(handle)) return;
321
358
  const list = await _readRecentsMeta();
322
359
  const i = list.findIndex(r => r.name === handle.name);
323
360
  let thumbDataUrl = (i >= 0 ? list[i].thumbDataUrl : null);
@@ -382,6 +419,37 @@ async function renderWelcomeRecents() {
382
419
  openCard.addEventListener('click', () => $('pickRoot').click());
383
420
  grid.appendChild(openCard);
384
421
 
422
+ // ☁ Облачные проекты — видна только если залогинен в Chatium.
423
+ // Создаёт серверную запись и открывает её как новый проект.
424
+ let isLoggedIn = false;
425
+ try {
426
+ const s = await window.appSettings?.get();
427
+ isLoggedIn = !!(s?.useChatium && s?.chatium?.token);
428
+ } catch {}
429
+ if (isLoggedIn) {
430
+ const cloudCard = document.createElement('div');
431
+ cloudCard.className = 'welcome-card open-card';
432
+ const cloudThumb = document.createElement('div');
433
+ cloudThumb.className = 'welcome-card-thumb';
434
+ cloudThumb.textContent = '☁';
435
+ const cloudMeta = document.createElement('div');
436
+ cloudMeta.className = 'welcome-card-meta';
437
+ const cloudName = document.createElement('div');
438
+ cloudName.className = 'welcome-card-name';
439
+ cloudName.textContent = 'Новый проект (облако)';
440
+ const cloudSub = document.createElement('div');
441
+ cloudSub.className = 'welcome-card-ts';
442
+ cloudSub.textContent = 'хранится на сервере';
443
+ cloudMeta.append(cloudName, cloudSub);
444
+ cloudCard.append(cloudThumb, cloudMeta);
445
+ cloudCard.addEventListener('click', () => {
446
+ if (window.cloudProjects?.createNew) window.cloudProjects.createNew();
447
+ });
448
+ grid.appendChild(cloudCard);
449
+ // (отдельная карточка «Мои облачные проекты» убрана — облачные теперь
450
+ // показываются inline в общем списке с ☁-бейджем.)
451
+ }
452
+
385
453
  // Сразу за «Открыть проект» — карточка «Шаблоны» (открывает библиотеку
386
454
  // шаблонов с серверa). Стиль такой же как у open-card — иконка + meta.
387
455
  const tplCard = document.createElement('div');
@@ -411,70 +479,194 @@ async function renderWelcomeRecents() {
411
479
  });
412
480
  grid.appendChild(tplCard);
413
481
 
414
- // Title меняем в зависимости от наличия recents.
415
- if (titleEl) titleEl.textContent = list.length ? 'Открыть проект · недавние' : 'Открыть проект';
482
+ // ---- Облачные проекты: добавляем в тот же grid с ☁ бейджем ----
483
+ // Cached рендерим сразу, fresh background merge через onListChange.
484
+ let cloudItems = [];
485
+ if (isLoggedIn && window.cloudProjects?.fetchListCached) {
486
+ const { cached, promise } = window.cloudProjects.fetchListCached();
487
+ cloudItems = cached;
488
+ // Когда фон-фетч обновит — перерендерим карточки in-place.
489
+ promise.then(items => {
490
+ if (state.filmHandle) return; // welcome больше не виден
491
+ if (JSON.stringify(items) === JSON.stringify(cloudItems)) return; // ничего не изменилось
492
+ renderWelcomeRecents().catch(() => {});
493
+ }).catch(() => {});
494
+ }
416
495
 
496
+ // ---- Объединённый список: cloud + folder, отсортированный по timestamp ----
497
+ // Каждый элемент: { type: 'cloud'|'recent', sortTs, item }.
498
+ // Для cloud берём максимум из (updatedAt | lastOpenedAt | createdAt) —
499
+ // юзер ожидает что недавно открытый проект (даже без сохранения) поднимется.
500
+ const merged = [];
501
+ for (const c of cloudItems) {
502
+ const ts = Math.max(c.lastOpenedAt || 0, c.updatedAt || 0, c.createdAt || 0);
503
+ merged.push({ type: 'cloud', sortTs: ts, item: c });
504
+ }
505
+ // Дедуп: cloud-проекты не должны показываться ещё и как «недавние папки».
506
+ // Источники дубля:
507
+ // 1) handle — cloud-shim (window.cloudFsShim.isCloudHandle) — фильтруем в touchRecent,
508
+ // но старые записи могли остаться от предыдущих версий.
509
+ // 2) имя совпадает с именем cloud-проекта — heuristic для legacy-записей.
510
+ const cloudNames = new Set(cloudItems.map(c => c.name).filter(Boolean));
417
511
  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
- }
512
+ if (window.cloudFsShim?.isCloudHandle?.(r.handle)) continue; // shim в IDB
513
+ if (cloudNames.has(r.name)) continue; // имя дубля
514
+ merged.push({ type: 'recent', sortTs: r.ts || 0, item: r });
515
+ }
516
+ merged.sort((a, b) => b.sortTs - a.sortTs);
517
+
518
+ // Title меняем в зависимости от наличия проектов.
519
+ if (titleEl) titleEl.textContent = merged.length ? 'Открыть проект · недавние' : 'Открыть проект';
520
+
521
+ for (const m of merged) {
522
+ if (m.type === 'cloud') grid.appendChild(makeCloudWelcomeCard(m.item));
523
+ else grid.appendChild(makeRecentWelcomeCard(m.item));
524
+ }
525
+ }
526
+
527
+ // Карточка локального (папочного) проекта — извлечена из renderWelcomeRecents.
528
+ function makeRecentWelcomeCard(r) {
529
+ const card = document.createElement('div');
530
+ card.className = 'welcome-card';
531
+ const thumb = document.createElement('div');
532
+ thumb.className = 'welcome-card-thumb';
533
+ if (r.thumb) {
534
+ const img = document.createElement('img');
535
+ img.src = URL.createObjectURL(r.thumb);
536
+ img.onload = () => setTimeout(() => URL.revokeObjectURL(img.src), 60_000);
537
+ thumb.appendChild(img);
538
+ } else thumb.textContent = '🎬';
539
+ const meta = document.createElement('div');
540
+ meta.className = 'welcome-card-meta';
541
+ const nameEl = document.createElement('div');
542
+ nameEl.className = 'welcome-card-name';
543
+ nameEl.textContent = r.name;
544
+ const tsEl = document.createElement('div');
545
+ tsEl.className = 'welcome-card-ts';
546
+ tsEl.textContent = fmtRelativeTime(r.ts);
547
+ meta.append(nameEl, tsEl);
548
+ card.append(thumb, meta);
549
+ const del = document.createElement('div');
550
+ del.className = 'welcome-card-del';
551
+ del.textContent = '×';
552
+ del.title = 'Удалить из недавних';
553
+ del.addEventListener('click', async e => {
554
+ e.stopPropagation();
555
+ if (!confirm(`Убрать «${r.name}» из недавних? (папка не удаляется)`)) return;
556
+ await removeRecent(r.name);
557
+ await renderWelcomeRecents();
558
+ });
559
+ card.appendChild(del);
560
+ card.addEventListener('click', async () => {
561
+ try {
562
+ if (r.handle) {
563
+ let g = (await r.handle.queryPermission({ mode: 'readwrite' })) === 'granted';
564
+ if (!g) g = (await r.handle.requestPermission({ mode: 'readwrite' })) === 'granted';
565
+ vlog('info', `welcome card click ${r.name}: granted=${g}`);
566
+ if (g) await openFilm(r.handle);
567
+ else alert('Доступ к папке не подтверждён.');
568
+ } else {
569
+ try {
570
+ const handle = await window.showDirectoryPicker({ mode: 'readwrite', id: 'video-editor-film' });
571
+ await openFilm(handle);
572
+ } catch (err) {
573
+ if (err.name === 'AbortError') return;
574
+ throw err;
470
575
  }
471
- } catch (err) {
472
- vlog('err', 'welcome card failed: ' + (err?.message || err));
473
- alert('Ошибка: ' + (err?.message || err));
474
576
  }
475
- });
476
- grid.appendChild(card);
477
- }
577
+ } catch (err) {
578
+ vlog('err', 'welcome card failed: ' + (err?.message || err));
579
+ alert('Ошибка: ' + (err?.message || err));
580
+ }
581
+ });
582
+ return card;
583
+ }
584
+
585
+ // Карточка облачного проекта. Визуально такая же как у папочного, плюс
586
+ // ☁ бейдж в углу обложки. Клик → cloudProjects.open() (использует local
587
+ // cache если синхронизирован, иначе скачивает manifest+файлы).
588
+ // ПКМ → меню «Открыть в Finder», «Обновить из облака», «Удалить с сервера».
589
+ function makeCloudWelcomeCard(p) {
590
+ const card = document.createElement('div');
591
+ card.className = 'welcome-card cloud-card';
592
+ card.dataset.cloudId = p.id;
593
+
594
+ const thumb = document.createElement('div');
595
+ thumb.className = 'welcome-card-thumb';
596
+ if (p.coverUrl) {
597
+ const img = document.createElement('img');
598
+ img.src = '/api/proxy?url=' + encodeURIComponent(p.coverUrl);
599
+ img.draggable = false;
600
+ img.onerror = () => { thumb.textContent = '🎬'; img.remove(); };
601
+ thumb.appendChild(img);
602
+ } else thumb.textContent = '🎬';
603
+ // ☁-бейдж в нижнем правом углу обложки.
604
+ const cloudBadge = document.createElement('div');
605
+ cloudBadge.className = 'welcome-card-cloud-badge';
606
+ cloudBadge.textContent = '☁';
607
+ cloudBadge.title = 'Облачный проект';
608
+ thumb.appendChild(cloudBadge);
609
+
610
+ const meta = document.createElement('div');
611
+ meta.className = 'welcome-card-meta';
612
+ const nameEl = document.createElement('div');
613
+ nameEl.className = 'welcome-card-name';
614
+ nameEl.textContent = p.name || '(без названия)';
615
+ const tsEl = document.createElement('div');
616
+ tsEl.className = 'welcome-card-ts';
617
+ tsEl.textContent = p.updatedAt ? fmtRelativeTime(p.updatedAt) : 'облако';
618
+ meta.append(nameEl, tsEl);
619
+ card.append(thumb, meta);
620
+
621
+ card.addEventListener('click', () => {
622
+ if (window.cloudProjects?.open) window.cloudProjects.open(p.id, p.name);
623
+ });
624
+ card.addEventListener('contextmenu', e => {
625
+ e.preventDefault();
626
+ e.stopPropagation();
627
+ showCloudCardContextMenu(p, e.clientX, e.clientY);
628
+ });
629
+ return card;
630
+ }
631
+
632
+ // ПКМ-меню для облачной welcome-карточки.
633
+ function showCloudCardContextMenu(p, clientX, clientY) {
634
+ const menu = $('nodeMenu');
635
+ if (!menu) return;
636
+ menu.innerHTML = '';
637
+ const add = (label, fn, opts = {}) => {
638
+ const b = document.createElement('button');
639
+ b.textContent = label;
640
+ if (opts.danger) b.style.color = '#f88';
641
+ b.addEventListener('click', () => { menu.classList.add('hidden'); fn(); });
642
+ menu.appendChild(b);
643
+ };
644
+ add('📂 Открыть в Finder', async () => {
645
+ if (!window.cloudFs?.openInFinder) {
646
+ alert('Доступно только в Electron-приложении');
647
+ return;
648
+ }
649
+ const r = await window.cloudFs.openInFinder(p.id);
650
+ if (!r?.ok) alert('Не удалось открыть: ' + (r?.error || 'unknown'));
651
+ });
652
+ add('↻ Обновить из облака', () => {
653
+ if (window.cloudProjects?.open) window.cloudProjects.open(p.id, p.name, { forceRefresh: true });
654
+ });
655
+ add('🗑 Удалить с сервера', async () => {
656
+ if (!confirm(`Удалить «${p.name}» с сервера? Локальная копия в userData останется.`)) return;
657
+ try {
658
+ const r = await fetch('/api/projects/' + encodeURIComponent(p.id), { method: 'DELETE' });
659
+ if (!r.ok) throw new Error('HTTP ' + r.status);
660
+ // Очищаем кэш и перерендериваем grid.
661
+ try {
662
+ const cached = JSON.parse(localStorage.getItem('cloudProjectsCache') || '[]');
663
+ localStorage.setItem('cloudProjectsCache', JSON.stringify(cached.filter(c => c.id !== p.id)));
664
+ } catch {}
665
+ await renderWelcomeRecents();
666
+ } catch (e) { alert('Не удалось удалить: ' + (e?.message || e)); }
667
+ }, { danger: true });
668
+ positionFloatingMenu(menu, clientX, clientY);
669
+ setTimeout(() => document.addEventListener('mousedown', closeNodeMenu, { once: true }), 0);
478
670
  }
479
671
 
480
672
  // Скопировать файл image-ноды в корень проекта как `.cover.<ext>`.
@@ -656,6 +848,8 @@ async function openFilm(handle) {
656
848
  loadUploadCache().catch(() => {});
657
849
  await refreshSidebar();
658
850
  showEmpty();
851
+ // Cloud-кнопки: показать «☁ Сохранить на сервер» когда проект открыт.
852
+ if (window.cloudProjects?.setVisibility) window.cloudProjects.setVisibility();
659
853
 
660
854
  const raw = localStorage.getItem(`lastBoard:${handle.name}`);
661
855
  if (raw) {
@@ -696,7 +890,13 @@ async function openFilm(handle) {
696
890
  // Закрыть текущий проект: выгрузить state, скрыть секции, восстановить
697
891
  // шапку, оставить запись в idb (чтобы Recent работал).
698
892
  async function closeProject() {
893
+ // Помечаем что юзер вышел явно — на следующем старте autoload пропускается.
894
+ // Cmd+R после close = welcome, а не реоткрытие.
895
+ try { localStorage.setItem('welcomeOnNextStart', '1'); } catch {}
699
896
  stopExternalWatcher();
897
+ // Сбрасываем UI таймлайна/превью — иначе при возврате через welcome
898
+ // в новый проект остаётся фрейм/дорожки прошлого.
899
+ if (typeof resetTimelineUI === 'function') resetTimelineUI();
700
900
  if (state.currentBoard?.urls) {
701
901
  for (const u of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(u);
702
902
  }
@@ -705,6 +905,9 @@ async function closeProject() {
705
905
  if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
706
906
  state.filmHandle = null;
707
907
  state.currentBoard = null;
908
+ // Сбрасываем cloud-маркеры (если был открыт облачный проект).
909
+ state.cloudProjectId = null;
910
+ state.cloudDirty = false;
708
911
  window.appProject?.notifyState(false);
709
912
  state.charactersInfo = [];
710
913
  state.locationsInfo = [];
@@ -712,6 +915,8 @@ async function closeProject() {
712
915
  state.selectedClipIds.clear();
713
916
  state.selectedTrackIds.clear();
714
917
  document.body.classList.add('no-project');
918
+ // Видимость cloud-кнопок зависит от наличия открытого проекта — переключаем.
919
+ if (window.cloudProjects?.setVisibility) window.cloudProjects.setVisibility();
715
920
  const sub = $('brandSub');
716
921
  if (sub) { sub.textContent = 'Видео-редактор'; sub.classList.remove('has-project'); }
717
922
  const boardEl = $('brandBoard');
@@ -794,21 +999,9 @@ function showBoardContextMenu(kind, item, clientX, clientY) {
794
999
  selectBoard({ kind, ...item }).then(() => openCharacterSettings()).catch(() => {});
795
1000
  });
796
1001
  }
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
- });
1002
+ // «💾 Сохранить как шаблон» скрыт: scene-templates по упрощённой
1003
+ // модели больше не используются (только project-templates). Код
1004
+ // saveCurrentBoardAsTemplate сохранён может пригодиться позже.
812
1005
  add('🗑 Удалить', async () => {
813
1006
  if (!confirm(`Удалить «${item.name}»? Папка переедет в _deleted, Cmd+Z восстановит.`)) return;
814
1007
  try { await deleteBoard(kind, item.name); }
@@ -838,6 +1031,8 @@ async function promptBoardAspectRatio(kind, item) {
838
1031
  if (!state.currentBoard.metadata.settings) state.currentBoard.metadata.settings = {};
839
1032
  state.currentBoard.metadata.settings.aspectRatio = chosen;
840
1033
  scheduleSave();
1034
+ // Обновить aspect-label в заголовке таймлайна (если открыт).
1035
+ if (typeof updateTimelineAspectLabel === 'function') updateTimelineAspectLabel();
841
1036
  } else {
842
1037
  // Доска не активна — пишем напрямую.
843
1038
  const meta = await loadBoardMetadata(item.handle);
@@ -1366,6 +1561,10 @@ async function selectBoard(board) {
1366
1561
  if (state.currentBoard?.urls) {
1367
1562
  for (const url of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(url);
1368
1563
  }
1564
+ // Сбрасываем UI таймлайна/превью (иначе остаётся фрейм/контент прошлой сцены
1565
+ // пока юзер не нажмёт ▶ — особенно заметно при переключении на сцену
1566
+ // без таймлайна вовсе).
1567
+ if (typeof resetTimelineUI === 'function') resetTimelineUI();
1369
1568
  clearSelection();
1370
1569
  state.selectedClipIds.clear();
1371
1570
  state.selectedTrackIds.clear();