🔀 merge dev

This commit is contained in:
Tw93
2023-04-08 20:29:02 +08:00
38 changed files with 3162 additions and 973 deletions

View File

@@ -83,7 +83,7 @@ jobs:
profile: minimal
override: true
target: x86_64-unknown-linux-musl
- name: Install Rust for windows-latest
if: inputs.platform == 'windows-latest'
uses: actions-rs/toolchain@v1
@@ -92,7 +92,7 @@ jobs:
profile: minimal
override: true
target: x86_64-pc-windows-msvc
- name: Install Rust for macos-latest
if: inputs.platform == 'macos-latest'
uses: actions-rs/toolchain@v1
@@ -101,20 +101,20 @@ jobs:
profile: minimal
override: true
target: x86_64-apple-darwin
- name: install dependencies (ubuntu only)
if: inputs.platform == 'ubuntu-20.04'
uses: awalsh128/cache-apt-pkgs-action@latest
with:
packages: libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra
version: 1.1
- name: install pake-cli local
shell: bash
run: |
run: |
echo "install pake on local"
npm install pake-cli
- name: rust cache restore
uses: actions/cache/restore@v3
with:
@@ -128,7 +128,7 @@ jobs:
- name: build with pake-cli
shell: pwsh
run: |
run: |
pwsh ./script/build_with_pake_cli.ps1
env:
URL: ${{ inputs.url }}
@@ -148,7 +148,7 @@ jobs:
name: output-${{ inputs.platform }}.zip
path: node_modules/pake-cli/output/*
retention-days: 3
- name: rust cache store
uses: actions/cache/save@v3
with:

6
.gitignore vendored
View File

@@ -30,7 +30,9 @@ output
package-lock.json
yarn.lock
pnpm-lock.yaml
dist/
dist
!dist/about_pake.html
!dist/cli.js
!dist/.gitkeep
.next/
src-tauri/.cargo/config
.next

70
bin/README.md vendored
View File

@@ -48,7 +48,7 @@ pake url [options]
### url
url 为你需要打包的网页链接 🔗,必须提供。
url 为你需要打包的网页链接 🔗 或者本地 html 文件,必须提供。
### [options]
@@ -100,32 +100,66 @@ url 为你需要打包的网页链接 🔗,必须提供。
#### [transparent]
是否开启沉浸式头部,默认为 `false` 不开启。使用下面的命令开启该功能
是否开启沉浸式头部,默认为 `false` 不开启,输入下面的命令开启沉浸式,推荐 MacOS 用户开启
```shell
--transparent
# 或者
-t
```
#### [fullscreen]
打开应用后是否开启全屏,默认为 `false`,使用下面的命令开启该功能。
```shell
--fullscreen
# 或者
-f
```
#### [resize]
是否可以拖动大小,默认为 `true` 可拖动。使用下面的命令来关闭该功能
是否可以拖动大小,默认为 `true` 可拖动,输入下面的命令则不能对窗口大小进行拉伸
```shell
--no-resizable
# 或者
-r
```
#### [fullscreen]
打开应用后是否开启全屏,默认为 `false`,输入下面的命令则会自动全屏。
```shell
--fullscreen
```
#### [user-agent]
自定义浏览器请求头, 默认为空。
```shell
--user-agent <value>
```
#### [show-menu]
显示菜单栏, 默认不显示,输入下面的命令则会显示,推荐 MacOS 用户开启。
```shell
--show-menu
```
#### [show-system-tray]
显示通知栏托盘, 默认不显示,输入下面的命令则会显示。
```shell
--show-system-tray
```
#### [system-tray-icon]
通知栏托盘图标,仅当显示通知栏托盘时有效, 图标必须为.ico 或者.png 格式的512\*512 像素的图片。
```shell
--system-tray-icon <value>
```
#### [copy-iter-file]
递归拷贝,当 url 为本地文件路径时候,若开启该选项,则将 url 路径文件所在文件夹以及所有子文件都拷贝到 pake 静态文件夹,默认不开启
```shell
--copy-iter-file
```
#### [multi-arch]
@@ -157,7 +191,7 @@ rustup target add x86_64-apple-darwin
#### [targets]
选择输出的包格式支持deb/appimage/all如果选择all,则同时打包debappimage该选项仅支持Linux默认为`all`。
选择输出的包格式,支持 deb/appimage/all如果选择 all,则同时打包 debappimage该选项仅支持 Linux默认为`all`。
```shell
--targets xxx

40
bin/README_EN.md vendored
View File

@@ -130,6 +130,46 @@ Use the command below to disable this feature.
-r
```
#### [user-agent]
Custom browser user agent, default is empty.
```shell
--user-agent <value>
```
#### [show-menu]
Display the menu bar, not display it by default, enter the following command and it will be displayed. MacOS users are recommended to enable.
```shell
--show-menu
```
#### [show-system-tray]
Display the notification tray, not display it by default, entering the following command will display.
```shell
--show-system-tray <value>
```
#### [system-tray-icon]
The notification tray icon is only valid when the notification tray is displayed. The icon must be a 512\*512 pixel image in .ico or .png format.
```shell
--system-tray-icon <value>
```
#### [copy-iter-file]
Recursive copy, when the url is a local file path, if this option is enabled, the folder where the url path file is located and all sub-files are copied to the pake static folder, which is not enabled by default
```shell
--copy-iter-file
```
#### [multi-arch]
Package results support both Intel and m1 chips, only for MacOS. The default is `false`.

View File

@@ -5,6 +5,7 @@ import { checkRustInstalled, installRust } from '@/helpers/rust.js';
import { PakeAppOptions } from '@/types.js';
import { IBuilder } from './base.js';
import { shellExec } from '@/utils/shell.js';
import {isChinaDomain} from '@/utils/ip_addr.js';
// @ts-expect-error 加上resolveJsonModule rollup会打包报错
// import tauriConf from '../../src-tauri/tauri.windows.conf.json';
import tauriConf from './tauriConf.js';
@@ -44,10 +45,22 @@ export default class LinuxBuilder implements IBuilder {
async build(url: string, options: PakeAppOptions) {
logger.debug('PakeAppOptions', options);
const { name } = options;
await mergeTauriConfig(url, options, tauriConf);
await shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build`);
const isChina = isChinaDomain("www.npmjs.com")
if (isChina) {
// crates.io也顺便换源
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const project_cn_conf = path.join(rust_project_dir, "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs.copyFile(project_cn_conf, project_conf);
const _ = await shellExec(
`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`
);
} else {
const _ = await shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
let arch: string;
if (process.arch === "x64") {
arch = "amd64";

View File

@@ -11,6 +11,7 @@ import tauriConf from './tauriConf.js';
import log from 'loglevel';
import { mergeTauriConfig } from './common.js';
import { npmDirectory } from '@/utils/dir.js';
import {isChinaDomain} from '@/utils/ip_addr.js';
import logger from '@/options/logger.js';
export default class MacBuilder implements IBuilder {
@@ -41,10 +42,30 @@ export default class MacBuilder implements IBuilder {
await mergeTauriConfig(url, options, tauriConf);
let dmgName: string;
if (options.multiArch) {
await shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build:mac`);
const isChina = isChinaDomain("www.npmjs.com")
if (isChina) {
// crates.io也顺便换源
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const project_cn_conf = path.join(rust_project_dir, "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs.copyFile(project_cn_conf, project_conf);
const _ = await shellExec(
`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build:mac`
);
} else {
const _ = await shellExec(`cd ${npmDirectory} && npm install && npm run build:mac`);
}
dmgName = `${name}_${tauriConf.package.version}_universal.dmg`;
} else {
await shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build`);
const isChina = isChinaDomain("www.npmjs.com")
if (isChina) {
const _ = await shellExec(
`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`
);
} else {
const _ = await shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
let arch = "x64";
if (process.arch === "arm64") {
arch = "aarch64";

View File

@@ -11,6 +11,7 @@ import tauriConf from './tauriConf.js';
import logger from '@/options/logger.js';
import { mergeTauriConfig } from './common.js';
import { npmDirectory } from '@/utils/dir.js';
import {isChinaDomain} from '@/utils/ip_addr.js';
export default class WinBuilder implements IBuilder {
async prepare() {
@@ -45,7 +46,20 @@ export default class WinBuilder implements IBuilder {
await mergeTauriConfig(url, options, tauriConf);
await shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build`);
const isChina = isChinaDomain("www.npmjs.com")
if (isChina) {
// crates.io也顺便换源
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const project_cn_conf = path.join(rust_project_dir, "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs.copyFile(project_cn_conf, project_conf);
const _ = await shellExec(
`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`
);
} else {
const _ = await shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
const language = tauriConf.tauri.bundle.windows.wix.language[0];
const arch = process.arch;
const msiName = `${name}_${tauriConf.package.version}_${arch}_${language}.msi`;

179
bin/builders/common.ts vendored
View File

@@ -1,10 +1,12 @@
import { PakeAppOptions } from '@/types.js';
import prompts from 'prompts';
import prompts, { override } from 'prompts';
import path from 'path';
import fs from 'fs/promises';
import fs2 from 'fs-extra';
import { npmDirectory } from '@/utils/dir.js';
import logger from '@/options/logger.js';
export async function promptText(message: string, initial?: string) {
const response = await prompts({
type: 'text',
@@ -15,6 +17,7 @@ export async function promptText(message: string, initial?: string) {
return response.content;
}
export async function mergeTauriConfig(
url: string,
options: PakeAppOptions,
@@ -26,6 +29,11 @@ export async function mergeTauriConfig(
fullscreen,
transparent,
resizable,
userAgent,
showMenu,
showSystemTray,
systemTrayIcon,
iterCopyFile,
identifier,
name,
} = options;
@@ -56,10 +64,132 @@ export async function mergeTauriConfig(
}
}
// logger.warn(JSON.stringify(tauriConf.pake.windows, null, 4));
Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions });
// 判断一下url类型是文件还是网站
// 如果是文件并且开启了递归拷贝功能则需要将该文件以及所在文件夹下的所有文件拷贝到src目录下否则只拷贝单个文件。
const url_exists = await fs.stat(url)
.then(() => true)
.catch(() => false);
if (url_exists) {
logger.warn("you input may a local file");
tauriConf.pake.windows[0].url_type = "local";
const file_name = path.basename(url);
const dir_name = path.dirname(url);
if (!iterCopyFile) {
const url_path = path.join(npmDirectory,"dist/", file_name);
await fs.copyFile(url, url_path);
} else {
const old_dir = path.join(npmDirectory,"dist/");
const new_dir = path.join(npmDirectory,"dist_bak/");
fs2.moveSync(old_dir, new_dir, {"overwrite": true});
fs2.copySync(dir_name, old_dir, {"overwrite": true});
// logger.warn("dir name", dir_name);
// 将dist_bak里面的cli.js和about_pake.html拷贝回去
const cli_path = path.join(new_dir, "cli.js")
const cli_path_target = path.join(old_dir, "cli.js")
const about_pake_path = path.join(new_dir, "about_pake.html");
const about_patk_path_target = path.join(old_dir, "about_pake.html")
fs.copyFile(cli_path, cli_path_target);
fs.copyFile(about_pake_path, about_patk_path_target);
}
tauriConf.pake.windows[0].url = file_name;
tauriConf.pake.windows[0].url_type = "local";
} else {
tauriConf.pake.windows[0].url_type = "web";
}
// 处理user-agent
logger.warn(userAgent);
if (userAgent.length > 0) {
if (process.platform === "win32") {
tauriConf.pake.user_agent.windows = userAgent;
}
if (process.platform === "linux") {
tauriConf.pake.user_agent.linux = userAgent;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = userAgent;
}
}
// 处理菜单栏
if (showMenu) {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = true;
}
if (process.platform === "linux") {
tauriConf.pake.menu.linux = true;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = true;
}
} else {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = false;
}
if (process.platform === "linux") {
tauriConf.pake.menu.linux = false;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = false;
}
}
// 处理托盘
if (showSystemTray) {
if (process.platform === "win32") {
tauriConf.pake.system_tray.windows = true;
}
if (process.platform === "linux") {
tauriConf.pake.system_tray.linux = true;
}
if (process.platform === "darwin") {
tauriConf.pake.system_tray.macos = true;
}
} else {
if (process.platform === "win32") {
tauriConf.pake.system_tray.windows = false;
}
if (process.platform === "linux") {
tauriConf.pake.system_tray.linux = false;
}
if (process.platform === "darwin") {
tauriConf.pake.system_tray.macos = false;
}
}
// 处理targets 暂时只对linux开放
if (process.platform === "linux") {
if (options.targets.length > 0) {
if (options.targets === "deb" || options.targets === "appimage" || options.targets === "all") {
tauriConf.tauri.bundle.targets = [options.targets];
}
}
} else {
tauriConf.tauri.bundle.targets = ["deb"];
}
Object.assign(tauriConf.tauri.windows[0], { url, ...tauriConfWindowOptions });
tauriConf.package.productName = name;
tauriConf.tauri.bundle.identifier = identifier;
// 删除映射关系
if (process.platform === "linux") {
delete tauriConf.tauri.bundle.deb.files;
}
// 处理应用图标
const exists = await fs.stat(options.icon)
.then(() => true)
.catch(() => false);
@@ -83,18 +213,21 @@ export async function mergeTauriConfig(
} else {
updateIconPath = false;
logger.warn(`icon file in Windows must be 256 * 256 pix with .ico type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["png/icon_256.ico"];
}
}
if (process.platform === "linux") {
if (customIconExt != ".png") {
updateIconPath = false;
logger.warn(`icon file in Linux must be 512 * 512 pix with .png type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["png/icon_512.png"];
}
}
if (process.platform === "darwin" && customIconExt !== ".icns") {
updateIconPath = false;
logger.warn(`icon file in MacOS must be .icns type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["icons/icon.icns"];
}
if (updateIconPath) {
tauriConf.tauri.bundle.icon = [options.icon];
@@ -120,7 +253,40 @@ export async function mergeTauriConfig(
}
}
// 处理托盘自定义图标
let useDefaultIcon = true; // 是否使用默认托盘图标
if (systemTrayIcon.length > 0) {
const icon_exists = await fs.stat(systemTrayIcon)
.then(() => true)
.catch(() => false);
if (icon_exists) {
// 需要判断图标格式默认只支持ico和png两种
let iconExt = path.extname(systemTrayIcon).toLowerCase();
if (iconExt == ".png" || iconExt == ".icon") {
useDefaultIcon = false;
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
tauriConf.tauri.systemTray.iconPath = `png/${name.toLowerCase()}${iconExt}`;
await fs.copyFile(systemTrayIcon, trayIcoPath);
} else {
logger.warn(`file type for system tray icon mut be .ico or .png , but you give ${iconExt}`);
logger.warn(`system tray icon file will not change with default.`);
}
} else {
logger.warn(`${systemTrayIcon} not exists!`)
logger.warn(`system tray icon file will not change with default.`);
}
}
// 处理托盘默认图标
if (useDefaultIcon) {
if (process.platform === "linux" || process.platform === "win32") {
tauriConf.tauri.systemTray.iconPath = tauriConf.tauri.bundle.icon[0];
} else {
tauriConf.tauri.systemTray.iconPath = "png/icon_512.png";
}
}
// 保存配置文件
let configPath = "";
switch (process.platform) {
case "win32": {
@@ -143,6 +309,15 @@ export async function mergeTauriConfig(
Buffer.from(JSON.stringify(bundleConf, null, '\t'), 'utf-8')
);
const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json')
await fs.writeFile(
pakeConfigPath,
Buffer.from(JSON.stringify(tauriConf.pake, null, 4), 'utf-8')
);
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake;
delete tauriConf2.tauri.bundle;
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json')
await fs.writeFile(

View File

@@ -1,11 +1,13 @@
import CommonConf from '../../src-tauri/tauri.conf.json';
import pakeConf from '../../src-tauri/pake.json';
import WinConf from '../../src-tauri/tauri.windows.conf.json';
import MacConf from '../../src-tauri/tauri.macos.conf.json';
import LinuxConf from '../../src-tauri/tauri.linux.conf.json';
let tauriConf = {
package: CommonConf.package,
tauri: CommonConf.tauri
tauri: CommonConf.tauri,
pake: pakeConf
}
switch (process.platform) {
case "win32": {

43
bin/cli.ts vendored
View File

@@ -13,17 +13,32 @@ program.version(packageJson.version).description('A command-line tool that can q
program
.showHelpAfterError()
.argument('[url]', 'the web URL you want to package', validateUrlInput)
.option('-n, --name <string>', 'application name')
.option('-i, --icon <string>', 'application icon', DEFAULT_PAKE_OPTIONS.icon)
.option('-w, --width <number>', 'window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
.option('-h, --height <number>', 'window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
.option('-f, --fullscreen', 'start in full screen mode', DEFAULT_PAKE_OPTIONS.fullscreen)
.option('-t, --transparent', 'transparent title bar', DEFAULT_PAKE_OPTIONS.transparent)
.option('-r, --no-resizable', 'whether the window can be resizable', DEFAULT_PAKE_OPTIONS.resizable)
.option('-d, --debug', 'debug', DEFAULT_PAKE_OPTIONS.debug)
.option('-m, --multi-arch', "available for Mac only, and supports both Intel and M1", DEFAULT_PAKE_OPTIONS.multiArch)
.option('--targets <string>', "Select the output package format, support deb/appimage/all, only for Linux", DEFAULT_PAKE_OPTIONS.targets)
.argument('[url]', 'the web url you want to package', validateUrlInput)
.option('--name <string>', 'application name')
.option('--icon <string>', 'application icon', DEFAULT_PAKE_OPTIONS.icon)
.option('--height <number>', 'window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
.option('--width <number>', 'window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
.option('--no-resizable', 'whether the window can be resizable', DEFAULT_PAKE_OPTIONS.resizable)
.option('--fullscreen', 'makes the packaged app start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
.option('--transparent', 'transparent title bar', DEFAULT_PAKE_OPTIONS.transparent)
.option('--user-agent <string>', 'custom user agent', DEFAULT_PAKE_OPTIONS.userAgent)
.option('--show-menu', 'show menu in app', DEFAULT_PAKE_OPTIONS.showMenu)
.option('--show-system-tray', 'show system tray in app', DEFAULT_PAKE_OPTIONS.showSystemTray)
.option('--system-tray-icon <string>', 'custom system tray icon', DEFAULT_PAKE_OPTIONS.systemTrayIcon)
.option('--iter-copy-file',
'copy all static file to pake app when url is a local file',
DEFAULT_PAKE_OPTIONS.iterCopyFile)
.option(
'-m, --multi-arch',
"available for Mac only, and supports both Intel and M1",
DEFAULT_PAKE_OPTIONS.multiArch
)
.option(
'--targets <string>',
'only for linux, default is "deb", option "appaimge" or "all"(deb & appimage)',
DEFAULT_PAKE_OPTIONS.targets
)
.option('--debug', 'debug', DEFAULT_PAKE_OPTIONS.transparent)
.action(async (url: string, options: PakeCliOptions) => {
await checkUpdateTips();
@@ -40,10 +55,10 @@ program
const builder = BuilderFactory.create();
await builder.prepare();
// logger.warn("you input url is ", url);
const appOptions = await handleInputOptions(options, url);
await builder.build(url, appOptions);
// logger.info(JSON.stringify(appOptions, null, 4));
builder.build(url, appOptions);
});
program.parse();

7
bin/defaults.ts vendored
View File

@@ -7,6 +7,13 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = {
fullscreen: false,
resizable: true,
transparent: false,
userAgent: '',
showMenu: false,
showSystemTray: false,
multiArch: false,
targets: 'deb',
iterCopyFile: false,
systemTrayIcon: '',
debug: false,
multiArch: false,
targets: "all",

12
bin/helpers/rust.ts vendored
View File

@@ -2,9 +2,17 @@ import { IS_WIN } from '@/utils/platform.js';
import ora from 'ora';
import shelljs from 'shelljs';
import { shellExec } from '../utils/shell.js';
import {isChinaDomain} from '@/utils/ip_addr.js'
const RustInstallScriptFocMac =
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
const is_china = isChinaDomain("sh.rustup.rs");
let RustInstallScriptFocMac = "";
if (is_china) {
RustInstallScriptFocMac =
'export RUSTUP_DIST_SERVER="https://rsproxy.cn" && export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup" && curl --proto "=https" --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh';
} else {
RustInstallScriptFocMac =
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
}
const RustInstallScriptForWin = 'winget install --id Rustlang.Rustup';
export async function installRust() {

11
bin/options/index.ts vendored
View File

@@ -3,15 +3,22 @@ import { getDomain } from '@/utils/url.js';
import { getIdentifier } from '../helpers/tauriConfig.js';
import { PakeAppOptions, PakeCliOptions } from '../types.js';
import { handleIcon } from './icon.js';
import fs from 'fs/promises';
export default async function handleOptions(options: PakeCliOptions, url: string): Promise<PakeAppOptions> {
const appOptions: PakeAppOptions = {
...options,
identifier: '',
};
const url_exists = await fs.stat(url)
.then(() => true)
.catch(() => false);
if (!appOptions.name) {
appOptions.name = await promptText('Please enter the name of your application.', getDomain(url));
if (!url_exists) {
appOptions.name = await promptText('please input your application name', getDomain(url));
} else {
appOptions.name = await promptText('please input your application name', "");
}
}
appOptions.identifier = getIdentifier(appOptions.name, url);

21
bin/types.ts vendored
View File

@@ -20,6 +20,27 @@ export interface PakeCliOptions {
/** 是否开启沉浸式头部,默认为 false 不开启 ƒ*/
transparent: boolean;
/** 自定义UA默认为不开启 ƒ*/
userAgent: string;
/** 开启菜单栏MacOS默认开启Windows,Linux默认不开启 ƒ*/
showMenu: boolean;
/** 开启系统托盘MacOS默认不开启Windows,Linux默认开启 ƒ*/
showSystemTray: boolean;
/** 托盘图标, Windows、Linux默认和应用图标共用一样的MacOS需要提别提供, 格式为png或者ico */
systemTrayIcon: string;
// /** 递归拷贝当url为本地文件路径时候若开启该选项则将url路径文件所在文件夹以及所有子文件都拷贝到pake静态文件夹默认不开启 */
iterCopyFile: false;
/** mutli arch, Supports both Intel and m1 chips, only for Mac */
multiArch: boolean;
// 包输出产物对linux用户有效默认为deb可选appimage, 或者all即同时输出deb和all;
targets: string;
/** 调试模式,会输出更多日志 */
debug: boolean;

