pake-cli 3.15.3 โ†’ 3.15.4

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.3";
23
+ var version = "3.15.4";
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.3",
3
+ "version": "3.15.4",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -2564,8 +2564,10 @@ dependencies = [
2564
2564
 
2565
2565
  [[package]]
2566
2566
  name = "pake"
2567
- version = "3.15.3"
2567
+ version = "3.15.4"
2568
2568
  dependencies = [
2569
+ "block2",
2570
+ "dispatch",
2569
2571
  "objc2",
2570
2572
  "objc2-app-kit",
2571
2573
  "objc2-foundation",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.15.3"
3
+ version = "3.15.4"
4
4
  description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with Rust."
5
5
  authors = ["Tw93"]
6
6
  license = "GPL-3.0-or-later"
@@ -25,7 +25,6 @@ tauri = { version = "2.10.2", features = [
25
25
  "tray-icon",
26
26
  "image-ico",
27
27
  "image-png",
28
- "macos-proxy",
29
28
  ] }
30
29
  tauri-plugin-window-state = "2.4.1"
31
30
  tauri-plugin-oauth = "2.0.0"
@@ -37,6 +36,8 @@ tauri-plugin-single-instance = "2.4.0"
37
36
  tauri-plugin-notification = "2.3.3"
38
37
 
39
38
  [target.'cfg(target_os = "macos")'.dependencies]
39
+ block2 = "0.6"
40
+ dispatch = "0.2"
40
41
  objc2 = "0.6"
41
42
  objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile"] }
42
43
  objc2-foundation = { version = "0.3", features = ["NSString"] }
@@ -51,6 +52,9 @@ windows-sys = { version = "0.61.2", features = [
51
52
  [features]
52
53
  # this feature is used for development builds from development cli
53
54
  cli-build = []
55
+ # Tauri's macOS proxy APIs require macOS 14 or later. The CLI enables this
56
+ # feature only after checking the host Darwin version.
57
+ macos-proxy = ["tauri/macos-proxy"]
54
58
  # by default Tauri runs in production mode
55
59
  # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
56
60
  default = ["custom-protocol"]
@@ -0,0 +1,187 @@
1
+ // macOS TLS certificate bypass for `--ignore-certificate-errors`.
2
+ //
3
+ // On macOS the webview is a WKWebView, which ignores the Chromium
4
+ // `--ignore-certificate-errors` flag entirely, so Pake's flag was a no-op here
5
+ // (see the Windows/Linux vs macOS split in window.rs). wry's own
6
+ // `WryNavigationDelegate` does not implement
7
+ // `webView:didReceiveAuthenticationChallenge:completionHandler:`, so WKWebView
8
+ // falls back to default validation and rejects self-signed certs.
9
+ //
10
+ // This installs a thin navigation-delegate proxy that accepts server trust only
11
+ // for the configured target host and forwards every other selector to wry's
12
+ // original delegate, so navigation policy, downloads, and page-load callbacks
13
+ // keep working untouched. It is installed only when the user opts in via
14
+ // `--ignore-certificate-errors`.
15
+
16
+ use objc2::rc::{Retained, Weak};
17
+ use objc2::runtime::{AnyObject, Bool, NSObject, NSObjectProtocol, Sel};
18
+ use objc2::{class, define_class, msg_send, DefinedClass, MainThreadMarker, MainThreadOnly};
19
+ use objc2_foundation::NSString;
20
+ use std::ffi::c_void;
21
+
22
+ // NSURLSessionAuthChallengeDisposition values.
23
+ const USE_CREDENTIAL: isize = 0;
24
+ const PERFORM_DEFAULT_HANDLING: isize = 1;
25
+
26
+ pub struct PakeCertDelegateIvars {
27
+ // wry's real navigation delegate; every non-challenge selector forwards here.
28
+ inner: Weak<AnyObject>,
29
+ allowed_host: String,
30
+ }
31
+
32
+ static CERT_BYPASS_ASSOCIATION_KEY: u8 = 0;
33
+
34
+ fn hosts_match(allowed_host: &str, challenge_host: &str) -> bool {
35
+ allowed_host
36
+ .trim_end_matches('.')
37
+ .eq_ignore_ascii_case(challenge_host.trim_end_matches('.'))
38
+ }
39
+
40
+ define_class!(
41
+ #[unsafe(super(NSObject))]
42
+ #[name = "PakeCertBypassDelegate"]
43
+ #[thread_kind = MainThreadOnly]
44
+ #[ivars = PakeCertDelegateIvars]
45
+ struct PakeCertBypassDelegate;
46
+
47
+ unsafe impl NSObjectProtocol for PakeCertBypassDelegate {}
48
+
49
+ impl PakeCertBypassDelegate {
50
+ #[unsafe(method(webView:didReceiveAuthenticationChallenge:completionHandler:))]
51
+ fn did_receive_challenge(
52
+ &self,
53
+ _webview: &AnyObject,
54
+ challenge: &AnyObject,
55
+ handler: &block2::Block<dyn Fn(isize, *mut AnyObject)>,
56
+ ) {
57
+ unsafe {
58
+ let space: *mut AnyObject = msg_send![challenge, protectionSpace];
59
+ if space.is_null() {
60
+ handler.call((PERFORM_DEFAULT_HANDLING, core::ptr::null_mut()));
61
+ return;
62
+ }
63
+ let host: *mut NSString = msg_send![space, host];
64
+ if host.is_null()
65
+ || !hosts_match(&self.ivars().allowed_host, &(&*host).to_string())
66
+ {
67
+ handler.call((PERFORM_DEFAULT_HANDLING, core::ptr::null_mut()));
68
+ return;
69
+ }
70
+ // Only server-trust challenges carry a non-null serverTrust; for
71
+ // anything else (e.g. HTTP basic auth) defer to default handling.
72
+ let server_trust: *mut AnyObject = msg_send![space, serverTrust];
73
+ if server_trust.is_null() {
74
+ handler.call((PERFORM_DEFAULT_HANDLING, core::ptr::null_mut()));
75
+ return;
76
+ }
77
+ let credential: *mut AnyObject =
78
+ msg_send![class!(NSURLCredential), credentialForTrust: server_trust];
79
+ handler.call((USE_CREDENTIAL, credential));
80
+ }
81
+ }
82
+
83
+ // WKWebView probes respondsToSelector: before calling optional delegate
84
+ // methods, so report our own methods plus everything wry implements.
85
+ #[unsafe(method(respondsToSelector:))]
86
+ fn responds_to_selector(&self, selector: Sel) -> Bool {
87
+ let responds: Bool = unsafe { msg_send![super(self), respondsToSelector: selector] };
88
+ if responds.as_bool() {
89
+ return Bool::YES;
90
+ }
91
+ self.ivars()
92
+ .inner
93
+ .load()
94
+ .map(|inner| unsafe { msg_send![&*inner, respondsToSelector: selector] })
95
+ .unwrap_or(Bool::NO)
96
+ }
97
+
98
+ // Fast-forward any selector we do not implement to wry's delegate.
99
+ #[unsafe(method(forwardingTargetForSelector:))]
100
+ fn forwarding_target(&self, _selector: Sel) -> *mut AnyObject {
101
+ self.ivars()
102
+ .inner
103
+ .load()
104
+ .map(|inner| Retained::as_ptr(&inner) as *mut AnyObject)
105
+ .unwrap_or(core::ptr::null_mut())
106
+ }
107
+ }
108
+ );
109
+
110
+ impl PakeCertBypassDelegate {
111
+ fn new(
112
+ inner: &Retained<AnyObject>,
113
+ allowed_host: String,
114
+ mtm: MainThreadMarker,
115
+ ) -> Retained<Self> {
116
+ let this = mtm
117
+ .alloc::<PakeCertBypassDelegate>()
118
+ .set_ivars(PakeCertDelegateIvars {
119
+ inner: Weak::from_retained(inner),
120
+ allowed_host,
121
+ });
122
+ unsafe { msg_send![super(this), init] }
123
+ }
124
+ }
125
+
126
+ /// Replace the WKWebView's navigation delegate with a proxy that accepts
127
+ /// invalid TLS certificates for the configured target host, then navigate to
128
+ /// that target. `webview_ptr` is the raw `WKWebView` from
129
+ /// `PlatformWebview::inner()`. Returns false if setup cannot be completed.
130
+ pub fn install_cert_bypass_and_navigate(
131
+ webview_ptr: *mut c_void,
132
+ allowed_host: String,
133
+ target_url: String,
134
+ ) -> bool {
135
+ if webview_ptr.is_null() || allowed_host.is_empty() || target_url.is_empty() {
136
+ return false;
137
+ }
138
+ let Some(mtm) = MainThreadMarker::new() else {
139
+ return false;
140
+ };
141
+ unsafe {
142
+ let webview: &AnyObject = &*(webview_ptr as *const AnyObject);
143
+ let existing: *mut AnyObject = msg_send![webview, navigationDelegate];
144
+ if existing.is_null() {
145
+ return false;
146
+ }
147
+ let Some(inner) = Retained::retain(existing) else {
148
+ return false;
149
+ };
150
+ let proxy = PakeCertBypassDelegate::new(&inner, allowed_host, mtm);
151
+ objc2::ffi::objc_setAssociatedObject(
152
+ webview as *const AnyObject as *mut AnyObject,
153
+ std::ptr::addr_of!(CERT_BYPASS_ASSOCIATION_KEY).cast(),
154
+ Retained::as_ptr(&proxy) as *mut AnyObject,
155
+ objc2::ffi::OBJC_ASSOCIATION_RETAIN_NONATOMIC,
156
+ );
157
+ let _: () = msg_send![webview, setNavigationDelegate: &*proxy];
158
+
159
+ let target_url = NSString::from_str(&target_url);
160
+ let ns_url: *mut AnyObject = msg_send![class!(NSURL), URLWithString: &*target_url];
161
+ if ns_url.is_null() {
162
+ return false;
163
+ }
164
+ let request: *mut AnyObject = msg_send![class!(NSURLRequest), requestWithURL: ns_url];
165
+ if request.is_null() {
166
+ return false;
167
+ }
168
+ let _: () = msg_send![webview, loadRequest: request];
169
+ true
170
+ }
171
+ }
172
+
173
+ #[cfg(test)]
174
+ mod tests {
175
+ use super::hosts_match;
176
+
177
+ #[test]
178
+ fn accepts_the_configured_host_case_insensitively() {
179
+ assert!(hosts_match("INTERNAL.EXAMPLE.COM", "internal.example.com"));
180
+ assert!(hosts_match("internal.example.com.", "internal.example.com"));
181
+ }
182
+
183
+ #[test]
184
+ fn rejects_other_hosts() {
185
+ assert!(!hosts_match("internal.example.com", "login.example.com"));
186
+ }
187
+ }
@@ -1,7 +1,7 @@
1
1
  // Menu functionality is only used on macOS; the module is gated in app/mod.rs.
2
- use crate::app::window::open_additional_window_safe;
2
+ use crate::app::window::{open_additional_window_safe, MultiWindowState};
3
3
  use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
4
- use tauri::{AppHandle, Manager, Wry};
4
+ use tauri::{AppHandle, Manager, WebviewWindow, Wry};
5
5
  use tauri_plugin_opener::OpenerExt;
6
6
 
7
7
  pub fn set_app_menu(
@@ -224,6 +224,36 @@ fn help_menu(app: &AppHandle<Wry>, title: &str) -> tauri::Result<Submenu<Wry>> {
224
224
  Ok(help_menu)
225
225
  }
226
226
 
227
+ // Resolve the app's real home URL from its window config. Split out from
228
+ // `home_url` so the mapping can be unit-tested without an AppHandle.
229
+ fn resolve_home_url(url_type: &str, url: &str) -> Option<tauri::Url> {
230
+ match url_type {
231
+ // A web app's configured url is already absolute.
232
+ "web" => tauri::Url::parse(url).ok(),
233
+ // A local-file app's url is only a basename; Tauri serves bundled assets
234
+ // from tauri://localhost on macOS (this menu is macOS-only). Resolving it
235
+ // against the currently loaded remote origin (as the old eval path did)
236
+ // would point at the wrong server.
237
+ "local" => tauri::Url::parse(&format!("tauri://localhost/{url}")).ok(),
238
+ _ => None,
239
+ }
240
+ }
241
+
242
+ fn home_url(app: &AppHandle) -> Option<tauri::Url> {
243
+ let state = app.try_state::<MultiWindowState>()?;
244
+ let window_config = state.pake_config.windows.first()?;
245
+ resolve_home_url(&window_config.url_type, &window_config.url)
246
+ }
247
+
248
+ fn focused_webview_window(app_handle: &AppHandle) -> Option<WebviewWindow> {
249
+ let windows = app_handle.webview_windows();
250
+ windows
251
+ .values()
252
+ .find(|window| window.is_focused().unwrap_or(false))
253
+ .cloned()
254
+ .or_else(|| windows.get("pake").cloned())
255
+ }
256
+
227
257
  pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
228
258
  match id {
229
259
  "new_window" => {
@@ -235,13 +265,13 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
235
265
  .open_url("https://github.com/tw93/Pake", None::<&str>);
236
266
  }
237
267
  "reload" => {
238
- if let Some(window) = app_handle.get_webview_window("pake") {
268
+ if let Some(window) = focused_webview_window(app_handle) {
239
269
  let _ = window.eval("window.location.reload()");
240
270
  }
241
271
  }
242
272
  "toggle_devtools" => {
243
273
  #[cfg(debug_assertions)] // Only allow in debug builds
244
- if let Some(window) = app_handle.get_webview_window("pake") {
274
+ if let Some(window) = focused_webview_window(app_handle) {
245
275
  if window.is_devtools_open() {
246
276
  window.close_devtools();
247
277
  } else {
@@ -250,69 +280,79 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
250
280
  }
251
281
  }
252
282
  "zoom_in" => {
253
- if let Some(window) = app_handle.get_webview_window("pake") {
283
+ if let Some(window) = focused_webview_window(app_handle) {
254
284
  let _ = window.eval("zoomIn()");
255
285
  }
256
286
  }
257
287
  "zoom_out" => {
258
- if let Some(window) = app_handle.get_webview_window("pake") {
288
+ if let Some(window) = focused_webview_window(app_handle) {
259
289
  let _ = window.eval("zoomOut()");
260
290
  }
261
291
  }
262
292
  "zoom_reset" => {
263
- if let Some(window) = app_handle.get_webview_window("pake") {
293
+ if let Some(window) = focused_webview_window(app_handle) {
264
294
  let _ = window.eval("setZoom('100%')");
265
295
  }
266
296
  }
267
297
  "go_back" => {
268
- if let Some(window) = app_handle.get_webview_window("pake") {
298
+ if let Some(window) = focused_webview_window(app_handle) {
269
299
  let _ = window.eval("window.history.back()");
270
300
  }
271
301
  }
272
302
  "go_forward" => {
273
- if let Some(window) = app_handle.get_webview_window("pake") {
303
+ if let Some(window) = focused_webview_window(app_handle) {
274
304
  let _ = window.eval("window.history.forward()");
275
305
  }
276
306
  }
277
307
  "go_home" => {
278
- if let Some(window) = app_handle.get_webview_window("pake") {
279
- let _ = window.eval("window.location.href = window.pakeConfig.url");
308
+ if let Some(window) = focused_webview_window(app_handle) {
309
+ // Native navigation works even from a blank error page (where
310
+ // eval cannot run) and resolves local-file apps to the correct
311
+ // bundled asset instead of a path on the current origin.
312
+ match home_url(app_handle) {
313
+ Some(url) => {
314
+ let _ = window.navigate(url);
315
+ }
316
+ None => {
317
+ let _ = window.eval("window.location.href = window.pakeConfig.url");
318
+ }
319
+ }
280
320
  }
281
321
  }
282
322
  "copy_url" => {
283
- if let Some(window) = app_handle.get_webview_window("pake") {
323
+ if let Some(window) = focused_webview_window(app_handle) {
284
324
  let _ = window.eval("navigator.clipboard.writeText(window.location.href)");
285
325
  }
286
326
  }
287
327
  "paste_and_match_style" => {
288
- if let Some(window) = app_handle.get_webview_window("pake") {
328
+ if let Some(window) = focused_webview_window(app_handle) {
289
329
  let _ = window.eval("triggerPasteAsPlainText()");
290
330
  }
291
331
  }
292
332
  "find" => {
293
- if let Some(window) = app_handle.get_webview_window("pake") {
333
+ if let Some(window) = focused_webview_window(app_handle) {
294
334
  let _ = window.eval("window.pakeFind?.open()");
295
335
  }
296
336
  }
297
337
  "find_next" => {
298
- if let Some(window) = app_handle.get_webview_window("pake") {
338
+ if let Some(window) = focused_webview_window(app_handle) {
299
339
  let _ = window.eval("window.pakeFind?.next()");
300
340
  }
301
341
  }
302
342
  "find_previous" => {
303
- if let Some(window) = app_handle.get_webview_window("pake") {
343
+ if let Some(window) = focused_webview_window(app_handle) {
304
344
  let _ = window.eval("window.pakeFind?.previous()");
305
345
  }
306
346
  }
307
347
  "clear_cache_restart" => {
308
- if let Some(window) = app_handle.get_webview_window("pake") {
348
+ if let Some(window) = focused_webview_window(app_handle) {
309
349
  if window.clear_all_browsing_data().is_ok() {
310
350
  app_handle.restart();
311
351
  }
312
352
  }
313
353
  }
314
354
  "always_on_top" => {
315
- if let Some(window) = app_handle.get_webview_window("pake") {
355
+ if let Some(window) = focused_webview_window(app_handle) {
316
356
  let is_on_top = window.is_always_on_top().unwrap_or(false);
317
357
  let _ = window.set_always_on_top(!is_on_top);
318
358
  }
@@ -320,3 +360,31 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
320
360
  _ => {}
321
361
  }
322
362
  }
363
+
364
+ #[cfg(test)]
365
+ mod tests {
366
+ use super::resolve_home_url;
367
+
368
+ #[test]
369
+ fn web_url_passes_through_unchanged() {
370
+ assert_eq!(
371
+ resolve_home_url("web", "https://github.com").map(|u| u.to_string()),
372
+ Some("https://github.com/".to_string())
373
+ );
374
+ }
375
+
376
+ #[test]
377
+ fn local_basename_resolves_to_bundled_asset_url() {
378
+ // The fix: a local app's basename must become the bundled asset URL,
379
+ // not a path resolved against whatever origin is currently loaded.
380
+ assert_eq!(
381
+ resolve_home_url("local", "launcher.html").map(|u| u.to_string()),
382
+ Some("tauri://localhost/launcher.html".to_string())
383
+ );
384
+ }
385
+
386
+ #[test]
387
+ fn unknown_url_type_is_none() {
388
+ assert!(resolve_home_url("bogus", "whatever").is_none());
389
+ }
390
+ }
@@ -1,3 +1,5 @@
1
+ #[cfg(target_os = "macos")]
2
+ pub mod cert;
1
3
  pub mod config;
2
4
  pub mod invoke;
3
5
  #[cfg(target_os = "macos")]
@@ -3,6 +3,8 @@ use crate::util::{
3
3
  check_file_or_append, get_data_dir, get_download_message_with_lang, sanitize_download_filename,
4
4
  show_toast, MessageType,
5
5
  };
6
+ #[cfg(target_os = "macos")]
7
+ use dispatch::Queue;
6
8
  #[cfg(target_os = "windows")]
7
9
  use std::{os::windows::ffi::OsStrExt, ptr, sync::OnceLock};
8
10
  use std::{
@@ -279,6 +281,27 @@ fn build_window(
279
281
  ))
280
282
  })?;
281
283
 
284
+ #[cfg(target_os = "macos")]
285
+ let cert_bypass_target = if label == "pake"
286
+ && window_config.ignore_certificate_errors
287
+ && window_config.url_type == "web"
288
+ {
289
+ Url::parse(&window_config.url).ok()
290
+ } else {
291
+ None
292
+ };
293
+
294
+ // The delegate must be installed before the first TLS challenge. Start on
295
+ // a neutral page, then navigate from the with_webview callback below.
296
+ #[cfg(target_os = "macos")]
297
+ let url = if cert_bypass_target.is_some() {
298
+ WebviewUrl::CustomProtocol(
299
+ Url::parse("about:blank").expect("about:blank must be a valid URL"),
300
+ )
301
+ } else {
302
+ url
303
+ };
304
+
282
305
  let user_agent = config.user_agent.get();
283
306
 
284
307
  let config_script = format!(
@@ -407,11 +430,6 @@ fn build_window(
407
430
  {
408
431
  linux_browser_args.push_str(" --ignore-certificate-errors");
409
432
  }
410
-
411
- #[cfg(target_os = "macos")]
412
- {
413
- window_builder = window_builder.additional_browser_args("--ignore-certificate-errors");
414
- }
415
433
  }
416
434
 
417
435
  if window_config.enable_wasm {
@@ -583,7 +601,35 @@ fn build_window(
583
601
 
584
602
  window_builder = window_builder.on_navigation(|_| true);
585
603
 
586
- window_builder.build()
604
+ let window = window_builder.build()?;
605
+
606
+ // macOS WKWebView ignores the Chromium --ignore-certificate-errors flag, so
607
+ // install a host-scoped delegate on the process-lifetime main window only.
608
+ // Queue setup after construction so wry cannot replace the proxy while it
609
+ // finishes initializing its own navigation delegate.
610
+ #[cfg(target_os = "macos")]
611
+ if let Some(target_url) = cert_bypass_target {
612
+ let allowed_host = target_url
613
+ .host_str()
614
+ .expect("web URLs must have a host")
615
+ .to_owned();
616
+ let cert_window = window.clone();
617
+ Queue::main().exec_async(move || {
618
+ if let Err(error) = cert_window.with_webview(move |webview| {
619
+ if !crate::app::cert::install_cert_bypass_and_navigate(
620
+ webview.inner(),
621
+ allowed_host,
622
+ target_url.to_string(),
623
+ ) {
624
+ eprintln!("[Pake] Failed to configure macOS certificate bypass.");
625
+ }
626
+ }) {
627
+ eprintln!("[Pake] Failed to access the macOS webview: {error}");
628
+ }
629
+ });
630
+ }
631
+
632
+ Ok(window)
587
633
  }
588
634
 
589
635
  #[cfg(all(test, target_os = "windows"))]
@@ -60,6 +60,20 @@ function handleShortcut(event) {
60
60
  }
61
61
  }
62
62
 
63
+ function handleWebShortcut(event) {
64
+ if (isNonMacDesktop() && event.ctrlKey) {
65
+ handleShortcut(event);
66
+ return;
67
+ }
68
+
69
+ const isMac = /mac/i.test(getDesktopPlatform());
70
+ const isMacScrollShortcut =
71
+ event.key === "ArrowUp" || event.key === "ArrowDown";
72
+ if (isMac && event.metaKey && isMacScrollShortcut) {
73
+ handleShortcut(event);
74
+ }
75
+ }
76
+
63
77
  function toggleNativeFullscreen(appWindow) {
64
78
  appWindow
65
79
  .isFullscreen()
@@ -437,7 +451,6 @@ const DOWNLOAD_PATH_PATTERNS = [
437
451
  "/files/",
438
452
  "/attachments/",
439
453
  "/assets/",
440
- "/releases/",
441
454
  "/dist/",
442
455
  ];
443
456
 
@@ -622,14 +635,7 @@ document.addEventListener("DOMContentLoaded", () => {
622
635
 
623
636
  if (window["pakeConfig"]?.disabled_web_shortcuts !== true) {
624
637
  document.addEventListener("keydown", handleWindowFullscreenShortcut, true);
625
- document.addEventListener("keyup", (event) => {
626
- if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {
627
- handleShortcut(event);
628
- }
629
- if (/macintosh|mac os x/i.test(navigator.userAgent) && event.metaKey) {
630
- handleShortcut(event);
631
- }
632
- });
638
+ document.addEventListener("keyup", handleWebShortcut);
633
639
  }
634
640
 
635
641
  document.addEventListener("keydown", handleClipboardShortcut, true);
@@ -663,9 +663,11 @@
663
663
  function getFindShortcutAction(event) {
664
664
  const userAgent = navigator.userAgent || "";
665
665
  const isMac = /macintosh|mac os x/i.test(userAgent);
666
- const hasModifier = isMac
667
- ? event.metaKey && !event.ctrlKey
668
- : event.ctrlKey && !event.metaKey;
666
+ if (isMac) {
667
+ return "";
668
+ }
669
+
670
+ const hasModifier = event.ctrlKey && !event.metaKey;
669
671
 
670
672
  if (!hasModifier || event.altKey) {
671
673
  return "";
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.15.3",
4
+ "version": "3.15.4",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {