├── .dockerignore ├── .ecrc.json ├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug-report.yml │ ├── config.yml │ └── feature.yml └── workflows │ ├── contribute_list.yml │ ├── docker-publish.yml │ ├── editorconfig-check.yml │ ├── pake-cli.yaml │ ├── pake_build_next.yaml │ ├── pake_build_single_app.yaml │ └── rust-code-quality-check.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── README_CN.md ├── README_JP.md ├── bin ├── README.md ├── README_CN.md ├── builders │ ├── BaseBuilder.ts │ ├── BuilderProvider.ts │ ├── LinuxBuilder.ts │ ├── MacBuilder.ts │ └── WinBuilder.ts ├── cli.ts ├── defaults.ts ├── dev.ts ├── helpers │ ├── merge.ts │ ├── rust.ts │ ├── tauriConfig.ts │ └── updater.ts ├── options │ ├── icon.ts │ ├── index.ts │ └── logger.ts ├── types.ts └── utils │ ├── combine.ts │ ├── dir.ts │ ├── info.ts │ ├── ip.ts │ ├── platform.ts │ ├── shell.ts │ ├── url.ts │ └── validate.ts ├── cli.js ├── default_app_list.json ├── dist └── cli.js ├── icns2png.py ├── package.json ├── rollup.config.js ├── script ├── app_config.mjs └── build_with_pake_cli.js ├── src-tauri ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── assets │ ├── com-tw93-weread.desktop │ └── main.wxs ├── build.rs ├── capabilities │ └── default.json ├── icons │ ├── chatgpt.icns │ ├── excalidraw.icns │ ├── flomo.icns │ ├── gemini.icns │ ├── grok.icns │ ├── icon.icns │ ├── icon.png │ ├── lizhi.icns │ ├── programmusic.icns │ ├── qwerty.icns │ ├── twitter.icns │ ├── wechat.icns │ ├── weread.icns │ ├── xiaohongshu.icns │ ├── youtube.icns │ └── youtubemusic.icns ├── info.plist ├── pake.json ├── png │ ├── chatgpt_256.ico │ ├── chatgpt_32.ico │ ├── chatgpt_512.png │ ├── excalidraw_256.ico │ ├── excalidraw_32.ico │ ├── excalidraw_512.png │ ├── flomo_256.ico │ ├── flomo_32.ico │ ├── flomo_512.png │ ├── gemini_256.ico │ ├── gemini_32.ico │ ├── gemini_512.png │ ├── grok_256.ico │ ├── grok_32.ico │ ├── grok_512.png │ ├── icon_256.ico │ ├── icon_32.ico │ ├── icon_512.png │ ├── lizhi_256.ico │ ├── lizhi_32.ico │ ├── lizhi_512.png │ ├── programmusic_256.ico │ ├── programmusic_32.ico │ ├── programmusic_512.png │ ├── qwerty_256.ico │ ├── qwerty_32.ico │ ├── qwerty_512.png │ ├── twitter_256.ico │ ├── twitter_32.ico │ ├── twitter_512.png │ ├── wechat_256.ico │ ├── wechat_32.ico │ ├── wechat_512.png │ ├── weread_256.ico │ ├── weread_32.ico │ ├── weread_512.png │ ├── xiaohongshu_256.ico │ ├── xiaohongshu_32.ico │ ├── xiaohongshu_512.png │ ├── youtube_256.ico │ ├── youtube_32.ico │ ├── youtube_512.png │ ├── youtubemusic_256.ico │ ├── youtubemusic_32.ico │ └── youtubemusic_512.png ├── rust_proxy.toml ├── src │ ├── app │ │ ├── config.rs │ │ ├── invoke.rs │ │ ├── mod.rs │ │ ├── setup.rs │ │ └── window.rs │ ├── inject │ │ ├── component.js │ │ ├── custom.js │ │ ├── event.js │ │ └── style.js │ ├── lib.rs │ ├── main.rs │ └── util.rs ├── tauri.conf.json ├── tauri.linux.conf.json ├── tauri.macos.conf.json └── tauri.windows.conf.json └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | 4 | **/target 5 | **/node_modules 6 | 7 | **/*.log 8 | **/*.md 9 | **/tmp 10 | 11 | Dockerfile 12 | -------------------------------------------------------------------------------- /.ecrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "Exclude": ["Cargo\\.lock$", "dist", "*\\.md"] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | # Use 4 spaces for Python, Rust and Bash files 12 | [*.{py,rs,sh}] 13 | indent_size = 4 14 | 15 | # Makefiles always use tabs for indentation 16 | [Makefile] 17 | indent_style = tab 18 | 19 | [*.bat] 20 | indent_size = 2 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | [*.ts] 26 | quote_type= "single" 27 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | bin/**/* linguist-vendored 2 | bin/* linguist-vendored 3 | dist/* linguist-vendored 4 | /cli.js linguist-vendored 5 | .github/**/* linguist-vendored 6 | .github/* linguist-vendored 7 | script/* linguist-vendored 8 | /icns2png.py linguist-vendored 9 | /rollup.config.js linguist-vendored 10 | src-tauri/src/inject/* linguist-vendored 11 | src-tauri/src/.pake/* linguist-vendored -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: ['tw93'] 2 | custom: ['https://miaoyan.app/cats.html?name=Pake'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug-report.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Problems with the software 3 | title: '[Bug] ' 4 | labels: ['bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | Thank you very much for your feedback! 10 | 11 | 有关讨论、建议或者咨询的内容请去往[讨论区](https://github.com/tw93/Pake/discussions)。 12 | 13 | For suggestions or help, please consider using [Github Discussion](https://github.com/tw93/Pake/discussions) instead. 14 | - type: checkboxes 15 | attributes: 16 | label: Search before asking 17 | description: > 18 | 🙊 辛苦提 bug 前,去找一下 [历史](https://github.com/tw93/Pake/issues?q=) 是否有提。辛苦提供系统版本、录屏或者截图、复现路径,期待解决的点——这几个说明能帮助我更好的解决问题,此外假如是讨论,建议辛苦去 [Discussions](https://github.com/tw93/Pake/discussions) 看看是否有类似的讨论。 19 | 20 | 🙊 Check out [Issues](https://github.com/tw93/Pake/issues?q=) before reporting. Please provide your system version, screencasts, screenshots, way to reproduce, and the expected result – helpful for me to understand and fix up this issue! Besides, for suggestions or something else, head to [Pake's Discussions Platform](https://github.com/tw93/Pake/discussions). 21 | options: 22 | - label: > 23 | 我在 [issues](https://github.com/tw93/Pake/issues?q=) 列表中搜索,没有找到类似的内容。 24 | 25 | I searched in the [issues](https://github.com/tw93/Pake/issues?q=) and found nothing similar. 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Pake version 30 | description: > 31 | Please provide the version of Pake you are using. If you are using the main/dev branch, please provide the commit id. 32 | validations: 33 | required: true 34 | - type: textarea 35 | attributes: 36 | label: System version 37 | description: > 38 | Please provide the version of System you are using. 39 | validations: 40 | required: true 41 | - type: textarea 42 | attributes: 43 | label: Node.js version 44 | description: > 45 | Please provide the Node.js version. 46 | validations: 47 | required: true 48 | - type: textarea 49 | attributes: 50 | label: Minimal reproduce step 51 | description: Please try to give reproducing steps to facilitate quick location of the problem. 52 | validations: 53 | required: true 54 | - type: textarea 55 | attributes: 56 | label: What did you expect to see? 57 | validations: 58 | required: true 59 | - type: textarea 60 | attributes: 61 | label: What did you see instead? 62 | validations: 63 | required: true 64 | - type: textarea 65 | attributes: 66 | label: Anything else? 67 | - type: checkboxes 68 | attributes: 69 | label: Are you willing to submit a PR? 70 | description: > 71 | We look forward to the community of developers or users helping solve Pake problems together. If you are willing to submit a PR to fix this problem, please check the box. 72 | options: 73 | - label: I'm willing to submit a PR! 74 | - type: markdown 75 | attributes: 76 | value: 'Thanks for completing our form!' 77 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Ask a question or get support 4 | url: https://github.com/tw93/Pake/discussions/categories/q-a 5 | about: Ask a question or request support for Pake 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: Feature 2 | description: Add new feature, improve code, and more 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thank you very much for your feature proposal! 9 | - type: checkboxes 10 | attributes: 11 | label: Search before asking 12 | description: > 13 | Please search [issues](https://github.com/tw93/Pake/issues?q=) to check if your issue has already been reported. 14 | options: 15 | - label: > 16 | 我在 [issues](https://github.com/tw93/Pake/issues?q=) 列表中搜索,没有找到类似的内容。 17 | 18 | I searched in the [issues](https://github.com/tw93/Pake/issues?q=) and found nothing similar. 19 | required: true 20 | - type: textarea 21 | attributes: 22 | label: Motivation 23 | description: Describe the motivations for this feature, like how it fixes the problem you meet. 24 | validations: 25 | required: true 26 | - type: textarea 27 | attributes: 28 | label: Solution 29 | description: Describe the proposed solution and add related materials like links if any. 30 | - type: textarea 31 | attributes: 32 | label: Alternatives 33 | description: Describe other alternative solutions or features you considered, but rejected. 34 | - type: textarea 35 | attributes: 36 | label: Anything else? 37 | - type: checkboxes 38 | attributes: 39 | label: Are you willing to submit a PR? 40 | description: > 41 | We look forward to the community of developers or users helping develop Pake features together. If you are willing to submit a PR to implement the feature, please check the box. 42 | options: 43 | - label: I'm willing to submit a PR! 44 | - type: markdown 45 | attributes: 46 | value: 'Thanks for completing our form!' 47 | -------------------------------------------------------------------------------- /.github/workflows/contribute_list.yml: -------------------------------------------------------------------------------- 1 | name: Build Contribute List 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | contrib-readme-job: 9 | runs-on: ubuntu-latest 10 | name: A job to automate contrib in readme 11 | steps: 12 | - name: Contribute List 13 | uses: akhilmhdh/contributors-readme-action@v2.3.6 14 | with: 15 | image_size: 90 16 | columns_per_row: 7 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.TOKEN }} 19 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker Image 2 | 3 | on: 4 | workflow_dispatch: # Manual 5 | 6 | env: 7 | REGISTRY: ghcr.io 8 | IMAGE_NAME: ${{ github.repository }} 9 | 10 | jobs: 11 | build-and-push-image: 12 | runs-on: ubuntu-22.04 13 | permissions: 14 | contents: read 15 | packages: write 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | 21 | - name: Set up Docker Buildx 22 | uses: docker/setup-buildx-action@v2 23 | 24 | - name: Log in to the Container registry 25 | uses: docker/login-action@v2 26 | with: 27 | registry: ${{ env.REGISTRY }} 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | 31 | - name: Extract metadata (tags, labels) for Docker 32 | id: meta 33 | uses: docker/metadata-action@v4 34 | with: 35 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 36 | tags: | 37 | type=raw,value=latest,enable={{is_default_branch}} 38 | type=sha 39 | 40 | - name: Build and push Docker image 41 | uses: docker/build-push-action@v4 42 | with: 43 | context: . 44 | push: true 45 | tags: ${{ steps.meta.outputs.tags }} 46 | labels: ${{ steps.meta.outputs.labels }} 47 | cache-from: type=gha 48 | cache-to: type=gha,mode=max 49 | platforms: linux/amd64 50 | -------------------------------------------------------------------------------- /.github/workflows/editorconfig-check.yml: -------------------------------------------------------------------------------- 1 | name: Check EditorConfig 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | actions: write 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | jobs: 17 | editorconfig-check: 18 | name: Run EditorConfig Check 19 | runs-on: ubuntu-latest 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: editorconfig-checker/action-editorconfig-checker@main 23 | - run: editorconfig-checker -config .ecrc.json 24 | -------------------------------------------------------------------------------- /.github/workflows/pake-cli.yaml: -------------------------------------------------------------------------------- 1 | name: Build App With Pake CLI 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | platform: 6 | description: 'Platform' 7 | required: true 8 | default: 'macos-latest' 9 | type: choice 10 | options: 11 | - 'windows-latest' 12 | - 'macos-latest' 13 | - 'ubuntu-24.04' 14 | url: 15 | description: 'URL' 16 | required: true 17 | name: 18 | description: 'Name, English, Linux no capital' 19 | required: true 20 | icon: 21 | description: 'Icon, Image URL, Optional' 22 | required: false 23 | width: 24 | description: 'Width, Optional' 25 | required: false 26 | default: '1200' 27 | height: 28 | description: 'Height, Optional' 29 | required: false 30 | default: '780' 31 | fullscreen: 32 | description: 'Fullscreen, At startup, Optional' 33 | required: false 34 | type: boolean 35 | default: false 36 | hide_title_bar: 37 | description: 'Hide TitleBar, MacOS only, Optional' 38 | required: false 39 | type: boolean 40 | default: false 41 | multi_arch: 42 | description: 'MultiArch, MacOS only, Optional' 43 | required: false 44 | type: boolean 45 | default: false 46 | targets: 47 | description: 'Targets, Linux only, Optional' 48 | required: false 49 | default: 'deb' 50 | type: choice 51 | options: 52 | - 'deb' 53 | - 'appimage' 54 | - 'rpm' 55 | 56 | jobs: 57 | build: 58 | name: ${{ inputs.platform }} 59 | runs-on: ${{ inputs.platform }} 60 | strategy: 61 | fail-fast: false 62 | 63 | steps: 64 | - name: Checkout repository 65 | uses: actions/checkout@v3 66 | 67 | - name: Install node 68 | uses: actions/setup-node@v3 69 | with: 70 | node-version: 18 71 | 72 | - name: Install Rust for ubuntu-24.04 73 | if: inputs.platform == 'ubuntu-24.04' 74 | uses: dtolnay/rust-toolchain@stable 75 | with: 76 | toolchain: stable 77 | target: x86_64-unknown-linux-musl 78 | 79 | - name: Install Rust for windows-latest 80 | if: inputs.platform == 'windows-latest' 81 | uses: dtolnay/rust-toolchain@stable 82 | with: 83 | toolchain: stable-x86_64-msvc 84 | target: x86_64-pc-windows-msvc 85 | 86 | - name: Install Rust for macos-latest 87 | if: inputs.platform == 'macos-latest' 88 | uses: dtolnay/rust-toolchain@stable 89 | with: 90 | toolchain: stable 91 | target: x86_64-apple-darwin 92 | 93 | - name: Install dependencies (ubuntu only) 94 | if: inputs.platform == 'ubuntu-24.04' 95 | uses: awalsh128/cache-apt-pkgs-action@v1.4.3 96 | with: 97 | packages: libsoup3.0-dev libdbus-1-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra 98 | version: 1.1 99 | 100 | - name: Install pake-cli local 101 | shell: bash 102 | run: | 103 | echo "install pake on local" 104 | npm install pake-cli 105 | 106 | - name: Rust cache restore 107 | uses: actions/cache/restore@v3 108 | id: cache_store 109 | with: 110 | path: | 111 | ~/.cargo/bin/ 112 | ~/.cargo/registry/index/ 113 | ~/.cargo/registry/cache/ 114 | ~/.cargo/git/db/ 115 | node_modules/pake-cli/src-tauri/target/ 116 | key: ${{ inputs.platform }}-cargo-${{ hashFiles('node_modules/pake-cli/src-tauri/Cargo.lock') }} 117 | 118 | - name: Install dependencies 119 | run: | 120 | npm install shelljs 121 | npm install axios 122 | 123 | - name: Build with pake-cli 124 | run: | 125 | node ./script/build_with_pake_cli.js 126 | env: 127 | URL: ${{ inputs.url }} 128 | NAME: ${{ inputs.name }} 129 | ICON: ${{ inputs.icon }} 130 | HEIGHT: ${{ inputs.height }} 131 | WIDTH: ${{ inputs.width }} 132 | HIDE_TITLE_BAR: ${{ inputs.hide_title_bar }} 133 | FULLSCREEN: ${{ inputs.fullscreen }} 134 | MULTI_ARCH: ${{ inputs.multi_arch }} 135 | TARGETS: ${{ inputs.targets }} 136 | PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig 137 | PKG_CONFIG_ALLOW_SYSTEM_LIBS: 1 138 | PKG_CONFIG_ALLOW_SYSTEM_CFLAGS: 1 139 | 140 | - name: Upload archive 141 | uses: actions/upload-artifact@v4 142 | with: 143 | name: output-${{ inputs.platform }}.zip 144 | path: node_modules/pake-cli/output/* 145 | retention-days: 3 146 | 147 | - name: Rust cache store 148 | uses: actions/cache/save@v3 149 | if: steps.cache_store.outputs.cache-hit != 'true' 150 | with: 151 | path: | 152 | ~/.cargo/bin/ 153 | ~/.cargo/registry/index/ 154 | ~/.cargo/registry/cache/ 155 | ~/.cargo/git/db/ 156 | node_modules/pake-cli/src-tauri/target/ 157 | key: ${{ inputs.platform }}-cargo-${{ hashFiles('node_modules/pake-cli/src-tauri/Cargo.lock') }} 158 | -------------------------------------------------------------------------------- /.github/workflows/pake_build_next.yaml: -------------------------------------------------------------------------------- 1 | name: Build All Popular Apps 2 | on: 3 | push: 4 | tags: 5 | - 'V*' 6 | 7 | jobs: 8 | read_apps_config: 9 | name: Read Apps Config 10 | runs-on: ubuntu-latest 11 | outputs: 12 | apps_name: ${{ steps.read-apps-config.outputs.apps_name }} 13 | apps_config: ${{ steps.read-apps-config.outputs.apps_config }} 14 | steps: 15 | - name: Checkout repository 16 | uses: actions/checkout@v3 17 | - name: Get Apps Config 18 | id: read-apps-config 19 | run: | 20 | echo "apps_name=$(jq -c '[.[] | .name]' default_app_list.json)" >> $GITHUB_OUTPUT 21 | echo "apps_config=$(jq -c '[.[]]' default_app_list.json)" >> $GITHUB_OUTPUT 22 | 23 | trigger_build: 24 | needs: read_apps_config 25 | name: ${{ matrix.title }} 26 | strategy: 27 | matrix: 28 | name: ${{ fromJson(needs.read_apps_config.outputs.apps_name) }} 29 | include: ${{ fromJSON(needs.read_apps_config.outputs.apps_config) }} 30 | uses: ./.github/workflows/pake_build_single_app.yaml 31 | with: 32 | name: ${{ matrix.name }} 33 | title: ${{ matrix.title }} 34 | name_zh: ${{ matrix.name_zh }} 35 | url: ${{ matrix.url }} 36 | -------------------------------------------------------------------------------- /.github/workflows/pake_build_single_app.yaml: -------------------------------------------------------------------------------- 1 | name: Build Single Popular App 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | name: 6 | description: 'App Name' 7 | required: true 8 | default: 'twitter' 9 | title: 10 | description: 'App Title' 11 | required: true 12 | default: 'Twitter' 13 | name_zh: 14 | description: 'App Name in Chinese' 15 | required: true 16 | default: '推特' 17 | url: 18 | description: 'App URL' 19 | required: true 20 | default: 'https://twitter.com/' 21 | workflow_call: 22 | inputs: 23 | name: 24 | description: 'App Name' 25 | type: string 26 | required: true 27 | default: 'twitter' 28 | title: 29 | description: 'App Title' 30 | required: true 31 | type: string 32 | default: 'Twitter' 33 | name_zh: 34 | description: 'App Name in Chinese' 35 | required: true 36 | type: string 37 | default: '推特' 38 | url: 39 | description: 'App URL' 40 | required: true 41 | type: string 42 | default: 'https://twitter.com/' 43 | 44 | jobs: 45 | build_single_app: 46 | name: ${{ inputs.title }} (${{ matrix.os }}) 47 | runs-on: ${{ matrix.os }} 48 | strategy: 49 | matrix: 50 | build: [linux, macos, windows] 51 | include: 52 | - build: linux 53 | os: ubuntu-latest 54 | rust: stable 55 | target: x86_64-unknown-linux-musl 56 | - build: windows 57 | os: windows-latest 58 | rust: stable-x86_64-msvc 59 | target: x86_64-pc-windows-msvc 60 | - build: macos 61 | os: macos-latest 62 | rust: stable 63 | target: x86_64-apple-darwin 64 | steps: 65 | - name: Checkout repository 66 | uses: actions/checkout@v3 67 | 68 | - name: Install Rust 69 | uses: dtolnay/rust-toolchain@stable 70 | with: 71 | toolchain: ${{ matrix.rust }} 72 | target: ${{ matrix.target }} 73 | 74 | - name: Install dependencies (ubuntu only) 75 | if: matrix.os == 'ubuntu-latest' 76 | uses: awalsh128/cache-apt-pkgs-action@v1.4.3 77 | with: 78 | packages: libdbus-1-dev libsoup3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra 79 | version: 1.1 80 | 81 | - name: Rust cache restore 82 | id: cache_store 83 | uses: actions/cache/restore@v3 84 | with: 85 | path: | 86 | ~/.cargo/bin/ 87 | ~/.cargo/registry/index/ 88 | ~/.cargo/registry/cache/ 89 | ~/.cargo/git/db/ 90 | src-tauri/target/ 91 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 92 | 93 | - name: Config App 94 | env: 95 | NAME: ${{ inputs.name }} 96 | TITLE: ${{ inputs.title }} 97 | NAME_ZH: ${{ inputs.name_zh }} 98 | URL: ${{ inputs.url }} 99 | run: | 100 | npm install 101 | npm run build:config 102 | 103 | - name: Build for Ubuntu 104 | if: matrix.os == 'ubuntu-latest' 105 | run: | 106 | npm run tauri build 107 | mkdir -p output/linux 108 | mv src-tauri/target/release/bundle/deb/*.deb output/linux/${{inputs.title}}_`arch`.deb 109 | mv src-tauri/target/release/bundle/appimage/*.AppImage output/linux/"${{inputs.title}}"_`arch`.AppImage 110 | 111 | - name: Build for macOS 112 | if: matrix.os == 'macos-latest' 113 | run: | 114 | rustup target add aarch64-apple-darwin 115 | rustup target add x86_64-apple-darwin 116 | npm run tauri build -- --target universal-apple-darwin 117 | mkdir -p output/macos 118 | mv src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg output/macos/"${{inputs.title}}".dmg 119 | 120 | - name: Build for Windows 121 | if: matrix.os == 'windows-latest' 122 | run: | 123 | npm run tauri build -- --target x86_64-pc-windows-msvc 124 | New-Item -Path "output\windows" -ItemType Directory 125 | Move-Item -Path "src-tauri\target\x86_64-pc-windows-msvc\release\bundle\msi\*.msi" -Destination "output\windows\${{inputs.title}}_x64.msi" 126 | 127 | - name: Restore Cargo Lock File(Windows Only) 128 | if: matrix.os == 'windows-latest' 129 | run: | 130 | git checkout -- src-tauri/Cargo.lock 131 | 132 | - name: Rust cache store 133 | uses: actions/cache/save@v3 134 | with: 135 | path: | 136 | ~/.cargo/bin/ 137 | ~/.cargo/registry/index/ 138 | ~/.cargo/registry/cache/ 139 | ~/.cargo/git/db/ 140 | src-tauri/target/ 141 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 142 | 143 | - name: Upload For Single Build 144 | uses: actions/upload-artifact@v4 145 | if: startsWith(github.ref, 'refs/tags/') != true 146 | with: 147 | path: 'output/*/*.*' 148 | 149 | - name: Upload For Release 150 | # arg info: https://github.com/ncipollo/release-action#release-action 151 | uses: ncipollo/release-action@v1 152 | if: startsWith(github.ref, 'refs/tags/') == true 153 | with: 154 | allowUpdates: true 155 | artifacts: 'output/*/*.*' 156 | token: ${{ secrets.GITHUB_TOKEN }} 157 | -------------------------------------------------------------------------------- /.github/workflows/rust-code-quality-check.yml: -------------------------------------------------------------------------------- 1 | name: Check Rust Code 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | permissions: 9 | actions: write 10 | contents: read 11 | 12 | concurrency: 13 | group: ${{ github.workflow }}-${{ github.ref }} 14 | cancel-in-progress: true 15 | 16 | defaults: 17 | run: 18 | shell: bash 19 | working-directory: src-tauri 20 | 21 | jobs: 22 | cargo-test: 23 | name: Test codebase on ${{ matrix.os }} (cargo test) 24 | runs-on: ${{ matrix.os }} 25 | strategy: 26 | matrix: 27 | os: 28 | - windows-latest 29 | - ubuntu-latest 30 | - macos-latest 31 | fail-fast: false 32 | steps: 33 | - uses: actions/checkout@v3 34 | - uses: actions-rust-lang/setup-rust-toolchain@v1 35 | - uses: rui314/setup-mold@v1 36 | - uses: taiki-e/install-action@v1 37 | with: 38 | tool: cargo-hack,nextest 39 | - name: Install dependencies for Ubuntu 40 | if: matrix.os == 'ubuntu-latest' 41 | uses: awalsh128/cache-apt-pkgs-action@v1.4.3 42 | with: 43 | packages: libdbus-1-dev libsoup3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev build-essential curl wget file libxdo-dev libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra 44 | version: 1.0 45 | - name: Run unit & integration tests with nextest 46 | run: cargo hack --feature-powerset --exclude-features cli-build nextest run --no-tests=pass 47 | 48 | cargo-clippy: 49 | name: Check codebase quality (cargo clippy) 50 | runs-on: ${{ matrix.os }} 51 | strategy: 52 | matrix: 53 | os: 54 | - windows-latest 55 | - ubuntu-latest 56 | - macos-latest 57 | fail-fast: false 58 | steps: 59 | - uses: actions/checkout@v3 60 | - uses: actions-rust-lang/setup-rust-toolchain@v1 61 | with: 62 | components: clippy 63 | - uses: taiki-e/install-action@cargo-hack 64 | - name: Install dependencies for Ubuntu 65 | if: matrix.os == 'ubuntu-latest' 66 | uses: awalsh128/cache-apt-pkgs-action@v1.4.3 67 | with: 68 | packages: libdbus-1-dev libsoup3.0-dev libjavascriptcoregtk-4.1-dev libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev gnome-video-effects gnome-video-effects-extra 69 | version: 1.0 70 | - name: Run all-features code quality checks 71 | run: cargo hack --feature-powerset --exclude-features cli-build --no-dev-deps clippy 72 | - name: Run normal code quality check 73 | run: cargo clippy 74 | 75 | cargo-fmt: 76 | name: Enforce codebase style (cargo fmt) 77 | runs-on: ubuntu-latest 78 | steps: 79 | - uses: actions/checkout@v3 80 | - uses: actions-rust-lang/setup-rust-toolchain@v1 81 | with: 82 | components: rustfmt 83 | - run: cargo fmt --all -- --color=always --check 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | *.local 13 | 14 | # Editor directories and files 15 | .vscode 16 | .idea 17 | .DS_Store 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | .npmrc 24 | output 25 | *.msi 26 | *.deb 27 | *.AppImage 28 | *.dmg 29 | 30 | package-lock.json 31 | yarn.lock 32 | pnpm-lock.yaml 33 | dist 34 | !dist/about_pake.html 35 | !dist/cli.js 36 | !dist/.gitkeep 37 | src-tauri/.cargo/config.toml 38 | src-tauri/.cargo/ 39 | .next 40 | src-tauri/.pake/ 41 | src-tauri/gen 42 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | src-tauri/target 2 | node_modules 3 | dist/**/* 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": true, 4 | "endOfLine": "lf", 5 | "bracketSameLine": false, 6 | "jsxSingleQuote": false, 7 | "printWidth": 140, 8 | "proseWrap": "preserve", 9 | "quoteProps": "as-needed", 10 | "semi": true, 11 | "singleQuote": true, 12 | "tabWidth": 2, 13 | "trailingComma": "all", 14 | "useTabs": false 15 | } 16 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of erotic language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | tw93@qq.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to Pake 2 | 3 | **Welcome to create [pull requests](https://github.com/tw93/Pake/compare/) for bugfix, new component, doc, example, suggestion and anything.** 4 | 5 | ## Branch Management 6 | 7 | ```mermaid 8 | graph LR 9 | b_dev(dev) --> b_main(main); 10 | contributions([Develop / Pull requests]) -.-> b_dev; 11 | ``` 12 | 13 | - `dev` branch 14 | - `dev` is the developing branch. 15 | - It's **RECOMMENDED** to commit feature PR to `dev`. 16 | - `main` branch 17 | - `main` is the release branch, we will make tag and publish version on this branch. 18 | - If it is a document modification, it can be submitted to this branch. 19 | 20 | ## More 21 | 22 | It is a good habit to create a feature request issue to discuss whether the feature is necessary before you implement it. However, it's unnecessary to create an issue to claim that you found a typo or improved the readability of documentation, just create a pull request. 23 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.4 2 | # Cargo build stage 3 | FROM rust:1.80-slim AS cargo-builder 4 | # Install Rust dependencies 5 | RUN --mount=type=cache,target=/var/cache/apt \ 6 | --mount=type=cache,target=/usr/local/cargo/registry \ 7 | apt-get update && apt-get install -y --no-install-recommends \ 8 | libdbus-1-dev libsoup3.0-dev libjavascriptcoregtk-4.0-dev \ 9 | libwebkit2gtk-4.0-dev build-essential curl wget libssl-dev \ 10 | libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \ 11 | gnome-video-effects 12 | COPY . /pake 13 | WORKDIR /pake/src-tauri 14 | # Build cargo packages and store cache 15 | RUN --mount=type=cache,target=/usr/local/cargo/registry \ 16 | cargo fetch && \ 17 | cargo build --release && \ 18 | mkdir -p /cargo-cache && \ 19 | cp -R /usr/local/cargo/registry /cargo-cache/ && \ 20 | cp -R /usr/local/cargo/git /cargo-cache/ 21 | # Verify the content of /cargo-cache && clean unnecessary files 22 | RUN ls -la /cargo-cache/registry && ls -la /cargo-cache/git && rm -rfd /cargo-cache/registry/src 23 | 24 | # Main build stage 25 | FROM rust:1.80-slim AS builder 26 | # Install Rust dependencies 27 | RUN --mount=type=cache,target=/var/cache/apt \ 28 | --mount=type=cache,target=/usr/local/cargo/registry \ 29 | apt-get update && apt-get install -y --no-install-recommends \ 30 | libdbus-1-dev libsoup3.0-dev libjavascriptcoregtk-4.1-dev \ 31 | libwebkit2gtk-4.1-dev build-essential curl wget libssl-dev \ 32 | libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev \ 33 | gnome-video-effects 34 | 35 | # Install Node.js 20.x 36 | RUN --mount=type=cache,target=/var/cache/apt \ 37 | curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ 38 | apt-get update && apt-get install -y nodejs 39 | 40 | # Copy project files 41 | COPY . /pake 42 | WORKDIR /pake 43 | 44 | # Copy Rust build artifacts 45 | COPY --from=cargo-builder /pake/src-tauri /pake/src-tauri 46 | COPY --from=cargo-builder /cargo-cache/git /usr/local/cargo/git 47 | COPY --from=cargo-builder /cargo-cache/registry /usr/local/cargo/registry 48 | 49 | # Install dependencies and build pake-cli 50 | RUN --mount=type=cache,target=/root/.npm \ 51 | npm install && \ 52 | npm run cli:build 53 | 54 | # Set up the entrypoint 55 | WORKDIR /output 56 | ENTRYPOINT ["node", "/pake/cli.js"] 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Tw93 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

