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 +7 -0
- package/main.js +77 -5
- package/package.json +1 -1
- package/renderer/board.js +74 -18
- package/renderer/styles.css +34 -0
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
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
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
|
-
//
|
|
54
|
-
//
|
|
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> chars` },
|
|
180
180
|
];
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
pill.
|
|
192
|
-
pill.
|
|
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
|
+
'&':'&','<':'<','>':'>','"':'"',"'":''',
|
|
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
|
// Первой картой — «Открыть проект». Кликается → дёргает скрытый
|
package/renderer/styles.css
CHANGED
|
@@ -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;
|