feat: persist launcher memory settings
This commit is contained in:
@@ -6,6 +6,7 @@
|
||||
|
||||
- интерактивный интерфейс профилей и настроек;
|
||||
- Tauri 2 native shell с безопасной командой `native_host`;
|
||||
- сохранение памяти профиля в локальном каталоге данных приложения;
|
||||
- адаптивное окно для Windows, Linux и macOS;
|
||||
- базовая структура, в которую будут добавлены манифесты, проверка файлов,
|
||||
Java 21 и запуск Minecraft.
|
||||
|
||||
+33
-1
@@ -1,5 +1,6 @@
|
||||
mod manifest;
|
||||
mod profile;
|
||||
mod settings;
|
||||
|
||||
use serde::Serialize;
|
||||
use tauri::{AppHandle, Manager};
|
||||
@@ -68,9 +69,40 @@ async fn sync_profile(app: AppHandle, manifest_json: String) -> Result<profile::
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn load_settings(app: AppHandle) -> Result<settings::LauncherSettings, String> {
|
||||
let data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?;
|
||||
tauri::async_runtime::spawn_blocking(move || settings::load(&data_dir))
|
||||
.await
|
||||
.map_err(|error| format!("Settings task failed: {error}"))?
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn save_settings(app: AppHandle, settings: settings::LauncherSettings) -> Result<settings::LauncherSettings, String> {
|
||||
let data_dir = app
|
||||
.path()
|
||||
.app_data_dir()
|
||||
.map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?;
|
||||
tauri::async_runtime::spawn_blocking(move || settings::save(&data_dir, settings))
|
||||
.await
|
||||
.map_err(|error| format!("Settings task failed: {error}"))?
|
||||
.map_err(|error| error.to_string())
|
||||
}
|
||||
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.invoke_handler(tauri::generate_handler![native_host, validate_manifest, inspect_profile, sync_profile])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
native_host,
|
||||
validate_manifest,
|
||||
inspect_profile,
|
||||
sync_profile,
|
||||
load_settings,
|
||||
save_settings
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running ShaCraft Launcher");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt, fs, io, path::Path};
|
||||
|
||||
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;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LauncherSettings {
|
||||
pub memory_mb: u16,
|
||||
}
|
||||
|
||||
impl Default for LauncherSettings {
|
||||
fn default() -> Self {
|
||||
Self { memory_mb: DEFAULT_MEMORY_MB }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum SettingsError {
|
||||
Io(io::Error),
|
||||
InvalidJson(serde_json::Error),
|
||||
InvalidMemory,
|
||||
}
|
||||
|
||||
impl fmt::Display for SettingsError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(data_dir: &Path) -> Result<LauncherSettings, SettingsError> {
|
||||
let path = data_dir.join(SETTINGS_FILE);
|
||||
let source = match fs::read_to_string(path) {
|
||||
Ok(source) => source,
|
||||
Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(LauncherSettings::default()),
|
||||
Err(error) => return Err(SettingsError::Io(error)),
|
||||
};
|
||||
let settings = serde_json::from_str(&source).map_err(SettingsError::InvalidJson)?;
|
||||
validate(&settings)?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
pub fn save(data_dir: &Path, settings: LauncherSettings) -> Result<LauncherSettings, SettingsError> {
|
||||
validate(&settings)?;
|
||||
fs::create_dir_all(data_dir).map_err(SettingsError::Io)?;
|
||||
|
||||
let target = data_dir.join(SETTINGS_FILE);
|
||||
let temporary = data_dir.join(".settings.json.shacraft.part");
|
||||
let contents = serde_json::to_vec_pretty(&settings).expect("LauncherSettings is serializable");
|
||||
fs::write(&temporary, contents).map_err(SettingsError::Io)?;
|
||||
fs::rename(temporary, target).map_err(SettingsError::Io)?;
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{load, save, LauncherSettings};
|
||||
use std::{fs, process, time::{SystemTime, UNIX_EPOCH}};
|
||||
|
||||
fn temporary_directory() -> std::path::PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"shacraft-settings-test-{}-{}",
|
||||
process::id(),
|
||||
SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos()
|
||||
))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn defaults_then_persists_memory() {
|
||||
let directory = temporary_directory();
|
||||
assert_eq!(load(&directory).unwrap().memory_mb, 6 * 1024);
|
||||
|
||||
let saved = save(&directory, LauncherSettings { memory_mb: 8 * 1024 }).unwrap();
|
||||
assert_eq!(saved.memory_mb, 8 * 1024);
|
||||
assert_eq!(load(&directory).unwrap().memory_mb, 8 * 1024);
|
||||
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsafe_memory_values() {
|
||||
let directory = temporary_directory();
|
||||
assert!(save(&directory, LauncherSettings { memory_mb: 512 }).is_err());
|
||||
}
|
||||
}
|
||||
+16
-1
@@ -41,6 +41,10 @@ type NativeHost = {
|
||||
launcherVersion: string
|
||||
}
|
||||
|
||||
type NativeSettings = {
|
||||
memoryMb: number
|
||||
}
|
||||
|
||||
const servers: Server[] = [
|
||||
{
|
||||
id: 'aoc',
|
||||
@@ -89,8 +93,19 @@ function App() {
|
||||
useEffect(() => {
|
||||
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))
|
||||
.catch(() => undefined)
|
||||
}, [])
|
||||
|
||||
const updateRam = (memoryGb: number) => {
|
||||
setRam(memoryGb)
|
||||
if ('__TAURI_INTERNALS__' in window) {
|
||||
invoke<NativeSettings>('save_settings', { settings: { memoryMb: memoryGb * 1024 } })
|
||||
.catch(() => undefined)
|
||||
}
|
||||
}
|
||||
|
||||
const repair = () => {
|
||||
if (selected.disabled) return
|
||||
setReady(false)
|
||||
@@ -230,7 +245,7 @@ function App() {
|
||||
</div>
|
||||
<label className="range-setting">
|
||||
<span><strong>Оперативная память</strong><b>{ram} ГБ</b></span>
|
||||
<input type="range" min="3" max="12" value={ram} onChange={(e) => setRam(Number(e.target.value))} />
|
||||
<input type="range" min="3" max="12" value={ram} onChange={(e) => updateRam(Number(e.target.value))} />
|
||||
<small>Для Aeronautics рекомендуется 6 ГБ</small>
|
||||
</label>
|
||||
<button className="setting-row"><span><FolderOpen />Папка игры</span><ChevronRight /></button>
|
||||
|
||||
Reference in New Issue
Block a user