mirror of
https://github.com/mmvanheusden/SteamDepotDownloaderGUI.git
synced 2026-02-04 13:41:18 +01:00
Compare commits
5 Commits
main
...
tailwind_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832b49d2c7 | ||
|
|
5cfd57b03e | ||
|
|
62f2410618 | ||
|
|
08148dca50 | ||
|
|
897ef0c820 |
@@ -1,20 +1,15 @@
|
||||
// @ts-check
|
||||
|
||||
import eslint from '@eslint/js';
|
||||
import {defineConfig} from 'eslint/config';
|
||||
import tseslint from 'typescript-eslint';
|
||||
|
||||
export default defineConfig(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: 'tsconfig.json',
|
||||
},
|
||||
project: 'tsconfig.json',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["src/**"],
|
||||
rules: {
|
||||
"semi": ["error", "always"], // semicolons
|
||||
|
||||
@@ -15,16 +15,16 @@
|
||||
"@tauri-apps/api": "2.9.1",
|
||||
"@tauri-apps/plugin-dialog": "2.6.0",
|
||||
"@tauri-apps/plugin-opener": "~2.5.3",
|
||||
"@tauri-apps/plugin-shell": "2.3.4",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"jquery": "^4.0.0",
|
||||
"preact": "^10.25.1",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.2",
|
||||
"@iconify-icon/react": "^3.0.3",
|
||||
"@preact/preset-vite": "^2.9.3",
|
||||
"@tauri-apps/cli": "2.9.6",
|
||||
"@types/jquery": "^3.5.33",
|
||||
"eslint": "^9.39.2",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.54.0",
|
||||
|
||||
2106
pnpm-lock.yaml
generated
2106
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
64
src-tauri/Cargo.lock
generated
64
src-tauri/Cargo.lock
generated
@@ -2588,16 +2588,6 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_pipe"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7d8fae84b431384b68627d0f9b3b1245fcf9f46f6c0e3dc902e9dce64edd1967"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pango"
|
||||
version = "0.18.3"
|
||||
@@ -3824,17 +3814,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_child"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e362d9935bc50f019969e2f9ecd66786612daae13e8f277be7bfb66e8bed3f7"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"sigchld",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shared_library"
|
||||
version = "0.1.9"
|
||||
@@ -3857,27 +3836,6 @@ version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "sigchld"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47106eded3c154e70176fc83df9737335c94ce22f821c32d17ed1db1f83badb1"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"os_pipe",
|
||||
"signal-hook",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook"
|
||||
version = "0.3.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"signal-hook-registry",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.8"
|
||||
@@ -4365,27 +4323,6 @@ dependencies = [
|
||||
"zbus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-plugin-shell"
|
||||
version = "2.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39b76f884a3937e04b631ffdc3be506088fa979369d25147361352f2f352e5ed"
|
||||
dependencies = [
|
||||
"encoding_rs",
|
||||
"log",
|
||||
"open",
|
||||
"os_pipe",
|
||||
"regex",
|
||||
"schemars 0.8.22",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"shared_child",
|
||||
"tauri",
|
||||
"tauri-plugin",
|
||||
"thiserror 2.0.18",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.9.2"
|
||||
@@ -4996,7 +4933,6 @@ dependencies = [
|
||||
"tauri-build",
|
||||
"tauri-plugin-dialog",
|
||||
"tauri-plugin-opener",
|
||||
"tauri-plugin-shell",
|
||||
"zip",
|
||||
]
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ tauri-build = { version = "2.4.1", features = [] }
|
||||
fix-path-env = { git = "https://github.com/tauri-apps/fix-path-env-rs", rev = "c4c45d503ea115a839aae718d02f79e7c7f0f673" }
|
||||
serde_json = "1.0.143"
|
||||
tauri = { version = "2.8.5", features = [] }
|
||||
tauri-plugin-shell = "2.3.1"
|
||||
tauri-plugin-dialog = "2.4.0"
|
||||
serde = { version = "1.0.219", features = ["derive"] }
|
||||
derive-getters = "0.5.0"
|
||||
|
||||
@@ -7,10 +7,7 @@
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"dialog:default",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-spawn",
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [
|
||||
|
||||
@@ -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<Mutex<BufReader<Box<dyn Read + Send>>>>,
|
||||
}
|
||||
|
||||
|
||||
/// The first terminal found. Used as default terminal.
|
||||
static WORKING_DIR: OnceLock<PathBuf> = 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 ??
|
||||
@@ -162,15 +155,13 @@ fn main() {
|
||||
})
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
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");
|
||||
}
|
||||
}
|
||||
@@ -5,19 +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<String>,
|
||||
password: Option<String>,
|
||||
app_id: String,
|
||||
depot_id: String,
|
||||
manifest_id: String,
|
||||
options: VectumOptions
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Getters)]
|
||||
pub struct VectumOptions {
|
||||
output_directory: Option<PathBuf>,
|
||||
directory_name: Option<String>
|
||||
output_location: Option<PathBuf>,
|
||||
output_directory_name: Option<String>
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +26,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),
|
||||
|
||||
@@ -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
|
||||
|
||||
90
src/App.tsx
Normal file
90
src/App.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import {useState} from "preact/hooks";
|
||||
import "./css/App.css";
|
||||
import {DownloaderOutput} from "./components/DownloaderOutput.tsx";
|
||||
import {DownloaderForm} from "./components/DownloaderForm.tsx";
|
||||
import {AppContext, AppSettings} from "./context.ts";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import { Settings } from "./components/Settings.tsx";
|
||||
|
||||
function App() {
|
||||
const username = useState<string>();
|
||||
const password = useState<string>();
|
||||
const appId = useState<string>();
|
||||
const depotId = useState<string>();
|
||||
const manifestId = useState<string>();
|
||||
const outputLocation = useState<string>();
|
||||
const outputFolderName = useState<string>();
|
||||
const downloading = useState<boolean>();
|
||||
const showSettings = useState<boolean>();
|
||||
const appSettings = useState<AppSettings>({
|
||||
// Settings defaults are defined here.
|
||||
outputDirectoryMode: "Manifest ID"
|
||||
});
|
||||
|
||||
|
||||
|
||||
return (
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
username,
|
||||
password,
|
||||
appId,
|
||||
depotId,
|
||||
manifestId,
|
||||
outputLocation,
|
||||
downloading,
|
||||
showSettings,
|
||||
outputFolderName,
|
||||
appSettings,
|
||||
}}
|
||||
>
|
||||
<main class="bg-[#0d1117] left-0 top-0 bottom-0 absolute right-0 select-none p-px">
|
||||
{showSettings[0]
|
||||
?<Settings />
|
||||
: <>
|
||||
<div class="text-white font-bold text-4xl text-center mb-1 font-['Hubot_Sans']">
|
||||
Steam Depot Downloader
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between gap-5">
|
||||
<div class="w-full max-w-1/2 pl-3">
|
||||
<DownloaderForm />
|
||||
</div>
|
||||
<div class="w-full max-w-1/2 pr-3">
|
||||
<DownloaderOutput />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</main>
|
||||
</AppContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
export async function startDownload(options: {
|
||||
username?: string;
|
||||
password?: string;
|
||||
appId: string;
|
||||
depotId: string;
|
||||
manifestId: string;
|
||||
outputLocation?: string;
|
||||
outputDirectoryName?: string;
|
||||
}) {
|
||||
|
||||
await invoke("download_depotdownloader"); // First make backend download DepotDownloader
|
||||
|
||||
// BLOCK INTERFACE & CLEARING TERMINAL
|
||||
|
||||
await invoke("start_download", {
|
||||
steamDownload: {
|
||||
...options,
|
||||
outputDirectoryName: options.outputDirectoryName == "" ? null : options.outputDirectoryName, // empty string becomes null.
|
||||
}
|
||||
}); // First make backend download DepotDownloader
|
||||
|
||||
|
||||
// UNBLOCK INTERFACE & CLEARING TERMINAL
|
||||
|
||||
}
|
||||
Binary file not shown.
105
src/components/DownloaderForm.tsx
Normal file
105
src/components/DownloaderForm.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {useContext} from "preact/hooks";
|
||||
import "../css/App.css";
|
||||
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 context = useContext(AppContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<form>
|
||||
<div class="flex flex-col gap-0.5 mb-2">
|
||||
<TextInput id="username" label="Username" valueState={context.username!} />
|
||||
<TextInput id="password" label="Password" valueState={context.password!} password={true} />
|
||||
<br />
|
||||
<NumberInput id="appId" label="App ID" valueState={context.appId!} required={true} />
|
||||
<NumberInput id="depotId" label="Depot ID" valueState={context.depotId!} required={true} />
|
||||
<NumberInput id="manifestId" label="Manifest ID" valueState={context.manifestId!} required={true} />
|
||||
<FileInput required={true} pathState={context.outputLocation!} />
|
||||
<br />
|
||||
<DownloadButton disabled={context.downloading![0]} downloadingState={context.downloading!} />
|
||||
</div>
|
||||
</form>
|
||||
<div class="flex justify-between gap-1">
|
||||
<InternetButton icon={"ic:sharp-discord"} title="Discord" href="https://discord.com/invite/3qCt4DT5qe" />
|
||||
<InternetButton icon={"simple-icons:steamdb"} title="SteamDB" href="https://steamdb.info/instantsearch" />
|
||||
<InternetButton icon={"mdi:youtube"} title="Tutorials" href="https://youtube.com/playlist?list=PLRAjc5plLScj967hnsYX-I3Vjw9C1v7Ca"/>
|
||||
<InternetButton icon={"bx:donate-heart"} title="Donate" href="https://paypal.me/onderkin"/>
|
||||
</div>
|
||||
<span>{context.appId}</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function DownloadButton(
|
||||
{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")!;
|
||||
|
||||
e.preventDefault(); // Block refreshing the page
|
||||
|
||||
form.reportValidity(); // Display native form validation
|
||||
|
||||
if (!form.checkValidity()) {
|
||||
console.warn("Form invalid!");
|
||||
return;
|
||||
}
|
||||
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],
|
||||
outputDirectoryName: context.outputFolderName![0],
|
||||
}).catch((e) => console.error(e));
|
||||
// setDownloading(false)
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="flex">
|
||||
<button disabled={disabled} onClick={onClick} type="submit" class="text-white border-black border-2 border-r-0 w-full bg-green-500 rounded-l-md py-1 font-bold text-2xl hover:bg-green-600 active:bg-green-700 active:scale-103 transition disabled:bg-red-500/70 disabled:pointer-events-none inline-flex items-center justify-between">
|
||||
{downloading
|
||||
? <>
|
||||
<div class="absolute flex ml-2">
|
||||
<Icon icon="line-md:downloading-loop" width="35" height="35" />
|
||||
</div>
|
||||
<span class="w-full">Downloading...</span>
|
||||
</> :
|
||||
<>
|
||||
<div class="absolute flex ml-2">
|
||||
<Icon icon="material-symbols:downloading-rounded" width="35" height="35" />
|
||||
</div>
|
||||
<span class="w-full">Download</span>
|
||||
</>
|
||||
}
|
||||
</button>
|
||||
<button onClick={() => context.showSettings} type="button" class="group text-white border-black w-15 bg-green-500 rounded-r-md border-2 ring-l-gray-800 py-1 font-bold text-2xl hover:bg-green-600 active:bg-green-700 transition disabled:bg-red-500/70 disabled:pointer-events-none inline-flex items-center text-center justify-center">
|
||||
<Icon icon="heroicons:cog" width="30" height="30" class="animate-spin [animation-play-state:paused] group-hover:[animation-play-state:running]"/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function InternetButton(
|
||||
{title, icon, href, disabled}: {title: string, icon: string, href?: string, disabled?: boolean}
|
||||
) {
|
||||
const onClick = () => {
|
||||
if (href) openUrl(href).catch((e) => console.error(e));
|
||||
};
|
||||
|
||||
return (
|
||||
<button disabled={disabled} onClick={onClick} type="button" class="text-white border-black grow gap-px px-1 bg-blue-500 rounded-md border py-0.5 font-semibold text-md hover:bg-blue-400 active:bg-blue-300 active:scale-103 transition-transform disabled:bg-red-500/70 disabled:pointer-events-none inline-flex items-center justify-center">
|
||||
<Icon icon={icon} height="20"/>{title}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
98
src/components/DownloaderOutput.tsx
Normal file
98
src/components/DownloaderOutput.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { Terminal } from "@xterm/xterm";
|
||||
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import { AppContext } from "../context";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
/* Parts of this file are derived from https://github.com/cablehead/tauri-xtermjs-nushell/blob/0bdd4a27ee2874de12e99bccd6c91d6ec5d28fbc/src/main.ts */
|
||||
|
||||
export function DownloaderOutput() {
|
||||
const context = useContext(AppContext);
|
||||
const [terminal, setTerminal] = useState<Terminal | undefined>();
|
||||
|
||||
// Hook on the "command-exited" Tauri emitter, and wait for it to emit, so we can flip the downloading state.
|
||||
listen("command-exited", () => {
|
||||
context.downloading?.[1]?.(false);
|
||||
}).catch(console.error);
|
||||
|
||||
const terminalWindowRef = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
setTerminal(registerTerminal(terminalWindowRef.current!));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div class="mt-2 h-full w-full mx-auto">
|
||||
<div class="border border-gray-300 rounded-md bg-gray-900 text-white shadow shadow-blue-200">
|
||||
<div class="text-md font-semibold w-full inline-flex my-px items-center">
|
||||
<span class="text-center w-full">Download output</span>
|
||||
{terminal &&
|
||||
<button onClick={() => { if (!context.downloading![0]) terminal.reset(); }} type="button" disabled={context.downloading![0] ?? false} class="disabled:cursor-not-allowed disabled:line-through disabled:text-gray-300 ml-auto mr-2 my-1 py-px px-2 border-2 rounded-xs border-red-500/75 font-normal enabled:hover:bg-red-200/30 enabled:active:bg-red-200/50">
|
||||
Clear
|
||||
</button>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div ref={terminalWindowRef} class="max-h-[70vh]"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
const registerTerminal: (terminalElement: HTMLElement) => Terminal = (terminalElement: HTMLElement) => {
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
const fitAddon = new FitAddon();
|
||||
const term = new Terminal({
|
||||
fontSize: 10,
|
||||
cursorBlink: true,
|
||||
rows: 100,
|
||||
cols: 100,
|
||||
theme: {
|
||||
background: "rgb(30,33,46)",
|
||||
},
|
||||
});
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(terminalElement);
|
||||
function fitTerminal() {
|
||||
fitAddon.fit();
|
||||
void invoke<string>("async_resize_pty", {
|
||||
rows: term.rows,
|
||||
cols: term.cols,
|
||||
});
|
||||
}
|
||||
|
||||
// Write data from pty into the terminal
|
||||
function writeToTerminal(data: string) {
|
||||
return new Promise<void>((r) => {
|
||||
term.write(data, () => r());
|
||||
});
|
||||
}
|
||||
|
||||
// Write data from the terminal to the pty
|
||||
function writeToPty(data: string) {
|
||||
void invoke("async_write_to_pty", {
|
||||
data,
|
||||
});
|
||||
}
|
||||
term.onData(writeToPty);
|
||||
addEventListener("resize", fitTerminal);
|
||||
fitTerminal();
|
||||
|
||||
async function readFromPty() {
|
||||
const data = await invoke<string>("async_read_from_pty");
|
||||
|
||||
if (data) {
|
||||
await writeToTerminal(data);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(readFromPty);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(readFromPty);
|
||||
|
||||
return term;
|
||||
};
|
||||
|
||||
76
src/components/FormInput.tsx
Normal file
76
src/components/FormInput.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
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";
|
||||
import "../css/App.css";
|
||||
|
||||
export type StringUseState = ReturnType<typeof useState<string>>;
|
||||
export type NumberUseState = ReturnType<typeof useState<number>>;
|
||||
export type BooleanUseState = ReturnType<typeof useState<boolean>>;
|
||||
|
||||
|
||||
export function TextInput({ id, label, placeholder, valueState, required, password, disabled }: { id: string, label?: string, placeholder?: string, valueState: StringUseState, required?: boolean, password?: boolean, disabled?: boolean }) {
|
||||
const [value, setValue] = valueState;
|
||||
const onInput = (e: InputEvent) => setValue((e.currentTarget as HTMLInputElement).value);
|
||||
|
||||
return (
|
||||
<>
|
||||
{label &&
|
||||
<label for={id} class={`text-md font-medium text-white ${required && "after:content-['*'] after:ml-1 after:text-xl after:text-red-500"}`}>
|
||||
{label}
|
||||
</label>
|
||||
}
|
||||
<input disabled={disabled} id={id} required={required} value={value} onInput={onInput} placeholder={placeholder} type={password ? "password": "text"} class="border text-sm rounded-lg block w-full bg-[#161b22] border-gray-600 placeholder-gray-400 text-white focus:border-blue-500 focus:shadow-[0px_0px_29px_-10px_rgba(59,130,246,0.5)] px-3 py-2 transition duration-300 disabled:bg-gray-700 disabled:placeholder-white disabled:line-through" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function NumberInput({ id, label, placeholder, valueState, required, min, max, step }: { id: string, label: string, placeholder?: string, valueState: StringUseState, required?: boolean, min?: number, max?: number, step?: number }) {
|
||||
const [value, setValue] = valueState;
|
||||
const onInput = (e: InputEvent) => setValue((e.currentTarget as HTMLInputElement).value);
|
||||
|
||||
return (
|
||||
<>
|
||||
<label for={id} class={`text-md font-medium text-white ${required && "after:content-['*'] after:ml-1 after:text-xl after:text-red-500"}`}>
|
||||
{label}
|
||||
</label>
|
||||
<input id={id} required={required} value={value} onInput={onInput} min={min ?? 1} max={max} step={step} placeholder={placeholder} type={"number"} class="border text-sm rounded-lg block w-full bg-[#161b22] border-gray-600 placeholder-gray-400 text-white focus:border-blue-500 focus:shadow-[0px_0px_29px_-10px_rgba(59,130,246,0.5)] px-3 py-2 transition duration-300" />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function FileInput({ required, pathState }: { 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 (
|
||||
<div class="flex justify-between gap-2">
|
||||
<button type="button" onClick={selectPath} class="text-white border-black grow px-px bg-gray-500 rounded-md border py-1 font-semibold text-md hover:bg-gray-400 active:bg-gray-300 active:scale-103 transition-transform disabled:bg-red-500/70 disabled:pointer-events-none inline-flex items-center justify-center gap-1">
|
||||
<Icon icon="subway:folder-2" />Choose output directory
|
||||
</button>
|
||||
<input required={required} type="text" hidden value={path}></input> {/* A hidden text input which holds the path useState value, so the form will be invalid when no path is selected. */}
|
||||
<button type="button" disabled={!path} onClick={previewPath} class=" text-white border-black grow px-px bg-gray-500 rounded-md border py-1 font-semibold text-md hover:bg-gray-400 active:bg-gray-300 enabled:active:scale-103 transition-transform disabled:bg-red-500/70 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1">
|
||||
<Icon icon="material-symbols:folder-eye" />Preview output directory
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/context.ts
Normal file
31
src/context.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {createContext} from "preact";
|
||||
import {BooleanUseState, StringUseState} from "./components/FormInput";
|
||||
import { Dispatch, useState } from "preact/hooks";
|
||||
import { SetStateAction } from "preact/compat";
|
||||
|
||||
|
||||
// Source: https://stackoverflow.com/a/75420688
|
||||
type NoUndefinedState<T> =
|
||||
T extends [infer S | undefined, Dispatch<SetStateAction<infer S | undefined>>]
|
||||
? [S, Dispatch<SetStateAction<S>>]
|
||||
: never;
|
||||
|
||||
export interface AppSettings {
|
||||
outputDirectoryMode: "Manifest ID" | "Custom"
|
||||
}
|
||||
|
||||
|
||||
interface AppContext {
|
||||
username: StringUseState;
|
||||
password: StringUseState;
|
||||
appId: StringUseState;
|
||||
depotId: StringUseState;
|
||||
manifestId: StringUseState;
|
||||
outputLocation: StringUseState;
|
||||
outputFolderName: StringUseState;
|
||||
downloading: BooleanUseState;
|
||||
showSettings: BooleanUseState;
|
||||
appSettings: NoUndefinedState<ReturnType<typeof useState<AppSettings>>>
|
||||
}
|
||||
|
||||
export const AppContext = createContext<Partial<AppContext>>({});
|
||||
16
src/css/App.css
Normal file
16
src/css/App.css
Normal file
@@ -0,0 +1,16 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hubot Sans';
|
||||
src: url('../assets/Hubot-Sans.woff2') format('woff2 supports variations'),
|
||||
url('../assets/Hubot-Sans.woff2') format('woff2-variations');
|
||||
font-weight: 700;
|
||||
font-stretch: expanded;
|
||||
}
|
||||
|
||||
/* Disable + and - buttons for number input */
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@font-face {
|
||||
font-family: 'Hubot Sans';
|
||||
src: url('../assets/Hubot-Sans.woff2') format('woff2 supports variations'),
|
||||
url('../assets/Hubot-Sans.woff2') format('woff2-variations');
|
||||
font-weight: 700;
|
||||
font-stretch: expanded;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Windows';
|
||||
src: url('../assets/Windows.woff') format('woff2 supports variations'),
|
||||
url('../assets/Windows.woff') format('woff2-variations');
|
||||
font-weight: 700;
|
||||
font-stretch: expanded;
|
||||
}
|
||||
|
||||
.f1-light {
|
||||
font-family: 'Hubot Sans', sans-serif;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.f2-light {
|
||||
font-family: 'Hubot Sans', sans-serif;
|
||||
}
|
||||
|
||||
/* The grey part */
|
||||
.settings-surrounding {
|
||||
display: none;
|
||||
position: fixed;
|
||||
z-index: 1;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
background-color: rgba(0, 0, 0, 0.33);
|
||||
}
|
||||
|
||||
.settings-content {
|
||||
position: relative;
|
||||
border-radius: 10px;
|
||||
overflow: auto;
|
||||
/*noinspection CssUnresolvedCustomProperty*/
|
||||
background-color: var(--bgColor-default, var(--color-canvas-default));
|
||||
margin: 1%;
|
||||
padding: 25px;
|
||||
border: 1.5px solid white;
|
||||
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1), 0 6px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-color-mode="light"] .settings-content {
|
||||
border: 1.5px solid black;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
[data-color-mode="auto"] .settings-content {
|
||||
border: 1.5px solid black;
|
||||
}
|
||||
}
|
||||
|
||||
.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background: black linear-gradient(to right, #0c1016, #ccc, #0c1016);
|
||||
}
|
||||
|
||||
[data-color-mode="light"] hr {
|
||||
filter: invert(1);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
[data-color-mode="auto"] hr {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
|
||||
.version-info {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
font-size: 0.9em;
|
||||
padding: 5px 10px;
|
||||
font-family: monospace;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.AnimatedEllipsis {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
vertical-align: bottom
|
||||
}
|
||||
|
||||
.AnimatedEllipsis::after {
|
||||
display: inline-block;
|
||||
content: "...";
|
||||
animation: AnimatedEllipsis-keyframes 1s steps(4, jump-none) infinite
|
||||
}
|
||||
|
||||
@keyframes AnimatedEllipsis-keyframes {
|
||||
0% {
|
||||
transform: translateX(-100%)
|
||||
}
|
||||
}
|
||||
|
||||
.opium-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
cursor: pointer;
|
||||
margin-left: 5px;
|
||||
margin-bottom: 4px;
|
||||
|
||||
border: 1px solid #000;
|
||||
background: linear-gradient(180deg, #8C8C8C 25%, #434343 75%);
|
||||
display: inline-block;
|
||||
font: 16px "Windows", monospace;
|
||||
padding: 2px 5px;
|
||||
color: darkred;
|
||||
text-decoration: none;
|
||||
|
||||
}
|
||||
|
||||
.opium-button:hover {
|
||||
cursor: zoom-in;
|
||||
background: linear-gradient(180deg, #b0b0b0 25%, #504f4f 75%);
|
||||
}
|
||||
|
||||
.opium-button:active {
|
||||
cursor: crosshair;
|
||||
border: 1px inset black;
|
||||
background: linear-gradient(180deg, #333232 25%, #504f4f 75%);
|
||||
}
|
||||
267
src/index.html
267
src/index.html
@@ -1,268 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html data-color-mode="auto" data-dark-theme="dark" data-light-theme="light" id="theme" lang="en">
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1" name="viewport"/>
|
||||
<title>SteamDepotDownloaderGUI</title>
|
||||
<link href="https://unpkg.com/@primer/css@21.3.6/dist/primer.css" rel="stylesheet"/>
|
||||
<link href="css/style.css" rel="stylesheet">
|
||||
<script defer src="./ts/preload.ts" type="module"></script>
|
||||
<script src="./ts/main.ts" type="module"></script>
|
||||
<script src="./ts/settings.ts" type="module"></script>
|
||||
<script src="../node_modules/@xterm/xterm/lib/xterm.js"></script>
|
||||
<link rel="stylesheet" href="../node_modules/@xterm/xterm/css/xterm.css" />
|
||||
</head>
|
||||
|
||||
<body class="select-none">
|
||||
<div class="f1-light text-center mb-1">Steam Depot Downloader</div>
|
||||
<div class="flex justify-between gap-2 w-svw flex-row">
|
||||
<div class="w-1/2 h-full" id="left-side">
|
||||
<div class="mx-auto">
|
||||
<form id="theform">
|
||||
<div class="form-group mx-3 mt-1">
|
||||
<div class="form-group-header">
|
||||
<label for="username">Username</label>
|
||||
</div>
|
||||
<input spellcheck="false" class="form-control input-block" id="username" placeholder="Leave empty for anonymous download"
|
||||
type="text"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group mx-3 mt-1">
|
||||
<div class="form-group-header">
|
||||
<label for="password">Password</label>
|
||||
</div>
|
||||
<input class="form-control input-block" id="password" placeholder="Leave empty for anonymous download"
|
||||
type="password"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group mx-3 mt-1 required">
|
||||
<div class="form-group-header">
|
||||
<label for="appid">App ID</label>
|
||||
</div>
|
||||
<input class="form-control input-block" id="appid" type="number"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group mx-3 mt-1 required">
|
||||
<div class="form-group-header">
|
||||
<label for="depotid">Depot ID</label>
|
||||
</div>
|
||||
<input class="form-control input-block" id="depotid" type="number"/>
|
||||
</div>
|
||||
|
||||
<div class="form-group mx-3 mt-1 required">
|
||||
<div class="form-group-header">
|
||||
<label for="manifestid">Manifest ID</label>
|
||||
</div>
|
||||
<input class="form-control input-block" id="manifestid" type="number"/>
|
||||
</div>
|
||||
|
||||
<div class="mx-3 mt-1 required">
|
||||
<div class="form-group-header">
|
||||
<label>Download Location</label>
|
||||
</div>
|
||||
<div aria-label="Pick the path/location where the game will be downloaded to."
|
||||
class="form-control btn btn-sm tooltipped tooltipped-ne" id="pickpath">
|
||||
Set location
|
||||
</div>
|
||||
|
||||
<div aria-disabled="true" aria-label="Check the location that has been selected."
|
||||
class="form-control btn btn-sm ml-2 tooltipped tooltipped-ne" id="checkpath">
|
||||
Open location
|
||||
</div>
|
||||
|
||||
<span class="Label mt-1 ml-3 Label--warning" id="busy">
|
||||
<span aria-label="Application is executing a task. Please be patient."
|
||||
class="tooltipped tooltipped-n">Busy<span class="AnimatedEllipsis"></span>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="mt-2" id="warning-banners">
|
||||
<div hidden id="dotnetwarning">
|
||||
<div class="flash flash-error mx-2 mt-2 color-shadow-medium" id="dotnetalert">
|
||||
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
<code><span class="text-italic">dotnet</span></code> was not found.
|
||||
<button class="btn btn-sm flash-action" id="dotnetalertbtn">
|
||||
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5
|
||||
0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
<span class="text-bold">Download</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div hidden id="emptywarning">
|
||||
<div class="flash flash-warn mx-2 mt-2 color-shadow-medium" id="emptyalert">
|
||||
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
Please fill in all required fields.
|
||||
</div>
|
||||
</div>
|
||||
<div id="internet-btns">
|
||||
<div class="form-group mt-3 ml-3 mr-3">
|
||||
<div class="BtnGroup d-flex">
|
||||
<button class="BtnGroup-item btn btn-block btn-primary flex-1" id="downloadbtn">
|
||||
<svg class="octicon filter-red" height="16"
|
||||
style="display: inline-block; user-select: none; vertical-align: text-bottom;"
|
||||
viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M7.47 10.78a.75.75 0 001.06 0l3.75-3.75a.75.75 0 00-1.06-1.06L8.75 8.44V1.75a.75.75 0 00-1.5 0v6.69L4.78 5.97a.75.75 0 00-1.06 1.06l3.75 3.75zM3.75 13a.75.75 0 000 1.5h8.5a.75.75 0 000-1.5h-8.5z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
Download
|
||||
</button>
|
||||
<button class="BtnGroup-item btn flex-0" id="settings-button">
|
||||
<svg fill="#8B949E" height="16"
|
||||
style="display: inline-block; user-select: none; vertical-align: text-bottom;"
|
||||
viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 0a8.2 8.2 0 0 1 .701.031C9.444.095 9.99.645 10.16 1.29l.288 1.107c.018.066.079.158.212.224.231.114.454.243.668.386.123.082.233.09.299.071l1.103-.303c.644-.176 1.392.021 1.82.63.27.385.506.792.704 1.218.315.675.111 1.422-.364 1.891l-.814.806c-.049.048-.098.147-.088.294.016.257.016.515 0 .772-.01.147.038.246.088.294l.814.806c.475.469.679 1.216.364 1.891a7.977 7.977 0 0 1-.704 1.217c-.428.61-1.176.807-1.82.63l-1.102-.302c-.067-.019-.177-.011-.3.071a5.909 5.909 0 0 1-.668.386c-.133.066-.194.158-.211.224l-.29 1.106c-.168.646-.715 1.196-1.458 1.26a8.006 8.006 0 0 1-1.402 0c-.743-.064-1.289-.614-1.458-1.26l-.289-1.106c-.018-.066-.079-.158-.212-.224a5.738 5.738 0 0 1-.668-.386c-.123-.082-.233-.09-.299-.071l-1.103.303c-.644.176-1.392-.021-1.82-.63a8.12 8.12 0 0 1-.704-1.218c-.315-.675-.111-1.422.363-1.891l.815-.806c.05-.048.098-.147.088-.294a6.214 6.214 0 0 1 0-.772c.01-.147-.038-.246-.088-.294l-.815-.806C.635 6.045.431 5.298.746 4.623a7.92 7.92 0 0 1 .704-1.217c.428-.61 1.176-.807 1.82-.63l1.102.302c.067.019.177.011.3-.071.214-.143.437-.272.668-.386.133-.066.194-.158.211-.224l.29-1.106C6.009.645 6.556.095 7.299.03 7.53.01 7.764 0 8 0Zm-.571 1.525c-.036.003-.108.036-.137.146l-.289 1.105c-.147.561-.549.967-.998 1.189-.173.086-.34.183-.5.29-.417.278-.97.423-1.529.27l-1.103-.303c-.109-.03-.175.016-.195.045-.22.312-.412.644-.573.99-.014.031-.021.11.059.19l.815.806c.411.406.562.957.53 1.456a4.709 4.709 0 0 0 0 .582c.032.499-.119 1.05-.53 1.456l-.815.806c-.081.08-.073.159-.059.19.162.346.353.677.573.989.02.03.085.076.195.046l1.102-.303c.56-.153 1.113-.008 1.53.27.161.107.328.204.501.29.447.222.85.629.997 1.189l.289 1.105c.029.109.101.143.137.146a6.6 6.6 0 0 0 1.142 0c.036-.003.108-.036.137-.146l.289-1.105c.147-.561.549-.967.998-1.189.173-.086.34-.183.5-.29.417-.278.97-.423 1.529-.27l1.103.303c.109.029.175-.016.195-.045.22-.313.411-.644.573-.99.014-.031.021-.11-.059-.19l-.815-.806c-.411-.406-.562-.957-.53-1.456a4.709 4.709 0 0 0 0-.582c-.032-.499.119-1.05.53-1.456l.815-.806c.081-.08.073-.159.059-.19a6.464 6.464 0 0 0-.573-.989c-.02-.03-.085-.076-.195-.046l-1.102.303c-.56.153-1.113.008-1.53-.27a4.44 4.44 0 0 0-.501-.29c-.447-.222-.85-.629-.997-1.189l-.289-1.105c-.029-.11-.101-.143-.137-.146a6.6 6.6 0 0 0-1.142 0ZM11 8a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM9.5 8a1.5 1.5 0 1 0-3.001.001A1.5 1.5 0 0 0 9.5 8Z">
|
||||
</path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div hidden id="nopathwarning">
|
||||
<div class="flash flash-warn mx-2 mt-2 color-shadow-medium" id="emptyalert">
|
||||
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
Please choose a download location.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flash mx-2 mt-2 color-shadow-medium" hidden id="downloadingnotice">
|
||||
<svg class="octicon" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M8 1.5a6.5 6.5 0 100 13 6.5 6.5 0 000-13zM0 8a8 8 0 1116 0A8 8 0 010 8zm6.5-.25A.75.75 0 017.25 7h1a.75.75 0 01.75.75v2.75h.25a.75.75 0 010 1.5h-2a.75.75 0 010-1.5h.25v-2h-.25a.75.75 0 01-.75-.75zM8 6a1 1 0 100-2 1 1 0 000 2z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
Downloading and extracting DepotDownloader<span class="AnimatedEllipsis"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-1/2 h-full px-2" id="right-side">
|
||||
<div class="mt-2 h-full w-full mx-auto">
|
||||
<div class="border border-gray-300 rounded-md bg-gray-900 text-white shadow shadow-blue-200">
|
||||
<div class="text-md font-semibold w-full inline-flex my-px items-center">
|
||||
<span class="text-center w-full">Download output</span>
|
||||
<button id="clear-terminal" class="disabled:pointer-events-none disabled:line-through disabled:text-gray-300 ml-auto py-px px-2 border-2 rounded-xs border-red-500/75 font-normal enabled:hover:bg-red-200/30 enabled:active:bg-red-200/50">
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
<div class="max-h-[70vh]" id="xtermjs"></div>
|
||||
</div>
|
||||
<div class="mt-3 justify-between flex flex-row gap-3">
|
||||
<div aria-label="Join the Discord server for rapid support." class="btn btn-sm tooltipped tooltipped-ne mb-1 w-full text-center items-center"
|
||||
id="smbtn1">
|
||||
|
||||
<svg fill="#8B949E" height="16" style="display: inline-block; vertical-align: text-bottom;" viewBox="0 0 16 16">
|
||||
<path d="M13.545 2.907a13.2 13.2 0 0 0-3.257-1.011.05.05 0 0 0-.052.025c-.141.25-.297.577-.406.833a12.2 12.2 0 0 0-3.658 0 8 8 0 0 0-.412-.833.05.05 0 0 0-.052-.025c-1.125.194-2.22.534-3.257 1.011a.04.04 0 0 0-.021.018C.356 6.024-.213 9.047.066 12.032q.003.022.021.037a13.3 13.3 0 0 0 3.995 2.02.05.05 0 0 0 .056-.019q.463-.63.818-1.329a.05.05 0 0 0-.01-.059l-.018-.011a9 9 0 0 1-1.248-.595.05.05 0 0 1-.02-.066l.015-.019q.127-.095.248-.195a.05.05 0 0 1 .051-.007c2.619 1.196 5.454 1.196 8.041 0a.05.05 0 0 1 .053.007q.121.1.248.195a.05.05 0 0 1-.004.085 8 8 0 0 1-1.249.594.05.05 0 0 0-.03.03.05.05 0 0 0 .003.041c.24.465.515.909.817 1.329a.05.05 0 0 0 .056.019 13.2 13.2 0 0 0 4.001-2.02.05.05 0 0 0 .021-.037c.334-3.451-.559-6.449-2.366-9.106a.03.03 0 0 0-.02-.019m-8.198 7.307c-.789 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.45.73 1.438 1.613 0 .888-.637 1.612-1.438 1.612m5.316 0c-.788 0-1.438-.724-1.438-1.612s.637-1.613 1.438-1.613c.807 0 1.451.73 1.438 1.613 0 .888-.631 1.612-1.438 1.612"/>
|
||||
</svg>
|
||||
Discord
|
||||
</div>
|
||||
|
||||
<div aria-label="Visit the SteamDB instant search website."
|
||||
class="btn btn-sm tooltipped tooltipped-n mb-1 w-full text-center items-center" id="smbtn2">
|
||||
<svg aria-hidden="true" class="octicon" fill="#8B949E" height="16" viewBox="0 0 128 128" width="16"
|
||||
xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M63.9 0C30.5 0 3.1 11.9.1 27.1l35.6 6.7c2.9-.9 6.2-1.3 9.6-1.3l16.7-10c-.2-2.5 1.3-5.1 4.7-7.2 4.8-3.1 12.3-4.8 19.9-4.8 5.2-.1 10.5.7 15 2.2 11.2 3.8 13.7 11.1 5.7 16.3-5.1 3.3-13.3 5-21.4 4.8l-22 7.9c-.2 1.6-1.3 3.1-3.4 4.5-5.9 3.8-17.4 4.7-25.6 1.9-3.6-1.2-6-3-7-4.8L2.5 38.4c2.3 3.6 6 6.9 10.8 9.8C5 53 0 59 0 65.5c0 6.4 4.8 12.3 12.9 17.1C4.8 87.3 0 93.2 0 99.6 0 115.3 28.6 128 64 128c35.3 0 64-12.7 64-28.4 0-6.4-4.8-12.3-12.9-17 8.1-4.8 12.9-10.7 12.9-17.1 0-6.5-5-12.6-13.4-17.4 8.3-5.1 13.3-11.4 13.3-18.2 0-16.5-28.7-29.9-64-29.9zm22.8 14.2c-5.2.1-10.2 1.2-13.4 3.3-5.5 3.6-3.8 8.5 3.8 11.1 7.6 2.6 18.1 1.8 23.6-1.8s3.8-8.5-3.8-11c-3.1-1-6.7-1.5-10.2-1.5zm.3 1.7c7.4 0 13.3 2.8 13.3 6.2 0 3.4-5.9 6.2-13.3 6.2s-13.3-2.8-13.3-6.2c0-3.4 5.9-6.2 13.3-6.2zM45.3 34.4c-1.6.1-3.1.2-4.6.4l9.1 1.7a10.8 5 0 1 1-8.1 9.3l-8.9-1.7c1 .9 2.4 1.7 4.3 2.4 6.4 2.2 15.4 1.5 20-1.5s3.2-7.2-3.2-9.3c-2.6-.9-5.7-1.3-8.6-1.3zM109 51v9.3c0 11-20.2 19.9-45 19.9-24.9 0-45-8.9-45-19.9v-9.2c11.5 5.3 27.4 8.6 44.9 8.6 17.6 0 33.6-3.3 45.2-8.7zm0 34.6v8.8c0 11-20.2 19.9-45 19.9-24.9 0-45-8.9-45-19.9v-8.8c11.6 5.1 27.4 8.2 45 8.2s33.5-3.1 45-8.2z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
SteamDB
|
||||
</div>
|
||||
|
||||
<div aria-label="Donate to the author of SteamDepotDownloaderGUI."
|
||||
class="btn btn-sm tooltipped tooltipped-n mb-1 w-full text-center items-center" id="smbtn3">
|
||||
<svg fill="#8B949E" height="16" style="display: inline-block; vertical-align: text-bottom;"
|
||||
viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M2 2.75A2.75 2.75 0 0 1 4.75 0c.983 0 1.873.42 2.57 1.232.268.318.497.668.68 1.042.183-.375.411-.725.68-1.044C9.376.42 10.266 0 11.25 0a2.75 2.75 0 0 1 2.45 4h.55c.966 0 1.75.784 1.75 1.75v2c0 .698-.409 1.301-1 1.582v4.918A1.75 1.75 0 0 1 13.25 16H2.75A1.75 1.75 0 0 1 1 14.25V9.332C.409 9.05 0 8.448 0 7.75v-2C0 4.784.784 4 1.75 4h.55c-.192-.375-.3-.8-.3-1.25ZM7.25 9.5H2.5v4.75c0 .138.112.25.25.25h4.5Zm1.5 0v5h4.5a.25.25 0 0 0 .25-.25V9.5Zm0-4V8h5.5a.25.25 0 0 0 .25-.25v-2a.25.25 0 0 0-.25-.25Zm-7 0a.25.25 0 0 0-.25.25v2c0 .138.112.25.25.25h5.5V5.5h-5.5Zm3-4a1.25 1.25 0 0 0 0 2.5h2.309c-.233-.818-.542-1.401-.878-1.793-.43-.502-.915-.707-1.431-.707ZM8.941 4h2.309a1.25 1.25 0 0 0 0-2.5c-.516 0-1 .205-1.43.707-.337.392-.646.975-.879 1.793Z"
|
||||
fill-rule="evenodd"></path>
|
||||
</svg>
|
||||
Donate
|
||||
</div>
|
||||
|
||||
<div aria-label="View the official SteamDepotDownloaderGUI tutorials."
|
||||
class="btn btn-sm tooltipped tooltipped-nw mb-1 w-full text-center items-center" id="smbtn4">
|
||||
<svg fill="#8B949E" style="display: inline-block; vertical-align: text-bottom;" height="16" width="16">
|
||||
<path d="M0 3.75C0 2.784.784 2 1.75 2h12.5c.966 0 1.75.784 1.75 1.75v8.5A1.75 1.75 0 0 1 14.25 14H1.75A1.75 1.75 0 0 1 0 12.25Zm1.75-.25a.25.25 0 0 0-.25.25v8.5c0 .138.112.25.25.25h12.5a.25.25 0 0 0 .25-.25v-8.5a.25.25 0 0 0-.25-.25Z"></path>
|
||||
<path d="M6 10.559V5.442a.25.25 0 0 1 .379-.215l4.264 2.559a.25.25 0 0 1 0 .428l-4.264 2.559A.25.25 0 0 1 6 10.559Z"></path>
|
||||
</svg>
|
||||
Tutorial
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="settings-surrounding" id="settings-surrounding">
|
||||
<div class="settings-content mx-auto max-w-2/3 h-[85vh] mt-4" id="settings-content">
|
||||
<label class="version-info" id="version-info">UNKNOWN</label>
|
||||
<button class="opium-button" id="opium-btn">aphex</button>
|
||||
<h2><b>Settings</b></h2>
|
||||
<h4><b>Appearance</b></h4>
|
||||
<div class="form-group">
|
||||
<div class="form-group-header">
|
||||
<label>Theme</label>
|
||||
</div>
|
||||
<div class="form-group-body">
|
||||
<div class="BtnGroup">
|
||||
<button aria-selected="true" class="BtnGroup-item btn btn-sm" id="theme-auto" type="button">
|
||||
Auto
|
||||
</button>
|
||||
<button class="BtnGroup-item btn btn-sm" id="theme-light" type="button">
|
||||
Light
|
||||
</button>
|
||||
<button class="BtnGroup-item btn btn-sm" id="theme-dark" type="button">
|
||||
Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h4><b>Output</b></h4>
|
||||
<form>
|
||||
<div class="form-group">
|
||||
<div class="form-group-header">
|
||||
<label for="folder-name-custom-input">Game directory name</label>
|
||||
</div>
|
||||
<div class="form-group-body">
|
||||
<div class="BtnGroup">
|
||||
<button aria-selected="true" class="BtnGroup-item btn btn-sm" id="folder-name-appid"
|
||||
type="button">
|
||||
Manifest ID
|
||||
</button>
|
||||
<button class="BtnGroup-item btn btn-sm" id="folder-name-custom" type="button">
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
<br>
|
||||
<input class="form-control input-block mt-2" disabled
|
||||
id="folder-name-custom-input" placeholder="DepotDownloader output directory name"
|
||||
type="text">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="main.tsx" type="module"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
||||
4
src/main.tsx
Normal file
4
src/main.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import {render} from "preact";
|
||||
import App from "./App";
|
||||
|
||||
render(<App />, document.getElementById("root")!);
|
||||
245
src/ts/main.ts
245
src/ts/main.ts
@@ -1,245 +0,0 @@
|
||||
import $ from "jquery";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import {open as openDialog} from "@tauri-apps/plugin-dialog";
|
||||
import {openPath, openUrl} from "@tauri-apps/plugin-opener";
|
||||
import "@xterm/xterm/css/xterm.css";
|
||||
import {Terminal} from "@xterm/xterm";
|
||||
import { FitAddon } from "@xterm/addon-fit";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
|
||||
/* Parts of this file are derived from https://github.com/cablehead/tauri-xtermjs-nushell/blob/0bdd4a27ee2874de12e99bccd6c91d6ec5d28fbc/src/main.ts */
|
||||
|
||||
function blockTerminalClearButton(state: boolean) {
|
||||
$("#clear-terminal").prop( "disabled", state );
|
||||
}
|
||||
|
||||
|
||||
function setLoadingState(state: boolean) {
|
||||
$("#busy").prop("hidden", !state);
|
||||
|
||||
// loop through all buttons and input fields and disable them
|
||||
for (const element of document.querySelectorAll("button, input, div[role='button']")) {
|
||||
if (element.closest("#settings-content")) continue;
|
||||
if (element.closest("#right-side")) continue;
|
||||
(element as any).disabled = state;
|
||||
}
|
||||
|
||||
// These elements need additional properties to be properly disabled
|
||||
$("#pickpath").prop("ariaDisabled", state);
|
||||
$("#downloadbtn").prop("ariaDisabled", state);
|
||||
|
||||
// disable internet buttons
|
||||
for (const element of document.querySelectorAll("#internet-btns div")) {
|
||||
element.ariaDisabled = String(state);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Returns list of IDs of invalid form fields
|
||||
const invalidFields = () => {
|
||||
const form = document.forms[0];
|
||||
|
||||
const invalidFields: string[] = [];
|
||||
for (const input of form) {
|
||||
const inputElement = input as HTMLInputElement;
|
||||
const valid = !(inputElement.value === "" && inputElement?.parentElement?.classList.contains("required"));
|
||||
if (!valid) {
|
||||
invalidFields.push(inputElement.id);
|
||||
}
|
||||
}
|
||||
// console.debug(`[${invalidFields.join(", ")}] fields invalid/empty`);
|
||||
|
||||
return invalidFields;
|
||||
};
|
||||
|
||||
const registerTerminal: (terminalElement: HTMLElement) => Promise<Terminal> = async (terminalElement: HTMLElement) => {
|
||||
const fitAddon = new FitAddon();
|
||||
const term = new Terminal({
|
||||
fontSize: 10,
|
||||
cursorBlink: true,
|
||||
rows: 100,
|
||||
cols: 100,
|
||||
theme: {
|
||||
background: "rgb(33, 33, 33)",
|
||||
},
|
||||
});
|
||||
term.loadAddon(fitAddon);
|
||||
term.open(terminalElement);
|
||||
async function fitTerminal() {
|
||||
fitAddon.fit();
|
||||
void invoke<string>("async_resize_pty", {
|
||||
rows: term.rows,
|
||||
cols: term.cols,
|
||||
});
|
||||
}
|
||||
|
||||
// Write data from pty into the terminal
|
||||
function writeToTerminal(data: string) {
|
||||
return new Promise<void>((r) => {
|
||||
term.write(data, () => r());
|
||||
});
|
||||
}
|
||||
|
||||
// Write data from the terminal to the pty
|
||||
function writeToPty(data: string) {
|
||||
void invoke("async_write_to_pty", {
|
||||
data,
|
||||
});
|
||||
}
|
||||
term.onData(writeToPty);
|
||||
addEventListener("resize", fitTerminal);
|
||||
await fitTerminal();
|
||||
|
||||
async function readFromPty() {
|
||||
const data = await invoke<string>("async_read_from_pty");
|
||||
|
||||
if (data) {
|
||||
await writeToTerminal(data);
|
||||
}
|
||||
window.requestAnimationFrame(readFromPty);
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(readFromPty);
|
||||
|
||||
return term;
|
||||
};
|
||||
|
||||
|
||||
$(async () => {
|
||||
let terminal = await registerTerminal($("#xtermjs")[0]);
|
||||
let downloadDirectory: string | null;
|
||||
|
||||
// Startup logic
|
||||
setLoadingState(true);
|
||||
await invoke("preload_vectum");
|
||||
setLoadingState(false);
|
||||
|
||||
$("#clear-terminal").on("click", async () => {
|
||||
terminal.reset()
|
||||
})
|
||||
|
||||
$("#pickpath").on("click", async () => {
|
||||
// Open a dialog
|
||||
downloadDirectory = await openDialog({
|
||||
title: "Choose where to save the game download.",
|
||||
multiple: false,
|
||||
directory: true,
|
||||
canCreateDirectories: true
|
||||
});
|
||||
|
||||
if (downloadDirectory == null) {
|
||||
// user cancelled
|
||||
$("#checkpath").prop("ariaDisabled", true);
|
||||
$("#checkpath").prop("disabled", true);
|
||||
return;
|
||||
}
|
||||
|
||||
$("#checkpath").prop("ariaDisabled", false);
|
||||
$("#checkpath").prop("disabled", false);
|
||||
$("#downloadbtn").prop("ariaDisabled", false);
|
||||
$("#nopathwarning").prop("hidden", true);
|
||||
|
||||
|
||||
console.log(downloadDirectory);
|
||||
});
|
||||
|
||||
$("#checkpath").on("click", async () => {
|
||||
console.log(`Checking path: ${downloadDirectory}`);
|
||||
|
||||
if (downloadDirectory != null) {
|
||||
await openPath(downloadDirectory);
|
||||
} else {
|
||||
$("#checkpath").prop("ariaDisabled", true);
|
||||
}
|
||||
});
|
||||
|
||||
$("#downloadbtn").on("click", async () => {
|
||||
console.log("download button clicked");
|
||||
|
||||
if (invalidFields().length > 0) {
|
||||
// Loop through invalid fields. If there are any, make those "errored" and block the download button.
|
||||
for (const id of invalidFields()) {
|
||||
document.getElementById(id)?.parentElement?.classList.toggle("errored", true);
|
||||
}
|
||||
$("#emptywarning").prop("hidden", false);
|
||||
$("#downloadbtn").prop("ariaDisabled", true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (downloadDirectory == null) {
|
||||
$("#nopathwarning").prop("hidden", false);
|
||||
$("#downloadbtn").prop("ariaDisabled", true);
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingState(true);
|
||||
$("#downloadingnotice").prop("hidden", false);
|
||||
$("#busy").prop("hidden", true); // Don't show the loader this time.
|
||||
|
||||
const directoryNameChoice = $("#folder-name-custom-input").val();
|
||||
|
||||
// Output path w/ directories chosen is: {downloadDirectory}/{directoryNameChoice}
|
||||
const vectumOptions = {
|
||||
output_directory: downloadDirectory || null, // if not specified let backend choose a path.
|
||||
directory_name: directoryNameChoice || null,
|
||||
};
|
||||
|
||||
const steamDownload = {
|
||||
// String || null translate to Some(String) || None
|
||||
username: String($("#username").val()).trim() || null,
|
||||
password: String($("#password").val()).trim() || null,
|
||||
app_id: $("#appid").val(),
|
||||
depot_id: $("#depotid").val(),
|
||||
manifest_id: $("#manifestid").val(),
|
||||
options: vectumOptions
|
||||
};
|
||||
|
||||
// console.debug(steamDownload);
|
||||
await invoke("download_depotdownloader");
|
||||
|
||||
$("#downloadingnotice").prop("hidden", true);
|
||||
setLoadingState(false);
|
||||
|
||||
console.debug("DepotDownloader download process completed. Starting game download...");
|
||||
|
||||
setLoadingState(true);
|
||||
await invoke("start_download", {steamDownload: steamDownload});
|
||||
|
||||
// Block clear terminal button (to avoid clearing ongoing download logs)
|
||||
blockTerminalClearButton(true);
|
||||
console.log("Send frontend data over to backend. Ready for next download.");
|
||||
});
|
||||
|
||||
$("#settings-button").on("click", async () => {
|
||||
$("#settings-surrounding").toggle();
|
||||
});
|
||||
|
||||
$("#settings-surrounding").on("click", (event) => {
|
||||
if (event.target === document.getElementById("settings-surrounding")) {
|
||||
$("#settings-surrounding").toggle();
|
||||
}
|
||||
});
|
||||
|
||||
$("#opium-btn").on("click", () => {
|
||||
openUrl("https://aphex.cc/index.html");
|
||||
});
|
||||
|
||||
|
||||
document.forms[0].addEventListener("input", (event) => {
|
||||
// Remove errored class. This is a bad way to do it, but it works for now.
|
||||
const target = event.target as HTMLElement;
|
||||
target?.parentElement?.classList.toggle("errored", false);
|
||||
|
||||
// If there are no more invalid fields, hide the warning and enable the download button again
|
||||
if (invalidFields().length === 0) {
|
||||
$("#emptywarning").prop("hidden", true);
|
||||
$("#downloadbtn").prop("ariaDisabled", false);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
listen<string>("command-exited", () => {
|
||||
setLoadingState(false);
|
||||
blockTerminalClearButton(false);
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import {message} from "@tauri-apps/plugin-dialog";
|
||||
import {invoke} from "@tauri-apps/api/core";
|
||||
import $ from "jquery";
|
||||
import {openUrl} from "@tauri-apps/plugin-opener";
|
||||
|
||||
|
||||
$(async () => {
|
||||
/* eslint-disable indent */
|
||||
switch (await invoke("internet_connection")) {
|
||||
case false: {
|
||||
await message("No internet connection! Can't proceed.", {
|
||||
title: "SteamDepotDownloaderGUI", kind: "error", okLabel: "Close"
|
||||
});
|
||||
}
|
||||
}
|
||||
/* eslint-enable indent */
|
||||
|
||||
// discord
|
||||
$("#smbtn1").on("click", () => {
|
||||
openUrl("https://discord.com/invite/3qCt4DT5qe");
|
||||
});
|
||||
// steamdb
|
||||
$("#smbtn2").on("click", () => {
|
||||
openUrl("https://steamdb.info/instantsearch");
|
||||
});
|
||||
// donate
|
||||
$("#smbtn3").on("click", () => {
|
||||
openUrl("https://paypal.me/onderkin");
|
||||
});
|
||||
// tutorial
|
||||
$("#smbtn4").on("click", () => {
|
||||
openUrl("https://youtube.com/playlist?list=PLRAjc5plLScj967hnsYX-I3Vjw9C1v7Ca");
|
||||
});
|
||||
});
|
||||
@@ -1,47 +0,0 @@
|
||||
import {getVersion} from "@tauri-apps/api/app";
|
||||
import $ from "jquery";
|
||||
import {openUrl} from "@tauri-apps/plugin-opener";
|
||||
|
||||
|
||||
$(async () => {
|
||||
$("#version-info").text(`v${await getVersion()}`);
|
||||
|
||||
$("#theme-auto").on("click", () => {
|
||||
setTheme("auto");
|
||||
});
|
||||
$("#theme-light").on("click", () => {
|
||||
setTheme("light");
|
||||
});
|
||||
$("#theme-dark").on("click", () => {
|
||||
setTheme("dark");
|
||||
});
|
||||
|
||||
$("#folder-name-appid").on("click", () => {
|
||||
$("#folder-name-custom").attr("aria-selected", "false");
|
||||
$("#folder-name-appid").attr("aria-selected", "true");
|
||||
$("#folder-name-custom-input").prop("disabled", true);
|
||||
$("#folder-name-custom-input").val("");
|
||||
});
|
||||
|
||||
// todo: fix folder-name-custom-input not disabled on untouched app state
|
||||
|
||||
$("#folder-name-custom").on("click", () => {
|
||||
$("#folder-name-appid").attr("aria-selected", "false");
|
||||
$("#folder-name-custom").attr("aria-selected", "true");
|
||||
$("#folder-name-custom-input").prop("disabled", false);
|
||||
});
|
||||
|
||||
console.log(await getVersion());
|
||||
|
||||
$("#version-info").on("click", async () => {
|
||||
await openUrl(`https://github.com/mmvanheusden/SteamDepotDownloaderGUI/releases/v${await getVersion()}`);
|
||||
});
|
||||
});
|
||||
|
||||
function setTheme(theme: string) {
|
||||
$("#theme-auto").attr("aria-selected", String(theme === "auto"));
|
||||
$("#theme-light").attr("aria-selected", String(theme === "light"));
|
||||
$("#theme-dark").attr("aria-selected", String(theme === "dark"));
|
||||
$("#theme").attr("data-color-mode", theme);
|
||||
localStorage.theme = theme;
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -3,25 +3,24 @@
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020",
|
||||
"DOM",
|
||||
"DOM.Iterable"
|
||||
],
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "preact",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
"include": ["src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
||||
10
tsconfig.node.json
Normal file
10
tsconfig.node.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"skipLibCheck": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import {defineConfig} from "vite";
|
||||
import preact from "@preact/preset-vite";
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// @ts-expect-error process is a nodejs global
|
||||
@@ -35,5 +36,6 @@ export default defineConfig(async () => ({
|
||||
},
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
preact(),
|
||||
]
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user