diff --git a/.gitattributes b/.gitattributes index cdd4590..ec23583 100644 --- a/.gitattributes +++ b/.gitattributes @@ -8,3 +8,4 @@ script/* linguist-vendored /icns2png.py linguist-vendored /rollup.config.js linguist-vendored src-tauri/src/inject/* linguist-vendored +src-tauri/src/.pake/* linguist-vendored \ No newline at end of file diff --git a/.github/workflows/rust-code-quality-check.yml b/.github/workflows/rust-code-quality-check.yml index 0236cf7..7af1d38 100644 --- a/.github/workflows/rust-code-quality-check.yml +++ b/.github/workflows/rust-code-quality-check.yml @@ -43,7 +43,7 @@ jobs: 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.0 - name: Run unit & integration tests with nextest - run: cargo hack --feature-powerset nextest run + run: cargo hack --feature-powerset --exclude-features cli-build nextest run # - name: Run documentation tests with cargo test # run: cargo hack --feature-powerset test --doc @@ -70,7 +70,7 @@ jobs: 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.0 - name: Run all-features code quality checks - run: cargo hack --feature-powerset --no-dev-deps clippy + run: cargo hack --feature-powerset --exclude-features cli-build --no-dev-deps clippy - name: Run normal code quality check run: cargo clippy diff --git a/.gitignore b/.gitignore index 43ac11e..f8267c8 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ dist src-tauri/.cargo/config src-tauri/.cargo/ .next +src-tauri/.pake/ \ No newline at end of file diff --git a/bin/builders/BaseBuilder.ts b/bin/builders/BaseBuilder.ts index d480be9..608e4b7 100644 --- a/bin/builders/BaseBuilder.ts +++ b/bin/builders/BaseBuilder.ts @@ -71,6 +71,10 @@ export default abstract class BaseBuilder { await this.buildAndCopy(url, this.options.targets); } + async start(url: string) { + await mergeConfig(url, this.options, tauriConfig); + } + async buildAndCopy(url: string, target: string) { const { name } = this.options; await mergeConfig(url, this.options, tauriConfig); @@ -98,7 +102,8 @@ export default abstract class BaseBuilder { abstract getFileName(): string; protected getBuildCommand(): string { - return 'npm run build'; + // the debug option should support `--debug` and `--release` + return this.options.debug ? 'npm run build:debug' : 'npm run build'; } protected getBasePath(): string { diff --git a/bin/cli.ts b/bin/cli.ts index 7c18ad7..5e4e856 100644 --- a/bin/cli.ts +++ b/bin/cli.ts @@ -38,6 +38,8 @@ program .option('--iter-copy-file', 'Copy files when URL is a local file', DEFAULT.iterCopyFile) .option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT.multiArch) .option('--targets ', 'Only for Linux, option "deb" or "appimage"', DEFAULT.targets) + .option('--inject [injects...]', 'Inject .js or .css for this app', DEFAULT.inject) + .option('--safe-domain [domains...]', 'Please enter the security domains that need to be configured', DEFAULT.safeDomain) .option('--debug', 'Debug mode', DEFAULT.debug) .version(packageJson.version, '-v, --version', 'Output the current version') .action(async (url: string, options: PakeCliOptions) => { diff --git a/bin/defaults.ts b/bin/defaults.ts index 108f001..23464d9 100644 --- a/bin/defaults.ts +++ b/bin/defaults.ts @@ -15,4 +15,13 @@ export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = { iterCopyFile: false, systemTrayIcon: '', debug: false, + inject: [], + safeDomain: [], }; + +// just for cli development +export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & {url: string} = { + ...DEFAULT_PAKE_OPTIONS, + url: 'https://weread.qq.com', + name: 'Weread', +} \ No newline at end of file diff --git a/bin/dev.ts b/bin/dev.ts new file mode 100644 index 0000000..f4b67cd --- /dev/null +++ b/bin/dev.ts @@ -0,0 +1,17 @@ +import log from 'loglevel'; +import {DEFAULT_DEV_PAKE_OPTIONS} from './defaults'; +import handleInputOptions from './options/index'; +import BuilderProvider from './builders/BuilderProvider'; + +async function startBuild() { + log.setDefaultLevel('debug'); + + const appOptions = await handleInputOptions(DEFAULT_DEV_PAKE_OPTIONS, DEFAULT_DEV_PAKE_OPTIONS.url); + log.debug('PakeAppOptions', appOptions); + + const builder = BuilderProvider.create(appOptions); + await builder.prepare(); + await builder.start(DEFAULT_DEV_PAKE_OPTIONS.url); +} + +startBuild(); \ No newline at end of file diff --git a/bin/helpers/merge.ts b/bin/helpers/merge.ts index d4095a3..c7438aa 100644 --- a/bin/helpers/merge.ts +++ b/bin/helpers/merge.ts @@ -2,8 +2,10 @@ import path from 'path'; import fsExtra from 'fs-extra'; import { npmDirectory } from '@/utils/dir'; +import combineFiles from '@/utils/combine'; import logger from '@/options/logger'; import { PakeAppOptions, PlatformMap } from '@/types'; +import { tauriConfigDirectory } from '../utils/dir'; export async function mergeConfig(url: string, options: PakeAppOptions, tauriConf: any) { const { @@ -19,6 +21,8 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon identifier, name, resizable = true, + inject, + safeDomain, } = options; const { platform } = process; @@ -57,9 +61,7 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon const filesToCopyBack = ['cli.js', 'about_pake.html']; await Promise.all( - filesToCopyBack.map(file => - fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file)), - ), + filesToCopyBack.map(file => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file))), ); } @@ -68,7 +70,24 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon } else { tauriConf.pake.windows[0].url_type = 'web'; // Set the secure domain for calling window.__TAURI__ to the application domain that has been set. - tauriConf.tauri.security.dangerousRemoteDomainIpcAccess[0].domain = new URL(url).hostname; + tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [ + { + domain: new URL(url).hostname, + windows: ['pake'], + enableTauriAPI: true, + }, + ]; + } + + if (safeDomain.length > 0) { + tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [ + ...tauriConf.tauri.security.dangerousRemoteDomainIpcAccess, + ...safeDomain.map(domain => ({ + domain, + windows: ['pake'], + enableTauriAPI: true, + })), + ]; } const platformMap: PlatformMap = { @@ -90,12 +109,9 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon delete tauriConf.tauri.bundle.deb.files; const validTargets = ['all', 'deb', 'appimage']; if (validTargets.includes(options.targets)) { - tauriConf.tauri.bundle.targets = - options.targets === 'all' ? ['deb', 'appimage'] : [options.targets]; + tauriConf.tauri.bundle.targets = options.targets === 'all' ? ['deb', 'appimage'] : [options.targets]; } else { - logger.warn( - `✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`, - ); + logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`); } } @@ -154,10 +170,7 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon // 需要判断图标格式,默认只支持ico和png两种 let iconExt = path.extname(systemTrayIcon).toLowerCase(); if (iconExt == '.png' || iconExt == '.ico') { - const trayIcoPath = path.join( - npmDirectory, - `src-tauri/png/${name.toLowerCase()}${iconExt}`, - ); + const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`); trayIconPath = `png/${name.toLowerCase()}${iconExt}`; await fsExtra.copy(systemTrayIcon, trayIcoPath); } else { @@ -171,24 +184,40 @@ export async function mergeConfig(url: string, options: PakeAppOptions, tauriCon } tauriConf.tauri.systemTray.iconPath = trayIconPath; + const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`); + // inject js or css files + if (inject?.length > 0) { + if (!inject.every(item => item.endsWith('.css') || item.endsWith('.js'))) { + logger.error('The injected file must be in either CSS or JS format.'); + return; + } + const files = inject.map(filepath => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath)); + tauriConf.pake.inject = files; + await combineFiles(files, injectFilePath); + } else { + tauriConf.pake.inject = []; + await fsExtra.writeFile(injectFilePath, ''); + } // Save config file. const platformConfigPaths: PlatformMap = { - win32: 'src-tauri/tauri.windows.conf.json', - darwin: 'src-tauri/tauri.macos.conf.json', - linux: 'src-tauri/tauri.linux.conf.json', + win32: 'tauri.windows.conf.json', + darwin: 'tauri.macos.conf.json', + linux: 'tauri.linux.conf.json', }; - const configPath = path.join(npmDirectory, platformConfigPaths[platform]); + const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]); const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } }; - await fsExtra.writeJson(configPath, bundleConf, { spaces: 4 }); - - const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json'); - await fsExtra.writeJson(pakeConfigPath, tauriConf.pake, { spaces: 4 }); + await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 }); + const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json'); + await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 }); 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 fsExtra.writeJson(configJsonPath, tauriConf2, { spaces: 4 }); + if (process.env.NODE_ENV === 'development') { + tauriConf2.tauri.bundle = bundleConf.tauri.bundle; + } + const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json'); + await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 }); } diff --git a/bin/types.ts b/bin/types.ts index 836d9bb..db15cf8 100644 --- a/bin/types.ts +++ b/bin/types.ts @@ -47,6 +47,12 @@ export interface PakeCliOptions { // Debug mode, outputs more logs debug: boolean; + + /** 需要注入页面的外部脚本 */ + inject: string[]; + + /* the domain that can use ipc or tauri javascript sdk */ + safeDomain: string[]; } export interface PakeAppOptions extends PakeCliOptions { diff --git a/bin/utils/combine.ts b/bin/utils/combine.ts new file mode 100644 index 0000000..0fc1939 --- /dev/null +++ b/bin/utils/combine.ts @@ -0,0 +1,14 @@ +import fs from 'fs'; + +export default async function combineFiles(files: string[], output: string) { + const contents = files.map(file => { + const fileContent = fs.readFileSync(file); + if (file.endsWith('.css')) { + return "window.addEventListener('DOMContentLoaded', (_event) => { const css = `" + fileContent + "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });"; + } + + return "window.addEventListener('DOMContentLoaded', (_event) => { " + fileContent + " });"; + }); + fs.writeFileSync(output, contents.join('\n')); + return files; +} \ No newline at end of file diff --git a/bin/utils/dir.ts b/bin/utils/dir.ts index cb5a697..8e19caf 100644 --- a/bin/utils/dir.ts +++ b/bin/utils/dir.ts @@ -6,3 +6,5 @@ const currentModulePath = fileURLToPath(import.meta.url); // Resolve the parent directory of the current module export const npmDirectory = path.join(path.dirname(currentModulePath), '..'); + +export const tauriConfigDirectory = process.env.NODE_ENV === 'development' ? path.join(npmDirectory, 'src-tauri', '.pake') : path.join(npmDirectory, 'src-tauri'); \ No newline at end of file diff --git a/dist/.gitkeep b/dist/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/dist/about_pake.html b/dist/about_pake.html deleted file mode 100644 index 83cb1d5..0000000 --- a/dist/about_pake.html +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - Document - - -
Welcome from Pake!
-

version: 1.0.9

- Project link
- Discussions
- Issues
-

LICENSE: MIT

- - \ No newline at end of file diff --git a/dist/cli.js b/dist/cli.js index ec36287..db315f4 100644 --- a/dist/cli.js +++ b/dist/cli.js @@ -1,307 +1,307 @@ -import chalk from 'chalk'; -import { InvalidArgumentError, program } from 'commander'; -import log from 'loglevel'; -import path from 'path'; -import fsExtra from 'fs-extra'; -import prompts from 'prompts'; -import shelljs from 'shelljs'; -import crypto from 'crypto'; -import ora from 'ora'; -import { fileURLToPath } from 'url'; -import dns from 'dns'; -import http from 'http'; -import { promisify } from 'util'; -import updateNotifier from 'update-notifier'; -import axios from 'axios'; -import { dir } from 'tmp-promise'; -import { fileTypeFromBuffer } from 'file-type'; -import psl from 'psl'; -import isUrl from 'is-url'; -import fs from 'fs'; - -var name = "pake-cli"; -var version = "2.2.5"; -var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 很简单的用 Rust 打包网页生成很小的桌面 App。"; -var engines = { - node: ">=16.0.0" -}; -var bin = { - pake: "./cli.js" -}; -var repository = { - type: "git", - url: "https://github.com/tw93/pake.git" -}; -var author = { - name: "Tw93", - email: "tw93@qq.com" -}; -var keywords = [ - "pake", - "pake-cli", - "rust", - "tauri", - "no-electron", - "productivity" -]; -var files = [ - "dist", - "src-tauri", - "cli.js" -]; -var scripts = { - start: "npm run dev", - dev: "npm run tauri dev", - build: "npm run tauri build --release", - "build:mac": "npm run tauri build -- --target universal-apple-darwin", - "build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh", - "build:all-windows": "pwsh ./script/build.ps1", - analyze: "cd src-tauri && cargo bloat --release --crates", - tauri: "tauri", - cli: "rollup -c rollup.config.js --watch", - "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", - prepublishOnly: "npm run cli:build" -}; -var type = "module"; -var exports = "./dist/pake.js"; -var license = "MIT"; -var dependencies = { - "@tauri-apps/api": "^1.4.0", - "@tauri-apps/cli": "^1.4.0", - axios: "^1.1.3", - chalk: "^5.1.2", - commander: "^11.0.0", - "file-type": "^18.0.0", - "fs-extra": "^11.1.0", - "is-url": "^1.2.4", - loglevel: "^1.8.1", - ora: "^6.1.2", - prompts: "^2.4.2", - psl: "^1.9.0", - shelljs: "^0.8.5", - "tmp-promise": "^3.0.3", - "update-notifier": "^6.0.2" -}; -var devDependencies = { - "@rollup/plugin-alias": "^4.0.2", - "@rollup/plugin-commonjs": "^23.0.2", - "@rollup/plugin-json": "^5.0.2", - "@rollup/plugin-terser": "^0.1.0", - "@types/fs-extra": "^9.0.13", - "@types/is-url": "^1.2.30", - "@types/page-icon": "^0.3.4", - "@types/prompts": "^2.4.1", - "@types/psl": "^1.1.0", - "@types/shelljs": "^0.8.11", - "@types/tmp": "^0.2.3", - "@types/update-notifier": "^6.0.1", - "app-root-path": "^3.1.0", - "cross-env": "^7.0.3", - rollup: "^3.3.0", - "rollup-plugin-typescript2": "^0.34.1", - tslib: "^2.4.1", - typescript: "^4.9.3" -}; -var packageJson = { - name: name, - version: version, - description: description, - engines: engines, - bin: bin, - repository: repository, - author: author, - keywords: keywords, - files: files, - scripts: scripts, - type: type, - exports: exports, - license: license, - dependencies: dependencies, - devDependencies: devDependencies -}; - -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$3 = { - security: { - csp: null, - dangerousRemoteDomainIpcAccess: [ - { - domain: "weread.qq.com", - windows: [ - "pake" - ], - enableTauriAPI: true - } - ] - }, - updater: { - active: false - }, - systemTray: { - iconPath: "png/weread_512.png", - iconAsTemplate: true - }, - allowlist: { - all: true, - fs: { - all: true, - scope: [ - "$DOWNLOAD/*" - ] - } - } -}; -var build = { - withGlobalTauri: true, - devPath: "../dist", - distDir: "../dist", - beforeBuildCommand: "", - beforeDevCommand: "" -}; -var CommonConf = { - "package": { - productName: "WeRead", - version: "1.0.0" -}, - tauri: tauri$3, - build: build -}; - -var tauri$2 = { - bundle: { - icon: [ - "png/weread_256.ico", - "png/weread_32.ico" - ], - identifier: "com.pake.weread", - active: true, - category: "DeveloperTool", - copyright: "", - externalBin: [ - ], - longDescription: "", - resources: [ - "png/weread_32.ico" - ], - shortDescription: "", - targets: [ - "msi" - ], - windows: { - certificateThumbprint: null, - digestAlgorithm: "sha256", - timestampUrl: "", - wix: { - language: [ - "en-US" - ], - template: "assets/main.wxs" - } - } - } -}; -var WinConf = { - tauri: tauri$2 -}; - -var tauri$1 = { - bundle: { - icon: [ - "icons/weread.icns" - ], - identifier: "com.pake.weread", - active: true, - category: "DeveloperTool", - copyright: "", - externalBin: [ - ], - longDescription: "", - macOS: { - entitlements: null, - exceptionDomain: "", - frameworks: [ - ], - providerShortName: null, - signingIdentity: null - }, - resources: [ - ], - shortDescription: "", - targets: [ - "dmg" - ] - } -}; -var MacConf = { - tauri: tauri$1 -}; - -var tauri = { - bundle: { - icon: [ - "png/weread_512.png" - ], - identifier: "com.pake.weread", - active: true, - category: "DeveloperTool", - copyright: "", - deb: { - depends: [ - "curl", - "wget" - ], - files: { - "/usr/share/applications/com-pake-weread.desktop": "assets/com-pake-weread.desktop" - } - }, - externalBin: [ - ], - longDescription: "", - resources: [ - ], - shortDescription: "", - targets: [ - "deb", - "appimage" - ] - } -}; -var LinuxConf = { - tauri: tauri -}; - +import chalk from 'chalk'; +import { InvalidArgumentError, program } from 'commander'; +import log from 'loglevel'; +import path from 'path'; +import fsExtra from 'fs-extra'; +import prompts from 'prompts'; +import shelljs from 'shelljs'; +import crypto from 'crypto'; +import ora from 'ora'; +import { fileURLToPath } from 'url'; +import dns from 'dns'; +import http from 'http'; +import { promisify } from 'util'; +import updateNotifier from 'update-notifier'; +import axios from 'axios'; +import { dir } from 'tmp-promise'; +import { fileTypeFromBuffer } from 'file-type'; +import psl from 'psl'; +import isUrl from 'is-url'; +import fs from 'fs'; + +var name = "pake-cli"; +var version = "2.2.5"; +var description = "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 很简单的用 Rust 打包网页生成很小的桌面 App。"; +var engines = { + node: ">=16.0.0" +}; +var bin = { + pake: "./cli.js" +}; +var repository = { + type: "git", + url: "https://github.com/tw93/pake.git" +}; +var author = { + name: "Tw93", + email: "tw93@qq.com" +}; +var keywords = [ + "pake", + "pake-cli", + "rust", + "tauri", + "no-electron", + "productivity" +]; +var files = [ + "dist", + "src-tauri", + "cli.js" +]; +var scripts = { + start: "npm run dev", + dev: "npm run tauri dev", + build: "npm run tauri build --release", + "build:mac": "npm run tauri build -- --target universal-apple-darwin", + "build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh", + "build:all-windows": "pwsh ./script/build.ps1", + analyze: "cd src-tauri && cargo bloat --release --crates", + tauri: "tauri", + cli: "rollup -c rollup.config.js --watch", + "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", + prepublishOnly: "npm run cli:build" +}; +var type = "module"; +var exports = "./dist/pake.js"; +var license = "MIT"; +var dependencies = { + "@tauri-apps/api": "^1.4.0", + "@tauri-apps/cli": "^1.4.0", + axios: "^1.1.3", + chalk: "^5.1.2", + commander: "^11.0.0", + "file-type": "^18.0.0", + "fs-extra": "^11.1.0", + "is-url": "^1.2.4", + loglevel: "^1.8.1", + ora: "^6.1.2", + prompts: "^2.4.2", + psl: "^1.9.0", + shelljs: "^0.8.5", + "tmp-promise": "^3.0.3", + "update-notifier": "^6.0.2" +}; +var devDependencies = { + "@rollup/plugin-alias": "^4.0.2", + "@rollup/plugin-commonjs": "^23.0.2", + "@rollup/plugin-json": "^5.0.2", + "@rollup/plugin-terser": "^0.1.0", + "@types/fs-extra": "^9.0.13", + "@types/is-url": "^1.2.30", + "@types/page-icon": "^0.3.4", + "@types/prompts": "^2.4.1", + "@types/psl": "^1.1.0", + "@types/shelljs": "^0.8.11", + "@types/tmp": "^0.2.3", + "@types/update-notifier": "^6.0.1", + "app-root-path": "^3.1.0", + "cross-env": "^7.0.3", + rollup: "^3.3.0", + "rollup-plugin-typescript2": "^0.34.1", + tslib: "^2.4.1", + typescript: "^4.9.3" +}; +var packageJson = { + name: name, + version: version, + description: description, + engines: engines, + bin: bin, + repository: repository, + author: author, + keywords: keywords, + files: files, + scripts: scripts, + type: type, + exports: exports, + license: license, + dependencies: dependencies, + devDependencies: devDependencies +}; + +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$3 = { + security: { + csp: null, + dangerousRemoteDomainIpcAccess: [ + { + domain: "weread.qq.com", + windows: [ + "pake" + ], + enableTauriAPI: true + } + ] + }, + updater: { + active: false + }, + systemTray: { + iconPath: "png/weread_512.png", + iconAsTemplate: true + }, + allowlist: { + all: true, + fs: { + all: true, + scope: [ + "$DOWNLOAD/*" + ] + } + } +}; +var build = { + withGlobalTauri: true, + devPath: "../dist", + distDir: "../dist", + beforeBuildCommand: "", + beforeDevCommand: "" +}; +var CommonConf = { + "package": { + productName: "WeRead", + version: "1.0.0" +}, + tauri: tauri$3, + build: build +}; + +var tauri$2 = { + bundle: { + icon: [ + "png/weread_256.ico", + "png/weread_32.ico" + ], + identifier: "com.pake.weread", + active: true, + category: "DeveloperTool", + copyright: "", + externalBin: [ + ], + longDescription: "", + resources: [ + "png/weread_32.ico" + ], + shortDescription: "", + targets: [ + "msi" + ], + windows: { + certificateThumbprint: null, + digestAlgorithm: "sha256", + timestampUrl: "", + wix: { + language: [ + "en-US" + ], + template: "assets/main.wxs" + } + } + } +}; +var WinConf = { + tauri: tauri$2 +}; + +var tauri$1 = { + bundle: { + icon: [ + "icons/weread.icns" + ], + identifier: "com.pake.weread", + active: true, + category: "DeveloperTool", + copyright: "", + externalBin: [ + ], + longDescription: "", + macOS: { + entitlements: null, + exceptionDomain: "", + frameworks: [ + ], + providerShortName: null, + signingIdentity: null + }, + resources: [ + ], + shortDescription: "", + targets: [ + "dmg" + ] + } +}; +var MacConf = { + tauri: tauri$1 +}; + +var tauri = { + bundle: { + icon: [ + "png/weread_512.png" + ], + identifier: "com.pake.weread", + active: true, + category: "DeveloperTool", + copyright: "", + deb: { + depends: [ + "curl", + "wget" + ], + files: { + "/usr/share/applications/com-pake-weread.desktop": "assets/com-pake-weread.desktop" + } + }, + externalBin: [ + ], + longDescription: "", + resources: [ + ], + shortDescription: "", + targets: [ + "deb", + "appimage" + ] + } +}; +var LinuxConf = { + tauri: tauri +}; + const platformConfigs = { win32: WinConf, darwin: MacConf, @@ -318,8 +318,8 @@ let tauriConfig = { package: CommonConf.package, build: CommonConf.build, pake: pakeConf, -}; - +}; + // Generates an identifier based on the given URL. function getIdentifier(url) { const postFixHash = crypto.createHash('md5').update(url).digest('hex').substring(0, 6); @@ -347,18 +347,19 @@ function getSpinner(text) { spinner: loadingType, color: 'cyan', }).start(); -} - +} + const { platform: platform$1 } = process; const IS_MAC = platform$1 === 'darwin'; const IS_WIN = platform$1 === 'win32'; -const IS_LINUX = platform$1 === 'linux'; - +const IS_LINUX = platform$1 === 'linux'; + // Convert the current module URL to a file path const currentModulePath = fileURLToPath(import.meta.url); // Resolve the parent directory of the current module -const npmDirectory = path.join(path.dirname(currentModulePath), '..'); - +const npmDirectory = path.join(path.dirname(currentModulePath), '..'); +const tauriConfigDirectory = path.join(npmDirectory, 'src-tauri'); + function shellExec(command) { return new Promise((resolve, reject) => { shelljs.exec(command, { async: true, silent: false, cwd: npmDirectory }, code => { @@ -370,8 +371,8 @@ function shellExec(command) { } }); }); -} - +} + const logger = { info(...msg) { log.info(...msg.map(m => chalk.white(m))); @@ -388,8 +389,8 @@ const logger = { success(...msg) { log.info(...msg.map(m => chalk.green(m))); }, -}; - +}; + const resolve = promisify(dns.resolve); const ping = async (host) => { const lookup = promisify(dns.lookup); @@ -433,8 +434,8 @@ async function isChinaIP(ip, domain) { logger.debug(`ping ${domain} failed!`); return true; } -} - +} + async function installRust() { const isActions = process.env.GITHUB_ACTIONS; const isInChina = await isChinaDomain('sh.rustup.rs'); @@ -455,10 +456,22 @@ async function installRust() { } function checkRustInstalled() { return shelljs.exec('rustc --version', { silent: true }).code === 0; -} - +} + +async function combineFiles(files, output) { + const contents = files.map(file => { + const fileContent = fs.readFileSync(file); + if (file.endsWith('.css')) { + return "window.addEventListener('DOMContentLoaded', (_event) => { const css = `" + fileContent + "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });"; + } + return "window.addEventListener('DOMContentLoaded', (_event) => { " + fileContent + " });"; + }); + fs.writeFileSync(output, contents.join('\n')); + return files; +} + async function mergeConfig(url, options, tauriConf) { - const { width, height, fullscreen, transparent, userAgent, showMenu, showSystemTray, systemTrayIcon, iterCopyFile, identifier, name, resizable = true, } = options; + const { width, height, fullscreen, transparent, userAgent, showMenu, showSystemTray, systemTrayIcon, iterCopyFile, identifier, name, resizable = true, inject, safeDomain, } = options; const { platform } = process; // Set Windows parameters. const tauriConfWindowOptions = { @@ -496,7 +509,23 @@ async function mergeConfig(url, options, tauriConf) { else { tauriConf.pake.windows[0].url_type = 'web'; // Set the secure domain for calling window.__TAURI__ to the application domain that has been set. - tauriConf.tauri.security.dangerousRemoteDomainIpcAccess[0].domain = new URL(url).hostname; + tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [ + { + domain: new URL(url).hostname, + windows: ['pake'], + enableTauriAPI: true, + }, + ]; + } + if (safeDomain.length > 0) { + tauriConf.tauri.security.dangerousRemoteDomainIpcAccess = [ + ...tauriConf.tauri.security.dangerousRemoteDomainIpcAccess, + ...safeDomain.map(domain => ({ + domain, + windows: ['pake'], + enableTauriAPI: true, + })), + ]; } const platformMap = { win32: 'windows', @@ -514,8 +543,7 @@ async function mergeConfig(url, options, tauriConf) { delete tauriConf.tauri.bundle.deb.files; const validTargets = ['all', 'deb', 'appimage']; if (validTargets.includes(options.targets)) { - tauriConf.tauri.bundle.targets = - options.targets === 'all' ? ['deb', 'appimage'] : [options.targets]; + tauriConf.tauri.bundle.targets = options.targets === 'all' ? ['deb', 'appimage'] : [options.targets]; } else { logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`); @@ -591,24 +619,39 @@ async function mergeConfig(url, options, tauriConf) { } } tauriConf.tauri.systemTray.iconPath = trayIconPath; + const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/_INJECT_.js`); + // inject js or css files + if (inject?.length > 0) { + if (!inject.every(item => item.endsWith('.css') || item.endsWith('.js'))) { + logger.error('The injected file must be in either CSS or JS format.'); + return; + } + const files = inject.map(filepath => path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath)); + tauriConf.pake.inject = files; + await combineFiles(files, injectFilePath); + } + else { + tauriConf.pake.inject = []; + await fsExtra.writeFile(injectFilePath, ''); + } // Save config file. const platformConfigPaths = { - win32: 'src-tauri/tauri.windows.conf.json', - darwin: 'src-tauri/tauri.macos.conf.json', - linux: 'src-tauri/tauri.linux.conf.json', + win32: 'tauri.windows.conf.json', + darwin: 'tauri.macos.conf.json', + linux: 'tauri.linux.conf.json', }; - const configPath = path.join(npmDirectory, platformConfigPaths[platform]); + const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]); const bundleConf = { tauri: { bundle: tauriConf.tauri.bundle } }; - await fsExtra.writeJson(configPath, bundleConf, { spaces: 4 }); - const pakeConfigPath = path.join(npmDirectory, 'src-tauri/pake.json'); - await fsExtra.writeJson(pakeConfigPath, tauriConf.pake, { spaces: 4 }); + await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 }); + const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json'); + await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 }); 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 fsExtra.writeJson(configJsonPath, tauriConf2, { spaces: 4 }); -} - + const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json'); + await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 }); +} + class BaseBuilder { constructor(options) { this.options = options; @@ -657,6 +700,9 @@ class BaseBuilder { async build(url) { await this.buildAndCopy(url, this.options.targets); } + async start(url) { + await mergeConfig(url, this.options, tauriConfig); + } async buildAndCopy(url, target) { const { name } = this.options; await mergeConfig(url, this.options, tauriConfig); @@ -678,7 +724,8 @@ class BaseBuilder { return target; } getBuildCommand() { - return 'npm run build'; + // the debug option should support `--debug` and `--release` + return this.options.debug ? 'npm run build:debug' : 'npm run build'; } getBasePath() { return 'src-tauri/target/release/bundle/'; @@ -686,8 +733,8 @@ class BaseBuilder { getBuildAppPath(npmDirectory, fileName, fileType) { return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`); } -} - +} + class MacBuilder extends BaseBuilder { constructor(options) { super(options); @@ -712,8 +759,8 @@ class MacBuilder extends BaseBuilder { ? 'src-tauri/target/universal-apple-darwin/release/bundle' : super.getBasePath(); } -} - +} + class WinBuilder extends BaseBuilder { constructor(options) { super(options); @@ -725,8 +772,8 @@ class WinBuilder extends BaseBuilder { const language = tauriConfig.tauri.bundle.windows.wix.language[0]; return `${name}_${tauriConfig.package.version}_${arch}_${language}`; } -} - +} + class LinuxBuilder extends BaseBuilder { constructor(options) { super(options); @@ -751,8 +798,8 @@ class LinuxBuilder extends BaseBuilder { } return super.getFileType(target); } -} - +} + const { platform } = process; const buildersMap = { darwin: MacBuilder, @@ -767,8 +814,8 @@ class BuilderProvider { } return new Builder(options); } -} - +} + const DEFAULT_PAKE_OPTIONS = { icon: '', height: 780, @@ -784,12 +831,14 @@ const DEFAULT_PAKE_OPTIONS = { iterCopyFile: false, systemTrayIcon: '', debug: false, -}; - + inject: [], + safeDomain: [], +}; + async function checkUpdateTips() { updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({ isGlobal: true }); -} - +} + async function handleIcon(options) { if (options.icon) { if (options.icon.startsWith('http')) { @@ -842,8 +891,8 @@ async function downloadIcon(iconUrl) { } throw error; } -} - +} + // Extracts the domain from a given URL. function getDomain(inputUrl) { try { @@ -881,8 +930,8 @@ function normalizeUrl(urlToNormalize) { else { throw new Error(`Your url "${urlWithProtocol}" is invalid`); } -} - +} + function resolveAppName(name, platform) { const domain = getDomain(name) || 'pake'; return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain; @@ -926,8 +975,8 @@ async function handleOptions(options, url) { }; appOptions.icon = await handleIcon(appOptions); return appOptions; -} - +} + function validateNumberInput(value) { const parsedValue = Number(value); if (isNaN(parsedValue)) { @@ -946,14 +995,14 @@ function validateUrlInput(url) { } } return url; -} - +} + const { green, yellow } = chalk; -const logo = `${chalk.green(' ____ _')} -${green('| _ \\ __ _| | _____')} -${green('| |_) / _` | |/ / _ \\')} -${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')} -${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')} +const logo = `${chalk.green(' ____ _')} +${green('| _ \\ __ _| | _____')} +${green('| |_) / _` | |/ / _ \\')} +${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')} +${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')} `; program .addHelpText('beforeAll', logo) @@ -974,6 +1023,8 @@ program .option('--iter-copy-file', 'Copy files when URL is a local file', DEFAULT_PAKE_OPTIONS.iterCopyFile) .option('--multi-arch', 'Only for Mac, supports both Intel and M1', DEFAULT_PAKE_OPTIONS.multiArch) .option('--targets ', 'Only for Linux, option "deb" or "appimage"', DEFAULT_PAKE_OPTIONS.targets) + .option('--inject [injects...]', 'inject .js or .css for this app', DEFAULT_PAKE_OPTIONS.inject) + .option('--safe-domain [domains...]', 'domains that can call window.__TAURI__ and use ipc', DEFAULT_PAKE_OPTIONS.safeDomain) .option('--debug', 'Debug mode', DEFAULT_PAKE_OPTIONS.debug) .version(packageJson.version, '-v, --version', 'Output the current version') .action(async (url, options) => { @@ -997,4 +1048,4 @@ program await builder.prepare(); await builder.build(url); }); -program.parse(); +program.parse(); diff --git a/package.json b/package.json index e38024f..361ea81 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,14 @@ "start": "npm run dev", "dev": "npm run tauri dev", "build": "npm run tauri build --release", + "build:debug": "npm run tauri build -- --debug", "build:mac": "npm run tauri build -- --target universal-apple-darwin", "build:all-unix": "chmod +x ./script/build.sh && ./script/build.sh", "build:all-windows": "pwsh ./script/build.ps1", "analyze": "cd src-tauri && cargo bloat --release --crates", "tauri": "tauri", "cli": "rollup -c rollup.config.js --watch", + "cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w", "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", "prepublishOnly": "npm run cli:build" }, @@ -66,6 +68,7 @@ "@rollup/plugin-alias": "^4.0.2", "@rollup/plugin-commonjs": "^23.0.2", "@rollup/plugin-json": "^5.0.2", + "@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-terser": "^0.1.0", "@types/fs-extra": "^9.0.13", "@types/is-url": "^1.2.30", diff --git a/plugins/pakeCliDevPlugin.js b/plugins/pakeCliDevPlugin.js new file mode 100644 index 0000000..33f8f16 --- /dev/null +++ b/plugins/pakeCliDevPlugin.js @@ -0,0 +1,50 @@ +import chalk from 'chalk'; +import {spawn, exec} from 'child_process'; + + +// just run in development mode +export default function pakeCliDevPlugin() { + let devChildProcess; + let cliChildProcess; + + let devHasStarted = false; + + return { + name: 'pake-cli-dev-plugin', + buildEnd() { + + const command = 'node'; + const cliCmdArgs = ['./dist/dev.js']; + + cliChildProcess = spawn(command, cliCmdArgs, {detached: true}); + + cliChildProcess.stdout.on('data', (data) => { + console.log(chalk.green(data.toString())); + }); + + cliChildProcess.stderr.on('data', (data) => { + console.error(chalk.yellow(data.toString())); + }); + + cliChildProcess.on('close', async (code) => { + console.log(chalk.yellow(`cli running end with code: ${code}`)); + if (devHasStarted) return; + devHasStarted = true; + devChildProcess = await exec('npm run tauri dev -- --config ./src-tauri/.pake/tauri.conf.json --features cli-build'); + + devChildProcess.stdout.on('data', (data) => { + console.log(chalk.green(data.toString())); + }); + + devChildProcess.stderr.on('data', (data) => { + console.error(chalk.yellow(data.toString())); + }); + + devChildProcess.on('close', (code) => { + console.log(chalk.yellow(`dev running end: ${code}`)); + process.exit(code); + }); + }); + } + } +} \ No newline at end of file diff --git a/rollup.config.js b/rollup.config.js index 0c21614..1aaf44b 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -4,12 +4,23 @@ import typescript from 'rollup-plugin-typescript2'; import alias from '@rollup/plugin-alias'; import commonjs from '@rollup/plugin-commonjs'; import json from '@rollup/plugin-json'; +import replace from '@rollup/plugin-replace'; + +import pakeCliDevPlugin from './plugins/pakeCliDevPlugin.js'; + +const isProduction = process.env.NODE_ENV === 'production'; +const devPlugins = !isProduction ? [pakeCliDevPlugin()] : []; export default { - input: 'bin/cli.ts', + input: isProduction ? 'bin/cli.ts' : 'bin/dev.ts', output: { - file: 'dist/cli.js', - format: 'es' + file: isProduction ? 'dist/cli.js' : 'dist/dev.js', + format: 'es', + sourcemap: !isProduction, + }, + watch: { + include: 'bin/**', + exclude: 'node_modules/**', }, plugins: [ json(), @@ -18,8 +29,12 @@ export default { clean: true, // 清理缓存 }), commonjs(), + replace({ + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), + }), alias({ entries: [{ find: '@', replacement: path.join(appRootPath.path, 'bin') }], }), + ...devPlugins, ], }; diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0650930..aaee0d6 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -24,6 +24,8 @@ tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-works cargo-bloat = "0.11.1" [features] +# this feature is used for development builds from development cli +cli-build = [] # by default Tauri runs in production mode # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL default = ["custom-protocol"] diff --git a/src-tauri/icons/figma.icns b/src-tauri/icons/figma.icns new file mode 100644 index 0000000..16a4920 Binary files /dev/null and b/src-tauri/icons/figma.icns differ diff --git a/src-tauri/icons/zbook.icns b/src-tauri/icons/zbook.icns new file mode 100644 index 0000000..16a4920 Binary files /dev/null and b/src-tauri/icons/zbook.icns differ diff --git a/src-tauri/pake.json b/src-tauri/pake.json index a97dcc7..96dd6fb 100644 --- a/src-tauri/pake.json +++ b/src-tauri/pake.json @@ -1,28 +1,29 @@ { - "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 - } + "windows": [ + { + "url": "https://weread.qq.com/", + "transparent": false, + "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": false, + "linux": false, + "windows": false + }, + "system_tray": { + "macos": false, + "linux": true, + "windows": true + }, + "inject": [] } diff --git a/src-tauri/src/app/invoke.rs b/src-tauri/src/app/invoke.rs index 2a91de8..5cf2f07 100644 --- a/src-tauri/src/app/invoke.rs +++ b/src-tauri/src/app/invoke.rs @@ -1,5 +1,5 @@ use crate::util::{check_file_or_append, get_download_message, show_toast, MessageType}; -use std::fs::File; +use std::fs::{self, File}; use std::io::Write; use tauri::api::http::{ClientBuilder, HttpRequestBuilder, ResponseType}; use tauri::{api, command, AppHandle, Manager, Window}; @@ -10,6 +10,12 @@ pub struct DownloadFileParams { filename: String, } +#[derive(serde::Deserialize)] +pub struct BinaryDownloadParams { + filename: String, + binary: Vec, +} + #[command] pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> { let window: Window = app.get_window("pake").unwrap(); @@ -42,3 +48,25 @@ pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result } } } + +#[command] +pub async fn download_file_by_binary( + app: AppHandle, + params: BinaryDownloadParams, +) -> Result<(), String> { + let window: Window = app.get_window("pake").unwrap(); + show_toast(&window, &get_download_message(MessageType::Start)); + 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_file_result = fs::write(file_path, ¶ms.binary); + match download_file_result { + Ok(_) => { + show_toast(&window, &get_download_message(MessageType::Success)); + Ok(()) + } + Err(e) => { + show_toast(&window, &get_download_message(MessageType::Failure)); + Err(e.to_string()) + } + } +} diff --git a/src-tauri/src/app/window.rs b/src-tauri/src/app/window.rs index 16c7f3e..c8485db 100644 --- a/src-tauri/src/app/window.rs +++ b/src-tauri/src/app/window.rs @@ -26,10 +26,11 @@ pub fn get_window(app: &mut App, config: PakeConfig, _data_dir: PathBuf) -> Wind .resizable(window_config.resizable) .fullscreen(window_config.fullscreen) .inner_size(window_config.width, window_config.height) - .disable_file_drop_handler() //Very annoying, otherwise dragging files to the window will not work. - .initialization_script(include_str!("../inject/style.js")) + .disable_file_drop_handler() + .initialization_script(include_str!("../inject/component.js")) .initialization_script(include_str!("../inject/event.js")) - .initialization_script(include_str!("../inject/component.js")); + .initialization_script(include_str!("../inject/style.js")) + .initialization_script(include_str!("../inject/custom.js")); //Very annoying, otherwise dragging files to the window will not work. #[cfg(target_os = "macos")] { diff --git a/src-tauri/src/inject/custom.js b/src-tauri/src/inject/custom.js new file mode 100644 index 0000000..9c01303 --- /dev/null +++ b/src-tauri/src/inject/custom.js @@ -0,0 +1,2 @@ +// This file is used to merge the injected external js and css files +// and you can also directly add script files that you want to attach to the application in this fil \ No newline at end of file diff --git a/src-tauri/src/inject/event.js b/src-tauri/src/inject/event.js index 0346441..71ecc13 100644 --- a/src-tauri/src/inject/event.js +++ b/src-tauri/src/inject/event.js @@ -68,12 +68,90 @@ document.addEventListener('DOMContentLoaded', () => { document.body.appendChild(topDom); const domEl = document.getElementById('pack-top-dom'); - domEl.addEventListener('mousedown', (e) => { - e.preventDefault(); - if (e.buttons === 1 && e.detail !== 2) { - appWindow.startDragging().then(); - } + +// Collect blob urls to blob by overriding window.URL.createObjectURL +function collectUrlToBlobs() { + const backupCreateObjectURL = window.URL.createObjectURL; + window.blobToUrlCaches = new Map(); + window.URL.createObjectURL = (blob) => { + const url = backupCreateObjectURL.call(window.URL, blob); + window.blobToUrlCaches.set(url, blob); + return url; + }; +} + +function convertBlobUrlToBinary(blobUrl) { + return new Promise((resolve) => { + const blob = window.blobToUrlCaches.get(blobUrl); + const reader = new FileReader(); + + reader.readAsArrayBuffer(blob); + reader.onload = () => { + resolve(Array.from(new Uint8Array(reader.result))); + }; }); +} + +function downladFromDataUri(dataURI, filename) { + const byteString = atob(dataURI.split(',')[1]); + // write the bytes of the string to an ArrayBuffer + const bufferArray = new ArrayBuffer(byteString.length); + + // create a view into the buffer + const binary = new Uint8Array(bufferArray); + + // set the bytes of the buffer to the correct values + for (var i = 0; i < byteString.length; i++) { + binary[i] = byteString.charCodeAt(i); + } + + // write the ArrayBuffer to a binary, and you're done + invoke('download_file_by_binary', { + params: { + filename, + binary: Array.from(binary) + }, + }); +} + +function downloadFromBlobUrl(blobUrl, filename) { + convertBlobUrlToBinary(blobUrl).then((binary) => { + invoke('download_file_by_binary', { + params: { + filename, + binary + }, + }); + }); +} + + +// detect blob download by createElement("a") +function detectDownloadByCreateAnchor() { + const createEle = document.createElement; + document.createElement = (el) => { + if (el !== 'a') return createEle.call(document, el); + const anchorEle = createEle.call(document, el); + + // use addEventListener to avoid overriding the original click event. + anchorEle.addEventListener('click', (e) => { + const url = anchorEle.href; + const filename = anchorEle.download || getFilenameFromUrl(url); + if (window.blobToUrlCaches.has(url)) { + downloadFromBlobUrl(url, filename); + // case: downoload from dataURL -> convert dataURL -> + } else if (url.startsWith('data:')) { + downladFromDataUri(url, filename); + } else { + handleExternalLink(e, url); + } + }, true); + + return anchorEle; + }; +} + + domEl.addEventListener('touchstart', () => { appWindow.startDragging().then(); @@ -95,6 +173,9 @@ document.addEventListener('DOMContentLoaded', () => { }); const isExternalLink = (url, host) => window.location.host !== host; + // process special download protocal['data:','blob:'] + const isSpecialDownload = (url) => ['blob', 'data'].some(protocal => url.startsWith(protocal)); + const isDownloadRequired = (url, anchorElement, e) => anchorElement.download || e.metaKey || e.ctrlKey || isDownloadLink(url); @@ -122,7 +203,7 @@ document.addEventListener('DOMContentLoaded', () => { } // Process download links for Rust to handle. - if (isDownloadRequired(absoluteUrl, anchorElement, e) && !externalDownLoadLink()) { + if (isDownloadRequired(absoluteUrl, anchorElement, e) && !externalDownLoadLink() && !isSpecialDownload(absoluteUrl)) { handleDownloadLink(e, absoluteUrl, filename); } } @@ -177,69 +258,6 @@ function getFilenameFromUrl(url) { return urlPath.substring(urlPath.lastIndexOf('/') + 1); } -// Collect blob urls to blob by overriding window.URL.createObjectURL -function collectUrlToBlobs() { - const backupCreateObjectURL = window.URL.createObjectURL; - window.blobToUrlCaches = new Map(); - window.URL.createObjectURL = (blob) => { - const url = backupCreateObjectURL.call(window.URL, blob); - window.blobToUrlCaches.set(url, blob); - return url; - }; -} - -function convertBlobUrlToBinary(blobUrl) { - return new Promise((resolve) => { - const blob = window.blobToUrlCaches.get(blobUrl); - const reader = new FileReader(); - - reader.readAsArrayBuffer(blob); - reader.onload = () => { - resolve(reader.result); - }; - }); -} - -async function downloadFromBlobUrl(blobUrl, filename) { - try { - const tauri = window.__TAURI__; - const binary = await convertBlobUrlToBinary(blobUrl); - - await tauri.fs.writeBinaryFile(filename, binary, { - dir: tauri.fs.BaseDirectory.Download, - }); - - const lang = getSystemLanguage(); - window.pakeToast(lang === 'en' ? 'Download successful, saved to download directory~' : '下载成功,已保存到下载目录~'); - } catch (error) { - console.error('Error downloading from Blob URL:', error); - } -} - - -// detect blob download by createElement("a") -function detectDownloadByCreateAnchor() { - const originalCreateElement = document.createElement; - - document.createElement = function(el, ...args) { - const element = originalCreateElement.call(this, el, ...args); - - if (el === 'a') { - element.addEventListener('click', (event) => { - const url = element.href; - if (window.blobToUrlCaches.has(url)) { - // Prevent default 'click' event if a blob URL is detected - event.preventDefault(); - const filename = element.download || getFilenameFromUrl(url); - downloadFromBlobUrl(url, filename); - } - }); - } - - return element; - }; -} - // Determine the language of the current system. function getSystemLanguage() { diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index a479041..917c870 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -7,7 +7,7 @@ mod app; mod util; use app::{invoke, menu, window}; -use invoke::download_file; +use invoke::{download_file, download_file_by_binary}; use menu::{get_menu, menu_event_handle}; use tauri_plugin_window_state::Builder as windowStatePlugin; use util::{get_data_dir, get_pake_config}; @@ -41,7 +41,10 @@ pub fn run_app() { tauri_app .plugin(windowStatePlugin::default().build()) - .invoke_handler(tauri::generate_handler![download_file]) + .invoke_handler(tauri::generate_handler![ + download_file, + download_file_by_binary + ]) .setup(|app| { let _window = get_window(app, pake_config, data_dir); // Prevent initial shaking diff --git a/src-tauri/src/util.rs b/src-tauri/src/util.rs index f43a335..4b82e87 100644 --- a/src-tauri/src/util.rs +++ b/src-tauri/src/util.rs @@ -4,9 +4,16 @@ use std::path::PathBuf; use tauri::{api, Config, Window}; pub fn get_pake_config() -> (PakeConfig, Config) { + #[cfg(feature = "cli-build")] + let pake_config: PakeConfig = serde_json::from_str(include_str!("../.pake/pake.json")) + .expect("Failed to parse pake config"); + #[cfg(not(feature = "cli-build"))] let pake_config: PakeConfig = serde_json::from_str(include_str!("../pake.json")).expect("Failed to parse pake config"); - + #[cfg(feature = "cli-build")] + let tauri_config: Config = serde_json::from_str(include_str!("../.pake/tauri.conf.json")) + .expect("Failed to parse tauri config"); + #[cfg(not(feature = "cli-build"))] let tauri_config: Config = serde_json::from_str(include_str!("../tauri.conf.json")) .expect("Failed to parse tauri config"); diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 2661614..306cb1f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,39 +1,43 @@ { - "package": { - "productName": "WeRead", - "version": "1.0.0" - }, - "tauri": { - "security": { - "csp": null, - "dangerousRemoteDomainIpcAccess": [ - { - "domain": "weread.qq.com", - "windows": ["pake"], - "enableTauriAPI": true + "package": { + "productName": "WeRead", + "version": "1.0.0" + }, + "tauri": { + "security": { + "csp": null, + "dangerousRemoteDomainIpcAccess": [ + { + "domain": "weread.qq.com", + "windows": [ + "pake" + ], + "enableTauriAPI": true + } + ] + }, + "updater": { + "active": false + }, + "systemTray": { + "iconPath": "png/icon_512.png", + "iconAsTemplate": true + }, + "allowlist": { + "all": true, + "fs": { + "all": true, + "scope": [ + "$DOWNLOAD/*" + ] + } } - ] }, - "updater": { - "active": false - }, - "systemTray": { - "iconPath": "png/weread_512.png", - "iconAsTemplate": true - }, - "allowlist": { - "all": true, - "fs": { - "all": true, - "scope": ["$DOWNLOAD/*"] - } + "build": { + "withGlobalTauri": true, + "devPath": "../dist", + "distDir": "../dist", + "beforeBuildCommand": "", + "beforeDevCommand": "" } - }, - "build": { - "withGlobalTauri": true, - "devPath": "../dist", - "distDir": "../dist", - "beforeBuildCommand": "", - "beforeDevCommand": "" - } } diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index cdfbfa1..e1d0328 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -1,23 +1,25 @@ { - "tauri": { - "bundle": { - "icon": ["icons/weread.icns"], - "identifier": "com.pake.weread", - "active": true, - "category": "DeveloperTool", - "copyright": "", - "externalBin": [], - "longDescription": "", - "macOS": { - "entitlements": null, - "exceptionDomain": "", - "frameworks": [], - "providerShortName": null, - "signingIdentity": null - }, - "resources": [], - "shortDescription": "", - "targets": ["dmg"] + "tauri": { + "bundle": { + "icon": ["icons/weread.icns"], + "identifier": "com.pake.weread", + "active": true, + "category": "DeveloperTool", + "copyright": "", + "externalBin": [], + "longDescription": "", + "macOS": { + "entitlements": null, + "exceptionDomain": "", + "frameworks": [], + "providerShortName": null, + "signingIdentity": null + }, + "resources": [], + "shortDescription": "", + "targets": [ + "dmg" + ] + } } - } }