diff --git a/README.md b/README.md index 1a9fcc0..f96e861 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ - Tauri 2 native shell с безопасной командой `native_host`; - сохранение памяти профиля в локальном каталоге данных приложения; - безопасное обнаружение установленной Java (включая `JAVA_HOME`) перед запуском; +- интеграция с фиксированным ShaCraft launcher v2 manifest для Aeronautics; - адаптивное окно для Windows, Linux и macOS; - базовая структура, в которую будут добавлены манифесты, проверка файлов, Java 21 и запуск Minecraft. diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 509cd52..459e94b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1,6 +1,7 @@ mod java; mod manifest; mod profile; +mod remote; mod settings; use serde::Serialize; @@ -76,6 +77,30 @@ async fn sync_profile(app: AppHandle, manifest_json: String) -> Result Result { + let data_dir = app.path().app_data_dir() + .map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?; + tauri::async_runtime::spawn_blocking(move || { + let manifest = remote::fetch_manifest(&profile_id).map_err(|error| error.to_string())?; + profile::inspect(&data_dir.join("profiles").join(&manifest.id), &manifest) + .map_err(|error| error.to_string()) + }).await.map_err(|error| format!("Profile inspection task failed: {error}"))? +} + +/// Downloads missing or changed ShaCraft-managed files from the fixed v2 endpoint. +#[tauri::command] +async fn sync_remote_profile(app: AppHandle, profile_id: String) -> Result { + let data_dir = app.path().app_data_dir() + .map_err(|error| format!("Cannot resolve launcher data directory: {error}"))?; + tauri::async_runtime::spawn_blocking(move || { + let manifest = remote::fetch_manifest(&profile_id).map_err(|error| error.to_string())?; + profile::sync(&data_dir.join("profiles").join(&manifest.id), &manifest) + .map_err(|error| error.to_string()) + }).await.map_err(|error| format!("Profile synchronization task failed: {error}"))? +} + #[tauri::command] async fn load_settings(app: AppHandle) -> Result { let data_dir = app @@ -108,6 +133,8 @@ pub fn run() { validate_manifest, inspect_profile, sync_profile, + inspect_remote_profile, + sync_remote_profile, load_settings, save_settings ]) diff --git a/src-tauri/src/remote.rs b/src-tauri/src/remote.rs new file mode 100644 index 0000000..aa02500 --- /dev/null +++ b/src-tauri/src/remote.rs @@ -0,0 +1,48 @@ +use crate::manifest::{self, Manifest}; +use reqwest::{blocking::Client, redirect::Policy}; +use std::{fmt, time::Duration}; + +const AERONAUTICS_MANIFEST: &str = + "https://shacraft.ru/api/launcher/v2/profiles/aeronautics/manifest"; + +#[derive(Debug)] +pub enum RemoteError { + UnknownProfile, + Network(reqwest::Error), + Status(reqwest::StatusCode), + TooLarge, + InvalidManifest(manifest::ManifestError), +} + +impl fmt::Display for RemoteError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnknownProfile => formatter.write_str("Unknown ShaCraft profile"), + Self::Network(error) => write!(formatter, "Cannot load ShaCraft manifest: {error}"), + Self::Status(status) => write!(formatter, "ShaCraft manifest request failed: {status}"), + Self::TooLarge => formatter.write_str("ShaCraft manifest is too large"), + Self::InvalidManifest(error) => write!(formatter, "ShaCraft manifest is invalid: {error}"), + } + } +} + +pub fn fetch_manifest(profile_id: &str) -> Result { + let url = match profile_id { + "aeronautics" => AERONAUTICS_MANIFEST, + _ => return Err(RemoteError::UnknownProfile), + }; + let client = Client::builder() + .timeout(Duration::from_secs(30)) + .redirect(Policy::none()) + .build() + .map_err(RemoteError::Network)?; + let response = client.get(url).send().map_err(RemoteError::Network)?; + if !response.status().is_success() { + return Err(RemoteError::Status(response.status())); + } + if response.content_length().is_some_and(|size| size > 2 * 1024 * 1024) { + return Err(RemoteError::TooLarge); + } + let source = response.text().map_err(RemoteError::Network)?; + manifest::validate_json(&source).map_err(RemoteError::InvalidManifest) +} diff --git a/src/main.tsx b/src/main.tsx index 9991454..82de3d2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -32,6 +32,7 @@ type Server = { version: string memory: string installed: boolean + profileId?: string disabled?: boolean } @@ -51,6 +52,19 @@ type JavaInstallation = { version: string } +type ProfileInspection = { + managedFiles: number + missingFiles: number + mismatchedFiles: number + upToDate: boolean +} + +type SyncResult = { + downloadedFiles: number + reusedFiles: number + downloadedBytes: number +} + const servers: Server[] = [ { id: 'aoc', @@ -61,6 +75,7 @@ const servers: Server[] = [ version: '1.21.1 · NeoForge', memory: '6 ГБ', installed: true, + profileId: 'aeronautics', }, { id: 'create', @@ -83,6 +98,9 @@ function App() { const [ram, setRam] = useState(6) const [nativeHost, setNativeHost] = useState(null) const [java, setJava] = useState(undefined) + const [profile, setProfile] = useState(null) + const [syncing, setSyncing] = useState(false) + const [syncError, setSyncError] = useState(null) useEffect(() => { if (progress === null) return @@ -106,6 +124,12 @@ function App() { invoke('detect_java') .then(setJava) .catch(() => setJava(null)) + invoke('inspect_remote_profile', { profileId: 'aeronautics' }) + .then((inspection) => { + setProfile(inspection) + setReady(inspection.upToDate) + }) + .catch(() => undefined) }, []) const updateRam = (memoryGb: number) => { @@ -116,8 +140,23 @@ function App() { } } - const repair = () => { + const repair = async () => { if (selected.disabled) return + if ('__TAURI_INTERNALS__' in window && selected.profileId) { + setSyncError(null) + setSyncing(true) + setReady(false) + try { + const result = await invoke('sync_remote_profile', { profileId: selected.profileId }) + setProfile({ managedFiles: result.downloadedFiles + result.reusedFiles, missingFiles: 0, mismatchedFiles: 0, upToDate: true }) + setReady(true) + } catch (error) { + setSyncError(error instanceof Error ? error.message : 'Не удалось синхронизировать сборку') + } finally { + setSyncing(false) + } + return + } setReady(false) setProgress(0) } @@ -205,7 +244,12 @@ function App() {
- {progress !== null ? ( + {syncing ? ( + <> + + Синхронизируем сборкуСкачиваем и проверяем файлы + + ) : progress !== null ? ( <> @@ -221,7 +265,7 @@ function App() { ) : ( <> - {ready ? 'Сборка готова' : 'Требуется проверка'}Обновлено сегодня в 21:42 + {ready ? 'Сборка готова' : 'Требуется проверка'}{syncError || (profile ? `${profile.managedFiles} файлов под контролем` : 'Проверяем локальные файлы')} )} {progress !== null &&
} @@ -232,16 +276,16 @@ function App() { {ram} ГБ памяти
-