feat: кубики/таймер/кисть/спонсоры и задел игровых систем
Показ анимации кубиков игрокам, закрытие оверлеев с окнами, ручной ввод количества, меньший радиус кисти, блок спонсоров в «О программе»; плюс черновик game systems / character sheet. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -688,6 +688,13 @@
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.aboutSponsorsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.aboutLink {
|
||||
color: var(--color-accent, #a78bfa);
|
||||
text-decoration: none;
|
||||
|
||||
@@ -62,6 +62,7 @@ import { MaterialEditModal, MaterialsManagerModal } from './MaterialsModals';
|
||||
import { PlayersManagerModal } from './PlayersModals';
|
||||
import { LaunchPlayersModal } from './LaunchPlayersModal';
|
||||
import { PresentationSettingsModal } from './PresentationSettingsModal';
|
||||
import { GameSystemsModal } from './GameSystemsModal';
|
||||
import { PlaylistsManagerModal } from './PlaylistsModal';
|
||||
import { isSceneDescriptionEmpty, sanitizeSceneDescriptionHtml } from './sceneDescriptionHtml';
|
||||
import { SceneDescriptionModal } from './SceneDescriptionModal';
|
||||
@@ -166,10 +167,28 @@ export function EditorApp() {
|
||||
const [launchPlayersOpen, setLaunchPlayersOpen] = useState(false);
|
||||
const [playlistsManagerOpen, setPlaylistsManagerOpen] = useState(false);
|
||||
const [presentationSettingsOpen, setPresentationSettingsOpen] = useState(false);
|
||||
const [gameSystemsOpen, setGameSystemsOpen] = useState(false);
|
||||
const [runMenuOpen, setRunMenuOpen] = useState(false);
|
||||
const [runMenuPos, setRunMenuPos] = useState<{ left: number; top: number } | null>(null);
|
||||
const runSplitRef = useRef<HTMLDivElement | null>(null);
|
||||
const [materialEdit, setMaterialEdit] = useState<ProjectMaterial | null | 'new'>(null);
|
||||
|
||||
const listGameSystems = useCallback(async () => {
|
||||
const res = await getDndApi().invoke(ipcChannels.gameSystems.list, {});
|
||||
return res.systems;
|
||||
}, []);
|
||||
const importGameSystem = useCallback(async () => {
|
||||
const res = await getDndApi().invoke(ipcChannels.gameSystems.importFromFile, {});
|
||||
return res.canceled ? null : res.system;
|
||||
}, []);
|
||||
const deleteGameSystem = useCallback(async (id: string) => {
|
||||
await getDndApi().invoke(ipcChannels.gameSystems.delete, { id });
|
||||
}, []);
|
||||
const previewGameSystem = useCallback(async (id: string) => {
|
||||
console.log('[gameSystems] preview stub', id);
|
||||
window.alert(`Preview (stub): ${id}`);
|
||||
}, []);
|
||||
|
||||
const onProjectNotice = useCallback(
|
||||
(code: ProjectNoticeCode) => {
|
||||
const handlers: Record<ProjectNoticeCode, () => void> = {
|
||||
@@ -1360,6 +1379,18 @@ export function EditorApp() {
|
||||
>
|
||||
{t('menu.presentationSettings')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="menuitem"
|
||||
className={styles.fileMenuItem}
|
||||
onClick={() => {
|
||||
setSettingsMenuOpen(false);
|
||||
setSettingsLangSubOpen(false);
|
||||
setGameSystemsOpen(true);
|
||||
}}
|
||||
>
|
||||
{t('menu.gameSystems')}
|
||||
</button>
|
||||
<div className={styles.fileMenuSubHost} role="presentation">
|
||||
<button
|
||||
type="button"
|
||||
@@ -1682,6 +1713,22 @@ export function EditorApp() {
|
||||
open={presentationSettingsOpen}
|
||||
onClose={() => setPresentationSettingsOpen(false)}
|
||||
/>
|
||||
<GameSystemsModal
|
||||
open={gameSystemsOpen}
|
||||
onClose={() => setGameSystemsOpen(false)}
|
||||
labels={{
|
||||
title: t('gameSystems.title'),
|
||||
import: t('gameSystems.import'),
|
||||
preview: t('gameSystems.preview'),
|
||||
remove: t('gameSystems.remove'),
|
||||
empty: t('gameSystems.empty'),
|
||||
close: t('gameSystems.close'),
|
||||
}}
|
||||
listSystems={listGameSystems}
|
||||
importSystem={importGameSystem}
|
||||
deleteSystem={deleteGameSystem}
|
||||
previewSystem={previewGameSystem}
|
||||
/>
|
||||
<PlaylistsManagerModal
|
||||
open={playlistsManagerOpen}
|
||||
onClose={() => setPlaylistsManagerOpen(false)}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import type { GameSystemListItem } from '../../shared/ipc/contracts';
|
||||
|
||||
export type { GameSystemListItem };
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
labels: {
|
||||
title: string;
|
||||
import: string;
|
||||
preview: string;
|
||||
remove: string;
|
||||
empty: string;
|
||||
close: string;
|
||||
};
|
||||
listSystems: () => Promise<GameSystemListItem[]>;
|
||||
importSystem: () => Promise<GameSystemListItem | null>;
|
||||
deleteSystem: (id: string) => Promise<void>;
|
||||
previewSystem: (id: string) => Promise<void>;
|
||||
};
|
||||
|
||||
export function GameSystemsModal({
|
||||
open,
|
||||
onClose,
|
||||
labels,
|
||||
listSystems,
|
||||
importSystem,
|
||||
deleteSystem,
|
||||
previewSystem,
|
||||
}: Props) {
|
||||
const [items, setItems] = useState<GameSystemListItem[]>([]);
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
void (async () => {
|
||||
setItems(await listSystems());
|
||||
})();
|
||||
}, [open, listSystems]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="game-systems-modal"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0,0,0,0.55)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
zIndex: 10000,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 560,
|
||||
maxHeight: '80vh',
|
||||
overflow: 'auto',
|
||||
background: 'rgba(10,10,14,0.98)',
|
||||
border: '1px solid rgba(255,255,255,0.12)',
|
||||
borderRadius: 12,
|
||||
padding: 16,
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 12 }}>
|
||||
<strong>{labels.title}</strong>
|
||||
<button type="button" data-testid="game-systems-close" onClick={onClose}>
|
||||
{labels.close}
|
||||
</button>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 12 }}>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="game-systems-import"
|
||||
disabled={busy}
|
||||
onClick={async () => {
|
||||
setBusy(true);
|
||||
try {
|
||||
await importSystem();
|
||||
setItems(await listSystems());
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{labels.import}
|
||||
</button>
|
||||
</div>
|
||||
{items.length === 0 ? (
|
||||
<div style={{ opacity: 0.6 }}>{labels.empty}</div>
|
||||
) : (
|
||||
items.map((s) => (
|
||||
<div
|
||||
key={s.id}
|
||||
data-testid={`game-system-${s.id}`}
|
||||
style={{
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
padding: '10px 0',
|
||||
borderBottom: '1px solid rgba(255,255,255,0.08)',
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
<div>{s.name}</div>
|
||||
<div style={{ opacity: 0.55, fontSize: 12 }}>
|
||||
{s.id} · r{s.revision}
|
||||
</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button type="button" onClick={() => void previewSystem(s.id)}>
|
||||
{labels.preview}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={async () => {
|
||||
await deleteSystem(s.id);
|
||||
setItems(await listSystems());
|
||||
}}
|
||||
>
|
||||
{labels.remove}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,15 @@ import {
|
||||
} from '../help/helpSections';
|
||||
import { useEditorI18n } from '../i18n/EditorI18nContext';
|
||||
|
||||
const ABOUT_SPONSORS = [
|
||||
{ kind: 'text', name: 'Исаков Артур' },
|
||||
{ kind: 'text', name: 'Подчуфарова Евгения' },
|
||||
{ kind: 'text', name: 'Никитин Виктор' },
|
||||
{ kind: 'text', name: 'Майер С.В. (Клуб «Серый Май»)' },
|
||||
{ kind: 'link', name: 'true_rus(КАВ)', url: 'https://truecorp.su/' },
|
||||
{ kind: 'text', name: 'Макс LonelyFlame Чурсин' },
|
||||
] as const;
|
||||
|
||||
type AppAboutModalProps = {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
@@ -80,6 +89,24 @@ export function AppAboutModal({ open, onClose, appVersion }: AppAboutModalProps)
|
||||
{t('app.about.websiteUrl')}
|
||||
</a>
|
||||
</div>
|
||||
<div className={styles.fieldLabel}>{t('app.about.sponsorsLabel')}</div>
|
||||
<div className={styles.aboutSponsorsList}>
|
||||
{ABOUT_SPONSORS.map((sponsor) =>
|
||||
sponsor.kind === 'link' ? (
|
||||
<a
|
||||
key={sponsor.name}
|
||||
className={styles.aboutLink}
|
||||
href={sponsor.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{sponsor.name}
|
||||
</a>
|
||||
) : (
|
||||
<div key={sponsor.name}>{sponsor.name}</div>
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.modalFooter}>
|
||||
|
||||
@@ -132,12 +132,20 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'menu.aboutLicense': 'О лицензии',
|
||||
'menu.checkUpdates': 'Проверить обновления',
|
||||
'menu.presentationSettings': 'Экран презентации',
|
||||
'menu.gameSystems': 'Игровые системы',
|
||||
'menu.language': 'Язык',
|
||||
'menu.langRu': 'Русский',
|
||||
'menu.langEn': 'English',
|
||||
'menu.aboutProgram': 'О программе',
|
||||
'menu.instructions': 'Инструкция',
|
||||
|
||||
'gameSystems.title': 'Игровые системы',
|
||||
'gameSystems.import': 'Импорт…',
|
||||
'gameSystems.preview': 'Просмотр',
|
||||
'gameSystems.remove': 'Удалить',
|
||||
'gameSystems.empty': 'Нет импортированных систем',
|
||||
'gameSystems.close': 'Закрыть',
|
||||
|
||||
'top.aboutApp': 'О приложении',
|
||||
|
||||
'app.about.title': 'О программе',
|
||||
@@ -152,6 +160,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'app.about.supportEmail': 'player.ttrpg@gmail.com',
|
||||
'app.about.websiteLabel': 'САЙТ И ОБНОВЛЕНИЯ',
|
||||
'app.about.websiteUrl': 'https://ttrpgplayer.ru/',
|
||||
'app.about.sponsorsLabel': 'СПОНСОРЫ',
|
||||
|
||||
'help.title': 'Инструкция',
|
||||
'help.navAria': 'Разделы инструкции',
|
||||
@@ -676,6 +685,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'dice.noResult': 'Пока нет броска',
|
||||
'dice.inc': 'Больше',
|
||||
'dice.dec': 'Меньше',
|
||||
'dice.showAnimationToPlayers': 'Показать анимацию игрокам',
|
||||
|
||||
'sessionTimer.duration': 'Длительность (MM:SS)',
|
||||
'sessionTimer.start': 'Старт',
|
||||
@@ -877,12 +887,20 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'menu.aboutLicense': 'About license',
|
||||
'menu.checkUpdates': 'Check for updates',
|
||||
'menu.presentationSettings': 'Presentation display',
|
||||
'menu.gameSystems': 'Game systems',
|
||||
'menu.language': 'Language',
|
||||
'menu.langRu': 'Русский',
|
||||
'menu.langEn': 'English',
|
||||
'menu.aboutProgram': 'About',
|
||||
'menu.instructions': 'Instructions',
|
||||
|
||||
'gameSystems.title': 'Game systems',
|
||||
'gameSystems.import': 'Import…',
|
||||
'gameSystems.preview': 'Preview',
|
||||
'gameSystems.remove': 'Remove',
|
||||
'gameSystems.empty': 'No imported systems',
|
||||
'gameSystems.close': 'Close',
|
||||
|
||||
'top.aboutApp': 'About',
|
||||
|
||||
'app.about.title': 'About',
|
||||
@@ -897,6 +915,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'app.about.supportEmail': 'player.ttrpg@gmail.com',
|
||||
'app.about.websiteLabel': 'WEBSITE & UPDATES',
|
||||
'app.about.websiteUrl': 'https://ttrpgplayer.ru/',
|
||||
'app.about.sponsorsLabel': 'SPONSORS',
|
||||
|
||||
'help.title': 'Instructions',
|
||||
'help.navAria': 'Instruction sections',
|
||||
@@ -1423,6 +1442,7 @@ export const EDITOR_MESSAGES: Record<EditorLocale, Record<string, string>> = {
|
||||
'dice.noResult': 'No roll yet',
|
||||
'dice.inc': 'More',
|
||||
'dice.dec': 'Fewer',
|
||||
'dice.showAnimationToPlayers': 'Show animation to players',
|
||||
|
||||
'sessionTimer.duration': 'Duration (MM:SS)',
|
||||
'sessionTimer.start': 'Start',
|
||||
|
||||
Reference in New Issue
Block a user