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

- 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
@@ -0,0 +1,257 @@
import React, { useEffect, useState } from 'react';
import { createPortal } from 'react-dom';
import { ipcChannels } from '../../../shared/ipc/contracts';
import { EULA_CURRENT_VERSION } from '../../../shared/license/eulaVersion';
import type { LicenseSnapshot } from '../../../shared/license/licenseSnapshot';
import { EULA_RU_MARKDOWN } from '../../legal/eulaRu';
import { getDndApi } from '../../shared/dndApi';
import { Button } from '../../shared/ui/controls';
import styles from '../EditorApp.module.css';
type LicenseTokenModalProps = {
open: boolean;
onClose: () => void;
onSaved: () => void;
};
export function LicenseTokenModal({ open, onClose, onSaved }: LicenseTokenModalProps) {
const [token, setToken] = useState('');
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!open) return;
setToken('');
setSaving(false);
setError(null);
}, [open]);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, open]);
if (!open) return null;
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>Указать ключ</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
×
</button>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ЛИЦЕНЗИОННЫЙ ТОКЕН</div>
<textarea
className={styles.licenseTextarea}
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="Вставьте токен, выданный сервером лицензий…"
spellCheck={false}
/>
</div>
{error ? <div className={styles.fieldError}>{error}</div> : null}
<div className={styles.modalFooter}>
<Button onClick={onClose} disabled={saving}>
Отмена
</Button>
<Button
variant="primary"
disabled={saving || !token.trim()}
onClick={() => {
void (async () => {
setSaving(true);
setError(null);
try {
await getDndApi().invoke(ipcChannels.license.setToken, { token: token.trim() });
onSaved();
onClose();
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
} finally {
setSaving(false);
}
})();
}}
>
Сохранить
</Button>
</div>
</div>
</>,
document.body,
);
}
type EulaModalProps = {
open: boolean;
onClose: () => void;
onAccepted: () => void;
};
export function EulaModal({ open, onClose, onAccepted }: EulaModalProps) {
const [saving, setSaving] = useState(false);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, open]);
if (!open) return null;
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>Лицензионное соглашение</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
×
</button>
</div>
<div className={styles.eulaScroll}>{EULA_RU_MARKDOWN}</div>
<div className={styles.modalFooter}>
<Button onClick={onClose} disabled={saving}>
Не принимаю
</Button>
<Button
variant="primary"
disabled={saving}
onClick={() => {
void (async () => {
setSaving(true);
try {
await getDndApi().invoke(ipcChannels.license.acceptEula, {
version: EULA_CURRENT_VERSION,
});
onAccepted();
onClose();
} finally {
setSaving(false);
}
})();
}}
>
Принимаю условия
</Button>
</div>
</div>
</>,
document.body,
);
}
type LicenseAboutModalProps = {
open: boolean;
onClose: () => void;
snapshot: LicenseSnapshot | null;
};
function reasonLabel(reason: LicenseSnapshot['reason']): string {
switch (reason) {
case 'ok':
return 'Активна';
case 'none':
return 'Ключ не указан';
case 'expired':
return 'Срок действия истёк';
case 'bad_signature':
return 'Недействительная подпись';
case 'bad_payload':
return 'Неверный формат токена';
case 'malformed':
return 'Повреждённый токен';
case 'not_yet_valid':
return 'Ещё не действует';
case 'wrong_device':
return 'Другой привязанный компьютер';
case 'revoked_remote':
return 'Отозвана на сервере';
default:
return reason;
}
}
export function LicenseAboutModal({ open, onClose, snapshot }: LicenseAboutModalProps) {
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === 'Escape') onClose();
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [onClose, open]);
if (!open) return null;
const expText =
snapshot?.summary?.exp != null
? new Date(snapshot.summary.exp * 1000).toLocaleString('ru-RU', {
dateStyle: 'long',
timeStyle: 'short',
})
: '—';
return createPortal(
<>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalBackdrop} />
<div role="dialog" aria-modal="true" className={styles.modalDialog}>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>О лицензии</div>
<button type="button" aria-label="Закрыть" onClick={onClose} className={styles.modalClose}>
×
</button>
</div>
{snapshot?.devSkip ? (
<div className={styles.fieldError}>
Режим разработки: проверка лицензии отключена (DND_SKIP_LICENSE).
</div>
) : null}
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>СТАТУС</div>
<div>{snapshot ? reasonLabel(snapshot.reason) : '—'}</div>
</div>
{snapshot?.summary ? (
<>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ПРОДУКТ</div>
<div>{snapshot.summary.pid}</div>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ID ЛИЦЕНЗИИ</div>
<div style={{ wordBreak: 'break-all' }}>{snapshot.summary.sub}</div>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>ОКОНЧАНИЕ</div>
<div>{expText}</div>
</div>
<div className={styles.fieldGrid}>
<div className={styles.fieldLabel}>УСТРОЙСТВО</div>
<div style={{ wordBreak: 'break-all' }}>{snapshot.deviceId}</div>
</div>
</>
) : (
<div className={styles.muted}>Нет данных лицензии.</div>
)}
<div className={styles.modalFooter}>
<Button variant="primary" onClick={onClose}>
Закрыть
</Button>
</div>
</div>
</>,
document.body,
);
}