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 +1 -1
- package/renderer/notifyPanel.js +111 -16
- package/renderer/state.js +24 -31
- package/renderer/styles.css +5 -2
package/package.json
CHANGED
package/renderer/notifyPanel.js
CHANGED
|
@@ -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
|
-
|
|
91
|
+
// openMode: 'manual' (юзер сам нажал на 🔔 — НЕ автозакрываем),
|
|
92
|
+
// 'auto' (открыли по событию — закроется через таймер),
|
|
93
|
+
// null (закрыто).
|
|
94
|
+
let openMode = null;
|
|
95
|
+
function setOpen(open, mode) {
|
|
58
96
|
_ensureUI();
|
|
59
97
|
panelOpen = open;
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
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,
|
|
323
|
+
// Init: ensure UI exists ASAP, drain pending toast queue, subscribe WS.
|
|
230
324
|
document.addEventListener('DOMContentLoaded', () => {
|
|
231
325
|
_ensureUI();
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
//
|
|
181
|
-
|
|
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
|
-
//
|
|
201
|
-
//
|
|
202
|
-
// -
|
|
203
|
-
// -
|
|
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
|
-
//
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
|
package/renderer/styles.css
CHANGED
|
@@ -124,8 +124,11 @@
|
|
|
124
124
|
display: flex; flex-direction: column; gap: 6px;
|
|
125
125
|
font-size: 11px; color: #777;
|
|
126
126
|
}
|
|
127
|
-
|
|
128
|
-
|
|
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;
|