kingkont 0.18.2 → 0.18.4

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.18.2",
3
+ "version": "0.18.4",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -21,12 +21,46 @@
21
21
 
22
22
  function _ensureUI() {
23
23
  if ($('notifyBtn')) return;
24
+ // CSS animations для бейджа/панели/новых row'ов — иначе юзер не
25
+ // замечает что событие пришло (прошлая версия с display:none→flex
26
+ // была инстант, не привлекала внимание).
27
+ if (!$('notifyPanelStyles')) {
28
+ const style = document.createElement('style');
29
+ style.id = 'notifyPanelStyles';
30
+ style.textContent = `
31
+ @keyframes notifyBellPulse {
32
+ 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(227,51,119,0.55); background: rgba(60,30,50,0.92); }
33
+ 40% { transform: scale(1.18); box-shadow: 0 0 0 8px rgba(227,51,119,0); background: rgba(120,40,80,0.95); }
34
+ 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(227,51,119,0); background: rgba(30,30,40,0.85); }
35
+ }
36
+ @keyframes notifyPanelSlide {
37
+ 0% { transform: translateY(12px) scale(0.96); opacity: 0; }
38
+ 100% { transform: translateY(0) scale(1); opacity: 1; }
39
+ }
40
+ @keyframes notifyRowFlash {
41
+ 0% { box-shadow: 0 0 0 0 rgba(227,51,119,0.0), inset 0 0 0 0 rgba(227,51,119,0.0); transform: translateX(-6px); opacity: 0.4; }
42
+ 18% { transform: translateX(0); opacity: 1; box-shadow: 0 0 14px 2px rgba(227,51,119,0.55), inset 2px 0 0 0 rgba(227,51,119,0.85); }
43
+ 100% { box-shadow: 0 0 0 0 rgba(227,51,119,0), inset 2px 0 0 0 rgba(227,51,119,0); transform: translateX(0); opacity: 1; }
44
+ }
45
+ #notifyBtn.pulsing {
46
+ animation: notifyBellPulse 0.9s ease-out 1;
47
+ }
48
+ #notifyPanel.opening {
49
+ animation: notifyPanelSlide 0.22s cubic-bezier(0.18, 0.89, 0.32, 1.28) 1;
50
+ transform-origin: bottom left;
51
+ }
52
+ #notifyList .notify-row.is-new {
53
+ animation: notifyRowFlash 1.6s ease-out 1;
54
+ }
55
+ `;
56
+ document.head.appendChild(style);
57
+ }
24
58
  // Кнопка-колокольчик в нижнем-левом углу — там есть свободное место,
25
59
  // не перекрывает sidebar/canvas-toolbar/welcome-status.
26
60
  const btn = document.createElement('button');
27
61
  btn.id = 'notifyBtn';
28
62
  btn.title = 'События (генерации, чат, ошибки)';
29
- btn.style.cssText = 'position:fixed; bottom:12px; left:12px; z-index:9998; background:rgba(30,30,40,0.85); border:1px solid #444; color:#ccc; font-size:14px; width:32px; height:32px; border-radius:50%; cursor:pointer; backdrop-filter:blur(4px); display:flex; align-items:center; justify-content:center;';
63
+ btn.style.cssText = 'position:fixed; bottom:12px; left:12px; z-index:9998; background:rgba(30,30,40,0.85); border:1px solid #444; color:#ccc; font-size:14px; width:32px; height:32px; border-radius:50%; cursor:pointer; backdrop-filter:blur(4px); display:flex; align-items:center; justify-content:center; transition:background 0.2s;';
30
64
  btn.innerHTML = '🔔';
31
65
  document.body.appendChild(btn);
32
66
  btn.addEventListener('click', toggle);
@@ -54,13 +88,32 @@
54
88
  $('notifyClear').addEventListener('click', () => { events.length = 0; unread = 0; render(); });
55
89
  }
56
90
 
