pake-cli 3.11.7 → 3.11.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,14 @@
1
1
  use crate::app::config::PakeConfig;
2
- use crate::util::get_data_dir;
2
+ use crate::util::{
3
+ check_file_or_append, get_data_dir, get_download_message_with_lang, show_toast, MessageType,
4
+ };
3
5
  use std::{
4
6
  path::PathBuf,
5
7
  str::FromStr,
6
8
  sync::atomic::{AtomicU32, Ordering},
7
9
  };
8
10
  use tauri::{
9
- webview::{NewWindowFeatures, NewWindowResponse},
11
+ webview::{DownloadEvent, NewWindowFeatures, NewWindowResponse},
10
12
  AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
11
13
  };
12
14
 
@@ -426,6 +428,61 @@ fn build_window(
426
428
  }
427
429
  }
428
430
 
431
+ // Capture webview-initiated downloads (blob:, data:, Content-Disposition,
432
+ // etc.) and write them to the OS Downloads folder. This is essential for
433
+ // sites with a strict Content-Security-Policy (e.g. Gemini): their
434
+ // `connect-src` blocks Tauri's IPC origin, so downloads cannot be routed
435
+ // through the JS bridge, and downloads triggered from a sandboxed iframe
436
+ // can't reach the IPC either. Letting the browser download natively and
437
+ // catching it here is independent of the page CSP and the IPC channel.
438
+ {
439
+ let download_handle = app.clone();
440
+ window_builder = window_builder.on_download(move |_webview, event| match event {
441
+ DownloadEvent::Requested { url, destination } => {
442
+ match download_handle.path().download_dir() {
443
+ Ok(download_dir) => {
444
+ let filename = destination
445
+ .file_name()
446
+ .map(|name| name.to_string_lossy().to_string())
447
+ .filter(|name| !name.is_empty())
448
+ .or_else(|| {
449
+ url.path_segments()
450
+ .and_then(|mut segments| segments.next_back())
451
+ .map(|segment| segment.to_string())
452
+ .filter(|segment| !segment.is_empty())
453
+ })
454
+ .unwrap_or_else(|| "download".to_string());
455
+
456
+ let target = download_dir.join(filename);
457
+ if let Some(path_str) = target.to_str() {
458
+ *destination = PathBuf::from(check_file_or_append(path_str));
459
+ }
460
+ }
461
+ Err(error) => {
462
+ eprintln!("[Pake] Failed to resolve download dir: {error}");
463
+ }
464
+ }
465
+ true
466
+ }
467
+ DownloadEvent::Finished {
468
+ url: _,
469
+ path: _,
470
+ success,
471
+ } => {
472
+ if let Some(window) = download_handle.get_webview_window("pake") {
473
+ let message_type = if success {
474
+ MessageType::Success
475
+ } else {
476
+ MessageType::Failure
477
+ };
478
+ show_toast(&window, &get_download_message_with_lang(message_type, None));
479
+ }
480
+ true
481
+ }
482
+ _ => true,
483
+ });
484
+ }
485
+
429
486
  window_builder = window_builder.on_navigation(|_| true);
430
487
 
431
488
  window_builder.build()