35
bin/utils/ip_addr.ts vendored Normal file
View File

@@ -0,0 +1,35 @@
import { exec } from 'child_process';
import { promisify } from 'util';
import dns from 'dns';
const resolve = promisify(dns.resolve);
async function isChinaDomain(domain: string): Promise<boolean> {
try {
// 解析域名为IP地址
const [ip] = await resolve(domain);
return await isChinaIP(ip);
} catch (error) {
// 域名无法解析返回false
return false;
}
}
async function isChinaIP(ip: string): Promise<boolean> {
return new Promise((resolve, reject) => {
exec(`ping -c 1 -w 1 ${ip}`, (error, stdout, stderr) => {
if (error) {
// 命令执行出错返回false
resolve(false);
} else {
// 解析输出信息,提取延迟值
const match = stdout.match(/time=(\d+\.\d+) ms/);
const latency = match ? parseFloat(match[1]) : 0;
// 判断延迟是否超过100ms
resolve(latency > 100);
}
});
});
}
export { isChinaDomain, isChinaIP };

13
bin/utils/validate.ts vendored
View File

@@ -1,5 +1,6 @@
import * as Commander from 'commander';
import { normalizeUrl } from './url.js';
import fs from 'fs';
export function validateNumberInput(value: string) {
const parsedValue = Number(value);
@@ -10,9 +11,13 @@ export function validateNumberInput(value: string) {
}
export function validateUrlInput(url: string) {
try {
return normalizeUrl(url);
} catch (error) {
throw new Commander.InvalidArgumentError(error.message);
if(!fs.existsSync(url)) {
try {
return normalizeUrl(url)
} catch (error) {
throw new Commander.InvalidArgumentError(error.message);
}
} else {
return url;
}
}

16
dist/about_pake.html vendored Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<h5>Welcome from Pake!</h5>
<p>version: 1.0.9</p>
<a href="https://github.com/tw93/Pake">Project link</a><br>
<a href="https://github.com/tw93/Pake/discussions">Discussions</a><br>
<a href="https://github.com/tw93/Pake/issues" >Issues</a><br>
<p>LICENSE: MIT</p>
</body>
</html>

453
dist/cli.js vendored
View File

@@ -3,9 +3,11 @@ import { program } from 'commander';
import log from 'loglevel';
import url, { fileURLToPath } from 'url';
import isurl from 'is-url';
import fs from 'fs';
import prompts from 'prompts';
import path from 'path';
import fs from 'fs/promises';
import fs$1 from 'fs/promises';
import fs2 from 'fs-extra';
import chalk from 'chalk';
import crypto from 'crypto';
import axios from 'axios';
@@ -13,31 +15,34 @@ import { fileTypeFromBuffer } from 'file-type';
import { dir } from 'tmp-promise';
import ora from 'ora';
import shelljs from 'shelljs';
import { exec } from 'child_process';
import { promisify } from 'util';
import dns from 'dns';
import updateNotifier from 'update-notifier';
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
/******************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */
function __awaiter(thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
}
const DEFAULT_PAKE_OPTIONS = {
@@ -47,6 +52,13 @@ const DEFAULT_PAKE_OPTIONS = {
fullscreen: false,
resizable: true,
transparent: false,
userAgent: '',
showMenu: false,
showSystemTray: false,
multiArch: false,
targets: 'deb',
iterCopyFile: false,
systemTrayIcon: '',
debug: false,
multiArch: false,
targets: "all",
@@ -1585,11 +1597,16 @@ function validateNumberInput(value) {
return parsedValue;
}
function validateUrlInput(url) {
try {
return normalizeUrl(url);
if (!fs.existsSync(url)) {
try {
return normalizeUrl(url);
}
catch (error) {
throw new Commander.InvalidArgumentError(error.message);
}
}
catch (error) {
throw new Commander.InvalidArgumentError(error.message);
else {
return url;
}
}
@@ -1626,7 +1643,7 @@ function promptText(message, initial) {
}
function mergeTauriConfig(url, options, tauriConf) {
return __awaiter(this, void 0, void 0, function* () {
const { width, height, fullscreen, transparent, resizable, identifier, name, } = options;
const { width, height, fullscreen, transparent, resizable, userAgent, showMenu, showSystemTray, systemTrayIcon, iterCopyFile, identifier, name, } = options;
const tauriConfWindowOptions = {
width,
height,
@@ -1652,10 +1669,120 @@ function mergeTauriConfig(url, options, tauriConf) {
process.exit();
}
}
Object.assign(tauriConf.tauri.windows[0], Object.assign({ url }, tauriConfWindowOptions));
// logger.warn(JSON.stringify(tauriConf.pake.windows, null, 4));
Object.assign(tauriConf.pake.windows[0], Object.assign({ url }, tauriConfWindowOptions));
// 判断一下url类型是文件还是网站
// 如果是文件并且开启了递归拷贝功能则需要将该文件以及所在文件夹下的所有文件拷贝到src目录下否则只拷贝单个文件。
const url_exists = yield fs$1.stat(url)
.then(() => true)
.catch(() => false);
if (url_exists) {
logger.warn("you input may a local file");
tauriConf.pake.windows[0].url_type = "local";
const file_name = path.basename(url);
const dir_name = path.dirname(url);
if (!iterCopyFile) {
const url_path = path.join(npmDirectory, "dist/", file_name);
yield fs$1.copyFile(url, url_path);
}
else {
const old_dir = path.join(npmDirectory, "dist/");
const new_dir = path.join(npmDirectory, "dist_bak/");
fs2.moveSync(old_dir, new_dir, { "overwrite": true });
fs2.copySync(dir_name, old_dir, { "overwrite": true });
// logger.warn("dir name", dir_name);
// 将dist_bak里面的cli.js和about_pake.html拷贝回去
const cli_path = path.join(new_dir, "cli.js");
const cli_path_target = path.join(old_dir, "cli.js");
const about_pake_path = path.join(new_dir, "about_pake.html");
const about_patk_path_target = path.join(old_dir, "about_pake.html");
fs$1.copyFile(cli_path, cli_path_target);
fs$1.copyFile(about_pake_path, about_patk_path_target);
}
tauriConf.pake.windows[0].url = file_name;
tauriConf.pake.windows[0].url_type = "local";
}
else {
tauriConf.pake.windows[0].url_type = "web";
}
// 处理user-agent
logger.warn(userAgent);
if (userAgent.length > 0) {
if (process.platform === "win32") {
tauriConf.pake.user_agent.windows = userAgent;
}
if (process.platform === "linux") {
tauriConf.pake.user_agent.linux = userAgent;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = userAgent;
}
}
// 处理菜单栏
if (showMenu) {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = true;
}
if (process.platform === "linux") {
tauriConf.pake.menu.linux = true;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = true;
}
}
else {
if (process.platform === "win32") {
tauriConf.pake.menu.windows = false;
}
if (process.platform === "linux") {
tauriConf.pake.menu.linux = false;
}
if (process.platform === "darwin") {
tauriConf.pake.user_agent.macos = false;
}
}
// 处理托盘
if (showSystemTray) {
if (process.platform === "win32") {
tauriConf.pake.system_tray.windows = true;
}
if (process.platform === "linux") {
tauriConf.pake.system_tray.linux = true;
}
if (process.platform === "darwin") {
tauriConf.pake.system_tray.macos = true;
}
}
else {
if (process.platform === "win32") {
tauriConf.pake.system_tray.windows = false;
}
if (process.platform === "linux") {
tauriConf.pake.system_tray.linux = false;
}
if (process.platform === "darwin") {
tauriConf.pake.system_tray.macos = false;
}
}
// 处理targets 暂时只对linux开放
if (process.platform === "linux") {
if (options.targets.length > 0) {
if (options.targets === "deb" || options.targets === "appimage" || options.targets === "all") {
tauriConf.tauri.bundle.targets = [options.targets];
}
}
}
else {
tauriConf.tauri.bundle.targets = ["deb"];
}
tauriConf.package.productName = name;
tauriConf.tauri.bundle.identifier = identifier;
const exists = yield fs.stat(options.icon)
// 删除映射关系
if (process.platform === "linux") {
delete tauriConf.tauri.bundle.deb.files;
}
// 处理应用图标
const exists = yield fs$1.stat(options.icon)
.then(() => true)
.catch(() => false);
if (process.platform === "linux") {
@@ -1674,22 +1801,25 @@ function mergeTauriConfig(url, options, tauriConf) {
if (customIconExt === ".ico") {
const ico_path = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}_32.ico`);
tauriConf.tauri.bundle.resources = [`png/${name.toLowerCase()}_32.ico`];
yield fs.copyFile(options.icon, ico_path);
yield fs$1.copyFile(options.icon, ico_path);
}
else {
updateIconPath = false;
logger.warn(`icon file in Windows must be 256 * 256 pix with .ico type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["png/icon_256.ico"];
}
}
if (process.platform === "linux") {
if (customIconExt != ".png") {
updateIconPath = false;
logger.warn(`icon file in Linux must be 512 * 512 pix with .png type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["png/icon_512.png"];
}
}
if (process.platform === "darwin" && customIconExt !== ".icns") {
updateIconPath = false;
logger.warn(`icon file in MacOS must be .icns type, but you give ${customIconExt}`);
tauriConf.tauri.bundle.icon = ["icons/icon.icns"];
}
if (updateIconPath) {
tauriConf.tauri.bundle.icon = [options.icon];
@@ -1716,6 +1846,41 @@ function mergeTauriConfig(url, options, tauriConf) {
}
}
}
// 处理托盘自定义图标
let useDefaultIcon = true; // 是否使用默认托盘图标
if (systemTrayIcon.length > 0) {
const icon_exists = yield fs$1.stat(systemTrayIcon)
.then(() => true)
.catch(() => false);
if (icon_exists) {
// 需要判断图标格式默认只支持ico和png两种
let iconExt = path.extname(systemTrayIcon).toLowerCase();
if (iconExt == ".png" || iconExt == ".icon") {
useDefaultIcon = false;
const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`);
tauriConf.tauri.systemTray.iconPath = `png/${name.toLowerCase()}${iconExt}`;
yield fs$1.copyFile(systemTrayIcon, trayIcoPath);
}
else {
logger.warn(`file type for system tray icon mut be .ico or .png , but you give ${iconExt}`);
logger.warn(`system tray icon file will not change with default.`);
}
}
else {
logger.warn(`${systemTrayIcon} not exists!`);
logger.warn(`system tray icon file will not change with default.`);
}
}
// 处理托盘默认图标
if (useDefaultIcon) {
if (process.platform === "linux" || process.platform === "win32") {
tauriConf.tauri.systemTray.iconPath = tauriConf.tauri.bundle.icon[0];
}
else {
tauriConf.tauri.systemTray.iconPath = "png/icon_512.png";
}
}
// 保存配置文件
let configPath = "";
switch (process.platform) {
case "win32": {
@@ -1732,9 +1897,14 @@ function mergeTauriConfig(url, options, tauriConf) {
}
}
let bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } };
yield fs.writeFile(configPath, Buffer.from(JSON.stringify(bundleConf, null, '\t'), 'utf-8'));
yield fs$1.writeFile(configPath, Buffer.from(JSON.stringify(bundleConf, null, 4), 'utf-8'));
const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json');
yield fs$1.writeFile(pakeConfigPath, Buffer.from(JSON.stringify(tauriConf.pake, null, 4), 'utf-8'));
let tauriConf2 = JSON.parse(JSON.stringify(tauriConf));
delete tauriConf2.pake;
delete tauriConf2.tauri.bundle;
const configJsonPath = path.join(npmDirectory, 'src-tauri/tauri.conf.json');
yield fs.writeFile(configJsonPath, Buffer.from(JSON.stringify(tauriConf, null, '\t'), 'utf-8'));
yield fs$1.writeFile(configJsonPath, Buffer.from(JSON.stringify(tauriConf2, null, 4), 'utf-8'));
});
}
@@ -1801,7 +1971,7 @@ function downloadIcon(iconUrl) {
}
const { path } = yield dir();
const iconPath = `${path}/icon.${fileDetails.ext}`;
yield fs.writeFile(iconPath, iconData);
yield fs$1.writeFile(iconPath, iconData);
return iconPath;
});
}
@@ -1809,8 +1979,16 @@ function downloadIcon(iconUrl) {
function handleOptions(options, url) {
return __awaiter(this, void 0, void 0, function* () {
const appOptions = Object.assign(Object.assign({}, options), { identifier: '' });
const url_exists = yield fs$1.stat(url)
.then(() => true)
.catch(() => false);
if (!appOptions.name) {
appOptions.name = yield promptText('Please enter the name of your application.', getDomain(url));
if (!url_exists) {
appOptions.name = yield promptText('please input your application name', getDomain(url));
}
else {
appOptions.name = yield promptText('please input your application name', "");
}
}
appOptions.identifier = getIdentifier(appOptions.name, url);
appOptions.icon = yield handleIcon(appOptions);
@@ -1831,7 +2009,50 @@ function shellExec(command) {
});
}
const RustInstallScriptFocMac = "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
const resolve = promisify(dns.resolve);
function isChinaDomain(domain) {
return __awaiter(this, void 0, void 0, function* () {
try {
// 解析域名为IP地址
const [ip] = yield resolve(domain);
return yield isChinaIP(ip);
}
catch (error) {
// 域名无法解析返回false
return false;
}
});
}
function isChinaIP(ip) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
exec(`ping -c 1 -w 1 ${ip}`, (error, stdout, stderr) => {
if (error) {
// 命令执行出错返回false
resolve(false);
}
else {
// 解析输出信息,提取延迟值
const match = stdout.match(/time=(\d+\.\d+) ms/);
const latency = match ? parseFloat(match[1]) : 0;
// 判断延迟是否超过100ms
resolve(latency > 100);
}
});
});
});
}
const is_china = isChinaDomain("sh.rustup.rs");
let RustInstallScriptFocMac = "";
if (is_china) {
RustInstallScriptFocMac =
'export RUSTUP_DIST_SERVER="https://rsproxy.cn" && export RUSTUP_UPDATE_ROOT="https://rsproxy.cn/rustup" && curl --proto "=https" --tlsv1.2 -sSf https://rsproxy.cn/rustup-init.sh | sh';
}
else {
RustInstallScriptFocMac =
"curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y";
}
const RustInstallScriptForWin = 'winget install --id Rustlang.Rustup';
function installRust() {
return __awaiter(this, void 0, void 0, function* () {
@@ -1852,24 +2073,22 @@ function checkRustInstalled() {
}
var tauri$3 = {
windows: [
{
url: "https://weread.qq.com/",
transparent: true,
fullscreen: false,
width: 1200,
height: 780,
resizable: true
}
],
security: {
csp: null
},
updater: {
active: false
},
systemTray: {
iconPath: "png/weread_512.png",
iconAsTemplate: true
},
allowlist: {
all: true
}
};
var build = {
withGlobalTauri: true,
devPath: "../dist",
distDir: "../dist",
beforeBuildCommand: "",
@@ -1884,6 +2103,39 @@ var CommonConf = {
build: build
};
var windows = [
{
url: "https://weread.qq.com/",
transparent: true,
fullscreen: false,
width: 1200,
height: 780,
resizable: true,
url_type: "web"
}
];
var user_agent = {
macos: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
linux: "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
windows: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
};
var menu = {
macos: true,
linux: false,
windows: false
};
var system_tray = {
macos: false,
linux: true,
windows: true
};
var pakeConf = {
windows: windows,
user_agent: user_agent,
menu: menu,
system_tray: system_tray
};
var tauri$2 = {
bundle: {
icon: [
@@ -1964,16 +2216,8 @@ var tauri = {
copyright: "",
deb: {
depends: [
"libwebkit2gtk-4.0-dev",
"build-essential",
"curl",
"wget",
"libssl-dev",
"libgtk-3-dev",
"libayatana-appindicator3-dev",
"librsvg2-dev",
"gnome-video-effects",
"gnome-video-effects-extra"
"wget"
],
files: {
"/usr/share/applications/com-tw93-weread.desktop": "assets/com-tw93-weread.desktop"
@@ -1997,7 +2241,8 @@ var LinuxConf = {
let tauriConf = {
package: CommonConf.package,
tauri: CommonConf.tauri
tauri: CommonConf.tauri,
pake: pakeConf
};
switch (process.platform) {
case "win32": {
@@ -2042,11 +2287,28 @@ class MacBuilder {
yield mergeTauriConfig(url, options, tauriConf);
let dmgName;
if (options.multiArch) {
yield shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build:mac`);
const isChina = isChinaDomain("www.npmjs.com");
if (isChina) {
// crates.io也顺便换源
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const project_cn_conf = path.join(rust_project_dir, "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs$1.copyFile(project_cn_conf, project_conf);
yield shellExec(`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build:mac`);
}
else {
yield shellExec(`cd ${npmDirectory} && npm install && npm run build:mac`);
}
dmgName = `${name}_${tauriConf.package.version}_universal.dmg`;
}
else {
yield shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build`);
const isChina = isChinaDomain("www.npmjs.com");
if (isChina) {
yield shellExec(`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`);
}
else {
yield shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
let arch = "x64";
if (process.arch === "arm64") {
arch = "aarch64";
@@ -2058,8 +2320,8 @@ class MacBuilder {
}
const appPath = this.getBuildAppPath(npmDirectory, dmgName, options.multiArch);
const distPath = path.resolve(`${name}.dmg`);
yield fs.copyFile(appPath, distPath);
yield fs.unlink(appPath);
yield fs$1.copyFile(appPath, distPath);
yield fs$1.unlink(appPath);
logger.success('Build success!');
logger.success('You can find the app installer in', distPath);
});
@@ -2104,14 +2366,25 @@ class WinBuilder {
logger.debug('PakeAppOptions', options);
const { name } = options;
yield mergeTauriConfig(url, options, tauriConf);
yield shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build`);
const isChina = isChinaDomain("www.npmjs.com");
if (isChina) {
// crates.io也顺便换源
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const project_cn_conf = path.join(rust_project_dir, "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs$1.copyFile(project_cn_conf, project_conf);
yield shellExec(`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`);
}
else {
yield shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
const language = tauriConf.tauri.bundle.windows.wix.language[0];
const arch = process.arch;
const msiName = `${name}_${tauriConf.package.version}_${arch}_${language}.msi`;
const appPath = this.getBuildAppPath(npmDirectory, msiName);
const distPath = path.resolve(`${name}.msi`);
yield fs.copyFile(appPath, distPath);
yield fs.unlink(appPath);
yield fs$1.copyFile(appPath, distPath);
yield fs$1.unlink(appPath);
logger.success('Build success!');
logger.success('You can find the app installer in', distPath);
});
@@ -2149,7 +2422,18 @@ class LinuxBuilder {
logger.debug('PakeAppOptions', options);
const { name } = options;
yield mergeTauriConfig(url, options, tauriConf);
yield shellExec(`cd "${npmDirectory}" && npm install --verbose && npm run build`);
const isChina = isChinaDomain("www.npmjs.com");
if (isChina) {
// crates.io也顺便换源
const rust_project_dir = path.join(npmDirectory, 'src-tauri', ".cargo");
const project_cn_conf = path.join(rust_project_dir, "cn_config.bak");
const project_conf = path.join(rust_project_dir, "config");
fs$1.copyFile(project_cn_conf, project_conf);
yield shellExec(`cd ${npmDirectory} && npm install --registry=https://registry.npmmirror.com && npm run build`);
}
else {
yield shellExec(`cd ${npmDirectory} && npm install && npm run build`);
}
let arch;
if (process.arch === "x64") {
arch = "amd64";
@@ -2161,8 +2445,8 @@ class LinuxBuilder {
const debName = `${name}_${tauriConf.package.version}_${arch}.deb`;
const appPath = this.getBuildAppPath(npmDirectory, "deb", debName);
const distPath = path.resolve(`${name}.deb`);
yield fs.copyFile(appPath, distPath);
yield fs.unlink(appPath);
yield fs$1.copyFile(appPath, distPath);
yield fs$1.unlink(appPath);
logger.success('Build Deb success!');
logger.success('You can find the deb app installer in', distPath);
}
@@ -2170,8 +2454,8 @@ class LinuxBuilder {
const appImageName = `${name}_${tauriConf.package.version}_${arch}.AppImage`;
const appImagePath = this.getBuildAppPath(npmDirectory, "appimage", appImageName);
const distAppPath = path.resolve(`${name}.AppImage`);
yield fs.copyFile(appImagePath, distAppPath);
yield fs.unlink(appImagePath);
yield fs$1.copyFile(appImagePath, distAppPath);
yield fs$1.unlink(appImagePath);
logger.success('Build AppImage success!');
logger.success('You can find the AppImage app installer in', distAppPath);
}
@@ -2250,6 +2534,7 @@ var dependencies = {
chalk: "^5.1.2",
commander: "^9.4.1",
"file-type": "^18.0.0",
"fs-extra": "^11.1.0",
"is-url": "^1.2.4",
loglevel: "^1.8.1",
ora: "^6.1.2",
@@ -2264,6 +2549,7 @@ var devDependencies = {
"@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-terser": "^0.1.0",
"@rollup/plugin-typescript": "^9.0.2",
"@types/fs-extra": "^9.0.13",
"@types/is-url": "^1.2.30",
"@types/page-icon": "^0.3.4",
"@types/prompts": "^2.4.1",
@@ -2304,17 +2590,22 @@ function checkUpdateTips() {
program.version(packageJson.version).description('A command-line tool that can quickly convert a webpage into a desktop application.');
program
.showHelpAfterError()
.argument('[url]', 'the web URL you want to package', validateUrlInput)
.option('-n, --name <string>', 'application name')
.option('-i, --icon <string>', 'application icon', DEFAULT_PAKE_OPTIONS.icon)
.option('-w, --width <number>', 'window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
.option('-h, --height <number>', 'window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
.option('-f, --fullscreen', 'start in full screen mode', DEFAULT_PAKE_OPTIONS.fullscreen)
.option('-t, --transparent', 'transparent title bar', DEFAULT_PAKE_OPTIONS.transparent)
.option('-r, --no-resizable', 'whether the window can be resizable', DEFAULT_PAKE_OPTIONS.resizable)
.option('-d, --debug', 'debug', DEFAULT_PAKE_OPTIONS.debug)
.argument('[url]', 'the web url you want to package', validateUrlInput)
.option('--name <string>', 'application name')
.option('--icon <string>', 'application icon', DEFAULT_PAKE_OPTIONS.icon)
.option('--height <number>', 'window height', validateNumberInput, DEFAULT_PAKE_OPTIONS.height)
.option('--width <number>', 'window width', validateNumberInput, DEFAULT_PAKE_OPTIONS.width)
.option('--no-resizable', 'whether the window can be resizable', DEFAULT_PAKE_OPTIONS.resizable)
.option('--fullscreen', 'makes the packaged app start in full screen', DEFAULT_PAKE_OPTIONS.fullscreen)
.option('--transparent', 'transparent title bar', DEFAULT_PAKE_OPTIONS.transparent)
.option('--user-agent <string>', 'custom user agent', DEFAULT_PAKE_OPTIONS.userAgent)
.option('--show-menu', 'show menu in app', DEFAULT_PAKE_OPTIONS.showMenu)
.option('--show-system-tray', 'show system tray in app', DEFAULT_PAKE_OPTIONS.showSystemTray)
.option('--system-tray-icon <string>', 'custom system tray icon', DEFAULT_PAKE_OPTIONS.systemTrayIcon)
.option('--iter-copy-file', 'copy all static file to pake app when url is a local file', DEFAULT_PAKE_OPTIONS.iterCopyFile)
.option('-m, --multi-arch', "available for Mac only, and supports both Intel and M1", DEFAULT_PAKE_OPTIONS.multiArch)
.option('--targets <string>', "Select the output package format, support deb/appimage/all, only for Linux", DEFAULT_PAKE_OPTIONS.targets)
.option('--targets <string>', 'only for linux, default is "deb", option "appaimge" or "all"(deb & appimage)', DEFAULT_PAKE_OPTIONS.targets)
.option('--debug', 'debug', DEFAULT_PAKE_OPTIONS.transparent)
.action((url, options) => __awaiter(void 0, void 0, void 0, function* () {
yield checkUpdateTips();
if (!url) {
@@ -2327,7 +2618,9 @@ program
}
const builder = BuilderFactory.create();
yield builder.prepare();
// logger.warn("you input url is ", url);
const appOptions = yield handleOptions(options, url);
yield builder.build(url, appOptions);
// logger.info(JSON.stringify(appOptions, null, 4));
builder.build(url, appOptions);
}));
program.parse();

View File

@@ -52,6 +52,7 @@
"chalk": "^5.1.2",
"commander": "^9.4.1",
"file-type": "^18.0.0",
"fs-extra": "^11.1.0",
"is-url": "^1.2.4",
"loglevel": "^1.8.1",
"ora": "^6.1.2",
@@ -66,6 +67,7 @@
"@rollup/plugin-json": "^5.0.1",
"@rollup/plugin-terser": "^0.1.0",
"@rollup/plugin-typescript": "^9.0.2",
"@types/fs-extra": "^9.0.13",
"@types/is-url": "^1.2.30",
"@types/page-icon": "^0.3.4",
"@types/prompts": "^2.4.1",

99
script/build.bat vendored Normal file
View File

@@ -0,0 +1,99 @@
@echo off
chcp 65001
if not exist node_modules (
call npm i
)
if not exist output (
mkdir output
)
if not exist output\windows (
mkdir output\windows
)
echo.
echo =======================
echo "build for windows"
echo =======================
echo.
:: total package number
set /A index=1
for /f %%a in (' find /c /v "" ^<"app.csv" ') do set /A total=%%a
:: ignore first header line
set /A total=total-1
set old_name=weread
set old_title=WeRead
set old_zh_name=微信阅读
set old_url=https://weread.qq.com/
:: set init name, we will recovery code to init when build finish.
set init_name=%old_name%
set init_title=%old_title%
set init_zh_name=%old_zh_name%
set init_url=%old_url%
:: for windows, we need replace package name to title
:: .\script\sd.exe "\"productName\": \"weread\"" "\"productName\": \"WeRead\"" src-tauri\tauri.conf.json
for /f "skip=1 tokens=1-4 delims=," %%i in (app.csv) do (
setlocal enabledelayedexpansion
set name=%%i
set title=%%j
set name_zh=%%k
set url=%%l
@echo on
::echo name is !name! !name_zh! !url!
:: replace url
.\script\sd.exe -s !old_url! !url! src-tauri\pake.json
::replace pacakge name
.\script\sd.exe !old_title! !title! src-tauri\tauri.conf.json
.\script\sd.exe !old_name! !name! src-tauri\tauri.conf.json
.\script\sd.exe !old_name! !name! src-tauri\tauri.windows.conf.json
echo.
::update package info
set old_zh_name=!name_zh!
set old_name=!name!
set old_title=!title!
set old_url=!url!
::build package
echo building package !index!/!total!
echo package name is !name! !name_zh!
echo npm run build:windows
@echo off
call npm run tauri build -- --target x86_64-pc-windows-msvc
move src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\*.msi output\windows\!title!_x64.msi
::rm cache
del /q /f /s src-tauri\target\x86_64-pc-windows-msvc\release\*.exe
del /q /f /s src-tauri\target\x86_64-pc-windows-msvc\release\resources\*.ico
del /q /f /s src-tauri\target\x86_64-pc-windows-msvc\release\png\*.ico
del /q /f /s src-tauri\target\x86_64-pc-windows-msvc\release\wix\*.*
del /q /f /s src-tauri\target\x86_64-pc-windows-msvc\release\app.*
rd /s /q src-tauri\target\x86_64-pc-windows-msvc\release\resources
rd /s /q src-tauri\target\x86_64-pc-windows-msvc\release\png
rd /s /q src-tauri\target\x86_64-pc-windows-msvc\release\wix
@echo on
echo package build success!
echo.
echo.
set /A index=index+1
@echo off
)
:: for windows, we need replace package name to lower again
:: .\script\sd.exe "\"productName\": \"WeRead\"" "\"productName\": \"weread\"" src-tauri\tauri.conf.json
echo "output dir is output\windows"
::recovery code
.\script\sd.exe %url% %init_url% src-tauri\pake.json
.\script\sd.exe %title% %init_title% src-tauri\tauri.conf.json
.\script\sd.exe %name% %init_name% src-tauri\tauri.conf.json
.\script\sd.exe %name% %init_name% src-tauri\tauri.windows.conf.json

20
script/build.ps1 vendored
View File

@@ -29,6 +29,7 @@ $identifier_prefix = "com.tw93"
# total package number
$index = 1
$total = (Get-Content ./app.csv | Measure-Object -Line).Lines
$pake_conf_path = "src-tauri/pake.json"
$common_conf_path = "src-tauri/tauri.conf.json"
$windows_conf_path = "src-tauri/tauri.windows.conf.json"
@@ -47,20 +48,27 @@ ForEach ($line in (Get-Content -Path .\app.csv | Select-Object -Skip 1)) {
Write-Host "name_zh = ${name_zh}"
Write-Host "url = ${url}"
Write-Host "=========================="
# -- replace url --
# -- replace url -- #
# clear url with regex
(Get-Content -Path $common_conf_path -Raw) -replace '"url":\s*"[^"]*"', '"url": ""' | Set-Content -Path $common_conf_path
(Get-Content -Path $pake_conf_path -Raw) -replace '"url":\s*"[^"]*"', '"url": ""' | Set-Content -Path $pake_conf_path
# replace url with no regex
(Get-Content -Path $common_conf_path -Raw) | ForEach-Object { $_.Replace('"url": ""', "`"url`": `"${url}`"") } | Set-Content $common_conf_path
(Get-Content -Path $pake_conf_path -Raw) | ForEach-Object { $_.Replace('"url": ""', "`"url`": `"${url}`"") } | Set-Content $pake_conf_path
# replace package name
# -- replace package name -- #
# clear package_name with regex
(Get-Content -Path $common_conf_path -Raw) -replace '"productName":\s*"[^"]*"', '"productName": ""' | Set-Content -Path $common_conf_path
# replace package_name with no regex
(Get-Content -Path $common_conf_path -Raw) | ForEach-Object { $_.Replace('"productName": ""', "`"productName`": `"${title}`"") } | Set-Content $common_conf_path
# -- replace icon --
# -- replace systemTray iconPath -- #
# clear systemTray iconPath with regex
(Get-Content -Path $common_conf_path -Raw) -replace '"iconPath":\s*"[^"]*"', '"iconPath": ""' | Set-Content -Path $common_conf_path
# replace systemTray iconPath with no regex
(Get-Content -Path $common_conf_path -Raw) | ForEach-Object { $_.Replace('"iconPath": ""', "`"iconPath`": `"png/${name}_32.ico`"") } | Set-Content $common_conf_path
# -- replace icon --
# clear icon path with regex
(Get-Content -Path $windows_conf_path -Raw) -replace '(?s)"icon":\s*\[[^\]]*\]', '"icon": []' | Set-Content -Path $windows_conf_path
# replace icon path with no regex
@@ -85,7 +93,7 @@ ForEach ($line in (Get-Content -Path .\app.csv | Select-Object -Skip 1)) {
if (-not (Test-Path "src-tauri\png\${name}_256.ico")) {
Copy-Item "src-tauri\png\icon_256.ico" "src-tauri\png\${name}_256.ico"
}
# build package
Write-Host "npm run build:windows"
npm run tauri build -- --target x86_64-pc-windows-msvc

23
script/build.sh vendored
View File

@@ -61,12 +61,12 @@ do
package_title=${arr[1]}
package_zh_name=${arr[2]}
url=${arr[3]}
# replace package info
# clear url with regex
$sd "\"url\": \"(.*?)\"," "\"url\": \"\"," src-tauri/tauri.conf.json
$sd "\"url\": \"(.*?)\"," "\"url\": \"\"," src-tauri/pake.json
# replace url with no regex
$sd -s "\"url\": \"\"," "\"url\": \"${url}\"," src-tauri/tauri.conf.json
$sd -s "\"url\": \"\"," "\"url\": \"${url}\"," src-tauri/pake.json
# for apple, need replace title
if [[ "$OSTYPE" =~ ^darwin ]]; then
@@ -106,14 +106,25 @@ do
echo "warning"
cp "src-tauri/png/icon_512.png" "src-tauri/png/${package_name}_512.png"
fi
# -- replace package name -- #
# clear package_name with regex
$sd "\"productName\": \"(.*?)\"," "\"productName\": \"\"," src-tauri/tauri.conf.json
# replace package_name with no regex
$sd -s "\"productName\": \"\"," "\"productName\": \"${package_prefix}-${package_name}\"," src-tauri/tauri.conf.json
# -- replace systemTray iconPath -- #
# clear systemTray iconPath with regex
$sd "\"iconPath\": \"(.*?)\"," "\"iconPath\": \"\"," src-tauri/tauri.conf.json
# replace systemTray iconPath with no regex
$sd -s "\"iconPath\": \"\"," "\"iconPath\": \"png/${package_name}_512.png\"," src-tauri/tauri.conf.json
# -- replace icon -- #
# clear icon path with regex
$sd "\"icon\": \[\"(.*?)\"\]," "\"icon\": [\"\"]," src-tauri/tauri.linux.conf.json
# replace icon path with no regex
$sd -s "\"icon\": [\"\"]," "\"icon\": [\"png/${package_name}_512.png\"]," src-tauri/tauri.linux.conf.json
# -- replace identifier -- #
# clear identifier with regex
$sd "\"identifier\": \"(.*?)\"," "\"identifier\": \"\"," src-tauri/tauri.linux.conf.json
# replace identifier with not regex
@@ -122,10 +133,10 @@ do
new_desktop="${PROJECT_FOLDER}/src-tauri/assets/${package_prefix}-${package_name}.desktop"
new_desktop_map_path="/usr/share/applications/${package_prefix}-${package_name}.desktop"
for file in `ls ${PROJECT_FOLDER}/src-tauri/assets/`
for file in `ls ${PROJECT_FOLDER}/src-tauri/assets/*.desktop`
do
mv "${PROJECT_FOLDER}/src-tauri/assets/${file}" "${new_desktop}"
echo mv "${PROJECT_FOLDER}/src-tauri/assets/${file}" "${new_desktop}"
mv "${file}" "${new_desktop}"
echo mv "${file}" "${new_desktop}"
done
# clear desktop file with regex
$sd "\"files\": \{\"(.*)\"\}" "\"files\": \{\"\"\}" src-tauri/tauri.linux.conf.json

BIN
script/sd.exe vendored

Binary file not shown.

View File

@@ -0,0 +1,14 @@
[source.crates-io]
# To use sparse index, change 'rsproxy' to 'rsproxy-sparse'
replace-with = 'rsproxy'
[source.rsproxy]
registry = "https://rsproxy.cn/crates.io-index"
[source.rsproxy-sparse]
registry = "sparse+https://rsproxy.cn/index/"
[registries.rsproxy]
index = "https://rsproxy.cn/crates.io-index"
[net]
git-fetch-with-cli = true

1957
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,10 @@
[package]
name = "app"
version = "0.1.0"
description = "Pake 打包工具"
description = "🤱🏻 Turn any webpage into a desktop app with Rust."
authors = ["Tw93"]
license = ""
repository = ""
license = "MIT"
repository = "https://github.com/tw93/Pake"
default-run = "app"
edition = "2021"
rust-version = "1.63.0"
@@ -15,15 +15,15 @@ rust-version = "1.63.0"
tauri-build = { version = "1.2.1", features = [] }
[dependencies]
serde_json = "1.0.91"
serde = { version = "1.0.152", features = ["derive"] }
tauri = { version = "1.2.4", features = [] }
serde_json = "1.0.89"
serde = { version = "1.0.150", features = ["derive"] }
tauri = { version = "1.2.1", features = ["api-all", "devtools", "system-tray"] }
image = "0.24.5"
home = "0.5.4"
tauri-utils = "1.2.1"
webbrowser = "0.8.7"
wry = "0.23.4"
dirs = "4.0"
home = "0.5"
dirs = "5.0"
libc = "0.2"
download_rs = { version = "0.2.0", features = ["sync_download"] }
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "dev" }
[features]
# by default Tauri runs in production mode
@@ -32,5 +32,3 @@ default = ["custom-protocol"]
# this feature is used used for production builds where `devPath` points to the filesystem
# DO NOT remove this
custom-protocol = ["tauri/custom-protocol"]
# Enable DevTools for debugging.
devtools = []

28
src-tauri/pake.json Normal file
View File

@@ -0,0 +1,28 @@
{
"windows": [
{
"url": "https://weread.qq.com/",
"transparent": true,
"fullscreen": false,
"width": 1200,
"height": 780,
"resizable": true,
"url_type": "web"
}
],
"user_agent": {
"macos": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15",
"linux": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36",
"windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36"
},
"menu": {
"macos": true,
"linux": false,
"windows": false
},
"system_tray": {
"macos": false,
"linux": true,
"windows": true
}
}

View File

@@ -0,0 +1,63 @@
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct WindowConfig {
pub url: String,
pub transparent: bool,
pub fullscreen: bool,
pub width: f64,
pub height: f64,
pub resizable: bool,
pub url_type: String,
}
#[derive(Debug, Deserialize)]
pub struct PlatformSpecific<T> {
pub macos: T,
pub linux: T,
pub windows: T,
}
impl<T> PlatformSpecific<T> {
pub const fn get(&self) -> &T {
#[cfg(target_os = "macos")]
let platform = &self.macos;
#[cfg(target_os = "linux")]
let platform = &self.linux;
#[cfg(target_os = "windows")]
let platform = &self.windows;
platform
}
}
impl<T> PlatformSpecific<T>
where
T: Copy,
{
pub const fn copied(&self) -> T {
*self.get()
}
}
pub type UserAgent = PlatformSpecific<String>;
pub type FunctionON = PlatformSpecific<bool>;
#[derive(Debug, Deserialize)]
pub struct PakeConfig {
pub windows: Vec<WindowConfig>,
pub user_agent: UserAgent,
pub menu: FunctionON,
pub system_tray: FunctionON,
}
impl PakeConfig {
pub fn show_menu(&self) -> bool {
self.menu.copied()
}
#[cfg(not(target_os = "macos"))]
pub fn show_system_tray(&self) -> bool {
self.system_tray.copied()
}
}

View File

@@ -0,0 +1,47 @@
use crate::util::{check_file_or_append, get_download_message, show_toast};
use download_rs::sync_download::Download;
use tauri::{api, command, AppHandle, Manager, Window};
#[derive(serde::Deserialize)]
pub struct DownloadFileParams {
url: String,
filename: String,
}
#[command]
pub fn drag_window(app: AppHandle) {
app.get_window("pake").unwrap().start_dragging().unwrap();
}
#[command]
pub fn fullscreen(app: AppHandle) {
let win = app.get_window("pake").unwrap();
if win.is_fullscreen().unwrap() {
win.set_fullscreen(false).unwrap();
} else {
win.set_fullscreen(true).unwrap();
}
}
#[tauri::command]
pub fn open_browser(app: AppHandle, url: String) {
api::shell::open(&app.shell_scope(), url, None).unwrap();
}
#[command]
pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> {
let window: Window = app.get_window("pake").unwrap();
let output_path = api::path::download_dir().unwrap().join(params.filename);
let file_path = check_file_or_append(output_path.to_str().unwrap());
let download = Download::new(&params.url, Some(&file_path), None);
match download.download() {
Ok(_) => {
show_toast(&window, &get_download_message());
Ok(())
}
Err(e) => {
show_toast(&window, &e.to_string());
Err(e.to_string())
}
}
}

106
src-tauri/src/app/menu.rs Normal file
View File

@@ -0,0 +1,106 @@
use tauri::MenuItem;
use tauri::{CustomMenuItem, Menu, Submenu, WindowMenuEvent};
#[cfg(any(target_os = "linux", target_os = "windows"))]
use tauri::{Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowBuilder, WindowUrl};
#[cfg(any(target_os = "linux", target_os = "windows"))]
use tauri_plugin_window_state::{AppHandleExt, StateFlags};
pub fn get_menu() -> Menu {
let close = CustomMenuItem::new("close".to_string(), "Close Window").accelerator("CmdOrCtrl+W");
let first_menu = Menu::new()
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Undo)
.add_native_item(MenuItem::Redo)
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::Separator)
.add_native_item(MenuItem::EnterFullScreen)
.add_native_item(MenuItem::Minimize)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Separator)
.add_item(close)
.add_native_item(MenuItem::Quit);
let app_menu = Submenu::new("File", first_menu);
Menu::new().add_submenu(app_menu)
}
pub fn menu_event_handle(event: WindowMenuEvent) {
if event.menu_item_id() == "close" {
event.window().minimize().expect("can't minimize window");
}
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn get_system_tray(show_menu: bool) -> SystemTray {
let hide_app = CustomMenuItem::new("hide_app".to_string(), "Hide App");
let show_app = CustomMenuItem::new("show_app".to_string(), "Show App");
let quit = CustomMenuItem::new("quit".to_string(), "Quit");
let about = CustomMenuItem::new("about".to_string(), "About");
let tray_menu = SystemTrayMenu::new().add_item(hide_app).add_item(show_app);
if show_menu {
let hide_menu = CustomMenuItem::new("hide_menu".to_string(), "Hide Menu");
let show_menu = CustomMenuItem::new("show_menu".to_string(), "Show Menu");
let tray_menu = tray_menu
.add_item(hide_menu)
.add_item(show_menu)
.add_item(quit)
.add_item(about);
SystemTray::new().with_menu(tray_menu)
} else {
let tray_menu = tray_menu.add_item(quit).add_item(about);
SystemTray::new().with_menu(tray_menu)
}
}
#[cfg(any(target_os = "linux", target_os = "windows"))]
pub fn system_tray_handle(app: &tauri::AppHandle, event: SystemTrayEvent) {
if let SystemTrayEvent::MenuItemClick { tray_id: _, id, .. } = event {
match id.as_str() {
"hide_app" => {
app.get_window("pake").unwrap().hide().unwrap();
}
"show_app" => {
app.get_window("pake").unwrap().show().unwrap();
}
"hide_menu" => {
app.get_window("pake")
.unwrap()
.menu_handle()
.hide()
.unwrap();
}
"show_menu" => {
app.get_window("pake")
.unwrap()
.menu_handle()
.show()
.unwrap();
}
"quit" => {
let _res = app.save_window_state(StateFlags::all());
// println!("save windows state result {:?}", _res);
std::process::exit(0);
}
"about" => {
let _about_window = WindowBuilder::new(
app,
"about",
WindowUrl::App(std::path::PathBuf::from("about_pake.html")),
)
.resizable(true)
.title("About")
.inner_size(600.0, 400.0)
.build()
.expect("can't open about!");
}
_ => {}
}
};
}

4
src-tauri/src/app/mod.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod config;
pub mod invoke;
pub mod menu;
pub mod window;

View File

@@ -0,0 +1,48 @@
use crate::app::config::PakeConfig;
use std::path::PathBuf;
use tauri::{App, Window, WindowBuilder, WindowUrl};
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
pub fn get_window(app: &mut App, config: PakeConfig, _data_dir: PathBuf) -> Window {
let window_config = config
.windows
.first()
.expect("At least one window configuration is required");
let user_agent = config.user_agent.get();
let url = match window_config.url_type.as_str() {
"web" => WindowUrl::App(window_config.url.parse().unwrap()),
"local" => WindowUrl::App(PathBuf::from(&window_config.url)),
_ => panic!("url type can only be web or local"),
};
let mut window_builder = WindowBuilder::new(app, "pake", url)
.title("")
.user_agent(user_agent)
.resizable(window_config.resizable)
.fullscreen(window_config.fullscreen)
.inner_size(window_config.width, window_config.height)
.visible(false) // Prevent initial shaking
.initialization_script(include_str!("../inject/style.js"))
.initialization_script(include_str!("../inject/index.js"));
#[cfg(target_os = "macos")]
{
let title_bar_style = if window_config.transparent {
TitleBarStyle::Overlay
} else {
TitleBarStyle::Visible
};
window_builder = window_builder.title_bar_style(title_bar_style)
}
#[cfg(not(target_os = "macos"))]
{
window_builder = window_builder.data_directory(_data_dir);
}
window_builder.build().unwrap()
}

View File

@@ -0,0 +1,170 @@
const shortcuts = {
ArrowUp: () => scrollTo(0, 0),
ArrowDown: () => scrollTo(0, document.body.scrollHeight),
ArrowLeft: () => window.history.back(),
ArrowRight: () => window.history.forward(),
'[': () => window.history.back(),
']': () => window.history.forward(),
r: () => window.location.reload(),
'-': () => zoomOut(),
'=': () => zoomIn(),
'+': () => zoomIn(),
0: () => setZoom('100%'),
};
function setZoom(zoom) {
const html = document.getElementsByTagName('html')[0];
html.style.zoom = zoom;
window.localStorage.setItem('htmlZoom', zoom);
}
function zoomCommon(zoomChange) {
const currentZoom = window.localStorage.getItem('htmlZoom') || '100%';
setZoom(zoomChange(currentZoom));
}
function zoomIn() {
zoomCommon((currentZoom) => `${Math.min(parseInt(currentZoom) + 10, 200)}%`);
}
function zoomOut() {
zoomCommon((currentZoom) => `${Math.max(parseInt(currentZoom) - 10, 30)}%`);
}
function handleShortcut(event) {
if (shortcuts[event.key]) {
event.preventDefault();
shortcuts[event.key]();
}
}
//这里参考 ChatGPT 的代码
const uid = () => window.crypto.getRandomValues(new Uint32Array(1))[0];
function transformCallback(callback = () => {}, once = false) {
const identifier = uid();
const prop = `_${identifier}`;
Object.defineProperty(window, prop, {
value: (result) => {
if (once) {
Reflect.deleteProperty(window, prop);
}
return callback(result);
},
writable: false,
configurable: true,
});
return identifier;
}
async function invoke(cmd, args) {
return new Promise((resolve, reject) => {
if (!window.__TAURI_POST_MESSAGE__)
reject('__TAURI_POST_MESSAGE__ does not exist~');
const callback = transformCallback((e) => {
resolve(e);
Reflect.deleteProperty(window, `_${error}`);
}, true);
const error = transformCallback((e) => {
reject(e);
Reflect.deleteProperty(window, `_${callback}`);
}, true);
window.__TAURI_POST_MESSAGE__({
cmd,
callback,
error,
...args,
});
});
}
document.addEventListener('DOMContentLoaded', () => {
const topDom = document.createElement('div');
topDom.id = 'pack-top-dom';
document.body.appendChild(topDom);
const domEl = document.getElementById('pack-top-dom');
domEl.addEventListener('mousedown', (e) => {
e.preventDefault();
if (e.buttons === 1 && e.detail !== 2) {
invoke('drag_window');
}
});
domEl.addEventListener('touchstart', () => {
invoke('drag_window');
});
domEl.addEventListener('dblclick', () => {
invoke('fullscreen');
});
document.addEventListener('keyup', (event) => {
if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) {
handleShortcut(event);
}
if (/macintosh|mac os x/i.test(navigator.userAgent) && event.metaKey) {
handleShortcut(event);
}
});
document.addEventListener('click', (e) => {
const anchorElement = e.target.closest('a');
if (anchorElement && anchorElement.href) {
const target = anchorElement.target;
anchorElement.target = '_self';
const hrefUrl = new URL(anchorElement.href);
const absoluteUrl = hrefUrl.href;
// 处理外部链接跳转
if (window.location.host !== hrefUrl.host && target === '_blank') {
e.preventDefault();
invoke('open_browser', { url: absoluteUrl });
return;
}
// 处理下载链接让Rust处理
if (/\.[a-zA-Z0-9]+$/i.test(absoluteUrl)) {
e.preventDefault();
// invoke('open_browser', { url: absoluteUrl });
invoke('download_file', {
params: {
url: absoluteUrl,
filename: getFilenameFromUrl(absoluteUrl),
},
});
}
}
});
setDefaultZoom();
});
function setDefaultZoom() {
const htmlZoom = window.localStorage.getItem('htmlZoom');
if (htmlZoom) {
setZoom(htmlZoom);
}
}
function getFilenameFromUrl(url) {
const urlPath = new URL(url).pathname;
const filename = urlPath.substring(urlPath.lastIndexOf('/') + 1);
return filename;
}
function pakeToast(msg) {
const m = document.createElement('div');
m.innerHTML = msg;
m.style.cssText =
'max-width:60%;min-width: 80px;padding:0 12px;height: 32px;color: rgb(255, 255, 255);line-height: 32px;text-align: center;border-radius: 8px;position: fixed; bottom:24px;right: 28px;z-index: 999999;background: rgba(0, 0, 0,.8);font-size: 13px;';
document.body.appendChild(m);
setTimeout(function () {
const d = 0.5;
m.style.transition =
'transform ' + d + 's ease-in, opacity ' + d + 's ease-in';
m.style.opacity = '0';
setTimeout(function () {
document.body.removeChild(m);
}, d * 1000);
}, 3000);
}

View File

@@ -1,39 +1,5 @@
/**
* @typedef {string} KeyboardKey `event.key` 的代号
* <https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key/Key_Values>
* @typedef {() => void} OnKeyDown 使用者按下 [CtrlKey] 或者 [KeyboardKey]时应该执行的行为
* Ctrl键或者Meta () 为首的快捷键清单
* 每个写在这里的 shortcuts 都会运行 {@link Event.preventDefault}.
* @type {Record<KeyboardKey, OnKeyDown>}
*/
const metaKeyShortcuts = {
ArrowUp: () => scrollTo(0, 0),
ArrowDown: () => scrollTo(0, document.body.scrollHeight),
"[": () => window.history.back(),
"]": () => window.history.forward(),
r: () => window.location.reload(),
"-": () => zoomOut(),
"=": () => zoomIn(),
"+": () => zoomIn(),
0: () => zoomCommon(() => "100%"),
};
const ctrlKeyShortcuts = {
ArrowUp: () => scrollTo(0, 0),
ArrowDown: () => scrollTo(0, document.body.scrollHeight),
ArrowLeft: () => window.history.back(),
ArrowRight: () => window.history.forward(),
r: () => window.location.reload(),
"-": () => zoomOut(),
"=": () => zoomIn(),
"+": () => zoomIn(),
0: () => zoomCommon(() => "100%"),
};
window.addEventListener("DOMContentLoaded", (_event) => {
const style = document.createElement("style");
style.innerHTML = `
window.addEventListener('DOMContentLoaded', (_event) => {
const css = `
#page #footer-wrapper,
.drawing-board .toolbar .toolbar-action,
.c-swiper-container,
@@ -49,6 +15,8 @@ window.addEventListener("DOMContentLoaded", (_event) => {
#masthead-ad,
#app > header > div > div.menu,
#root > div > div.fixed.top-0.left-0.w-64.h-screen.p-10.pb-0.flex.flex-col.justify-between > div > div.space-y-4 > a:nth-child(3),
#app > div.layout > div.main-container > div.side-bar > div,
#app > div.layout > div.main-container > div.side-bar > li.divider,
#Rightbar > div:nth-child(6) > div.sidebar_compliance {
display: none !important;
}
@@ -130,7 +98,8 @@ window.addEventListener("DOMContentLoaded", (_event) => {
top: 30px;
}
.geist-page nav.dashboard_nav__PRmJv {
.geist-page nav.dashboard_nav__PRmJv,
#app > div.layout > div.header-container.showSearchBoxOrHeaderFixed > header > a {
padding-top:10px;
}

View File

@@ -1,278 +1,73 @@
// at the top of main.rs - that will prevent the console from showing
#![windows_subsystem = "windows"]
extern crate image;
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
use dirs::download_dir;
use std::path::PathBuf;
use tauri_utils::config::{Config, WindowConfig};
use wry::{
application::{
event::{Event, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoop},
menu::MenuType,
window::{Fullscreen, Window, WindowBuilder},
},
webview::WebViewBuilder,
};
mod app;
mod util;
#[cfg(target_os = "macos")]
use wry::application::{
accelerator::{Accelerator, SysMods},
keyboard::KeyCode,
menu::{MenuBar as Menu, MenuItem, MenuItemAttributes},
platform::macos::WindowBuilderExtMacOS,
};
use app::{invoke, menu, window};
use invoke::{download_file, drag_window, fullscreen, open_browser};
use menu::{get_menu, menu_event_handle};
use tauri_plugin_window_state::{Builder as windowStatePlugin, StateFlags, WindowExt};
use util::{get_data_dir, get_pake_config};
use window::get_window;
#[cfg(target_os = "windows")]
use wry::application::window::Icon;
pub fn run_app() {
let (pake_config, tauri_config) = get_pake_config();
let show_menu = pake_config.show_menu();
let menu = get_menu();
let data_dir = get_data_dir(tauri_config);
#[cfg(any(target_os = "linux", target_os = "windows"))]
use wry::webview::WebContext;
let mut tauri_app = tauri::Builder::default();
enum UserEvent {
DownloadStarted(String, String),
DownloadComplete(Option<PathBuf>, bool),
}
fn main() -> wry::Result<()> {
#[cfg(target_os = "macos")]
let (menu_bar_menu, close_item) = {
let mut menu_bar_menu = Menu::new();
let mut first_menu = Menu::new();
first_menu.add_native_item(MenuItem::Hide);
first_menu.add_native_item(MenuItem::EnterFullScreen);
first_menu.add_native_item(MenuItem::Minimize);
first_menu.add_native_item(MenuItem::Separator);
first_menu.add_native_item(MenuItem::Copy);
first_menu.add_native_item(MenuItem::Cut);
first_menu.add_native_item(MenuItem::Paste);
first_menu.add_native_item(MenuItem::Undo);
first_menu.add_native_item(MenuItem::Redo);
first_menu.add_native_item(MenuItem::SelectAll);
first_menu.add_native_item(MenuItem::Separator);
let close_item = first_menu.add_item(
MenuItemAttributes::new("CloseWindow")
.with_accelerators(&Accelerator::new(SysMods::Cmd, KeyCode::KeyW)),
);
first_menu.add_native_item(MenuItem::Quit);
menu_bar_menu.add_submenu("App", true, first_menu);
(menu_bar_menu, close_item)
};
#[cfg(any(target_os = "linux", target_os = "windows"))]
let (
package_name,
WindowConfig {
url,
width,
height,
resizable,
fullscreen,
..
},
) = {
let (package_name, windows_config) = get_windows_config();
(
package_name
.expect("can't get package name in config file")
.to_lowercase(),
windows_config.unwrap_or_default(),
)
};
#[cfg(target_os = "macos")]
let WindowConfig {
url,
width,
height,
resizable,
transparent,
fullscreen,
..
} = get_windows_config().1.unwrap_or_default();
let event_loop: EventLoop<UserEvent> = EventLoop::with_user_event();
let proxy = event_loop.create_proxy();
let common_window = WindowBuilder::new()
.with_title("")
.with_resizable(resizable)
.with_fullscreen(if fullscreen {
Some(Fullscreen::Borderless(None))
} else {
None
})
.with_inner_size(wry::application::dpi::LogicalSize::new(width, height));
#[cfg(target_os = "windows")]
let window = {
let mut icon_path = format!("png/{}_32.ico", package_name);
// If there is no setting, use the default one.
if !std::path::Path::new(&icon_path).exists() {
icon_path = "png/icon_32.ico".to_string();
}
let icon = load_icon(std::path::Path::new(&icon_path));
common_window
.with_decorations(true)
.with_window_icon(Some(icon))
.build(&event_loop)
.unwrap()
};
#[cfg(target_os = "linux")]
let window = common_window.build(&event_loop).unwrap();
#[cfg(target_os = "macos")]
let window = common_window
.with_fullsize_content_view(true)
.with_titlebar_buttons_hidden(false)
.with_titlebar_transparent(transparent)
.with_title_hidden(true)
.with_menu(menu_bar_menu)
.build(&event_loop)
.unwrap();
// Handling events of JS -> Rust
let handler = move |window: &Window, req: String| {
if req == "drag_window" {
let _ = window.drag_window();
} else if req == "fullscreen" {
let is_maximized = window.is_maximized();
window.set_maximized(!is_maximized);
} else if req.starts_with("open_browser") {
let href = req.replace("open_browser:", "");
webbrowser::open(&href).expect("no browser");
}
};
let download_started = {
let proxy = proxy.clone();
move |uri: String, default_path: &mut PathBuf| {
let path = download_dir()
.unwrap()
.join(default_path.display().to_string())
.as_path()
.to_path_buf();
*default_path = path.clone();
let submitted = proxy
.send_event(UserEvent::DownloadStarted(uri, path.display().to_string()))
.is_ok();
submitted
}
};
let download_completed = {
move |_uri, path, success| {
let _ = proxy.send_event(UserEvent::DownloadComplete(path, success));
}
};
#[cfg(target_os = "macos")]
let webview = {
let user_agent_string = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.1 Safari/605.1.15";
WebViewBuilder::new(window)?
.with_user_agent(user_agent_string)
.with_url(&url.to_string())?
.with_devtools(cfg!(feature = "devtools"))
.with_initialization_script(include_str!("pake.js"))
.with_ipc_handler(handler)
.with_back_forward_navigation_gestures(true)
.with_download_started_handler(download_started)
.with_download_completed_handler(download_completed)
.build()?
};
#[cfg(any(target_os = "linux", target_os = "windows"))]
let webview = {
let home_dir = match home::home_dir() {
Some(path1) => path1,
None => panic!("Error, can't found you home dir!!"),
};
#[cfg(target_os = "windows")]
let data_dir = home_dir.join("AppData").join("Roaming").join(package_name);
#[cfg(target_os = "linux")]
let data_dir = home_dir.join(".config").join(package_name);
if !data_dir.exists() {
std::fs::create_dir(&data_dir)
.unwrap_or_else(|_| panic!("can't create dir {}", data_dir.display()));
}
let mut web_content = WebContext::new(Some(data_dir));
#[cfg(target_os = "windows")]
let user_agent_string = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
#[cfg(target_os = "linux")]
let user_agent_string = "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36";
WebViewBuilder::new(window)?
.with_user_agent(user_agent_string)
.with_url(&url.to_string())?
.with_devtools(cfg!(feature = "devtools"))
.with_initialization_script(include_str!("pake.js"))
.with_ipc_handler(handler)
.with_web_context(&mut web_content)
.with_download_started_handler(download_started)
.with_download_completed_handler(download_completed)
.build()?
};
#[cfg(feature = "devtools")]
{
webview.open_devtools();
if show_menu {
tauri_app = tauri_app.menu(menu).on_menu_event(menu_event_handle);
}
event_loop.run(move |event, _, control_flow| {
*control_flow = ControlFlow::Wait;
#[cfg(not(target_os = "macos"))]
{
use menu::{get_system_tray, system_tray_handle};
match event {
Event::NewEvents(StartCause::Init) => println!("Wry has started!"),
Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} => *control_flow = ControlFlow::Exit,
Event::MenuEvent {
menu_id,
origin: MenuType::MenuBar,
..
} => {
#[cfg(target_os = "macos")]
if menu_id == close_item.clone().id() {
webview.window().set_minimized(true);
}
println!("Clicked on {menu_id:?}");
}
Event::UserEvent(UserEvent::DownloadStarted(uri, temp_dir)) => {
println!("Download: {uri}");
println!("Will write to: {temp_dir:?}");
}
Event::UserEvent(UserEvent::DownloadComplete(_, success)) => {
println!("Succeeded: {success}");
if success {
let _ = webview
.evaluate_script("window.pakeToast('Downloaded to download folder~')");
} else {
println!("No output path")
}
}
_ => (),
let show_system_tray = pake_config.show_system_tray();
let system_tray = get_system_tray(show_menu);
if show_system_tray {
tauri_app = tauri_app
.system_tray(system_tray)
.on_system_tray_event(system_tray_handle);
}
});
}
tauri_app
.plugin(windowStatePlugin::default().build())
.invoke_handler(tauri::generate_handler![
drag_window,
fullscreen,
open_browser,
download_file
])
.setup(|app| {
let _window = get_window(app, pake_config, data_dir);
// Prevent initial shaking
_window.restore_state(StateFlags::all()).unwrap();
_window.show().unwrap();
#[cfg(feature = "devtools")]
{
_window.open_devtools();
}
Ok(())
})
.on_window_event(|event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event.event() {
event.window().minimize().unwrap();
api.prevent_close();
}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
fn get_windows_config() -> (Option<String>, Option<WindowConfig>) {
let config_file = include_str!("../tauri.conf.json");
let config: Config = serde_json::from_str(config_file).expect("failed to parse windows config");
(
config.package.product_name.clone(),
config.tauri.windows.first().cloned(),
)
}
#[cfg(target_os = "windows")]
fn load_icon(path: &std::path::Path) -> Icon {
let (icon_rgba, icon_width, icon_height) = {
// alternatively, you can embed the icon in the binary through `include_bytes!` macro and use `image::load_from_memory`
let image = image::open(path)
.expect("Failed to open icon path")
.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
(rgba, width, height)
};
Icon::from_rgba(icon_rgba, icon_width, icon_height).expect("Failed to open icon")
fn main() {
run_app()
}

60
src-tauri/src/util.rs Normal file
View File

@@ -0,0 +1,60 @@
use crate::app::config::PakeConfig;
use dirs::config_dir;
use libc::getenv;
use std::ffi::CStr;
use std::path::PathBuf;
use tauri::{Config, Window};
pub fn get_pake_config() -> (PakeConfig, Config) {
let pake_config: PakeConfig =
serde_json::from_str(include_str!("../pake.json")).expect("Failed to parse pake config");
let tauri_config: Config = serde_json::from_str(include_str!("../tauri.conf.json"))
.expect("Failed to parse tauri config");
(pake_config, tauri_config)
}
pub fn get_data_dir(_tauri_config: Config) -> PathBuf {
{
let package_name = _tauri_config.package.product_name.unwrap();
let data_dir = config_dir()
.expect("Failed to get data dirname")
.join(package_name);
if !data_dir.exists() {
std::fs::create_dir(&data_dir)
.unwrap_or_else(|_| panic!("Can't create dir {}", data_dir.display()));
}
data_dir
}
}
pub fn show_toast(window: &Window, message: &str) {
let script = format!(r#"pakeToast("{}");"#, message);
window.eval(&script).unwrap();
}
pub fn get_download_message() -> String {
let lang_env_var = unsafe { CStr::from_ptr(getenv(b"LANG\0".as_ptr() as *const i8)) };
let lang_str = lang_env_var.to_string_lossy().to_lowercase();
if lang_str.starts_with("zh") {
"下载成功,已保存到下载目录~".to_string()
} else {
"Download successful, saved to download directory~".to_string()
}
}
// Check if the file exists, if it exists, add a number to file name
pub fn check_file_or_append(file_path: &str) -> String {
let mut new_path = PathBuf::from(file_path);
let mut counter = 1;
while new_path.exists() {
let file_stem = new_path.file_stem().unwrap().to_string_lossy().to_string();
let extension = new_path.extension().unwrap().to_string_lossy().to_string();
let parent_dir = new_path.parent().unwrap();
new_path = parent_dir.join(format!("{}-{}.{}", file_stem, counter, extension));
counter += 1;
}
new_path.to_string_lossy().into_owned()
}

View File

@@ -1,31 +1,28 @@
{
"package": {
"productName": "WeRead",
"version": "1.0.0"
},
"tauri": {
"windows": [
{
"url": "https://weread.qq.com/",
"transparent": true,
"fullscreen": false,
"width": 1200,
"height": 780,
"resizable": true
}
],
"security": {
"csp": null
},
"updater": {
"active": false
}
},
"build": {
"devPath": "../dist",
"distDir": "../dist",
"beforeBuildCommand": "",
"beforeDevCommand": ""
}
"package": {
"productName": "WeRead",
"version": "1.0.0"
},
"tauri": {
"security": {
"csp": null
},
"updater": {
"active": false
},
"systemTray": {
"iconPath": "png/weread_512.png",
"iconAsTemplate": true
},
"allowlist": {
"all": true
}
},
"build": {
"withGlobalTauri": true,
"devPath": "../dist",
"distDir": "../dist",
"beforeBuildCommand": "",
"beforeDevCommand": ""
}
}

View File

@@ -1,32 +1,33 @@
{
"tauri": {
"bundle": {
"icon": ["png/weread_512.png"],
"identifier": "com.tw93.weread",
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": [
"libwebkit2gtk-4.0-dev",
"build-essential",
"curl",
"wget",
"libssl-dev",
"libgtk-3-dev",
"libayatana-appindicator3-dev",
"librsvg2-dev",
"gnome-video-effects",
"gnome-video-effects-extra"
],
"files": {"/usr/share/applications/com-tw93-weread.desktop": "assets/com-tw93-weread.desktop"}
},
"externalBin": [],
"longDescription": "",
"resources": [],
"shortDescription": "",
"targets": ["deb", "appimage"]
}
}
"tauri": {
"bundle": {
"icon": ["png/weread_512.png"],
"identifier": "com.tw93.weread",
"active": true,
"category": "DeveloperTool",
"copyright": "",
"deb": {
"depends": [
"libwebkit2gtk-4.0-dev",
"build-essential",
"curl",
"wget",
"libssl-dev",
"libgtk-3-dev",
"libayatana-appindicator3-dev",
"librsvg2-dev",
"gnome-video-effects",
"gnome-video-effects-extra"
],
"files": {
"/usr/share/applications/com-tw93-weread.desktop": "assets/com-tw93-weread.desktop"
}
},
"externalBin": [],
"longDescription": "",
"resources": [],
"shortDescription": "",
"targets": ["deb", "appimage"]
}
}
}