diff options
| author | 2026-01-13 15:37:55 +0800 | |
|---|---|---|
| committer | 2026-01-13 15:37:55 +0800 | |
| commit | 6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6 (patch) | |
| tree | bb84869afeb316e2510018e2ba33c651488f3e71 | |
| parent | b7e7f8de3d2200ef34510cda3601a50f62af798d (diff) | |
| download | DropOut-6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6.tar.gz DropOut-6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6.zip | |
feat: add offline account management and version fetching functionality
| -rw-r--r-- | src-tauri/Cargo.toml | 1 | ||||
| -rw-r--r-- | src-tauri/src/core/auth.rs | 28 | ||||
| -rw-r--r-- | src-tauri/src/core/downloader.rs | 51 | ||||
| -rw-r--r-- | src-tauri/src/core/manifest.rs | 8 | ||||
| -rw-r--r-- | src-tauri/src/core/mod.rs | 2 | ||||
| -rw-r--r-- | src-tauri/src/main.rs | 35 | ||||
| -rw-r--r-- | ui/src/App.svelte | 85 |
7 files changed, 197 insertions, 13 deletions
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 54a0d3d..0091ed4 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -13,6 +13,7 @@ reqwest = { version = "0.13.1", features = ["json", "blocking"] } serde_json = "1.0.149" tauri = { version = "2.0.0", features = [] } tauri-plugin-shell = "2.0.0" +uuid = { version = "1.10.0", features = ["v3", "v4", "serde"] } [build-dependencies] tauri-build = { version = "2.0.0", features = [] } diff --git a/src-tauri/src/core/auth.rs b/src-tauri/src/core/auth.rs new file mode 100644 index 0000000..39c4ce0 --- /dev/null +++ b/src-tauri/src/core/auth.rs @@ -0,0 +1,28 @@ +use serde::{Deserialize, Serialize}; +use std::sync::Mutex; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OfflineAccount { + pub username: String, + pub uuid: String, +} + +pub struct AccountState { + pub active_account: Mutex<Option<OfflineAccount>>, +} + +impl AccountState { + pub fn new() -> Self { + Self { + active_account: Mutex::new(None), + } + } +} + +pub fn generate_offline_uuid(username: &str) -> String { + // Generate a UUID v3 (MD5-based) using the username as the name + // This provides a consistent UUID for the same username + let namespace = Uuid::NAMESPACE_OID; + Uuid::new_v3(&namespace, username.as_bytes()).to_string() +} diff --git a/src-tauri/src/core/downloader.rs b/src-tauri/src/core/downloader.rs new file mode 100644 index 0000000..8d717be --- /dev/null +++ b/src-tauri/src/core/downloader.rs @@ -0,0 +1,51 @@ +use std::path::PathBuf; +use tokio::sync::mpsc; +use serde::{Serialize, Deserialize}; + +#[derive(Debug, Clone)] +pub struct DownloadTask { + pub url: String, + pub path: PathBuf, + pub sha1: Option<String>, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum DownloadProgress { + Started(String), + Progress { file: String, downloaded: u64, total: u64 }, + Finished(String), + Error(String, String), +} + +pub struct Downloader { + sender: mpsc::Sender<DownloadProgress>, +} + +impl Downloader { + pub fn new(sender: mpsc::Sender<DownloadProgress>) -> Self { + Self { sender } + } + + pub async fn download(&self, tasks: Vec<DownloadTask>) { + // TODO: Implement parallel download with limits + // Use futures::stream::StreamExt::buffer_unordered + + for task in tasks { + if let Err(_) = self.sender.send(DownloadProgress::Started(task.url.clone())).await { + break; + } + + // Simulate download for now or implement basic + // Ensure directory exists + if let Some(parent) = task.path.parent() { + let _ = tokio::fs::create_dir_all(parent).await; + } + + // Real implementation would use reqwest here + + if let Err(_) = self.sender.send(DownloadProgress::Finished(task.url)).await { + break; + } + } + } +} diff --git a/src-tauri/src/core/manifest.rs b/src-tauri/src/core/manifest.rs index 1450e77..11ebc5a 100644 --- a/src-tauri/src/core/manifest.rs +++ b/src-tauri/src/core/manifest.rs @@ -1,19 +1,19 @@ -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use std::error::Error; -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct VersionManifest { pub latest: Latest, pub versions: Vec<Version>, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Latest { pub release: String, pub snapshot: String, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct Version { pub id: String, #[serde(rename = "type")] diff --git a/src-tauri/src/core/mod.rs b/src-tauri/src/core/mod.rs index 640fc64..320ab82 100644 --- a/src-tauri/src/core/mod.rs +++ b/src-tauri/src/core/mod.rs @@ -1 +1,3 @@ pub mod manifest; +pub mod auth; +pub mod downloader; diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index fdd0794..402f58f 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -1,6 +1,8 @@ // Prevents additional console window on Windows in release, DO NOT REMOVE!! #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] +use tauri::State; + mod core; mod launcher; @@ -23,10 +25,41 @@ async fn start_game() -> Result<String, String> { } } +#[tauri::command] +async fn get_versions() -> Result<Vec<core::manifest::Version>, String> { + match core::manifest::fetch_version_manifest().await { + Ok(manifest) => Ok(manifest.versions), + Err(e) => Err(e.to_string()), + } +} + +#[tauri::command] +async fn login_offline( + state: State<'_, core::auth::AccountState>, + username: String, +) -> Result<core::auth::OfflineAccount, String> { + let uuid = core::auth::generate_offline_uuid(&username); + let account = core::auth::OfflineAccount { + username, + uuid, + }; + + *state.active_account.lock().unwrap() = Some(account.clone()); + Ok(account) +} + +#[tauri::command] +async fn get_active_account( + state: State<'_, core::auth::AccountState>, +) -> Result<Option<core::auth::OfflineAccount>, String> { + Ok(state.active_account.lock().unwrap().clone()) +} + fn main() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) - .invoke_handler(tauri::generate_handler![start_game]) + .manage(core::auth::AccountState::new()) + .invoke_handler(tauri::generate_handler![start_game, get_versions, login_offline, get_active_account]) .run(tauri::generate_context!()) .expect("error while running tauri application"); } diff --git a/ui/src/App.svelte b/ui/src/App.svelte index 292c719..18ef5e5 100644 --- a/ui/src/App.svelte +++ b/ui/src/App.svelte @@ -4,6 +4,65 @@ let status = "Ready"; + interface Version { + id: string; + type: string; + url: string; + time: string; + releaseTime: string; + } + + interface OfflineAccount { + username: string; + uuid: string; + } + + let versions: Version[] = []; + let selectedVersion = ""; + let currentAccount: OfflineAccount | null = null; + + onMount(async () => { + checkAccount(); + try { + versions = await invoke("get_versions"); + if (versions.length > 0) { + // Find latest release or default to first + const latest = versions.find(v => v.type === 'release'); + selectedVersion = latest ? latest.id : versions[0].id; + } + } catch (e) { + console.error("Failed to fetch versions:", e); + status = "Error fetching versions: " + e; + } + }); + + async function checkAccount() { + try { + const acc = await invoke("get_active_account"); + currentAccount = acc as OfflineAccount | null; + } catch (e) { + console.error("Failed to check account:", e); + } + } + + async function login() { + if (currentAccount) { + if (confirm("Logout " + currentAccount.username + "?")) { + currentAccount = null; + // Note: Backend state persists until restarted or overwritten. + } + return; + } + const username = prompt("Enter username for offline login:"); + if (username) { + try { + currentAccount = await invoke("login_offline", { username }); + } catch(e) { + alert("Login failed: " + e); + } + } + } + async function startGame() { status = "Launching (Simulated)..."; console.log("Invoking start_game..."); @@ -73,12 +132,18 @@ <!-- Bottom Bar --> <div class="h-24 bg-zinc-900 border-t border-zinc-800 flex items-center px-8 justify-between z-20 shadow-2xl"> - <div class="flex items-center gap-4"> - <div class="w-12 h-12 rounded bg-gradient-to-tr from-indigo-500 to-purple-500 shadow-lg flex items-center justify-center text-white font-bold text-xl">S</div> + <div class="flex items-center gap-4 cursor-pointer hover:opacity-80 transition-opacity" onclick={login} role="button" tabindex="0" onkeydown={(e) => e.key === 'Enter' && login()}> + <div class="w-12 h-12 rounded bg-gradient-to-tr from-indigo-500 to-purple-500 shadow-lg flex items-center justify-center text-white font-bold text-xl overflow-hidden"> + {#if currentAccount} + <img src={`https://minotar.net/avatar/${currentAccount.username}/48`} alt={currentAccount.username} class="w-full h-full"> + {:else} + ? + {/if} + </div> <div> - <div class="font-bold text-white text-lg">Steve</div> + <div class="font-bold text-white text-lg">{currentAccount ? currentAccount.username : "Click to Login"}</div> <div class="text-xs text-zinc-400 flex items-center gap-1"> - <span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> Online + <span class="w-1.5 h-1.5 rounded-full {currentAccount ? 'bg-green-500' : 'bg-zinc-500'}"></span> {currentAccount ? 'Ready' : 'Guest'} </div> </div> </div> @@ -86,10 +151,14 @@ <div class="flex items-center gap-4"> <div class="flex flex-col items-end mr-2"> <label class="text-xs text-zinc-500 mb-1 uppercase font-bold tracking-wider">Version</label> - <select class="bg-zinc-950 text-zinc-200 border border-zinc-700 rounded px-4 py-2 hover:border-zinc-500 transition-colors cursor-pointer outline-none focus:ring-1 focus:ring-indigo-500 w-48"> - <option>Latest Release (1.20.4)</option> - <option>1.19.2</option> - <option>1.8.9</option> + <select bind:value={selectedVersion} class="bg-zinc-950 text-zinc-200 border border-zinc-700 rounded px-4 py-2 hover:border-zinc-500 transition-colors cursor-pointer outline-none focus:ring-1 focus:ring-indigo-500 w-48"> + {#if versions.length === 0} + <option>Loading...</option> + {:else} + {#each versions as version} + <option value={version.id}>{version.id} ({version.type})</option> + {/each} + {/if} </select> </div> |