pake-cli 3.15.5 โ 3.15.7
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 +60 -15
- package/package.json +7 -7
- package/src-tauri/Cargo.lock +9 -6
- package/src-tauri/Cargo.toml +14 -1
- package/src-tauri/src/app/invoke.rs +59 -10
- 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 +24 -49
- package/src-tauri/src/app/window.rs +109 -26
- package/src-tauri/src/inject/event.js +92 -45
- package/src-tauri/src/lib.rs +29 -11
- 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
|
@@ -14,16 +14,14 @@ import fs from 'fs';
|
|
|
14
14
|
import fs$1 from 'fs/promises';
|
|
15
15
|
import { dir } from 'tmp-promise';
|
|
16
16
|
import { fileTypeFromBuffer } from 'file-type';
|
|
17
|
-
import icongen from 'icon-gen';
|
|
18
|
-
import sharp from 'sharp';
|
|
19
17
|
import * as psl from 'psl';
|
|
20
18
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
21
19
|
|
|
22
20
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.15.
|
|
21
|
+
var version = "3.15.7";
|
|
24
22
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
23
|
var engines = {
|
|
26
|
-
node: ">=
|
|
24
|
+
node: ">=20.9.0"
|
|
27
25
|
};
|
|
28
26
|
var packageManager = "pnpm@10.26.2";
|
|
29
27
|
var bin = {
|
|
@@ -78,14 +76,13 @@ var dependencies = {
|
|
|
78
76
|
chalk: "^5.6.2",
|
|
79
77
|
commander: "^14.0.3",
|
|
80
78
|
execa: "^9.6.1",
|
|
81
|
-
"file-type": "^21.3.
|
|
79
|
+
"file-type": "^21.3.4",
|
|
82
80
|
"fs-extra": "^11.3.3",
|
|
83
|
-
"icon-gen": "^5.0.0",
|
|
84
81
|
loglevel: "^1.9.2",
|
|
85
82
|
ora: "^9.3.0",
|
|
86
83
|
prompts: "^2.4.2",
|
|
87
84
|
psl: "^1.15.0",
|
|
88
|
-
sharp: "^0.
|
|
85
|
+
sharp: "^0.35.0",
|
|
89
86
|
"tmp-promise": "^3.0.3",
|
|
90
87
|
"update-notifier": "^7.3.1"
|
|
91
88
|
};
|
|
@@ -111,8 +108,9 @@ var devDependencies = {
|
|
|
111
108
|
};
|
|
112
109
|
var pnpm = {
|
|
113
110
|
overrides: {
|
|
114
|
-
sharp: "^0.
|
|
115
|
-
"@img/sharp-libvips-darwin-arm64": "1.
|
|
111
|
+
sharp: "^0.35.0",
|
|
112
|
+
"@img/sharp-libvips-darwin-arm64": "1.3.0",
|
|
113
|
+
tmp: "0.2.7"
|
|
116
114
|
},
|
|
117
115
|
onlyBuiltDependencies: [
|
|
118
116
|
"esbuild",
|
|
@@ -2113,6 +2111,9 @@ function getIconSourcePriority(url, appName) {
|
|
|
2113
2111
|
: ['domain', 'dashboard'];
|
|
2114
2112
|
}
|
|
2115
2113
|
|
|
2114
|
+
async function loadSharp$1() {
|
|
2115
|
+
return (await import('sharp')).default;
|
|
2116
|
+
}
|
|
2116
2117
|
const ICO_HEADER_SIZE = 6;
|
|
2117
2118
|
const ICO_DIR_ENTRY_SIZE = 16;
|
|
2118
2119
|
const ICO_TYPE_ICON = 1;
|
|
@@ -2246,6 +2247,7 @@ async function pickLargestFrameAsPng(buffer, entries) {
|
|
|
2246
2247
|
// Fallback: let sharp render directly from the ICO buffer. sharp picks the
|
|
2247
2248
|
// largest embedded frame on its own.
|
|
2248
2249
|
try {
|
|
2250
|
+
const sharp = await loadSharp$1();
|
|
2249
2251
|
return await sharp(buffer).png().toBuffer();
|
|
2250
2252
|
}
|
|
2251
2253
|
catch {
|
|
@@ -2267,6 +2269,7 @@ async function ensureMultiResolutionIco(sourcePath, outputPath, preferredSize =
|
|
|
2267
2269
|
if (!sourcePng) {
|
|
2268
2270
|
return await writeIcoWithPreferredSize(sourcePath, outputPath, preferredSize);
|
|
2269
2271
|
}
|
|
2272
|
+
const sharp = await loadSharp$1();
|
|
2270
2273
|
const frames = await Promise.all(desiredSizes.map(async (size) => {
|
|
2271
2274
|
// Reuse an existing exact-size PNG frame when possible to keep any
|
|
2272
2275
|
// hand-tuned small icon (e.g. a 16x16 with deliberate pixel hinting).
|
|
@@ -2333,6 +2336,9 @@ function buildIcoFromPngBuffers(frames) {
|
|
|
2333
2336
|
return output;
|
|
2334
2337
|
}
|
|
2335
2338
|
|
|
2339
|
+
async function loadSharp() {
|
|
2340
|
+
return (await import('sharp')).default;
|
|
2341
|
+
}
|
|
2336
2342
|
const ICON_CONFIG = {
|
|
2337
2343
|
minFileSize: 100,
|
|
2338
2344
|
supportedFormats: [
|
|
@@ -2353,8 +2359,20 @@ const ICON_CONFIG = {
|
|
|
2353
2359
|
const PLATFORM_CONFIG = {
|
|
2354
2360
|
win: { format: '.ico', sizes: [...WIN_STANDARD_ICO_SIZES] },
|
|
2355
2361
|
linux: { format: '.png', size: 512 },
|
|
2356
|
-
macos: { format: '.icns'
|
|
2362
|
+
macos: { format: '.icns' },
|
|
2357
2363
|
};
|
|
2364
|
+
const MACOS_ICONSET_FILES = [
|
|
2365
|
+
['icon_16x16.png', 16],
|
|
2366
|
+
['icon_16x16@2x.png', 32],
|
|
2367
|
+
['icon_32x32.png', 32],
|
|
2368
|
+
['icon_32x32@2x.png', 64],
|
|
2369
|
+
['icon_128x128.png', 128],
|
|
2370
|
+
['icon_128x128@2x.png', 256],
|
|
2371
|
+
['icon_256x256.png', 256],
|
|
2372
|
+
['icon_256x256@2x.png', 512],
|
|
2373
|
+
['icon_512x512.png', 512],
|
|
2374
|
+
['icon_512x512@2x.png', 1024],
|
|
2375
|
+
];
|
|
2358
2376
|
const API_KEYS = {
|
|
2359
2377
|
logoDev: ['pk_JLLMUKGZRpaG5YclhXaTkg', 'pk_Ph745P8mQSeYFfW2Wk039A'],
|
|
2360
2378
|
brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
|
|
@@ -2413,6 +2431,7 @@ async function preprocessIcon(inputPath) {
|
|
|
2413
2431
|
if (!shouldNormalize) {
|
|
2414
2432
|
return inputPath;
|
|
2415
2433
|
}
|
|
2434
|
+
const sharp = await loadSharp();
|
|
2416
2435
|
const { path: tempDir } = await dir();
|
|
2417
2436
|
const outputPath = path.join(tempDir, 'icon-normalized.png');
|
|
2418
2437
|
await sharp(inputPath).ensureAlpha().png().toFile(outputPath);
|
|
@@ -2430,6 +2449,7 @@ async function preprocessIcon(inputPath) {
|
|
|
2430
2449
|
*/
|
|
2431
2450
|
async function applyMacOSMask(inputPath) {
|
|
2432
2451
|
try {
|
|
2452
|
+
const sharp = await loadSharp();
|
|
2433
2453
|
const { path: tempDir } = await dir();
|
|
2434
2454
|
const outputPath = path.join(tempDir, 'icon-macos-rounded.png');
|
|
2435
2455
|
// 1. Create a 1024x1024 rounded rect mask
|
|
@@ -2473,6 +2493,32 @@ async function applyMacOSMask(inputPath) {
|
|
|
2473
2493
|
return inputPath;
|
|
2474
2494
|
}
|
|
2475
2495
|
}
|
|
2496
|
+
async function generateMacOSIcns(inputPath, outputDir, iconName) {
|
|
2497
|
+
const sharp = await loadSharp();
|
|
2498
|
+
const iconsetPath = path.join(outputDir, `${iconName}.iconset`);
|
|
2499
|
+
const outputPath = path.join(outputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
|
|
2500
|
+
await fsExtra.ensureDir(iconsetPath);
|
|
2501
|
+
const source = sharp(inputPath);
|
|
2502
|
+
await Promise.all(MACOS_ICONSET_FILES.map(async ([fileName, size]) => {
|
|
2503
|
+
await source
|
|
2504
|
+
.clone()
|
|
2505
|
+
.resize(size, size, {
|
|
2506
|
+
fit: 'contain',
|
|
2507
|
+
background: ICON_CONFIG.transparentBackground,
|
|
2508
|
+
})
|
|
2509
|
+
.ensureAlpha()
|
|
2510
|
+
.png()
|
|
2511
|
+
.toFile(path.join(iconsetPath, fileName));
|
|
2512
|
+
}));
|
|
2513
|
+
await execa('/usr/bin/iconutil', [
|
|
2514
|
+
'-c',
|
|
2515
|
+
'icns',
|
|
2516
|
+
iconsetPath,
|
|
2517
|
+
'-o',
|
|
2518
|
+
outputPath,
|
|
2519
|
+
]);
|
|
2520
|
+
return outputPath;
|
|
2521
|
+
}
|
|
2476
2522
|
/**
|
|
2477
2523
|
* Converts icon to platform-specific format
|
|
2478
2524
|
*/
|
|
@@ -2487,6 +2533,7 @@ async function convertIconFormat(inputPath, appName) {
|
|
|
2487
2533
|
const iconName = getIconBaseName(appName);
|
|
2488
2534
|
// Generate platform-specific format
|
|
2489
2535
|
if (IS_WIN) {
|
|
2536
|
+
const sharp = await loadSharp();
|
|
2490
2537
|
const icoPath = path.join(platformOutputDir, `${iconName}_256${PLATFORM_CONFIG.win.format}`);
|
|
2491
2538
|
const sourceBuffer = await fsExtra.readFile(processedInputPath);
|
|
2492
2539
|
const frames = await Promise.all(PLATFORM_CONFIG.win.sizes.map(async (size) => {
|
|
@@ -2505,6 +2552,7 @@ async function convertIconFormat(inputPath, appName) {
|
|
|
2505
2552
|
return icoPath;
|
|
2506
2553
|
}
|
|
2507
2554
|
if (IS_LINUX) {
|
|
2555
|
+
const sharp = await loadSharp();
|
|
2508
2556
|
const outputPath = path.join(platformOutputDir, `${iconName}_${PLATFORM_CONFIG.linux.size}${PLATFORM_CONFIG.linux.format}`);
|
|
2509
2557
|
// Ensure we convert to proper PNG format with correct size
|
|
2510
2558
|
await sharp(processedInputPath)
|
|
@@ -2519,11 +2567,7 @@ async function convertIconFormat(inputPath, appName) {
|
|
|
2519
2567
|
}
|
|
2520
2568
|
// macOS
|
|
2521
2569
|
const macIconPath = await applyMacOSMask(processedInputPath);
|
|
2522
|
-
await
|
|
2523
|
-
report: false,
|
|
2524
|
-
icns: { name: iconName, sizes: PLATFORM_CONFIG.macos.sizes },
|
|
2525
|
-
});
|
|
2526
|
-
const outputPath = path.join(platformOutputDir, `${iconName}${PLATFORM_CONFIG.macos.format}`);
|
|
2570
|
+
const outputPath = await generateMacOSIcns(macIconPath, platformOutputDir, iconName);
|
|
2527
2571
|
return (await fsExtra.pathExists(outputPath)) ? outputPath : null;
|
|
2528
2572
|
}
|
|
2529
2573
|
catch (error) {
|
|
@@ -2538,6 +2582,7 @@ async function isLinuxBundleIconReady(iconPath) {
|
|
|
2538
2582
|
return false;
|
|
2539
2583
|
}
|
|
2540
2584
|
try {
|
|
2585
|
+
const sharp = await loadSharp();
|
|
2541
2586
|
const { width, height } = await sharp(iconPath).metadata();
|
|
2542
2587
|
return (width === PLATFORM_CONFIG.linux.size &&
|
|
2543
2588
|
height === PLATFORM_CONFIG.linux.size);
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pake-cli",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.7",
|
|
4
4
|
"description": "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ",
|
|
5
5
|
"engines": {
|
|
6
|
-
"node": ">=
|
|
6
|
+
"node": ">=20.9.0"
|
|
7
7
|
},
|
|
8
8
|
"packageManager": "pnpm@10.26.2",
|
|
9
9
|
"bin": {
|
|
@@ -58,14 +58,13 @@
|
|
|
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
|
-
"icon-gen": "^5.0.0",
|
|
64
63
|
"loglevel": "^1.9.2",
|
|
65
64
|
"ora": "^9.3.0",
|
|
66
65
|
"prompts": "^2.4.2",
|
|
67
66
|
"psl": "^1.15.0",
|
|
68
|
-
"sharp": "^0.
|
|
67
|
+
"sharp": "^0.35.0",
|
|
69
68
|
"tmp-promise": "^3.0.3",
|
|
70
69
|
"update-notifier": "^7.3.1"
|
|
71
70
|
},
|
|
@@ -91,8 +90,9 @@
|
|
|
91
90
|
},
|
|
92
91
|
"pnpm": {
|
|
93
92
|
"overrides": {
|
|
94
|
-
"sharp": "^0.
|
|
95
|
-
"@img/sharp-libvips-darwin-arm64": "1.
|
|
93
|
+
"sharp": "^0.35.0",
|
|
94
|
+
"@img/sharp-libvips-darwin-arm64": "1.3.0",
|
|
95
|
+
"tmp": "0.2.7"
|
|
96
96
|
},
|
|
97
97
|
"onlyBuiltDependencies": [
|
|
98
98
|
"esbuild",
|
package/src-tauri/Cargo.lock
CHANGED
|
@@ -2564,13 +2564,14 @@ dependencies = [
|
|
|
2564
2564
|
|
|
2565
2565
|
[[package]]
|
|
2566
2566
|
name = "pake"
|
|
2567
|
-
version = "3.15.
|
|
2567
|
+
version = "3.15.7"
|
|
2568
2568
|
dependencies = [
|
|
2569
2569
|
"block2",
|
|
2570
2570
|
"dispatch",
|
|
2571
2571
|
"objc2",
|
|
2572
2572
|
"objc2-app-kit",
|
|
2573
2573
|
"objc2-foundation",
|
|
2574
|
+
"objc2-web-kit",
|
|
2574
2575
|
"serde",
|
|
2575
2576
|
"serde_json",
|
|
2576
2577
|
"tauri",
|
|
@@ -2584,6 +2585,8 @@ dependencies = [
|
|
|
2584
2585
|
"tauri-plugin-single-instance",
|
|
2585
2586
|
"tauri-plugin-window-state",
|
|
2586
2587
|
"tokio",
|
|
2588
|
+
"webkit2gtk",
|
|
2589
|
+
"webview2-com",
|
|
2587
2590
|
"windows-sys 0.61.2",
|
|
2588
2591
|
]
|
|
2589
2592
|
|
|
@@ -3042,9 +3045,9 @@ dependencies = [
|
|
|
3042
3045
|
|
|
3043
3046
|
[[package]]
|
|
3044
3047
|
name = "quinn-proto"
|
|
3045
|
-
version = "0.11.
|
|
3048
|
+
version = "0.11.15"
|
|
3046
3049
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3047
|
-
checksum = "
|
|
3050
|
+
checksum = "4fcb935c5bec503c2f0e306bdd3e58bb9029dcb14fa8d9ac76e3a5256ac0763e"
|
|
3048
3051
|
dependencies = [
|
|
3049
3052
|
"bytes",
|
|
3050
3053
|
"getrandom 0.3.4",
|
|
@@ -3072,7 +3075,7 @@ dependencies = [
|
|
|
3072
3075
|
"once_cell",
|
|
3073
3076
|
"socket2",
|
|
3074
3077
|
"tracing",
|
|
3075
|
-
"windows-sys 0.
|
|
3078
|
+
"windows-sys 0.59.0",
|
|
3076
3079
|
]
|
|
3077
3080
|
|
|
3078
3081
|
[[package]]
|
|
@@ -3420,9 +3423,9 @@ dependencies = [
|
|
|
3420
3423
|
|
|
3421
3424
|
[[package]]
|
|
3422
3425
|
name = "rustls-webpki"
|
|
3423
|
-
version = "0.103.
|
|
3426
|
+
version = "0.103.13"
|
|
3424
3427
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
3425
|
-
checksum = "
|
|
3428
|
+
checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
|
|
3426
3429
|
dependencies = [
|
|
3427
3430
|
"ring",
|
|
3428
3431
|
"rustls-pki-types",
|
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.7"
|
|
4
4
|
description = "๐คฑ๐ป Turn any webpage into a desktop app with Rust."
|
|
5
5
|
authors = ["Tw93"]
|
|
6
6
|
license = "GPL-3.0-or-later"
|
|
@@ -41,6 +41,17 @@ dispatch = "0.2"
|
|
|
41
41
|
objc2 = "0.6"
|
|
42
42
|
objc2-app-kit = { version = "0.3", features = ["NSApplication", "NSDockTile"] }
|
|
43
43
|
objc2-foundation = { version = "0.3", features = ["NSString"] }
|
|
44
|
+
objc2-web-kit = { version = "0.3", default-features = false, features = [
|
|
45
|
+
"std",
|
|
46
|
+
"WKUserContentController",
|
|
47
|
+
"WKWebViewConfiguration",
|
|
48
|
+
] }
|
|
49
|
+
|
|
50
|
+
[target.'cfg(target_os = "linux")'.dependencies]
|
|
51
|
+
# Used for native WebKitGTK go_back/go_forward on error pages (no page JS).
|
|
52
|
+
# Version and features must match what wry resolves, or PlatformWebview::inner
|
|
53
|
+
# hands back a WebView from a different crate instance.
|
|
54
|
+
webkit2gtk = { version = "=2.0.2", features = ["v2_38"] }
|
|
44
55
|
|
|
45
56
|
[target.'cfg(windows)'.dependencies]
|
|
46
57
|
windows-sys = { version = "0.61.2", features = [
|
|
@@ -48,6 +59,8 @@ windows-sys = { version = "0.61.2", features = [
|
|
|
48
59
|
"Win32_UI_Shell",
|
|
49
60
|
"Win32_UI_WindowsAndMessaging",
|
|
50
61
|
] }
|
|
62
|
+
# Used for native WebView2 GoBack/GoForward on error pages (no page JS).
|
|
63
|
+
webview2-com = "0.38"
|
|
51
64
|
|
|
52
65
|
[features]
|
|
53
66
|
# 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,7 +130,10 @@ 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
|
|
|
@@ -195,12 +220,12 @@ pub fn set_dock_badge_label(app: AppHandle, label: Option<String>) -> Result<(),
|
|
|
195
220
|
|
|
196
221
|
#[command]
|
|
197
222
|
pub async fn update_theme_mode(app: AppHandle, mode: String) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
223
|
+
let theme = if mode == "dark" {
|
|
224
|
+
Theme::Dark
|
|
225
|
+
} else {
|
|
226
|
+
Theme::Light
|
|
227
|
+
};
|
|
228
|
+
for window in app.webview_windows().values() {
|
|
204
229
|
let _ = window.set_theme(Some(theme));
|
|
205
230
|
}
|
|
206
231
|
}
|
|
@@ -216,3 +241,27 @@ pub fn set_zoom(window: WebviewWindow, percent: f64) -> Result<(), String> {
|
|
|
216
241
|
.set_zoom(factor)
|
|
217
242
|
.map_err(|e| format!("Failed to set zoom: {}", e))
|
|
218
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,11 +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
|
+
};
|
|
2
4
|
use crate::cancel_startup_reveal;
|
|
3
5
|
use std::str::FromStr;
|
|
4
6
|
use std::sync::{atomic::AtomicBool, Arc, Mutex};
|
|
5
7
|
use std::time::{Duration, Instant};
|
|
6
8
|
use tauri::{
|
|
7
9
|
menu::{MenuBuilder, MenuItemBuilder},
|
|
8
|
-
tray::{TrayIconBuilder, TrayIconEvent},
|
|
10
|
+
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
|
|
9
11
|
AppHandle, Manager,
|
|
10
12
|
};
|
|
11
13
|
use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut};
|
|
@@ -53,22 +55,13 @@ pub fn set_system_tray(
|
|
|
53
55
|
open_additional_window_safe(app);
|
|
54
56
|
}
|
|
55
57
|
"hide_app" => {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}
|
|
58
|
+
// Hide every webview (main + multi-window clones), not only "pake".
|
|
59
|
+
cancel_startup_reveal(&menu_revealed);
|
|
60
|
+
hide_all_app_windows(app);
|
|
60
61
|
}
|
|
61
62
|
"show_app" => {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
let _ = window.show();
|
|
65
|
-
reapply_window_icon(&window);
|
|
66
|
-
#[cfg(target_os = "linux")]
|
|
67
|
-
if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
68
|
-
let _ = window.set_fullscreen(true);
|
|
69
|
-
let _ = window.set_focus();
|
|
70
|
-
}
|
|
71
|
-
}
|
|
63
|
+
cancel_startup_reveal(&menu_revealed);
|
|
64
|
+
show_all_app_windows(app, _init_fullscreen);
|
|
72
65
|
}
|
|
73
66
|
"quit" => {
|
|
74
67
|
let flags = if _init_fullscreen {
|
|
@@ -82,24 +75,19 @@ pub fn set_system_tray(
|
|
|
82
75
|
_ => (),
|
|
83
76
|
})
|
|
84
77
|
.on_tray_icon_event(move |tray, event| {
|
|
85
|
-
if let TrayIconEvent::Click {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
99
|
-
let _ = window.set_fullscreen(true);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
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);
|
|
103
91
|
}
|
|
104
92
|
}
|
|
105
93
|
});
|
|
@@ -159,21 +147,8 @@ pub fn set_global_shortcut(
|
|
|
159
147
|
*last_triggered = Instant::now();
|
|
160
148
|
|
|
161
149
|
if shortcut_hotkey.eq(event) {
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let is_visible = window.is_visible().unwrap_or(false);
|
|
165
|
-
if is_visible {
|
|
166
|
-
let _ = window.hide();
|
|
167
|
-
} else {
|
|
168
|
-
let _ = window.show();
|
|
169
|
-
reapply_window_icon(&window);
|
|
170
|
-
let _ = window.set_focus();
|
|
171
|
-
#[cfg(target_os = "linux")]
|
|
172
|
-
if _init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
173
|
-
let _ = window.set_fullscreen(true);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
}
|
|
150
|
+
cancel_startup_reveal(&startup_revealed);
|
|
151
|
+
toggle_all_app_windows(app, _init_fullscreen);
|
|
177
152
|
}
|
|
178
153
|
}
|
|
179
154
|
})
|
|
@@ -5,6 +5,10 @@ use crate::util::{
|
|
|
5
5
|
};
|
|
6
6
|
#[cfg(target_os = "macos")]
|
|
7
7
|
use dispatch::Queue;
|
|
8
|
+
#[cfg(target_os = "macos")]
|
|
9
|
+
use objc2::MainThreadMarker;
|
|
10
|
+
#[cfg(target_os = "macos")]
|
|
11
|
+
use objc2_web_kit::WKUserContentController;
|
|
8
12
|
#[cfg(target_os = "windows")]
|
|
9
13
|
use std::{os::windows::ffi::OsStrExt, ptr, sync::OnceLock};
|
|
10
14
|
use std::{
|
|
@@ -28,6 +32,24 @@ use tauri::Theme;
|
|
|
28
32
|
#[cfg(target_os = "macos")]
|
|
29
33
|
use tauri::TitleBarStyle;
|
|
30
34
|
|
|
35
|
+
#[cfg(target_os = "macos")]
|
|
36
|
+
fn prepare_macos_new_window_configuration(features: &NewWindowFeatures) -> tauri::Result<()> {
|
|
37
|
+
let mtm = MainThreadMarker::new().ok_or_else(|| {
|
|
38
|
+
std::io::Error::other("macOS new-window configuration must run on the main thread")
|
|
39
|
+
})?;
|
|
40
|
+
// WebKit requires the exact target configuration it supplied to be used
|
|
41
|
+
// for the new page. Replace only the inherited content controller so Wry
|
|
42
|
+
// can register Pake's IPC handler and scripts once on the new webview.
|
|
43
|
+
let controller = unsafe { WKUserContentController::new(mtm) };
|
|
44
|
+
unsafe {
|
|
45
|
+
features
|
|
46
|
+
.opener()
|
|
47
|
+
.target_configuration
|
|
48
|
+
.setUserContentController(&controller);
|
|
49
|
+
}
|
|
50
|
+
Ok(())
|
|
51
|
+
}
|
|
52
|
+
|
|
31
53
|
#[cfg(target_os = "windows")]
|
|
32
54
|
fn build_proxy_browser_arg(url: &Url) -> Option<String> {
|
|
33
55
|
let host = url.host_str()?;
|
|
@@ -186,15 +208,21 @@ fn open_requested_window(
|
|
|
186
208
|
Ok(window)
|
|
187
209
|
}
|
|
188
210
|
|
|
211
|
+
/// Open a multi-window clone of the home app. The window is built hidden and
|
|
212
|
+
/// revealed on its first real page load (see `lib.rs` on_page_load) so Cmd+N
|
|
213
|
+
/// does not flash an empty shell the way the main window used to.
|
|
189
214
|
pub fn open_additional_window_safe(app: &AppHandle) {
|
|
190
215
|
#[cfg(target_os = "windows")]
|
|
191
216
|
{
|
|
192
217
|
let app_handle = app.clone();
|
|
193
218
|
std::thread::spawn(move || {
|
|
194
219
|
if let Ok(window) = open_additional_window(&app_handle) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
220
|
+
// Fallback if PageLoadEvent::Finished never arrives.
|
|
221
|
+
let fallback = window.clone();
|
|
222
|
+
tauri::async_runtime::spawn(async move {
|
|
223
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
|
|
224
|
+
reveal_built_window(&fallback);
|
|
225
|
+
});
|
|
198
226
|
}
|
|
199
227
|
});
|
|
200
228
|
}
|
|
@@ -202,13 +230,79 @@ pub fn open_additional_window_safe(app: &AppHandle) {
|
|
|
202
230
|
#[cfg(not(target_os = "windows"))]
|
|
203
231
|
{
|
|
204
232
|
if let Ok(window) = open_additional_window(app) {
|
|
205
|
-
let
|
|
206
|
-
|
|
207
|
-
|
|
233
|
+
let fallback = window.clone();
|
|
234
|
+
tauri::async_runtime::spawn(async move {
|
|
235
|
+
tokio::time::sleep(tokio::time::Duration::from_millis(3_000)).await;
|
|
236
|
+
reveal_built_window(&fallback);
|
|
237
|
+
});
|
|
208
238
|
}
|
|
209
239
|
}
|
|
210
240
|
}
|
|
211
241
|
|
|
242
|
+
/// Show a window that was built hidden once content is ready (or the fallback
|
|
243
|
+
/// timer fires). No-ops when already visible so page-load and fallback can race.
|
|
244
|
+
pub fn reveal_built_window(window: &WebviewWindow) {
|
|
245
|
+
if window.is_visible().unwrap_or(true) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
let _ = window.show();
|
|
249
|
+
reapply_window_icon(window);
|
|
250
|
+
let _ = window.set_focus();
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/// True when any Pake webview window is currently on screen.
|
|
254
|
+
///
|
|
255
|
+
/// A minimized window does not count. Windows keeps `IsWindowVisible` true while
|
|
256
|
+
/// a window is iconic, and `hide_on_close` minimizes before hiding, so treating
|
|
257
|
+
/// minimized as visible makes the tray toggle hide an already-invisible window
|
|
258
|
+
/// instead of restoring it (#1343).
|
|
259
|
+
pub fn any_app_window_visible(app: &AppHandle) -> bool {
|
|
260
|
+
app.webview_windows().values().any(|window| {
|
|
261
|
+
window.is_visible().unwrap_or(false) && !window.is_minimized().unwrap_or(false)
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/// Hide every webview window (main + multi-window clones). Used by tray Hide
|
|
266
|
+
/// and the activation shortcut so secondary windows are not left on screen.
|
|
267
|
+
pub fn hide_all_app_windows(app: &AppHandle) {
|
|
268
|
+
for window in app.webview_windows().values() {
|
|
269
|
+
let _ = window.hide();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Show every webview window, reassert icons, and focus the main window.
|
|
274
|
+
pub fn show_all_app_windows(app: &AppHandle, init_fullscreen: bool) {
|
|
275
|
+
let windows = app.webview_windows();
|
|
276
|
+
for window in windows.values() {
|
|
277
|
+
let _ = window.unminimize();
|
|
278
|
+
let _ = window.show();
|
|
279
|
+
reapply_window_icon(window);
|
|
280
|
+
#[cfg(target_os = "linux")]
|
|
281
|
+
if init_fullscreen && !window.is_fullscreen().unwrap_or(false) {
|
|
282
|
+
let _ = window.set_fullscreen(true);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
#[cfg(not(target_os = "linux"))]
|
|
286
|
+
let _ = init_fullscreen;
|
|
287
|
+
|
|
288
|
+
if let Some(main) = windows.get("pake") {
|
|
289
|
+
let _ = main.set_focus();
|
|
290
|
+
} else if let Some(any) = windows.values().next() {
|
|
291
|
+
let _ = any.set_focus();
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/// Tray-click / activation-shortcut toggle: hide all if anything is visible,
|
|
296
|
+
/// otherwise show all. Cancels startup reveal when the caller has already done
|
|
297
|
+
/// so; this helper only touches visibility.
|
|
298
|
+
pub fn toggle_all_app_windows(app: &AppHandle, init_fullscreen: bool) {
|
|
299
|
+
if any_app_window_visible(app) {
|
|
300
|
+
hide_all_app_windows(app);
|
|
301
|
+
} else {
|
|
302
|
+
show_all_app_windows(app, init_fullscreen);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
212
306
|
fn build_window_with_label(
|
|
213
307
|
app: &AppHandle,
|
|
214
308
|
config: &PakeConfig,
|
|
@@ -522,26 +616,10 @@ fn build_window(
|
|
|
522
616
|
}
|
|
523
617
|
|
|
524
618
|
if let Some(features) = new_window_features {
|
|
525
|
-
// Reuse only opener-provided position/size on macOS; sharing the opener
|
|
526
|
-
// WKWebViewConfiguration triggers duplicate WKScriptMessageHandler
|
|
527
|
-
// registrations on macOS 26+ and crashes the app (issue #1194).
|
|
528
619
|
#[cfg(target_os = "macos")]
|
|
529
|
-
|
|
530
|
-
if let Some(position) = features.position() {
|
|
531
|
-
window_builder = window_builder.position(position.x, position.y);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
if let Some(size) = features.size() {
|
|
535
|
-
window_builder = window_builder.inner_size(size.width, size.height);
|
|
536
|
-
}
|
|
620
|
+
prepare_macos_new_window_configuration(&features)?;
|
|
537
621
|
|
|
538
|
-
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
#[cfg(not(target_os = "macos"))]
|
|
542
|
-
{
|
|
543
|
-
window_builder = window_builder.window_features(features).focused(true);
|
|
544
|
-
}
|
|
622
|
+
window_builder = window_builder.window_features(features).focused(true);
|
|
545
623
|
}
|
|
546
624
|
|
|
547
625
|
// Capture webview-initiated downloads (blob:, data:, Content-Disposition,
|
|
@@ -553,7 +631,7 @@ fn build_window(
|
|
|
553
631
|
// catching it here is independent of the page CSP and the IPC channel.
|
|
554
632
|
{
|
|
555
633
|
let download_handle = app.clone();
|
|
556
|
-
window_builder = window_builder.on_download(move |
|
|
634
|
+
window_builder = window_builder.on_download(move |webview, event| match event {
|
|
557
635
|
DownloadEvent::Requested { url, destination } => {
|
|
558
636
|
match download_handle.path().download_dir() {
|
|
559
637
|
Ok(download_dir) => {
|
|
@@ -585,7 +663,12 @@ fn build_window(
|
|
|
585
663
|
path: _,
|
|
586
664
|
success,
|
|
587
665
|
} => {
|
|
588
|
-
|
|
666
|
+
// Toast on the window that started the download (including
|
|
667
|
+
// secondary multi-window labels), not a hard-coded "pake".
|
|
668
|
+
let toast_window = download_handle
|
|
669
|
+
.get_webview_window(webview.label())
|
|
670
|
+
.or_else(|| download_handle.get_webview_window("pake"));
|
|
671
|
+
if let Some(window) = toast_window {
|
|
589
672
|
let message_type = if success {
|
|
590
673
|
MessageType::Success
|
|
591
674
|
} 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,12 +448,12 @@ const PREVIEWABLE_MEDIA_EXTENSIONS = [
|
|
|
446
448
|
"m4a",
|
|
447
449
|
];
|
|
448
450
|
|
|
449
|
-
// Path fragments that often host real file downloads.
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
const DOWNLOAD_PATH_PATTERNS = ["/download/"
|
|
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/"];
|
|
455
457
|
|
|
456
458
|
// Language detection utilities
|
|
457
459
|
function getUserLanguage() {
|
|
@@ -527,6 +529,52 @@ function isDownloadableFile(url) {
|
|
|
527
529
|
}
|
|
528
530
|
}
|
|
529
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
|
+
|
|
530
578
|
function normalizeAnchorHref(rawHref) {
|
|
531
579
|
return typeof rawHref === "string" ? rawHref.trim() : "";
|
|
532
580
|
}
|
|
@@ -660,8 +708,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
660
708
|
const isSpecialDownload = (url) =>
|
|
661
709
|
["blob", "data"].some((protocol) => url.startsWith(protocol));
|
|
662
710
|
|
|
663
|
-
|
|
664
|
-
|
|
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);
|
|
665
716
|
|
|
666
717
|
const handleExternalLink = (url) => {
|
|
667
718
|
// Don't try to open blob: or data: URLs with shell
|
|
@@ -685,12 +736,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
685
736
|
|
|
686
737
|
if (linkUrl.hostname === currentUrl.hostname) return true;
|
|
687
738
|
|
|
688
|
-
//
|
|
689
|
-
|
|
690
|
-
const parts = hostname.split(".");
|
|
691
|
-
return parts.length >= 2 ? parts.slice(-2).join(".") : hostname;
|
|
692
|
-
};
|
|
693
|
-
|
|
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.
|
|
694
741
|
return (
|
|
695
742
|
getRootDomain(currentUrl.hostname) === getRootDomain(linkUrl.hostname)
|
|
696
743
|
);
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -28,10 +28,13 @@ const GDK_BACKEND: &str = "GDK_BACKEND";
|
|
|
28
28
|
use app::{
|
|
29
29
|
invoke::{
|
|
30
30
|
clear_dock_badge, download_file, increment_dock_badge, send_notification, set_dock_badge,
|
|
31
|
-
set_dock_badge_label, set_zoom, update_theme_mode,
|
|
31
|
+
set_dock_badge_label, set_zoom, update_theme_mode, webview_navigate,
|
|
32
32
|
},
|
|
33
33
|
setup::{set_global_shortcut, set_system_tray},
|
|
34
|
-
window::{
|
|
34
|
+
window::{
|
|
35
|
+
open_additional_window_safe, reapply_window_icon, reveal_built_window, set_window,
|
|
36
|
+
MultiWindowState,
|
|
37
|
+
},
|
|
35
38
|
};
|
|
36
39
|
use util::get_pake_config;
|
|
37
40
|
|
|
@@ -244,16 +247,14 @@ pub fn run_app() {
|
|
|
244
247
|
));
|
|
245
248
|
}
|
|
246
249
|
|
|
247
|
-
// Reveal
|
|
250
|
+
// Reveal hidden windows after the first real document finishes loading so
|
|
248
251
|
// slow WKWebView cold starts do not expose an empty but interactive shell.
|
|
249
|
-
//
|
|
250
|
-
//
|
|
251
|
-
|
|
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
|
+
{
|
|
252
256
|
let page_load_revealed = startup_window_revealed.clone();
|
|
253
257
|
app_builder = app_builder.on_page_load(move |webview, payload| {
|
|
254
|
-
if webview.label() != "pake" {
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
258
|
if !matches!(payload.event(), PageLoadEvent::Finished) {
|
|
258
259
|
return;
|
|
259
260
|
}
|
|
@@ -262,8 +263,24 @@ pub fn run_app() {
|
|
|
262
263
|
if is_placeholder_startup_url(payload.url()) {
|
|
263
264
|
return;
|
|
264
265
|
}
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
}
|
|
267
284
|
}
|
|
268
285
|
});
|
|
269
286
|
}
|
|
@@ -283,6 +300,7 @@ pub fn run_app() {
|
|
|
283
300
|
clear_dock_badge,
|
|
284
301
|
update_theme_mode,
|
|
285
302
|
set_zoom,
|
|
303
|
+
webview_navigate,
|
|
286
304
|
])
|
|
287
305
|
.setup(move |app| {
|
|
288
306
|
app.manage(MultiWindowState::new(
|