├── .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 |
2 |
3 |
4 |
5 | Pake
6 | 利用 Rust 轻松构建轻量级多端桌面应用
7 |
21 |
22 |
23 | ## 特征
24 |
25 | - 🎐 相比传统的 Electron 套壳打包,要小将近 20 倍,5M 上下。
26 | - 🚀 Pake 的底层使用的 Rust Tauri 框架,性能体验较 JS 框架要轻快不少,内存小很多。
27 | - 📦 不是单纯打包,实现了快捷键的透传、沉浸式的窗口、拖动、样式改写、去广告、产品的极简风格定制。
28 | - 👻 只是一个很简单的小玩具,用 Tauri 替代之前套壳网页打包的老思路,其实 PWA 也很好。
29 |
30 | ## 常用包下载
31 |
32 |
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 | 
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------