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 CHANGED
@@ -19,6 +19,16 @@
19
19
  </div>
20
20
  </div>
21
21
  <button id="pickRoot" class="primary">Открыть проект (папку)</button>
22
+ <!-- ☁ Новый облачный проект — создаёт запись на Chatium (требует логин)
23
+ и локальную копию в userData/cloud-projects/<id>/. Работа с
24
+ облачным проектом идёт локально, save → explicit-кнопка ниже. -->
25
+ <button id="newCloudProject" class="primary" title="Создать новый проект (хранится на сервере)" style="display:none; margin-top:6px;">☁ Новый проект (облако)</button>
26
+ <button id="openCloudProjects" title="Открыть один из своих облачных проектов" style="display:none; margin-top:4px; font-size:12px;">📂 Мои облачные проекты…</button>
27
+ <!-- ☁ Сохранить на сервер — видна когда открыт ЛЮБОЙ проект
28
+ (cloud или папочный). Облачный — обновляет существующую запись;
29
+ папочный — открывает диалог «куда сохранить» (новый/обновить
30
+ существующий, см. saveCurrentProjectAsCloud). -->
31
+ <button id="saveProjectCloud" title="Загрузить весь проект на сервер" style="display:none; margin-top:6px;">☁ Сохранить на сервер</button>
22
32
  <div id="rootInfo" style="font-size: 12px; color: #aaa; word-break: break-all;"></div>
23
33
  </div>
24
34
 
@@ -41,7 +51,12 @@
41
51
  <span>Сцены</span>
42
52
  <span style="display:flex; gap:4px;">
43
53
  <button id="newEpisode" disabled title="Создать сцену">+</button>
44
- <button id="openTemplates" title="Открыть библиотеку шаблонов" style="font-size:12px;">🎨</button>
54
+ <!-- Кнопка 🎨 «Шаблоны» убрана из sidebar'а внутри проекта:
55
+ scene-templates в текущей модели не используются (см. упрощение
56
+ concepts), а project-templates имеют смысл только с welcome-экрана.
57
+ Хук window.cloudProjects.openListModal() вешает её ПКМ для admin-
58
+ use-cases, если понадобится. -->
59
+ <button id="openTemplates" title="Открыть библиотеку шаблонов" style="font-size:12px; display:none;">🎨</button>
45
60
  </span>
46
61
  </h3>
47
62
  <div class="sidebar-list" id="episodeList"></div>
@@ -80,6 +95,14 @@
80
95
  <div class="preview-stage" id="previewStage">
81
96
  <video class="timeline-preview" id="timelinePreview" playsinline></video>
82
97
  <img class="timeline-preview-img hidden" id="timelinePreviewImg" alt="">
98
+ <!-- Overlay-controls внизу превью: play/pause + текущее/общее время + закрыть.
99
+ Показывается когда есть клипы. Дублирует #timelinePlay из таймлайна
100
+ (тот же click-handler срабатывает через эту кнопку). -->
101
+ <div class="preview-controls hidden" id="previewControls">
102
+ <button id="previewPlayBtn" title="Play / Pause">▶</button>
103
+ <span id="previewTimeInfo" class="preview-time">0.0 / 0.0 с</span>
104
+ <button id="previewCloseBtn" title="Свернуть превью">×</button>
105
+ </div>
83
106
  </div>
84
107
  </aside>
85
108
 
@@ -135,6 +158,7 @@
135
158
  <div class="timeline-panel hidden" id="timelinePanel">
136
159
  <div class="timeline-header">
137
160
  <strong>🎬 Таймлайн</strong>
161
+ <span id="timelineAspect" title="Соотношение сторон сцены — настраивается в ПКМ на board-item" style="color:#aac8e6; font-size:11px; font-family:ui-monospace, monospace; padding:1px 6px; border:1px solid #2a3854; border-radius:3px;"></span>
138
162
  <span id="timelineDuration" style="color:#888; font-size:12px;"></span>
139
163
  <span id="timelinePlayheadInfo" style="color:#888; font-size:11px; font-family:ui-monospace, monospace;">0.0 с</span>
