kingkont 0.8.7 → 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/assets/templates.jpg +0 -0
- package/index.html +42 -3
- package/lib/providers.js +131 -2
- package/main.js +120 -0
- package/package.json +1 -1
- package/preload.js +38 -12
- package/renderer/board.js +345 -87
- package/renderer/cloudFs.js +271 -0
- package/renderer/cloudProjects.js +612 -0
- package/renderer/generate.js +7 -0
- package/renderer/settings.js +40 -1
- package/renderer/state.js +5 -0
- package/renderer/styles.css +97 -17
- package/renderer/templates.js +286 -62
- package/renderer/timeline.js +149 -7
- package/server.js +63 -1
- package/skill/SKILL.md +109 -0
package/renderer/settings.js
CHANGED
|
@@ -347,6 +347,15 @@ async function renderNodeBody(node, body) {
|
|
|
347
347
|
if (nodeEl) { _mediaHydrationObserver.unobserve(nodeEl); nodeEl.__hydrate = null; }
|
|
348
348
|
|
|
349
349
|
if (node.type === 'audio') {
|
|
350
|
+
// Промпт (текст TTS) виден всегда — даже после генерации. Юзер
|
|
351
|
+
// должен понимать что говорит голос без открытия Settings-modal'я.
|
|
352
|
+
const promptText = node.generated?.rawPrompt || node.generated?.prompt;
|
|
353
|
+
if (promptText) {
|
|
354
|
+
const pp = document.createElement('div');
|
|
355
|
+
pp.className = 'audio-prompt';
|
|
356
|
+
pp.textContent = promptText;
|
|
357
|
+
body.appendChild(pp);
|
|
358
|
+
}
|
|
350
359
|
const fname = document.createElement('div');
|
|
351
360
|
fname.className = 'filename';
|
|
352
361
|
fname.textContent = node.file;
|
|
@@ -416,7 +425,20 @@ async function renderNodeBody(node, body) {
|
|
|
416
425
|
// Раньше "metadata" грузил chunk для duration/dimensions с каждой ноды на selectBoard.
|
|
417
426
|
media.preload = 'none';
|
|
418
427
|
}
|
|
419
|
-
media
|
|
428
|
+
// Click на media-элементе = выбрать ноду (как клик по header'у).
|
|
429
|
+
// НЕ делаем preventDefault — иначе сломаются нативные video-controls
|
|
430
|
+
// (play/pause/seek). stopPropagation чтобы не запустился rubber-band
|
|
431
|
+
// селектор холста.
|
|
432
|
+
media.addEventListener('mousedown', e => {
|
|
433
|
+
e.stopPropagation();
|
|
434
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
|
435
|
+
toggleSelection(node.id);
|
|
436
|
+
} else if (!state.selectedNodeIds.has(node.id)) {
|
|
437
|
+
clearSelection();
|
|
438
|
+
state.selectedNodeIds.add(node.id);
|
|
439
|
+
}
|
|
440
|
+
renderSelection();
|
|
441
|
+
});
|
|
420
442
|
// Заменяем placeholder ИЛИ ранее показанный thumbnail
|
|
421
443
|
const target = (nodeEl && nodeEl.__thumbEl && nodeEl.__thumbEl.parentNode)
|
|
422
444
|
? nodeEl.__thumbEl
|
|
@@ -497,6 +519,23 @@ async function renderNodeBody(node, body) {
|
|
|
497
519
|
playRow.appendChild(playBtn);
|
|
498
520
|
body.appendChild(playRow);
|
|
499
521
|
}
|
|
522
|
+
} else if (node.type === 'image' && !node.file && (node.generated?.rawPrompt || node.generated?.prompt)) {
|
|
523
|
+
// Fallback: image-нода с промптом, но без файла (ещё не генерилась
|
|
524
|
+
// и status != 'draft'). Показываем preview-блок чтобы юзер видел
|
|
525
|
+
// что собирается генерироваться.
|
|
526
|
+
const wrap = document.createElement('div');
|
|
527
|
+
wrap.className = 'gen-pending';
|
|
528
|
+
const ic = document.createElement('div');
|
|
529
|
+
ic.style.cssText = 'font-size:36px; opacity:0.7;';
|
|
530
|
+
ic.textContent = '🖼';
|
|
531
|
+
const st = document.createElement('div');
|
|
532
|
+
st.className = 'state-text';
|
|
533
|
+
st.textContent = 'Не сгенерировано — открой ↻ для запуска';
|
|
534
|
+
const pp = document.createElement('div');
|
|
535
|
+
pp.className = 'prompt-preview';
|
|
536
|
+
pp.textContent = node.generated?.rawPrompt || node.generated?.prompt || '';
|
|
537
|
+
wrap.append(ic, st, pp);
|
|
538
|
+
body.appendChild(wrap);
|
|
500
539
|
}
|
|
501
540
|
}
|
|
502
541
|
|
package/renderer/state.js
CHANGED
|
@@ -455,6 +455,11 @@ function getFileType(file) {
|
|
|
455
455
|
|
|
456
456
|
const state = {
|
|
457
457
|
filmHandle: null,
|
|
458
|
+
// Облачный проект: id записи в t_spaces_server_projects_K1 (если открыт
|
|
459
|
+
// cloud-проект, иначе null). filmHandle всё равно валидный (cloudFs-shim).
|
|
460
|
+
cloudProjectId: null,
|
|
461
|
+
// Есть несохранённые изменения (показываем «•» возле «Сохранить на сервер»).
|
|
462
|
+
cloudDirty: false,
|
|
458
463
|
currentBoard: null, // { kind, name, key, handle, metadata, urls }
|
|
459
464
|
genKind: 'image',
|
|
460
465
|
imageModel: localStorage.getItem('imageModel') || 'nano-banana-2', // 'nano-banana-2' | 'nano-banana-pro' | 'grok' | ...
|
package/renderer/styles.css
CHANGED
|
@@ -230,15 +230,18 @@
|
|
|
230
230
|
margin-bottom: 14px; text-align: center;
|
|
231
231
|
flex-shrink: 0;
|
|
232
232
|
}
|
|
233
|
-
/* Recents
|
|
234
|
-
|
|
233
|
+
/* Recents — плитка (responsive grid). Карточки фиксированной ширины,
|
|
234
|
+
заполняют ряд слева направо, при нехватке места переносятся. Скроллится
|
|
235
|
+
вертикально, если карточек много. */
|
|
235
236
|
.welcome-recent-grid {
|
|
236
|
-
display:
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
display: grid;
|
|
238
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
239
|
+
gap: 16px;
|
|
240
|
+
overflow-y: auto; overflow-x: hidden;
|
|
241
|
+
padding: 8px 52px 24px;
|
|
242
|
+
align-content: start;
|
|
243
|
+
}
|
|
244
|
+
.welcome-recent-grid::-webkit-scrollbar { width: 8px; }
|
|
242
245
|
|
|
243
246
|
/* === Глобальные dark-scrollbars === */
|
|
244
247
|
/* Firefox */
|
|
@@ -257,8 +260,9 @@
|
|
|
257
260
|
background: #232323; border: 1px solid #333; border-radius: 8px;
|
|
258
261
|
overflow: hidden; cursor: pointer; transition: border-color 0.12s, transform 0.12s;
|
|
259
262
|
display: flex; flex-direction: column;
|
|
260
|
-
|
|
261
|
-
|
|
263
|
+
/* Grid-layout: ширина задаётся grid-column'ом (responsive),
|
|
264
|
+
fixed-width убран. position:relative нужен для absolute-расположенного × delete. */
|
|
265
|
+
position: relative;
|
|
262
266
|
}
|
|
263
267
|
/* Карточка «Открыть проект» — стилизована под обычную recent-карточку,
|
|
264
268
|
но контент не превью, а большая иконка + надпись. */
|
|
@@ -284,6 +288,18 @@
|
|
|
284
288
|
color: #444; font-size: 32px;
|
|
285
289
|
}
|
|
286
290
|
.welcome-card-thumb img { width: 100%; height: 100%; object-fit: cover; display: block; }
|
|
291
|
+
/* ☁-бейдж в углу обложки облачного проекта. Делает cloud/folder-проекты
|
|
292
|
+
визуально различимыми в общем grid'е (без необходимости второго рядa). */
|
|
293
|
+
.welcome-card-thumb { position: relative; }
|
|
294
|
+
.welcome-card-cloud-badge {
|
|
295
|
+
position: absolute; right: 6px; bottom: 6px;
|
|
296
|
+
background: rgba(20, 30, 50, 0.85); color: #9cf;
|
|
297
|
+
border: 1px solid rgba(160, 200, 255, 0.35);
|
|
298
|
+
border-radius: 50%; width: 22px; height: 22px;
|
|
299
|
+
display: flex; align-items: center; justify-content: center;
|
|
300
|
+
font-size: 13px; line-height: 1;
|
|
301
|
+
backdrop-filter: blur(2px);
|
|
302
|
+
}
|
|
287
303
|
.welcome-card-meta { padding: 8px 12px; }
|
|
288
304
|
.welcome-card-name { font-size: 13px; color: #ddd; word-break: break-all; }
|
|
289
305
|
.welcome-card-ts { font-size: 11px; color: #666; margin-top: 2px; }
|
|
@@ -731,8 +747,16 @@
|
|
|
731
747
|
.fs-modal #fsClose {
|
|
732
748
|
position: absolute; top: 16px; right: 16px;
|
|
733
749
|
background: rgba(0,0,0,0.6); border: 1px solid #444; color: #fff;
|
|
734
|
-
font-size: 22px; line-height: 1;
|
|
750
|
+
font-size: 22px; line-height: 1;
|
|
751
|
+
/* Увеличен hit-area: квадрат 40×40, центрированный «×». Раньше padding
|
|
752
|
+
был 4px 12px → размер ~30×30, и нижне-правые пиксели оказывались под
|
|
753
|
+
<img>/<video> в .fs-stage (одна стека, нет z-index). z-index:1100
|
|
754
|
+
гарантирует что × ВСЕГДА сверху всего внутри fs-modal. */
|
|
755
|
+
width: 40px; height: 40px;
|
|
756
|
+
display: flex; align-items: center; justify-content: center;
|
|
757
|
+
padding: 0; border-radius: 4px;
|
|
735
758
|
cursor: pointer;
|
|
759
|
+
z-index: 1100;
|
|
736
760
|
}
|
|
737
761
|
.fs-modal #fsClose:hover { background: rgba(255,68,68,0.7); }
|
|
738
762
|
|
|
@@ -779,6 +803,7 @@
|
|
|
779
803
|
font-size: 36px; line-height: 1; padding: 4px 14px; border-radius: 6px;
|
|
780
804
|
cursor: pointer; user-select: none;
|
|
781
805
|
transition: background 0.12s;
|
|
806
|
+
z-index: 1050;
|
|
782
807
|
}
|
|
783
808
|
.fs-modal .fs-nav:hover { background: rgba(0,0,0,0.85); }
|
|
784
809
|
.fs-modal .fs-nav:disabled { opacity: 0.3; cursor: default; }
|
|
@@ -945,6 +970,44 @@
|
|
|
945
970
|
.preview-panel .timeline-preview-img.hidden,
|
|
946
971
|
.preview-panel .timeline-preview.hidden { display: none; }
|
|
947
972
|
|
|
973
|
+
/* Overlay-контролы поверх preview-стейджа. Видимы пока есть клипы.
|
|
974
|
+
Полу-прозрачный фон + лёгкий blur — читаемо поверх любой картинки.
|
|
975
|
+
Контейнер не переносит на новую строку (white-space:nowrap) даже на
|
|
976
|
+
узком preview — лучше горизонтальный скролл, чем разорванный таймер. */
|
|
977
|
+
.preview-controls {
|
|
978
|
+
position: absolute; left: 50%; bottom: 16px; transform: translateX(-50%);
|
|
979
|
+
display: flex; align-items: center; gap: 10px;
|
|
980
|
+
padding: 8px 14px; border-radius: 999px;
|
|
981
|
+
background: rgba(0,0,0,0.55); border: 1px solid rgba(255,255,255,0.1);
|
|
982
|
+
backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
|
|
983
|
+
z-index: 10; opacity: 0.45; transition: opacity 0.15s;
|
|
984
|
+
white-space: nowrap; flex-wrap: nowrap;
|
|
985
|
+
max-width: calc(100% - 16px);
|
|
986
|
+
}
|
|
987
|
+
.preview-stage:hover .preview-controls,
|
|
988
|
+
.preview-controls.is-playing { opacity: 1; }
|
|
989
|
+
.preview-controls.hidden { display: none; }
|
|
990
|
+
.preview-controls button {
|
|
991
|
+
background: transparent; border: none; color: #fff;
|
|
992
|
+
font-size: 24px; line-height: 1;
|
|
993
|
+
width: 32px; height: 32px;
|
|
994
|
+
display: flex; align-items: center; justify-content: center;
|
|
995
|
+
padding: 0; cursor: pointer; border-radius: 50%;
|
|
996
|
+
flex-shrink: 0;
|
|
997
|
+
}
|
|
998
|
+
.preview-controls button:hover { background: rgba(255,255,255,0.15); }
|
|
999
|
+
.preview-controls .preview-time {
|
|
1000
|
+
font-family: ui-monospace, 'SF Mono', monospace;
|
|
1001
|
+
font-size: 15px; color: #fff;
|
|
1002
|
+
min-width: 90px; text-align: center;
|
|
1003
|
+
white-space: nowrap; flex-shrink: 0;
|
|
1004
|
+
}
|
|
1005
|
+
/* «×» закрытия — чуть тоньше play-кнопки чтобы не доминировал визуально. */
|
|
1006
|
+
.preview-controls #previewCloseBtn {
|
|
1007
|
+
font-size: 20px; opacity: 0.75;
|
|
1008
|
+
}
|
|
1009
|
+
.preview-controls #previewCloseBtn:hover { opacity: 1; background: rgba(255,68,68,0.55); }
|
|
1010
|
+
|
|
948
1011
|
/* === Таймлайн (нижняя панель, full-width) === */
|
|
949
1012
|
.timeline-panel {
|
|
950
1013
|
border-top: 1px solid #333; background: #1c1c1c;
|
|
@@ -1237,8 +1300,20 @@
|
|
|
1237
1300
|
background: #141414;
|
|
1238
1301
|
}
|
|
1239
1302
|
.gen-pending .prompt-preview {
|
|
1240
|
-
font-size:
|
|
1241
|
-
max-height
|
|
1303
|
+
font-size: 12px; color: #bbb; line-height: 1.4; text-align: center;
|
|
1304
|
+
/* Большой max-height — на нодах побольше виден весь промпт; на маленьких
|
|
1305
|
+
— обрезается со скроллом. Раньше было 60px (~3 строки) — мало. */
|
|
1306
|
+
max-height: 240px; overflow-y: auto; word-break: break-word;
|
|
1307
|
+
width: 100%;
|
|
1308
|
+
}
|
|
1309
|
+
/* Промпт под filename аудио-ноды (постоянно — даже после генерации).
|
|
1310
|
+
Для TTS юзеру важно видеть текст без открытия Settings-modal'я. */
|
|
1311
|
+
.node-body .audio-prompt {
|
|
1312
|
+
font-size: 11px; color: #aaa; line-height: 1.4;
|
|
1313
|
+
margin: 4px 0 6px;
|
|
1314
|
+
max-height: 80px; overflow-y: auto; word-break: break-word;
|
|
1315
|
+
background: #1a1a1a; padding: 6px 8px; border-radius: 4px;
|
|
1316
|
+
border: 1px solid #2a2a2a;
|
|
1242
1317
|
}
|
|
1243
1318
|
.gen-pending .state-text { font-size: 11px; color: #888; }
|
|
1244
1319
|
.gen-error {
|
|
@@ -1248,15 +1323,20 @@
|
|
|
1248
1323
|
}
|
|
1249
1324
|
.gen-error button { margin-top: 8px; }
|
|
1250
1325
|
|
|
1251
|
-
|
|
1326
|
+
/* Эта empty-overlay только для #emptyState (заглушка на пустом холсте).
|
|
1327
|
+
Раньше селектор был `.empty` (глобально) — он ловил, например,
|
|
1328
|
+
`.balance-info.empty` (модификатор «баланс=0») и делал её
|
|
1329
|
+
position:absolute; inset:0 — pill растекалась во весь viewport.
|
|
1330
|
+
Теперь специфично через id, конфликта нет. */
|
|
1331
|
+
#emptyState {
|
|
1252
1332
|
position: absolute; inset: 0;
|
|
1253
1333
|
display: flex; align-items: center; justify-content: center;
|
|
1254
1334
|
color: #666; font-size: 15px; flex-direction: column; gap: 14px;
|
|
1255
1335
|
text-align: center; padding: 24px; pointer-events: none;
|
|
1256
1336
|
}
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
.
|
|
1337
|
+
#emptyState h3 { color: #aaa; font-size: 18px; }
|
|
1338
|
+
#emptyState p { max-width: 420px; line-height: 1.6; }
|
|
1339
|
+
#emptyState.hidden { display: none; }
|
|
1260
1340
|
|
|
1261
1341
|
.unsupported { margin: auto; max-width: 540px; padding: 32px; background: #2a2a2a; border-radius: 12px; border: 1px solid #444; }
|
|
1262
1342
|
.unsupported h1 { margin-bottom: 12px; }
|