English | 简体中文 | 日本語

2 |

3 | 4 |

5 |

Pake

6 |

利用 Rust 轻松构建轻量级多端桌面应用

7 |
8 | 9 | twitter 10 | 11 | telegram 12 | 13 | GitHub downloads 14 | 15 | GitHub commit 16 | 17 | GitHub closed issues 18 | 19 | 在 Colab 中打开 20 |
21 |
支持 Mac / Windows / Linux,关于 常用包下载命令行一键打包定制开发 可见下面文档,也欢迎去 讨论区 交流。
22 | 23 | ## 特征 24 | 25 | - 🎐 相比传统的 Electron 套壳打包,要小将近 20 倍,5M 上下。 26 | - 🚀 Pake 的底层使用的 Rust Tauri 框架,性能体验较 JS 框架要轻快不少,内存小很多。 27 | - 📦 不是单纯打包,实现了快捷键的透传、沉浸式的窗口、拖动、样式改写、去广告、产品的极简风格定制。 28 | - 👻 只是一个很简单的小玩具,用 Tauri 替代之前套壳网页打包的老思路,其实 PWA 也很好。 29 | 30 | ## 常用包下载 31 | 32 | 33 | 34 | 39 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 55 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 71 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 87 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 103 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 119 | 124 | 125 | 126 | 127 | 128 | 129 |
WeRead 35 | Mac 36 | Windows 37 | Linux 38 | Twitter 40 | Mac 41 | Windows 42 | Linux 43 |
Grok 51 | Mac 52 | Windows 53 | Linux 54 | DeepSeek 56 | Mac 57 | Windows 58 | Linux 59 |
ChatGPT 67 | Mac 68 | Windows 69 | Linux 70 | Gemini 72 | Mac 73 | Windows 74 | Linux 75 |
YouTube Music 83 | Mac 84 | Windows 85 | Linux 86 | YouTube 88 | Mac 89 | Windows 90 | Linux 91 |
LiZhi 99 | Mac 100 | Windows 101 | Linux 102 | ProgramMusic 104 | Mac 105 | Windows 106 | Linux 107 |
Excalidraw 115 | Mac 116 | Windows 117 | Linux 118 | XiaoHongShu 120 | Mac 121 | Windows 122 | Linux 123 |
130 | 131 |
132 | 133 | 🏂 更多应用可去 Release下载,此外点击可展开快捷键说明 134 | 135 |
136 | 137 | | Mac | Windows/Linux | 功能 | 138 | | --------------------------- | ------------------------------ | ------------------ | 139 | | + [ | Ctrl + | 返回上一个页面 | 140 | | + ] | Ctrl + | 去下一个页面 | 141 | | + | Ctrl + | 自动滚动到页面顶部 | 142 | | + | Ctrl + | 自动滚动到页面底部 | 143 | | + r | Ctrl + r | 刷新页面 | 144 | | + w | Ctrl + w | 隐藏窗口,非退出 | 145 | | + - | Ctrl + - | 缩小页面 | 146 | | + + | Ctrl + + | 放大页面 | 147 | | + = | Ctrl + = | 放大页面 | 148 | | + 0 | Ctrl + 0 | 重置页面缩放 | 149 | 150 | 此外还支持双击头部进行全屏切换,拖拽头部进行移动窗口,Mac 用户支持手势方式返回和去下一页,还有其他需求,欢迎提过来。 151 | 152 |
153 | 154 | ## 开始之前 155 | 156 | 1. **小白用户**:使用 「常用包下载」 方式来把玩 Pake 的能力,可去 [讨论群](https://github.com/tw93/Pake/discussions) 寻求帮助,也可试试 [Action](https://github.com/tw93/Pake/wiki/%E5%9C%A8%E7%BA%BF%E7%BC%96%E8%AF%91%EF%BC%88%E6%99%AE%E9%80%9A%E7%94%A8%E6%88%B7%E4%BD%BF%E7%94%A8%EF%BC%89) 方式。 157 | 2. **开发用户**:使用 「命令行一键打包」,对 Mac 比较友好,Windows / Linux 需折腾下 [环境配置](https://tauri.app/start/prerequisites/)。 158 | 3. **折腾用户**:假如你前端和 Rust 都会,那可试试下面的 「[定制开发](#定制开发)」,可深度二次开发定制你的功能。 159 | 160 | ## 命令行一键打包 161 | 162 | ![Pake](https://raw.githubusercontent.com/tw93/static/main/pake/pake.gif) 163 | 164 | **Pake 提供了命令行工具,可以更快捷方便地一键自定义打你需要的包,详细可见 [文档](./bin/README_CN.md)。** 165 | 166 | ```bash 167 | # 使用 npm 进行安装 168 | npm install -g pake-cli 169 | 170 | # 命令使用 171 | pake url [OPTIONS]... 172 | 173 | # 随便玩玩,首次由于安装环境会有些慢,后面就快了 174 | pake https://weekly.tw93.fun --name Weekly --hide-title-bar 175 | ``` 176 | 177 | 假如你不太会使用命令行,或许使用 **GitHub Actions 在线编译多系统版本** 是一个不错的选择,可查看 [文档](https://github.com/tw93/Pake/wiki/%E5%9C%A8%E7%BA%BF%E7%BC%96%E8%AF%91%EF%BC%88%E6%99%AE%E9%80%9A%E7%94%A8%E6%88%B7%E4%BD%BF%E7%94%A8%EF%BC%89)。 178 | 179 | ## 定制开发 180 | 181 | 开始前请确保电脑已经安装了 Rust `>=1.63` 和 Node `>=16 如 16.18.1` 的环境,此外需参考 [Tauri 文档](https://tauri.app/start/prerequisites/) 快速配置好环境才可以开始使用,假如你太不懂,使用上面的命令行打包会更加合适。 182 | 183 | ```sh 184 | # 安装依赖 185 | npm i 186 | 187 | # 本地开发[右键可打开调试模式] 188 | npm run dev 189 | 190 | # 打包应用 191 | npm run build 192 | 193 | ``` 194 | 195 | ## 高级使用 196 | 197 | 1. 代码结构可参考 [文档](https://github.com/tw93/Pake/wiki/Pake-%E7%9A%84%E4%BB%A3%E7%A0%81%E7%BB%93%E6%9E%84%E8%AF%B4%E6%98%8E),便于你在开发前了解更多。 198 | 2. 修改 src-tauri 目录下 `pake.json` 中的 `url` 和 `productName` 字段,需同步修改下 `tauri.config.json` 中的 `domain` 字段,以及 `tauri.xxx.conf.json` 中的 `icon` 和 `identifier` 字段,其中 `icon` 可以从 icons 目录选择一个,也可以去 [macOSicons](https://macosicons.com/#/) 下载符合效果的。 199 | 3. 关于窗口属性设置,可以在 `pake.json` 修改 windows 属性对应的 `width/height`,fullscreen 是否全屏,resizable 是否可以调整大小,假如想适配 Mac 沉浸式头部,可以将 hideTitleBar 设置成 `true`,找到 Header 元素加一个 padding-top 样式即可,不想适配改成 `false` 也行。 200 | 4. 此外样式改写、屏蔽广告、逻辑代码注入、容器消息通信、自定义快捷键可见 [高级用法](https://github.com/tw93/Pake/wiki/Pake-%E7%9A%84%E9%AB%98%E7%BA%A7%E7%94%A8%E6%B3%95)。 201 | 202 | ## 开发者 203 | 204 | Pake 的发展离不开这些 Hacker 们,一起贡献了大量能力,也欢迎关注他们 ❤️ 205 | 206 | 207 | 208 | 209 | 216 | 223 | 230 | 237 | 244 | 251 | 258 | 259 | 266 | 273 | 280 | 287 | 294 | 301 | 308 | 309 | 316 | 323 | 330 | 337 | 344 | 351 | 358 | 359 | 366 | 373 | 380 | 387 | 394 | 401 | 408 | 409 | 416 | 423 | 430 | 437 | 444 | 451 | 458 | 459 | 466 |
210 | 211 | tw93 212 |
213 | Tw93 214 |
215 |
217 | 218 | Tlntin 219 |
220 | Tlntin 221 |
222 |
224 | 225 | jeasonnow 226 |
227 | Santree 228 |
229 |
231 | 232 | pan93412 233 |
234 | Pan93412 235 |
236 |
238 | 239 | stone-w4tch3r 240 |
241 | Данил Бизимов 242 |
243 |
245 | 246 | wanghanzhen 247 |
248 | Volare 249 |
250 |
252 | 253 | liby 254 |
255 | Bryan Lee 256 |
257 |
260 | 261 | essesoul 262 |
263 | Essesoul 264 |
265 |
267 | 268 | YangguangZhou 269 |
270 | Jerry Zhou 271 |
272 |
274 | 275 | AielloChan 276 |
277 | Aiello 278 |
279 |
281 | 282 | m1911star 283 |
284 | Horus 285 |
286 |
288 | 289 | Pake-Actions 290 |
291 | Pake Actions 292 |
293 |
295 | 296 | eltociear 297 |
298 | Ikko Eltociear Ashimine 299 |
300 |
302 | 303 | mattbajorek 304 |
305 | Matt Bajorek 306 |
307 |
310 | 311 | QingZ11 312 |
313 | Steam 314 |
315 |
317 | 318 | Tianj0o 319 |
320 | Qitianjia 321 |
322 |
324 | 325 | xinyii 326 |
327 | Yi Xin 328 |
329 |
331 | 332 | exposir 333 |
334 | 孟世博 335 |
336 |
338 | 339 | 2nthony 340 |
341 | 2nthony 342 |
343 |
345 | 346 | ACGNnsj 347 |
348 | Null 349 |
350 |
352 | 353 | imabutahersiddik 354 |
355 | Abu Taher Siddik 356 |
357 |
360 | 361 | kidylee 362 |
363 | An Li 364 |
365 |
367 | 368 | nekomeowww 369 |
370 | Ayaka Neko 371 |
372 |
374 | 375 | turkyden 376 |
377 | Dengju Deng 378 |
379 |
381 | 382 | Fechin 383 |
384 | Fechin 385 |
386 |
388 | 389 | ImgBotApp 390 |
391 | Imgbot 392 |
393 |
395 | 396 | droid-Q 397 |
398 | Jiaqi Gu 399 |
400 |
402 | 403 | Milo123459 404 |
405 | Milo 406 |
407 |
410 | 411 | princemaple 412 |
413 | Po Chen 414 |
415 |
417 | 418 | geekvest 419 |
420 | Null 421 |
422 |
424 | 425 | houhoz 426 |
427 | Hyzhao 428 |
429 |
431 | 432 | lakca 433 |
434 | Null 435 |
436 |
438 | 439 | liudonghua123 440 |
441 | Liudonghua 442 |
443 |
445 | 446 | liusishan 447 |
448 | Liusishan 449 |
450 |
452 | 453 | piaoyidage 454 |
455 | Ranger 456 |
457 |
460 | 461 | hetz 462 |
463 | 贺天卓 464 |
465 |
467 | 468 | 469 | ## 常见问题 470 | 471 | 1. 页面中对图片元素鼠标右键打开菜单中选择下载图片或者其他事件不生效(常见于 MacOS 系统)。该问题是因为 MacOS 内置的 webview 无法支持该功能。 472 | 473 | ## 支持 474 | 475 | 1. 我有两只猫,一只叫汤圆,一只叫可乐,假如 Pake 让你生活更美好,可以给汤圆可乐 喂罐头 🥩。 476 | 2. 如果你喜欢 Pake,可以在 Github Star,更欢迎 [推荐](https://twitter.com/intent/tweet?url=https://github.com/tw93/Pake&text=%23Pake%20%E4%B8%80%E4%B8%AA%E5%BE%88%E7%AE%80%E5%8D%95%E7%9A%84%E7%94%A8%20Rust%20%E6%89%93%E5%8C%85%E7%BD%91%E9%A1%B5%E7%94%9F%E6%88%90%20Mac%20App%20%E7%9A%84%E5%B7%A5%E5%85%B7%EF%BC%8C%E7%9B%B8%E6%AF%94%E4%BC%A0%E7%BB%9F%E7%9A%84%20Electron%20%E5%A5%97%E5%A3%B3%E6%89%93%E5%8C%85%EF%BC%8C%E5%A4%A7%E5%B0%8F%E8%A6%81%E5%B0%8F%E5%B0%86%E8%BF%91%2040%20%E5%80%8D%EF%BC%8C%E4%B8%80%E8%88%AC%202M%20%E5%B7%A6%E5%8F%B3%EF%BC%8C%E5%BA%95%E5%B1%82%E4%BD%BF%E7%94%A8Tauri%20%EF%BC%8C%E6%80%A7%E8%83%BD%E4%BD%93%E9%AA%8C%E8%BE%83%20JS%20%E6%A1%86%E6%9E%B6%E8%A6%81%E8%BD%BB%E5%BF%AB%E4%B8%8D%E5%B0%91%EF%BC%8C%E5%86%85%E5%AD%98%E5%B0%8F%E5%BE%88%E5%A4%9A%EF%BC%8C%E6%94%AF%E6%8C%81%E5%BE%AE%E4%BF%A1%E8%AF%BB%E4%B9%A6%E3%80%81Twitter%E3%80%81Youtube%E3%80%81RunCode%E3%80%81Flomo%E3%80%81%E8%AF%AD%E9%9B%80%E7%AD%89%EF%BC%8C%E5%8F%AF%E4%BB%A5%E5%BE%88%E6%96%B9%E4%BE%BF%E4%BA%8C%E6%AC%A1%E5%BC%80%E5%8F%91~) 给你志同道合的朋友使用。 477 | 3. 可以关注我的 [Twitter](https://twitter.com/HiTw93) 获取到最新的 Pake 更新消息,也欢迎加入 [Telegram](https://t.me/+GclQS9ZnxyI2ODQ1) 聊天群。 478 | 4. 希望大伙玩的过程中有一种学习新技术的喜悦感,假如你发现有很适合做成桌面 App 的网页也很欢迎告诉我。 479 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 |

English | 简体中文

