Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f23d1025d2
|
||
|
|
a8573c4b6a
|
||
|
|
4054fdd06b
|
||
|
|
0b4e639630
|
||
|
|
c65fb5e1a5
|
||
|
|
6486498c74
|
||
|
|
cf72d143f3
|
||
|
|
ee7c50c6f5
|
||
|
|
f7317c8232
|
||
|
|
9856cf2426
|
No files matched your search
@@ -10,11 +10,14 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
||||||
"@fortawesome/react-fontawesome": "3.2.0",
|
"@fortawesome/react-fontawesome": "3.2.0",
|
||||||
"@tauri-apps/api": "2.10.1",
|
"@tauri-apps/api": "2.10.1",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "2.6.0",
|
"@tauri-apps/plugin-dialog": "2.6.0",
|
||||||
"@tauri-apps/plugin-fs": "2.4.5",
|
"@tauri-apps/plugin-fs": "2.4.5",
|
||||||
"@tauri-apps/plugin-http": "2.5.7",
|
"@tauri-apps/plugin-http": "2.5.7",
|
||||||
|
"@tauri-apps/plugin-notification": "2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "2.5.3",
|
"@tauri-apps/plugin-opener": "2.5.3",
|
||||||
"@tauri-apps/plugin-os": "2.3.2",
|
"@tauri-apps/plugin-os": "2.3.2",
|
||||||
|
"@tauri-apps/plugin-window-state": "2.4.1",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"pretty-bytes": "7.1.0",
|
"pretty-bytes": "7.1.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
@@ -25,7 +28,7 @@
|
|||||||
"@tailwindcss/postcss": "4.2.1",
|
"@tailwindcss/postcss": "4.2.1",
|
||||||
"@tauri-apps/cli": "2.10.1",
|
"@tauri-apps/cli": "2.10.1",
|
||||||
"@types/crypto-js": "4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/node": "25.3.5",
|
"@types/node": "25.4.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"crypto-js": "4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
@@ -263,16 +266,22 @@
|
|||||||
|
|
||||||
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="],
|
"@tauri-apps/cli-win32-x64-msvc": ["@tauri-apps/cli-win32-x64-msvc@2.10.1", "", { "os": "win32", "cpu": "x64" }, "sha512-6Cn7YpPFwzChy0ERz6djKEmUehWrYlM+xTaNzGPgZocw3BD7OfwfWHKVWxXzdjEW2KfKkHddfdxK1XXTYqBRLg=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": ["@tauri-apps/plugin-clipboard-manager@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CUlb5Hqi2oZbcZf4VUyUH53XWPPdtpw43EUpCza5HWZJwxEoDowFzNUDt1tRUXA8Uq+XPn17Ysfptip33sG4eQ=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
"@tauri-apps/plugin-dialog": ["@tauri-apps/plugin-dialog@2.6.0", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-q4Uq3eY87TdcYzXACiYSPhmpBA76shgmQswGkSVio4C82Sz2W4iehe9TnKYwbq7weHiL88Yw19XZm7v28+Micg=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="],
|
"@tauri-apps/plugin-fs": ["@tauri-apps/plugin-fs@2.4.5", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-dVxWWGE6VrOxC7/jlhyE+ON/Cc2REJlM35R3PJX3UvFw2XwYhLGQVAIyrehenDdKjotipjYEVc4YjOl3qq90fA=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="],
|
"@tauri-apps/plugin-http": ["@tauri-apps/plugin-http@2.5.7", "", { "dependencies": { "@tauri-apps/api": "^2.10.1" } }, "sha512-+F2lEH/c9b0zSsOXKq+5hZNcd9F4IIKCK1T17RqMwpCmVnx2aoqY8yIBccCd25HTYUb3j6NPVbRax/m00hKG8A=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-notification": ["@tauri-apps/plugin-notification@2.3.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-Zw+ZH18RJb41G4NrfHgIuofJiymusqN+q8fGUIIV7vyCH+5sSn5coqRv/MWB9qETsUs97vmU045q7OyseCV3Qg=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="],
|
"@tauri-apps/plugin-opener": ["@tauri-apps/plugin-opener@2.5.3", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ=="],
|
||||||
|
|
||||||
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
|
"@tauri-apps/plugin-os": ["@tauri-apps/plugin-os@2.3.2", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A=="],
|
||||||
|
|
||||||
|
"@tauri-apps/plugin-window-state": ["@tauri-apps/plugin-window-state@2.4.1", "", { "dependencies": { "@tauri-apps/api": "^2.8.0" } }, "sha512-OuvdrzyY8Q5Dbzpj+GcrnV1iCeoZbcFdzMjanZMMcAEUNy/6PH5pxZPXpaZLOR7whlzXiuzx0L9EKZbH7zpdRw=="],
|
||||||
|
|
||||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||||
|
|
||||||
"@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="],
|
"@types/crypto-js": ["@types/crypto-js@4.2.2", "", {}, "sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ=="],
|
||||||
@@ -283,7 +292,7 @@
|
|||||||
|
|
||||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@25.3.5", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA=="],
|
"@types/node": ["@types/node@25.4.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
"@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "lncvrt-games-launcher",
|
"name": "lncvrt-games-launcher",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "1.6.5",
|
"version": "1.7.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
"@fortawesome/free-solid-svg-icons": "7.2.0",
|
||||||
"@fortawesome/react-fontawesome": "3.2.0",
|
"@fortawesome/react-fontawesome": "3.2.0",
|
||||||
"@tauri-apps/api": "2.10.1",
|
"@tauri-apps/api": "2.10.1",
|
||||||
|
"@tauri-apps/plugin-clipboard-manager": "2.3.2",
|
||||||
"@tauri-apps/plugin-dialog": "2.6.0",
|
"@tauri-apps/plugin-dialog": "2.6.0",
|
||||||
"@tauri-apps/plugin-fs": "2.4.5",
|
"@tauri-apps/plugin-fs": "2.4.5",
|
||||||
"@tauri-apps/plugin-http": "2.5.7",
|
"@tauri-apps/plugin-http": "2.5.7",
|
||||||
|
"@tauri-apps/plugin-notification": "2.3.3",
|
||||||
"@tauri-apps/plugin-opener": "2.5.3",
|
"@tauri-apps/plugin-opener": "2.5.3",
|
||||||
"@tauri-apps/plugin-os": "2.3.2",
|
"@tauri-apps/plugin-os": "2.3.2",
|
||||||
|
"@tauri-apps/plugin-window-state": "2.4.1",
|
||||||
"next": "16.1.6",
|
"next": "16.1.6",
|
||||||
"pretty-bytes": "7.1.0",
|
"pretty-bytes": "7.1.0",
|
||||||
"react": "19.2.4",
|
"react": "19.2.4",
|
||||||
@@ -31,7 +34,7 @@
|
|||||||
"@tailwindcss/postcss": "4.2.1",
|
"@tailwindcss/postcss": "4.2.1",
|
||||||
"@tauri-apps/cli": "2.10.1",
|
"@tauri-apps/cli": "2.10.1",
|
||||||
"@types/crypto-js": "4.2.2",
|
"@types/crypto-js": "4.2.2",
|
||||||
"@types/node": "25.3.5",
|
"@types/node": "25.4.0",
|
||||||
"@types/react": "19.2.14",
|
"@types/react": "19.2.14",
|
||||||
"@types/react-dom": "19.2.3",
|
"@types/react-dom": "19.2.3",
|
||||||
"crypto-js": "4.2.0",
|
"crypto-js": "4.2.0",
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
[build]
|
|
||||||
target = "x86_64-win7-windows-msvc"
|
|
||||||
|
|
||||||
[unstable]
|
|
||||||
build-std = ["std", "panic_abort"]
|
|
||||||
|
|
||||||
[toolchain]
|
|
||||||
channel = "nightly"
|
|
||||||
+14
-7
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "lncvrt-games-launcher"
|
name = "lncvrt-games-launcher"
|
||||||
version = "1.6.5"
|
version = "1.7.0"
|
||||||
authors = ["Lncvrt"]
|
authors = ["Lncvrt"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
tauri-build = { version = "2.5.6", features = [] }
|
tauri-build = { version = "2.5.6", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tauri = { version = "2.10.3", features = [] }
|
tauri = { version = "2.10.3", features = ["macos-private-api"] }
|
||||||
tauri-plugin-opener = "2.5.3"
|
tauri-plugin-opener = "2.5.3"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
@@ -20,16 +20,23 @@ tauri-plugin-os = "2.3.2"
|
|||||||
reqwest = { version = "0.13.2", default-features = false, features = ["stream", "rustls"] }
|
reqwest = { version = "0.13.2", default-features = false, features = ["stream", "rustls"] }
|
||||||
tokio = "1.50.0"
|
tokio = "1.50.0"
|
||||||
futures-util = { version = "0.3.32", features = ["io"] }
|
futures-util = { version = "0.3.32", features = ["io"] }
|
||||||
|
tauri-plugin-decorum = "1.1.1"
|
||||||
tauri-plugin-fs = "2.4.5"
|
tauri-plugin-fs = "2.4.5"
|
||||||
zip = "8.2.0"
|
zip = "8.2.0"
|
||||||
libc = "0.2.183"
|
|
||||||
tauri-plugin-dialog = "2.6.0"
|
tauri-plugin-dialog = "2.6.0"
|
||||||
|
tauri-plugin-notification = "2.3.3"
|
||||||
|
sysinfo = "0.38.4"
|
||||||
|
sha2 = "0.10.9"
|
||||||
|
tauri-plugin-clipboard-manager = "2.3.2"
|
||||||
|
tauri-plugin-prevent-default = "4.0.4"
|
||||||
|
openssl = { version = "0.10.75", features = ["v110", "vendored"] }
|
||||||
|
base64 = "0.22.1"
|
||||||
tauri-plugin-http = "2.5.7"
|
tauri-plugin-http = "2.5.7"
|
||||||
|
dashmap = "6.1.0"
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
shlex = "1.3.0"
|
|
||||||
|
|
||||||
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
|
||||||
tauri-plugin-single-instance = "2.4.0"
|
tauri-plugin-single-instance = "2.4.0"
|
||||||
|
tauri-plugin-window-state = "2.4.1"
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "windows")'.dependencies]
|
||||||
|
tauri-plugin-decorum = "1.1.1"
|
||||||
@@ -9,8 +9,23 @@
|
|||||||
"core:default",
|
"core:default",
|
||||||
"opener:default",
|
"opener:default",
|
||||||
"os:default",
|
"os:default",
|
||||||
|
"core:window:default",
|
||||||
|
"core:window:allow-close",
|
||||||
|
"core:window:allow-center",
|
||||||
|
"core:window:allow-minimize",
|
||||||
|
"core:window:allow-maximize",
|
||||||
|
"core:window:allow-set-size",
|
||||||
|
"core:window:allow-set-focus",
|
||||||
|
"core:window:allow-is-maximized",
|
||||||
|
"core:window:allow-start-dragging",
|
||||||
|
"core:window:allow-toggle-maximize",
|
||||||
|
"core:window:allow-request-user-attention",
|
||||||
|
"decorum:allow-show-snap-overlay",
|
||||||
"fs:default",
|
"fs:default",
|
||||||
"dialog:default",
|
"dialog:default",
|
||||||
|
"notification:default",
|
||||||
|
"clipboard-manager:default",
|
||||||
|
"clipboard-manager:allow-write-text",
|
||||||
"fs:allow-applocaldata-read",
|
"fs:allow-applocaldata-read",
|
||||||
"fs:allow-applocaldata-write",
|
"fs:allow-applocaldata-write",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"identifier": "desktop-capability",
|
||||||
|
"platforms": [
|
||||||
|
"macOS",
|
||||||
|
"windows",
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"windows": [
|
||||||
|
"main"
|
||||||
|
],
|
||||||
|
"permissions": [
|
||||||
|
"window-state:default"
|
||||||
|
]
|
||||||
|
}
|
||||||
+506
-32
@@ -1,4 +1,13 @@
|
|||||||
|
use base64::{Engine as _, engine::general_purpose};
|
||||||
|
use dashmap::DashMap;
|
||||||
use futures_util::stream::StreamExt;
|
use futures_util::stream::StreamExt;
|
||||||
|
use openssl::pkey::PKey;
|
||||||
|
use openssl::sign::Verifier;
|
||||||
|
use sha2::{Digest, Sha512};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::time::Instant;
|
||||||
use std::{
|
use std::{
|
||||||
fs::{File, create_dir_all},
|
fs::{File, create_dir_all},
|
||||||
io::{BufReader, copy},
|
io::{BufReader, copy},
|
||||||
@@ -6,9 +15,42 @@ use std::{
|
|||||||
process::Command,
|
process::Command,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
use sysinfo::System;
|
||||||
|
use tauri::window::{ProgressBarState, ProgressBarStatus};
|
||||||
use tauri::{AppHandle, Emitter, Manager};
|
use tauri::{AppHandle, Emitter, Manager};
|
||||||
|
use tauri_plugin_os::platform;
|
||||||
|
use tauri_plugin_prevent_default::Flags;
|
||||||
|
use tokio::io::AsyncReadExt;
|
||||||
use tokio::{io::AsyncWriteExt, time::timeout};
|
use tokio::{io::AsyncWriteExt, time::timeout};
|
||||||
use zip::ZipArchive;
|
use zip::ZipArchive;
|
||||||
|
use zip::result::ZipError;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
|
||||||
|
static CANCEL_MAP: OnceLock<DashMap<String, bool>> = OnceLock::new();
|
||||||
|
|
||||||
|
fn cancel_map() -> &'static DashMap<String, bool> {
|
||||||
|
CANCEL_MAP.get_or_init(DashMap::new)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_cancelled(name: &str) -> bool {
|
||||||
|
cancel_map().get(name).map(|v| *v).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
fn is_running_by_path(path: &Path) -> bool {
|
||||||
|
let sys = System::new_all();
|
||||||
|
let target = match path.canonicalize() {
|
||||||
|
Ok(t) => t,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
sys.processes().values().any(|proc| {
|
||||||
|
proc.exe()
|
||||||
|
.and_then(|exe| exe.canonicalize().ok())
|
||||||
|
.map_or(false, |exe| exe == target)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn should_skip(name: &str) -> bool {
|
fn should_skip(name: &str) -> bool {
|
||||||
name.starts_with("__MACOSX/")
|
name.starts_with("__MACOSX/")
|
||||||
@@ -17,23 +59,33 @@ fn should_skip(name: &str) -> bool {
|
|||||||
|| name.ends_with(".DS_Store")
|
|| name.ends_with(".DS_Store")
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
async fn unzip_to_dir(app: AppHandle, zip_path: PathBuf, out_dir: PathBuf, name: String) -> String {
|
||||||
|
let _ = app.emit("unzip-start", format!("{}", name));
|
||||||
let res = tauri::async_runtime::spawn_blocking(move || {
|
let res = tauri::async_runtime::spawn_blocking(move || {
|
||||||
let file = File::open(zip_path)?;
|
let file = File::open(zip_path)?;
|
||||||
let mut archive = ZipArchive::new(BufReader::new(file))?;
|
let mut archive = ZipArchive::new(BufReader::new(file))?;
|
||||||
|
let total = archive.len();
|
||||||
|
|
||||||
for i in 0..archive.len() {
|
for i in 0..total {
|
||||||
let mut entry = archive.by_index(i)?;
|
let mut entry = archive.by_index(i)?;
|
||||||
let name = entry.name();
|
let entry_name = entry.name();
|
||||||
|
|
||||||
if should_skip(name) {
|
if should_skip(entry_name) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let outpath = out_dir.join(name);
|
let outpath = out_dir.join(entry_name);
|
||||||
|
if !outpath.starts_with(&out_dir) {
|
||||||
|
return Err(ZipError::InvalidArchive("Path traversal detected".into()));
|
||||||
|
}
|
||||||
|
|
||||||
if entry.is_dir() {
|
if entry.is_dir() {
|
||||||
create_dir_all(&outpath)?;
|
create_dir_all(&outpath)?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(mode) = entry.unix_mode() {
|
||||||
|
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if let Some(parent) = outpath.parent() {
|
if let Some(parent) = outpath.parent() {
|
||||||
create_dir_all(parent)?;
|
create_dir_all(parent)?;
|
||||||
@@ -41,9 +93,16 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
|||||||
|
|
||||||
let mut outfile = File::create(&outpath)?;
|
let mut outfile = File::create(&outpath)?;
|
||||||
copy(&mut entry, &mut outfile)?;
|
copy(&mut entry, &mut outfile)?;
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
if let Some(mode) = entry.unix_mode() {
|
||||||
|
std::fs::set_permissions(&outpath, std::fs::Permissions::from_mode(mode))?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let _ = app.emit("unzip-progress", format!("{}:{}:{}", name, i + 1, total));
|
||||||
|
}
|
||||||
|
|
||||||
Ok::<(), zip::result::ZipError>(())
|
Ok::<(), zip::result::ZipError>(())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
@@ -55,79 +114,381 @@ async fn unzip_to_dir(zip_path: PathBuf, out_dir: PathBuf) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn download(app: AppHandle, url: String, name: String) -> String {
|
fn folder_size(app: AppHandle, version: String) -> String {
|
||||||
let client = reqwest::Client::new();
|
let path = match app.path().app_local_data_dir() {
|
||||||
let resp = match client.get(&url).send().await {
|
Ok(p) => p.join("game").join(&version),
|
||||||
Ok(r) => r,
|
|
||||||
Err(_) => return "-1".to_string(),
|
Err(_) => return "-1".to_string(),
|
||||||
};
|
};
|
||||||
|
fn inner(path: &Path) -> u64 {
|
||||||
|
let mut size = 0;
|
||||||
|
if let Ok(entries) = fs::read_dir(path) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
if let Ok(metadata) = entry.metadata() {
|
||||||
|
if metadata.is_file() {
|
||||||
|
size += metadata.len();
|
||||||
|
} else if metadata.is_dir() {
|
||||||
|
size += inner(&entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size
|
||||||
|
}
|
||||||
|
|
||||||
let total_size = resp.content_length().unwrap_or(0);
|
let p = Path::new(&path);
|
||||||
|
if p.exists() && p.is_dir() {
|
||||||
|
inner(p).to_string()
|
||||||
|
} else {
|
||||||
|
"-1".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut downloaded: u64 = 0;
|
#[allow(unused_variables)]
|
||||||
let mut stream = resp.bytes_stream();
|
#[tauri::command]
|
||||||
|
async fn download(
|
||||||
|
app: AppHandle,
|
||||||
|
url: String,
|
||||||
|
name: String,
|
||||||
|
executable: String,
|
||||||
|
hash: String,
|
||||||
|
) -> String {
|
||||||
|
println!("[download] Starting download for '{}' from '{}'", name, url);
|
||||||
|
let _ = app.emit("download-start", format!("{}", name));
|
||||||
|
let window = app.get_webview_window("main").expect("main window missing");
|
||||||
|
|
||||||
let downloads_path = match app.path().app_local_data_dir() {
|
let downloads_path = match app.path().app_local_data_dir() {
|
||||||
Ok(p) => p.join("downloads"),
|
Ok(p) => p.join("downloads"),
|
||||||
Err(_) => return "-1".to_string(),
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Failed to resolve app_local_data_dir: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
let game_path = match app.path().app_local_data_dir() {
|
let game_path = match app.path().app_local_data_dir() {
|
||||||
Ok(p) => p.join("game"),
|
Ok(p) => p.join("game"),
|
||||||
Err(_) => return "-1".to_string(),
|
Err(e) => {
|
||||||
|
println!("[download] ERROR: Failed to resolve game path: {}", e);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let download_part_path = downloads_path.join(format!("{}.part", name));
|
let download_part_path = downloads_path.join(format!("{}.part", name));
|
||||||
let download_zip_path = downloads_path.join(format!("{}.zip", name));
|
let download_zip_path = downloads_path.join(format!("{}.zip", name));
|
||||||
|
println!("[download] Part file: {:?}", download_part_path);
|
||||||
if download_part_path.exists() {
|
println!("[download] Zip file: {:?}", download_zip_path);
|
||||||
let _ = tokio::fs::remove_file(&download_part_path).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
let _ = tokio::fs::create_dir_all(&downloads_path).await;
|
let _ = tokio::fs::create_dir_all(&downloads_path).await;
|
||||||
|
|
||||||
if let Ok(true) = tokio::fs::try_exists(&game_path.join(name.clone())).await {
|
let resume_from: u64 = match tokio::fs::metadata(&download_part_path).await {
|
||||||
let _ = tokio::fs::remove_dir_all(&game_path.join(name.clone())).await;
|
Ok(meta) => {
|
||||||
|
let size = meta.len();
|
||||||
|
println!(
|
||||||
|
"[download] Found existing .part file ({} bytes), will attempt to resume",
|
||||||
|
size
|
||||||
|
);
|
||||||
|
size
|
||||||
|
}
|
||||||
|
Err(_) => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut request = client.get(&url);
|
||||||
|
if resume_from > 0 {
|
||||||
|
request = request.header("Range", format!("bytes={}-", resume_from));
|
||||||
|
println!("[download] Requesting range: bytes={}-", resume_from);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let resp = match request.send().await {
|
||||||
|
Ok(r) => {
|
||||||
|
println!("[download] HTTP request succeeded, status: {}", r.status());
|
||||||
|
r
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[download] ERROR: HTTP request failed: {:?}", e);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let status = resp.status();
|
||||||
|
|
||||||
|
let actual_resume_from = if resume_from > 0 && status == reqwest::StatusCode::OK {
|
||||||
|
println!(
|
||||||
|
"[download] Server does not support range requests (got 200), restarting from scratch"
|
||||||
|
);
|
||||||
|
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||||
|
0
|
||||||
|
} else if resume_from > 0 && status == reqwest::StatusCode::PARTIAL_CONTENT {
|
||||||
|
println!("[download] Server accepted range request (206 Partial Content)");
|
||||||
|
resume_from
|
||||||
|
} else if resume_from > 0 {
|
||||||
|
println!(
|
||||||
|
"[download] Unexpected status {} while resuming, restarting",
|
||||||
|
status
|
||||||
|
);
|
||||||
|
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
let remaining_size = resp.content_length().unwrap_or(0);
|
||||||
|
let total_size = actual_resume_from + remaining_size;
|
||||||
|
println!(
|
||||||
|
"[download] Resume offset: {} bytes | Remaining: {} bytes | Total: {} bytes ({:.2} MB)",
|
||||||
|
actual_resume_from,
|
||||||
|
remaining_size,
|
||||||
|
total_size,
|
||||||
|
total_size as f64 / 1_048_576.0
|
||||||
|
);
|
||||||
|
let _ = app.emit("download-size", format!("{}:{}", &name, &total_size));
|
||||||
|
|
||||||
|
let mut file = if actual_resume_from > 0 {
|
||||||
|
match tokio::fs::OpenOptions::new()
|
||||||
|
.append(true)
|
||||||
|
.open(&download_part_path)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(f) => {
|
||||||
|
println!("[download] Opened .part file in append mode for resume");
|
||||||
|
f
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Failed to open .part file for append: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Ok(true) = tokio::fs::try_exists(&game_path.join(&name)).await {
|
||||||
|
println!(
|
||||||
|
"[download] Existing game dir found, removing: {:?}",
|
||||||
|
game_path.join(&name)
|
||||||
|
);
|
||||||
|
let _ = tokio::fs::remove_dir_all(&game_path.join(&name)).await;
|
||||||
|
}
|
||||||
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||||
|
|
||||||
let mut file = match tokio::fs::File::create(download_part_path.clone()).await {
|
match tokio::fs::File::create(&download_part_path).await {
|
||||||
Ok(f) => f,
|
Ok(f) => {
|
||||||
Err(_) => return "-1".to_string(),
|
println!("[download] Created new .part file");
|
||||||
|
f
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[download] ERROR: Failed to create .part file: {}", e);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let mut downloaded: u64 = actual_resume_from;
|
||||||
|
let mut stream = resp.bytes_stream();
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let mut chunk_count: u64 = 0;
|
||||||
|
|
||||||
while let Ok(Some(chunk_result)) = timeout(Duration::from_secs(5), stream.next()).await {
|
while let Ok(Some(chunk_result)) = timeout(Duration::from_secs(5), stream.next()).await {
|
||||||
|
if is_cancelled(&name) {
|
||||||
|
println!("[download] Cancelled '{}' during chunk loop", name);
|
||||||
|
cancel_map().remove(&name);
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::None),
|
||||||
|
progress: Some(0),
|
||||||
|
});
|
||||||
|
return "0".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
let chunk = match chunk_result {
|
let chunk = match chunk_result {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return "-1".to_string(),
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Failed to read chunk #{}: {}",
|
||||||
|
chunk_count, e
|
||||||
|
);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(_) = file.write_all(&chunk).await {
|
chunk_count += 1;
|
||||||
|
|
||||||
|
if let Err(e) = file.write_all(&chunk).await {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Failed to write chunk #{} to disk: {}",
|
||||||
|
chunk_count, e
|
||||||
|
);
|
||||||
|
let _ = &app.emit("download-stop", format!("{}", name));
|
||||||
return "-1".to_string();
|
return "-1".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
downloaded += chunk.len() as u64;
|
downloaded += chunk.len() as u64;
|
||||||
|
|
||||||
|
let progress = if total_size > 0 {
|
||||||
|
((downloaded as f64) / (total_size as f64)) * 100.0
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let session_bytes = downloaded - actual_resume_from;
|
||||||
|
let elapsed_secs = start.elapsed().as_secs_f64();
|
||||||
|
let speed = if elapsed_secs > 0.0 {
|
||||||
|
session_bytes as f64 / elapsed_secs
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let eta_secs = if total_size > downloaded && speed > 0.0 {
|
||||||
|
(total_size - downloaded) as f64 / speed
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
if chunk_count % 50 == 0 || downloaded == total_size {
|
||||||
|
println!(
|
||||||
|
"[download] [{:.1}%] {:.2} MB / {:.2} MB | Speed: {:.1} KB/s | ETA: {:.1}s | Chunks: {}",
|
||||||
|
progress,
|
||||||
|
downloaded as f64 / 1_048_576.0,
|
||||||
|
total_size as f64 / 1_048_576.0,
|
||||||
|
speed / 1024.0,
|
||||||
|
eta_secs,
|
||||||
|
chunk_count,
|
||||||
|
);
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::Normal),
|
||||||
|
progress: Some(progress as u64),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let _ = app.emit(
|
||||||
|
"download-progress",
|
||||||
|
format!(
|
||||||
|
"{}:{:.8}:{}:{}:{:.2}",
|
||||||
|
&name, progress, downloaded, speed, eta_secs
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if total_size > 0 && downloaded < total_size {
|
if total_size > 0 && downloaded < total_size {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Download incomplete — got {} of {} bytes ({:.1}% complete)",
|
||||||
|
downloaded,
|
||||||
|
total_size,
|
||||||
|
(downloaded as f64 / total_size as f64) * 100.0
|
||||||
|
);
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::None),
|
||||||
|
progress: Some(0),
|
||||||
|
});
|
||||||
return "-1".to_string();
|
return "-1".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let _ = app.emit("download-finishing", format!("{}", &name));
|
println!(
|
||||||
|
"[download] Download complete: {} bytes total in {:.2}s (this session)",
|
||||||
|
downloaded,
|
||||||
|
start.elapsed().as_secs_f64()
|
||||||
|
);
|
||||||
|
println!("[download] Verifying SHA-512 hash...");
|
||||||
|
let _ = app.emit("download-hash-checking", format!("{}", &name));
|
||||||
|
|
||||||
if let Err(_) = tokio::fs::rename(download_part_path.clone(), download_zip_path.clone()).await {
|
let download_hash = {
|
||||||
|
let mut file = match tokio::fs::File::open(&download_part_path).await {
|
||||||
|
Ok(f) => f,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Failed to open .part file for hashing: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let mut hasher = Sha512::new();
|
||||||
|
let mut bytes_hashed: u64 = 0;
|
||||||
|
let mut buffer = [0u8; 8192];
|
||||||
|
loop {
|
||||||
|
let bytes_read = match file.read(&mut buffer).await {
|
||||||
|
Ok(n) => n,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[download] ERROR: Failed to read file during hashing: {}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if bytes_read == 0 {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
hasher.update(&buffer[..bytes_read]);
|
||||||
|
bytes_hashed += bytes_read as u64;
|
||||||
|
}
|
||||||
|
drop(file);
|
||||||
|
println!("[download] Hashed {} bytes", bytes_hashed);
|
||||||
|
format!("{:x}", hasher.finalize())
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("[download] Expected hash: {}", hash);
|
||||||
|
println!("[download] Computed hash: {}", download_hash);
|
||||||
|
|
||||||
|
if hash != download_hash {
|
||||||
|
println!("[download] ERROR: Hash mismatch! Deleting corrupt .part file.");
|
||||||
|
let _ = tokio::fs::remove_file(&download_part_path).await;
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::None),
|
||||||
|
progress: Some(0),
|
||||||
|
});
|
||||||
return "-1".to_string();
|
return "-1".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
let unzip_res = unzip_to_dir(download_zip_path.clone(), game_path.join(&name)).await;
|
println!("[download] Hash verified OK");
|
||||||
|
println!("[download] Renaming .part -> .zip: {:?}", download_zip_path);
|
||||||
|
if let Err(e) = tokio::fs::rename(&download_part_path, &download_zip_path).await {
|
||||||
|
println!("[download] ERROR: Failed to rename .part to .zip: {}", e);
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::None),
|
||||||
|
progress: Some(0),
|
||||||
|
});
|
||||||
|
return "-1".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
let _ = tokio::fs::remove_file(download_zip_path.clone()).await;
|
let _ = tokio::fs::create_dir_all(&game_path.join(&name)).await;
|
||||||
|
|
||||||
|
println!("[download] Unzipping to: {:?}", game_path.join(&name));
|
||||||
|
let unzip_res = unzip_to_dir(
|
||||||
|
app.clone(),
|
||||||
|
download_zip_path.clone(),
|
||||||
|
game_path.join(&name),
|
||||||
|
name.clone(),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
println!("[download] Removing zip file: {:?}", download_zip_path);
|
||||||
|
let _ = tokio::fs::remove_file(&download_zip_path).await;
|
||||||
|
|
||||||
if unzip_res == "-1" {
|
if unzip_res == "-1" {
|
||||||
|
println!("[download] ERROR: Unzip failed for '{}'", name);
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::None),
|
||||||
|
progress: Some(0),
|
||||||
|
});
|
||||||
return "-1".to_string();
|
return "-1".to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
return "1".to_string();
|
println!(
|
||||||
|
"[download] SUCCESS: '{}' downloaded, verified, and extracted",
|
||||||
|
&name
|
||||||
|
);
|
||||||
|
let _ = window.set_progress_bar(ProgressBarState {
|
||||||
|
status: Some(ProgressBarStatus::None),
|
||||||
|
progress: Some(0),
|
||||||
|
});
|
||||||
|
"1".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
@@ -148,12 +509,87 @@ fn launch_game(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(_) = Command::new(game_folder.join(&executable))
|
let exe_path = game_folder.join(&executable);
|
||||||
|
|
||||||
|
//if already running on macos, it'll auto take the user to that proccess
|
||||||
|
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||||
|
{
|
||||||
|
use tauri_plugin_dialog::DialogExt;
|
||||||
|
use tauri_plugin_dialog::MessageDialogKind;
|
||||||
|
|
||||||
|
if !use_wine && is_running_by_path(&exe_path) {
|
||||||
|
app.dialog()
|
||||||
|
.message(format!(
|
||||||
|
"{} is already running, if this doesn't seem true, try to kill the proccess.",
|
||||||
|
display_name
|
||||||
|
))
|
||||||
|
.kind(MessageDialogKind::Error)
|
||||||
|
.title("Game already running")
|
||||||
|
.show(|_| {});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
if use_wine {
|
||||||
|
let quoted_path = format!("\"{}\"", exe_path.to_string_lossy());
|
||||||
|
let cmd = wine_command.replace("%path%", "ed_path);
|
||||||
|
|
||||||
|
if let Err(_) = Command::new("bash")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(cmd)
|
||||||
.current_dir(&game_folder)
|
.current_dir(&game_folder)
|
||||||
.spawn()
|
.spawn()
|
||||||
{
|
{
|
||||||
|
eprintln!("Failed to launch game with Wine");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if platform() == "macos" {
|
||||||
|
if let Err(_) = Command::new("open")
|
||||||
|
.arg(&executable)
|
||||||
|
.current_dir(&game_folder)
|
||||||
|
.spawn()
|
||||||
|
{
|
||||||
|
eprintln!("Failed to launch game on macOS");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Err(_) = Command::new(&exe_path).current_dir(&game_folder).spawn() {
|
||||||
eprintln!("Failed to launch game");
|
eprintln!("Failed to launch game");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn verify_signature(body: String, signature: String, public_key: String) -> bool {
|
||||||
|
let Ok(pem) = general_purpose::STANDARD.decode(public_key) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(pubkey) = PKey::public_key_from_pem(&pem) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Ok(sig) = general_purpose::STANDARD.decode(signature) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut verifier = match Verifier::new_without_digest(&pubkey) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(_) => return false,
|
||||||
|
};
|
||||||
|
|
||||||
|
verifier
|
||||||
|
.verify_oneshot(&sig, body.as_bytes())
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn cancel_download(name: String) {
|
||||||
|
println!("[cancel_download] Cancellation requested for '{}'", name);
|
||||||
|
cancel_map().insert(name.clone(), true);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@@ -161,6 +597,25 @@ pub fn run() {
|
|||||||
#[allow(unused_variables)]
|
#[allow(unused_variables)]
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_http::init())
|
.plugin(tauri_plugin_http::init())
|
||||||
|
.plugin(
|
||||||
|
tauri_plugin_prevent_default::Builder::new()
|
||||||
|
.with_flags(
|
||||||
|
Flags::FIND
|
||||||
|
| Flags::CARET_BROWSING
|
||||||
|
| Flags::DEV_TOOLS
|
||||||
|
| Flags::DOWNLOADS
|
||||||
|
| Flags::FOCUS_MOVE
|
||||||
|
| Flags::RELOAD
|
||||||
|
| Flags::SOURCE
|
||||||
|
| Flags::OPEN
|
||||||
|
| Flags::PRINT
|
||||||
|
| Flags::CONTEXT_MENU,
|
||||||
|
)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.plugin(tauri_plugin_window_state::Builder::new().build())
|
||||||
|
.plugin(tauri_plugin_clipboard_manager::init())
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
|
||||||
if let Some(window) = app.get_webview_window("main") {
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
let _ = window.set_focus();
|
let _ = window.set_focus();
|
||||||
@@ -168,9 +623,28 @@ pub fn run() {
|
|||||||
}))
|
}))
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
.plugin(tauri_plugin_fs::init())
|
.plugin(tauri_plugin_fs::init())
|
||||||
|
.plugin(tauri_plugin_decorum::init())
|
||||||
.plugin(tauri_plugin_os::init())
|
.plugin(tauri_plugin_os::init())
|
||||||
.plugin(tauri_plugin_opener::init())
|
.plugin(tauri_plugin_opener::init())
|
||||||
.invoke_handler(tauri::generate_handler![download, launch_game])
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
download,
|
||||||
|
launch_game,
|
||||||
|
folder_size,
|
||||||
|
verify_signature,
|
||||||
|
cancel_download
|
||||||
|
])
|
||||||
|
.setup(|app| {
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
{
|
||||||
|
use tauri_plugin_decorum::WebviewWindowExt;
|
||||||
|
if let Some(main_window) = app.get_webview_window("main") {
|
||||||
|
if let Err(e) = main_window.create_overlay_titlebar() {
|
||||||
|
eprintln!("Failed to create overlay titlebar: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://schema.tauri.app/config/2",
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
"productName": "Lncvrt Games Launcher",
|
"productName": "Lncvrt Games Launcher",
|
||||||
"version": "1.6.5",
|
"version": "1.7.0",
|
||||||
"identifier": "xyz.lncvrt.games-launcher",
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
"build": {
|
"build": {
|
||||||
"beforeDevCommand": "bun next dev --webpack",
|
"beforeDevCommand": "next dev --webpack",
|
||||||
"devUrl": "http://localhost:3000",
|
"devUrl": "http://localhost:3000",
|
||||||
"beforeBuildCommand": "bun next build",
|
"beforeBuildCommand": "next build",
|
||||||
"frontendDist": "../out"
|
"frontendDist": "../out"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"withGlobalTauri": true,
|
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"title": "Lncvrt Games Launcher",
|
"title": "Lncvrt Games Launcher",
|
||||||
@@ -25,13 +24,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": false
|
||||||
"icon": ["icons/icon.ico"],
|
|
||||||
"targets": "nsis",
|
|
||||||
"windows": {
|
|
||||||
"webviewInstallMode": {
|
|
||||||
"type": "embedBootstrapper"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Lncvrt Games Launcher",
|
||||||
|
"version": "1.7.0",
|
||||||
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "next dev --webpack",
|
||||||
|
"devUrl": "http://localhost:3000",
|
||||||
|
"beforeBuildCommand": "next build",
|
||||||
|
"frontendDist": "../out"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Lncvrt Games Launcher",
|
||||||
|
"width": 1000,
|
||||||
|
"height": 600,
|
||||||
|
"minWidth": 875,
|
||||||
|
"minHeight": 500
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Lncvrt Games Launcher",
|
||||||
|
"version": "1.7.0",
|
||||||
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "next dev --webpack",
|
||||||
|
"devUrl": "http://localhost:3000",
|
||||||
|
"beforeBuildCommand": "next build",
|
||||||
|
"frontendDist": "../out"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"macOSPrivateApi": true,
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Lncvrt Games Launcher",
|
||||||
|
"width": 1000,
|
||||||
|
"height": 600,
|
||||||
|
"minWidth": 875,
|
||||||
|
"minHeight": 500,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"hiddenTitle": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": true,
|
||||||
|
"targets": ["app"],
|
||||||
|
"macOS": {
|
||||||
|
"minimumSystemVersion": "13.0"
|
||||||
|
},
|
||||||
|
"icon": ["icons/icon.icns"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://schema.tauri.app/config/2",
|
||||||
|
"productName": "Lncvrt Games Launcher",
|
||||||
|
"version": "1.7.0",
|
||||||
|
"identifier": "xyz.lncvrt.games-launcher",
|
||||||
|
"build": {
|
||||||
|
"beforeDevCommand": "next dev --webpack",
|
||||||
|
"devUrl": "http://localhost:3000",
|
||||||
|
"beforeBuildCommand": "next build",
|
||||||
|
"frontendDist": "../out"
|
||||||
|
},
|
||||||
|
"app": {
|
||||||
|
"withGlobalTauri": true,
|
||||||
|
"windows": [
|
||||||
|
{
|
||||||
|
"title": "Lncvrt Games Launcher",
|
||||||
|
"width": 1000,
|
||||||
|
"height": 632,
|
||||||
|
"minWidth": 875,
|
||||||
|
"minHeight": 532,
|
||||||
|
"titleBarStyle": "Overlay",
|
||||||
|
"hiddenTitle": true,
|
||||||
|
"decorations": false,
|
||||||
|
"shadow": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"security": {
|
||||||
|
"csp": null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"bundle": {
|
||||||
|
"active": false,
|
||||||
|
"icon": ["icons/icon.ico"]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ body {
|
|||||||
--col6: rgb(96, 96, 96);
|
--col6: rgb(96, 96, 96);
|
||||||
--col7: rgb(112, 112, 112);
|
--col7: rgb(112, 112, 112);
|
||||||
--col8: rgb(128, 128, 128);
|
--col8: rgb(128, 128, 128);
|
||||||
|
--col9: rgb(144, 144, 144);
|
||||||
}
|
}
|
||||||
|
|
||||||
.red-theme {
|
.red-theme {
|
||||||
@@ -26,6 +27,7 @@ body {
|
|||||||
--col6: rgb(120, 96, 96);
|
--col6: rgb(120, 96, 96);
|
||||||
--col7: rgb(136, 112, 112);
|
--col7: rgb(136, 112, 112);
|
||||||
--col8: rgb(152, 128, 128);
|
--col8: rgb(152, 128, 128);
|
||||||
|
--col9: rgb(168, 144, 144);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue-theme {
|
.blue-theme {
|
||||||
@@ -38,6 +40,7 @@ body {
|
|||||||
--col6: rgb(96, 96, 120);
|
--col6: rgb(96, 96, 120);
|
||||||
--col7: rgb(112, 112, 136);
|
--col7: rgb(112, 112, 136);
|
||||||
--col8: rgb(128, 128, 152);
|
--col8: rgb(128, 128, 152);
|
||||||
|
--col9: rgb(144, 144, 168);
|
||||||
}
|
}
|
||||||
|
|
||||||
.purple-theme {
|
.purple-theme {
|
||||||
@@ -50,6 +53,7 @@ body {
|
|||||||
--col6: rgb(108, 96, 120);
|
--col6: rgb(108, 96, 120);
|
||||||
--col7: rgb(124, 112, 136);
|
--col7: rgb(124, 112, 136);
|
||||||
--col8: rgb(140, 128, 152);
|
--col8: rgb(140, 128, 152);
|
||||||
|
--col9: rgb(156, 144, 168);
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.button {
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ import { GetIconForUser } from '@/lib/BerryDash'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
import { verifySignature } from '@/lib/Util'
|
||||||
|
|
||||||
interface BaseEntry {
|
interface BaseEntry {
|
||||||
id: number
|
id: number
|
||||||
@@ -92,8 +94,12 @@ export default function BerryDashLeaderboards () {
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://games.lncvrt.xyz/api/berrydash/account?username='
|
'https://games.lncvrt.xyz/api/berrydash/account?username='
|
||||||
)
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (data.success) {
|
if (
|
||||||
|
(await verifySignature(JSON.stringify(data), signature)) &&
|
||||||
|
data.success
|
||||||
|
) {
|
||||||
let accounts = data.data as Account[]
|
let accounts = data.data as Account[]
|
||||||
|
|
||||||
accounts = accounts.map(acc => {
|
accounts = accounts.map(acc => {
|
||||||
@@ -128,8 +134,13 @@ export default function BerryDashLeaderboards () {
|
|||||||
? 'legacy'
|
? 'legacy'
|
||||||
: 'total')
|
: 'total')
|
||||||
)
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
if (data.success) setEntries(data.data as LeaderboardEntry[])
|
if (
|
||||||
|
(await verifySignature(JSON.stringify(data), signature)) &&
|
||||||
|
data.success
|
||||||
|
)
|
||||||
|
setEntries(data.data as LeaderboardEntry[])
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setEntries([])
|
setEntries([])
|
||||||
@@ -166,6 +177,7 @@ export default function BerryDashLeaderboards () {
|
|||||||
if (selected == -1) router.push('/game?id=1')
|
if (selected == -1) router.push('/game?id=1')
|
||||||
else {
|
else {
|
||||||
setSelected(-1)
|
setSelected(-1)
|
||||||
|
setSelectedBerryOption(0)
|
||||||
setEntries([])
|
setEntries([])
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
@@ -175,7 +187,13 @@ export default function BerryDashLeaderboards () {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='box h-[calc(100vh-84px)]'>
|
<div
|
||||||
|
className={`box ${
|
||||||
|
platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-116px)]'
|
||||||
|
: 'h-[calc(100vh-84px)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{selected == -1 ? (
|
{selected == -1 ? (
|
||||||
<>
|
<>
|
||||||
<p className='text-center mt-2 text-xl'>Select a Leaderboard</p>
|
<p className='text-center mt-2 text-xl'>Select a Leaderboard</p>
|
||||||
@@ -228,7 +246,13 @@ export default function BerryDashLeaderboards () {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-col gap-2 overflow-y-auto ${
|
className={`flex flex-col gap-2 overflow-y-auto ${
|
||||||
selected == 1 ? 'h-[calc(100vh-136px)]' : 'h-[calc(100vh-96px)]'
|
selected == 1
|
||||||
|
? platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-156px)]'
|
||||||
|
: 'h-[calc(100vh-124px)]'
|
||||||
|
: platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-128px)]'
|
||||||
|
: 'h-[calc(100vh-96px)]'
|
||||||
} px-1`}
|
} px-1`}
|
||||||
>
|
>
|
||||||
{entries.map((item, index) => {
|
{entries.map((item, index) => {
|
||||||
@@ -291,9 +315,9 @@ export default function BerryDashLeaderboards () {
|
|||||||
{selected == 1 && (
|
{selected == 1 && (
|
||||||
<div className='flex justify-center'>
|
<div className='flex justify-center'>
|
||||||
<select
|
<select
|
||||||
|
className='mt-1.25 bg-(--col2) border border-(--col4) rounded-md'
|
||||||
value={selectedBerryOption}
|
value={selectedBerryOption}
|
||||||
onChange={e => setSelectedBerryOption(Number(e.target.value))}
|
onChange={e => setSelectedBerryOption(Number(e.target.value))}
|
||||||
className='leaderboard-select mt-0.75'
|
|
||||||
>
|
>
|
||||||
<option value='0'>Normal Berry</option>
|
<option value='0'>Normal Berry</option>
|
||||||
<option value='1'>Poison Berry</option>
|
<option value='1'>Poison Berry</option>
|
||||||
|
|||||||
@@ -4,8 +4,7 @@
|
|||||||
@apply bg-(--col1) border border-(--col3) rounded-lg w-auto p-1;
|
@apply bg-(--col1) border border-(--col3) rounded-lg w-auto p-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.leaderboard-button,
|
.leaderboard-button {
|
||||||
.leaderboard-select {
|
|
||||||
@apply bg-(--col2) hover:bg-(--col4) border border-(--col4) hover:border-(--col6) rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
@apply bg-(--col2) hover:bg-(--col4) border border-(--col4) hover:border-(--col6) rounded-lg px-4 py-2 inline-block transition-all duration-200 cursor-pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,13 @@ export default function Installs () {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='downloads-container'>
|
<div className='downloads-container'>
|
||||||
<div className='downloads-scroll h-[calc(100vh-84px)]'>
|
<div
|
||||||
|
className={`downloads-scroll ${
|
||||||
|
platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-116px)]'
|
||||||
|
: 'h-[calc(100vh-84px)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{category == -1 &&
|
{category == -1 &&
|
||||||
Object.entries(game.categoryNames)
|
Object.entries(game.categoryNames)
|
||||||
.sort(([a], [b]) => Number(b) - Number(a))
|
.sort(([a], [b]) => Number(b) - Number(a))
|
||||||
|
|||||||
+227
-11
@@ -24,6 +24,11 @@ import { Game } from '@/types/Game'
|
|||||||
import { listen } from '@tauri-apps/api/event'
|
import { listen } from '@tauri-apps/api/event'
|
||||||
import { usePathname } from 'next/navigation'
|
import { usePathname } from 'next/navigation'
|
||||||
import { arch, platform } from '@tauri-apps/plugin-os'
|
import { arch, platform } from '@tauri-apps/plugin-os'
|
||||||
|
import { notifyUser } from '@/lib/Notifications'
|
||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission
|
||||||
|
} from '@tauri-apps/plugin-notification'
|
||||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||||
|
|
||||||
import VersionsDownloadPopup from '@/componets/popups/VersionsDownload'
|
import VersionsDownloadPopup from '@/componets/popups/VersionsDownload'
|
||||||
@@ -31,6 +36,8 @@ import GamesDownloadPopup from '@/componets/popups/GamesDownload'
|
|||||||
import DownloadsPopup from '@/componets/popups/Downloads'
|
import DownloadsPopup from '@/componets/popups/Downloads'
|
||||||
import VersionVersionPopup from '@/componets/popups/VersionVersion'
|
import VersionVersionPopup from '@/componets/popups/VersionVersion'
|
||||||
import { fetch } from '@tauri-apps/plugin-http'
|
import { fetch } from '@tauri-apps/plugin-http'
|
||||||
|
import { verifySignature } from '@/lib/Util'
|
||||||
|
import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window'
|
||||||
|
|
||||||
const roboto = Roboto({
|
const roboto = Roboto({
|
||||||
subsets: ['latin']
|
subsets: ['latin']
|
||||||
@@ -173,23 +180,131 @@ export default function RootLayout ({
|
|||||||
}, [popupMode, selectedGame, pathname, viewingInfoFromDownloads])
|
}, [popupMode, selectedGame, pathname, viewingInfoFromDownloads])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unlistenProgress: (() => void) | null = null
|
const unlisteners: (() => void)[] = []
|
||||||
|
|
||||||
listen<string>('download-finishing', event => {
|
const setupListeners = async () => {
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-size', event => {
|
||||||
|
const [displayName, sizeStr] = event.payload.split(':')
|
||||||
|
const size = Number(sizeStr)
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], size }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-progress', async event => {
|
||||||
|
const [displayName, progStr, totalSizeStr, speedStr, etaSecsStr] =
|
||||||
|
event.payload.split(':')
|
||||||
|
const prog = Number(progStr)
|
||||||
|
const progBytes = Number(totalSizeStr)
|
||||||
|
const speed = Number(speedStr)
|
||||||
|
const etaSecs = Number(etaSecsStr)
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
progress: prog,
|
||||||
|
progressBytes: progBytes,
|
||||||
|
speed,
|
||||||
|
etaSecs
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-hash-checking', event => {
|
||||||
const displayName = event.payload
|
const displayName = event.payload
|
||||||
setDownloadProgress(prev => {
|
setDownloadProgress(prev => {
|
||||||
const i = prev.findIndex(d => d.version === displayName)
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
if (i === -1) return prev
|
if (i === -1) return prev
|
||||||
const copy = [...prev]
|
const copy = [...prev]
|
||||||
copy[i] = { ...copy[i], finishing: true }
|
copy[i] = { ...copy[i], hash_checking: true, downloading: false }
|
||||||
return copy
|
return copy
|
||||||
})
|
})
|
||||||
}).then(f => (unlistenProgress = f))
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('unzip-start', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], hash_checking: false, unzipping: true }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('unzip-progress', async event => {
|
||||||
|
const [displayName, unzippedStr, unzipTotalStr] =
|
||||||
|
event.payload.split(':')
|
||||||
|
const unzipped = Number(unzippedStr)
|
||||||
|
const unzipTotal = Number(unzipTotalStr)
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
unzipped,
|
||||||
|
unzipTotal
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-start', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
downloading: true,
|
||||||
|
paused: false,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
unlisteners.push(
|
||||||
|
await listen<string>('download-stop', event => {
|
||||||
|
const displayName = event.payload
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === displayName)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = { ...copy[i], downloading: false }
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setupListeners()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unlistenProgress?.()
|
unlisteners.forEach(unlisten => unlisten())
|
||||||
}
|
}
|
||||||
}, [downloadProgress])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
@@ -201,11 +316,17 @@ export default function RootLayout ({
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
'https://games.lncvrt.xyz/api/launcher/latest'
|
'https://games.lncvrt.xyz/api/launcher/latest'
|
||||||
)
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
const data = await response.text()
|
const data = await response.text()
|
||||||
|
if (await verifySignature(data, signature)) {
|
||||||
if (data !== client) {
|
if (data !== client) {
|
||||||
setOutdated(true)
|
setOutdated(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
setLoadingText('Failed to check latest version.')
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setLoadingText('Failed to check latest version.')
|
setLoadingText('Failed to check latest version.')
|
||||||
return
|
return
|
||||||
@@ -215,8 +336,14 @@ export default function RootLayout ({
|
|||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
|
`https://games.lncvrt.xyz/api/launcher/versions?platform=${platform()}&arch=${arch()}`
|
||||||
)
|
)
|
||||||
|
const signature = response.headers.get('x-signature') ?? ''
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
|
if (await verifySignature(JSON.stringify(data), signature)) {
|
||||||
setServerVersionList(data)
|
setServerVersionList(data)
|
||||||
|
} else {
|
||||||
|
setLoadingText('Failed to download versions list.')
|
||||||
|
return
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
setLoadingText('Failed to download versions list.')
|
setLoadingText('Failed to download versions list.')
|
||||||
return
|
return
|
||||||
@@ -226,6 +353,10 @@ export default function RootLayout ({
|
|||||||
setDownloadedVersionsConfig(versionsConfig)
|
setDownloadedVersionsConfig(versionsConfig)
|
||||||
setNormalConfig(normalConfig)
|
setNormalConfig(normalConfig)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
|
|
||||||
|
if (!(await isPermissionGranted())) {
|
||||||
|
await requestPermission()
|
||||||
|
}
|
||||||
})()
|
})()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -243,7 +374,27 @@ export default function RootLayout ({
|
|||||||
if (newVersions.length === 0) return
|
if (newVersions.length === 0) return
|
||||||
|
|
||||||
const newDownloads = newVersions.map(
|
const newDownloads = newVersions.map(
|
||||||
version => new DownloadProgress(version, false, true, false)
|
version =>
|
||||||
|
new DownloadProgress(
|
||||||
|
version,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
setDownloadProgress(prev => [...prev, ...newDownloads])
|
setDownloadProgress(prev => [...prev, ...newDownloads])
|
||||||
@@ -286,8 +437,12 @@ export default function RootLayout ({
|
|||||||
'&downloadId=' +
|
'&downloadId=' +
|
||||||
info.download
|
info.download
|
||||||
)
|
)
|
||||||
|
const signature = downloadInfoRequest.headers.get('x-signature') ?? ''
|
||||||
const data = await downloadInfoRequest.json()
|
const data = await downloadInfoRequest.json()
|
||||||
if (!data.success) {
|
if (
|
||||||
|
!(await verifySignature(JSON.stringify(data), signature)) ||
|
||||||
|
!data.success
|
||||||
|
) {
|
||||||
setDownloadProgress(prev =>
|
setDownloadProgress(prev =>
|
||||||
prev.map(d =>
|
prev.map(d =>
|
||||||
d.version === versionId
|
d.version === versionId
|
||||||
@@ -295,12 +450,35 @@ export default function RootLayout ({
|
|||||||
: d
|
: d
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (normalConfig?.settings.allowNotifications)
|
||||||
|
await notifyUser(
|
||||||
|
'Download Failed',
|
||||||
|
`The download for version ${info.displayName} has failed.`
|
||||||
|
)
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Critical
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.version === versionId
|
||||||
|
? {
|
||||||
|
...d,
|
||||||
|
url: data.data.url,
|
||||||
|
executable: info.executable,
|
||||||
|
hash: data.data.hash
|
||||||
|
}
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
const res = await invoke<string>('download', {
|
const res = await invoke<string>('download', {
|
||||||
url: data.data.url,
|
url: data.data.url,
|
||||||
name: info.id
|
name: info.id,
|
||||||
|
executable: info.executable,
|
||||||
|
hash: data.data.hash
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res === '1') {
|
if (res === '1') {
|
||||||
@@ -319,7 +497,23 @@ export default function RootLayout ({
|
|||||||
writeVersionsConfig(updated)
|
writeVersionsConfig(updated)
|
||||||
return updated
|
return updated
|
||||||
})
|
})
|
||||||
} else {
|
} else if (res == '0') {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === info.id)
|
||||||
|
if (i === -1) return prev
|
||||||
|
if (prev[i].canceled) {
|
||||||
|
return prev.filter((_, idx) => idx !== i)
|
||||||
|
}
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
downloading: false,
|
||||||
|
paused: true,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
} else if (res == '-1') {
|
||||||
setDownloadProgress(prev =>
|
setDownloadProgress(prev =>
|
||||||
prev.map(d =>
|
prev.map(d =>
|
||||||
d.version === versionId
|
d.version === versionId
|
||||||
@@ -327,6 +521,14 @@ export default function RootLayout ({
|
|||||||
: d
|
: d
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
if (normalConfig?.settings.allowNotifications)
|
||||||
|
await notifyUser(
|
||||||
|
'Download Failed',
|
||||||
|
`The download for version ${info.displayName} has failed.`
|
||||||
|
)
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Critical
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
setDownloadQueue(prev => prev.slice(1))
|
setDownloadQueue(prev => prev.slice(1))
|
||||||
@@ -386,7 +588,17 @@ export default function RootLayout ({
|
|||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<>
|
<>
|
||||||
<div className='w-screen h-screen flex items-center justify-center'>
|
<div
|
||||||
|
className='relative z-2 w-screen border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||||
|
hidden={platformName != 'windows'}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-screen ${
|
||||||
|
platformName == 'windows'
|
||||||
|
? 'h-[calc(100vh-64px)]'
|
||||||
|
: 'h-screen'
|
||||||
|
} flex items-center justify-center`}
|
||||||
|
>
|
||||||
{outdated ? (
|
{outdated ? (
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<p className='text-8xl mb-4'>Outdated Launcher!</p>
|
<p className='text-8xl mb-4'>Outdated Launcher!</p>
|
||||||
@@ -452,6 +664,10 @@ export default function RootLayout ({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
<div
|
||||||
|
className='relative z-2 ml-59.75 w-[calc(100vw-239px)] border-b border-b-(--col3) h-8.25 bg-(--col1)'
|
||||||
|
hidden={platformName != 'windows'}
|
||||||
|
/>
|
||||||
<div className='relative z-0'>
|
<div className='relative z-0'>
|
||||||
<main className='ml-60'>{children}</main>
|
<main className='ml-60'>{children}</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+8
-1
@@ -9,6 +9,7 @@ import {
|
|||||||
faShieldHalved,
|
faShieldHalved,
|
||||||
faWarning
|
faWarning
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
export default function Installs () {
|
export default function Installs () {
|
||||||
@@ -51,7 +52,13 @@ export default function Installs () {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className='downloads-container'>
|
<div className='downloads-container'>
|
||||||
<div className='downloads-scroll h-[calc(100vh-84px)]'>
|
<div
|
||||||
|
className={`downloads-scroll ${
|
||||||
|
platform() == 'windows'
|
||||||
|
? 'h-[calc(100vh-116px)]'
|
||||||
|
: 'h-[calc(100vh-84px)]'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{downloadedVersionsConfig &&
|
{downloadedVersionsConfig &&
|
||||||
Object.keys(downloadedVersionsConfig.list).length ? (
|
Object.keys(downloadedVersionsConfig.list).length ? (
|
||||||
getListOfGames()
|
getListOfGames()
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import { useEffect, useState } from 'react'
|
|||||||
import { Setting } from '@/componets/Setting'
|
import { Setting } from '@/componets/Setting'
|
||||||
import { writeNormalConfig } from '@/lib/BazookaManager'
|
import { writeNormalConfig } from '@/lib/BazookaManager'
|
||||||
import { useGlobal } from '@/app/GlobalProvider'
|
import { useGlobal } from '@/app/GlobalProvider'
|
||||||
|
import { copyToClipboard } from '@/lib/Clipboard'
|
||||||
import { platform } from '@tauri-apps/plugin-os'
|
import { platform } from '@tauri-apps/plugin-os'
|
||||||
|
|
||||||
export default function Settings () {
|
export default function Settings () {
|
||||||
|
const [allowNotifications, setAllowNotifications] = useState(false)
|
||||||
const [alwaysShowGamesInSidebar, setAlwaysShowGamesInSidebar] =
|
const [alwaysShowGamesInSidebar, setAlwaysShowGamesInSidebar] =
|
||||||
useState(false)
|
useState(false)
|
||||||
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
|
const [useWineOnUnixWhenNeeded, setUseWineOnUnixWhenNeeded] = useState(false)
|
||||||
@@ -19,6 +21,7 @@ export default function Settings () {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
;(async () => {
|
;(async () => {
|
||||||
while (normalConfig != null) {
|
while (normalConfig != null) {
|
||||||
|
setAllowNotifications(normalConfig.settings.allowNotifications)
|
||||||
setAlwaysShowGamesInSidebar(
|
setAlwaysShowGamesInSidebar(
|
||||||
normalConfig.settings.alwaysShowGamesInSidebar
|
normalConfig.settings.alwaysShowGamesInSidebar
|
||||||
)
|
)
|
||||||
@@ -38,6 +41,29 @@ export default function Settings () {
|
|||||||
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
<p className='text-3xl ml-4 mt-4'>Settings</p>
|
||||||
{loaded && (
|
{loaded && (
|
||||||
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
<div className='ml-4 mt-4 bg-(--col1) border border-(--col3) rounded-lg p-4 w-fit h-fit'>
|
||||||
|
<Setting
|
||||||
|
label='Allow sending notifications'
|
||||||
|
value={allowNotifications}
|
||||||
|
onChange={async () => {
|
||||||
|
if (!normalConfig) return
|
||||||
|
setAllowNotifications(!allowNotifications)
|
||||||
|
setNormalConfig({
|
||||||
|
...normalConfig,
|
||||||
|
settings: {
|
||||||
|
...normalConfig.settings,
|
||||||
|
allowNotifications: !allowNotifications
|
||||||
|
}
|
||||||
|
})
|
||||||
|
await writeNormalConfig({
|
||||||
|
...normalConfig,
|
||||||
|
settings: {
|
||||||
|
...normalConfig.settings,
|
||||||
|
allowNotifications: !allowNotifications
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
title='This setting does as you expect, allow the launcher to send notifications for when stuff like downloading is done.'
|
||||||
|
/>
|
||||||
<Setting
|
<Setting
|
||||||
label='Always show games in sidebar'
|
label='Always show games in sidebar'
|
||||||
value={alwaysShowGamesInSidebar}
|
value={alwaysShowGamesInSidebar}
|
||||||
@@ -147,6 +173,9 @@ export default function Settings () {
|
|||||||
)}
|
)}
|
||||||
<p
|
<p
|
||||||
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
className='fixed bottom-1.5 right-1.5 rounded-md cursor-pointer px-1 border z-100 transition-colors btntheme1'
|
||||||
|
onClick={async () => {
|
||||||
|
await copyToClipboard(`v${version}`, normalConfig)
|
||||||
|
}}
|
||||||
title='The current launcher version.'
|
title='The current launcher version.'
|
||||||
>
|
>
|
||||||
v{version}
|
v{version}
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
type ProgressBarProps = {
|
||||||
|
progress: number
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ProgressBar ({ progress, className }: ProgressBarProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full bg-(--col1) border border-(--col5) rounded-full h-4 overflow-hidden ${className}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className='bg-(--col8) border-r border-r-(--col5) h-full'
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import { useGlobal } from '@/app/GlobalProvider'
|
|||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation'
|
||||||
|
import { getCurrentWindow } from '@tauri-apps/api/window'
|
||||||
import { Lexend } from 'next/font/google'
|
import { Lexend } from 'next/font/google'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
@@ -44,7 +45,35 @@ export default function Sidebar () {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className='sidebar'>
|
<aside className='sidebar'>
|
||||||
<div className='flex items-center h-10 w-60'>
|
<div
|
||||||
|
className='macos-drag'
|
||||||
|
hidden={platform() != 'macos'}
|
||||||
|
onMouseDown={e => {
|
||||||
|
if (e.buttons === 1) {
|
||||||
|
if (e.detail === 2) {
|
||||||
|
getCurrentWindow().toggleMaximize()
|
||||||
|
} else {
|
||||||
|
getCurrentWindow().startDragging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
className={`flex items-center h-10 w-60 ${
|
||||||
|
(platform() == 'windows' ? 'pl-1 pt-1' : 'pl-2 pt-2') +
|
||||||
|
(platform() == 'macos' ? ' mt-7' : '')
|
||||||
|
}`}
|
||||||
|
onMouseDown={e => {
|
||||||
|
if (platform() != 'macos') return
|
||||||
|
if (e.buttons === 1) {
|
||||||
|
if (e.detail === 2) {
|
||||||
|
getCurrentWindow().toggleMaximize()
|
||||||
|
} else {
|
||||||
|
getCurrentWindow().startDragging()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Image draggable={false} src={Icon} width={36} height={36} alt='' />
|
<Image draggable={false} src={Icon} width={36} height={36} alt='' />
|
||||||
<p className={`ml-1 text-[16px] whitespace-nowrap ${lexend.className}`}>
|
<p className={`ml-1 text-[16px] whitespace-nowrap ${lexend.className}`}>
|
||||||
Lncvrt Games Launcher
|
Lncvrt Games Launcher
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
import { useGlobal } from '@/app/GlobalProvider'
|
import { useGlobal } from '@/app/GlobalProvider'
|
||||||
|
import prettyBytes from 'pretty-bytes'
|
||||||
|
import ProgressBar from '../ProgressBar'
|
||||||
|
import { formatEtaSmart } from '@/lib/Util'
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
|
import { faCancel, faPause, faPlay } from '@fortawesome/free-solid-svg-icons'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { writeVersionsConfig } from '@/lib/BazookaManager'
|
||||||
|
import { notifyUser } from '@/lib/Notifications'
|
||||||
|
import { getCurrentWindow, UserAttentionType } from '@tauri-apps/api/window'
|
||||||
|
|
||||||
export default function DownloadsPopup () {
|
export default function DownloadsPopup () {
|
||||||
const {
|
const {
|
||||||
@@ -6,7 +15,9 @@ export default function DownloadsPopup () {
|
|||||||
getVersionInfo,
|
getVersionInfo,
|
||||||
setDownloadProgress,
|
setDownloadProgress,
|
||||||
downloadQueue,
|
downloadQueue,
|
||||||
setDownloadQueue
|
setDownloadQueue,
|
||||||
|
normalConfig,
|
||||||
|
setDownloadedVersionsConfig
|
||||||
} = useGlobal()
|
} = useGlobal()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -16,7 +27,130 @@ export default function DownloadsPopup () {
|
|||||||
{downloadProgress.map((v, i) => {
|
{downloadProgress.map((v, i) => {
|
||||||
const queuePosition = downloadQueue.indexOf(v.version)
|
const queuePosition = downloadQueue.indexOf(v.version)
|
||||||
return (
|
return (
|
||||||
<div key={i} className='popup-entry flex flex-col justify-between'>
|
<div
|
||||||
|
key={i}
|
||||||
|
className='popup-entry flex flex-col justify-between relative'
|
||||||
|
>
|
||||||
|
<div className='absolute right-2 top-2 flex flex-row gap-2'>
|
||||||
|
<div
|
||||||
|
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||||
|
hidden={!v.downloading && !v.paused}
|
||||||
|
onClick={async () => {
|
||||||
|
if (v.downloading) {
|
||||||
|
await invoke('cancel_download', { name: v.version })
|
||||||
|
} else {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === v.version)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
paused: false,
|
||||||
|
downloading: true
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
const res = await invoke<string>('download', {
|
||||||
|
url: v.url,
|
||||||
|
name: v.version,
|
||||||
|
executable: v.executable,
|
||||||
|
hash: v.hash
|
||||||
|
})
|
||||||
|
|
||||||
|
if (res === '1') {
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.filter(d => d.version !== v.version)
|
||||||
|
)
|
||||||
|
setDownloadedVersionsConfig(prev => {
|
||||||
|
if (!prev) return prev
|
||||||
|
|
||||||
|
const updated = {
|
||||||
|
...prev,
|
||||||
|
list: {
|
||||||
|
...prev.list,
|
||||||
|
[v.version]: Date.now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeVersionsConfig(updated)
|
||||||
|
return updated
|
||||||
|
})
|
||||||
|
} else if (res == '0') {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === v.version)
|
||||||
|
if (i === -1) return prev
|
||||||
|
if (prev[i].canceled) {
|
||||||
|
return prev.filter((_, idx) => idx !== i)
|
||||||
|
}
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
downloading: false,
|
||||||
|
paused: true,
|
||||||
|
failed: false
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
} else if (res == '-1') {
|
||||||
|
setDownloadProgress(prev =>
|
||||||
|
prev.map(d =>
|
||||||
|
d.version === v.version
|
||||||
|
? {
|
||||||
|
...d,
|
||||||
|
queued: false,
|
||||||
|
failed: true,
|
||||||
|
progress: 0
|
||||||
|
}
|
||||||
|
: d
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if (normalConfig?.settings.allowNotifications)
|
||||||
|
await notifyUser(
|
||||||
|
'Download Failed',
|
||||||
|
`The download for version ${
|
||||||
|
getVersionInfo(v.version)?.displayName
|
||||||
|
} has failed.`
|
||||||
|
)
|
||||||
|
await getCurrentWindow().requestUserAttention(
|
||||||
|
UserAttentionType.Critical
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon={v.downloading ? faPause : faPlay}
|
||||||
|
className='w-6 h-6'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className='cursor-pointer bg-(--col5) hover:bg-(--col7) border border-(--col7) hover:border-(--col9) transition-colors w-8 h-8 flex items-center justify-center rounded-full'
|
||||||
|
hidden={!v.downloading && !v.paused}
|
||||||
|
onClick={async () => {
|
||||||
|
if (!v.paused) {
|
||||||
|
await invoke('cancel_download', { name: v.version })
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === v.version)
|
||||||
|
if (i === -1) return prev
|
||||||
|
const copy = [...prev]
|
||||||
|
copy[i] = {
|
||||||
|
...copy[i],
|
||||||
|
canceled: true
|
||||||
|
}
|
||||||
|
return copy
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
setDownloadProgress(prev => {
|
||||||
|
const i = prev.findIndex(d => d.version === v.version)
|
||||||
|
if (i === -1) return prev
|
||||||
|
return prev.filter((_, idx) => idx !== i)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faCancel} className='w-6 h-6' />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<p className='text-2xl text-center'>
|
<p className='text-2xl text-center'>
|
||||||
{getVersionInfo(v.version)?.displayName}
|
{getVersionInfo(v.version)?.displayName}
|
||||||
</p>
|
</p>
|
||||||
@@ -49,13 +183,41 @@ export default function DownloadsPopup () {
|
|||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
) : v.finishing ? (
|
) : v.hash_checking ? (
|
||||||
<span className='text-green-300 inline-block w-full text-center'>
|
<span className='text-blue-300 inline-block w-full text-center'>
|
||||||
Finishing...
|
Verifying file integerty...
|
||||||
</span>
|
</span>
|
||||||
|
) : v.unzipping ? (
|
||||||
|
<div className='flex flex-col gap-1 w-full'>
|
||||||
|
<span className='text-center'>
|
||||||
|
Unzipped {v.unzipped} / {v.unzipTotal} files
|
||||||
|
</span>
|
||||||
|
<ProgressBar
|
||||||
|
progress={(v.unzipped / v.unzipTotal) * 100}
|
||||||
|
className='w-full'
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className='flex flex-col gap-1 w-full'>
|
<div className='flex flex-col gap-1 w-full'>
|
||||||
<span className='text-center'>Downloading...</span>
|
<span className='text-center'>
|
||||||
|
Downloaded{' '}
|
||||||
|
{prettyBytes(v.progressBytes, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
})}{' '}
|
||||||
|
of{' '}
|
||||||
|
{prettyBytes(v.size, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
})}{' '}
|
||||||
|
(ETA: {formatEtaSmart(v.etaSecs)} • Speed:{' '}
|
||||||
|
{prettyBytes(v.speed, {
|
||||||
|
minimumFractionDigits: 1,
|
||||||
|
maximumFractionDigits: 1
|
||||||
|
})}
|
||||||
|
/s)
|
||||||
|
</span>
|
||||||
|
<ProgressBar progress={v.progress} className='w-full' />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,11 +4,15 @@ import {
|
|||||||
faArrowUpRightFromSquare,
|
faArrowUpRightFromSquare,
|
||||||
faCheck,
|
faCheck,
|
||||||
faCode,
|
faCode,
|
||||||
|
faHardDrive,
|
||||||
faShieldHalved,
|
faShieldHalved,
|
||||||
faWarning
|
faWarning
|
||||||
} from '@fortawesome/free-solid-svg-icons'
|
} from '@fortawesome/free-solid-svg-icons'
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||||
import { useGlobal } from '@/app/GlobalProvider'
|
import { useGlobal } from '@/app/GlobalProvider'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { message } from '@tauri-apps/plugin-dialog'
|
import { message } from '@tauri-apps/plugin-dialog'
|
||||||
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
import { BaseDirectory, exists, remove } from '@tauri-apps/plugin-fs'
|
||||||
import { writeVersionsConfig } from '@/lib/BazookaManager'
|
import { writeVersionsConfig } from '@/lib/BazookaManager'
|
||||||
@@ -28,6 +32,16 @@ export default function VersionVersionPopup () {
|
|||||||
setSelectedVersionList,
|
setSelectedVersionList,
|
||||||
downloadVersions
|
downloadVersions
|
||||||
} = useGlobal()
|
} = useGlobal()
|
||||||
|
const [versionSize, setVersionSize] = useState<number>(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (viewingInfoFromDownloads) return
|
||||||
|
invoke<string>('folder_size', {
|
||||||
|
version: managingVersion
|
||||||
|
}).then(size => {
|
||||||
|
setVersionSize(Number(size))
|
||||||
|
})
|
||||||
|
}, [managingVersion, setVersionSize, viewingInfoFromDownloads])
|
||||||
|
|
||||||
if (!managingVersion || !downloadedVersionsConfig) return <></>
|
if (!managingVersion || !downloadedVersionsConfig) return <></>
|
||||||
|
|
||||||
@@ -78,6 +92,21 @@ export default function VersionVersionPopup () {
|
|||||||
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
<FontAwesomeIcon icon={faCode} color='lightgray' />
|
||||||
<p>Developer: {gameInfo?.developer}</p>
|
<p>Developer: {gameInfo?.developer}</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
className='entry-info-item btntheme2'
|
||||||
|
hidden={viewingInfoFromDownloads}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faHardDrive} color='lightgray' />
|
||||||
|
<p>
|
||||||
|
Size on disk:{' '}
|
||||||
|
{versionSize && versionSize > 0
|
||||||
|
? prettyBytes(versionSize, {
|
||||||
|
minimumFractionDigits: 2,
|
||||||
|
maximumFractionDigits: 2
|
||||||
|
})
|
||||||
|
: 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
className='entry-info-item btntheme2'
|
className='entry-info-item btntheme2'
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { NormalConfig } from '@/types/NormalConfig'
|
||||||
|
import { notifyUser } from './Notifications'
|
||||||
|
|
||||||
|
export async function copyToClipboard (
|
||||||
|
text: string,
|
||||||
|
normalConfig: NormalConfig | null
|
||||||
|
) {
|
||||||
|
if (normalConfig?.settings.allowNotifications)
|
||||||
|
await notifyUser('Copied', 'Text "' + text + '" copied to clipboard')
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
isPermissionGranted,
|
||||||
|
requestPermission,
|
||||||
|
sendNotification
|
||||||
|
} from '@tauri-apps/plugin-notification'
|
||||||
|
|
||||||
|
export async function notifyUser (title: string, body: string) {
|
||||||
|
let permissionGranted = await isPermissionGranted()
|
||||||
|
if (!permissionGranted) {
|
||||||
|
const permission = await requestPermission()
|
||||||
|
permissionGranted = permission === 'granted'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (permissionGranted) {
|
||||||
|
sendNotification({ title, body })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import { message } from '@tauri-apps/plugin-dialog'
|
|||||||
import { BaseDirectory, exists, stat } from '@tauri-apps/plugin-fs'
|
import { BaseDirectory, exists, stat } from '@tauri-apps/plugin-fs'
|
||||||
import { openPath } from '@tauri-apps/plugin-opener'
|
import { openPath } from '@tauri-apps/plugin-opener'
|
||||||
import { join, appLocalDataDir } from '@tauri-apps/api/path'
|
import { join, appLocalDataDir } from '@tauri-apps/api/path'
|
||||||
|
import { invoke } from '@tauri-apps/api/core'
|
||||||
|
|
||||||
export const openFolder = async (name: string) => {
|
export const openFolder = async (name: string) => {
|
||||||
const relativePath = await join('game', name)
|
const relativePath = await join('game', name)
|
||||||
@@ -48,3 +49,16 @@ export const formatEtaSmart = (seconds: number) => {
|
|||||||
const h = Math.floor((seconds % 86400) / 3600)
|
const h = Math.floor((seconds % 86400) / 3600)
|
||||||
return `${d}d ${h}h`
|
return `${d}d ${h}h`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const verifySignature = async (body: string, signature: string) => {
|
||||||
|
try {
|
||||||
|
const result = await invoke<boolean>('verify_signature', {
|
||||||
|
body: body ?? '',
|
||||||
|
signature: signature ?? '',
|
||||||
|
publicKey: process.env.NEXT_PUBLIC_PUBLIC_SIGNING_KEY ?? ''
|
||||||
|
})
|
||||||
|
return Boolean(result)
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
export class DownloadProgress {
|
export class DownloadProgress {
|
||||||
constructor (
|
constructor (
|
||||||
public version: string,
|
public version: string,
|
||||||
|
public progress: number,
|
||||||
|
public progressBytes: number,
|
||||||
public failed: boolean,
|
public failed: boolean,
|
||||||
|
public downloading: boolean,
|
||||||
|
public paused: boolean,
|
||||||
|
public canceled: boolean,
|
||||||
public queued: boolean,
|
public queued: boolean,
|
||||||
public finishing: boolean
|
public hash_checking: boolean,
|
||||||
|
public speed: number,
|
||||||
|
public size: number,
|
||||||
|
public etaSecs: number,
|
||||||
|
public unzipping: boolean,
|
||||||
|
public unzipTotal: number,
|
||||||
|
public unzipped: number,
|
||||||
|
public url: string | null,
|
||||||
|
public executable: string | null,
|
||||||
|
public hash: string | null
|
||||||
) {}
|
) {}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user