add settings, redesign settings panel
This commit is contained in:
@ -9,7 +9,7 @@
|
||||
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||
*/
|
||||
import path from 'path';
|
||||
import { app, BrowserWindow, shell, ipcMain } from 'electron';
|
||||
import { app, BrowserWindow, shell, ipcMain, Tray, Menu, nativeImage } from 'electron';
|
||||
import { autoUpdater } from 'electron-updater';
|
||||
import log from 'electron-log';
|
||||
import MenuBuilder from './menu';
|
||||
@ -58,6 +58,60 @@ class AppUpdater {
|
||||
}
|
||||
}
|
||||
|
||||
let launcherSettings = {
|
||||
autoLaunch: false,
|
||||
startInTray: false,
|
||||
closeToTray: true,
|
||||
};
|
||||
|
||||
const ensureTray = () => {
|
||||
if (tray) return;
|
||||
|
||||
const RESOURCES_PATH = app.isPackaged
|
||||
? path.join(process.resourcesPath, 'assets')
|
||||
: path.join(__dirname, '../../assets');
|
||||
|
||||
const iconPath = path.join(RESOURCES_PATH, 'icon.png');
|
||||
const image = nativeImage.createFromPath(iconPath);
|
||||
|
||||
tray = new Tray(image);
|
||||
tray.setToolTip('Popa Launcher');
|
||||
|
||||
const menu = Menu.buildFromTemplate([
|
||||
{
|
||||
label: 'Открыть',
|
||||
click: () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Выход',
|
||||
click: () => app.quit(),
|
||||
},
|
||||
]);
|
||||
|
||||
tray.setContextMenu(menu);
|
||||
|
||||
tray.on('double-click', () => {
|
||||
if (!mainWindow) return;
|
||||
mainWindow.show();
|
||||
mainWindow.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const applyLoginItemSettings = () => {
|
||||
// Работает на Windows/macOS. На Linux зависит от окружения.
|
||||
app.setLoginItemSettings({
|
||||
openAtLogin: launcherSettings.autoLaunch,
|
||||
openAsHidden: launcherSettings.startInTray, // чтобы стартовал скрытым
|
||||
});
|
||||
};
|
||||
|
||||
let tray: Tray | null = null;
|
||||
|
||||
let mainWindow: BrowserWindow | null = null;
|
||||
|
||||
ipcMain.on('ipc-example', async (event, arg) => {
|
||||
@ -101,6 +155,34 @@ const installExtensions = async () => {
|
||||
.catch(console.log);
|
||||
};
|
||||
|
||||
ipcMain.handle('apply-launcher-settings', (_e, payload) => {
|
||||
launcherSettings = {
|
||||
...launcherSettings,
|
||||
autoLaunch: Boolean(payload?.autoLaunch),
|
||||
startInTray: Boolean(payload?.startInTray),
|
||||
closeToTray: payload?.closeToTray === false ? false : true,
|
||||
};
|
||||
|
||||
applyLoginItemSettings();
|
||||
|
||||
// если попросили трей — убедимся что он есть
|
||||
if (launcherSettings.startInTray || launcherSettings.closeToTray) {
|
||||
ensureTray();
|
||||
}
|
||||
|
||||
// если окно уже создано и ещё не показано — решаем, показывать или нет
|
||||
if (mainWindow) {
|
||||
if (launcherSettings.startInTray) {
|
||||
// оставляем скрытым
|
||||
mainWindow.hide();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
});
|
||||
|
||||
const createWindow = async () => {
|
||||
if (isDebug) {
|
||||
await installExtensions();
|
||||
@ -130,6 +212,14 @@ const createWindow = async () => {
|
||||
},
|
||||
});
|
||||
|
||||
mainWindow.on('close', (e) => {
|
||||
if (launcherSettings.closeToTray) {
|
||||
e.preventDefault();
|
||||
ensureTray();
|
||||
mainWindow?.hide();
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.loadURL(resolveHtmlPath('index.html'));
|
||||
|
||||
mainWindow.on('ready-to-show', () => {
|
||||
@ -139,7 +229,10 @@ const createWindow = async () => {
|
||||
if (process.env.START_MINIMIZED) {
|
||||
mainWindow.minimize();
|
||||
} else {
|
||||
mainWindow.show();
|
||||
setTimeout(() => {
|
||||
if (!mainWindow) return;
|
||||
if (!launcherSettings.startInTray) mainWindow.show();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -19,6 +19,7 @@ export type Channels =
|
||||
| 'overall-progress'
|
||||
| 'stop-minecraft'
|
||||
| 'minecraft-started'
|
||||
| 'apply-launcher-settings'
|
||||
| 'minecraft-stopped';
|
||||
|
||||
const electronHandler = {
|
||||
|
||||
@ -100,14 +100,40 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
|
||||
};
|
||||
|
||||
const App = () => {
|
||||
const getInitialRoute = () => {
|
||||
try {
|
||||
const settingsRaw = localStorage.getItem('launcher_settings');
|
||||
const settings = settingsRaw ? JSON.parse(settingsRaw) : null;
|
||||
|
||||
if (!settings?.rememberLastRoute) return ['/'];
|
||||
|
||||
const saved = localStorage.getItem('last_route');
|
||||
return [saved || '/'];
|
||||
} catch {
|
||||
return ['/'];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<Router initialEntries={getInitialRoute()}>
|
||||
<AppLayout />
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
const AppLayout = () => {
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
const settingsRaw = localStorage.getItem('launcher_settings');
|
||||
const settings = settingsRaw ? JSON.parse(settingsRaw) : null;
|
||||
if (!settings?.rememberLastRoute) return;
|
||||
|
||||
localStorage.setItem('last_route', location.pathname);
|
||||
} catch {}
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffect(() => {
|
||||
const applySettings = () => {
|
||||
try {
|
||||
@ -116,15 +142,8 @@ const AppLayout = () => {
|
||||
|
||||
const settings = JSON.parse(raw);
|
||||
|
||||
document.body.classList.toggle(
|
||||
'reduce-motion',
|
||||
Boolean(settings.reduceMotion),
|
||||
);
|
||||
|
||||
document.body.classList.toggle(
|
||||
'no-blur',
|
||||
settings.blurEffects === false,
|
||||
);
|
||||
document.body.classList.toggle('reduce-motion', Boolean(settings.reduceMotion));
|
||||
document.body.classList.toggle('no-blur', settings.blurEffects === false);
|
||||
|
||||
const ui = document.getElementById('app-ui');
|
||||
if (ui && typeof settings.uiScale === 'number') {
|
||||
@ -140,12 +159,33 @@ const AppLayout = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const pushLauncherSettingsToMain = async () => {
|
||||
try {
|
||||
const raw = localStorage.getItem('launcher_settings');
|
||||
const s = raw ? JSON.parse(raw) : null;
|
||||
|
||||
await window.electron.ipcRenderer.invoke('apply-launcher-settings', {
|
||||
autoLaunch: Boolean(s?.autoLaunch),
|
||||
startInTray: Boolean(s?.startInTray),
|
||||
closeToTray: s?.closeToTray !== false,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to push launcher settings to main', e);
|
||||
}
|
||||
};
|
||||
|
||||
// применяем при загрузке
|
||||
applySettings();
|
||||
pushLauncherSettingsToMain();
|
||||
|
||||
// применяем после сохранения настроек
|
||||
window.addEventListener('settings-updated', applySettings);
|
||||
return () => window.removeEventListener('settings-updated', applySettings);
|
||||
window.addEventListener('settings-updated', pushLauncherSettingsToMain);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('settings-updated', applySettings);
|
||||
window.removeEventListener('settings-updated', pushLauncherSettingsToMain);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Просто используйте window.open без useNavigate
|
||||
@ -153,7 +193,6 @@ const AppLayout = () => {
|
||||
window.open('https://account.ely.by/register', '_blank');
|
||||
};
|
||||
const [username, setUsername] = useState<string | null>(null);
|
||||
const location = useLocation();
|
||||
const path = location.pathname;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
152
src/renderer/components/CustomComponents/SettingCheckboxRow.tsx
Normal file
152
src/renderer/components/CustomComponents/SettingCheckboxRow.tsx
Normal file
@ -0,0 +1,152 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
|
||||
type Props = {
|
||||
title: string;
|
||||
description?: string;
|
||||
checked: boolean;
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const GRADIENT =
|
||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
||||
|
||||
export default function SettingCheckboxRow({
|
||||
title,
|
||||
description,
|
||||
checked,
|
||||
onChange,
|
||||
disabled,
|
||||
}: Props) {
|
||||
const toggle = () => {
|
||||
if (disabled) return;
|
||||
onChange(!checked);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
onClick={toggle}
|
||||
role="checkbox"
|
||||
aria-checked={checked}
|
||||
tabIndex={disabled ? -1 : 0}
|
||||
onKeyDown={(e) => {
|
||||
if (disabled) return;
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onChange(!checked);
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: '1vw',
|
||||
p: '0.9vw',
|
||||
borderRadius: '1.1vw',
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
userSelect: 'none',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
|
||||
background: checked
|
||||
? 'linear-gradient(120deg, rgba(242,113,33,0.12), rgba(233,64,205,0.10))'
|
||||
: 'rgba(255,255,255,0.04)',
|
||||
|
||||
border: checked
|
||||
? '1px solid rgba(233,64,205,0.45)'
|
||||
: '1px solid rgba(255,255,255,0.10)',
|
||||
|
||||
transition: 'all 0.18s ease',
|
||||
'&:hover': disabled
|
||||
? undefined
|
||||
: {
|
||||
background: checked
|
||||
? 'linear-gradient(120deg, rgba(242,113,33,0.15), rgba(233,64,205,0.12))'
|
||||
: 'rgba(255,255,255,0.06)',
|
||||
border: checked
|
||||
? '1px solid rgba(233,64,205,0.55)'
|
||||
: '1px solid rgba(255,255,255,0.14)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{/* text */}
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
sx={{
|
||||
fontFamily: 'Benzin-Bold',
|
||||
color: 'rgba(255,255,255,0.92)',
|
||||
fontSize: '1.3vw',
|
||||
lineHeight: 1.15,
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
|
||||
{description && (
|
||||
<Typography
|
||||
sx={{
|
||||
mt: '0.25vw',
|
||||
color: 'rgba(255,255,255,0.60)',
|
||||
fontWeight: 700,
|
||||
fontSize: '1vw',
|
||||
lineHeight: 1.2,
|
||||
}}
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* glass checkbox */}
|
||||
<Box
|
||||
sx={{
|
||||
flexShrink: 0,
|
||||
width: '2.6vw',
|
||||
height: '2.6vw',
|
||||
borderRadius: '0.75vw',
|
||||
background: 'rgba(0,0,0,0.22)',
|
||||
border: checked
|
||||
? '1px solid rgba(233,64,205,0.55)'
|
||||
: '1px solid rgba(255,255,255,0.14)',
|
||||
boxShadow: checked
|
||||
? '0 0.9vw 2.2vw rgba(233,64,205,0.18)'
|
||||
: '0 0.9vw 2.2vw rgba(0,0,0,0.25)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
}}
|
||||
>
|
||||
{/* check */}
|
||||
<Box
|
||||
sx={{
|
||||
borderRadius: '0.45vw',
|
||||
background: checked ? GRADIENT : 'rgba(255,255,255,0.08)',
|
||||
border: '1px solid rgba(255,255,255,0.10)',
|
||||
display: 'grid',
|
||||
placeItems: 'center',
|
||||
transform: checked ? 'scale(1)' : 'scale(0.92)',
|
||||
transition: 'transform 0.16s ease',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="14"
|
||||
height="14"
|
||||
viewBox="0 0 24 24"
|
||||
style={{
|
||||
opacity: checked ? 1 : 0,
|
||||
transition: 'opacity 0.14s ease',
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M20 6L9 17l-5-5"
|
||||
fill="none"
|
||||
stroke="white"
|
||||
strokeWidth="2.6"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@ -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