add settings, redesign settings panel

This commit is contained in:
aurinex
2025-12-15 22:41:38 +05:00
parent 6adc64dab8
commit cd7ad5039e
5 changed files with 437 additions and 147 deletions

View File

@ -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>