kingkont 0.9.1 → 0.9.3

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
@@ -70,6 +70,13 @@
70
70
 
71
71
  <!-- Welcome-экран: виден только когда body.no-project -->
72
72
  <div class="welcome" id="welcome">
73
+ <!-- Топ-правый угол: identity + балансы. Позиционирование absolute,
74
+ чтобы не ехало с центрированной шапкой. Заполняется renderWelcomeStatus
75
+ (board.js) при каждом показе welcome И при auth-changed/balance-tick. -->
76
+ <div class="welcome-status" id="welcomeStatus" style="-webkit-app-region: no-drag;">
77
+ <div class="welcome-status-balances" id="welcomeStatusBalances"></div>
78
+ <div class="welcome-status-identity" id="welcomeStatusIdentity"></div>
79
+ </div>
73
80
  <div class="welcome-inner">
74
81
  <div class="welcome-header">
75
82
  <img class="welcome-logo" src="assets/icon.png" alt="" draggable="false" id="welcomeLogo" title="Дабл-клик — настройки" style="cursor:pointer;">
package/main.js CHANGED
@@ -369,12 +369,84 @@ function runChatiumLoginFlow() {
369
369
  });
370
370
  }
371
371
 
372
+ // Inline-base64 логотипа — страница callback'а живёт на random-port localhost
373
+ // и не имеет доступа к assets/. Кэшируем в module-scope чтобы не читать
374
+ // файл на каждый login.
375
+ let _logoDataUri = null;
376
+ function getLogoDataUri() {
377
+ if (_logoDataUri !== null) return _logoDataUri;
378
+ try {
379
+ const buf = fs.readFileSync(path.join(__dirname, 'assets', 'icon.png'));
380
+ _logoDataUri = `data:image/png;base64,${buf.toString('base64')}`;
381
+ } catch { _logoDataUri = ''; }
382
+ return _logoDataUri;
383
+ }
384
+
372
385
  function authResultPage(kind, message) {
373
- const color = kind === 'ok' ? '#16a34a' : '#dc2626';
374
- return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>KingKont</title>
375
- <style>body{font-family:-apple-system,BlinkMacSystemFont,sans-serif;max-width:520px;margin:80px auto;padding:0 20px;text-align:center;color:#222}
376
- h1{color:${color}}p{color:#555;line-height:1.5}</style></head>
377
- <body><h1>${kind === 'ok' ? 'Подключено' : 'Ошибка'}</h1><p>${message}</p></body></html>`;
386
+ const ok = kind === 'ok';
387
+ const title = ok ? 'Подключено' : 'Ошибка';
388
+ const accent = ok ? '#5cb85c' : '#e53e3e';
389
+ const logo = getLogoDataUri();
390
+ return `<!DOCTYPE html><html lang="ru"><head>
391
+ <meta charset="utf-8">
392
+ <meta name="viewport" content="width=device-width, initial-scale=1">
393
+ <title>KingKont — ${title}</title>
394
+ <style>
395
+ * { box-sizing: border-box; }
396
+ html, body { margin: 0; padding: 0; height: 100%; }
397
+ body {
398
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
399
+ background: #1a1a1a; color: #e0e0e0;
400
+ display: flex; align-items: center; justify-content: center;
401
+ min-height: 100vh; padding: 24px;
402
+ }
403
+ .card {
404
+ text-align: center; max-width: 460px; width: 100%;
405
+ padding: 48px 32px;
406
+ }
407
+ .logo {
408
+ width: 120px; height: 120px; margin: 0 auto 24px; display: block;
409
+ background: #1f1f1f; border-radius: 24px; padding: 12px;
410
+ box-shadow: 0 8px 48px rgba(227, 51, 119, 0.22);
411
+ object-fit: contain;
412
+ }
413
+ h1 {
414
+ font-size: 28px; font-weight: 600; margin: 0 0 6px;
415
+ color: #fff;
416
+ }
417
+ .accent {
418
+ color: ${accent}; font-size: 14px; letter-spacing: 0.5px;
419
+ text-transform: uppercase; margin-bottom: 20px;
420
+ }
421
+ p {
422
+ color: #aaa; line-height: 1.6; font-size: 15px;
423
+ margin: 0 0 32px;
424
+ }
425
+ button {
426
+ background: #e33377; color: #fff; border: none; cursor: pointer;
427
+ font-size: 16px; font-weight: 600; letter-spacing: 0.3px;
428
+ padding: 14px 48px; border-radius: 8px;
429
+ transition: background 0.12s, transform 0.06s;
430
+ -webkit-tap-highlight-color: transparent;
431
+ }
432
+ button:hover { background: #d12a6a; }
433
+ button:active { transform: scale(0.98); }
434
+ .hint {
435
+ color: #555; font-size: 11px; margin-top: 16px;
436
+ font-family: ui-monospace, 'SF Mono', monospace;
437
+ }
438
+ </style>
439
+ </head>
440
+ <body>
441
+ <div class="card">
442
+ ${logo ? `<img class="logo" src="${logo}" alt="KingKont">` : ''}
443
+ <div class="accent">KingKont</div>
444
+ <h1>${title}</h1>
445
+ <p>${message}</p>
446
+ <button onclick="window.close()">Закрыть</button>
447
+ <div class="hint">Если вкладка не закрылась — закрой её сам</div>
448
+ </div>
449
+ </body></html>`;
378
450
  }
379
451
 
380
452
  // ===== Settings window =====
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kingkont",
3
- "version": "0.9.1",
3
+ "version": "0.9.3",
4
4
  "description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
5
5
  "main": "main.js",
6
6
  "bin": {
package/renderer/board.js CHANGED
@@ -50,8 +50,11 @@ window.addEventListener('DOMContentLoaded', async () => {
50
50
  // Сбрасываем кэш — он содержал данные старого юзера.
51
51
  localStorage.removeItem('cloudProjectsCache');
52
52
  localStorage.removeItem('cloudProjectsLastOpened');
53
- // Если открыт welcome (нет проекта)перерисовать. fetchListCached
54
- // в renderWelcomeRecents подтянет свежий список с сервера.
53
+ // Welcome top-right identity-pill + balancesперерисовываем сразу
54
+ // (даже если открыт проект и welcome скрыт — обновится для следующего показа).
55
+ renderWelcomeIdentity().catch(() => {});
56
+ refreshBalance().catch(() => {});
57
+ // Если открыт welcome (нет проекта) — перерисовать grid.
55
58
  if (!state || !window.cloudProjects) return;
56
59
  if (window.cloudProjects.setVisibility) window.cloudProjects.setVisibility();
57
60
  if (!document.body.classList.contains('no-project')) return;
@@ -162,14 +165,11 @@ window.addEventListener('DOMContentLoaded', async () => {
162
165
  // red (≤0). Также экспортирована глобально как window.refreshBalance —
163
166
  // чтобы settings-окно могло триггерить обновление после login/logout.
164
167
  async function refreshBalance() {
165
- const wrap = document.getElementById('balancesAll');
166
- if (!wrap) return;
167
168
  let data = {};
168
169
  try {
169
170
  const r = await fetch('/api/balance/all');
170
171
  if (r.ok) data = await r.json();
171
172
  } catch {}
172
- wrap.innerHTML = '';
173
173
  // Один pill на провайдер. Если у провайдера нет данных (выключен или
174
174
  // API не дал баланс) — pill не рендерим.
175
175
  const pills = [
@@ -178,24 +178,78 @@ async function refreshBalance() {
178
178
  { key: 'openrouter', label: 'OpenRouter', low: 0.5, fmt: (a) => `<b>$${a.toFixed(2)}</b>` },
179
179
  { key: 'elevenlabs', label: 'ElevenLabs', low: 1000, fmt: (a) => `<b>${a.toLocaleString('ru-RU')}</b>&nbsp;chars` },
180
180
  ];
181
- for (const p of pills) {
182
- const d = data[p.key];
183
- if (!d || typeof d.amount !== 'number') continue;
184
- const pill = document.createElement('span');
185
- pill.className = 'balance-info';
186
- pill.title = `Баланс ${p.label}` + (p.onClick ? ' · клик — лог списаний' : '');
187
- if (d.amount > 0 && d.amount < (p.low || 0)) pill.classList.add('low');
188
- if (d.amount <= 0) pill.classList.add('empty');
189
- pill.innerHTML = `<span class="dot"></span><span style="color:#888;font-size:10px;margin-right:4px;">${p.label}</span><span>${p.fmt(d.amount)}</span>`;
190
- if (p.onClick) {
191
- pill.style.cursor = 'pointer';
192
- pill.addEventListener('click', p.onClick);
181
+ function renderInto(wrap) {
182
+ if (!wrap) return;
183
+ wrap.innerHTML = '';
184
+ for (const p of pills) {
185
+ const d = data[p.key];
186
+ if (!d || typeof d.amount !== 'number') continue;
187
+ const pill = document.createElement('span');
188
+ pill.className = 'balance-info';
189
+ pill.title = `Баланс ${p.label}` + (p.onClick ? ' · клик — лог списаний' : '');
190
+ if (d.amount > 0 && d.amount < (p.low || 0)) pill.classList.add('low');
191
+ if (d.amount <= 0) pill.classList.add('empty');
192
+ pill.innerHTML = `<span class="dot"></span><span style="color:#888;font-size:10px;margin-right:4px;">${p.label}</span><span>${p.fmt(d.amount)}</span>`;
193
+ if (p.onClick) {
194
+ pill.style.cursor = 'pointer';
195
+ pill.addEventListener('click', p.onClick);
196
+ }
197
+ wrap.appendChild(pill);
193
198
  }
194
- wrap.appendChild(pill);
195
199
  }
200
+ // Sidebar-footer (когда открыт проект).
201
+ renderInto(document.getElementById('balancesAll'));
202
+ // Welcome top-right (когда no-project).
203
+ renderInto(document.getElementById('welcomeStatusBalances'));
196
204
  }
197
205
  window.refreshBalance = refreshBalance;
198
206
 
207
+ // Identity-pill в правом верхнем углу welcome-экрана.
208
+ // Показывает кто залогинен в KingKont (или предлагает войти).
209
+ async function renderWelcomeIdentity() {
210
+ const wrap = document.getElementById('welcomeStatusIdentity');
211
+ if (!wrap) return;
212
+ wrap.innerHTML = '';
213
+ let status = null;
214
+ try { status = await window.appChatium?.status?.(); } catch {}
215
+ if (status?.connected) {
216
+ // Имя: предпочитаем display-name (login/name из auth~me), fallback — userId.
217
+ const name = status.login || status.name || status.userId || 'KingKont';
218
+ const sub = status.userId && status.userId !== name ? `· ${status.userId.slice(0, 8)}` : '';
219
+ wrap.innerHTML = `
220
+ <span style="color:#5c5; font-size:13px; line-height:1;">●</span>
221
+ <span class="who">${escapeHtml(name)}</span>
222
+ <span class="who-sub">${escapeHtml(sub)}</span>
223
+ `;
224
+ const logoutBtn = document.createElement('button');
225
+ logoutBtn.textContent = 'Выйти';
226
+ logoutBtn.title = 'Logout из KingKont';
227
+ logoutBtn.addEventListener('click', async () => {
228
+ if (!confirm('Выйти из KingKont?')) return;
229
+ try { await window.appChatium?.logout?.(); } catch {}
230
+ });
231
+ wrap.appendChild(logoutBtn);
232
+ } else {
233
+ wrap.innerHTML = `
234
+ <span style="color:#888; font-size:13px; line-height:1;">●</span>
235
+ <span class="who" style="color:#aaa;">Не залогинен</span>
236
+ `;
237
+ const loginBtn = document.createElement('button');
238
+ loginBtn.textContent = 'Войти';
239
+ loginBtn.title = 'Войти в KingKont';
240
+ loginBtn.addEventListener('click', async () => {
241
+ try { await window.appChatium?.login?.(); } catch (e) { alert('Login failed: ' + (e?.message || e)); }
242
+ });
243
+ wrap.appendChild(loginBtn);
244
+ }
245
+ }
246
+ window.renderWelcomeIdentity = renderWelcomeIdentity;
247
+ function escapeHtml(s) {
248
+ return String(s || '').replace(/[&<>"']/g, c => ({
249
+ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;',
250
+ }[c]));
251
+ }
252
+
199
253
  // === Лог списаний (модал с историей кредитов) ===
200
254
  async function openTxLog() {
201
255
  const modal = document.getElementById('txLogModal');
@@ -397,6 +451,8 @@ async function renderWelcomeRecents() {
397
451
  if (!grid || !wrap) return;
398
452
  grid.innerHTML = '';
399
453
  wrap.style.display = '';
454
+ // Identity-pill в правом верхнем — обновляем при каждом показе welcome.
455
+ renderWelcomeIdentity().catch(() => {});
400
456
  const list = await getRecents();
401
457
 
402
458
  // Первой картой — «Открыть проект». Кликается → дёргает скрытый
@@ -213,6 +213,40 @@
213
213
  vertical-align: middle; margin-left: 6px;
214
214
  }
215
215
  .welcome-sub { font-size: 12px; color: #888; letter-spacing: 0.5px; text-transform: uppercase; }
216
+ /* Топ-правый блок welcome-экрана: identity + balances. Положение fixed,
217
+ чтобы не зависеть от центрированной .welcome-inner колонки. */
218
+ .welcome-status {
219
+ position: fixed; top: 16px; right: 24px; z-index: 60;
220
+ display: flex; flex-direction: column; align-items: flex-end; gap: 6px;
221
+ pointer-events: none; /* контейнер прозрачен, дочерние сами включают pointer-events */
222
+ }
223
+ .welcome-status-identity {
224
+ pointer-events: auto;
225
+ display: flex; align-items: center; gap: 8px;
226
+ background: rgba(30, 32, 40, 0.7); border: 1px solid #333;
227
+ border-radius: 999px; padding: 4px 10px;
228
+ font-size: 12px; color: #ccc;
229
+ backdrop-filter: blur(6px);
230
+ }
231
+ .welcome-status-identity .who {
232
+ color: #e0e0e0; font-weight: 500; max-width: 240px;
233
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
234
+ }
235
+ .welcome-status-identity .who-sub {
236
+ color: #888; font-size: 10px;
237
+ font-family: ui-monospace, 'SF Mono', monospace;
238
+ }
239
+ .welcome-status-identity button {
240
+ background: transparent; border: none; color: #9ab;
241
+ font-size: 11px; cursor: pointer; padding: 2px 6px; border-radius: 4px;
242
+ }
243
+ .welcome-status-identity button:hover { background: rgba(255,255,255,0.06); color: #cde; }
244
+ .welcome-status-balances {
245
+ pointer-events: auto;
246
+ display: flex; flex-direction: row; gap: 6px; flex-wrap: wrap;
247
+ justify-content: flex-end;
248
+ }
249
+ /* Используем те же .balance-info pill'ы что и в sidebar-footer'е. */
216
250
  .welcome-open {
217
251
  margin-top: 16px;
218
252
  padding: 12px 28px; font-size: 15px; font-weight: 600;