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.
|
* `./src/main.js` using webpack. This gives us some performance wins.
|
||||||
*/
|
*/
|
||||||
import path from 'path';
|
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 { autoUpdater } from 'electron-updater';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import MenuBuilder from './menu';
|
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;
|
let mainWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
ipcMain.on('ipc-example', async (event, arg) => {
|
ipcMain.on('ipc-example', async (event, arg) => {
|
||||||
@ -101,6 +155,34 @@ const installExtensions = async () => {
|
|||||||
.catch(console.log);
|
.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 () => {
|
const createWindow = async () => {
|
||||||
if (isDebug) {
|
if (isDebug) {
|
||||||
await installExtensions();
|
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.loadURL(resolveHtmlPath('index.html'));
|
||||||
|
|
||||||
mainWindow.on('ready-to-show', () => {
|
mainWindow.on('ready-to-show', () => {
|
||||||
@ -139,7 +229,10 @@ const createWindow = async () => {
|
|||||||
if (process.env.START_MINIMIZED) {
|
if (process.env.START_MINIMIZED) {
|
||||||
mainWindow.minimize();
|
mainWindow.minimize();
|
||||||
} else {
|
} else {
|
||||||
mainWindow.show();
|
setTimeout(() => {
|
||||||
|
if (!mainWindow) return;
|
||||||
|
if (!launcherSettings.startInTray) mainWindow.show();
|
||||||
|
}, 2000);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -19,6 +19,7 @@ export type Channels =
|
|||||||
| 'overall-progress'
|
| 'overall-progress'
|
||||||
| 'stop-minecraft'
|
| 'stop-minecraft'
|
||||||
| 'minecraft-started'
|
| 'minecraft-started'
|
||||||
|
| 'apply-launcher-settings'
|
||||||
| 'minecraft-stopped';
|
| 'minecraft-stopped';
|
||||||
|
|
||||||
const electronHandler = {
|
const electronHandler = {
|
||||||
|
|||||||
@ -100,14 +100,40 @@ const AuthCheck = ({ children }: { children: ReactNode }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const App = () => {
|
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 (
|
return (
|
||||||
<Router>
|
<Router initialEntries={getInitialRoute()}>
|
||||||
<AppLayout />
|
<AppLayout />
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AppLayout = () => {
|
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(() => {
|
useEffect(() => {
|
||||||
const applySettings = () => {
|
const applySettings = () => {
|
||||||
try {
|
try {
|
||||||
@ -116,15 +142,8 @@ const AppLayout = () => {
|
|||||||
|
|
||||||
const settings = JSON.parse(raw);
|
const settings = JSON.parse(raw);
|
||||||
|
|
||||||
document.body.classList.toggle(
|
document.body.classList.toggle('reduce-motion', Boolean(settings.reduceMotion));
|
||||||
'reduce-motion',
|
document.body.classList.toggle('no-blur', settings.blurEffects === false);
|
||||||
Boolean(settings.reduceMotion),
|
|
||||||
);
|
|
||||||
|
|
||||||
document.body.classList.toggle(
|
|
||||||
'no-blur',
|
|
||||||
settings.blurEffects === false,
|
|
||||||
);
|
|
||||||
|
|
||||||
const ui = document.getElementById('app-ui');
|
const ui = document.getElementById('app-ui');
|
||||||
if (ui && typeof settings.uiScale === 'number') {
|
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();
|
applySettings();
|
||||||
|
pushLauncherSettingsToMain();
|
||||||
|
|
||||||
// применяем после сохранения настроек
|
// применяем после сохранения настроек
|
||||||
window.addEventListener('settings-updated', applySettings);
|
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
|
// Просто используйте window.open без useNavigate
|
||||||
@ -153,7 +193,6 @@ const AppLayout = () => {
|
|||||||
window.open('https://account.ely.by/register', '_blank');
|
window.open('https://account.ely.by/register', '_blank');
|
||||||
};
|
};
|
||||||
const [username, setUsername] = useState<string | null>(null);
|
const [username, setUsername] = useState<string | null>(null);
|
||||||
const location = useLocation();
|
|
||||||
const path = location.pathname;
|
const path = location.pathname;
|
||||||
|
|
||||||
useEffect(() => {
|
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 CustomNotification from '../components/Notifications/CustomNotification';
|
||||||
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
import type { NotificationPosition } from '../components/Notifications/CustomNotification';
|
||||||
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
|
import { isNotificationsEnabled, getNotifPositionFromSettings } from '../utils/notifications';
|
||||||
|
import SettingCheckboxRow from '../components/CustomComponents/SettingCheckboxRow';
|
||||||
|
|
||||||
type SettingsState = {
|
type SettingsState = {
|
||||||
// UI
|
// UI
|
||||||
@ -25,8 +26,9 @@ type SettingsState = {
|
|||||||
blurEffects: boolean;
|
blurEffects: boolean;
|
||||||
|
|
||||||
// Launcher / app
|
// Launcher / app
|
||||||
autoUpdate: boolean;
|
autoLaunch: boolean;
|
||||||
startInTray: boolean;
|
startInTray: boolean;
|
||||||
|
closeToTray: boolean;
|
||||||
|
|
||||||
// Game
|
// Game
|
||||||
autoRotateSkinViewer: boolean;
|
autoRotateSkinViewer: boolean;
|
||||||
@ -35,6 +37,9 @@ type SettingsState = {
|
|||||||
// Notifications
|
// Notifications
|
||||||
notifications: boolean;
|
notifications: boolean;
|
||||||
notificationPosition: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
|
notificationPosition: 'top-right' | 'top-center' | 'top-left' | 'bottom-right' | 'bottom-center' | 'bottom-left';
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
rememberLastRoute: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const STORAGE_KEY = 'launcher_settings';
|
const STORAGE_KEY = 'launcher_settings';
|
||||||
@ -42,19 +47,67 @@ const STORAGE_KEY = 'launcher_settings';
|
|||||||
const GRADIENT =
|
const GRADIENT =
|
||||||
'linear-gradient(71deg, #F27121 0%, #E940CD 70%, #8A2387 100%)';
|
'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 = {
|
const defaultSettings: SettingsState = {
|
||||||
uiScale: 100,
|
uiScale: 100,
|
||||||
reduceMotion: false,
|
reduceMotion: false,
|
||||||
blurEffects: true,
|
blurEffects: true,
|
||||||
|
|
||||||
autoUpdate: true,
|
|
||||||
startInTray: false,
|
startInTray: false,
|
||||||
|
autoLaunch: false,
|
||||||
|
closeToTray: true,
|
||||||
|
|
||||||
autoRotateSkinViewer: true,
|
autoRotateSkinViewer: true,
|
||||||
walkingSpeed: 0.5,
|
walkingSpeed: 0.5,
|
||||||
|
|
||||||
notifications: true,
|
notifications: true,
|
||||||
notificationPosition: 'bottom-center',
|
notificationPosition: 'bottom-center',
|
||||||
|
|
||||||
|
rememberLastRoute: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
function safeParseSettings(raw: string | null): SettingsState | null {
|
function safeParseSettings(raw: string | null): SettingsState | null {
|
||||||
@ -164,6 +217,40 @@ const mapNotifPosition = (
|
|||||||
return { vertical, horizontal };
|
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 Settings = () => {
|
||||||
const [lastSavedSettings, setLastSavedSettings] = useState<SettingsState>(() => {
|
const [lastSavedSettings, setLastSavedSettings] = useState<SettingsState>(() => {
|
||||||
if (typeof window === 'undefined') return defaultSettings;
|
if (typeof window === 'undefined') return defaultSettings;
|
||||||
@ -185,6 +272,11 @@ const Settings = () => {
|
|||||||
return safeParseSettings(localStorage.getItem(STORAGE_KEY)) ?? defaultSettings;
|
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(() => {
|
const dirty = useMemo(() => {
|
||||||
return JSON.stringify(settings) !== JSON.stringify(lastSavedSettings);
|
return JSON.stringify(settings) !== JSON.stringify(lastSavedSettings);
|
||||||
}, [settings, lastSavedSettings]);
|
}, [settings, lastSavedSettings]);
|
||||||
@ -234,40 +326,6 @@ const Settings = () => {
|
|||||||
document.body.classList.toggle('no-blur', !settings.blurEffects);
|
document.body.classList.toggle('no-blur', !settings.blurEffects);
|
||||||
}, [settings.reduceMotion, 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 = {
|
const controlSx = {
|
||||||
'& .MuiFormControlLabel-label': {
|
'& .MuiFormControlLabel-label': {
|
||||||
fontFamily: 'Benzin-Bold',
|
fontFamily: 'Benzin-Bold',
|
||||||
@ -395,39 +453,24 @@ const Settings = () => {
|
|||||||
max={120}
|
max={120}
|
||||||
step={5}
|
step={5}
|
||||||
onChange={(_, v) => setSettings((s) => ({ ...s, uiScale: v as number }))}
|
onChange={(_, v) => setSettings((s) => ({ ...s, uiScale: v as number }))}
|
||||||
sx={{
|
sx={SLIDER_SX}
|
||||||
mt: 0.4,
|
|
||||||
'& .MuiSlider-thumb': { boxShadow: '0 10px 22px rgba(0,0,0,0.45)' },
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
||||||
|
|
||||||
<FormControlLabel
|
<SettingCheckboxRow
|
||||||
control={
|
title="Уменьшить анимации"
|
||||||
<Switch
|
description="Отключить все анимации лаунчера"
|
||||||
checked={settings.reduceMotion}
|
checked={settings.reduceMotion}
|
||||||
onChange={(e) =>
|
onChange={setFlag('reduceMotion')}
|
||||||
setSettings((s) => ({ ...s, reduceMotion: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Уменьшить анимации"
|
|
||||||
sx={controlSx}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<SettingCheckboxRow
|
||||||
control={
|
title="Эффекты размытия (blur)"
|
||||||
<Switch
|
description="Компоненты будут прозрачными без размытия"
|
||||||
checked={settings.blurEffects}
|
checked={settings.blurEffects}
|
||||||
onChange={(e) =>
|
onChange={setFlag('blurEffects')}
|
||||||
setSettings((s) => ({ ...s, blurEffects: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Эффекты размытия (blur)"
|
|
||||||
sx={controlSx}
|
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Glass>
|
</Glass>
|
||||||
@ -436,17 +479,11 @@ const Settings = () => {
|
|||||||
<SectionTitle>Уведомления</SectionTitle>
|
<SectionTitle>Уведомления</SectionTitle>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
||||||
<FormControlLabel
|
<SettingCheckboxRow
|
||||||
control={
|
title="Включить уведомления"
|
||||||
<Switch
|
description="Уведомления о каких-либо действиях"
|
||||||
checked={settings.notifications}
|
checked={settings.notifications}
|
||||||
onChange={(e) =>
|
onChange={setFlag('notifications')}
|
||||||
setSettings((s) => ({ ...s, notifications: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Включить уведомления"
|
|
||||||
sx={controlSx}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<NotificationPositionPicker
|
<NotificationPositionPicker
|
||||||
@ -475,19 +512,12 @@ const Settings = () => {
|
|||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '2vw', minWidth: 0 }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '2vw', minWidth: 0 }}>
|
||||||
<Glass>
|
<Glass>
|
||||||
<SectionTitle>Игра</SectionTitle>
|
<SectionTitle>Игра</SectionTitle>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
||||||
<FormControlLabel
|
<SettingCheckboxRow
|
||||||
control={
|
title="Автоповорот персонажа в профиле"
|
||||||
<Switch
|
description="Прокрут игрового персонажа в профиле"
|
||||||
checked={settings.autoRotateSkinViewer}
|
checked={settings.autoRotateSkinViewer}
|
||||||
onChange={(e) =>
|
onChange={setFlag('autoRotateSkinViewer')}
|
||||||
setSettings((s) => ({ ...s, autoRotateSkinViewer: e.target.checked }))
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Автоповорот персонажа в профиле"
|
|
||||||
sx={controlSx}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
@ -500,13 +530,9 @@ const Settings = () => {
|
|||||||
max={1}
|
max={1}
|
||||||
step={0.05}
|
step={0.05}
|
||||||
onChange={(_, v) => setSettings((s) => ({ ...s, walkingSpeed: v as number }))}
|
onChange={(_, v) => setSettings((s) => ({ ...s, walkingSpeed: v as number }))}
|
||||||
sx={{ mt: 0.4 }}
|
sx={SLIDER_SX}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Typography sx={{ color: 'rgba(255,255,255,0.60)', fontWeight: 700, fontSize: '0.9vw' }}>
|
|
||||||
Эти значения можно прокинуть в Profile: autoRotate и walkingSpeed.
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Glass>
|
</Glass>
|
||||||
|
|
||||||
@ -514,54 +540,33 @@ const Settings = () => {
|
|||||||
<SectionTitle>Лаунчер</SectionTitle>
|
<SectionTitle>Лаунчер</SectionTitle>
|
||||||
|
|
||||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1.1vw' }}>
|
||||||
<FormControlLabel
|
<SettingCheckboxRow
|
||||||
control={
|
title="Запускать вместе с системой"
|
||||||
<Switch
|
description="Лаунчер будет запускаться при старте Windows"
|
||||||
checked={settings.autoUpdate}
|
checked={settings.autoLaunch}
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, autoUpdate: e.target.checked }))}
|
onChange={setFlag('autoLaunch')}
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Автообновление данных (где поддерживается)"
|
|
||||||
sx={controlSx}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormControlLabel
|
<SettingCheckboxRow
|
||||||
control={
|
title="Запускать свернутым (в трей)"
|
||||||
<Switch
|
description="Окно не показывается при старте"
|
||||||
checked={settings.startInTray}
|
checked={settings.startInTray}
|
||||||
onChange={(e) => setSettings((s) => ({ ...s, startInTray: e.target.checked }))}
|
onChange={setFlag('startInTray')}
|
||||||
/>
|
|
||||||
}
|
|
||||||
label="Запускать свернутым (в трей)"
|
|
||||||
sx={controlSx}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider sx={{ borderColor: 'rgba(255,255,255,0.10)' }} />
|
<SettingCheckboxRow
|
||||||
|
title="При закрытии сворачивать в трей"
|
||||||
|
description="Крестик не закрывает приложение полностью"
|
||||||
|
checked={settings.closeToTray}
|
||||||
|
onChange={setFlag('closeToTray')}
|
||||||
|
/>
|
||||||
|
|
||||||
<Button
|
<SettingCheckboxRow
|
||||||
onClick={() => {
|
title="Запоминать последнюю страницу"
|
||||||
// просто пример действия
|
description="После перезапуска откроется тот же раздел"
|
||||||
try {
|
checked={settings.rememberLastRoute}
|
||||||
localStorage.removeItem('launcher_cache');
|
onChange={setFlag('rememberLastRoute')}
|
||||||
} 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>
|
|
||||||
</Box>
|
</Box>
|
||||||
</Glass>
|
</Glass>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
Reference in New Issue
Block a user