kingkont 0.18.15 → 0.19.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/lib/eventStore.js +12 -2
- package/lib/providers.js +130 -5
- package/lib/settings.js +18 -0
- package/main.js +10 -0
- package/package.json +1 -1
- package/preload.js +5 -0
- package/renderer/board.js +256 -101
- package/renderer/cloudFs.js +8 -0
- package/renderer/cloudProjects.js +28 -3
- package/renderer/generate.js +4 -1
- package/renderer/notifyPanel.js +53 -37
- package/renderer/state.js +5 -0
- package/renderer/styles.css +31 -2
- package/server.js +180 -2
package/renderer/board.js
CHANGED
|
@@ -44,23 +44,43 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
44
44
|
// - logout → чистим кэш + перерисовываем (cloud-карточки исчезнут,
|
|
45
45
|
// fetchListCached не дёрнет /api/projects без логина).
|
|
46
46
|
// setCloudButtonsVisibility пересоберёт sidebar-кнопки (☁ Новый и пр.).
|
|
47
|
+
// Универсальный handler смены auth (login/logout) — общий для Electron-IPC
|
|
48
|
+
// и web WS-event 'auth:changed'.
|
|
49
|
+
async function _onAuthChanged(state) {
|
|
50
|
+
try {
|
|
51
|
+
localStorage.removeItem('cloudProjectsCache');
|
|
52
|
+
localStorage.removeItem('cloudProjectsLastOpened');
|
|
53
|
+
localStorage.removeItem('chatiumStatusCache');
|
|
54
|
+
renderWelcomeIdentity({ force: true }).catch(() => {});
|
|
55
|
+
refreshBalance().catch(() => {});
|
|
56
|
+
if (!window.cloudProjects) return;
|
|
57
|
+
if (window.cloudProjects.setVisibility) window.cloudProjects.setVisibility();
|
|
58
|
+
if (!document.body.classList.contains('no-project')) return;
|
|
59
|
+
await renderWelcomeRecents();
|
|
60
|
+
} catch (e) { vlog('err', 'auth-changed handler: ' + (e?.message || e)); }
|
|
61
|
+
}
|
|
47
62
|
if (window.appChatium?.onAuthChanged) {
|
|
48
|
-
window.appChatium.onAuthChanged(
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
window.appChatium.onAuthChanged(_onAuthChanged);
|
|
64
|
+
} else {
|
|
65
|
+
// Web-mode: подписка на WS-канал auth:changed (server publish'ит
|
|
66
|
+
// после web-login flow / logout).
|
|
67
|
+
try {
|
|
68
|
+
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
|
69
|
+
const ws = new WebSocket(`${proto}://${location.host}/ws`);
|
|
70
|
+
ws.onopen = () => {
|
|
71
|
+
try { ws.send(JSON.stringify({ type: 'subscribe', channel: 'auth:changed' })); } catch {}
|
|
72
|
+
};
|
|
73
|
+
ws.onmessage = (e) => {
|
|
74
|
+
let m; try { m = JSON.parse(e.data); } catch { return; }
|
|
75
|
+
if (m?.type === 'event' && m.channel === 'auth:changed') {
|
|
76
|
+
_onAuthChanged(m.event?.kind || 'login');
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
// Если WS отвалится — потеряем live-уведомления auth-changed,
|
|
80
|
+
// но identity всё равно подтянется при следующем render'е через
|
|
81
|
+
// 10-минутный refresh-window. Не reconnect'имся аггресивно.
|
|
82
|
+
ws.onerror = () => {};
|
|
83
|
+
} catch (e) { vlog('warn', 'auth WS subscribe failed: ' + e?.message); }
|
|
64
84
|
}
|
|
65
85
|
// Восстановить состояние панелей таймлайна/превью/реплик
|
|
66
86
|
const tlOpen = localStorage.getItem('timelineOpen') === '1';
|
|
@@ -100,18 +120,25 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
100
120
|
refreshBalance().catch(() => {});
|
|
101
121
|
setInterval(() => { refreshBalance().catch(() => {}); }, 60_000);
|
|
102
122
|
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
|
|
123
|
+
// Autoload-стратегия: открыть последний проект ТОЛЬКО при Cmd+R (reload),
|
|
124
|
+
// когда юзер был в проекте. На cold start (полное закрытие → запуск)
|
|
125
|
+
// показываем welcome. Юзер: «при запуске я вижу страницу проекта (а не
|
|
126
|
+
// стартовую страницу)».
|
|
127
|
+
//
|
|
128
|
+
// Differentiation: main-process IPC `app:consume-cold-start` возвращает
|
|
129
|
+
// true ровно один раз за app-launch. Cmd+R → false (main продолжает
|
|
130
|
+
// жить, renderer перезагружается). Полный quit+restart → новый main →
|
|
131
|
+
// флаг снова true. Это надёжнее sessionStorage (которое в Electron
|
|
132
|
+
// может переживать quit при persistent partition).
|
|
133
|
+
let isColdStart = false;
|
|
134
|
+
try { isColdStart = await window.appProject?.consumeColdStart?.(); } catch {}
|
|
135
|
+
// skipAutoload: cold start ИЛИ explicit welcome-флаги.
|
|
136
|
+
const skipAutoload = isColdStart
|
|
137
|
+
|| localStorage.getItem('welcomeOnNextStart') === '1'
|
|
111
138
|
|| localStorage.getItem('lastLocation') === 'welcome';
|
|
112
139
|
if (skipAutoload) {
|
|
113
140
|
localStorage.removeItem('welcomeOnNextStart');
|
|
114
|
-
vlog('info',
|
|
141
|
+
vlog('info', `restore: skipped (cold-start=${isColdStart}, welcome state persisted)`);
|
|
115
142
|
} else {
|
|
116
143
|
try {
|
|
117
144
|
const recents = await getRecents();
|
|
@@ -135,8 +162,27 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|
|
135
162
|
const openSettingsFromLogo = () => {
|
|
136
163
|
if (window.appSettings?.openSettingsWindow) window.appSettings.openSettingsWindow();
|
|
137
164
|
};
|
|
138
|
-
document.getElementById('brandLogo')?.addEventListener('dblclick', openSettingsFromLogo);
|
|
139
165
|
document.getElementById('welcomeLogo')?.addEventListener('dblclick', openSettingsFromLogo);
|
|
166
|
+
// Brand-logo в шапке проекта: single-click → возврат на welcome,
|
|
167
|
+
// dblclick → настройки. Юзер: «на странице проекта сделай клик по
|
|
168
|
+
// левому-верхнему логотипу возвратом в список проектов».
|
|
169
|
+
// Single-click задерживаем на 250ms — за это время dblclick отменит таймер.
|
|
170
|
+
const brandLogo = document.getElementById('brandLogo');
|
|
171
|
+
if (brandLogo) {
|
|
172
|
+
brandLogo.style.cursor = 'pointer';
|
|
173
|
+
brandLogo.title = 'Клик — на главную; дабл-клик — настройки';
|
|
174
|
+
brandLogo.addEventListener('click', () => {
|
|
175
|
+
if (brandLogo._dblTimer) { clearTimeout(brandLogo._dblTimer); brandLogo._dblTimer = null; }
|
|
176
|
+
brandLogo._dblTimer = setTimeout(() => {
|
|
177
|
+
brandLogo._dblTimer = null;
|
|
178
|
+
if (state.filmHandle && typeof closeProject === 'function') closeProject();
|
|
179
|
+
}, 250);
|
|
180
|
+
});
|
|
181
|
+
brandLogo.addEventListener('dblclick', () => {
|
|
182
|
+
if (brandLogo._dblTimer) { clearTimeout(brandLogo._dblTimer); brandLogo._dblTimer = null; }
|
|
183
|
+
openSettingsFromLogo();
|
|
184
|
+
});
|
|
185
|
+
}
|
|
140
186
|
// ПКМ на brand-area (sidebar header) — действия проектного уровня
|
|
141
187
|
// (сохранить как шаблон / обновить шаблон).
|
|
142
188
|
document.querySelector('.brand')?.addEventListener('contextmenu', e => {
|
|
@@ -202,6 +248,7 @@ async function refreshBalance() {
|
|
|
202
248
|
}
|
|
203
249
|
}
|
|
204
250
|
|
|
251
|
+
function _renderBalancesInto(data) {
|
|
205
252
|
// Один pill на провайдер. Если у провайдера нет данных (выключен или
|
|
206
253
|
// API не дал баланс) — pill не рендерим.
|
|
207
254
|
const pills = [
|
|
@@ -210,10 +257,13 @@ async function refreshBalance() {
|
|
|
210
257
|
{ key: 'openrouter', label: 'OpenRouter', low: 0.5, fmt: (a) => `<b>$${a.toFixed(2)}</b>` },
|
|
211
258
|
{ key: 'elevenlabs', label: 'ElevenLabs', low: 1000, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b> chars` },
|
|
212
259
|
];
|
|
213
|
-
function renderInto(wrap) {
|
|
260
|
+
function renderInto(wrap, opts = {}) {
|
|
214
261
|
if (!wrap) return;
|
|
215
262
|
wrap.innerHTML = '';
|
|
216
263
|
for (const p of pills) {
|
|
264
|
+
// На welcome (welcomeStatusBalances) kingkont-кредиты показываются
|
|
265
|
+
// ВНУТРИ identity-pill (см. _renderIdentity) — здесь не дублируем.
|
|
266
|
+
if (opts.skipKingkont && p.key === 'kingkont') continue;
|
|
217
267
|
const d = data[p.key];
|
|
218
268
|
if (!d || typeof d.amount !== 'number') continue;
|
|
219
269
|
const pill = document.createElement('span');
|
|
@@ -229,10 +279,15 @@ async function refreshBalance() {
|
|
|
229
279
|
wrap.appendChild(pill);
|
|
230
280
|
}
|
|
231
281
|
}
|
|
232
|
-
// Sidebar-footer (когда открыт проект)
|
|
282
|
+
// Sidebar-footer (когда открыт проект) — все балансы.
|
|
233
283
|
renderInto(document.getElementById('balancesAll'));
|
|
234
|
-
// Welcome top-right (
|
|
235
|
-
renderInto(document.getElementById('welcomeStatusBalances'));
|
|
284
|
+
// Welcome top-right — БЕЗ kingkont (он встроен в identity-pill).
|
|
285
|
+
renderInto(document.getElementById('welcomeStatusBalances'), { skipKingkont: true });
|
|
286
|
+
// refreshBalance мог обновить cache → identity-pill должен показать
|
|
287
|
+
// новое значение. Зовём только если уже на welcome (не лезть в проект).
|
|
288
|
+
if (document.body.classList.contains('no-project')) {
|
|
289
|
+
renderWelcomeIdentity().catch(() => {});
|
|
290
|
+
}
|
|
236
291
|
}
|
|
237
292
|
window.refreshBalance = refreshBalance;
|
|
238
293
|
|
|
@@ -240,13 +295,19 @@ window.refreshBalance = refreshBalance;
|
|
|
240
295
|
// при возврате на welcome (без flicker «пусто → потом запрос → потом текст»).
|
|
241
296
|
// На фоне дёргаем настоящий status и обновляем pill.
|
|
242
297
|
const _STATUS_CACHE_KEY = 'chatiumStatusCache';
|
|
298
|
+
// Refresh-окно: фоновый запрос профиля делаем не чаще раз в 10 минут.
|
|
299
|
+
// Юзер: «перестань запрашивать профиль каждый раз когда я возвращаюсь
|
|
300
|
+
// на главную страницу». Раньше каждый show welcome → новый HTTP-запрос
|
|
301
|
+
// к Chatium /auth~me — лишний трафик, мигание pill'а.
|
|
302
|
+
const _STATUS_REFRESH_WINDOW_MS = 10 * 60 * 1000;
|
|
303
|
+
const _STATUS_HARD_TTL_MS = 24 * 3600 * 1000;
|
|
243
304
|
function _readStatusCache() {
|
|
244
305
|
try {
|
|
245
306
|
const raw = localStorage.getItem(_STATUS_CACHE_KEY);
|
|
246
307
|
if (!raw) return null;
|
|
247
308
|
const obj = JSON.parse(raw);
|
|
248
|
-
if (Date.now() - (obj.ts || 0) >
|
|
249
|
-
return obj.status || null;
|
|
309
|
+
if (Date.now() - (obj.ts || 0) > _STATUS_HARD_TTL_MS) return null; // 24h hard max
|
|
310
|
+
return { status: obj.status || null, ts: obj.ts || 0 };
|
|
250
311
|
} catch { return null; }
|
|
251
312
|
}
|
|
252
313
|
function _writeStatusCache(status) {
|
|
@@ -255,19 +316,31 @@ function _writeStatusCache(status) {
|
|
|
255
316
|
|
|
256
317
|
// Identity-pill в правом верхнем углу welcome-экрана.
|
|
257
318
|
// Показывает кто залогинен в KingKont (или предлагает войти).
|
|
258
|
-
|
|
319
|
+
// opts.force=true — игнорирует rate-limit, всё равно фетчит (login/logout flow).
|
|
320
|
+
async function renderWelcomeIdentity(opts = {}) {
|
|
259
321
|
const wrap = document.getElementById('welcomeStatusIdentity');
|
|
260
322
|
if (!wrap) return;
|
|
261
|
-
// 1) Сначала рендерим из кэша (если есть) — мгновенно.
|
|
262
323
|
const cached = _readStatusCache();
|
|
263
|
-
if (cached) _renderIdentity(wrap, cached);
|
|
264
|
-
//
|
|
324
|
+
if (cached?.status) _renderIdentity(wrap, cached.status);
|
|
325
|
+
// Re-fetch только если: кэш отсутствует ИЛИ старше 10 мин ИЛИ force.
|
|
326
|
+
const ageMs = cached ? (Date.now() - cached.ts) : Infinity;
|
|
327
|
+
const needFetch = opts.force || !cached || ageMs > _STATUS_REFRESH_WINDOW_MS;
|
|
328
|
+
if (!needFetch) return;
|
|
265
329
|
let status = null;
|
|
330
|
+
// Electron preload — preferred (читает свой settings + дёргает auth~me).
|
|
266
331
|
try { status = await window.appChatium?.status?.(); } catch {}
|
|
332
|
+
// Browser-fallback: тот же ответ, но через server endpoint.
|
|
333
|
+
// appChatium в web undefined → status здесь будет null.
|
|
334
|
+
if (!status) {
|
|
335
|
+
try {
|
|
336
|
+
const r = await fetch('/api/auth/status');
|
|
337
|
+
if (r.ok) status = await r.json();
|
|
338
|
+
} catch {}
|
|
339
|
+
}
|
|
267
340
|
if (status) {
|
|
268
341
|
_writeStatusCache(status);
|
|
269
342
|
_renderIdentity(wrap, status);
|
|
270
|
-
} else if (!cached) {
|
|
343
|
+
} else if (!cached?.status) {
|
|
271
344
|
// Ни кэша, ни ответа — рисуем «не залогинен» по дефолту.
|
|
272
345
|
_renderIdentity(wrap, { connected: false });
|
|
273
346
|
}
|
|
@@ -275,24 +348,33 @@ async function renderWelcomeIdentity() {
|
|
|
275
348
|
function _renderIdentity(wrap, status) {
|
|
276
349
|
wrap.innerHTML = '';
|
|
277
350
|
if (status?.connected) {
|
|
278
|
-
//
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
const u = status.user || status.profile || status.account || {};
|
|
351
|
+
// Chatium-профиль: всё в `userInfo` (displayName/firstName/lastName/
|
|
352
|
+
// confirmedEmail/confirmedPhone/imageUrl). Также проверяем legacy-
|
|
353
|
+
// имена (user/profile/account) — на случай других версий API.
|
|
354
|
+
const u = status.userInfo || status.user || status.profile || status.account || {};
|
|
283
355
|
let name = status.displayName || status.name || status.fullName
|
|
284
356
|
|| status.login || status.username || status.userName
|
|
357
|
+
|| u.displayName || u.name || u.fullName
|
|
358
|
+
|| (u.firstName && u.lastName ? `${u.firstName} ${u.lastName}` : null)
|
|
359
|
+
|| u.firstName || u.lastName
|
|
360
|
+
|| u.login || u.username
|
|
285
361
|
|| status.confirmedEmail || status.email
|
|
286
|
-
|| u.
|
|
362
|
+
|| u.confirmedEmail || u.email
|
|
363
|
+
|| u.confirmedPhone || u.phone
|
|
287
364
|
|| null;
|
|
288
365
|
let sub = '';
|
|
289
|
-
|
|
366
|
+
// sub: email если основное имя — НЕ email; иначе userId-префикс.
|
|
367
|
+
const subEmail = u.confirmedEmail || u.email || status.confirmedEmail || status.email;
|
|
368
|
+
if (name && subEmail && subEmail !== name) {
|
|
369
|
+
sub = `· ${subEmail}`;
|
|
370
|
+
} else if (name && status.userId && status.userId !== name) {
|
|
290
371
|
sub = `· ${status.userId.slice(0, 8)}`;
|
|
291
372
|
}
|
|
373
|
+
// Avatar — слева от имени, если imageUrl есть. Через server proxy
|
|
374
|
+
// (CDN-картинка может требовать referer).
|
|
375
|
+
const avatarUrl = u.imageUrl || u.avatarUrl || u.photoUrl || null;
|
|
292
376
|
if (!name) {
|
|
293
|
-
//
|
|
294
|
-
// нет ни email, ни displayName. Показываем userId-префикс
|
|
295
|
-
// компактно вместо некрасивого 24-символьного hash'а целиком.
|
|
377
|
+
// Fallback на userId-prefix.
|
|
296
378
|
name = '👤 ' + (status.userId || 'KingKont').slice(0, 10);
|
|
297
379
|
sub = status.userId ? `…${status.userId.slice(-4)}` : '';
|
|
298
380
|
}
|
|
@@ -301,17 +383,40 @@ function _renderIdentity(wrap, status) {
|
|
|
301
383
|
// сервер (см. main.js [chatium:status] логи тоже).
|
|
302
384
|
const noHumanField = !status.displayName && !status.name && !status.fullName
|
|
303
385
|
&& !status.login && !status.confirmedEmail && !status.email
|
|
304
|
-
&& !u.displayName && !u.
|
|
386
|
+
&& !u.displayName && !u.firstName && !u.lastName && !u.confirmedEmail;
|
|
305
387
|
if (noHumanField) {
|
|
306
388
|
console.warn('[chat-identity] No human-readable name field. Status object:', status);
|
|
307
389
|
console.warn('[chat-identity] _allKeys:', status._allKeys);
|
|
308
390
|
console.warn('[chat-identity] _raw:', status._raw);
|
|
309
391
|
}
|
|
392
|
+
// Avatar (если есть imageUrl) — слева. Прокси через server, чтобы CDN
|
|
393
|
+
// не требовал referer. Lazy-error → скрыть img при failed load.
|
|
394
|
+
const avatarHtml = avatarUrl
|
|
395
|
+
? `<img src="/api/proxy?url=${encodeURIComponent(avatarUrl)}" alt="" class="who-avatar" onerror="this.remove()">`
|
|
396
|
+
: `<span style="color:#5c5; font-size:13px; line-height:1;">●</span>`;
|
|
310
397
|
wrap.innerHTML = `
|
|
311
|
-
|
|
398
|
+
${avatarHtml}
|
|
312
399
|
<span class="who">${escapeHtml(name)}</span>
|
|
313
400
|
<span class="who-sub">${escapeHtml(sub)}</span>
|
|
314
401
|
`;
|
|
402
|
+
// KingKont-кредиты — встраиваем chip ВНУТРИ identity-pill (юзер
|
|
403
|
+
// попросил «кредиты kingkont внеси туда же где профиль»). Берём
|
|
404
|
+
// из balances-кэша (refreshBalance его наполняет). Из общего
|
|
405
|
+
// welcomeStatusBalances kingkont теперь убирается (см. ниже).
|
|
406
|
+
const balances = _readBalancesCache();
|
|
407
|
+
const kk = balances?.kingkont;
|
|
408
|
+
if (kk && typeof kk.amount === 'number') {
|
|
409
|
+
const credits = document.createElement('span');
|
|
410
|
+
credits.className = 'who-credits';
|
|
411
|
+
const low = kk.amount > 0 && kk.amount < 100;
|
|
412
|
+
const empty = kk.amount <= 0;
|
|
413
|
+
if (low) credits.classList.add('low');
|
|
414
|
+
if (empty) credits.classList.add('empty');
|
|
415
|
+
credits.title = 'Баланс KingKont · клик — лог списаний';
|
|
416
|
+
credits.innerHTML = `<b>${kk.amount.toLocaleString('ru-RU')}</b> credits`;
|
|
417
|
+
credits.addEventListener('click', () => window.openTxLog?.());
|
|
418
|
+
wrap.appendChild(credits);
|
|
419
|
+
}
|
|
315
420
|
// Если имя не найдено — добавляем visible debug-pill с available keys,
|
|
316
421
|
// чтобы юзер мог тебе показать что отдаёт Chatium без копания в консоли.
|
|
317
422
|
if (noHumanField && Array.isArray(status._allKeys) && status._allKeys.length) {
|
|
@@ -326,7 +431,14 @@ function _renderIdentity(wrap, status) {
|
|
|
326
431
|
logoutBtn.title = 'Logout из KingKont';
|
|
327
432
|
logoutBtn.addEventListener('click', async () => {
|
|
328
433
|
if (!confirm('Выйти из KingKont?')) return;
|
|
329
|
-
|
|
434
|
+
// Electron: preload IPC. Web: server endpoint.
|
|
435
|
+
try {
|
|
436
|
+
if (window.appChatium?.logout) await window.appChatium.logout();
|
|
437
|
+
else await fetch('/api/auth/logout', { method: 'POST' });
|
|
438
|
+
} catch {}
|
|
439
|
+
// Очистим status-кэш и форсим re-render.
|
|
440
|
+
try { localStorage.removeItem('chatiumStatusCache'); } catch {}
|
|
441
|
+
renderWelcomeIdentity({ force: true }).catch(() => {});
|
|
330
442
|
});
|
|
331
443
|
wrap.appendChild(logoutBtn);
|
|
332
444
|
} else {
|
|
@@ -338,7 +450,21 @@ function _renderIdentity(wrap, status) {
|
|
|
338
450
|
loginBtn.textContent = 'Войти';
|
|
339
451
|
loginBtn.title = 'Войти в KingKont';
|
|
340
452
|
loginBtn.addEventListener('click', async () => {
|
|
341
|
-
|
|
453
|
+
// Electron: preload IPC (откроет внешний браузер, дождётся callback'а
|
|
454
|
+
// на random localhost-port). Web: server endpoint (этот же сервер
|
|
455
|
+
// принимает callback на /api/auth/login/callback).
|
|
456
|
+
if (window.appChatium?.login) {
|
|
457
|
+
try { await window.appChatium.login(); } catch (e) { alert('Login failed: ' + (e?.message || e)); }
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
try {
|
|
461
|
+
const r = await fetch('/api/auth/login/start', { method: 'POST' });
|
|
462
|
+
const d = await r.json();
|
|
463
|
+
if (!r.ok || !d?.loginUrl) throw new Error(d?.error || 'login start failed');
|
|
464
|
+
// Открываем в новой вкладке. После успеха она auto-close,
|
|
465
|
+
// а наша WS-подписка (см. _wireAuthWS) подтянет новый status.
|
|
466
|
+
window.open(d.loginUrl, '_blank', 'noopener');
|
|
467
|
+
} catch (e) { alert('Login failed: ' + (e?.message || e)); }
|
|
342
468
|
});
|
|
343
469
|
wrap.appendChild(loginBtn);
|
|
344
470
|
}
|
|
@@ -605,10 +731,18 @@ async function _renderWelcomeRecentsInner() {
|
|
|
605
731
|
|
|
606
732
|
// ☁ Облачные проекты — видна только если залогинен в Chatium.
|
|
607
733
|
// Создаёт серверную запись и открывает её как новый проект.
|
|
734
|
+
// Web fallback: если preload appSettings нет — спрашиваем сервер
|
|
735
|
+
// через /api/auth/status (тот же chatium-токен из settings.json).
|
|
608
736
|
let isLoggedIn = false;
|
|
609
737
|
try {
|
|
610
|
-
|
|
611
|
-
|
|
738
|
+
if (window.appSettings?.get) {
|
|
739
|
+
const s = await window.appSettings.get();
|
|
740
|
+
isLoggedIn = !!(s?.useChatium && s?.chatium?.token);
|
|
741
|
+
} else {
|
|
742
|
+
const r = await fetch('/api/auth/status');
|
|
743
|
+
const st = await r.json();
|
|
744
|
+
isLoggedIn = !!st?.connected;
|
|
745
|
+
}
|
|
612
746
|
} catch {}
|
|
613
747
|
if (isLoggedIn) {
|
|
614
748
|
const cloudCard = document.createElement('div');
|
|
@@ -1403,65 +1537,77 @@ async function openFilm(handle) {
|
|
|
1403
1537
|
// Закрыть текущий проект: выгрузить state, скрыть секции, восстановить
|
|
1404
1538
|
// шапку, оставить запись в idb (чтобы Recent работал).
|
|
1405
1539
|
async function closeProject() {
|
|
1406
|
-
//
|
|
1407
|
-
//
|
|
1408
|
-
|
|
1540
|
+
// Каждый шаг wrap'нут try/catch — раньше любой throw ломал весь close
|
|
1541
|
+
// (юзер видел «приложение падает», на самом деле renderer вис в полузакрытом
|
|
1542
|
+
// состоянии). Логируем что упало, но продолжаем дальше.
|
|
1543
|
+
const safe = (label, fn) => {
|
|
1544
|
+
try { return fn(); }
|
|
1545
|
+
catch (e) { console.warn(`[closeProject] ${label} failed:`, e?.message || e); }
|
|
1546
|
+
};
|
|
1547
|
+
const safeAwait = async (label, fn) => {
|
|
1548
|
+
try { await fn(); }
|
|
1549
|
+
catch (e) { console.warn(`[closeProject] ${label} failed:`, e?.message || e); }
|
|
1550
|
+
};
|
|
1551
|
+
|
|
1552
|
+
safe('persist welcome flag', () => {
|
|
1409
1553
|
localStorage.setItem('welcomeOnNextStart', '1');
|
|
1410
1554
|
localStorage.setItem('lastLocation', 'welcome');
|
|
1411
|
-
}
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
if (
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
//
|
|
1427
|
-
if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
|
|
1555
|
+
});
|
|
1556
|
+
await safeAwait('chat resetInMemory', async () => {
|
|
1557
|
+
if (window.kingChat?.resetInMemory) await window.kingChat.resetInMemory();
|
|
1558
|
+
});
|
|
1559
|
+
safe('chat close', () => { window.kingChat?.close?.(); });
|
|
1560
|
+
safe('stopExternalWatcher', () => { if (typeof stopExternalWatcher === 'function') stopExternalWatcher(); });
|
|
1561
|
+
safe('resetTimelineUI', () => { if (typeof resetTimelineUI === 'function') resetTimelineUI(); });
|
|
1562
|
+
safe('revoke board urls', () => {
|
|
1563
|
+
if (state.currentBoard?.urls) {
|
|
1564
|
+
for (const u of Object.values(state.currentBoard.urls)) URL.revokeObjectURL(u);
|
|
1565
|
+
}
|
|
1566
|
+
});
|
|
1567
|
+
safe('revokeMentionThumbCache', () => {
|
|
1568
|
+
if (typeof revokeMentionThumbCache === 'function') revokeMentionThumbCache();
|
|
1569
|
+
});
|
|
1570
|
+
// Reset state.
|
|
1428
1571
|
state.filmHandle = null;
|
|
1429
1572
|
state.currentBoard = null;
|
|
1430
|
-
// Сбрасываем cloud-маркеры (если был открыт облачный проект).
|
|
1431
1573
|
state.cloudProjectId = null;
|
|
1432
1574
|
state.cloudDirty = false;
|
|
1433
|
-
window.appProject?.notifyState(false);
|
|
1434
|
-
|
|
1435
|
-
|
|
1575
|
+
safe('notifyState', () => { window.appProject?.notifyState(false); });
|
|
1576
|
+
safe('dispatch project-changed', () => {
|
|
1577
|
+
window.dispatchEvent(new CustomEvent('project-changed'));
|
|
1578
|
+
});
|
|
1436
1579
|
state.charactersInfo = [];
|
|
1437
1580
|
state.locationsInfo = [];
|
|
1438
1581
|
state.selectedNodeIds.clear();
|
|
1439
1582
|
state.selectedClipIds.clear();
|
|
1440
1583
|
state.selectedTrackIds.clear();
|
|
1441
1584
|
document.body.classList.add('no-project');
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
const
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1585
|
+
safe('cloud setVisibility', () => { window.cloudProjects?.setVisibility?.(); });
|
|
1586
|
+
safe('reset sidebar header', () => {
|
|
1587
|
+
const sub = $('brandSub');
|
|
1588
|
+
if (sub) { sub.textContent = 'Видео-редактор'; sub.classList.remove('has-project'); }
|
|
1589
|
+
const boardEl = $('brandBoard');
|
|
1590
|
+
if (boardEl) { boardEl.style.display = 'none'; boardEl.innerHTML = ''; }
|
|
1591
|
+
const rootInfo = $('rootInfo'); if (rootInfo) rootInfo.textContent = '';
|
|
1592
|
+
});
|
|
1593
|
+
safe('clear sidebar lists', () => {
|
|
1594
|
+
for (const id of ['characterList','locationList','episodeList']) {
|
|
1595
|
+
const el = $(id); if (el) el.innerHTML = '';
|
|
1596
|
+
}
|
|
1597
|
+
});
|
|
1598
|
+
safe('disable buttons', () => {
|
|
1599
|
+
for (const id of ['newEpisode','newCharacter','newLocation','repliquesBtn',
|
|
1600
|
+
'addText','genText','genAudio','genImage','genVideo','genSfx','genMusic']) {
|
|
1601
|
+
const b = $(id); if (b) b.disabled = true;
|
|
1602
|
+
}
|
|
1603
|
+
const bb = $('boardBadge'); if (bb) bb.style.display = 'none';
|
|
1604
|
+
const cs = $('charSettingsBtn'); if (cs) cs.style.display = 'none';
|
|
1605
|
+
});
|
|
1606
|
+
safe('clearCanvasKeepSvg', () => {
|
|
1607
|
+
if (typeof clearCanvasKeepSvg === 'function') clearCanvasKeepSvg();
|
|
1608
|
+
});
|
|
1609
|
+
await safeAwait('renderWelcomeRecents', () => renderWelcomeRecents());
|
|
1610
|
+
safe('showEmpty', () => { if (typeof showEmpty === 'function') showEmpty(); });
|
|
1465
1611
|
}
|
|
1466
1612
|
|
|
1467
1613
|
// =================== Sidebar ===================
|
|
@@ -2766,7 +2912,16 @@ function showNodeContextMenu(node, clientX, clientY) {
|
|
|
2766
2912
|
if (node.generated) add('⚙ Параметры', () => showNodeSettings(node));
|
|
2767
2913
|
if (node.status === 'generating') {
|
|
2768
2914
|
add('⏹ Остановить', () => stopJob(node.id));
|
|
2769
|
-
|
|
2915
|
+
// «Перезапустить» — открывает модалку с предзаполненными параметрами
|
|
2916
|
+
// (как «Перегенерировать» для готовой ноды). Раньше тут был
|
|
2917
|
+
// restartJob(id) — рестартил без модалки. Юзер: «нужно чтобы показывал
|
|
2918
|
+
// как раньше». Сначала стопим текущую генерацию, чтобы regenerateNode
|
|
2919
|
+
// пропустил guard `if (status==='generating') return`.
|
|
2920
|
+
add('↻ Перезапустить', async () => {
|
|
2921
|
+
await stopJob(node.id);
|
|
2922
|
+
// status уже сброшен на 'error' в stopJob → regenerateNode пройдёт.
|
|
2923
|
+
regenerateNode(node);
|
|
2924
|
+
});
|
|
2770
2925
|
} else if (node.status === 'draft') {
|
|
2771
2926
|
add('▶ Запустить генерацию', () => regenerateNode(node));
|
|
2772
2927
|
add('✎ Изменить и запустить', () => regenerateNode(node));
|
package/renderer/cloudFs.js
CHANGED
|
@@ -288,5 +288,13 @@
|
|
|
288
288
|
getCloudProjectId,
|
|
289
289
|
// exposed для save-to-server: пройтись по всем файлам in-memory map'а.
|
|
290
290
|
_memProject,
|
|
291
|
+
// Web-only helper: прямой write в backend этого проекта. Используется
|
|
292
|
+
// в cloudProjects.openCloudProject для скачивания manifest+файлов с
|
|
293
|
+
// сервера в память (window.cloudFs только в Electron preload).
|
|
294
|
+
async writeFile(projectId, relPath, data) {
|
|
295
|
+
const backend = makeBackend(projectId);
|
|
296
|
+
await backend.ensure();
|
|
297
|
+
return backend.write(relPath, data);
|
|
298
|
+
},
|
|
291
299
|
};
|
|
292
300
|
})();
|
|
@@ -25,7 +25,29 @@
|
|
|
25
25
|
// -------------- helpers --------------
|
|
26
26
|
function $(id) { return document.getElementById(id); }
|
|
27
27
|
function isLoggedIn(s) { return !!(s?.useChatium && s?.chatium?.token); }
|
|
28
|
-
|
|
28
|
+
// Electron: preload IPC возвращает полный settings.json. В web preload
|
|
29
|
+
// недоступен → опрашиваем /api/auth/status и эмулируем settings-shape
|
|
30
|
+
// (с token-плейсхолдером — реальный токен на сервере, renderer'у он
|
|
31
|
+
// не нужен напрямую, HTTP-запросы идут на наш же сервер с auth-header'ом).
|
|
32
|
+
async function getSettings() {
|
|
33
|
+
if (window.appSettings?.get) {
|
|
34
|
+
try { return await window.appSettings.get(); } catch { return {}; }
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const r = await fetch('/api/auth/status');
|
|
38
|
+
const status = await r.json();
|
|
39
|
+
if (status?.connected) {
|
|
40
|
+
return {
|
|
41
|
+
useChatium: true,
|
|
42
|
+
useOpenrouter: true,
|
|
43
|
+
useElevenlabs: true,
|
|
44
|
+
useKie: true,
|
|
45
|
+
chatium: { token: 'present', userId: status.userId, base: status.base },
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
} catch {}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
29
51
|
|
|
30
52
|
// Прогресс — переиспользуем существующий tplProgress из templates.js.
|
|
31
53
|
const PROGRESS = (typeof TPL_PROGRESS !== 'undefined') ? TPL_PROGRESS : {
|
|
@@ -360,8 +382,11 @@
|
|
|
360
382
|
|
|
361
383
|
async function writeCloudFile(projectId, relPath, data) {
|
|
362
384
|
if (window.cloudFs) return window.cloudFs.write(projectId, relPath, data);
|
|
363
|
-
//
|
|
364
|
-
//
|
|
385
|
+
// Web fallback: пишем в shared in-memory backend, тот же что
|
|
386
|
+
// используют cloudFsShim handle'ы (т.е. файлы появятся в filmHandle
|
|
387
|
+
// через FSAH-like API). Без этого openCloudProject в браузере
|
|
388
|
+
// download'ил manifest+файлы и просто терял их → sidebar пустой.
|
|
389
|
+
if (window.cloudFsShim?.writeFile) return window.cloudFsShim.writeFile(projectId, relPath, data);
|
|
365
390
|
return false;
|
|
366
391
|
}
|
|
367
392
|
|
package/renderer/generate.js
CHANGED
|
@@ -2065,7 +2065,10 @@ async function pollJob(job, nodeId, bKey, boardHandle, kind) {
|
|
|
2065
2065
|
}
|
|
2066
2066
|
if (pd.status === 'done') {
|
|
2067
2067
|
logJob(nodeId, `done, downloading ${pd.url?.slice(0,80)}`);
|
|
2068
|
-
|
|
2068
|
+
// data: URLs (OpenRouter image-gen возвращает inline base64) — берём
|
|
2069
|
+
// напрямую без proxy. Браузерный fetch их понимает natively.
|
|
2070
|
+
const fetchUrl = pd.url.startsWith('data:') ? pd.url : `/api/proxy?url=${encodeURIComponent(pd.url)}`;
|
|
2071
|
+
const blob = await (await fetch(fetchUrl)).blob();
|
|
2069
2072
|
const ext = kind === 'image' ? 'jpg' : 'mp4';
|
|
2070
2073
|
const sub = kind === 'image' ? 'frames' : 'clips';
|
|
2071
2074
|
const dir = await getOrCreateBoardSubdir(boardHandle, sub);
|