57
- function setOpen(open) {
91
+ // openMode: 'manual' (юзер сам нажал на 🔔 — НЕ автозакрываем),
92
+ // 'auto' (открыли по событию — закроется через таймер),
93
+ // null (закрыто).
94
+ let openMode = null;
95
+ function setOpen(open, mode) {
58
96
  _ensureUI();
59
97
  panelOpen = open;
60
- $('notifyPanel').style.display = open ? 'flex' : 'none';
61
- if (open) { unread = 0; updateBadge(); render(); }
98
+ openMode = open ? (mode || 'manual') : null;
99
+ const panel = $('notifyPanel');
100
+ panel.style.display = open ? 'flex' : 'none';
101
+ if (open) {
102
+ // Слайд-анимация при открытии — иначе display:none→flex слишком
103
+ // резкий, юзер не успевает заметить «панель появилась». Переустанавливаем
104
+ // класс через requestAnimationFrame чтобы анимация рестартовала
105
+ // даже если панель только что закрылась.
106
+ panel.classList.remove('opening');
107
+ requestAnimationFrame(() => {
108
+ requestAnimationFrame(() => panel.classList.add('opening'));
109
+ });
110
+ unread = 0;
111
+ updateBadge();
112
+ render();
113
+ }
62
114
  }
63
- function toggle() { setOpen(!panelOpen); }
115
+ function toggle() { setOpen(!panelOpen, 'manual'); }
116
+ function isManuallyOpen() { return panelOpen && openMode === 'manual'; }
64
117
 
65
118
  function updateBadge() {
66
119
  const b = $('notifyBadge');
@@ -90,6 +143,11 @@
90
143
  const time = `${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}:${String(dt.getSeconds()).padStart(2,'0')}`;
91
144
  const clickable = !!e.target;
92
145
  row.style.cssText = `position:relative; padding:8px 10px 8px 10px; border-radius:4px; margin-bottom:4px; background:${c}22; border-left:3px solid ${c}; cursor:${clickable ? 'pointer' : 'default'}; transition:background 0.1s;`;
146
+ row.classList.add('notify-row');
147
+ // Свежее событие — flash-анимация. Окно 2.2s достаточное чтобы
148
+ // юзер заметил, и не настолько большое чтобы старый event при
149
+ // re-render'е (например после удаления) ложно мигнул.
150
+ if (Date.now() - e.ts < 2200) row.classList.add('is-new');
93
151
  if (clickable) row.title = 'Перейти к объекту';
94
152
  const targetHint = clickable ? `<span style="color:#9ab; font-size:10px; margin-left:6px;">↗</span>` : '';
95
153
  row.innerHTML = `<div style="display:flex; justify-content:space-between; gap:8px; align-items:flex-start; padding-right:18px;">
@@ -158,6 +216,33 @@
158
216
  } else {
159
217
  render();
160
218
  }
219
+ _pulseBell();
220
+ // Авто-открытие панели на 3.5s — юзер видит событие сразу, потом
221
+ // панель прячется. Если юзер сам открыл — не трогаем (проверка в
222
+ // openAuto). Каждое новое событие сбрасывает таймер.
223
+ _scheduleAutoFlash();
224
+ }
225
+ // Bell-pulse: одна короткая анимация. Перезапускаем класс через
226
+ // void offsetWidth — иначе подряд идущие события не «пульсируют»
227
+ // повторно (CSS animation не рестартует на тот же класс).
228
+ function _pulseBell() {
229
+ const btn = $('notifyBtn');
230
+ if (!btn) return;
231
+ btn.classList.remove('pulsing');
232
+ void btn.offsetWidth;
233
+ btn.classList.add('pulsing');
234
+ }
235
+ // Auto-flash: открыть в auto-режиме и закрыть через таймер. Срабатывает
236
+ // на КАЖДОЕ addEvent. Если юзер уже manual'но открыл — открытие no-op'ится.
237
+ let _autoFlashT = null;
238
+ function _scheduleAutoFlash() {
239
+ if (panelOpen && openMode === 'manual') return; // юзер сам открыл — не лезем
240
+ if (!panelOpen) setOpen(true, 'auto');
241
+ if (_autoFlashT) clearTimeout(_autoFlashT);
242
+ _autoFlashT = setTimeout(() => {
243
+ _autoFlashT = null;
244
+ if (openMode === 'auto') setOpen(false);
245
+ }, 5000);
161
246
  }
162
247
 
163
248
  // Navigate from notification to scene/node.
@@ -224,20 +309,30 @@
224
309
  setTimeout(() => { el.style.boxShadow = ''; }, 2500);
225
310
  }, 200);
226
311
  }
227
- window.notifyPanel = { open: () => setOpen(true), close: () => setOpen(false), toggle, addEvent, render };
312
+ // openAuto для showToast: открывает в режиме auto (закроется по таймеру
313
+ // если showToast.close('auto') не отменён). open() без аргумента — manual.
314
+ window.notifyPanel = {
315
+ open: (mode) => setOpen(true, mode || 'manual'),
316
+ openAuto: () => setOpen(true, 'auto'),
317
+ closeAuto: () => { if (openMode === 'auto') setOpen(false); },
318
+ close: () => setOpen(false),
319
+ toggle, addEvent, render,
320
+ isManuallyOpen,
321
+ };
228
322
 
229
- // Init: ensure UI exists ASAP, hook into showToast и WS-events.
323
+ // Init: ensure UI exists ASAP, drain pending toast queue, subscribe WS.
230
324
  document.addEventListener('DOMContentLoaded', () => {
231
325
  _ensureUI();
232
- // Wrap showToast чтобы дублировать в панель + forward target для
233
- // click-навигации (showToast(text, kind, {target: {...}})).
234
- if (typeof window.showToast === 'function') {
235
- const orig = window.showToast;
236
- window.showToast = function (text, kind, opts) {
237
- try { addEvent({ kind: kind || 'info', text, target: opts?.target || null }); } catch {}
238
- return orig.apply(this, arguments);
239
- };
240
- }
326
+ // Дренируем showToast которые вызвались до загрузки notifyPanel.
327
+ // (state.js:showToast копит в __pendingToastQueue если addEvent
328
+ // ещё не доступен.)
329
+ try {
330
+ const q = window.__pendingToastQueue;
331
+ if (Array.isArray(q)) {
332
+ for (const ev of q) addEvent(ev);
333
+ q.length = 0;
334
+ }
335
+ } catch {}
241
336
  // WS-канал jobs:all для start/end/done/failed events. Connect к /ws.
242
337
  function _connect() {
243
338
  let ws;
package/renderer/state.js CHANGED
@@ -177,8 +177,10 @@ async function systemNotify(title, body, opts = {}) {
177
177
  try { perm = await Notification.requestPermission(); } catch {}
178
178
  }
179
179
  if (perm !== 'granted') {
180
- // Не вышлоfallback на toast (юзер хоть так увидит).
181
- if (typeof showToast === 'function') showToast('🔔 ' + title + (body ? ': ' + body : ''), 'info');
180
+ // Без OS-нотификациисобытие УЖЕ добавлено в notifyPanel через
181
+ // showToast-wrapper (single-source-of-truth). Раньше тут был fallback
182
+ // на showToast → дубль события с префиксом «🔔 KingKont:» БЕЗ target,
183
+ // поэтому без клика. Теперь молча пропускаем.
182
184
  return null;
183
185
  }
184
186
  try {
@@ -197,36 +199,27 @@ async function systemNotify(title, body, opts = {}) {
197
199
  }
198
200
  window.systemNotify = systemNotify;
199
201
 
200
- // Глобальный toast для информативных уведомлений (генерация завершилась,
201
- // resume сработал, ...). Стек справа сверху. Auto-dismiss:
202
- // - ok/info 10s (юзер должен заметить успех)
203
- // - error — 30s (важно, не пропустить)
202
+ // Глобальная сигналка событий. Раньше показывался bottom-left toast,
203
+ // но дубль с notifyPanel'ом юзера утомил («toast не нужен»). Теперь:
204
+ // - событие добавляется в notifyPanel (через wrapper в notifyPanel.js)
205
+ // - панель открывается АВТОМАТИЧЕСКИ ненадолго (3s) юзер видит
206
+ // событие сразу, потом панель скрывается, бейдж остаётся.
207
+ // Возвращаем фейковый «токен» для совместимости с местами которые ждут
208
+ // результат showToast (там ничего с ним не делают, но на всякий случай).
204
209
  function showToast(text, kind, opts) {
205
- // opts.target: {projectKey, boardKey, nodeId} toast становится кликабельным.
206
- let host = document.getElementById('toastHost');
207
- if (!host) {
208
- host = document.createElement('div');
209
- host.id = 'toastHost';
210
- // Bottom-left стек — рядом с 🔔 кнопкой нотификаций. Новые toast'ы
211
- // появляются СНИЗУ (flex-direction: column-reverse), так что они
212
- // «всплывают» вверх от кнопки.
213
- host.style.cssText = 'position:fixed; left:52px; bottom:12px; z-index:9999; display:flex; flex-direction:column-reverse; gap:8px; pointer-events:none; max-width:340px;';
214
- document.body.appendChild(host);
215
- }
216
- const t = document.createElement('div');
217
- const colors = {
218
- ok: 'background:#1f4a1f; border:1px solid #4cc44c; color:#dcffdc;',
219
- error: 'background:#4a1f1f; border:1px solid #c44c4c; color:#ffdcdc;',
220
- info: 'background:#1f2f4a; border:1px solid #4c7cc4; color:#dceaff;',
221
- };
222
- t.style.cssText = `${colors[kind] || colors.info} padding:10px 14px; border-radius:8px; font-size:13px; font-weight:500; pointer-events:auto; box-shadow:0 6px 20px rgba(0,0,0,0.5); cursor:pointer; line-height:1.4; word-break:break-word;`;
223
- t.textContent = text;
224
- t.title = 'Кликни чтобы закрыть';
225
- t.addEventListener('click', () => t.remove());
226
- host.appendChild(t);
227
- const ttl = kind === 'error' ? 30000 : 10000;
228
- setTimeout(() => t.remove(), ttl);
229
- return t;
210
+ // Bottom-left toast убрансобытие напрямую идёт в notifyPanel,
211
+ // панель сама подсветится (см. _scheduleAutoFlash в notifyPanel.js).
212
+ // Если notifyPanel ещё не загружен (DOM-readiness race) — копим
213
+ // в очередь, она дренируется при создании панели.
214
+ try {
215
+ if (window.notifyPanel?.addEvent) {
216
+ window.notifyPanel.addEvent({ kind: kind || 'info', text, target: opts?.target || null });
217
+ } else {
218
+ (window.__pendingToastQueue = window.__pendingToastQueue || [])
219
+ .push({ kind: kind || 'info', text, target: opts?.target || null });
220
+ }
221
+ } catch {}
222
+ return { _stub: true };
230
223
  }
231
224
  window.showToast = showToast;
232
225
 
@@ -124,8 +124,11 @@
124
124
  display: flex; flex-direction: column; gap: 6px;
125
125
  font-size: 11px; color: #777;
126
126
  }
127
- .sidebar-footer .hint { color: #777; font-size: 11px; line-height: 1.4; }
128
- .sidebar-footer .jobs-info { color: #aaccdd; font-size: 11px; }
127
+ /* «В фоне: N» и hint «Перетаскивай файлы…» отступаем влево на ширину
128
+ 🔔-кнопки (32px + 12px gap слева от sidebar = 44px), чтобы кнопка
129
+ событий (position:fixed; left:12px; bottom:12px) их не перекрывала. */
130
+ .sidebar-footer .hint { color: #777; font-size: 11px; line-height: 1.4; padding-left: 44px; }
131
+ .sidebar-footer .jobs-info { color: #aaccdd; font-size: 11px; padding-left: 44px; }
129
132
  .sidebar-footer .balance-info {
130
133
  display: flex; align-items: center; gap: 6px; font-size: 11px;
131
134
  color: #c4c4c4; padding: 4px 8px; background: #2a2a2a;