Files
DndGamePlayer/app/renderer/editor/license/EditorLicenseModals.tsx
T
Ivan Fontosh 2fa20da94d Лицензия, редактор, пульт и сборка
- 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
2026-04-19 20:11:24 +08:00

258 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
);
}