feat: persist local player profile
This commit is contained in:
@@ -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
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user