pake-cli 3.15.4 โ 3.15.6
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/README.md +1 -1
- package/dist/cli.js +7 -6
- package/package.json +7 -6
- package/src-tauri/Cargo.lock +6 -4
- package/src-tauri/Cargo.toml +9 -1
- package/src-tauri/src/app/invoke.rs +70 -11
- package/src-tauri/src/app/menu.rs +24 -4
- package/src-tauri/src/app/mod.rs +1 -0
- package/src-tauri/src/app/navigation.rs +83 -0
- package/src-tauri/src/app/setup.rs +31 -45
- package/src-tauri/src/app/window.rs +85 -8
- package/src-tauri/src/inject/event.js +92 -46
- package/src-tauri/src/lib.rs +170 -26
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ Use Pake (npm i -g pake-cli) to package webpages as desktop apps. Read https://u
|
|
|
202
202
|
|
|
203
203
|
## Development
|
|
204
204
|
|
|
205
|
-
Requires Rust `>=1.85` and Node `>=22` (recommended LTS; `>=
|
|
205
|
+
Requires Rust `>=1.85` and Node `>=22` (recommended LTS; `>=20` also works). For detailed installation guide, see [Tauri documentation](https://v2.tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.
|
|
206
206
|
|
|
207
207
|
```bash
|
|
208
208
|
# Install dependencies
|
package/dist/cli.js
CHANGED
|
@@ -20,10 +20,10 @@ 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.6";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
|
-
node: ">=
|
|
26
|
+
node: ">=20.0.0"
|
|
27
27
|
};
|
|
28
28
|
var packageManager = "pnpm@10.26.2";
|
|
29
29
|
var bin = {
|
|
@@ -78,14 +78,14 @@ var dependencies = {
|
|
|
78
78
|
chalk: "^5.6.2",
|
|
79
79
|
commander: "^14.0.3",
|
|
80
80
|
execa: "^9.6.1",
|
|
81
|
-
"file-type": "^21.3.
|
|
81
|
+
"file-type": "^21.3.4",
|
|
82
82
|
"fs-extra": "^11.3.3",
|
|
83
83
|
"icon-gen": "^5.0.0",
|
|
84
84
|
loglevel: "^1.9.2",
|
|
85
85
|
ora: "^9.3.0",
|
|
86
86
|
prompts: "^2.4.2",
|
|
87
87
|
psl: "^1.15.0",
|
|
88
|
-
sharp: "^0.
|
|
88
|
+
sharp: "^0.35.0",
|
|
89
89
|
"tmp-promise": "^3.0.3",
|
|
90
90
|
"update-notifier": "^7.3.1"
|
|
91
91
|
};
|
|
@@ -111,8 +111,9 @@ var devDependencies = {
|
|
|
111
111
|
};
|
|
112
112
|
var pnpm = {
|
|
113
113
|
overrides: {
|
|
114
|
-
sharp: "^0.
|
|
115
|
-
"@img/sharp-libvips-darwin-arm64": "1.
|
|
114
|
+
sharp: "^0.35.0",
|
|
115
|
+
"@img/sharp-libvips-darwin-arm64": "1.3.0",
|
|
116
|
+
tmp: "0.2.7"
|
|
116
117
|
},
|
|
117
118
|
onlyBuiltDependencies: [
|
|
118
119
|
"esbuild",
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pake-cli",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.6",
|
|
4
4
|
"description": "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=20.0.0"
|
|
7
7
|
},
|
|
8
8
|
"packageManager": "pnpm@10.26.2",
|
|
9
9
|
"bin": {
|
|
@@ -58,14 +58,14 @@
|
|
|
58
58
|
"chalk": "^5.6.2",
|
|
59
59
|
"commander": "^14.0.3",
|
|
60
60
|
"execa": "^9.6.1",
|
|
61
|
-
"file-type": "^21.3.
|
|
61
|
+
"file-type": "^21.3.4",
|
|
62
62
|
"fs-extra": "^11.3.3",
|
|
63
63
|
"icon-gen": "^5.0.0",
|
|
64
64
|
"loglevel": "^1.9.2",
|
|
65
65
|
"ora": "^9.3.0",
|
|
66
66
|
"prompts": "^2.4.2",
|
|
67
67
|
"psl": "^1.15.0",
|
|
68
|
-
"sharp": "^0.
|
|
68
|
+
"sharp": "^0.35.0",
|
|
69
69
|
"tmp-promise": "^3.0.3",
|
|
70
70
|
"update-notifier": "^7.3.1"
|
|
71
71
|
},
|
|
@@ -91,8 +91,9 @@
|
|
|
91
91
|
},
|
|
92
92
|
"pnpm": {
|
|
93
93
|
"overrides": {
|
|
94
|
-
"sharp": "^0.
|
|
95
|
-
"@img/sharp-libvips-darwin-arm64": "1.
|
|
94
|
+
"sharp": "^0.35.0",
|
|
95
|
+
"@img/sharp-libvips-darwin-arm64": "1.3.0",
|
|
96
|
+
"tmp": "0.2.7"
|
|
96
97
|
},
|
|
97
98
|
"onlyBuiltDependencies": [
|
|
98
99
|
"esbuild",
|
package/src-tauri/Cargo.lock
CHANGED
|
@@ -2564,7 +2564,7 @@ dependencies = [
|
|
|
2564
2564
|
|
|
2565
2565
|
[[package]]
|
|
2566
2566
|
name = "pake"
|
|
2567
|
-
version = "3.15.
|
|
2567
|
+
version = "3.15.6"
|
|
2568
2568
|
dependencies = [
|
|
2569
2569
|
"block2",
|
|
2570
2570
|
"dispatch",
|
|
@@ -2584,6 +2584,8 @@ dependencies = [
|
|
|
2584
2584
|
"tauri-plugin-single-instance",
|
|
2585
2585
|
"tauri-plugin-window-state",
|
|
2586
2586
|
"tokio",
|
|
2587
|
+
"webkit2gtk",
|
|
2588
|
+
"webview2-com",
|
|
2587
2589
|
"windows-sys 0.61.2",
|
|
2588
2590
|
]
|
|
2589
2591
|
|
|
@@ -3042,9 +3044,9 @@ dependencies = [
|
|
|
3042
3044
|
|
|
3043
3045
|
[[package]]
|
|
3044
3046
|
name = "quinn-proto"
|
|
3045
|
-
version = "0.11.
|
|
3047
|
+
version = "0.11.14"
|
|
3046
3048
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3047
|
-
checksum = "
|
|
3049
|
+
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
|
3048
3050
|
dependencies = [
|
|
3049
3051
|
"bytes",
|
|
3050
3052
|
"getrandom 0.3.4",
|
|
@@ -3072,7 +3074,7 @@ dependencies = [
|
|
|
3072
3074
|
"once_cell",
|
|
3073
3075
|
"socket2",
|
|
3074
3076
|
"tracing",
|
|
3075
|
-
"windows-sys 0.
|
|
3077
|
+
"windows-sys 0.59.0",
|
|
3076
3078
|
]
|
|
3077
3079
|
|
|
3078
3080
|
[[package]]
|
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.6"
|
|
4
4
|
description = "๐คฑ๐ป Turn any webpage into a desktop app with Rust."
|
|
5
5
|
authors = ["Tw93"]
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
@@ -42,12 +42,20 @@ objc2 = "0.6"
|
|
|
42
42
|
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile"] }
|
|
43
43
|
objc2-foundation = { version = "0.3", features = ["NSString"] }
|
|
44
44
|
|
|
45
|
+
[target.'cfg(target_os = "linux")'.dependencies]
|
|
46
|
+
# Used for native WebKitGTK go_back/go_forward on error pages (no page JS).
|
|
47
|
+
# Version and features must match what wry resolves, or PlatformWebview::inner
|
|
48
|
+
# hands back a WebView from a different crate instance.
|
|
49
|
+
webkit2gtk = { version = "=2.0.2", features = ["v2_38"] }
|
|
50
|
+
|
|
45
51
|
[target.'cfg(windows)'.dependencies]
|
|
46
52
|
windows-sys = { version = "0.61.2", features = [
|
|
47
53
|
"Win32_Foundation",
|
|
48
54
|
"Win32_UI_Shell",
|
|
49
55
|
"Win32_UI_WindowsAndMessaging",
|
|
50
56
|
] }
|
|
57
|
+
# Used for native WebView2 GoBack/GoForward on error pages (no page JS).
|
|
58
|
+
webview2-com = "0.38"
|
|
51
59
|
|
|
52
60
|
[features]
|
|
53
61
|
# this feature is used for development builds from development cli
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
use crate::app::navigation::{history_step, reload_window};
|
|
1
2
|
use crate::util::{
|
|
2
3
|
check_file_or_append, get_download_message_with_lang, sanitize_download_filename, show_toast,
|
|
3
4
|
MessageType,
|
|
@@ -8,6 +9,7 @@ use std::str::FromStr;
|
|
|
8
9
|
use std::sync::atomic::{AtomicI64, Ordering};
|
|
9
10
|
use tauri::http::Method;
|
|
10
11
|
use tauri::{command, AppHandle, Manager, Url, WebviewWindow};
|
|
12
|
+
use tauri_plugin_http::reqwest::header::{HeaderValue, COOKIE};
|
|
11
13
|
use tauri_plugin_http::reqwest::{ClientBuilder, Request};
|
|
12
14
|
|
|
13
15
|
use tauri::Theme;
|
|
@@ -82,10 +84,30 @@ pub struct NotificationParams {
|
|
|
82
84
|
icon: String,
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
87
|
+
/// Build a Cookie header from the webview session so authenticated downloads
|
|
88
|
+
/// match what the page itself would request. Best-effort: missing cookies
|
|
89
|
+
/// fall through to an anonymous request.
|
|
90
|
+
fn cookie_header_for_url(window: &WebviewWindow, url: &Url) -> Option<HeaderValue> {
|
|
91
|
+
let cookies = window.cookies_for_url(url.clone()).ok()?;
|
|
92
|
+
if cookies.is_empty() {
|
|
93
|
+
return None;
|
|
94
|
+
}
|
|
95
|
+
let header = cookies
|
|
96
|
+
.iter()
|
|
97
|
+
.map(|cookie| format!("{}={}", cookie.name(), cookie.value()))
|
|
98
|
+
.collect::<Vec<_>>()
|
|
99
|
+
.join("; ");
|
|
100
|
+
HeaderValue::from_str(&header).ok()
|
|
101
|
+
}
|
|
88
102
|
|
|
103
|
+
#[command]
|
|
104
|
+
pub async fn download_file(
|
|
105
|
+
window: WebviewWindow,
|
|
106
|
+
app: AppHandle,
|
|
107
|
+
params: DownloadFileParams,
|
|
108
|
+
) -> Result<(), String> {
|
|
109
|
+
// Toast on the calling window (secondary windows included), not a hard-coded
|
|
110
|
+
// main-window label. Tauri injects the invoker as `window`.
|
|
89
111
|
show_toast(
|
|
90
112
|
&window,
|
|
91
113
|
&get_download_message_with_lang(MessageType::Start, params.language.clone()),
|
|
@@ -108,14 +130,27 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result
|
|
|
108
130
|
|
|
109
131
|
let url = Url::from_str(¶ms.url).map_err(|e| format!("Invalid URL: {}", e))?;
|
|
110
132
|
|
|
111
|
-
let request = Request::new(Method::GET, url);
|
|
133
|
+
let mut request = Request::new(Method::GET, url.clone());
|
|
134
|
+
if let Some(cookie_header) = cookie_header_for_url(&window, &url) {
|
|
135
|
+
request.headers_mut().insert(COOKIE, cookie_header);
|
|
136
|
+
}
|
|
112
137
|
|
|
113
138
|
let response = client.execute(request).await;
|
|
114
139
|
|
|
115
140
|
match response {
|
|
116
141
|
Ok(mut res) => {
|
|
142
|
+
// Transport success is not download success: 403/404 HTML error pages
|
|
143
|
+
// must not be written as files or toasted as successful downloads.
|
|
144
|
+
if !res.status().is_success() {
|
|
145
|
+
show_toast(
|
|
146
|
+
&window,
|
|
147
|
+
&get_download_message_with_lang(MessageType::Failure, params.language),
|
|
148
|
+
);
|
|
149
|
+
return Err(format!("Download failed with HTTP status {}", res.status()));
|
|
150
|
+
}
|
|
151
|
+
|
|
117
152
|
let mut file =
|
|
118
|
-
File::create(file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
|
153
|
+
File::create(&file_path).map_err(|e| format!("Failed to create file: {}", e))?;
|
|
119
154
|
|
|
120
155
|
while let Some(chunk) = res
|
|
121
156
|
.chunk()
|
|
@@ -185,12 +220,12 @@ pub fn set_dock_badge_label(app: AppHandle, label: Option<String>) -> Result<(),
|
|
|
185
220
|
|
|
186
221
|
#[command]
|
|
187
222
|
pub async fn update_theme_mode(app: AppHandle, mode: String) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
223
|
+
let theme = if mode == "dark" {
|
|
224
|
+
Theme::Dark
|
|
225
|
+
} else {
|
|
226
|
+
Theme::Light
|
|
227
|
+
};
|
|
228
|
+
for window in app.webview_windows().values() {
|
|
194
229
|
let _ = window.set_theme(Some(theme));
|
|
195
230
|
}
|
|
196
231
|
}
|
|
@@ -206,3 +241,27 @@ pub fn set_zoom(window: WebviewWindow, percent: f64) -> Result<(), String> {
|
|
|
206
241
|
.set_zoom(factor)
|
|
207
242
|
.map_err(|e| format!("Failed to set zoom: {}", e))
|
|
208
243
|
}
|
|
244
|
+
|
|
245
|
+
/// Native navigation for injected shortcuts (Linux/Windows Ctrl+R / [ / ]).
|
|
246
|
+
/// Blank error pages have no JS context, so page `history` / `location` calls
|
|
247
|
+
/// are no-ops; these use the platform webview API instead.
|
|
248
|
+
#[command]
|
|
249
|
+
pub fn webview_navigate(window: WebviewWindow, action: String) -> Result<(), String> {
|
|
250
|
+
match action.as_str() {
|
|
251
|
+
"reload" => {
|
|
252
|
+
reload_window(&window);
|
|
253
|
+
Ok(())
|
|
254
|
+
}
|
|
255
|
+
"back" => {
|
|
256
|
+
history_step(&window, true);
|
|
257
|
+
Ok(())
|
|
258
|
+
}
|
|
259
|
+
"forward" => {
|
|
260
|
+
history_step(&window, false);
|
|
261
|
+
Ok(())
|
|
262
|
+
}
|
|
263
|
+
other => Err(format!(
|
|
264
|
+
"Unknown webview_navigate action '{other}' (expected reload|back|forward)"
|
|
265
|
+
)),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
// Menu functionality is only used on macOS; the module is gated in app/mod.rs.
|
|
2
|
+
use crate::app::navigation::{history_step, reload_window};
|
|
2
3
|
use crate::app::window::{open_additional_window_safe, MultiWindowState};
|
|
4
|
+
use std::io::Write;
|
|
5
|
+
use std::process::{Command, Stdio};
|
|
3
6
|
use tauri::menu::{AboutMetadata, Menu, MenuItem, PredefinedMenuItem, Submenu};
|
|
4
7
|
use tauri::{AppHandle, Manager, WebviewWindow, Wry};
|
|
5
8
|
use tauri_plugin_opener::OpenerExt;
|
|
@@ -254,6 +257,18 @@ fn focused_webview_window(app_handle: &AppHandle) -> Option<WebviewWindow> {
|
|
|
254
257
|
.or_else(|| windows.get("pake").cloned())
|
|
255
258
|
}
|
|
256
259
|
|
|
260
|
+
/// Copy text via pbcopy so it works when the page has no JS context (error
|
|
261
|
+
/// shells) and when the Clipboard API is blocked by the site CSP.
|
|
262
|
+
fn copy_text_to_pasteboard(text: &str) {
|
|
263
|
+
let Ok(mut child) = Command::new("pbcopy").stdin(Stdio::piped()).spawn() else {
|
|
264
|
+
return;
|
|
265
|
+
};
|
|
266
|
+
if let Some(stdin) = child.stdin.as_mut() {
|
|
267
|
+
let _ = stdin.write_all(text.as_bytes());
|
|
268
|
+
}
|
|
269
|
+
let _ = child.wait();
|
|
270
|
+
}
|
|
271
|
+
|
|
257
272
|
pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
258
273
|
match id {
|
|
259
274
|
"new_window" => {
|
|
@@ -266,7 +281,8 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
266
281
|
}
|
|
267
282
|
"reload" => {
|
|
268
283
|
if let Some(window) = focused_webview_window(app_handle) {
|
|
269
|
-
|
|
284
|
+
// Native reload works on blank error pages where eval cannot.
|
|
285
|
+
reload_window(&window);
|
|
270
286
|
}
|
|
271
287
|
}
|
|
272
288
|
"toggle_devtools" => {
|
|
@@ -296,12 +312,12 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
296
312
|
}
|
|
297
313
|
"go_back" => {
|
|
298
314
|
if let Some(window) = focused_webview_window(app_handle) {
|
|
299
|
-
|
|
315
|
+
history_step(&window, true);
|
|
300
316
|
}
|
|
301
317
|
}
|
|
302
318
|
"go_forward" => {
|
|
303
319
|
if let Some(window) = focused_webview_window(app_handle) {
|
|
304
|
-
|
|
320
|
+
history_step(&window, false);
|
|
305
321
|
}
|
|
306
322
|
}
|
|
307
323
|
"go_home" => {
|
|
@@ -321,7 +337,11 @@ pub fn handle_menu_click(app_handle: &AppHandle, id: &str) {
|
|
|
321
337
|
}
|
|
322
338
|
"copy_url" => {
|
|
323
339
|
if let Some(window) = focused_webview_window(app_handle) {
|
|
324
|
-
|
|
340
|
+
// Prefer the native webview URL so copy still works on error
|
|
341
|
+
// pages that have no document.location / clipboard API.
|
|
342
|
+
if let Ok(url) = window.url() {
|
|
343
|
+
copy_text_to_pasteboard(url.as_str());
|
|
344
|
+
}
|
|
325
345
|
}
|
|
326
346
|
}
|
|
327
347
|
"paste_and_match_style" => {
|
package/src-tauri/src/app/mod.rs
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
//! Cross-platform webview navigation that does not depend on page JS.
|
|
2
|
+
//! Blank error shells have no JS context, so `eval("history.back()")` and
|
|
3
|
+
//! `location.reload()` are dead exactly when the user is stuck.
|
|
4
|
+
|
|
5
|
+
use tauri::WebviewWindow;
|
|
6
|
+
|
|
7
|
+
/// Reload the current document via the native webview API.
|
|
8
|
+
pub fn reload_window(window: &WebviewWindow) {
|
|
9
|
+
if let Err(error) = window.reload() {
|
|
10
|
+
eprintln!("[Pake] Failed to reload webview: {error}");
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Step the webview history without requiring a live page JS context.
|
|
15
|
+
pub fn history_step(window: &WebviewWindow, back: bool) {
|
|
16
|
+
let stepped = window.with_webview(move |webview| {
|
|
17
|
+
history_step_platform(&webview, back);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
if stepped.is_err() {
|
|
21
|
+
// Last resort when with_webview is unavailable (rare). Still fails on
|
|
22
|
+
// blank error shells, but preserves SPA history when the page is live.
|
|
23
|
+
let script = if back {
|
|
24
|
+
"window.history.back()"
|
|
25
|
+
} else {
|
|
26
|
+
"window.history.forward()"
|
|
27
|
+
};
|
|
28
|
+
if let Err(error) = window.eval(script) {
|
|
29
|
+
eprintln!("[Pake] Failed to step webview history: {error}");
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[cfg(target_os = "macos")]
|
|
35
|
+
fn history_step_platform(webview: &tauri::webview::PlatformWebview, back: bool) {
|
|
36
|
+
use objc2::msg_send;
|
|
37
|
+
use objc2::runtime::AnyObject;
|
|
38
|
+
|
|
39
|
+
let ptr = webview.inner() as *mut AnyObject;
|
|
40
|
+
if ptr.is_null() {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
unsafe {
|
|
44
|
+
if back {
|
|
45
|
+
let _: *mut AnyObject = msg_send![ptr, goBack];
|
|
46
|
+
} else {
|
|
47
|
+
let _: *mut AnyObject = msg_send![ptr, goForward];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
#[cfg(target_os = "linux")]
|
|
53
|
+
fn history_step_platform(webview: &tauri::webview::PlatformWebview, back: bool) {
|
|
54
|
+
use webkit2gtk::WebViewExt;
|
|
55
|
+
|
|
56
|
+
let gtk_webview = webview.inner();
|
|
57
|
+
if back {
|
|
58
|
+
gtk_webview.go_back();
|
|
59
|
+
} else {
|
|
60
|
+
gtk_webview.go_forward();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#[cfg(windows)]
|
|
65
|
+
fn history_step_platform(webview: &tauri::webview::PlatformWebview, back: bool) {
|
|
66
|
+
use webview2_com::Microsoft::Web::WebView2::Win32::ICoreWebView2Controller;
|
|
67
|
+
|
|
68
|
+
let controller = webview.controller();
|
|
69
|
+
unsafe {
|
|
70
|
+
// ICoreWebView2Controller::CoreWebView2 returns the underlying browser.
|
|
71
|
+
let Ok(core) = controller.CoreWebView2() else {
|
|
72
|
+
return;
|
|
73
|
+
};
|
|
74
|
+
if back {
|
|
75
|
+
let _ = core.GoBack();
|
|
76
|
+
} else {
|
|
77
|
+
let _ = core.GoForward();
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
#[cfg(not(any(target_os = "macos", target_os = "linux", windows)))]
|
|
83
|
+
fn history_step_platform(_webview: &tauri::webview::PlatformWebview, _back: bool) {}
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
use crate::app::window::{
|
|
1
|
+
use crate::app::window::{
|
|
2
|
+
hide_all_app_windows, open_additional_window_safe, show_all_app_windows, toggle_all_app_windows,
|
|
3
|
+
};
|
|
4
|
+
use crate::cancel_startup_reveal;
|
|
2
5
|
use std::str::FromStr;
|
|
3
|
-
use std::sync::{Arc, Mutex};
|
|
6
|
+
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
|
4
7
|
use std::time::{Duration, Instant};
|
|
5
8
|
use tauri::{
|
|
6
9
|
menu::{MenuBuilder, MenuItemBuilder},
|
|
7
|
-
tray::{TrayIconBuilder, TrayIconEvent},
|
|
10
|
+
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
|
8
11
|
AppHandle, Manager,
|
|
9
12
|
};
|
|
10
13
|
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
|
@@ -16,6 +19,7 @@ pub fn set_system_tray(
|
|
|
16
19
|
tray_icon_path: &str,
|
|
17
20
|
_init_fullscreen: bool,
|
|
18
21
|
allow_multi_window: bool,
|
|
22
|
+
startup_revealed: Arc<AtomicBool>,
|
|
19
23
|
) -> tauri::Result<()> {
|
|
20
24
|
if !show_system_tray {
|
|
21
25
|
app.remove_tray_by_id("pake-tray");
|
|
@@ -42,6 +46,8 @@ pub fn set_system_tray(
|
|
|
42
46
|
|
|
43
47
|
app.app_handle().remove_tray_by_id("pake-tray");
|
|
44
48
|
|
|
49
|
+
let menu_revealed = startup_revealed.clone();
|
|
50
|
+
let click_revealed = startup_revealed;
|
|
45
51
|
let mut tray_builder = TrayIconBuilder::new()
|
|
46
52
|
.menu(&menu)
|
|
47
53
|
.on_menu_event(move |app, event| match event.id().as_ref() {
|
|
@@ -49,20 +55,13 @@ pub fn set_system_tray(
|
|
|
49
55
|
open_additional_window_safe(app);
|
|
50
56
|
}
|
|
51
57
|
"hide_app" => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
58
|
+
// Hide every webview (main + multi-window clones), not only "pake".
|
|
59
|
+
cancel_startup_reveal(&menu_revealed);
|
|
60
|
+
hide_all_app_windows(app);
|
|
55
61
|
}
|
|
56
62
|
"show_app" => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
reapply_window_icon(&window);
|
|
60
|
-
#[cfg(target_os = "linux")]
|
|
61
|
-
if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
62
|
-
let _ = window.set_fullscreen(true);
|
|
63
|
-
let _ = window.set_focus();
|
|
64
|
-
}
|
|
65
|
-
}
|
|
63
|
+
cancel_startup_reveal(&menu_revealed);
|
|
64
|
+
show_all_app_windows(app, _init_fullscreen);
|
|
66
65
|
}
|
|
67
66
|
"quit" => {
|
|
68
67
|
let flags = if _init_fullscreen {
|
|
@@ -76,22 +75,19 @@ pub fn set_system_tray(
|
|
|
76
75
|
_ => (),
|
|
77
76
|
})
|
|
78
77
|
.on_tray_icon_event(move |tray, event| {
|
|
79
|
-
if let TrayIconEvent::Click {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
}
|
|
78
|
+
if let TrayIconEvent::Click {
|
|
79
|
+
button,
|
|
80
|
+
button_state,
|
|
81
|
+
..
|
|
82
|
+
} = event
|
|
83
|
+
{
|
|
84
|
+
// Windows emits Click twice per physical click (Down then Up).
|
|
85
|
+
// Reacting to both runs the toggle twice, so a hidden window is
|
|
86
|
+
// shown and immediately re-hidden and the tray looks dead (#1343).
|
|
87
|
+
if button == MouseButton::Left && button_state == MouseButtonState::Up {
|
|
88
|
+
// Any tray toggle claims visibility control from startup reveal.
|
|
89
|
+
cancel_startup_reveal(&click_revealed);
|
|
90
|
+
toggle_all_app_windows(tray.app_handle(), _init_fullscreen);
|
|
95
91
|
}
|
|
96
92
|
}
|
|
97
93
|
});
|
|
@@ -120,6 +116,7 @@ pub fn set_global_shortcut(
|
|
|
120
116
|
app: &AppHandle,
|
|
121
117
|
shortcut: String,
|
|
122
118
|
_init_fullscreen: bool,
|
|
119
|
+
startup_revealed: Arc<AtomicBool>,
|
|
123
120
|
) -> tauri::Result<()> {
|
|
124
121
|
if shortcut.is_empty() {
|
|
125
122
|
return Ok(());
|
|
@@ -139,6 +136,7 @@ pub fn set_global_shortcut(
|
|
|
139
136
|
tauri_plugin_global_shortcut::Builder::new()
|
|
140
137
|
.with_handler({
|
|
141
138
|
let last_triggered = Arc::clone(&last_triggered);
|
|
139
|
+
let startup_revealed = startup_revealed.clone();
|
|
142
140
|
move |app, event, _shortcut| {
|
|
143
141
|
let Ok(mut last_triggered) = last_triggered.lock() else {
|
|
144
142
|
return;
|
|
@@ -149,20 +147,8 @@ pub fn set_global_shortcut(
|
|
|
149
147
|
*last_triggered = Instant::now();
|
|
150
148
|
|
|
151
149
|
if shortcut_hotkey.eq(event) {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
if is_visible {
|
|
155
|
-
let _ = window.hide();
|
|
156
|
-
} else {
|
|
157
|
-
let _ = window.show();
|
|
158
|
-
reapply_window_icon(&window);
|
|
159
|
-
let _ = window.set_focus();
|
|
160
|
-
#[cfg(target_os = "linux")]
|
|
161
|
-
if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
162
|
-
let _ = window.set_fullscreen(true);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
150
|
+
cancel_startup_reveal(&startup_revealed);
|
|
151
|
+
toggle_all_app_windows(app, _init_fullscreen);
|
|
166
152
|
}
|
|
167
153
|
}
|
|
168
154
|
})
|
|
@@ -186,15 +186,21 @@ fn open_requested_window(
|
|
|
186
186
|
Ok(window)
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
+
/// Open a multi-window clone of the home app. The window is built hidden and
|
|
190
|
+
/// revealed on its first real page load (see `lib.rs` on_page_load) so Cmd+N
|
|
191
|
+
/// does not flash an empty shell the way the main window used to.
|
|
189
192
|
pub fn open_additional_window_safe(app: &AppHandle) {
|
|
190
193
|
#[cfg(target_os = "windows")]
|
|
191
194
|
{
|
|
192
195
|
let app_handle = app.clone();
|
|
193
196
|
std::thread::spawn(move || {
|
|
194
197
|
if let Ok(window) = open_additional_window(&app_handle) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
+
// Fallback if PageLoadEvent::Finished never arrives.
|
|
199
|
+
let fallback = window.clone();
|
|
200
|
+
tauri::async_runtime::spawn(async move {
|
|
201
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
|
|
202
|
+
reveal_built_window(&fallback);
|
|
203
|
+
});
|
|
198
204
|
}
|
|
199
205
|
});
|
|
200
206
|
}
|
|
@@ -202,13 +208,79 @@ pub fn open_additional_window_safe(app: &AppHandle) {
|
|
|
202
208
|
#[cfg(not(target_os = "windows"))]
|
|
203
209
|
{
|
|
204
210
|
if let Ok(window) = open_additional_window(app) {
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
211
|
+
let fallback = window.clone();
|
|
212
|
+
tauri::async_runtime::spawn(async move {
|
|
213
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
|
|
214
|
+
reveal_built_window(&fallback);
|
|
215
|
+
});
|
|
208
216
|
}
|
|
209
217
|
}
|
|
210
218
|
}
|
|
211
219
|
|
|
220
|
+
/// Show a window that was built hidden once content is ready (or the fallback
|
|
221
|
+
/// timer fires). No-ops when already visible so page-load and fallback can race.
|
|
222
|
+
pub fn reveal_built_window(window: &WebviewWindow) {
|
|
223
|
+
if window.is_visible().unwrap_or(true) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
let _ = window.show();
|
|
227
|
+
reapply_window_icon(window);
|
|
228
|
+
let _ = window.set_focus();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/// True when any Pake webview window is currently on screen.
|
|
232
|
+
///
|
|
233
|
+
/// A minimized window does not count. Windows keeps `IsWindowVisible` true while
|
|
234
|
+
/// a window is iconic, and `hide_on_close` minimizes before hiding, so treating
|
|
235
|
+
/// minimized as visible makes the tray toggle hide an already-invisible window
|
|
236
|
+
/// instead of restoring it (#1343).
|
|
237
|
+
pub fn any_app_window_visible(app: &AppHandle) -> bool {
|
|
238
|
+
app.webview_windows().values().any(|window| {
|
|
239
|
+
window.is_visible().unwrap_or(false) && !window.is_minimized().unwrap_or(false)
|
|
240
|
+
})
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/// Hide every webview window (main + multi-window clones). Used by tray Hide
|
|
244
|
+
/// and the activation shortcut so secondary windows are not left on screen.
|
|
245
|
+
pub fn hide_all_app_windows(app: &AppHandle) {
|
|
246
|
+
for window in app.webview_windows().values() {
|
|
247
|
+
let _ = window.hide();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// Show every webview window, reassert icons, and focus the main window.
|
|
252
|
+
pub fn show_all_app_windows(app: &AppHandle, init_fullscreen: bool) {
|
|
253
|
+
let windows = app.webview_windows();
|
|
254
|
+
for window in windows.values() {
|
|
255
|
+
let _ = window.unminimize();
|
|
256
|
+
let _ = window.show();
|
|
257
|
+
reapply_window_icon(window);
|
|
258
|
+
#[cfg(target_os = "linux")]
|
|
259
|
+
if init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
260
|
+
let _ = window.set_fullscreen(true);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
#[cfg(not(target_os = "linux"))]
|
|
264
|
+
let _ = init_fullscreen;
|
|
265
|
+
|
|
266
|
+
if let Some(main) = windows.get("pake") {
|
|
267
|
+
let _ = main.set_focus();
|
|
268
|
+
} else if let Some(any) = windows.values().next() {
|
|
269
|
+
let _ = any.set_focus();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Tray-click / activation-shortcut toggle: hide all if anything is visible,
|
|
274
|
+
/// otherwise show all. Cancels startup reveal when the caller has already done
|
|
275
|
+
/// so; this helper only touches visibility.
|
|
276
|
+
pub fn toggle_all_app_windows(app: &AppHandle, init_fullscreen: bool) {
|
|
277
|
+
if any_app_window_visible(app) {
|
|
278
|
+
hide_all_app_windows(app);
|
|
279
|
+
} else {
|
|
280
|
+
show_all_app_windows(app, init_fullscreen);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
212
284
|
fn build_window_with_label(
|
|
213
285
|
app: &AppHandle,
|
|
214
286
|
config: &PakeConfig,
|
|
@@ -553,7 +625,7 @@ fn build_window(
|
|
|
553
625
|
// catching it here is independent of the page CSP and the IPC channel.
|
|
554
626
|
{
|
|
555
627
|
let download_handle = app.clone();
|
|
556
|
-
window_builder = window_builder.on_download(move |
|
|
628
|
+
window_builder = window_builder.on_download(move |webview, event| match event {
|
|
557
629
|
DownloadEvent::Requested { url, destination } => {
|
|
558
630
|
match download_handle.path().download_dir() {
|
|
559
631
|
Ok(download_dir) => {
|
|
@@ -585,7 +657,12 @@ fn build_window(
|
|
|
585
657
|
path: _,
|
|
586
658
|
success,
|
|
587
659
|
} => {
|
|
588
|
-
|
|
660
|
+
// Toast on the window that started the download (including
|
|
661
|
+
// secondary multi-window labels), not a hard-coded "pake".
|
|
662
|
+
let toast_window = download_handle
|
|
663
|
+
.get_webview_window(webview.label())
|
|
664
|
+
.or_else(|| download_handle.get_webview_window("pake"));
|
|
665
|
+
if let Some(window) = toast_window {
|
|
589
666
|
let message_type = if success {
|
|
590
667
|
MessageType::Success
|
|
591
668
|
} else {
|
|
@@ -1,11 +1,35 @@
|
|
|
1
|
+
// Prefer native webview navigation over page JS so Ctrl+R / [ / ] still work
|
|
2
|
+
// on blank error shells (no JS context). Falls back to history/location when
|
|
3
|
+
// the IPC bridge is unavailable.
|
|
4
|
+
function nativeNavigate(action) {
|
|
5
|
+
const invoke = window.__TAURI__?.core?.invoke;
|
|
6
|
+
if (invoke) {
|
|
7
|
+
invoke("webview_navigate", { action }).catch(() => {
|
|
8
|
+
fallbackNavigate(action);
|
|
9
|
+
});
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
fallbackNavigate(action);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function fallbackNavigate(action) {
|
|
16
|
+
if (action === "reload") {
|
|
17
|
+
window.location.reload();
|
|
18
|
+
} else if (action === "back") {
|
|
19
|
+
window.history.back();
|
|
20
|
+
} else if (action === "forward") {
|
|
21
|
+
window.history.forward();
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
1
25
|
const shortcuts = {
|
|
2
|
-
"[": () =>
|
|
3
|
-
"]": () =>
|
|
26
|
+
"[": () => nativeNavigate("back"),
|
|
27
|
+
"]": () => nativeNavigate("forward"),
|
|
4
28
|
"-": () => zoomOut(),
|
|
5
29
|
"=": () => zoomIn(),
|
|
6
30
|
"+": () => zoomIn(),
|
|
7
31
|
0: () => setZoom("100%"),
|
|
8
|
-
r: () =>
|
|
32
|
+
r: () => nativeNavigate("reload"),
|
|
9
33
|
ArrowUp: () => scrollTo(0, 0),
|
|
10
34
|
ArrowDown: () => scrollTo(0, document.body.scrollHeight),
|
|
11
35
|
};
|
|
@@ -370,34 +394,12 @@ const DOWNLOADABLE_FILE_EXTENSIONS = {
|
|
|
370
394
|
"apk",
|
|
371
395
|
"ipa",
|
|
372
396
|
],
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
"sqlite",
|
|
380
|
-
"yaml",
|
|
381
|
-
"yml",
|
|
382
|
-
"toml",
|
|
383
|
-
"ini",
|
|
384
|
-
"cfg",
|
|
385
|
-
"conf",
|
|
386
|
-
"log",
|
|
387
|
-
],
|
|
388
|
-
code: [
|
|
389
|
-
"js",
|
|
390
|
-
"ts",
|
|
391
|
-
"jsx",
|
|
392
|
-
"tsx",
|
|
393
|
-
"css",
|
|
394
|
-
"scss",
|
|
395
|
-
"sass",
|
|
396
|
-
"less",
|
|
397
|
-
"sh",
|
|
398
|
-
"bat",
|
|
399
|
-
"ps1",
|
|
400
|
-
],
|
|
397
|
+
// Navigable web formats (json/xml/js/css/html and friends) are intentionally
|
|
398
|
+
// omitted: documentation and SPA content negotiation often serve them as
|
|
399
|
+
// in-app pages. Real file downloads still match via the download attribute,
|
|
400
|
+
// ?download / ?attachment, or binary extensions below.
|
|
401
|
+
data: ["csv", "sql", "db", "sqlite"],
|
|
402
|
+
scripts: ["sh", "bat", "ps1"],
|
|
401
403
|
fonts: ["ttf", "otf", "woff", "woff2", "eot"],
|
|
402
404
|
design: ["ai", "psd", "sketch", "fig", "xd"],
|
|
403
405
|
system: [
|
|
@@ -446,13 +448,12 @@ const PREVIEWABLE_MEDIA_EXTENSIONS = [
|
|
|
446
448
|
"m4a",
|
|
447
449
|
];
|
|
448
450
|
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
];
|
|
451
|
+
// Path fragments that often host real file downloads. Keep this list narrow:
|
|
452
|
+
// SPA roots such as "/assets/", "/dist/", "/files/", "/releases/", and
|
|
453
|
+
// "/attachments/" have already been mistaken for downloads in the wild.
|
|
454
|
+
// Prefer real file extensions, the download attribute, or ?download /
|
|
455
|
+
// ?attachment query hints whenever possible.
|
|
456
|
+
const DOWNLOAD_PATH_PATTERNS = ["/download/"];
|
|
456
457
|
|
|
457
458
|
// Language detection utilities
|
|
458
459
|
function getUserLanguage() {
|
|
@@ -528,6 +529,52 @@ function isDownloadableFile(url) {
|
|
|
528
529
|
}
|
|
529
530
|
}
|
|
530
531
|
|
|
532
|
+
// Public suffixes where the registrable domain needs more than two labels.
|
|
533
|
+
// Not exhaustive; covers common packaging targets that last-two-label
|
|
534
|
+
// matching would collapse incorrectly (e.g. amazon.co.uk vs evil.co.uk,
|
|
535
|
+
// or every *.github.io site into one "domain").
|
|
536
|
+
const MULTI_PART_PUBLIC_SUFFIXES = [
|
|
537
|
+
"co.uk",
|
|
538
|
+
"org.uk",
|
|
539
|
+
"ac.uk",
|
|
540
|
+
"gov.uk",
|
|
541
|
+
"com.au",
|
|
542
|
+
"net.au",
|
|
543
|
+
"org.au",
|
|
544
|
+
"co.jp",
|
|
545
|
+
"ne.jp",
|
|
546
|
+
"or.jp",
|
|
547
|
+
"co.kr",
|
|
548
|
+
"co.in",
|
|
549
|
+
"com.br",
|
|
550
|
+
"com.cn",
|
|
551
|
+
"com.tw",
|
|
552
|
+
"com.hk",
|
|
553
|
+
"com.sg",
|
|
554
|
+
"github.io",
|
|
555
|
+
"gitlab.io",
|
|
556
|
+
"pages.dev",
|
|
557
|
+
];
|
|
558
|
+
|
|
559
|
+
function getRootDomain(hostname) {
|
|
560
|
+
const normalized = String(hostname || "").toLowerCase();
|
|
561
|
+
if (!normalized) {
|
|
562
|
+
return "";
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
const parts = normalized.split(".").filter(Boolean);
|
|
566
|
+
if (parts.length <= 1) {
|
|
567
|
+
return normalized;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const lastTwo = parts.slice(-2).join(".");
|
|
571
|
+
if (MULTI_PART_PUBLIC_SUFFIXES.includes(lastTwo) && parts.length >= 3) {
|
|
572
|
+
return parts.slice(-3).join(".");
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return lastTwo;
|
|
576
|
+
}
|
|
577
|
+
|
|
531
578
|
function normalizeAnchorHref(rawHref) {
|
|
532
579
|
return typeof rawHref === "string" ? rawHref.trim() : "";
|
|
533
580
|
}
|
|
@@ -661,8 +708,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
661
708
|
const isSpecialDownload = (url) =>
|
|
662
709
|
["blob", "data"].some((protocol) => url.startsWith(protocol));
|
|
663
710
|
|
|
664
|
-
|
|
665
|
-
|
|
711
|
+
// Cmd/Ctrl+click is a browser "open related" gesture, not "save as".
|
|
712
|
+
// Only the download attribute and downloadable-file heuristics force a
|
|
713
|
+
// download; modifiers must not rewrite ordinary navigation.
|
|
714
|
+
const isDownloadRequired = (url, anchorElement, _e) =>
|
|
715
|
+
Boolean(anchorElement.download) || isDownloadableFile(url);
|
|
666
716
|
|
|
667
717
|
const handleExternalLink = (url) => {
|
|
668
718
|
// Don't try to open blob: or data: URLs with shell
|
|
@@ -686,12 +736,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
686
736
|
|
|
687
737
|
if (linkUrl.hostname === currentUrl.hostname) return true;
|
|
688
738
|
|
|
689
|
-
//
|
|
690
|
-
|
|
691
|
-
const parts = hostname.split(".");
|
|
692
|
-
return parts.length >= 2 ? parts.slice(-2).join(".") : hostname;
|
|
693
|
-
};
|
|
694
|
-
|
|
739
|
+
// e.g. www.bilibili.com and m.bilibili.com share bilibili.com;
|
|
740
|
+
// amazon.co.uk must not share a root with evil.co.uk.
|
|
695
741
|
return (
|
|
696
742
|
getRootDomain(currentUrl.hostname) === getRootDomain(linkUrl.hostname)
|
|
697
743
|
);
|
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")]
|
|
@@ -22,13 +28,65 @@ const GDK_BACKEND: &str = "GDK_BACKEND";
|
|
|
22
28
|
use app::{
|
|
23
29
|
invoke::{
|
|
24
30
|
clear_dock_badge, download_file, increment_dock_badge, send_notification, set_dock_badge,
|
|
25
|
-
set_dock_badge_label, set_zoom, update_theme_mode,
|
|
31
|
+
set_dock_badge_label, set_zoom, update_theme_mode, webview_navigate,
|
|
26
32
|
},
|
|
27
33
|
setup::{set_global_shortcut, set_system_tray},
|
|
28
|
-
window::{
|
|
34
|
+
window::{
|
|
35
|
+
open_additional_window_safe, reapply_window_icon, reveal_built_window, set_window,
|
|
36
|
+
MultiWindowState,
|
|
37
|
+
},
|
|
29
38
|
};
|
|
30
39
|
use util::get_pake_config;
|
|
31
40
|
|
|
41
|
+
/// Placeholder documents used before the real target URL navigates (e.g. macOS
|
|
42
|
+
/// cert-bypass starts on about:blank). Revealing on these would reintroduce the
|
|
43
|
+
/// blank-window flash the page-load gate is meant to prevent.
|
|
44
|
+
fn is_placeholder_startup_url(url: &Url) -> bool {
|
|
45
|
+
url.scheme().eq_ignore_ascii_case("about")
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/// First automatic reveal wins. Returns true if the caller should show the window.
|
|
49
|
+
fn claim_startup_reveal(revealed: &AtomicBool) -> bool {
|
|
50
|
+
!revealed.swap(true, Ordering::AcqRel)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// User took control of main-window visibility (tray, shortcut, dock, second
|
|
54
|
+
/// instance, hide-on-close). Drop any pending page-load / fallback reveal so a
|
|
55
|
+
/// slow cold start cannot re-open a window the user just hid.
|
|
56
|
+
pub(crate) fn cancel_startup_reveal(revealed: &AtomicBool) {
|
|
57
|
+
revealed.store(true, Ordering::Release);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
fn reveal_startup_window(window: WebviewWindow, init_fullscreen: bool, revealed: &Arc<AtomicBool>) {
|
|
61
|
+
if !claim_startup_reveal(revealed) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
tauri::async_runtime::spawn(async move {
|
|
66
|
+
let _ = window.show();
|
|
67
|
+
reapply_window_icon(&window);
|
|
68
|
+
|
|
69
|
+
// Fixed: Linux fullscreen issue with virtual keyboard
|
|
70
|
+
#[cfg(target_os = "linux")]
|
|
71
|
+
{
|
|
72
|
+
if init_fullscreen {
|
|
73
|
+
let _ = window.set_fullscreen(true);
|
|
74
|
+
// Ensure webview maintains focus for input after fullscreen
|
|
75
|
+
let _ = window.set_focus();
|
|
76
|
+
} else {
|
|
77
|
+
// Fix: Ubuntu 24.04/GNOME window buttons non-functional until resize (#1122)
|
|
78
|
+
// The window manager needs time to process the MapWindow event before
|
|
79
|
+
// accepting focus requests. Without this, decorations remain non-interactive.
|
|
80
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(30)).await;
|
|
81
|
+
let _ = window.set_focus();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[cfg(not(target_os = "linux"))]
|
|
86
|
+
let _ = init_fullscreen;
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
32
90
|
#[cfg(any(target_os = "linux", test))]
|
|
33
91
|
fn is_disabled_env_value(value: &str) -> bool {
|
|
34
92
|
matches!(
|
|
@@ -149,6 +207,7 @@ pub fn run_app() {
|
|
|
149
207
|
let multi_instance = pake_config.multi_instance;
|
|
150
208
|
let multi_window = pake_config.multi_window;
|
|
151
209
|
let _enable_find = pake_config.windows[0].enable_find;
|
|
210
|
+
let startup_window_revealed = Arc::new(AtomicBool::new(false));
|
|
152
211
|
|
|
153
212
|
let window_state_plugin = WindowStatePlugin::default()
|
|
154
213
|
.with_state_flags(if init_fullscreen {
|
|
@@ -172,11 +231,13 @@ pub fn run_app() {
|
|
|
172
231
|
|
|
173
232
|
// Only add single instance plugin if multiple instances are not allowed
|
|
174
233
|
if !multi_instance {
|
|
234
|
+
let instance_revealed = startup_window_revealed.clone();
|
|
175
235
|
app_builder = app_builder.plugin(tauri_plugin_single_instance::init(
|
|
176
236
|
move |app, _args, _cwd| {
|
|
177
237
|
if multi_window {
|
|
178
238
|
open_additional_window_safe(app);
|
|
179
239
|
} else if let Some(window) = app.get_webview_window("pake") {
|
|
240
|
+
cancel_startup_reveal(&instance_revealed);
|
|
180
241
|
let _ = window.unminimize();
|
|
181
242
|
let _ = window.show();
|
|
182
243
|
reapply_window_icon(&window);
|
|
@@ -186,6 +247,49 @@ pub fn run_app() {
|
|
|
186
247
|
));
|
|
187
248
|
}
|
|
188
249
|
|
|
250
|
+
// Reveal hidden windows after the first real document finishes loading so
|
|
251
|
+
// slow WKWebView cold starts do not expose an empty but interactive shell.
|
|
252
|
+
// - Main label "pake": once-only latch + start_to_tray opt-out.
|
|
253
|
+
// - Secondary multi-window labels ("pake-N"): reveal if still hidden (Cmd+N).
|
|
254
|
+
// start_to_tray keeps the main window hidden until the user opens it.
|
|
255
|
+
{
|
|
256
|
+
let page_load_revealed = startup_window_revealed.clone();
|
|
257
|
+
app_builder = app_builder.on_page_load(move |webview, payload| {
|
|
258
|
+
if !matches!(payload.event(), PageLoadEvent::Finished) {
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
// Skip about:blank (and other about: placeholders) used by the macOS
|
|
262
|
+
// cert-bypass path before the real target URL navigates.
|
|
263
|
+
if is_placeholder_startup_url(payload.url()) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let label = webview.label();
|
|
268
|
+
if label == "pake" {
|
|
269
|
+
if start_to_tray {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if let Some(window) = webview.app_handle().get_webview_window("pake") {
|
|
273
|
+
reveal_startup_window(window, init_fullscreen, &page_load_revealed);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Multi-window clones (pake-1, pake-2, โฆ) built hidden by
|
|
279
|
+
// open_additional_window_safe.
|
|
280
|
+
if label.starts_with("pake-") {
|
|
281
|
+
if let Some(window) = webview.app_handle().get_webview_window(label) {
|
|
282
|
+
reveal_built_window(&window);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Clone before setup moves the Arc into tray / shortcut / fallback handlers.
|
|
289
|
+
let close_revealed = startup_window_revealed.clone();
|
|
290
|
+
#[cfg(target_os = "macos")]
|
|
291
|
+
let reopen_revealed = startup_window_revealed.clone();
|
|
292
|
+
|
|
189
293
|
app_builder
|
|
190
294
|
.invoke_handler(tauri::generate_handler![
|
|
191
295
|
download_file,
|
|
@@ -196,6 +300,7 @@ pub fn run_app() {
|
|
|
196
300
|
clear_dock_badge,
|
|
197
301
|
update_theme_mode,
|
|
198
302
|
set_zoom,
|
|
303
|
+
webview_navigate,
|
|
199
304
|
])
|
|
200
305
|
.setup(move |app| {
|
|
201
306
|
app.manage(MultiWindowState::new(
|
|
@@ -222,34 +327,30 @@ pub fn run_app() {
|
|
|
222
327
|
&pake_config.system_tray_path,
|
|
223
328
|
init_fullscreen,
|
|
224
329
|
multi_window,
|
|
330
|
+
startup_window_revealed.clone(),
|
|
331
|
+
)?;
|
|
332
|
+
set_global_shortcut(
|
|
333
|
+
app.app_handle(),
|
|
334
|
+
activation_shortcut,
|
|
335
|
+
init_fullscreen,
|
|
336
|
+
startup_window_revealed.clone(),
|
|
225
337
|
)?;
|
|
226
|
-
set_global_shortcut(app.app_handle(), activation_shortcut, init_fullscreen)?;
|
|
227
338
|
|
|
228
339
|
// Show window after state restoration to prevent position flashing
|
|
229
|
-
//
|
|
340
|
+
// once its first page finishes. A fallback keeps offline or stalled
|
|
341
|
+
// pages reachable without exposing a blank webview during normal startup.
|
|
230
342
|
if !start_to_tray {
|
|
231
343
|
let window_clone = window.clone();
|
|
232
344
|
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
|
-
}
|
|
345
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(
|
|
346
|
+
STARTUP_WINDOW_FALLBACK_DELAY,
|
|
347
|
+
))
|
|
348
|
+
.await;
|
|
349
|
+
reveal_startup_window(window_clone, init_fullscreen, &startup_window_revealed);
|
|
252
350
|
});
|
|
351
|
+
} else {
|
|
352
|
+
// Tray/shortcut already hold clones that cancel user-driven toggles.
|
|
353
|
+
drop(startup_window_revealed);
|
|
253
354
|
}
|
|
254
355
|
|
|
255
356
|
Ok(())
|
|
@@ -257,6 +358,8 @@ pub fn run_app() {
|
|
|
257
358
|
.on_window_event(move |_window, _event| {
|
|
258
359
|
if let tauri::WindowEvent::CloseRequested { api, .. } = _event {
|
|
259
360
|
if hide_on_close && _window.label() == "pake" {
|
|
361
|
+
// User dismissed the window; do not let startup reveal reopen it.
|
|
362
|
+
cancel_startup_reveal(&close_revealed);
|
|
260
363
|
// Hide window when hide_on_close is enabled (regardless of tray status)
|
|
261
364
|
let window = _window.clone();
|
|
262
365
|
tauri::async_runtime::spawn(async move {
|
|
@@ -291,7 +394,7 @@ pub fn run_app() {
|
|
|
291
394
|
eprintln!("[Pake] Fatal error while building Tauri application: {error}");
|
|
292
395
|
std::process::exit(1);
|
|
293
396
|
})
|
|
294
|
-
.run(|_app, _event| {
|
|
397
|
+
.run(move |_app, _event| {
|
|
295
398
|
// Handle macOS dock icon click to reopen hidden window
|
|
296
399
|
#[cfg(target_os = "macos")]
|
|
297
400
|
if let tauri::RunEvent::Reopen {
|
|
@@ -301,6 +404,7 @@ pub fn run_app() {
|
|
|
301
404
|
{
|
|
302
405
|
if !has_visible_windows {
|
|
303
406
|
if let Some(window) = _app.get_webview_window("pake") {
|
|
407
|
+
cancel_startup_reveal(&reopen_revealed);
|
|
304
408
|
let _ = window.show();
|
|
305
409
|
reapply_window_icon(&window);
|
|
306
410
|
let _ = window.set_focus();
|
|
@@ -318,6 +422,46 @@ pub fn run() {
|
|
|
318
422
|
mod tests {
|
|
319
423
|
use super::*;
|
|
320
424
|
|
|
425
|
+
#[test]
|
|
426
|
+
fn placeholder_startup_urls_cover_about_blank() {
|
|
427
|
+
let blank: Url = "about:blank".parse().unwrap();
|
|
428
|
+
let srcdoc: Url = "about:srcdoc".parse().unwrap();
|
|
429
|
+
let https: Url = "https://github.com/".parse().unwrap();
|
|
430
|
+
let tauri: Url = "tauri://localhost/".parse().unwrap();
|
|
431
|
+
|
|
432
|
+
assert!(is_placeholder_startup_url(&blank));
|
|
433
|
+
assert!(is_placeholder_startup_url(&srcdoc));
|
|
434
|
+
assert!(!is_placeholder_startup_url(&https));
|
|
435
|
+
assert!(!is_placeholder_startup_url(&tauri));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
#[test]
|
|
439
|
+
fn first_claim_wins_startup_reveal() {
|
|
440
|
+
let revealed = AtomicBool::new(false);
|
|
441
|
+
assert!(claim_startup_reveal(&revealed));
|
|
442
|
+
assert!(!claim_startup_reveal(&revealed));
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
#[test]
|
|
446
|
+
fn user_show_then_hide_blocks_automatic_startup_reveal() {
|
|
447
|
+
// Slow page load: user opens from tray/shortcut, then hides again.
|
|
448
|
+
// Page-load finish and the 3s fallback must not reopen the window.
|
|
449
|
+
let revealed = AtomicBool::new(false);
|
|
450
|
+
cancel_startup_reveal(&revealed); // explicit show
|
|
451
|
+
cancel_startup_reveal(&revealed); // explicit hide
|
|
452
|
+
assert!(
|
|
453
|
+
!claim_startup_reveal(&revealed),
|
|
454
|
+
"automatic reveal must stay cancelled after user visibility control"
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
#[test]
|
|
459
|
+
fn cancel_before_any_claim_blocks_reveal() {
|
|
460
|
+
let revealed = AtomicBool::new(false);
|
|
461
|
+
cancel_startup_reveal(&revealed);
|
|
462
|
+
assert!(!claim_startup_reveal(&revealed));
|
|
463
|
+
}
|
|
464
|
+
|
|
321
465
|
#[test]
|
|
322
466
|
fn linux_webkit_safe_mode_stays_on_by_default() {
|
|
323
467
|
assert!(should_enable_linux_webkit_safe_mode_from_values(
|