GraphicsManager: постобработка (bloom/FXAA/виньетка/цветокор/DoF) + тени/SSAO, 10 пресетов, mobile-safe, выкл по умолчанию. Новые материалы примитивов (chrome/water/iridescent + улучшены glass/neon). API game.graphics.*. Графика + Стартовый экран (Ken Burns) + Экран загрузки вынесены из Настроек в новый GameDecorModal, открываются из вкладки «Игра» (группа «Оформление»). Вики-раздел «Графика и эффекты» (GR1-GR4) + AI-контекст обновлён. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
441 lines
30 KiB
JavaScript
441 lines
30 KiB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
|
||
import cl from './GameSettingsModal.module.css';
|
||
import Icon from './Icon';
|
||
|
||
/**
|
||
* GameDecorModal — «оформление» игры: Графика и эффекты, Стартовый экран
|
||
* (Ken Burns), Экран загрузки. Вынесено из настроек во вкладку «Игра»
|
||
* (TopRibbon) тремя отдельными кнопками. Открывается с нужной секцией через
|
||
* проп `section` ('graphics' | 'startscreen' | 'loadingscreen'), вверху —
|
||
* табы для переключения между ними.
|
||
*
|
||
* Props:
|
||
* open — открыто ли
|
||
* section — какую секцию показать первой
|
||
* initial — { loading_screen:{...}, graphics:{...} } (из scene/проекта)
|
||
* onClose — закрыть без сохранения
|
||
* onSave(data) — data = { loading_screen, graphics } (как в GameSettingsModal)
|
||
*/
|
||
const MAX_IMG_BYTES = 500 * 1024;
|
||
|
||
const TABS = [
|
||
{ id: 'graphics', title: 'Графика', icon: 'sparkles' },
|
||
{ id: 'startscreen', title: 'Стартовый экран', icon: 'loader' },
|
||
{ id: 'loadingscreen', title: 'Экран загрузки', icon: 'loader' },
|
||
];
|
||
|
||
const GameDecorModal = ({ open, section = 'graphics', initial, onClose, onSave }) => {
|
||
const [tab, setTab] = useState(section);
|
||
|
||
// Экран загрузки
|
||
const [loadingLogo, setLoadingLogo] = useState('');
|
||
const [loadingAccent, setLoadingAccent] = useState('#ffc020');
|
||
const [loadingSpinner, setLoadingSpinner] = useState(true);
|
||
const [loadingSkip, setLoadingSkip] = useState(false);
|
||
// Стартовый Ken-Burns экран
|
||
const [lsEnabled, setLsEnabled] = useState(true);
|
||
const [lsBackground, setLsBackground] = useState('');
|
||
const [lsCover, setLsCover] = useState('');
|
||
const [lsStyle, setLsStyle] = useState('ken-burns');
|
||
const [lsPlaceName, setLsPlaceName] = useState('');
|
||
const [lsStudioName, setLsStudioName] = useState('');
|
||
const [lsVerified, setLsVerified] = useState(false);
|
||
const [lsDuration, setLsDuration] = useState(2.5);
|
||
const [lsProgressBar, setLsProgressBar] = useState(true);
|
||
// Графика
|
||
const [gfxPreset, setGfxPreset] = useState('off');
|
||
const [gfxBloom, setGfxBloom] = useState(false);
|
||
const [gfxFxaa, setGfxFxaa] = useState(false);
|
||
const [gfxVignette, setGfxVignette] = useState(false);
|
||
const [gfxSsao, setGfxSsao] = useState(false);
|
||
const [gfxSaturation, setGfxSaturation] = useState(1.0);
|
||
const [gfxContrast, setGfxContrast] = useState(1.0);
|
||
const [error, setError] = useState('');
|
||
|
||
const logoInputRef = useRef(null);
|
||
const lsBgInputRef = useRef(null);
|
||
const lsCoverInputRef = useRef(null);
|
||
|
||
useEffect(() => {
|
||
if (!open) return;
|
||
setTab(section || 'graphics');
|
||
const ls = initial?.loading_screen || {};
|
||
setLoadingLogo(ls.logo || '');
|
||
setLoadingAccent(ls.accentColor || '#ffc020');
|
||
setLoadingSpinner(ls.defaultSpinner !== false);
|
||
setLoadingSkip(!!ls.defaultSkipButton);
|
||
setLsEnabled(ls.enabled !== false);
|
||
setLsBackground(ls.background || '');
|
||
setLsCover(ls.cover || '');
|
||
setLsStyle(ls.style || 'ken-burns');
|
||
setLsPlaceName(ls.placeName || '');
|
||
setLsStudioName(ls.studioName || '');
|
||
setLsVerified(!!ls.verified);
|
||
setLsDuration(Number.isFinite(ls.duration) && ls.duration > 0 ? ls.duration : 2.5);
|
||
setLsProgressBar(ls.progressBar !== false);
|
||
const gx = initial?.graphics || {};
|
||
setGfxPreset(gx.preset || 'off');
|
||
setGfxBloom(!!(gx.bloom && gx.bloom.enabled));
|
||
setGfxFxaa(!!gx.fxaa);
|
||
setGfxVignette(!!(gx.vignette && gx.vignette.enabled));
|
||
setGfxSsao(!!gx.ssao);
|
||
setGfxSaturation((gx.grading && Number.isFinite(gx.grading.saturation)) ? gx.grading.saturation : 1.0);
|
||
setGfxContrast((gx.grading && Number.isFinite(gx.grading.contrast)) ? gx.grading.contrast : 1.0);
|
||
setError('');
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [open, section]);
|
||
|
||
if (!open) return null;
|
||
|
||
const handleLogoSelect = (e) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Логотип: только PNG, JPG или WEBP'); return; }
|
||
if (file.size > MAX_IMG_BYTES) { setError('Логотип слишком большой (макс. 500 КБ)'); return; }
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => { setLoadingLogo(ev.target.result); setError(''); };
|
||
reader.readAsDataURL(file);
|
||
};
|
||
const handleLsImage = (e, setter) => {
|
||
const file = e.target.files?.[0];
|
||
if (!file) return;
|
||
if (!/^image\/(png|jpeg|webp)$/.test(file.type)) { setError('Только PNG, JPG или WEBP'); return; }
|
||
if (file.size > MAX_IMG_BYTES) { setError('Изображение слишком большое (макс. 500 КБ)'); return; }
|
||
const reader = new FileReader();
|
||
reader.onload = (ev) => { setter(ev.target.result); setError(''); };
|
||
reader.readAsDataURL(file);
|
||
};
|
||
|
||
const applyPresetToToggles = (preset) => {
|
||
setGfxPreset(preset);
|
||
const P = {
|
||
off: { b: false, f: false, v: false, s: false, sat: 1.0, con: 1.0 },
|
||
low: { b: true, f: true, v: false, s: false, sat: 1.0, con: 1.0 },
|
||
medium: { b: true, f: true, v: true, s: false, sat: 1.1, con: 1.05 },
|
||
high: { b: true, f: true, v: true, s: true, sat: 1.2, con: 1.1 },
|
||
ultra: { b: true, f: true, v: true, s: true, sat: 1.25, con: 1.12 },
|
||
cinematic: { b: true, f: true, v: true, s: true, sat: 1.05, con: 1.18 },
|
||
vivid: { b: true, f: true, v: false, s: false, sat: 1.5, con: 1.1 },
|
||
night: { b: true, f: true, v: true, s: true, sat: 0.85, con: 1.2 },
|
||
retro: { b: false, f: false, v: true, s: false, sat: 0.7, con: 1.3 },
|
||
soft: { b: true, f: true, v: true, s: false, sat: 1.05, con: 0.95 },
|
||
}[preset];
|
||
if (!P) return;
|
||
setGfxBloom(P.b); setGfxFxaa(P.f); setGfxVignette(P.v);
|
||
setGfxSsao(P.s); setGfxSaturation(P.sat); setGfxContrast(P.con);
|
||
};
|
||
|
||
const handleSubmit = (e) => {
|
||
e.preventDefault();
|
||
onSave({
|
||
loading_screen: {
|
||
logo: loadingLogo || null,
|
||
accentColor: loadingAccent || '#ffc020',
|
||
defaultSpinner: loadingSpinner,
|
||
defaultSkipButton: loadingSkip,
|
||
enabled: lsEnabled,
|
||
background: lsBackground || null,
|
||
cover: lsCover || null,
|
||
style: lsStyle || 'ken-burns',
|
||
placeName: lsPlaceName.trim(),
|
||
studioName: lsStudioName.trim(),
|
||
verified: lsVerified,
|
||
duration: Math.max(1, Math.min(10, Number(lsDuration) || 2.5)),
|
||
progressBar: lsProgressBar,
|
||
},
|
||
graphics: {
|
||
preset: gfxPreset,
|
||
bloom: { enabled: gfxBloom },
|
||
fxaa: gfxFxaa,
|
||
vignette: { enabled: gfxVignette },
|
||
ssao: gfxSsao,
|
||
grading: {
|
||
enabled: (gfxSaturation !== 1.0 || gfxContrast !== 1.0),
|
||
saturation: Number(gfxSaturation) || 1.0,
|
||
contrast: Number(gfxContrast) || 1.0,
|
||
},
|
||
},
|
||
});
|
||
};
|
||
|
||
return (
|
||
<div className={cl.overlay} onClick={(e) => { if (e.target === e.currentTarget) onClose(); }}>
|
||
<div className={cl.modal}>
|
||
<div className={cl.header}>
|
||
<div className={cl.title}><Icon name="sparkles" size={16} /> Оформление игры</div>
|
||
<button className={cl.closeBtn} onClick={onClose}><Icon name="close" size={14} /></button>
|
||
</div>
|
||
|
||
{/* Табы разделов */}
|
||
<div style={{ display: 'flex', gap: 6, padding: '0 18px', marginTop: 6, flexWrap: 'wrap' }}>
|
||
{TABS.map((t) => (
|
||
<button
|
||
key={t.id}
|
||
type="button"
|
||
onClick={() => setTab(t.id)}
|
||
style={{
|
||
display: 'flex', alignItems: 'center', gap: 6,
|
||
padding: '8px 14px', borderRadius: 8, cursor: 'pointer',
|
||
border: '1px solid ' + (tab === t.id ? 'rgba(120,150,255,0.6)' : 'rgba(255,255,255,0.10)'),
|
||
background: tab === t.id ? 'rgba(90,120,255,0.18)' : 'transparent',
|
||
color: tab === t.id ? '#dfe6ff' : '#aab', fontSize: 13, fontWeight: 600,
|
||
}}
|
||
>
|
||
<Icon name={t.icon} size={13} /> {t.title}
|
||
</button>
|
||
))}
|
||
</div>
|
||
|
||
<form onSubmit={handleSubmit}>
|
||
<div className={cl.body}>
|
||
|
||
{/* === ГРАФИКА === */}
|
||
{tab === 'graphics' && (
|
||
<div className={cl.field}>
|
||
<div className={cl.fieldHint} style={{ marginBottom: 10 }}>
|
||
Свечение, цветокоррекция, тени и сглаживание (как шейдеры).
|
||
По умолчанию выключено. На слабых устройствах тяжёлые эффекты
|
||
урезаются автоматически. Также управляется из скриптов (game.graphics).
|
||
</div>
|
||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, marginBottom: 12 }}>
|
||
<span style={{ fontSize: 12, color: '#aab' }}>Пресет</span>
|
||
<select className={cl.select} value={gfxPreset}
|
||
onChange={(e) => applyPresetToToggles(e.target.value)}>
|
||
<option value="off">Выключено</option>
|
||
<option value="low">Низкое (свечение)</option>
|
||
<option value="medium">Среднее</option>
|
||
<option value="high">Высокое</option>
|
||
<option value="ultra">Ультра (с глубиной резкости)</option>
|
||
<option value="cinematic">🎬 Кино</option>
|
||
<option value="vivid">🌈 Сочное</option>
|
||
<option value="night">🌙 Ночь</option>
|
||
<option value="retro">📺 Ретро</option>
|
||
<option value="soft">☁️ Мягкое</option>
|
||
</select>
|
||
</label>
|
||
<div className={cl.togglesRow}>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={gfxBloom} onChange={(e) => { setGfxBloom(e.target.checked); setGfxPreset('custom'); }} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Свечение (Bloom)</div>
|
||
<div className={cl.toggleHint}><span>Яркие объекты светятся</span></div>
|
||
</div>
|
||
</label>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={gfxFxaa} onChange={(e) => { setGfxFxaa(e.target.checked); setGfxPreset('custom'); }} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Сглаживание</div>
|
||
<div className={cl.toggleHint}><span>Убирает «лесенки» на краях</span></div>
|
||
</div>
|
||
</label>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={gfxVignette} onChange={(e) => { setGfxVignette(e.target.checked); setGfxPreset('custom'); }} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Виньетка</div>
|
||
<div className={cl.toggleHint}><span>Мягкое затемнение по краям</span></div>
|
||
</div>
|
||
</label>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={gfxSsao} onChange={(e) => { setGfxSsao(e.target.checked); setGfxPreset('custom'); }} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Контактные тени (SSAO)</div>
|
||
<div className={cl.toggleHint}><span>Затемнение в углах и стыках</span></div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 16, marginTop: 12, flexWrap: 'wrap' }}>
|
||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 160 }}>
|
||
<span style={{ fontSize: 12, color: '#aab' }}>Насыщенность: {gfxSaturation.toFixed(2)}</span>
|
||
<input type="range" min="0.5" max="2" step="0.05" value={gfxSaturation}
|
||
onChange={(e) => { setGfxSaturation(Number(e.target.value)); setGfxPreset('custom'); }} />
|
||
</label>
|
||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, flex: 1, minWidth: 160 }}>
|
||
<span style={{ fontSize: 12, color: '#aab' }}>Контраст: {gfxContrast.toFixed(2)}</span>
|
||
<input type="range" min="0.5" max="1.6" step="0.05" value={gfxContrast}
|
||
onChange={(e) => { setGfxContrast(Number(e.target.value)); setGfxPreset('custom'); }} />
|
||
</label>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* === СТАРТОВЫЙ ЭКРАН (Ken Burns) === */}
|
||
{tab === 'startscreen' && (
|
||
<div className={cl.field}>
|
||
<div className={cl.fieldHint} style={{ marginBottom: 10 }}>
|
||
Что видит игрок при входе в игру: размытый фон с медленным движением (Ken Burns), карточка-витрина по центру, название места и автор.
|
||
</div>
|
||
<label className={cl.toggleRow} style={{ marginBottom: 10 }}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={lsEnabled} onChange={(e) => setLsEnabled(e.target.checked)} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Показывать стартовый экран</div>
|
||
<div className={cl.toggleHint}><span>Если выключено — игрок сразу попадает в 3D-сцену</span></div>
|
||
</div>
|
||
</label>
|
||
{lsEnabled && (
|
||
<>
|
||
<div style={{ display: 'flex', gap: 14, marginBottom: 12, flexWrap: 'wrap' }}>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||
<div style={{
|
||
width: 130, height: 74, borderRadius: 8, background: '#15192a',
|
||
border: '1px solid rgba(255,255,255,0.12)', overflow: 'hidden',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
backgroundImage: lsBackground ? `url(${lsBackground})` : 'none',
|
||
backgroundSize: 'cover', backgroundPosition: 'center',
|
||
}}>
|
||
{!lsBackground && <span style={{ color: '#5a6178', fontSize: 11 }}>фон (размытый)</span>}
|
||
</div>
|
||
<button type="button" className={cl.actionBtn} onClick={() => lsBgInputRef.current?.click()}>
|
||
<Icon name="folder" size={14} /> Фон
|
||
</button>
|
||
{lsBackground && (
|
||
<button type="button" className={cl.removeBtn} style={{ alignSelf: 'flex-start' }} onClick={() => setLsBackground('')}>
|
||
<Icon name="close" size={13} /> Убрать
|
||
</button>
|
||
)}
|
||
<input ref={lsBgInputRef} type="file" accept="image/png,image/jpeg,image/webp"
|
||
style={{ display: 'none' }} onChange={(e) => handleLsImage(e, setLsBackground)} />
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||
<div style={{
|
||
width: 74, height: 74, borderRadius: 12, background: '#15192a',
|
||
border: '1px solid rgba(255,255,255,0.12)', overflow: 'hidden',
|
||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||
backgroundImage: lsCover ? `url(${lsCover})` : 'none',
|
||
backgroundSize: 'cover', backgroundPosition: 'center',
|
||
}}>
|
||
{!lsCover && <span style={{ color: '#5a6178', fontSize: 10, textAlign: 'center' }}>карточка</span>}
|
||
</div>
|
||
<button type="button" className={cl.actionBtn} onClick={() => lsCoverInputRef.current?.click()}>
|
||
<Icon name="folder" size={14} /> Карточка
|
||
</button>
|
||
{lsCover && (
|
||
<button type="button" className={cl.removeBtn} style={{ alignSelf: 'flex-start' }} onClick={() => setLsCover('')}>
|
||
<Icon name="close" size={13} /> Убрать
|
||
</button>
|
||
)}
|
||
<input ref={lsCoverInputRef} type="file" accept="image/png,image/jpeg,image/webp"
|
||
style={{ display: 'none' }} onChange={(e) => handleLsImage(e, setLsCover)} />
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, flex: 1, minWidth: 180 }}>
|
||
<input type="text" className={cl.input} placeholder="Название места (по умолчанию = название игры)"
|
||
value={lsPlaceName} maxLength={40}
|
||
onChange={(e) => setLsPlaceName(e.target.value)} />
|
||
<input type="text" className={cl.input} placeholder="Имя автора"
|
||
value={lsStudioName} maxLength={40}
|
||
onChange={(e) => setLsStudioName(e.target.value)} />
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={lsVerified} onChange={(e) => setLsVerified(e.target.checked)} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Галочка verified</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 14, alignItems: 'flex-end', flexWrap: 'wrap' }}>
|
||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||
<span style={{ fontSize: 12, color: '#aab' }}>Стиль анимации</span>
|
||
<select className={cl.input} value={lsStyle} onChange={(e) => setLsStyle(e.target.value)}>
|
||
<option value="ken-burns">Ken Burns (плавный pan+zoom)</option>
|
||
<option value="static">Статичный фон</option>
|
||
<option value="parallax">Параллакс (по мыши)</option>
|
||
<option value="particles">Частицы (искры)</option>
|
||
</select>
|
||
</label>
|
||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||
<span style={{ fontSize: 12, color: '#aab' }}>Длительность: {Number(lsDuration).toFixed(1)} с</span>
|
||
<input type="range" min="1" max="10" step="0.5" value={lsDuration}
|
||
onChange={(e) => setLsDuration(Number(e.target.value))}
|
||
style={{ width: 160 }} />
|
||
</label>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={lsProgressBar} onChange={(e) => setLsProgressBar(e.target.checked)} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Прогресс-бар</div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* === ЭКРАН ЗАГРУЗКИ === */}
|
||
{tab === 'loadingscreen' && (
|
||
<div className={cl.field}>
|
||
<div className={cl.fieldHint} style={{ marginBottom: 10 }}>
|
||
Логотип и цвет акцента для экранов загрузки между мирами (game.loading).
|
||
</div>
|
||
<div style={{ display: 'flex', gap: 14, alignItems: 'center', marginBottom: 12 }}>
|
||
<div style={{
|
||
width: 96, height: 54, borderRadius: 8, background: '#15192a',
|
||
border: '1px solid rgba(255,255,255,0.12)', display: 'flex',
|
||
alignItems: 'center', justifyContent: 'center', overflow: 'hidden', flex: '0 0 auto',
|
||
}}>
|
||
{loadingLogo
|
||
? <img src={loadingLogo} alt="Логотип" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
|
||
: <span style={{ color: '#5a6178', fontSize: 11 }}>лого = обложка</span>}
|
||
</div>
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||
<button type="button" className={cl.actionBtn} onClick={() => logoInputRef.current?.click()}>
|
||
<Icon name="folder" size={14} /> Логотип игры
|
||
</button>
|
||
{loadingLogo && (
|
||
<button type="button" className={cl.removeBtn} style={{ alignSelf: 'flex-start' }} onClick={() => setLoadingLogo('')}>
|
||
<Icon name="close" size={13} /> Убрать
|
||
</button>
|
||
)}
|
||
<input ref={logoInputRef} type="file" accept="image/png,image/jpeg,image/webp"
|
||
style={{ display: 'none' }} onChange={handleLogoSelect} />
|
||
</div>
|
||
<label style={{ display: 'flex', flexDirection: 'column', gap: 4, marginLeft: 'auto' }}>
|
||
<span style={{ fontSize: 12, color: '#aab' }}>Цвет акцента</span>
|
||
<input type="color" value={loadingAccent}
|
||
onChange={(e) => setLoadingAccent(e.target.value)}
|
||
style={{ width: 48, height: 32, border: 'none', background: 'none', cursor: 'pointer' }} />
|
||
</label>
|
||
</div>
|
||
<div className={cl.togglesRow}>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={loadingSpinner} onChange={(e) => setLoadingSpinner(e.target.checked)} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Спиннер</div>
|
||
<div className={cl.toggleHint}><span>Показывать «ЗАГРУЗКА» по умолчанию</span></div>
|
||
</div>
|
||
</label>
|
||
<label className={cl.toggleRow}>
|
||
<input type="checkbox" className={cl.toggle}
|
||
checked={loadingSkip} onChange={(e) => setLoadingSkip(e.target.checked)} />
|
||
<div className={cl.toggleText}>
|
||
<div className={cl.toggleTitle}>Кнопка «Пропустить»</div>
|
||
<div className={cl.toggleHint}><span>Показывать по умолчанию</span></div>
|
||
</div>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{error && <div className={cl.error}><Icon name="warning" size={14} /> {error}</div>}
|
||
</div>
|
||
|
||
<div className={cl.footer}>
|
||
<button type="button" className={cl.secondaryBtn} onClick={onClose}>Отмена</button>
|
||
<button type="submit" className={cl.primaryBtn}>
|
||
<Icon name="save" size={13} /> Сохранить
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default GameDecorModal;
|