feat(editor): побочные сюжетные линии и импорт/экспорт линий
Добавлена полноценная поддержка побочных сюжетных линий в редакторе и на пульте: визуальное выделение компонент графа, запрет недопустимых связей между основной и побочными линиями, метки «ПОБОЧНАЯ» и названия линий. Реализован partial export/import сюжетных линий: - экспорт выбранных линий в урезанный .ttrpg.zip с manifest в project.json; - импорт в открытый проект с выбором линий, разрешением конфликтов названий сцен (одна модалка на операцию) и отчётом о результате; - новое окно источника импорта: «Из проекта» (dropdown, без текущего) или «Из файла»; на главном экране — только полный импорт из файла. Исправлены гонки при открытии/закрытии проекта (сериализация open/close в main, сброс зависших состояний UI), залипание оверлея прогресса экспорта и старт Electron в dev после готовности Vite. Добавлены unit-тесты для lineage, export/import и контрактов zipStore. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+96
-29
@@ -2,11 +2,19 @@ import { context } from 'esbuild';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { spawn } from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const root = path.resolve(__dirname, '..');
|
||||
|
||||
const electronEnv = {
|
||||
...process.env,
|
||||
NODE_ENV: 'development',
|
||||
DND_SKIP_LICENSE: process.env.DND_SKIP_LICENSE ?? '1',
|
||||
VITE_DEV_SERVER_URL: 'http://localhost:5173/',
|
||||
};
|
||||
|
||||
function spawnShell(command, opts = {}) {
|
||||
const child = spawn(command, {
|
||||
cwd: root,
|
||||
@@ -41,7 +49,83 @@ function killTree(child) {
|
||||
}
|
||||
}
|
||||
|
||||
function waitForVite(url = 'http://127.0.0.1:5173/editor.html', timeoutMs = 60000) {
|
||||
const started = Date.now();
|
||||
return new Promise((resolve, reject) => {
|
||||
const tick = () => {
|
||||
const req = http.get(url, (res) => {
|
||||
res.resume();
|
||||
resolve();
|
||||
});
|
||||
req.on('error', () => {
|
||||
if (Date.now() - started > timeoutMs) {
|
||||
reject(new Error(`Timed out waiting for Vite at ${url}`));
|
||||
return;
|
||||
}
|
||||
setTimeout(tick, 300);
|
||||
});
|
||||
req.setTimeout(2000, () => {
|
||||
req.destroy();
|
||||
});
|
||||
};
|
||||
tick();
|
||||
});
|
||||
}
|
||||
|
||||
let shuttingDown = false;
|
||||
let electron = null;
|
||||
let vite = null;
|
||||
let dispose = async () => {};
|
||||
let restartTimer = null;
|
||||
let electronStarted = false;
|
||||
let restartingElectron = false;
|
||||
|
||||
async function shutdown() {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
if (restartTimer) clearTimeout(restartTimer);
|
||||
killTree(vite);
|
||||
killTree(electron);
|
||||
await dispose();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
function startElectron() {
|
||||
if (shuttingDown) return;
|
||||
restartingElectron = false;
|
||||
if (electron) killTree(electron);
|
||||
electron = spawnShell('npx electron .', { env: electronEnv });
|
||||
electron.once('exit', () => {
|
||||
if (restartingElectron || shuttingDown) return;
|
||||
void shutdown();
|
||||
});
|
||||
electronStarted = true;
|
||||
}
|
||||
|
||||
function scheduleElectronRestart() {
|
||||
if (shuttingDown || !electronStarted) return;
|
||||
if (restartTimer) clearTimeout(restartTimer);
|
||||
restartTimer = setTimeout(() => {
|
||||
restartTimer = null;
|
||||
console.log('[dev] main/preload rebuilt — restarting Electron…');
|
||||
restartingElectron = true;
|
||||
startElectron();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
function createRestartPlugin() {
|
||||
return {
|
||||
name: 'restart-electron-on-rebuild',
|
||||
setup(build) {
|
||||
build.onEnd((result) => {
|
||||
if (result.errors.length === 0) scheduleElectronRestart();
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function watchMainAndPreload() {
|
||||
const restartPlugin = createRestartPlugin();
|
||||
const main = await context({
|
||||
entryPoints: [path.join(root, 'app/main/index.ts')],
|
||||
outfile: path.join(root, 'dist/main/index.cjs'),
|
||||
@@ -52,6 +136,7 @@ async function watchMainAndPreload() {
|
||||
sourcemap: true,
|
||||
external: ['electron'],
|
||||
define: { 'process.env.NODE_ENV': JSON.stringify('development') },
|
||||
plugins: [restartPlugin],
|
||||
});
|
||||
await main.rebuild();
|
||||
await main.watch();
|
||||
@@ -66,6 +151,7 @@ async function watchMainAndPreload() {
|
||||
sourcemap: true,
|
||||
external: ['electron'],
|
||||
define: { 'process.env.NODE_ENV': JSON.stringify('development') },
|
||||
plugins: [restartPlugin],
|
||||
});
|
||||
await preload.rebuild();
|
||||
await preload.watch();
|
||||
@@ -75,42 +161,23 @@ async function watchMainAndPreload() {
|
||||
};
|
||||
}
|
||||
|
||||
const dispose = await watchMainAndPreload();
|
||||
const vite = spawnShell('npx vite dev --strictPort', {
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'development',
|
||||
DND_SKIP_LICENSE: process.env.DND_SKIP_LICENSE ?? '1',
|
||||
VITE_DEV_SERVER_URL: 'http://localhost:5173/',
|
||||
},
|
||||
});
|
||||
const electron = spawnShell('npx electron .', {
|
||||
env: {
|
||||
...process.env,
|
||||
NODE_ENV: 'development',
|
||||
DND_SKIP_LICENSE: process.env.DND_SKIP_LICENSE ?? '1',
|
||||
VITE_DEV_SERVER_URL: 'http://localhost:5173/',
|
||||
},
|
||||
dispose = await watchMainAndPreload();
|
||||
|
||||
vite = spawnShell('npx vite dev --strictPort', {
|
||||
env: electronEnv,
|
||||
});
|
||||
|
||||
let shuttingDown = false;
|
||||
try {
|
||||
await waitForVite();
|
||||
} catch (err) {
|
||||
console.error('[dev] Vite did not start in time:', err);
|
||||
}
|
||||
|
||||
const shutdown = async () => {
|
||||
if (shuttingDown) return;
|
||||
shuttingDown = true;
|
||||
killTree(vite);
|
||||
killTree(electron);
|
||||
await dispose();
|
||||
process.exit(0);
|
||||
};
|
||||
startElectron();
|
||||
|
||||
process.on('SIGINT', () => void shutdown());
|
||||
process.on('SIGTERM', () => void shutdown());
|
||||
|
||||
electron.once('exit', () => {
|
||||
void shutdown();
|
||||
});
|
||||
|
||||
vite.once('exit', (code) => {
|
||||
if (code !== 0 && code !== null && !shuttingDown) {
|
||||
void shutdown();
|
||||
|
||||
Reference in New Issue
Block a user