pake-cli 3.13.0 → 3.14.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 +19 -3
- package/dist/cli.js +126 -59
- package/package.json +1 -1
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/icons/notion.icns +0 -0
- package/src-tauri/icons/youtubemusic.icns +0 -0
- package/src-tauri/pake.json +1 -0
- package/src-tauri/png/notion_256.ico +0 -0
- package/src-tauri/png/notion_32.ico +0 -0
- package/src-tauri/png/notion_512.png +0 -0
- package/src-tauri/png/youtubemusic_256.ico +0 -0
- package/src-tauri/png/youtubemusic_32.ico +0 -0
- package/src-tauri/png/youtubemusic_512.png +0 -0
- package/src-tauri/src/app/config.rs +2 -0
- package/src-tauri/src/app/invoke.rs +5 -2
- package/src-tauri/src/app/window.rs +17 -4
- package/src-tauri/src/inject/event.js +288 -27
- package/src-tauri/src/inject/style.js +28 -3
- package/src-tauri/src/util.rs +49 -0
- package/src-tauri/tauri.conf.json +1 -1
package/README.md
CHANGED
|
@@ -130,6 +130,22 @@
|
|
|
130
130
|
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Excalidraw.png width=600/></td>
|
|
131
131
|
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/XiaoHongShu.png width=600/></td>
|
|
132
132
|
</tr>
|
|
133
|
+
<tr>
|
|
134
|
+
<td>Notion
|
|
135
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Notion.dmg">Mac</a>
|
|
136
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Notion_x64.msi">Windows</a>
|
|
137
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Notion_x86_64.deb">Linux</a>
|
|
138
|
+
</td>
|
|
139
|
+
<td>Flomo
|
|
140
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Flomo.dmg">Mac</a>
|
|
141
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Flomo_x64.msi">Windows</a>
|
|
142
|
+
<a href="https://github.com/tw93/Pake/releases/latest/download/Flomo_x86_64.deb">Linux</a>
|
|
143
|
+
</td>
|
|
144
|
+
</tr>
|
|
145
|
+
<tr>
|
|
146
|
+
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Notion.png width=600/></td>
|
|
147
|
+
<td><img src=https://raw.githubusercontent.com/tw93/static/main/pake/Flomo.png width=600/></td>
|
|
148
|
+
</tr>
|
|
133
149
|
</table>
|
|
134
150
|
|
|
135
151
|
<details>
|
|
@@ -153,8 +169,9 @@
|
|
|
153
169
|
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>H</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>H</kbd> | Go to Home Page |
|
|
154
170
|
| <kbd>⌘</kbd> + <kbd>⌥</kbd> + <kbd>I</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>I</kbd> | Toggle Developer Tools (Debug Only) |
|
|
155
171
|
| <kbd>⌘</kbd> + <kbd>⇧</kbd> + <kbd>⌫</kbd> | <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>Del</kbd> | Clear Cache & Restart |
|
|
172
|
+
| <kbd>⌃</kbd> + <kbd>⌘</kbd> + <kbd>F</kbd> | <kbd>F11</kbd> | Toggle native window fullscreen |
|
|
156
173
|
|
|
157
|
-
In addition, double-click the title bar to switch to full-screen mode. For Mac users, you can also use the gesture to go to the previous or next page and drag the title bar to move the window. The new menu also offers options for navigation, zoom, and window controls.
|
|
174
|
+
In addition, double-click the title bar to switch to full-screen mode. On Windows and Linux, use `--hide-window-decorations` for a frameless window with a top drag region. For Mac users, you can also use the gesture to go to the previous or next page and drag the title bar to move the window. The new menu also offers options for navigation, zoom, and window controls.
|
|
158
175
|
|
|
159
176
|
</details>
|
|
160
177
|
|
|
@@ -203,8 +220,7 @@ Pake's development can not be without these Hackers. They contributed a lot of c
|
|
|
203
220
|
## Support
|
|
204
221
|
|
|
205
222
|
- The most direct way to support me is getting [Mole for Mac](https://mole.fit), my paid Mac cleanup app.
|
|
206
|
-
- If Pake helped you, [share it](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20Turn%20any%20webpage%20into%20a%20desktop%20app%20with%20one%20command.%20Nearly%2020x%20smaller%20than%20Electron%20packages,%20supports%20macOS%20Windows%20Linux)
|
|
207
|
-
- Got ideas or bugs? Open an issue or PR, feel free to contribute your best AI model.
|
|
223
|
+
- If Pake helped you, give it a star, [share it](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=Pake%20-%20Turn%20any%20webpage%20into%20a%20desktop%20app%20with%20one%20command.%20Nearly%2020x%20smaller%20than%20Electron%20packages,%20supports%20macOS%20Windows%20Linux), or open an issue or PR.
|
|
208
224
|
- I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href="https://cats.tw93.fun?name=Pake" target="_blank">canned food 🥩</a>.
|
|
209
225
|
|
|
210
226
|
<details>
|
package/dist/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import * as psl from 'psl';
|
|
|
20
20
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
21
21
|
|
|
22
22
|
var name = "pake-cli";
|
|
23
|
-
var version = "3.
|
|
23
|
+
var version = "3.14.0";
|
|
24
24
|
var description = "🤱🏻 Turn any webpage into a desktop app with one command. 🤱🏻 一键打包网页生成轻量桌面应用。";
|
|
25
25
|
var engines = {
|
|
26
26
|
node: ">=18.0.0"
|
|
@@ -435,9 +435,13 @@ async function combineFiles(files, output) {
|
|
|
435
435
|
});`;
|
|
436
436
|
}
|
|
437
437
|
const fileContent = await fs$1.readFile(file);
|
|
438
|
-
|
|
438
|
+
// Keep the closing `});` on its own line. If the injected file ends in a
|
|
439
|
+
// line comment without a trailing newline, appending ` });` on the same
|
|
440
|
+
// line would comment it out and break the wrapper (mirrors the .css
|
|
441
|
+
// branch above, which already closes on a separate line).
|
|
442
|
+
return ("window.addEventListener('DOMContentLoaded', (_event) => {\n" +
|
|
439
443
|
fileContent +
|
|
440
|
-
'
|
|
444
|
+
'\n});');
|
|
441
445
|
}));
|
|
442
446
|
await fs$1.writeFile(output, contents.join('\n'));
|
|
443
447
|
return files;
|
|
@@ -508,6 +512,18 @@ function filterLinuxTargets(targets) {
|
|
|
508
512
|
function needsTemporaryDebForZst(targets) {
|
|
509
513
|
return targets.includes('zst') && !targets.includes('deb');
|
|
510
514
|
}
|
|
515
|
+
// Resolves the Tauri `bundle.targets` list for a Linux build from a
|
|
516
|
+
// comma-separated --targets string (e.g. the distro-aware default
|
|
517
|
+
// "deb,appimage"). zst is repacked from the deb payload, so it maps to a deb
|
|
518
|
+
// bundle. hasValidTarget is false only when no known target is present, which
|
|
519
|
+
// is the single case that should warn and fall back to the default.
|
|
520
|
+
function resolveLinuxBundleTargets(targets) {
|
|
521
|
+
const requested = filterLinuxTargets(targets);
|
|
522
|
+
const bundleTargets = [
|
|
523
|
+
...new Set(requested.map((target) => (target === 'zst' ? 'deb' : target))),
|
|
524
|
+
];
|
|
525
|
+
return { bundleTargets, hasValidTarget: requested.length > 0 };
|
|
526
|
+
}
|
|
511
527
|
|
|
512
528
|
/**
|
|
513
529
|
* Pure transform from CLI options to the window-config slice that gets
|
|
@@ -519,6 +535,7 @@ function needsTemporaryDebForZst(targets) {
|
|
|
519
535
|
function buildWindowConfigOverrides(options, platform = asSupportedPlatform(process.platform)) {
|
|
520
536
|
const platformHideOnClose = options.hideOnClose ?? platform === 'darwin';
|
|
521
537
|
const platformHideTitleBar = platform === 'darwin' ? options.hideTitleBar : false;
|
|
538
|
+
const platformHideWindowDecorations = platform !== 'darwin' ? options.hideWindowDecorations : false;
|
|
522
539
|
return {
|
|
523
540
|
width: options.width,
|
|
524
541
|
height: options.height,
|
|
@@ -526,6 +543,7 @@ function buildWindowConfigOverrides(options, platform = asSupportedPlatform(proc
|
|
|
526
543
|
maximize: options.maximize,
|
|
527
544
|
resizable: options.resizable ?? true,
|
|
528
545
|
hide_title_bar: platformHideTitleBar,
|
|
546
|
+
hide_window_decorations: platformHideWindowDecorations,
|
|
529
547
|
activation_shortcut: options.activationShortcut,
|
|
530
548
|
always_on_top: options.alwaysOnTop,
|
|
531
549
|
dark_mode: options.darkMode,
|
|
@@ -596,30 +614,31 @@ async function handleLocalFile(url, useLocalFile, tauriConf) {
|
|
|
596
614
|
tauriConf.pake.windows[0].url_type = 'web';
|
|
597
615
|
}
|
|
598
616
|
}
|
|
599
|
-
|
|
600
|
-
const linuxBundle = tauriConf.bundle.linux;
|
|
601
|
-
if (!linuxBundle) {
|
|
602
|
-
throw new Error('Linux bundle configuration is missing from tauri.linux.conf.json; cannot build Linux target.');
|
|
603
|
-
}
|
|
604
|
-
delete linuxBundle.deb.files;
|
|
605
|
-
const linuxName = generateLinuxPackageName(name);
|
|
606
|
-
const desktopFileName = `com.pake.${linuxName}.desktop`;
|
|
607
|
-
const iconName = `${linuxName}_512`;
|
|
608
|
-
const { title } = options;
|
|
617
|
+
function buildLinuxDesktopContent(name, title, linuxBinaryName) {
|
|
609
618
|
const chineseName = title && /[\u4e00-\u9fa5]/.test(title) ? title : null;
|
|
610
|
-
|
|
619
|
+
return `[Desktop Entry]
|
|
611
620
|
Version=1.0
|
|
612
621
|
Type=Application
|
|
613
622
|
Name=${name}
|
|
614
623
|
${chineseName ? `Name[zh_CN]=${chineseName}` : ''}
|
|
615
624
|
Comment=${name}
|
|
616
625
|
Exec=${linuxBinaryName}
|
|
617
|
-
Icon=${
|
|
626
|
+
Icon=${linuxBinaryName}
|
|
618
627
|
Categories=Network;WebBrowser;Utility;
|
|
619
628
|
MimeType=text/html;text/xml;application/xhtml_xml;
|
|
620
629
|
StartupNotify=true
|
|
621
630
|
Terminal=false
|
|
622
631
|
`;
|
|
632
|
+
}
|
|
633
|
+
async function mergeLinuxConfig(options, name, tauriConf, linuxBinaryName) {
|
|
634
|
+
const linuxBundle = tauriConf.bundle.linux;
|
|
635
|
+
if (!linuxBundle) {
|
|
636
|
+
throw new Error('Linux bundle configuration is missing from tauri.linux.conf.json; cannot build Linux target.');
|
|
637
|
+
}
|
|
638
|
+
delete linuxBundle.deb.files;
|
|
639
|
+
const linuxName = generateLinuxPackageName(name);
|
|
640
|
+
const desktopFileName = `com.pake.${linuxName}.desktop`;
|
|
641
|
+
const desktopContent = buildLinuxDesktopContent(name, options.title, linuxBinaryName);
|
|
623
642
|
const srcAssetsDir = path.join(npmDirectory, 'src-tauri/assets');
|
|
624
643
|
const srcDesktopFilePath = path.join(srcAssetsDir, desktopFileName);
|
|
625
644
|
await fsExtra.ensureDir(srcAssetsDir);
|
|
@@ -634,19 +653,44 @@ Terminal=false
|
|
|
634
653
|
linuxBundle.rpm.files = {
|
|
635
654
|
[desktopInstallPath]: `assets/${desktopFileName}`,
|
|
636
655
|
};
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
// zst is repacked from the deb payload, so Tauri itself bundles a deb.
|
|
646
|
-
tauriConf.bundle.targets = [baseTarget === 'zst' ? 'deb' : baseTarget];
|
|
656
|
+
// options.targets reaches here already stripped of any -arm64 suffix by the
|
|
657
|
+
// LinuxBuilder constructor, and may carry several comma-separated formats
|
|
658
|
+
// (e.g. the distro-aware default "deb,appimage"). Validate the parsed list
|
|
659
|
+
// rather than string-matching the whole value, so a valid multi-target
|
|
660
|
+
// default no longer trips the "must be one of ..." warning on every build.
|
|
661
|
+
const { bundleTargets, hasValidTarget } = resolveLinuxBundleTargets(options.targets);
|
|
662
|
+
if (hasValidTarget) {
|
|
663
|
+
tauriConf.bundle.targets = bundleTargets;
|
|
647
664
|
}
|
|
648
665
|
else {
|
|
649
|
-
logger.warn(`✼ The target must be one of ${
|
|
666
|
+
logger.warn(`✼ The target must be one of ${LINUX_TARGET_TYPES.join(', ')}, the default 'deb' will be used.`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
async function resolveSystemTrayIconPath(systemTrayIcon, defaultTrayIconPath, safeAppName, iconOutputDir = path.join(npmDirectory, 'src-tauri/png')) {
|
|
670
|
+
if (systemTrayIcon.length === 0) {
|
|
671
|
+
return defaultTrayIconPath;
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const iconExt = path.extname(systemTrayIcon).toLowerCase();
|
|
675
|
+
if (iconExt !== '.png' && iconExt !== '.ico') {
|
|
676
|
+
logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
|
|
677
|
+
logger.warn(`✼ Default system tray icon will be used.`);
|
|
678
|
+
return defaultTrayIconPath;
|
|
679
|
+
}
|
|
680
|
+
if (!(await fsExtra.pathExists(systemTrayIcon))) {
|
|
681
|
+
logger.warn(`✼ System tray icon "${systemTrayIcon}" was not found.`);
|
|
682
|
+
logger.warn(`✼ Default system tray icon will be used.`);
|
|
683
|
+
return defaultTrayIconPath;
|
|
684
|
+
}
|
|
685
|
+
const trayIconPath = `png/${safeAppName}${iconExt}`;
|
|
686
|
+
const trayIcoPath = path.join(iconOutputDir, `${safeAppName}${iconExt}`);
|
|
687
|
+
await fsExtra.copy(systemTrayIcon, trayIcoPath);
|
|
688
|
+
return trayIconPath;
|
|
689
|
+
}
|
|
690
|
+
catch (err) {
|
|
691
|
+
logger.warn(`✼ Failed to apply system tray icon "${systemTrayIcon}": ${err instanceof Error ? err.message : String(err)}`);
|
|
692
|
+
logger.warn(`✼ Default system tray icon will remain unchanged.`);
|
|
693
|
+
return defaultTrayIconPath;
|
|
650
694
|
}
|
|
651
695
|
}
|
|
652
696
|
async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
|
|
@@ -709,26 +753,8 @@ async function mergeIcons(options, name, tauriConf, platform, safeAppName) {
|
|
|
709
753
|
tauriConf.bundle.icon = [iconInfo.defaultIcon];
|
|
710
754
|
}
|
|
711
755
|
// Set tray icon path.
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
try {
|
|
715
|
-
await fsExtra.pathExists(options.systemTrayIcon);
|
|
716
|
-
const iconExt = path.extname(options.systemTrayIcon).toLowerCase();
|
|
717
|
-
if (iconExt === '.png' || iconExt === '.ico') {
|
|
718
|
-
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${safeAppName}${iconExt}`);
|
|
719
|
-
trayIconPath = `png/${safeAppName}${iconExt}`;
|
|
720
|
-
await fsExtra.copy(options.systemTrayIcon, trayIcoPath);
|
|
721
|
-
}
|
|
722
|
-
else {
|
|
723
|
-
logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`);
|
|
724
|
-
logger.warn(`✼ Default system tray icon will be used.`);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
catch (err) {
|
|
728
|
-
logger.warn(`✼ Failed to apply system tray icon "${options.systemTrayIcon}": ${err instanceof Error ? err.message : String(err)}`);
|
|
729
|
-
logger.warn(`✼ Default system tray icon will remain unchanged.`);
|
|
730
|
-
}
|
|
731
|
-
}
|
|
756
|
+
const defaultTrayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0];
|
|
757
|
+
const trayIconPath = await resolveSystemTrayIconPath(options.systemTrayIcon, defaultTrayIconPath, safeAppName);
|
|
732
758
|
tauriConf.pake.system_tray_path = trayIconPath;
|
|
733
759
|
delete tauriConf.app.trayIcon;
|
|
734
760
|
}
|
|
@@ -803,6 +829,9 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
803
829
|
if (options.hideTitleBar && platform !== 'darwin') {
|
|
804
830
|
logger.warn('✼ --hide-title-bar is only supported on macOS and will be ignored on this platform.');
|
|
805
831
|
}
|
|
832
|
+
if (options.hideWindowDecorations && platform === 'darwin') {
|
|
833
|
+
logger.warn('✼ --hide-window-decorations is only supported on Windows and Linux and will be ignored on this platform.');
|
|
834
|
+
}
|
|
806
835
|
const tauriConfWindowOptions = buildWindowConfigOverrides(options, platform);
|
|
807
836
|
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
|
|
808
837
|
tauriConf.productName = name;
|
|
@@ -1141,6 +1170,16 @@ class BaseBuilder {
|
|
|
1141
1170
|
throw retryError;
|
|
1142
1171
|
}
|
|
1143
1172
|
}
|
|
1173
|
+
// With --no-bundle there is no installer to copy; surface the raw
|
|
1174
|
+
// executable the build produced instead.
|
|
1175
|
+
if (this.options.bundle === false) {
|
|
1176
|
+
await this.copyRawBinary(npmDirectory, name);
|
|
1177
|
+
if (logSuccess) {
|
|
1178
|
+
logger.success('✔ Build success!');
|
|
1179
|
+
logger.success('✔ Raw binary located in', path.resolve(this.getRawBinaryPath(name)));
|
|
1180
|
+
}
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1144
1183
|
// Copy app
|
|
1145
1184
|
const fileName = this.getFileName();
|
|
1146
1185
|
const fileType = this.getFileType(target);
|
|
@@ -1508,17 +1547,11 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1508
1547
|
arch =
|
|
1509
1548
|
buildType === 'rpm' || buildType === 'appimage' ? 'aarch64' : 'arm64';
|
|
1510
1549
|
}
|
|
1550
|
+
else if (this.buildArch === 'x64') {
|
|
1551
|
+
arch = buildType === 'rpm' ? 'x86_64' : 'amd64';
|
|
1552
|
+
}
|
|
1511
1553
|
else {
|
|
1512
|
-
|
|
1513
|
-
arch = buildType === 'rpm' ? 'x86_64' : 'amd64';
|
|
1514
|
-
}
|
|
1515
|
-
else {
|
|
1516
|
-
arch = this.buildArch;
|
|
1517
|
-
if (this.buildArch === 'arm64' &&
|
|
1518
|
-
(buildType === 'rpm' || buildType === 'appimage')) {
|
|
1519
|
-
arch = 'aarch64';
|
|
1520
|
-
}
|
|
1521
|
-
}
|
|
1554
|
+
arch = this.buildArch;
|
|
1522
1555
|
}
|
|
1523
1556
|
if (this.currentBuildType === 'rpm') {
|
|
1524
1557
|
return `${name}-${version}-1.${arch}`;
|
|
@@ -1526,6 +1559,11 @@ class LinuxBuilder extends BaseBuilder {
|
|
|
1526
1559
|
return `${name}_${version}_${arch}`;
|
|
1527
1560
|
}
|
|
1528
1561
|
async build(url) {
|
|
1562
|
+
// --no-bundle: build the executable once with no per-format packaging loop.
|
|
1563
|
+
if (this.options.bundle === false) {
|
|
1564
|
+
await this.buildAndCopy(url, 'deb');
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1529
1567
|
const targets = filterLinuxTargets(this.options.targets);
|
|
1530
1568
|
if (targets.length === 0) {
|
|
1531
1569
|
throw new Error(`No valid Linux target in "${this.options.targets}". Valid targets: ${LINUX_TARGET_TYPES.join(', ')}.`);
|
|
@@ -1684,6 +1722,11 @@ post_remove() {
|
|
|
1684
1722
|
? (this.getTauriTarget(this.buildArch, 'linux') ?? undefined)
|
|
1685
1723
|
: undefined;
|
|
1686
1724
|
let fullCommand = this.buildBaseCommand(packageManager, configPath, buildTarget);
|
|
1725
|
+
// --no-bundle: build the executable only, skipping .deb/.rpm/.appimage
|
|
1726
|
+
// packaging entirely (e.g. RPM-based distros where the bundler aborts).
|
|
1727
|
+
if (this.options.bundle === false) {
|
|
1728
|
+
return `${fullCommand} --no-bundle`;
|
|
1729
|
+
}
|
|
1687
1730
|
if (this.currentBuildType) {
|
|
1688
1731
|
fullCommand += ` --bundles ${this.currentBuildType}`;
|
|
1689
1732
|
}
|
|
@@ -2245,6 +2288,19 @@ async function convertIconFormat(inputPath, appName) {
|
|
|
2245
2288
|
return null;
|
|
2246
2289
|
}
|
|
2247
2290
|
}
|
|
2291
|
+
async function isLinuxBundleIconReady(iconPath) {
|
|
2292
|
+
if (!IS_LINUX || path.extname(iconPath).toLowerCase() !== '.png') {
|
|
2293
|
+
return false;
|
|
2294
|
+
}
|
|
2295
|
+
try {
|
|
2296
|
+
const { width, height } = await sharp(iconPath).metadata();
|
|
2297
|
+
return (width === PLATFORM_CONFIG.linux.size &&
|
|
2298
|
+
height === PLATFORM_CONFIG.linux.size);
|
|
2299
|
+
}
|
|
2300
|
+
catch {
|
|
2301
|
+
return false;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2248
2304
|
/**
|
|
2249
2305
|
* Processes downloaded or local icon for platform-specific format
|
|
2250
2306
|
*/
|
|
@@ -2254,7 +2310,7 @@ async function processIcon(iconPath, appName) {
|
|
|
2254
2310
|
// Check if already in correct platform format
|
|
2255
2311
|
const ext = path.extname(iconPath).toLowerCase();
|
|
2256
2312
|
const isCorrectFormat = (IS_WIN && ext === '.ico') ||
|
|
2257
|
-
(IS_LINUX &&
|
|
2313
|
+
(IS_LINUX && (await isLinuxBundleIconReady(iconPath))) ||
|
|
2258
2314
|
(!IS_WIN && !IS_LINUX && ext === '.icns');
|
|
2259
2315
|
if (isCorrectFormat) {
|
|
2260
2316
|
return await copyWindowsIconIfNeeded(iconPath, appName);
|
|
@@ -2660,6 +2716,11 @@ async function handleOptions(options, url) {
|
|
|
2660
2716
|
if (!options.internalUrlRegex && options.safeDomain) {
|
|
2661
2717
|
appOptions.internalUrlRegex = safeDomainsToRegex(options.safeDomain);
|
|
2662
2718
|
}
|
|
2719
|
+
// --no-bundle is Linux-only; keep normal packaging on other platforms.
|
|
2720
|
+
if (appOptions.bundle === false && platform !== 'linux') {
|
|
2721
|
+
logger.warn('✼ --no-bundle is only supported on Linux; ignoring it.');
|
|
2722
|
+
appOptions.bundle = true;
|
|
2723
|
+
}
|
|
2663
2724
|
const iconPath = await handleIcon(appOptions, url);
|
|
2664
2725
|
appOptions.icon = iconPath || '';
|
|
2665
2726
|
return appOptions;
|
|
@@ -2672,6 +2733,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2672
2733
|
fullscreen: false,
|
|
2673
2734
|
maximize: false,
|
|
2674
2735
|
hideTitleBar: false,
|
|
2736
|
+
hideWindowDecorations: false,
|
|
2675
2737
|
alwaysOnTop: false,
|
|
2676
2738
|
appVersion: '1.0.0',
|
|
2677
2739
|
darkMode: false,
|
|
@@ -2702,6 +2764,7 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2702
2764
|
incognito: false,
|
|
2703
2765
|
wasm: false,
|
|
2704
2766
|
enableDragDrop: false,
|
|
2767
|
+
bundle: true,
|
|
2705
2768
|
keepBinary: false,
|
|
2706
2769
|
multiInstance: false,
|
|
2707
2770
|
multiWindow: false,
|
|
@@ -2772,6 +2835,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2772
2835
|
.option('--use-local-file', 'Use local file packaging', DEFAULT_PAKE_OPTIONS.useLocalFile)
|
|
2773
2836
|
.option('--fullscreen', 'Start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
|
|
2774
2837
|
.option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT_PAKE_OPTIONS.hideTitleBar)
|
|
2838
|
+
.option('--hide-window-decorations', 'Hide native window decorations on Windows and Linux', DEFAULT_PAKE_OPTIONS.hideWindowDecorations)
|
|
2775
2839
|
.option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch)
|
|
2776
2840
|
.option('--inject <files>', 'Inject local CSS/JS files into the page', (val, previous) => {
|
|
2777
2841
|
if (!val)
|
|
@@ -2841,6 +2905,9 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2841
2905
|
.addOption(new Option('--keep-binary', 'Keep raw binary file alongside installer')
|
|
2842
2906
|
.default(DEFAULT_PAKE_OPTIONS.keepBinary)
|
|
2843
2907
|
.hideHelp())
|
|
2908
|
+
.addOption(new Option('--no-bundle', 'Skip packaging, output only the raw executable (Linux; for RPM distros where the bundler aborts)')
|
|
2909
|
+
.default(DEFAULT_PAKE_OPTIONS.bundle)
|
|
2910
|
+
.hideHelp())
|
|
2844
2911
|
.addOption(new Option('--multi-instance', 'Allow multiple app instances')
|
|
2845
2912
|
.default(DEFAULT_PAKE_OPTIONS.multiInstance)
|
|
2846
2913
|
.hideHelp())
|
|
@@ -2863,8 +2930,8 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2863
2930
|
.default(DEFAULT_PAKE_OPTIONS.zoom)
|
|
2864
2931
|
.argParser((value) => {
|
|
2865
2932
|
const zoom = Number(value);
|
|
2866
|
-
if (!Number.
|
|
2867
|
-
throw new Error('--zoom must be
|
|
2933
|
+
if (!Number.isInteger(zoom) || zoom < 50 || zoom > 200) {
|
|
2934
|
+
throw new Error('--zoom must be an integer between 50 and 200');
|
|
2868
2935
|
}
|
|
2869
2936
|
return zoom;
|
|
2870
2937
|
})
|
package/package.json
CHANGED
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
Binary file
|
|
Binary file
|
package/src-tauri/pake.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
use crate::util::{
|
|
1
|
+
use crate::util::{
|
|
2
|
+
check_file_or_append, get_download_message_with_lang, sanitize_download_filename, show_toast,
|
|
3
|
+
MessageType,
|
|
4
|
+
};
|
|
2
5
|
use std::fs::File;
|
|
3
6
|
use std::io::Write;
|
|
4
7
|
use std::str::FromStr;
|
|
@@ -93,7 +96,7 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result
|
|
|
93
96
|
.download_dir()
|
|
94
97
|
.map_err(|e| format!("Failed to get download dir: {}", e))?;
|
|
95
98
|
|
|
96
|
-
let output_path = download_dir.join(¶ms.filename);
|
|
99
|
+
let output_path = download_dir.join(sanitize_download_filename(¶ms.filename));
|
|
97
100
|
|
|
98
101
|
let path_str = output_path.to_str().ok_or("Invalid output path")?;
|
|
99
102
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
use crate::app::config::PakeConfig;
|
|
2
2
|
use crate::util::{
|
|
3
|
-
check_file_or_append, get_data_dir, get_download_message_with_lang,
|
|
3
|
+
check_file_or_append, get_data_dir, get_download_message_with_lang, sanitize_download_filename,
|
|
4
|
+
show_toast, MessageType,
|
|
4
5
|
};
|
|
5
6
|
use std::{
|
|
6
7
|
path::PathBuf,
|
|
@@ -289,9 +290,17 @@ fn build_window(
|
|
|
289
290
|
// any script that reads it (e.g. fullscreen polyfill checks for an opt-out
|
|
290
291
|
// flag), and toast must register `window.pakeToast` before Rust code
|
|
291
292
|
// calls show_toast().
|
|
293
|
+
window_builder = window_builder.initialization_script(&config_script);
|
|
294
|
+
|
|
295
|
+
// find.js is opt-in via --enable-find and no-ops at runtime when disabled,
|
|
296
|
+
// so only inject its ~700 lines when the feature is on. Avoids parsing the
|
|
297
|
+
// find UI on every page load in the common (find-off) case. Matches the
|
|
298
|
+
// enable_find gating already applied to the Find menu item.
|
|
299
|
+
if window_config.enable_find {
|
|
300
|
+
window_builder = window_builder.initialization_script(include_str!("../inject/find.js"));
|
|
301
|
+
}
|
|
302
|
+
|
|
292
303
|
window_builder = window_builder
|
|
293
|
-
.initialization_script(&config_script)
|
|
294
|
-
.initialization_script(include_str!("../inject/find.js"))
|
|
295
304
|
.initialization_script(include_str!("../inject/toast.js"))
|
|
296
305
|
.initialization_script(include_str!("../inject/fullscreen.js"))
|
|
297
306
|
.initialization_script(include_str!("../inject/event.js"))
|
|
@@ -371,6 +380,10 @@ fn build_window(
|
|
|
371
380
|
{
|
|
372
381
|
window_builder = window_builder.data_directory(_data_dir).theme(theme);
|
|
373
382
|
|
|
383
|
+
if window_config.hide_window_decorations {
|
|
384
|
+
window_builder = window_builder.decorations(false);
|
|
385
|
+
}
|
|
386
|
+
|
|
374
387
|
if !config.proxy_url.is_empty() {
|
|
375
388
|
if let Ok(proxy_url) = Url::from_str(&config.proxy_url) {
|
|
376
389
|
parsed_proxy_url = Some(proxy_url.clone());
|
|
@@ -456,7 +469,7 @@ fn build_window(
|
|
|
456
469
|
})
|
|
457
470
|
.unwrap_or_else(|| "download".to_string());
|
|
458
471
|
|
|
459
|
-
let target = download_dir.join(filename);
|
|
472
|
+
let target = download_dir.join(sanitize_download_filename(&filename));
|
|
460
473
|
if let Some(path_str) = target.to_str() {
|
|
461
474
|
*destination = PathBuf::from(check_file_or_append(path_str));
|
|
462
475
|
}
|
|
@@ -15,25 +15,32 @@ function setZoom(zoom) {
|
|
|
15
15
|
// CSS hacks. `transform: scale` and `html.style.zoom` break complex SPAs like
|
|
16
16
|
// ChatGPT: the page shifts right on Windows and parts of the UI stop repainting
|
|
17
17
|
// on macOS. Native zoom recalculates layout exactly like a browser does.
|
|
18
|
+
const zoomPercent = normalizeZoomPercent(zoom);
|
|
19
|
+
const normalizedZoom = `${zoomPercent}%`;
|
|
18
20
|
const invoke = window.__TAURI__?.core?.invoke;
|
|
19
21
|
if (invoke) {
|
|
20
|
-
invoke("set_zoom", { percent:
|
|
22
|
+
invoke("set_zoom", { percent: zoomPercent }).catch(() => {});
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
window.localStorage.setItem("htmlZoom",
|
|
25
|
+
window.localStorage.setItem("htmlZoom", normalizedZoom);
|
|
24
26
|
}
|
|
25
27
|
|
|
26
28
|
function zoomCommon(zoomChange) {
|
|
27
29
|
const currentZoom = window.localStorage.getItem("htmlZoom") || "100%";
|
|
28
|
-
setZoom(zoomChange(currentZoom));
|
|
30
|
+
setZoom(zoomChange(normalizeZoomPercent(currentZoom)));
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
function zoomIn() {
|
|
32
|
-
zoomCommon((currentZoom) => `${Math.min(
|
|
34
|
+
zoomCommon((currentZoom) => `${Math.min(currentZoom + 10, 200)}%`);
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
function zoomOut() {
|
|
36
|
-
zoomCommon((currentZoom) => `${Math.max(
|
|
38
|
+
zoomCommon((currentZoom) => `${Math.max(currentZoom - 10, 30)}%`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function normalizeZoomPercent(zoom) {
|
|
42
|
+
const parsed = parseFloat(zoom);
|
|
43
|
+
return Number.isFinite(parsed) ? parsed : 100;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
let pasteAsPlainTextPending = false;
|
|
@@ -53,6 +60,220 @@ function handleShortcut(event) {
|
|
|
53
60
|
}
|
|
54
61
|
}
|
|
55
62
|
|
|
63
|
+
function toggleNativeFullscreen(appWindow) {
|
|
64
|
+
appWindow
|
|
65
|
+
.isFullscreen()
|
|
66
|
+
.then((fullscreen) => appWindow.setFullscreen(!fullscreen))
|
|
67
|
+
.catch((error) => {
|
|
68
|
+
console.warn("[Pake] Failed to toggle native fullscreen:", error);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function handleWindowFullscreenShortcut(event) {
|
|
73
|
+
if (
|
|
74
|
+
!event.isTrusted ||
|
|
75
|
+
event.repeat ||
|
|
76
|
+
event.key !== "F11" ||
|
|
77
|
+
!isNonMacDesktop()
|
|
78
|
+
) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const appWindow = window.__TAURI__?.window?.getCurrentWindow?.();
|
|
83
|
+
if (!appWindow) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
event.stopImmediatePropagation();
|
|
89
|
+
toggleNativeFullscreen(appWindow);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function getDesktopPlatform() {
|
|
93
|
+
return (
|
|
94
|
+
navigator.userAgentData?.platform ||
|
|
95
|
+
navigator.platform ||
|
|
96
|
+
navigator.userAgent
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function isNonMacDesktop() {
|
|
101
|
+
return /win|linux/i.test(getDesktopPlatform());
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function hasImmersiveHeader(config = window["pakeConfig"] || {}) {
|
|
105
|
+
return /mac/i.test(getDesktopPlatform())
|
|
106
|
+
? config.hide_title_bar === true
|
|
107
|
+
: config.hide_window_decorations === true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isEditableElement(element) {
|
|
111
|
+
if (!element) return false;
|
|
112
|
+
|
|
113
|
+
const tagName = element.tagName;
|
|
114
|
+
return (
|
|
115
|
+
tagName === "INPUT" || tagName === "TEXTAREA" || element.isContentEditable
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function hasSelectedText() {
|
|
120
|
+
return Boolean(window.getSelection?.()?.toString());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const NON_TEXT_INPUT_TYPES = new Set([
|
|
124
|
+
"button",
|
|
125
|
+
"checkbox",
|
|
126
|
+
"color",
|
|
127
|
+
"file",
|
|
128
|
+
"hidden",
|
|
129
|
+
"image",
|
|
130
|
+
"radio",
|
|
131
|
+
"range",
|
|
132
|
+
"reset",
|
|
133
|
+
"submit",
|
|
134
|
+
]);
|
|
135
|
+
|
|
136
|
+
function isTextInputElement(element) {
|
|
137
|
+
return (
|
|
138
|
+
element?.tagName === "INPUT" &&
|
|
139
|
+
!NON_TEXT_INPUT_TYPES.has((element.type || "text").toLowerCase())
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function selectEditableElement(element) {
|
|
144
|
+
if (typeof element.select === "function") {
|
|
145
|
+
element.select();
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (element.isContentEditable) {
|
|
150
|
+
const range = document.createRange();
|
|
151
|
+
range.selectNodeContents(element);
|
|
152
|
+
const selection = window.getSelection?.();
|
|
153
|
+
if (!selection) return false;
|
|
154
|
+
|
|
155
|
+
selection.removeAllRanges();
|
|
156
|
+
selection.addRange(range);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function canPasteIntoEditableElement(element) {
|
|
164
|
+
if (!isEditableElement(element)) return false;
|
|
165
|
+
|
|
166
|
+
if (element.tagName === "INPUT") {
|
|
167
|
+
return (
|
|
168
|
+
isTextInputElement(element) &&
|
|
169
|
+
element.disabled !== true &&
|
|
170
|
+
element.readOnly !== true
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (element.tagName === "TEXTAREA") {
|
|
175
|
+
return element.disabled !== true && element.readOnly !== true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function insertTextIntoEditableElement(element, text) {
|
|
182
|
+
if (!text) return false;
|
|
183
|
+
|
|
184
|
+
if (document.execCommand("insertText", false, text)) {
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (
|
|
189
|
+
element &&
|
|
190
|
+
(isTextInputElement(element) || element.tagName === "TEXTAREA") &&
|
|
191
|
+
typeof element.setRangeText === "function"
|
|
192
|
+
) {
|
|
193
|
+
const valueLength =
|
|
194
|
+
typeof element.value === "string" ? element.value.length : 0;
|
|
195
|
+
const start =
|
|
196
|
+
typeof element.selectionStart === "number"
|
|
197
|
+
? element.selectionStart
|
|
198
|
+
: valueLength;
|
|
199
|
+
const end =
|
|
200
|
+
typeof element.selectionEnd === "number" ? element.selectionEnd : start;
|
|
201
|
+
element.setRangeText(text, start, end, "end");
|
|
202
|
+
element.dispatchEvent?.(new Event("input", { bubbles: true }));
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function runBrowserPasteCommand() {
|
|
210
|
+
try {
|
|
211
|
+
return document.execCommand("paste") === true;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function pasteClipboardText(activeElement) {
|
|
218
|
+
if (runBrowserPasteCommand()) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const readText = navigator.clipboard?.readText;
|
|
223
|
+
if (typeof readText !== "function") {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
readText
|
|
228
|
+
.call(navigator.clipboard)
|
|
229
|
+
.then((text) => {
|
|
230
|
+
insertTextIntoEditableElement(activeElement, text);
|
|
231
|
+
})
|
|
232
|
+
.catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function handleClipboardShortcut(event) {
|
|
236
|
+
if (
|
|
237
|
+
event.isTrusted !== true ||
|
|
238
|
+
!isNonMacDesktop() ||
|
|
239
|
+
!event.ctrlKey ||
|
|
240
|
+
event.metaKey ||
|
|
241
|
+
event.altKey ||
|
|
242
|
+
event.shiftKey
|
|
243
|
+
) {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const key = event.key?.toLowerCase();
|
|
248
|
+
const activeElement = document.activeElement;
|
|
249
|
+
const isEditable = isEditableElement(activeElement);
|
|
250
|
+
|
|
251
|
+
if (key === "c" && (isEditable || hasSelectedText())) {
|
|
252
|
+
document.execCommand("copy");
|
|
253
|
+
event.preventDefault();
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (key === "x" && isEditable) {
|
|
258
|
+
document.execCommand("cut");
|
|
259
|
+
event.preventDefault();
|
|
260
|
+
return true;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (key === "v" && canPasteIntoEditableElement(activeElement)) {
|
|
264
|
+
event.preventDefault();
|
|
265
|
+
pasteClipboardText(activeElement);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (key === "a" && isEditable && selectEditableElement(activeElement)) {
|
|
270
|
+
event.preventDefault();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
|
|
56
277
|
const DOWNLOADABLE_FILE_EXTENSIONS = {
|
|
57
278
|
documents: [
|
|
58
279
|
"pdf",
|
|
@@ -275,12 +496,34 @@ function canNavigateAuthUrl(url) {
|
|
|
275
496
|
return normalizedUrl !== "" && normalizedUrl !== "about:blank";
|
|
276
497
|
}
|
|
277
498
|
|
|
499
|
+
function isAppleAuthPopup(url, name) {
|
|
500
|
+
if (name === "AppleAuthentication") {
|
|
501
|
+
return true;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
return (
|
|
506
|
+
new URL(url, window.location.href).hostname.toLowerCase() ===
|
|
507
|
+
"appleid.apple.com"
|
|
508
|
+
);
|
|
509
|
+
} catch (error) {
|
|
510
|
+
return false;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
278
514
|
function navigateInCurrentWindow(url) {
|
|
279
515
|
window.location.href = url;
|
|
280
516
|
return window;
|
|
281
517
|
}
|
|
282
518
|
|
|
283
519
|
function openAuthNavigation(originalWindowOpen, url, name, specs) {
|
|
520
|
+
if (isAppleAuthPopup(url, name)) {
|
|
521
|
+
const authWindow = originalWindowOpen.call(window, url, name, specs);
|
|
522
|
+
if (authWindow) {
|
|
523
|
+
return authWindow;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
284
527
|
if (shouldNavigateAuthInCurrentWindow() && canNavigateAuthUrl(url)) {
|
|
285
528
|
return navigateInCurrentWindow(url);
|
|
286
529
|
}
|
|
@@ -309,7 +552,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
309
552
|
}
|
|
310
553
|
}
|
|
311
554
|
|
|
312
|
-
if (!document.getElementById("pake-top-dom")) {
|
|
555
|
+
if (!document.getElementById("pake-top-dom") && hasImmersiveHeader()) {
|
|
313
556
|
const topDom = document.createElement("div");
|
|
314
557
|
topDom.id = "pake-top-dom";
|
|
315
558
|
document.body.appendChild(topDom);
|
|
@@ -317,24 +560,25 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
317
560
|
|
|
318
561
|
const domEl = document.getElementById("pake-top-dom");
|
|
319
562
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
domEl.addEventListener("mousedown", (e) => {
|
|
325
|
-
e.preventDefault();
|
|
326
|
-
if (e.buttons === 1 && e.detail !== 2) {
|
|
563
|
+
if (domEl) {
|
|
564
|
+
domEl.addEventListener("touchstart", () => {
|
|
327
565
|
appWindow.startDragging();
|
|
328
|
-
}
|
|
329
|
-
});
|
|
566
|
+
});
|
|
330
567
|
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
568
|
+
domEl.addEventListener("mousedown", (e) => {
|
|
569
|
+
e.preventDefault();
|
|
570
|
+
if (e.buttons === 1 && e.detail !== 2) {
|
|
571
|
+
appWindow.startDragging();
|
|
572
|
+
}
|
|
334
573
|
});
|
|
335
|
-
|
|
574
|
+
|
|
575
|
+
domEl.addEventListener("dblclick", () => {
|
|
576
|
+
toggleNativeFullscreen(appWindow);
|
|
577
|
+
});
|
|
578
|
+
}
|
|
336
579
|
|
|
337
580
|
if (window["pakeConfig"]?.disabled_web_shortcuts !== true) {
|
|
581
|
+
document.addEventListener("keydown", handleWindowFullscreenShortcut, true);
|
|
338
582
|
document.addEventListener("keyup", (event) => {
|
|
339
583
|
if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {
|
|
340
584
|
handleShortcut(event);
|
|
@@ -345,6 +589,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
345
589
|
});
|
|
346
590
|
}
|
|
347
591
|
|
|
592
|
+
document.addEventListener("keydown", handleClipboardShortcut, true);
|
|
593
|
+
|
|
348
594
|
document.addEventListener(
|
|
349
595
|
"paste",
|
|
350
596
|
(event) => {
|
|
@@ -486,13 +732,19 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
486
732
|
|
|
487
733
|
if (isInternalUrl(absoluteUrl)) {
|
|
488
734
|
// With --new-window the Rust on_new_window handler opens an in-app
|
|
489
|
-
// window
|
|
490
|
-
//
|
|
491
|
-
//
|
|
735
|
+
// window. Without it, leaving target="_blank" untouched lets the
|
|
736
|
+
// native webview escalate the click to a system-browser "new window".
|
|
737
|
+
//
|
|
738
|
+
// Many SPAs (e.g. Plane) tag in-app links with target="_blank" but
|
|
739
|
+
// route the click themselves via a React onClick that calls
|
|
740
|
+
// preventDefault + client-side navigation. Forcing a full
|
|
741
|
+
// window.location reload here (and stopping propagation) would defeat
|
|
742
|
+
// that handler and reload the whole app on every click. Instead,
|
|
743
|
+
// retarget the link to "_self" so the webview never opens a browser
|
|
744
|
+
// window, then let the page's own handler run. If nothing intercepts
|
|
745
|
+
// the click, the default _self navigation keeps it inside the app.
|
|
492
746
|
if (!window.pakeConfig?.new_window) {
|
|
493
|
-
|
|
494
|
-
e.stopImmediatePropagation();
|
|
495
|
-
window.location.href = absoluteUrl;
|
|
747
|
+
anchorElement.target = "_self";
|
|
496
748
|
}
|
|
497
749
|
return;
|
|
498
750
|
}
|
|
@@ -1124,8 +1376,17 @@ function getFilenameFromUrl(url) {
|
|
|
1124
1376
|
|
|
1125
1377
|
// Detect image type from URL or data URI
|
|
1126
1378
|
if (url.startsWith("data:image/")) {
|
|
1127
|
-
|
|
1128
|
-
|
|
1379
|
+
// Read only the MIME subtype: stop at ';' (params) or ',' (data),
|
|
1380
|
+
// whichever comes first, so we never fold the encoding/payload into
|
|
1381
|
+
// the extension. Map structured suffixes (svg+xml -> svg) and jpeg.
|
|
1382
|
+
const semicolon = url.indexOf(";");
|
|
1383
|
+
const comma = url.indexOf(",");
|
|
1384
|
+
let end = url.length;
|
|
1385
|
+
if (semicolon !== -1) end = Math.min(end, semicolon);
|
|
1386
|
+
if (comma !== -1) end = Math.min(end, comma);
|
|
1387
|
+
let ext = url.substring(11, end).split("+")[0];
|
|
1388
|
+
if (ext === "jpeg") ext = "jpg";
|
|
1389
|
+
filename = `image-${timestamp}.${ext}`;
|
|
1129
1390
|
} else {
|
|
1130
1391
|
// Default to common image extensions based on common patterns
|
|
1131
1392
|
if (url.includes("jpg") || url.includes("jpeg")) {
|
|
@@ -10,7 +10,7 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
10
10
|
#Bottom > div.content > div.inner,
|
|
11
11
|
#Rightbar .sep20:nth-of-type(5),
|
|
12
12
|
#Rightbar > div.box:nth-child(4),
|
|
13
|
-
#Main > div.box:nth-child(8) > div
|
|
13
|
+
#Main > div.box:nth-child(8) > div,
|
|
14
14
|
#Wrapper > div.sep20,
|
|
15
15
|
#Main > div.box:nth-child(8),
|
|
16
16
|
#masthead-ad,
|
|
@@ -403,6 +403,11 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
403
403
|
padding-top: 0px;
|
|
404
404
|
}
|
|
405
405
|
|
|
406
|
+
#notion-app .notion-sidebar,#notion-app .notion-topbar{
|
|
407
|
+
padding-top: 20px;
|
|
408
|
+
box-sizing: content-box;
|
|
409
|
+
}
|
|
410
|
+
|
|
406
411
|
#header-area > div > .css-gtiexd > div:nth-child(1) > div, #header-area .logoIcon .user-info{
|
|
407
412
|
padding-top: 20px;
|
|
408
413
|
}
|
|
@@ -497,9 +502,29 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
|
|
497
502
|
}
|
|
498
503
|
`;
|
|
499
504
|
const isMac = /Mac/i.test(navigator.userAgent);
|
|
500
|
-
if (window["pakeConfig"]
|
|
505
|
+
if (hasImmersiveHeader(window["pakeConfig"])) {
|
|
501
506
|
const topPaddingStyleElement = document.createElement("style");
|
|
502
|
-
topPaddingStyleElement.textContent =
|
|
507
|
+
topPaddingStyleElement.textContent = isMac
|
|
508
|
+
? topPaddingCSS
|
|
509
|
+
: `
|
|
510
|
+
#pake-top-dom:active {
|
|
511
|
+
cursor: grabbing;
|
|
512
|
+
cursor: -webkit-grabbing;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
#pake-top-dom {
|
|
516
|
+
position: fixed;
|
|
517
|
+
background: transparent;
|
|
518
|
+
top: 0;
|
|
519
|
+
width: 100%;
|
|
520
|
+
height: 20px;
|
|
521
|
+
cursor: grab;
|
|
522
|
+
-webkit-app-region: drag;
|
|
523
|
+
user-select: none;
|
|
524
|
+
-webkit-user-select: none;
|
|
525
|
+
z-index: 99999;
|
|
526
|
+
}
|
|
527
|
+
`;
|
|
503
528
|
document.head.appendChild(topPaddingStyleElement);
|
|
504
529
|
}
|
|
505
530
|
});
|
package/src-tauri/src/util.rs
CHANGED
|
@@ -111,6 +111,22 @@ pub fn get_download_message_with_lang(
|
|
|
111
111
|
.to_string()
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
pub fn sanitize_download_filename(filename: &str) -> String {
|
|
115
|
+
let Some(candidate) = filename
|
|
116
|
+
.rsplit(['/', '\\'])
|
|
117
|
+
.find(|part| !part.trim().is_empty())
|
|
118
|
+
.map(str::trim)
|
|
119
|
+
else {
|
|
120
|
+
return "download".to_string();
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
if candidate == "." || candidate == ".." {
|
|
124
|
+
"download".to_string()
|
|
125
|
+
} else {
|
|
126
|
+
candidate.to_string()
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
/// Check if the file exists. If it does, append `-N` to the stem until a free
|
|
115
131
|
/// path is found.
|
|
116
132
|
///
|
|
@@ -218,6 +234,39 @@ mod tests {
|
|
|
218
234
|
let _ = fs::remove_dir_all(path.parent().unwrap());
|
|
219
235
|
}
|
|
220
236
|
|
|
237
|
+
#[test]
|
|
238
|
+
fn sanitize_download_filename_keeps_plain_names() {
|
|
239
|
+
assert_eq!(sanitize_download_filename("report.pdf"), "report.pdf");
|
|
240
|
+
assert_eq!(sanitize_download_filename(" report.pdf "), "report.pdf");
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
#[test]
|
|
244
|
+
fn sanitize_download_filename_takes_the_final_path_segment() {
|
|
245
|
+
assert_eq!(
|
|
246
|
+
sanitize_download_filename("../../private/report.pdf"),
|
|
247
|
+
"report.pdf"
|
|
248
|
+
);
|
|
249
|
+
assert_eq!(
|
|
250
|
+
sanitize_download_filename("..\\private\\report.pdf"),
|
|
251
|
+
"report.pdf"
|
|
252
|
+
);
|
|
253
|
+
assert_eq!(
|
|
254
|
+
sanitize_download_filename("nested/path/archive.tar.gz"),
|
|
255
|
+
"archive.tar.gz"
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn sanitize_download_filename_falls_back_for_empty_or_parent_segments() {
|
|
261
|
+
for filename in ["", " ", "/", "\\", ".", "..", "../..", "..\\.."] {
|
|
262
|
+
assert_eq!(
|
|
263
|
+
sanitize_download_filename(filename),
|
|
264
|
+
"download",
|
|
265
|
+
"{filename}"
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
221
270
|
#[test]
|
|
222
271
|
fn download_message_falls_back_to_english_for_unknown_locale() {
|
|
223
272
|
let msg = get_download_message_with_lang(MessageType::Start, Some("fr-FR".to_string()));
|