kingkont 0.2.0
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/README.md +71 -0
- package/assets/PROJECT_CLAUDE.md +149 -0
- package/assets/logo-1024.png +0 -0
- package/assets/logo-256.png +0 -0
- package/assets/logo-512.png +0 -0
- package/assets/logo-square.svg +53 -0
- package/assets/logo.icns +0 -0
- package/assets/logo.iconset/icon_1024x1024.png +0 -0
- package/assets/logo.iconset/icon_128x128.png +0 -0
- package/assets/logo.iconset/icon_128x128@2x.png +0 -0
- package/assets/logo.iconset/icon_16x16.png +0 -0
- package/assets/logo.iconset/icon_16x16@2x.png +0 -0
- package/assets/logo.iconset/icon_256x256.png +0 -0
- package/assets/logo.iconset/icon_256x256@2x.png +0 -0
- package/assets/logo.iconset/icon_32x32.png +0 -0
- package/assets/logo.iconset/icon_32x32@2x.png +0 -0
- package/assets/logo.iconset/icon_512x512.png +0 -0
- package/assets/logo.iconset/icon_512x512@2x.png +0 -0
- package/assets/logo.iconset/icon_64x64.png +0 -0
- package/assets/logo.svg +53 -0
- package/bin/kingkont.js +88 -0
- package/index.html +9465 -0
- package/main.js +356 -0
- package/package.json +60 -0
- package/preload.js +51 -0
- package/scripts/patch-electron-name.sh +70 -0
- package/server.js +427 -0
package/main.js
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
// Electron main process: поднимает локальный сервер и открывает окно.
|
|
2
|
+
const { app, BrowserWindow, Menu, shell, ipcMain, session } = require('electron');
|
|
3
|
+
|
|
4
|
+
// Имя приложения для меню/about/Dock-tooltip — меняем как можно раньше,
|
|
5
|
+
// до старта меню и BrowserWindow.
|
|
6
|
+
app.setName('KingKont');
|
|
7
|
+
const { spawn } = require('node:child_process');
|
|
8
|
+
const path = require('node:path');
|
|
9
|
+
const fs = require('node:fs');
|
|
10
|
+
const { start } = require('./server');
|
|
11
|
+
|
|
12
|
+
let win = null;
|
|
13
|
+
let port = null;
|
|
14
|
+
|
|
15
|
+
// ===== Native player (опциональный, питон-процесс с mpv) =====
|
|
16
|
+
let nativeProc = null;
|
|
17
|
+
let nativeWsUrl = null;
|
|
18
|
+
function nativePlayerPaths() {
|
|
19
|
+
const dir = path.join(__dirname, 'native_player');
|
|
20
|
+
return {
|
|
21
|
+
dir,
|
|
22
|
+
py: path.join(dir, '.venv', 'bin', 'python'),
|
|
23
|
+
script: path.join(dir, 'player.py'),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function startNativePlayer() {
|
|
27
|
+
if (nativeProc && nativeWsUrl) return Promise.resolve(nativeWsUrl);
|
|
28
|
+
const { dir, py, script } = nativePlayerPaths();
|
|
29
|
+
if (!fs.existsSync(py) || !fs.existsSync(script)) {
|
|
30
|
+
return Promise.reject(new Error(
|
|
31
|
+
'native_player не настроен: запусти `bash native_player/setup.sh` (требуется brew install mpv и python3.12)'
|
|
32
|
+
));
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolveOk, reject) => {
|
|
35
|
+
const proc = spawn(py, [script, '--port', '0'], {
|
|
36
|
+
cwd: dir,
|
|
37
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
38
|
+
env: { ...process.env, PYTHONUNBUFFERED: '1' },
|
|
39
|
+
});
|
|
40
|
+
let resolved = false;
|
|
41
|
+
let stderrBuf = '';
|
|
42
|
+
const onLine = (line) => {
|
|
43
|
+
const m = line.match(/^NATIVE_PLAYER_PORT=(\d+)/);
|
|
44
|
+
if (m && !resolved) {
|
|
45
|
+
resolved = true;
|
|
46
|
+
nativeProc = proc;
|
|
47
|
+
nativeWsUrl = `ws://127.0.0.1:${m[1]}`;
|
|
48
|
+
resolveOk(nativeWsUrl);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
let stdoutBuf = '';
|
|
52
|
+
proc.stdout.on('data', (chunk) => {
|
|
53
|
+
stdoutBuf += chunk.toString('utf-8');
|
|
54
|
+
let i;
|
|
55
|
+
while ((i = stdoutBuf.indexOf('\n')) >= 0) {
|
|
56
|
+
onLine(stdoutBuf.slice(0, i));
|
|
57
|
+
stdoutBuf = stdoutBuf.slice(i + 1);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
proc.stderr.on('data', (c) => {
|
|
61
|
+
const s = c.toString('utf-8');
|
|
62
|
+
stderrBuf += s;
|
|
63
|
+
process.stderr.write('[native_player] ' + s);
|
|
64
|
+
});
|
|
65
|
+
proc.on('exit', (code, signal) => {
|
|
66
|
+
const wasResolved = resolved;
|
|
67
|
+
nativeProc = null;
|
|
68
|
+
nativeWsUrl = null;
|
|
69
|
+
if (!wasResolved) {
|
|
70
|
+
reject(new Error(`native_player упал до старта (code=${code}, signal=${signal}): ${stderrBuf.slice(-500)}`));
|
|
71
|
+
} else if (win && !win.isDestroyed()) {
|
|
72
|
+
win.webContents.send('native-player:exited', { code, signal });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
setTimeout(() => {
|
|
76
|
+
if (!resolved) {
|
|
77
|
+
try { proc.kill(); } catch {}
|
|
78
|
+
reject(new Error('native_player не ответил за 10s'));
|
|
79
|
+
}
|
|
80
|
+
}, 10000);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function stopNativePlayer() {
|
|
84
|
+
if (!nativeProc) return;
|
|
85
|
+
try { nativeProc.kill(); } catch {}
|
|
86
|
+
nativeProc = null;
|
|
87
|
+
nativeWsUrl = null;
|
|
88
|
+
}
|
|
89
|
+
ipcMain.handle('native-player:start', async () => {
|
|
90
|
+
const url = await startNativePlayer();
|
|
91
|
+
return { url };
|
|
92
|
+
});
|
|
93
|
+
ipcMain.handle('native-player:stop', async () => {
|
|
94
|
+
stopNativePlayer();
|
|
95
|
+
return { ok: true };
|
|
96
|
+
});
|
|
97
|
+
// Флаг «приложение закрывается» — чтобы win.on('close') не блокировал quit.
|
|
98
|
+
let isQuitting = false;
|
|
99
|
+
app.on('before-quit', () => { isQuitting = true; stopNativePlayer(); });
|
|
100
|
+
|
|
101
|
+
// ===== Settings persisted в userData/settings.json =====
|
|
102
|
+
// Хранит API-ключи (KIE, ElevenLabs) и прочие конфиги. При старте main.js
|
|
103
|
+
// прокидывает ключи в process.env, чтобы server.js подхватил без переписки.
|
|
104
|
+
function settingsPath() {
|
|
105
|
+
return path.join(app.getPath('userData'), 'settings.json');
|
|
106
|
+
}
|
|
107
|
+
function readSettings() {
|
|
108
|
+
try { return JSON.parse(fs.readFileSync(settingsPath(), 'utf-8')); }
|
|
109
|
+
catch { return {}; }
|
|
110
|
+
}
|
|
111
|
+
function writeSettings(obj) {
|
|
112
|
+
try {
|
|
113
|
+
fs.mkdirSync(path.dirname(settingsPath()), { recursive: true });
|
|
114
|
+
fs.writeFileSync(settingsPath(), JSON.stringify(obj, null, 2), 'utf-8');
|
|
115
|
+
return true;
|
|
116
|
+
} catch (e) { console.warn('settings write failed:', e.message); return false; }
|
|
117
|
+
}
|
|
118
|
+
function applySettingsToEnv(s) {
|
|
119
|
+
if (s?.kieKey) process.env.KIE_API_KEY = s.kieKey;
|
|
120
|
+
if (s?.elevenKey) process.env.ELEVENLABS_API_KEY = s.elevenKey;
|
|
121
|
+
if (s?.openrouterKey) process.env.OPENROUTER_API_KEY = s.openrouterKey;
|
|
122
|
+
}
|
|
123
|
+
// Шаблон CLAUDE.md для проектной папки — отдаётся renderer'у по запросу,
|
|
124
|
+
// тот пишет в выбранную FSAH-папку при openFilm если файла ещё нет.
|
|
125
|
+
ipcMain.handle('claudeMd:template', () => {
|
|
126
|
+
try { return fs.readFileSync(path.join(__dirname, 'assets', 'PROJECT_CLAUDE.md'), 'utf-8'); }
|
|
127
|
+
catch { return ''; }
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
ipcMain.handle('settings:get', () => readSettings());
|
|
131
|
+
ipcMain.handle('settings:save', (_e, partial) => {
|
|
132
|
+
const cur = readSettings();
|
|
133
|
+
const next = { ...cur, ...(partial || {}) };
|
|
134
|
+
writeSettings(next);
|
|
135
|
+
applySettingsToEnv(next);
|
|
136
|
+
return next;
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ===== Recents persisted в userData/recents.json (переживает quota-reset IDB) =====
|
|
140
|
+
function recentsPath() {
|
|
141
|
+
return path.join(app.getPath('userData'), 'recents.json');
|
|
142
|
+
}
|
|
143
|
+
ipcMain.handle('recents:read', async () => {
|
|
144
|
+
try {
|
|
145
|
+
const data = fs.readFileSync(recentsPath(), 'utf-8');
|
|
146
|
+
const arr = JSON.parse(data);
|
|
147
|
+
return Array.isArray(arr) ? arr : [];
|
|
148
|
+
} catch { return []; }
|
|
149
|
+
});
|
|
150
|
+
ipcMain.handle('recents:write', async (_e, arr) => {
|
|
151
|
+
try {
|
|
152
|
+
fs.mkdirSync(path.dirname(recentsPath()), { recursive: true });
|
|
153
|
+
fs.writeFileSync(recentsPath(), JSON.stringify(arr, null, 2), 'utf-8');
|
|
154
|
+
return true;
|
|
155
|
+
} catch (e) { console.warn('recents write failed:', e.message); return false; }
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
ipcMain.handle('window:close', () => {
|
|
159
|
+
if (win && !win.isDestroyed()) win.close();
|
|
160
|
+
});
|
|
161
|
+
// Renderer уведомляет нас об открытом проекте — нужно чтобы win.on('close')
|
|
162
|
+
// решал: возврат в welcome или реальный quit.
|
|
163
|
+
let projectOpen = false;
|
|
164
|
+
ipcMain.on('project:state', (_e, isOpen) => { projectOpen = !!isOpen; });
|
|
165
|
+
ipcMain.handle('window:minimize', () => {
|
|
166
|
+
if (!win || win.isDestroyed()) return;
|
|
167
|
+
// На macOS hide() убирает окно но оставляет приложение в Dock —
|
|
168
|
+
// именно то что нужно «свернуть приложение».
|
|
169
|
+
if (process.platform === 'darwin') app.hide();
|
|
170
|
+
else win.minimize();
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// ===== Application menu =====
|
|
174
|
+
// Кнопки UI вызываем через executeJavaScript(..., true) — третий аргумент
|
|
175
|
+
// userGesture=true превращает synthetic click в trusted-event, иначе
|
|
176
|
+
// showDirectoryPicker (FSAH) и подобные API упадут SecurityError.
|
|
177
|
+
// Шорткаты undo/redo/cut/copy/paste/delete renderer уже ловит сам через
|
|
178
|
+
// keydown — accelerator на них в меню не вешаем, иначе меню перехватит.
|
|
179
|
+
function buildMenu() {
|
|
180
|
+
const isMac = process.platform === 'darwin';
|
|
181
|
+
const trigger = (sel) => {
|
|
182
|
+
if (!win || win.isDestroyed()) return;
|
|
183
|
+
win.webContents.executeJavaScript(`document.querySelector(${JSON.stringify(sel)})?.click()`, true);
|
|
184
|
+
};
|
|
185
|
+
const sendToRenderer = (channel) => {
|
|
186
|
+
if (win && !win.isDestroyed()) win.webContents.send(channel);
|
|
187
|
+
};
|
|
188
|
+
const template = [
|
|
189
|
+
...(isMac ? [{ role: 'appMenu' }] : []),
|
|
190
|
+
{
|
|
191
|
+
label: 'Файл',
|
|
192
|
+
submenu: [
|
|
193
|
+
{
|
|
194
|
+
label: 'Открыть проект…',
|
|
195
|
+
accelerator: 'CmdOrCtrl+O',
|
|
196
|
+
click: () => trigger('#pickRoot'),
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
label: 'Новая сцена',
|
|
200
|
+
accelerator: 'CmdOrCtrl+N',
|
|
201
|
+
click: () => trigger('#newEpisode'),
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
label: 'Новый персонаж',
|
|
205
|
+
click: () => trigger('#newCharacter'),
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
label: 'Новая локация',
|
|
209
|
+
click: () => trigger('#newLocation'),
|
|
210
|
+
},
|
|
211
|
+
{ type: 'separator' },
|
|
212
|
+
{
|
|
213
|
+
label: 'Настройки…',
|
|
214
|
+
accelerator: 'CmdOrCtrl+,',
|
|
215
|
+
click: () => sendToRenderer('menu:open-settings'),
|
|
216
|
+
},
|
|
217
|
+
{ type: 'separator' },
|
|
218
|
+
{
|
|
219
|
+
label: 'Закрыть',
|
|
220
|
+
accelerator: 'CmdOrCtrl+W',
|
|
221
|
+
// Cmd+W: если проект открыт → закрывает проект (renderer решает),
|
|
222
|
+
// если проект уже закрыт → закрывает окно (на macOS приложение
|
|
223
|
+
// остаётся в Dock; на Windows/Linux окно — последнее, app.quit).
|
|
224
|
+
click: () => sendToRenderer('menu:close-window-or-project'),
|
|
225
|
+
},
|
|
226
|
+
...(isMac ? [] : [{ type: 'separator' }, { role: 'quit' }]),
|
|
227
|
+
],
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
label: 'Правка',
|
|
231
|
+
submenu: [
|
|
232
|
+
{ label: 'Отменить', click: () => sendToRenderer('menu:undo') },
|
|
233
|
+
{ label: 'Повторить', click: () => sendToRenderer('menu:redo') },
|
|
234
|
+
{ type: 'separator' },
|
|
235
|
+
{ role: 'cut' },
|
|
236
|
+
{ role: 'copy' },
|
|
237
|
+
{ role: 'paste' },
|
|
238
|
+
{ role: 'selectAll' },
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: 'Вид',
|
|
243
|
+
submenu: [
|
|
244
|
+
{ label: 'Таймлайн', click: () => trigger('#timelineBtn') },
|
|
245
|
+
{ type: 'separator' },
|
|
246
|
+
{ role: 'reload' },
|
|
247
|
+
{ role: 'toggleDevTools' },
|
|
248
|
+
{ type: 'separator' },
|
|
249
|
+
{ role: 'resetZoom' },
|
|
250
|
+
{ role: 'zoomIn' },
|
|
251
|
+
{ role: 'zoomOut' },
|
|
252
|
+
{ type: 'separator' },
|
|
253
|
+
{ role: 'togglefullscreen' },
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
{ role: 'windowMenu' },
|
|
257
|
+
];
|
|
258
|
+
Menu.setApplicationMenu(Menu.buildFromTemplate(template));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function createWindow() {
|
|
262
|
+
win = new BrowserWindow({
|
|
263
|
+
width: 1400,
|
|
264
|
+
height: 900,
|
|
265
|
+
minWidth: 800,
|
|
266
|
+
minHeight: 600,
|
|
267
|
+
title: 'KingKont',
|
|
268
|
+
titleBarStyle: process.platform === 'darwin' ? 'hiddenInset' : 'default',
|
|
269
|
+
icon: path.join(__dirname, 'assets', process.platform === 'win32' ? 'logo-1024.png' : 'logo.icns'),
|
|
270
|
+
show: false,
|
|
271
|
+
backgroundColor: '#1a1a1a',
|
|
272
|
+
webPreferences: {
|
|
273
|
+
contextIsolation: true,
|
|
274
|
+
nodeIntegration: false,
|
|
275
|
+
preload: path.join(__dirname, 'preload.js'),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
win.once('ready-to-show', () => win.show());
|
|
279
|
+
// Крестик окна с открытым проектом → возврат в welcome (а не quit).
|
|
280
|
+
// Если welcome — пропускаем close, app.quit() сработает по window-all-closed.
|
|
281
|
+
win.on('close', (e) => {
|
|
282
|
+
// app.quit() / Cmd+Q идёт через before-quit → isQuitting=true → пропускаем.
|
|
283
|
+
if (isQuitting) return;
|
|
284
|
+
if (projectOpen) {
|
|
285
|
+
e.preventDefault();
|
|
286
|
+
win.webContents.send('menu:close-project');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
// Внешние ссылки — в системный браузер
|
|
290
|
+
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
291
|
+
shell.openExternal(url);
|
|
292
|
+
return { action: 'deny' };
|
|
293
|
+
});
|
|
294
|
+
win.loadURL(`http://localhost:${port}`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
app.whenReady().then(async () => {
|
|
298
|
+
// Dock icon на macOS — отдельно от BrowserWindow.icon.
|
|
299
|
+
if (process.platform === 'darwin' && app.dock) {
|
|
300
|
+
try { app.dock.setIcon(path.join(__dirname, 'assets', 'logo-1024.png')); } catch (e) { console.warn('dock icon failed:', e.message); }
|
|
301
|
+
}
|
|
302
|
+
// Auto-grant File System Access API permissions для localhost — иначе после
|
|
303
|
+
// перезагрузки приложения handle жив, queryPermission='prompt', а
|
|
304
|
+
// requestPermission без user-gesture тихо проваливается, и пользователю
|
|
305
|
+
// приходится открывать папку заново. Безопасно: разрешаем только нашему
|
|
306
|
+
// localhost-серверу (Electron loadURL).
|
|
307
|
+
const sess = session.defaultSession;
|
|
308
|
+
const ALLOW_PERMS = new Set([
|
|
309
|
+
'fileSystem', 'file-system',
|
|
310
|
+
'clipboard-read', 'clipboard-write',
|
|
311
|
+
'clipboard-sanitized-write',
|
|
312
|
+
]);
|
|
313
|
+
const allow = (origin, perm) => {
|
|
314
|
+
if (!origin) return false;
|
|
315
|
+
if (!origin.startsWith('http://localhost:') && !origin.startsWith('http://127.0.0.1:')) return false;
|
|
316
|
+
return ALLOW_PERMS.has(perm);
|
|
317
|
+
};
|
|
318
|
+
sess.setPermissionCheckHandler((wc, permission, requestingOrigin) => {
|
|
319
|
+
return allow(requestingOrigin, permission);
|
|
320
|
+
});
|
|
321
|
+
sess.setPermissionRequestHandler((wc, permission, callback, details) => {
|
|
322
|
+
return callback(allow(details?.requestingUrl || '', permission));
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Прокидываем API-ключи из settings.json в process.env ДО старта server.js.
|
|
326
|
+
applySettingsToEnv(readSettings());
|
|
327
|
+
|
|
328
|
+
// Фиксированный порт нужен потому что Chromium хранит IndexedDB и FSAH-handle
|
|
329
|
+
// per-origin (http://localhost:PORT). Со случайным портом каждый запуск
|
|
330
|
+
// получал бы новую пустую IDB. Если порт занят — fallback на случайный, но
|
|
331
|
+
// тогда recents-handle потеряется (это меньшая беда, чем падение запуска).
|
|
332
|
+
try {
|
|
333
|
+
port = await start(17893);
|
|
334
|
+
} catch {
|
|
335
|
+
console.warn('port 17893 busy, falling back to random');
|
|
336
|
+
try { port = await start(0); }
|
|
337
|
+
catch (e) {
|
|
338
|
+
console.error('Server failed to start:', e);
|
|
339
|
+
app.quit();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
await createWindow();
|
|
344
|
+
buildMenu();
|
|
345
|
+
|
|
346
|
+
app.on('activate', () => {
|
|
347
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
app.on('window-all-closed', () => {
|
|
352
|
+
// На всех платформах: закрытие последнего окна = выход. На macOS можно
|
|
353
|
+
// оставлять процесс в Dock (стандарт), но в нашем случае welcome — тот
|
|
354
|
+
// самый «начальный» экран; закрытие крестика однозначно «я закончил».
|
|
355
|
+
app.quit();
|
|
356
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kingkont",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "KingKont · Chatium — нод-редактор сцен с AI-генерацией (картинки/видео/голос/SFX/музыка/текст)",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kingkont": "bin/kingkont.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"main.js",
|
|
11
|
+
"preload.js",
|
|
12
|
+
"server.js",
|
|
13
|
+
"index.html",
|
|
14
|
+
"assets/**/*",
|
|
15
|
+
"bin/**/*",
|
|
16
|
+
"scripts/**/*",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=18"
|
|
21
|
+
},
|
|
22
|
+
"keywords": ["video-editor", "ai", "kie", "elevenlabs", "openrouter", "scene-editor", "electron"],
|
|
23
|
+
"author": "Tim",
|
|
24
|
+
"license": "UNLICENSED",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"start": "electron .",
|
|
27
|
+
"server": "node server.js",
|
|
28
|
+
"pack": "electron-builder --dir",
|
|
29
|
+
"dist": "electron-builder",
|
|
30
|
+
"postinstall": "if [ \"$(uname)\" = \"Darwin\" ] && [ -d node_modules/electron ]; then bash scripts/patch-electron-name.sh || true; fi"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"electron": "^32.2.0"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"electron-builder": "^25.1.8"
|
|
37
|
+
},
|
|
38
|
+
"build": {
|
|
39
|
+
"appId": "com.kingkont.editor",
|
|
40
|
+
"productName": "KingKont",
|
|
41
|
+
"files": [
|
|
42
|
+
"main.js",
|
|
43
|
+
"preload.js",
|
|
44
|
+
"server.js",
|
|
45
|
+
"index.html",
|
|
46
|
+
"assets/**/*",
|
|
47
|
+
"package.json"
|
|
48
|
+
],
|
|
49
|
+
"extraResources": [
|
|
50
|
+
{ "from": ".env", "to": ".env" }
|
|
51
|
+
],
|
|
52
|
+
"mac": {
|
|
53
|
+
"category": "public.app-category.video",
|
|
54
|
+
"target": "dmg",
|
|
55
|
+
"icon": "assets/logo.icns"
|
|
56
|
+
},
|
|
57
|
+
"win": { "target": "nsis", "icon": "assets/logo-1024.png" },
|
|
58
|
+
"linux": { "target": "AppImage", "icon": "assets/logo-1024.png" }
|
|
59
|
+
}
|
|
60
|
+
}
|
package/preload.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Preload: пробрасывает в renderer (index.html) минимальный API для нативного плеера.
|
|
2
|
+
// Веб-версия работает без него (window.nativePlayer === undefined → editor использует только web preview).
|
|
3
|
+
const { contextBridge, ipcRenderer, webUtils } = require('electron');
|
|
4
|
+
|
|
5
|
+
contextBridge.exposeInMainWorld('nativePlayer', {
|
|
6
|
+
// Старт нативного плеера. Резолвится URL'ом WebSocket (ws://127.0.0.1:PORT) либо ошибкой.
|
|
7
|
+
start: () => ipcRenderer.invoke('native-player:start'),
|
|
8
|
+
// Остановить процесс плеера (и его окно).
|
|
9
|
+
stop: () => ipcRenderer.invoke('native-player:stop'),
|
|
10
|
+
// Абсолютный путь файла (Electron-специфично, через webUtils.getPathForFile).
|
|
11
|
+
pathForFile: (file) => {
|
|
12
|
+
try { return webUtils.getPathForFile(file) || null; } catch { return null; }
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Application menu → renderer events. Пунктов мало, делаем тонкий event-bus.
|
|
17
|
+
contextBridge.exposeInMainWorld('appMenu', {
|
|
18
|
+
on: (channel, cb) => {
|
|
19
|
+
const allowed = ['open-film', 'new-episode', 'new-character', 'new-location',
|
|
20
|
+
'undo', 'redo', 'toggle-timeline', 'close-project',
|
|
21
|
+
'close-window-or-project', 'open-settings'];
|
|
22
|
+
if (!allowed.includes(channel)) return;
|
|
23
|
+
ipcRenderer.on(`menu:${channel}`, (_e, payload) => cb(payload));
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
contextBridge.exposeInMainWorld('appWindow', {
|
|
28
|
+
close: () => ipcRenderer.invoke('window:close'),
|
|
29
|
+
minimize: () => ipcRenderer.invoke('window:minimize'),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
contextBridge.exposeInMainWorld('appProject', {
|
|
33
|
+
notifyState: (isOpen) => ipcRenderer.send('project:state', !!isOpen),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Recents — JSON-файл в userData (переживает любой перезапуск, даже kill -9).
|
|
37
|
+
// Хранится плоский массив {name, thumbDataUrl, ts}; FSAH-handle лежит
|
|
38
|
+
// параллельно в IDB и связывается по name.
|
|
39
|
+
contextBridge.exposeInMainWorld('recentsStore', {
|
|
40
|
+
read: () => ipcRenderer.invoke('recents:read'),
|
|
41
|
+
write: (arr) => ipcRenderer.invoke('recents:write', arr),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
contextBridge.exposeInMainWorld('appSettings', {
|
|
45
|
+
get: () => ipcRenderer.invoke('settings:get'),
|
|
46
|
+
save: (partial) => ipcRenderer.invoke('settings:save', partial),
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
contextBridge.exposeInMainWorld('claudeMd', {
|
|
50
|
+
template: () => ipcRenderer.invoke('claudeMd:template'),
|
|
51
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Полностью переименовывает dev-Electron в KingKont:
|
|
3
|
+
# Electron.app → KingKont.app, Electron Helper(.X).app → KingKont Helper(.X).app,
|
|
4
|
+
# бинарь MacOS/Electron → MacOS/KingKont (и в helpers), Info.plist'ы,
|
|
5
|
+
# node_modules/electron/path.txt → новый путь, ad-hoc resign.
|
|
6
|
+
# В Activity Monitor / `ps` процессы видны как "KingKont", а не "Electron".
|
|
7
|
+
# Вызывается postinstall'ом — после `npm install` восстанавливает rename.
|
|
8
|
+
set -e
|
|
9
|
+
DIST="node_modules/electron/dist"
|
|
10
|
+
APP="$DIST/Electron.app"
|
|
11
|
+
NEW="$DIST/KingKont.app"
|
|
12
|
+
[ -d "$APP" ] || [ -d "$NEW" ] || { echo "skip: $APP not found"; exit 0; }
|
|
13
|
+
|
|
14
|
+
# Уже переименовано?
|
|
15
|
+
if [ -d "$NEW" ] && [ ! -d "$APP" ]; then
|
|
16
|
+
echo "✓ already renamed to KingKont"
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
echo "→ Электрон в KingKont…"
|
|
21
|
+
|
|
22
|
+
# 1) Переименовать main bundle и его бинарь
|
|
23
|
+
mv "$APP" "$NEW"
|
|
24
|
+
mv "$NEW/Contents/MacOS/Electron" "$NEW/Contents/MacOS/KingKont"
|
|
25
|
+
|
|
26
|
+
# 2) Helpers — каждый Helper.app со своим бинарём и Info.plist
|
|
27
|
+
FW="$NEW/Contents/Frameworks"
|
|
28
|
+
for HELPER in "Electron Helper.app" "Electron Helper (GPU).app" "Electron Helper (Plugin).app" "Electron Helper (Renderer).app"; do
|
|
29
|
+
[ -d "$FW/$HELPER" ] || continue
|
|
30
|
+
NEW_HELPER="$(echo "$HELPER" | sed 's/^Electron /KingKont /')"
|
|
31
|
+
mv "$FW/$HELPER" "$FW/$NEW_HELPER"
|
|
32
|
+
OLD_BIN="$(echo "$HELPER" | sed 's/\.app$//')" # «Electron Helper», «Electron Helper (GPU)»…
|
|
33
|
+
NEW_BIN="$(echo "$OLD_BIN" | sed 's/^Electron /KingKont /')"
|
|
34
|
+
mv "$FW/$NEW_HELPER/Contents/MacOS/$OLD_BIN" "$FW/$NEW_HELPER/Contents/MacOS/$NEW_BIN"
|
|
35
|
+
PLIST="$FW/$NEW_HELPER/Contents/Info.plist"
|
|
36
|
+
/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable $NEW_BIN" "$PLIST" 2>/dev/null || true
|
|
37
|
+
/usr/libexec/PlistBuddy -c "Set :CFBundleName $NEW_BIN" "$PLIST" 2>/dev/null || true
|
|
38
|
+
if /usr/libexec/PlistBuddy -c "Print :CFBundleDisplayName" "$PLIST" >/dev/null 2>&1; then
|
|
39
|
+
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $NEW_BIN" "$PLIST"
|
|
40
|
+
fi
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
# 3) Info.plist главного app
|
|
44
|
+
PLIST="$NEW/Contents/Info.plist"
|
|
45
|
+
/usr/libexec/PlistBuddy -c "Set :CFBundleExecutable KingKont" "$PLIST"
|
|
46
|
+
/usr/libexec/PlistBuddy -c "Set :CFBundleName KingKont" "$PLIST"
|
|
47
|
+
if /usr/libexec/PlistBuddy -c "Print :CFBundleDisplayName" "$PLIST" >/dev/null 2>&1; then
|
|
48
|
+
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName KingKont" "$PLIST"
|
|
49
|
+
else
|
|
50
|
+
/usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string KingKont" "$PLIST"
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# 4) node_modules/electron/path.txt: cli.js использует это для spawn-а
|
|
54
|
+
echo -n "KingKont.app/Contents/MacOS/KingKont" > node_modules/electron/path.txt
|
|
55
|
+
|
|
56
|
+
# 5) Заменить иконку бандла на наш logo.icns. Имя файла CFBundleIconFile
|
|
57
|
+
# в Info.plist оставляем как есть (electron.icns) — иначе пришлось бы и
|
|
58
|
+
# его править. Просто перезаписываем содержимое.
|
|
59
|
+
if [ -f "assets/logo.icns" ]; then
|
|
60
|
+
cp "assets/logo.icns" "$NEW/Contents/Resources/electron.icns"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
# 6) Ad-hoc resign (после изменения Info.plist/иконки macOS теряет валидность подписи).
|
|
64
|
+
codesign --force --deep --sign - "$NEW" >/dev/null 2>&1 || true
|
|
65
|
+
|
|
66
|
+
# 7) Сбросить кэш LaunchServices, чтобы macOS видел новое имя/иконку
|
|
67
|
+
/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
|
|
68
|
+
-f "$NEW" >/dev/null 2>&1 || true
|
|
69
|
+
|
|
70
|
+
echo "✓ KingKont готов"
|