pake-cli 3.11.7 โ 3.11.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +74 -87
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/src/app/invoke.rs +1 -49
- package/src-tauri/src/app/window.rs +59 -2
- package/src-tauri/src/inject/event.js +27 -126
- package/src-tauri/src/lib.rs +4 -6
- package/src-tauri/tauri.conf.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
|
20
20
|
import fs$1 from 'fs';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.11.
|
|
23
|
+
var version = "3.11.8";
|
|
24
24
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -248,38 +248,10 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
248
248
|
if (error.timedOut) {
|
|
249
249
|
throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
|
|
250
250
|
}
|
|
251
|
-
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
|
|
255
|
-
if (process.platform === 'linux' &&
|
|
256
|
-
(lowerError.includes('linuxdeploy') ||
|
|
257
|
-
lowerError.includes('appimage') ||
|
|
258
|
-
lowerError.includes('strip'))) {
|
|
259
|
-
errorMsg +=
|
|
260
|
-
'\n\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n' +
|
|
261
|
-
'Linux AppImage Build Failed\n' +
|
|
262
|
-
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n' +
|
|
263
|
-
'Cause: Strip tool incompatibility with glibc 2.38+\n' +
|
|
264
|
-
' (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
|
|
265
|
-
'Quick fix:\n' +
|
|
266
|
-
' NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
|
|
267
|
-
'Alternatives:\n' +
|
|
268
|
-
' โข Use DEB format: pake <url> --targets deb\n' +
|
|
269
|
-
' โข Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
|
|
270
|
-
' โข Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
|
|
271
|
-
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ';
|
|
272
|
-
if (lowerError.includes('fuse') ||
|
|
273
|
-
lowerError.includes('operation not permitted') ||
|
|
274
|
-
lowerError.includes('/dev/fuse')) {
|
|
275
|
-
errorMsg +=
|
|
276
|
-
'\n\nDocker / Container hint:\n' +
|
|
277
|
-
' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' +
|
|
278
|
-
' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' +
|
|
279
|
-
' or run on the host directly.';
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
throw new Error(errorMsg);
|
|
251
|
+
// AppImage/linuxdeploy guidance is added by the caller (BaseBuilder), which
|
|
252
|
+
// knows the build target. We only have the command line here (the tool's
|
|
253
|
+
// diagnostics stream to the terminal via stdio:inherit, not into the error).
|
|
254
|
+
throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`);
|
|
283
255
|
}
|
|
284
256
|
}
|
|
285
257
|
|
|
@@ -808,7 +780,7 @@ function getBuildTimeout() {
|
|
|
808
780
|
}
|
|
809
781
|
let packageManagerCache = null;
|
|
810
782
|
function parseMajorVersion(version) {
|
|
811
|
-
const match = version.match(/^(\d+)/);
|
|
783
|
+
const match = version.match(/^v?(\d+)/);
|
|
812
784
|
return match ? Number(match[1]) : null;
|
|
813
785
|
}
|
|
814
786
|
function getPinnedPnpmMajorVersion() {
|
|
@@ -834,21 +806,10 @@ async function detectPackageManager() {
|
|
|
834
806
|
return packageManagerCache;
|
|
835
807
|
}
|
|
836
808
|
const { execa } = await import('execa');
|
|
809
|
+
let pnpmVersion;
|
|
837
810
|
try {
|
|
838
811
|
const { stdout } = await execa('pnpm', ['--version']);
|
|
839
|
-
|
|
840
|
-
const pinnedPnpmMajor = getPinnedPnpmMajorVersion();
|
|
841
|
-
if (pnpmMajor !== null &&
|
|
842
|
-
pinnedPnpmMajor !== null &&
|
|
843
|
-
pnpmMajor !== pinnedPnpmMajor &&
|
|
844
|
-
(await detectNpm(execa))) {
|
|
845
|
-
logger.warn(`โผ Detected pnpm v${stdout.trim()}, but Pake is pinned to ${packageJson.packageManager}; using npm for package installation instead.`);
|
|
846
|
-
packageManagerCache = 'npm';
|
|
847
|
-
return 'npm';
|
|
848
|
-
}
|
|
849
|
-
logger.info('โบ Using pnpm for package management.');
|
|
850
|
-
packageManagerCache = 'pnpm';
|
|
851
|
-
return 'pnpm';
|
|
812
|
+
pnpmVersion = stdout.trim();
|
|
852
813
|
}
|
|
853
814
|
catch {
|
|
854
815
|
if (await detectNpm(execa)) {
|
|
@@ -858,6 +819,24 @@ async function detectPackageManager() {
|
|
|
858
819
|
}
|
|
859
820
|
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
860
821
|
}
|
|
822
|
+
const normalizedPnpmVersion = pnpmVersion.startsWith('v')
|
|
823
|
+
? pnpmVersion
|
|
824
|
+
: `v${pnpmVersion}`;
|
|
825
|
+
const pnpmMajor = parseMajorVersion(pnpmVersion);
|
|
826
|
+
const pinnedPnpmMajor = getPinnedPnpmMajorVersion();
|
|
827
|
+
if (pnpmMajor !== null &&
|
|
828
|
+
pinnedPnpmMajor !== null &&
|
|
829
|
+
pnpmMajor !== pinnedPnpmMajor) {
|
|
830
|
+
if (!(await detectNpm(execa))) {
|
|
831
|
+
throw new Error(`Detected pnpm ${normalizedPnpmVersion}, but Pake is pinned to ${packageJson.packageManager}. Install npm so Pake can fall back, or use pnpm ${pinnedPnpmMajor}.x to match the project pin.`);
|
|
832
|
+
}
|
|
833
|
+
logger.warn(`โผ Detected pnpm ${normalizedPnpmVersion}, but Pake is pinned to ${packageJson.packageManager}; using npm for package management instead.`);
|
|
834
|
+
packageManagerCache = 'npm';
|
|
835
|
+
return 'npm';
|
|
836
|
+
}
|
|
837
|
+
logger.info('โบ Using pnpm for package management.');
|
|
838
|
+
packageManagerCache = 'pnpm';
|
|
839
|
+
return 'pnpm';
|
|
861
840
|
}
|
|
862
841
|
function getInstallCommand(packageManager, useCnMirror) {
|
|
863
842
|
const registryOption = useCnMirror
|
|
@@ -913,23 +892,29 @@ async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
|
|
|
913
892
|
logger.warn(`โผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
|
|
914
893
|
}
|
|
915
894
|
}
|
|
916
|
-
/**
|
|
917
|
-
* Returns true when an error string looks like the well-known Tauri+linuxdeploy
|
|
918
|
-
* strip failure that we automatically retry with NO_STRIP=1.
|
|
919
|
-
*/
|
|
920
|
-
function isLinuxDeployStripError(error) {
|
|
921
|
-
if (!(error instanceof Error) || !error.message) {
|
|
922
|
-
return false;
|
|
923
|
-
}
|
|
924
|
-
const message = error.message.toLowerCase();
|
|
925
|
-
return (message.includes('linuxdeploy') ||
|
|
926
|
-
message.includes('failed to run linuxdeploy') ||
|
|
927
|
-
message.includes('strip:') ||
|
|
928
|
-
message.includes('unable to recognise the format of the input file') ||
|
|
929
|
-
message.includes('appimage tool failed') ||
|
|
930
|
-
message.includes('strip tool'));
|
|
931
|
-
}
|
|
932
895
|
|
|
896
|
+
// Appended to the error when a Linux AppImage build fails for good. linuxdeploy's
|
|
897
|
+
// diagnostics stream to the terminal (stdio: 'inherit') and never reach
|
|
898
|
+
// error.message, so we cannot name the exact cause. We only reach here after
|
|
899
|
+
// NO_STRIP=1 has been applied and still failed, so strip is shown as ruled out.
|
|
900
|
+
const APPIMAGE_BAR = 'โ'.repeat(56);
|
|
901
|
+
const APPIMAGE_FAILURE_GUIDANCE = `\n\n${APPIMAGE_BAR}\n` +
|
|
902
|
+
'Linux AppImage Build Failed\n' +
|
|
903
|
+
`${APPIMAGE_BAR}\n\n` +
|
|
904
|
+
'The AppImage bundler (linuxdeploy) failed. Common causes and fixes:\n\n' +
|
|
905
|
+
' โข Strip incompatibility (glibc 2.38+): NO_STRIP=1 was already applied and\n' +
|
|
906
|
+
' the build still failed, so strip is likely not the cause.\n' +
|
|
907
|
+
' โข Missing gdk-pixbuf loaders (e.g. "cannot stat\n' +
|
|
908
|
+
" '/usr/lib/gdk-pixbuf-2.0/...'\"): install them, then rebuild:\n" +
|
|
909
|
+
' Arch: sudo pacman -S gdk-pixbuf2 librsvg\n' +
|
|
910
|
+
' Debian: sudo apt install librsvg2-common gdk-pixbuf2.0-bin\n' +
|
|
911
|
+
' Fedora: sudo dnf install gdk-pixbuf2-modules librsvg2\n' +
|
|
912
|
+
' then: gdk-pixbuf-query-loaders --update-cache\n' +
|
|
913
|
+
' โข Running in Docker/container: AppImage needs /dev/fuse:\n' +
|
|
914
|
+
' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n\n' +
|
|
915
|
+
'Still stuck? Build a DEB instead: pake <url> --targets deb\n' +
|
|
916
|
+
'Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
|
|
917
|
+
APPIMAGE_BAR;
|
|
933
918
|
class BaseBuilder {
|
|
934
919
|
constructor(options) {
|
|
935
920
|
this.options = options;
|
|
@@ -1021,14 +1006,11 @@ class BaseBuilder {
|
|
|
1021
1006
|
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
1022
1007
|
};
|
|
1023
1008
|
const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined;
|
|
1024
|
-
|
|
1025
|
-
//
|
|
1026
|
-
//
|
|
1027
|
-
if (
|
|
1028
|
-
|
|
1029
|
-
logger.warn('โ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+');
|
|
1030
|
-
logger.warn('โ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage');
|
|
1031
|
-
}
|
|
1009
|
+
const isLinuxAppImage = process.platform === 'linux' && target === 'appimage';
|
|
1010
|
+
// AppImage builds can fail at the linuxdeploy strip step on glibc 2.38+.
|
|
1011
|
+
// A real failure now prints full guidance, so only hint in debug mode.
|
|
1012
|
+
if (isLinuxAppImage && !buildEnv.NO_STRIP && this.options.debug) {
|
|
1013
|
+
logger.warn('โ AppImage strip step can fail on glibc 2.38+; Pake will auto-retry with NO_STRIP=1.');
|
|
1032
1014
|
}
|
|
1033
1015
|
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
|
|
1034
1016
|
const buildTimeout = getBuildTimeout();
|
|
@@ -1036,21 +1018,26 @@ class BaseBuilder {
|
|
|
1036
1018
|
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1037
1019
|
}
|
|
1038
1020
|
catch (error) {
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
!buildEnv.NO_STRIP &&
|
|
1042
|
-
isLinuxDeployStripError(error);
|
|
1043
|
-
if (shouldRetryWithoutStrip) {
|
|
1044
|
-
logger.warn('โ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
|
|
1045
|
-
buildEnv = {
|
|
1046
|
-
...buildEnv,
|
|
1047
|
-
NO_STRIP: '1',
|
|
1048
|
-
};
|
|
1049
|
-
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1021
|
+
if (!isLinuxAppImage) {
|
|
1022
|
+
throw error;
|
|
1050
1023
|
}
|
|
1051
|
-
|
|
1024
|
+
// linuxdeploy's diagnostics stream to the terminal (stdio: 'inherit') and
|
|
1025
|
+
// never reach error.message, so we cannot classify the cause. strip is the
|
|
1026
|
+
// most common AppImage failure, so retry once with NO_STRIP=1; if that
|
|
1027
|
+
// (or an already-NO_STRIP run) still fails, surface all known causes.
|
|
1028
|
+
if (buildEnv.NO_STRIP) {
|
|
1029
|
+
error.message += APPIMAGE_FAILURE_GUIDANCE;
|
|
1052
1030
|
throw error;
|
|
1053
1031
|
}
|
|
1032
|
+
logger.warn('โ AppImage build failed, retrying once with NO_STRIP=1 (common glibc 2.38+ strip issue).');
|
|
1033
|
+
buildEnv = { ...buildEnv, NO_STRIP: '1' };
|
|
1034
|
+
try {
|
|
1035
|
+
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1036
|
+
}
|
|
1037
|
+
catch (retryError) {
|
|
1038
|
+
retryError.message += APPIMAGE_FAILURE_GUIDANCE;
|
|
1039
|
+
throw retryError;
|
|
1040
|
+
}
|
|
1054
1041
|
}
|
|
1055
1042
|
// Copy app
|
|
1056
1043
|
const fileName = this.getFileName();
|
|
@@ -2334,8 +2321,8 @@ function resolveLocalAppName(filePath, platform) {
|
|
|
2334
2321
|
return generateLinuxPackageName(baseName) || 'pake-app';
|
|
2335
2322
|
}
|
|
2336
2323
|
const normalized = baseName
|
|
2337
|
-
.replace(/[^a-zA-Z0-9\u4e00-\u9fff
|
|
2338
|
-
.replace(/^[
|
|
2324
|
+
.replace(/[^a-zA-Z0-9\u4e00-\u9fff .-]/g, '')
|
|
2325
|
+
.replace(/^[ .-]+/, '')
|
|
2339
2326
|
.replace(/\s+/g, ' ')
|
|
2340
2327
|
.trim();
|
|
2341
2328
|
return normalized || 'pake-app';
|
|
@@ -2343,7 +2330,7 @@ function resolveLocalAppName(filePath, platform) {
|
|
|
2343
2330
|
function isValidName(name, platform) {
|
|
2344
2331
|
const reg = platform === 'linux'
|
|
2345
2332
|
? /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/
|
|
2346
|
-
: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff
|
|
2333
|
+
: /^[a-zA-Z0-9\u4e00-\u9fff][a-zA-Z0-9\u4e00-\u9fff .-]*$/;
|
|
2347
2334
|
return !!name && reg.test(name);
|
|
2348
2335
|
}
|
|
2349
2336
|
async function handleOptions(options, url) {
|
|
@@ -2364,7 +2351,7 @@ async function handleOptions(options, url) {
|
|
|
2364
2351
|
}
|
|
2365
2352
|
if (name && !isValidName(name, platform)) {
|
|
2366
2353
|
const LINUX_NAME_ERROR = `โ Name should only include lowercase letters, numbers, and dashes (not leading dashes). Examples: com-123-xxx, 123pan, pan123, weread, we-read, 123.`;
|
|
2367
|
-
const DEFAULT_NAME_ERROR = `โ Name should only include letters, numbers, dashes, and spaces (not leading dashes and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, 123.`;
|
|
2354
|
+
const DEFAULT_NAME_ERROR = `โ Name should only include letters, numbers, dots, dashes, and spaces (not leading dots, dashes, and spaces). Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read, We Read, Vectorizer.AI, 123.`;
|
|
2368
2355
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
|
2369
2356
|
if (isActions) {
|
|
2370
2357
|
logger.error(errorMsg);
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
use crate::util::{check_file_or_append, get_download_message_with_lang, show_toast, MessageType};
|
|
2
|
-
use std::fs::
|
|
2
|
+
use std::fs::File;
|
|
3
3
|
use std::io::Write;
|
|
4
4
|
use std::str::FromStr;
|
|
5
5
|
use std::sync::atomic::{AtomicI64, Ordering};
|
|
@@ -73,13 +73,6 @@ pub struct DownloadFileParams {
|
|
|
73
73
|
language: Option<String>,
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
#[derive(serde::Deserialize)]
|
|
77
|
-
pub struct BinaryDownloadParams {
|
|
78
|
-
filename: String,
|
|
79
|
-
binary: Vec<u8>,
|
|
80
|
-
language: Option<String>,
|
|
81
|
-
}
|
|
82
|
-
|
|
83
76
|
#[derive(serde::Deserialize)]
|
|
84
77
|
pub struct NotificationParams {
|
|
85
78
|
title: String,
|
|
@@ -147,47 +140,6 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result
|
|
|
147
140
|
}
|
|
148
141
|
}
|
|
149
142
|
|
|
150
|
-
#[command]
|
|
151
|
-
pub async fn download_file_by_binary(
|
|
152
|
-
app: AppHandle,
|
|
153
|
-
params: BinaryDownloadParams,
|
|
154
|
-
) -> Result<(), String> {
|
|
155
|
-
let window: WebviewWindow = app.get_webview_window("pake").ok_or("Window not found")?;
|
|
156
|
-
|
|
157
|
-
show_toast(
|
|
158
|
-
&window,
|
|
159
|
-
&get_download_message_with_lang(MessageType::Start, params.language.clone()),
|
|
160
|
-
);
|
|
161
|
-
|
|
162
|
-
let download_dir = app
|
|
163
|
-
.path()
|
|
164
|
-
.download_dir()
|
|
165
|
-
.map_err(|e| format!("Failed to get download dir: {}", e))?;
|
|
166
|
-
|
|
167
|
-
let output_path = download_dir.join(¶ms.filename);
|
|
168
|
-
|
|
169
|
-
let path_str = output_path.to_str().ok_or("Invalid output path")?;
|
|
170
|
-
|
|
171
|
-
let file_path = check_file_or_append(path_str);
|
|
172
|
-
|
|
173
|
-
match fs::write(file_path, ¶ms.binary) {
|
|
174
|
-
Ok(_) => {
|
|
175
|
-
show_toast(
|
|
176
|
-
&window,
|
|
177
|
-
&get_download_message_with_lang(MessageType::Success, params.language.clone()),
|
|
178
|
-
);
|
|
179
|
-
Ok(())
|
|
180
|
-
}
|
|
181
|
-
Err(e) => {
|
|
182
|
-
show_toast(
|
|
183
|
-
&window,
|
|
184
|
-
&get_download_message_with_lang(MessageType::Failure, params.language),
|
|
185
|
-
);
|
|
186
|
-
Err(e.to_string())
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
143
|
#[command]
|
|
192
144
|
pub fn send_notification(app: AppHandle, params: NotificationParams) -> Result<(), String> {
|
|
193
145
|
use tauri_plugin_notification::NotificationExt;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
use crate::app::config::PakeConfig;
|
|
2
|
-
use crate::util::
|
|
2
|
+
use crate::util::{
|
|
3
|
+
check_file_or_append, get_data_dir, get_download_message_with_lang, show_toast, MessageType,
|
|
4
|
+
};
|
|
3
5
|
use std::{
|
|
4
6
|
path::PathBuf,
|
|
5
7
|
str::FromStr,
|
|
6
8
|
sync::atomic::{AtomicU32, Ordering},
|
|
7
9
|
};
|
|
8
10
|
use tauri::{
|
|
9
|
-
webview::{NewWindowFeatures, NewWindowResponse},
|
|
11
|
+
webview::{DownloadEvent, NewWindowFeatures, NewWindowResponse},
|
|
10
12
|
AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
|
|
11
13
|
};
|
|
12
14
|
|
|
@@ -426,6 +428,61 @@ fn build_window(
|
|
|
426
428
|
}
|
|
427
429
|
}
|
|
428
430
|
|
|
431
|
+
// Capture webview-initiated downloads (blob:, data:, Content-Disposition,
|
|
432
|
+
// etc.) and write them to the OS Downloads folder. This is essential for
|
|
433
|
+
// sites with a strict Content-Security-Policy (e.g. Gemini): their
|
|
434
|
+
// `connect-src` blocks Tauri's IPC origin, so downloads cannot be routed
|
|
435
|
+
// through the JS bridge, and downloads triggered from a sandboxed iframe
|
|
436
|
+
// can't reach the IPC either. Letting the browser download natively and
|
|
437
|
+
// catching it here is independent of the page CSP and the IPC channel.
|
|
438
|
+
{
|
|
439
|
+
let download_handle = app.clone();
|
|
440
|
+
window_builder = window_builder.on_download(move |_webview, event| match event {
|
|
441
|
+
DownloadEvent::Requested { url, destination } => {
|
|
442
|
+
match download_handle.path().download_dir() {
|
|
443
|
+
Ok(download_dir) => {
|
|
444
|
+
let filename = destination
|
|
445
|
+
.file_name()
|
|
446
|
+
.map(|name| name.to_string_lossy().to_string())
|
|
447
|
+
.filter(|name| !name.is_empty())
|
|
448
|
+
.or_else(|| {
|
|
449
|
+
url.path_segments()
|
|
450
|
+
.and_then(|mut segments| segments.next_back())
|
|
451
|
+
.map(|segment| segment.to_string())
|
|
452
|
+
.filter(|segment| !segment.is_empty())
|
|
453
|
+
})
|
|
454
|
+
.unwrap_or_else(|| "download".to_string());
|
|
455
|
+
|
|
456
|
+
let target = download_dir.join(filename);
|
|
457
|
+
if let Some(path_str) = target.to_str() {
|
|
458
|
+
*destination = PathBuf::from(check_file_or_append(path_str));
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
Err(error) => {
|
|
462
|
+
eprintln!("[Pake] Failed to resolve download dir: {error}");
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
true
|
|
466
|
+
}
|
|
467
|
+
DownloadEvent::Finished {
|
|
468
|
+
url: _,
|
|
469
|
+
path: _,
|
|
470
|
+
success,
|
|
471
|
+
} => {
|
|
472
|
+
if let Some(window) = download_handle.get_webview_window("pake") {
|
|
473
|
+
let message_type = if success {
|
|
474
|
+
MessageType::Success
|
|
475
|
+
} else {
|
|
476
|
+
MessageType::Failure
|
|
477
|
+
};
|
|
478
|
+
show_toast(&window, &get_download_message_with_lang(message_type, None));
|
|
479
|
+
}
|
|
480
|
+
true
|
|
481
|
+
}
|
|
482
|
+
_ => true,
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
429
486
|
window_builder = window_builder.on_navigation(|_| true);
|
|
430
487
|
|
|
431
488
|
window_builder.build()
|
|
@@ -340,118 +340,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
340
340
|
true,
|
|
341
341
|
);
|
|
342
342
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const blob = window.blobToUrlCaches.get(blobUrl);
|
|
357
|
-
if (!blob) {
|
|
358
|
-
fetch(blobUrl)
|
|
359
|
-
.then((res) => res.arrayBuffer())
|
|
360
|
-
.then((buffer) => resolve(Array.from(new Uint8Array(buffer))))
|
|
361
|
-
.catch(reject);
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
const reader = new FileReader();
|
|
365
|
-
reader.readAsArrayBuffer(blob);
|
|
366
|
-
reader.onload = () => {
|
|
367
|
-
resolve(Array.from(new Uint8Array(reader.result)));
|
|
368
|
-
};
|
|
369
|
-
reader.onerror = () => reject(reader.error);
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
function downloadFromDataUri(dataURI, filename) {
|
|
374
|
-
try {
|
|
375
|
-
const byteString = atob(dataURI.split(",")[1]);
|
|
376
|
-
// write the bytes of the string to an ArrayBuffer
|
|
377
|
-
const bufferArray = new ArrayBuffer(byteString.length);
|
|
378
|
-
|
|
379
|
-
// create a view into the buffer
|
|
380
|
-
const binary = new Uint8Array(bufferArray);
|
|
381
|
-
|
|
382
|
-
// set the bytes of the buffer to the correct values
|
|
383
|
-
for (let i = 0; i < byteString.length; i++) {
|
|
384
|
-
binary[i] = byteString.charCodeAt(i);
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// write the ArrayBuffer to a binary, and you're done
|
|
388
|
-
const userLanguage = getUserLanguage();
|
|
389
|
-
invoke("download_file_by_binary", {
|
|
390
|
-
params: {
|
|
391
|
-
filename,
|
|
392
|
-
binary: Array.from(binary),
|
|
393
|
-
language: userLanguage,
|
|
394
|
-
},
|
|
395
|
-
}).catch((error) => {
|
|
396
|
-
console.error("Failed to download data URI file:", filename, error);
|
|
397
|
-
showDownloadError(filename);
|
|
398
|
-
});
|
|
399
|
-
} catch (error) {
|
|
400
|
-
console.error("Failed to process data URI:", dataURI, error);
|
|
401
|
-
showDownloadError(filename || "file");
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function downloadFromBlobUrl(blobUrl, filename) {
|
|
406
|
-
convertBlobUrlToBinary(blobUrl)
|
|
407
|
-
.then((binary) => {
|
|
408
|
-
const userLanguage = getUserLanguage();
|
|
409
|
-
invoke("download_file_by_binary", {
|
|
410
|
-
params: {
|
|
411
|
-
filename,
|
|
412
|
-
binary,
|
|
413
|
-
language: userLanguage,
|
|
414
|
-
},
|
|
415
|
-
}).catch((error) => {
|
|
416
|
-
console.error("Failed to download blob file:", filename, error);
|
|
417
|
-
showDownloadError(filename);
|
|
418
|
-
});
|
|
419
|
-
})
|
|
420
|
-
.catch((error) => {
|
|
421
|
-
console.error("Failed to convert blob to binary:", blobUrl, error);
|
|
422
|
-
showDownloadError(filename);
|
|
423
|
-
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
// detect blob download by createElement("a")
|
|
427
|
-
function detectDownloadByCreateAnchor() {
|
|
428
|
-
const createEle = document.createElement;
|
|
429
|
-
document.createElement = (el) => {
|
|
430
|
-
if (el !== "a") return createEle.call(document, el);
|
|
431
|
-
const anchorEle = createEle.call(document, el);
|
|
432
|
-
|
|
433
|
-
// use addEventListener to avoid overriding the original click event.
|
|
434
|
-
anchorEle.addEventListener(
|
|
435
|
-
"click",
|
|
436
|
-
(e) => {
|
|
437
|
-
const url = anchorEle.href;
|
|
438
|
-
const filename = anchorEle.download || getFilenameFromUrl(url);
|
|
439
|
-
if (window.blobToUrlCaches.has(url)) {
|
|
440
|
-
e.preventDefault();
|
|
441
|
-
e.stopImmediatePropagation();
|
|
442
|
-
downloadFromBlobUrl(url, filename);
|
|
443
|
-
// case: download from dataURL -> convert dataURL ->
|
|
444
|
-
} else if (url.startsWith("data:")) {
|
|
445
|
-
e.preventDefault();
|
|
446
|
-
e.stopImmediatePropagation();
|
|
447
|
-
downloadFromDataUri(url, filename);
|
|
448
|
-
}
|
|
449
|
-
},
|
|
450
|
-
true,
|
|
451
|
-
);
|
|
452
|
-
|
|
453
|
-
return anchorEle;
|
|
454
|
-
};
|
|
343
|
+
// Trigger a native browser download via a transient anchor click. The Rust
|
|
344
|
+
// on_download handler then writes the file to the Downloads folder. This is
|
|
345
|
+
// used for blob:/data: URLs because routing their bytes through the Tauri
|
|
346
|
+
// IPC fails on strict-CSP sites (e.g. Gemini), whose connect-src blocks the
|
|
347
|
+
// IPC origin. The native download path is independent of the page CSP.
|
|
348
|
+
function triggerNativeDownload(url, filename) {
|
|
349
|
+
const anchor = document.createElement("a");
|
|
350
|
+
anchor.href = url;
|
|
351
|
+
anchor.download = filename || "";
|
|
352
|
+
anchor.style.display = "none";
|
|
353
|
+
document.body.appendChild(anchor);
|
|
354
|
+
anchor.click();
|
|
355
|
+
document.body.removeChild(anchor);
|
|
455
356
|
}
|
|
456
357
|
|
|
457
358
|
// process special download protocol['data:','blob:']
|
|
@@ -587,11 +488,16 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
587
488
|
return;
|
|
588
489
|
}
|
|
589
490
|
|
|
590
|
-
// Process download links
|
|
591
|
-
if (
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
491
|
+
// Process download links.
|
|
492
|
+
if (isDownloadRequired(absoluteUrl, anchorElement, e)) {
|
|
493
|
+
// Let the browser download blob:/data: URLs natively; the Rust
|
|
494
|
+
// on_download handler saves them to the Downloads folder. Routing them
|
|
495
|
+
// through the IPC fails on strict-CSP sites (e.g. Gemini), whose
|
|
496
|
+
// connect-src blocks the IPC origin, and on downloads triggered from a
|
|
497
|
+
// sandboxed iframe where the IPC can't be reached.
|
|
498
|
+
if (isSpecialDownload(absoluteUrl)) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
595
501
|
e.preventDefault();
|
|
596
502
|
e.stopImmediatePropagation();
|
|
597
503
|
const userLanguage = getUserLanguage();
|
|
@@ -625,9 +531,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
625
531
|
// Prevent some special websites from executing in advance, before the click event is triggered.
|
|
626
532
|
document.addEventListener("click", detectAnchorElementClick, true);
|
|
627
533
|
|
|
628
|
-
collectUrlToBlobs();
|
|
629
|
-
detectDownloadByCreateAnchor();
|
|
630
|
-
|
|
631
534
|
// Rewrite the window.open function.
|
|
632
535
|
const originalWindowOpen = window.open;
|
|
633
536
|
window.open = function (url, name, specs) {
|
|
@@ -863,12 +766,10 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
863
766
|
const filename = getFilenameFromUrl(imageUrl) || "image";
|
|
864
767
|
|
|
865
768
|
// Handle different URL types
|
|
866
|
-
if (imageUrl
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
downloadFromBlobUrl(imageUrl, filename);
|
|
871
|
-
}
|
|
769
|
+
if (isSpecialDownload(imageUrl)) {
|
|
770
|
+
// Download blob:/data: natively so it works under strict CSP; the Rust
|
|
771
|
+
// on_download handler saves it to the Downloads folder.
|
|
772
|
+
triggerNativeDownload(imageUrl, filename);
|
|
872
773
|
} else {
|
|
873
774
|
// Regular HTTP(S) image
|
|
874
775
|
const userLanguage = getUserLanguage();
|
package/src-tauri/src/lib.rs
CHANGED
|
@@ -13,9 +13,8 @@ const WINDOW_SHOW_DELAY: u64 = 50;
|
|
|
13
13
|
|
|
14
14
|
use app::{
|
|
15
15
|
invoke::{
|
|
16
|
-
clear_cache_and_restart, clear_dock_badge, download_file,
|
|
17
|
-
|
|
18
|
-
update_theme_mode,
|
|
16
|
+
clear_cache_and_restart, clear_dock_badge, download_file, increment_dock_badge,
|
|
17
|
+
send_notification, set_dock_badge, set_dock_badge_label, update_theme_mode,
|
|
19
18
|
},
|
|
20
19
|
setup::{set_global_shortcut, set_system_tray},
|
|
21
20
|
window::{open_additional_window_safe, set_window, MultiWindowState},
|
|
@@ -43,7 +42,7 @@ pub fn run_app() {
|
|
|
43
42
|
let start_to_tray = pake_config.windows[0].start_to_tray && show_system_tray; // Only valid when tray is enabled
|
|
44
43
|
let multi_instance = pake_config.multi_instance;
|
|
45
44
|
let multi_window = pake_config.multi_window;
|
|
46
|
-
let
|
|
45
|
+
let _enable_find = pake_config.windows[0].enable_find;
|
|
47
46
|
|
|
48
47
|
let window_state_plugin = WindowStatePlugin::default()
|
|
49
48
|
.with_state_flags(if init_fullscreen {
|
|
@@ -81,7 +80,6 @@ pub fn run_app() {
|
|
|
81
80
|
app_builder
|
|
82
81
|
.invoke_handler(tauri::generate_handler![
|
|
83
82
|
download_file,
|
|
84
|
-
download_file_by_binary,
|
|
85
83
|
send_notification,
|
|
86
84
|
increment_dock_badge,
|
|
87
85
|
set_dock_badge,
|
|
@@ -99,7 +97,7 @@ pub fn run_app() {
|
|
|
99
97
|
// --- Menu Construction Start ---
|
|
100
98
|
#[cfg(target_os = "macos")]
|
|
101
99
|
{
|
|
102
|
-
app::menu::set_app_menu(app.app_handle(), multi_window,
|
|
100
|
+
app::menu::set_app_menu(app.app_handle(), multi_window, _enable_find)?;
|
|
103
101
|
|
|
104
102
|
// Event Handling for Custom Menu Item
|
|
105
103
|
app.on_menu_event(move |app_handle, event| {
|