140
164
  <span class="spacer"></span>
@@ -146,7 +170,13 @@
146
170
  <button id="timelinePlay" title="Воспроизвести">▶</button>
147
171
  <button id="timelineNative" title="Нативный плеер (mpv)" style="display:none;">🖥</button>
148
172
  <button id="timelineExport" title="Экспорт mp4 (ffmpeg)">💾</button>
149
- <button id="timelineClear" title="Очистить" class="danger">⌫</button>
173
+ <!-- Кнопка «Удалить всё» переехала в меню «⋯» (опасное действие, не на виду). -->
174
+ <div class="tl-more-wrap" style="position:relative;">
175
+ <button id="timelineMore" title="Ещё">⋯</button>
176
+ <div id="timelineMoreMenu" class="add-menu hidden" style="position:absolute; top:100%; right:0; margin-top:4px; min-width:200px;">
177
+ <button id="timelineClear" class="danger" style="color:#f88;">⌫ Удалить всё с таймлайна</button>
178
+ </div>
179
+ </div>
150
180
  <button id="timelineClose" title="Закрыть таймлайн" style="margin-left:6px;">×</button>
151
181
  </div>
152
182
  <div class="timeline-body">
@@ -537,7 +567,10 @@
537
567
  <div class="modal-card" style="min-width: 600px; max-width: 90vw; max-height: 90vh;">
538
568
  <div style="display:flex; align-items:center; gap:8px; margin-bottom:12px; flex-wrap:wrap;">
539
569
  <h3 style="margin:0; flex:1;">Шаблоны</h3>
540
- <button id="tplSaveCurrent" class="primary" title="Сохранить текущую сцену как шаблон">+ Сцена</button>
570
+ <!-- Сцена скрыта: scene-templates по упрощённой модели больше не
571
+ используются (но код saveCurrentBoardAsTemplate оставлен — может
572
+ пригодиться). -->
573
+ <button id="tplSaveCurrent" class="primary" title="Сохранить текущую сцену как шаблон" style="display:none;">+ Сцена</button>
541
574
  <button id="tplSaveProject" class="primary" title="Сохранить весь проект (все сцены, персонажи, локации) как шаблон">+ Проект</button>
542
575
  <button id="tplRefresh" title="Обновить список">↻</button>
543
576
  <button id="tplClose">×</button>
@@ -557,12 +590,18 @@
557
590
  </div>
558
591
 
559
592
  <script src="renderer/state.js"></script>
593
+ <!-- cloudFs.js даёт shim FileSystemDirectoryHandle для облачных проектов.
594
+ Грузим ДО board.js — board.js обращается к window.cloudFsShim в openFilm. -->
595
+ <script src="renderer/cloudFs.js"></script>
560
596
  <script src="renderer/board.js"></script>
561
597
  <script src="renderer/settings.js"></script>
562
598
  <script src="renderer/media.js"></script>
563
599
  <script src="renderer/generate.js"></script>
564
600
  <script src="renderer/timeline.js"></script>
565
601
  <script src="renderer/templates.js"></script>
602
+ <!-- cloudProjects.js — после templates.js, переиспользует TPL_PROGRESS,
603
+ walkBoardFiles, collectProjectBoards, uploadProjectCoverIfAny из templates.js. -->
604
+ <script src="renderer/cloudProjects.js"></script>
566
605
 
567
606
  </body>
568
607
  </html>
package/lib/providers.js CHANGED
@@ -26,6 +26,11 @@ const CHATIUM_PATHS = {
26
26
  // Формат шаблона:
27
27
  // { id, name, kind, manifest, files: { 'frames/x.jpg': cdnUrl, ... }, createdAt }
28
28
  templates: '/app/spaces/server/api/templates',
29
+ // Cloud-projects (НЕ шаблоны) — редактируемые проекты юзера на сервере.
30
+ // Owner-only видимость, save-to-server по explicit-кнопке.
31
+ // Формат:
32
+ // { id, name, manifest: { boards: [{ kind, name, scene, files: {relPath: cdnUrl} }] }, files, coverUrl }
33
+ projects: '/app/spaces/server/api/projects',
29
34
  };
