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 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.7";
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
- let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;
252
- // Provide helpful guidance for common Linux AppImage build failures
253
- // caused by strip tool incompatibility with modern glibc (2.38+)
254
- const lowerError = errorMessage.toLowerCase();
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
- const pnpmMajor = parseMajorVersion(stdout.trim());
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
- // Warn users about potential AppImage build failures on modern Linux systems.
1025
- // The linuxdeploy tool bundled in Tauri uses an older strip tool that doesn't
1026
- // recognize the .relr.dyn section introduced in glibc 2.38+.
1027
- if (process.platform === 'linux' && target === 'appimage') {
1028
- if (!buildEnv.NO_STRIP) {
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
- const shouldRetryWithoutStrip = process.platform === 'linux' &&
1040
- target === 'appimage' &&
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
- else {
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 -]/g, '')
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.11.7",
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"
@@ -2564,7 +2564,7 @@ dependencies = [
2564
2564
 
2565
2565
  [[package]]
2566
2566
  name = "pake"
2567
- version = "3.11.7"
2567
+ version = "3.11.8"
2568
2568
  dependencies = [
2569
2569
  "objc2",
2570
2570
  "objc2-app-kit",
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "pake"
3
- version = "3.11.7"
3
+ version = "3.11.8"
4
4
  description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with Rust."
5
5
  authors = ["Tw93"]
6
6
  license = "MIT"
@@ -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::{self, File};
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(&params.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, &params.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::get_data_dir;
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
- // Collect blob urls to blob by overriding window.URL.createObjectURL
344
- function collectUrlToBlobs() {
345
- const backupCreateObjectURL = window.URL.createObjectURL;
346
- window.blobToUrlCaches = new Map();
347
- window.URL.createObjectURL = (blob) => {
348
- const url = backupCreateObjectURL.call(window.URL, blob);
349
- window.blobToUrlCaches.set(url, blob);
350
- return url;
351
- };
352
- }
353
-
354
- function convertBlobUrlToBinary(blobUrl) {
355
- return new Promise((resolve, reject) => {
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 for Rust to handle.
591
- if (
592
- isDownloadRequired(absoluteUrl, anchorElement, e) &&
593
- !isSpecialDownload(absoluteUrl)
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.startsWith("data:")) {
867
- downloadFromDataUri(imageUrl, filename);
868
- } else if (imageUrl.startsWith("blob:")) {
869
- if (window.blobToUrlCaches && window.blobToUrlCaches.has(imageUrl)) {
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();
@@ -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, download_file_by_binary,
17
- increment_dock_badge, send_notification, set_dock_badge, set_dock_badge_label,
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 enable_find = pake_config.windows[0].enable_find;
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, enable_find)?;
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| {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "productName": "Weekly",
3
3
  "identifier": "com.pake.weekly",
4
- "version": "3.11.7",
4
+ "version": "3.11.8",
5
5
  "app": {
6
6
  "withGlobalTauri": true,
7
7
  "trayIcon": {