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/index.html +42 -3
- package/lib/providers.js +127 -2
- package/main.js +120 -0
- package/package.json +1 -1
- package/preload.js +38 -12
- package/renderer/board.js +286 -87
- package/renderer/cloudFs.js +271 -0
- package/renderer/cloudProjects.js +612 -0
- package/renderer/generate.js +7 -0
- package/renderer/settings.js +32 -24
- 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
|
|
|
@@ -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> credits` },
|
|
177
|
+
{ key: 'kie', label: 'KIE', low: 5, fmt: (a) => `<b>${a.toLocaleString('ru-RU', { maximumFractionDigits: 2 })}</b> 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> 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
|
-
//
|
|
415
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
});
|
|
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();
|