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/index.html +42 -3
- package/lib/providers.js +115 -2
- package/main.js +120 -0
- package/package.json +1 -1
- package/preload.js +38 -12
- package/renderer/board.js +285 -87
- package/renderer/cloudFs.js +271 -0
- package/renderer/cloudProjects.js +612 -0
- package/renderer/generate.js +7 -0
- package/renderer/state.js +5 -0
- package/renderer/styles.css +69 -5
- package/renderer/templates.js +8 -6
- package/renderer/timeline.js +149 -7
- package/server.js +55 -1
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
-
//
|
|
415
|
-
|
|
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
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
798
|
-
//
|
|
799
|
-
//
|
|
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();
|