pake-cli 3.11.6 โ 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 +95 -82
- package/package.json +3 -2
- 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/pake-badge-test.html +0 -1
- package/dist/test-local.html +0 -1
- package/src-tauri/gen/schemas/acl-manifests.json +0 -1
- package/src-tauri/gen/schemas/capabilities.json +0 -1
- package/src-tauri/gen/schemas/desktop-schema.json +0 -3331
- package/src-tauri/gen/schemas/macOS-schema.json +0 -3331
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"
|
|
@@ -31,7 +31,7 @@ var bin = {
|
|
|
31
31
|
};
|
|
32
32
|
var repository = {
|
|
33
33
|
type: "git",
|
|
34
|
-
url: "git+https://github.com/tw93/
|
|
34
|
+
url: "git+https://github.com/tw93/Pake.git"
|
|
35
35
|
};
|
|
36
36
|
var author = {
|
|
37
37
|
name: "Tw93",
|
|
@@ -62,6 +62,7 @@ var scripts = {
|
|
|
62
62
|
test: "pnpm run cli:build && cross-env PAKE_CREATE_APP=1 node tests/index.js",
|
|
63
63
|
format: "prettier --write . --ignore-unknown && find tests -name '*.js' -exec sed -i '' 's/[[:space:]]*$//' {} \\; && cd src-tauri && cargo fmt --verbose",
|
|
64
64
|
"format:check": "prettier --check . --ignore-unknown",
|
|
65
|
+
"release:check": "node scripts/check-release-version.mjs && pnpm run format:check && npx vitest run && pnpm run cli:build && npm pack --dry-run --ignore-scripts",
|
|
65
66
|
update: "pnpm update --verbose && cd src-tauri && cargo update",
|
|
66
67
|
prepublishOnly: "pnpm run cli:build"
|
|
67
68
|
};
|
|
@@ -247,38 +248,10 @@ async function shellExec(command, timeout = 300000, env) {
|
|
|
247
248
|
if (error.timedOut) {
|
|
248
249
|
throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
|
|
249
250
|
}
|
|
250
|
-
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
if (process.platform === 'linux' &&
|
|
255
|
-
(lowerError.includes('linuxdeploy') ||
|
|
256
|
-
lowerError.includes('appimage') ||
|
|
257
|
-
lowerError.includes('strip'))) {
|
|
258
|
-
errorMsg +=
|
|
259
|
-
'\n\nโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n' +
|
|
260
|
-
'Linux AppImage Build Failed\n' +
|
|
261
|
-
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ\n\n' +
|
|
262
|
-
'Cause: Strip tool incompatibility with glibc 2.38+\n' +
|
|
263
|
-
' (affects Debian Trixie, Arch Linux, and other modern distros)\n\n' +
|
|
264
|
-
'Quick fix:\n' +
|
|
265
|
-
' NO_STRIP=1 pake <url> --targets appimage --debug\n\n' +
|
|
266
|
-
'Alternatives:\n' +
|
|
267
|
-
' โข Use DEB format: pake <url> --targets deb\n' +
|
|
268
|
-
' โข Update binutils: sudo apt install binutils (or pacman -S binutils)\n' +
|
|
269
|
-
' โข Detailed guide: https://github.com/tw93/Pake/blob/main/docs/faq.md\n' +
|
|
270
|
-
'โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ';
|
|
271
|
-
if (lowerError.includes('fuse') ||
|
|
272
|
-
lowerError.includes('operation not permitted') ||
|
|
273
|
-
lowerError.includes('/dev/fuse')) {
|
|
274
|
-
errorMsg +=
|
|
275
|
-
'\n\nDocker / Container hint:\n' +
|
|
276
|
-
' AppImage tooling needs access to /dev/fuse. When running inside Docker, add:\n' +
|
|
277
|
-
' --privileged --device /dev/fuse --security-opt apparmor=unconfined\n' +
|
|
278
|
-
' or run on the host directly.';
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
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}`);
|
|
282
255
|
}
|
|
283
256
|
}
|
|
284
257
|
|
|
@@ -806,6 +779,24 @@ function getBuildTimeout() {
|
|
|
806
779
|
return 900000;
|
|
807
780
|
}
|
|
808
781
|
let packageManagerCache = null;
|
|
782
|
+
function parseMajorVersion(version) {
|
|
783
|
+
const match = version.match(/^v?(\d+)/);
|
|
784
|
+
return match ? Number(match[1]) : null;
|
|
785
|
+
}
|
|
786
|
+
function getPinnedPnpmMajorVersion() {
|
|
787
|
+
const packageManager = packageJson.packageManager;
|
|
788
|
+
const match = packageManager?.match(/^pnpm@(\d+)/);
|
|
789
|
+
return match ? Number(match[1]) : null;
|
|
790
|
+
}
|
|
791
|
+
async function detectNpm(execa) {
|
|
792
|
+
try {
|
|
793
|
+
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
catch {
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
809
800
|
/**
|
|
810
801
|
* Returns 'pnpm' when available, otherwise 'npm'. Throws if neither is found.
|
|
811
802
|
* Cached after the first successful detection so tests can call repeatedly.
|
|
@@ -815,23 +806,37 @@ async function detectPackageManager() {
|
|
|
815
806
|
return packageManagerCache;
|
|
816
807
|
}
|
|
817
808
|
const { execa } = await import('execa');
|
|
809
|
+
let pnpmVersion;
|
|
818
810
|
try {
|
|
819
|
-
await execa('pnpm', ['--version']
|
|
820
|
-
|
|
821
|
-
packageManagerCache = 'pnpm';
|
|
822
|
-
return 'pnpm';
|
|
811
|
+
const { stdout } = await execa('pnpm', ['--version']);
|
|
812
|
+
pnpmVersion = stdout.trim();
|
|
823
813
|
}
|
|
824
814
|
catch {
|
|
825
|
-
|
|
826
|
-
await execa('npm', ['--version'], { stdio: 'ignore' });
|
|
815
|
+
if (await detectNpm(execa)) {
|
|
827
816
|
logger.info('โบ pnpm not available, using npm for package management.');
|
|
828
817
|
packageManagerCache = 'npm';
|
|
829
818
|
return 'npm';
|
|
830
819
|
}
|
|
831
|
-
|
|
832
|
-
|
|
820
|
+
throw new Error('Neither pnpm nor npm is available. Please install a package manager.');
|
|
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.`);
|
|
833
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';
|
|
834
836
|
}
|
|
837
|
+
logger.info('โบ Using pnpm for package management.');
|
|
838
|
+
packageManagerCache = 'pnpm';
|
|
839
|
+
return 'pnpm';
|
|
835
840
|
}
|
|
836
841
|
function getInstallCommand(packageManager, useCnMirror) {
|
|
837
842
|
const registryOption = useCnMirror
|
|
@@ -887,23 +892,29 @@ async function configureCargoRegistry(tauriSrcPath, useCnMirror) {
|
|
|
887
892
|
logger.warn(`โผ ${projectConf} still references rsproxy.cn. Remove it or set ${CN_MIRROR_ENV}=1 if you want to use the CN mirror.`);
|
|
888
893
|
}
|
|
889
894
|
}
|
|
890
|
-
/**
|
|
891
|
-
* Returns true when an error string looks like the well-known Tauri+linuxdeploy
|
|
892
|
-
* strip failure that we automatically retry with NO_STRIP=1.
|
|
893
|
-
*/
|
|
894
|
-
function isLinuxDeployStripError(error) {
|
|
895
|
-
if (!(error instanceof Error) || !error.message) {
|
|
896
|
-
return false;
|
|
897
|
-
}
|
|
898
|
-
const message = error.message.toLowerCase();
|
|
899
|
-
return (message.includes('linuxdeploy') ||
|
|
900
|
-
message.includes('failed to run linuxdeploy') ||
|
|
901
|
-
message.includes('strip:') ||
|
|
902
|
-
message.includes('unable to recognise the format of the input file') ||
|
|
903
|
-
message.includes('appimage tool failed') ||
|
|
904
|
-
message.includes('strip tool'));
|
|
905
|
-
}
|
|
906
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;
|
|
907
918
|
class BaseBuilder {
|
|
908
919
|
constructor(options) {
|
|
909
920
|
this.options = options;
|
|
@@ -995,14 +1006,11 @@ class BaseBuilder {
|
|
|
995
1006
|
...(process.env.NO_STRIP ? { NO_STRIP: process.env.NO_STRIP } : {}),
|
|
996
1007
|
};
|
|
997
1008
|
const resolveExecEnv = () => Object.keys(buildEnv).length > 0 ? buildEnv : undefined;
|
|
998
|
-
|
|
999
|
-
//
|
|
1000
|
-
//
|
|
1001
|
-
if (
|
|
1002
|
-
|
|
1003
|
-
logger.warn('โ Building AppImage on Linux may fail due to strip incompatibility with glibc 2.38+');
|
|
1004
|
-
logger.warn('โ If build fails, retry with: NO_STRIP=1 pake <url> --targets appimage');
|
|
1005
|
-
}
|
|
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.');
|
|
1006
1014
|
}
|
|
1007
1015
|
const buildCommand = `cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`;
|
|
1008
1016
|
const buildTimeout = getBuildTimeout();
|
|
@@ -1010,21 +1018,26 @@ class BaseBuilder {
|
|
|
1010
1018
|
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1011
1019
|
}
|
|
1012
1020
|
catch (error) {
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
!buildEnv.NO_STRIP &&
|
|
1016
|
-
isLinuxDeployStripError(error);
|
|
1017
|
-
if (shouldRetryWithoutStrip) {
|
|
1018
|
-
logger.warn('โ AppImage build failed during linuxdeploy strip step, retrying with NO_STRIP=1 automatically.');
|
|
1019
|
-
buildEnv = {
|
|
1020
|
-
...buildEnv,
|
|
1021
|
-
NO_STRIP: '1',
|
|
1022
|
-
};
|
|
1023
|
-
await shellExec(buildCommand, buildTimeout, resolveExecEnv());
|
|
1021
|
+
if (!isLinuxAppImage) {
|
|
1022
|
+
throw error;
|
|
1024
1023
|
}
|
|
1025
|
-
|
|
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;
|
|
1026
1030
|
throw error;
|
|
1027
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
|
+
}
|
|
1028
1041
|
}
|
|
1029
1042
|
// Copy app
|
|
1030
1043
|
const fileName = this.getFileName();
|
|
@@ -2308,8 +2321,8 @@ function resolveLocalAppName(filePath, platform) {
|
|
|
2308
2321
|
return generateLinuxPackageName(baseName) || 'pake-app';
|
|
2309
2322
|
}
|
|
2310
2323
|
const normalized = baseName
|
|
2311
|
-
.replace(/[^a-zA-Z0-9\u4e00-\u9fff
|
|
2312
|
-
.replace(/^[
|
|
2324
|
+
.replace(/[^a-zA-Z0-9\u4e00-\u9fff .-]/g, '')
|
|
2325
|
+
.replace(/^[ .-]+/, '')
|
|
2313
2326
|
.replace(/\s+/g, ' ')
|
|
2314
2327
|
.trim();
|
|
2315
2328
|
return normalized || 'pake-app';
|
|
@@ -2317,7 +2330,7 @@ function resolveLocalAppName(filePath, platform) {
|
|
|
2317
2330
|
function isValidName(name, platform) {
|
|
2318
2331
|
const reg = platform === 'linux'
|
|
2319
2332
|
? /^[a-z0-9\u4e00-\u9fff][a-z0-9\u4e00-\u9fff-]*$/
|
|
2320
|
-
: /^[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 .-]*$/;
|
|
2321
2334
|
return !!name && reg.test(name);
|
|
2322
2335
|
}
|
|
2323
2336
|
async function handleOptions(options, url) {
|
|
@@ -2338,7 +2351,7 @@ async function handleOptions(options, url) {
|
|
|
2338
2351
|
}
|
|
2339
2352
|
if (name && !isValidName(name, platform)) {
|
|
2340
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.`;
|
|
2341
|
-
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.`;
|
|
2342
2355
|
const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR;
|
|
2343
2356
|
if (isActions) {
|
|
2344
2357
|
logger.error(errorMsg);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pake-cli",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.8",
|
|
4
4
|
"description": "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18.0.0"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
14
|
-
"url": "git+https://github.com/tw93/
|
|
14
|
+
"url": "git+https://github.com/tw93/Pake.git"
|
|
15
15
|
},
|
|
16
16
|
"author": {
|
|
17
17
|
"name": "Tw93",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
"test": "pnpm run cli:build && cross-env PAKE_CREATE_APP=1 node tests/index.js",
|
|
43
43
|
"format": "prettier --write . --ignore-unknown && find tests -name '*.js' -exec sed -i '' 's/[[:space:]]*$//' {} \\; && cd src-tauri && cargo fmt --verbose",
|
|
44
44
|
"format:check": "prettier --check . --ignore-unknown",
|
|
45
|
+
"release:check": "node scripts/check-release-version.mjs && pnpm run format:check && npx vitest run && pnpm run cli:build && npm pack --dry-run --ignore-scripts",
|
|
45
46
|
"update": "pnpm update --verbose && cd src-tauri && cargo update",
|
|
46
47
|
"prepublishOnly": "pnpm run cli:build"
|
|
47
48
|
},
|
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();
|