summaryrefslogtreecommitdiffstatshomepage
diff options
context:
space:
mode:
authorHsiangNianian <i@jyunko.cn>2026-01-13 15:37:55 +0800
committerHsiangNianian <i@jyunko.cn>2026-01-13 15:37:55 +0800
commit6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6 (patch)
treebb84869afeb316e2510018e2ba33c651488f3e71
parentb7e7f8de3d2200ef34510cda3601a50f62af798d (diff)
downloadDropOut-6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6.tar.gz
DropOut-6fbb5f19cab02f3a0f18cdeda3da02e717b69cd6.zip
feat: add offline account management and version fetching functionality
-rw-r--r--src-tauri/Cargo.toml1
-rw-r--r--src-tauri/src/core/auth.rs28
-rw-r--r--src-tauri/src/core/downloader.rs51
-rw-r--r--src-tauri/src/core/manifest.rs8
-rw-r--r--src-tauri/src/core/mod.rs2
-rw-r--r--src-tauri/src/main.rs35
-rw-r--r--ui/src/App.svelte85
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>