Лицензия, редактор, пульт и сборка

- Main: license service, IPC, router; закрытие окон; yauzl закрытие zip (EMFILE), zipRead тест
- Editor: стабильный projectState без мигания, логотип и меню, строки UI, LayoutShell overlay
- Control: ластик для всех типов эффектов, затухание/нарастание музыки при смене сцены
- Сборка: vite, build/dev scripts, obfuscate-main и build-env скрипты с тестами; package.json

Made-with: Cursor
This commit is contained in:
Ivan Fontosh
2026-04-19 20:11:24 +08:00
parent 5e7dc5ea19
commit 2fa20da94d
40 changed files with 2629 additions and 211 deletions
+1 -63
View File
@@ -4,7 +4,6 @@ import fs from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import yauzl from 'yauzl';
import { ZipFile } from 'yazl';
import { isSceneGraphEdgeRejected } from '../../shared/graph/sceneGraphEdgeRules';
@@ -26,6 +25,7 @@ import { getAppSemanticVersion } from '../versionInfo';
import { reconcileAssetFiles } from './assetPrune';
import { getLegacyProjectsRootDirs, getProjectsCacheRootDir, getProjectsRootDir } from './paths';
import { readProjectJsonFromZip, unzipToDir } from './yauzlProjectZip';
type ProjectIndexEntry = {
id: ProjectId;
@@ -1096,68 +1096,6 @@ async function atomicWriteFile(filePath: string, contents: string): Promise<void
}
}
function unzipToDir(zipPath: string, outDir: string): Promise<void> {
return new Promise((resolve, reject) => {
yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => {
if (err) return reject(err);
const zipFile = zip;
zipFile.readEntry();
zipFile.on('entry', (entry: yauzl.Entry) => {
const filePath = path.join(outDir, entry.fileName);
if (entry.fileName.endsWith('/')) {
fssync.mkdirSync(filePath, { recursive: true });
zipFile.readEntry();
return;
}
fssync.mkdirSync(path.dirname(filePath), { recursive: true });
zipFile.openReadStream(entry, (streamErr, readStream) => {
if (streamErr) return reject(streamErr);
readStream.on('error', reject);
const writeStream = fssync.createWriteStream(filePath);
writeStream.on('error', reject);
readStream.pipe(writeStream);
writeStream.on('close', () => zipFile.readEntry());
});
});
zipFile.on('end', resolve);
zipFile.on('error', reject);
});
});
}
async function readProjectJsonFromZip(zipPath: string): Promise<Project> {
return new Promise<Project>((resolve, reject) => {
yauzl.open(zipPath, { lazyEntries: true }, (err, zip) => {
if (err) return reject(err);
const zipFile = zip;
zipFile.readEntry();
zipFile.on('entry', (entry: yauzl.Entry) => {
if (entry.fileName !== 'project.json') {
zipFile.readEntry();
return;
}
zipFile.openReadStream(entry, (streamErr, readStream) => {
if (streamErr) return reject(streamErr);
const chunks: Buffer[] = [];
readStream.on('data', (c: Buffer) => chunks.push(c));
readStream.on('error', reject);
readStream.on('end', () => {
try {
const raw = Buffer.concat(chunks).toString('utf8');
const parsed = JSON.parse(raw) as unknown as Project;
resolve(parsed);
} catch (e) {
reject(e instanceof Error ? e : new Error('Failed to parse project.json'));
}
});
});
});
zipFile.on('error', reject);
zipFile.on('end', () => reject(new Error('project.json not found in zip')));
});
});
}
/** Уже сжатые контейнеры/кодеки — в ZIP кладём без deflate, качество не трогаем; project.json и сырьё — deflate 9. */
function zipOptionsForRelativeEntry(rel: string): { compressionLevel: number } {
const norm = rel.replace(/\\/gu, '/').toLowerCase();