30
35
 
31
36
  const CHATIUM_CDN = 'https://fs.chatium.ru/get';
@@ -193,8 +198,32 @@ async function kieFetch(path, options = {}) {
193
198
  return data;
194
199
  }
195
200
 
201
+ // KIE имеет два namespace'а recordInfo: legacy `/jobs/recordInfo` и
202
+ // «playground» `/playground/recordInfo` для nano-banana-2/pro и других
203
+ // новых моделей. createTask унифицированный, но recordInfo — нет:
204
+ // для playground-моделей `/jobs/recordInfo` отвечает «task id is blank».
205
+ //
206
+ // Поскольку из taskId не определишь какой namespace — пробуем сначала
207
+ // один, если он отвечает 'task id is blank' → пробуем playground.
196
208
  async function kiePoll(taskId) {
197
- return await kieFetch(`/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`);
209
+ if (!taskId || taskId === 'undefined') {
210
+ throw new Error('kiePoll: taskId пустой');
211
+ }
212
+ try {
213
+ return await kieFetch(`/api/v1/jobs/recordInfo?taskId=${encodeURIComponent(taskId)}`);
214
+ } catch (e) {
215
+ const msg = String(e?.message || '');
216
+ // KIE'шный маркер «не та область» — пробуем playground-endpoint.
217
+ if (/task id is blank|not found|invalid task/i.test(msg)) {
218
+ try {
219
+ return await kieFetch(`/api/v1/playground/recordInfo?taskId=${encodeURIComponent(taskId)}`);
220
+ } catch (e2) {
221
+ // Если оба упали — пробрасываем оригинальную (с context на оба пути).
222
+ throw new Error(`KIE recordInfo: jobs=${msg.slice(0,100)} | playground=${String(e2.message).slice(0,100)}`);
223
+ }
224
+ }
225
+ throw e;
226
+ }
198
227
  }
199
228
 
200
229
  // =============================================================================
@@ -317,7 +346,16 @@ async function _startGenerationViaKie({ kind, prompt, key, imageInputs, videoInp
317
346
  method: 'POST',
318
347
  body: JSON.stringify({ model: fullModel, input }),
319
348
  });
320
- return { taskId: data.data?.taskId, provider: 'kie' };
349
+ // Защита: KIE иногда возвращает 200 с code=200 но без data.taskId (например
350
+ // когда модель не поддерживает текущий input — null aspect_ratio для grok,
351
+ // emtpy image_input для image-to-image и т.д.). Без этого кидка клиент
352
+ // получит taskId=undefined, потом будет poll'ить с строкой 'undefined' и
353
+ // KIE ответит «generate playground failed, task id is blank».
354
+ if (!data?.data?.taskId) {
355
+ const dump = JSON.stringify(data || {}).slice(0, 400);
356
+ throw new Error(`KIE createTask вернул без taskId (model=${fullModel}, input keys=${Object.keys(input).join(',')}). Ответ: ${dump}`);
357
+ }
358
+ return { taskId: data.data.taskId, provider: 'kie' };
321
359
  }
322
360
 