2 | 3 | ## Installation 4 | 5 | Ensure that your Node.js version is 18.0 or higher (e.g., 18.20.2). Avoid using `sudo` for the installation. If you encounter permission issues with npm, refer to [How to fix npm throwing error without sudo](https://stackoverflow.com/questions/16151018/how-to-fix-npm-throwing-error-without-sudo). 6 | 7 | ```bash 8 | npm install pake-cli -g 9 | ``` 10 | 11 |
12 | Considerations for Windows & Linux Users 13 | 14 | - **CRITICAL**: Consult [Tauri prerequisites](https://tauri.app/start/prerequisites/) before proceeding. 15 | - For Windows users (ensure that `Win10 SDK (10.0.19041.0)` and `Visual Studio build tool 2022 (>=17.2)` are installed), additional installations are required: 16 | 17 | 1. Microsoft Visual C++ 2015-2022 Redistributable (x64) 18 | 2. Microsoft Visual C++ 2015-2022 Redistributable (x86) 19 | 3. Microsoft Visual C++ 2012 Redistributable (x86) (optional) 20 | 4. Microsoft Visual C++ 2013 Redistributable (x86) (optional) 21 | 5. Microsoft Visual C++ 2008 Redistributable (x86) (optional) 22 | 23 | - For Ubuntu users, execute the following commands to install the required libraries before compiling: 24 | 25 | ```bash 26 | sudo apt install libdbus-1-dev \ 27 | libsoup-3.0-dev \ 28 | libjavascriptcoregtk-4.1-dev \ 29 | libwebkit2gtk-4.1-dev \ 30 | build-essential \ 31 | curl \ 32 | wget \ 33 | libssl-dev \ 34 | libgtk-3-dev \ 35 | libayatana-appindicator3-dev \ 36 | librsvg2-dev \ 37 | gnome-video-effects \ 38 | gnome-video-effects-extra 39 | ``` 40 | 41 |
42 | 43 | ## CLI Usage 44 | 45 | ```bash 46 | pake [url] [options] 47 | ``` 48 | 49 | The packaged application will be located in the current working directory by default. The first packaging might take some time due to environment configuration. Please be patient. 50 | 51 | > **Note**: Packaging requires the Rust environment. If Rust is not installed, you will be prompted for installation confirmation. In case of installation failure or timeout, you can [install it manually](https://www.rust-lang.org/tools/install). 52 | 53 | ### [url] 54 | 55 | The URL is the link to the web page you want to package or the path to a local HTML file. This is mandatory. 56 | 57 | ### [options] 58 | 59 | Various options are available for customization. You can pass corresponding arguments during packaging to achieve the desired configuration. 60 | 61 | #### [name] 62 | 63 | Specify the application name. If not provided, you will be prompted to enter it. It is recommended to use English. 64 | 65 | ```shell 66 | --name 67 | ``` 68 | 69 | #### [icon] 70 | 71 | Specify the application icon. Supports both local and remote files. By default, it uses the Pake brand icon. For custom icons, visit [icon icons](https://icon-icons.com) or [macOSicons](https://macosicons.com/#/). 72 | 73 | - For macOS, use `.icns` format. 74 | - For Windows, use `.ico` format. 75 | - For Linux, use `.png` format. 76 | 77 | ```shell 78 | --icon 79 | ``` 80 | 81 | #### [height] 82 | 83 | Set the height of the application window. Default is `780px`. 84 | 85 | ```shell 86 | --height 87 | ``` 88 | 89 | #### [width] 90 | 91 | Set the width of the application window. Default is `1200px`. 92 | 93 | ```shell 94 | --width 95 | ``` 96 | 97 | #### [hide-title-bar] 98 | 99 | Enable or disable immersive header. Default is `false`. Use the following command to enable this feature, macOS only. 100 | 101 | ```shell 102 | --hide-title-bar 103 | ``` 104 | 105 | #### [fullscreen] 106 | 107 | Determine whether the application launches in full screen. Default is `false`. Use the following command to enable full 108 | screen. 109 | 110 | ```shell 111 | --fullscreen 112 | ``` 113 | 114 | #### [activation-shortcut] 115 | 116 | Set the activation shortcut for the application. Default is ` `, it does not take effect, you can customize the activation shortcut with the following commands, e.g. `CmdOrControl+Shift+P`, use can refer to [available-modifiers](https://www.electronjs.org/docs/latest/api/accelerator#available-modifiers). 117 | 118 | ```shell 119 | --activation-shortcut 120 | ``` 121 | 122 | #### [always-on-top] 123 | 124 | Sets whether the window is always at the top level, defaults to `false`. 125 | 126 | ```shell 127 | --always-on-top 128 | ``` 129 | 130 | #### [app-version] 131 | 132 | Set the version number of the packaged application to be consistent with the naming format of version in package.json, defaulting to `1.0.0`. 133 | 134 | ```shell 135 | --app-version 136 | ``` 137 | 138 | #### [dark-mode] 139 | 140 | Force Mac to package applications using dark mode, default is `false`. 141 | 142 | ```shell 143 | --dark-mode 144 | ``` 145 | 146 | #### [disabled-web-shortcuts] 147 | 148 | Sets whether to disable web shortcuts in the original Pake container, defaults to `false`. 149 | 150 | ```shell 151 | --disabled-web-shortcuts 152 | ``` 153 | 154 | #### [multi-arch] 155 | 156 | Package the application to support both Intel and M1 chips, exclusively for macOS. Default is `false`. 157 | 158 | ##### Prerequisites 159 | 160 | - Note: After enabling this option, Rust must be installed using rustup from the official Rust website. Installation via brew is not supported. 161 | - For Intel chip users, install the arm64 cross-platform package to support M1 chips using the following command: 162 | 163 | ```shell 164 | rustup target add aarch64-apple-darwin 165 | ``` 166 | 167 | - For M1 chip users, install the x86 cross-platform package to support Intel chips using the following command: 168 | 169 | ```shell 170 | rustup target add x86_64-apple-darwin 171 | ``` 172 | 173 | ##### Usage 174 | 175 | ```shell 176 | --multi-arch 177 | ``` 178 | 179 | #### [targets] 180 | 181 | Choose the output package format, supporting `deb`, `appimage`, `rpm`, This option is only applicable to Linux and defaults to `deb`. 182 | 183 | ```shell 184 | --targets 185 | ``` 186 | 187 | #### [user-agent] 188 | 189 | Customize the browser user agent. Default is empty. 190 | 191 | ```shell 192 | --user-agent 193 | ``` 194 | 195 | #### [show-system-tray] 196 | 197 | Display the system tray. Default is not to display. Use the following command to enable the system tray. 198 | 199 | ```shell 200 | --show-system-tray 201 | ``` 202 | 203 | #### [system-tray-icon] 204 | 205 | Specify the system tray icon. This is only effective when the system tray is enabled. The icon must be in `.ico` or `.png` format and should be an image with dimensions ranging from 32x32 to 256x256 pixels. 206 | 207 | ```shell 208 | --system-tray-icon 209 | ``` 210 | 211 | #### [installer-language] 212 | 213 | Set the Windows Installer language. Options include `zh-CN`, `ja-JP`, More at [Tauri Document](https://tauri.app/distribute/windows-installer/#internationalization). Default is `en-US`. 214 | 215 | ```shell 216 | --installer-language 217 | ``` 218 | 219 | #### [use-local-file] 220 | 221 | Enable recursive copying. When the URL is a local file path, enabling this option will copy the folder containing the file specified in the URL, as well as all sub-files, to the Pake static folder. This is disabled by default. 222 | 223 | ```shell 224 | --use-local-file 225 | ``` 226 | 227 | #### [inject] 228 | 229 | Using `inject`, you can inject local absolute and relative path `css` and `js` files into the page you specify the `url` to customize it. For example, an adblock script that can be applied to any web page, or a `css` that optimizes the `UI` of a page, you can write it once to customize it. would only need to write the `app` once to generalize it to any other page. 230 | 231 | ```shell 232 | --inject ./tools/style.css,./tools/hotkey.js 233 | ``` 234 | 235 | #### [proxy-url] 236 | 237 | If you need to proxy requests for some reason, you can set the proxy address using the `proxy-url` option. 238 | 239 | ```shell 240 | --proxy-url 241 | ``` 242 | 243 | #### [debug] 244 | 245 | The typed package has dev-tools for debugging, in addition to outputting more log messages for debugging. 246 | 247 | ```shell 248 | --debug 249 | ``` 250 | 251 | ### Wait a moment 252 | 253 | After completing the above steps, your application should be successfully packaged. Please note that the packaging process may take some time depending on your system configuration and network conditions. Be patient, and once the packaging is complete, you can find the application installer in the specified directory. 254 | 255 | ## Development 256 | 257 | The `DEFAULT_DEV_PAKE_OPTIONS` configuration in `bin/defaults.ts` can be modified at development time to match the `pake-cli` configuration description. 258 | 259 | ```typescript 260 | export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = { 261 | ...DEFAULT_PAKE_OPTIONS, 262 | url: 'https://weread.qq.com', 263 | name: 'Weread', 264 | }; 265 | ``` 266 | 267 | then 268 | 269 | ```bash 270 | npm run cli:dev 271 | ``` 272 | 273 | The script will read the above configuration and packages the specified `app` using `watch` mode, and changes to the `pake-cli` code and `pake` are hot updated in real time. 274 | 275 | ## Docker 276 | 277 | ```shell 278 | # On Linux, you can run the Pake CLI via Docker 279 | docker run -it --rm \ # Run interactively, remove container after exit 280 | -v YOUR_DIR:/output \ # Files from container's /output will be in YOU_DIR 281 | ghcr.io/tw93/pake \ 282 | 283 | 284 | # For example: 285 | docker run -it --rm \ 286 | -v ./packages:/output \ 287 | ghcr.io/tw93/pake \ 288 | https://example.com --name myapp --icon ./icon.png 289 | 290 | ``` 291 | -------------------------------------------------------------------------------- /bin/README_CN.md: -------------------------------------------------------------------------------- 1 |

English | 简体中文

2 | 3 | ## 安装 4 | 5 | 请确保您的 Node.js 版本为 18 或更高版本(例如 18.7)。请避免使用 `sudo` 进行安装。如果 npm 报告权限问题,请参考 [如何在不使用 sudo 的情况下修复 npm 报错](https://stackoverflow.com/questions/16151018/how-to-fix-npm-throwing-error-without-sudo)。 6 | 7 | ```bash 8 | npm install pake-cli -g 9 | ``` 10 | 11 |
12 | Windows/Linux 注意事项 13 | 14 | - **非常重要**:请参阅 Tauri 的 [依赖项指南](https://tauri.app/start/prerequisites/)。 15 | - 对于 Windows 用户,请确保至少安装了 `Win10 SDK(10.0.19041.0)` 和 `Visual Studio Build Tools 2022(版本 17.2 或更高)`,此外还需要安装以下组件: 16 | 17 | 1. Microsoft Visual C++ 2015-2022 Redistributable (x64) 18 | 2. Microsoft Visual C++ 2015-2022 Redistributable (x86) 19 | 3. Microsoft Visual C++ 2012 Redistributable (x86)(可选) 20 | 4. Microsoft Visual C++ 2013 Redistributable (x86)(可选) 21 | 5. Microsoft Visual C++ 2008 Redistributable (x86)(可选) 22 | 23 | - 对于 Ubuntu 用户,在开始之前,建议运行以下命令以安装所需的依赖项: 24 | 25 | ```bash 26 | sudo apt install libdbus-1-dev \ 27 | libsoup3.0-dev \ 28 | libjavascriptcoregtk-4.1-dev \ 29 | libwebkit2gtk-4.1-dev \ 30 | build-essential \ 31 | curl \ 32 | wget \ 33 | libssl-dev \ 34 | libgtk-3-dev \ 35 | libayatana-appindicator3-dev \ 36 | librsvg2-dev \ 37 | gnome-video-effects \ 38 | gnome-video-effects-extra 39 | ``` 40 | 41 |
42 | 43 | ## 命令行使用 44 | 45 | ```bash 46 | pake [url] [options] 47 | ``` 48 | 49 | 应用程序的打包结果将默认保存在当前工作目录。由于首次打包需要配置环境,这可能需要一些时间,请耐心等待。 50 | 51 | > **注意**:打包过程需要使用 `Rust` 环境。如果您没有安装 `Rust`,系统会提示您是否要安装。如果遇到安装失败或超时的问题,您可以 [手动安装](https://www.rust-lang.org/tools/install)。 52 | 53 | ### [url] 54 | 55 | `url` 是您需要打包的网页链接 🔗 或本地 HTML 文件的路径,此参数为必填。 56 | 57 | ### [options] 58 | 59 | 您可以通过传递以下选项来定制打包过程: 60 | 61 | #### [name] 62 | 63 | 指定应用程序的名称,如果在输入时未指定,系统会提示您输入,建议使用单个英文名称,不要出现下划线或者中文。 64 | 65 | ```shell 66 | --name 67 | ``` 68 | 69 | #### [icon] 70 | 71 | 指定应用程序的图标,支持本地或远程文件。默认使用 Pake 的内置图标。您可以访问 [icon-icons](https://icon-icons.com) 72 | 或 [macOSicons](https://macosicons.com/#/) 下载自定义图标。 73 | 74 | - macOS 要求使用 `.icns` 格式。 75 | - Windows 要求使用 `.ico` 格式。 76 | - Linux 要求使用 `.png` 格式。 77 | 78 | ```shell 79 | --icon 80 | ``` 81 | 82 | #### [height] 83 | 84 | 设置应用窗口的高度,默认为 `780px`。 85 | 86 | ```shell 87 | --height 88 | ``` 89 | 90 | #### [width] 91 | 92 | 设置应用窗口的宽度,默认为 `1200px`。 93 | 94 | ```shell 95 | --width 96 | ``` 97 | 98 | #### [hide-title-bar] 99 | 100 | 设置是否启用沉浸式头部,默认为 `false`(不启用)。当前只对 macOS 上有效。 101 | 102 | ```shell 103 | --hide-title-bar 104 | ``` 105 | 106 | #### [fullscreen] 107 | 108 | 设置应用程序是否在启动时自动全屏,默认为 `false`。使用以下命令可以设置应用程序启动时自动全屏。 109 | 110 | ```shell 111 | --fullscreen 112 | ``` 113 | 114 | #### [activation-shortcut] 115 | 116 | 设置应用程序的激活快捷键。默认为空,不生效,可以使用以下命令自定义激活快捷键,例如 `CmdOrControl+Shift+P`,使用可参考 [available-modifiers](https://www.electronjs.org/docs/latest/api/accelerator#available-modifiers)。 117 | 118 | ```shell 119 | --activation-shortcut 120 | ``` 121 | 122 | #### [always-on-top] 123 | 124 | 设置是否窗口一直在最顶层,默认为 `false`。 125 | 126 | ```shell 127 | --always-on-top 128 | ``` 129 | 130 | #### [app-version] 131 | 132 | 设置打包应用的版本号,和 package.json 里面 version 命名格式一致,默认为 `1.0.0`。 133 | 134 | ```shell 135 | --app-version 136 | ``` 137 | 138 | #### [dark-mode] 139 | 140 | 强制 Mac 打包应用使用黑暗模式,默认为 `false`。 141 | 142 | ```shell 143 | --dark-mode 144 | ``` 145 | 146 | #### [disabled-web-shortcuts] 147 | 148 | 设置是否禁用原有 Pake 容器里面的网页操作快捷键,默认为 `false`。 149 | 150 | ```shell 151 | --disabled-web-shortcuts 152 | ``` 153 | 154 | #### [multi-arch] 155 | 156 | 设置打包结果同时支持 Intel 和 M1 芯片,仅适用于 macOS,默认为 `false`。 157 | 158 | ##### 准备工作 159 | 160 | - 注意:启用此选项后,需要使用 rust 官网的 rustup 安装 rust,不支持通过 brew 安装。 161 | - 对于 Intel 芯片用户,需要安装 arm64 跨平台包,以使安装包支持 M1 芯片。使用以下命令安装: 162 | 163 | ```shell 164 | rustup target add aarch64-apple-darwin 165 | ``` 166 | 167 | - 对于 M1 芯片用户,需要安装 x86 跨平台包,以使安装包支持 Intel 芯片。使用以下命令安装: 168 | 169 | ```shell 170 | rustup target add x86_64-apple-darwin 171 | ``` 172 | 173 | ##### 使用方法 174 | 175 | ```shell 176 | --multi-arch 177 | ``` 178 | 179 | #### [targets] 180 | 181 | 选择输出的包格式,支持 `deb`、`appimage`、`rpm`,此选项仅适用于 Linux,默认为 `deb`。 182 | 183 | ```shell 184 | --targets 185 | ``` 186 | 187 | #### [user-agent] 188 | 189 | 自定义浏览器的用户代理请求头,默认为空。 190 | 191 | ```shell 192 | --user-agent 193 | ``` 194 | 195 | #### [show-system-tray] 196 | 197 | 设置是否显示通知栏托盘,默认不显示。 198 | 199 | ```shell 200 | --show-system-tray 201 | ``` 202 | 203 | #### [system-tray-icon] 204 | 205 | 设置通知栏托盘图标,仅在启用通知栏托盘时有效。图标必须为 `.ico` 或 `.png` 格式,分辨率为 32x32 到 256x256 像素。 206 | 207 | ```shell 208 | --system-tray-icon 209 | ``` 210 | 211 | #### [installer-language] 212 | 213 | 设置 Windows 安装包语言。支持 `zh-CN`、`ja-JP`,更多在 [Tauri 文档](https://tauri.app/distribute/windows-installer/#internationalization)。默认为 `en-US`。 214 | 215 | ```shell 216 | --installer-language 217 | ``` 218 | 219 | #### [use-local-file] 220 | 221 | 当 `url` 为本地文件路径时,如果启用此选项,则会递归地将 `url` 路径文件所在的文件夹及其所有子文件复 222 | 223 | 制到 Pake 的静态文件夹。默认不启用。 224 | 225 | ```shell 226 | --use-local-file 227 | ``` 228 | 229 | #### [inject] 230 | 231 | 使用 `inject` 可以通过本地的绝对、相对路径的 `css` `js` 文件注入到你所指定 `url` 的页面中,从而为 232 | 233 | 其做定制化改造。举个例子:一段可以通用到任何网页的广告屏蔽脚本,或者是优化页面 `UI` 展的 `css`,你 234 | 235 | 只需要书写一次可以将其通用到任何其他网页打包的 `app`。 236 | 237 | ```shell 238 | --inject ./tools/style.css --inject ./tools/hotkey.js 239 | ``` 240 | 241 | #### [proxy-url] 242 | 243 | 假如你由于某些缘故需要代理请求,你可以通过 `proxy-url` 选项来设置代理地址。 244 | 245 | ```shell 246 | --proxy-url 247 | ``` 248 | 249 | #### [debug] 250 | 251 | 打出来的包具备 deb-tools 的调试模式,此外还会输出更多的日志信息用于调试。 252 | 253 | ```shell 254 | --debug 255 | ``` 256 | 257 | ### 稍等片刻 258 | 259 | 完成上述步骤后,您的应用程序应该已经成功打包。请注意,根据您的系统配置和网络状况,打包过程可能需要一些时间。请耐心等待,一旦打包完成,您就可以在指定的目录中找到应用程序安装包。 260 | 261 | ## 开发调试 262 | 263 | 开发时可以修改 `bin/defaults.ts` 中 `DEFAULT_DEV_PAKE_OPTIONS` 配置,配置项和 `pake-cli` 配置说明保持一致 264 | 265 | ```typescript 266 | export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = { 267 | ...DEFAULT_PAKE_OPTIONS, 268 | url: 'https://weread.qq.com', 269 | name: 'Weread', 270 | }; 271 | ``` 272 | 273 | 之后运行 274 | 275 | ```bash 276 | npm run cli:dev 277 | ``` 278 | 279 | 脚本会读取上述配置并使用 `watch` 模式打包指定的 `app`,对 `pake-cli` 代码和 `pake` 的修改都会实时热更新。 280 | 281 | ## Docker 使用 282 | 283 | ```shell 284 | # 在Linux上,您可以通过 Docker 运行 Pake CLI。 285 | docker run -it --rm \ # Run interactively, remove container after exit 286 | -v YOUR_DIR:/output \ # Files from container's /output will be in YOU_DIR 287 | ghcr.io/tw93/pake \ 288 | 289 | 290 | # For example: 291 | docker run -it --rm \ 292 | -v ./packages:/output \ 293 | ghcr.io/tw93/pake \ 294 | https://example.com --name myapp --icon ./icon.png 295 | 296 | ``` 297 | -------------------------------------------------------------------------------- /bin/builders/BaseBuilder.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fsExtra from 'fs-extra'; 3 | import chalk from 'chalk'; 4 | import prompts from 'prompts'; 5 | 6 | import { PakeAppOptions } from '@/types'; 7 | import { checkRustInstalled, installRust } from '@/helpers/rust'; 8 | import { mergeConfig } from '@/helpers/merge'; 9 | import tauriConfig from '@/helpers/tauriConfig'; 10 | import { npmDirectory } from '@/utils/dir'; 11 | import { getSpinner } from '@/utils/info'; 12 | import { shellExec } from '@/utils/shell'; 13 | import { isChinaDomain } from '@/utils/ip'; 14 | import { IS_MAC } from '@/utils/platform'; 15 | import logger from '@/options/logger'; 16 | 17 | export default abstract class BaseBuilder { 18 | protected options: PakeAppOptions; 19 | 20 | protected constructor(options: PakeAppOptions) { 21 | this.options = options; 22 | } 23 | 24 | async prepare() { 25 | const tauriSrcPath = path.join(npmDirectory, 'src-tauri'); 26 | const tauriTargetPath = path.join(tauriSrcPath, 'target'); 27 | const tauriTargetPathExists = await fsExtra.pathExists(tauriTargetPath); 28 | 29 | if (!IS_MAC && !tauriTargetPathExists) { 30 | logger.warn('✼ The first use requires installing system dependencies.'); 31 | logger.warn('✼ See more in https://tauri.app/start/prerequisites/.'); 32 | } 33 | 34 | if (!checkRustInstalled()) { 35 | const res = await prompts({ 36 | type: 'confirm', 37 | message: 'Rust not detected. Install now?', 38 | name: 'value', 39 | }); 40 | 41 | if (res.value) { 42 | await installRust(); 43 | } else { 44 | logger.error('✕ Rust required to package your webapp.'); 45 | process.exit(0); 46 | } 47 | } 48 | 49 | const isChina = await isChinaDomain('www.npmjs.com'); 50 | const spinner = getSpinner('Installing package...'); 51 | const rustProjectDir = path.join(tauriSrcPath, '.cargo'); 52 | const projectConf = path.join(rustProjectDir, 'config.toml'); 53 | await fsExtra.ensureDir(rustProjectDir); 54 | 55 | if (isChina) { 56 | logger.info('✺ Located in China, using npm/rsProxy CN mirror.'); 57 | const projectCnConf = path.join(tauriSrcPath, 'rust_proxy.toml'); 58 | await fsExtra.copy(projectCnConf, projectConf); 59 | await shellExec(`cd "${npmDirectory}" && npm install --registry=https://registry.npmmirror.com`); 60 | } else { 61 | await shellExec(`cd "${npmDirectory}" && npm install`); 62 | } 63 | spinner.succeed(chalk.green('Package installed!')); 64 | if (!tauriTargetPathExists) { 65 | logger.warn('✼ The first packaging may be slow, please be patient and wait, it will be faster afterwards.'); 66 | } 67 | } 68 | 69 | async build(url: string) { 70 | await this.buildAndCopy(url, this.options.targets); 71 | } 72 | 73 | async start(url: string) { 74 | await mergeConfig(url, this.options, tauriConfig); 75 | } 76 | 77 | async buildAndCopy(url: string, target: string) { 78 | const { name } = this.options; 79 | await mergeConfig(url, this.options, tauriConfig); 80 | 81 | // Build app 82 | const spinner = getSpinner('Building app...'); 83 | setTimeout(() => spinner.stop(), 3000); 84 | await shellExec(`cd "${npmDirectory}" && ${this.getBuildCommand()}`); 85 | 86 | // Copy app 87 | const fileName = this.getFileName(); 88 | const fileType = this.getFileType(target); 89 | const appPath = this.getBuildAppPath(npmDirectory, fileName, fileType); 90 | const distPath = path.resolve(`${name}.${fileType}`); 91 | await fsExtra.copy(appPath, distPath); 92 | await fsExtra.remove(appPath); 93 | logger.success('✔ Build success!'); 94 | logger.success('✔ App installer located in', distPath); 95 | } 96 | 97 | protected getFileType(target: string): string { 98 | return target; 99 | } 100 | 101 | abstract getFileName(): string; 102 | 103 | protected getBuildCommand(): string { 104 | // the debug option should support `--debug` and `--release` 105 | return this.options.debug ? 'npm run build:debug' : 'npm run build'; 106 | } 107 | 108 | protected getBasePath(): string { 109 | const basePath = this.options.debug ? 'debug' : 'release'; 110 | return `src-tauri/target/${basePath}/bundle/`; 111 | } 112 | 113 | protected getBuildAppPath(npmDirectory: string, fileName: string, fileType: string): string { 114 | return path.join(npmDirectory, this.getBasePath(), fileType.toLowerCase(), `${fileName}.${fileType}`); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /bin/builders/BuilderProvider.ts: -------------------------------------------------------------------------------- 1 | import BaseBuilder from './BaseBuilder'; 2 | import MacBuilder from './MacBuilder'; 3 | import WinBuilder from './WinBuilder'; 4 | import LinuxBuilder from './LinuxBuilder'; 5 | import { PakeAppOptions } from '@/types'; 6 | 7 | const { platform } = process; 8 | 9 | const buildersMap: Record BaseBuilder> = { 10 | darwin: MacBuilder, 11 | win32: WinBuilder, 12 | linux: LinuxBuilder, 13 | }; 14 | 15 | export default class BuilderProvider { 16 | static create(options: PakeAppOptions): BaseBuilder { 17 | const Builder = buildersMap[platform]; 18 | if (!Builder) { 19 | throw new Error('The current system is not supported!'); 20 | } 21 | return new Builder(options); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bin/builders/LinuxBuilder.ts: -------------------------------------------------------------------------------- 1 | import BaseBuilder from './BaseBuilder'; 2 | import { PakeAppOptions } from '@/types'; 3 | import tauriConfig from '@/helpers/tauriConfig'; 4 | 5 | export default class LinuxBuilder extends BaseBuilder { 6 | constructor(options: PakeAppOptions) { 7 | super(options); 8 | } 9 | 10 | getFileName() { 11 | const { name, targets } = this.options; 12 | const version = tauriConfig.version; 13 | 14 | let arch = process.arch === 'x64' ? 'amd64' : process.arch; 15 | if (arch === 'arm64' && (targets === 'rpm' || targets === 'appimage')) { 16 | arch = 'aarch64'; 17 | } 18 | 19 | // The RPM format uses different separators and version number formats 20 | if (targets === 'rpm') { 21 | return `${name}-${version}-1.${arch}`; 22 | } 23 | 24 | return `${name}_${version}_${arch}`; 25 | } 26 | 27 | // Customize it, considering that there are all targets. 28 | async build(url: string) { 29 | const targetTypes = ['deb', 'appimage', 'rpm']; 30 | for (const target of targetTypes) { 31 | if (this.options.targets === target) { 32 | await this.buildAndCopy(url, target); 33 | } 34 | } 35 | } 36 | 37 | protected getFileType(target: string): string { 38 | if (target === 'appimage') { 39 | return 'AppImage'; 40 | } 41 | return super.getFileType(target); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /bin/builders/MacBuilder.ts: -------------------------------------------------------------------------------- 1 | import tauriConfig from '@/helpers/tauriConfig'; 2 | import { PakeAppOptions } from '@/types'; 3 | import BaseBuilder from './BaseBuilder'; 4 | 5 | export default class MacBuilder extends BaseBuilder { 6 | constructor(options: PakeAppOptions) { 7 | super(options); 8 | this.options.targets = 'dmg'; 9 | } 10 | 11 | getFileName(): string { 12 | const { name } = this.options; 13 | let arch: string; 14 | if (this.options.multiArch) { 15 | arch = 'universal'; 16 | } else { 17 | arch = process.arch === 'arm64' ? 'aarch64' : process.arch; 18 | } 19 | return `${name}_${tauriConfig.version}_${arch}`; 20 | } 21 | 22 | protected getBuildCommand(): string { 23 | return this.options.multiArch ? 'npm run build:mac' : super.getBuildCommand(); 24 | } 25 | 26 | protected getBasePath(): string { 27 | return this.options.multiArch 28 | ? 'src-tauri/target/universal-apple-darwin/release/bundle' 29 | : super.getBasePath(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /bin/builders/WinBuilder.ts: -------------------------------------------------------------------------------- 1 | import BaseBuilder from './BaseBuilder'; 2 | import { PakeAppOptions } from '@/types'; 3 | import tauriConfig from '@/helpers/tauriConfig'; 4 | 5 | export default class WinBuilder extends BaseBuilder { 6 | constructor(options: PakeAppOptions) { 7 | super(options); 8 | this.options.targets = 'msi'; 9 | } 10 | 11 | getFileName(): string { 12 | const { name } = this.options; 13 | const { arch } = process; 14 | const language = tauriConfig.bundle.windows.wix.language[0]; 15 | return `${name}_${tauriConfig.version}_${arch}_${language}`; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /bin/cli.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { program, Option } from 'commander'; 3 | import log from 'loglevel'; 4 | import packageJson from '../package.json'; 5 | import BuilderProvider from './builders/BuilderProvider'; 6 | import { DEFAULT_PAKE_OPTIONS as DEFAULT, DEFAULT_PAKE_OPTIONS } from './defaults'; 7 | import { checkUpdateTips } from './helpers/updater'; 8 | import handleInputOptions from './options/index'; 9 | 10 | import { PakeCliOptions } from './types'; 11 | import { validateNumberInput, validateUrlInput } from './utils/validate'; 12 | 13 | const { green, yellow } = chalk; 14 | const logo = `${chalk.green(' ____ _')} 15 | ${green('| _ \\ __ _| | _____')} 16 | ${green('| |_) / _` | |/ / _ \\')} 17 | ${green('| __/ (_| | < __/')} ${yellow('https://github.com/tw93/pake')} 18 | ${green('|_| \\__,_|_|\\_\\___| can turn any webpage into a desktop app with Rust.')} 19 | `; 20 | 21 | program.addHelpText('beforeAll', logo).usage(`[url] [options]`).showHelpAfterError(); 22 | 23 | program 24 | .argument('[url]', 'The web URL you want to package', validateUrlInput) 25 | .option('--name ', 'Application name') 26 | .option('--icon ', 'Application icon', DEFAULT.icon) 27 | .option('--width ', 'Window width', validateNumberInput, DEFAULT.width) 28 | .option('--height ', 'Window height', validateNumberInput, DEFAULT.height) 29 | .option('--use-local-file', 'Use local file packaging', DEFAULT.useLocalFile) 30 | .option('--fullscreen', 'Start in full screen', DEFAULT.fullscreen) 31 | .option('--hide-title-bar', 'For Mac, hide title bar', DEFAULT.hideTitleBar) 32 | .option('--multi-arch', 'For Mac, both Intel and M1', DEFAULT.multiArch) 33 | .option('--inject ', 'Injection of .js or .css files', DEFAULT.inject) 34 | .option('--debug', 'Debug build and more output', DEFAULT.debug) 35 | .addOption(new Option('--proxy-url ', 'Proxy URL for all network requests').default(DEFAULT_PAKE_OPTIONS.proxyUrl).hideHelp()) 36 | .addOption(new Option('--user-agent ', 'Custom user agent').default(DEFAULT.userAgent).hideHelp()) 37 | .addOption(new Option('--targets ', 'For Linux, option "deb" or "appimage"').default(DEFAULT.targets).hideHelp()) 38 | .addOption(new Option('--app-version ', 'App version, the same as package.json version').default(DEFAULT.appVersion).hideHelp()) 39 | .addOption(new Option('--always-on-top', 'Always on the top level').default(DEFAULT.alwaysOnTop).hideHelp()) 40 | .addOption(new Option('--dark-mode', 'Force Mac app to use dark mode').default(DEFAULT.darkMode).hideHelp()) 41 | .addOption(new Option('--disabled-web-shortcuts', 'Disabled webPage shortcuts').default(DEFAULT.disabledWebShortcuts).hideHelp()) 42 | .addOption( 43 | new Option('--activation-shortcut ', 'Shortcut key to active App').default(DEFAULT_PAKE_OPTIONS.activationShortcut).hideHelp(), 44 | ) 45 | .addOption(new Option('--show-system-tray', 'Show system tray in app').default(DEFAULT.showSystemTray).hideHelp()) 46 | .addOption(new Option('--system-tray-icon ', 'Custom system tray icon').default(DEFAULT.systemTrayIcon).hideHelp()) 47 | .addOption(new Option('--installer-language ', 'Installer language').default(DEFAULT.installerLanguage).hideHelp()) 48 | .version(packageJson.version, '-v, --version', 'Output the current version') 49 | .action(async (url: string, options: PakeCliOptions) => { 50 | await checkUpdateTips(); 51 | 52 | if (!url) { 53 | program.outputHelp(str => { 54 | return str 55 | .split('\n') 56 | .filter(line => !/((-h,|--help)|((-v|-V),|--version))\s+.+$/.test(line)) 57 | .join('\n'); 58 | }); 59 | process.exit(0); 60 | } 61 | 62 | log.setDefaultLevel('info'); 63 | if (options.debug) { 64 | log.setLevel('debug'); 65 | } 66 | 67 | const appOptions = await handleInputOptions(options, url); 68 | log.debug('PakeAppOptions', appOptions); 69 | 70 | const builder = BuilderProvider.create(appOptions); 71 | await builder.prepare(); 72 | await builder.build(url); 73 | }); 74 | 75 | program.parse(); 76 | -------------------------------------------------------------------------------- /bin/defaults.ts: -------------------------------------------------------------------------------- 1 | import { PakeCliOptions } from './types.js'; 2 | 3 | export const DEFAULT_PAKE_OPTIONS: PakeCliOptions = { 4 | icon: '', 5 | height: 780, 6 | width: 1200, 7 | fullscreen: false, 8 | resizable: true, 9 | hideTitleBar: false, 10 | alwaysOnTop: false, 11 | appVersion: '1.0.0', 12 | darkMode: false, 13 | disabledWebShortcuts: false, 14 | activationShortcut: '', 15 | userAgent: '', 16 | showSystemTray: false, 17 | multiArch: false, 18 | targets: 'deb', 19 | useLocalFile: false, 20 | systemTrayIcon: '', 21 | proxyUrl: "", 22 | debug: false, 23 | inject: [], 24 | installerLanguage: 'en-US', 25 | }; 26 | 27 | // Just for cli development 28 | export const DEFAULT_DEV_PAKE_OPTIONS: PakeCliOptions & { url: string } = { 29 | ...DEFAULT_PAKE_OPTIONS, 30 | url: 'https://weread.qq.com', 31 | name: 'WeRead', 32 | hideTitleBar: true, 33 | }; 34 | -------------------------------------------------------------------------------- /bin/dev.ts: -------------------------------------------------------------------------------- 1 | import log from 'loglevel'; 2 | import { DEFAULT_DEV_PAKE_OPTIONS } from './defaults'; 3 | import handleInputOptions from './options/index'; 4 | import BuilderProvider from './builders/BuilderProvider'; 5 | 6 | async function startBuild() { 7 | log.setDefaultLevel('debug'); 8 | 9 | const appOptions = await handleInputOptions(DEFAULT_DEV_PAKE_OPTIONS, DEFAULT_DEV_PAKE_OPTIONS.url); 10 | log.debug('PakeAppOptions', appOptions); 11 | 12 | const builder = BuilderProvider.create(appOptions); 13 | await builder.prepare(); 14 | await builder.start(DEFAULT_DEV_PAKE_OPTIONS.url); 15 | } 16 | 17 | startBuild(); 18 | -------------------------------------------------------------------------------- /bin/helpers/merge.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fsExtra from 'fs-extra'; 3 | 4 | import { npmDirectory } from '@/utils/dir'; 5 | import combineFiles from '@/utils/combine'; 6 | import logger from '@/options/logger'; 7 | import { PakeAppOptions, PlatformMap } from '@/types'; 8 | import { tauriConfigDirectory } from '@/utils/dir'; 9 | 10 | export async function mergeConfig(url: string, options: PakeAppOptions, tauriConf: any) { 11 | const { 12 | width, 13 | height, 14 | fullscreen, 15 | hideTitleBar, 16 | alwaysOnTop, 17 | appVersion, 18 | darkMode, 19 | disabledWebShortcuts, 20 | activationShortcut, 21 | userAgent, 22 | showSystemTray, 23 | systemTrayIcon, 24 | useLocalFile, 25 | identifier, 26 | name, 27 | resizable = true, 28 | inject, 29 | proxyUrl, 30 | installerLanguage, 31 | } = options; 32 | 33 | const { platform } = process; 34 | 35 | // Set Windows parameters. 36 | const tauriConfWindowOptions = { 37 | width, 38 | height, 39 | fullscreen, 40 | resizable, 41 | hide_title_bar: hideTitleBar, 42 | activation_shortcut: activationShortcut, 43 | always_on_top: alwaysOnTop, 44 | dark_mode: darkMode, 45 | disabled_web_shortcuts: disabledWebShortcuts, 46 | }; 47 | Object.assign(tauriConf.pake.windows[0], { url, ...tauriConfWindowOptions }); 48 | 49 | tauriConf.productName = name; 50 | tauriConf.identifier = identifier; 51 | tauriConf.version = appVersion; 52 | 53 | if (platform == 'win32') { 54 | tauriConf.bundle.windows.wix.language[0] = installerLanguage; 55 | } 56 | 57 | //Judge the type of URL, whether it is a file or a website. 58 | const pathExists = await fsExtra.pathExists(url); 59 | if (pathExists) { 60 | logger.warn('✼ Your input might be a local file.'); 61 | tauriConf.pake.windows[0].url_type = 'local'; 62 | 63 | const fileName = path.basename(url); 64 | const dirName = path.dirname(url); 65 | 66 | const distDir = path.join(npmDirectory, 'dist'); 67 | const distBakDir = path.join(npmDirectory, 'dist_bak'); 68 | 69 | if (!useLocalFile) { 70 | const urlPath = path.join(distDir, fileName); 71 | await fsExtra.copy(url, urlPath); 72 | } else { 73 | fsExtra.moveSync(distDir, distBakDir, { overwrite: true }); 74 | fsExtra.copySync(dirName, distDir, { overwrite: true }); 75 | 76 | // ignore it, because about_pake.html have be erased. 77 | // const filesToCopyBack = ['cli.js', 'about_pake.html']; 78 | const filesToCopyBack = ['cli.js']; 79 | await Promise.all(filesToCopyBack.map(file => fsExtra.copy(path.join(distBakDir, file), path.join(distDir, file)))); 80 | } 81 | 82 | tauriConf.pake.windows[0].url = fileName; 83 | tauriConf.pake.windows[0].url_type = 'local'; 84 | } else { 85 | tauriConf.pake.windows[0].url_type = 'web'; 86 | } 87 | 88 | const platformMap: PlatformMap = { 89 | win32: 'windows', 90 | linux: 'linux', 91 | darwin: 'macos', 92 | }; 93 | const currentPlatform = platformMap[platform]; 94 | 95 | if (userAgent.length > 0) { 96 | tauriConf.pake.user_agent[currentPlatform] = userAgent; 97 | } 98 | 99 | tauriConf.pake.system_tray[currentPlatform] = showSystemTray; 100 | 101 | // Processing targets are currently only open to Linux. 102 | if (platform === 'linux') { 103 | delete tauriConf.bundle.linux.deb.files; 104 | const validTargets = ['deb', 'appimage', 'rpm']; 105 | if (validTargets.includes(options.targets)) { 106 | tauriConf.bundle.targets = [options.targets]; 107 | } else { 108 | logger.warn(`✼ The target must be one of ${validTargets.join(', ')}, the default 'deb' will be used.`); 109 | } 110 | } 111 | 112 | // Set icon. 113 | const platformIconMap: PlatformMap = { 114 | win32: { 115 | fileExt: '.ico', 116 | path: `png/${name.toLowerCase()}_256.ico`, 117 | defaultIcon: 'png/icon_256.ico', 118 | message: 'Windows icon must be .ico and 256x256px.', 119 | }, 120 | linux: { 121 | fileExt: '.png', 122 | path: `png/${name.toLowerCase()}_512.png`, 123 | defaultIcon: 'png/icon_512.png', 124 | message: 'Linux icon must be .png and 512x512px.', 125 | }, 126 | darwin: { 127 | fileExt: '.icns', 128 | path: `icons/${name.toLowerCase()}.icns`, 129 | defaultIcon: 'icons/icon.icns', 130 | message: 'macOS icon must be .icns type.', 131 | }, 132 | }; 133 | const iconInfo = platformIconMap[platform]; 134 | const exists = await fsExtra.pathExists(options.icon); 135 | if (exists) { 136 | let updateIconPath = true; 137 | let customIconExt = path.extname(options.icon).toLowerCase(); 138 | 139 | if (customIconExt !== iconInfo.fileExt) { 140 | updateIconPath = false; 141 | logger.warn(`✼ ${iconInfo.message}, but you give ${customIconExt}`); 142 | tauriConf.bundle.icon = [iconInfo.defaultIcon]; 143 | } else { 144 | const iconPath = path.join(npmDirectory, 'src-tauri/', iconInfo.path); 145 | tauriConf.bundle.resources = [iconInfo.path]; 146 | await fsExtra.copy(options.icon, iconPath); 147 | } 148 | 149 | if (updateIconPath) { 150 | tauriConf.bundle.icon = [options.icon]; 151 | } else { 152 | logger.warn(`✼ Icon will remain as default.`); 153 | } 154 | } else { 155 | logger.warn('✼ Custom icon path may be invalid, default icon will be used instead.'); 156 | tauriConf.bundle.icon = [iconInfo.defaultIcon]; 157 | } 158 | 159 | // Set tray icon path. 160 | let trayIconPath = platform === 'darwin' ? 'png/icon_512.png' : tauriConf.bundle.icon[0]; 161 | if (systemTrayIcon.length > 0) { 162 | try { 163 | await fsExtra.pathExists(systemTrayIcon); 164 | // 需要判断图标格式,默认只支持ico和png两种 165 | let iconExt = path.extname(systemTrayIcon).toLowerCase(); 166 | if (iconExt == '.png' || iconExt == '.ico') { 167 | const trayIcoPath = path.join(npmDirectory, `src-tauri/png/${name.toLowerCase()}${iconExt}`); 168 | trayIconPath = `png/${name.toLowerCase()}${iconExt}`; 169 | await fsExtra.copy(systemTrayIcon, trayIcoPath); 170 | } else { 171 | logger.warn(`✼ System tray icon must be .ico or .png, but you provided ${iconExt}.`); 172 | logger.warn(`✼ Default system tray icon will be used.`); 173 | } 174 | } catch { 175 | logger.warn(`✼ ${systemTrayIcon} not exists!`); 176 | logger.warn(`✼ Default system tray icon will remain unchanged.`); 177 | } 178 | } 179 | 180 | tauriConf.app.trayIcon.iconPath = trayIconPath; 181 | tauriConf.pake.system_tray_path = trayIconPath; 182 | 183 | delete tauriConf.app.trayIcon; 184 | 185 | const injectFilePath = path.join(npmDirectory, `src-tauri/src/inject/custom.js`); 186 | 187 | // inject js or css files 188 | if (inject?.length > 0) { 189 | if (!inject.every(item => item.endsWith('.css') || item.endsWith('.js'))) { 190 | logger.error('The injected file must be in either CSS or JS format.'); 191 | return; 192 | } 193 | const files = inject.map(filepath => (path.isAbsolute(filepath) ? filepath : path.join(process.cwd(), filepath))); 194 | tauriConf.pake.inject = files; 195 | await combineFiles(files, injectFilePath); 196 | } else { 197 | tauriConf.pake.inject = []; 198 | await fsExtra.writeFile(injectFilePath, ''); 199 | } 200 | tauriConf.pake.proxy_url = proxyUrl || ''; 201 | 202 | // Save config file. 203 | const platformConfigPaths: PlatformMap = { 204 | win32: 'tauri.windows.conf.json', 205 | darwin: 'tauri.macos.conf.json', 206 | linux: 'tauri.linux.conf.json', 207 | }; 208 | 209 | const configPath = path.join(tauriConfigDirectory, platformConfigPaths[platform]); 210 | 211 | const bundleConf = { bundle: tauriConf.bundle }; 212 | console.log('pakeConfig', tauriConf.pake); 213 | await fsExtra.outputJSON(configPath, bundleConf, { spaces: 4 }); 214 | const pakeConfigPath = path.join(tauriConfigDirectory, 'pake.json'); 215 | await fsExtra.outputJSON(pakeConfigPath, tauriConf.pake, { spaces: 4 }); 216 | 217 | let tauriConf2 = JSON.parse(JSON.stringify(tauriConf)); 218 | delete tauriConf2.pake; 219 | 220 | // delete tauriConf2.bundle; 221 | if (process.env.NODE_ENV === 'development') { 222 | tauriConf2.bundle = bundleConf.bundle; 223 | } 224 | const configJsonPath = path.join(tauriConfigDirectory, 'tauri.conf.json'); 225 | await fsExtra.outputJSON(configJsonPath, tauriConf2, { spaces: 4 }); 226 | } 227 | -------------------------------------------------------------------------------- /bin/helpers/rust.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import shelljs from 'shelljs'; 3 | 4 | import { getSpinner } from '@/utils/info'; 5 | import { IS_WIN } from '@/utils/platform'; 6 | import { shellExec } from '@/utils/shell'; 7 | import { isChinaDomain } from '@/utils/ip'; 8 | 9 | export async function installRust() { 10 | const isActions = process.env.GITHUB_ACTIONS; 11 | const isInChina = await isChinaDomain('sh.rustup.rs'); 12 | const rustInstallScriptForMac = 13 | isInChina && !isActions 14 | ? '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' 15 | : "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y"; 16 | const rustInstallScriptForWindows = 'winget install --id Rustlang.Rustup'; 17 | 18 | const spinner = getSpinner('Downloading Rust...'); 19 | 20 | try { 21 | await shellExec(IS_WIN ? rustInstallScriptForWindows : rustInstallScriptForMac); 22 | spinner.succeed(chalk.green('Rust installed successfully!')); 23 | } catch (error) { 24 | console.error('Error installing Rust:', error.message); 25 | spinner.fail(chalk.red('Rust installation failed!')); 26 | process.exit(1); 27 | } 28 | } 29 | 30 | export function checkRustInstalled() { 31 | return shelljs.exec('rustc --version', { silent: true }).code === 0; 32 | } 33 | -------------------------------------------------------------------------------- /bin/helpers/tauriConfig.ts: -------------------------------------------------------------------------------- 1 | import pakeConf from '../../src-tauri/pake.json'; 2 | import CommonConf from '../../src-tauri/tauri.conf.json'; 3 | import WinConf from '../../src-tauri/tauri.windows.conf.json'; 4 | import MacConf from '../../src-tauri/tauri.macos.conf.json'; 5 | import LinuxConf from '../../src-tauri/tauri.linux.conf.json'; 6 | 7 | const platformConfigs = { 8 | win32: WinConf, 9 | darwin: MacConf, 10 | linux: LinuxConf, 11 | }; 12 | 13 | const { platform } = process; 14 | // @ts-ignore 15 | const platformConfig = platformConfigs[platform]; 16 | 17 | let tauriConfig = { 18 | ...CommonConf, 19 | bundle: platformConfig.bundle, 20 | app: { 21 | ...CommonConf.app, 22 | trayIcon: { 23 | ...(platformConfig?.app?.trayIcon ?? {}), 24 | }, 25 | }, 26 | build: CommonConf.build, 27 | pake: pakeConf, 28 | }; 29 | 30 | export default tauriConfig; 31 | -------------------------------------------------------------------------------- /bin/helpers/updater.ts: -------------------------------------------------------------------------------- 1 | import updateNotifier from 'update-notifier'; 2 | import packageJson from '../../package.json'; 3 | 4 | export async function checkUpdateTips() { 5 | updateNotifier({ pkg: packageJson, updateCheckInterval: 1000 * 60 }).notify({ isGlobal: true }); 6 | } 7 | -------------------------------------------------------------------------------- /bin/options/icon.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import axios from 'axios'; 3 | import fsExtra from 'fs-extra'; 4 | import chalk from 'chalk'; 5 | import { dir } from 'tmp-promise'; 6 | 7 | import logger from './logger'; 8 | import { npmDirectory } from '@/utils/dir'; 9 | import { IS_LINUX, IS_WIN } from '@/utils/platform'; 10 | import { getSpinner } from '@/utils/info'; 11 | import { fileTypeFromBuffer } from 'file-type'; 12 | import { PakeAppOptions } from '@/types'; 13 | 14 | export async function handleIcon(options: PakeAppOptions) { 15 | if (options.icon) { 16 | if (options.icon.startsWith('http')) { 17 | return downloadIcon(options.icon); 18 | } else { 19 | return path.resolve(options.icon); 20 | } 21 | } else { 22 | logger.warn('✼ No icon given, default in use. For a custom icon, use --icon option.'); 23 | const iconPath = IS_WIN 24 | ? 'src-tauri/png/icon_256.ico' 25 | : IS_LINUX 26 | ? 'src-tauri/png/icon_512.png' 27 | : 'src-tauri/icons/icon.icns'; 28 | return path.join(npmDirectory, iconPath); 29 | } 30 | } 31 | 32 | export async function downloadIcon(iconUrl: string) { 33 | const spinner = getSpinner('Downloading icon...'); 34 | try { 35 | const iconResponse = await axios.get(iconUrl, { responseType: 'arraybuffer' }); 36 | const iconData = await iconResponse.data; 37 | 38 | if (!iconData) { 39 | return null; 40 | } 41 | 42 | const fileDetails = await fileTypeFromBuffer(iconData); 43 | if (!fileDetails) { 44 | return null; 45 | } 46 | 47 | const { path: tempPath } = await dir(); 48 | let iconPath = `${tempPath}/icon.${fileDetails.ext}`; 49 | // Fix this for linux 50 | if (IS_LINUX) { 51 | iconPath = 'png/linux_temp.png'; 52 | await fsExtra.outputFile(`${npmDirectory}/src-tauri/${iconPath}`, iconData); 53 | } else { 54 | await fsExtra.outputFile(iconPath, iconData); 55 | } 56 | await fsExtra.outputFile(iconPath, iconData); 57 | spinner.succeed(chalk.green('Icon downloaded successfully!')); 58 | return iconPath; 59 | } catch (error) { 60 | spinner.fail(chalk.red('Icon download failed!')); 61 | if (error.response && error.response.status === 404) { 62 | return null; 63 | } 64 | throw error; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /bin/options/index.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from 'fs-extra'; 2 | import logger from '@/options/logger'; 3 | 4 | import { handleIcon } from './icon'; 5 | import { getDomain } from '@/utils/url'; 6 | import { getIdentifier, promptText, capitalizeFirstLetter } from '@/utils/info'; 7 | import { PakeAppOptions, PakeCliOptions, PlatformMap } from '@/types'; 8 | 9 | function resolveAppName(name: string, platform: NodeJS.Platform): string { 10 | const domain = getDomain(name) || 'pake'; 11 | return platform !== 'linux' ? capitalizeFirstLetter(domain) : domain; 12 | } 13 | 14 | function isValidName(name: string, platform: NodeJS.Platform): boolean { 15 | const platformRegexMapping: PlatformMap = { 16 | linux: /^[a-z0-9]+(-[a-z0-9]+)*$/, 17 | default: /^[a-zA-Z0-9]+([-a-zA-Z0-9])*$/, 18 | }; 19 | const reg = platformRegexMapping[platform] || platformRegexMapping.default; 20 | return !!name && reg.test(name); 21 | } 22 | 23 | export default async function handleOptions(options: PakeCliOptions, url: string): Promise { 24 | const { platform } = process; 25 | const isActions = process.env.GITHUB_ACTIONS; 26 | let name = options.name; 27 | 28 | const pathExists = await fsExtra.pathExists(url); 29 | if (!options.name) { 30 | const defaultName = pathExists ? '' : resolveAppName(url, platform); 31 | const promptMessage = 'Enter your application name'; 32 | const namePrompt = await promptText(promptMessage, defaultName); 33 | name = namePrompt || defaultName; 34 | } 35 | 36 | if (!isValidName(name, platform)) { 37 | const LINUX_NAME_ERROR = `✕ name should only include lowercase letters, numbers, and dashes, and must contain at least one lowercase letter. Examples: com-123-xxx, 123pan, pan123, weread, we-read.`; 38 | const DEFAULT_NAME_ERROR = `✕ Name should only include letters and numbers, and dashes (dashes must not at the beginning), and must contain at least one letter. Examples: 123pan, 123Pan, Pan123, weread, WeRead, WERead, we-read.`; 39 | const errorMsg = platform === 'linux' ? LINUX_NAME_ERROR : DEFAULT_NAME_ERROR; 40 | logger.error(errorMsg); 41 | if (isActions) { 42 | name = resolveAppName(url, platform); 43 | logger.warn(`✼ Inside github actions, use the default name: ${name}`); 44 | } else { 45 | process.exit(1); 46 | } 47 | } 48 | 49 | const appOptions: PakeAppOptions = { 50 | ...options, 51 | name, 52 | identifier: getIdentifier(url), 53 | }; 54 | 55 | appOptions.icon = await handleIcon(appOptions); 56 | 57 | return appOptions; 58 | } 59 | -------------------------------------------------------------------------------- /bin/options/logger.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import log from 'loglevel'; 3 | 4 | const logger = { 5 | info(...msg: any[]) { 6 | log.info(...msg.map(m => chalk.white(m))); 7 | }, 8 | debug(...msg: any[]) { 9 | log.debug(...msg); 10 | }, 11 | error(...msg: any[]) { 12 | log.error(...msg.map(m => chalk.red(m))); 13 | }, 14 | warn(...msg: any[]) { 15 | log.info(...msg.map(m => chalk.yellow(m))); 16 | }, 17 | success(...msg: any[]) { 18 | log.info(...msg.map(m => chalk.green(m))); 19 | }, 20 | }; 21 | 22 | export default logger; 23 | -------------------------------------------------------------------------------- /bin/types.ts: -------------------------------------------------------------------------------- 1 | export interface PlatformMap { 2 | [key: string]: any; 3 | } 4 | 5 | export interface PakeCliOptions { 6 | // Application name 7 | name?: string; 8 | 9 | // Application icon 10 | icon: string; 11 | 12 | // Application window width, default 1200px 13 | width: number; 14 | 15 | // Application window height, default 780px 16 | height: number; 17 | 18 | // Whether the window is resizable, default true 19 | resizable: boolean; 20 | 21 | // Whether the window can be fullscreen, default false 22 | fullscreen: boolean; 23 | 24 | // Enable immersive header, default false. 25 | hideTitleBar: boolean; 26 | 27 | // Enable windows always on top, default false 28 | alwaysOnTop: boolean; 29 | 30 | // App version, the same as package.json version, default 1.0.0 31 | appVersion: string; 32 | 33 | // Force Mac to use dark mode, default false 34 | darkMode: boolean; 35 | 36 | // Disable web shortcuts, default false 37 | disabledWebShortcuts: boolean; 38 | 39 | // Set a shortcut key to wake up the app, default empty 40 | activationShortcut: string; 41 | 42 | // Custom User-Agent, default off 43 | userAgent: string; 44 | 45 | // Enable system tray, default off for macOS, on for Windows and Linux 46 | showSystemTray: boolean; 47 | 48 | // Tray icon, default same as app icon for Windows and Linux, macOS requires separate png or ico 49 | systemTrayIcon: string; 50 | 51 | // Recursive copy, when url is a local file path, if this option is enabled, the url path file and all its subFiles will be copied to the pake static file folder, default off 52 | useLocalFile: false; 53 | 54 | // Multi arch, supports both Intel and M1 chips, only for Mac 55 | multiArch: boolean; 56 | 57 | // Package output, valid for Linux users, default is deb, optional appimage, or all (i.e., output both deb and all); 58 | targets: string; 59 | 60 | // Debug mode, outputs more logs 61 | debug: boolean; 62 | 63 | /** External scripts that need to be injected into the page. */ 64 | inject: string[]; 65 | 66 | // Set Api Proxy 67 | proxyUrl: string; 68 | 69 | // Installer language, valid for Windows users, default is en-US 70 | installerLanguage: string; 71 | } 72 | 73 | export interface PakeAppOptions extends PakeCliOptions { 74 | identifier: string; 75 | } 76 | -------------------------------------------------------------------------------- /bin/utils/combine.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | 3 | export default async function combineFiles(files: string[], output: string) { 4 | const contents = files.map(file => { 5 | const fileContent = fs.readFileSync(file); 6 | if (file.endsWith('.css')) { 7 | return ( 8 | "window.addEventListener('DOMContentLoaded', (_event) => { const css = `" + 9 | fileContent + 10 | "`; const style = document.createElement('style'); style.innerHTML = css; document.head.appendChild(style); });" 11 | ); 12 | } 13 | 14 | return "window.addEventListener('DOMContentLoaded', (_event) => { " + fileContent + ' });'; 15 | }); 16 | fs.writeFileSync(output, contents.join('\n')); 17 | return files; 18 | } 19 | -------------------------------------------------------------------------------- /bin/utils/dir.ts: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { fileURLToPath } from 'url'; 3 | 4 | // Convert the current module URL to a file path 5 | const currentModulePath = fileURLToPath(import.meta.url); 6 | 7 | // Resolve the parent directory of the current module 8 | export const npmDirectory = path.join(path.dirname(currentModulePath), '..'); 9 | 10 | export const tauriConfigDirectory = 11 | process.env.NODE_ENV === 'development' 12 | ? path.join(npmDirectory, 'src-tauri', '.pake') 13 | : path.join(npmDirectory, 'src-tauri'); 14 | -------------------------------------------------------------------------------- /bin/utils/info.ts: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto'; 2 | import prompts from 'prompts'; 3 | import ora from 'ora'; 4 | import chalk from 'chalk'; 5 | 6 | // Generates an identifier based on the given URL. 7 | export function getIdentifier(url: string) { 8 | const postFixHash = crypto.createHash('md5').update(url).digest('hex').substring(0, 6); 9 | return `com.pake.${postFixHash}`; 10 | } 11 | 12 | export async function promptText(message: string, initial?: string): Promise { 13 | const response = await prompts({ 14 | type: 'text', 15 | name: 'content', 16 | message, 17 | initial, 18 | }); 19 | return response.content; 20 | } 21 | 22 | export function capitalizeFirstLetter(string: string) { 23 | return string.charAt(0).toUpperCase() + string.slice(1); 24 | } 25 | 26 | export function getSpinner(text: string) { 27 | const loadingType = { 28 | interval: 80, 29 | frames: ['✦', '✶', '✺', '✵', '✸', '✹', '✺'], 30 | }; 31 | return ora({ 32 | text: `${chalk.cyan(text)}\n`, 33 | spinner: loadingType, 34 | color: 'cyan', 35 | }).start(); 36 | } 37 | -------------------------------------------------------------------------------- /bin/utils/ip.ts: -------------------------------------------------------------------------------- 1 | import dns from 'dns'; 2 | import http from 'http'; 3 | import { promisify } from 'util'; 4 | 5 | import logger from '@/options/logger'; 6 | 7 | const resolve = promisify(dns.resolve); 8 | 9 | const ping = async (host: string) => { 10 | const lookup = promisify(dns.lookup); 11 | const ip = await lookup(host); 12 | const start = new Date(); 13 | 14 | // Prevent timeouts from affecting user experience. 15 | const requestPromise = new Promise((resolve, reject) => { 16 | const req = http.get(`http://${ip.address}`, res => { 17 | const delay = new Date().getTime() - start.getTime(); 18 | res.resume(); 19 | resolve(delay); 20 | }); 21 | 22 | req.on('error', err => { 23 | reject(err); 24 | }); 25 | }); 26 | 27 | const timeoutPromise = new Promise((_, reject) => { 28 | setTimeout(() => { 29 | reject(new Error('Request timed out after 3 seconds')); 30 | }, 1000); 31 | }); 32 | 33 | return Promise.race([requestPromise, timeoutPromise]); 34 | }; 35 | 36 | async function isChinaDomain(domain: string): Promise { 37 | try { 38 | const [ip] = await resolve(domain); 39 | return await isChinaIP(ip, domain); 40 | } catch (error) { 41 | logger.debug(`${domain} can't be parse!`); 42 | return true; 43 | } 44 | } 45 | 46 | async function isChinaIP(ip: string, domain: string): Promise { 47 | try { 48 | const delay = await ping(ip); 49 | logger.debug(`${domain} latency is ${delay} ms`); 50 | return delay > 1000; 51 | } catch (error) { 52 | logger.debug(`ping ${domain} failed!`); 53 | return true; 54 | } 55 | } 56 | 57 | export { isChinaDomain, isChinaIP }; 58 | -------------------------------------------------------------------------------- /bin/utils/platform.ts: -------------------------------------------------------------------------------- 1 | const { platform } = process; 2 | 3 | export const IS_MAC = platform === 'darwin'; 4 | export const IS_WIN = platform === 'win32'; 5 | export const IS_LINUX = platform === 'linux'; 6 | -------------------------------------------------------------------------------- /bin/utils/shell.ts: -------------------------------------------------------------------------------- 1 | import shelljs from 'shelljs'; 2 | import { npmDirectory } from './dir'; 3 | 4 | export function shellExec(command: string) { 5 | return new Promise((resolve, reject) => { 6 | shelljs.exec(command, { async: true, silent: false, cwd: npmDirectory }, code => { 7 | if (code === 0) { 8 | resolve(0); 9 | } else { 10 | reject(new Error(`Error occurred while executing command "${command}". Exit code: ${code}`)); 11 | } 12 | }); 13 | }); 14 | } 15 | -------------------------------------------------------------------------------- /bin/utils/url.ts: -------------------------------------------------------------------------------- 1 | import * as psl from 'psl'; 2 | import isUrl from 'is-url'; 3 | 4 | // Extracts the domain from a given URL. 5 | export function getDomain(inputUrl: string): string | null { 6 | try { 7 | const url = new URL(inputUrl); 8 | // Use PSL to parse domain names. 9 | const parsed = psl.parse(url.hostname); 10 | 11 | // If domain is available, split it and return the SLD. 12 | if ('domain' in parsed && parsed.domain) { 13 | return parsed.domain.split('.')[0]; 14 | } else { 15 | return null; 16 | } 17 | } catch (error) { 18 | return null; 19 | } 20 | } 21 | 22 | // Appends 'https://' protocol to the URL if not present. 23 | export function appendProtocol(inputUrl: string): string { 24 | try { 25 | new URL(inputUrl); 26 | return inputUrl; 27 | } catch { 28 | return `https://${inputUrl}`; 29 | } 30 | } 31 | 32 | // Normalizes the URL by ensuring it has a protocol and is valid. 33 | export function normalizeUrl(urlToNormalize: string): string { 34 | const urlWithProtocol = appendProtocol(urlToNormalize); 35 | 36 | if (isUrl(urlWithProtocol)) { 37 | return urlWithProtocol; 38 | } else { 39 | throw new Error(`Your url "${urlWithProtocol}" is invalid`); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /bin/utils/validate.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import { InvalidArgumentError } from 'commander'; 3 | import { normalizeUrl } from './url'; 4 | 5 | export function validateNumberInput(value: string) { 6 | const parsedValue = Number(value); 7 | if (isNaN(parsedValue)) { 8 | throw new InvalidArgumentError('Not a number.'); 9 | } 10 | return parsedValue; 11 | } 12 | 13 | export function validateUrlInput(url: string) { 14 | const isFile = fs.existsSync(url); 15 | 16 | if (!isFile) { 17 | try { 18 | return normalizeUrl(url); 19 | } catch (error) { 20 | throw new InvalidArgumentError(error.message); 21 | } 22 | } 23 | 24 | return url; 25 | } 26 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import './dist/cli.js'; 3 | -------------------------------------------------------------------------------- /default_app_list.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "name": "grok", 4 | "title": "Grok", 5 | "name_zh": "Grok", 6 | "url": "https://grok.com/" 7 | }, 8 | { 9 | "name": "gemini", 10 | "title": "Gemini", 11 | "name_zh": "Gemini", 12 | "url": "https://gemini.google.com/" 13 | }, 14 | { 15 | "name": "excalidraw", 16 | "title": "Excalidraw", 17 | "name_zh": "Excalidraw", 18 | "url": "https://excalidraw.com/" 19 | }, 20 | { 21 | "name": "programmusic", 22 | "title": "ProgramMusic", 23 | "name_zh": "ProgramMusic", 24 | "url": "https://musicforprogramming.net/" 25 | }, 26 | { 27 | "name": "twitter", 28 | "title": "Twitter", 29 | "name_zh": "推特", 30 | "url": "https://twitter.com/" 31 | }, 32 | { 33 | "name": "youtube", 34 | "title": "YouTube", 35 | "name_zh": "YouTube", 36 | "url": "https://www.youtube.com" 37 | }, 38 | { 39 | "name": "chatgpt", 40 | "title": "ChatGPT", 41 | "name_zh": "ChatGPT", 42 | "url": "https://chatgpt.com/" 43 | }, 44 | { 45 | "name": "flomo", 46 | "title": "Flomo", 47 | "name_zh": "浮墨", 48 | "url": "https://v.flomoapp.com/mine" 49 | }, 50 | { 51 | "name": "qwerty", 52 | "title": "Qwerty", 53 | "name_zh": "Qwerty", 54 | "url": "https://qwerty.kaiyi.cool/" 55 | }, 56 | { 57 | "name": "lizhi", 58 | "title": "LiZhi", 59 | "name_zh": "李志", 60 | "url": "https://lizhi.turkyden.com/?from=pake" 61 | }, 62 | { 63 | "name": "xiaohongshu", 64 | "title": "XiaoHongShu", 65 | "name_zh": "小红书", 66 | "url": "https://www.xiaohongshu.com/explore" 67 | }, 68 | { 69 | "name": "youtubemusic", 70 | "title": "YouTubeMusic", 71 | "name_zh": "YouTubeMusic", 72 | "url": "https://music.youtube.com/" 73 | }, 74 | { 75 | "name": "weread", 76 | "title": "WeRead", 77 | "name_zh": "微信阅读", 78 | "url": "https://weread.qq.com/" 79 | } 80 | ] 81 | -------------------------------------------------------------------------------- /icns2png.py: -------------------------------------------------------------------------------- 1 | """ 2 | 批量将icns文件转成png文件 3 | Batch convert ICNS files to PNG files 4 | """ 5 | import os 6 | 7 | try: 8 | from PIL import Image 9 | except ImportError: 10 | os.system("pip install Pillow") 11 | from PIL import Image 12 | 13 | if __name__ == "__main__": 14 | now_dir = os.path.dirname(os.path.abspath(__file__)) 15 | icons_dir = os.path.join(now_dir, "src-tauri", "icons") 16 | png_dir = os.path.join(now_dir, "src-tauri", "png") 17 | if not os.path.exists(png_dir): 18 | os.mkdir(png_dir) 19 | file_list = os.listdir(icons_dir) 20 | file_list = [file for file in file_list if file.endswith(".icns")] 21 | for file in file_list: 22 | icns_path = os.path.join(icons_dir, file) 23 | image = Image.open(icns_path) 24 | image_512 = image.copy().resize((512, 512)) 25 | image_256 = image.copy().resize((256, 256)) 26 | image_32 = image.copy().resize((32, 32)) 27 | image_name = os.path.splitext(file)[0] 28 | image_512_path = os.path.join(png_dir, image_name + "_512.png") 29 | image_256_path = os.path.join(png_dir, image_name + "_256.ico") 30 | image_32_path = os.path.join(png_dir, image_name + "_32.ico") 31 | image_512.save(image_512_path, "PNG") 32 | image_256.save(image_256_path, "ICO") 33 | image_32.save(image_32_path, "ICO") 34 | print("png file write success.") 35 | print(f"There are {len(os.listdir(png_dir))} png picture in ", png_dir) 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pake-cli", 3 | "version": "3.1.1", 4 | "description": "🤱🏻 Turn any webpage into a desktop app with Rust. 🤱🏻 利用 Rust 轻松构建轻量级多端桌面应用。", 5 | "engines": { 6 | "node": ">=16.0.0" 7 | }, 8 | "bin": { 9 | "pake": "./cli.js" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/tw93/pake.git" 14 | }, 15 | "author": { 16 | "name": "Tw93", 17 | "email": "tw93@qq.com" 18 | }, 19 | "keywords": [ 20 | "pake", 21 | "pake-cli", 22 | "rust", 23 | "tauri", 24 | "no-electron", 25 | "productivity" 26 | ], 27 | "files": [ 28 | "dist", 29 | "src-tauri", 30 | "cli.js" 31 | ], 32 | "scripts": { 33 | "start": "npm run dev", 34 | "dev": "npm run tauri dev", 35 | "build": "npm run tauri build --release", 36 | "build:debug": "npm run tauri build -- --debug", 37 | "build:mac": "npm run tauri build -- --target universal-apple-darwin", 38 | "build:config": "chmod +x script/app_config.mjs && node script/app_config.mjs", 39 | "analyze": "cd src-tauri && cargo bloat --release --crates", 40 | "tauri": "tauri", 41 | "cli": "rollup -c rollup.config.js --watch", 42 | "cli:dev": "cross-env NODE_ENV=development rollup -c rollup.config.js -w", 43 | "cli:build": "cross-env NODE_ENV=production rollup -c rollup.config.js", 44 | "prepublishOnly": "npm run cli:build" 45 | }, 46 | "type": "module", 47 | "exports": "./dist/pake.js", 48 | "license": "MIT", 49 | "dependencies": { 50 | "@tauri-apps/api": "^1.6.0", 51 | "@tauri-apps/cli": "^2.1.0", 52 | "axios": "^1.7.9", 53 | "chalk": "^5.4.1", 54 | "commander": "^11.1.0", 55 | "file-type": "^18.7.0", 56 | "fs-extra": "^11.2.0", 57 | "is-url": "^1.2.4", 58 | "loglevel": "^1.9.2", 59 | "ora": "^7.0.1", 60 | "prompts": "^2.4.2", 61 | "psl": "^1.15.0", 62 | "shelljs": "^0.8.5", 63 | "tmp-promise": "^3.0.3", 64 | "update-notifier": "^7.3.1" 65 | }, 66 | "devDependencies": { 67 | "@rollup/plugin-alias": "^5.1.1", 68 | "@rollup/plugin-commonjs": "^25.0.8", 69 | "@rollup/plugin-json": "^6.1.0", 70 | "@rollup/plugin-replace": "^5.0.7", 71 | "@rollup/plugin-terser": "^0.4.4", 72 | "@types/fs-extra": "^11.0.4", 73 | "@types/is-url": "^1.2.32", 74 | "@types/node": "^20.17.10", 75 | "@types/page-icon": "^0.3.6", 76 | "@types/prompts": "^2.4.9", 77 | "@types/psl": "^1.1.3", 78 | "@types/shelljs": "^0.8.15", 79 | "@types/tmp": "^0.2.6", 80 | "@types/update-notifier": "^6.0.8", 81 | "app-root-path": "^3.1.0", 82 | "cross-env": "^7.0.3", 83 | "rollup": "^4.29.1", 84 | "rollup-plugin-typescript2": "^0.36.0", 85 | "tslib": "^2.8.1", 86 | "typescript": "^5.7.2" 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import appRootPath from 'app-root-path'; 3 | import typescript from 'rollup-plugin-typescript2'; 4 | import alias from '@rollup/plugin-alias'; 5 | import commonjs from '@rollup/plugin-commonjs'; 6 | import json from '@rollup/plugin-json'; 7 | import replace from '@rollup/plugin-replace'; 8 | import chalk from 'chalk'; 9 | import { spawn, exec } from 'child_process'; 10 | 11 | const isProduction = process.env.NODE_ENV === 'production'; 12 | const devPlugins = !isProduction ? [pakeCliDevPlugin()] : []; 13 | 14 | export default { 15 | input: isProduction ? 'bin/cli.ts' : 'bin/dev.ts', 16 | output: { 17 | file: isProduction ? 'dist/cli.js' : 'dist/dev.js', 18 | format: 'es', 19 | sourcemap: !isProduction, 20 | }, 21 | watch: { 22 | include: 'bin/**', 23 | exclude: 'node_modules/**', 24 | }, 25 | plugins: [ 26 | json(), 27 | typescript({ 28 | tsconfig: 'tsconfig.json', 29 | clean: true, // Clear cache 30 | }), 31 | commonjs(), 32 | replace({ 33 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 34 | preventAssignment: true, 35 | }), 36 | alias({ 37 | entries: [{ find: '@', replacement: path.join(appRootPath.path, 'bin') }], 38 | }), 39 | ...devPlugins, 40 | ], 41 | }; 42 | 43 | function pakeCliDevPlugin() { 44 | let devChildProcess; 45 | let cliChildProcess; 46 | 47 | let devHasStarted = false; 48 | 49 | return { 50 | name: 'pake-cli-dev-plugin', 51 | buildEnd() { 52 | const command = 'node'; 53 | const cliCmdArgs = ['./dist/dev.js']; 54 | 55 | cliChildProcess = spawn(command, cliCmdArgs, { detached: true }); 56 | 57 | cliChildProcess.stdout.on('data', data => { 58 | console.log(chalk.green(data.toString())); 59 | }); 60 | 61 | cliChildProcess.stderr.on('data', data => { 62 | console.error(chalk.yellow(data.toString())); 63 | }); 64 | 65 | cliChildProcess.on('close', async code => { 66 | console.log(chalk.yellow(`cli running end with code: ${code}`)); 67 | if (devHasStarted) return; 68 | devHasStarted = true; 69 | devChildProcess = await exec( 70 | 'npm run tauri dev -- --config ./src-tauri/.pake/tauri.conf.json --features cli-build', 71 | ); 72 | 73 | devChildProcess.stdout.on('data', data => { 74 | console.log(chalk.green(data.toString())); 75 | }); 76 | 77 | devChildProcess.stderr.on('data', data => { 78 | console.error(chalk.yellow(data.toString())); 79 | }); 80 | 81 | devChildProcess.on('close', code => { 82 | console.log(chalk.yellow(`dev running end: ${code}`)); 83 | process.exit(code); 84 | }); 85 | }); 86 | }, 87 | }; 88 | } 89 | -------------------------------------------------------------------------------- /script/app_config.mjs: -------------------------------------------------------------------------------- 1 | import pakeJson from '../src-tauri/pake.json' assert { type: 'json' }; 2 | import tauriJson from '../src-tauri/tauri.conf.json' assert { type: 'json' }; 3 | import windowsJson from '../src-tauri/tauri.windows.conf.json' assert { type: 'json' }; 4 | import macosJson from '../src-tauri/tauri.macos.conf.json' assert { type: 'json' }; 5 | import linuxJson from '../src-tauri/tauri.linux.conf.json' assert { type: 'json' }; 6 | 7 | import { writeFileSync, existsSync, copyFileSync } from 'fs'; 8 | import os from 'os'; 9 | 10 | const desktopEntry = `[Desktop Entry] 11 | Encoding=UTF-8 12 | Categories=Office 13 | Exec=com-pake-${process.env.NAME} 14 | Icon=com-pake-${process.env.NAME} 15 | Name=com-pake-${process.env.NAME} 16 | Name[zh_CN]=${process.env.NAME_ZH} 17 | StartupNotify=true 18 | Terminal=false 19 | Type=Application 20 | `; 21 | 22 | const variables = { 23 | url: process.env.URL, 24 | name: process.env.NAME, 25 | title: process.env.TITLE, 26 | nameZh: process.env.NAME_ZH, 27 | 28 | pakeConfigPath: 'src-tauri/pake.json', 29 | tauriConfigPath: 'src-tauri/tauri.conf.json', 30 | identifier: `com.pake.${process.env.NAME}`, 31 | 32 | linux: { 33 | configFilePath: 'src-tauri/tauri.linux.conf.json', 34 | iconPath: `src-tauri/png/${process.env.NAME}_512.png`, 35 | productName: `com-pake-${process.env.NAME}`, 36 | defaultIconPath: 'src-tauri/png/icon_512.png', 37 | icon: [`png/${process.env.NAME}_512.png`], 38 | desktopEntry, 39 | desktopEntryPath: `src-tauri/assets/com-pake-${process.env.NAME}.desktop`, 40 | desktopEntryConfig: { 41 | configKey: `/usr/share/applications/com-pake-${process.env.NAME}.desktop`, 42 | configValue: `assets/com-pake-${process.env.NAME}.desktop`, 43 | }, 44 | }, 45 | macos: { 46 | configFilePath: 'src-tauri/tauri.macos.conf.json', 47 | iconPath: `src-tauri/icons/${process.env.NAME}.icns`, 48 | defaultPath: 'src-tauri/icons/icon.icns', 49 | icon: [`icons/${process.env.NAME}.icns`], 50 | }, 51 | windows: { 52 | configFilePath: 'src-tauri/tauri.windows.conf.json', 53 | iconPath: `src-tauri/png/${process.env.NAME}_32.ico`, 54 | defaultPath: 'src-tauri/png/icon_32.ico', 55 | hdIconPath: `src-tauri/png/${process.env.NAME}_256.ico`, 56 | hdDefaultPath: 'src-tauri/png/icon_256.ico', 57 | icon: [`png/${process.env.NAME}_256.ico`, `png/${process.env.NAME}_32.ico`], 58 | resources: [`png/${process.env.NAME}_32.ico`], 59 | }, 60 | }; 61 | 62 | validate(); 63 | 64 | updatePakeJson(); 65 | 66 | updateTauriJson(); 67 | 68 | let platformVariables; 69 | let platformConfig; 70 | 71 | switch (os.platform()) { 72 | case 'linux': 73 | platformVariables = variables.linux; 74 | platformConfig = linuxJson; 75 | updateDesktopEntry(); 76 | break; 77 | case 'darwin': 78 | platformVariables = variables.macos; 79 | platformConfig = macosJson; 80 | break; 81 | case 'win32': 82 | platformConfig = windowsJson; 83 | platformVariables = variables.windows; 84 | updateResources(); 85 | updateIconFile(platformVariables.hdIconPath, platformVariables.hdDefaultPath); 86 | break; 87 | } 88 | 89 | updateIconFile(platformVariables.iconPath, platformVariables.defaultIconPath); 90 | 91 | updatePlatformConfig(platformConfig, platformVariables); 92 | 93 | save(); 94 | 95 | function validate() { 96 | if (!('URL' in process.env)) { 97 | console.log('URL is not set'); 98 | process.exit(1); 99 | } 100 | 101 | console.log(`URL: ${process.env.URL}`); 102 | 103 | if (!('NAME' in process.env)) { 104 | console.log('NAME is not set'); 105 | process.exit(1); 106 | } 107 | 108 | console.log(`NAME: ${process.env.NAME}`); 109 | 110 | if (!('TITLE' in process.env)) { 111 | console.log('TITLE is not set'); 112 | process.exit(1); 113 | } 114 | 115 | console.log(`TITLE: ${process.env.TITLE}`); 116 | 117 | if (!('NAME_ZH' in process.env)) { 118 | console.log('NAME_ZH is not set'); 119 | process.exit(1); 120 | } 121 | 122 | console.log(`NAME_ZH: ${process.env.NAME_ZH}`); 123 | } 124 | 125 | function updatePakeJson() { 126 | pakeJson.windows[0].url = variables.url; 127 | } 128 | 129 | function updateTauriJson() { 130 | tauriJson.productName = variables.title; 131 | writeFileSync('src-tauri/tauri.conf.json', JSON.stringify(tauriJson, null, 2)); 132 | } 133 | 134 | function updateIconFile(iconPath, defaultIconPath) { 135 | if (!existsSync(iconPath)) { 136 | console.warn(`Icon for ${process.env.NAME} not found, will use default icon`); 137 | copyFileSync(defaultIconPath, iconPath); 138 | } 139 | } 140 | 141 | function updatePlatformConfig(platformConfig, platformVariables) { 142 | platformConfig.bundle['icon'] = platformVariables.icon; 143 | platformConfig.identifier = variables.identifier; 144 | } 145 | 146 | function save() { 147 | writeFileSync(variables.pakeConfigPath, JSON.stringify(pakeJson, null, 2)); 148 | writeFileSync(variables.tauriConfigPath, JSON.stringify(tauriJson, null, 2)); 149 | 150 | writeFileSync(variables.linux.configFilePath, JSON.stringify(linuxJson, null, 2)); 151 | writeFileSync(platformVariables.configFilePath, JSON.stringify(platformConfig, null, 2)); 152 | 153 | writeFileSync(variables.macos.configFilePath, JSON.stringify(macosJson, null, 2)); 154 | 155 | writeFileSync(variables.windows.configFilePath, JSON.stringify(windowsJson, null, 2)); 156 | } 157 | 158 | function updateDesktopEntry() { 159 | linuxJson.bundle.linux.deb.files = {}; 160 | linuxJson.bundle.linux.deb.files[variables.linux.desktopEntryConfig.configKey] = variables.linux.desktopEntryConfig.configValue; 161 | writeFileSync(variables.linux.desktopEntryPath, variables.linux.desktopEntry); 162 | } 163 | 164 | function updateResources() { 165 | windowsJson.bundle.resources = variables.windows.resources; 166 | } 167 | -------------------------------------------------------------------------------- /script/build_with_pake_cli.js: -------------------------------------------------------------------------------- 1 | import shelljs from 'shelljs'; 2 | import axios from 'axios'; 3 | import fs from 'fs'; 4 | 5 | const { exec, cd, mv } = shelljs; 6 | 7 | console.log('Welcome to use pake-cli to build app'); 8 | console.log('Node.js info in your localhost ', process.version); 9 | console.log('\n=======================\n'); 10 | console.log('Pake parameters is: '); 11 | console.log('url: ', process.env.URL); 12 | console.log('name: ', process.env.NAME); 13 | console.log('icon: ', process.env.ICON); 14 | console.log('height: ', process.env.HEIGHT); 15 | console.log('width: ', process.env.WIDTH); 16 | console.log('fullscreen: ', process.env.FULLSCREEN); 17 | console.log('hide-title-bar: ', process.env.HIDE_TITLE_BAR); 18 | console.log('is multi arch? only for Mac: ', process.env.MULTI_ARCH); 19 | console.log('targets type? only for Linux: ', process.env.TARGETS); 20 | console.log('===========================\n'); 21 | 22 | cd('node_modules/pake-cli'); 23 | let params = `node cli.js ${process.env.URL} --name ${process.env.NAME} --height ${process.env.HEIGHT} --width ${process.env.WIDTH}`; 24 | 25 | if (process.env.HIDE_TITLE_BAR === 'true') { 26 | params = `${params} --hide-title-bar`; 27 | } 28 | 29 | if (process.env.FULLSCREEN === 'true') { 30 | params = `${params} --fullscreen`; 31 | } 32 | 33 | if (process.env.MULTI_ARCH === 'true') { 34 | exec('rustup target add aarch64-apple-darwin'); 35 | params = `${params} --multi-arch`; 36 | } 37 | 38 | if (process.env.TARGETS) { 39 | params = `${params} --targets ${process.env.TARGETS}`; 40 | } 41 | 42 | if (process.platform === 'win32' || process.platform === 'linux') { 43 | params = `${params} --show-system-tray`; 44 | } 45 | 46 | const downloadIcon = async iconFile => { 47 | try { 48 | const response = await axios.get(process.env.ICON, { responseType: 'arraybuffer' }); 49 | fs.writeFileSync(iconFile, response.data); 50 | return `${params} --icon ${iconFile}`; 51 | } catch (error) { 52 | console.error('Error occurred during icon download: ', error); 53 | } 54 | }; 55 | 56 | const main = async () => { 57 | if (process.env.ICON && process.env.ICON !== '') { 58 | let iconFile; 59 | switch (process.platform) { 60 | case 'linux': 61 | iconFile = 'icon.png'; 62 | break; 63 | case 'darwin': 64 | iconFile = 'icon.icns'; 65 | break; 66 | case 'win32': 67 | iconFile = 'icon.ico'; 68 | break; 69 | default: 70 | console.log("Unable to detect your OS system, won't download the icon!"); 71 | process.exit(1); 72 | } 73 | 74 | params = await downloadIcon(iconFile); 75 | } else { 76 | console.log("Won't download the icon as ICON environment variable is not defined!"); 77 | } 78 | 79 | console.log('Pake parameters is: ', params); 80 | console.log('Compile....'); 81 | exec(params); 82 | 83 | if (!fs.existsSync('output')) { 84 | fs.mkdirSync('output'); 85 | } 86 | mv(`${process.env.NAME}.*`, 'output/'); 87 | console.log('Build Success'); 88 | cd('../..'); 89 | }; 90 | 91 | main(); 92 | -------------------------------------------------------------------------------- /src-tauri/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | -------------------------------------------------------------------------------- /src-tauri/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "pake" 3 | version = "3.1.0" 4 | description = "🤱🏻 Turn any webpage into a desktop app with Rust." 5 | authors = ["Tw93"] 6 | license = "MIT" 7 | repository = "https://github.com/tw93/Pake" 8 | edition = "2021" 9 | rust-version = "1.78.0" 10 | 11 | [lib] 12 | name = "app_lib" 13 | crate-type = ["staticlib", "cdylib", "lib"] 14 | 15 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 16 | 17 | [build-dependencies] 18 | tauri-build = { version = "2.0.4", features = [] } 19 | 20 | [dependencies] 21 | serde_json = "1.0.134" 22 | serde = { version = "1.0.217", features = ["derive"] } 23 | tokio = { version = "1.42.0", features = ["full"] } 24 | tauri = { version = "2.2.0", features = ["tray-icon", "image-ico", "image-png", "macos-proxy"] } 25 | tauri-plugin-window-state = "2.2.0" 26 | tauri-plugin-oauth = "2.0.0" 27 | tauri-plugin-http = "2.2.0" 28 | tauri-plugin-global-shortcut = { version = "2.2.0" } 29 | tauri-plugin-shell = "2.2.0" 30 | tauri-plugin-single-instance = "2.2.0" 31 | tauri-plugin-notification = "2.2.0" 32 | 33 | [features] 34 | # this feature is used for development builds from development cli 35 | cli-build = [] 36 | # by default Tauri runs in production mode 37 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL 38 | default = ["custom-protocol"] 39 | # this feature is used for production builds where `devPath` points to the filesystem 40 | # DO NOT remove this 41 | custom-protocol = ["tauri/custom-protocol"] 42 | -------------------------------------------------------------------------------- /src-tauri/assets/com-tw93-weread.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Encoding=UTF-8 3 | Categories=Office 4 | Exec=com-pake-weread 5 | Icon=com-pake-weread 6 | Name=com-pake-weread 7 | Name[zh_CN]=微信阅读 8 | StartupNotify=true 9 | Terminal=false 10 | Type=Application 11 | -------------------------------------------------------------------------------- /src-tauri/assets/main.wxs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {{#if allow_downgrades}} 41 | 42 | {{else}} 43 | 44 | {{/if}} 45 | 46 | 47 | Installed AND NOT UPGRADINGPRODUCTCODE 48 | 49 | 50 | 51 | 52 | {{#if banner_path}} 53 | 54 | {{/if}} 55 | {{#if dialog_image_path}} 56 | 57 | {{/if}} 58 | {{#if license}} 59 | 60 | {{/if}} 61 | 62 | 63 | 64 | 65 | 66 | 67 | {{#if homepage}} 68 | 69 | 70 | 71 | {{/if}} 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | WIXUI_EXITDIALOGOPTIONALCHECKBOX = 1 and NOT Installed 86 | 87 | 88 | 89 | {{#unless license}} 90 | 91 | 1 96 | 1 101 | {{/unless}} 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | {{#each deep_link_protocols as |protocol| ~}} 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | {{/each~}} 140 | 141 | 142 | 143 | {{#each file_associations as |association| ~}} 144 | {{#each association.ext as |ext| ~}} 145 | 146 | 147 | 148 | 149 | 150 | {{/each~}} 151 | {{/each~}} 152 | 153 | {{#each binaries as |bin| ~}} 154 | 155 | 156 | 157 | {{/each~}} 158 | {{#if enable_elevated_update_task}} 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | {{/if}} 169 | {{resources}} 170 | 171 | 172 | 177 | 178 | 180 | 181 | 187 | 188 | 189 | 190 | 191 | 192 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | {{#each merge_modules as |msm| ~}} 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | {{/each~}} 214 | 215 | 224 | 225 | 226 | 227 | {{#each resource_file_ids as |resource_file_id| ~}} 228 | 229 | {{/each~}} 230 | 231 | {{#if enable_elevated_update_task}} 232 | 233 | 234 | 235 | {{/if}} 236 | 237 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 252 | 253 | {{#each binaries as |bin| ~}} 254 | 255 | {{/each~}} 256 | 257 | 258 | 259 | 260 | {{#each component_group_refs as |id| ~}} 261 | 262 | {{/each~}} 263 | {{#each component_refs as |id| ~}} 264 | 265 | {{/each~}} 266 | {{#each feature_group_refs as |id| ~}} 267 | 268 | {{/each~}} 269 | {{#each feature_refs as |id| ~}} 270 | 271 | {{/each~}} 272 | {{#each merge_refs as |id| ~}} 273 | 274 | {{/each~}} 275 | 276 | 277 | {{#if install_webview}} 278 | 279 | 280 | 281 | 282 | 283 | 284 | {{#if download_bootstrapper}} 285 | 286 | 287 | 288 | 289 | 290 | 291 | {{/if}} 292 | 293 | 294 | {{#if webview2_bootstrapper_path}} 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | {{/if}} 303 | 304 | 305 | {{#if webview2_installer_path}} 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | {{/if}} 314 | 315 | {{/if}} 316 | 317 | {{#if enable_elevated_update_task}} 318 | 319 | 326 | 327 | 328 | NOT(REMOVE) 329 | 330 | 331 | 332 | 337 | 338 | 339 | (REMOVE = "ALL") AND NOT UPGRADINGPRODUCTCODE 340 | 341 | 342 | {{/if}} 343 | 344 | 345 | AUTOLAUNCHAPP AND NOT Installed 346 | 347 | 348 | 349 | 350 | 351 | -------------------------------------------------------------------------------- /src-tauri/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | tauri_build::build() 3 | } 4 | -------------------------------------------------------------------------------- /src-tauri/capabilities/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "../gen/schemas/desktop-schema.json", 3 | "identifier": "pake-capability", 4 | "description": "Capability for the pake app.", 5 | "webviews": ["pake"], 6 | "remote": { 7 | "urls": ["https://*.*"] 8 | }, 9 | "permissions": [ 10 | "shell:allow-open", 11 | "core:window:allow-theme", 12 | "core:window:allow-start-dragging", 13 | "core:window:allow-toggle-maximize", 14 | "core:window:allow-is-fullscreen", 15 | "core:window:allow-set-fullscreen", 16 | "core:webview:allow-internal-toggle-devtools", 17 | "notification:allow-is-permission-granted", 18 | "notification:allow-notify", 19 | "notification:allow-get-active", 20 | "notification:allow-register-listener", 21 | "notification:allow-register-action-types", 22 | "notification:default" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /src-tauri/icons/chatgpt.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/chatgpt.icns -------------------------------------------------------------------------------- /src-tauri/icons/excalidraw.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/excalidraw.icns -------------------------------------------------------------------------------- /src-tauri/icons/flomo.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/flomo.icns -------------------------------------------------------------------------------- /src-tauri/icons/gemini.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/gemini.icns -------------------------------------------------------------------------------- /src-tauri/icons/grok.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/grok.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/icon.icns -------------------------------------------------------------------------------- /src-tauri/icons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/icon.png -------------------------------------------------------------------------------- /src-tauri/icons/lizhi.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/lizhi.icns -------------------------------------------------------------------------------- /src-tauri/icons/programmusic.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/programmusic.icns -------------------------------------------------------------------------------- /src-tauri/icons/qwerty.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/qwerty.icns -------------------------------------------------------------------------------- /src-tauri/icons/twitter.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/twitter.icns -------------------------------------------------------------------------------- /src-tauri/icons/wechat.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/wechat.icns -------------------------------------------------------------------------------- /src-tauri/icons/weread.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/weread.icns -------------------------------------------------------------------------------- /src-tauri/icons/xiaohongshu.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/xiaohongshu.icns -------------------------------------------------------------------------------- /src-tauri/icons/youtube.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/youtube.icns -------------------------------------------------------------------------------- /src-tauri/icons/youtubemusic.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/icons/youtubemusic.icns -------------------------------------------------------------------------------- /src-tauri/info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NSCameraUsageDescription 5 | Request camera access 6 | NSMicrophoneUsageDescription 7 | Request microphone access 8 | NSAppTransportSecurity 9 | 10 | NSAllowsArbitraryLoads 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src-tauri/pake.json: -------------------------------------------------------------------------------- 1 | { 2 | "windows": [ 3 | { 4 | "url": "https://weread.qq.com", 5 | "url_type": "web", 6 | "hide_title_bar": true, 7 | "fullscreen": false, 8 | "width": 1200, 9 | "height": 780, 10 | "resizable": true, 11 | "always_on_top": false, 12 | "dark_mode": false, 13 | "activation_shortcut": "", 14 | "disabled_web_shortcuts": false 15 | } 16 | ], 17 | "user_agent": { 18 | "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", 19 | "linux": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36", 20 | "windows": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36" 21 | }, 22 | "system_tray": { 23 | "macos": false, 24 | "linux": true, 25 | "windows": true 26 | }, 27 | "system_tray_path": "icons/icon.png", 28 | "inject": [], 29 | "proxy_url": "" 30 | } 31 | -------------------------------------------------------------------------------- /src-tauri/png/chatgpt_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/chatgpt_256.ico -------------------------------------------------------------------------------- /src-tauri/png/chatgpt_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/chatgpt_32.ico -------------------------------------------------------------------------------- /src-tauri/png/chatgpt_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/chatgpt_512.png -------------------------------------------------------------------------------- /src-tauri/png/excalidraw_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/excalidraw_256.ico -------------------------------------------------------------------------------- /src-tauri/png/excalidraw_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/excalidraw_32.ico -------------------------------------------------------------------------------- /src-tauri/png/excalidraw_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/excalidraw_512.png -------------------------------------------------------------------------------- /src-tauri/png/flomo_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/flomo_256.ico -------------------------------------------------------------------------------- /src-tauri/png/flomo_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/flomo_32.ico -------------------------------------------------------------------------------- /src-tauri/png/flomo_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/flomo_512.png -------------------------------------------------------------------------------- /src-tauri/png/gemini_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/gemini_256.ico -------------------------------------------------------------------------------- /src-tauri/png/gemini_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/gemini_32.ico -------------------------------------------------------------------------------- /src-tauri/png/gemini_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/gemini_512.png -------------------------------------------------------------------------------- /src-tauri/png/grok_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/grok_256.ico -------------------------------------------------------------------------------- /src-tauri/png/grok_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/grok_32.ico -------------------------------------------------------------------------------- /src-tauri/png/grok_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/grok_512.png -------------------------------------------------------------------------------- /src-tauri/png/icon_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/icon_256.ico -------------------------------------------------------------------------------- /src-tauri/png/icon_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/icon_32.ico -------------------------------------------------------------------------------- /src-tauri/png/icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/icon_512.png -------------------------------------------------------------------------------- /src-tauri/png/lizhi_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/lizhi_256.ico -------------------------------------------------------------------------------- /src-tauri/png/lizhi_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/lizhi_32.ico -------------------------------------------------------------------------------- /src-tauri/png/lizhi_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/lizhi_512.png -------------------------------------------------------------------------------- /src-tauri/png/programmusic_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/programmusic_256.ico -------------------------------------------------------------------------------- /src-tauri/png/programmusic_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/programmusic_32.ico -------------------------------------------------------------------------------- /src-tauri/png/programmusic_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/programmusic_512.png -------------------------------------------------------------------------------- /src-tauri/png/qwerty_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/qwerty_256.ico -------------------------------------------------------------------------------- /src-tauri/png/qwerty_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/qwerty_32.ico -------------------------------------------------------------------------------- /src-tauri/png/qwerty_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/qwerty_512.png -------------------------------------------------------------------------------- /src-tauri/png/twitter_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/twitter_256.ico -------------------------------------------------------------------------------- /src-tauri/png/twitter_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/twitter_32.ico -------------------------------------------------------------------------------- /src-tauri/png/twitter_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/twitter_512.png -------------------------------------------------------------------------------- /src-tauri/png/wechat_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/wechat_256.ico -------------------------------------------------------------------------------- /src-tauri/png/wechat_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/wechat_32.ico -------------------------------------------------------------------------------- /src-tauri/png/wechat_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/wechat_512.png -------------------------------------------------------------------------------- /src-tauri/png/weread_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/weread_256.ico -------------------------------------------------------------------------------- /src-tauri/png/weread_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/weread_32.ico -------------------------------------------------------------------------------- /src-tauri/png/weread_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/weread_512.png -------------------------------------------------------------------------------- /src-tauri/png/xiaohongshu_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/xiaohongshu_256.ico -------------------------------------------------------------------------------- /src-tauri/png/xiaohongshu_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/xiaohongshu_32.ico -------------------------------------------------------------------------------- /src-tauri/png/xiaohongshu_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/xiaohongshu_512.png -------------------------------------------------------------------------------- /src-tauri/png/youtube_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/youtube_256.ico -------------------------------------------------------------------------------- /src-tauri/png/youtube_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/youtube_32.ico -------------------------------------------------------------------------------- /src-tauri/png/youtube_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/youtube_512.png -------------------------------------------------------------------------------- /src-tauri/png/youtubemusic_256.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/youtubemusic_256.ico -------------------------------------------------------------------------------- /src-tauri/png/youtubemusic_32.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/youtubemusic_32.ico -------------------------------------------------------------------------------- /src-tauri/png/youtubemusic_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/png/youtubemusic_512.png -------------------------------------------------------------------------------- /src-tauri/rust_proxy.toml: -------------------------------------------------------------------------------- 1 | [source.crates-io] 2 | replace-with = 'rsproxy-sparse' 3 | [source.rsproxy] 4 | registry = "https://rsproxy.cn/crates.io-index" 5 | [source.rsproxy-sparse] 6 | registry = "sparse+https://rsproxy.cn/index/" 7 | [registries.rsproxy] 8 | index = "https://rsproxy.cn/crates.io-index" 9 | [net] 10 | git-fetch-with-cli = true 11 | -------------------------------------------------------------------------------- /src-tauri/src/app/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize)] 4 | pub struct WindowConfig { 5 | pub url: String, 6 | pub hide_title_bar: bool, 7 | pub fullscreen: bool, 8 | pub width: f64, 9 | pub height: f64, 10 | pub resizable: bool, 11 | pub url_type: String, 12 | pub always_on_top: bool, 13 | pub dark_mode: bool, 14 | pub disabled_web_shortcuts: bool, 15 | pub activation_shortcut: String, 16 | } 17 | 18 | #[derive(Debug, Serialize, Deserialize)] 19 | pub struct PlatformSpecific { 20 | pub macos: T, 21 | pub linux: T, 22 | pub windows: T, 23 | } 24 | 25 | impl PlatformSpecific { 26 | pub const fn get(&self) -> &T { 27 | #[cfg(target_os = "macos")] 28 | let platform = &self.macos; 29 | #[cfg(target_os = "linux")] 30 | let platform = &self.linux; 31 | #[cfg(target_os = "windows")] 32 | let platform = &self.windows; 33 | 34 | platform 35 | } 36 | } 37 | 38 | impl PlatformSpecific 39 | where 40 | T: Copy, 41 | { 42 | pub const fn copied(&self) -> T { 43 | *self.get() 44 | } 45 | } 46 | 47 | pub type UserAgent = PlatformSpecific; 48 | pub type FunctionON = PlatformSpecific; 49 | 50 | #[derive(Debug, Serialize, Deserialize)] 51 | pub struct PakeConfig { 52 | pub windows: Vec, 53 | pub user_agent: UserAgent, 54 | pub system_tray: FunctionON, 55 | pub system_tray_path: String, 56 | pub proxy_url: String, 57 | } 58 | 59 | impl PakeConfig { 60 | pub fn show_system_tray(&self) -> bool { 61 | self.system_tray.copied() 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src-tauri/src/app/invoke.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{check_file_or_append, get_download_message, show_toast, MessageType}; 2 | use std::fs::{self, File}; 3 | use std::io::Write; 4 | use std::str::FromStr; 5 | use tauri::http::Method; 6 | use tauri::{command, AppHandle, Manager, Url, WebviewWindow}; 7 | use tauri_plugin_http::reqwest::{ClientBuilder, Request}; 8 | 9 | #[derive(serde::Deserialize)] 10 | pub struct DownloadFileParams { 11 | url: String, 12 | filename: String, 13 | } 14 | 15 | #[derive(serde::Deserialize)] 16 | pub struct BinaryDownloadParams { 17 | filename: String, 18 | binary: Vec, 19 | } 20 | 21 | #[derive(serde::Deserialize)] 22 | pub struct NotificationParams { 23 | title: String, 24 | body: String, 25 | icon: String, 26 | } 27 | 28 | #[command] 29 | pub async fn download_file(app: AppHandle, params: DownloadFileParams) -> Result<(), String> { 30 | let window: WebviewWindow = app.get_webview_window("pake").unwrap(); 31 | show_toast(&window, &get_download_message(MessageType::Start)); 32 | 33 | let output_path = app.path().download_dir().unwrap().join(params.filename); 34 | let file_path = check_file_or_append(output_path.to_str().unwrap()); 35 | let client = ClientBuilder::new().build().unwrap(); 36 | 37 | let response = client 38 | .execute(Request::new( 39 | Method::GET, 40 | Url::from_str(¶ms.url).unwrap(), 41 | )) 42 | .await; 43 | 44 | match response { 45 | Ok(res) => { 46 | let bytes = res.bytes().await.unwrap(); 47 | 48 | let mut file = File::create(file_path).unwrap(); 49 | file.write_all(&bytes).unwrap(); 50 | show_toast(&window, &get_download_message(MessageType::Success)); 51 | Ok(()) 52 | } 53 | Err(e) => { 54 | show_toast(&window, &get_download_message(MessageType::Failure)); 55 | Err(e.to_string()) 56 | } 57 | } 58 | } 59 | 60 | #[command] 61 | pub async fn download_file_by_binary( 62 | app: AppHandle, 63 | params: BinaryDownloadParams, 64 | ) -> Result<(), String> { 65 | let window: WebviewWindow = app.get_webview_window("pake").unwrap(); 66 | show_toast(&window, &get_download_message(MessageType::Start)); 67 | let output_path = app.path().download_dir().unwrap().join(params.filename); 68 | let file_path = check_file_or_append(output_path.to_str().unwrap()); 69 | let download_file_result = fs::write(file_path, ¶ms.binary); 70 | match download_file_result { 71 | Ok(_) => { 72 | show_toast(&window, &get_download_message(MessageType::Success)); 73 | Ok(()) 74 | } 75 | Err(e) => { 76 | show_toast(&window, &get_download_message(MessageType::Failure)); 77 | Err(e.to_string()) 78 | } 79 | } 80 | } 81 | 82 | #[command] 83 | pub fn send_notification(app: AppHandle, params: NotificationParams) -> Result<(), String> { 84 | use tauri_plugin_notification::NotificationExt; 85 | app.notification() 86 | .builder() 87 | .title(¶ms.title) 88 | .body(¶ms.body) 89 | .icon(¶ms.icon) 90 | .show() 91 | .unwrap(); 92 | Ok(()) 93 | } 94 | -------------------------------------------------------------------------------- /src-tauri/src/app/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod invoke; 3 | pub mod setup; 4 | pub mod window; 5 | -------------------------------------------------------------------------------- /src-tauri/src/app/setup.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::sync::{Arc, Mutex}; 3 | use std::time::{Duration, Instant}; 4 | use tauri::{ 5 | menu::{MenuBuilder, MenuItemBuilder}, 6 | tray::TrayIconBuilder, 7 | AppHandle, Manager, 8 | }; 9 | use tauri_plugin_global_shortcut::{GlobalShortcutExt, Shortcut}; 10 | use tauri_plugin_window_state::{AppHandleExt, StateFlags}; 11 | 12 | pub fn set_system_tray(app: &AppHandle, show_system_tray: bool) -> tauri::Result<()> { 13 | if !show_system_tray { 14 | app.remove_tray_by_id("pake-tray"); 15 | return Ok(()); 16 | } 17 | 18 | let hide_app = MenuItemBuilder::with_id("hide_app", "Hide").build(app)?; 19 | let show_app = MenuItemBuilder::with_id("show_app", "Show").build(app)?; 20 | let quit = MenuItemBuilder::with_id("quit", "Quit").build(app)?; 21 | 22 | let menu = MenuBuilder::new(app) 23 | .items(&[&hide_app, &show_app, &quit]) 24 | .build()?; 25 | 26 | app.app_handle().remove_tray_by_id("pake-tray"); 27 | 28 | let tray = TrayIconBuilder::new() 29 | .menu(&menu) 30 | .on_menu_event(move |app, event| match event.id().as_ref() { 31 | "hide_app" => { 32 | if let Some(window) = app.get_webview_window("pake") { 33 | window.minimize().unwrap(); 34 | } 35 | } 36 | "show_app" => { 37 | if let Some(window) = app.get_webview_window("pake") { 38 | window.show().unwrap(); 39 | } 40 | } 41 | "quit" => { 42 | app.save_window_state(StateFlags::all()).unwrap(); 43 | std::process::exit(0); 44 | } 45 | _ => (), 46 | }) 47 | .icon(app.default_window_icon().unwrap().clone()) 48 | .build(app)?; 49 | 50 | tray.set_icon_as_template(false)?; 51 | Ok(()) 52 | } 53 | 54 | pub fn set_global_shortcut(app: &AppHandle, shortcut: String) -> tauri::Result<()> { 55 | if shortcut.is_empty() { 56 | return Ok(()); 57 | } 58 | 59 | let app_handle = app.clone(); 60 | let shortcut_hotkey = Shortcut::from_str(&shortcut).unwrap(); 61 | let last_triggered = Arc::new(Mutex::new(Instant::now())); 62 | 63 | app_handle 64 | .plugin( 65 | tauri_plugin_global_shortcut::Builder::new() 66 | .with_handler({ 67 | let last_triggered = Arc::clone(&last_triggered); 68 | move |app, event, _shortcut| { 69 | let mut last_triggered = last_triggered.lock().unwrap(); 70 | if Instant::now().duration_since(*last_triggered) 71 | < Duration::from_millis(300) 72 | { 73 | return; 74 | } 75 | *last_triggered = Instant::now(); 76 | 77 | if shortcut_hotkey.eq(event) { 78 | if let Some(window) = app.get_webview_window("pake") { 79 | let is_visible = window.is_visible().unwrap(); 80 | if is_visible { 81 | window.hide().unwrap(); 82 | } else { 83 | window.show().unwrap(); 84 | window.set_focus().unwrap(); 85 | } 86 | } 87 | } 88 | } 89 | }) 90 | .build(), 91 | ) 92 | .expect("Failed to set global shortcut"); 93 | 94 | app.global_shortcut().register(shortcut_hotkey).unwrap(); 95 | 96 | Ok(()) 97 | } 98 | -------------------------------------------------------------------------------- /src-tauri/src/app/window.rs: -------------------------------------------------------------------------------- 1 | use crate::app::config::PakeConfig; 2 | use crate::util::get_data_dir; 3 | use std::{path::PathBuf, str::FromStr}; 4 | use tauri::{App, Config, Url, WebviewUrl, WebviewWindow, WebviewWindowBuilder}; 5 | 6 | #[cfg(target_os = "macos")] 7 | use tauri::{Theme, TitleBarStyle}; 8 | 9 | pub fn set_window(app: &mut App, config: &PakeConfig, tauri_config: &Config) -> WebviewWindow { 10 | let package_name = tauri_config.clone().product_name.unwrap(); 11 | let _data_dir = get_data_dir(app.handle(), package_name); 12 | 13 | let window_config = config 14 | .windows 15 | .first() 16 | .expect("At least one window configuration is required"); 17 | 18 | let user_agent = config.user_agent.get(); 19 | 20 | let url = match window_config.url_type.as_str() { 21 | "web" => WebviewUrl::App(window_config.url.parse().unwrap()), 22 | "local" => WebviewUrl::App(PathBuf::from(&window_config.url)), 23 | _ => panic!("url type can only be web or local"), 24 | }; 25 | 26 | let config_script = format!( 27 | "window.pakeConfig = {}", 28 | serde_json::to_string(&window_config).unwrap() 29 | ); 30 | 31 | let mut window_builder = WebviewWindowBuilder::new(app, "pake", url) 32 | .title("") 33 | .visible(false) 34 | .user_agent(user_agent) 35 | .resizable(window_config.resizable) 36 | .fullscreen(window_config.fullscreen) 37 | .inner_size(window_config.width, window_config.height) 38 | .always_on_top(window_config.always_on_top) 39 | .disable_drag_drop_handler() 40 | .initialization_script(&config_script) 41 | .initialization_script(include_str!("../inject/component.js")) 42 | .initialization_script(include_str!("../inject/event.js")) 43 | .initialization_script(include_str!("../inject/style.js")) 44 | .initialization_script(include_str!("../inject/custom.js")); 45 | 46 | if !config.proxy_url.is_empty() { 47 | window_builder = 48 | window_builder.proxy_url(Url::from_str(config.proxy_url.as_str()).unwrap()); 49 | } 50 | 51 | #[cfg(target_os = "macos")] 52 | { 53 | let title_bar_style = if window_config.hide_title_bar { 54 | TitleBarStyle::Overlay 55 | } else { 56 | TitleBarStyle::Visible 57 | }; 58 | window_builder = window_builder.title_bar_style(title_bar_style); 59 | 60 | if window_config.dark_mode { 61 | window_builder = window_builder.theme(Some(Theme::Dark)); 62 | } 63 | } 64 | 65 | #[cfg(not(target_os = "macos"))] 66 | { 67 | window_builder = window_builder 68 | .data_directory(_data_dir) 69 | .title(app.package_info().name.clone()); 70 | } 71 | 72 | window_builder.build().expect("Failed to build window") 73 | } 74 | -------------------------------------------------------------------------------- /src-tauri/src/inject/component.js: -------------------------------------------------------------------------------- 1 | document.addEventListener('DOMContentLoaded', () => { 2 | // Toast 3 | function pakeToast(msg) { 4 | const m = document.createElement('div'); 5 | m.innerHTML = msg; 6 | m.style.cssText = 7 | '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;'; 8 | document.body.appendChild(m); 9 | setTimeout(function () { 10 | const d = 0.5; 11 | m.style.transition = 'transform ' + d + 's ease-in, opacity ' + d + 's ease-in'; 12 | m.style.opacity = '0'; 13 | setTimeout(function () { 14 | document.body.removeChild(m); 15 | }, d * 1000); 16 | }, 3000); 17 | } 18 | 19 | window.pakeToast = pakeToast; 20 | }); 21 | -------------------------------------------------------------------------------- /src-tauri/src/inject/custom.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tw93/Pake/98fe6f6c16701dd958a6497c768741786fe0672b/src-tauri/src/inject/custom.js -------------------------------------------------------------------------------- /src-tauri/src/inject/event.js: -------------------------------------------------------------------------------- 1 | const shortcuts = { 2 | '[': () => window.history.back(), 3 | ']': () => window.history.forward(), 4 | '-': () => zoomOut(), 5 | '=': () => zoomIn(), 6 | '+': () => zoomIn(), 7 | 0: () => setZoom('100%'), 8 | r: () => window.location.reload(), 9 | ArrowUp: () => scrollTo(0, 0), 10 | ArrowDown: () => scrollTo(0, document.body.scrollHeight), 11 | }; 12 | 13 | function setZoom(zoom) { 14 | const html = document.getElementsByTagName('html')[0]; 15 | html.style.zoom = zoom; 16 | window.localStorage.setItem('htmlZoom', zoom); 17 | } 18 | 19 | function zoomCommon(zoomChange) { 20 | const currentZoom = window.localStorage.getItem('htmlZoom') || '100%'; 21 | setZoom(zoomChange(currentZoom)); 22 | } 23 | 24 | function zoomIn() { 25 | zoomCommon(currentZoom => `${Math.min(parseInt(currentZoom) + 10, 200)}%`); 26 | } 27 | 28 | function zoomOut() { 29 | zoomCommon(currentZoom => `${Math.max(parseInt(currentZoom) - 10, 30)}%`); 30 | } 31 | 32 | function handleShortcut(event) { 33 | if (shortcuts[event.key]) { 34 | event.preventDefault(); 35 | shortcuts[event.key](); 36 | } 37 | } 38 | 39 | // Judgment of file download. 40 | function isDownloadLink(url) { 41 | // prettier-ignore 42 | const fileExtensions = [ 43 | '3gp', '7z', 'ai', 'apk', 'avi', 'bmp', 'csv', 'dmg', 'doc', 'docx', 44 | 'fla', 'flv', 'gif', 'gz', 'gzip', 'ico', 'iso', 'indd', 'jar', 'jpeg', 45 | 'jpg', 'm3u8', 'mov', 'mp3', 'mp4', 'mpa', 'mpg', 'mpeg', 'msi', 'odt', 46 | 'ogg', 'ogv', 'pdf', 'png', 'ppt', 'pptx', 'psd', 'rar', 'raw', 47 | 'svg', 'swf', 'tar', 'tif', 'tiff', 'ts', 'txt', 'wav', 'webm', 'webp', 48 | 'wma', 'wmv', 'xls', 'xlsx', 'xml', 'zip', 'json', 'yaml', '7zip', 'mkv', 49 | ]; 50 | const downloadLinkPattern = new RegExp(`\\.(${fileExtensions.join('|')})$`, 'i'); 51 | return downloadLinkPattern.test(url); 52 | } 53 | 54 | document.addEventListener('DOMContentLoaded', () => { 55 | const tauri = window.__TAURI__; 56 | const appWindow = tauri.window.getCurrentWindow(); 57 | const invoke = tauri.core.invoke; 58 | 59 | if (!document.getElementById('pake-top-dom')) { 60 | const topDom = document.createElement('div'); 61 | topDom.id = 'pake-top-dom'; 62 | document.body.appendChild(topDom); 63 | } 64 | 65 | const domEl = document.getElementById('pake-top-dom'); 66 | 67 | domEl.addEventListener('touchstart', () => { 68 | appWindow.startDragging(); 69 | }); 70 | 71 | domEl.addEventListener('mousedown', e => { 72 | e.preventDefault(); 73 | if (e.buttons === 1 && e.detail !== 2) { 74 | appWindow.startDragging(); 75 | } 76 | }); 77 | 78 | domEl.addEventListener('dblclick', () => { 79 | appWindow.isFullscreen().then(fullscreen => { 80 | appWindow.setFullscreen(!fullscreen); 81 | }); 82 | }); 83 | 84 | if (window['pakeConfig']?.disabled_web_shortcuts !== true) { 85 | document.addEventListener('keyup', event => { 86 | if (/windows|linux/i.test(navigator.userAgent) && event.ctrlKey) { 87 | handleShortcut(event); 88 | } 89 | if (/macintosh|mac os x/i.test(navigator.userAgent) && event.metaKey) { 90 | handleShortcut(event); 91 | } 92 | }); 93 | } 94 | 95 | // Collect blob urls to blob by overriding window.URL.createObjectURL 96 | function collectUrlToBlobs() { 97 | const backupCreateObjectURL = window.URL.createObjectURL; 98 | window.blobToUrlCaches = new Map(); 99 | window.URL.createObjectURL = blob => { 100 | const url = backupCreateObjectURL.call(window.URL, blob); 101 | window.blobToUrlCaches.set(url, blob); 102 | return url; 103 | }; 104 | } 105 | 106 | function convertBlobUrlToBinary(blobUrl) { 107 | return new Promise(resolve => { 108 | const blob = window.blobToUrlCaches.get(blobUrl); 109 | const reader = new FileReader(); 110 | 111 | reader.readAsArrayBuffer(blob); 112 | reader.onload = () => { 113 | resolve(Array.from(new Uint8Array(reader.result))); 114 | }; 115 | }); 116 | } 117 | 118 | function downloadFromDataUri(dataURI, filename) { 119 | const byteString = atob(dataURI.split(',')[1]); 120 | // write the bytes of the string to an ArrayBuffer 121 | const bufferArray = new ArrayBuffer(byteString.length); 122 | 123 | // create a view into the buffer 124 | const binary = new Uint8Array(bufferArray); 125 | 126 | // set the bytes of the buffer to the correct values 127 | for (let i = 0; i < byteString.length; i++) { 128 | binary[i] = byteString.charCodeAt(i); 129 | } 130 | 131 | // write the ArrayBuffer to a binary, and you're done 132 | invoke('download_file_by_binary', { 133 | params: { 134 | filename, 135 | binary: Array.from(binary), 136 | }, 137 | }); 138 | } 139 | 140 | function downloadFromBlobUrl(blobUrl, filename) { 141 | convertBlobUrlToBinary(blobUrl).then(binary => { 142 | invoke('download_file_by_binary', { 143 | params: { 144 | filename, 145 | binary, 146 | }, 147 | }); 148 | }); 149 | } 150 | 151 | // detect blob download by createElement("a") 152 | function detectDownloadByCreateAnchor() { 153 | const createEle = document.createElement; 154 | document.createElement = el => { 155 | if (el !== 'a') return createEle.call(document, el); 156 | const anchorEle = createEle.call(document, el); 157 | 158 | // use addEventListener to avoid overriding the original click event. 159 | anchorEle.addEventListener( 160 | 'click', 161 | e => { 162 | const url = anchorEle.href; 163 | const filename = anchorEle.download || getFilenameFromUrl(url); 164 | if (window.blobToUrlCaches.has(url)) { 165 | downloadFromBlobUrl(url, filename); 166 | // case: download from dataURL -> convert dataURL -> 167 | } else if (url.startsWith('data:')) { 168 | downloadFromDataUri(url, filename); 169 | } 170 | }, 171 | true, 172 | ); 173 | 174 | return anchorEle; 175 | }; 176 | } 177 | 178 | // process special download protocol['data:','blob:'] 179 | const isSpecialDownload = url => ['blob', 'data'].some(protocol => url.startsWith(protocol)); 180 | 181 | const isDownloadRequired = (url, anchorElement, e) => anchorElement.download || e.metaKey || e.ctrlKey || isDownloadLink(url); 182 | 183 | const handleExternalLink = url => { 184 | invoke('plugin:shell|open', { 185 | path: url, 186 | }); 187 | }; 188 | 189 | const detectAnchorElementClick = e => { 190 | const anchorElement = e.target.closest('a'); 191 | 192 | if (anchorElement && anchorElement.href) { 193 | const target = anchorElement.target; 194 | const hrefUrl = new URL(anchorElement.href); 195 | const absoluteUrl = hrefUrl.href; 196 | let filename = anchorElement.download || getFilenameFromUrl(absoluteUrl); 197 | 198 | // Handling external link redirection, _blank will automatically open. 199 | if (target === '_blank') { 200 | e.preventDefault(); 201 | return; 202 | } 203 | 204 | if (target === '_new') { 205 | e.preventDefault(); 206 | handleExternalLink(absoluteUrl); 207 | return; 208 | } 209 | 210 | // Process download links for Rust to handle. 211 | if (isDownloadRequired(absoluteUrl, anchorElement, e) && !isSpecialDownload(absoluteUrl)) { 212 | e.preventDefault(); 213 | invoke('download_file', { params: { url: absoluteUrl, filename } }); 214 | } 215 | } 216 | }; 217 | 218 | // Prevent some special websites from executing in advance, before the click event is triggered. 219 | document.addEventListener('click', detectAnchorElementClick, true); 220 | 221 | collectUrlToBlobs(); 222 | detectDownloadByCreateAnchor(); 223 | 224 | // Rewrite the window.open function. 225 | const originalWindowOpen = window.open; 226 | window.open = function (url, name, specs) { 227 | // Apple login and google login 228 | if (name === 'AppleAuthentication') { 229 | //do nothing 230 | } else if (specs && (specs.includes('height=') || specs.includes('width='))) { 231 | location.href = url; 232 | } else { 233 | const baseUrl = window.location.origin + window.location.pathname; 234 | const hrefUrl = new URL(url, baseUrl); 235 | handleExternalLink(hrefUrl.href); 236 | } 237 | // Call the original window.open function to maintain its normal functionality. 238 | return originalWindowOpen.call(window, url, name, specs); 239 | }; 240 | 241 | // Set the default zoom, There are problems with Loop without using try-catch. 242 | try { 243 | setDefaultZoom(); 244 | } catch (e) { 245 | console.log(e); 246 | } 247 | 248 | // Fix Chinese input method "Enter" on Safari 249 | document.addEventListener( 250 | 'keydown', 251 | e => { 252 | if (e.keyCode === 229) e.stopPropagation(); 253 | }, 254 | true, 255 | ); 256 | }); 257 | 258 | document.addEventListener('DOMContentLoaded', function () { 259 | let permVal = 'granted'; 260 | window.Notification = function (title, options) { 261 | const { invoke } = window.__TAURI__.core; 262 | const body = options?.body || ''; 263 | let icon = options?.icon || ''; 264 | 265 | // If the icon is a relative path, convert to full path using URI 266 | if (icon.startsWith('/')) { 267 | icon = window.location.origin + icon; 268 | } 269 | 270 | invoke('send_notification', { 271 | params: { 272 | title, 273 | body, 274 | icon, 275 | }, 276 | }); 277 | }; 278 | 279 | window.Notification.requestPermission = async () => 'granted'; 280 | 281 | Object.defineProperty(window.Notification, 'permission', { 282 | enumerable: true, 283 | get: () => permVal, 284 | set: v => { 285 | permVal = v; 286 | }, 287 | }); 288 | }); 289 | 290 | function setDefaultZoom() { 291 | const htmlZoom = window.localStorage.getItem('htmlZoom'); 292 | if (htmlZoom) { 293 | setZoom(htmlZoom); 294 | } 295 | } 296 | 297 | function getFilenameFromUrl(url) { 298 | const urlPath = new URL(url).pathname; 299 | return urlPath.substring(urlPath.lastIndexOf('/') + 1); 300 | } 301 | -------------------------------------------------------------------------------- /src-tauri/src/inject/style.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', _event => { 2 | // Customize and transform existing functions 3 | const contentCSS = ` 4 | #page #footer-wrapper, 5 | .drawing-board .toolbar .toolbar-action, 6 | .c-swiper-container, 7 | .download_entry, 8 | .lang, .copyright, 9 | .wwads-cn, .adsbygoogle, 10 | #Bottom > div.content > div.inner, 11 | #Rightbar .sep20:nth-of-type(5), 12 | #Rightbar > div.box:nth-child(4), 13 | #Main > div.box:nth-child(8) > div 14 | #Wrapper > div.sep20, 15 | #Main > div.box:nth-child(8), 16 | #masthead-ad, 17 | #app > header > div > div.menu, 18 | #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), 19 | #app > div.layout > div.main-container > div.side-bar > div, 20 | #app > div.layout > div.main-container > div.side-bar > li.divider, 21 | #Rightbar > div:nth-child(6) > div.sidebar_compliance, 22 | #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside > div > div > a.ChatPageFollowTwitterLink_followLink__Gl2tt, 23 | #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside > div > div > a.Button_buttonBase__0QP_m.Button_primary__pIDjn.ChatPageDownloadLinks_downloadButton__amBRh, 24 | #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside > div > div > section a[href*="/contact"] { 25 | display: none !important; 26 | } 27 | 28 | #app > header .right .avatar.logged-in{ 29 | opacity: 0; 30 | transition: opacity 0.3s; 31 | } 32 | 33 | #app > header .right .avatar.logged-in:hover{ 34 | opacity: 1; 35 | } 36 | 37 | html::-webkit-scrollbar { 38 | display: none !important; 39 | } 40 | 41 | #__next .ChatPageSidebar_menuFooter__E1KTY,#__next > div.PageWithSidebarLayout_centeringDiv___L9br > div > aside > div > menu > section:nth-child(6) { 42 | display: none; 43 | } 44 | 45 | #__next > div.overflow-hidden.w-full.h-full .min-h-\\[20px\\].items-start.gap-4.whitespace-pre-wrap.break-words { 46 | word-break: break-all; 47 | } 48 | 49 | #__next .PageWithSidebarLayout_mainSection__i1yOg { 50 | width: 100%; 51 | max-width: 1000px; 52 | } 53 | 54 | #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside{ 55 | min-width: 260px; 56 | } 57 | 58 | #__next > div.overflow-hidden.w-full.h-full.relative.flex.z-0 > div.relative.flex.h-full.max-w-full.flex-1.overflow-hidden > div > main > div.absolute.left-2.top-2.z-10.hidden.md\\:inline-block{ 59 | margin-top:20px; 60 | margin-left: 10px; 61 | } 62 | 63 | .chakra-ui-light #app .chakra-heading, 64 | .chakra-ui-dark #app .chakra-heading, 65 | .chakra-ui-light #app .chakra-stack, 66 | .chakra-ui-dark #app .chakra-stack, 67 | .app-main .sidebar-mouse-in-out, 68 | .chakra-modal__content-container .chakra-modal__header > div > div, 69 | #__next > div.PageWithSidebarLayout_centeringDiv___L9br > section > header { 70 | padding-top: 10px; 71 | } 72 | 73 | #__next .overflow-hidden>.hidden.bg-gray-900 span.rounded-md.bg-yellow-200 { 74 | display: none; 75 | } 76 | 77 | #__next .absolute .px-3.pt-2.pb-3.text-center { 78 | visibility: hidden; 79 | padding-bottom: 4px; 80 | } 81 | 82 | #__next .h-full.w-full .text-center.text-xs.text-gray-600>span { 83 | visibility: hidden; 84 | height: 15px; 85 | } 86 | 87 | #__next > div.overflow-hidden.w-full.h-full.relative.flex > div.dark.hidden.flex-shrink-0.bg-gray-900.md\\:flex.md\\:w-\\[260px\\].md\\:flex-col > div > div > nav { 88 | width: 100%; 89 | } 90 | 91 | .panel.give_me .nav_view { 92 | top: 164px !important; 93 | } 94 | 95 | #Wrapper{ 96 | background-color: #F8F8F8 !important; 97 | background-image:none !important; 98 | } 99 | 100 | #Top { 101 | border-bottom: none; 102 | } 103 | 104 | #global > div.header-container.showSearchBoxOrHeaderFixed > header > div.right > div > div.dropdown-nav{ 105 | display: none; 106 | } 107 | 108 | #__next > div.AnnouncementWrapper_container__Z51yh > div > aside > div > div > menu > section:nth-child(4) > section, #__next > div.AnnouncementWrapper_container__Z51yh > div > aside > div > div > menu > section:nth-child(4){ 109 | display: none; 110 | } 111 | 112 | #react-root [data-testid="placementTracking"] article, 113 | #react-root a[href*="quick_promote_web"], 114 | #react-root [data-testid="AppTabBar_Explore_Link"], 115 | #react-root a[href*="/lists"][role="link"][aria-label], 116 | #react-root a[href*="/i/communitynotes"][role="link"][aria-label], 117 | #react-root a[role="link"][aria-label="Communities"], 118 | #react-root a[href*="/i/verified-orgs-signup"][role="link"][aria-label] { 119 | display: none !important; 120 | } 121 | 122 | #react-root [data-testid="DMDrawer"], 123 | #root > main > footer.justify-center.ease-in { 124 | visibility: hidden !important; 125 | } 126 | 127 | #__next > div.overflow-hidden.w-full.h-full .absolute.bottom-0.left-0.w-full > div.text-center.text-xs { 128 | visibility: hidden !important; 129 | height: 0px !important; 130 | } 131 | 132 | #react-root [data-testid="primaryColumn"] > div > div { 133 | position: relative !important; 134 | } 135 | 136 | #react-root [data-testid="sidebarColumn"] { 137 | visibility: hidden !important; 138 | width: 0 !important; 139 | margin: 0 !important; 140 | padding: 0 !important; 141 | z-index: 1 !important; 142 | } 143 | 144 | @media only screen and (min-width: 1000px) { 145 | #react-root main[role="main"] { 146 | align-items: center !important; 147 | overflow-x: clip !important; 148 | } 149 | 150 | #react-root [data-testid="primaryColumn"] { 151 | width: 700px !important; 152 | max-width: 700px !important; 153 | margin: 0 auto !important; 154 | } 155 | #react-root [data-testid="primaryColumn"] > div > div:last-child, 156 | #react-root [data-testid="primaryColumn"] > div > div:last-child div { 157 | max-width: unset !important; 158 | } 159 | 160 | #react-root div[aria-label][role="group"][id^="id__"] { 161 | margin-right: 81px !important; 162 | } 163 | 164 | #react-root header[role="banner"] { 165 | position: fixed !important; 166 | left: 0 !important; 167 | } 168 | 169 | #react-root header[role="banner"] > div > div > div { 170 | justify-content: center !important; 171 | padding-top: 0; 172 | overflow-x: hidden; 173 | } 174 | 175 | #react-root form[role="search"] > div:nth-child(1) > div { 176 | background-color: transparent !important; 177 | } 178 | 179 | #react-root h1[role="heading"] { 180 | padding-top: 4px !important; 181 | } 182 | 183 | #react-root header[role="banner"] 184 | nav[role="navigation"] 185 | * 186 | div[dir="auto"]:not([aria-label]) 187 | > span, 188 | #react-root [data-testid="SideNav_AccountSwitcher_Button"] > div:not(:first-child) { 189 | display: inline-block !important; 190 | opacity: 0 !important; 191 | transition: 0.5s cubic-bezier(0.2, 0.8, 0.2, 1); 192 | } 193 | #react-root header[role="banner"] 194 | nav[role="navigation"]:hover 195 | * 196 | div[dir="auto"]:not([aria-label]) 197 | > span, 198 | #react-root [data-testid="SideNav_AccountSwitcher_Button"]:hover > div:not(:first-child) { 199 | opacity: 1 !important; 200 | } 201 | #react-root header[role="banner"] nav[role="navigation"]:hover > * > div { 202 | backdrop-filter: blur(12px) !important; 203 | } 204 | #react-root header[role="banner"] nav[role="navigation"] > a { 205 | position: relative; 206 | } 207 | 208 | #react-root header[role="banner"] nav[role="navigation"] > a::before { 209 | content: ""; 210 | position: absolute; 211 | top: 0px; 212 | right: -40px; 213 | bottom: 0px; 214 | left: 0px; 215 | } 216 | #react-root [data-testid="SideNav_AccountSwitcher_Button"] { 217 | bottom: 18px !important; 218 | left: 1px !important; 219 | } 220 | 221 | #react-root [data-testid="SideNav_NewTweet_Button"], #react-root [aria-label="Twitter Blue"]{ 222 | display: none; 223 | } 224 | } 225 | 226 | @media only screen and (min-width: 1265px) { 227 | #react-root [data-testid="sidebarColumn"] form[role="search"] { 228 | visibility: visible !important; 229 | position: fixed !important; 230 | top: 12px !important; 231 | right: 16px !important; 232 | } 233 | 234 | #react-root [data-testid="sidebarColumn"] input[placeholder="Search Twitter"] { 235 | width: 150px; 236 | } 237 | 238 | #react-root [data-testid="sidebarColumn"] form[role="search"]:focus-within { 239 | width: 374px !important; 240 | backdrop-filter: blur(12px) !important; 241 | } 242 | 243 | #react-root [data-testid="sidebarColumn"] input[placeholder="Search Twitter"]:focus { 244 | width: 328px !important; 245 | } 246 | 247 | #react-root div[style*="left: -12px"] { 248 | left: unset !important; 249 | } 250 | 251 | #react-root div[style="left: -8px; width: 306px;"] { 252 | left: unset !important; 253 | width: 374px !important; 254 | } 255 | 256 | #react-root .searchFilters { 257 | visibility: visible !important; 258 | position: fixed; 259 | top: 12px; 260 | right: 16px; 261 | width: 240px; 262 | } 263 | #react-root .searchFilters > div > div:first-child { 264 | display: none; 265 | } 266 | } 267 | 268 | @media (min-width:1280px){ 269 | #__next .text-base.xl\\:max-w-3xl, #__next form.stretch.xl\\:max-w-3xl { 270 | max-width: 48rem; 271 | } 272 | } 273 | 274 | #__next .prose ol li p { 275 | margin: 0; 276 | display: inline; 277 | } 278 | `; 279 | const contentStyleElement = document.createElement('style'); 280 | contentStyleElement.innerHTML = contentCSS; 281 | document.head.appendChild(contentStyleElement); 282 | 283 | // Top spacing adapts to head-hiding scenarios 284 | const topPaddingCSS = ` 285 | #layout > ytmusic-nav-bar{ 286 | padding-top: 20px; 287 | } 288 | 289 | .columns .column #header, 290 | .main > div > div.panel.give_me > div.header { 291 | padding-top: 30px; 292 | } 293 | 294 | ytd-masthead>#container.style-scope.ytd-masthead { 295 | padding-top: 12px; 296 | } 297 | 298 | #__next header.HeaderBar_header__jn5ju { 299 | padding-top: 16px; 300 | } 301 | 302 | #root > .excalidraw-app> .excalidraw-container .App-menu.App-menu_top{ 303 | margin-top: 15px; 304 | } 305 | 306 | .geist-page nav.dashboard_nav__PRmJv, 307 | #app > div.layout > div.header-container.showSearchBoxOrHeaderFixed > header > a { 308 | padding-top:10px; 309 | } 310 | 311 | .geist-page .submenu button{ 312 | margin-top:24px; 313 | } 314 | 315 | .container-with-note #home, .container-with-note #switcher{ 316 | top: 30px; 317 | } 318 | 319 | #__next .overflow-hidden>.overflow-x-hidden .scrollbar-trigger > nav { 320 | padding-top: 12px; 321 | } 322 | 323 | #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main > div.flex.h-full.flex-col > div.flex-1.overflow-hidden > div > div.absolute.left-0.right-0 > div > div.flex.items-center.gap-2 > button{ 324 | margin-left: 60px; 325 | margin-right: -10px; 326 | } 327 | 328 | #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.dark.flex-shrink-0.overflow-x-hidden.bg-black > div > div > div > div > nav > div.flex.flex-col.pt-2.empty\\:hidden.dark\\:border-white\\/20 > a, 329 | #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main > div.group.fixed.bottom-3.right-3.z-10.hidden.gap-1.lg\\:flex > div, 330 | #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary > div > div > div > div > nav > div.flex.flex-col.pt-2.empty\\:hidden.dark\\:border-white\\/20 > a { 331 | display: none; 332 | } 333 | 334 | #__next .md\\:px-\\[60px\\].text-token-text-secondary.text-xs.text-center.py-2.px-2.relative{ 335 | visibility:hidden; 336 | } 337 | 338 | #__next>div>div>.flex.h-screen.w-full.flex-col.items-center { 339 | padding-top: 20px; 340 | } 341 | 342 | .h-dvh.flex-grow .bg-gradient-to-b.from-background.via-background { 343 | padding-top: 40px; 344 | } 345 | 346 | body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.z-\\[21\\].flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary.max-md\\:\\!w-0 > div > div > div > nav > div.flex.justify-between.h-\\[60px\\].items-center.md\\:h-header-height { 347 | padding-top: 25px; 348 | } 349 | 350 | body > div.relative.flex.h-full.w-full.overflow-hidden.transition-colors.z-0 > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main > div.composer-parent.flex.h-full.flex-col.focus-visible\\:outline-0 > div.flex-1.overflow-hidden.\\@container\\/thread > div > div.absolute.left-0.right-0 > div{ 351 | padding-top: 35px; 352 | } 353 | 354 | #__next .sticky.left-0.right-0.top-0.z-20.bg-black{ 355 | padding-top: 0px; 356 | } 357 | 358 | #header-area > div > .css-gtiexd > div:nth-child(1) > div, #header-area .logoIcon .user-info{ 359 | padding-top: 20px; 360 | } 361 | 362 | #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.flex-shrink-0.overflow-x-hidden.bg-token-sidebar-surface-primary > div > div > div > div > nav, #__next > div.relative.z-0.flex.h-full.w-full.overflow-hidden > div.relative.flex.h-full.max-w-full.flex-1.flex-col.overflow-hidden > main { 363 | padding-top: 6px; 364 | } 365 | 366 | #__next > div.AnnouncementWrapper_container__Z51yh > div > aside.SidebarLayout_sidebar__SXeDJ.SidebarLayout_left__k163a > div > div > header{ 367 | padding-left: 84px; 368 | padding-top: 10px; 369 | } 370 | 371 | #page .main_header, .cb-layout-basic--navbar, 372 | #app .splitpanes.splitpanes--horizontal.no-splitter header, 373 | .fui-FluentProvider .fui-Button[data-testid="HomeButton"], 374 | #__next > div.PageWithSidebarLayout_centeringDiv___L9br > aside .ChatPageSidebar_logo__9PIXq { 375 | padding-top: 20px; 376 | } 377 | 378 | #tabs-sidebar--tabpanel-0 > div.tw-flex.tw-items-center.tw-mb-\\[12px\\].tw-mt-\\[14px\\].tw-px-4 { 379 | padding-top: 15px; 380 | } 381 | 382 | #tabs-sidebar--tabpanel-1 > div > div.tw-p-\\[16px\\].tw-flex.tw-flex-col.tw-gap-1\\.5{ 383 | padding-top: 30px; 384 | } 385 | 386 | #tabs-sidebar--tabpanel-2 > div > h2 { 387 | padding-top: 20px; 388 | height: 70px; 389 | } 390 | 391 | .lark > .dashboard-sidebar, .lark > .dashboard-sidebar > .sidebar-user-info , .lark > .dashboard-sidebar .index-module_wrapper_F-Wbq{ 392 | padding-top:15px; 393 | } 394 | 395 | #app-root .mat-mdc-tooltip-trigger.main-menu-button.mdc-icon-button { 396 | margin-top: 15px; 397 | } 398 | 399 | .lark > .main-wrapper [data-testid="aside"] { 400 | top: 15px; 401 | } 402 | 403 | #global > div.header-container > .mask-paper { 404 | padding-top: 20px; 405 | } 406 | 407 | #background.ytd-masthead { 408 | height: 68px; 409 | } 410 | 411 | .wrap.h1body-exist.max-container > div.menu-tocs > div.menu-btn{ 412 | top: 28px; 413 | } 414 | 415 | #pake-top-dom:active { 416 | cursor: grabbing; 417 | cursor: -webkit-grabbing; 418 | } 419 | 420 | #pake-top-dom{ 421 | position:fixed; 422 | background:transparent; 423 | top:0; 424 | width: 100%; 425 | height: 20px; 426 | cursor: grab; 427 | -webkit-app-region: drag; 428 | user-select: none; 429 | -webkit-user-select: none; 430 | z-index: 99999; 431 | } 432 | 433 | @media (max-width:767px){ 434 | #__next .overflow-hidden.w-full .max-w-full>.sticky.top-0 { 435 | padding-top: 20px; 436 | } 437 | 438 | #__next > div.overflow-hidden.w-full.h-full main.relative.h-full.w-full.flex-1 > .flex-1.overflow-hidden .h-32.md\\:h-48.flex-shrink-0{ 439 | height: 0px; 440 | } 441 | } 442 | `; 443 | const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0; 444 | if (window['pakeConfig']?.hide_title_bar && isMac) { 445 | const topPaddingStyleElement = document.createElement('style'); 446 | topPaddingStyleElement.innerHTML = topPaddingCSS; 447 | document.head.appendChild(topPaddingStyleElement); 448 | } 449 | }); 450 | -------------------------------------------------------------------------------- /src-tauri/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg_attr(mobile, tauri::mobile_entry_point)] 2 | mod app; 3 | mod util; 4 | 5 | use tauri::Manager; 6 | use tauri_plugin_window_state::Builder as WindowStatePlugin; 7 | use tauri_plugin_window_state::StateFlags; 8 | 9 | #[cfg(target_os = "macos")] 10 | use std::time::Duration; 11 | 12 | use app::{ 13 | invoke::{download_file, download_file_by_binary, send_notification}, 14 | setup::{set_global_shortcut, set_system_tray}, 15 | window::set_window, 16 | }; 17 | use util::get_pake_config; 18 | 19 | pub fn run_app() { 20 | let (pake_config, tauri_config) = get_pake_config(); 21 | let tauri_app = tauri::Builder::default(); 22 | 23 | let show_system_tray = pake_config.show_system_tray(); 24 | let activation_shortcut = pake_config.windows[0].activation_shortcut.clone(); 25 | let init_fullscreen = pake_config.windows[0].fullscreen; 26 | 27 | let window_state_plugin = WindowStatePlugin::default() 28 | .with_state_flags(if init_fullscreen { 29 | StateFlags::FULLSCREEN 30 | } else { 31 | // Prevent flickering on the first open. 32 | StateFlags::all() & !StateFlags::VISIBLE 33 | }) 34 | .build(); 35 | 36 | #[allow(deprecated)] 37 | tauri_app 38 | .plugin(window_state_plugin) 39 | .plugin(tauri_plugin_oauth::init()) 40 | .plugin(tauri_plugin_http::init()) 41 | .plugin(tauri_plugin_shell::init()) 42 | .plugin(tauri_plugin_notification::init()) 43 | .plugin(tauri_plugin_single_instance::init(|_, _, _| ())) 44 | .invoke_handler(tauri::generate_handler![ 45 | download_file, 46 | download_file_by_binary, 47 | send_notification, 48 | ]) 49 | .setup(move |app| { 50 | let window = set_window(app, &pake_config, &tauri_config); 51 | set_system_tray(app.app_handle(), show_system_tray).unwrap(); 52 | set_global_shortcut(app.app_handle(), activation_shortcut).unwrap(); 53 | // Prevent flickering on the first open. 54 | window.show().unwrap(); 55 | Ok(()) 56 | }) 57 | .on_window_event(|_window, _event| { 58 | #[cfg(target_os = "macos")] 59 | if let tauri::WindowEvent::CloseRequested { api, .. } = _event { 60 | let window = _window.clone(); 61 | tauri::async_runtime::spawn(async move { 62 | if window.is_fullscreen().unwrap_or(false) { 63 | window.set_fullscreen(false).unwrap(); 64 | tokio::time::sleep(Duration::from_millis(900)).await; 65 | } 66 | window.minimize().unwrap(); 67 | window.hide().unwrap(); 68 | }); 69 | api.prevent_close(); 70 | } 71 | }) 72 | .run(tauri::generate_context!()) 73 | .expect("error while running tauri application"); 74 | } 75 | 76 | pub fn run() { 77 | run_app() 78 | } 79 | -------------------------------------------------------------------------------- /src-tauri/src/main.rs: -------------------------------------------------------------------------------- 1 | #![cfg_attr( 2 | all(not(debug_assertions), target_os = "windows"), 3 | windows_subsystem = "windows" 4 | )] 5 | 6 | fn main() { 7 | app_lib::run() 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::app::config::PakeConfig; 2 | use std::env; 3 | use std::path::PathBuf; 4 | use tauri::{AppHandle, Config, Manager, WebviewWindow}; 5 | 6 | pub fn get_pake_config() -> (PakeConfig, Config) { 7 | #[cfg(feature = "cli-build")] 8 | let pake_config: PakeConfig = serde_json::from_str(include_str!("../.pake/pake.json")) 9 | .expect("Failed to parse pake config"); 10 | 11 | #[cfg(not(feature = "cli-build"))] 12 | let pake_config: PakeConfig = 13 | serde_json::from_str(include_str!("../pake.json")).expect("Failed to parse pake config"); 14 | 15 | #[cfg(feature = "cli-build")] 16 | let tauri_config: Config = serde_json::from_str(include_str!("../.pake/tauri.conf.json")) 17 | .expect("Failed to parse tauri config"); 18 | 19 | #[cfg(not(feature = "cli-build"))] 20 | let tauri_config: Config = serde_json::from_str(include_str!("../tauri.conf.json")) 21 | .expect("Failed to parse tauri config"); 22 | 23 | (pake_config, tauri_config) 24 | } 25 | 26 | pub fn get_data_dir(app: &AppHandle, package_name: String) -> PathBuf { 27 | { 28 | let data_dir = app 29 | .path() 30 | .config_dir() 31 | .expect("Failed to get data dirname") 32 | .join(package_name); 33 | 34 | if !data_dir.exists() { 35 | std::fs::create_dir(&data_dir) 36 | .unwrap_or_else(|_| panic!("Can't create dir {}", data_dir.display())); 37 | } 38 | data_dir 39 | } 40 | } 41 | 42 | pub fn show_toast(window: &WebviewWindow, message: &str) { 43 | let script = format!(r#"pakeToast("{}");"#, message); 44 | window.eval(&script).unwrap(); 45 | } 46 | 47 | pub enum MessageType { 48 | Start, 49 | Success, 50 | Failure, 51 | } 52 | 53 | pub fn get_download_message(message_type: MessageType) -> String { 54 | let default_start_message = "Start downloading~"; 55 | let chinese_start_message = "开始下载中~"; 56 | 57 | let default_success_message = "Download successful, saved to download directory~"; 58 | let chinese_success_message = "下载成功,已保存到下载目录~"; 59 | 60 | let default_failure_message = "Download failed, please check your network connection~"; 61 | let chinese_failure_message = "下载失败,请检查你的网络连接~"; 62 | 63 | env::var("LANG") 64 | .map(|lang| { 65 | if lang.starts_with("zh") { 66 | match message_type { 67 | MessageType::Start => chinese_start_message, 68 | MessageType::Success => chinese_success_message, 69 | MessageType::Failure => chinese_failure_message, 70 | } 71 | } else { 72 | match message_type { 73 | MessageType::Start => default_start_message, 74 | MessageType::Success => default_success_message, 75 | MessageType::Failure => default_failure_message, 76 | } 77 | } 78 | }) 79 | .unwrap_or_else(|_| match message_type { 80 | MessageType::Start => default_start_message, 81 | MessageType::Success => default_success_message, 82 | MessageType::Failure => default_failure_message, 83 | }) 84 | .to_string() 85 | } 86 | 87 | // Check if the file exists, if it exists, add a number to file name 88 | pub fn check_file_or_append(file_path: &str) -> String { 89 | let mut new_path = PathBuf::from(file_path); 90 | let mut counter = 0; 91 | 92 | while new_path.exists() { 93 | let file_stem = new_path.file_stem().unwrap().to_string_lossy().to_string(); 94 | let extension = new_path.extension().unwrap().to_string_lossy().to_string(); 95 | let parent_dir = new_path.parent().unwrap(); 96 | 97 | let new_file_stem = match file_stem.rfind('-') { 98 | Some(index) if file_stem[index + 1..].parse::().is_ok() => { 99 | let base_name = &file_stem[..index]; 100 | counter = file_stem[index + 1..].parse::().unwrap() + 1; 101 | format!("{}-{}", base_name, counter) 102 | } 103 | _ => { 104 | counter += 1; 105 | format!("{}-{}", file_stem, counter) 106 | } 107 | }; 108 | 109 | new_path = parent_dir.join(format!("{}.{}", new_file_stem, extension)); 110 | } 111 | 112 | new_path.to_string_lossy().into_owned() 113 | } 114 | -------------------------------------------------------------------------------- /src-tauri/tauri.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "WeRead", 3 | "identifier": "com.pake.weread", 4 | "version": "1.0.0", 5 | "app": { 6 | "withGlobalTauri": true, 7 | "trayIcon": { 8 | "iconPath": "png/weread_512.png", 9 | "iconAsTemplate": false, 10 | "id": "pake-tray" 11 | } 12 | }, 13 | "build": { 14 | "frontendDist": "../dist" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src-tauri/tauri.linux.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "productName": "we-read", 3 | "bundle": { 4 | "icon": ["png/weread_512.png"], 5 | "active": true, 6 | "linux": { 7 | "deb": { 8 | "depends": ["curl", "wget"], 9 | "files": { "/usr/share/applications/com-pake-weread.desktop": "assets/com-pake-weread.desktop" } 10 | } 11 | }, 12 | "targets": ["deb", "appimage"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src-tauri/tauri.macos.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundle": { 3 | "icon": ["icons/weread.icns"], 4 | "active": true, 5 | "macOS": {}, 6 | "targets": ["dmg"] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src-tauri/tauri.windows.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "bundle": { 3 | "icon": ["png/weread_256.ico", "png/weread_32.ico"], 4 | "active": true, 5 | "resources": ["png/weread_32.ico"], 6 | "targets": ["msi"], 7 | "windows": { 8 | "digestAlgorithm": "sha256", 9 | "wix": { 10 | "language": ["en-US"], 11 | "template": "assets/main.wxs" 12 | } 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "target": "es2020", 5 | "types": [ 6 | "node" 7 | ], 8 | "lib": [ 9 | "es2020", 10 | "dom" 11 | ], 12 | "esModuleInterop": true, 13 | "resolveJsonModule": true, 14 | "allowSyntheticDefaultImports": true, 15 | "noImplicitAny": true, 16 | "moduleResolution": "node", 17 | "sourceMap": true, 18 | "outDir": "dist", 19 | "baseUrl": ".", 20 | "paths": { 21 | "@/*": [ 22 | "bin/*" 23 | ] 24 | } 25 | }, 26 | "include": [ 27 | "bin/**/*" 28 | ] 29 | } 30 | --------------------------------------------------------------------------------