kingkont 0.7.99 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.7.99",
3
+ "version": "0.8.0",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -1885,15 +1885,19 @@ function showNodeContextMenu(node, clientX, clientY) {
1885
1885
  add('📥 Вставить (Cmd+V)', () => pasteClipboardNodes(), { disabled: !canPaste });
1886
1886
  const canReplace = !!(state.clipboard?.length);
1887
1887
  add('⇄ Заменить из буфера', () => replaceNodeFromClipboard(node), { disabled: !canReplace });
1888
- if (node.type === 'audio' && node.file) {
1889
- add('⬇ Скачать аудио', async () => {
1888
+ // «Сохранить как…» — для любого медиа-файла (image / audio / video).
1889
+ // Показывает нативный системный диалог через FS-Access-API; если API
1890
+ // недоступен (web-режим) — auto-сохранение в Downloads/.
1891
+ if ((node.type === 'image' || node.type === 'audio' || node.type === 'video') && node.file) {
1892
+ add('💾 Сохранить как…', async () => {
1890
1893
  try {
1891
1894
  const fh = await resolveBoardFile(state.currentBoard.handle, node.file);
1892
1895
  const file = await fh.getFile();
1893
- await downloadAudioAtSpeed(file, node.file, 1);
1896
+ const suggested = node.file.split('/').pop();
1897
+ await saveAsToDisk(file, suggested);
1894
1898
  } catch (err) {
1895
- console.error('Download failed:', err);
1896
- alert('Не удалось скачать: ' + (err?.message || err));
1899
+ console.error('Save failed:', err);
1900
+ alert('Не удалось сохранить: ' + (err?.message || err));
1897
1901
  }
1898
1902
  });
1899
1903
  }
package/renderer/media.js CHANGED
@@ -152,6 +152,29 @@ async function downloadAudioAtSpeed(file, filename, speed, onProgress) {
152
152
  await ff.deleteFile(outName).catch(() => {});
153
153
  }
154
154
 
155
+ // «Сохранить как…» — показывает нативный системный диалог выбора пути
156
+ // (через FS-Access-API), куда сохранить файл. Если API недоступен или
157
+ // юзер ничего не выбрал — fallback на triggerDownload (auto-сохранение
158
+ // в Downloads/). Возвращает true если файл сохранён, false если отменено.
159
+ async function saveAsToDisk(file, suggestedName) {
160
+ if (typeof window.showSaveFilePicker === 'function') {
161
+ try {
162
+ const handle = await window.showSaveFilePicker({ suggestedName });
163
+ const w = await handle.createWritable();
164
+ await w.write(file);
165
+ await w.close();
166
+ return true;
167
+ } catch (err) {
168
+ // AbortError — юзер закрыл диалог. Не fallback'аемся, чтобы не
169
+ // дёргать auto-download против его воли.
170
+ if (err?.name === 'AbortError') return false;
171
+ // Любая другая ошибка — fall through на triggerDownload.
172
+ }
173
+ }
174
+ triggerDownload(file, suggestedName);
175
+ return true;
176
+ }
177
+
155
178
  function triggerDownload(blob, filename) {
156
179
  const url = URL.createObjectURL(blob);
157
180
  const a = document.createElement('a');
package/renderer/state.js CHANGED
@@ -439,6 +439,20 @@ function getFileType(file) {
439
439
  } catch {}
440
440
  })();
441
441
 
442
+ // One-time migration: дефолт video-разрешения раньше был 720p — сбрасываем
443
+ // тех, у кого залипло '720p' (вероятно, они никогда явно не выбирали),
444
+ // на минимальное '480p'. После явного выбора 720p/1080p — он сохранится
445
+ // как и раньше, миграция повторно не отрабатывает.
446
+ (function migrateVideoResolutionDefault() {
447
+ try {
448
+ if (localStorage.getItem('videoResolutionResetMin') === '1') return;
449
+ if (localStorage.getItem('videoResolution') === '720p') {
450
+ localStorage.setItem('videoResolution', '480p');
451
+ }
452
+ localStorage.setItem('videoResolutionResetMin', '1');
453
+ } catch {}
454
+ })();
455
+
442
456
  const state = {
443
457
  filmHandle: null,
444
458
  currentBoard: null, // { kind, name, key, handle, metadata, urls }
@@ -449,7 +463,7 @@ const state = {
449
463
  videoModel: localStorage.getItem('videoModel') || 'seedance-2', // 'seedance-2' | 'kling-o1' | 'kling-3.0' | ...
450
464
  ttsModel: localStorage.getItem('ttsModel') || 'qwen/qwen3-tts', // qwen/elevenlabs/v3/minimax/speech-02-hd/gemini
451
465
  videoDuration: +(localStorage.getItem('videoDuration') || 5),
452
- videoResolution: localStorage.getItem('videoResolution') || '720p',
466
+ videoResolution: localStorage.getItem('videoResolution') || '480p',
453
467
  videoAspect: localStorage.getItem('videoAspect') || '9:16',
454
468
  jobs: new Map(), // nodeId -> { boardKey, boardHandle, kind, taskId }
455
469
  undoStack: [], // {type, ...payload}