pake-cli 3.3.6 โ†’ 3.4.0

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 CHANGED
@@ -29,6 +29,7 @@
29
29
  - **Beginners**: Download ready-made [Popular Packages](#popular-packages) or use [Online Building](docs/github-actions-usage.md) with no environment setup required
30
30
  - **Developers**: Install [CLI Tool](docs/cli-usage.md) for one-command packaging of any website with customizable icons, window settings, and more
31
31
  - **Advanced Users**: Clone the project locally for [Custom Development](#development), or check [Advanced Usage](docs/advanced-usage.md) for style customization and feature enhancement
32
+ - **Troubleshooting**: Check [FAQ](docs/faq.md) for common issues and solutions
32
33
 
33
34
  ## Popular Packages
34
35
 
@@ -434,6 +435,13 @@ Pake's development can not be without these Hackers. They contributed a lot of c
434
435
  <sub><b>Jiaqi Gu</b></sub>
435
436
  </a>
436
437
  </td>
438
+ <td align="center">
439
+ <a href="https://github.com/JohannLai">
440
+ <img src="https://avatars.githubusercontent.com/u/10769405?v=4" width="90;" alt="JohannLai"/>
441
+ <br />
442
+ <sub><b>Johannlai</b></sub>
443
+ </a>
444
+ </td>
437
445
  <td align="center">
438
446
  <a href="https://github.com/Jason6987">
439
447
  <img src="https://avatars.githubusercontent.com/u/140222795?v=4" width="90;" alt="Jason6987"/>
@@ -441,6 +449,8 @@ Pake's development can not be without these Hackers. They contributed a lot of c
441
449
  <sub><b>Luminall</b></sub>
442
450
  </a>
443
451
  </td>
452
+ </tr>
453
+ <tr>
444
454
  <td align="center">
445
455
  <a href="https://github.com/Milo123459">
446
456
  <img src="https://avatars.githubusercontent.com/u/50248166?v=4" width="90;" alt="Milo123459"/>
@@ -448,8 +458,6 @@ Pake's development can not be without these Hackers. They contributed a lot of c
448
458
  <sub><b>Milo</b></sub>
449
459
  </a>
450
460
  </td>
451
- </tr>
452
- <tr>
453
461
  <td align="center">
454
462
  <a href="https://github.com/princemaple">
455
463
  <img src="https://avatars.githubusercontent.com/u/1329716?v=4" width="90;" alt="princemaple"/>
@@ -492,6 +500,8 @@ Pake's development can not be without these Hackers. They contributed a lot of c
492
500
  <sub><b>Null</b></sub>
493
501
  </a>
494
502
  </td>
503
+ </tr>
504
+ <tr>
495
505
  <td align="center">
496
506
  <a href="https://github.com/liudonghua123">
497
507
  <img src="https://avatars.githubusercontent.com/u/2276718?v=4" width="90;" alt="liudonghua123"/>
@@ -499,8 +509,6 @@ Pake's development can not be without these Hackers. They contributed a lot of c
499
509
  <sub><b>Liudonghua</b></sub>
500
510
  </a>
501
511
  </td>
502
- </tr>
503
- <tr>
504
512
  <td align="center">
505
513
  <a href="https://github.com/liusishan">
506
514
  <img src="https://avatars.githubusercontent.com/u/33129823?v=4" width="90;" alt="liusishan"/>
package/dist/cli.js CHANGED
@@ -6,6 +6,7 @@ import path from 'path';
6
6
  import fsExtra from 'fs-extra';
7
7
  import { fileURLToPath } from 'url';
8
8
  import prompts from 'prompts';
9
+ import os from 'os';
9
10
  import { execa, execaSync } from 'execa';
10
11
  import crypto from 'crypto';
11
12
  import ora from 'ora';
@@ -22,7 +23,7 @@ import sharp from 'sharp';
22
23
  import * as psl from 'psl';
23
24
 
24
25
  var name = "pake-cli";
25
- var version = "3.3.6";
26
+ var version = "3.4.0";
26
27
  var description = "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚";
27
28
  var engines = {
28
29
  node: ">=18.0.0"
@@ -74,12 +75,12 @@ var license = "MIT";
74
75
  var dependencies = {
75
76
  "@tauri-apps/api": "^2.8.0",
76
77
  "@tauri-apps/cli": "^2.8.4",
77
- axios: "^1.11.0",
78
- chalk: "^5.6.0",
78
+ axios: "^1.12.2",
79
+ chalk: "^5.6.2",
79
80
  commander: "^12.1.0",
80
81
  execa: "^9.6.0",
81
82
  "file-type": "^18.7.0",
82
- "fs-extra": "^11.3.1",
83
+ "fs-extra": "^11.3.2",
83
84
  "icon-gen": "^5.0.0",
84
85
  loglevel: "^1.9.2",
85
86
  ora: "^8.2.0",
@@ -96,7 +97,7 @@ var devDependencies = {
96
97
  "@rollup/plugin-replace": "^6.0.2",
97
98
  "@rollup/plugin-terser": "^0.4.4",
98
99
  "@types/fs-extra": "^11.0.4",
99
- "@types/node": "^20.19.13",
100
+ "@types/node": "^20.19.21",
100
101
  "@types/page-icon": "^0.3.6",
101
102
  "@types/prompts": "^2.4.9",
102
103
  "@types/tmp": "^0.2.6",
@@ -104,10 +105,10 @@ var devDependencies = {
104
105
  "app-root-path": "^3.1.0",
105
106
  "cross-env": "^7.0.3",
106
107
  prettier: "^3.6.2",
107
- rollup: "^4.50.0",
108
+ rollup: "^4.52.4",
108
109
  "rollup-plugin-typescript2": "^0.36.0",
109
110
  tslib: "^2.8.1",
110
- typescript: "^5.9.2"
111
+ typescript: "^5.9.3"
111
112
  };
112
113
  var packageJson = {
113
114
  name: name,
@@ -200,11 +201,11 @@ const IS_MAC = platform$1 === 'darwin';
200
201
  const IS_WIN = platform$1 === 'win32';
201
202
  const IS_LINUX = platform$1 === 'linux';
202
203
 
203
- async function shellExec(command, timeout = 300000, env) {
204
+ async function shellExec(command, timeout = 300000, env, showOutput = false) {
204
205
  try {
205
206
  const { exitCode } = await execa(command, {
206
207
  cwd: npmDirectory,
207
- stdio: ['inherit', 'pipe', 'inherit'], // Hide stdout verbose, keep stderr
208
+ stdio: showOutput ? 'inherit' : ['inherit', 'pipe', 'inherit'],
208
209
  shell: true,
209
210
  timeout,
210
211
  env: env ? { ...process.env, ...env } : process.env,
@@ -217,7 +218,18 @@ async function shellExec(command, timeout = 300000, env) {
217
218
  if (error.timedOut) {
218
219
  throw new Error(`Command timed out after ${timeout}ms: "${command}". Try increasing timeout or check network connectivity.`);
219
220
  }
220
- throw new Error(`Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`);
221
+ let errorMsg = `Error occurred while executing command "${command}". Exit code: ${exitCode}. Details: ${errorMessage}`;
222
+ if (process.platform === 'linux' &&
223
+ (errorMessage.includes('linuxdeploy') ||
224
+ errorMessage.includes('appimage') ||
225
+ errorMessage.includes('strip'))) {
226
+ errorMsg +=
227
+ '\n\nLinux AppImage build error. Try one of these solutions:\n' +
228
+ ' 1. Run with: NO_STRIP=true pake <url> --targets appimage\n' +
229
+ ' 2. Use DEB format instead: pake <url> --targets deb\n' +
230
+ ' 3. See detailed solutions: https://github.com/tw93/Pake/blob/main/docs/faq.md';
231
+ }
232
+ throw new Error(errorMsg);
221
233
  }
222
234
  }
223
235
 
@@ -284,6 +296,52 @@ async function isChinaIP(ip, domain) {
284
296
  }
285
297
  }
286
298
 
299
+ function normalizePathForComparison(targetPath) {
300
+ const normalized = path.normalize(targetPath);
301
+ return IS_WIN ? normalized.toLowerCase() : normalized;
302
+ }
303
+ function getCargoHomeCandidates() {
304
+ const candidates = new Set();
305
+ if (process.env.CARGO_HOME) {
306
+ candidates.add(process.env.CARGO_HOME);
307
+ }
308
+ const homeDir = os.homedir();
309
+ if (homeDir) {
310
+ candidates.add(path.join(homeDir, '.cargo'));
311
+ }
312
+ if (IS_WIN && process.env.USERPROFILE) {
313
+ candidates.add(path.join(process.env.USERPROFILE, '.cargo'));
314
+ }
315
+ return Array.from(candidates).filter(Boolean);
316
+ }
317
+ function ensureCargoBinOnPath() {
318
+ const currentPath = process.env.PATH || '';
319
+ const segments = currentPath.split(path.delimiter).filter(Boolean);
320
+ const normalizedSegments = new Set(segments.map((segment) => normalizePathForComparison(segment)));
321
+ const additions = [];
322
+ let cargoHomeSet = Boolean(process.env.CARGO_HOME);
323
+ for (const cargoHome of getCargoHomeCandidates()) {
324
+ const binDir = path.join(cargoHome, 'bin');
325
+ if (fsExtra.pathExistsSync(binDir) &&
326
+ !normalizedSegments.has(normalizePathForComparison(binDir))) {
327
+ additions.push(binDir);
328
+ normalizedSegments.add(normalizePathForComparison(binDir));
329
+ }
330
+ if (!cargoHomeSet && fsExtra.pathExistsSync(cargoHome)) {
331
+ process.env.CARGO_HOME = cargoHome;
332
+ cargoHomeSet = true;
333
+ }
334
+ }
335
+ if (additions.length) {
336
+ const prefix = additions.join(path.delimiter);
337
+ process.env.PATH = segments.length
338
+ ? `${prefix}${path.delimiter}${segments.join(path.delimiter)}`
339
+ : prefix;
340
+ }
341
+ }
342
+ function ensureRustEnv() {
343
+ ensureCargoBinOnPath();
344
+ }
287
345
  async function installRust() {
288
346
  const isActions = process.env.GITHUB_ACTIONS;
289
347
  const isInChina = await isChinaDomain('sh.rustup.rs');
@@ -293,8 +351,9 @@ async function installRust() {
293
351
  const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup';
294
352
  const spinner = getSpinner('Downloading Rust...');
295
353
  try {
296
- await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac);
354
+ await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac, 300000, undefined, true);
297
355
  spinner.succeed(chalk.green('โœ” Rust installed successfully!'));
356
+ ensureRustEnv();
298
357
  }
299
358
  catch (error) {
300
359
  spinner.fail(chalk.red('โœ• Rust installation failed!'));
@@ -303,6 +362,7 @@ async function installRust() {
303
362
  }
304
363
  }
305
364
  function checkRustInstalled() {
365
+ ensureCargoBinOnPath();
306
366
  try {
307
367
  execaSync('rustc', ['--version']);
308
368
  return true;
@@ -343,11 +403,31 @@ function generateLinuxPackageName(name) {
343
403
  .replace(/-+/g, '-');
344
404
  }
345
405
  function generateIdentifierSafeName(name) {
346
- return name
347
- .replace(/[^a-zA-Z0-9]/g, '')
348
- .toLowerCase();
406
+ const cleaned = name.replace(/[^a-zA-Z0-9\u4e00-\u9fff]/g, '').toLowerCase();
407
+ if (cleaned === '') {
408
+ const fallback = Array.from(name)
409
+ .map((char) => {
410
+ const code = char.charCodeAt(0);
411
+ if ((code >= 48 && code <= 57) ||
412
+ (code >= 65 && code <= 90) ||
413
+ (code >= 97 && code <= 122)) {
414
+ return char.toLowerCase();
415
+ }
416
+ return code.toString(16);
417
+ })
418
+ .join('')
419
+ .slice(0, 50);
420
+ return fallback || 'pake-app';
421
+ }
422
+ return cleaned;
349
423
  }
350
424
 
425
+ /**
426
+ * Helper function to generate safe lowercase app name for file paths
427
+ */
428
+ function getSafeAppName(name) {
429
+ return generateSafeFilename(name).toLowerCase();
430
+ }
351
431
  async function mergeConfig(url, options, tauriConf) {
352
432
  // Ensure .pake directory exists and copy source templates if needed
353
433
  const srcTauriDir = path.join(npmDirectory, 'src-tauri');
@@ -368,7 +448,7 @@ async function mergeConfig(url, options, tauriConf) {
368
448
  await fsExtra.copy(sourcePath, destPath);
369
449
  }
370
450
  }));
371
- const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, } = options;
451
+ const { width, height, fullscreen, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name, resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, } = options;
372
452
  const { platform } = process;
373
453
  const platformHideOnClose = hideOnClose ?? platform === 'darwin';
374
454
  const tauriConfWindowOptions = {
@@ -438,7 +518,7 @@ async function mergeConfig(url, options, tauriConf) {
438
518
  // Remove hardcoded desktop files and regenerate with correct app name
439
519
  delete tauriConf.bundle.linux.deb.files;
440
520
  // Generate correct desktop file configuration
441
- const appNameSafe = generateSafeFilename(name).toLowerCase();
521
+ const appNameSafe = getSafeAppName(name);
442
522
  const identifier = `com.pake.${appNameSafe}`;
443
523
  const desktopFileName = `${identifier}.desktop`;
444
524
  // Create desktop file content
@@ -489,22 +569,23 @@ StartupNotify=true
489
569
  }
490
570
  }
491
571
  // Set icon.
572
+ const safeAppName = getSafeAppName(name);
492
573
  const platformIconMap = {
493
574
  win32: {
494
575
  fileExt: '.ico',
495
- path: `png/${generateSafeFilename(name).toLowerCase()}_256.ico`,
576
+ path: `png/${safeAppName}_256.ico`,
496
577
  defaultIcon: 'png/icon_256.ico',
497
578
  message: 'Windows icon must be .ico and 256x256px.',
498
579
  },
499
580
  linux: {
500
581
  fileExt: '.png',
501
- path: `png/${generateSafeFilename(name).toLowerCase()}_512.png`,
582
+ path: `png/${safeAppName}_512.png`,
502
583
  defaultIcon: 'png/icon_512.png',
503
584
  message: 'Linux icon must be .png and 512x512px.',
504
585
  },
505
586
  darwin: {
506
587
  fileExt: '.icns',
507
- path: `icons/${generateSafeFilename(name).toLowerCase()}.icns`,
588
+ path: `icons/${safeAppName}.icns`,
508
589
  defaultIcon: 'icons/icon.icns',
509
590
  message: 'macOS icon must be .icns type.',
510
591
  },
@@ -548,8 +629,8 @@ StartupNotify=true
548
629
  // ้œ€่ฆๅˆคๆ–ญๅ›พๆ ‡ๆ ผๅผ๏ผŒ้ป˜่ฎคๅชๆ”ฏๆŒicoๅ’Œpngไธค็ง
549
630
  let iconExt = path.extname(systemTrayIcon).toLowerCase();
550
631
  if (iconExt == '.png' || iconExt == '.ico') {
551
- const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${generateSafeFilename(name).toLowerCase()}${iconExt}`);
552
- trayIconPath = `png/${generateSafeFilename(name).toLowerCase()}${iconExt}`;
632
+ const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
633
+ trayIconPath = `png/${safeAppName}${iconExt}`;
553
634
  await fsExtra.copy(systemTrayIcon, trayIcoPath);
554
635
  }
555
636
  else {
@@ -583,6 +664,7 @@ StartupNotify=true
583
664
  await fsExtra.writeFile(injectFilePath, '');
584
665
  }
585
666
  tauriConf.pake.proxy_url = proxyUrl || '';
667
+ tauriConf.pake.multi_instance = multiInstance;
586
668
  // Configure WASM support with required HTTP headers
587
669
  if (wasm) {
588
670
  tauriConf.app.security = {
@@ -659,6 +741,7 @@ class BaseBuilder {
659
741
  logger.warn('โœผ The first use requires installing system dependencies.');
660
742
  logger.warn('โœผ See more in https://tauri.app/start/prerequisites/.');
661
743
  }
744
+ ensureRustEnv();
662
745
  if (!checkRustInstalled()) {
663
746
  const res = await prompts({
664
747
  type: 'confirm',
@@ -691,10 +774,10 @@ class BaseBuilder {
691
774
  logger.info(`โœบ Located in China, using ${packageManager}/rsProxy CN mirror.`);
692
775
  const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
693
776
  await fsExtra.copy(projectCnConf, projectConf);
694
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption} --silent`, timeout, buildEnv);
777
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, buildEnv, this.options.debug);
695
778
  }
696
779
  else {
697
- await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption} --silent`, timeout, buildEnv);
780
+ await shellExec(`cd "${npmDirectory}" && ${packageManager} install${peerDepsOption}`, timeout, buildEnv, this.options.debug);
698
781
  }
699
782
  spinner.succeed(chalk.green('Package installed!'));
700
783
  if (!tauriTargetPathExists) {
@@ -719,8 +802,11 @@ class BaseBuilder {
719
802
  buildSpinner.stop();
720
803
  // Show static message to keep the status visible
721
804
  logger.warn('โœธ Building app...');
722
- const buildEnv = this.getBuildEnvironment();
723
- await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, this.getBuildTimeout(), buildEnv);
805
+ const buildEnv = {
806
+ ...this.getBuildEnvironment(),
807
+ ...(process.env.NO_STRIP && { NO_STRIP: process.env.NO_STRIP }),
808
+ };
809
+ await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand(packageManager)}`, this.getBuildTimeout(), buildEnv, this.options.debug);
724
810
  // Copy app
725
811
  const fileName = this.getFileName();
726
812
  const fileType = this.getFileType(target);
@@ -1162,6 +1248,7 @@ const DEFAULT_PAKE_OPTIONS = {
1162
1248
  wasm: false,
1163
1249
  enableDragDrop: false,
1164
1250
  keepBinary: false,
1251
+ multiInstance: false,
1165
1252
  };
1166
1253
 
1167
1254
  async function checkUpdateTips() {
@@ -1190,7 +1277,9 @@ const API_KEYS = {
1190
1277
  brandfetch: ['1idqvJC0CeFSeyp3Yf7', '1idej-yhU_ThggIHFyG'],
1191
1278
  };
1192
1279
  function generateIconPath(appName, isDefault = false) {
1193
- const safeName = isDefault ? 'icon' : generateSafeFilename(appName).toLowerCase();
1280
+ const safeName = isDefault
1281
+ ? 'icon'
1282
+ : generateSafeFilename(appName).toLowerCase();
1194
1283
  const baseName = safeName;
1195
1284
  if (IS_WIN) {
1196
1285
  return path.join(npmDirectory, 'src-tauri', 'png', `${baseName}_256.ico`);
@@ -1603,8 +1692,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
1603
1692
  program
1604
1693
  .addHelpText('beforeAll', logo)
1605
1694
  .usage(`[url] [options]`)
1606
- .showHelpAfterError()
1607
- .helpOption(false);
1695
+ .showHelpAfterError();
1608
1696
  program
1609
1697
  .argument('[url]', 'The web URL you want to package', validateUrlInput)
1610
1698
  // Refer to https://github.com/tj/commander.js#custom-option-processing, turn string array into a string connected with custom connectors.
@@ -1683,6 +1771,9 @@ program
1683
1771
  .addOption(new Option('--keep-binary', 'Keep raw binary file alongside installer')
1684
1772
  .default(DEFAULT_PAKE_OPTIONS.keepBinary)
1685
1773
  .hideHelp())
1774
+ .addOption(new Option('--multi-instance', 'Allow multiple app instances')
1775
+ .default(DEFAULT_PAKE_OPTIONS.multiInstance)
1776
+ .hideHelp())
1686
1777
  .addOption(new Option('--installer-language <string>', 'Installer language')
1687
1778
  .default(DEFAULT_PAKE_OPTIONS.installerLanguage)
1688
1779
  .hideHelp())
@@ -1690,12 +1781,12 @@ program
1690
1781
  .configureHelp({
1691
1782
  sortSubcommands: true,
1692
1783
  optionTerm: (option) => {
1693
- if (option.flags === '-v, --version')
1784
+ if (option.flags === '-v, --version' || option.flags === '-h, --help')
1694
1785
  return '';
1695
1786
  return option.flags;
1696
1787
  },
1697
1788
  optionDescription: (option) => {
1698
- if (option.flags === '-v, --version')
1789
+ if (option.flags === '-v, --version' || option.flags === '-h, --help')
1699
1790
  return '';
1700
1791
  return option.description;
1701
1792
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pake-cli",
3
- "version": "3.3.6",
3
+ "version": "3.4.0",
4
4
  "description": "๐Ÿคฑ๐Ÿป Turn any webpage into a desktop app with one command. ๐Ÿคฑ๐Ÿป ไธ€้”ฎๆ‰“ๅŒ…็ฝ‘้กต็”Ÿๆˆ่ฝป้‡ๆกŒ้ขๅบ”็”จใ€‚",
5
5
  "engines": {
6
6
  "node": ">=18.0.0"
@@ -52,12 +52,12 @@
52
52
  "dependencies": {
53
53
  "@tauri-apps/api": "^2.8.0",
54
54
  "@tauri-apps/cli": "^2.8.4",
55
- "axios": "^1.11.0",
56
- "chalk": "^5.6.0",
55
+ "axios": "^1.12.2",
56
+ "chalk": "^5.6.2",
57
57
  "commander": "^12.1.0",
58
58
  "execa": "^9.6.0",
59
59
  "file-type": "^18.7.0",
60
- "fs-extra": "^11.3.1",
60
+ "fs-extra": "^11.3.2",
61
61
  "icon-gen": "^5.0.0",
62
62
  "loglevel": "^1.9.2",
63
63
  "ora": "^8.2.0",
@@ -74,7 +74,7 @@
74
74
  "@rollup/plugin-replace": "^6.0.2",
75
75
  "@rollup/plugin-terser": "^0.4.4",
76
76
  "@types/fs-extra": "^11.0.4",
77
- "@types/node": "^20.19.13",
77
+ "@types/node": "^20.19.21",
78
78
  "@types/page-icon": "^0.3.6",
79
79
  "@types/prompts": "^2.4.9",
80
80
  "@types/tmp": "^0.2.6",
@@ -82,9 +82,9 @@
82
82
  "app-root-path": "^3.1.0",
83
83
  "cross-env": "^7.0.3",
84
84
  "prettier": "^3.6.2",
85
- "rollup": "^4.50.0",
85
+ "rollup": "^4.52.4",
86
86
  "rollup-plugin-typescript2": "^0.36.0",
87
87
  "tslib": "^2.8.1",
88
- "typescript": "^5.9.2"
88
+ "typescript": "^5.9.3"
89
89
  }
90
90
  }
@@ -31,5 +31,6 @@
31
31
  },
32
32
  "system_tray_path": "png/icon_512.png",
33
33
  "inject": [],
34
- "proxy_url": ""
34
+ "proxy_url": "",
35
+ "multi_instance": false
35
36
  }
@@ -1,6 +1,6 @@
1
1
  # This file is automatically @generated by Cargo.
2
2
  # It is not intended for manual editing.
3
- version = 3
3
+ version = 4
4
4
 
5
5
  [[package]]
6
6
  name = "addr2line"
@@ -3546,10 +3546,11 @@ dependencies = [
3546
3546
 
3547
3547
  [[package]]
3548
3548
  name = "serde"
3549
- version = "1.0.219"
3549
+ version = "1.0.228"
3550
3550
  source = "registry+https://github.com/rust-lang/crates.io-index"
3551
- checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
3551
+ checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
3552
3552
  dependencies = [
3553
+ "serde_core",
3553
3554
  "serde_derive",
3554
3555
  ]
3555
3556
 
@@ -3564,11 +3565,20 @@ dependencies = [
3564
3565
  "typeid",
3565
3566
  ]
3566
3567
 
3568
+ [[package]]
3569
+ name = "serde_core"
3570
+ version = "1.0.228"
3571
+ source = "registry+https://github.com/rust-lang/crates.io-index"
3572
+ checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
3573
+ dependencies = [
3574
+ "serde_derive",
3575
+ ]
3576
+
3567
3577
  [[package]]
3568
3578
  name = "serde_derive"
3569
- version = "1.0.219"
3579
+ version = "1.0.228"
3570
3580
  source = "registry+https://github.com/rust-lang/crates.io-index"
3571
- checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
3581
+ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
3572
3582
  dependencies = [
3573
3583
  "proc-macro2",
3574
3584
  "quote",
@@ -3588,14 +3598,15 @@ dependencies = [
3588
3598
 
3589
3599
  [[package]]
3590
3600
  name = "serde_json"
3591
- version = "1.0.143"
3601
+ version = "1.0.145"
3592
3602
  source = "registry+https://github.com/rust-lang/crates.io-index"
3593
- checksum = "d401abef1d108fbd9cbaebc3e46611f4b1021f714a0597a71f41ee463f5f4a5a"
3603
+ checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c"
3594
3604
  dependencies = [
3595
3605
  "itoa",
3596
3606
  "memchr",
3597
3607
  "ryu",
3598
3608
  "serde",
3609
+ "serde_core",
3599
3610
  ]
3600
3611
 
3601
3612
  [[package]]
@@ -15,19 +15,19 @@ crate-type = ["staticlib", "cdylib", "lib"]
15
15
  # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
16
16
 
17
17
  [build-dependencies]
18
- tauri-build = { version = "2.4.0", features = [] }
18
+ tauri-build = { version = "2.4.1", features = [] }
19
19
 
20
20
  [dependencies]
21
- serde_json = "1.0.143"
22
- serde = { version = "1.0.219", features = ["derive"] }
21
+ serde_json = "1.0.145"
22
+ serde = { version = "1.0.228", features = ["derive"] }
23
23
  tokio = { version = "1.47.1", features = ["full"] }
24
- tauri = { version = "2.8.4", features = ["tray-icon", "image-ico", "image-png", "macos-proxy"] }
24
+ tauri = { version = "2.8.5", features = ["tray-icon", "image-ico", "image-png", "macos-proxy"] }
25
25
  tauri-plugin-window-state = "2.4.0"
26
26
  tauri-plugin-oauth = "2.0.0"
27
27
  tauri-plugin-http = "2.5.2"
28
28
  tauri-plugin-global-shortcut = { version = "2.3.0" }
29
29
  tauri-plugin-shell = "2.3.1"
30
- tauri-plugin-single-instance = "2.3.3"
30
+ tauri-plugin-single-instance = "2.3.4"
31
31
  tauri-plugin-notification = "2.3.1"
32
32
 
33
33
  [features]
@@ -30,5 +30,6 @@
30
30
  },
31
31
  "system_tray_path": "icons/icon.png",
32
32
  "inject": [],
33
- "proxy_url": ""
33
+ "proxy_url": "",
34
+ "multi_instance": false
34
35
  }
@@ -59,6 +59,8 @@ pub struct PakeConfig {
59
59
  pub system_tray: FunctionON,
60
60
  pub system_tray_path: String,
61
61
  pub proxy_url: String,
62
+ #[serde(default)]
63
+ pub multi_instance: bool,
62
64
  }
63
65
 
64
66
  impl PakeConfig {
@@ -100,21 +100,6 @@ const DOWNLOADABLE_FILE_EXTENSIONS = {
100
100
  "scss",
101
101
  "sass",
102
102
  "less",
103
- "html",
104
- "htm",
105
- "php",
106
- "py",
107
- "java",
108
- "cpp",
109
- "c",
110
- "h",
111
- "cs",
112
- "rb",
113
- "go",
114
- "rs",
115
- "swift",
116
- "kt",
117
- "scala",
118
103
  "sh",
119
104
  "bat",
120
105
  "ps1",
@@ -163,6 +148,22 @@ function isChineseLanguage(language = getUserLanguage()) {
163
148
  );
164
149
  }
165
150
 
151
+ // User notification helper
152
+ function showDownloadError(filename) {
153
+ const isChinese = isChineseLanguage();
154
+ const message = isChinese
155
+ ? `ไธ‹่ฝฝๅคฑ่ดฅ: ${filename}`
156
+ : `Download failed: ${filename}`;
157
+
158
+ if (window.Notification && Notification.permission === "granted") {
159
+ new Notification(isChinese ? "ไธ‹่ฝฝ้”™่ฏฏ" : "Download Error", {
160
+ body: message,
161
+ });
162
+ } else {
163
+ console.error(message);
164
+ }
165
+ }
166
+
166
167
  // Unified file detection - replaces both isDownloadLink and isFileLink
167
168
  function isDownloadableFile(url) {
168
169
  try {
@@ -266,36 +267,42 @@ document.addEventListener("DOMContentLoaded", () => {
266
267
  }
267
268
 
268
269
  // write the ArrayBuffer to a binary, and you're done
269
- const userLanguage = navigator.language || navigator.userLanguage;
270
+ const userLanguage = getUserLanguage();
270
271
  invoke("download_file_by_binary", {
271
272
  params: {
272
273
  filename,
273
274
  binary: Array.from(binary),
274
275
  language: userLanguage,
275
276
  },
276
- }).catch(error => {
277
- console.error('Failed to download data URI file:', filename, error);
277
+ }).catch((error) => {
278
+ console.error("Failed to download data URI file:", filename, error);
279
+ showDownloadError(filename);
278
280
  });
279
281
  } catch (error) {
280
- console.error('Failed to process data URI:', dataURI, error);
282
+ console.error("Failed to process data URI:", dataURI, error);
283
+ showDownloadError(filename || "file");
281
284
  }
282
285
  }
283
286
 
284
287
  function downloadFromBlobUrl(blobUrl, filename) {
285
- convertBlobUrlToBinary(blobUrl).then((binary) => {
286
- const userLanguage = navigator.language || navigator.userLanguage;
287
- invoke("download_file_by_binary", {
288
- params: {
289
- filename,
290
- binary,
291
- language: userLanguage,
292
- },
293
- }).catch(error => {
294
- console.error('Failed to download blob file:', filename, error);
288
+ convertBlobUrlToBinary(blobUrl)
289
+ .then((binary) => {
290
+ const userLanguage = getUserLanguage();
291
+ invoke("download_file_by_binary", {
292
+ params: {
293
+ filename,
294
+ binary,
295
+ language: userLanguage,
296
+ },
297
+ }).catch((error) => {
298
+ console.error("Failed to download blob file:", filename, error);
299
+ showDownloadError(filename);
300
+ });
301
+ })
302
+ .catch((error) => {
303
+ console.error("Failed to convert blob to binary:", blobUrl, error);
304
+ showDownloadError(filename);
295
305
  });
296
- }).catch(error => {
297
- console.error('Failed to convert blob to binary:', blobUrl, error);
298
- });
299
306
  }
300
307
 
301
308
  // detect blob download by createElement("a")
@@ -335,14 +342,14 @@ document.addEventListener("DOMContentLoaded", () => {
335
342
  const handleExternalLink = (url) => {
336
343
  // Don't try to open blob: or data: URLs with shell
337
344
  if (isSpecialDownload(url)) {
338
- console.warn('Cannot open special URL with shell:', url);
345
+ console.warn("Cannot open special URL with shell:", url);
339
346
  return;
340
347
  }
341
348
 
342
349
  invoke("plugin:shell|open", {
343
350
  path: url,
344
- }).catch(error => {
345
- console.error('Failed to open URL with shell:', url, error);
351
+ }).catch((error) => {
352
+ console.error("Failed to open URL with shell:", url, error);
346
353
  });
347
354
  };
348
355
 
@@ -370,7 +377,7 @@ document.addEventListener("DOMContentLoaded", () => {
370
377
 
371
378
  const detectAnchorElementClick = (e) => {
372
379
  // Safety check: ensure e.target exists and is an Element with closest method
373
- if (!e.target || typeof e.target.closest !== 'function') {
380
+ if (!e.target || typeof e.target.closest !== "function") {
374
381
  return;
375
382
  }
376
383
  const anchorElement = e.target.closest("a");
@@ -383,20 +390,21 @@ document.addEventListener("DOMContentLoaded", () => {
383
390
 
384
391
  // Handle _blank links: same domain navigates in-app, cross-domain opens new window
385
392
  if (target === "_blank") {
386
- e.preventDefault();
387
- e.stopImmediatePropagation();
388
-
389
393
  if (isSameDomain(absoluteUrl)) {
390
- window.location.href = absoluteUrl;
391
- } else {
392
- const newWindow = originalWindowOpen.call(
393
- window,
394
- absoluteUrl,
395
- "_blank",
396
- "width=1200,height=800,scrollbars=yes,resizable=yes",
397
- );
398
- if (!newWindow) handleExternalLink(absoluteUrl);
394
+ // For same-domain links, let the browser/SPA handle it naturally
395
+ // This prevents full page reload in SPA apps like Discord
396
+ return;
399
397
  }
398
+
399
+ e.preventDefault();
400
+ e.stopImmediatePropagation();
401
+ const newWindow = originalWindowOpen.call(
402
+ window,
403
+ absoluteUrl,
404
+ "_blank",
405
+ "width=1200,height=800,scrollbars=yes,resizable=yes",
406
+ );
407
+ if (!newWindow) handleExternalLink(absoluteUrl);
400
408
  return;
401
409
  }
402
410
 
@@ -446,39 +454,24 @@ document.addEventListener("DOMContentLoaded", () => {
446
454
  // Rewrite the window.open function.
447
455
  const originalWindowOpen = window.open;
448
456
  window.open = function (url, name, specs) {
449
- // Apple login and google login
450
457
  if (name === "AppleAuthentication") {
451
- //do nothing
452
- } else if (
453
- specs &&
454
- (specs.includes("height=") || specs.includes("width="))
455
- ) {
456
- location.href = url;
457
- } else {
458
+ return originalWindowOpen.call(window, url, name, specs);
459
+ }
460
+
461
+ try {
458
462
  const baseUrl = window.location.origin + window.location.pathname;
459
463
  const hrefUrl = new URL(url, baseUrl);
460
464
  const absoluteUrl = hrefUrl.href;
461
465
 
462
- // Apply same domain logic as anchor links
463
- if (isSameDomain(absoluteUrl)) {
464
- // Same domain: navigate in app or open new window based on specs
465
- if (name === "_blank" || !name) {
466
- return originalWindowOpen.call(
467
- window,
468
- absoluteUrl,
469
- "_blank",
470
- "width=1200,height=800,scrollbars=yes,resizable=yes",
471
- );
472
- } else {
473
- location.href = absoluteUrl;
474
- }
475
- } else {
476
- // Cross domain: open in external browser
466
+ if (!isSameDomain(absoluteUrl)) {
477
467
  handleExternalLink(absoluteUrl);
468
+ return null;
478
469
  }
470
+
471
+ return originalWindowOpen.call(window, absoluteUrl, name, specs);
472
+ } catch (error) {
473
+ return originalWindowOpen.call(window, url, name, specs);
479
474
  }
480
- // Call the original window.open function to maintain its normal functionality.
481
- return originalWindowOpen.call(window, url, name, specs);
482
475
  };
483
476
 
484
477
  // Set the default zoom, There are problems with Loop without using try-catch.
@@ -686,13 +679,16 @@ document.addEventListener("DOMContentLoaded", () => {
686
679
  }
687
680
  } else {
688
681
  // Regular HTTP(S) image
689
- const userLanguage = navigator.language || navigator.userLanguage;
682
+ const userLanguage = getUserLanguage();
690
683
  invoke("download_file", {
691
684
  params: {
692
685
  url: imageUrl,
693
686
  filename: filename,
694
687
  language: userLanguage,
695
688
  },
689
+ }).catch((error) => {
690
+ console.error("Failed to download image:", filename, error);
691
+ showDownloadError(filename);
696
692
  });
697
693
  }
698
694
  }
@@ -723,7 +719,10 @@ document.addEventListener("DOMContentLoaded", () => {
723
719
  }
724
720
 
725
721
  // Check for parent elements with background images
726
- const parentWithBg = target && typeof target.closest === 'function' ? target.closest('[style*="background-image"]') : null;
722
+ const parentWithBg =
723
+ target && typeof target.closest === "function"
724
+ ? target.closest('[style*="background-image"]')
725
+ : null;
727
726
  if (parentWithBg) {
728
727
  const bgImage = parentWithBg.style.backgroundImage;
729
728
  const urlMatch = bgImage.match(/url\(["']?([^"')]+)["']?\)/);
@@ -737,7 +736,7 @@ document.addEventListener("DOMContentLoaded", () => {
737
736
 
738
737
  // Simplified menu builder
739
738
  function buildMenuItems(type, data) {
740
- const userLanguage = navigator.language || navigator.userLanguage;
739
+ const userLanguage = getUserLanguage();
741
740
  const items = [];
742
741
 
743
742
  switch (type) {
@@ -764,6 +763,9 @@ document.addEventListener("DOMContentLoaded", () => {
764
763
  const filename = getFilenameFromUrl(data.url);
765
764
  invoke("download_file", {
766
765
  params: { url: data.url, filename, language: userLanguage },
766
+ }).catch((error) => {
767
+ console.error("Failed to download file:", filename, error);
768
+ showDownloadError(filename);
767
769
  });
768
770
  }),
769
771
  );
@@ -792,7 +794,10 @@ document.addEventListener("DOMContentLoaded", () => {
792
794
  const mediaInfo = getMediaInfo(target);
793
795
 
794
796
  // Check for links (but not if it's media)
795
- const linkElement = target && typeof target.closest === 'function' ? target.closest("a") : null;
797
+ const linkElement =
798
+ target && typeof target.closest === "function"
799
+ ? target.closest("a")
800
+ : null;
796
801
  const isLink = linkElement && linkElement.href && !mediaInfo.isMedia;
797
802
 
798
803
  // Only show custom menu for media or links
@@ -24,6 +24,7 @@ pub fn run_app() {
24
24
  let hide_on_close = pake_config.windows[0].hide_on_close;
25
25
  let activation_shortcut = pake_config.windows[0].activation_shortcut.clone();
26
26
  let init_fullscreen = pake_config.windows[0].fullscreen;
27
+ let multi_instance = pake_config.multi_instance;
27
28
 
28
29
  let window_state_plugin = WindowStatePlugin::default()
29
30
  .with_state_flags(if init_fullscreen {
@@ -35,19 +36,25 @@ pub fn run_app() {
35
36
  .build();
36
37
 
37
38
  #[allow(deprecated)]
38
- tauri_app
39
+ let mut app_builder = tauri_app
39
40
  .plugin(window_state_plugin)
40
41
  .plugin(tauri_plugin_oauth::init())
41
42
  .plugin(tauri_plugin_http::init())
42
43
  .plugin(tauri_plugin_shell::init())
43
- .plugin(tauri_plugin_notification::init())
44
- .plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
44
+ .plugin(tauri_plugin_notification::init());
45
+
46
+ // Only add single instance plugin if multiple instances are not allowed
47
+ if !multi_instance {
48
+ app_builder = app_builder.plugin(tauri_plugin_single_instance::init(|app, _args, _cwd| {
45
49
  if let Some(window) = app.get_webview_window("pake") {
46
50
  let _ = window.unminimize();
47
51
  let _ = window.show();
48
52
  let _ = window.set_focus();
49
53
  }
50
- }))
54
+ }));
55
+ }
56
+
57
+ app_builder
51
58
  .invoke_handler(tauri::generate_handler![
52
59
  download_file,
53
60
  download_file_by_binary,
@@ -1,10 +0,0 @@
1
- [source.crates-io]
2
- replace-with = 'rsproxy-sparse'
3
- [source.rsproxy]
4
- registry = "https://rsproxy.cn/crates.io-index"
5
- [source.rsproxy-sparse]
6
- registry = "sparse+https://rsproxy.cn/index/"
7
- [registries.rsproxy]
8
- index = "https://rsproxy.cn/crates.io-index"
9
- [net]
10
- git-fetch-with-cli = true