From 62f241061899af6751d7a36fb2bdda200e48d650 Mon Sep 17 00:00:00 2001 From: Maarten van Heusden <50550545+mmvanheusden@users.noreply.github.com> Date: Tue, 3 Feb 2026 14:38:53 +0100 Subject: [PATCH] feat: expand form --- src-tauri/src/main.rs | 38 ++++++++---------- src-tauri/src/steam.rs | 12 +++--- src-tauri/src/terminal.rs | 6 +-- src/App.tsx | 38 +++++++++++++++--- src/components/DownloaderForm.tsx | 60 +++++++++++++++++++++-------- src/components/DownloaderOutput.tsx | 2 +- src/components/FormInput.tsx | 42 +++++++++++++++++++- src/context.ts | 11 +++--- 8 files changed, 148 insertions(+), 61 deletions(-) diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 1fb4a2c8..3b182c5c 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -11,7 +11,7 @@ use portable_pty::{native_pty_system, PtyPair, PtySize}; use std::io::ErrorKind::AlreadyExists; use std::io::{BufReader, Read, Write}; use std::path::{Path, PathBuf}; -use std::sync::{Arc, OnceLock}; +use std::sync::{Arc}; use std::time::Duration; use std::{env, thread}; use tauri::async_runtime::Mutex; @@ -23,24 +23,12 @@ struct AppState { reader: Arc>>>, } - -/// The first terminal found. Used as default terminal. -static WORKING_DIR: OnceLock = OnceLock::new(); - -/// This function is called every time the app is reloaded/started. It quickly populates the [`TERMINAL`] variable with a working terminal. -#[tauri::command] -async fn preload_vectum(app: AppHandle) { - // Only fill these variables once. - - if WORKING_DIR.get().is_none() { - WORKING_DIR.set(Path::join(&app.path().local_data_dir().unwrap(), "SteamDepotDownloaderGUI")).expect("Failed to configure working directory") - } -} - #[tauri::command] async fn start_download(steam_download: steam::SteamDownload, app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { - // Also change working directory + let working_dir: PathBuf = get_working_dir(&app); + // std::env::set_current_dir(&WORKING_DIR.get().unwrap()).unwrap(); + dbg!(&steam_download); println!("\n-------------------------DEBUG INFO------------------------"); println!("received these values from frontend:"); @@ -50,11 +38,11 @@ async fn start_download(steam_download: steam::SteamDownload, app: AppHandle, st println!("\t- Depot ID: {}", steam_download.depot_id()); println!("\t- Manifest ID: {}", steam_download.manifest_id()); println!("\t- Output Path: {}", steam_download.output_path()); - println!("\t- Working directory: {}", &WORKING_DIR.get().unwrap().display()); + println!("\t- Working directory: {}", &working_dir.display()); println!("----------------------------------------------------------\n"); /* Build the command and spawn it in our terminal */ - let mut cmd = terminal::create_depotdownloader_command(&steam_download, WORKING_DIR.get().unwrap()); + let mut cmd = terminal::create_depotdownloader_command(&steam_download, &working_dir); // add the $TERM env variable so we can use clear and other commands #[cfg(target_os = "windows")] @@ -81,12 +69,13 @@ async fn start_download(steam_download: steam::SteamDownload, app: AppHandle, st /// Downloads the DepotDownloader zip file from the internet based on the OS. #[tauri::command] -async fn download_depotdownloader() { +async fn download_depotdownloader(app: AppHandle) { + let working_dir: PathBuf = get_working_dir(&app); let url = get_depotdownloader_url(); // Where we store the DepotDownloader zip. let zip_filename = format!("DepotDownloader-v{}-{}.zip", DEPOTDOWNLOADER_VERSION, env::consts::OS); - let depotdownloader_zip = Path::join(WORKING_DIR.get().unwrap(), Path::new(&zip_filename)); + let depotdownloader_zip = Path::join(&working_dir, Path::new(&zip_filename)); if let Err(e) = depotdownloader::download_file(url.as_str(), depotdownloader_zip.as_path()).await { @@ -100,7 +89,7 @@ async fn download_depotdownloader() { println!("Downloaded DepotDownloader for {} to {}", env::consts::OS, depotdownloader_zip.display()); } - depotdownloader::unzip(depotdownloader_zip.as_path(), WORKING_DIR.get().unwrap()).unwrap(); + depotdownloader::unzip(depotdownloader_zip.as_path(), &working_dir).unwrap(); println!("Succesfully extracted DepotDownloader zip."); } @@ -122,6 +111,10 @@ pub fn get_os() -> &'static str { } } +pub fn get_working_dir(app: &AppHandle) -> PathBuf { + Path::join(&app.path().local_data_dir().unwrap(), "SteamDepotDownloaderGUI") +} + fn main() { // macOS: change dir to documents because upon opening, our current dir by default is "/". // todo: Is this still needed ?? @@ -167,10 +160,9 @@ fn main() { start_download, download_depotdownloader, internet_connection, - preload_vectum, async_write_to_pty, async_read_from_pty, async_resize_pty, ]).run(tauri::generate_context!()) .expect("error while running tauri application"); -} +} \ No newline at end of file diff --git a/src-tauri/src/steam.rs b/src-tauri/src/steam.rs index c5dbcec8..1a773c07 100644 --- a/src-tauri/src/steam.rs +++ b/src-tauri/src/steam.rs @@ -5,13 +5,15 @@ use std::path::PathBuf; /// Represents the data required to download a Steam depot. #[derive(Deserialize, Debug, Getters)] +#[serde(rename_all = "camelCase")] pub struct SteamDownload { username: Option, password: Option, - app_id: String, - depot_id: String, - manifest_id: String, - options: VectumOptions + app_id: u32, + depot_id: u32, + manifest_id: u32, + output_location: Option, + output_directory_name: Option } #[derive(Debug, Deserialize, Getters)] @@ -30,7 +32,7 @@ impl SteamDownload { /// The directory where the download should happen pub fn output_path(&self) -> String { let sep = std::path::MAIN_SEPARATOR.to_string(); - match (&self.options.output_directory, &self.options.directory_name) { + match (&self.output_location, &self.output_directory_name) { (Some(output_dir), Some(dir_name)) => format!("{}{}{}", output_dir.display(), sep, dir_name), (Some(output_dir), None) => format!("{}{}{}", output_dir.display(), sep, &self.manifest_id), (None, Some(dir_name)) => format!(".{}{}", sep, dir_name), diff --git a/src-tauri/src/terminal.rs b/src-tauri/src/terminal.rs index 952eb22a..e238f4d7 100644 --- a/src-tauri/src/terminal.rs +++ b/src-tauri/src/terminal.rs @@ -70,9 +70,9 @@ pub fn create_depotdownloader_command(steam_download: &SteamDownload, cwd: &Path command.args(["-password", &steam_download.password().clone().unwrap()]); } - command.args(["-app", steam_download.app_id()]); - command.args(["-depot", steam_download.depot_id()]); - command.args(["-manifest", steam_download.manifest_id()]); + command.args(["-app", &steam_download.app_id().to_string()]); + command.args(["-depot", &steam_download.depot_id().to_string()]); + command.args(["-manifest", &steam_download.manifest_id().to_string()]); command.args(["-dir", &steam_download.output_path()]); command diff --git a/src/App.tsx b/src/App.tsx index 859497ae..26fd77d2 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,6 +3,7 @@ import "./css/App.css"; import {DownloaderOutput} from "./components/DownloaderOutput.tsx"; import {DownloaderForm} from "./components/DownloaderForm.tsx"; import {AppContext} from "./context.ts"; +import {invoke} from "@tauri-apps/api/core"; function App() { const username = useState(); @@ -10,19 +11,25 @@ function App() { const appId = useState(); const depotId = useState(); const manifestId = useState(); - + const outputLocation = useState(); + return (
-
+
Steam Depot Downloader
- +
@@ -38,6 +45,25 @@ function App() { export default App; -export function startDownload() { +export async function startDownload(options: { + username?: string; + password?: string; + appId: number; + depotId: number; + manifestId: number; + outputLocation?: string; + outputDirectoryName?: string; +}) { -} \ No newline at end of file + await invoke("download_depotdownloader"); // First make backend download DepotDownloader + + // BLOCK INTERFACE & CLEARING TERMINAL + + await invoke("start_download", { + steamDownload: options + }); // First make backend download DepotDownloader + + + // UNBLOCK INTERFACE & CLEARING TERMINAL + +} diff --git a/src/components/DownloaderForm.tsx b/src/components/DownloaderForm.tsx index 9bc0dfab..7fc66ea3 100644 --- a/src/components/DownloaderForm.tsx +++ b/src/components/DownloaderForm.tsx @@ -1,32 +1,35 @@ -import {useContext} from "preact/hooks"; +import {useContext, useState} from "preact/hooks"; import "../css/App.css"; -import {NumberInput, TextInput} from "./FormInput"; +import {BooleanUseState, FileInput, NumberInput, TextInput} from "./FormInput"; import {startDownload} from "../App"; import {Icon} from "@iconify-icon/react"; import {AppContext} from "../context"; +import {openUrl} from "@tauri-apps/plugin-opener"; export function DownloaderForm() { + const downloading = useState(); const context = useContext(AppContext); + return ( <>
-
+

+
- -
+
- - - - + + + +
); @@ -34,8 +37,9 @@ export function DownloaderForm() { function DownloadButton( - {disabled, downloading}: {disabled?: boolean, downloading?: boolean} + {disabled, downloadingState}: {disabled?: boolean, downloadingState: BooleanUseState} ) { + const [downloading, setDownloading] = downloadingState; const context = useContext(AppContext); const onClick = (e: MouseEvent) => { const form: HTMLFormElement = (e.target as HTMLButtonElement).closest("form")!; @@ -48,13 +52,35 @@ function DownloadButton( console.warn("Form invalid!"); return; } - console.trace(context); - startDownload(); + setDownloading(true) + startDownload({ + username: context.username![0], + password: context.password![0], + appId: context.appId![0]!, + depotId: context.depotId![0]!, + manifestId: context.manifestId![0]!, + outputLocation: context.outputLocation![0]!, + }).catch((e) => console.error(e)); + // setDownloading(false) }; return ( - ); } @@ -62,12 +88,12 @@ function DownloadButton( function InternetButton( {title, icon, href, disabled}: {title: string, icon: string, href?: string, disabled?: boolean} ) { - const onClick = (e: MouseEvent) => { - // go to url + const onClick = () => { + if (href) openUrl(href).catch((e) => console.error(e)); }; return ( - ); diff --git a/src/components/DownloaderOutput.tsx b/src/components/DownloaderOutput.tsx index 963b737b..d6af3f9c 100644 --- a/src/components/DownloaderOutput.tsx +++ b/src/components/DownloaderOutput.tsx @@ -4,7 +4,7 @@ export function DownloaderOutput() {
Download output -
diff --git a/src/components/FormInput.tsx b/src/components/FormInput.tsx index 45c086bd..e6bef6f4 100644 --- a/src/components/FormInput.tsx +++ b/src/components/FormInput.tsx @@ -1,7 +1,11 @@ +import {Icon} from "@iconify-icon/react"; import {useState} from "preact/hooks"; +import {open as openDialog} from "@tauri-apps/plugin-dialog"; +import {openPath} from "@tauri-apps/plugin-opener"; export type StringUseState = ReturnType>; export type NumberUseState = ReturnType>; +export type BooleanUseState = ReturnType>; export function TextInput({ label, placeholder, valueState, required, password }: { label: string, placeholder?: string, valueState: StringUseState, required?: boolean, password?: bool }) { @@ -27,7 +31,43 @@ export function NumberInput({ label, placeholder, valueState, required, min, max - + ); +} + +export function FileInput({ pathState }: { label: string, placeholder?: string, valueState: NumberUseState, required?: boolean, pathState: StringUseState }) { + const [path, setPath] = pathState; + const selectPath = () => { + openDialog({ + title: "Choose where to save the game download.", + multiple: false, + directory: true, + canCreateDirectories: true, + }) + .then((selectedPath) => { + if (!selectedPath) { + console.warn("Nothing selected, doing nothing."); + return; + } + setPath(selectedPath) + }) + .catch((e) => console.error(e)); + } + const previewPath = () => { + openPath(path!).catch((e) => console.error(e)); + } + + + return ( +
+ + {/* A hidden text input which holds the path useState value, so the form will be invalid when no path is selected. */} + +
+ ); } \ No newline at end of file diff --git a/src/context.ts b/src/context.ts index f8860cb7..53289193 100644 --- a/src/context.ts +++ b/src/context.ts @@ -2,11 +2,12 @@ import {createContext} from "preact"; import {NumberUseState, StringUseState} from "./components/FormInput"; interface AppContext { - username: StringUseState; - password: StringUseState; - appId: NumberUseState; - depotId: NumberUseState; - manifestId: NumberUseState; + username: StringUseState; + password: StringUseState; + appId: NumberUseState; + depotId: NumberUseState; + manifestId: NumberUseState; + outputLocation: StringUseState; } export const AppContext = createContext>({}); \ No newline at end of file