323
361
  /**
@@ -768,6 +806,75 @@ async function deleteTemplate(id, s) {
768
806
  return true;
769
807
  }
770
808
 
809
+ // =============================================================================
810
+ // PUBLIC API: cloud projects (CRUD на Chatium-сервере).
811
+ // =============================================================================
812
+ // Cloud-project — редактируемый проект юзера. В отличие от templates:
813
+ // - открыть может только owner/admin
814
+ // - save => перезаписывает запись (по explicit-кнопке клиента)
815
+ // - локально лежит в userData/cloud-projects/<id>/ (Electron создаёт
816
+ // папку автоматически — никакого showDirectoryPicker)
817
+ // - в web-режиме (без FS) — сохраняется только in-memory + сразу на сервер
818
+
819
+ async function listProjects(s) {
820
+ const headers = chatiumAuthHeaders(s);
821
+ logCall('GET ', 'Chatium', chatiumBase(s) + CHATIUM_PATHS.projects);
822
+ const r = await fetch(chatiumBase(s) + CHATIUM_PATHS.projects, { headers });
823
+ const text = await r.text();
824
+ let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
825
+ if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
826
+ return d;
827
+ }
828
+
829
+ async function getProject(id, s) {
830
+ const headers = chatiumAuthHeaders(s);
831
+ const url = `${chatiumBase(s)}${CHATIUM_PATHS.projects}~get?id=${encodeURIComponent(id)}`;
832
+ logCall('GET ', 'Chatium', url);
833
+ const r = await fetch(url, { headers });
834
+ const text = await r.text();
835
+ let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
836
+ if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
837
+ return d;
838
+ }
839
+
840
+ async function createProject(body, s) {
841
+ const headers = { ...chatiumAuthHeaders(s), 'Content-Type': 'application/json' };
842
+ logCall('POST', 'Chatium', chatiumBase(s) + CHATIUM_PATHS.projects, `name=${body?.name}`);
843
+ const r = await fetch(chatiumBase(s) + CHATIUM_PATHS.projects, {
844
+ method: 'POST', headers, body: JSON.stringify(body),
845
+ });
846
+ const text = await r.text();
847
+ let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
848
+ if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
849
+ return d;
850
+ }
851
+
852
+ async function updateProject(id, body, s) {
853
+ const headers = { ...chatiumAuthHeaders(s), 'Content-Type': 'application/json' };
854
+ const url = `${chatiumBase(s)}${CHATIUM_PATHS.projects}~update?id=${encodeURIComponent(id)}`;
855
+ logCall('POST', 'Chatium', url,
856
+ `name=${body?.name} files=${Object.keys(body?.files || {}).length}`);
857
+ const r = await fetch(url, {
858
+ method: 'POST', headers, body: JSON.stringify(body),
859
+ });
860
+ const text = await r.text();
861
+ let d; try { d = JSON.parse(text); } catch { d = { raw: text }; }
862
+ if (!r.ok) throw new Error(d?.error || d?.reason || `HTTP ${r.status}`);
863
+ return d;
864
+ }
865
+
866
+ async function deleteProject(id, s) {
867
+ const headers = chatiumAuthHeaders(s);
868
+ const url = `${chatiumBase(s)}${CHATIUM_PATHS.projects}~delete?id=${encodeURIComponent(id)}`;
869
+ logCall('POST', 'Chatium', url);
870
+ const r = await fetch(url, { method: 'POST', headers });
871
+ if (!r.ok) {
872
+ const text = await r.text().catch(() => '');
873
+ throw new Error(text || `HTTP ${r.status}`);
874
+ }
875
+ return true;
876
+ }
877
+
771
878
  async function listElevenVoices() {
772
879
  const key = process.env.ELEVENLABS_API_KEY;
773
880
  if (!key) throw new Error('ELEVENLABS_API_KEY не задан');
@@ -808,6 +915,12 @@ module.exports = {
808
915
  createTemplate,
809
916
  updateTemplate,
810
917
  deleteTemplate,
918
+ // Cloud projects (CRUD на Chatium-сервере)
919
+ listProjects,
920
+ getProject,
921
+ createProject,
922
+ updateProject,
923
+ deleteProject,
811
924
  // Constants (для server.js / тестов)
812
925
  KIE_IMAGE_MODELS,
813
926
  KIE_VIDEO_MODELS,
package/main.js CHANGED
@@ -207,6 +207,15 @@ const CHATIUM_AUTH_TIMEOUT_MS = 5 * 60 * 1000;
207
207
 
208
208
  let chatiumLoginInflight = null;
209
209
 
210
+ // Уведомить main-window (если открыто) о смене auth-статуса. Renderer
211
+ // (board.js) подписан через window.appChatium.onAuthChanged и при login
212
+ // дёргает refresh облачных проектов, при logout — чистит кэш + welcome.
213
+ function notifyAuthChanged(state) {
214
+ if (win && !win.isDestroyed()) {
215
+ win.webContents.send('chatium:auth-changed', state); // 'login' | 'logout'
216
+ }
217
+ }
218
+
210
219
  ipcMain.handle('chatium:login', async () => {
211
220
  if (chatiumLoginInflight) {
212
221
  return chatiumLoginInflight; // несколько кликов — один и тот же flow
@@ -227,6 +236,7 @@ ipcMain.handle('chatium:login', async () => {
227
236
  },
228
237
  };
229
238
  writeSettings(next);
239
+ notifyAuthChanged('login');
230
240
  return { ok: true, ...result };
231
241
  } finally {
232
242
  chatiumLoginInflight = null;
@@ -253,6 +263,7 @@ ipcMain.handle('chatium:logout', async () => {
253
263
  const next = { ...s };
254
264
  delete next.chatium;
255
265
  writeSettings(next);
266
+ notifyAuthChanged('logout');
256
267
  return { ok: true };
257
268
  });
258
269
 
@@ -553,6 +564,115 @@ ipcMain.handle('recents:write', async (_e, arr) => {
553
564
  } catch (e) { console.warn('recents write failed:', e.message); return false; }
554
565
  });
555
566
 
567
+ // =============================================================================
568
+ // Cloud-projects: локальная копия серверного проекта в userData.
569
+ //
570
+ // Формат — те же scene.json + media-папки что у обычных проектов.
571
+ // Каждый cloud-project живёт в `userData/cloud-projects/<projectId>/`.
572
+ // IPC-API мимикрирует FileSystemDirectoryHandle (минимум — entries / read /
573
+ // write / mkdir / remove), чтобы renderer мог использовать тот же код
574
+ // обработки board'ов через cloud-fs shim (см. renderer/cloudFs.js).
575
+ //
576
+ // Важное правило безопасности: КАЖДЫЙ путь должен быть ВНУТРИ
577
+ // userData/cloud-projects/. Path-traversal через `../` блокируется через
578
+ // path.resolve + startsWith проверку.
579
+ // =============================================================================
580
+ function cloudRoot() {
581
+ const p = path.join(app.getPath('userData'), 'cloud-projects');
582
+ fs.mkdirSync(p, { recursive: true });
583
+ return p;
584
+ }
585
+ function cloudResolve(projectId, relPath) {
586
+ // Безопасный resolve: гарантируем что результат внутри cloudRoot.
587
+ const root = cloudRoot();
588
+ const projectDir = path.join(root, String(projectId).replace(/[\\/]/g, '_'));
589
+ const abs = path.normalize(path.join(projectDir, relPath || ''));
590
+ if (!abs.startsWith(projectDir)) throw new Error('path-traversal');
591
+ return abs;
592
+ }
593
+ ipcMain.handle('cloudFs:ensureProject', (_e, projectId) => {
594
+ const dir = cloudResolve(projectId, '');
595
+ fs.mkdirSync(dir, { recursive: true });
596
+ return dir;
597
+ });
598
+ ipcMain.handle('cloudFs:list', async (_e, projectId, relPath) => {
599
+ // Возвращает [{name, kind: 'file'|'dir'}] — аналог .entries() FSAH.
600
+ try {
601
+ const dir = cloudResolve(projectId, relPath || '');
602
+ const out = [];
603
+ for (const ent of fs.readdirSync(dir, { withFileTypes: true })) {
604
+ out.push({ name: ent.name, kind: ent.isDirectory() ? 'dir' : 'file' });
605
+ }
606
+ return out;
607
+ } catch (e) {
608
+ if (e.code === 'ENOENT') return [];
609
+ throw e;
610
+ }
611
+ });
612
+ ipcMain.handle('cloudFs:exists', (_e, projectId, relPath) => {
613
+ try { fs.statSync(cloudResolve(projectId, relPath)); return true; }
614
+ catch { return false; }
615
+ });
616
+ ipcMain.handle('cloudFs:stat', (_e, projectId, relPath) => {
617
+ try {
618
+ const s = fs.statSync(cloudResolve(projectId, relPath));
619
+ return { size: s.size, mtimeMs: s.mtimeMs, isFile: s.isFile(), isDirectory: s.isDirectory() };
620
+ } catch { return null; }
621
+ });
622
+ ipcMain.handle('cloudFs:read', (_e, projectId, relPath) => {
623
+ // Возвращает Buffer (передаётся как Uint8Array в renderer через electron-IPC).
624
+ return fs.readFileSync(cloudResolve(projectId, relPath));
625
+ });
626
+ ipcMain.handle('cloudFs:readText', (_e, projectId, relPath) => {
627
+ try { return fs.readFileSync(cloudResolve(projectId, relPath), 'utf-8'); }
628
+ catch (e) { if (e.code === 'ENOENT') return null; throw e; }
629
+ });
630
+ ipcMain.handle('cloudFs:write', (_e, projectId, relPath, data) => {
631
+ // data: Uint8Array | string. Создаём родительскую папку если нет.
632
+ const abs = cloudResolve(projectId, relPath);
633
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
634
+ if (typeof data === 'string') fs.writeFileSync(abs, data);
635
+ else fs.writeFileSync(abs, Buffer.from(data));
636
+ return true;
637
+ });
638
+ ipcMain.handle('cloudFs:mkdir', (_e, projectId, relPath) => {
639
+ fs.mkdirSync(cloudResolve(projectId, relPath), { recursive: true });
640
+ return true;
641
+ });
642
+ ipcMain.handle('cloudFs:remove', (_e, projectId, relPath) => {
643
+ // Безопасно: только внутри project-folder.
644
+ const abs = cloudResolve(projectId, relPath);
645
+ try {
646
+ const s = fs.statSync(abs);
647
+ if (s.isDirectory()) fs.rmSync(abs, { recursive: true, force: true });
648
+ else fs.unlinkSync(abs);
649
+ return true;
650
+ } catch { return false; }
651
+ });
652
+ ipcMain.handle('cloudFs:listProjects', () => {
653
+ // Возвращает [{ projectId }] — список локальных копий, для recovery после
654
+ // restart. Может быть из них уже удалены на сервере — клиент сам пингует
655
+ // /api/projects при старте.
656
+ try {
657
+ return fs.readdirSync(cloudRoot(), { withFileTypes: true })
658
+ .filter(e => e.isDirectory())
659
+ .map(e => ({ projectId: e.name }));
660
+ } catch { return []; }
661
+ });
662
+ // Открыть локальную папку cloud-проекта в Finder/Explorer (через ПКМ-меню
663
+ // в welcome-grid'е). Безопасно: shell.openPath работает только если путь
664
+ // существует и внутри cloudRoot (cloudResolve гарантирует это).
665
+ ipcMain.handle('cloudFs:openInFinder', async (_e, projectId) => {
666
+ try {
667
+ const dir = cloudResolve(projectId, '');
668
+ fs.mkdirSync(dir, { recursive: true }); // на случай если ещё не скачан
669
+ const err = await shell.openPath(dir);
670
+ return { ok: !err, error: err || null };
671
+ } catch (e) {
672
+ return { ok: false, error: e.message };
673
+ }
674
+ });
675
+
556
676
  ipcMain.handle('window:close', () => {
557
677
  if (win && !win.isDestroyed()) win.close();
558
678
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.8.8",
3
+ "version": "0.9.0",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/preload.js CHANGED
@@ -1,17 +1,18 @@
1
- // Preload: пробрасывает в renderer (index.html) минимальный API для нативного плеера.
2
- // Веб-версия работает без него (window.nativePlayer === undefined → editor использует только web preview).
1
+ // Preload: пробрасывает в renderer (index.html) минимальный API.
3
2
  const { contextBridge, ipcRenderer, webUtils } = require('electron');
4
3
 
5
- contextBridge.exposeInMainWorld('nativePlayer', {
6
- // Старт нативного плеера. Резолвится URL'ом WebSocket (ws://127.0.0.1:PORT) либо ошибкой.
7
- start: () => ipcRenderer.invoke('native-player:start'),
8
- // Остановить процесс плеера его окно).
9
- stop: () => ipcRenderer.invoke('native-player:stop'),
10
- // Абсолютный путь файла (Electron-специфично, через webUtils.getPathForFile).
11
- pathForFile: (file) => {
12
- try { return webUtils.getPathForFile(file) || null; } catch { return null; }
13
- },
14
- });
4
+ // Нативный плеер (mpv через python-обёртку) отключён — web-превью покрывает
5
+ // все use-cases. Код в main.js (`native-player:start`/`stop`) и в
6
+ // renderer/timeline.js (init гейтится `if (!window.nativePlayer) return`)
7
+ // сохранён раскомментируй блок ниже, чтобы включить обратно.
8
+ //
9
+ // contextBridge.exposeInMainWorld('nativePlayer', {
10
+ // start: () => ipcRenderer.invoke('native-player:start'),
11
+ // stop: () => ipcRenderer.invoke('native-player:stop'),
12
+ // pathForFile: (file) => {
13
+ // try { return webUtils.getPathForFile(file) || null; } catch { return null; }
14
+ // },
15
+ // });
15
16
 
16
17
  // Application menu → renderer events. Пунктов мало, делаем тонкий event-bus.
17
18
  contextBridge.exposeInMainWorld('appMenu', {
@@ -78,9 +79,34 @@ contextBridge.exposeInMainWorld('appChatium', {
78
79
  login: () => ipcRenderer.invoke('chatium:login'),
79
80
  logout: () => ipcRenderer.invoke('chatium:logout'),
80
81
  status: () => ipcRenderer.invoke('chatium:status'),
82
+ // Подписка на изменения auth-статуса (login/logout из settings-window).
83
+ // cb получает 'login' | 'logout'. Возвращает unsubscribe.
84
+ onAuthChanged: (cb) => {
85
+ const handler = (_e, state) => cb(state);
86
+ ipcRenderer.on('chatium:auth-changed', handler);
87
+ return () => ipcRenderer.removeListener('chatium:auth-changed', handler);
88
+ },
81
89
  });
82
90
 
83
91
  contextBridge.exposeInMainWorld('claudeMd', {
84
92
  template: () => ipcRenderer.invoke('claudeMd:template'),
85
93
  installSkill: () => ipcRenderer.invoke('claudeMd:installSkill'),
86
94
  });
95
+
96
+ // Cloud-projects: тонкая обёртка над userData/cloud-projects/<id>/.
97
+ // Используется renderer'ом в режиме «облачного проекта» (без showDirectoryPicker).
98
+ // На веб-версии (без preload) — undefined; web-shell использует in-memory модель
99
+ // + сразу-на-сервер (см. renderer/cloudFs.js fallback).
100
+ contextBridge.exposeInMainWorld('cloudFs', {
101
+ ensureProject: (id) => ipcRenderer.invoke('cloudFs:ensureProject', id),
102
+ list: (id, rel) => ipcRenderer.invoke('cloudFs:list', id, rel),
103
+ exists: (id, rel) => ipcRenderer.invoke('cloudFs:exists', id, rel),
104
+ stat: (id, rel) => ipcRenderer.invoke('cloudFs:stat', id, rel),
105
+ read: (id, rel) => ipcRenderer.invoke('cloudFs:read', id, rel),
106
+ readText: (id, rel) => ipcRenderer.invoke('cloudFs:readText', id, rel),
107
+ write: (id, rel, data) => ipcRenderer.invoke('cloudFs:write', id, rel, data),
108
+ mkdir: (id, rel) => ipcRenderer.invoke('cloudFs:mkdir', id, rel),
109
+ remove: (id, rel) => ipcRenderer.invoke('cloudFs:remove', id, rel),
110
+ listProjects: () => ipcRenderer.invoke('cloudFs:listProjects'),
111
+ openInFinder: (id) => ipcRenderer.invoke('cloudFs:openInFinder', id),
112
+ });