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.
- package/LICENSE +674 -21
- package/README.md +4 -0
- package/dist/cli.js +211 -110
- package/package.json +3 -2
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +2 -2
- package/src-tauri/src/app/invoke.rs +1 -49
- package/src-tauri/src/app/window.rs +59 -2
- package/src-tauri/src/inject/event.js +27 -126
- package/src-tauri/src/lib.rs +4 -6
- package/src-tauri/tauri.conf.json +1 -1
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
use crate::app::config::PakeConfig;
|
|
2
|
-
use crate::util::
|
|
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
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
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();
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -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,
|
|
17
|
-
|
|
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
|
|
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,
|
|
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| {
|