pake-cli 3.10.1 โ 3.11.1
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 +2 -2
- package/dist/cli.js +141 -42
- package/dist/code-review-graph.js +283 -0
- package/dist/test-local.html +1 -0
- package/package.json +1 -1
- package/src-tauri/.pake/pake.json +1 -1
- package/src-tauri/.pake/tauri.conf.json +7 -8
- package/src-tauri/.pake/tauri.macos.conf.json +4 -5
- package/src-tauri/Cargo.lock +1 -1
- package/src-tauri/Cargo.toml +1 -1
- package/src-tauri/entitlements.plist +7 -0
- package/src-tauri/src/app/window.rs +104 -47
- package/src-tauri/src/inject/event.js +29 -4
- package/src-tauri/tauri.conf.json +1 -1
- package/src-tauri/tauri.macos.conf.json +2 -0
- /package/src-tauri/{info.plist โ Info.plist} +0 -0
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ First-time packaging requires environment setup and may be slower, subsequent bu
|
|
|
177
177
|
|
|
178
178
|
## Development
|
|
179
179
|
|
|
180
|
-
Requires Rust `>=1.85` and Node `>=22`. For detailed installation guide, see [Tauri documentation](https://tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.
|
|
180
|
+
Requires Rust `>=1.85` and Node `>=22`. For detailed installation guide, see [Tauri documentation](https://v2.tauri.app/start/prerequisites/). If unfamiliar with development environment, use the CLI tool instead.
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
183
|
# Install dependencies
|
|
@@ -202,7 +202,7 @@ Pake's development can not be without these Hackers. They contributed a lot of c
|
|
|
202
202
|
|
|
203
203
|
## Support
|
|
204
204
|
|
|
205
|
-
<a href="https://miaoyan.app/cats.html?name=Pake"><img src="https://
|
|
205
|
+
<a href="https://miaoyan.app/cats.html?name=Pake"><img src="https://rawcdn.githack.com/tw93/MiaoYan/vercel/assets/sponsors.svg" width="1000px" /></a>
|
|
206
206
|
|
|
207
207
|
1. I have two cats, TangYuan and Coke. If you think Pake delights your life, you can feed them <a href="https://miaoyan.app/cats.html?name=Pake" target="_blank">food ๐ฅฉ</a>.
|
|
208
208
|
2. If you like Pake, you can star it on GitHub. Also, welcome to [recommend Pake](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) to your friends.
|
package/dist/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import * as psl from 'psl';
|
|
|
22
22
|
import { InvalidArgumentError, program as program$1, Option } from 'commander';
|
|
23
23
|
|
|
24
24
|
var name = "pake-cli";
|
|
25
|
-
var version = "3.
|
|
25
|
+
var version = "3.11.1";
|
|
26
26
|
var description = "๐คฑ๐ป Turn any webpage into a desktop app with one command. ๐คฑ๐ป ไธ้ฎๆๅ
็ฝ้กต็ๆ่ฝป้ๆก้ขๅบ็จใ";
|
|
27
27
|
var engines = {
|
|
28
28
|
node: ">=18.0.0"
|
|
@@ -171,14 +171,29 @@ let tauriConfig = {
|
|
|
171
171
|
pake: pakeConf,
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
-
// Generates
|
|
175
|
-
|
|
174
|
+
// Generates a stable identifier based on the app URL (and optionally name).
|
|
175
|
+
// When name is provided it is included in the hash so two apps wrapping
|
|
176
|
+
// the same URL can coexist. Omitting name preserves backward compatibility
|
|
177
|
+
// with identifiers generated before V3.10.1.
|
|
178
|
+
function getIdentifier(url, name) {
|
|
179
|
+
const hashInput = name ? `${url}::${name}` : url;
|
|
176
180
|
const postFixHash = crypto
|
|
177
181
|
.createHash('md5')
|
|
178
|
-
.update(
|
|
182
|
+
.update(hashInput)
|
|
179
183
|
.digest('hex')
|
|
180
184
|
.substring(0, 6);
|
|
181
|
-
return `com.pake
|
|
185
|
+
return `com.pake.a${postFixHash}`;
|
|
186
|
+
}
|
|
187
|
+
function resolveIdentifier(url, explicitName, customIdentifier) {
|
|
188
|
+
const trimmedIdentifier = customIdentifier?.trim();
|
|
189
|
+
if (trimmedIdentifier) {
|
|
190
|
+
if (!/^[a-zA-Z][a-zA-Z0-9.-]*[a-zA-Z0-9]$/.test(trimmedIdentifier)) {
|
|
191
|
+
throw new Error(`Invalid identifier "${trimmedIdentifier}". Must start with a letter, ` +
|
|
192
|
+
`contain only letters, digits, hyphens, and dots, and end with a letter or digit.`);
|
|
193
|
+
}
|
|
194
|
+
return trimmedIdentifier;
|
|
195
|
+
}
|
|
196
|
+
return getIdentifier(url, explicitName);
|
|
182
197
|
}
|
|
183
198
|
async function promptText(message, initial) {
|
|
184
199
|
const response = await prompts({
|
|
@@ -484,7 +499,7 @@ async function mergeConfig(url, options, tauriConf) {
|
|
|
484
499
|
await fsExtra.copy(sourcePath, destPath);
|
|
485
500
|
}
|
|
486
501
|
}));
|
|
487
|
-
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, } = options;
|
|
502
|
+
const { width, height, fullscreen, maximize, hideTitleBar, alwaysOnTop, appVersion, darkMode, disabledWebShortcuts, activationShortcut, userAgent, showSystemTray, systemTrayIcon, useLocalFile, identifier, name = 'pake-app', resizable = true, inject, proxyUrl, installerLanguage, hideOnClose, incognito, title, wasm, enableDragDrop, multiInstance, multiWindow, startToTray, forceInternalNavigation, internalUrlRegex, zoom, minWidth, minHeight, ignoreCertificateErrors, newWindow, camera, microphone, } = options;
|
|
488
503
|
const { platform } = process;
|
|
489
504
|
const platformHideOnClose = hideOnClose ?? platform === 'darwin';
|
|
490
505
|
const tauriConfWindowOptions = {
|
|
@@ -667,7 +682,15 @@ Terminal=false
|
|
|
667
682
|
// Avoid copying if source and destination are the same
|
|
668
683
|
const absoluteDestPath = path.resolve(iconPath);
|
|
669
684
|
if (resolvedIconPath !== absoluteDestPath) {
|
|
670
|
-
|
|
685
|
+
try {
|
|
686
|
+
await fsExtra.copy(resolvedIconPath, iconPath);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
if (!(error instanceof Error &&
|
|
690
|
+
error.message.includes('Source and destination must not be the same'))) {
|
|
691
|
+
throw error;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
671
694
|
}
|
|
672
695
|
}
|
|
673
696
|
if (updateIconPath) {
|
|
@@ -739,6 +762,26 @@ Terminal=false
|
|
|
739
762
|
},
|
|
740
763
|
};
|
|
741
764
|
}
|
|
765
|
+
// Write entitlements dynamically on macOS so camera/microphone are opt-in
|
|
766
|
+
if (platform === 'darwin') {
|
|
767
|
+
const entitlementEntries = [];
|
|
768
|
+
if (camera) {
|
|
769
|
+
entitlementEntries.push(' <key>com.apple.security.device.camera</key>\n <true/>');
|
|
770
|
+
}
|
|
771
|
+
if (microphone) {
|
|
772
|
+
entitlementEntries.push(' <key>com.apple.security.device.audio-input</key>\n <true/>');
|
|
773
|
+
}
|
|
774
|
+
const entitlementsContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
775
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
776
|
+
<plist version="1.0">
|
|
777
|
+
<dict>
|
|
778
|
+
${entitlementEntries.join('\n')}
|
|
779
|
+
</dict>
|
|
780
|
+
</plist>
|
|
781
|
+
`;
|
|
782
|
+
const entitlementsPath = path.join(npmDirectory, 'src-tauri', 'entitlements.plist');
|
|
783
|
+
await fsExtra.writeFile(entitlementsPath, entitlementsContent);
|
|
784
|
+
}
|
|
742
785
|
// Save config file.
|
|
743
786
|
const platformConfigPaths = {
|
|
744
787
|
win32: 'tauri.windows.conf.json',
|
|
@@ -761,13 +804,20 @@ class BaseBuilder {
|
|
|
761
804
|
this.options = options;
|
|
762
805
|
}
|
|
763
806
|
getBuildEnvironment() {
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
807
|
+
if (!IS_MAC) {
|
|
808
|
+
return undefined;
|
|
809
|
+
}
|
|
810
|
+
const currentPath = process.env.PATH || '';
|
|
811
|
+
const systemToolsPath = '/usr/bin';
|
|
812
|
+
const buildPath = currentPath.startsWith(`${systemToolsPath}:`)
|
|
813
|
+
? currentPath
|
|
814
|
+
: `${systemToolsPath}:${currentPath}`;
|
|
815
|
+
return {
|
|
816
|
+
CFLAGS: '-fno-modules',
|
|
817
|
+
CXXFLAGS: '-fno-modules',
|
|
818
|
+
MACOSX_DEPLOYMENT_TARGET: '14.0',
|
|
819
|
+
PATH: buildPath,
|
|
820
|
+
};
|
|
771
821
|
}
|
|
772
822
|
getInstallTimeout() {
|
|
773
823
|
// Windows needs more time due to native compilation and antivirus scanning
|
|
@@ -799,6 +849,21 @@ class BaseBuilder {
|
|
|
799
849
|
}
|
|
800
850
|
}
|
|
801
851
|
}
|
|
852
|
+
async copyFileWithSamePathGuard(sourcePath, destinationPath) {
|
|
853
|
+
if (path.resolve(sourcePath) === path.resolve(destinationPath)) {
|
|
854
|
+
return;
|
|
855
|
+
}
|
|
856
|
+
try {
|
|
857
|
+
await fsExtra.copy(sourcePath, destinationPath, { overwrite: true });
|
|
858
|
+
}
|
|
859
|
+
catch (error) {
|
|
860
|
+
if (error instanceof Error &&
|
|
861
|
+
error.message.includes('Source and destination must not be the same')) {
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
throw error;
|
|
865
|
+
}
|
|
866
|
+
}
|
|
802
867
|
async prepare() {
|
|
803
868
|
const tauriSrcPath = path.join(npmDirectory, 'src-tauri');
|
|
804
869
|
const tauriTargetPath = path.join(tauriSrcPath, 'target');
|
|
@@ -844,7 +909,7 @@ class BaseBuilder {
|
|
|
844
909
|
if (isChina) {
|
|
845
910
|
logger.info(`โบ Located in China, using ${packageManager}/rsProxy CN mirror.`);
|
|
846
911
|
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
847
|
-
await
|
|
912
|
+
await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
848
913
|
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
849
914
|
}
|
|
850
915
|
else {
|
|
@@ -863,7 +928,7 @@ class BaseBuilder {
|
|
|
863
928
|
usedMirror = true;
|
|
864
929
|
try {
|
|
865
930
|
const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml');
|
|
866
|
-
await
|
|
931
|
+
await this.copyFileWithSamePathGuard(projectCnConf, projectConf);
|
|
867
932
|
await shellExec(`cd "${npmDirectory}" && ${packageManager} install${registryOption}${peerDepsOption}`, timeout, { ...buildEnv, CI: 'true' });
|
|
868
933
|
retrySpinner.succeed(chalk.green('Package installed with CN mirror!'));
|
|
869
934
|
}
|
|
@@ -972,16 +1037,16 @@ class BaseBuilder {
|
|
|
972
1037
|
const appBundleName = path.basename(appBundlePath);
|
|
973
1038
|
const appDest = path.join('/Applications', appBundleName);
|
|
974
1039
|
if (await fsExtra.pathExists(appDest)) {
|
|
975
|
-
|
|
1040
|
+
logger.warn(` Existing ${appBundleName} in /Applications will be replaced.`);
|
|
976
1041
|
}
|
|
977
|
-
|
|
978
|
-
|
|
1042
|
+
// fsExtra.move uses fs.rename (atomic on same filesystem) and falls back
|
|
1043
|
+
// to copy+remove only when moving across volumes.
|
|
1044
|
+
await fsExtra.move(appBundlePath, appDest, { overwrite: true });
|
|
979
1045
|
logger.success(`โ ${appBundleName.replace(/\.app$/, '')} installed to /Applications`);
|
|
980
|
-
logger.success('โ Local app bundle removed');
|
|
981
1046
|
}
|
|
982
1047
|
catch (error) {
|
|
983
1048
|
logger.error(`โ Failed to install ${appName}: ${error}`);
|
|
984
|
-
logger.info(`
|
|
1049
|
+
logger.info(` App bundle still available at: ${appBundlePath}`);
|
|
985
1050
|
}
|
|
986
1051
|
}
|
|
987
1052
|
getFileType(target) {
|
|
@@ -999,33 +1064,21 @@ class BaseBuilder {
|
|
|
999
1064
|
message.includes('appimage tool failed') ||
|
|
1000
1065
|
message.includes('strip tool'));
|
|
1001
1066
|
}
|
|
1002
|
-
/**
|
|
1003
|
-
* ่งฃๆ็ฎๆ ๆถๆ
|
|
1004
|
-
*/
|
|
1005
1067
|
resolveTargetArch(requestedArch) {
|
|
1006
1068
|
if (requestedArch === 'auto' || !requestedArch) {
|
|
1007
1069
|
return process.arch;
|
|
1008
1070
|
}
|
|
1009
1071
|
return requestedArch;
|
|
1010
1072
|
}
|
|
1011
|
-
/**
|
|
1012
|
-
* ่ทๅTauriๆๅปบ็ฎๆ
|
|
1013
|
-
*/
|
|
1014
1073
|
getTauriTarget(arch, platform = process.platform) {
|
|
1015
1074
|
const platformMappings = BaseBuilder.ARCH_MAPPINGS[platform];
|
|
1016
1075
|
if (!platformMappings)
|
|
1017
1076
|
return null;
|
|
1018
1077
|
return platformMappings[arch] || null;
|
|
1019
1078
|
}
|
|
1020
|
-
/**
|
|
1021
|
-
* ่ทๅๆถๆๆพ็คบๅ็งฐ๏ผ็จไบๆไปถๅ๏ผ
|
|
1022
|
-
*/
|
|
1023
1079
|
getArchDisplayName(arch) {
|
|
1024
1080
|
return BaseBuilder.ARCH_DISPLAY_NAMES[arch] || arch;
|
|
1025
1081
|
}
|
|
1026
|
-
/**
|
|
1027
|
-
* ๆๅปบๅบ็กๆๅปบๅฝไปค
|
|
1028
|
-
*/
|
|
1029
1082
|
buildBaseCommand(packageManager, configPath, target) {
|
|
1030
1083
|
const baseCommand = this.options.debug
|
|
1031
1084
|
? `${packageManager} run build:debug`
|
|
@@ -1042,9 +1095,6 @@ class BaseBuilder {
|
|
|
1042
1095
|
}
|
|
1043
1096
|
return fullCommand;
|
|
1044
1097
|
}
|
|
1045
|
-
/**
|
|
1046
|
-
* ่ทๅๆๅปบ็นๆงๅ่กจ
|
|
1047
|
-
*/
|
|
1048
1098
|
getBuildFeatures() {
|
|
1049
1099
|
const features = ['cli-build'];
|
|
1050
1100
|
// Add macos-proxy feature for modern macOS (Darwin 23+ = macOS 14+)
|
|
@@ -1153,7 +1203,6 @@ class BaseBuilder {
|
|
|
1153
1203
|
}
|
|
1154
1204
|
}
|
|
1155
1205
|
BaseBuilder.packageManagerCache = null;
|
|
1156
|
-
// ๆถๆๆ ๅฐ้
็ฝฎ
|
|
1157
1206
|
BaseBuilder.ARCH_MAPPINGS = {
|
|
1158
1207
|
darwin: {
|
|
1159
1208
|
arm64: 'aarch64-apple-darwin',
|
|
@@ -1169,7 +1218,6 @@ BaseBuilder.ARCH_MAPPINGS = {
|
|
|
1169
1218
|
x64: 'x86_64-unknown-linux-gnu',
|
|
1170
1219
|
},
|
|
1171
1220
|
};
|
|
1172
|
-
// ๆถๆๅ็งฐๆ ๅฐ๏ผ็จไบๆไปถๅ็ๆ๏ผ
|
|
1173
1221
|
BaseBuilder.ARCH_DISPLAY_NAMES = {
|
|
1174
1222
|
arm64: 'aarch64',
|
|
1175
1223
|
x64: 'x64',
|
|
@@ -1827,6 +1875,23 @@ function generateIconServiceUrls(domain) {
|
|
|
1827
1875
|
`https://www.${domain}/favicon.ico`,
|
|
1828
1876
|
];
|
|
1829
1877
|
}
|
|
1878
|
+
/**
|
|
1879
|
+
* Generates dashboard-icons URLs for an app name.
|
|
1880
|
+
* Uses walkxcode/dashboard-icons as a final fallback for selfhosted apps.
|
|
1881
|
+
* Keeps matching conservative to avoid overriding valid site-specific icons.
|
|
1882
|
+
*/
|
|
1883
|
+
function generateDashboardIconUrls(appName) {
|
|
1884
|
+
const baseUrl = 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons/png';
|
|
1885
|
+
const name = appName.toLowerCase().trim();
|
|
1886
|
+
const slugs = new Set();
|
|
1887
|
+
// Exact name
|
|
1888
|
+
slugs.add(name);
|
|
1889
|
+
// Replace spaces with hyphens
|
|
1890
|
+
slugs.add(name.replace(/\s+/g, '-'));
|
|
1891
|
+
return [...slugs]
|
|
1892
|
+
.filter((s) => s.length > 0)
|
|
1893
|
+
.map((slug) => `${baseUrl}/${slug}.png`);
|
|
1894
|
+
}
|
|
1830
1895
|
/**
|
|
1831
1896
|
* Attempts to fetch favicon from website
|
|
1832
1897
|
*/
|
|
@@ -1834,11 +1899,11 @@ async function tryGetFavicon(url, appName) {
|
|
|
1834
1899
|
try {
|
|
1835
1900
|
const domain = new URL(url).hostname;
|
|
1836
1901
|
const spinner = getSpinner(`Fetching icon from ${domain}...`);
|
|
1837
|
-
const serviceUrls = generateIconServiceUrls(domain);
|
|
1838
1902
|
const isCI = process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true';
|
|
1839
1903
|
const downloadTimeout = isCI
|
|
1840
1904
|
? ICON_CONFIG.downloadTimeout.ci
|
|
1841
1905
|
: ICON_CONFIG.downloadTimeout.default;
|
|
1906
|
+
const serviceUrls = generateIconServiceUrls(domain);
|
|
1842
1907
|
for (const serviceUrl of serviceUrls) {
|
|
1843
1908
|
try {
|
|
1844
1909
|
const faviconPath = await downloadIcon(serviceUrl, false, downloadTimeout);
|
|
@@ -1858,6 +1923,30 @@ async function tryGetFavicon(url, appName) {
|
|
|
1858
1923
|
continue;
|
|
1859
1924
|
}
|
|
1860
1925
|
}
|
|
1926
|
+
// Final fallback for selfhosted apps behind auth where domain-based
|
|
1927
|
+
// services cannot access the site favicon.
|
|
1928
|
+
if (appName) {
|
|
1929
|
+
const dashboardIconUrls = generateDashboardIconUrls(appName);
|
|
1930
|
+
for (const iconUrl of dashboardIconUrls) {
|
|
1931
|
+
try {
|
|
1932
|
+
const iconPath = await downloadIcon(iconUrl, false, downloadTimeout);
|
|
1933
|
+
if (!iconPath)
|
|
1934
|
+
continue;
|
|
1935
|
+
const convertedPath = await convertIconFormat(iconPath, appName);
|
|
1936
|
+
if (convertedPath) {
|
|
1937
|
+
const finalPath = await copyWindowsIconIfNeeded(convertedPath, appName);
|
|
1938
|
+
spinner.succeed(chalk.green(`Icon found via dashboard-icons fallback for "${appName}"!`));
|
|
1939
|
+
return finalPath;
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
catch (error) {
|
|
1943
|
+
if (error instanceof Error) {
|
|
1944
|
+
logger.debug(`Dashboard icon ${iconUrl} failed: ${error.message}`);
|
|
1945
|
+
}
|
|
1946
|
+
continue;
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1861
1950
|
spinner.warn(`No favicon found for ${domain}. Using default.`);
|
|
1862
1951
|
return null;
|
|
1863
1952
|
}
|
|
@@ -2015,10 +2104,11 @@ async function handleOptions(options, url) {
|
|
|
2015
2104
|
process.exit(1);
|
|
2016
2105
|
}
|
|
2017
2106
|
}
|
|
2107
|
+
const resolvedName = name || 'pake-app';
|
|
2018
2108
|
const appOptions = {
|
|
2019
2109
|
...options,
|
|
2020
|
-
name,
|
|
2021
|
-
identifier:
|
|
2110
|
+
name: resolvedName,
|
|
2111
|
+
identifier: resolveIdentifier(url, options.name, options.identifier),
|
|
2022
2112
|
};
|
|
2023
2113
|
const iconPath = await handleIcon(appOptions, url);
|
|
2024
2114
|
appOptions.icon = iconPath || '';
|
|
@@ -2075,6 +2165,8 @@ const DEFAULT_PAKE_OPTIONS = {
|
|
|
2075
2165
|
ignoreCertificateErrors: false,
|
|
2076
2166
|
newWindow: false,
|
|
2077
2167
|
install: false,
|
|
2168
|
+
camera: false,
|
|
2169
|
+
microphone: false,
|
|
2078
2170
|
};
|
|
2079
2171
|
|
|
2080
2172
|
function validateNumberInput(value) {
|
|
@@ -2114,6 +2206,7 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2114
2206
|
.showHelpAfterError()
|
|
2115
2207
|
.argument('[url]', 'The web URL you want to package', validateUrlInput)
|
|
2116
2208
|
.option('--name <string>', 'Application name')
|
|
2209
|
+
.addOption(new Option('--identifier <string>', 'Application identifier / bundle ID').hideHelp())
|
|
2117
2210
|
.option('--icon <string>', 'Application icon', DEFAULT_PAKE_OPTIONS.icon)
|
|
2118
2211
|
.option('--width <number>', 'Window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
|
|
2119
2212
|
.option('--height <number>', 'Window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
|
|
@@ -2231,10 +2324,16 @@ ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with
|
|
|
2231
2324
|
.addOption(new Option('--iterative-build', 'Turn on rapid build mode (app only, no dmg/deb/msi), good for debugging')
|
|
2232
2325
|
.default(DEFAULT_PAKE_OPTIONS.iterativeBuild)
|
|
2233
2326
|
.hideHelp())
|
|
2234
|
-
.addOption(new Option('--new-window', 'Allow new
|
|
2327
|
+
.addOption(new Option('--new-window', 'Allow sites to open new windows (for auth flows, tabs, branches)')
|
|
2235
2328
|
.default(DEFAULT_PAKE_OPTIONS.newWindow)
|
|
2236
2329
|
.hideHelp())
|
|
2237
2330
|
.option('--install', 'Auto-install app to /Applications (macOS) after build and remove local bundle', DEFAULT_PAKE_OPTIONS.install)
|
|
2331
|
+
.addOption(new Option('--camera', 'Request camera permission on macOS')
|
|
2332
|
+
.default(DEFAULT_PAKE_OPTIONS.camera)
|
|
2333
|
+
.hideHelp())
|
|
2334
|
+
.addOption(new Option('--microphone', 'Request microphone permission on macOS')
|
|
2335
|
+
.default(DEFAULT_PAKE_OPTIONS.microphone)
|
|
2336
|
+
.hideHelp())
|
|
2238
2337
|
.version(packageJson.version, '-v, --version')
|
|
2239
2338
|
.configureHelp({
|
|
2240
2339
|
sortSubcommands: true,
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
import * as path from 'path';
|
|
6
|
+
class CodeReviewGraph {
|
|
7
|
+
graph;
|
|
8
|
+
constructor() {
|
|
9
|
+
this.graph = {
|
|
10
|
+
contributors: new Map(),
|
|
11
|
+
prs: [],
|
|
12
|
+
reviewRelations: new Map(),
|
|
13
|
+
timeRange: { start: '', end: '' }
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
async build() {
|
|
17
|
+
console.log('Analyzing repository data...\n');
|
|
18
|
+
await this.analyzeCommits();
|
|
19
|
+
await this.analyzePRs();
|
|
20
|
+
this.analyzeReviewRelations();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
async analyzeCommits() {
|
|
24
|
+
try {
|
|
25
|
+
// Commands are hardcoded internal commands, not user input
|
|
26
|
+
const logOutput = execSync('git log --format="%H|%an|%ae|%ad|%s" --date=short -500', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
27
|
+
const lines = logOutput.trim().split('\n');
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
const parts = line.split('|');
|
|
30
|
+
if (parts.length < 5)
|
|
31
|
+
continue;
|
|
32
|
+
const [, name, email, date] = parts;
|
|
33
|
+
if (!this.graph.contributors.has(email)) {
|
|
34
|
+
this.graph.contributors.set(email, {
|
|
35
|
+
name,
|
|
36
|
+
email,
|
|
37
|
+
commits: 0,
|
|
38
|
+
prsOpened: 0,
|
|
39
|
+
prsMerged: 0,
|
|
40
|
+
reviewsGiven: 0,
|
|
41
|
+
reviewsReceived: 0,
|
|
42
|
+
firstCommit: date,
|
|
43
|
+
lastCommit: date,
|
|
44
|
+
filesChanged: new Set()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
const contributor = this.graph.contributors.get(email);
|
|
48
|
+
contributor.commits++;
|
|
49
|
+
contributor.lastCommit = date;
|
|
50
|
+
if (!this.graph.timeRange.start || date < this.graph.timeRange.start) {
|
|
51
|
+
this.graph.timeRange.start = date;
|
|
52
|
+
}
|
|
53
|
+
if (!this.graph.timeRange.end || date > this.graph.timeRange.end) {
|
|
54
|
+
this.graph.timeRange.end = date;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
console.warn('Could not analyze commits:', error.message);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async analyzePRs() {
|
|
63
|
+
try {
|
|
64
|
+
const prOutput = execSync('gh pr list --state all --limit 100 --json number,title,author,state,mergedAt,createdAt,mergedBy 2>/dev/null || echo "[]"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] });
|
|
65
|
+
const prs = JSON.parse(prOutput.trim() || '[]');
|
|
66
|
+
this.graph.prs = prs;
|
|
67
|
+
for (const pr of prs) {
|
|
68
|
+
const authorEmail = this.findEmailByUsername(pr.author.login);
|
|
69
|
+
if (authorEmail && this.graph.contributors.has(authorEmail)) {
|
|
70
|
+
const contributor = this.graph.contributors.get(authorEmail);
|
|
71
|
+
contributor.prsOpened++;
|
|
72
|
+
if (pr.state === 'MERGED') {
|
|
73
|
+
contributor.prsMerged++;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
if (pr.mergedBy?.login) {
|
|
77
|
+
const mergerEmail = this.findEmailByUsername(pr.mergedBy.login);
|
|
78
|
+
if (mergerEmail && this.graph.contributors.has(mergerEmail)) {
|
|
79
|
+
const merger = this.graph.contributors.get(mergerEmail);
|
|
80
|
+
merger.reviewsGiven++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
console.warn('Could not analyze PRs:', error.message);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
findEmailByUsername(username) {
|
|
90
|
+
const usernameLower = username.toLowerCase();
|
|
91
|
+
for (const [email, contributor] of this.graph.contributors) {
|
|
92
|
+
if (contributor.name.toLowerCase().includes(usernameLower) ||
|
|
93
|
+
email.toLowerCase().includes(usernameLower)) {
|
|
94
|
+
return email;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
analyzeReviewRelations() {
|
|
100
|
+
for (const pr of this.graph.prs) {
|
|
101
|
+
if (pr.mergedBy?.login && pr.author.login !== pr.mergedBy.login) {
|
|
102
|
+
const authorKey = pr.author.login;
|
|
103
|
+
const mergerKey = pr.mergedBy.login;
|
|
104
|
+
if (!this.graph.reviewRelations.has(mergerKey)) {
|
|
105
|
+
this.graph.reviewRelations.set(mergerKey, new Map());
|
|
106
|
+
}
|
|
107
|
+
const relations = this.graph.reviewRelations.get(mergerKey);
|
|
108
|
+
relations.set(authorKey, (relations.get(authorKey) || 0) + 1);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
generateMermaidGraph() {
|
|
113
|
+
const lines = [
|
|
114
|
+
'%% Code Review Graph for Pake',
|
|
115
|
+
'%% Generated automatically - do not edit manually',
|
|
116
|
+
'',
|
|
117
|
+
'flowchart TB',
|
|
118
|
+
' subgraph Contributors["Top Contributors"]'
|
|
119
|
+
];
|
|
120
|
+
const sortedContributors = Array.from(this.graph.contributors.values())
|
|
121
|
+
.sort((a, b) => b.commits - a.commits)
|
|
122
|
+
.slice(0, 15);
|
|
123
|
+
const nodeMap = new Map();
|
|
124
|
+
let idx = 0;
|
|
125
|
+
for (const c of sortedContributors) {
|
|
126
|
+
const nodeId = `C${idx}`;
|
|
127
|
+
nodeMap.set(c.email, nodeId);
|
|
128
|
+
const prBadge = c.prsMerged > 0 ? ` PRs:${c.prsMerged}` : '';
|
|
129
|
+
const label = `${c.name}(${c.commits})${prBadge}`;
|
|
130
|
+
lines.push(` ${nodeId}["${label}"]`);
|
|
131
|
+
idx++;
|
|
132
|
+
}
|
|
133
|
+
lines.push(' end');
|
|
134
|
+
lines.push('');
|
|
135
|
+
const addedEdges = new Set();
|
|
136
|
+
for (const [reviewer, relations] of this.graph.reviewRelations) {
|
|
137
|
+
// reviewer is already a string (the login), not an object
|
|
138
|
+
const reviewerEmail = this.findEmailByUsername(reviewer);
|
|
139
|
+
if (!reviewerEmail || !nodeMap.has(reviewerEmail))
|
|
140
|
+
continue;
|
|
141
|
+
const reviewerNode = nodeMap.get(reviewerEmail);
|
|
142
|
+
for (const [author, count] of relations) {
|
|
143
|
+
// author is already a string (the login)
|
|
144
|
+
const authorEmail = this.findEmailByUsername(author);
|
|
145
|
+
if (!authorEmail || !nodeMap.has(authorEmail))
|
|
146
|
+
continue;
|
|
147
|
+
const authorNode = nodeMap.get(authorEmail);
|
|
148
|
+
const edgeKey = `${reviewerNode}-${authorNode}`;
|
|
149
|
+
if (!addedEdges.has(edgeKey)) {
|
|
150
|
+
lines.push(` ${reviewerNode} -.->|"reviews"| ${authorNode}`);
|
|
151
|
+
addedEdges.add(edgeKey);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
lines.push('');
|
|
156
|
+
lines.push(' subgraph Recent_Merges["Recent Merged PRs"]');
|
|
157
|
+
const mergedPRs = this.graph.prs
|
|
158
|
+
.filter(pr => pr.state === 'MERGED')
|
|
159
|
+
.slice(0, 8);
|
|
160
|
+
for (let i = 0; i < mergedPRs.length; i++) {
|
|
161
|
+
const pr = mergedPRs[i];
|
|
162
|
+
const prNode = `PR${i}`;
|
|
163
|
+
const title = pr.title.length > 30 ? pr.title.substring(0, 30) + '...' : pr.title;
|
|
164
|
+
lines.push(` ${prNode}["#${pr.number}: ${title}"]`);
|
|
165
|
+
}
|
|
166
|
+
lines.push(' end');
|
|
167
|
+
lines.push('');
|
|
168
|
+
lines.push(' subgraph Stats["Statistics"]');
|
|
169
|
+
lines.push(` TotalContributors["Total Contributors: ${this.graph.contributors.size}"]`);
|
|
170
|
+
lines.push(` TotalPRs["Total PRs Analyzed: ${this.graph.prs.length}"]`);
|
|
171
|
+
lines.push(` MergedPRs["Merged PRs: ${this.graph.prs.filter(p => p.state === 'MERGED').length}"]`);
|
|
172
|
+
lines.push(` TimeRange["Period: ${this.graph.timeRange.start} to ${this.graph.timeRange.end}"]`);
|
|
173
|
+
lines.push(' end');
|
|
174
|
+
lines.push('');
|
|
175
|
+
lines.push(' %% Styling');
|
|
176
|
+
lines.push(' classDef contributor fill:#e1f5fe,stroke:#01579b,stroke-width:2px');
|
|
177
|
+
lines.push(' classDef reviewer fill:#fff3e0,stroke:#e65100,stroke-width:2px');
|
|
178
|
+
lines.push(' classDef pr fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px');
|
|
179
|
+
lines.push(' classDef stats fill:#f3e5f5,stroke:#6a1b9a,stroke-width:1px');
|
|
180
|
+
for (let i = 0; i < idx; i++) {
|
|
181
|
+
lines.push(` class C${i} contributor`);
|
|
182
|
+
}
|
|
183
|
+
for (let i = 0; i < mergedPRs.length; i++) {
|
|
184
|
+
lines.push(` class PR${i} pr`);
|
|
185
|
+
}
|
|
186
|
+
lines.push(' class TotalContributors,TotalPRs,MergedPRs,TimeRange stats');
|
|
187
|
+
return lines.join('\n');
|
|
188
|
+
}
|
|
189
|
+
generateJSON() {
|
|
190
|
+
const contributors = Array.from(this.graph.contributors.values())
|
|
191
|
+
.map(c => ({
|
|
192
|
+
...c,
|
|
193
|
+
filesChanged: Array.from(c.filesChanged)
|
|
194
|
+
}))
|
|
195
|
+
.sort((a, b) => b.commits - a.commits);
|
|
196
|
+
return {
|
|
197
|
+
metadata: {
|
|
198
|
+
generatedAt: new Date().toISOString(),
|
|
199
|
+
repository: 'tw93/Pake',
|
|
200
|
+
timeRange: this.graph.timeRange
|
|
201
|
+
},
|
|
202
|
+
summary: {
|
|
203
|
+
totalContributors: this.graph.contributors.size,
|
|
204
|
+
totalPRs: this.graph.prs.length,
|
|
205
|
+
mergedPRs: this.graph.prs.filter(p => p.state === 'MERGED').length,
|
|
206
|
+
closedPRs: this.graph.prs.filter(p => p.state === 'CLOSED').length,
|
|
207
|
+
openPRs: this.graph.prs.filter(p => p.state === 'OPEN').length
|
|
208
|
+
},
|
|
209
|
+
contributors: contributors.slice(0, 20),
|
|
210
|
+
recentPRs: this.graph.prs.slice(0, 20),
|
|
211
|
+
reviewRelations: Object.fromEntries(Array.from(this.graph.reviewRelations.entries()).map(([k, v]) => [
|
|
212
|
+
k,
|
|
213
|
+
Object.fromEntries(v)
|
|
214
|
+
]))
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
printSummary() {
|
|
218
|
+
console.log('\n===============================================================');
|
|
219
|
+
console.log(' Code Review Graph Summary ');
|
|
220
|
+
console.log('===============================================================\n');
|
|
221
|
+
console.log(`Time Range: ${this.graph.timeRange.start} to ${this.graph.timeRange.end}`);
|
|
222
|
+
console.log(`Total Contributors: ${this.graph.contributors.size}`);
|
|
223
|
+
console.log(`Total PRs Analyzed: ${this.graph.prs.length}`);
|
|
224
|
+
console.log(` Merged: ${this.graph.prs.filter(p => p.state === 'MERGED').length}`);
|
|
225
|
+
console.log(` Closed: ${this.graph.prs.filter(p => p.state === 'CLOSED').length}`);
|
|
226
|
+
console.log(` Open: ${this.graph.prs.filter(p => p.state === 'OPEN').length}\n`);
|
|
227
|
+
console.log('Top Contributors by Commits:');
|
|
228
|
+
const sorted = Array.from(this.graph.contributors.values())
|
|
229
|
+
.sort((a, b) => b.commits - a.commits)
|
|
230
|
+
.slice(0, 10);
|
|
231
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
232
|
+
const c = sorted[i];
|
|
233
|
+
const rank = i === 0 ? '1.' : i === 1 ? '2.' : i === 2 ? '3.' : ' ';
|
|
234
|
+
const prInfo = c.prsMerged > 0 ? ` (PRs merged: ${c.prsMerged})` : '';
|
|
235
|
+
console.log(` ${rank} ${c.name}: ${c.commits} commits${prInfo}`);
|
|
236
|
+
}
|
|
237
|
+
if (this.graph.reviewRelations.size > 0) {
|
|
238
|
+
console.log('\nReview Relationships (Merger -> Author):');
|
|
239
|
+
for (const [reviewer, relations] of this.graph.reviewRelations) {
|
|
240
|
+
const relationsStr = Array.from(relations.entries())
|
|
241
|
+
.map(([author, count]) => `${author}(${count})`)
|
|
242
|
+
.join(', ');
|
|
243
|
+
console.log(` ${reviewer} -> ${relationsStr}`);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
console.log('\n');
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
const program = new Command();
|
|
250
|
+
program
|
|
251
|
+
.name('code-review-graph')
|
|
252
|
+
.description('Generate code review graph for Pake project')
|
|
253
|
+
.version('1.0.0')
|
|
254
|
+
.option('-o, --output <path>', 'Output file path', 'code-review-graph.mmd')
|
|
255
|
+
.option('-f, --format <format>', 'Output format (mermaid|json)', 'mermaid')
|
|
256
|
+
.option('-s, --stdout', 'Print to stdout instead of file', false)
|
|
257
|
+
.action(async (options) => {
|
|
258
|
+
const graph = new CodeReviewGraph();
|
|
259
|
+
await graph.build();
|
|
260
|
+
graph.printSummary();
|
|
261
|
+
let output;
|
|
262
|
+
if (options.format === 'json') {
|
|
263
|
+
output = JSON.stringify(graph.generateJSON(), null, 2);
|
|
264
|
+
}
|
|
265
|
+
else {
|
|
266
|
+
output = graph.generateMermaidGraph();
|
|
267
|
+
}
|
|
268
|
+
if (options.stdout) {
|
|
269
|
+
console.log(output);
|
|
270
|
+
}
|
|
271
|
+
else {
|
|
272
|
+
const outputPath = path.resolve(options.output);
|
|
273
|
+
fs.writeFileSync(outputPath, output, 'utf-8');
|
|
274
|
+
console.log(`Graph saved to: ${outputPath}`);
|
|
275
|
+
if (options.format === 'mermaid') {
|
|
276
|
+
console.log('\nTo view this graph:');
|
|
277
|
+
console.log(' 1. Use GitHub markdown (paste into .md file)');
|
|
278
|
+
console.log(' 2. Use VS Code with Mermaid extension');
|
|
279
|
+
console.log(' 3. Visit https://mermaid.live/ and paste the content');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
});
|
|
283
|
+
program.parse();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<html><body><h1>Hello Pake</h1></body></html>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"productName": "
|
|
3
|
-
"identifier": "com.pake.
|
|
2
|
+
"productName": "twitter",
|
|
3
|
+
"identifier": "com.pake.ac7d1d0",
|
|
4
4
|
"version": "1.0.0",
|
|
5
5
|
"app": {
|
|
6
6
|
"withGlobalTauri": true,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
},
|
|
15
15
|
"bundle": {
|
|
16
16
|
"icon": [
|
|
17
|
-
"icons/
|
|
17
|
+
"icons/icon.icns"
|
|
18
18
|
],
|
|
19
19
|
"active": true,
|
|
20
20
|
"targets": [
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
"macOS": {
|
|
24
24
|
"signingIdentity": "-",
|
|
25
25
|
"hardenedRuntime": true,
|
|
26
|
+
"entitlements": "entitlements.plist",
|
|
27
|
+
"infoPlist": "Info.plist",
|
|
26
28
|
"dmg": {
|
|
27
29
|
"background": "assets/macos/dmg/background.png",
|
|
28
30
|
"windowSize": {
|
|
@@ -38,10 +40,7 @@
|
|
|
38
40
|
"y": 250
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
|
-
}
|
|
42
|
-
"resources": [
|
|
43
|
-
"icons/pakeinstallsmokecodex.icns"
|
|
44
|
-
]
|
|
43
|
+
}
|
|
45
44
|
},
|
|
46
|
-
"mainBinaryName": "pake-
|
|
45
|
+
"mainBinaryName": "pake-twitter"
|
|
47
46
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bundle": {
|
|
3
3
|
"icon": [
|
|
4
|
-
"icons/
|
|
4
|
+
"icons/icon.icns"
|
|
5
5
|
],
|
|
6
6
|
"active": true,
|
|
7
7
|
"targets": [
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"macOS": {
|
|
11
11
|
"signingIdentity": "-",
|
|
12
12
|
"hardenedRuntime": true,
|
|
13
|
+
"entitlements": "entitlements.plist",
|
|
14
|
+
"infoPlist": "Info.plist",
|
|
13
15
|
"dmg": {
|
|
14
16
|
"background": "assets/macos/dmg/background.png",
|
|
15
17
|
"windowSize": {
|
|
@@ -25,9 +27,6 @@
|
|
|
25
27
|
"y": 250
|
|
26
28
|
}
|
|
27
29
|
}
|
|
28
|
-
}
|
|
29
|
-
"resources": [
|
|
30
|
-
"icons/pakeinstallsmokecodex.icns"
|
|
31
|
-
]
|
|
30
|
+
}
|
|
32
31
|
}
|
|
33
32
|
}
|
package/src-tauri/Cargo.lock
CHANGED
package/src-tauri/Cargo.toml
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
use crate::app::config::PakeConfig;
|
|
2
2
|
use crate::util::get_data_dir;
|
|
3
3
|
use std::{path::PathBuf, str::FromStr, sync::Mutex};
|
|
4
|
-
use tauri::{
|
|
4
|
+
use tauri::{
|
|
5
|
+
webview::{NewWindowFeatures, NewWindowResponse},
|
|
6
|
+
AppHandle, Config, Manager, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder,
|
|
7
|
+
};
|
|
5
8
|
|
|
6
9
|
#[cfg(target_os = "macos")]
|
|
7
10
|
use tauri::{Theme, TitleBarStyle};
|
|
@@ -54,6 +57,41 @@ pub fn open_additional_window(app: &AppHandle) -> tauri::Result<WebviewWindow> {
|
|
|
54
57
|
build_window_with_label(app, &state.pake_config, &state.tauri_config, &label)
|
|
55
58
|
}
|
|
56
59
|
|
|
60
|
+
struct WindowBuildOptions<'a> {
|
|
61
|
+
label: &'a str,
|
|
62
|
+
url: WebviewUrl,
|
|
63
|
+
visible: bool,
|
|
64
|
+
new_window_features: Option<NewWindowFeatures>,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
fn open_requested_window(
|
|
68
|
+
app: &AppHandle,
|
|
69
|
+
config: &PakeConfig,
|
|
70
|
+
tauri_config: &Config,
|
|
71
|
+
target_url: Url,
|
|
72
|
+
features: NewWindowFeatures,
|
|
73
|
+
) -> tauri::Result<WebviewWindow> {
|
|
74
|
+
let state = app.state::<MultiWindowState>();
|
|
75
|
+
let label = state.next_window_label();
|
|
76
|
+
let window = build_window(
|
|
77
|
+
app,
|
|
78
|
+
config,
|
|
79
|
+
tauri_config,
|
|
80
|
+
WindowBuildOptions {
|
|
81
|
+
label: &label,
|
|
82
|
+
url: WebviewUrl::External(target_url.clone()),
|
|
83
|
+
visible: true,
|
|
84
|
+
new_window_features: Some(features),
|
|
85
|
+
},
|
|
86
|
+
)?;
|
|
87
|
+
|
|
88
|
+
let title = target_url.host_str().unwrap_or(target_url.as_str());
|
|
89
|
+
let _ = window.set_title(title);
|
|
90
|
+
let _ = window.set_focus();
|
|
91
|
+
|
|
92
|
+
Ok(window)
|
|
93
|
+
}
|
|
94
|
+
|
|
57
95
|
pub fn open_additional_window_safe(app: &AppHandle) {
|
|
58
96
|
#[cfg(target_os = "windows")]
|
|
59
97
|
{
|
|
@@ -81,22 +119,51 @@ fn build_window_with_label(
|
|
|
81
119
|
tauri_config: &Config,
|
|
82
120
|
label: &str,
|
|
83
121
|
) -> tauri::Result<WebviewWindow> {
|
|
84
|
-
let package_name = tauri_config.clone().product_name.unwrap();
|
|
85
|
-
let _data_dir = get_data_dir(app, package_name);
|
|
86
|
-
|
|
87
122
|
let window_config = config
|
|
88
123
|
.windows
|
|
89
124
|
.first()
|
|
90
125
|
.expect("At least one window configuration is required");
|
|
91
|
-
|
|
92
|
-
let user_agent = config.user_agent.get();
|
|
93
|
-
|
|
94
126
|
let url = match window_config.url_type.as_str() {
|
|
95
127
|
"web" => WebviewUrl::App(window_config.url.parse().unwrap()),
|
|
96
128
|
"local" => WebviewUrl::App(PathBuf::from(&window_config.url)),
|
|
97
129
|
_ => panic!("url type can only be web or local"),
|
|
98
130
|
};
|
|
99
131
|
|
|
132
|
+
build_window(
|
|
133
|
+
app,
|
|
134
|
+
config,
|
|
135
|
+
tauri_config,
|
|
136
|
+
WindowBuildOptions {
|
|
137
|
+
label,
|
|
138
|
+
url,
|
|
139
|
+
visible: false,
|
|
140
|
+
new_window_features: None,
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn build_window(
|
|
146
|
+
app: &AppHandle,
|
|
147
|
+
config: &PakeConfig,
|
|
148
|
+
tauri_config: &Config,
|
|
149
|
+
opts: WindowBuildOptions,
|
|
150
|
+
) -> tauri::Result<WebviewWindow> {
|
|
151
|
+
let WindowBuildOptions {
|
|
152
|
+
label,
|
|
153
|
+
url,
|
|
154
|
+
visible,
|
|
155
|
+
new_window_features,
|
|
156
|
+
} = opts;
|
|
157
|
+
let package_name = tauri_config.clone().product_name.unwrap();
|
|
158
|
+
let _data_dir = get_data_dir(app, package_name);
|
|
159
|
+
|
|
160
|
+
let window_config = config
|
|
161
|
+
.windows
|
|
162
|
+
.first()
|
|
163
|
+
.expect("At least one window configuration is required");
|
|
164
|
+
|
|
165
|
+
let user_agent = config.user_agent.get();
|
|
166
|
+
|
|
100
167
|
let config_script = format!(
|
|
101
168
|
"window.pakeConfig = {}",
|
|
102
169
|
serde_json::to_string(&window_config).unwrap()
|
|
@@ -113,7 +180,7 @@ fn build_window_with_label(
|
|
|
113
180
|
|
|
114
181
|
let mut window_builder = WebviewWindowBuilder::new(app, label, url)
|
|
115
182
|
.title(effective_title)
|
|
116
|
-
.visible(
|
|
183
|
+
.visible(visible)
|
|
117
184
|
.user_agent(user_agent)
|
|
118
185
|
.resizable(window_config.resizable)
|
|
119
186
|
.maximized(window_config.maximize);
|
|
@@ -164,8 +231,24 @@ fn build_window_with_label(
|
|
|
164
231
|
}
|
|
165
232
|
|
|
166
233
|
if window_config.new_window {
|
|
167
|
-
|
|
168
|
-
|
|
234
|
+
let app_handle = app.clone();
|
|
235
|
+
let popup_config = config.clone();
|
|
236
|
+
let popup_tauri_config = tauri_config.clone();
|
|
237
|
+
window_builder = window_builder.on_new_window(move |target_url, features| {
|
|
238
|
+
match open_requested_window(
|
|
239
|
+
&app_handle,
|
|
240
|
+
&popup_config,
|
|
241
|
+
&popup_tauri_config,
|
|
242
|
+
target_url,
|
|
243
|
+
features,
|
|
244
|
+
) {
|
|
245
|
+
Ok(window) => NewWindowResponse::Create { window },
|
|
246
|
+
Err(error) => {
|
|
247
|
+
eprintln!("[Pake] Failed to open requested window: {error}");
|
|
248
|
+
NewWindowResponse::Deny
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
});
|
|
169
252
|
}
|
|
170
253
|
|
|
171
254
|
// Add initialization scripts
|
|
@@ -285,47 +368,21 @@ fn build_window_with_label(
|
|
|
285
368
|
println!("Proxy configured: {}", config.proxy_url);
|
|
286
369
|
}
|
|
287
370
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return true;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Check for OAuth/authentication domains
|
|
298
|
-
let auth_patterns = [
|
|
299
|
-
"accounts.google.com",
|
|
300
|
-
"login.microsoftonline.com",
|
|
301
|
-
"github.com/login",
|
|
302
|
-
"appleid.apple.com",
|
|
303
|
-
"facebook.com",
|
|
304
|
-
"twitter.com",
|
|
305
|
-
];
|
|
306
|
-
|
|
307
|
-
let auth_paths = ["/oauth/", "/auth/", "/authorize", "/login"];
|
|
308
|
-
|
|
309
|
-
// Allow if matches auth patterns
|
|
310
|
-
for pattern in &auth_patterns {
|
|
311
|
-
if url_str.contains(pattern) {
|
|
312
|
-
#[cfg(debug_assertions)]
|
|
313
|
-
println!("Allowing OAuth navigation to: {}", url_str);
|
|
314
|
-
return true;
|
|
315
|
-
}
|
|
371
|
+
if let Some(features) = new_window_features {
|
|
372
|
+
// window_features() crashes on macOS; only apply on other platforms.
|
|
373
|
+
#[cfg(target_os = "macos")]
|
|
374
|
+
{
|
|
375
|
+
let _ = features;
|
|
376
|
+
window_builder = window_builder.focused(true);
|
|
316
377
|
}
|
|
317
378
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
println!("Allowing auth path navigation to: {}", url_str);
|
|
322
|
-
return true;
|
|
323
|
-
}
|
|
379
|
+
#[cfg(not(target_os = "macos"))]
|
|
380
|
+
{
|
|
381
|
+
window_builder = window_builder.window_features(features).focused(true);
|
|
324
382
|
}
|
|
383
|
+
}
|
|
325
384
|
|
|
326
|
-
|
|
327
|
-
true
|
|
328
|
-
});
|
|
385
|
+
window_builder = window_builder.on_navigation(|_| true);
|
|
329
386
|
|
|
330
387
|
window_builder.build()
|
|
331
388
|
}
|
|
@@ -23,6 +23,7 @@ function setZoom(zoom) {
|
|
|
23
23
|
body.style.height = `${100 / zoomValue}%`;
|
|
24
24
|
} else {
|
|
25
25
|
html.style.zoom = zoom;
|
|
26
|
+
window.dispatchEvent(new Event("resize"));
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
window.localStorage.setItem("htmlZoom", zoom);
|
|
@@ -336,14 +337,21 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
336
337
|
}
|
|
337
338
|
|
|
338
339
|
function convertBlobUrlToBinary(blobUrl) {
|
|
339
|
-
return new Promise((resolve) => {
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
340
341
|
const blob = window.blobToUrlCaches.get(blobUrl);
|
|
342
|
+
if (!blob) {
|
|
343
|
+
fetch(blobUrl)
|
|
344
|
+
.then((res) => res.arrayBuffer())
|
|
345
|
+
.then((buffer) => resolve(Array.from(new Uint8Array(buffer))))
|
|
346
|
+
.catch(reject);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
341
349
|
const reader = new FileReader();
|
|
342
|
-
|
|
343
350
|
reader.readAsArrayBuffer(blob);
|
|
344
351
|
reader.onload = () => {
|
|
345
352
|
resolve(Array.from(new Uint8Array(reader.result)));
|
|
346
353
|
};
|
|
354
|
+
reader.onerror = () => reject(reader.error);
|
|
347
355
|
});
|
|
348
356
|
}
|
|
349
357
|
|
|
@@ -503,9 +511,26 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
503
511
|
const absoluteUrl = hrefUrl.href;
|
|
504
512
|
let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl);
|
|
505
513
|
|
|
506
|
-
//
|
|
514
|
+
// Keep OAuth/authentication flows inside the app when popup support is enabled.
|
|
507
515
|
if (window.isAuthLink(absoluteUrl)) {
|
|
508
|
-
console.log("[Pake]
|
|
516
|
+
console.log("[Pake] Handling OAuth navigation in-app:", absoluteUrl);
|
|
517
|
+
|
|
518
|
+
if (window.pakeConfig?.new_window) {
|
|
519
|
+
e.preventDefault();
|
|
520
|
+
e.stopImmediatePropagation();
|
|
521
|
+
|
|
522
|
+
const authWindow = originalWindowOpen.call(
|
|
523
|
+
window,
|
|
524
|
+
absoluteUrl,
|
|
525
|
+
"_blank",
|
|
526
|
+
"width=1200,height=800,scrollbars=yes,resizable=yes",
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
if (!authWindow) {
|
|
530
|
+
window.location.href = absoluteUrl;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
509
534
|
return;
|
|
510
535
|
}
|
|
511
536
|
|
|
File without changes
|