diff --git a/src-tauri/src/settings.rs b/src-tauri/src/settings.rs index e943d7a..66c1355 100644 --- a/src-tauri/src/settings.rs +++ b/src-tauri/src/settings.rs @@ -5,16 +5,23 @@ const SETTINGS_FILE: &str = "settings.json"; const MIN_MEMORY_MB: u16 = 3 * 1024; const MAX_MEMORY_MB: u16 = 12 * 1024; const DEFAULT_MEMORY_MB: u16 = 6 * 1024; +const DEFAULT_NICKNAME: &str = "Emil"; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LauncherSettings { pub memory_mb: u16, + #[serde(default = "default_nickname")] + pub nickname: String, +} + +fn default_nickname() -> String { + DEFAULT_NICKNAME.into() } impl Default for LauncherSettings { fn default() -> Self { - Self { memory_mb: DEFAULT_MEMORY_MB } + Self { memory_mb: DEFAULT_MEMORY_MB, nickname: DEFAULT_NICKNAME.into() } } } @@ -23,6 +30,7 @@ pub enum SettingsError { Io(io::Error), InvalidJson(serde_json::Error), InvalidMemory, + InvalidNickname, } impl fmt::Display for SettingsError { @@ -31,6 +39,7 @@ impl fmt::Display for SettingsError { Self::Io(error) => write!(formatter, "Cannot access launcher settings: {error}"), Self::InvalidJson(error) => write!(formatter, "Cannot read launcher settings: {error}"), Self::InvalidMemory => write!(formatter, "Memory allocation must be between 3 and 12 GiB"), + Self::InvalidNickname => write!(formatter, "Nickname must be 3-16 ASCII letters, numbers, or underscores"), } } } @@ -63,6 +72,9 @@ fn validate(settings: &LauncherSettings) -> Result<(), SettingsError> { if !(MIN_MEMORY_MB..=MAX_MEMORY_MB).contains(&settings.memory_mb) || settings.memory_mb % 1024 != 0 { return Err(SettingsError::InvalidMemory); } + if !(3..=16).contains(&settings.nickname.len()) || !settings.nickname.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') { + return Err(SettingsError::InvalidNickname); + } Ok(()) } @@ -84,7 +96,7 @@ mod tests { let directory = temporary_directory(); assert_eq!(load(&directory).unwrap().memory_mb, 6 * 1024); - let saved = save(&directory, LauncherSettings { memory_mb: 8 * 1024 }).unwrap(); + let saved = save(&directory, LauncherSettings { memory_mb: 8 * 1024, nickname: "Emil".into() }).unwrap(); assert_eq!(saved.memory_mb, 8 * 1024); assert_eq!(load(&directory).unwrap().memory_mb, 8 * 1024); @@ -94,6 +106,7 @@ mod tests { #[test] fn rejects_unsafe_memory_values() { let directory = temporary_directory(); - assert!(save(&directory, LauncherSettings { memory_mb: 512 }).is_err()); + assert!(save(&directory, LauncherSettings { memory_mb: 512, nickname: "Emil".into() }).is_err()); + assert!(save(&directory, LauncherSettings { memory_mb: 6 * 1024, nickname: "невалидный".into() }).is_err()); } } diff --git a/src/main.tsx b/src/main.tsx index 82de3d2..1d99443 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -44,6 +44,7 @@ type NativeHost = { type NativeSettings = { memoryMb: number + nickname: string } type JavaInstallation = { @@ -96,6 +97,7 @@ function App() { const [ready, setReady] = useState(true) const [settingsOpen, setSettingsOpen] = useState(false) const [ram, setRam] = useState(6) + const [nickname, setNickname] = useState('Emil') const [nativeHost, setNativeHost] = useState(null) const [java, setJava] = useState(undefined) const [profile, setProfile] = useState(null) @@ -119,7 +121,7 @@ function App() { if (!('__TAURI_INTERNALS__' in window)) return invoke('native_host').then(setNativeHost).catch(() => setNativeHost(null)) invoke('load_settings') - .then((settings) => setRam(settings.memoryMb / 1024)) + .then((settings) => { setRam(settings.memoryMb / 1024); setNickname(settings.nickname) }) .catch(() => undefined) invoke('detect_java') .then(setJava) @@ -135,11 +137,17 @@ function App() { const updateRam = (memoryGb: number) => { setRam(memoryGb) if ('__TAURI_INTERNALS__' in window) { - invoke('save_settings', { settings: { memoryMb: memoryGb * 1024 } }) + invoke('save_settings', { settings: { memoryMb: memoryGb * 1024, nickname } }) .catch(() => undefined) } } + const saveNickname = () => { + if ('__TAURI_INTERNALS__' in window && /^[A-Za-z0-9_]{3,16}$/.test(nickname)) { + invoke('save_settings', { settings: { memoryMb: ram * 1024, nickname } }).catch(() => undefined) + } + } + const repair = async () => { if (selected.disabled) return if ('__TAURI_INTERNALS__' in window && selected.profileId) { @@ -218,7 +226,7 @@ function App() {
ES - Émileoffline-профиль + {nickname}локальный профиль
@@ -302,6 +310,11 @@ function App() { updateRam(Number(e.target.value))} /> Для Aeronautics рекомендуется 6 ГБ +
Папка игры {nativeHost ? 'В каталоге лаунчера' : 'Определяется…'} diff --git a/src/styles.css b/src/styles.css index ce1eac1..b123fc5 100644 --- a/src/styles.css +++ b/src/styles.css @@ -144,6 +144,12 @@ button:focus-visible, input:focus-visible { outline: 2px solid var(--green); out .setting-row.static { cursor: default; } .setting-row.static:hover { color: #abb3ac; } .setting-row small { color: #7f8a80; font-size: 11px; } +.text-setting { display: grid; gap: 9px; padding: 18px 0; border-bottom: 1px solid var(--line); } +.text-setting > span { display: flex; align-items: baseline; justify-content: space-between; } +.text-setting strong { font-size: 12px; } +.text-setting span small, .text-setting > small { color: #7f8a80; font-size: 11px; } +.text-setting input { width: 100%; box-sizing: border-box; background: #0d110e; color: #eef2ed; border: 1px solid #384239; padding: 10px 11px; font: 600 13px/1 Manrope, sans-serif; outline: none; } +.text-setting input:focus { border-color: var(--accent); } .drawer-note { position: absolute; left: 28px; right: 28px; bottom: 26px; padding: 13px 15px; background: #17231a; color: #8fb193; border-radius: 10px; font-size: 10px; } @media (max-width: 1160px) {