kingkont 0.7.3 → 0.7.5
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 +56 -5
- package/package.json +28 -9
- package/server.js +45 -2
- package/updates.html +205 -0
package/index.html
CHANGED
|
@@ -1343,7 +1343,15 @@
|
|
|
1343
1343
|
</div>
|
|
1344
1344
|
</div>
|
|
1345
1345
|
</label>
|
|
1346
|
-
<label id="
|
|
1346
|
+
<label id="ttsModelRow" style="display: none;">Модель TTS
|
|
1347
|
+
<div class="seg-control" style="flex-wrap:wrap;">
|
|
1348
|
+
<button class="seg active" data-tts-model="qwen/qwen3-tts" type="button" title="Qwen TTS — мульти-язык, ready-голоса">Qwen TTS</button>
|
|
1349
|
+
<button class="seg" data-tts-model="elevenlabs/v3" type="button" title="ElevenLabs v3 — лучший EN, тоны">ElevenLabs v3</button>
|
|
1350
|
+
<button class="seg" data-tts-model="minimax/speech-02-hd" type="button" title="MiniMax Speech HD — клон-голоса">MiniMax Speech HD</button>
|
|
1351
|
+
<button class="seg" data-tts-model="google/gemini-3.1-flash-tts-preview" type="button" title="Gemini 3.1 Flash TTS">Gemini Flash TTS</button>
|
|
1352
|
+
</div>
|
|
1353
|
+
</label>
|
|
1354
|
+
<label id="voiceRow" style="display: none;">Голос
|
|
1347
1355
|
<select id="genVoice"></select>
|
|
1348
1356
|
</label>
|
|
1349
1357
|
<label id="tonesRow" style="display: none;">Тоны
|
|
@@ -1729,6 +1737,7 @@ const state = {
|
|
|
1729
1737
|
genKind: 'image',
|
|
1730
1738
|
imageModel: 'nano-banana-2', // 'nano-banana-2' | 'grok' | ...
|
|
1731
1739
|
videoModel: localStorage.getItem('videoModel') || 'seedance-2', // 'seedance-2' | 'kling-o1' | 'kling-3.0' | ...
|
|
1740
|
+
ttsModel: localStorage.getItem('ttsModel') || 'qwen/qwen3-tts', // qwen/elevenlabs/v3/minimax/speech-02-hd/gemini
|
|
1732
1741
|
videoDuration: +(localStorage.getItem('videoDuration') || 5),
|
|
1733
1742
|
videoResolution: localStorage.getItem('videoResolution') || '720p',
|
|
1734
1743
|
videoAspect: localStorage.getItem('videoAspect') || '9:16',
|
|
@@ -3760,6 +3769,8 @@ async function openGenerateForRef(fromNode, clientX, clientY, forceKind) {
|
|
|
3760
3769
|
|
|
3761
3770
|
$('videoModelRow').style.display = forceKind === 'video' ? '' : 'none';
|
|
3762
3771
|
$('voiceRow').style.display = forceKind === 'audio' ? '' : 'none';
|
|
3772
|
+
|
|
3773
|
+
$('ttsModelRow').style.display = forceKind === 'audio' ? '' : 'none';
|
|
3763
3774
|
$('tonesRow').style.display = forceKind === 'audio' ? '' : 'none';
|
|
3764
3775
|
const titleEl = $('genTitle');
|
|
3765
3776
|
if (titleEl) {
|
|
@@ -5013,6 +5024,8 @@ async function regenerateNode(node) {
|
|
|
5013
5024
|
$('videoModelRow').style.display = state.genKind === 'video' ? '' : 'none';
|
|
5014
5025
|
$('voiceRow').style.display = state.genKind === 'audio' ? '' : 'none';
|
|
5015
5026
|
|
|
5027
|
+
$('ttsModelRow').style.display = state.genKind === 'audio' ? '' : 'none';
|
|
5028
|
+
|
|
5016
5029
|
if (g.modelKey && state.genKind === 'image') {
|
|
5017
5030
|
state.imageModel = g.modelKey;
|
|
5018
5031
|
document.querySelectorAll('#genModal [data-img-model]').forEach(b =>
|
|
@@ -5028,7 +5041,9 @@ async function regenerateNode(node) {
|
|
|
5028
5041
|
syncVideoModelActive();
|
|
5029
5042
|
}
|
|
5030
5043
|
if (state.genKind === 'audio') {
|
|
5031
|
-
|
|
5044
|
+
if (g.ttsModel) state.ttsModel = g.ttsModel;
|
|
5045
|
+
syncTtsModelActive();
|
|
5046
|
+
if (state.ttsModel === 'elevenlabs/v3') await loadVoices();
|
|
5032
5047
|
if (g.voiceId) $('genVoice').value = g.voiceId;
|
|
5033
5048
|
state.activeTones = (g.tones || []).slice();
|
|
5034
5049
|
state.toneSuggestions = (g.tones || []).slice();
|
|
@@ -5175,6 +5190,7 @@ async function regenerateInto(node, kind, rawPrompt, opts = {}) {
|
|
|
5175
5190
|
|
|
5176
5191
|
const seedGen = kind === 'audio'
|
|
5177
5192
|
? { kind, prompt: resolvedPrompt, rawPrompt, model: modelId, voiceId, voiceName,
|
|
5193
|
+
ttsModel: state.ttsModel || node.generated?.ttsModel || 'qwen/qwen3-tts',
|
|
5178
5194
|
tones: [...state.activeTones], state: 'submitting' }
|
|
5179
5195
|
: { kind, prompt: resolvedPrompt, rawPrompt, modelKey, model: modelId,
|
|
5180
5196
|
refs: refs ? refs.map(r => ({ name: r.name, type: r.type, file: r.file })) : [],
|
|
@@ -5961,6 +5977,8 @@ function openPhraseFor(charInfo) {
|
|
|
5961
5977
|
|
|
5962
5978
|
$('videoModelRow').style.display = 'none';
|
|
5963
5979
|
$('voiceRow').style.display = '';
|
|
5980
|
+
|
|
5981
|
+
$('ttsModelRow').style.display = '';
|
|
5964
5982
|
$('tonesRow').style.display = '';
|
|
5965
5983
|
loadVoices().then(() => {
|
|
5966
5984
|
if (charInfo.voice) $('genVoice').value = charInfo.voice;
|
|
@@ -6142,6 +6160,8 @@ async function openGenModal(kind) {
|
|
|
6142
6160
|
|
|
6143
6161
|
$('videoModelRow').style.display = kind === 'video' ? '' : 'none';
|
|
6144
6162
|
$('voiceRow').style.display = kind === 'audio' ? '' : 'none';
|
|
6163
|
+
|
|
6164
|
+
$('ttsModelRow').style.display = kind === 'audio' ? '' : 'none';
|
|
6145
6165
|
$('tonesRow').style.display = kind === 'audio' ? '' : 'none';
|
|
6146
6166
|
// Заголовок модалки = действие
|
|
6147
6167
|
const title = $('genTitle');
|
|
@@ -6562,8 +6582,14 @@ document.querySelectorAll('#genModal [data-kind]').forEach(b => {
|
|
|
6562
6582
|
|
|
6563
6583
|
$('videoModelRow').style.display = state.genKind === 'video' ? '' : 'none';
|
|
6564
6584
|
$('voiceRow').style.display = state.genKind === 'audio' ? '' : 'none';
|
|
6585
|
+
|
|
6586
|
+
$('ttsModelRow').style.display = state.genKind === 'audio' ? '' : 'none';
|
|
6565
6587
|
$('tonesRow').style.display = state.genKind === 'audio' ? '' : 'none';
|
|
6566
|
-
if (state.genKind === 'audio') {
|
|
6588
|
+
if (state.genKind === 'audio') {
|
|
6589
|
+
syncTtsModelActive();
|
|
6590
|
+
if (state.ttsModel === 'elevenlabs/v3') loadVoices();
|
|
6591
|
+
renderTones();
|
|
6592
|
+
}
|
|
6567
6593
|
const ph = state.genKind === 'audio'
|
|
6568
6594
|
? 'Текст, который надо озвучить...'
|
|
6569
6595
|
: 'Что должно быть. Печатай @ чтобы вставить ссылку на ноду...';
|
|
@@ -6825,6 +6851,25 @@ document.querySelectorAll('#genModal [data-vid-model]').forEach(b => {
|
|
|
6825
6851
|
localStorage.setItem('videoModel', state.videoModel);
|
|
6826
6852
|
});
|
|
6827
6853
|
});
|
|
6854
|
+
// Переключатель модели TTS
|
|
6855
|
+
document.querySelectorAll('#genModal [data-tts-model]').forEach(b => {
|
|
6856
|
+
b.addEventListener('click', () => {
|
|
6857
|
+
document.querySelectorAll('#genModal [data-tts-model]').forEach(x => x.classList.remove('active'));
|
|
6858
|
+
b.classList.add('active');
|
|
6859
|
+
state.ttsModel = b.dataset.ttsModel;
|
|
6860
|
+
localStorage.setItem('ttsModel', state.ttsModel);
|
|
6861
|
+
// voiceRow с ElevenLabs-голосами имеет смысл только для elevenlabs/v3.
|
|
6862
|
+
const showVoice = state.ttsModel === 'elevenlabs/v3';
|
|
6863
|
+
$('voiceRow').style.display = showVoice ? '' : 'none';
|
|
6864
|
+
});
|
|
6865
|
+
});
|
|
6866
|
+
function syncTtsModelActive() {
|
|
6867
|
+
document.querySelectorAll('#genModal [data-tts-model]').forEach(b =>
|
|
6868
|
+
b.classList.toggle('active', b.dataset.ttsModel === state.ttsModel));
|
|
6869
|
+
// Скрыть voiceRow если модель не elevenlabs (только для неё имеет смысл список).
|
|
6870
|
+
const showVoice = state.ttsModel === 'elevenlabs/v3';
|
|
6871
|
+
$('voiceRow').style.display = showVoice ? '' : 'none';
|
|
6872
|
+
}
|
|
6828
6873
|
// Подсветить активную video-модель при открытии modal'а
|
|
6829
6874
|
function syncVideoModelActive() {
|
|
6830
6875
|
document.querySelectorAll('#genModal [data-vid-model]').forEach(b =>
|
|
@@ -6956,6 +7001,7 @@ $('genSubmit').addEventListener('click', async () => {
|
|
|
6956
7001
|
kind: 'audio',
|
|
6957
7002
|
prompt: finalText, rawPrompt,
|
|
6958
7003
|
model: 'eleven_v3', voiceId, voiceName,
|
|
7004
|
+
ttsModel: state.ttsModel || 'qwen/qwen3-tts',
|
|
6959
7005
|
tones: [...state.activeTones],
|
|
6960
7006
|
},
|
|
6961
7007
|
};
|
|
@@ -7116,11 +7162,14 @@ async function runTTSJob(node, text, boardHandle, bKey, voiceId) {
|
|
|
7116
7162
|
n.generated = { ...(n.generated || {}), state: 'submitting' };
|
|
7117
7163
|
});
|
|
7118
7164
|
const provider = await plannedProvider('tts');
|
|
7119
|
-
|
|
7165
|
+
// ttsModel может быть сохранён в node.generated.ttsModel (при regenerate)
|
|
7166
|
+
// или в текущем глобальном state.ttsModel (новая генерация).
|
|
7167
|
+
const ttsModel = node.generated?.ttsModel || state.ttsModel || 'qwen/qwen3-tts';
|
|
7168
|
+
logJob(node.id, `→ POST /api/tts → ${provider} (model=${ttsModel} voice=${voiceId || '—'})`);
|
|
7120
7169
|
const r = await fetch('/api/tts', {
|
|
7121
7170
|
method: 'POST',
|
|
7122
7171
|
headers: { 'Content-Type': 'application/json' },
|
|
7123
|
-
body: JSON.stringify({ text, voiceId,
|
|
7172
|
+
body: JSON.stringify({ text, voiceId, ttsModel }),
|
|
7124
7173
|
});
|
|
7125
7174
|
logJob(node.id, `← via ${r.headers.get('x-provider') || '?'} HTTP ${r.status}`);
|
|
7126
7175
|
if (!r.ok) {
|
|
@@ -7785,6 +7834,8 @@ async function openGenAudioForTimeline(charInfo, track, time) {
|
|
|
7785
7834
|
|
|
7786
7835
|
$('videoModelRow').style.display = 'none';
|
|
7787
7836
|
$('voiceRow').style.display = '';
|
|
7837
|
+
|
|
7838
|
+
$('ttsModelRow').style.display = '';
|
|
7788
7839
|
$('tonesRow').style.display = '';
|
|
7789
7840
|
$('sourceRefRow').style.display = 'none';
|
|
7790
7841
|
$('charsPickRow').style.display = 'none';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kingkont",
|
|
3
|
-
"version": "0.7.
|
|
4
|
-
"description": "KingKont
|
|
3
|
+
"version": "0.7.5",
|
|
4
|
+
"description": "KingKont \u00b7 Chatium \u2014 \u043d\u043e\u0434-\u0440\u0435\u0434\u0430\u043a\u0442\u043e\u0440 \u0441\u0446\u0435\u043d \u0441 AI-\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0435\u0439 (\u043a\u0430\u0440\u0442\u0438\u043d\u043a\u0438/\u0432\u0438\u0434\u0435\u043e/\u0433\u043e\u043b\u043e\u0441/SFX/\u043c\u0443\u0437\u044b\u043a\u0430/\u0442\u0435\u043a\u0441\u0442)",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"kingkont": "bin/kingkont.js"
|
|
@@ -16,12 +16,21 @@
|
|
|
16
16
|
"bin/**/*",
|
|
17
17
|
"scripts/**/*",
|
|
18
18
|
"skill/**/*",
|
|
19
|
-
"README.md"
|
|
19
|
+
"README.md",
|
|
20
|
+
"updates.html"
|
|
20
21
|
],
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=18"
|
|
23
24
|
},
|
|
24
|
-
"keywords": [
|
|
25
|
+
"keywords": [
|
|
26
|
+
"video-editor",
|
|
27
|
+
"ai",
|
|
28
|
+
"kie",
|
|
29
|
+
"elevenlabs",
|
|
30
|
+
"openrouter",
|
|
31
|
+
"scene-editor",
|
|
32
|
+
"electron"
|
|
33
|
+
],
|
|
25
34
|
"author": "Tim",
|
|
26
35
|
"license": "UNLICENSED",
|
|
27
36
|
"scripts": {
|
|
@@ -47,17 +56,27 @@
|
|
|
47
56
|
"index.html",
|
|
48
57
|
"settings.html",
|
|
49
58
|
"assets/**/*",
|
|
50
|
-
"package.json"
|
|
59
|
+
"package.json",
|
|
60
|
+
"updates.html"
|
|
51
61
|
],
|
|
52
62
|
"extraResources": [
|
|
53
|
-
{
|
|
63
|
+
{
|
|
64
|
+
"from": ".env",
|
|
65
|
+
"to": ".env"
|
|
66
|
+
}
|
|
54
67
|
],
|
|
55
68
|
"mac": {
|
|
56
69
|
"category": "public.app-category.video",
|
|
57
70
|
"target": "dmg",
|
|
58
71
|
"icon": "assets/logo.icns"
|
|
59
72
|
},
|
|
60
|
-
"win": {
|
|
61
|
-
|
|
73
|
+
"win": {
|
|
74
|
+
"target": "nsis",
|
|
75
|
+
"icon": "assets/logo-1024.png"
|
|
76
|
+
},
|
|
77
|
+
"linux": {
|
|
78
|
+
"target": "AppImage",
|
|
79
|
+
"icon": "assets/logo-1024.png"
|
|
80
|
+
}
|
|
62
81
|
}
|
|
63
|
-
}
|
|
82
|
+
}
|
package/server.js
CHANGED
|
@@ -743,19 +743,62 @@ async function handleMusic(req, res) {
|
|
|
743
743
|
}
|
|
744
744
|
|
|
745
745
|
// ---------- /api/tts (Chatium ИЛИ ElevenLabs v3) ----------
|
|
746
|
+
// Body клиента (всё опц. кроме text):
|
|
747
|
+
// {
|
|
748
|
+
// text,
|
|
749
|
+
// ttsModel?: 'qwen/qwen3-tts' | 'elevenlabs/v3' | 'minimax/speech-02-hd'
|
|
750
|
+
// | 'google/gemini-3.1-flash-tts-preview',
|
|
751
|
+
// voice?: string, // voiceId/speaker зависит от модели
|
|
752
|
+
// voiceId?: string, // legacy alias для voice (ElevenLabs)
|
|
753
|
+
// modelId?: string, // legacy: 'eleven_v3' и т.д.
|
|
754
|
+
// // Любые per-provider параметры пробрасываются в Chatium как есть:
|
|
755
|
+
// stability, similarity_boost, style, speed, language_code,
|
|
756
|
+
// pitch, volume, emotion, sample_rate, audio_format, language_boost,
|
|
757
|
+
// mode, speaker, voice_description, style_instruction, ...
|
|
758
|
+
// }
|
|
759
|
+
const TTS_PASSTHROUGH = new Set([
|
|
760
|
+
'voice', 'voiceId', 'voice_id', 'speaker',
|
|
761
|
+
'language', 'language_code', 'language_boost',
|
|
762
|
+
'speed', 'pitch', 'volume',
|
|
763
|
+
'stability', 'similarity_boost', 'style', 'style_instruction',
|
|
764
|
+
'audio_format', 'sample_rate', 'bitrate', 'channel', 'emotion',
|
|
765
|
+
'subtitle_enable', 'english_normalization',
|
|
766
|
+
'voice_description', 'reference_audio', 'reference_text', 'mode',
|
|
767
|
+
'previous_text', 'next_text',
|
|
768
|
+
]);
|
|
746
769
|
async function handleTts(req, res) {
|
|
747
|
-
const
|
|
770
|
+
const body = await readJson(req);
|
|
771
|
+
const text = body.text;
|
|
748
772
|
if (!text) return send(res, 400, { error: 'нужен text' });
|
|
749
773
|
const s = getSettings();
|
|
750
774
|
|
|
775
|
+
// Chatium-путь — поддерживает 4 модели (Qwen TTS, ElevenLabs v3,
|
|
776
|
+
// MiniMax Speech HD, Gemini 3.1 Flash TTS — см. spaces/api/execAudioNode.ts).
|
|
751
777
|
if (s.useChatium && s.chatium?.token && s.chatium?.base) {
|
|
752
|
-
|
|
778
|
+
const ttsBody = { kind: 'tts', text };
|
|
779
|
+
// Маппинг легаси `modelId` → `model` (старый клиент шлёт modelId='eleven_v3').
|
|
780
|
+
if (body.ttsModel) ttsBody.model = body.ttsModel;
|
|
781
|
+
else if (body.modelId === 'eleven_v3') ttsBody.model = 'elevenlabs/v3';
|
|
782
|
+
else if (body.modelId) ttsBody.model = body.modelId;
|
|
783
|
+
// voice: поддерживаем оба имени (voice или voiceId).
|
|
784
|
+
if (body.voice) ttsBody.voice = body.voice;
|
|
785
|
+
else if (body.voiceId) ttsBody.voice = body.voiceId;
|
|
786
|
+
// Per-model passthrough.
|
|
787
|
+
for (const k of Object.keys(body)) {
|
|
788
|
+
if (k === 'text' || k === 'voice' || k === 'voiceId' || k === 'ttsModel' || k === 'modelId') continue;
|
|
789
|
+
if (TTS_PASSTHROUGH.has(k)) ttsBody[k] = body[k];
|
|
790
|
+
}
|
|
791
|
+
return handleAudioViaChatium(res, s, ttsBody);
|
|
753
792
|
}
|
|
793
|
+
|
|
794
|
+
// Прямой ElevenLabs (только eleven_v3, остальные модели только через Chatium).
|
|
754
795
|
if (!s.useElevenlabs) {
|
|
755
796
|
return send(res, 503, { error: 'Аудио-коннектор отключён. Включите Chatium или ElevenLabs.' });
|
|
756
797
|
}
|
|
757
798
|
const key = process.env.ELEVENLABS_API_KEY;
|
|
758
799
|
if (!key) return send(res, 500, { error: 'ELEVENLABS_API_KEY не задан' });
|
|
800
|
+
const voiceId = body.voiceId || body.voice || 'JBFqnCBsd6RMkjVDRZzb';
|
|
801
|
+
const modelId = body.modelId || 'eleven_v3';
|
|
759
802
|
logProviderCall('POST', 'ElevenLabs', `${ELEVEN_BASE}/v1/text-to-speech/${voiceId}`, `model=${modelId} text=${text.length}ch`);
|
|
760
803
|
const r = await fetch(`${ELEVEN_BASE}/v1/text-to-speech/${voiceId}`, {
|
|
761
804
|
method: 'POST',
|
package/updates.html
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ru">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>Обновления</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
--bg: #1a1a1a;
|
|
10
|
+
--card: #232323;
|
|
11
|
+
--border: #333;
|
|
12
|
+
--text: #e0e0e0;
|
|
13
|
+
--muted: #888;
|
|
14
|
+
--accent: #7c3aed;
|
|
15
|
+
--accent-hover: #6d28d9;
|
|
16
|
+
--ok: #16a34a;
|
|
17
|
+
}
|
|
18
|
+
* { box-sizing: border-box; }
|
|
19
|
+
html, body { height: 100%; }
|
|
20
|
+
body {
|
|
21
|
+
margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
22
|
+
background: var(--bg); color: var(--text); font-size: 13px;
|
|
23
|
+
-webkit-app-region: drag;
|
|
24
|
+
}
|
|
25
|
+
.wrap { padding: 24px 28px; max-height: 100vh; overflow: auto; -webkit-app-region: no-drag; }
|
|
26
|
+
h1 { margin: 0 0 4px 0; font-size: 18px; font-weight: 600; }
|
|
27
|
+
.sub { color: var(--muted); font-size: 12px; margin-bottom: 18px; }
|
|
28
|
+
.card { background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 16px; }
|
|
29
|
+
|
|
30
|
+
.versions { display: flex; gap: 24px; margin-bottom: 14px; }
|
|
31
|
+
.versions .v { flex: 1; }
|
|
32
|
+
.versions .v label { display: block; color: var(--muted); font-size: 11px; margin-bottom: 4px; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
33
|
+
.versions .v .num { font-size: 18px; font-family: ui-monospace, monospace; color: var(--text); }
|
|
34
|
+
.badge { display: inline-block; margin-left: 6px; padding: 2px 8px; font-size: 10px;
|
|
35
|
+
border-radius: 999px; vertical-align: middle; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
36
|
+
.badge.ok { background: rgba(22,163,74,0.15); color: var(--ok); border: 1px solid rgba(22,163,74,0.4); }
|
|
37
|
+
.badge.new { background: rgba(124,58,237,0.18); color: #a78bfa; border: 1px solid rgba(124,58,237,0.5); }
|
|
38
|
+
|
|
39
|
+
.install-cmd {
|
|
40
|
+
background: #0e0e0e; border: 1px solid #2a2a2a; border-radius: 6px;
|
|
41
|
+
padding: 10px 14px; font-family: ui-monospace, monospace; font-size: 12px;
|
|
42
|
+
color: #aae06a; display: flex; align-items: center; gap: 10px; margin-top: 8px;
|
|
43
|
+
}
|
|
44
|
+
.install-cmd code { flex: 1; }
|
|
45
|
+
.install-cmd .copy {
|
|
46
|
+
padding: 4px 10px; font-size: 11px; background: #2a2a2a; color: var(--text);
|
|
47
|
+
border: 1px solid #3a3a3a; border-radius: 4px; cursor: pointer;
|
|
48
|
+
}
|
|
49
|
+
.install-cmd .copy:hover { background: #333; }
|
|
50
|
+
|
|
51
|
+
.changelog { font-size: 12px; color: #c4c4c4; line-height: 1.6; max-height: 240px; overflow-y: auto;
|
|
52
|
+
margin-top: 12px; padding: 10px 14px; background: #1c1c1c; border-radius: 6px; border: 1px solid #2a2a2a; }
|
|
53
|
+
.changelog .item { padding: 4px 0; }
|
|
54
|
+
.changelog .item .v { color: var(--muted); font-family: ui-monospace, monospace; margin-right: 6px; }
|
|
55
|
+
|
|
56
|
+
.actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
|
|
57
|
+
button {
|
|
58
|
+
padding: 7px 14px; font-size: 13px; font-weight: 500; cursor: pointer;
|
|
59
|
+
border: 0; border-radius: 6px; transition: background 0.15s;
|
|
60
|
+
}
|
|
61
|
+
button.primary { background: var(--accent); color: #fff; }
|
|
62
|
+
button.primary:hover { background: var(--accent-hover); }
|
|
63
|
+
button.secondary { background: #333; color: var(--text); }
|
|
64
|
+
button.secondary:hover { background: #444; }
|
|
65
|
+
button:disabled { opacity: 0.5; cursor: wait; }
|
|
66
|
+
|
|
67
|
+
.spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid #333;
|
|
68
|
+
border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite;
|
|
69
|
+
vertical-align: middle; margin-right: 6px; }
|
|
70
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
71
|
+
|
|
72
|
+
.error { color: #ef4444; font-size: 12px; margin-top: 12px; }
|
|
73
|
+
</style>
|
|
74
|
+
</head>
|
|
75
|
+
<body>
|
|
76
|
+
<div class="wrap">
|
|
77
|
+
<h1>Обновления KingKont</h1>
|
|
78
|
+
<div class="sub">Источник: <code>registry.npmjs.org/kingkont</code></div>
|
|
79
|
+
|
|
80
|
+
<div class="card">
|
|
81
|
+
<div class="versions">
|
|
82
|
+
<div class="v">
|
|
83
|
+
<label>Установлено</label>
|
|
84
|
+
<div class="num" id="vCurrent">—</div>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="v">
|
|
87
|
+
<label>Доступно</label>
|
|
88
|
+
<div class="num" id="vLatest"><span class="spinner"></span></div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
<div id="updateBlock" style="display:none;">
|
|
92
|
+
<div style="display:flex; gap:8px; align-items:center; margin-top:6px;">
|
|
93
|
+
<button class="primary" id="installBtn" style="padding:8px 16px;">⬇ Установить и перезапустить</button>
|
|
94
|
+
<span style="color:#888; font-size:11px;">или вручную:</span>
|
|
95
|
+
</div>
|
|
96
|
+
<div class="install-cmd">
|
|
97
|
+
<code id="installCmd">npm i -g kingkont@latest</code>
|
|
98
|
+
<button class="copy" id="copyBtn">Скопировать</button>
|
|
99
|
+
</div>
|
|
100
|
+
<pre id="installLog" style="display:none; max-height:160px; overflow-y:auto; margin-top:10px;
|
|
101
|
+
font-family: ui-monospace, monospace; font-size: 11px; line-height: 1.4;
|
|
102
|
+
background: #0e0e0e; border: 1px solid #2a2a2a; border-radius: 4px;
|
|
103
|
+
padding: 8px 10px; color: #aaa; white-space: pre-wrap; word-break: break-all;"></pre>
|
|
104
|
+
<div id="installDone" style="display:none; color: var(--ok); margin-top: 10px; font-size: 12px;">
|
|
105
|
+
✓ Установлено! Можно перезапустить:
|
|
106
|
+
<button class="primary" id="relaunchBtn" style="padding:5px 12px; font-size:12px; margin-left:6px;">Перезапустить</button>
|
|
107
|
+
</div>
|
|
108
|
+
</div>
|
|
109
|
+
<div id="upToDate" style="display:none; color: var(--ok); font-size: 12px; margin-top: 6px;">
|
|
110
|
+
✓ У вас последняя версия.
|
|
111
|
+
</div>
|
|
112
|
+
<div class="error" id="errMsg" style="display:none;"></div>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="actions">
|
|
116
|
+
<button class="secondary" id="recheckBtn">Проверить ещё раз</button>
|
|
117
|
+
<button class="primary" id="closeBtn">Закрыть</button>
|
|
118
|
+
</div>
|
|
119
|
+
</div>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
const $ = (id) => document.getElementById(id);
|
|
123
|
+
|
|
124
|
+
async function check() {
|
|
125
|
+
$('errMsg').style.display = 'none';
|
|
126
|
+
$('updateBlock').style.display = 'none';
|
|
127
|
+
$('upToDate').style.display = 'none';
|
|
128
|
+
$('vLatest').innerHTML = '<span class="spinner"></span>';
|
|
129
|
+
$('recheckBtn').disabled = true;
|
|
130
|
+
try {
|
|
131
|
+
const r = await window.appUpdates.check();
|
|
132
|
+
$('vCurrent').textContent = r.current;
|
|
133
|
+
$('vLatest').textContent = r.latest;
|
|
134
|
+
if (r.isNew) {
|
|
135
|
+
$('vLatest').insertAdjacentHTML('beforeend', '<span class="badge new">новая версия</span>');
|
|
136
|
+
$('updateBlock').style.display = '';
|
|
137
|
+
$('installCmd').textContent = `npm i -g kingkont@${r.latest}`;
|
|
138
|
+
} else {
|
|
139
|
+
$('vLatest').insertAdjacentHTML('beforeend', '<span class="badge ok">актуально</span>');
|
|
140
|
+
$('upToDate').style.display = '';
|
|
141
|
+
}
|
|
142
|
+
} catch (e) {
|
|
143
|
+
$('vLatest').textContent = '—';
|
|
144
|
+
$('errMsg').style.display = '';
|
|
145
|
+
$('errMsg').textContent = 'Не удалось проверить: ' + (e?.message || String(e));
|
|
146
|
+
} finally {
|
|
147
|
+
$('recheckBtn').disabled = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
$('recheckBtn').addEventListener('click', check);
|
|
152
|
+
$('closeBtn').addEventListener('click', () => window.appUpdates.closeWindow());
|
|
153
|
+
$('copyBtn').addEventListener('click', async () => {
|
|
154
|
+
const cmd = $('installCmd').textContent;
|
|
155
|
+
try {
|
|
156
|
+
await navigator.clipboard.writeText(cmd);
|
|
157
|
+
const b = $('copyBtn');
|
|
158
|
+
const orig = b.textContent;
|
|
159
|
+
b.textContent = 'Скопировано';
|
|
160
|
+
setTimeout(() => { b.textContent = orig; }, 1500);
|
|
161
|
+
} catch {}
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
let _unsubLog = null;
|
|
165
|
+
$('installBtn').addEventListener('click', async () => {
|
|
166
|
+
const installBtn = $('installBtn');
|
|
167
|
+
const recheckBtn = $('recheckBtn');
|
|
168
|
+
const log = $('installLog');
|
|
169
|
+
const done = $('installDone');
|
|
170
|
+
installBtn.disabled = true; recheckBtn.disabled = true;
|
|
171
|
+
installBtn.textContent = 'Устанавливаю…';
|
|
172
|
+
done.style.display = 'none';
|
|
173
|
+
log.style.display = '';
|
|
174
|
+
log.textContent = '';
|
|
175
|
+
// Подписываемся на стрим вывода npm.
|
|
176
|
+
_unsubLog?.();
|
|
177
|
+
_unsubLog = window.appUpdates.onInstallOutput(({ stream, text }) => {
|
|
178
|
+
log.textContent += text;
|
|
179
|
+
log.scrollTop = log.scrollHeight;
|
|
180
|
+
if (stream === 'stderr') log.style.color = '#f88';
|
|
181
|
+
});
|
|
182
|
+
// Берём latest из текущей надписи (или пытаемся 'latest').
|
|
183
|
+
const targetMatch = $('installCmd').textContent.match(/kingkont@(\S+)/);
|
|
184
|
+
const target = targetMatch ? targetMatch[1] : 'latest';
|
|
185
|
+
try {
|
|
186
|
+
await window.appUpdates.install(target);
|
|
187
|
+
done.style.display = '';
|
|
188
|
+
installBtn.textContent = '✓ Установлено';
|
|
189
|
+
} catch (e) {
|
|
190
|
+
log.style.color = '#f88';
|
|
191
|
+
log.textContent += `\n[error] ${e?.message || String(e)}`;
|
|
192
|
+
installBtn.disabled = false;
|
|
193
|
+
installBtn.textContent = '⬇ Попробовать снова';
|
|
194
|
+
} finally {
|
|
195
|
+
recheckBtn.disabled = false;
|
|
196
|
+
_unsubLog?.();
|
|
197
|
+
_unsubLog = null;
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
$('relaunchBtn').addEventListener('click', () => window.appUpdates.relaunch());
|
|
201
|
+
|
|
202
|
+
check();
|
|
203
|
+
</script>
|
|
204
|
+
</body>
|
|
205
|
+
</html>
|