diff --git a/bun.lock b/bun.lock index 4dde25f69..ffb2251ca 100644 --- a/bun.lock +++ b/bun.lock @@ -145,6 +145,7 @@ "svg2pdf.js": "^2.7.0", "svgpath": "^2.6.0", "twirlwind": "^0.3.0", + "unifont": "^0.7.4", "yoga-layout": "npm:@open-pencil/yoga-layout@3.3.0-grid.3", }, "devDependencies": { @@ -1531,6 +1532,8 @@ "css-line-break": ["css-line-break@2.1.0", "", { "dependencies": { "utrie": "^1.0.2" } }, "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w=="], + "css-tree": ["css-tree@3.2.1", "", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="], + "cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], @@ -1567,6 +1570,8 @@ "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], + "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], "detective-amd": ["detective-amd@6.1.0", "", { "dependencies": { "ast-module-types": "^6.0.1", "escodegen": "^2.1.0", "get-amd-module-type": "^6.0.2", "node-source-walk": "^7.0.1" }, "bin": { "detective-amd": "bin/cli.js" } }, "sha512-fmI6LGMvotqd49QaA3ZYw+q0aGp2yXmMjzIuY6fH9j9YFIXY/73yDhMwhX9cPbhWd+AH06NH1Di/LKOuCH0Ubg=="], @@ -2139,6 +2144,8 @@ "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], + "mdn-data": ["mdn-data@2.27.1", "", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="], + "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], @@ -2271,6 +2278,8 @@ "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], + "node-fetch-native": ["node-fetch-native@1.6.7", "", {}, "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q=="], + "node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="], "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], @@ -2291,6 +2300,8 @@ "obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="], + "ofetch": ["ofetch@1.5.1", "", { "dependencies": { "destr": "^2.0.5", "node-fetch-native": "^1.6.7", "ufo": "^1.6.1" } }, "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA=="], + "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], @@ -2771,6 +2782,8 @@ "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "unifont": ["unifont@0.7.4", "", { "dependencies": { "css-tree": "^3.1.0", "ofetch": "^1.5.1", "ohash": "^2.0.11" } }, "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg=="], + "unique-string": ["unique-string@2.0.0", "", { "dependencies": { "crypto-random-string": "^2.0.0" } }, "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg=="], "unist-util-is": ["unist-util-is@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g=="], diff --git a/desktop/Cargo.lock b/desktop/Cargo.lock index e01f3b95b..d2111edd1 100644 --- a/desktop/Cargo.lock +++ b/desktop/Cargo.lock @@ -440,6 +440,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.44" @@ -1366,8 +1372,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -1377,9 +1385,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", "wasip2", + "wasm-bindgen", ] [[package]] @@ -1545,7 +1555,7 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.13.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" @@ -1682,6 +1692,7 @@ dependencies = [ "tokio", "tokio-rustls", "tower-service", + "webpki-roots", ] [[package]] @@ -1862,7 +1873,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.13.1", + "hashbrown 0.12.3", "serde", ] @@ -2168,6 +2179,12 @@ version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "mac" version = "0.1.1" @@ -2639,10 +2656,11 @@ dependencies = [ [[package]] name = "open_pencil" -version = "0.13.1" +version = "0.13.2" dependencies = [ "fix-path-env", "font-kit", + "reqwest 0.12.28", "serde", "serde_json", "tauri", @@ -3109,6 +3127,61 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror 2.0.18", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e" +dependencies = [ + "bytes", + "getrandom 0.3.4", + "lru-slab", + "rand 0.9.4", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror 2.0.18", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.60.2", +] + [[package]] name = "quote" version = "1.0.44" @@ -3149,6 +3222,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", +] + [[package]] name = "rand_chacha" version = "0.2.2" @@ -3169,6 +3252,16 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + [[package]] name = "rand_core" version = "0.5.1" @@ -3187,6 +3280,15 @@ dependencies = [ "getrandom 0.2.17", ] +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", +] + [[package]] name = "rand_hc" version = "0.2.0" @@ -3289,6 +3391,44 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", +] + [[package]] name = "reqwest" version = "0.13.2" @@ -3366,6 +3506,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + [[package]] name = "rustc_version" version = "0.4.1" @@ -3420,6 +3566,7 @@ version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ + "web-time", "zeroize", ] @@ -3467,6 +3614,12 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + [[package]] name = "same-file" version = "1.0.6" @@ -3688,6 +3841,18 @@ dependencies = [ "serde_core", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_with" version = "3.17.0" @@ -4114,7 +4279,7 @@ dependencies = [ "percent-encoding", "plist", "raw-window-handle", - "reqwest", + "reqwest 0.13.2", "serde", "serde_json", "serde_repr", @@ -4341,7 +4506,7 @@ dependencies = [ "minisign-verify", "osakit", "percent-encoding", - "reqwest", + "reqwest 0.13.2", "rustls", "semver", "serde", @@ -4564,6 +4729,21 @@ dependencies = [ "zerovec", ] +[[package]] +name = "tinyvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.49.0" @@ -5144,6 +5324,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webkit2gtk" version = "2.0.2" @@ -5197,6 +5387,15 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "webpki-roots" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf85cb06032201fa7c6f829d7db5a7e5aa45bcc0655327713065f6f0576731bf" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webview2-com" version = "0.38.2" diff --git a/desktop/Cargo.toml b/desktop/Cargo.toml index 90acdcaa4..71e94284b 100644 --- a/desktop/Cargo.toml +++ b/desktop/Cargo.toml @@ -23,6 +23,7 @@ tauri = { version = "2", features = ["devtools"] } tauri-plugin-opener = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" +reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } tauri-plugin-dialog = "2.6.0" tauri-plugin-fs = { version = "2.4.5", features = ["watch"] } tauri-plugin-shell = "2" diff --git a/desktop/src/http.rs b/desktop/src/http.rs new file mode 100644 index 000000000..b0462cb7c --- /dev/null +++ b/desktop/src/http.rs @@ -0,0 +1,84 @@ +use reqwest::{ + header::{HeaderMap, HeaderName, HeaderValue}, + Method, +}; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Deserialize, Serialize)] +pub struct ProxyHttpHeader { + name: String, + value: String, +} + +#[derive(Deserialize)] +pub struct ProxyHttpRequest { + url: String, + method: Option, + headers: Option>, + body: Option>, +} + +#[derive(Serialize)] +pub struct ProxyHttpResponse { + status: u16, + headers: Vec, + body: Vec, +} + +fn request_headers(headers: Option>) -> HeaderMap { + let mut mapped = HeaderMap::new(); + for header in headers.unwrap_or_default() { + let Ok(header_name) = HeaderName::from_bytes(header.name.as_bytes()) else { + continue; + }; + let Ok(header_value) = HeaderValue::from_str(&header.value) else { + continue; + }; + mapped.insert(header_name, header_value); + } + mapped +} + +fn request_method(method: Option) -> Result { + let raw = method.unwrap_or_else(|| "GET".into()); + Method::from_bytes(raw.as_bytes()).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn proxy_http_request(request: ProxyHttpRequest) -> Result { + let parsed = reqwest::Url::parse(&request.url).map_err(|e| e.to_string())?; + if !matches!(parsed.scheme(), "http" | "https") { + return Err("Only HTTP(S) requests are supported".into()); + } + + let client = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::limited(5)) + .build() + .map_err(|e| e.to_string())?; + let mut builder = client + .request(request_method(request.method)?, parsed) + .headers(request_headers(request.headers)); + if let Some(body) = request.body { + builder = builder.body(body); + } + + let response = builder.send().await.map_err(|e| e.to_string())?; + let status = response.status().as_u16(); + let headers = response + .headers() + .iter() + .filter_map(|(name, value)| { + Some(ProxyHttpHeader { + name: name.as_str().to_owned(), + value: value.to_str().ok()?.to_owned(), + }) + }) + .collect(); + let body = response.bytes().await.map_err(|e| e.to_string())?.to_vec(); + + Ok(ProxyHttpResponse { + status, + headers, + body, + }) +} diff --git a/desktop/src/lib.rs b/desktop/src/lib.rs index 4902d9a7d..6ae99b108 100644 --- a/desktop/src/lib.rs +++ b/desktop/src/lib.rs @@ -1,5 +1,6 @@ mod fig_container; mod fonts; +mod http; mod menu; mod menu_events; #[cfg(target_os = "macos")] @@ -7,6 +8,7 @@ mod window; use fig_container::build_fig_file; use fonts::{list_system_fonts, load_system_font}; +use http::proxy_http_request; use menu::install_app_menu; use menu_events::handle_menu_event; use std::{ @@ -118,6 +120,7 @@ pub fn run() { build_fig_file, list_system_fonts, load_system_font, + proxy_http_request, take_pending_open ]) .plugin(tauri_plugin_opener::init()) diff --git a/packages/core/package.json b/packages/core/package.json index 6bba49a94..d974a0545 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -2,167 +2,169 @@ "name": "@open-pencil/core", "version": "0.13.2", "license": "MIT", - "type": "module", - "imports": { - "#core/*": "./src/*" - }, - "sideEffects": false, - "exports": { - "./package.json": "./package.json", - ".": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js", - "import": "./dist/index.js" - }, - "./color": { - "types": "./dist/color/index.d.ts", - "default": "./dist/color/index.js", - "import": "./dist/color/index.js" - }, - "./text": { - "types": "./dist/text/index.d.ts", - "default": "./dist/text/index.js", - "import": "./dist/text/index.js" - }, - "./vector": { - "types": "./dist/vector/index.d.ts", - "default": "./dist/vector/index.js", - "import": "./dist/vector/index.js" - }, - "./figma-api": { - "types": "./dist/figma-api/index.d.ts", - "default": "./dist/figma-api/index.js", - "import": "./dist/figma-api/index.js" - }, - "./icons": { - "types": "./dist/icons/index.d.ts", - "default": "./dist/icons/index.js", - "import": "./dist/icons/index.js" - }, - "./canvas": { - "types": "./dist/canvas/index.d.ts", - "default": "./dist/canvas/index.js", - "import": "./dist/canvas/index.js" - }, - "./design-jsx": { - "types": "./dist/design-jsx/index.d.ts", - "default": "./dist/design-jsx/index.js", - "import": "./dist/design-jsx/index.js" - }, - "./editor": { - "types": "./dist/editor/index.d.ts", - "default": "./dist/editor/index.js", - "import": "./dist/editor/index.js" - }, - "./tools": { - "types": "./dist/tools/index.d.ts", - "default": "./dist/tools/index.js", - "import": "./dist/tools/index.js" - }, - "./profiler": { - "types": "./dist/profiler/index.d.ts", - "default": "./dist/profiler/index.js", - "import": "./dist/profiler/index.js" - }, - "./rpc": { - "types": "./dist/rpc/index.d.ts", - "default": "./dist/rpc/index.js", - "import": "./dist/rpc/index.js" - }, - "./lint": { - "types": "./dist/lint/index.d.ts", - "default": "./dist/lint/index.js", - "import": "./dist/lint/index.js" - }, - "./io": { - "types": "./dist/io/index.d.ts", - "default": "./dist/io/index.js", - "import": "./dist/io/index.js" - }, - "./io/formats/fig": { - "types": "./dist/io/formats/fig/index.d.ts", - "default": "./dist/io/formats/fig/index.js", - "import": "./dist/io/formats/fig/index.js" - }, - "./io/formats/pen": { - "types": "./dist/io/formats/pen/index.d.ts", - "default": "./dist/io/formats/pen/index.js", - "import": "./dist/io/formats/pen/index.js" - }, - "./io/formats/jsx": { - "types": "./dist/io/formats/jsx/index.d.ts", - "default": "./dist/io/formats/jsx/index.js", - "import": "./dist/io/formats/jsx/index.js" - }, - "./io/formats/raster": { - "types": "./dist/io/formats/raster/index.d.ts", - "default": "./dist/io/formats/raster/index.js", - "import": "./dist/io/formats/raster/index.js" - }, - "./io/formats/svg": { - "types": "./dist/io/formats/svg/index.d.ts", - "default": "./dist/io/formats/svg/index.js", - "import": "./dist/io/formats/svg/index.js" - }, - "./kiwi": { - "types": "./dist/kiwi/index.d.ts", - "default": "./dist/kiwi/index.js", - "import": "./dist/kiwi/index.js" - }, - "./clipboard": { - "types": "./dist/clipboard.d.ts", - "default": "./dist/clipboard.js", - "import": "./dist/clipboard.js" - }, - "./constants": { - "types": "./dist/constants.d.ts", - "default": "./dist/constants.js", - "import": "./dist/constants.js" - }, - "./random": { - "types": "./dist/random.d.ts", - "default": "./dist/random.js", - "import": "./dist/random.js" - }, - "./xpath": { - "types": "./dist/xpath.d.ts", - "default": "./dist/xpath.js", - "import": "./dist/xpath.js" - }, - "./canvaskit": { - "types": "./dist/canvaskit.d.ts", - "default": "./dist/canvaskit.js", - "import": "./dist/canvaskit.js" - }, - "./layout": { - "types": "./dist/layout.d.ts", - "default": "./dist/layout.js", - "import": "./dist/layout.js" - } - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "files": [ - "dist", - "assets" - ], - "scripts": { - "build": "bunx tsdown --config tsdown.config.ts", - "prepublishOnly": "bun run build" - }, "repository": { "type": "git", "url": "git+https://github.com/open-pencil/open-pencil.git", "directory": "packages/core" }, + "files": [ + "dist", + "assets" + ], + "type": "module", + "sideEffects": false, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "imports": { + "#core/*": "./src/*" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + }, + "./color": { + "types": "./dist/color/index.d.ts", + "import": "./dist/color/index.js", + "default": "./dist/color/index.js" + }, + "./text": { + "types": "./dist/text/index.d.ts", + "import": "./dist/text/index.js", + "default": "./dist/text/index.js" + }, + "./vector": { + "types": "./dist/vector/index.d.ts", + "import": "./dist/vector/index.js", + "default": "./dist/vector/index.js" + }, + "./figma-api": { + "types": "./dist/figma-api/index.d.ts", + "import": "./dist/figma-api/index.js", + "default": "./dist/figma-api/index.js" + }, + "./icons": { + "types": "./dist/icons/index.d.ts", + "import": "./dist/icons/index.js", + "default": "./dist/icons/index.js" + }, + "./canvas": { + "types": "./dist/canvas/index.d.ts", + "import": "./dist/canvas/index.js", + "default": "./dist/canvas/index.js" + }, + "./design-jsx": { + "types": "./dist/design-jsx/index.d.ts", + "import": "./dist/design-jsx/index.js", + "default": "./dist/design-jsx/index.js" + }, + "./editor": { + "types": "./dist/editor/index.d.ts", + "import": "./dist/editor/index.js", + "default": "./dist/editor/index.js" + }, + "./tools": { + "types": "./dist/tools/index.d.ts", + "import": "./dist/tools/index.js", + "default": "./dist/tools/index.js" + }, + "./profiler": { + "types": "./dist/profiler/index.d.ts", + "import": "./dist/profiler/index.js", + "default": "./dist/profiler/index.js" + }, + "./rpc": { + "types": "./dist/rpc/index.d.ts", + "import": "./dist/rpc/index.js", + "default": "./dist/rpc/index.js" + }, + "./lint": { + "types": "./dist/lint/index.d.ts", + "import": "./dist/lint/index.js", + "default": "./dist/lint/index.js" + }, + "./io": { + "types": "./dist/io/index.d.ts", + "import": "./dist/io/index.js", + "default": "./dist/io/index.js" + }, + "./io/formats/fig": { + "types": "./dist/io/formats/fig/index.d.ts", + "import": "./dist/io/formats/fig/index.js", + "default": "./dist/io/formats/fig/index.js" + }, + "./io/formats/pen": { + "types": "./dist/io/formats/pen/index.d.ts", + "import": "./dist/io/formats/pen/index.js", + "default": "./dist/io/formats/pen/index.js" + }, + "./io/formats/jsx": { + "types": "./dist/io/formats/jsx/index.d.ts", + "import": "./dist/io/formats/jsx/index.js", + "default": "./dist/io/formats/jsx/index.js" + }, + "./io/formats/raster": { + "types": "./dist/io/formats/raster/index.d.ts", + "import": "./dist/io/formats/raster/index.js", + "default": "./dist/io/formats/raster/index.js" + }, + "./io/formats/svg": { + "types": "./dist/io/formats/svg/index.d.ts", + "import": "./dist/io/formats/svg/index.js", + "default": "./dist/io/formats/svg/index.js" + }, + "./kiwi": { + "types": "./dist/kiwi/index.d.ts", + "import": "./dist/kiwi/index.js", + "default": "./dist/kiwi/index.js" + }, + "./clipboard": { + "types": "./dist/clipboard.d.ts", + "import": "./dist/clipboard.js", + "default": "./dist/clipboard.js" + }, + "./constants": { + "types": "./dist/constants.d.ts", + "import": "./dist/constants.js", + "default": "./dist/constants.js" + }, + "./random": { + "types": "./dist/random.d.ts", + "import": "./dist/random.js", + "default": "./dist/random.js" + }, + "./xpath": { + "types": "./dist/xpath.d.ts", + "import": "./dist/xpath.js", + "default": "./dist/xpath.js" + }, + "./canvaskit": { + "types": "./dist/canvaskit.d.ts", + "import": "./dist/canvaskit.js", + "default": "./dist/canvaskit.js" + }, + "./layout": { + "types": "./dist/layout.d.ts", + "import": "./dist/layout.js", + "default": "./dist/layout.js" + } + }, "publishConfig": { "access": "public", "provenance": true }, + "scripts": { + "build": "bunx tsdown --config tsdown.config.ts", + "prepublishOnly": "bun run build" + }, "dependencies": { "@chenglou/pretext": "^0.0.7", "@iconify/utils": "^3.1.0", "@open-pencil/kiwi": "workspace:*", + "@open-pencil/pen": "workspace:*", + "@open-pencil/scene-graph": "workspace:*", "@tauri-apps/api": "^2", "acorn": "^8.16.0", "canvaskit-wasm": "^0.40.0", @@ -180,9 +182,8 @@ "svg2pdf.js": "^2.7.0", "svgpath": "^2.6.0", "twirlwind": "^0.3.0", - "yoga-layout": "npm:@open-pencil/yoga-layout@3.3.0-grid.3", - "@open-pencil/scene-graph": "workspace:*", - "@open-pencil/pen": "workspace:*" + "unifont": "^0.7.4", + "yoga-layout": "npm:@open-pencil/yoga-layout@3.3.0-grid.3" }, "devDependencies": { "@types/culori": "^4.0.1", diff --git a/packages/core/src/text/fonts.ts b/packages/core/src/text/fonts.ts index 16ba5b08c..c35ec319a 100644 --- a/packages/core/src/text/fonts.ts +++ b/packages/core/src/text/fonts.ts @@ -1,12 +1,13 @@ import type { CanvasKit, TypefaceFontProvider } from 'canvaskit-wasm' -import { uniq } from 'es-toolkit/array' import type { SceneGraph } from '@open-pencil/scene-graph' -import { DEFAULT_FONT_FAMILY, IS_BROWSER, GOOGLE_FONTS_API_KEY } from '#core/constants' +import { DEFAULT_FONT_FAMILY, IS_BROWSER } from '#core/constants' import { fontFaceRenderFamily, parseFontStyle } from '#core/text/face' import { fontFallbackEntry } from '#core/text/fallbacks' import type { FontFallbackScript } from '#core/text/fallbacks' +import { WebFontResolver } from '#core/text/web-fonts' +import type { WebFontFetch, WebFontProviderId } from '#core/text/web-fonts' export interface FontInfo { family: string @@ -16,7 +17,7 @@ export interface FontInfo { } export type LocalFontAccessState = 'unsupported' | 'prompt' | 'granted' | 'denied' -export type FontFamilySource = 'local' | 'google' | 'bundled' +export type FontFamilySource = 'local' | 'bundled' | 'fallback' | WebFontProviderId export interface FontFamilyOption { family: string @@ -136,10 +137,7 @@ export class FontManager { private localFontAccessState: LocalFontAccessState = IS_BROWSER ? 'prompt' : 'unsupported' private downloadedFontCache: DownloadedFontCache | null = null private fallbackUserAgent: string | undefined - private googleFontsCache = new Map>() - private googleFontsFailed = new Set() - private googleFamiliesCache: string[] | null = null - private googleFamiliesPromise: Promise | null = null + private webFonts = new WebFontResolver() private registeredRenderFamilies = new Set() private cjkFallbackFamilies: string[] = [] private cjkFallbackPromise: Promise | null = null @@ -175,6 +173,18 @@ export class FontManager { this.fallbackUserAgent = userAgent } + setOnlineFontProviders(settings: Partial>): void { + this.webFonts.setEnabled(settings) + } + + setWebFontFetch(fetcher: WebFontFetch | null): void { + this.webFonts.setRemoteFetch(fetcher) + } + + enabledOnlineFontProviders(): WebFontProviderId[] { + return this.webFonts.enabledProviders() + } + async loadCachedFont(family: string, style = 'Regular'): Promise { const cached = await this.readDownloadedFont(family, style) if (!cached) return null @@ -219,17 +229,30 @@ export class FontManager { async listFamilyOptions(): Promise { const fonts = this.localFonts ?? (await this.requestLocalFontAccess()) - const googleFamilies = await this.listGoogleFamilies() + const webFontFamilies = await Promise.all( + this.enabledOnlineFontProviders().map(async (provider) => ({ + provider, + families: await this.webFonts.listFamilies(provider) + })) + ) const byFamily = new Map() byFamily.set(DEFAULT_FONT_FAMILY, { family: DEFAULT_FONT_FAMILY, source: 'bundled' }) - for (const family of googleFamilies) byFamily.set(family, { family, source: 'google' }) + for (const { provider, families } of webFontFamilies) { + for (const family of families) { + if (!byFamily.has(family)) byFamily.set(family, { family, source: provider }) + } + } for (const font of fonts) byFamily.set(font.family, { family: font.family, source: 'local' }) return [...byFamily.values()].sort((a, b) => a.family.localeCompare(b.family)) } + preloadWebFontFamilies(): void { + this.webFonts.preloadFamilies() + } + preloadGoogleFamilies(): void { - if (this.googleFamiliesCache || this.googleFamiliesPromise) return - this.googleFamiliesPromise = this.loadGoogleFamilies() + if (!this.webFonts.enabledProviders().includes('google')) return + void this.webFonts.listFamilies('google') } async fetchBundledFont(url: string): Promise { @@ -274,13 +297,15 @@ export class FontManager { if (typeof fetch !== 'undefined') { try { - const buffer = await this.fetchGoogleFont(family, style) + const normalized = normalizeFontFamily(family) + const families = normalized === family ? [family] : [family, normalized] + const buffer = await this.webFonts.fetchFont(families, style) if (buffer) { await this.writeDownloadedFont(family, style, buffer) return this.registerAndCache(family, style, buffer) } } catch (e) { - console.warn(`Google Fonts fetch failed for "${family}" ${style}:`, e) + console.warn(`Web font fetch failed for "${family}" ${style}:`, e) } } @@ -450,79 +475,6 @@ export class FontManager { } } - private async retryWithNormalizedFamily(family: string): Promise | null> { - const normalized = normalizeFontFamily(family) - if (normalized === family) { - this.googleFontsFailed.add(family) - return null - } - const result = await this.fetchGoogleFontFiles(normalized) - if (result) this.googleFontsCache.set(family, result) - else this.googleFontsFailed.add(family) - return result - } - - private async listGoogleFamilies(): Promise { - if (this.googleFamiliesCache) return this.googleFamiliesCache - this.googleFamiliesPromise ??= this.loadGoogleFamilies() - return this.googleFamiliesPromise - } - - private async loadGoogleFamilies(): Promise { - if (typeof fetch === 'undefined') return [] - - try { - const response = await fetch( - `https://www.googleapis.com/webfonts/v1/webfonts?key=${GOOGLE_FONTS_API_KEY}` - ) - if (!response.ok) return [] - const data = (await response.json()) as { items?: Array<{ family?: string }> } - const families = uniq(data.items?.flatMap((item) => (item.family ? [item.family] : [])) ?? []) - this.googleFamiliesCache = families.sort() - return this.googleFamiliesCache - } catch { - return [] - } - } - - private async fetchGoogleFontFiles(family: string): Promise | null> { - if (this.googleFontsCache.has(family)) return this.googleFontsCache.get(family) ?? null - if (this.googleFontsFailed.has(family)) return null - - const url = `https://www.googleapis.com/webfonts/v1/webfonts?family=${encodeURIComponent(family)}&key=${GOOGLE_FONTS_API_KEY}` - let response: Response - try { - response = await fetch(url) - } catch { - this.googleFontsFailed.add(family) - return null - } - if (!response.ok) return this.retryWithNormalizedFamily(family) - - const data = (await response.json()) as { - items?: Array<{ files?: Record }> - } - const files = data.items?.[0]?.files - if (!files) return this.retryWithNormalizedFamily(family) - - this.googleFontsCache.set(family, files) - return files - } - - private async fetchGoogleFont(family: string, style: string): Promise { - const files = await this.fetchGoogleFontFiles(family) - if (!files) return null - - const variant = styleToVariant(style) - const ttfUrl = files[variant] ?? files['regular'] - if (!ttfUrl) return null - - const response = await fetch(ttfUrl) - if (!response.ok) return null - - return response.arrayBuffer() - } - private async findLocalFont( family: string, style?: string, diff --git a/packages/core/src/text/index.ts b/packages/core/src/text/index.ts index ff39a74d9..5b67d991c 100644 --- a/packages/core/src/text/index.ts +++ b/packages/core/src/text/index.ts @@ -3,3 +3,4 @@ export * from './style-runs' export * from './direction' export * from './fonts' export * from './fallbacks' +export * from './web-fonts' diff --git a/packages/core/src/text/web-fonts.ts b/packages/core/src/text/web-fonts.ts new file mode 100644 index 000000000..a61961613 --- /dev/null +++ b/packages/core/src/text/web-fonts.ts @@ -0,0 +1,216 @@ +import { + createUnifont, + providers, + type RemoteFontSource, + type ResolveFontOptions, + type ResolveFontResult, + type Unifont +} from 'unifont' + +import { IS_BROWSER } from '#core/constants' +import { parseFontStyle } from '#core/text/face' + +export const WEB_FONT_PROVIDER_IDS = ['google', 'fontsource', 'bunny', 'fontshare'] as const +export type WebFontProviderId = (typeof WEB_FONT_PROVIDER_IDS)[number] + +export const WEB_FONT_PROVIDER_LABELS: Record = { + google: 'Google Fonts', + fontsource: 'Fontsource', + bunny: 'Bunny Fonts', + fontshare: 'Fontshare' +} + +export const DEFAULT_WEB_FONT_PROVIDER_SETTINGS: Record = { + google: true, + fontsource: true, + bunny: false, + fontshare: false +} + +export type WebFontFetch = (url: string, init?: RequestInit) => Promise + +type WebFontProvider = + | ReturnType + | ReturnType + | ReturnType + | ReturnType +type WebUnifont = Unifont<[WebFontProvider]> +type WebFontResolveOptions = Pick + +const providerFactories = { + google: providers.google, + fontsource: providers.fontsource, + bunny: providers.bunny, + fontshare: providers.fontshare +} satisfies Record WebFontProvider> + +async function createProviderUnifont(provider: WebFontProviderId): Promise { + return createUnifont([providerFactories[provider]()], { throwOnError: false }) +} + +function isRemoteFontSource( + source: RemoteFontSource | { name: string } +): source is RemoteFontSource { + return 'url' in source +} + +export class WebFontResolver { + private enabled = new Set( + WEB_FONT_PROVIDER_IDS.filter((provider) => DEFAULT_WEB_FONT_PROVIDER_SETTINGS[provider]) + ) + private unifontPromises = new Map>() + private familiesCache = new Map() + private familiesPromises = new Map>() + private failedFonts = new Set() + private fontPromises = new Map>() + private remoteFetch: WebFontFetch | null = null + + setEnabled(settings: Partial>): void { + this.enabled = new Set(WEB_FONT_PROVIDER_IDS.filter((provider) => settings[provider] === true)) + } + + setRemoteFetch(fetcher: WebFontFetch | null): void { + this.remoteFetch = fetcher + this.unifontPromises.clear() + this.familiesPromises.clear() + this.familiesCache.clear() + } + + enabledProviders(): WebFontProviderId[] { + return WEB_FONT_PROVIDER_IDS.filter((provider) => this.enabled.has(provider)) + } + + preloadFamilies(): void { + if (IS_BROWSER && !this.remoteFetch) return + for (const provider of this.enabledProviders()) void this.listFamilies(provider) + } + + async listFamilies(provider: WebFontProviderId): Promise { + const cached = this.familiesCache.get(provider) + if (cached) return cached + + let promise = this.familiesPromises.get(provider) + if (!promise) { + promise = this.loadFamilies(provider) + this.familiesPromises.set(provider, promise) + } + return promise + } + + async fetchFont(families: string[], style: string): Promise { + const providers = this.enabledProviders() + if (providers.length === 0 || (IS_BROWSER && !this.remoteFetch)) return null + + for (const family of families) { + for (const provider of providers) { + const buffer = await this.fetchFromProvider(family, style, provider) + if (buffer) return buffer + } + } + + return null + } + + private async withFetchProxy(operation: () => Promise): Promise { + if (!this.remoteFetch) return operation() + + const originalFetch = globalThis.fetch + globalThis.fetch = ((input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === 'string' || input instanceof URL ? input.toString() : input.url + if (url.startsWith('https://') || url.startsWith('http://')) { + return ( + this.remoteFetch?.(url, init) ?? Promise.reject(new TypeError('No font proxy fetcher')) + ) + } + return originalFetch(input, init) + }) as typeof fetch + + try { + return await operation() + } finally { + globalThis.fetch = originalFetch + } + } + + private async fetchRemote(url: string, init?: RequestInit): Promise { + if (this.remoteFetch) return this.remoteFetch(url, init) + return fetch(url, init) + } + + private async unifont(provider: WebFontProviderId): Promise { + let promise = this.unifontPromises.get(provider) + if (!promise) { + promise = this.withFetchProxy(() => createProviderUnifont(provider)) + this.unifontPromises.set(provider, promise) + } + return promise + } + + private async loadFamilies(provider: WebFontProviderId): Promise { + if (typeof fetch === 'undefined' || (IS_BROWSER && !this.remoteFetch)) return [] + + try { + const unifont = await this.unifont(provider) + const listedFamilies = await this.withFetchProxy(() => unifont.listFonts()) + const families = listedFamilies ? [...new Set(listedFamilies)].sort() : [] + this.familiesCache.set(provider, families) + return families + } catch { + this.familiesCache.set(provider, []) + return [] + } + } + + private async fetchFromProvider( + family: string, + style: string, + provider: WebFontProviderId + ): Promise { + const key = `${provider}|${family}|${style}` + if (this.failedFonts.has(key)) return null + + let promise = this.fontPromises.get(key) + if (!promise) { + promise = this.loadFromProvider(family, style, provider) + this.fontPromises.set(key, promise) + } + + const result = await promise + this.fontPromises.delete(key) + if (!result) this.failedFonts.add(key) + return result + } + + private async loadFromProvider( + family: string, + style: string, + provider: WebFontProviderId + ): Promise { + try { + const parsed = parseFontStyle(style) + const unifont = await this.unifont(provider) + const options = { + weights: [String(parsed.weight)], + styles: [parsed.italic ? 'italic' : 'normal'], + formats: ['ttf'], + subsets: ['latin'] + } satisfies WebFontResolveOptions + const result = await this.withFetchProxy(() => + unifont.resolveFont(family, options) + ) + + const sources = result.fonts + .toSorted((a, b) => (a.meta?.priority ?? 0) - (b.meta?.priority ?? 0)) + .flatMap((font) => font.src.filter(isRemoteFontSource)) + if (sources.length === 0) return null + const source = + sources.find((item) => item.format === 'truetype' || item.format === 'ttf') ?? sources[0] + + const response = await this.fetchRemote(source.url) + if (!response.ok) return null + return await response.arrayBuffer() + } catch { + return null + } + } +} diff --git a/packages/vue/src/i18n/messages.ts b/packages/vue/src/i18n/messages.ts index ea76d0485..cf4871d14 100644 --- a/packages/vue/src/i18n/messages.ts +++ b/packages/vue/src/i18n/messages.ts @@ -347,6 +347,48 @@ export const dialogMessages = i18n('dialogs', { closeTab: params('Close {name}'), showUI: params('Show UI ({shortcut})'), fontSettings: 'Font settings', + fontSettingsDesktopDescription: + 'Access system fonts, online providers, fallback packs, and cached downloads.', + fontSettingsBrowserDescription: + 'Allow browser access to local fonts and manage online font providers.', + localFonts: 'Local fonts', + onlineFonts: 'Online fonts', + downloadedCache: 'Downloaded cache', + lastUpdated: 'Last updated', + enabled: 'Enabled', + disabled: 'Disabled', + denied: 'Denied', + unavailable: 'Unavailable', + notRequested: 'Not requested', + never: 'Never', + systemFontAccess: 'System font access', + systemFontsAvailable: 'System fonts are available.', + allowBrowserFontAccess: 'Allow browser font access when system fonts are missing.', + allow: 'Allow', + requesting: 'Requesting…', + onlineFontProviders: 'Online font providers', + downloadMissingWebFonts: 'Download missing web fonts through enabled providers.', + webFontProvidersRequireDesktopApp: + 'Online font provider catalogs are unavailable in the web app. Download the desktop app to browse and load provider fonts.', + enable: 'Enable', + disable: 'Disable', + fallbackPacks: 'Fallback packs', + downloadFallbackPacksDescription: + 'Download CJK and Arabic fallbacks before opening files that need them.', + download: 'Download', + downloading: 'Downloading…', + refresh: 'Refresh', + clearCache: 'Clear cache', + localFontAccessEnabled: 'Local font access enabled.', + localFontAccessNotGranted: 'Local font access was not granted.', + onlineFontProvidersEnabled: 'Online font providers enabled.', + onlineFontProvidersDisabled: 'Online font providers disabled.', + fontProviderEnabled: params('Enabled {provider}.'), + fontProviderDisabled: params('Disabled {provider}.'), + fallbackFontsDownloaded: 'Fallback fonts downloaded.', + fallbackFontsDownloadFailed: 'Could not download fallback fonts.', + downloadedFontCacheCleared: 'Downloaded font cache cleared.', + downloadedFontCacheClearFailed: 'Could not clear downloaded font cache.', you: 'You', youSuffix: 'you', followingPeerStop: params('Following {name} (click to stop)'), diff --git a/packages/vue/src/locales/de.json b/packages/vue/src/locales/de.json index 27c0519dc..132a179e6 100644 --- a/packages/vue/src/locales/de.json +++ b/packages/vue/src/locales/de.json @@ -342,6 +342,44 @@ "closeTab": "{name} schließen", "showUI": "UI anzeigen ({shortcut})", "fontSettings": "Schrifteinstellungen", + "fontSettingsDesktopDescription": "Auf Systemschriften, Online-Anbieter, Fallback-Pakete und zwischengespeicherte Downloads zugreifen.", + "fontSettingsBrowserDescription": "Browserzugriff auf lokale Schriften erlauben und Online-Schriftanbieter verwalten.", + "localFonts": "Lokale Schriften", + "onlineFonts": "Online-Schriften", + "downloadedCache": "Download-Cache", + "lastUpdated": "Zuletzt aktualisiert", + "enabled": "Aktiviert", + "disabled": "Deaktiviert", + "denied": "Verweigert", + "unavailable": "Nicht verfügbar", + "notRequested": "Nicht angefragt", + "never": "Nie", + "systemFontAccess": "Zugriff auf Systemschriften", + "systemFontsAvailable": "Systemschriften sind verfügbar.", + "allowBrowserFontAccess": "Browser-Schriftzugriff erlauben, wenn Systemschriften fehlen.", + "allow": "Erlauben", + "requesting": "Wird angefragt…", + "onlineFontProviders": "Online-Schriftanbieter", + "downloadMissingWebFonts": "Fehlende Web-Schriften über aktivierte Anbieter herunterladen.", + "webFontProvidersRequireDesktopApp": "Online-Schriftanbieter-Kataloge sind in der Web-App nicht verfügbar. Lade die Desktop-App herunter, um Anbieter-Schriften zu durchsuchen und zu laden.", + "enable": "Aktivieren", + "disable": "Deaktivieren", + "fallbackPacks": "Fallback-Pakete", + "downloadFallbackPacksDescription": "CJK- und Arabisch-Fallbacks vor dem Öffnen von Dateien herunterladen, die sie benötigen.", + "download": "Herunterladen", + "downloading": "Wird heruntergeladen…", + "refresh": "Aktualisieren", + "clearCache": "Cache leeren", + "localFontAccessEnabled": "Lokaler Schriftzugriff aktiviert.", + "localFontAccessNotGranted": "Lokaler Schriftzugriff wurde nicht gewährt.", + "onlineFontProvidersEnabled": "Online-Schriftanbieter aktiviert.", + "onlineFontProvidersDisabled": "Online-Schriftanbieter deaktiviert.", + "fontProviderEnabled": "{provider} aktiviert.", + "fontProviderDisabled": "{provider} deaktiviert.", + "fallbackFontsDownloaded": "Fallback-Schriften heruntergeladen.", + "fallbackFontsDownloadFailed": "Fallback-Schriften konnten nicht heruntergeladen werden.", + "downloadedFontCacheCleared": "Download-Schriftcache geleert.", + "downloadedFontCacheClearFailed": "Download-Schriftcache konnte nicht geleert werden.", "you": "Du", "youSuffix": "du", "followingPeerStop": "Folge {name} (zum Stoppen klicken)", diff --git a/packages/vue/src/locales/es.json b/packages/vue/src/locales/es.json index ead3b3269..90ab16272 100644 --- a/packages/vue/src/locales/es.json +++ b/packages/vue/src/locales/es.json @@ -342,6 +342,44 @@ "closeTab": "Cerrar {name}", "showUI": "Mostrar interfaz ({shortcut})", "fontSettings": "Ajustes de fuentes", + "fontSettingsDesktopDescription": "Accede a fuentes del sistema, proveedores en línea, paquetes de respaldo y descargas en caché.", + "fontSettingsBrowserDescription": "Permite al navegador acceder a fuentes locales y gestiona proveedores de fuentes en línea.", + "localFonts": "Fuentes locales", + "onlineFonts": "Fuentes en línea", + "downloadedCache": "Caché descargada", + "lastUpdated": "Última actualización", + "enabled": "Activado", + "disabled": "Desactivado", + "denied": "Denegado", + "unavailable": "No disponible", + "notRequested": "No solicitado", + "never": "Nunca", + "systemFontAccess": "Acceso a fuentes del sistema", + "systemFontsAvailable": "Las fuentes del sistema están disponibles.", + "allowBrowserFontAccess": "Permite el acceso a fuentes del navegador cuando falten fuentes del sistema.", + "allow": "Permitir", + "requesting": "Solicitando…", + "onlineFontProviders": "Proveedores de fuentes en línea", + "downloadMissingWebFonts": "Descarga fuentes web faltantes mediante los proveedores activados.", + "webFontProvidersRequireDesktopApp": "Los catálogos de proveedores de fuentes en línea no están disponibles en la app web. Descarga la app de escritorio para explorar y cargar fuentes de proveedores.", + "enable": "Activar", + "disable": "Desactivar", + "fallbackPacks": "Paquetes de respaldo", + "downloadFallbackPacksDescription": "Descarga respaldos CJK y árabes antes de abrir archivos que los necesiten.", + "download": "Descargar", + "downloading": "Descargando…", + "refresh": "Actualizar", + "clearCache": "Borrar caché", + "localFontAccessEnabled": "Acceso a fuentes locales activado.", + "localFontAccessNotGranted": "No se concedió acceso a fuentes locales.", + "onlineFontProvidersEnabled": "Proveedores de fuentes en línea activados.", + "onlineFontProvidersDisabled": "Proveedores de fuentes en línea desactivados.", + "fontProviderEnabled": "{provider} activado.", + "fontProviderDisabled": "{provider} desactivado.", + "fallbackFontsDownloaded": "Fuentes de respaldo descargadas.", + "fallbackFontsDownloadFailed": "No se pudieron descargar las fuentes de respaldo.", + "downloadedFontCacheCleared": "Caché de fuentes descargadas borrada.", + "downloadedFontCacheClearFailed": "No se pudo borrar la caché de fuentes descargadas.", "you": "Tú", "youSuffix": "tú", "followingPeerStop": "Siguiendo a {name} (haz clic para detener)", diff --git a/packages/vue/src/locales/fr.json b/packages/vue/src/locales/fr.json index 2754f45bc..1fb8e290c 100644 --- a/packages/vue/src/locales/fr.json +++ b/packages/vue/src/locales/fr.json @@ -342,6 +342,44 @@ "closeTab": "Fermer {name}", "showUI": "Afficher l’interface ({shortcut})", "fontSettings": "Réglages des polices", + "fontSettingsDesktopDescription": "Accédez aux polices système, aux fournisseurs en ligne, aux packs de secours et aux téléchargements en cache.", + "fontSettingsBrowserDescription": "Autorisez l’accès du navigateur aux polices locales et gérez les fournisseurs de polices en ligne.", + "localFonts": "Polices locales", + "onlineFonts": "Polices en ligne", + "downloadedCache": "Cache téléchargé", + "lastUpdated": "Dernière mise à jour", + "enabled": "Activé", + "disabled": "Désactivé", + "denied": "Refusé", + "unavailable": "Indisponible", + "notRequested": "Non demandé", + "never": "Jamais", + "systemFontAccess": "Accès aux polices système", + "systemFontsAvailable": "Les polices système sont disponibles.", + "allowBrowserFontAccess": "Autorisez l’accès du navigateur aux polices lorsque des polices système manquent.", + "allow": "Autoriser", + "requesting": "Demande…", + "onlineFontProviders": "Fournisseurs de polices en ligne", + "downloadMissingWebFonts": "Télécharger les polices web manquantes via les fournisseurs activés.", + "webFontProvidersRequireDesktopApp": "Les catalogues de fournisseurs de polices en ligne ne sont pas disponibles dans l’app web. Téléchargez l’app de bureau pour parcourir et charger les polices des fournisseurs.", + "enable": "Activer", + "disable": "Désactiver", + "fallbackPacks": "Packs de secours", + "downloadFallbackPacksDescription": "Téléchargez les polices de secours CJK et arabes avant d’ouvrir les fichiers qui en ont besoin.", + "download": "Télécharger", + "downloading": "Téléchargement…", + "refresh": "Actualiser", + "clearCache": "Vider le cache", + "localFontAccessEnabled": "Accès aux polices locales activé.", + "localFontAccessNotGranted": "L’accès aux polices locales n’a pas été accordé.", + "onlineFontProvidersEnabled": "Fournisseurs de polices en ligne activés.", + "onlineFontProvidersDisabled": "Fournisseurs de polices en ligne désactivés.", + "fontProviderEnabled": "{provider} activé.", + "fontProviderDisabled": "{provider} désactivé.", + "fallbackFontsDownloaded": "Polices de secours téléchargées.", + "fallbackFontsDownloadFailed": "Impossible de télécharger les polices de secours.", + "downloadedFontCacheCleared": "Cache des polices téléchargées vidé.", + "downloadedFontCacheClearFailed": "Impossible de vider le cache des polices téléchargées.", "you": "Vous", "youSuffix": "vous", "followingPeerStop": "Suit {name} (cliquer pour arrêter)", diff --git a/packages/vue/src/locales/it.json b/packages/vue/src/locales/it.json index dab3a13ce..e68262056 100644 --- a/packages/vue/src/locales/it.json +++ b/packages/vue/src/locales/it.json @@ -342,6 +342,44 @@ "closeTab": "Chiudi {name}", "showUI": "Mostra interfaccia ({shortcut})", "fontSettings": "Impostazioni font", + "fontSettingsDesktopDescription": "Accedi ai font di sistema, ai provider online, ai pacchetti di fallback e ai download nella cache.", + "fontSettingsBrowserDescription": "Consenti al browser di accedere ai font locali e gestisci i provider di font online.", + "localFonts": "Font locali", + "onlineFonts": "Font online", + "downloadedCache": "Cache scaricata", + "lastUpdated": "Ultimo aggiornamento", + "enabled": "Abilitato", + "disabled": "Disabilitato", + "denied": "Negato", + "unavailable": "Non disponibile", + "notRequested": "Non richiesto", + "never": "Mai", + "systemFontAccess": "Accesso ai font di sistema", + "systemFontsAvailable": "I font di sistema sono disponibili.", + "allowBrowserFontAccess": "Consenti l’accesso ai font del browser quando mancano font di sistema.", + "allow": "Consenti", + "requesting": "Richiesta…", + "onlineFontProviders": "Provider di font online", + "downloadMissingWebFonts": "Scarica i font web mancanti tramite i provider abilitati.", + "webFontProvidersRequireDesktopApp": "I cataloghi dei provider di font online non sono disponibili nell’app web. Scarica l’app desktop per sfogliare e caricare i font dei provider.", + "enable": "Abilita", + "disable": "Disabilita", + "fallbackPacks": "Pacchetti di fallback", + "downloadFallbackPacksDescription": "Scarica fallback CJK e arabi prima di aprire file che li richiedono.", + "download": "Scarica", + "downloading": "Download…", + "refresh": "Aggiorna", + "clearCache": "Svuota cache", + "localFontAccessEnabled": "Accesso ai font locali abilitato.", + "localFontAccessNotGranted": "Accesso ai font locali non concesso.", + "onlineFontProvidersEnabled": "Provider di font online abilitati.", + "onlineFontProvidersDisabled": "Provider di font online disabilitati.", + "fontProviderEnabled": "{provider} abilitato.", + "fontProviderDisabled": "{provider} disabilitato.", + "fallbackFontsDownloaded": "Font di fallback scaricati.", + "fallbackFontsDownloadFailed": "Impossibile scaricare i font di fallback.", + "downloadedFontCacheCleared": "Cache dei font scaricati svuotata.", + "downloadedFontCacheClearFailed": "Impossibile svuotare la cache dei font scaricati.", "you": "Tu", "youSuffix": "tu", "followingPeerStop": "Segui {name} (clic per interrompere)", diff --git a/packages/vue/src/locales/ja.json b/packages/vue/src/locales/ja.json index 94ae6e96c..51da0161d 100644 --- a/packages/vue/src/locales/ja.json +++ b/packages/vue/src/locales/ja.json @@ -342,6 +342,44 @@ "closeTab": "{name} を閉じる", "showUI": "UIを表示 ({shortcut})", "fontSettings": "フォント設定", + "fontSettingsDesktopDescription": "システムフォント、オンラインプロバイダー、フォールバックパック、キャッシュ済みダウンロードにアクセスします。", + "fontSettingsBrowserDescription": "ローカルフォントへのブラウザーアクセスを許可し、オンラインフォントプロバイダーを管理します。", + "localFonts": "ローカルフォント", + "onlineFonts": "オンラインフォント", + "downloadedCache": "ダウンロード済みキャッシュ", + "lastUpdated": "最終更新", + "enabled": "有効", + "disabled": "無効", + "denied": "拒否", + "unavailable": "利用不可", + "notRequested": "未要求", + "never": "なし", + "systemFontAccess": "システムフォントアクセス", + "systemFontsAvailable": "システムフォントを利用できます。", + "allowBrowserFontAccess": "システムフォントが見つからない場合にブラウザーのフォントアクセスを許可します。", + "allow": "許可", + "requesting": "要求中…", + "onlineFontProviders": "オンラインフォントプロバイダー", + "downloadMissingWebFonts": "有効なプロバイダーから不足しているWebフォントをダウンロードします。", + "webFontProvidersRequireDesktopApp": "オンラインフォントプロバイダーのカタログはWebアプリでは利用できません。プロバイダーのフォントを参照して読み込むにはデスクトップアプリをダウンロードしてください。", + "enable": "有効にする", + "disable": "無効にする", + "fallbackPacks": "フォールバックパック", + "downloadFallbackPacksDescription": "必要なファイルを開く前に、CJKとアラビア語のフォールバックをダウンロードします。", + "download": "ダウンロード", + "downloading": "ダウンロード中…", + "refresh": "更新", + "clearCache": "キャッシュをクリア", + "localFontAccessEnabled": "ローカルフォントアクセスを有効にしました。", + "localFontAccessNotGranted": "ローカルフォントアクセスは許可されませんでした。", + "onlineFontProvidersEnabled": "オンラインフォントプロバイダーを有効にしました。", + "onlineFontProvidersDisabled": "オンラインフォントプロバイダーを無効にしました。", + "fontProviderEnabled": "{provider} を有効にしました。", + "fontProviderDisabled": "{provider} を無効にしました。", + "fallbackFontsDownloaded": "フォールバックフォントをダウンロードしました。", + "fallbackFontsDownloadFailed": "フォールバックフォントをダウンロードできませんでした。", + "downloadedFontCacheCleared": "ダウンロード済みフォントキャッシュをクリアしました。", + "downloadedFontCacheClearFailed": "ダウンロード済みフォントキャッシュをクリアできませんでした。", "you": "あなた", "youSuffix": "あなた", "followingPeerStop": "{name} をフォロー中 (クリックして停止)", diff --git a/packages/vue/src/locales/pl.json b/packages/vue/src/locales/pl.json index d6babfcc2..9837bef4a 100644 --- a/packages/vue/src/locales/pl.json +++ b/packages/vue/src/locales/pl.json @@ -342,6 +342,44 @@ "closeTab": "Zamknij {name}", "showUI": "Pokaż interfejs ({shortcut})", "fontSettings": "Ustawienia czcionek", + "fontSettingsDesktopDescription": "Uzyskaj dostęp do czcionek systemowych, dostawców online, pakietów zapasowych i pobranych plików w pamięci podręcznej.", + "fontSettingsBrowserDescription": "Zezwól przeglądarce na dostęp do lokalnych czcionek i zarządzaj dostawcami czcionek online.", + "localFonts": "Czcionki lokalne", + "onlineFonts": "Czcionki online", + "downloadedCache": "Pobrana pamięć podręczna", + "lastUpdated": "Ostatnia aktualizacja", + "enabled": "Włączone", + "disabled": "Wyłączone", + "denied": "Odmówiono", + "unavailable": "Niedostępne", + "notRequested": "Nie zażądano", + "never": "Nigdy", + "systemFontAccess": "Dostęp do czcionek systemowych", + "systemFontsAvailable": "Czcionki systemowe są dostępne.", + "allowBrowserFontAccess": "Zezwól przeglądarce na dostęp do czcionek, gdy brakuje czcionek systemowych.", + "allow": "Zezwól", + "requesting": "Żądanie…", + "onlineFontProviders": "Dostawcy czcionek online", + "downloadMissingWebFonts": "Pobieraj brakujące czcionki webowe przez włączonych dostawców.", + "webFontProvidersRequireDesktopApp": "Katalogi dostawców czcionek online nie są dostępne w aplikacji webowej. Pobierz aplikację desktopową, aby przeglądać i ładować czcionki dostawców.", + "enable": "Włącz", + "disable": "Wyłącz", + "fallbackPacks": "Pakiety zapasowe", + "downloadFallbackPacksDescription": "Pobierz zapasowe czcionki CJK i arabskie przed otwarciem plików, które ich wymagają.", + "download": "Pobierz", + "downloading": "Pobieranie…", + "refresh": "Odśwież", + "clearCache": "Wyczyść pamięć podręczną", + "localFontAccessEnabled": "Dostęp do lokalnych czcionek włączony.", + "localFontAccessNotGranted": "Nie przyznano dostępu do lokalnych czcionek.", + "onlineFontProvidersEnabled": "Dostawcy czcionek online włączeni.", + "onlineFontProvidersDisabled": "Dostawcy czcionek online wyłączeni.", + "fontProviderEnabled": "Włączono {provider}.", + "fontProviderDisabled": "Wyłączono {provider}.", + "fallbackFontsDownloaded": "Pobrano czcionki zapasowe.", + "fallbackFontsDownloadFailed": "Nie udało się pobrać czcionek zapasowych.", + "downloadedFontCacheCleared": "Wyczyszczono pamięć podręczną pobranych czcionek.", + "downloadedFontCacheClearFailed": "Nie udało się wyczyścić pamięci podręcznej pobranych czcionek.", "you": "Ty", "youSuffix": "ty", "followingPeerStop": "Obserwujesz {name} (kliknij, aby zatrzymać)", diff --git a/packages/vue/src/locales/ru.json b/packages/vue/src/locales/ru.json index 85db5e418..dbd928cf3 100644 --- a/packages/vue/src/locales/ru.json +++ b/packages/vue/src/locales/ru.json @@ -342,6 +342,44 @@ "closeTab": "Закрыть {name}", "showUI": "Показать интерфейс ({shortcut})", "fontSettings": "Настройки шрифтов", + "fontSettingsDesktopDescription": "Доступ к системным шрифтам, онлайн-провайдерам, резервным наборам и кэшу загрузок.", + "fontSettingsBrowserDescription": "Разрешите браузеру доступ к локальным шрифтам и управляйте онлайн-провайдерами шрифтов.", + "localFonts": "Локальные шрифты", + "onlineFonts": "Онлайн-шрифты", + "downloadedCache": "Кэш загрузок", + "lastUpdated": "Последнее обновление", + "enabled": "Включено", + "disabled": "Отключено", + "denied": "Запрещено", + "unavailable": "Недоступно", + "notRequested": "Не запрошено", + "never": "Никогда", + "systemFontAccess": "Доступ к системным шрифтам", + "systemFontsAvailable": "Системные шрифты доступны.", + "allowBrowserFontAccess": "Разрешите доступ браузера к шрифтам, если системные шрифты отсутствуют.", + "allow": "Разрешить", + "requesting": "Запрос…", + "onlineFontProviders": "Онлайн-провайдеры шрифтов", + "downloadMissingWebFonts": "Загружайте отсутствующие веб-шрифты через включённых провайдеров.", + "webFontProvidersRequireDesktopApp": "Каталоги провайдеров онлайн-шрифтов недоступны в веб-приложении. Скачайте настольное приложение, чтобы просматривать и загружать шрифты провайдеров.", + "enable": "Включить", + "disable": "Отключить", + "fallbackPacks": "Резервные наборы", + "downloadFallbackPacksDescription": "Загрузите резервные CJK- и арабские шрифты перед открытием файлов, которым они нужны.", + "download": "Загрузить", + "downloading": "Загрузка…", + "refresh": "Обновить", + "clearCache": "Очистить кэш", + "localFontAccessEnabled": "Доступ к локальным шрифтам включён.", + "localFontAccessNotGranted": "Доступ к локальным шрифтам не предоставлен.", + "onlineFontProvidersEnabled": "Онлайн-провайдеры шрифтов включены.", + "onlineFontProvidersDisabled": "Онлайн-провайдеры шрифтов отключены.", + "fontProviderEnabled": "{provider} включён.", + "fontProviderDisabled": "{provider} отключён.", + "fallbackFontsDownloaded": "Резервные шрифты загружены.", + "fallbackFontsDownloadFailed": "Не удалось загрузить резервные шрифты.", + "downloadedFontCacheCleared": "Кэш загруженных шрифтов очищен.", + "downloadedFontCacheClearFailed": "Не удалось очистить кэш загруженных шрифтов.", "you": "Вы", "youSuffix": "вы", "followingPeerStop": "Следите за {name} (нажмите, чтобы остановить)", diff --git a/packages/vue/src/locales/zh-CN.json b/packages/vue/src/locales/zh-CN.json index b5afaa350..47540b66e 100644 --- a/packages/vue/src/locales/zh-CN.json +++ b/packages/vue/src/locales/zh-CN.json @@ -342,6 +342,44 @@ "closeTab": "关闭 {name}", "showUI": "显示界面({shortcut})", "fontSettings": "字体设置", + "fontSettingsDesktopDescription": "访问系统字体、在线提供商、后备字体包和缓存下载。", + "fontSettingsBrowserDescription": "允许浏览器访问本地字体,并管理在线字体提供商。", + "localFonts": "本地字体", + "onlineFonts": "在线字体", + "downloadedCache": "下载缓存", + "lastUpdated": "上次更新", + "enabled": "已启用", + "disabled": "已停用", + "denied": "已拒绝", + "unavailable": "不可用", + "notRequested": "未请求", + "never": "从不", + "systemFontAccess": "系统字体访问", + "systemFontsAvailable": "系统字体可用。", + "allowBrowserFontAccess": "缺少系统字体时,允许浏览器访问字体。", + "allow": "允许", + "requesting": "正在请求…", + "onlineFontProviders": "在线字体提供商", + "downloadMissingWebFonts": "通过已启用的提供商下载缺失的网页字体。", + "webFontProvidersRequireDesktopApp": "网页版暂不支持在线字体提供商目录。请下载桌面应用来浏览和加载提供商字体。", + "enable": "启用", + "disable": "停用", + "fallbackPacks": "后备字体包", + "downloadFallbackPacksDescription": "在打开需要这些字体的文件前,下载 CJK 和阿拉伯语后备字体。", + "download": "下载", + "downloading": "正在下载…", + "refresh": "刷新", + "clearCache": "清除缓存", + "localFontAccessEnabled": "本地字体访问已启用。", + "localFontAccessNotGranted": "未授予本地字体访问权限。", + "onlineFontProvidersEnabled": "在线字体提供商已启用。", + "onlineFontProvidersDisabled": "在线字体提供商已停用。", + "fontProviderEnabled": "已启用 {provider}。", + "fontProviderDisabled": "已停用 {provider}。", + "fallbackFontsDownloaded": "后备字体已下载。", + "fallbackFontsDownloadFailed": "无法下载后备字体。", + "downloadedFontCacheCleared": "已清除下载的字体缓存。", + "downloadedFontCacheClearFailed": "无法清除下载的字体缓存。", "you": "你", "youSuffix": "你", "followingPeerStop": "正在跟随 {name}(点击停止)", diff --git a/src/app/ai/chat/model.ts b/src/app/ai/chat/model.ts index caa5cf28d..3ea771d59 100644 --- a/src/app/ai/chat/model.ts +++ b/src/app/ai/chat/model.ts @@ -7,6 +7,9 @@ import type { LanguageModel } from 'ai' import type { AIProviderID } from '@open-pencil/core/constants' +import { isTauri } from '@/app/tauri/env' +import { tauriFetch } from '@/app/tauri/http' + export type ModelConfig = { providerID: AIProviderID apiKey: string @@ -27,13 +30,19 @@ export function resolveLanguageModelID( return config.modelID } +function desktopFetch(): typeof fetch | undefined { + return isTauri() ? tauriFetch : undefined +} + export function createLanguageModel(config: ModelConfig): LanguageModel { const effectiveModelID = resolveLanguageModelID(config) + const fetch = desktopFetch() switch (config.providerID) { case 'openrouter': { const openrouter = createOpenRouter({ apiKey: config.apiKey, + fetch, headers: { 'X-OpenRouter-Title': 'OpenPencil', 'HTTP-Referer': 'https://github.com/open-pencil/open-pencil' @@ -42,39 +51,42 @@ export function createLanguageModel(config: ModelConfig): LanguageModel { return openrouter(effectiveModelID) } case 'anthropic': { - const anthropic = createAnthropic({ apiKey: config.apiKey }) + const anthropic = createAnthropic({ apiKey: config.apiKey, fetch }) return anthropic(effectiveModelID) } case 'openai': { - const openai = createOpenAI({ apiKey: config.apiKey }) + const openai = createOpenAI({ apiKey: config.apiKey, fetch }) return openai(effectiveModelID) } case 'google': { - const google = createGoogleGenerativeAI({ apiKey: config.apiKey }) + const google = createGoogleGenerativeAI({ apiKey: config.apiKey, fetch }) return google(effectiveModelID) } case 'deepseek': { - const deepseek = createDeepSeek({ apiKey: config.apiKey }) + const deepseek = createDeepSeek({ apiKey: config.apiKey, fetch }) return deepseek(effectiveModelID) } case 'zai': { const zai = createAnthropic({ apiKey: config.apiKey, - baseURL: 'https://api.z.ai/api/anthropic' + baseURL: 'https://api.z.ai/api/anthropic', + fetch }) return zai(effectiveModelID) } case 'minimax': { const minimax = createOpenAI({ apiKey: config.apiKey, - baseURL: 'https://api.minimax.io/v1' + baseURL: 'https://api.minimax.io/v1', + fetch }) return minimax.chat(effectiveModelID) } case 'openai-compatible': { const custom = createOpenAI({ apiKey: config.apiKey, - baseURL: config.customBaseURL + baseURL: config.customBaseURL, + fetch }) return config.customAPIType === 'responses' ? custom.responses(effectiveModelID) @@ -83,7 +95,8 @@ export function createLanguageModel(config: ModelConfig): LanguageModel { case 'anthropic-compatible': { const custom = createAnthropic({ apiKey: config.apiKey, - baseURL: config.customBaseURL + baseURL: config.customBaseURL, + fetch }) return custom(effectiveModelID) } diff --git a/src/app/automation/bridge/figma-factory.ts b/src/app/automation/bridge/figma-factory.ts index c965e1de9..d27212537 100644 --- a/src/app/automation/bridge/figma-factory.ts +++ b/src/app/automation/bridge/figma-factory.ts @@ -1,7 +1,7 @@ import { FigmaAPI } from '@open-pencil/core/figma-api' import type { EditorStore } from '@/app/editor/active-store' -import { listFonts } from '@/app/editor/fonts' +import { listFamilies, listFonts } from '@/app/editor/fonts' export function makeFigmaFromStore(store: EditorStore): FigmaAPI { const api = new FigmaAPI(store.graph) @@ -20,10 +20,15 @@ export function makeFigmaFromStore(store: EditorStore): FigmaAPI { api.exportImage = (nodeIds, opts) => store.renderExportImage(nodeIds, opts.scale ?? 1, opts.format ?? 'PNG') api.listAvailableFontsAsync = async () => { - const fonts = await listFonts() - return fonts.flatMap(({ family, styles }) => + const [systemFonts, familyOptions] = await Promise.all([listFonts(), listFamilies()]) + const fonts = systemFonts.flatMap(({ family, styles }) => styles.map((style) => ({ fontName: { family, style } })) ) + const seenFamilies = new Set(systemFonts.map(({ family }) => family)) + for (const { family } of familyOptions) { + if (!seenFamilies.has(family)) fonts.push({ fontName: { family, style: 'Regular' } }) + } + return fonts } return api } diff --git a/src/app/editor/fonts/index.ts b/src/app/editor/fonts/index.ts index 9fc624141..17bc1ba48 100644 --- a/src/app/editor/fonts/index.ts +++ b/src/app/editor/fonts/index.ts @@ -1,32 +1,71 @@ import { useLocalStorage } from '@vueuse/core' +import { watch } from 'vue' import { + DEFAULT_WEB_FONT_PROVIDER_SETTINGS, + WEB_FONT_PROVIDER_IDS, fontManager, styleToWeight, type FontFamilyOption, - type LocalFontAccessState + type LocalFontAccessState, + type WebFontProviderId } from '@open-pencil/core/text' import type { SceneGraph } from '@open-pencil/scene-graph' +import { dialogMessages } from '@open-pencil/vue' import { clearDownloadedFontCache as clearTauriDownloadedFontCache, createTauriDownloadedFontCache, downloadedFontCacheSummary as tauriDownloadedFontCacheSummary } from '@/app/editor/fonts/cache' +import { toast } from '@/app/shell/ui' import { isTauri } from '@/app/tauri/env' +import { tauriFetch } from '@/app/tauri/http' if (typeof navigator !== 'undefined') { fontManager.setFallbackUserAgent(navigator.userAgent) } -export const googleFontsEnabled = useLocalStorage('op-google-fonts-enabled', true) +export type FontProviderSettings = Record + +export const onlineFontsEnabled = useLocalStorage('op-online-fonts-enabled', true) +export const fontProviderSettings = useLocalStorage( + 'op-font-providers', + DEFAULT_WEB_FONT_PROVIDER_SETTINGS +) + +watch( + [onlineFontsEnabled, fontProviderSettings], + () => { + fontManager.setOnlineFontProviders( + onlineFontsEnabled.value + ? Object.fromEntries( + WEB_FONT_PROVIDER_IDS.map((provider) => [ + provider, + fontProviderSettings.value[provider] + ]) + ) + : {} + ) + }, + { deep: true, immediate: true } +) let tauriFontCacheConfigured = false +let webFontUnavailableToastShown = false + +function showWebFontUnavailableToast(): void { + if (webFontUnavailableToastShown || isTauri() || !onlineFontsEnabled.value) return + if (!WEB_FONT_PROVIDER_IDS.some((provider) => fontProviderSettings.value[provider])) return + webFontUnavailableToastShown = true + toast.warning(dialogMessages.get().webFontProvidersRequireDesktopApp) +} function configureTauriFontCache() { if (tauriFontCacheConfigured || !isTauri()) return tauriFontCacheConfigured = true fontManager.setDownloadedFontCache(createTauriDownloadedFontCache()) + fontManager.setWebFontFetch(tauriFetch) } configureTauriFontCache() @@ -59,7 +98,7 @@ export function preloadFonts(): void { void getTauriFonts().then(registerFontFaces) return } - if (googleFontsEnabled.value) fontManager.preloadGoogleFamilies() + if (onlineFontsEnabled.value) fontManager.preloadWebFontFamilies() } export function localFontAccessState(): LocalFontAccessState { @@ -99,11 +138,17 @@ function registerFontFaces(fonts: TauriFontFamily[]): void { export async function listFamilies(): Promise { configureTauriFontCache() if (isTauri()) { - const fonts = await getTauriFonts() - return fonts.map((f) => ({ family: f.family, source: 'local' })) + const [systemFonts, webFonts] = await Promise.all([ + getTauriFonts(), + fontManager.listFamilyOptions() + ]) + const byFamily = new Map(webFonts.map((font) => [font.family, font])) + for (const font of systemFonts) + byFamily.set(font.family, { family: font.family, source: 'local' }) + return [...byFamily.values()].sort((a, b) => a.family.localeCompare(b.family)) } - const fonts = await fontManager.listFamilyOptions() - return googleFontsEnabled.value ? fonts : fonts.filter((font) => font.source !== 'google') + showWebFontUnavailableToast() + return fontManager.listFamilyOptions() } export async function listFonts(): Promise { @@ -156,5 +201,7 @@ export async function loadFont(family: string, style = 'Regular'): Promise ({ name, value })) +} + +async function bodyToBytes(body: BodyInit | null | undefined): Promise { + if (body == null) return undefined + if (typeof body === 'string') return [...new TextEncoder().encode(body)] + if (body instanceof ArrayBuffer) return [...new Uint8Array(body)] + if (ArrayBuffer.isView(body)) + return [...new Uint8Array(body.buffer, body.byteOffset, body.byteLength)] + if (body instanceof Blob) return [...new Uint8Array(await body.arrayBuffer())] + if (body instanceof URLSearchParams) return [...new TextEncoder().encode(body.toString())] + if (body instanceof FormData) { + return [...new Uint8Array(await new Response(body).arrayBuffer())] + } + throw new TypeError('Streaming request bodies are not supported by the desktop HTTP bridge yet') +} + +export async function tauriFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const request = new Request(input, init) + const { invoke } = await import('@tauri-apps/api/core') + const payload: ProxyHttpRequest = { + url: request.url, + method: request.method, + headers: headersToProxyHeaders(request.headers), + body: await bodyToBytes(init?.body) + } + const response = await invoke('proxy_http_request', { request: payload }) + return new Response(new Uint8Array(response.body), { + status: response.status, + headers: response.headers.map(({ name, value }): [string, string] => [name, value]) + }) +} diff --git a/src/components/FontPicker.vue b/src/components/FontPicker.vue index 22d51b98c..99687eaeb 100644 --- a/src/components/FontPicker.vue +++ b/src/components/FontPicker.vue @@ -11,6 +11,8 @@ import { requestLocalFontAccess } from '@/app/editor/fonts' +import { WEB_FONT_PROVIDER_IDS } from '@open-pencil/core/text' + import type { FontPickerUi } from '@open-pencil/vue' const modelValue = defineModel({ required: true }) @@ -42,7 +44,7 @@ const localFontAccess = { } function loadPreviewFont(family: string, source: string) { - if (source !== 'google') return + if (!WEB_FONT_PROVIDER_IDS.includes(source as (typeof WEB_FONT_PROVIDER_IDS)[number])) return if (previewFontLoads.has(family)) return previewFontLoads.add(family) void loadFont(family) diff --git a/src/components/FontSettings/FontSettingsPopover.vue b/src/components/FontSettings/FontSettingsPopover.vue index 643467d99..19fa64570 100644 --- a/src/components/FontSettings/FontSettingsPopover.vue +++ b/src/components/FontSettings/FontSettingsPopover.vue @@ -4,6 +4,8 @@ import { onMounted, ref } from 'vue' import { isTauri } from '@/app/tauri/env' import { useFontSettings } from '@/components/FontSettings/use' +import { WEB_FONT_PROVIDER_IDS, WEB_FONT_PROVIDER_LABELS } from '@open-pencil/core/text' +import type { WebFontProviderId } from '@open-pencil/core/text' import { useI18n } from '@open-pencil/vue' import Tip from '@/components/ui/Tip.vue' import { useButtonUI } from '@/components/ui/button' @@ -29,6 +31,7 @@ const primaryButton = useButtonUI({ ui: { base: 'w-full px-2 py-1.5 text-[10px] font-medium disabled:opacity-50' } }) const showDownloadedFonts = isTauri() +const webFontProviderIds = WEB_FONT_PROVIDER_IDS const popoverOpen = ref(false) const { @@ -40,12 +43,14 @@ const { cacheUpdatedLabel, canRequestLocalFonts, status, - googleFontsEnabled, + onlineFontsEnabled, + fontProviderSettings, clearCache, downloadFallbacks, refreshSummary, requestAccess, - setGoogleFontsEnabled + setFontProviderEnabled, + setOnlineFontsEnabled } = useFontSettings() function setPopoverOpen(value: boolean) { @@ -53,6 +58,16 @@ function setPopoverOpen(value: boolean) { if (value) void refreshSummary() } +function isProviderEnabled(provider: WebFontProviderId) { + return fontProviderSettings.value[provider] +} + +function onProviderToggle(provider: WebFontProviderId, event: Event) { + const input = event.target + if (!(input instanceof HTMLInputElement)) return + setFontProviderEnabled(provider, input.checked) +} + onMounted(() => { void refreshSummary() }) @@ -88,8 +103,8 @@ onMounted(() => {

{{ showDownloadedFonts - ? 'Access system fonts, Google Fonts, fallback packs, and cached downloads.' - : 'Allow browser access to local fonts and manage Google Fonts.' + ? dialogs.fontSettingsDesktopDescription + : dialogs.fontSettingsBrowserDescription }}

@@ -97,19 +112,21 @@ onMounted(() => {
- Local fonts + {{ dialogs.localFonts }} {{ accessStateLabel }}
- Google Fonts - {{ googleFontsEnabled ? 'Enabled' : 'Disabled' }} + {{ dialogs.onlineFonts }} + {{ + onlineFontsEnabled ? dialogs.enabled : dialogs.disabled + }}
- Downloaded cache + {{ dialogs.downloadedCache }} {{ cacheCount }} fonts · {{ cacheSize }}
- Last updated + {{ dialogs.lastUpdated }} {{ cacheUpdatedLabel }}
@@ -117,12 +134,12 @@ onMounted(() => {
-

System font access

+

{{ dialogs.systemFontAccess }}

{{ accessState === 'granted' - ? 'System fonts are available.' - : 'Allow browser font access when system fonts are missing.' + ? dialogs.systemFontsAvailable + : dialogs.allowBrowserFontAccess }}

@@ -133,26 +150,48 @@ onMounted(() => { :disabled="busyAction !== null || !canRequestLocalFonts" @click="requestAccess" > - {{ busyAction === 'access' ? 'Requesting…' : 'Allow' }} + {{ busyAction === 'access' ? dialogs.requesting : dialogs.allow }}
-
-
-

Google Fonts

-

- Show fonts from Google in the font picker. -

+
+
+
+

+ {{ dialogs.onlineFontProviders }} +

+

+ {{ dialogs.downloadMissingWebFonts }} +

+
+ +
+ +
+
-
{ class="grid grid-cols-[1fr_auto] gap-2 rounded border border-border p-2" >
-

Fallback packs

+

{{ dialogs.fallbackPacks }}

- Download CJK and Arabic fallbacks before opening files that need them. + {{ dialogs.downloadFallbackPacksDescription }}

@@ -185,7 +224,7 @@ onMounted(() => { :disabled="busyAction !== null" @click="refreshSummary" > - Refresh + {{ dialogs.refresh }}
diff --git a/src/components/FontSettings/use.ts b/src/components/FontSettings/use.ts index bfd604cdc..09f6b0a74 100644 --- a/src/components/FontSettings/use.ts +++ b/src/components/FontSettings/use.ts @@ -1,14 +1,21 @@ import { computed, ref } from 'vue' -import type { FontFamilyOption, LocalFontAccessState } from '@open-pencil/core/text' +import type { + FontFamilyOption, + LocalFontAccessState, + WebFontProviderId +} from '@open-pencil/core/text' +import { useI18n } from '@open-pencil/vue' import { clearDownloadedFontCache, downloadedFontCacheSummary, - googleFontsEnabled, + fontProviderSettings, localFontAccessState, + onlineFontsEnabled, predownloadFallbackFonts, - requestLocalFontAccess + requestLocalFontAccess, + type FontProviderSettings } from '@/app/editor/fonts' import type { DownloadedFontCacheSummary } from '@/app/editor/fonts/cache' @@ -20,7 +27,8 @@ export interface FontSettingsActions { localFontAccessState: () => LocalFontAccessState predownloadFallbackFonts: () => Promise requestLocalFontAccess: () => Promise - googleFontsEnabled: { value: boolean } + onlineFontsEnabled: { value: boolean } + fontProviderSettings: { value: FontProviderSettings } } export type FontSettingsBusyAction = 'access' | 'download' | 'clear' | 'refresh' @@ -31,23 +39,26 @@ const defaultActions: FontSettingsActions = { localFontAccessState, predownloadFallbackFonts, requestLocalFontAccess, - googleFontsEnabled + onlineFontsEnabled, + fontProviderSettings } export function useFontSettings(actions: FontSettingsActions = defaultActions) { + const { dialogs } = useI18n() const cacheCount = ref(0) const cacheByteLength = ref(0) const cacheUpdatedAt = ref(null) const accessState = ref(actions.localFontAccessState()) const busyAction = ref(null) const status = ref('') - const googleFontsEnabled = actions.googleFontsEnabled + const onlineFontsEnabled = actions.onlineFontsEnabled + const fontProviderSettings = actions.fontProviderSettings const accessStateLabel = computed(() => { - if (accessState.value === 'granted') return 'Enabled' - if (accessState.value === 'denied') return 'Denied' - if (accessState.value === 'unsupported') return 'Unavailable' - return 'Not requested' + if (accessState.value === 'granted') return dialogs.value.enabled + if (accessState.value === 'denied') return dialogs.value.denied + if (accessState.value === 'unsupported') return dialogs.value.unavailable + return dialogs.value.notRequested }) const cacheSize = computed(() => { @@ -56,7 +67,7 @@ export function useFontSettings(actions: FontSettingsActions = defaultActions) { }) const cacheUpdatedLabel = computed(() => { - if (cacheUpdatedAt.value === null) return 'Never' + if (cacheUpdatedAt.value === null) return dialogs.value.never return new Date(cacheUpdatedAt.value).toLocaleDateString() }) @@ -83,18 +94,27 @@ export function useFontSettings(actions: FontSettingsActions = defaultActions) { try { await actions.requestLocalFontAccess() accessState.value = actions.localFontAccessState() - status.value = 'Local font access enabled.' + status.value = dialogs.value.localFontAccessEnabled } catch { accessState.value = actions.localFontAccessState() - status.value = 'Local font access was not granted.' + status.value = dialogs.value.localFontAccessNotGranted } finally { busyAction.value = null } } - function setGoogleFontsEnabled(enabled: boolean) { - googleFontsEnabled.value = enabled - status.value = enabled ? 'Google Fonts enabled.' : 'Google Fonts disabled.' + function setOnlineFontsEnabled(enabled: boolean) { + onlineFontsEnabled.value = enabled + status.value = enabled + ? dialogs.value.onlineFontProvidersEnabled + : dialogs.value.onlineFontProvidersDisabled + } + + function setFontProviderEnabled(provider: WebFontProviderId, enabled: boolean) { + fontProviderSettings.value = { ...fontProviderSettings.value, [provider]: enabled } + status.value = enabled + ? dialogs.value.fontProviderEnabled({ provider }) + : dialogs.value.fontProviderDisabled({ provider }) } async function downloadFallbacks() { @@ -103,9 +123,9 @@ export function useFontSettings(actions: FontSettingsActions = defaultActions) { try { await actions.predownloadFallbackFonts() await refreshSummary() - status.value = 'Fallback fonts downloaded.' + status.value = dialogs.value.fallbackFontsDownloaded } catch { - status.value = 'Could not download fallback fonts.' + status.value = dialogs.value.fallbackFontsDownloadFailed } finally { busyAction.value = null } @@ -117,9 +137,9 @@ export function useFontSettings(actions: FontSettingsActions = defaultActions) { try { await actions.clearDownloadedFontCache() await refreshSummary() - status.value = 'Downloaded font cache cleared.' + status.value = dialogs.value.downloadedFontCacheCleared } catch { - status.value = 'Could not clear downloaded font cache.' + status.value = dialogs.value.downloadedFontCacheClearFailed } finally { busyAction.value = null } @@ -134,11 +154,13 @@ export function useFontSettings(actions: FontSettingsActions = defaultActions) { cacheSize, cacheUpdatedLabel, status, - googleFontsEnabled, + onlineFontsEnabled, + fontProviderSettings, clearCache, downloadFallbacks, refreshSummary, requestAccess, - setGoogleFontsEnabled + setOnlineFontsEnabled, + setFontProviderEnabled } } diff --git a/tests/e2e/fonts/settings.spec.ts b/tests/e2e/fonts/settings.spec.ts index 14fe6b0a5..e6ea54b40 100644 --- a/tests/e2e/fonts/settings.spec.ts +++ b/tests/e2e/fonts/settings.spec.ts @@ -26,11 +26,16 @@ test('font settings popover exposes web font access without desktop-only cache a await expect(page.getByText('Allow browser access to local fonts')).toBeVisible() await expect(page.getByTestId('font-settings-request-access')).toBeVisible() - await expect(page.getByTestId('font-settings-toggle-google-fonts')).toHaveText('Disable') - await page.getByTestId('font-settings-toggle-google-fonts').click() - await expect(page.getByTestId('font-settings-toggle-google-fonts')).toHaveText('Enable') - await page.getByTestId('font-settings-toggle-google-fonts').click() - await expect(page.getByTestId('font-settings-toggle-google-fonts')).toHaveText('Disable') + await expect(page.getByTestId('font-settings-toggle-online-fonts')).toHaveText('Disable') + await expect(page.getByTestId('font-settings-provider-google')).toBeChecked() + await expect(page.getByTestId('font-settings-provider-fontsource')).toBeChecked() + await expect(page.getByTestId('font-settings-provider-bunny')).not.toBeChecked() + await expect(page.getByTestId('font-settings-provider-fontshare')).not.toBeChecked() + await page.getByTestId('font-settings-toggle-online-fonts').click() + await expect(page.getByTestId('font-settings-toggle-online-fonts')).toHaveText('Enable') + await expect(page.getByTestId('font-settings-provider-google')).toBeDisabled() + await page.getByTestId('font-settings-toggle-online-fonts').click() + await expect(page.getByTestId('font-settings-toggle-online-fonts')).toHaveText('Disable') await expect(page.getByTestId('font-settings-download-fallbacks')).toHaveCount(0) await expect(page.getByTestId('font-settings-refresh-cache')).toHaveCount(0) await expect(page.getByTestId('font-settings-clear-cache')).toHaveCount(0)