From 6c2bc72fbb3796e3535da44750e2820e65f34314 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Mon, 9 Mar 2026 15:58:58 +0100 Subject: [PATCH] v1.2.4 - proxy external images through imgproxy protocol for WebKitGTK compat, hide copy/open buttons for external images --- package.json | 2 +- src-tauri/Cargo.lock | 3 +- src-tauri/Cargo.toml | 4 +- src-tauri/src/lib.rs | 95 +++++++++++++++++++++++++++++++- src-tauri/tauri.conf.json | 4 +- src/lib/components/Editor.svelte | 24 +++++++- tsconfig.json | 36 ++++++------ 7 files changed, 140 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 8f78054..d19d62e 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "helixnotes", "private": true, "license": "AGPL-3.0-or-later", - "version": "1.2.3", + "version": "1.2.4", "type": "module", "scripts": { "dev": "vite dev", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 1034241..9074872 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1850,7 +1850,7 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "helixnotes" -version = "1.2.3" +version = "1.2.4" dependencies = [ "arboard", "chrono", @@ -3938,6 +3938,7 @@ checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ "base64 0.22.1", "bytes", + "futures-channel", "futures-core", "futures-util", "http", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index abcc4a6..02fbd54 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helixnotes" -version = "1.2.3" +version = "1.2.4" description = "Local markdown note-taking app" authors = ["HelixNotes"] license = "AGPL-3.0-or-later" @@ -35,7 +35,7 @@ tokio = { version = "1", features = ["full"] } dirs = "6" regex = "1" zip = { version = "2", default-features = false, features = ["deflate"] } -reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "stream", "rustls-tls", "blocking"] } futures = "0.3" rayon = "1" rustls = { version = "0.23", default-features = false, features = ["ring", "logging", "std", "tls12"] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9174e0e..9ee530b 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -149,7 +149,80 @@ pub fn run() { commands::ai_ask, commands::get_install_type, commands::get_pending_open_file, - ]); + ]) + .register_asynchronous_uri_scheme_protocol("imgproxy", |_ctx, request, responder| { + let path = request.uri().path().to_string(); + std::thread::spawn(move || { + let encoded = path.strip_prefix('/').unwrap_or(&path); + let external_url = percent_decode(encoded); + + if !external_url.starts_with("http://") && !external_url.starts_with("https://") { + let _ = responder.respond( + tauri::http::Response::builder() + .status(400) + .body(Vec::new()) + .unwrap(), + ); + return; + } + + let client = match reqwest::blocking::Client::builder() + .timeout(std::time::Duration::from_secs(30)) + .build() + { + Ok(c) => c, + Err(_) => { + let _ = responder.respond( + tauri::http::Response::builder() + .status(502) + .body(Vec::new()) + .unwrap(), + ); + return; + } + }; + + match client.get(&external_url).send() { + Ok(resp) => { + let content_type = resp + .headers() + .get("content-type") + .and_then(|h| h.to_str().ok()) + .unwrap_or("application/octet-stream") + .to_string(); + let status = resp.status().as_u16(); + match resp.bytes() { + Ok(bytes) => { + let _ = responder.respond( + tauri::http::Response::builder() + .status(status) + .header("Content-Type", &content_type) + .header("Access-Control-Allow-Origin", "*") + .body(bytes.to_vec()) + .unwrap(), + ); + } + Err(_) => { + let _ = responder.respond( + tauri::http::Response::builder() + .status(502) + .body(Vec::new()) + .unwrap(), + ); + } + } + } + Err(_) => { + let _ = responder.respond( + tauri::http::Response::builder() + .status(502) + .body(Vec::new()) + .unwrap(), + ); + } + } + }); + }); #[cfg(not(target_os = "android"))] { @@ -251,3 +324,23 @@ fn setup_tray(app: &mut tauri::App) -> Result<(), Box> { Ok(()) } + +fn percent_decode(input: &str) -> String { + let mut output = Vec::with_capacity(input.len()); + let bytes = input.as_bytes(); + let mut i = 0; + while i < bytes.len() { + if bytes[i] == b'%' && i + 2 < bytes.len() { + if let Ok(decoded) = + u8::from_str_radix(std::str::from_utf8(&bytes[i + 1..i + 3]).unwrap_or(""), 16) + { + output.push(decoded); + i += 3; + continue; + } + } + output.push(bytes[i]); + i += 1; + } + String::from_utf8_lossy(&output).to_string() +} diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index ecb2a13..38c466c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "HelixNotes", - "version": "1.2.3", + "version": "1.2.4", "identifier": "com.helixnotes.app", "build": { "frontendDist": "../build", @@ -24,7 +24,7 @@ } ], "security": { - "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' asset: http://asset.localhost https: blob: data:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self' asset: http://asset.localhost", + "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' asset: http://asset.localhost imgproxy: http://imgproxy.localhost https: blob: data:; font-src 'self' data:; style-src 'self' 'unsafe-inline'; frame-src 'self' asset: http://asset.localhost", "assetProtocol": { "enable": true, "scope": ["**/*", "/**", "**/.helixnotes/**"] diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index b78cfe3..16cd2c4 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -1180,10 +1180,19 @@ ]; function resolveImageSrc(src: string): string { - // Already an absolute URL or data URI - if (src.startsWith('http') || src.startsWith('data:') || src.startsWith('asset:') || src.startsWith('blob:')) { + // Already a proxied, asset, data, or blob URL + if (src.startsWith('data:') || src.startsWith('asset:') || src.startsWith('blob:') || src.startsWith('imgproxy:') || src.startsWith('http://imgproxy.localhost') || src.startsWith('https://imgproxy.localhost')) { return src; } + // Already an asset-localhost URL + if (src.startsWith('http://asset.localhost') || src.startsWith('https://asset.localhost')) { + return src; + } + // External http/https URLs: proxy through Tauri's imgproxy protocol + // to bypass WebKitGTK restrictions on loading external resources + if (src.startsWith('http://') || src.startsWith('https://')) { + return convertFileSrc(src, 'imgproxy'); + } // Decode percent-encoding (%20 → space, etc.) for filesystem resolution let decoded = decodeURIComponent(src); // Fix multiple leading slashes (from broken saves) @@ -1876,6 +1885,15 @@ function stripAssetSrc(src: string): string { // blob: URLs are not persistable — they were temporary browser references if (src.startsWith('blob:')) return ''; + // Convert imgproxy:// URLs back to original external URLs for saving + if (src.startsWith('imgproxy:') || src.startsWith('http://imgproxy.localhost') || src.startsWith('https://imgproxy.localhost')) { + try { + const url = new URL(src); + return decodeURIComponent(url.pathname.substring(1)); + } catch { + return src; + } + } // Convert asset:// URLs back to relative paths for saving if (!src.startsWith('asset:') && !src.startsWith('http://asset.localhost')) return src; let absPath = ''; @@ -4489,7 +4507,7 @@ - {#if !isMobile} + {#if !isMobile && !imageToolbar.src.startsWith('imgproxy:') && !imageToolbar.src.startsWith('http://imgproxy.localhost')}