pake-cli 3.15.4 โ 3.15.5
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/dist/cli.js +1 -1
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/app/invoke.rs +11 -1
- package/src-tauri/src/app/setup.rs +12 -1
- package/src-tauri/src/inject/event.js +6 -7
- package/src-tauri/src/lib.rs +150 -24
- package/src-tauri/tauri.conf.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as psl from 'psl';
|
|
|
20
20
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.15.
|
|
23
|
+
var version = "3.15.5";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -114,8 +114,18 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result
|
|
|
114
114
|
|
|
115
115
|
match response {
|
|
116
116
|
Ok(mut res) => {
|
|
117
|
+
// Transport success is not download success: 403/404 HTML error pages
|
|
118
|
+
// must not be written as files or toasted as successful downloads.
|
|
119
|
+
if !res.status().is_success() {
|
|
120
|
+
show_toast(
|
|
121
|
+
&window,
|
|
122
|
+
&get_download_message_with_lang(MessageType::Failure, params.language),
|
|
123
|
+
);
|
|
124
|
+
return Err(format!("Download failed with HTTP status {}", res.status()));
|
|
125
|
+
}
|
|
126
|
+
|
|
117
127
|
let mut file =
|
|
118
|
-
File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
|
128
|
+
File::create(&file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
|
119
129
|
|
|
120
130
|
while let Some(chunk) = res
|
|
121
131
|
.chunk()
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use crate::app::window::{open_additional_window_safe, reapply_window_icon};
|
|
2
|
+
use crate::cancel_startup_reveal;
|
|
2
3
|
use std::str::FromStr;
|
|
3
|
-
use std::sync::{Arc, Mutex};
|
|
4
|
+
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
|
4
5
|
use std::time::{Duration, Instant};
|
|
5
6
|
use tauri::{
|
|
6
7
|
menu::{MenuBuilder, MenuItemBuilder},
|
|
@@ -16,6 +17,7 @@ pub fn set_system_tray(
|
|
|
16
17
|
tray_icon_path: &str,
|
|
17
18
|
_init_fullscreen: bool,
|
|
18
19
|
allow_multi_window: bool,
|
|
20
|
+
startup_revealed: Arc<AtomicBool>,
|
|
19
21
|
) -> tauri::Result<()> {
|
|
20
22
|
if !show_system_tray {
|
|
21
23
|
app.remove_tray_by_id("pake-tray");
|
|
@@ -42,6 +44,8 @@ pub fn set_system_tray(
|
|
|
42
44
|
|
|
43
45
|
app.app_handle().remove_tray_by_id("pake-tray");
|
|
44
46
|
|
|
47
|
+
let menu_revealed = startup_revealed.clone();
|
|
48
|
+
let click_revealed = startup_revealed;
|
|
45
49
|
let mut tray_builder = TrayIconBuilder::new()
|
|
46
50
|
.menu(&menu)
|
|
47
51
|
.on_menu_event(move |app, event| match event.id().as_ref() {
|
|
@@ -50,11 +54,13 @@ pub fn set_system_tray(
|
|
|
50
54
|
}
|
|
51
55
|
"hide_app" => {
|
|
52
56
|
if let Some(window) = app.get_webview_window("pake") {
|
|
57
|
+
cancel_startup_reveal(&menu_revealed);
|
|
53
58
|
let _ = window.minimize();
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
"show_app" => {
|
|
57
62
|
if let Some(window) = app.get_webview_window("pake") {
|
|
63
|
+
cancel_startup_reveal(&menu_revealed);
|
|
58
64
|
let _ = window.show();
|
|
59
65
|
reapply_window_icon(&window);
|
|
60
66
|
#[cfg(target_os = "linux")]
|
|
@@ -79,6 +85,8 @@ pub fn set_system_tray(
|
|
|
79
85
|
if let TrayIconEvent::Click { button, .. } = event {
|
|
80
86
|
if button == tauri::tray::MouseButton::Left {
|
|
81
87
|
if let Some(window) = tray.app_handle().get_webview_window("pake") {
|
|
88
|
+
// Any tray toggle claims visibility control from startup reveal.
|
|
89
|
+
cancel_startup_reveal(&click_revealed);
|
|
82
90
|
let is_visible = window.is_visible().unwrap_or(false);
|
|
83
91
|
if is_visible {
|
|
84
92
|
let _ = window.hide();
|
|
@@ -120,6 +128,7 @@ pub fn set_global_shortcut(
|
|
|
120
128
|
app: &AppHandle,
|
|
121
129
|
shortcut: String,
|
|
122
130
|
_init_fullscreen: bool,
|
|
131
|
+
startup_revealed: Arc<AtomicBool>,
|
|
123
132
|
) -> tauri::Result<()> {
|
|
124
133
|
if shortcut.is_empty() {
|
|
125
134
|
return Ok(());
|
|
@@ -139,6 +148,7 @@ pub fn set_global_shortcut(
|
|
|
139
148
|
tauri_plugin_global_shortcut::Builder::new()
|
|
140
149
|
.with_handler({
|
|
141
150
|
let last_triggered = Arc::clone(&last_triggered);
|
|
151
|
+
let startup_revealed = startup_revealed.clone();
|
|
142
152
|
move |app, event, _shortcut| {
|
|
143
153
|
let Ok(mut last_triggered) = last_triggered.lock() else {
|
|
144
154
|
return;
|
|
@@ -150,6 +160,7 @@ pub fn set_global_shortcut(
|
|
|
150
160
|
|
|
151
161
|
if shortcut_hotkey.eq(event) {
|
|
152
162
|
if let Some(window) = app.get_webview_window("pake") {
|
|
163
|
+
cancel_startup_reveal(&startup_revealed);
|
|
153
164
|
let is_visible = window.is_visible().unwrap_or(false);
|
|
154
165
|
if is_visible {
|
|
155
166
|
let _ = window.hide();
|
|
@@ -446,13 +446,12 @@ const PREVIEWABLE_MEDIA_EXTENSIONS = [
|
|
|
446
446
|
"m4a",
|
|
447
447
|
];
|
|
448
448
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
];
|
|
449
|
+
// Path fragments that often host real file downloads. Do not add broad static
|
|
450
|
+
// asset roots such as "/assets/" or "/dist/": many SPAs use those as in-app
|
|
451
|
+
// routes (e.g. MEXC /assets/future) and would be intercepted as downloads.
|
|
452
|
+
// Extensionless links under these paths still match; prefer real extensions,
|
|
453
|
+
// download attributes, or ?download / ?attachment query hints when possible.
|
|
454
|
+
const DOWNLOAD_PATH_PATTERNS = ["/download/", "/files/", "/attachments/"];
|
|
456
455
|
|
|
457
456
|
// Language detection utilities
|
|
458
457
|
function getUserLanguage() {
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -2,14 +2,20 @@
|
|
|
2
2
|
mod app;
|
|
3
3
|
mod util;
|
|
4
4
|
|
|
5
|
-
use
|
|
5
|
+
use std::sync::{
|
|
6
|
+
atomic::{AtomicBool, Ordering},
|
|
7
|
+
Arc,
|
|
8
|
+
};
|
|
9
|
+
use tauri::{webview::PageLoadEvent, Manager, Url, WebviewWindow};
|
|
6
10
|
use tauri_plugin_window_state::Builder as WindowStatePlugin;
|
|
7
11
|
use tauri_plugin_window_state::StateFlags;
|
|
8
12
|
|
|
9
13
|
#[cfg(target_os = "macos")]
|
|
10
14
|
use std::time::Duration;
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
// Fallback when PageLoadEvent::Finished never arrives (offline / stalled).
|
|
17
|
+
// Deliberately longer than a paint tick so the normal path can win first.
|
|
18
|
+
const STARTUP_WINDOW_FALLBACK_DELAY: u64 = 3_000;
|
|
13
19
|
#[cfg(target_os = "linux")]
|
|
14
20
|
const PAKE_LINUX_WEBKIT_SAFE_MODE: &str = "PAKE_LINUX_WEBKIT_SAFE_MODE";
|
|
15
21
|
#[cfg(target_os = "linux")]
|
|
@@ -29,6 +35,55 @@ use app::{
|
|
|
29
35
|
};
|
|
30
36
|
use util::get_pake_config;
|
|
31
37
|
|
|
38
|
+
/// Placeholder documents used before the real target URL navigates (e.g. macOS
|
|
39
|
+
/// cert-bypass starts on about:blank). Revealing on these would reintroduce the
|
|
40
|
+
/// blank-window flash the page-load gate is meant to prevent.
|
|
41
|
+
fn is_placeholder_startup_url(url: &Url) -> bool {
|
|
42
|
+
url.scheme().eq_ignore_ascii_case("about")
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// First automatic reveal wins. Returns true if the caller should show the window.
|
|
46
|
+
fn claim_startup_reveal(revealed: &AtomicBool) -> bool {
|
|
47
|
+
!revealed.swap(true, Ordering::AcqRel)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// User took control of main-window visibility (tray, shortcut, dock, second
|
|
51
|
+
/// instance, hide-on-close). Drop any pending page-load / fallback reveal so a
|
|
52
|
+
/// slow cold start cannot re-open a window the user just hid.
|
|
53
|
+
pub(crate) fn cancel_startup_reveal(revealed: &AtomicBool) {
|
|
54
|
+
revealed.store(true, Ordering::Release);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn reveal_startup_window(window: WebviewWindow, init_fullscreen: bool, revealed: &Arc<AtomicBool>) {
|
|
58
|
+
if !claim_startup_reveal(revealed) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
tauri::async_runtime::spawn(async move {
|
|
63
|
+
let _ = window.show();
|
|
64
|
+
reapply_window_icon(&window);
|
|
65
|
+
|
|
66
|
+
// Fixed: Linux fullscreen issue with virtual keyboard
|
|
67
|
+
#[cfg(target_os = "linux")]
|
|
68
|
+
{
|
|
69
|
+
if init_fullscreen {
|
|
70
|
+
let _ = window.set_fullscreen(true);
|
|
71
|
+
// Ensure webview maintains focus for input after fullscreen
|
|
72
|
+
let _ = window.set_focus();
|
|
73
|
+
} else {
|
|
74
|
+
// Fix: Ubuntu 24.04/GNOME window buttons non-functional until resize (#1122)
|
|
75
|
+
// The window manager needs time to process the MapWindow event before
|
|
76
|
+
// accepting focus requests. Without this, decorations remain non-interactive.
|
|
77
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
|
|
78
|
+
let _ = window.set_focus();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[cfg(not(target_os = "linux"))]
|
|
83
|
+
let _ = init_fullscreen;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
32
87
|
#[cfg(any(target_os = "linux", test))]
|
|
33
88
|
fn is_disabled_env_value(value: &str) -> bool {
|
|
34
89
|
matches!(
|
|
@@ -149,6 +204,7 @@ pub fn run_app() {
|
|
|
149
204
|
let multi_instance = pake_config.multi_instance;
|
|
150
205
|
let multi_window = pake_config.multi_window;
|
|
151
206
|
let _enable_find = pake_config.windows[0].enable_find;
|
|
207
|
+
let startup_window_revealed = Arc::new(AtomicBool::new(false));
|
|
152
208
|
|
|
153
209
|
let window_state_plugin = WindowStatePlugin::default()
|
|
154
210
|
.with_state_flags(if init_fullscreen {
|
|
@@ -172,11 +228,13 @@ pub fn run_app() {
|
|
|
172
228
|
|
|
173
229
|
// Only add single instance plugin if multiple instances are not allowed
|
|
174
230
|
if !multi_instance {
|
|
231
|
+
let instance_revealed = startup_window_revealed.clone();
|
|
175
232
|
app_builder = app_builder.plugin(tauri_plugin_single_instance::init(
|
|
176
233
|
move |app, _args, _cwd| {
|
|
177
234
|
if multi_window {
|
|
178
235
|
open_additional_window_safe(app);
|
|
179
236
|
} else if let Some(window) = app.get_webview_window("pake") {
|
|
237
|
+
cancel_startup_reveal(&instance_revealed);
|
|
180
238
|
let _ = window.unminimize();
|
|
181
239
|
let _ = window.show();
|
|
182
240
|
reapply_window_icon(&window);
|
|
@@ -186,6 +244,35 @@ pub fn run_app() {
|
|
|
186
244
|
));
|
|
187
245
|
}
|
|
188
246
|
|
|
247
|
+
// Reveal the main window after the first real document finishes loading so
|
|
248
|
+
// slow WKWebView cold starts do not expose an empty but interactive shell.
|
|
249
|
+
// start_to_tray keeps the window hidden for the whole session until the user
|
|
250
|
+
// opens it from the tray / shortcut.
|
|
251
|
+
if !start_to_tray {
|
|
252
|
+
let page_load_revealed = startup_window_revealed.clone();
|
|
253
|
+
app_builder = app_builder.on_page_load(move |webview, payload| {
|
|
254
|
+
if webview.label() != "pake" {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if !matches!(payload.event(), PageLoadEvent::Finished) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
// Skip about:blank (and other about: placeholders) used by the macOS
|
|
261
|
+
// cert-bypass path before the real target URL navigates.
|
|
262
|
+
if is_placeholder_startup_url(payload.url()) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
if let Some(window) = webview.app_handle().get_webview_window("pake") {
|
|
266
|
+
reveal_startup_window(window, init_fullscreen, &page_load_revealed);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Clone before setup moves the Arc into tray / shortcut / fallback handlers.
|
|
272
|
+
let close_revealed = startup_window_revealed.clone();
|
|
273
|
+
#[cfg(target_os = "macos")]
|
|
274
|
+
let reopen_revealed = startup_window_revealed.clone();
|
|
275
|
+
|
|
189
276
|
app_builder
|
|
190
277
|
.invoke_handler(tauri::generate_handler![
|
|
191
278
|
download_file,
|
|
@@ -222,34 +309,30 @@ pub fn run_app() {
|
|
|
222
309
|
&pake_config.system_tray_path,
|
|
223
310
|
init_fullscreen,
|
|
224
311
|
multi_window,
|
|
312
|
+
startup_window_revealed.clone(),
|
|
313
|
+
)?;
|
|
314
|
+
set_global_shortcut(
|
|
315
|
+
app.app_handle(),
|
|
316
|
+
activation_shortcut,
|
|
317
|
+
init_fullscreen,
|
|
318
|
+
startup_window_revealed.clone(),
|
|
225
319
|
)?;
|
|
226
|
-
set_global_shortcut(app.app_handle(), activation_shortcut, init_fullscreen)?;
|
|
227
320
|
|
|
228
321
|
// Show window after state restoration to prevent position flashing
|
|
229
|
-
//
|
|
322
|
+
// once its first page finishes. A fallback keeps offline or stalled
|
|
323
|
+
// pages reachable without exposing a blank webview during normal startup.
|
|
230
324
|
if !start_to_tray {
|
|
231
325
|
let window_clone = window.clone();
|
|
232
326
|
tauri::async_runtime::spawn(async move {
|
|
233
|
-
tokio::time::sleep(tokio::time::Duration::from_millis(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
#[cfg(target_os = "linux")]
|
|
239
|
-
{
|
|
240
|
-
if init_fullscreen {
|
|
241
|
-
let _ = window_clone.set_fullscreen(true);
|
|
242
|
-
// Ensure webview maintains focus for input after fullscreen
|
|
243
|
-
let _ = window_clone.set_focus();
|
|
244
|
-
} else {
|
|
245
|
-
// Fix: Ubuntu 24.04/GNOME window buttons non-functional until resize (#1122)
|
|
246
|
-
// The window manager needs time to process the MapWindow event before
|
|
247
|
-
// accepting focus requests. Without this, decorations remain non-interactive.
|
|
248
|
-
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
|
|
249
|
-
let _ = window_clone.set_focus();
|
|
250
|
-
}
|
|
251
|
-
}
|
|
327
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(
|
|
328
|
+
STARTUP_WINDOW_FALLBACK_DELAY,
|
|
329
|
+
))
|
|
330
|
+
.await;
|
|
331
|
+
reveal_startup_window(window_clone, init_fullscreen, &startup_window_revealed);
|
|
252
332
|
});
|
|
333
|
+
} else {
|
|
334
|
+
// Tray/shortcut already hold clones that cancel user-driven toggles.
|
|
335
|
+
drop(startup_window_revealed);
|
|
253
336
|
}
|
|
254
337
|
|
|
255
338
|
Ok(())
|
|
@@ -257,6 +340,8 @@ pub fn run_app() {
|
|
|
257
340
|
.on_window_event(move |_window, _event| {
|
|
258
341
|
if let tauri::WindowEvent::CloseRequested { api, .. } = _event {
|
|
259
342
|
if hide_on_close && _window.label() == "pake" {
|
|
343
|
+
// User dismissed the window; do not let startup reveal reopen it.
|
|
344
|
+
cancel_startup_reveal(&close_revealed);
|
|
260
345
|
// Hide window when hide_on_close is enabled (regardless of tray status)
|
|
261
346
|
let window = _window.clone();
|
|
262
347
|
tauri::async_runtime::spawn(async move {
|
|
@@ -291,7 +376,7 @@ pub fn run_app() {
|
|
|
291
376
|
eprintln!("[Pake] Fatal error while building Tauri application: {error}");
|
|
292
377
|
std::process::exit(1);
|
|
293
378
|
})
|
|
294
|
-
.run(|_app, _event| {
|
|
379
|
+
.run(move |_app, _event| {
|
|
295
380
|
// Handle macOS dock icon click to reopen hidden window
|
|
296
381
|
#[cfg(target_os = "macos")]
|
|
297
382
|
if let tauri::RunEvent::Reopen {
|
|
@@ -301,6 +386,7 @@ pub fn run_app() {
|
|
|
301
386
|
{
|
|
302
387
|
if !has_visible_windows {
|
|
303
388
|
if let Some(window) = _app.get_webview_window("pake") {
|
|
389
|
+
cancel_startup_reveal(&reopen_revealed);
|
|
304
390
|
let _ = window.show();
|
|
305
391
|
reapply_window_icon(&window);
|
|
306
392
|
let _ = window.set_focus();
|
|
@@ -318,6 +404,46 @@ pub fn run() {
|
|
|
318
404
|
mod tests {
|
|
319
405
|
use super::*;
|
|
320
406
|
|
|
407
|
+
#[test]
|
|
408
|
+
fn placeholder_startup_urls_cover_about_blank() {
|
|
409
|
+
let blank: Url = "about:blank".parse().unwrap();
|
|
410
|
+
let srcdoc: Url = "about:srcdoc".parse().unwrap();
|
|
411
|
+
let https: Url = "https://github.com/".parse().unwrap();
|
|
412
|
+
let tauri: Url = "tauri://localhost/".parse().unwrap();
|
|
413
|
+
|
|
414
|
+
assert!(is_placeholder_startup_url(&blank));
|
|
415
|
+
assert!(is_placeholder_startup_url(&srcdoc));
|
|
416
|
+
assert!(!is_placeholder_startup_url(&https));
|
|
417
|
+
assert!(!is_placeholder_startup_url(&tauri));
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
#[test]
|
|
421
|
+
fn first_claim_wins_startup_reveal() {
|
|
422
|
+
let revealed = AtomicBool::new(false);
|
|
423
|
+
assert!(claim_startup_reveal(&revealed));
|
|
424
|
+
assert!(!claim_startup_reveal(&revealed));
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
#[test]
|
|
428
|
+
fn user_show_then_hide_blocks_automatic_startup_reveal() {
|
|
429
|
+
// Slow page load: user opens from tray/shortcut, then hides again.
|
|
430
|
+
// Page-load finish and the 3s fallback must not reopen the window.
|
|
431
|
+
let revealed = AtomicBool::new(false);
|
|
432
|
+
cancel_startup_reveal(&revealed); // explicit show
|
|
433
|
+
cancel_startup_reveal(&revealed); // explicit hide
|
|
434
|
+
assert!(
|
|
435
|
+
!claim_startup_reveal(&revealed),
|
|
436
|
+
"automatic reveal must stay cancelled after user visibility control"
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
#[test]
|
|
441
|
+
fn cancel_before_any_claim_blocks_reveal() {
|
|
442
|
+
let revealed = AtomicBool::new(false);
|
|
443
|
+
cancel_startup_reveal(&revealed);
|
|
444
|
+
assert!(!claim_startup_reveal(&revealed));
|
|
445
|
+
}
|
|
446
|
+
|
|
321
447
|
#[test]
|
|
322
448
|
fn linux_webkit_safe_mode_stays_on_by_default() {
|
|
323
449
|
assert!(should_enable_linux_webkit_safe_mode_from_values(
|