pake-cli 3.15.3 โ 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 +3 -1
- package/src-tauri/Cargo.toml +6 -2
- package/src-tauri/src/app/cert.rs +187 -0
- package/src-tauri/src/app/invoke.rs +11 -1
- package/src-tauri/src/app/menu.rs +86 -18
- package/src-tauri/src/app/mod.rs +2 -0
- package/src-tauri/src/app/setup.rs +12 -1
- package/src-tauri/src/app/window.rs +52 -6
- package/src-tauri/src/inject/event.js +21 -16
- package/src-tauri/src/inject/find.js +5 -3
- 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "pake"
|
|
3
|
-
version = "3.15.
|
|
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"
|
|
@@ -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
|
+
}
|
|
@@ -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,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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
279
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
+
}
|
package/src-tauri/src/app/mod.rs
CHANGED
|
@@ -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();
|
|
@@ -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()
|
|
@@ -432,14 +446,12 @@ const PREVIEWABLE_MEDIA_EXTENSIONS = [
|
|
|
432
446
|
"m4a",
|
|
433
447
|
];
|
|
434
448
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
"/dist/",
|
|
442
|
-
];
|
|
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/"];
|
|
443
455
|
|
|
444
456
|
// Language detection utilities
|
|
445
457
|
function getUserLanguage() {
|
|
@@ -622,14 +634,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
622
634
|
|
|
623
635
|
if (window["pakeConfig"]?.disabled_web_shortcuts !== true) {
|
|
624
636
|
document.addEventListener("keydown", handleWindowFullscreenShortcut, true);
|
|
625
|
-
document.addEventListener("keyup",
|
|
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
|
-
});
|
|
637
|
+
document.addEventListener("keyup", handleWebShortcut);
|
|
633
638
|
}
|
|
634
639
|
|
|
635
640
|
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
|
-
|
|
667
|
-
|
|
668
|
-
|
|
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 "";
|
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(
|