Files
DndGamePlayer/app/renderer/editor/PresentationSettingsModal.tsx
T
Ivan Fontosh 746b862cb9 feat(editor): настройки экрана презентации при запуске сессии
Выбор монитора и полноэкранного режима в настройках; при «Запустить» окно презентации открывается на выбранном дисплее автоматически.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 13:05:50 +08:00

140 lines
4.8 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 type { DisplayInfo, PresentationSettings } from '../../shared/types/presentationSettings';
import { getDndApi } from '../shared/dndApi';
import { Button } from '../shared/ui/controls';
import styles from './EditorApp.module.css';
import { useEditorI18n } from './i18n/EditorI18nContext';
import modalStyles from './PresentationSettingsModal.module.css';
export function PresentationSettingsModal({
open,
onClose,
}: {
open: boolean;
onClose: () => void;
}) {
const { t } = useEditorI18n();
const api = getDndApi();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [displays, setDisplays] = useState<DisplayInfo[]>([]);
const [draft, setDraft] = useState<PresentationSettings>({ displayId: null, fullScreen: true });
useEffect(() => {
if (!open) return;
let cancelled = false;
setLoading(true);
void (async () => {
try {
const [{ displays: list }, { settings }] = await Promise.all([
api.invoke(ipcChannels.windows.listDisplays, {}),
api.invoke(ipcChannels.preferences.getPresentation, {}),
]);
if (cancelled) return;
setDisplays(list);
setDraft(settings);
} finally {
if (!cancelled) setLoading(false);
}
})();
return () => {
cancelled = true;
};
}, [api, open]);
if (!open) return null;
const save = async () => {
setSaving(true);
try {
const primaryId = displays.find((d) => d.isPrimary)?.id ?? null;
const settings: PresentationSettings = {
...draft,
displayId: draft.displayId === primaryId ? null : draft.displayId,
};
await api.invoke(ipcChannels.preferences.setPresentation, { settings });
onClose();
} finally {
setSaving(false);
}
};
const isDisplaySelected = (d: DisplayInfo) =>
draft.displayId === d.id || (draft.displayId === null && d.isPrimary);
const displayLabel = (d: DisplayInfo) => {
const primary = d.isPrimary ? ` (${t('settings.presentation.primary')})` : '';
return `${d.label}${primary} — ${d.width}×${d.height}`;
};
return createPortal(
<>
<div className={styles.modalBackdrop} aria-hidden onClick={onClose} />
<div
className={[styles.modalDialog, modalStyles.dialog].join(' ')}
role="dialog"
aria-modal="true"
data-testid="presentation-settings-modal"
>
<div className={styles.modalHeader}>
<div className={styles.modalTitle}>{t('settings.presentation.title')}</div>
<button
type="button"
aria-label={t('common.close')}
onClick={onClose}
className={styles.modalClose}
>
×
</button>
</div>
<div>
<p className={modalStyles.hint}>{t('settings.presentation.hint')}</p>
<section className={modalStyles.section}>
<div className={modalStyles.sectionTitle}>{t('settings.presentation.display')}</div>
{loading ? (
<div className={modalStyles.loading}>{t('settings.presentation.loading')}</div>
) : (
<div className={modalStyles.list} role="radiogroup" aria-label={t('settings.presentation.display')}>
{displays.map((d) => (
<label key={d.id} className={modalStyles.row}>
<input
type="radio"
name="presentation-display"
checked={isDisplaySelected(d)}
onChange={() => setDraft((prev) => ({ ...prev, displayId: d.id }))}
data-testid={`presentation-display-${d.id}`}
/>
<span>{displayLabel(d)}</span>
</label>
))}
</div>
)}
</section>
<label className={modalStyles.fullScreenRow}>
<input
type="checkbox"
checked={draft.fullScreen}
onChange={(e) => setDraft((prev) => ({ ...prev, fullScreen: e.target.checked }))}
data-testid="presentation-fullscreen"
/>
<span>{t('settings.presentation.fullScreen')}</span>
</label>
</div>
<div className={styles.modalFooter}>
<Button variant="ghost" onClick={onClose} disabled={saving}>
{t('common.cancel')}
</Button>
<Button variant="primary" onClick={() => void save()} disabled={loading || saving} data-testid="presentation-settings-save">
{t('common.save')}
</Button>
</div>
</div>
</>,
document.body,
);
}