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 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.4";
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.15.4",
3
+ "version": "3.15.5",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -2564,7 +2564,7 @@ dependencies = [
2564
2564
 
2565
2565
  [[package]]
2566
2566
  name = "pake"
2567
- version = "3.15.4"
2567
+ version = "3.15.5"
2568
2568
  dependencies = [
2569
2569
  "block2",
2570
2570
  "dispatch",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.15.4"
3
+ version = "3.15.5"
4
4
  description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with Rust."
5
5
  authors = ["Tw93"]
6
6
  license = "GPL-3.0-or-later"
@@ -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
- const DOWNLOAD_PATH_PATTERNS = [
450
- "/download/",
451
- "/files/",
452
- "/attachments/",
453
- "/assets/",
454
- "/dist/",
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() {
@@ -2,14 +2,20 @@
2
2
  mod app;
3
3
  mod util;
4
4
 
5
- use tauri::Manager;
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
- const WINDOW_SHOW_DELAY: u64 = 50;
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
- // Unless start_to_tray is enabled, then keep it hidden
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(WINDOW_SHOW_DELAY)).await;
234
- let _ = window_clone.show();
235
- reapply_window_icon(&window_clone);
236
-
237
- // Fixed: Linux fullscreen issue with virtual keyboard
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(
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.15.4",
4
+ "version": "3.15.5",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {