feat: persist launcher memory settings
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
- интерактивный интерфейс профилей и настроек;
|
- интерактивный интерфейс профилей и настроек;
|
||||||
- Tauri 2 native shell с безопасной командой `native_host`;
|
- Tauri 2 native shell с безопасной командой `native_host`;
|
||||||
|
- сохранение памяти профиля в локальном каталоге данных приложения;
|
||||||
- адаптивное окно для Windows, Linux и macOS;
|
- адаптивное окно для Windows, Linux и macOS;
|
||||||
- базовая структура, в которую будут добавлены манифесты, проверка файлов,
|
- базовая структура, в которую будут добавлены манифесты, проверка файлов,
|
||||||
Java 21 и запуск Minecraft.
|
Java 21 и запуск Minecraft.
|
||||||
|
|||||||
+33
-1
@@ -1,5 +1,6 @@
|
|||||||
mod manifest;
|
mod manifest;
|
||||||
mod profile;
|
mod profile;
|
||||||
|
mod settings;
|
||||||
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use tauri::{AppHandle, Manager};
|
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())
|
.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() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
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!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running ShaCraft Launcher");
|
.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
|
launcherVersion: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NativeSettings = {
|
||||||
|
memoryMb: number
|
||||||
|
}
|
||||||
|
|
||||||
const servers: Server[] = [
|
const servers: Server[] = [
|
||||||
{
|
{
|
||||||
id: 'aoc',
|
id: 'aoc',
|
||||||
@@ -89,8 +93,19 @@ function App() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!('__TAURI_INTERNALS__' in window)) return
|
if (!('__TAURI_INTERNALS__' in window)) return
|
||||||
invoke<NativeHost>('native_host').then(setNativeHost).catch(() => setNativeHost(null))
|
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 = () => {
|
const repair = () => {
|
||||||
if (selected.disabled) return
|
if (selected.disabled) return
|
||||||
setReady(false)
|
setReady(false)
|
||||||
@@ -230,7 +245,7 @@ function App() {
|
|||||||
</div>
|
</div>
|
||||||
<label className="range-setting">
|
<label className="range-setting">
|
||||||
<span><strong>Оперативная память</strong><b>{ram} ГБ</b></span>
|
<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>
|
<small>Для Aeronautics рекомендуется 6 ГБ</small>
|
||||||
</label>
|
</label>
|
||||||
<button className="setting-row"><span><FolderOpen />Папка игры</span><ChevronRight /></button>
|
<button className="setting-row"><span><FolderOpen />Папка игры</span><ChevronRight /></button>
|
||||||
|
|||||||
Reference in New Issue
Block a user