@@ -340,118 +340,19 @@ document.addEventListener("DOMContentLoaded", () => {
340
340
  true,
341
341
  );
342
342
 
343
- // Collect blob urls to blob by overriding window.URL.createObjectURL
344
- function collectUrlToBlobs() {
345
- const backupCreateObjectURL = window.URL.createObjectURL;
346
- window.blobToUrlCaches = new Map();
347
- window.URL.createObjectURL = (blob) => {
348
- const url = backupCreateObjectURL.call(window.URL, blob);
349
- window.blobToUrlCaches.set(url, blob);
350
- return url;
351
- };
352
- }
353
-
354
- function convertBlobUrlToBinary(blobUrl) {
355
- return new Promise((resolve, reject) => {
356
- const blob = window.blobToUrlCaches.get(blobUrl);
357
- if (!blob) {
358
- fetch(blobUrl)
359
- .then((res) => res.arrayBuffer())
360
- .then((buffer) => resolve(Array.from(new Uint8Array(buffer))))
361
- .catch(reject);
362
- return;
363
- }
364
- const reader = new FileReader();
365
- reader.readAsArrayBuffer(blob);
366
- reader.onload = () => {
367
- resolve(Array.from(new Uint8Array(reader.result)));
368
- };
369
- reader.onerror = () => reject(reader.error);
370
- });
371
- }
372
-
373
- function downloadFromDataUri(dataURI, filename) {
374
- try {
375
- const byteString = atob(dataURI.split(",")[1]);
376
- // write the bytes of the string to an ArrayBuffer
377
- const bufferArray = new ArrayBuffer(byteString.length);
378
-
379
- // create a view into the buffer
380
- const binary = new Uint8Array(bufferArray);
381
-
382
- // set the bytes of the buffer to the correct values
383
- for (let i = 0; i < byteString.length; i++) {
384
- binary[i] = byteString.charCodeAt(i);
385
- }
386
-
387
- // write the ArrayBuffer to a binary, and you're done
388
- const userLanguage = getUserLanguage();
389
- invoke("download_file_by_binary", {
390
- params: {
391
- filename,
392
- binary: Array.from(binary),
393
- language: userLanguage,
394
- },
395
- }).catch((error) => {
396
- console.error("Failed to download data URI file:", filename, error);
397
- showDownloadError(filename);
398
- });
399
- } catch (error) {
400
- console.error("Failed to process data URI:", dataURI, error);
401
- showDownloadError(filename || "file");
402
- }
403
- }
404
-
405
- function downloadFromBlobUrl(blobUrl, filename) {
406
- convertBlobUrlToBinary(blobUrl)
407
- .then((binary) => {
408
- const userLanguage = getUserLanguage();
409
- invoke("download_file_by_binary", {
410
- params: {
411
- filename,
412
- binary,
413
- language: userLanguage,
414
- },
415
- }).catch((error) => {
416
- console.error("Failed to download blob file:", filename, error);
417
- showDownloadError(filename);
418
- });
419
- })
420
- .catch((error) => {
421
- console.error("Failed to convert blob to binary:", blobUrl, error);
422
- showDownloadError(filename);
423
- });
424
- }
425
-
426
- // detect blob download by createElement("a")
427
- function detectDownloadByCreateAnchor() {
428
- const createEle = document.createElement;
429
- document.createElement = (el) => {
430
- if (el !== "a") return createEle.call(document, el);
431
- const anchorEle = createEle.call(document, el);
432
-
433
- // use addEventListener to avoid overriding the original click event.
434
- anchorEle.addEventListener(
435
- "click",
436
- (e) => {
437
- const url = anchorEle.href;
438
- const filename = anchorEle.download || getFilenameFromUrl(url);
439
- if (window.blobToUrlCaches.has(url)) {
440
- e.preventDefault();
441
- e.stopImmediatePropagation();
442
- downloadFromBlobUrl(url, filename);
443
- // case: download from dataURL -> convert dataURL ->
444
- } else if (url.startsWith("data:")) {
445
- e.preventDefault();
446
- e.stopImmediatePropagation();
447
- downloadFromDataUri(url, filename);
448
- }
449
- },
450
- true,
451
- );
452
-
453
- return anchorEle;
454
- };
343
+ // Trigger a native browser download via a transient anchor click. The Rust
344
+ // on_download handler then writes the file to the Downloads folder. This is
345
+ // used for blob:/data: URLs because routing their bytes through the Tauri
346
+ // IPC fails on strict-CSP sites (e.g. Gemini), whose connect-src blocks the
347
+ // IPC origin. The native download path is independent of the page CSP.
348
+ function triggerNativeDownload(url, filename) {
349
+ const anchor = document.createElement("a");
350
+ anchor.href = url;
351
+ anchor.download = filename || "";
352
+ anchor.style.display = "none";
353
+ document.body.appendChild(anchor);
354
+ anchor.click();
355
+ document.body.removeChild(anchor);
455
356
  }
456
357
 
457
358
  // process special download protocol['data:','blob:']
@@ -587,11 +488,16 @@ document.addEventListener("DOMContentLoaded", () => {
587
488
  return;
588
489
  }
589
490
 
590
- // Process download links for Rust to handle.
591
- if (
592
- isDownloadRequired(absoluteUrl, anchorElement, e) &&
593
- !isSpecialDownload(absoluteUrl)
594
- ) {
491
+ // Process download links.
492
+ if (isDownloadRequired(absoluteUrl, anchorElement, e)) {
493
+ // Let the browser download blob:/data: URLs natively; the Rust
494
+ // on_download handler saves them to the Downloads folder. Routing them
495
+ // through the IPC fails on strict-CSP sites (e.g. Gemini), whose
496
+ // connect-src blocks the IPC origin, and on downloads triggered from a
497
+ // sandboxed iframe where the IPC can't be reached.
498
+ if (isSpecialDownload(absoluteUrl)) {
499
+ return;
500
+ }
595
501
  e.preventDefault();
596
502
  e.stopImmediatePropagation();
597
503
  const userLanguage = getUserLanguage();
@@ -625,9 +531,6 @@ document.addEventListener("DOMContentLoaded", () => {
625
531
  // Prevent some special websites from executing in advance, before the click event is triggered.
626
532
  document.addEventListener("click", detectAnchorElementClick, true);
627
533
 
628
- collectUrlToBlobs();
629
- detectDownloadByCreateAnchor();
630
-
631
534
  // Rewrite the window.open function.
632
535
  const originalWindowOpen = window.open;
633
536
  window.open = function (url, name, specs) {
@@ -863,12 +766,10 @@ document.addEventListener("DOMContentLoaded", () => {
863
766
  const filename = getFilenameFromUrl(imageUrl) || "image";
864
767
 
865
768
  // Handle different URL types
866
- if (imageUrl.startsWith("data:")) {
867
- downloadFromDataUri(imageUrl, filename);
868
- } else if (imageUrl.startsWith("blob:")) {
869
- if (window.blobToUrlCaches && window.blobToUrlCaches.has(imageUrl)) {
870
- downloadFromBlobUrl(imageUrl, filename);
871
- }
769
+ if (isSpecialDownload(imageUrl)) {
770
+ // Download blob:/data: natively so it works under strict CSP; the Rust
771
+ // on_download handler saves it to the Downloads folder.
772
+ triggerNativeDownload(imageUrl, filename);
872
773
  } else {
873
774
  // Regular HTTP(S) image
874
775
  const userLanguage = getUserLanguage();
@@ -13,9 +13,8 @@ const WINDOW_SHOW_DELAY: u64 = 50;
13
13
 
14
14
  use app::{
15
15
  invoke::{
16
- clear_cache_and_restart, clear_dock_badge, download_file, download_file_by_binary,
17
- increment_dock_badge, send_notification, set_dock_badge, set_dock_badge_label,
18
- update_theme_mode,
16
+ clear_cache_and_restart, clear_dock_badge, download_file, increment_dock_badge,
17
+ send_notification, set_dock_badge, set_dock_badge_label, update_theme_mode,
19
18
  },
20
19
  setup::{set_global_shortcut, set_system_tray},
21
20
  window::{open_additional_window_safe, set_window, MultiWindowState},
@@ -43,7 +42,7 @@ pub fn run_app() {
43
42
  let start_to_tray = pake_config.windows[0].start_to_tray && show_system_tray; // Only valid when tray is enabled
44
43
  let multi_instance = pake_config.multi_instance;
45
44
  let multi_window = pake_config.multi_window;
46
- let enable_find = pake_config.windows[0].enable_find;
45
+ let _enable_find = pake_config.windows[0].enable_find;
47
46
 
48
47
  let window_state_plugin = WindowStatePlugin::default()
49
48
  .with_state_flags(if init_fullscreen {
@@ -81,7 +80,6 @@ pub fn run_app() {
81
80
  app_builder
82
81
  .invoke_handler(tauri::generate_handler![
83
82
  download_file,
84
- download_file_by_binary,
85
83
  send_notification,
86
84
  increment_dock_badge,
87
85
  set_dock_badge,
@@ -99,7 +97,7 @@ pub fn run_app() {
99
97
  // --- Menu Construction Start ---
100
98
  #[cfg(target_os = "macos")]
101
99
  {
102
- app::menu::set_app_menu(app.app_handle(), multi_window, enable_find)?;
100
+ app::menu::set_app_menu(app.app_handle(), multi_window, _enable_find)?;
103
101
 
104
102
  // Event Handling for Custom Menu Item
105
103
  app.on_menu_event(move |app_handle, event| {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.11.7",
4
+ "version": "3.11.9",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {