add settings, redesign settings panel
This commit is contained in:
@ -17,6 +17,7 @@ import {
|
||||
import CustomNotification from '../components/Notifications/CustomNotification';
|
||||
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
||||
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
|
||||
import SettingCheckboxRow from '../components/CustomComponents/SettingCheckboxRow';
|
||||
|
||||
type SettingsState = {
|
||||
// UI
|
||||
@ -25,8 +26,9 @@ type SettingsState = {
|
||||
blurEffects: boolean;
|
||||
|
||||
// Launcher / app
|
||||
autoUpdate: boolean;
|
||||
autoLaunch: boolean;
|
||||
startInTray: boolean;
|
||||
closeToTray: boolean;
|
||||
|
||||
// Game
|
||||
autoRotateSkinViewer: boolean;
|
||||
@ -35,6 +37,9 @@ type SettingsState = {
|
||||
// Notifications
|
||||
notifications: boolean;
|
||||
notificationPosition: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
|
||||
|
||||
// Navigation
|
||||
rememberLastRoute: boolean;
|
||||
};
|
||||
|
||||
const STORAGE_KEY = 'launcher_settings';
|
||||
@ -42,19 +47,67 @@ const STORAGE_KEY = 'launcher_settings';
|
||||
const GRADIENT =
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
||||
|
||||
const SLIDER_SX = {
|
||||
mt: 0.6,
|
||||
|
||||
'& .MuiSlider-rail': {
|
||||
opacity: 1,
|
||||
height: '0.55vw',
|
||||
borderRadius: '999px',
|
||||
backgroundColor: 'rgba(255,255,255,0.10)',
|
||||
},
|
||||
|
||||
'& .MuiSlider-track': {
|
||||
height: '0.55vw',
|
||||
border: 'none',
|
||||
borderRadius: '999px',
|
||||
background:
|
||||
'linear-gradient(90deg, rgba(242,113,33,1) 0%, rgba(233,64,205,1) 55%, rgba(138,35,135,1) 100%)',
|
||||
boxShadow: '0 0.6vw 1.6vw rgba(233,64,205,0.18)',
|
||||
},
|
||||
|
||||
'& .MuiSlider-thumb': {
|
||||
width: '1.65vw',
|
||||
height: '1.65vw',
|
||||
borderRadius: '999px',
|
||||
backgroundColor: 'rgba(10,10,20,0.92)',
|
||||
border: '2px solid rgba(255,255,255,0.18)',
|
||||
boxShadow: '0 0 1.6vw rgba(233,64,205,0.35)',
|
||||
transition: 'transform 0.15s ease, box-shadow 0.15s ease',
|
||||
'&:before': { display: 'none' },
|
||||
'&:hover, &.Mui-focusVisible': {
|
||||
width: '1.95vw',
|
||||
height: '1.95vw',
|
||||
boxShadow: '0 0 2.2vw rgba(242,113,33,0.35)',
|
||||
},
|
||||
'&:active': { width: '1.95vw', height: '1.95vw', },
|
||||
},
|
||||
|
||||
'& .MuiSlider-valueLabel': {
|
||||
borderRadius: '999px',
|
||||
background: 'rgba(10,10,20,0.92)',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
backdropFilter: 'blur(12px)',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
},
|
||||
} as const;
|
||||
|
||||
const defaultSettings: SettingsState = {
|
||||
uiScale: 100,
|
||||
reduceMotion: false,
|
||||
blurEffects: true,
|
||||
|
||||
autoUpdate: true,
|
||||
startInTray: false,
|
||||
autoLaunch: false,
|
||||
closeToTray: true,
|
||||
|
||||
autoRotateSkinViewer: true,
|
||||
walkingSpeed: 0.5,
|
||||
|
||||
notifications: true,
|
||||
notificationPosition: 'bottom-center',
|
||||
|
||||
rememberLastRoute: true,
|
||||
};
|
||||
|
||||
function safeParseSettings(raw: string | null): SettingsState | null {
|
||||
@ -164,6 +217,40 @@ const mapNotifPosition = (
|
||||
return { vertical, horizontal };
|
||||
};
|
||||
|
||||
const SectionTitle = ({ children }: { children: string }) => (
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.25vw',
|
||||
lineHeight: 1.1,
|
||||
backgroundImage: GRADIENT,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: '0.9vw',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const Glass = ({ children }: { children: React.ReactNode }) => (
|
||||
<Paper
|
||||
className="glass glass--soft"
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: '1.2vw',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: '1.8vw' }}>{children}</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const Settings = () => {
|
||||
const [lastSavedSettings, setLastSavedSettings] = useState<SettingsState>(() => {
|
||||
if (typeof window === 'undefined') return defaultSettings;
|
||||
@ -185,6 +272,11 @@ const Settings = () => {
|
||||
return safeParseSettings(localStorage.getItem(STORAGE_KEY)) ?? defaultSettings;
|
||||
});
|
||||
|
||||
const setFlag =
|
||||
<K extends keyof SettingsState>(key: K) =>
|
||||
(v: SettingsState[K]) =>
|
||||
setSettings((s) => ({ ...s, [key]: v }));
|
||||
|
||||
const dirty = useMemo(() => {
|
||||
return JSON.stringify(settings) !== JSON.stringify(lastSavedSettings);
|
||||
}, [settings, lastSavedSettings]);
|
||||
@ -234,40 +326,6 @@ const Settings = () => {
|
||||
document.body.classList.toggle('no-blur', !settings.blurEffects);
|
||||
}, [settings.reduceMotion, settings.blurEffects]);
|
||||
|
||||
const SectionTitle = ({ children }: { children: string }) => (
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
fontSize: '1.25vw',
|
||||
lineHeight: 1.1,
|
||||
backgroundImage: GRADIENT,
|
||||
WebkitBackgroundClip: 'text',
|
||||
WebkitTextFillColor: 'transparent',
|
||||
mb: '0.9vw',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
|
||||
const Glass = ({ children }: { children: React.ReactNode }) => (
|
||||
<Paper
|
||||
className="glass glass--soft"
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderRadius: '1.2vw',
|
||||
overflow: 'hidden',
|
||||
background:
|
||||
'radial-gradient(circle at 10% 10%, rgba(242,113,33,0.14), transparent 55%), radial-gradient(circle at 90% 20%, rgba(233,64,205,0.12), transparent 55%), rgba(10,10,20,0.86)',
|
||||
border: '1px solid rgba(255,255,255,0.08)',
|
||||
boxShadow: '0 1.2vw 3.2vw rgba(0,0,0,0.55)',
|
||||
color: 'white',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ p: '1.8vw' }}>{children}</Box>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
const controlSx = {
|
||||
'& .MuiFormControlLabel-label': {
|
||||
fontFamily: 'Benzin-Bold',
|
||||
@ -395,39 +453,24 @@ const Settings = () => {
|
||||
max={120}
|
||||
step={5}
|
||||
onChange={(_, v) => setSettings((s) => ({ ...s, uiScale: v as number }))}
|
||||
sx={{
|
||||
mt: 0.4,
|
||||
'& .MuiSlider-thumb': { boxShadow: '0 10px 22px rgba(0,0,0,0.45)' },
|
||||
}}
|
||||
sx={SLIDER_SX}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.reduceMotion}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({ ...s, reduceMotion: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Уменьшить анимации"
|
||||
sx={controlSx}
|
||||
<SettingCheckboxRow
|
||||
title="Уменьшить анимации"
|
||||
description="Отключить все анимации лаунчера"
|
||||
checked={settings.reduceMotion}
|
||||
onChange={setFlag('reduceMotion')}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.blurEffects}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({ ...s, blurEffects: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Эффекты размытия (blur)"
|
||||
sx={controlSx}
|
||||
<SettingCheckboxRow
|
||||
title="Эффекты размытия (blur)"
|
||||
description="Компоненты будут прозрачными без размытия"
|
||||
checked={settings.blurEffects}
|
||||
onChange={setFlag('blurEffects')}
|
||||
/>
|
||||
</Box>
|
||||
</Glass>
|
||||
@ -436,17 +479,11 @@ const Settings = () => {
|
||||
<SectionTitle>Уведомления</SectionTitle>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.notifications}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({ ...s, notifications: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Включить уведомления"
|
||||
sx={controlSx}
|
||||
<SettingCheckboxRow
|
||||
title="Включить уведомления"
|
||||
description="Уведомления о каких-либо действиях"
|
||||
checked={settings.notifications}
|
||||
onChange={setFlag('notifications')}
|
||||
/>
|
||||
|
||||
<NotificationPositionPicker
|
||||
@ -475,19 +512,12 @@ const Settings = () => {
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '2vw', minWidth: 0 }}>
|
||||
<Glass>
|
||||
<SectionTitle>Игра</SectionTitle>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.autoRotateSkinViewer}
|
||||
onChange={(e) =>
|
||||
setSettings((s) => ({ ...s, autoRotateSkinViewer: e.target.checked }))
|
||||
}
|
||||
/>
|
||||
}
|
||||
label="Автоповорот персонажа в профиле"
|
||||
sx={controlSx}
|
||||
<SettingCheckboxRow
|
||||
title="Автоповорот персонажа в профиле"
|
||||
description="Прокрут игрового персонажа в профиле"
|
||||
checked={settings.autoRotateSkinViewer}
|
||||
onChange={setFlag('autoRotateSkinViewer')}
|
||||
/>
|
||||
|
||||
<Box>
|
||||
@ -500,13 +530,9 @@ const Settings = () => {
|
||||
max={1}
|
||||
step={0.05}
|
||||
onChange={(_, v) => setSettings((s) => ({ ...s, walkingSpeed: v as number }))}
|
||||
sx={{ mt: 0.4 }}
|
||||
sx={SLIDER_SX}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.60)', fontWeight: 700, fontSize: '0.9vw' }}>
|
||||
Эти значения можно прокинуть в Profile: autoRotate и walkingSpeed.
|
||||
</Typography>
|
||||
</Box>
|
||||
</Glass>
|
||||
|
||||
@ -514,54 +540,33 @@ const Settings = () => {
|
||||
<SectionTitle>Лаунчер</SectionTitle>
|
||||
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.autoUpdate}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, autoUpdate: e.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="Автообновление данных (где поддерживается)"
|
||||
sx={controlSx}
|
||||
<SettingCheckboxRow
|
||||
title="Запускать вместе с системой"
|
||||
description="Лаунчер будет запускаться при старте Windows"
|
||||
checked={settings.autoLaunch}
|
||||
onChange={setFlag('autoLaunch')}
|
||||
/>
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={settings.startInTray}
|
||||
onChange={(e) => setSettings((s) => ({ ...s, startInTray: e.target.checked }))}
|
||||
/>
|
||||
}
|
||||
label="Запускать свернутым (в трей)"
|
||||
sx={controlSx}
|
||||
<SettingCheckboxRow
|
||||
title="Запускать свернутым (в трей)"
|
||||
description="Окно не показывается при старте"
|
||||
checked={settings.startInTray}
|
||||
onChange={setFlag('startInTray')}
|
||||
/>
|
||||
|
||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
||||
<SettingCheckboxRow
|
||||
title="При закрытии сворачивать в трей"
|
||||
description="Крестик не закрывает приложение полностью"
|
||||
checked={settings.closeToTray}
|
||||
onChange={setFlag('closeToTray')}
|
||||
/>
|
||||
|
||||
<Button
|
||||
onClick={() => {
|
||||
// просто пример действия
|
||||
try {
|
||||
localStorage.removeItem('launcher_cache');
|
||||
} catch {}
|
||||
}}
|
||||
disableRipple
|
||||
sx={{
|
||||
borderRadius: '999px',
|
||||
py: '0.8vw',
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: '#fff',
|
||||
background: 'rgba(255,255,255,0.10)',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
'&:hover': { background: 'rgba(255,255,255,0.14)' },
|
||||
}}
|
||||
>
|
||||
Очистить кэш (пример)
|
||||
</Button>
|
||||
|
||||
<Typography sx={{ color: 'rgba(255,255,255,0.60)', fontWeight: 700, fontSize: '0.9vw' }}>
|
||||
Кнопка-заглушка: можно подключить к вашим реальным ключам localStorage.
|
||||
</Typography>
|
||||
<SettingCheckboxRow
|
||||
title="Запоминать последнюю страницу"
|
||||
description="После перезапуска откроется тот же раздел"
|
||||
checked={settings.rememberLastRoute}
|
||||
onChange={setFlag('rememberLastRoute')}
|
||||
/>
|
||||
</Box>
|
||||
</Glass>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user