mirror of
https://github.com/moku-project/Moku.git
synced 2026-06-13 01:09:56 -05:00
Fix: Tauri-Plugin-HTTP for Windows Auth Support (Major WIP)
This commit is contained in:
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@tauri-apps/api": "^2.0.0",
|
||||||
|
"@tauri-apps/plugin-http": "^2.5.8",
|
||||||
"@tauri-apps/plugin-os": "^2.3.2",
|
"@tauri-apps/plugin-os": "^2.3.2",
|
||||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
|||||||
Generated
+10
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@tauri-apps/api':
|
'@tauri-apps/api':
|
||||||
specifier: ^2.0.0
|
specifier: ^2.0.0
|
||||||
version: 2.10.1
|
version: 2.10.1
|
||||||
|
'@tauri-apps/plugin-http':
|
||||||
|
specifier: ^2.5.8
|
||||||
|
version: 2.5.8
|
||||||
'@tauri-apps/plugin-os':
|
'@tauri-apps/plugin-os':
|
||||||
specifier: ^2.3.2
|
specifier: ^2.3.2
|
||||||
version: 2.3.2
|
version: 2.3.2
|
||||||
@@ -445,6 +448,9 @@ packages:
|
|||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-http@2.5.8':
|
||||||
|
resolution: {integrity: sha512-oxd7oypzQeu8kAfFCrw534Kq7Cw+NzozcnCY21O4rz3A+veJiIiuSCMIprgGcZOcLAXFP9GmDhKUbhuKWcunRw==}
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.3.2':
|
'@tauri-apps/plugin-os@2.3.2':
|
||||||
resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==}
|
resolution: {integrity: sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==}
|
||||||
|
|
||||||
@@ -1053,6 +1059,10 @@ snapshots:
|
|||||||
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
|
'@tauri-apps/cli-win32-ia32-msvc': 2.10.1
|
||||||
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
|
'@tauri-apps/cli-win32-x64-msvc': 2.10.1
|
||||||
|
|
||||||
|
'@tauri-apps/plugin-http@2.5.8':
|
||||||
|
dependencies:
|
||||||
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|
||||||
'@tauri-apps/plugin-os@2.3.2':
|
'@tauri-apps/plugin-os@2.3.2':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tauri-apps/api': 2.10.1
|
'@tauri-apps/api': 2.10.1
|
||||||
|
|||||||
Generated
+119
-2
@@ -425,7 +425,7 @@ dependencies = [
|
|||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation 0.10.1",
|
||||||
"core-graphics-types",
|
"core-graphics-types",
|
||||||
"foreign-types",
|
"foreign-types 0.5.0",
|
||||||
"libc",
|
"libc",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -952,6 +952,15 @@ version = "0.2.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
|
||||||
|
dependencies = [
|
||||||
|
"foreign-types-shared 0.1.1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types"
|
name = "foreign-types"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -959,7 +968,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"foreign-types-macros",
|
"foreign-types-macros",
|
||||||
"foreign-types-shared",
|
"foreign-types-shared 0.3.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -973,6 +982,12 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "foreign-types-shared"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "foreign-types-shared"
|
name = "foreign-types-shared"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -1005,6 +1020,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1553,6 +1569,22 @@ dependencies = [
|
|||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-tls"
|
||||||
|
version = "0.6.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
|
"tower-service",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -2122,6 +2154,7 @@ name = "moku"
|
|||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
|
"reqwest 0.12.28",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sysinfo 0.32.1",
|
"sysinfo 0.32.1",
|
||||||
@@ -2133,6 +2166,8 @@ dependencies = [
|
|||||||
"tauri-plugin-process",
|
"tauri-plugin-process",
|
||||||
"tauri-plugin-shell",
|
"tauri-plugin-shell",
|
||||||
"tauri-plugin-updater",
|
"tauri-plugin-updater",
|
||||||
|
"tokio",
|
||||||
|
"urlencoding",
|
||||||
"walkdir",
|
"walkdir",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2157,6 +2192,23 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "native-tls"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2"
|
||||||
|
dependencies = [
|
||||||
|
"libc",
|
||||||
|
"log",
|
||||||
|
"openssl",
|
||||||
|
"openssl-probe",
|
||||||
|
"openssl-sys",
|
||||||
|
"schannel",
|
||||||
|
"security-framework",
|
||||||
|
"security-framework-sys",
|
||||||
|
"tempfile",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ndk"
|
name = "ndk"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
@@ -2493,12 +2545,50 @@ dependencies = [
|
|||||||
"pathdiff",
|
"pathdiff",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl"
|
||||||
|
version = "0.10.76"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.11.0",
|
||||||
|
"cfg-if",
|
||||||
|
"foreign-types 0.3.2",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"openssl-macros",
|
||||||
|
"openssl-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "openssl-probe"
|
name = "openssl-probe"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "openssl-sys"
|
||||||
|
version = "0.9.112"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"libc",
|
||||||
|
"pkg-config",
|
||||||
|
"vcpkg",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "option-ext"
|
name = "option-ext"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@@ -3281,17 +3371,21 @@ dependencies = [
|
|||||||
"cookie",
|
"cookie",
|
||||||
"cookie_store 0.22.1",
|
"cookie_store 0.22.1",
|
||||||
"encoding_rs",
|
"encoding_rs",
|
||||||
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
"h2",
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
"hyper",
|
"hyper",
|
||||||
"hyper-rustls",
|
"hyper-rustls",
|
||||||
|
"hyper-tls",
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
"mime",
|
||||||
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"quinn",
|
"quinn",
|
||||||
@@ -3302,6 +3396,7 @@ dependencies = [
|
|||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
"sync_wrapper",
|
"sync_wrapper",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-native-tls",
|
||||||
"tokio-rustls",
|
"tokio-rustls",
|
||||||
"tower",
|
"tower",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
@@ -4740,6 +4835,16 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-native-tls"
|
||||||
|
version = "0.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
|
||||||
|
dependencies = [
|
||||||
|
"native-tls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-rustls"
|
name = "tokio-rustls"
|
||||||
version = "0.26.4"
|
version = "0.26.4"
|
||||||
@@ -5050,6 +5155,12 @@ dependencies = [
|
|||||||
"serde_derive",
|
"serde_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urlencoding"
|
||||||
|
version = "2.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urlpattern"
|
name = "urlpattern"
|
||||||
version = "0.3.0"
|
version = "0.3.0"
|
||||||
@@ -5095,6 +5206,12 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "vcpkg"
|
||||||
|
version = "0.2.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version-compare"
|
name = "version-compare"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
|
|||||||
@@ -27,6 +27,9 @@ serde_json = "1"
|
|||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
sysinfo = "0.32"
|
sysinfo = "0.32"
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
|
urlencoding = "2"
|
||||||
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
|
reqwest = { version = "0.12", features = ["blocking"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "../gen/schemas/desktop-schema.json",
|
||||||
|
"identifier": "http-scope",
|
||||||
|
"description": "HTTP fetch scope",
|
||||||
|
"windows": ["main"],
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"identifier": "http:default",
|
||||||
|
"allow": [
|
||||||
|
{ "url": "http://*:*/*" },
|
||||||
|
{ "url": "https://*:*/*" },
|
||||||
|
{ "url": "http://*/*" },
|
||||||
|
{ "url": "https://*/*" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -11,6 +11,14 @@ use walkdir::WalkDir;
|
|||||||
|
|
||||||
struct ServerState(Mutex<Option<CommandChild>>);
|
struct ServerState(Mutex<Option<CommandChild>>);
|
||||||
|
|
||||||
|
#[derive(Default, Clone)]
|
||||||
|
struct AuthCredentials {
|
||||||
|
user: String,
|
||||||
|
pass: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AuthState(Mutex<AuthCredentials>);
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
pub struct StorageInfo {
|
pub struct StorageInfo {
|
||||||
manga_bytes: u64,
|
manga_bytes: u64,
|
||||||
@@ -569,6 +577,14 @@ fn restart_app(app: tauri::AppHandle) {
|
|||||||
tauri::process::restart(&app.env());
|
tauri::process::restart(&app.env());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
fn set_auth_credentials(app: tauri::AppHandle, user: String, pass: String) {
|
||||||
|
let state = app.state::<AuthState>();
|
||||||
|
let mut creds = state.0.lock().unwrap();
|
||||||
|
creds.user = user;
|
||||||
|
creds.pass = pass;
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
pub fn run() {
|
pub fn run() {
|
||||||
tauri::Builder::default()
|
tauri::Builder::default()
|
||||||
@@ -579,6 +595,71 @@ pub fn run() {
|
|||||||
.plugin(tauri_plugin_process::init())
|
.plugin(tauri_plugin_process::init())
|
||||||
.plugin(tauri_plugin_updater::Builder::new().build())
|
.plugin(tauri_plugin_updater::Builder::new().build())
|
||||||
.manage(ServerState(Mutex::new(None)))
|
.manage(ServerState(Mutex::new(None)))
|
||||||
|
.manage(AuthState(Mutex::new(AuthCredentials::default())))
|
||||||
|
.register_asynchronous_uri_scheme_protocol("moku", |ctx, request, responder| {
|
||||||
|
use tauri_plugin_http::reqwest;
|
||||||
|
|
||||||
|
// moku://proxy/<percent-encoded-absolute-url>
|
||||||
|
let raw_uri = request.uri().to_string();
|
||||||
|
let encoded = raw_uri
|
||||||
|
.split_once("://")
|
||||||
|
.and_then(|(_, rest)| rest.split_once('/'))
|
||||||
|
.map(|(_, after)| after.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let target_url = match urlencoding::decode(&encoded) {
|
||||||
|
Ok(u) => u.into_owned(),
|
||||||
|
Err(_) => encoded,
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("[moku] target_url={:?}", target_url);
|
||||||
|
|
||||||
|
let auth_state = ctx.app_handle().state::<AuthState>();
|
||||||
|
let creds = auth_state.0.lock().unwrap().clone();
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let result: Result<(Vec<u8>, String), String> = async {
|
||||||
|
let client = reqwest::Client::builder()
|
||||||
|
.build()
|
||||||
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let mut req = client.get(&target_url);
|
||||||
|
if !creds.user.is_empty() && !creds.pass.is_empty() {
|
||||||
|
req = req.basic_auth(&creds.user, Some(&creds.pass));
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp = req.send().await.map_err(|e| e.to_string())?;
|
||||||
|
let content_type = resp
|
||||||
|
.headers()
|
||||||
|
.get(reqwest::header::CONTENT_TYPE)
|
||||||
|
.and_then(|v| v.to_str().ok())
|
||||||
|
.unwrap_or("image/jpeg")
|
||||||
|
.to_string();
|
||||||
|
let bytes = resp.bytes().await.map_err(|e| e.to_string())?;
|
||||||
|
Ok((bytes.to_vec(), content_type))
|
||||||
|
}.await;
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok((bytes, content_type)) => {
|
||||||
|
responder.respond(
|
||||||
|
tauri::http::Response::builder()
|
||||||
|
.header("Content-Type", content_type)
|
||||||
|
.header("Access-Control-Allow-Origin", "*")
|
||||||
|
.body(bytes)
|
||||||
|
.unwrap_or_else(|_| tauri::http::Response::builder().status(500).body(vec![]).unwrap())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("[moku] error: {}", e);
|
||||||
|
responder.respond(
|
||||||
|
tauri::http::Response::builder()
|
||||||
|
.status(502)
|
||||||
|
.body(vec![])
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_storage_info,
|
get_storage_info,
|
||||||
get_default_downloads_path,
|
get_default_downloads_path,
|
||||||
@@ -591,6 +672,7 @@ pub fn run() {
|
|||||||
list_releases,
|
list_releases,
|
||||||
download_and_install_update,
|
download_and_install_update,
|
||||||
restart_app,
|
restart_app,
|
||||||
|
set_auth_credentials,
|
||||||
])
|
])
|
||||||
.setup(|_app| Ok(()))
|
.setup(|_app| Ok(()))
|
||||||
.on_window_event(|window, event| {
|
.on_window_event(|window, event| {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { onMount, untrack } from "svelte";
|
import { onMount, untrack } from "svelte";
|
||||||
import { Play, ArrowRight, ArrowLeft, BookOpen, Clock, Fire, TrendUp, CalendarBlank, CheckCircle, PushPin, X as XIcon, MagnifyingGlass, ListBullets } from "phosphor-svelte";
|
import { Play, ArrowRight, ArrowLeft, BookOpen, Clock, Fire, TrendUp, CalendarBlank, CheckCircle, PushPin, X as XIcon, MagnifyingGlass, ListBullets } from "phosphor-svelte";
|
||||||
import { gql, thumbUrl } from "../../lib/client";
|
import { gql, thumbUrl } from "../../lib/client";
|
||||||
import { fetchAuthenticated } from "../../lib/auth";
|
import { getBlobUrl } from "../../lib/imageCache";
|
||||||
import Thumbnail from "../shared/Thumbnail.svelte";
|
import Thumbnail from "../shared/Thumbnail.svelte";
|
||||||
import { GET_LIBRARY, GET_CHAPTERS, GET_MANGA, GET_CATEGORIES } from "../../lib/queries";
|
import { GET_LIBRARY, GET_CHAPTERS, GET_MANGA, GET_CATEGORIES } from "../../lib/queries";
|
||||||
import { cache, CACHE_KEYS } from "../../lib/cache";
|
import { cache, CACHE_KEYS } from "../../lib/cache";
|
||||||
@@ -121,22 +121,15 @@
|
|||||||
activeSlot?.kind === "continue" ? (activeSlot.entry?.thumbnailUrl ?? "") : ""
|
activeSlot?.kind === "continue" ? (activeSlot.entry?.thumbnailUrl ?? "") : ""
|
||||||
);
|
);
|
||||||
let heroThumb = $state("");
|
let heroThumb = $state("");
|
||||||
const heroThumbCache = new Map<string, string>();
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const path = heroThumbSrc;
|
const path = heroThumbSrc;
|
||||||
const mode = store.settings.serverAuthMode ?? "NONE";
|
const mode = store.settings.serverAuthMode ?? "NONE";
|
||||||
if (!path) { heroThumb = ""; return; }
|
if (!path) { heroThumb = ""; return; }
|
||||||
if (mode !== "BASIC_AUTH") { heroThumb = thumbUrl(path); return; }
|
if (mode !== "BASIC_AUTH") { heroThumb = thumbUrl(path); return; }
|
||||||
if (heroThumbCache.has(path)) { heroThumb = heroThumbCache.get(path)!; return; }
|
// Use tauri-plugin-http backed getBlobUrl which handles auth and bypasses CORS
|
||||||
heroThumb = "";
|
getBlobUrl(thumbUrl(path))
|
||||||
fetchAuthenticated(thumbUrl(path), { method: "GET" })
|
.then(url => { heroThumb = url; })
|
||||||
.then(r => r.blob())
|
.catch(() => { heroThumb = ""; });
|
||||||
.then(blob => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
heroThumbCache.set(path, url);
|
|
||||||
heroThumb = url;
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
});
|
});
|
||||||
const heroTitle = $derived(activeSlot?.kind === "pinned" ? (activeSlot.manga?.title ?? "") : activeSlot?.kind === "continue" ? (activeSlot.entry?.mangaTitle ?? "") : "");
|
const heroTitle = $derived(activeSlot?.kind === "pinned" ? (activeSlot.manga?.title ?? "") : activeSlot?.kind === "continue" ? (activeSlot.entry?.mangaTitle ?? "") : "");
|
||||||
const heroManga = $derived(activeSlot?.kind === "pinned" ? activeSlot.manga : activeSlot?.kind === "continue" ? libraryManga.find(m => m.id === activeSlot.entry?.mangaId) : null);
|
const heroManga = $derived(activeSlot?.kind === "pinned" ? activeSlot.manga : activeSlot?.kind === "continue" ? libraryManga.find(m => m.id === activeSlot.entry?.mangaId) : null);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter, LibraryContentFilter } from "../../store/state.svelte";
|
import type { LibrarySortMode, LibrarySortDir, LibraryStatusFilter, LibraryContentFilter } from "../../store/state.svelte";
|
||||||
import type { Manga, Category, Chapter } from "../../lib/types";
|
import type { Manga, Category, Chapter } from "../../lib/types";
|
||||||
import ContextMenu, { type MenuEntry } from "../shared/ContextMenu.svelte";
|
import ContextMenu, { type MenuEntry } from "../shared/ContextMenu.svelte";
|
||||||
|
import Thumbnail from "../shared/Thumbnail.svelte";
|
||||||
|
|
||||||
const CARD_MIN_W = 130;
|
const CARD_MIN_W = 130;
|
||||||
const CARD_GAP = 16;
|
const CARD_GAP = 16;
|
||||||
@@ -920,7 +921,7 @@
|
|||||||
onpointerleave={onCardPointerLeave}
|
onpointerleave={onCardPointerLeave}
|
||||||
>
|
>
|
||||||
<div class="cover-wrap">
|
<div class="cover-wrap">
|
||||||
<img src={thumbUrl(m.thumbnailUrl)} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" loading="lazy" decoding="async" draggable="false" />
|
<Thumbnail src={m.thumbnailUrl} alt={m.title} class="cover" style="object-fit:{store.settings.libraryCropCovers ? 'cover' : 'contain'}" draggable="false" />
|
||||||
{#if m.downloadCount}<span class="badge-dl">{m.downloadCount}</span>{/if}
|
{#if m.downloadCount}<span class="badge-dl">{m.downloadCount}</span>{/if}
|
||||||
{#if m.unreadCount}<span class="badge-unread">{m.unreadCount}</span>{/if}
|
{#if m.unreadCount}<span class="badge-unread">{m.unreadCount}</span>{/if}
|
||||||
{#if selectMode}
|
{#if selectMode}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
CircleNotch, MagnifyingGlassMinus, MagnifyingGlassPlus,
|
CircleNotch, MagnifyingGlassMinus, MagnifyingGlassPlus,
|
||||||
Bookmark, BookOpen, MonitorPlay, MapPin, Check,
|
Bookmark, BookOpen, MonitorPlay, MapPin, Check,
|
||||||
} from "phosphor-svelte";
|
} from "phosphor-svelte";
|
||||||
import { gql, thumbUrl } from "../../lib/client";
|
import { gql, thumbUrl, plainThumbUrl } from "../../lib/client";
|
||||||
import { fetchAuthenticated } from "../../lib/auth";
|
import { getBlobUrl } from "../../lib/imageCache";
|
||||||
import { store as appStore } from "../../store/state.svelte";
|
import { store as appStore } from "../../store/state.svelte";
|
||||||
import { FETCH_CHAPTER_PAGES, MARK_CHAPTER_READ, ENQUEUE_DOWNLOAD, ENQUEUE_CHAPTERS_DOWNLOAD } from "../../lib/queries";
|
import { FETCH_CHAPTER_PAGES, MARK_CHAPTER_READ, ENQUEUE_DOWNLOAD, ENQUEUE_CHAPTERS_DOWNLOAD } from "../../lib/queries";
|
||||||
import { store, closeReader, openReader, addHistory, updateSettings, checkAndMarkCompleted, setSettingsOpen, addBookmark, removeBookmark, addMarker, removeMarker, updateMarker } from "../../store/state.svelte";
|
import { store, closeReader, openReader, addHistory, updateSettings, checkAndMarkCompleted, setSettingsOpen, addBookmark, removeBookmark, addMarker, removeMarker, updateMarker } from "../../store/state.svelte";
|
||||||
@@ -43,16 +43,16 @@
|
|||||||
if (!inflight.has(chapterId)) {
|
if (!inflight.has(chapterId)) {
|
||||||
const p = gql<{ fetchChapterPages: { pages: string[] } }>(FETCH_CHAPTER_PAGES, { chapterId })
|
const p = gql<{ fetchChapterPages: { pages: string[] } }>(FETCH_CHAPTER_PAGES, { chapterId })
|
||||||
.then(async d => {
|
.then(async d => {
|
||||||
const rawUrls = d.fetchChapterPages.pages.map(thumbUrl);
|
|
||||||
const mode = appStore.settings.serverAuthMode ?? "NONE";
|
const mode = appStore.settings.serverAuthMode ?? "NONE";
|
||||||
const urls = mode === "BASIC_AUTH"
|
const rawUrls = d.fetchChapterPages.pages.map(p => plainThumbUrl(p));
|
||||||
? await Promise.all(rawUrls.map(u =>
|
let urls: string[];
|
||||||
fetchAuthenticated(u, { method: "GET" })
|
if (mode === "BASIC_AUTH") {
|
||||||
.then(r => r.blob())
|
// Pre-fetch all pages via tauri-plugin-http (bypasses CORS + auth headers)
|
||||||
.then(b => URL.createObjectURL(b))
|
// in parallel so they're cached and ready to display immediately
|
||||||
.catch(() => u)
|
urls = await Promise.all(rawUrls.map(u => getBlobUrl(u).catch(() => u)));
|
||||||
))
|
} else {
|
||||||
: rawUrls;
|
urls = rawUrls.map(u => thumbUrl(u));
|
||||||
|
}
|
||||||
pageCache.set(chapterId, urls);
|
pageCache.set(chapterId, urls);
|
||||||
return urls;
|
return urls;
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onDestroy } from "svelte";
|
import { thumbUrl, plainThumbUrl } from "../../lib/client";
|
||||||
import { thumbUrl } from "../../lib/client";
|
|
||||||
import { fetchAuthenticated } from "../../lib/auth";
|
|
||||||
import { store } from "../../store/state.svelte";
|
import { store } from "../../store/state.svelte";
|
||||||
|
import { getBlobUrl } from "../../lib/imageCache";
|
||||||
|
|
||||||
let {
|
let {
|
||||||
src,
|
src,
|
||||||
@@ -22,40 +21,22 @@
|
|||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
} = $props();
|
} = $props();
|
||||||
|
|
||||||
const blobCache = new Map<string, string>();
|
const isAuth = $derived(store.settings.serverAuthMode === "BASIC_AUTH");
|
||||||
|
|
||||||
let resolved = $state("");
|
// Plain URL for non-auth users — fast, no overhead
|
||||||
let current = "";
|
const plainResolved = $derived(src ? thumbUrl(src) : "");
|
||||||
|
|
||||||
|
// Blob URL for auth users — fetched with Authorization header
|
||||||
|
let blobUrl = $state("");
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
const path = src;
|
if (!isAuth || !src) { blobUrl = ""; return; }
|
||||||
const mode = store.settings.serverAuthMode ?? "NONE";
|
const fullUrl = plainThumbUrl(src);
|
||||||
|
getBlobUrl(fullUrl)
|
||||||
if (path === current) return;
|
.then(u => { blobUrl = u; })
|
||||||
current = path;
|
.catch(() => { blobUrl = ""; });
|
||||||
|
|
||||||
if (!path) { resolved = ""; return; }
|
|
||||||
|
|
||||||
if (mode !== "BASIC_AUTH") {
|
|
||||||
resolved = thumbUrl(path);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (blobCache.has(path)) {
|
|
||||||
resolved = blobCache.get(path)!;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
resolved = "";
|
|
||||||
fetchAuthenticated(thumbUrl(path), { method: "GET" })
|
|
||||||
.then(r => r.blob())
|
|
||||||
.then(blob => {
|
|
||||||
const url = URL.createObjectURL(blob);
|
|
||||||
blobCache.set(path, url);
|
|
||||||
if (current === path) resolved = url;
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const resolved = $derived(isAuth ? blobUrl || undefined : plainResolved || undefined);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img src={resolved} {alt} class={className} {loading} {decoding} {onerror} {...rest} />
|
<img src={resolved} {alt} class={className} {loading} {decoding} {onerror} {...rest} />
|
||||||
|
|||||||
+8
-16
@@ -10,25 +10,17 @@ function getServerUrl(): string {
|
|||||||
|
|
||||||
function gqlUrl(): string { return `${getServerUrl()}/api/graphql`; }
|
function gqlUrl(): string { return `${getServerUrl()}/api/graphql`; }
|
||||||
|
|
||||||
export function thumbUrl(path: string): string {
|
// Returns a clean absolute URL with no embedded credentials.
|
||||||
|
export function plainThumbUrl(path: string): string {
|
||||||
if (!path) return "";
|
if (!path) return "";
|
||||||
if (path.startsWith("http")) return path;
|
if (path.startsWith("http")) return path;
|
||||||
|
return `${getServerUrl()}${path}`;
|
||||||
|
}
|
||||||
|
|
||||||
const base = getServerUrl();
|
// Same as plainThumbUrl — credentials are never embedded in URLs.
|
||||||
const mode = store.settings.serverAuthMode;
|
// Auth users load images via getBlobUrl (imageCache.ts) instead.
|
||||||
|
export function thumbUrl(path: string): string {
|
||||||
if (mode === "BASIC_AUTH") {
|
return plainThumbUrl(path);
|
||||||
const user = store.settings.serverAuthUser?.trim() ?? "";
|
|
||||||
const pass = store.settings.serverAuthPass?.trim() ?? "";
|
|
||||||
if (user && pass) {
|
|
||||||
const url = new URL(`${base}${path}`);
|
|
||||||
url.username = user;
|
|
||||||
url.password = pass;
|
|
||||||
return url.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${base}${path}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GQLResponse<T> {
|
interface GQLResponse<T> {
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { fetch as tauriFetch } from "@tauri-apps/plugin-http";
|
||||||
|
import { store } from "../store/state.svelte";
|
||||||
|
|
||||||
|
const cache = new Map<string, string>();
|
||||||
|
const inflight = new Map<string, Promise<string>>();
|
||||||
|
|
||||||
|
function getAuthHeaders(): Record<string, string> {
|
||||||
|
const mode = store.settings.serverAuthMode;
|
||||||
|
if (mode === "BASIC_AUTH") {
|
||||||
|
const user = store.settings.serverAuthUser?.trim() ?? "";
|
||||||
|
const pass = store.settings.serverAuthPass?.trim() ?? "";
|
||||||
|
if (user && pass) {
|
||||||
|
return { Authorization: `Basic ${btoa(`${user}:${pass}`)}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlobUrl(url: string): Promise<string> {
|
||||||
|
if (!url) return "";
|
||||||
|
|
||||||
|
const cached = cache.get(url);
|
||||||
|
if (cached) return cached;
|
||||||
|
|
||||||
|
const existing = inflight.get(url);
|
||||||
|
if (existing) return existing;
|
||||||
|
|
||||||
|
const promise = tauriFetch(url, {
|
||||||
|
method: "GET",
|
||||||
|
headers: getAuthHeaders(),
|
||||||
|
})
|
||||||
|
.then(res => {
|
||||||
|
if (!res.ok) throw new Error(`${res.status}`);
|
||||||
|
return res.blob();
|
||||||
|
})
|
||||||
|
.then(blob => {
|
||||||
|
const blobUrl = URL.createObjectURL(blob);
|
||||||
|
cache.set(url, blobUrl);
|
||||||
|
inflight.delete(url);
|
||||||
|
return blobUrl;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
inflight.delete(url);
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
inflight.set(url, promise);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user