feat: persist local player profile

This commit is contained in:
emil28092005
2026-09-06 01:24:45 +03:00
parent 693330e7e8
commit 7f86bee413
3 changed files with 38 additions and 6 deletions
+16 -3
View File
@@ -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());
}
}
+16 -3
View File
@@ -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<NativeHost | null>(null)
const [java, setJava] = useState<JavaInstallation | null | undefined>(undefined)
const [profile, setProfile] = useState<ProfileInspection | null>(null)
@@ -119,7 +121,7 @@ function App() {
if (!('__TAURI_INTERNALS__' in window)) return
invoke<NativeHost>('native_host').then(setNativeHost).catch(() => setNativeHost(null))
invoke<NativeSettings>('load_settings')
.then((settings) => setRam(settings.memoryMb / 1024))
.then((settings) => { setRam(settings.memoryMb / 1024); setNickname(settings.nickname) })
.catch(() => undefined)
invoke<JavaInstallation | null>('detect_java')
.then(setJava)
@@ -135,11 +137,17 @@ function App() {
const updateRam = (memoryGb: number) => {
setRam(memoryGb)
if ('__TAURI_INTERNALS__' in window) {
invoke<NativeSettings>('save_settings', { settings: { memoryMb: memoryGb * 1024 } })
invoke<NativeSettings>('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<NativeSettings>('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() {
<div className="account-chip">
<span className="avatar">ES</span>
<span><strong>Émile</strong><small>offline-профиль</small></span>
<span><strong>{nickname}</strong><small>локальный профиль</small></span>
<ChevronRight size={16} />
</div>
</aside>
@@ -302,6 +310,11 @@ function App() {
<input type="range" min="3" max="12" value={ram} onChange={(e) => updateRam(Number(e.target.value))} />
<small>Для Aeronautics рекомендуется 6 ГБ</small>
</label>
<label className="text-setting">
<span><strong>Игровой ник</strong><small>Локальный профиль</small></span>
<input value={nickname} maxLength={16} onChange={(event) => setNickname(event.target.value)} onBlur={saveNickname} placeholder="Player" />
<small>Латинские буквы, цифры и _ · от 3 до 16 символов</small>
</label>
<div className="setting-row static">
<span><FolderOpen />Папка игры</span>
<small>{nativeHost ? 'В каталоге лаунчера' : 'Определяется…'}</small>
+6
View File
@@ -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) {