├── .eslintrc.json
├── .github
└── workflows
│ ├── ci.yml
│ ├── pr.yml
│ └── updater.yml
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode
└── extensions.json
├── LICENSE
├── README.md
├── README_CN.md
├── README_EN.md
├── UPDATE_LOG.md
├── index.html
├── md
├── demo1.png
├── demo2.png
├── demo3.png
├── demo4.png
├── demo5.png
├── demo6.png
├── demo7.png
└── icon.png
├── package.json
├── postcss.config.cjs
├── public
└── vite.svg
├── scripts
├── aarch.mjs
├── publish.mjs
├── updatelog.mjs
└── updater.mjs
├── src-tauri
├── .gitignore
├── Cargo.lock
├── Cargo.toml
├── build.rs
├── icons
│ ├── 128x128.png
│ ├── 128x128@2x.png
│ ├── 32x32.png
│ ├── Square107x107Logo.png
│ ├── Square142x142Logo.png
│ ├── Square150x150Logo.png
│ ├── Square284x284Logo.png
│ ├── Square30x30Logo.png
│ ├── Square310x310Logo.png
│ ├── Square44x44Logo.png
│ ├── Square71x71Logo.png
│ ├── Square89x89Logo.png
│ ├── StoreLogo.png
│ ├── build.sh
│ ├── icon.icns
│ ├── icon.ico
│ └── icon.png
├── src
│ ├── cmds.rs
│ ├── config
│ │ ├── common_config.rs
│ │ ├── config.rs
│ │ ├── draft.rs
│ │ └── mod.rs
│ ├── core
│ │ ├── clipboard.rs
│ │ ├── database.rs
│ │ ├── handle.rs
│ │ ├── mod.rs
│ │ ├── sysopt.rs
│ │ ├── tray.rs
│ │ └── window_manager.rs
│ ├── main.rs
│ └── utils
│ │ ├── dirs.rs
│ │ ├── dispatch_util.rs
│ │ ├── hotkey_util.rs
│ │ ├── img_util.rs
│ │ ├── json_util.rs
│ │ ├── log_print.rs
│ │ ├── mod.rs
│ │ ├── string_util.rs
│ │ └── window_util.rs
└── tauri.conf.json
├── src
├── App.vue
├── assets
│ ├── about-icon.png
│ ├── about.svg
│ ├── backspace.svg
│ ├── delete.svg
│ ├── fuzhi.svg
│ ├── gh-desktop.png
│ ├── shezhi.svg
│ ├── sousuo.svg
│ └── vue.svg
├── components
│ ├── ClipBoardItem.vue
│ ├── ClipBoardList.vue
│ ├── HotKeyItem.vue
│ ├── KeyMapBar.vue
│ ├── MainLayout.vue
│ ├── SearchBar.vue
│ ├── TagGroup.vue
│ └── child
│ │ ├── clipboard
│ │ └── SearchNoResult.vue
│ │ └── config
│ │ ├── HotKeyInput.vue
│ │ ├── LeftSectionItem.vue
│ │ ├── RightSectionAbout.vue
│ │ ├── RightSectionCommonConfig.vue
│ │ └── base
│ │ ├── BaseSelect.vue
│ │ └── BaseSwitch.vue
├── config
│ └── constants.js
├── i18n
│ ├── index.js
│ └── locales
│ │ ├── en.yaml
│ │ └── zh.yaml
├── main.js
├── router
│ └── router.js
├── service
│ ├── cmds.js
│ ├── globalListener.js
│ ├── msg.js
│ ├── recordService.js
│ ├── shortCutUtil.js
│ ├── store.js
│ └── windowUtil.js
├── style.css
└── views
│ ├── Config.vue
│ └── Main.vue
├── tailwind.config.cjs
├── vite.config.js
└── yarn.lock
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "node": true
4 | },
5 | "extends": ["eslint:recommended", "plugin:vue/vue3-recommended", "prettier"],
6 | "rules": {}
7 | }
8 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Release CI
2 |
3 | on:
4 | workflow_dispatch:
5 | push:
6 | tags:
7 | - v**
8 |
9 | jobs:
10 | publish-tauri:
11 | permissions:
12 | contents: write
13 | strategy:
14 | fail-fast: false
15 | matrix:
16 | platform: [macos-latest, windows-latest]
17 |
18 | runs-on: ${{ matrix.platform }}
19 | steps:
20 | - uses: actions/checkout@v4
21 | - name: setup node
22 | uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 | - name: install Rust stable
26 | uses: dtolnay/rust-toolchain@stable
27 | - name: install dependencies (ubuntu only)
28 | if: matrix.platform == 'ubuntu-20.04'
29 | run: |
30 | sudo apt-get update
31 | sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
32 | - name: install frontend dependencies
33 | run: yarn install # change this to npm or pnpm depending on which one you use
34 | - uses: tauri-apps/tauri-action@v0
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 | TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
38 | TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }}
39 | with:
40 | tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
41 | releaseName: "App v__VERSION__"
42 | releaseBody: "See the assets to download this version and install."
43 | releaseDraft: true
44 | prerelease: false
45 |
--------------------------------------------------------------------------------
/.github/workflows/pr.yml:
--------------------------------------------------------------------------------
1 | name: Pull request
2 |
3 | # This action works with pull requests and pushes
4 | on:
5 | pull_request:
6 | push:
7 | branches:
8 | - master
9 |
10 | jobs:
11 | prettier:
12 | runs-on: ubuntu-latest
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v2
17 | with:
18 | # Make sure the actual branch is checked out when running on pull requests
19 | ref: ${{ github.head_ref }}
20 |
21 | - name: Reconfigure git to use HTTP authentication
22 | run: >
23 | git config --global url."https://github.com/".insteadOf
24 | ssh://git@github.com/
25 |
26 | - name: Get yarn cache dir path
27 | id: yarn-cache-dir-path
28 | run: echo "::set-output name=dir::$(yarn cache dir)"
29 |
30 | - name: Yarn Cache
31 | uses: actions/cache@v2
32 | id: yarn-cache
33 | with:
34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
36 | restore-keys: |
37 | ${{ runner.os }}-yarn-
38 |
39 | - name: Yarn install
40 | run: yarn install
41 |
42 | - name: Run Prettier
43 | uses: creyD/prettier_action@v4.3
44 | with:
45 | prettier_options: --check **/*.{js,md,vue,css,html}
46 | github_token: ${{ secrets.GITHUB_TOKEN }}
47 |
--------------------------------------------------------------------------------
/.github/workflows/updater.yml:
--------------------------------------------------------------------------------
1 | name: Updater CI
2 |
3 | on: workflow_dispatch
4 |
5 | jobs:
6 | release-update:
7 | runs-on: macos-latest
8 | steps:
9 | - name: Checkout repository
10 | uses: actions/checkout@v2
11 |
12 | - name: Reconfigure git to use HTTP authentication
13 | run: >
14 | git config --global url."https://github.com/".insteadOf
15 | ssh://git@github.com/
16 |
17 | - name: Get yarn cache dir path
18 | id: yarn-cache-dir-path
19 | run: echo "::set-output name=dir::$(yarn cache dir)"
20 |
21 | - name: Yarn Cache
22 | uses: actions/cache@v2
23 | id: yarn-cache
24 | with:
25 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
27 | restore-keys: |
28 | ${{ runner.os }}-yarn-
29 |
30 | - name: Yarn install
31 | run: yarn install
32 |
33 | - name: Release updater file
34 | run: yarn run updater
35 | env:
36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.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
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | src-tauri
2 | dist
3 | node_modules
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {}
2 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["Vue.volar", "esbenp.prettier-vscode"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2022] [ChurchTao]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Lanaya
5 |
6 |
7 |
8 |
9 | 一个简洁易用的剪贴板管理
10 |
11 |
12 |
17 |
18 |
22 |
23 | ## 简介
24 |
25 | `Lanaya` 来自于`DOTA2`中的圣堂刺客, 简洁易用,全键盘操作的剪贴板管理工具
26 |
27 | ### 当初写这个项目是学习阶段,写的简陋,目前正在准备重写 2.0 版本,敬请期待...
28 |
29 | ## 功能
30 |
31 | - 通过关键词搜索
32 | - 全快捷键操作
33 | - 设置历史条数范围
34 | - 多语言
35 | - 自动更新
36 | - 输入 `f:xxx` 搜索收藏的记录
37 | - 输入 `t:xxx` 搜索标签分类
38 |
39 | ## 未完成
40 |
41 | - [x] 引入`taildwind`管理 css
42 | - [x] 新增复制图片历史的功能
43 | - [ ] 增加主题
44 | - [x] 新增收藏夹功能
45 | - [ ] 增加`Windows`,`Linux`的适配
46 | - [x] 使用`Rust`实现后台监听剪切板
47 | - [x] 使用`Rust`实现`Sqlite`的数据库操作
48 |
49 | ## 下载
50 |
51 | 从 [release](https://github.com/ChurchTao/Lanaya/releases) 中下载.
52 |
53 | ### Mac 用户
54 |
55 | 如果提示`软件已损坏,请移到废纸篓`,可以使用命令 `xattr -cr /Applications/Lanaya.app` 解决
56 |
57 | ## 开发
58 |
59 | 你需要安装 `Rust` 和 `Nodejs`,详细步骤查看 [这里](https://tauri.app/zh-cn/v1/guides/getting-started/prerequisites) ,然后按如下命令进行开发
60 |
61 | ```shell
62 | npm install
63 | ```
64 |
65 | 然后
66 |
67 | ```shell
68 | cargo install tauri-cli # 提示没有 cargo tauri 命令需先执行安装
69 | cargo tauri dev
70 | ```
71 |
72 | 如果需要构建
73 |
74 | ```shell
75 | cargo tauri build
76 | ```
77 |
78 | ## 截图
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 | ## 建议
91 |
92 | 👏🏻 非常欢迎提`Issue`和`PR`!毕竟一个人的力量有限。
93 |
94 | ## 技术栈
95 |
96 | `Lanaya` 基于如下技术栈:
97 |
98 | - [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
99 | - [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
100 | - [vue3](https://github.com/vuejs/core): An approachable, performant and versatile framework for building web user interfaces.
101 | - [tailwindlabs](https://github.com/tailwindlabs) Creators of Tailwind CSS and Headless UI, and authors of Refactoring UI.
102 |
103 | ## License
104 |
105 | Apache-2.0 license. See [License here](./LICENSE) for details.
106 |
--------------------------------------------------------------------------------
/README_CN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Lanaya
5 |
6 |
7 |
8 |
9 | 一个简洁易用的剪贴板管理
10 |
11 |
12 |
17 |
18 |
22 |
23 | ## 简介
24 |
25 | `Lanaya` 来自于`DOTA2`中的圣堂刺客, 简洁易用,全键盘操作的剪贴板管理工具
26 |
27 | ## 功能
28 |
29 | - 通过关键词搜索
30 | - 全快捷键操作
31 | - 设置历史条数范围
32 | - 多语言
33 | - 自动更新
34 | - 输入 `f:xxx` 搜索收藏的记录
35 | - 输入 `t:xxx` 搜索标签分类
36 |
37 | ## 未完成
38 |
39 | - [x] 引入`taildwind`管理 css
40 | - [x] 新增复制图片历史的功能
41 | - [ ] 增加主题
42 | - [x] 新增收藏夹功能
43 | - [ ] 增加`Windows`,`Linux`的适配
44 | - [x] 使用`Rust`实现后台监听剪切板
45 | - [x] 使用`Rust`实现`Sqlite`的数据库操作
46 |
47 | ## 下载
48 |
49 | 从 [release](https://github.com/ChurchTao/Lanaya/releases) 中下载.
50 |
51 | ### Mac 用户
52 |
53 | 如果提示`软件已损坏,请移到废纸篓`,可以使用命令 `xattr -cr /Applications/Lanaya.app` 解决
54 |
55 | ## 开发
56 |
57 | 你需要安装 `Rust` 和 `Nodejs`,详细步骤查看 [这里](https://tauri.app/zh-cn/v1/guides/getting-started/prerequisites) ,然后按如下命令进行开发
58 |
59 | ```shell
60 | npm install
61 | ```
62 |
63 | 然后
64 |
65 | ```shell
66 | cargo install tauri-cli # 提示没有 cargo tauri 命令需先执行安装
67 | cargo tauri dev
68 | ```
69 |
70 | 如果需要构建
71 |
72 | ```shell
73 | cargo tauri build
74 | ```
75 |
76 | ## 截图
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | ## 建议
89 |
90 | 👏🏻 非常欢迎提`Issue`和`PR`!毕竟一个人的力量有限。
91 |
92 | ## 技术栈
93 |
94 | `Lanaya` 基于如下技术栈:
95 |
96 | - [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
97 | - [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
98 | - [vue3](https://github.com/vuejs/core): An approachable, performant and versatile framework for building web user interfaces.
99 | - [tailwindlabs](https://github.com/tailwindlabs) Creators of Tailwind CSS and Headless UI, and authors of Refactoring UI.
100 |
101 | ## License
102 |
103 | Apache-2.0 license. See [License here](./LICENSE) for details.
104 |
--------------------------------------------------------------------------------
/README_EN.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Lanaya
5 |
6 |
7 |
8 |
9 | A clipboard management with easy to use.
10 |
11 |
12 |
17 |
18 |
22 |
23 | ## Introduction
24 |
25 | Lanaya comes from Templar Assassin in Dota2, which is a clipboard management software with convenient and simple interaction.
26 |
27 | ### When I first wrote this project, it was in the learning stage and the writing was rudimentary. I am currently preparing to rewrite version 2.0, so stay tuned...
28 |
29 | ## Features
30 |
31 | - Search by keywords.
32 | - All shortcut to manage.
33 | - Setting history record range.
34 | - Multi-language.
35 | - Auto updater.
36 | - Input `f:xxx` to search favorite records.
37 | - Tag records and search by tags with `t:xxx,yyy`.
38 |
39 | ## Todos
40 |
41 | - [x] add `taildwind` to manage css.
42 | - [x] add copy image history.
43 | - [ ] add theme.
44 | - [x] add favorite.
45 | - [ ] add `Windows`,`Linux` support.
46 | - [x] use `Rust` to implement clipboard listener.
47 | - [x] use `Rust` to implement `Sqlite` database operation.
48 |
49 | ## Download
50 |
51 | Download from [release](https://github.com/ChurchTao/Lanaya/releases).
52 |
53 | ### Mac OS
54 |
55 | If you got error: 'Lanaya' is damaged and can’t be opened. You should move it to the Trash. You can use `xattr -cr /Applications/Lanaya.app` to solve it.
56 |
57 | ## Development
58 |
59 | You should install Rust and Nodejs, see [here](https://tauri.app/v1/guides/getting-started/prerequisites) for more details. Then install Nodejs packages.
60 |
61 | ```shell
62 | npm install
63 | ```
64 |
65 | Then run
66 |
67 | ```shell
68 | cargo install tauri-cli # output with [no such subcommand: `tauri`] please install first
69 |
70 | cargo tauri dev
71 | ```
72 |
73 | Or you can build it
74 |
75 | ```shell
76 | cargo tauri build
77 | ```
78 |
79 | ## Screenshots
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 | ## Contributions
92 |
93 | Issue and PR welcome!
94 |
95 | ## Acknowledgement
96 |
97 | Lanaya was based on or inspired by these projects and so on:
98 |
99 | - [tauri-apps/tauri](https://github.com/tauri-apps/tauri): Build smaller, faster, and more secure desktop applications with a web frontend.
100 | - [vitejs/vite](https://github.com/vitejs/vite): Next generation frontend tooling. It's fast!
101 | - [vue3](https://github.com/vuejs/core): An approachable, performant and versatile framework for building web user interfaces.
102 | - [tailwindlabs](https://github.com/tailwindlabs) Creators of Tailwind CSS and Headless UI, and authors of Refactoring UI.
103 |
104 | ## License
105 |
106 | Apache-2.0 license. See [License here](./LICENSE) for details.
107 |
--------------------------------------------------------------------------------
/UPDATE_LOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All changes will be documented in this file.
4 |
5 | ## v1.2.1
6 |
7 | - CN
8 | - 新增`windows`系统支持 [#12](https://github.com/ChurchTao/Lanaya/pull/12)
9 | - 暂时关闭了`自动更新`功能,因为官方的打包程序有问题
10 | - EN
11 | - Add windows support [#12](https://github.com/ChurchTao/Lanaya/pull/12)
12 | - Temporarily turned off the `auto update` function, because the official packaging program has problems
13 |
14 | ## v1.1.9
15 |
16 | ### Feature
17 |
18 | - CN
19 | - 新增功能`自动粘贴`,可以自动粘贴前一个窗口的焦点输入框 [#8](https://github.com/ChurchTao/Lanaya/pull/8)
20 | - 新增配置`删除前确认` [#7](https://github.com/ChurchTao/Lanaya/pull/7)
21 | - EN
22 | - Add function `auto paste`, can automatically paste the focus input box of the previous window [#8](https://github.com/ChurchTao/Lanaya/pull/8)
23 | - Add configuration `delete before confirm` [#7](https://github.com/ChurchTao/Lanaya/pull/7)
24 |
25 | ## v1.1.8
26 |
27 | ### Feature
28 |
29 | - CN
30 | - 新增功能,添加标签、搜索标签
31 | - EN
32 | - Add function, add tag, search tag
33 |
34 | ## v1.1.7
35 |
36 | - CN
37 |
38 | - 窗口出现时,会自动聚焦到搜索框 [#4](https://github.com/ChurchTao/Lanaya/pull/4)
39 | - 修复了当复制 html 标签时,列表框结果展示异常的问题 [#5](https://github.com/ChurchTao/Lanaya/pull/5)
40 |
41 | - EN
42 | - When the window appears, it will automatically focus on the search box [#4](https://github.com/ChurchTao/Lanaya/pull/4)
43 | - Fixed the problem that the list box result display is abnormal when copying html tags [#5](https://github.com/ChurchTao/Lanaya/pull/5)
44 |
45 | ## v1.1.6
46 |
47 | - CN
48 | - 新增引入了 `daisyui`
49 | - 升级了 `vite`
50 | - 修复了`限制条数`功能不生效的问题
51 | - EN
52 | - Added `daisyui`
53 | - Upgraded `vite`
54 | - Fixed the problem that the `limit number` function does not take effect
55 |
56 | ## v1.1.5
57 |
58 | - CN
59 | - 我忘了解开 `blur` 的注释..
60 | - EN
61 | - I forgot to remove the comment of `blur`
62 |
63 | ## v1.1.4
64 |
65 | ### Bug Fixes
66 |
67 | - CN
68 | - 修复了因为 `objc` 导致的内存泄露
69 | - 修复了显示速度
70 | - EN
71 | - Fixed the memory leak caused by `objc`
72 | - Fixed the display speed
73 |
74 | ## v1.1.3
75 |
76 | - CN
77 | - 修复了截取字符串时,不安全的切片导致的崩溃问题
78 | - EN
79 | - Fixed the problem of unsafe slice caused by crash when cutting strings
80 |
81 | ## v1.1.2
82 |
83 | ### Feature
84 |
85 | - CN
86 | - 新增`删除某一条`功能
87 | - EN:
88 | - add `delete one` feature.
89 |
90 | ### Bug Fixes
91 |
92 | - CN
93 | - 修复了大文本复制时,会导致 UI 渲染卡顿的问题
94 | - 修复了搜索收藏记录时,无法搜索到图片的问题
95 | - 修复了清空记录时,会清除收藏的记录的问题
96 | - EN
97 | - Fixed the problem that large text copy will cause UI rendering to stall
98 | - Fixed the problem that the image cannot be searched when searching for favorite records
99 | - Fixed the problem that the record will be cleared when the record is cleared
100 |
101 | ## v1.1.1
102 |
103 | ### Bug Fixes
104 |
105 | - CN
106 | - 修复了搜索时的 SQL 注入,导致崩溃的问题
107 | - EN
108 | - Fixed the problem of SQL injection when searching, causing a crash
109 |
110 | ## v1.1.0
111 |
112 | ### Feature
113 |
114 | - CN
115 | - 新增`收藏`功能
116 | - EN:
117 | - add `favorite` feature.
118 |
119 | ### Bug Fixes
120 |
121 | - CN
122 | - 修复复制 html 时,会导致显示问题
123 | - EN
124 | - Fixed the problem that copying html will cause display problems
125 |
126 | ## v1.0.3
127 |
128 | ### Feature
129 |
130 | - CN
131 | - 新增复制图片历史的功能
132 | - 使用`Rust`实现后台监听剪切板
133 | - 使用`Rust`实现`Sqlite`的数据库操作
134 | - EN:
135 | - add copy image history.
136 | - use `Rust` to implement clipboard listener.
137 | - use `Rust` to implement `Sqlite` database
138 |
139 | ### Bug Fixes
140 |
141 | - CN
142 | - 唤起窗口不限于主界面
143 | - 优化搜索高亮显示效果
144 | - 修复了复制文件时,会导致循环替换复制顺序的问题
145 | - EN
146 | - Wake up the window is not limited to the main interface
147 | - Optimize the search highlight display effect
148 | - Fixed the problem that copying files will cause the loop replacement copy order
149 |
150 | ## v1.0.2
151 |
152 | ### Bug Fixes
153 |
154 | - CN
155 | - 修复了复制文件时,会导致循环替换复制顺序的问题
156 | - EN
157 | - Fixed the problem that copying files will cause the loop replacement copy order
158 |
159 | ## v1.0.1
160 |
161 | ### Bug Fixes
162 |
163 | - CN
164 | - 修复了复制图片时,因 SCP 协议导致的图片无法显示的问题
165 | - EN
166 | - Fixed the problem that the image could not be displayed due to the SCP protocol when copying the image
167 |
168 | ## v1.0.0
169 |
170 | ### Feature
171 |
172 | - CN
173 | - 新增复制图片历史的功能
174 | - 使用`Rust`实现后台监听剪切板
175 | - 使用`Rust`实现`Sqlite`的数据库操作
176 | - EN:
177 | - add copy image history.
178 | - use `Rust` to implement clipboard listener.
179 | - use `Rust` to implement `Sqlite` database
180 |
181 | ### Bug Fixes
182 |
183 | - CN
184 | - 唤起窗口不限于主界面
185 | - 优化搜索高亮显示效果
186 | - EN
187 | - Wake up the window is not limited to the main interface
188 | - Optimize the search highlight display effect
189 |
190 | ## v0.1.1
191 |
192 | ### Feature
193 |
194 | - 实装 限制剪切板记录条数
195 |
196 | ### Bug Fixes
197 |
198 | - 主界面失去焦点时,不会自动关闭界面
199 |
200 | ## v0.1.0
201 |
202 | ### Feature
203 |
204 | - 实装 多语言
205 | - 实装 自动更新
206 | - 实装 快捷键设置
207 | - 实装 开机启动
208 |
209 | ## v0.0.9
210 |
211 | ### Feature
212 |
213 | - 这是一个测试版本,新增了,设置页面,自动版本更新(手动检测的还没写好),自动打包 ci,以及一些交互优化,过年了,先这样啦~
214 | - This is a test version, with new additions, settings page, automatic version update (manual detection has not yet been written), automatic packaging ci, and some interactive optimizations, Chinese New Year, let’s do this first~
215 | - 实装 快捷键设置
216 | - 实装 开机启动
217 |
218 | ## v0.0.8
219 |
220 | ### Feature
221 |
222 | - 新增了配置、关于两个页面,功能未全部实现
223 | - 优化主页面的交互
224 |
225 | ## v0.0.1
226 |
227 | ### Documentation
228 |
229 | - Base function complete.
230 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Lanaya
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/md/demo1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo1.png
--------------------------------------------------------------------------------
/md/demo2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo2.png
--------------------------------------------------------------------------------
/md/demo3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo3.png
--------------------------------------------------------------------------------
/md/demo4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo4.png
--------------------------------------------------------------------------------
/md/demo5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo5.png
--------------------------------------------------------------------------------
/md/demo6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo6.png
--------------------------------------------------------------------------------
/md/demo7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/demo7.png
--------------------------------------------------------------------------------
/md/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/md/icon.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "lanaya",
3 | "private": true,
4 | "version": "1.2.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "aarch": "node scripts/aarch.mjs",
11 | "updater": "node scripts/updater.mjs",
12 | "publish": "node scripts/publish.mjs",
13 | "tauri": "tauri",
14 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore --fix src",
15 | "format": "prettier --write .",
16 | "check": "prettier --check **/*.{js,md,vue,css,html}"
17 | },
18 | "dependencies": {
19 | "@headlessui/vue": "^1.7.16",
20 | "@heroicons/vue": "^2.0.18",
21 | "@intlify/unplugin-vue-i18n": "^1.4.0",
22 | "@tauri-apps/api": "^1.5.1",
23 | "hotkeys-js": "^3.12.0",
24 | "md5": "^2.3.0",
25 | "pinia": "^2.1.7",
26 | "vue": "^3.3.7",
27 | "vue-i18n": "^9.6.0",
28 | "vue-router": "^4.2.5"
29 | },
30 | "devDependencies": {
31 | "@actions/github": "^6.0.0",
32 | "@tauri-apps/cli": "^1.5.6",
33 | "@vitejs/plugin-vue": "^4.4.0",
34 | "autoprefixer": "^10.4.16",
35 | "daisyui": "^3.9.3",
36 | "eslint": "^8.52.0",
37 | "eslint-config-prettier": "^9.0.0",
38 | "eslint-plugin-vue": "^9.18.0",
39 | "fs-extra": "^11.1.1",
40 | "node-fetch": "^3.3.2",
41 | "postcss": "^8.4.31",
42 | "prettier": "3.0.3",
43 | "tailwindcss": "^3.3.5",
44 | "vite": "^4.5.0"
45 | }
46 | }
--------------------------------------------------------------------------------
/postcss.config.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/vite.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scripts/aarch.mjs:
--------------------------------------------------------------------------------
1 | /**
2 | * Build and upload assets for macOS(aarch)
3 | */
4 | import fs from "fs-extra";
5 | import path from "path";
6 | import { exit } from "process";
7 | import { createRequire } from "module";
8 | import { getOctokit } from "@actions/github";
9 |
10 | const require = createRequire(import.meta.url);
11 |
12 | async function resolve() {
13 | if (!process.env.GITHUB_TOKEN) {
14 | throw new Error("GITHUB_TOKEN is required");
15 | }
16 | if (!process.env.GITHUB_REPOSITORY) {
17 | throw new Error("GITHUB_REPOSITORY is required");
18 | }
19 | if (!process.env.TAURI_PRIVATE_KEY) {
20 | throw new Error("TAURI_PRIVATE_KEY is required");
21 | }
22 | if (!process.env.TAURI_KEY_PASSWORD) {
23 | throw new Error("TAURI_KEY_PASSWORD is required");
24 | }
25 |
26 | const { version } = require("../package.json");
27 |
28 | const cwd = process.cwd();
29 | const bundlePath = path.join(cwd, "src-tauri/target/release/bundle");
30 | const join = (p) => path.join(bundlePath, p);
31 |
32 | const appPathList = [
33 | join("macos/Lanaya.aarch64.app.tar.gz"),
34 | join("macos/Lanaya.aarch64.app.tar.gz.sig"),
35 | ];
36 |
37 | for (const appPath of appPathList) {
38 | if (fs.pathExistsSync(appPath)) {
39 | fs.removeSync(appPath);
40 | }
41 | }
42 |
43 | fs.copyFileSync(join("macos/Lanaya.app.tar.gz"), appPathList[0]);
44 | fs.copyFileSync(join("macos/Lanaya.app.tar.gz.sig"), appPathList[1]);
45 |
46 | const options = { owner: "ChurchTao", repo: "Lanaya" };
47 | const github = getOctokit(process.env.GITHUB_TOKEN);
48 | const { data: release } = await github.rest.repos.getReleaseByTag({
49 | ...options,
50 | tag: `v${version}`,
51 | });
52 |
53 | if (!release.id) throw new Error("failed to find the release");
54 |
55 | await uploadAssets(release.id, [
56 | join(`dmg/Lanaya_${version}_aarch64.dmg`),
57 | ...appPathList,
58 | ]);
59 | }
60 |
61 | // From tauri-apps/tauri-action
62 | // https://github.com/tauri-apps/tauri-action/blob/dev/packages/action/src/upload-release-assets.ts
63 | async function uploadAssets(releaseId, assets) {
64 | const github = getOctokit(process.env.GITHUB_TOKEN);
65 |
66 | // Determine content-length for header to upload asset
67 | const contentLength = (filePath) => fs.statSync(filePath).size;
68 |
69 | for (const assetPath of assets) {
70 | const headers = {
71 | "content-type": "application/zip",
72 | "content-length": contentLength(assetPath),
73 | };
74 |
75 | const ext = path.extname(assetPath);
76 | const filename = path.basename(assetPath).replace(ext, "");
77 | const assetName = path.dirname(assetPath).includes(`target${path.sep}debug`)
78 | ? `${filename}-debug${ext}`
79 | : `${filename}${ext}`;
80 |
81 | console.log(`[INFO]: Uploading ${assetName}...`);
82 | try {
83 | await github.rest.repos.uploadReleaseAsset({
84 | headers,
85 | name: assetName,
86 | data: fs.readFileSync(assetPath),
87 | owner: "ChurchTao",
88 | repo: "Lanaya",
89 | release_id: releaseId,
90 | });
91 | } catch (error) {
92 | console.log(error.message);
93 | }
94 | }
95 | }
96 |
97 | if (process.platform === "darwin" && process.arch === "arm64") {
98 | resolve();
99 | } else {
100 | console.error("invalid");
101 | exit(1);
102 | }
103 |
--------------------------------------------------------------------------------
/scripts/publish.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs-extra";
2 | import { createRequire } from "module";
3 | import { execSync } from "child_process";
4 | import { resolveUpdateLog } from "./updatelog.mjs";
5 |
6 | const require = createRequire(import.meta.url);
7 |
8 | // publish
9 | async function resolvePublish() {
10 | const flag = process.argv[2] ?? "patch";
11 | const packageJson = require("../package.json");
12 | const tauriJson = require("../src-tauri/tauri.conf.json");
13 |
14 | let [a, b, c] = packageJson.version.split(".").map(Number);
15 |
16 | if (flag === "major") {
17 | a += 1;
18 | b = 0;
19 | c = 0;
20 | } else if (flag === "minor") {
21 | b += 1;
22 | c = 0;
23 | } else if (flag === "patch") {
24 | c += 1;
25 | } else throw new Error(`invalid flag "${flag}"`);
26 |
27 | const nextVersion = `${a}.${b}.${c}`;
28 | packageJson.version = nextVersion;
29 | tauriJson.package.version = nextVersion;
30 |
31 | // 发布更新前先写更新日志
32 | const nextTag = `v${nextVersion}`;
33 | await resolveUpdateLog(nextTag);
34 |
35 | await fs.writeFile(
36 | "./package.json",
37 | JSON.stringify(packageJson, undefined, 2),
38 | );
39 | await fs.writeFile(
40 | "./src-tauri/tauri.conf.json",
41 | JSON.stringify(tauriJson, undefined, 2),
42 | );
43 |
44 | execSync("git add ./package.json");
45 | execSync("git add ./src-tauri/tauri.conf.json");
46 | execSync("git add ./UPDATE_LOG.md");
47 | execSync(`git commit -m "v${nextVersion}"`);
48 | execSync(`git tag -a v${nextVersion} -m "v${nextVersion}"`);
49 | execSync(`git push`);
50 | execSync(`git push origin v${nextVersion}`);
51 | console.log(`Publish Successfully...`);
52 | }
53 |
54 | resolvePublish();
55 |
--------------------------------------------------------------------------------
/scripts/updatelog.mjs:
--------------------------------------------------------------------------------
1 | import fs from "fs-extra";
2 | import path from "path";
3 |
4 | const UPDATE_LOG = "UPDATE_LOG.md";
5 |
6 | // parse the UPDATELOG.md
7 | export async function resolveUpdateLog(tag) {
8 | const cwd = process.cwd();
9 |
10 | const reTitle = /^## v[\d\.]+/;
11 | const reEnd = /^---/;
12 |
13 | const file = path.join(cwd, UPDATE_LOG);
14 |
15 | if (!(await fs.pathExists(file))) {
16 | throw new Error("could not found UPDATELOG.md");
17 | }
18 |
19 | const data = await fs.readFile(file).then((d) => d.toString("utf8"));
20 |
21 | const map = {};
22 | let p = "";
23 |
24 | data.split("\n").forEach((line) => {
25 | if (reTitle.test(line)) {
26 | p = line.slice(3).trim();
27 | if (!map[p]) {
28 | map[p] = [];
29 | } else {
30 | throw new Error(`Tag ${p} dup`);
31 | }
32 | } else if (reEnd.test(line)) {
33 | p = "";
34 | } else if (p) {
35 | map[p].push(line);
36 | }
37 | });
38 |
39 | if (!map[tag]) {
40 | throw new Error(`could not found "${tag}" in UPDATELOG.md`);
41 | }
42 |
43 | return map[tag].join("\n").trim();
44 | }
45 |
--------------------------------------------------------------------------------
/scripts/updater.mjs:
--------------------------------------------------------------------------------
1 | import fetch from "node-fetch";
2 | import { getOctokit, context } from "@actions/github";
3 | import { resolveUpdateLog } from "./updatelog.mjs";
4 |
5 | const UPDATE_TAG_NAME = "updater";
6 | const UPDATE_JSON_FILE = "update.json";
7 |
8 | /// generate update.json
9 | /// upload to update tag's release asset
10 | async function resolveUpdater() {
11 | if (process.env.GITHUB_TOKEN === undefined) {
12 | throw new Error("GITHUB_TOKEN is required");
13 | }
14 |
15 | const options = { owner: context.repo.owner, repo: context.repo.repo };
16 | const github = getOctokit(process.env.GITHUB_TOKEN);
17 |
18 | const { data: tags } = await github.rest.repos.listTags({
19 | ...options,
20 | per_page: 10,
21 | page: 1,
22 | });
23 |
24 | // get the latest publish tag
25 | const tag = tags.find((t) => t.name.startsWith("v"));
26 |
27 | console.log(tag);
28 |
29 | const { data: latestRelease } = await github.rest.repos.getReleaseByTag({
30 | ...options,
31 | tag: tag.name,
32 | });
33 |
34 | const updateData = {
35 | name: tag.name,
36 | notes: await resolveUpdateLog(tag.name), // use updatelog.md
37 | pub_date: new Date().toISOString(),
38 | platforms: {
39 | "darwin-aarch64": { signature: "", url: "" },
40 | "darwin-intel": { signature: "", url: "" },
41 | // "linux-x86_64": { signature: "", url: "" },
42 | "windows-x86_64": { signature: "", url: "" },
43 | },
44 | };
45 |
46 | const promises = latestRelease.assets.map(async (asset) => {
47 | const { name, browser_download_url } = asset;
48 |
49 | // win64 url
50 | if (name.endsWith(".msi.zip") && name.includes("en-US")) {
51 | updateData.platforms["windows-x86_64"].url = browser_download_url;
52 | }
53 | // win64 signature
54 | if (name.endsWith(".msi.zip.sig") && name.includes("en-US")) {
55 | const sig = await getSignature(browser_download_url);
56 | updateData.platforms["windows-x86_64"].signature = sig;
57 | }
58 |
59 | // darwin url (intel)
60 | if (name.endsWith(".app.tar.gz") && !name.includes("aarch")) {
61 | updateData.platforms["darwin-intel"].url = browser_download_url;
62 | }
63 | // darwin signature (intel)
64 | if (name.endsWith(".app.tar.gz.sig") && !name.includes("aarch")) {
65 | const sig = await getSignature(browser_download_url);
66 | updateData.platforms["darwin-intel"].signature = sig;
67 | }
68 |
69 | // darwin url (aarch)
70 | if (name.endsWith("aarch64.app.tar.gz")) {
71 | updateData.platforms["darwin-aarch64"].url = browser_download_url;
72 | }
73 | // darwin signature (aarch)
74 | if (name.endsWith("aarch64.app.tar.gz.sig")) {
75 | const sig = await getSignature(browser_download_url);
76 | updateData.platforms["darwin-aarch64"].signature = sig;
77 | }
78 |
79 | // // linux url
80 | // if (name.endsWith(".AppImage.tar.gz")) {
81 | // updateData.platforms["linux-x86_64"].url = browser_download_url;
82 | // }
83 | // // linux signature
84 | // if (name.endsWith(".AppImage.tar.gz.sig")) {
85 | // const sig = await getSignature(browser_download_url);
86 | // updateData.platforms["linux-x86_64"].signature = sig;
87 | // }
88 | });
89 |
90 | await Promise.allSettled(promises);
91 | console.log(updateData);
92 |
93 | // maybe should test the signature as well
94 | // delete the null field
95 | Object.entries(updateData.platforms).forEach(([key, value]) => {
96 | if (!value.url) {
97 | console.log(`[Error]: failed to parse release for "${key}"`);
98 | delete updateData.platforms[key];
99 | }
100 | });
101 |
102 | // update the update.json
103 | const { data: updateRelease } = await github.rest.repos.getReleaseByTag({
104 | ...options,
105 | tag: UPDATE_TAG_NAME,
106 | });
107 |
108 | // delete the old assets
109 | for (let asset of updateRelease.assets) {
110 | if (asset.name === UPDATE_JSON_FILE) {
111 | await github.rest.repos.deleteReleaseAsset({
112 | ...options,
113 | asset_id: asset.id,
114 | });
115 | }
116 | }
117 |
118 | // upload new assets
119 | await github.rest.repos.uploadReleaseAsset({
120 | ...options,
121 | release_id: updateRelease.id,
122 | name: UPDATE_JSON_FILE,
123 | data: JSON.stringify(updateData, null, 2),
124 | });
125 | }
126 |
127 | // get the signature file content
128 | async function getSignature(url) {
129 | const response = await fetch(url, {
130 | method: "GET",
131 | headers: { "Content-Type": "application/octet-stream" },
132 | });
133 |
134 | return response.text();
135 | }
136 |
137 | resolveUpdater().catch(console.error);
138 |
--------------------------------------------------------------------------------
/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 = "lanaya"
3 | version = "1.2.1"
4 | description = "a simple clipboard manager"
5 | authors = ["churchTao"]
6 | license = "Apache-2.0 license"
7 | repository = "https://github.com/ChurchTao/Lanaya"
8 | default-run = "lanaya"
9 | edition = "2021"
10 | rust-version = "1.59"
11 |
12 | [build-dependencies]
13 | tauri-build = { version = "1.2.1", features = [] }
14 |
15 | [dependencies]
16 | serde_json = "1.0"
17 | serde = { version = "1.0", features = ["derive"] }
18 | tauri = { version = "1.2.3", features = [
19 | "global-shortcut-all",
20 | "macos-private-api",
21 | "notification-all",
22 | "os-all",
23 | "shell-open",
24 | "system-tray",
25 | "window-all",
26 | ] }
27 | window-shadows = { git = "https://github.com/tauri-apps/window-shadows" }
28 | auto-launch = "0.4"
29 | once_cell = "1.17.0"
30 | anyhow = "1.0"
31 | parking_lot = "0.12.1"
32 | dunce = "1.0.3"
33 | rust-crypto = { version = "0.2.36" }
34 | rusqlite = { version = "0.28.0", features = ["bundled"] }
35 | chrono = "0.4.23"
36 | arboard = "3.2.1"
37 | base64 = "0.21.0"
38 | image = "0.24.5"
39 | rdev = "0.5.2"
40 | active-win-pos-rs = "0.7"
41 | winapi = { version = "0.3", features = ["winuser"] }
42 |
43 | [target.'cfg(target_os = "macos")'.dependencies]
44 | cocoa = "0.24.1"
45 |
46 | [features]
47 | # by default Tauri runs in production mode
48 | # when `tauri dev` runs it is executed with `cargo run --no-default-features` if `devPath` is an URL
49 | default = ["custom-protocol"]
50 | # this feature is used for production builds where `devPath` points to the filesystem
51 | # DO NOT remove this
52 | custom-protocol = ["tauri/custom-protocol"]
53 |
--------------------------------------------------------------------------------
/src-tauri/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | tauri_build::build()
3 | }
4 |
--------------------------------------------------------------------------------
/src-tauri/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/128x128.png
--------------------------------------------------------------------------------
/src-tauri/icons/128x128@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/128x128@2x.png
--------------------------------------------------------------------------------
/src-tauri/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/32x32.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square107x107Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square107x107Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square142x142Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square142x142Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square150x150Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square150x150Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square284x284Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square284x284Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square30x30Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square30x30Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square310x310Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square310x310Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square44x44Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square44x44Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square71x71Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square71x71Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/Square89x89Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/Square89x89Logo.png
--------------------------------------------------------------------------------
/src-tauri/icons/StoreLogo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/StoreLogo.png
--------------------------------------------------------------------------------
/src-tauri/icons/build.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | sips -z 16 16 icon.png --out tmp.iconset/icon_16x16.png
4 | sips -z 32 32 icon.png --out tmp.iconset/icon_16x16@2x.png
5 | sips -z 32 32 icon.png --out tmp.iconset/icon_32x32.png
6 | sips -z 64 64 icon.png --out tmp.iconset/icon_32x32@2x.png
7 | sips -z 128 128 icon.png --out tmp.iconset/icon_128x128.png
8 | sips -z 256 256 icon.png --out tmp.iconset/icon_128x128@2x.png
9 | sips -z 256 256 icon.png --out tmp.iconset/icon_256x256.png
10 | sips -z 512 512 icon.png --out tmp.iconset/icon_256x256@2x.png
11 | sips -z 512 512 icon.png --out tmp.iconset/icon_512x512.png
12 | sips -z 1024 1024 icon.png --out tmp.iconset/icon_512x512@2x.png
13 |
14 | iconutil -c icns tmp.iconset -o icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.icns:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/icon.icns
--------------------------------------------------------------------------------
/src-tauri/icons/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/icon.ico
--------------------------------------------------------------------------------
/src-tauri/icons/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src-tauri/icons/icon.png
--------------------------------------------------------------------------------
/src-tauri/src/cmds.rs:
--------------------------------------------------------------------------------
1 | use crate::{
2 | config,
3 | config::{CommonConfig, Config},
4 | core::{
5 | clipboard::{ClipBoardOprator, ImageDataDB},
6 | database::{QueryReq, Record, SqliteDB},
7 | handle::Handle,
8 | },
9 | log_err,
10 | utils::{json_util, dispatch_util},
11 | utils::window_util::{focus_window},
12 | PreviousProcessId,
13 | };
14 | use tauri::State;
15 |
16 | type CmdResult = Result;
17 |
18 | #[tauri::command]
19 | pub fn get_common_config() -> CmdResult {
20 | Ok(Config::common().data().clone())
21 | }
22 |
23 | #[tauri::command]
24 | pub fn set_common_config(config: CommonConfig) -> CmdResult {
25 | Config::common().draft().patch_config(config);
26 | Config::common().apply();
27 | log_err!(Config::common().data().save_file());
28 |
29 | // todo enable_auto_launch
30 | // todo hotkeys
31 | Handle::refresh_common_config();
32 | log_err!(Handle::update_systray());
33 | Ok(())
34 | }
35 |
36 | #[tauri::command]
37 | pub async fn change_language(language: String) -> CmdResult {
38 | let _ = config::modify_common_config(CommonConfig {
39 | language: Some(language),
40 | ..CommonConfig::default()
41 | })
42 | .await;
43 | Ok(())
44 | }
45 |
46 | #[tauri::command]
47 | pub async fn change_record_limit(limit: u32) -> CmdResult {
48 | let _ = config::modify_common_config(CommonConfig {
49 | record_limit: Some(limit),
50 | ..CommonConfig::default()
51 | })
52 | .await;
53 | Ok(())
54 | }
55 |
56 | #[tauri::command]
57 | pub async fn change_auto_launch(enable: bool) -> CmdResult {
58 | let _ = config::modify_common_config(CommonConfig {
59 | enable_auto_launch: Some(enable),
60 | ..CommonConfig::default()
61 | })
62 | .await;
63 | Ok(())
64 | }
65 |
66 | #[tauri::command]
67 | pub async fn change_auto_paste(enable: bool) -> CmdResult {
68 | if enable == true {
69 | dispatch_util::request_permissions();
70 | }
71 | let _ = config::modify_common_config(CommonConfig {
72 | enable_auto_paste: Some(enable),
73 | ..CommonConfig::default()
74 | })
75 | .await;
76 | Ok(())
77 | }
78 |
79 | #[tauri::command]
80 | pub async fn change_delete_confirm(enable: bool) -> CmdResult {
81 | let _ = config::modify_common_config(CommonConfig {
82 | enable_delete_confirm: Some(enable),
83 | ..CommonConfig::default()
84 | }).await;
85 | Ok(())
86 | }
87 |
88 | #[tauri::command]
89 | pub async fn change_theme_mode(theme_mode: String) -> CmdResult {
90 | let _ = config::modify_common_config(CommonConfig {
91 | theme_mode: Some(theme_mode),
92 | ..CommonConfig::default()
93 | })
94 | .await;
95 | Ok(())
96 | }
97 |
98 | #[tauri::command]
99 | pub async fn change_hotkeys(hotkeys: Vec) -> CmdResult {
100 | let _ = config::modify_common_config(CommonConfig {
101 | hotkeys: Some(hotkeys),
102 | ..CommonConfig::default()
103 | })
104 | .await;
105 | Ok(())
106 | }
107 |
108 | #[tauri::command]
109 | pub fn clear_data() -> bool {
110 | match SqliteDB::new().clear_data() {
111 | Ok(()) => true,
112 | Err(_) => false,
113 | }
114 | }
115 |
116 | #[tauri::command]
117 | pub fn insert_record(r: Record) -> bool {
118 | match SqliteDB::new().insert_record(r) {
119 | Ok(_i) => true,
120 | Err(e) => {
121 | println!("err:{}", e);
122 | false
123 | }
124 | }
125 | }
126 |
127 | #[tauri::command]
128 | pub fn insert_if_not_exist(r: Record) -> bool {
129 | match SqliteDB::new().insert_if_not_exist(r) {
130 | Ok(_i) => true,
131 | Err(e) => {
132 | println!("err:{}", e);
133 | false
134 | }
135 | }
136 | }
137 |
138 | #[tauri::command]
139 | pub fn find_all_record() -> Vec {
140 | SqliteDB::new().find_all().unwrap()
141 | }
142 |
143 | #[tauri::command]
144 | pub fn mark_favorite(id: u64) -> bool {
145 | match SqliteDB::new().mark_favorite(id) {
146 | Ok(_i) => true,
147 | Err(e) => {
148 | println!("err:{}", e);
149 | false
150 | }
151 | }
152 | }
153 |
154 | #[tauri::command]
155 | pub fn save_tags(id: u64, tags: String) -> bool {
156 | match SqliteDB::new().save_tags(id, tags) {
157 | Ok(_i) => true,
158 | Err(e) => {
159 | println!("err:{}", e);
160 | false
161 | }
162 | }
163 | }
164 |
165 | #[tauri::command]
166 | pub fn delete_by_id(id: u64) -> bool {
167 | match SqliteDB::new().delete_by_id(id) {
168 | Ok(_i) => true,
169 | Err(e) => {
170 | println!("err:{}", e);
171 | false
172 | }
173 | }
174 | }
175 |
176 | #[tauri::command]
177 | pub fn find_by_key(query: QueryReq) -> Vec {
178 | SqliteDB::new().find_by_key(query).unwrap()
179 | }
180 |
181 | #[tauri::command]
182 | pub fn delete_over_limit(limit: usize) -> bool {
183 | match SqliteDB::new().delete_over_limit(limit) {
184 | Ok(res) => res,
185 | Err(e) => {
186 | println!("err:{}", e);
187 | false
188 | }
189 | }
190 | }
191 |
192 | #[tauri::command]
193 | pub fn write_to_clip(id: u64) -> bool {
194 | let record = SqliteDB::new().find_by_id(id);
195 | match record {
196 | Ok(r) => {
197 | if r.data_type == "text" {
198 | let _ = ClipBoardOprator::set_text(r.content);
199 | } else if r.data_type == "image" {
200 | let image_data: ImageDataDB = json_util::parse(&r.content).unwrap();
201 | let _ = ClipBoardOprator::set_image(image_data);
202 | }
203 | true
204 | }
205 | Err(e) => {
206 | println!("err:{}", e);
207 | false
208 | }
209 | }
210 | }
211 |
212 | #[tauri::command]
213 | pub fn focus_previous_window(previous_process_id: State) -> CmdResult {
214 | focus_window(*previous_process_id.0.lock().unwrap());
215 | Ok(())
216 | }
217 |
218 | #[tauri::command]
219 | pub fn paste_in_previous_window(previous_process_id: State) -> CmdResult {
220 | focus_window(*previous_process_id.0.lock().unwrap());
221 | dispatch_util::paste();
222 | Ok(())
223 | }
--------------------------------------------------------------------------------
/src-tauri/src/config/common_config.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::{dirs, json_util};
2 | use anyhow::Result;
3 | use serde::{Deserialize, Serialize};
4 |
5 | #[derive(Default, Debug, Clone, Deserialize, Serialize)]
6 | pub struct CommonConfig {
7 | // i18n
8 | pub language: Option,
9 | /// `light` or `dark` or todo `system`
10 | pub theme_mode: Option,
11 | /// can the app auto startup
12 | pub enable_auto_launch: Option,
13 | /// can the app paste automatically in previous window
14 | pub enable_auto_paste: Option,
15 | /// delete confirm
16 | pub enable_delete_confirm: Option,
17 | /// hotkey map
18 | /// format: {func},{key}
19 | pub hotkeys: Option>,
20 | // pub font_family: Option,
21 | // pub font_size: Option,
22 | pub record_limit: Option,
23 | }
24 |
25 | impl CommonConfig {
26 | pub fn new() -> Self {
27 | match dirs::config_path().and_then(|path| json_util::read::(&path)) {
28 | Ok(config) => {
29 | // 先拿 template
30 | // 再拿 config 去覆盖
31 | let mut template = Self::template();
32 | template.merge(config);
33 | template
34 | }
35 | Err(_) => Self::template(),
36 | }
37 | }
38 |
39 | pub fn template() -> Self {
40 | // todo windows 快捷键不一样,需要兼容
41 | Self {
42 | language: match cfg!(feature = "default-meta") {
43 | false => Some("zh".into()),
44 | true => Some("en".into()),
45 | },
46 | theme_mode: Some("light".into()),
47 | enable_auto_launch: Some(false),
48 | enable_auto_paste: Some(false),
49 | enable_delete_confirm: Some(true),
50 | record_limit: Some(100),
51 | hotkeys: Some(vec![
52 | "clear-history:8+16+91".into(),
53 | "global-shortcut:16+67+91".into(),
54 | ]),
55 | }
56 | }
57 |
58 | pub fn save_file(&self) -> Result<()> {
59 | json_util::save(&dirs::config_path()?, &self)
60 | }
61 |
62 | pub fn merge(&mut self, other: Self) {
63 | if let Some(language) = other.language {
64 | self.language = Some(language);
65 | }
66 | if let Some(theme_mode) = other.theme_mode {
67 | self.theme_mode = Some(theme_mode);
68 | }
69 | if let Some(enable_auto_launch) = other.enable_auto_launch {
70 | self.enable_auto_launch = Some(enable_auto_launch);
71 | }
72 | if let Some(enable_auto_paste) = other.enable_auto_paste {
73 | self.enable_auto_paste = Some(enable_auto_paste);
74 | }
75 | if let Some(delete_confirm) = other.enable_delete_confirm {
76 | self.enable_delete_confirm = Some(delete_confirm);
77 | }
78 | if let Some(record_limit) = other.record_limit {
79 | self.record_limit = Some(record_limit);
80 | }
81 | if let Some(hotkeys) = other.hotkeys {
82 | self.hotkeys = Some(hotkeys);
83 | }
84 | }
85 |
86 | pub fn patch_config(&mut self, patch: CommonConfig) {
87 | macro_rules! patch {
88 | ($key: tt) => {
89 | if patch.$key.is_some() {
90 | self.$key = patch.$key;
91 | }
92 | };
93 | }
94 | patch!(language);
95 | patch!(theme_mode);
96 | patch!(enable_auto_launch);
97 | patch!(enable_auto_paste);
98 | patch!(enable_delete_confirm);
99 | patch!(hotkeys);
100 | patch!(record_limit);
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src-tauri/src/config/config.rs:
--------------------------------------------------------------------------------
1 | use super::{CommonConfig, Draft};
2 | use crate::{
3 | core::handle,
4 | core::sysopt,
5 | log_err,
6 | utils::{dirs, json_util},
7 | };
8 | use anyhow::Result;
9 | use once_cell::sync::OnceCell;
10 | use std::fs;
11 |
12 | pub struct Config {
13 | common_config: Draft,
14 | }
15 |
16 | impl Config {
17 | pub fn global() -> &'static Config {
18 | static CONFIG: OnceCell = OnceCell::new();
19 |
20 | CONFIG.get_or_init(|| Config {
21 | common_config: Draft::from(CommonConfig::new()),
22 | })
23 | }
24 |
25 | pub fn common() -> Draft {
26 | Self::global().common_config.clone()
27 | }
28 |
29 | /// 初始化配置
30 | pub fn init_config() -> Result<()> {
31 | log_err!(dirs::app_home_dir().map(|app_dir| {
32 | if !app_dir.exists() {
33 | let _ = fs::create_dir_all(&app_dir);
34 | }
35 | }));
36 | log_err!(dirs::app_data_dir().map(|app_dir| {
37 | if !app_dir.exists() {
38 | let _ = fs::create_dir_all(&app_dir);
39 | }
40 | }));
41 | log_err!(dirs::app_data_img_dir().map(|app_dir| {
42 | if !app_dir.exists() {
43 | let _ = fs::create_dir_all(&app_dir);
44 | }
45 | }));
46 | log_err!(dirs::config_path().map(|path| {
47 | if !path.exists() {
48 | log_err!(json_util::save(&path, &CommonConfig::template()));
49 | }
50 | }));
51 | Ok(())
52 | }
53 | }
54 |
55 | /// 修改通用配置文件的入口
56 | pub async fn modify_common_config(patch: CommonConfig) -> Result<()> {
57 | Config::common().draft().patch_config(patch.clone());
58 |
59 | let auto_launch = patch.enable_auto_launch;
60 | let auto_paste = patch.enable_auto_paste;
61 | let delete_confirm = patch.enable_delete_confirm;
62 | let language = patch.language;
63 | let theme_mode = patch.theme_mode;
64 | let record_limit = patch.record_limit;
65 | let hotkeys = patch.hotkeys;
66 |
67 | match {
68 | if auto_launch.is_some() {
69 | sysopt::Sysopt::global().update_launch()?;
70 | }
71 |
72 | if hotkeys.is_some() {
73 | let _ = handle::Handle::refresh_global_shortcut();
74 | handle::Handle::notice_to_window(handle::MsgTypeEnum::ChangeHotKeys, hotkeys)?;
75 | }
76 |
77 | if language.is_some() {
78 | handle::Handle::update_systray()?;
79 | handle::Handle::notice_to_window(handle::MsgTypeEnum::ChangeLanguage, language)?;
80 | }
81 |
82 | if theme_mode.is_some() {
83 | // todo send msg to frontend
84 | }
85 |
86 | if record_limit.is_some() {
87 | handle::Handle::notice_to_window(handle::MsgTypeEnum::ChangeRecordLimit, record_limit)?;
88 | }
89 |
90 | if auto_paste.is_some() {
91 | handle::Handle::notice_to_window(handle::MsgTypeEnum::ChangeAutoPaste, auto_paste)?;
92 | }
93 |
94 | if delete_confirm.is_some() {
95 | handle::Handle::notice_to_window(handle::MsgTypeEnum::ChangeDeleteConfirm, delete_confirm)?;
96 | }
97 |
98 | >::Ok(())
99 | } {
100 | Ok(()) => {
101 | Config::common().apply();
102 | Config::common().data().save_file()?;
103 | Ok(())
104 | }
105 | Err(err) => {
106 | Config::common().discard();
107 | Err(err)
108 | }
109 | }
110 | }
111 |
112 | #[test]
113 | fn test_config() {
114 | log_err!(Config::init_config());
115 | let common = Config::common();
116 | println!("common: {:?}", common.data());
117 | }
118 |
--------------------------------------------------------------------------------
/src-tauri/src/config/draft.rs:
--------------------------------------------------------------------------------
1 | use super::CommonConfig;
2 | use parking_lot::{MappedMutexGuard, Mutex, MutexGuard};
3 | use std::sync::Arc;
4 |
5 | // 草稿,用于修改配置,T=正式数据 Option=草稿数据
6 | #[derive(Debug, Clone)]
7 | pub struct Draft {
8 | inner: Arc)>>,
9 | }
10 |
11 | macro_rules! draft_define {
12 | ($id: ident) => {
13 | impl Draft<$id> {
14 | // 获取正式数据
15 | #[allow(unused)]
16 | pub fn data(&self) -> MappedMutexGuard<$id> {
17 | MutexGuard::map(self.inner.lock(), |guard| &mut guard.0)
18 | }
19 |
20 | // 获取最后修改记录
21 | pub fn latest(&self) -> MappedMutexGuard<$id> {
22 | MutexGuard::map(self.inner.lock(), |inner| {
23 | if inner.1.is_none() {
24 | &mut inner.0
25 | } else {
26 | inner.1.as_mut().unwrap()
27 | }
28 | })
29 | }
30 |
31 | // 获取草稿数据
32 | pub fn draft(&self) -> MappedMutexGuard<$id> {
33 | MutexGuard::map(self.inner.lock(), |inner| {
34 | if inner.1.is_none() {
35 | inner.1 = Some(inner.0.clone());
36 | }
37 | inner.1.as_mut().unwrap()
38 | })
39 | }
40 |
41 | // 同步草稿数据到正式数据
42 | pub fn apply(&self) -> Option<$id> {
43 | let mut inner = self.inner.lock();
44 | match inner.1.take() {
45 | Some(draft) => {
46 | let old_value = inner.0.to_owned();
47 | inner.0 = draft.to_owned();
48 | Some(old_value)
49 | }
50 | None => None,
51 | }
52 | }
53 |
54 | // 丢弃草稿数据
55 | #[allow(unused)]
56 | pub fn discard(&self) -> Option<$id> {
57 | let mut inner = self.inner.lock();
58 | inner.1.take()
59 | }
60 | }
61 |
62 | impl From<$id> for Draft<$id> {
63 | fn from(data: $id) -> Self {
64 | Draft {
65 | inner: Arc::new(Mutex::new((data, None))),
66 | }
67 | }
68 | }
69 | };
70 | }
71 |
72 | draft_define!(CommonConfig);
73 |
--------------------------------------------------------------------------------
/src-tauri/src/config/mod.rs:
--------------------------------------------------------------------------------
1 | mod common_config;
2 | mod config;
3 | mod draft;
4 |
5 | pub use self::common_config::*;
6 | pub use self::config::*;
7 | pub use self::draft::*;
8 |
--------------------------------------------------------------------------------
/src-tauri/src/core/clipboard.rs:
--------------------------------------------------------------------------------
1 | use super::database;
2 | use super::handle::{self, MsgTypeEnum};
3 | use crate::config::Config;
4 | use crate::core::database::Record;
5 | use crate::utils::{img_util, json_util, string_util};
6 | use anyhow::Result;
7 | use arboard::Clipboard;
8 | use chrono::Duration;
9 | use serde::{Deserialize, Serialize};
10 |
11 | use std::thread;
12 | const CHANGE_DEFAULT_MSG: &str = "ok";
13 |
14 | pub struct ClipboardWatcher;
15 |
16 | pub struct ClipBoardOprator;
17 |
18 | #[derive(Default, Debug, Clone, Deserialize, Serialize)]
19 | pub struct ImageDataDB {
20 | pub width: usize,
21 | pub height: usize,
22 | pub base64: String,
23 | }
24 |
25 | impl ClipBoardOprator {
26 | pub fn set_text(text: String) -> Result<()> {
27 | let mut clipboard = Clipboard::new()?;
28 | clipboard.set_text(text)?;
29 | Ok(())
30 | }
31 |
32 | pub fn set_image(data: ImageDataDB) -> Result<()> {
33 | let mut clipboard = Clipboard::new()?;
34 | let img_data = img_util::base64_to_rgba8(&data.base64).unwrap();
35 | clipboard.set_image(img_data)?;
36 | Ok(())
37 | }
38 | }
39 |
40 | impl ClipboardWatcher {
41 | pub fn start() {
42 | tauri::async_runtime::spawn(async {
43 | // 1000毫秒检测一次剪切板变化
44 | let wait_millis = 1000i64;
45 | let mut last_content_md5 = String::new();
46 | let mut last_img_md5 = String::new();
47 | let mut clipboard = Clipboard::new().unwrap();
48 | println!("start clipboard watcher");
49 | loop {
50 | let mut need_notify = false;
51 | let db = database::SqliteDB::new();
52 | let text = clipboard.get_text();
53 | let _ = text.map(|text| {
54 | let content_origin = text.clone();
55 | let content = text.trim();
56 | let md5 = string_util::md5(&content_origin);
57 | if !content.is_empty() && md5 != last_content_md5 {
58 | // 说明有新内容
59 | let content_preview = if content.len() > 1000 {
60 | Some(content.chars().take(1000).collect())
61 | } else {
62 | Some(content.to_string())
63 | };
64 | let res = db.insert_if_not_exist(Record {
65 | content: content_origin,
66 | content_preview,
67 | data_type: "text".to_string(),
68 | is_favorite: false,
69 | ..Default::default()
70 | });
71 | match res {
72 | Ok(_) => {
73 | need_notify = true;
74 | }
75 | Err(e) => {
76 | println!("insert record error: {}", e);
77 | }
78 | }
79 | last_content_md5 = md5;
80 | }
81 | });
82 |
83 | let img = clipboard.get_image();
84 | let _ = img.map(|img| {
85 | let img_md5 = string_util::md5_by_bytes(&img.bytes);
86 | if img_md5 != last_img_md5 {
87 | // 有新图片产生
88 | let base64 = img_util::rgba8_to_base64(&img);
89 | let content_db = ImageDataDB {
90 | width: img.width,
91 | height: img.height,
92 | base64,
93 | };
94 | // 压缩画质作为预览图,防止渲染时非常卡顿
95 | let jpeg_base64 = img_util::rgba8_to_jpeg_base64(&img, 75);
96 | let content_preview_db = ImageDataDB {
97 | width: img.width,
98 | height: img.height,
99 | base64: jpeg_base64,
100 | };
101 | let content = json_util::stringfy(&content_db).unwrap();
102 | let content_preview = json_util::stringfy(&content_preview_db).unwrap();
103 | let res = db.insert_if_not_exist(Record {
104 | content,
105 | content_preview: Some(content_preview),
106 | data_type: "image".to_string(),
107 | is_favorite: false,
108 | ..Default::default()
109 | });
110 | match res {
111 | Ok(_) => {
112 | drop(img);
113 | need_notify = true;
114 | }
115 | Err(e) => {
116 | println!("insert record error: {}", e);
117 | }
118 | }
119 | last_img_md5 = img_md5;
120 | }
121 | });
122 | let limit = Config::common().latest().record_limit.clone();
123 | if let Some(l) = limit {
124 | let res = db.delete_over_limit(l as usize);
125 | if let Ok(success) = res {
126 | if success {
127 | need_notify = true;
128 | }
129 | }
130 | }
131 | if need_notify {
132 | handle::Handle::notice_to_window(
133 | MsgTypeEnum::ChangeClipBoard,
134 | CHANGE_DEFAULT_MSG,
135 | )
136 | .unwrap();
137 | }
138 | thread::sleep(Duration::milliseconds(wait_millis).to_std().unwrap());
139 | }
140 | });
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/src-tauri/src/core/database.rs:
--------------------------------------------------------------------------------
1 | use crate::utils::dirs::app_data_dir;
2 | use crate::utils::string_util;
3 | use anyhow::Result;
4 | use rusqlite::{Connection, OpenFlags};
5 | use std::fs::File;
6 | use std::path::Path;
7 |
8 | #[derive(serde::Serialize, serde::Deserialize, Debug, Default, PartialEq)]
9 | pub struct Record {
10 | pub id: u64,
11 | pub content: String,
12 | pub content_preview: Option,
13 | // data_type(文本=text、图片=image)
14 | pub data_type: String,
15 | pub md5: String,
16 | pub create_time: u64,
17 | pub is_favorite: bool,
18 | pub tags: String,
19 | // 仅在搜索返回时使用
20 | pub content_highlight: Option,
21 | }
22 |
23 | #[derive(serde::Serialize, serde::Deserialize, Debug, Default)]
24 | pub struct QueryReq {
25 | pub key: Option,
26 | pub limit: Option,
27 | pub is_favorite: Option,
28 | pub tags: Option>,
29 | }
30 |
31 | pub struct SqliteDB {
32 | conn: Connection,
33 | }
34 |
35 | const SQLITE_FILE: &str = "data_v1_1_8.sqlite";
36 |
37 | #[allow(unused)]
38 | impl SqliteDB {
39 | pub fn new() -> Self {
40 | let data_dir = app_data_dir().unwrap().join(SQLITE_FILE);
41 | let c = Connection::open_with_flags(data_dir, OpenFlags::SQLITE_OPEN_READ_WRITE).unwrap();
42 | SqliteDB { conn: c }
43 | }
44 |
45 | pub fn init() {
46 | let data_dir = app_data_dir().unwrap().join(SQLITE_FILE);
47 | if !Path::new(&data_dir).exists() {
48 | File::create(&data_dir).unwrap();
49 | }
50 | let c = Connection::open_with_flags(data_dir, OpenFlags::SQLITE_OPEN_READ_WRITE).unwrap();
51 | let sql = r#"
52 | create table if not exists record
53 | (
54 | id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
55 | content TEXT,
56 | content_preview TEXT,
57 | data_type VARCHAR(20) DEFAULT '',
58 | md5 VARCHAR(200) DEFAULT '',
59 | create_time INTEGER,
60 | is_favorite INTEGER DEFAULT 0,
61 | tags VARCHAR(256) DEFAULT ''
62 | );
63 | "#;
64 | c.execute(sql, ()).unwrap();
65 | }
66 |
67 | pub fn insert_record(&self, r: Record) -> Result {
68 | let sql = "insert into record (content,md5,create_time,is_favorite,data_type,content_preview) values (?1,?2,?3,?4,?5,?6)";
69 | let md5 = string_util::md5(r.content.as_str());
70 | let now = chrono::Local::now().timestamp_millis() as u64;
71 | let content_preview = r.content_preview.unwrap_or("".to_string());
72 | let res = self.conn.execute(
73 | sql,
74 | (
75 | &r.content,
76 | md5,
77 | now,
78 | &r.is_favorite,
79 | &r.data_type,
80 | content_preview,
81 | ),
82 | )?;
83 | Ok(self.conn.last_insert_rowid())
84 | }
85 |
86 | fn find_record_by_md5(&self, md5: String) -> Result {
87 | let sql = "SELECT id, content, md5, create_time, is_favorite FROM record WHERE md5 = ?1";
88 | let r = self.conn.query_row(sql, [md5], |row| {
89 | Ok(Record {
90 | id: row.get(0)?,
91 | ..Default::default()
92 | })
93 | })?;
94 | Ok(r)
95 | }
96 |
97 | // 更新时间
98 | fn update_record_create_time(&self, r: Record) -> Result<()> {
99 | let sql = "update record set create_time = ?2 where id = ?1";
100 | // 获取当前毫秒级时间戳
101 | let now = chrono::Local::now().timestamp_millis() as u64;
102 | self.conn.execute(sql, [&r.id, &now])?;
103 | Ok(())
104 | }
105 |
106 | pub fn insert_if_not_exist(&self, r: Record) -> Result<()> {
107 | let md5 = string_util::md5(r.content.as_str());
108 | match self.find_record_by_md5(md5) {
109 | Ok(res) => {
110 | self.update_record_create_time(res)?;
111 | }
112 | Err(_e) => {
113 | self.insert_record(r)?;
114 | }
115 | }
116 | Ok(())
117 | }
118 |
119 | pub fn md5_is_exist(&self, md5: String) -> Result {
120 | let sql = "SELECT count(*) FROM record WHERE md5 = ?1";
121 | let count: u32 = self.conn.query_row(sql, [md5], |row| row.get(0))?;
122 | Ok(count > 0)
123 | }
124 |
125 | // 清除数据
126 | pub fn clear_data(&self) -> Result<()> {
127 | let sql = "delete from record where is_favorite = 0";
128 | self.conn.execute(sql, ())?;
129 | Ok(())
130 | }
131 |
132 | pub fn delete_by_id(&self, id: u64) -> Result<()> {
133 | let sql = "delete from record where id = ?1";
134 | self.conn.execute(sql, [&id])?;
135 | Ok(())
136 | }
137 |
138 | // 标记为收藏,如有已经收藏了的则取消收藏
139 | pub fn mark_favorite(&self, id: u64) -> Result<()> {
140 | let record = self.find_by_id(id)?;
141 | let sql = "update record set is_favorite = ?2 where id = ?1";
142 | let is_favorite = if record.is_favorite { 0 } else { 1 };
143 | self.conn.execute(sql, [&id, &is_favorite])?;
144 | Ok(())
145 | }
146 |
147 | pub fn save_tags(&self, id: u64, tags: String) -> Result<()> {
148 | let sql = "update record set tags = ?2 where id = ?1";
149 | self.conn.execute(sql, (&id, &tags))?;
150 | Ok(())
151 | }
152 |
153 | pub fn find_all(&self) -> Result> {
154 | let sql = "SELECT id, content_preview, data_type, md5, create_time, is_favorite, tags FROM record order by create_time desc";
155 | let mut stmt = self.conn.prepare(sql)?;
156 | let mut rows = stmt.query([])?;
157 | let mut res = vec![];
158 | while let Some(row) = rows.next()? {
159 | let data_type: String = row.get(2)?;
160 | let content: String = row.get(1)?;
161 | let tags: String = row.get(6)?;
162 | let r = Record {
163 | id: row.get(0)?,
164 | content,
165 | content_preview: None,
166 | data_type,
167 | md5: row.get(3)?,
168 | create_time: row.get(4)?,
169 | is_favorite: row.get(5)?,
170 | content_highlight: None,
171 | tags,
172 | };
173 | res.push(r);
174 | }
175 | Ok(res)
176 | }
177 |
178 | pub fn find_by_key(&self, req: QueryReq) -> Result> {
179 | let mut sql: String = String::new();
180 | sql.push_str(
181 | "SELECT id, content_preview, md5, create_time, is_favorite, data_type, tags FROM record where 1=1",
182 | );
183 | let mut limit: usize = 300;
184 | let mut params: Vec = vec![];
185 | if let Some(l) = req.limit {
186 | limit = l;
187 | }
188 | params.push(limit.to_string());
189 | if let Some(k) = &req.key {
190 | params.push(format!("%{}%", k));
191 | sql.push_str(
192 | format!(" and data_type='text' and content like ?{}", params.len()).as_str(),
193 | );
194 | }
195 | if let Some(is_fav) = req.is_favorite {
196 | let is_fav_int = if is_fav { 1 } else { 0 };
197 | params.push(is_fav_int.to_string());
198 | sql.push_str(format!(" and is_favorite = ?{}", params.len()).as_str());
199 | }
200 | if let Some(tags) = req.tags {
201 | for tag in tags.iter() {
202 | params.push(format!("%{}%", tag));
203 | sql.push_str(format!(" and tags like ?{}", params.len()).as_str());
204 | }
205 | }
206 | let sql = format!("{} order by create_time desc limit ?1", sql);
207 | let mut stmt = self.conn.prepare(&sql)?;
208 | let mut rows = stmt.query(rusqlite::params_from_iter(params))?;
209 | let mut res = vec![];
210 | while let Some(row) = rows.next()? {
211 | let data_type: String = row.get(5)?;
212 | let content: String = row.get(1)?;
213 | let tags: String = row.get(6)?;
214 | let content_highlight = req
215 | .key
216 | .as_ref()
217 | .map(|key| string_util::highlight(key.as_str(), content.as_str()));
218 | let r = Record {
219 | id: row.get(0)?,
220 | content,
221 | content_preview: None,
222 | data_type,
223 | md5: row.get(2)?,
224 | create_time: row.get(3)?,
225 | is_favorite: row.get(4)?,
226 | content_highlight,
227 | tags,
228 | };
229 | res.push(r);
230 | }
231 | Ok(res)
232 | }
233 |
234 | //删除超过limit的记录
235 | pub fn delete_over_limit(&self, limit: usize) -> Result {
236 | // 先查询count,如果count - limit > 50 就删除 超出limit部分记录 主要是防止频繁重建数据库
237 | let mut stmt = self
238 | .conn
239 | .prepare("SELECT count(*) FROM record where is_favorite = 0")?;
240 | let mut rows = stmt.query([])?;
241 | let count: usize = rows.next()?.unwrap().get(0).unwrap();
242 | if count < 10 + limit {
243 | return Ok(false);
244 | }
245 | let remove_num = count - limit;
246 | let sql = "DELETE FROM record WHERE is_favorite = 0 and id in (SELECT id FROM record where is_favorite = 0 order by create_time asc limit ?1)";
247 | self.conn.execute(sql, [remove_num])?;
248 | Ok(true)
249 | }
250 |
251 | pub fn find_by_id(&self, id: u64) -> Result {
252 | let sql = "SELECT id, content, data_type, md5, create_time, is_favorite, tags FROM record where id = ?1";
253 | let r = self.conn.query_row(sql, [&id], |row| {
254 | Ok(Record {
255 | id: row.get(0)?,
256 | content: row.get(1)?,
257 | content_preview: None,
258 | data_type: row.get(2)?,
259 | md5: row.get(3)?,
260 | create_time: row.get(4)?,
261 | is_favorite: row.get(5)?,
262 | content_highlight: None,
263 | tags: row.get(6)?,
264 | })
265 | })?;
266 | Ok(r)
267 | }
268 | }
269 |
270 | #[test]
271 | fn test_sqlite_insert() {
272 | SqliteDB::init();
273 | let r = Record {
274 | content: "123456".to_string(),
275 | md5: "e10adc3949ba59abbe56e057f20f883e".to_string(),
276 | create_time: 1234568,
277 | ..Default::default()
278 | };
279 | assert_eq!(SqliteDB::new().insert_record(r).unwrap(), 1_i64)
280 | }
281 |
--------------------------------------------------------------------------------
/src-tauri/src/core/handle.rs:
--------------------------------------------------------------------------------
1 | use super::{
2 | tray::Tray,
3 | window_manager::{WindowInfo, WindowType},
4 | };
5 | use crate::{config::Config, log_err, utils::{hotkey_util, window_util}, PreviousProcessId};
6 | use anyhow::{bail, Result};
7 | use once_cell::sync::OnceCell;
8 | use parking_lot::Mutex;
9 | use serde::Serialize;
10 | use std::sync::Arc;
11 | use tauri::{AppHandle, GlobalShortcutManager, Manager, Window};
12 | use tauri::State;
13 | use window_shadows::set_shadow;
14 |
15 | #[derive(Debug, Default, Clone)]
16 | pub struct Handle {
17 | pub app_handle: Arc>>,
18 | }
19 |
20 | pub enum MsgTypeEnum {
21 | ChangeLanguage,
22 | ChangeRecordLimit,
23 | ChangeHotKeys,
24 | ChangeClipBoard,
25 | ChangeAutoPaste,
26 | ChangeDeleteConfirm,
27 | }
28 |
29 | impl Handle {
30 | pub fn global() -> &'static Handle {
31 | static HANDLE: OnceCell = OnceCell::new();
32 |
33 | HANDLE.get_or_init(|| Handle {
34 | app_handle: Arc::new(Mutex::new(None)),
35 | })
36 | }
37 |
38 | pub fn init(&self, app_handle: AppHandle) {
39 | *self.app_handle.lock() = Some(app_handle);
40 | }
41 |
42 | pub fn get_window(&self) -> Option {
43 | self.app_handle
44 | .lock()
45 | .as_ref()
46 | .and_then(|a| a.get_window("main"))
47 | }
48 |
49 | fn get_manager(&self) -> Result {
50 | let app_handle = self.app_handle.lock();
51 | if app_handle.is_none() {
52 | bail!("failed to get the hotkey manager");
53 | }
54 | Ok(app_handle.as_ref().unwrap().global_shortcut_manager())
55 | }
56 |
57 | pub fn refresh_common_config() {
58 | if let Some(window) = Self::global().get_window() {
59 | log_err!(window.emit("lanaya://refresh-common-config", "yes"));
60 | }
61 | }
62 |
63 | pub fn update_systray() -> Result<()> {
64 | let app_handle = Self::global().app_handle.lock();
65 | if app_handle.is_none() {
66 | bail!("update_systray unhandled error");
67 | }
68 | Tray::update_systray(app_handle.as_ref().unwrap())?;
69 | Ok(())
70 | }
71 |
72 | #[allow(unused)]
73 | pub fn update_systray_select_item() -> Result<()> {
74 | let app_handle = Self::global().app_handle.lock();
75 | if app_handle.is_none() {
76 | bail!("update_systray_select_item unhandled error");
77 | }
78 | Tray::update_select_item(app_handle.as_ref().unwrap())?;
79 | Ok(())
80 | }
81 |
82 | pub fn notice_to_window(msg_type: MsgTypeEnum, msg: S) -> Result<()> {
83 | let app_handle = Self::global().app_handle.lock();
84 | if app_handle.is_none() {
85 | bail!("notice_all_window unhandled error");
86 | }
87 | match msg_type {
88 | MsgTypeEnum::ChangeLanguage => {
89 | let window = app_handle.as_ref().unwrap().get_window("main");
90 | if window.is_some() {
91 | if let Some(win) = window {
92 | win.emit("lanaya://change-language", msg)?;
93 | };
94 | }
95 | }
96 | MsgTypeEnum::ChangeRecordLimit => {
97 | let window = app_handle.as_ref().unwrap().get_window("main");
98 | if window.is_some() {
99 | if let Some(win) = window {
100 | win.emit("lanaya://change-record-limit", msg)?;
101 | };
102 | }
103 | }
104 | MsgTypeEnum::ChangeHotKeys => {
105 | let window = app_handle.as_ref().unwrap().get_window("main");
106 | if window.is_some() {
107 | if let Some(win) = window {
108 | win.emit("lanaya://change-hotkeys", msg)?;
109 | };
110 | }
111 | }
112 | MsgTypeEnum::ChangeClipBoard => {
113 | let window = app_handle.as_ref().unwrap().get_window("main");
114 | if window.is_some() {
115 | if let Some(win) = window {
116 | win.emit("lanaya://change-clipboard", msg)?;
117 | };
118 | }
119 | }
120 | MsgTypeEnum::ChangeAutoPaste => {
121 | let window = app_handle.as_ref().unwrap().get_window("main");
122 | if window.is_some() {
123 | if let Some(win) = window {
124 | win.emit("lanaya://change-auto-paste", msg)?;
125 | };
126 | }
127 | }
128 | MsgTypeEnum::ChangeDeleteConfirm => {
129 | let window = app_handle.as_ref().unwrap().get_window("main");
130 | if window.is_some() {
131 | if let Some(win) = window {
132 | win.emit("lanaya://change-delete-confirm", msg)?;
133 | };
134 | }
135 | }
136 | }
137 | Ok(())
138 | }
139 |
140 | pub fn refresh_global_shortcut() -> Result<()> {
141 | let hotkeys_new = Config::common().latest().hotkeys.clone();
142 | if let Some(hotkeys) = hotkeys_new {
143 | // hotkeys 中找到 global-shortcut 开头的第一个
144 | let mut global_shortcut = None;
145 | for hotkey in hotkeys {
146 | if hotkey.starts_with("global-shortcut") {
147 | global_shortcut = Some(hotkey);
148 | break;
149 | }
150 | }
151 | // 目前只有一个 global-shortcut,此处可以改为循环
152 | if let Some(global_shortcut) = global_shortcut {
153 | let mut shortcut_manager = Self::global().get_manager()?;
154 | let _ = shortcut_manager.unregister_all();
155 | let hot_key_arr: Vec<&str> = global_shortcut.split(':').collect();
156 | if hot_key_arr[1].is_empty() {
157 | // 如果没有配置 global-shortcut,则等于清空快捷键,直接返回
158 | return Ok(());
159 | }
160 | let hot_key_arr: Vec<&str> = hot_key_arr[1].split('+').collect();
161 | let hot_key_arr: Vec = hot_key_arr
162 | .iter()
163 | .map(|x| x.parse::().unwrap())
164 | .collect();
165 | let short_cut_name = hotkey_util::get_short_cut_name(hot_key_arr, true);
166 | let _ = shortcut_manager.register(short_cut_name.as_str(), || {
167 | Self::open_window(WindowType::Main)
168 | });
169 | }
170 | }
171 | Ok(())
172 | }
173 |
174 | pub fn open_window(window_type: WindowType) {
175 | let binding = Self::global().app_handle.lock();
176 | let previous_process_id: State = binding
177 | .as_ref()
178 | .expect("Couldn't get app_handle")
179 | .state();
180 | let mut p_id = previous_process_id.0.lock().unwrap();
181 | *p_id = window_util::get_active_process_id();
182 | let app_handle = binding.as_ref().unwrap();
183 |
184 | let window_info = match window_type {
185 | WindowType::Config => WindowInfo::config(),
186 | WindowType::Main => WindowInfo::main(),
187 | };
188 |
189 | let label = window_info.label.as_str();
190 | let title = window_info.title.as_str();
191 | let url = window_info.url.as_str();
192 |
193 | if let Some(window) = app_handle.get_window(label) {
194 | if window.is_visible().unwrap() {
195 | let _ = window.close();
196 | return;
197 | }
198 | let _ = window.unminimize();
199 | let _ = window.show();
200 | let _ = window.set_focus();
201 | return;
202 | }
203 |
204 | let new_window = tauri::window::WindowBuilder::new(
205 | app_handle,
206 | label.to_string(),
207 | tauri::WindowUrl::App(url.into()),
208 | )
209 | .title(title)
210 | .center()
211 | .visible(false)
212 | .resizable(window_info.resizable)
213 | .fullscreen(window_info.fullscreenable)
214 | .always_on_top(window_info.always_on_top)
215 | .inner_size(window_info.width, window_info.height)
216 | .transparent(window_info.transparent)
217 | .decorations(window_info.decorations)
218 | .skip_taskbar(window_info.skip_taskbar)
219 | .center()
220 | .build();
221 | match new_window {
222 | Ok(window) => {
223 | let _ = window.show();
224 | let _ = window.set_focus();
225 | if let WindowType::Main = window_type {
226 | set_shadow(&window, true).expect("Unsupported platform!");
227 | }
228 | }
229 | Err(e) => {
230 | println!("create_window error: {}", e);
231 | }
232 | }
233 | }
234 |
235 | }
236 |
--------------------------------------------------------------------------------
/src-tauri/src/core/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod clipboard;
2 | pub mod database;
3 | pub mod handle;
4 | pub mod sysopt;
5 | pub mod tray;
6 | pub mod window_manager;
7 |
--------------------------------------------------------------------------------
/src-tauri/src/core/sysopt.rs:
--------------------------------------------------------------------------------
1 | use crate::{config::Config, log_err};
2 | use anyhow::{anyhow, Result};
3 | use auto_launch::{AutoLaunch, AutoLaunchBuilder};
4 | use once_cell::sync::OnceCell;
5 | use parking_lot::Mutex;
6 | use std::sync::Arc;
7 | use tauri::utils::platform::current_exe;
8 |
9 | pub struct Sysopt {
10 | auto_launch: Arc>>,
11 | }
12 |
13 | impl Sysopt {
14 | pub fn global() -> &'static Sysopt {
15 | static SYSOPT: OnceCell = OnceCell::new();
16 |
17 | SYSOPT.get_or_init(|| Sysopt {
18 | auto_launch: Arc::new(Mutex::new(None)),
19 | })
20 | }
21 |
22 | pub fn init_launch(&self) -> Result<()> {
23 | let enable = { Config::common().latest().enable_auto_launch };
24 | let enable = enable.unwrap_or(false);
25 |
26 | println!("enable auto launch: {}", enable);
27 |
28 | let app_exe = current_exe()?;
29 | let app_exe = dunce::canonicalize(app_exe)?;
30 | let app_name = app_exe
31 | .file_stem()
32 | .and_then(|f| f.to_str())
33 | .ok_or(anyhow!("failed to get file stem"))?;
34 | let app_path = app_exe
35 | .as_os_str()
36 | .to_str()
37 | .ok_or(anyhow!("failed to get app_path"))?
38 | .to_string();
39 | #[cfg(target_os = "windows")]
40 | let app_path = format!("\"{app_path}\"");
41 |
42 | // use the /Applications/Lanaya.app path
43 | #[cfg(target_os = "macos")]
44 | let app_path = (|| -> Option {
45 | let path = std::path::PathBuf::from(&app_path);
46 | let path = path.parent()?.parent()?.parent()?;
47 | let extension = path.extension()?.to_str()?;
48 | match extension == "app" {
49 | true => Some(path.as_os_str().to_str()?.to_string()),
50 | false => None,
51 | }
52 | })()
53 | .unwrap_or(app_path);
54 | println!("app_path: {}", app_path);
55 |
56 | let auto = AutoLaunchBuilder::new()
57 | .set_app_name(app_name)
58 | .set_app_path(&app_path)
59 | .build()?;
60 |
61 | if let Ok(true) = auto.is_enabled() {
62 | if enable {
63 | return Ok(());
64 | }
65 | }
66 |
67 | // macos每次启动都更新登录项,避免重复设置登录项
68 | #[cfg(target_os = "macos")]
69 | let _ = auto.disable();
70 |
71 | if enable {
72 | auto.enable()?;
73 | }
74 | *self.auto_launch.lock() = Some(auto);
75 |
76 | Ok(())
77 | }
78 |
79 | pub fn update_launch(&self) -> Result<()> {
80 | let auto_launch = self.auto_launch.lock();
81 |
82 | if auto_launch.is_none() {
83 | drop(auto_launch);
84 | return self.init_launch();
85 | }
86 | let enable = { Config::common().latest().enable_auto_launch };
87 | let enable = enable.unwrap_or(false);
88 | let auto_launch = auto_launch.as_ref().unwrap();
89 |
90 | match enable {
91 | true => auto_launch.enable()?,
92 | false => log_err!(auto_launch.disable()), // 忽略关闭的错误
93 | };
94 |
95 | Ok(())
96 | }
97 |
98 | /// todo listen clipboard loop
99 | #[allow(unused)]
100 | pub fn init_clipboard_listener(&self) {}
101 | }
102 |
--------------------------------------------------------------------------------
/src-tauri/src/core/tray.rs:
--------------------------------------------------------------------------------
1 | use super::handle::Handle;
2 | use super::window_manager::WindowType;
3 | use crate::config;
4 | use crate::config::{CommonConfig, Config};
5 | use anyhow::Result;
6 | use tauri::{
7 | AppHandle, CustomMenuItem, Manager, SystemTrayEvent, SystemTrayMenu, SystemTrayMenuItem,
8 | SystemTraySubmenu,
9 | };
10 |
11 | pub struct Tray {}
12 |
13 | impl Tray {
14 | pub fn tray_menu(app_handle: &AppHandle) -> SystemTrayMenu {
15 | let zh = { Config::common().latest().language == Some("zh".into()) };
16 | let version = app_handle.package_info().version.to_string();
17 | if zh {
18 | SystemTrayMenu::new()
19 | .add_item(CustomMenuItem::new("open_window", "显示界面"))
20 | .add_item(CustomMenuItem::new("hide_window", "隐藏界面").accelerator("Esc"))
21 | .add_native_item(SystemTrayMenuItem::Separator)
22 | .add_submenu(SystemTraySubmenu::new(
23 | "语言",
24 | SystemTrayMenu::new()
25 | .add_item(CustomMenuItem::new("language_zh", "简体中文"))
26 | .add_item(CustomMenuItem::new("language_en", "English")),
27 | ))
28 | .add_item(CustomMenuItem::new("more_config", "更多设置"))
29 | .add_native_item(SystemTrayMenuItem::Separator)
30 | .add_item(CustomMenuItem::new("app_version", format!("版本 {version}")).disabled())
31 | .add_item(CustomMenuItem::new("quit", "退出").accelerator("CmdOrControl+Q"))
32 | } else {
33 | SystemTrayMenu::new()
34 | .add_item(CustomMenuItem::new("open_window", "Show Window"))
35 | .add_item(CustomMenuItem::new("hide_window", "Hide Window").accelerator("Esc"))
36 | .add_native_item(SystemTrayMenuItem::Separator)
37 | .add_submenu(SystemTraySubmenu::new(
38 | "Language",
39 | SystemTrayMenu::new()
40 | .add_item(CustomMenuItem::new("language_zh", "简体中文"))
41 | .add_item(CustomMenuItem::new("language_en", "English")),
42 | ))
43 | .add_item(CustomMenuItem::new("more_config", "More Config"))
44 | .add_native_item(SystemTrayMenuItem::Separator)
45 | .add_item(
46 | CustomMenuItem::new("app_version", format!("Version {version}")).disabled(),
47 | )
48 | .add_item(CustomMenuItem::new("quit", "Quit").accelerator("CmdOrControl+Q"))
49 | }
50 | }
51 |
52 | pub fn update_systray(app_handle: &AppHandle) -> Result<()> {
53 | app_handle
54 | .tray_handle()
55 | .set_menu(Tray::tray_menu(app_handle))?;
56 | Tray::update_select_item(app_handle)?;
57 | Ok(())
58 | }
59 |
60 | pub fn update_select_item(app_handle: &AppHandle) -> Result<()> {
61 | let language = { Config::common().latest().language.clone() };
62 |
63 | let tray = app_handle.tray_handle();
64 |
65 | if let Some(language) = language {
66 | if language == "zh" {
67 | let _ = tray.get_item("language_zh").set_selected(true);
68 | let _ = tray.get_item("language_en").set_selected(false);
69 | } else {
70 | let _ = tray.get_item("language_en").set_selected(true);
71 | let _ = tray.get_item("language_zh").set_selected(false);
72 | }
73 | }
74 | Ok(())
75 | }
76 |
77 | pub fn on_system_tray_event(app_handle: &AppHandle, event: SystemTrayEvent) {
78 | match event {
79 | SystemTrayEvent::MenuItemClick { id, .. } => match id.as_str() {
80 | "open_window" => {
81 | Handle::open_window(WindowType::Main);
82 | }
83 | "hide_window" => {
84 | let window = app_handle.get_window("main");
85 | if let Some(window) = window {
86 | window.hide().unwrap();
87 | }
88 | }
89 | "language_zh" => change_language("zh".into()),
90 | "language_en" => change_language("en".into()),
91 | "more_config" => Handle::open_window(WindowType::Config),
92 | "quit" => {
93 | app_handle.exit(0);
94 | std::process::exit(0);
95 | }
96 | _ => {}
97 | },
98 | #[cfg(target_os = "windows")]
99 | SystemTrayEvent::LeftClick { .. } => {
100 | if let Some(window) = app_handle.get_window("main") {
101 | if let Err(err) = window.show() {
102 | println!("Failed to show window: {}", err);
103 | }
104 | if let Err(err) = window.set_focus() {
105 | println!("Failed to set focus on window: {}", err);
106 | }
107 | }
108 | }
109 | _ => {}
110 | }
111 | }
112 | }
113 |
114 | // 切换模式语言
115 | fn change_language(mode: String) {
116 | tauri::async_runtime::spawn(async move {
117 | match config::modify_common_config(CommonConfig {
118 | language: Some(mode),
119 | ..CommonConfig::default()
120 | })
121 | .await
122 | {
123 | Ok(_) => println!("change_language: success"),
124 | Err(err) => println!("change_language: {}", err),
125 | }
126 | });
127 | }
128 |
--------------------------------------------------------------------------------
/src-tauri/src/core/window_manager.rs:
--------------------------------------------------------------------------------
1 | pub enum WindowType {
2 | Config,
3 | Main,
4 | }
5 | pub struct WindowInfo {
6 | pub label: String,
7 | pub title: String,
8 | pub url: String,
9 | pub width: f64,
10 | pub height: f64,
11 | pub resizable: bool,
12 | // todo tauri no this API
13 | // minimizable: bool,
14 | pub fullscreenable: bool,
15 | pub always_on_top: bool,
16 | pub transparent: bool,
17 | pub decorations: bool,
18 | pub skip_taskbar: bool,
19 | }
20 |
21 | impl WindowInfo {
22 | pub fn main() -> Self {
23 | WindowInfo {
24 | label: "main".into(),
25 | title: "Lanaya".into(),
26 | url: "/".into(),
27 | width: 800.0,
28 | height: 600.0,
29 | resizable: false,
30 | // minimizable: false,
31 | fullscreenable: false,
32 | always_on_top: false,
33 | transparent: true,
34 | decorations: false,
35 | skip_taskbar: true,
36 | }
37 | }
38 | pub fn config() -> Self {
39 | WindowInfo {
40 | label: "config".into(),
41 | title: "设置".into(),
42 | url: "/config".into(),
43 | width: 640.0,
44 | height: 480.0,
45 | resizable: false,
46 | // minimizable: false,
47 | fullscreenable: false,
48 | always_on_top: false,
49 | transparent: false,
50 | decorations: true,
51 | skip_taskbar: false,
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src-tauri/src/main.rs:
--------------------------------------------------------------------------------
1 | #![cfg_attr(
2 | all(not(debug_assertions), target_os = "windows"),
3 | windows_subsystem = "windows"
4 | )]
5 |
6 | use tauri::{App, Manager, SystemTray};
7 | use window_shadows::set_shadow;
8 |
9 | use crate::config::Config;
10 | use crate::core::clipboard;
11 | use crate::core::database::SqliteDB;
12 | use crate::core::sysopt;
13 | use crate::core::tray;
14 | mod cmds;
15 | mod config;
16 | mod core;
17 | mod utils;
18 | use std::sync::Mutex;
19 |
20 | pub struct PreviousProcessId(Mutex);
21 |
22 | fn main() {
23 | let app = tauri::Builder::default()
24 | .setup(|app| {
25 | set_up(app);
26 | Ok(())
27 | })
28 | .system_tray(SystemTray::new())
29 | .on_system_tray_event(core::tray::Tray::on_system_tray_event)
30 | .manage(PreviousProcessId(Default::default()))
31 | .invoke_handler(tauri::generate_handler![
32 | cmds::get_common_config,
33 | cmds::set_common_config,
34 | cmds::change_language,
35 | cmds::change_record_limit,
36 | cmds::change_auto_launch,
37 | cmds::change_auto_paste,
38 | cmds::change_theme_mode,
39 | cmds::change_hotkeys,
40 | cmds::clear_data,
41 | cmds::insert_record,
42 | cmds::insert_if_not_exist,
43 | cmds::find_all_record,
44 | cmds::mark_favorite,
45 | cmds::save_tags,
46 | cmds::find_by_key,
47 | cmds::delete_over_limit,
48 | cmds::write_to_clip,
49 | cmds::delete_by_id,
50 | cmds::focus_previous_window,
51 | cmds::paste_in_previous_window,
52 | cmds::change_delete_confirm,
53 | ])
54 | .build(tauri::generate_context!())
55 | .expect("error while build tauri application");
56 |
57 | app.run(|app_handle, e| match e {
58 | tauri::RunEvent::ExitRequested { api, .. } => {
59 | api.prevent_exit();
60 | }
61 | tauri::RunEvent::Exit => {
62 | app_handle.exit(0);
63 | }
64 | _ => {}
65 | });
66 | }
67 |
68 | fn set_up(app: &mut App) {
69 | // Make the docker NOT to have an active app when started
70 | #[cfg(target_os = "macos")]
71 | app.set_activation_policy(tauri::ActivationPolicy::Accessory);
72 | let window = app.get_window("main").unwrap();
73 | set_shadow(&window, true).expect("Unsupported platform!");
74 | core::handle::Handle::global().init(app.app_handle());
75 | log_err!(Config::init_config());
76 | log_err!(tray::Tray::update_systray(&app.app_handle()));
77 | log_err!(sysopt::Sysopt::global().init_launch());
78 | let _ = core::handle::Handle::refresh_global_shortcut();
79 | SqliteDB::init();
80 | clipboard::ClipboardWatcher::start();
81 | }
82 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/dirs.rs:
--------------------------------------------------------------------------------
1 | use anyhow::Result;
2 | use std::path::PathBuf;
3 | use tauri::api::path::home_dir;
4 |
5 | static APP_DIR: &str = "lanaya";
6 | static CONFIG_FILE: &str = "config.json";
7 |
8 | /// get the app home dir
9 | pub fn app_home_dir() -> Result {
10 | #[cfg(target_os = "windows")]
11 | {
12 | use tauri::utils::platform::current_exe;
13 |
14 | let app_exe = current_exe()?;
15 | let app_exe = dunce::canonicalize(app_exe)?;
16 | let app_dir = app_exe
17 | .parent()
18 | .ok_or(anyhow::anyhow!("failed to get the portable app dir"))?;
19 | Ok(PathBuf::from(app_dir).join(".config").join(APP_DIR))
20 |
21 | }
22 |
23 | #[cfg(not(target_os = "windows"))]
24 | Ok(home_dir()
25 | .ok_or(anyhow::anyhow!("failed to get the app home dir"))?
26 | .join(".config")
27 | .join(APP_DIR))
28 | }
29 |
30 | /// logs dir
31 | #[allow(unused)]
32 | pub fn app_logs_dir() -> Result {
33 | Ok(app_home_dir()?.join("logs"))
34 | }
35 |
36 | pub fn config_path() -> Result {
37 | Ok(app_home_dir()?.join(CONFIG_FILE))
38 | }
39 |
40 | #[allow(unused)]
41 | pub fn app_data_dir() -> Result {
42 | Ok(app_home_dir()?.join("data"))
43 | }
44 |
45 | pub fn app_data_img_dir() -> Result {
46 | Ok(app_data_dir()?.join("img"))
47 | }
48 |
49 | #[test]
50 | fn test() {
51 | println!("app_home_dir: {:?}", app_home_dir());
52 | println!("app_logs_dir: {:?}", app_logs_dir());
53 | println!("config_path: {:?}", config_path());
54 | println!("app_data_dir: {:?}", app_data_dir());
55 | }
56 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/dispatch_util.rs:
--------------------------------------------------------------------------------
1 | use rdev::{simulate, EventType, Key, SimulateError};
2 | use std::{thread, time};
3 |
4 | pub fn paste() {
5 | // Run command + v to paste
6 | // Same approach as both Maccy and Clipy, reference: https://github.com/p0deje/Maccy/blob/master/Maccy/Clipboard.swift#L101
7 | dispatch(&EventType::KeyPress(Key::MetaLeft));
8 | dispatch(&EventType::KeyPress(Key::KeyV));
9 | dispatch(&EventType::KeyRelease(Key::KeyV));
10 | dispatch(&EventType::KeyRelease(Key::MetaLeft));
11 | }
12 |
13 | pub fn request_permissions() {
14 | // Simply press and release the shift key. First time the OS will ask for permissions, then do it without asking.
15 | dispatch(&EventType::KeyPress(Key::ShiftLeft));
16 | dispatch(&EventType::KeyRelease(Key::ShiftLeft));
17 | }
18 |
19 | fn dispatch(event_type: &EventType) {
20 | match simulate(event_type) {
21 | Ok(()) => (),
22 | Err(SimulateError) => {
23 | println!("We could not dispatch {:?}", event_type);
24 | }
25 | }
26 | // Let the OS catchup (at least MacOS)
27 | sleep(20)
28 | }
29 |
30 | fn sleep(ms: u64) {
31 | let delay = time::Duration::from_millis(ms);
32 | thread::sleep(delay);
33 | }
--------------------------------------------------------------------------------
/src-tauri/src/utils/hotkey_util.rs:
--------------------------------------------------------------------------------
1 | /// keyCode to keyName
2 | /// @param {Number} keyCode
3 | /// @return {String} keyName
4 | fn key_code_to_name(key_code: u32) -> String {
5 | match key_code {
6 | 8 => "backspace",
7 | 9 => "tab",
8 | 12 => "clear",
9 | 13 => "enter",
10 | 27 => "esc",
11 | 32 => "space",
12 | 37 => "left",
13 | 38 => "up",
14 | 39 => "right",
15 | 40 => "down",
16 | 46 => "delete",
17 | 45 => "insert",
18 | 36 => "home",
19 | 35 => "end",
20 | 33 => "pageup",
21 | 34 => "pagedown",
22 | 20 => "capslock",
23 | 96 => "num_0",
24 | 97 => "num_1",
25 | 98 => "num_2",
26 | 99 => "num_3",
27 | 100 => "num_4",
28 | 101 => "num_5",
29 | 102 => "num_6",
30 | 103 => "num_7",
31 | 104 => "num_8",
32 | 105 => "num_9",
33 | 106 => "num_multiply",
34 | 107 => "num_add",
35 | 108 => "num_enter",
36 | 109 => "num_subtract",
37 | 110 => "num_decimal",
38 | 111 => "num_divide",
39 | 188 => ",",
40 | 190 => ".",
41 | 191 => "/",
42 | 192 => "`",
43 | 189 => "-",
44 | 187 => "=",
45 | 186 => ";",
46 | 222 => "'",
47 | 219 => "[",
48 | 221 => "]",
49 | 220 => "\\",
50 | _ => "",
51 | }
52 | .to_string()
53 | }
54 |
55 | fn modifier_code_to_name(modifier_code: u32) -> String {
56 | match modifier_code {
57 | 16 => "shift",
58 | 18 => "alt",
59 | 17 => "ctrl",
60 | 91 => "command",
61 | _ => "",
62 | }
63 | .to_string()
64 | }
65 |
66 | pub fn get_short_cut_name(key_code_arr: Vec, is_first_word_upper_case: bool) -> String {
67 | let mut key_str = String::new();
68 | let mut modifier = String::new();
69 | let mut normal_key = String::new();
70 | for key_code in key_code_arr {
71 | if !modifier_code_to_name(key_code).is_empty() {
72 | modifier += &capitalized(&modifier_code_to_name(key_code), is_first_word_upper_case);
73 | modifier += "+";
74 | } else if !key_code_to_name(key_code).is_empty() {
75 | key_str = capitalized(&key_code_to_name(key_code), is_first_word_upper_case);
76 | } else {
77 | normal_key = capitalized(
78 | &String::from_utf8(vec![key_code as u8]).unwrap(),
79 | is_first_word_upper_case,
80 | );
81 | }
82 | }
83 | if modifier.is_empty() && key_str.is_empty() {
84 | return "".to_string();
85 | }
86 | // 若只有modifier,不显示
87 | if !modifier.is_empty() && key_str.is_empty() && normal_key.is_empty() {
88 | return "".to_string();
89 | }
90 | // 若只有keyStr,不显示
91 | if modifier.is_empty() && !key_str.is_empty() && !normal_key.is_empty() {
92 | return "".to_string();
93 | }
94 | modifier + &key_str + &normal_key
95 | }
96 |
97 | fn capitalized(name: &str, is_first_word_upper_case: bool) -> String {
98 | let name = name.to_lowercase();
99 | if !is_first_word_upper_case {
100 | return name;
101 | }
102 | let capitalized_first = name.chars().next().unwrap().to_uppercase().to_string();
103 | if name.len() == 1 {
104 | return capitalized_first;
105 | }
106 | let rest = &name[1..];
107 | capitalized_first + rest
108 | }
109 |
110 | #[test]
111 | fn test() {
112 | let hot_key_str = "global-shortcut:16+67+91";
113 | let hot_key_arr: Vec<&str> = hot_key_str.split(":").collect();
114 | let hot_key_arr: Vec<&str> = hot_key_arr[1].split("+").collect();
115 | let hot_key_arr: Vec = hot_key_arr
116 | .iter()
117 | .map(|x| x.parse::().unwrap())
118 | .collect();
119 | let short_cut_name = get_short_cut_name(hot_key_arr, true);
120 | println!("{}", short_cut_name);
121 | }
122 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/img_util.rs:
--------------------------------------------------------------------------------
1 | use super::string_util;
2 | use anyhow::Result;
3 | use arboard::ImageData;
4 | use image::ImageEncoder;
5 | use std::io::{BufReader, BufWriter, Cursor};
6 |
7 | pub fn rgba8_to_base64(img: &ImageData) -> String {
8 | let mut bytes: Vec = Vec::new();
9 | image::codecs::png::PngEncoder::new(BufWriter::new(Cursor::new(&mut bytes)))
10 | .write_image(
11 | &img.bytes,
12 | img.width as u32,
13 | img.height as u32,
14 | image::ColorType::Rgba8,
15 | )
16 | .unwrap();
17 | string_util::base64_encode(bytes.as_slice())
18 | }
19 |
20 | pub fn rgba8_to_jpeg_base64(img: &ImageData, quality: u8) -> String {
21 | let mut bytes: Vec = Vec::new();
22 | image::codecs::jpeg::JpegEncoder::new_with_quality(
23 | BufWriter::new(Cursor::new(&mut bytes)),
24 | quality,
25 | )
26 | .write_image(
27 | &img.bytes,
28 | img.width as u32,
29 | img.height as u32,
30 | image::ColorType::Rgba8,
31 | )
32 | .unwrap();
33 | string_util::base64_encode(bytes.as_slice())
34 | }
35 |
36 | pub fn base64_to_rgba8(base64: &str) -> Result {
37 | let bytes = string_util::base64_decode(base64);
38 | let reader =
39 | image::io::Reader::with_format(BufReader::new(Cursor::new(bytes)), image::ImageFormat::Png);
40 | match reader.decode() {
41 | Ok(img) => {
42 | let rgba = img.into_rgba8();
43 | let (width, height) = rgba.dimensions();
44 | Ok(ImageData {
45 | width: width as usize,
46 | height: height as usize,
47 | bytes: rgba.into_raw().into(),
48 | })
49 | }
50 | Err(_) => Err(anyhow::anyhow!("decode image error")),
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/json_util.rs:
--------------------------------------------------------------------------------
1 | use anyhow::{bail, Context, Result};
2 | use serde::{de::DeserializeOwned, Serialize};
3 | use serde_json;
4 | use std::{fs, path::PathBuf};
5 |
6 | pub fn read(path: &PathBuf) -> Result {
7 | if !path.exists() {
8 | bail!("file not found \"{}\"", path.display());
9 | }
10 |
11 | let json_str = fs::read_to_string(path)
12 | .context(format!("failed to read the file \"{}\"", path.display()))?;
13 |
14 | serde_json::from_str::(&json_str).context(format!(
15 | "failed to read the file with yaml format \"{}\"",
16 | path.display()
17 | ))
18 | }
19 |
20 | pub fn save(path: &PathBuf, data: &T) -> Result<()> {
21 | let data_str = serde_json::to_string(data)?;
22 | println!("data_str: {}", data_str);
23 | let path_str = path.as_os_str().to_string_lossy().to_string();
24 | fs::write(path, data_str.as_bytes()).context(format!("failed to save file \"{path_str}\""))
25 | }
26 |
27 | pub fn parse(json_str: &str) -> Result {
28 | serde_json::from_str::(json_str).context("failed to parse json string")
29 | }
30 |
31 | pub fn stringfy(data: &T) -> Result {
32 | Ok(serde_json::to_string(data)?)
33 | }
34 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/log_print.rs:
--------------------------------------------------------------------------------
1 | #[macro_export]
2 | macro_rules! log_err {
3 | ($result: expr) => {
4 | if let Err(err) = $result {
5 | println!("err: {}", err);
6 | }
7 | };
8 |
9 | ($result: expr, $err_str: expr) => {
10 | if let Err(_) = $result {
11 | println!("err: {}", $err_str);
12 | }
13 | };
14 | }
15 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/mod.rs:
--------------------------------------------------------------------------------
1 | pub mod dirs;
2 | pub mod hotkey_util;
3 | pub mod img_util;
4 | pub mod json_util;
5 | pub mod log_print;
6 | pub mod string_util;
7 | pub mod dispatch_util;
8 | pub mod window_util;
9 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/string_util.rs:
--------------------------------------------------------------------------------
1 | use base64::engine::general_purpose;
2 | use base64::Engine;
3 | use crypto::digest::Digest;
4 | use crypto::md5::Md5;
5 |
6 | pub fn md5(s: &str) -> String {
7 | let mut hasher = Md5::new();
8 | hasher.input_str(s);
9 | hasher.result_str()
10 | }
11 |
12 | #[allow(unused)]
13 | pub fn md5_by_bytes(bytes: &[u8]) -> String {
14 | let mut hasher = Md5::new();
15 | hasher.input(bytes);
16 | hasher.result_str()
17 | }
18 |
19 | pub fn base64_encode(bytes: &[u8]) -> String {
20 | general_purpose::STANDARD.encode(bytes)
21 | }
22 |
23 | pub fn base64_decode(base64: &str) -> Vec {
24 | general_purpose::STANDARD.decode(base64).unwrap()
25 | }
26 |
27 | // 如果content中 包含key的话,就把key用key 高亮起来
28 | pub fn highlight(key: &str, content: &str) -> String {
29 | let mut res = String::new();
30 | let mut start = 0;
31 | let mut end;
32 |
33 | while let Some(i) = content[start..].to_lowercase().find(&key.to_lowercase()) {
34 | end = start + i;
35 | res.push_str(&content[start..end]);
36 | res.push_str(&format!(
37 | "[highlight]{}[/highlight]",
38 | &content[end..end + key.len()]
39 | ));
40 | start = end + key.len();
41 | }
42 | res.push_str(&content[start..]);
43 | res = escape_html(&res);
44 | res = res
45 | .replace("[highlight]", "")
46 | .replace("[/highlight]", " ");
47 | res
48 | }
49 |
50 | fn escape_html(html: &str) -> String {
51 | html.replace("<", "<").replace(">", ">")
52 | }
53 |
54 | #[test]
55 | fn test_highlight() {
56 | let res = highlight("hello", "hello worldhello");
57 | println!("{}", res);
58 | }
59 |
--------------------------------------------------------------------------------
/src-tauri/src/utils/window_util.rs:
--------------------------------------------------------------------------------
1 | #[cfg(target_os = "macos")]
2 | use active_win_pos_rs::get_active_window;
3 |
4 | #[cfg(target_os = "macos")]
5 | use cocoa::base::{nil};
6 |
7 | #[cfg(target_os = "macos")]
8 | use cocoa::appkit::{NSRunningApplication, NSApplicationActivateIgnoringOtherApps};
9 |
10 | #[cfg(target_os = "windows")]
11 | use winapi::um::winuser::{GetForegroundWindow, SetForegroundWindow};
12 |
13 | #[cfg(target_os = "windows")]
14 | use winapi::um::winuser::GetWindowThreadProcessId;
15 |
16 | #[cfg(target_os = "windows")]
17 | use winapi::shared::minwindef::LPDWORD;
18 |
19 | pub fn get_active_process_id() -> i32 {
20 | #[cfg(target_os = "macos")]
21 | {
22 | match get_active_window() {
23 | Ok(active_window) => {
24 | let process_id: i32 = active_window.process_id.try_into().unwrap();
25 | process_id
26 | },
27 | Err(()) => {
28 | println!("error occurred while getting the active window");
29 | 0
30 | }
31 | }
32 | }
33 |
34 | #[cfg(target_os = "windows")]
35 | unsafe {
36 | let hwnd = GetForegroundWindow();
37 | if hwnd.is_null() {
38 | println!("Could not get active window");
39 | return 0;
40 | }
41 |
42 | let mut process_id: u32 = 0;
43 |
44 | GetWindowThreadProcessId(hwnd, &mut process_id as LPDWORD);
45 |
46 | if process_id == 0 {
47 | println!("Could not get process id of active window");
48 | return 0;
49 | }
50 |
51 | process_id as i32
52 | }
53 | }
54 |
55 | pub fn focus_window(process_id: i32) {
56 | if process_id == 0 {
57 | return;
58 | }
59 |
60 | #[cfg(target_os = "macos")]
61 | unsafe {
62 | let current_app = NSRunningApplication::runningApplicationWithProcessIdentifier(nil, process_id);
63 | current_app.activateWithOptions_(NSApplicationActivateIgnoringOtherApps);
64 | }
65 |
66 | #[cfg(target_os = "windows")]
67 | unsafe {
68 | let hwnd = GetForegroundWindow();
69 |
70 | if hwnd.is_null() {
71 | println!("Could not get active window");
72 | return;
73 | }
74 |
75 | if SetForegroundWindow(hwnd) == 0 {
76 | println!("Could not focus on window");
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/src-tauri/tauri.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "build": {
3 | "beforeBuildCommand": "npm run build",
4 | "beforeDevCommand": "npm run dev",
5 | "devPath": "http://localhost:5173",
6 | "distDir": "../dist"
7 | },
8 | "package": {
9 | "productName": "Lanaya",
10 | "version": "1.2.1"
11 | },
12 | "tauri": {
13 | "macOSPrivateApi": true,
14 | "allowlist": {
15 | "shell": {
16 | "open": true
17 | },
18 | "window": {
19 | "all": true
20 | },
21 | "globalShortcut": {
22 | "all": true
23 | },
24 | "notification": {
25 | "all": true
26 | },
27 | "os": {
28 | "all": true
29 | }
30 | },
31 | "systemTray": {
32 | "iconPath": "icons/icon.png",
33 | "iconAsTemplate": true
34 | },
35 | "bundle": {
36 | "active": true,
37 | "targets": "all",
38 | "category": "DeveloperTool",
39 | "copyright": "© 2023 churchTao All Rights Reserved",
40 | "deb": {
41 | "depends": []
42 | },
43 | "externalBin": [],
44 | "icon": [
45 | "icons/32x32.png",
46 | "icons/128x128.png",
47 | "icons/128x128@2x.png",
48 | "icons/icon.icns",
49 | "icons/icon.ico"
50 | ],
51 | "identifier": "com.church.lanaya",
52 | "longDescription": "Easy to use, full keyboard, clipboard manager",
53 | "macOS": {
54 | "entitlements": null,
55 | "exceptionDomain": "",
56 | "frameworks": [],
57 | "providerShortName": null,
58 | "signingIdentity": null
59 | },
60 | "resources": [],
61 | "shortDescription": "Clipboard manager",
62 | "windows": {
63 | "certificateThumbprint": null,
64 | "digestAlgorithm": "sha256",
65 | "timestampUrl": "",
66 | "wix": {
67 | "language": [
68 | "zh-CN",
69 | "en-US"
70 | ]
71 | }
72 | }
73 | },
74 | "security": {
75 | "csp": "default-src 'self';img-src 'self' data: *;"
76 | },
77 | "updater": {
78 | "active": false,
79 | "dialog": true,
80 | "endpoints": [
81 | "https://github.com/churchTao/Lanaya/releases/download/updater/update.json"
82 | ],
83 | "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDFFQkYyRTBBMzk3MkM0QTcKUldTbnhISTVDaTYvSHJXbFpZYzNydW8yU0lEN2JjOFFYTHNpZFZnMmxRTWM1SUtjM0ZlcThlaVkK"
84 | },
85 | "windows": [
86 | {
87 | "fullscreen": false,
88 | "height": 600,
89 | "resizable": false,
90 | "title": "Lanaya",
91 | "width": 800,
92 | "transparent": true,
93 | "decorations": false,
94 | "skipTaskbar": true,
95 | "center": true
96 | }
97 | ]
98 | }
99 | }
--------------------------------------------------------------------------------
/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/about-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src/assets/about-icon.png
--------------------------------------------------------------------------------
/src/assets/about.svg:
--------------------------------------------------------------------------------
1 |
3 |
6 |
--------------------------------------------------------------------------------
/src/assets/backspace.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/delete.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/fuzhi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/gh-desktop.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ChurchTao/Lanaya/8e50414f8d5c8fc70b6dfac0af9cc91813ee87b8/src/assets/gh-desktop.png
--------------------------------------------------------------------------------
/src/assets/shezhi.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/sousuo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/assets/vue.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ClipBoardItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
11 |
15 |
19 | {{ idx + 1 }}
20 |
21 |
32 |
47 |
51 |
56 |
62 |
67 |
68 |
69 |
70 |
74 |
79 |
80 |
86 |
87 |
88 |
89 |
90 |
93 |
98 |
99 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
225 |
255 |
272 |
--------------------------------------------------------------------------------
/src/components/ClipBoardList.vue:
--------------------------------------------------------------------------------
1 |
2 | (mouseenter = true)"
6 | @mouseleave="() => (mouseenter = false)"
7 | >
8 |
9 |
33 |
34 |
35 |
57 |
95 |
--------------------------------------------------------------------------------
/src/components/HotKeyItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{
4 | item
5 | }}
6 |
{{ $t(tips) }}
7 |
8 |
9 |
16 |
35 |
--------------------------------------------------------------------------------
/src/components/KeyMapBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
13 |
18 |
19 |
20 |
37 |
44 |
--------------------------------------------------------------------------------
/src/components/MainLayout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
13 |
--------------------------------------------------------------------------------
/src/components/SearchBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
17 |
18 |
19 |
31 |
37 |
--------------------------------------------------------------------------------
/src/components/TagGroup.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
10 |
16 |
21 |
22 | {{ tag }}
23 |
30 |
36 |
37 |
38 |
39 |
72 |
73 |
74 |
75 |
76 |
118 |
119 |
124 |
--------------------------------------------------------------------------------
/src/components/child/clipboard/SearchNoResult.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
23 |
{{ noResultText }}
24 |
25 |
26 |
27 |
35 |
36 |
--------------------------------------------------------------------------------
/src/components/child/config/HotKeyInput.vue:
--------------------------------------------------------------------------------
1 |
2 |
30 |
31 |
32 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/components/child/config/LeftSectionItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
13 |
14 |
15 |
16 |
{{ name }}
17 |
18 |
19 |
39 |
40 |
67 |
--------------------------------------------------------------------------------
/src/components/child/config/RightSectionAbout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
{{ appName }}
8 |
9 | {{ $t("config.about.version") }} {{ appVersion }}
10 |
11 |
12 |
15 |
19 | {{ $t("config.about.change-log") }}
20 |
21 |
22 |
26 | {{ $t("config.about.check-update") }}
27 |
28 |
29 |
30 |
41 |
42 |
43 |
44 |
73 |
74 |
79 |
--------------------------------------------------------------------------------
/src/components/child/config/RightSectionCommonConfig.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | {{ $t("config.common.enable_auto_launch") }}
6 |
7 |
8 |
12 |
13 |
14 |
15 |
16 | {{ $t("config.common.language") }}
17 |
18 |
19 |
24 |
25 |
26 |
27 |
28 | {{ $t("config.common.theme_mode") }}
29 |
30 |
31 |
36 |
37 |
38 |
39 |
40 | {{ $t("config.common.record_limit") }}
41 |
42 |
43 |
48 |
49 |
50 |
54 |
55 | {{ $t("config.common.enable_auto_paste") }}
56 |
57 |
58 |
62 |
63 |
64 |
65 |
66 | {{ $t("config.common.enable_delete_confirm") }}
67 |
68 |
69 |
73 |
74 |
75 |
76 |
77 | {{ $t("config.common.hotkeys") }}
78 |
79 |
80 |
87 |
88 |
89 |
90 |
91 |
92 |
97 |
231 |
232 |
237 |
--------------------------------------------------------------------------------
/src/components/child/config/base/BaseSelect.vue:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
11 | {{ modelValue.name }}
12 |
15 |
16 |
17 |
18 |
19 |
24 |
27 |
34 |
40 | {{ department.name }}
47 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
89 |
--------------------------------------------------------------------------------
/src/components/child/config/base/BaseSwitch.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
13 |
14 |
15 |
16 |
29 |
--------------------------------------------------------------------------------
/src/config/constants.js:
--------------------------------------------------------------------------------
1 | export const languageOptions = [
2 | {
3 | name: "简体中文",
4 | value: "zh",
5 | },
6 | {
7 | name: "English",
8 | value: "en",
9 | },
10 | ];
11 |
12 | export const themeOptions = [
13 | {
14 | name: "Light",
15 | value: "light",
16 | },
17 | {
18 | name: "Dark",
19 | value: "dark",
20 | },
21 | ];
22 |
23 | export const recordLimitOptions = [
24 | { name: "50", value: 50 },
25 | { name: "100", value: 100 },
26 | { name: "200", value: 200 },
27 | { name: "300", value: 300 },
28 | ];
29 |
30 | export const hotkeys_func_enum = {
31 | COPY: "copy",
32 | QUICK_COPY: "quick-copy",
33 | MOVE_SELECTED: "move-selected",
34 | CLOSE_WINDOW: "close-window",
35 | GLOBAL_SHORTCUT: "global-shortcut",
36 | CLEAR_HISTORY: "clear-history",
37 | };
38 |
39 | /**
40 | * { keymap: ["⏎"], tips: "hotkeys.copy" },
41 | { keymap: ["⌘"], tips: "hotkeys.quick-copy" },
42 | { keymap: ["↑", "↓"], tips: "hotkeys.move-selected" },
43 | { keymap: ["Esc"], tips: "hotkeys.close-window" },
44 | */
45 | export const defaultHotkeys = [
46 | {
47 | func: hotkeys_func_enum.COPY,
48 | keys: [13],
49 | },
50 | {
51 | func: hotkeys_func_enum.QUICK_COPY,
52 | keys: [91],
53 | },
54 | {
55 | func: hotkeys_func_enum.MOVE_SELECTED,
56 | keys: [38, 40],
57 | },
58 | {
59 | func: hotkeys_func_enum.CLOSE_WINDOW,
60 | keys: [27],
61 | },
62 | {
63 | func: hotkeys_func_enum.CLEAR_HISTORY,
64 | keys: [],
65 | },
66 | {
67 | func: hotkeys_func_enum.GLOBAL_SHORTCUT,
68 | keys: [],
69 | },
70 | ];
71 |
--------------------------------------------------------------------------------
/src/i18n/index.js:
--------------------------------------------------------------------------------
1 | import { createI18n } from "vue-i18n";
2 | import en from "./locales/en.yaml";
3 | import zh from "./locales/zh.yaml";
4 |
5 | let language = window.localStorage.getItem("language") || "zh";
6 |
7 | export const i18n = createI18n({
8 | locale: language,
9 | fallbackLocale: language,
10 | legacy: false,
11 | messages: {
12 | en,
13 | zh,
14 | },
15 | });
16 |
17 | export const i18nt = i18n.global.t;
18 |
19 | export function setLanguage(locale) {
20 | i18n.global.locale.value = locale;
21 | window.localStorage.setItem("language", locale);
22 | }
23 |
--------------------------------------------------------------------------------
/src/i18n/locales/en.yaml:
--------------------------------------------------------------------------------
1 | search:
2 | placeholder: "Search"
3 | tags:
4 | placeholder: "Add tag"
5 | hotkeys:
6 | copy: "Copy"
7 | quick-copy: "Quick Copy"
8 | move-selected: "Move Selected"
9 | close-window: "Close"
10 | clear-history: "Clear History"
11 | global-shortcut: "Pop Up"
12 | dialogs:
13 | delete_favorite:
14 | title: "Delete favorite?"
15 | message: "Are you sure you want to delete this favorite?"
16 | config:
17 | section:
18 | common: "Common"
19 | about: "About"
20 | common:
21 | enable_auto_launch: "Auto Launch"
22 | language: "Language"
23 | record_limit: "Record Limit"
24 | theme_mode: "Theme(unrealized)"
25 | enable_auto_paste: "Auto paste (requires accessibility permissions)"
26 | hotkeys: "Hotkeys"
27 | hotkeys_placeholder: "Input Shortcut"
28 | enable_delete_confirm: "Delete need confirm"
29 | about:
30 | version: "Version"
31 | change-log: "Change Log"
32 | check-update: "Check Update"
33 | thanks: "If you find it useful, maybe you can give me a star."
34 |
--------------------------------------------------------------------------------
/src/i18n/locales/zh.yaml:
--------------------------------------------------------------------------------
1 | search:
2 | placeholder: "关键词搜索"
3 | tags:
4 | placeholder: "输入标签"
5 | hotkeys:
6 | copy: "复制"
7 | quick-copy: "快捷复制"
8 | move-selected: "移动选择"
9 | close-window: "关闭窗口"
10 | clear-history: "清空历史"
11 | global-shortcut: "全局唤起"
12 | dialogs:
13 | delete_favorite:
14 | title: "删除收藏?"
15 | message: "你确认要删除这条记录吗?"
16 | config:
17 | section:
18 | common: "通用"
19 | about: "关于"
20 | common:
21 | enable_auto_launch: "开机时启动"
22 | language: "语言"
23 | record_limit: "历史记录条数"
24 | theme_mode: "主题(敬请期待)"
25 | enable_auto_paste: "自动粘贴 (需要获取权限)"
26 | hotkeys: "快捷键"
27 | hotkeys_placeholder: "输入键盘快捷键"
28 | enable_delete_confirm: "删除需要确认"
29 | about:
30 | version: "版本号"
31 | change-log: "更新日志"
32 | check-update: "检查更新"
33 | thanks: "如果你觉得好用,也许可以给我点个star。"
34 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import "./style.css";
3 | import App from "./App.vue";
4 | import router from "./router/router.js";
5 | import { createPinia } from "pinia";
6 | import { listenLanguageChange } from "./service/globalListener";
7 | import { i18n, setLanguage } from "./i18n";
8 |
9 | const pinia = createPinia();
10 | const app = createApp(App);
11 | app.use(router);
12 | app.use(i18n);
13 | app.use(pinia);
14 | app.mount("#app");
15 | listenLanguageChange((data) => {
16 | setLanguage(data);
17 | });
18 |
--------------------------------------------------------------------------------
/src/router/router.js:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHistory } from "vue-router";
2 |
3 | import Main from "../views/Main.vue";
4 | import Config from "../views/Config.vue";
5 |
6 | const routes = [
7 | { path: "/", component: Main },
8 | { path: "/config", component: Config },
9 | ];
10 |
11 | const router = createRouter({
12 | history: createWebHistory(),
13 | routes,
14 | });
15 |
16 | export default router;
17 |
--------------------------------------------------------------------------------
/src/service/cmds.js:
--------------------------------------------------------------------------------
1 | import { invoke } from "@tauri-apps/api/tauri";
2 | import { sendNotice } from "@/service/msg";
3 |
4 | export async function getCommonConfig() {
5 | return invoke("get_common_config");
6 | }
7 |
8 | export async function setCommonConfig(config) {
9 | return invoke("set_common_config", { config });
10 | }
11 |
12 | export async function setLanguage(language) {
13 | return invoke("change_language", { language });
14 | }
15 |
16 | export async function setRecordLimit(limit) {
17 | return invoke("change_record_limit", { limit });
18 | }
19 |
20 | export async function setAutoLaunch(enable) {
21 | return invoke("change_auto_launch", { enable });
22 | }
23 |
24 | export async function setAutoPaste(enable) {
25 | return invoke("change_auto_paste", { enable });
26 | }
27 |
28 | export async function setThemeMode(themeMode) {
29 | return invoke("change_theme_mode", { themeMode });
30 | }
31 |
32 | export async function setHotkeys(hotkeys) {
33 | return invoke("change_hotkeys", { hotkeys });
34 | }
35 |
36 | export async function clearData() {
37 | return invoke("clear_data");
38 | }
39 |
40 | export async function insertRecord(record) {
41 | return invoke("insert_record", { record });
42 | }
43 |
44 | export async function insertIfNotExist(r) {
45 | return invoke("insert_if_not_exist", { r });
46 | }
47 |
48 | export async function findAllRecord() {
49 | return invoke("find_all_record");
50 | }
51 |
52 | export async function markFavorite(id) {
53 | return invoke("mark_favorite", { id });
54 | }
55 |
56 | export async function saveTags(id, tags) {
57 | return invoke("save_tags", { id, tags: tags.join(",") });
58 | }
59 |
60 | export async function findByKey(query) {
61 | return invoke("find_by_key", { query });
62 | }
63 |
64 | export async function deleteOverLimit(limit) {
65 | return invoke("delete_over_limit", { limit });
66 | }
67 |
68 | export async function writeToClip(id) {
69 | invoke("write_to_clip", { id });
70 | sendNotice("", "Copy!");
71 | }
72 |
73 | export async function deleteById(id) {
74 | return invoke("delete_by_id", { id });
75 | }
76 |
77 | export async function focusPreviousWindow() {
78 | return invoke("focus_previous_window");
79 | }
80 |
81 | export async function pasteInPreviousWindow() {
82 | return invoke("paste_in_previous_window");
83 | }
84 |
85 | export async function setDeleteConfirm(enable) {
86 | return invoke("change_delete_confirm", { enable });
87 | }
88 |
--------------------------------------------------------------------------------
/src/service/globalListener.js:
--------------------------------------------------------------------------------
1 | import { listen } from "@tauri-apps/api/event";
2 |
3 | export const listenLanguageChange = async (consumer) => {
4 | const unListen = await listen("lanaya://change-language", async (event) => {
5 | consumer(event.payload);
6 | });
7 | return unListen;
8 | };
9 |
10 | export const listenRecordLimitChange = async (consumer) => {
11 | const unListen = await listen(
12 | "lanaya://change-record-limit",
13 | async (event) => {
14 | consumer(event.payload);
15 | },
16 | );
17 | return unListen;
18 | };
19 |
20 | export const listenHotkeysChange = async (consumer) => {
21 | const unListen = await listen("lanaya://change-hotkeys", async (event) => {
22 | consumer(event.payload);
23 | });
24 | return unListen;
25 | };
26 |
27 | export const listenClipboardChange = async (consumer) => {
28 | const unListen = await listen("lanaya://change-clipboard", async (event) => {
29 | consumer(event.payload);
30 | });
31 | return unListen;
32 | };
33 |
34 | export const listenAutoPasteChange = async (consumer) => {
35 | const unListen = await listen("lanaya://change-auto-paste", async (event) => {
36 | consumer(event.payload);
37 | });
38 | return unListen;
39 | };
40 |
41 | export const listenDeleteConfirmChange = async (consumer) => {
42 | const unListen = await listen(
43 | "lanaya://change-delete-confirm",
44 | async (event) => {
45 | consumer(event.payload);
46 | },
47 | );
48 | return unListen;
49 | };
50 |
51 | export const listenWindowBlur = async (consumer) => {
52 | const unlistenBlur = await listen("tauri://blur", async (event) => {
53 | consumer(event);
54 | });
55 | return unlistenBlur;
56 | };
57 |
--------------------------------------------------------------------------------
/src/service/msg.js:
--------------------------------------------------------------------------------
1 | import {
2 | isPermissionGranted,
3 | requestPermission,
4 | sendNotification,
5 | } from "@tauri-apps/api/notification";
6 |
7 | export async function sendNotice(title, body) {
8 | let permissionGranted = await isPermissionGranted();
9 | if (!permissionGranted) {
10 | const permission = await requestPermission();
11 | permissionGranted = permission === "granted";
12 | }
13 | if (permissionGranted) {
14 | sendNotification({ title, body });
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/service/recordService.js:
--------------------------------------------------------------------------------
1 | import {
2 | clearData,
3 | insertIfNotExist,
4 | findAllRecord,
5 | markFavorite,
6 | saveTags as saveTagsCmd,
7 | findByKey,
8 | deleteOverLimit,
9 | } from "./cmds";
10 |
11 | /**
12 | struct Record {
13 | pub id: u64,
14 | pub content: String,
15 | pub md5: String,
16 | pub create_time: u64,
17 | pub is_favorite: bool,
18 | }
19 |
20 | pub struct QueryReq {
21 | pub key: Option,
22 | pub limit: Option,
23 | pub is_favorite: Option,
24 | }
25 | */
26 |
27 | async function selectPage(searchKey = "", isFavorite = undefined, limit = 300) {
28 | // 如果 searchKey 以f:开头,那么就是查询收藏的记录
29 | if (searchKey === "") {
30 | return await findAllRecord(limit);
31 | }
32 | if (searchKey.startsWith("f:")) {
33 | isFavorite = true;
34 | searchKey = searchKey.substring(2);
35 | }
36 | let query = {
37 | limit,
38 | };
39 | if (searchKey.startsWith("t:")) {
40 | query.tags = searchKey.substring(2).split(",").filter(Boolean);
41 | } else if (searchKey !== "") {
42 | query.key = searchKey;
43 | }
44 | if (isFavorite !== undefined) {
45 | query.is_favorite = isFavorite;
46 | }
47 | return await findByKey(query);
48 | }
49 |
50 | async function insertRecord(content, limit = 300) {
51 | let newRecord = {
52 | id: 0,
53 | content: content,
54 | md5: "",
55 | is_favorite: false,
56 | create_time: 0,
57 | };
58 | await insertIfNotExist(newRecord);
59 | removeOverLimit(limit);
60 | }
61 |
62 | /**
63 | * 删除超过limit的记录
64 | * @param {*} limit
65 | * @returns
66 | */
67 | async function removeOverLimit(limit = 300) {
68 | await deleteOverLimit(limit);
69 | }
70 |
71 | async function updateRecord(record) {
72 | await insertIfNotExist(record);
73 | }
74 |
75 | async function clearAll() {
76 | return await clearData();
77 | }
78 |
79 | async function markFav(id) {
80 | await markFavorite(id);
81 | }
82 |
83 | async function saveTags(id, tags) {
84 | return await saveTagsCmd(id, tags);
85 | }
86 |
87 | export { selectPage, insertRecord, updateRecord, clearAll, markFav, saveTags };
88 |
--------------------------------------------------------------------------------
/src/service/shortCutUtil.js:
--------------------------------------------------------------------------------
1 | // Special Keys
2 | const _keyMap = {
3 | 8: { name: "backspace", keyStr: "⌫" },
4 | 9: { name: "tab", keyStr: "Tab" },
5 | 12: { name: "clear", keyStr: "Clear" },
6 | 13: { name: "enter", keyStr: "↩" },
7 | 27: { name: "esc", keyStr: "Esc" },
8 | 32: { name: "space", keyStr: "Space" },
9 | 37: { name: "left", keyStr: "←" },
10 | 38: { name: "up", keyStr: "↑" },
11 | 39: { name: "right", keyStr: "→" },
12 | 40: { name: "down", keyStr: "↓" },
13 | 46: { name: "delete", keyStr: "Del" },
14 | 45: { name: "insert", keyStr: "Ins" },
15 | 36: { name: "home", keyStr: "↖" },
16 | 35: { name: "end", keyStr: "↘" },
17 | 33: { name: "pageup", keyStr: "⇞" },
18 | 34: { name: "pagedown", keyStr: "⇟" },
19 | 20: { name: "capslock", keyStr: "⇪" },
20 | 96: { name: "num_0", keyStr: "0" },
21 | 97: { name: "num_1", keyStr: "1" },
22 | 98: { name: "num_2", keyStr: "2" },
23 | 99: { name: "num_3", keyStr: "3" },
24 | 100: { name: "num_4", keyStr: "4" },
25 | 101: { name: "num_5", keyStr: "5" },
26 | 102: { name: "num_6", keyStr: "6" },
27 | 103: { name: "num_7", keyStr: "7" },
28 | 104: { name: "num_8", keyStr: "8" },
29 | 105: { name: "num_9", keyStr: "9" },
30 | 106: { name: "num_multiply", keyStr: "*" },
31 | 107: { name: "num_add", keyStr: "+" },
32 | 108: { name: "num_enter", keyStr: "⏎" },
33 | 109: { name: "num_subtract", keyStr: "-" },
34 | 110: { name: "num_decimal", keyStr: "." },
35 | 111: { name: "num_divide", keyStr: "/" },
36 | 188: { name: ",", keyStr: "," },
37 | 190: { name: ".", keyStr: "." },
38 | 191: { name: "/", keyStr: "/" },
39 | 192: { name: "`", keyStr: "`" },
40 | 189: { name: "-", keyStr: "-" },
41 | 187: { name: "=", keyStr: "=" },
42 | 186: { name: ";", keyStr: ";" },
43 | 222: { name: "'", keyStr: "'" },
44 | 219: { name: "[", keyStr: "[" },
45 | 221: { name: "]", keyStr: "]" },
46 | 220: { name: "\\", keyStr: "\\" },
47 | };
48 | const _modifier = {
49 | 16: { name: "shift", keyStr: "⇧" },
50 | 18: { name: "alt", keyStr: "⌥" },
51 | 17: { name: "ctrl", keyStr: "⌃" },
52 | 91: { name: "command", keyStr: "⌘" },
53 | };
54 |
55 | export function getShortCutShow(keyCodeArr) {
56 | keyCodeArr = keyCodeArr.sort((a, b) => a - b);
57 | let keyStr = "";
58 | let modifier = "";
59 | let normalKey = "";
60 | keyCodeArr.forEach((keyCode) => {
61 | keyCode = parseInt(keyCode);
62 | if (_modifier[keyCode]) {
63 | modifier += _modifier[keyCode].keyStr;
64 | } else if (_keyMap[keyCode]) {
65 | keyStr = _keyMap[keyCode].keyStr;
66 | } else {
67 | normalKey = String.fromCharCode(keyCode).toUpperCase();
68 | }
69 | });
70 | if (modifier === "" && keyStr === "") {
71 | return "";
72 | }
73 | // 若只有modifier,不显示
74 | if (modifier !== "" && keyStr === "" && normalKey === "") {
75 | return "";
76 | }
77 | // 若只有keyStr,不显示
78 | if (modifier === "" && keyStr !== "" && normalKey !== "") {
79 | return "";
80 | }
81 | return modifier + keyStr + normalKey;
82 | }
83 |
84 | // 返回数组形式
85 | export function getShortCutShowAnyway(keyCodeArr) {
86 | keyCodeArr = keyCodeArr.sort((a, b) => a - b);
87 | let keyStr = [];
88 | let modifier = [];
89 | let normalKey = [];
90 | keyCodeArr.forEach((keyCode) => {
91 | keyCode = parseInt(keyCode);
92 | if (_modifier[keyCode]) {
93 | modifier.push(_modifier[keyCode].keyStr);
94 | } else if (_keyMap[keyCode]) {
95 | keyStr.push(_keyMap[keyCode].keyStr);
96 | } else {
97 | normalKey.push(String.fromCharCode(keyCode).toUpperCase());
98 | }
99 | });
100 | return [...modifier, ...keyStr, ...normalKey];
101 | }
102 |
103 | /**
104 | * 返回如:commond+shift+c 或者 Commond+Shift+C
105 | * @param {*} keyCodeArr
106 | * @param {*} isFirstWordUpperCase
107 | * @returns
108 | */
109 | export function getShortCutName(keyCodeArr, isFirstWordUpperCase = false) {
110 | keyCodeArr = keyCodeArr.sort((a, b) => a - b);
111 | let keyStr = "";
112 | let modifier = "";
113 | let normalKey = "";
114 | keyCodeArr.forEach((keyCode) => {
115 | if (_modifier[keyCode]) {
116 | modifier +=
117 | capitalized(_modifier[keyCode].name, isFirstWordUpperCase) + "+";
118 | } else if (_keyMap[keyCode]) {
119 | keyStr = capitalized(_keyMap[keyCode].name, isFirstWordUpperCase);
120 | } else {
121 | normalKey = capitalized(
122 | String.fromCharCode(keyCode),
123 | isFirstWordUpperCase,
124 | );
125 | }
126 | });
127 | if (modifier === "" && keyStr === "") {
128 | return "";
129 | }
130 | // 若只有modifier,不显示
131 | if (modifier !== "" && keyStr === "" && normalKey === "") {
132 | return "";
133 | }
134 | // 若只有keyStr,不显示
135 | if (modifier === "" && keyStr !== "" && normalKey !== "") {
136 | return "";
137 | }
138 | return modifier + keyStr + normalKey;
139 | }
140 |
141 | function capitalized(name, isFirstWordUpperCase = false) {
142 | name = name.toLowerCase();
143 | if (!isFirstWordUpperCase) {
144 | return name;
145 | }
146 | const capitalizedFirst = name[0].toUpperCase();
147 | if (name.length === 1) {
148 | return capitalizedFirst;
149 | }
150 | const rest = name.slice(1);
151 | return capitalizedFirst + rest;
152 | }
153 |
154 | export function isDiff(keyCodeArr1, keyCodeArr2) {
155 | if (keyCodeArr1.length !== keyCodeArr2.length) {
156 | return true;
157 | }
158 | keyCodeArr1 = keyCodeArr1.sort((a, b) => a - b);
159 | keyCodeArr2 = keyCodeArr2.sort((a, b) => a - b);
160 | for (let i = 0; i < keyCodeArr1.length; i++) {
161 | if (keyCodeArr1[i] !== keyCodeArr2[i]) {
162 | return true;
163 | }
164 | }
165 | return false;
166 | }
167 |
--------------------------------------------------------------------------------
/src/service/store.js:
--------------------------------------------------------------------------------
1 | import { defineStore } from "pinia";
2 | import { getCommonConfig, setCommonConfig } from "../service/cmds";
3 | import {
4 | listenRecordLimitChange,
5 | listenAutoPasteChange,
6 | listenDeleteConfirmChange,
7 | } from "@/service/globalListener";
8 | import { defaultHotkeys } from "../config/constants";
9 |
10 | export const globalData = defineStore("globalData", {
11 | state: () => {
12 | return {
13 | common_config: {
14 | language: "zh",
15 | theme_mode: "light",
16 | enable_auto_launch: false,
17 | enable_auto_paste: false,
18 | enable_delete_confirm: false,
19 | hotkeys: ["clear-history:8+18", "global-shortcut:18+32"],
20 | record_limit: 100,
21 | },
22 | short_cut: [],
23 | };
24 | },
25 | getters: {
26 | config(state) {
27 | return state.common_config;
28 | },
29 | short_cuts(state) {
30 | return state.short_cut;
31 | },
32 | },
33 | actions: {
34 | async initCommonConfig() {
35 | console.log("initCommonConfig");
36 | await this.refreshCommonConfig();
37 | await this.initListener();
38 | },
39 | async initListener() {
40 | await listenRecordLimitChange((newLimitNum) => {
41 | this.common_config.record_limit = newLimitNum;
42 | });
43 | await listenAutoPasteChange((value) => {
44 | this.common_config.enable_auto_paste = value;
45 | });
46 | await listenDeleteConfirmChange((value) => {
47 | console.log("listenDeleteConfirmChange", value);
48 | this.common_config.enable_delete_confirm = value;
49 | });
50 | },
51 | async refreshCommonConfig() {
52 | let res = await getCommonConfig();
53 | this.common_config = res;
54 | if (res.hotkeys) {
55 | await this.patchShotCuts(res.hotkeys);
56 | }
57 | },
58 | async patchShotCuts(hotkeys) {
59 | let short_cuts = [];
60 | defaultHotkeys.forEach((item) => {
61 | let find = hotkeys.find((hotkey) => {
62 | return hotkey.startsWith(item.func);
63 | });
64 | if (find) {
65 | let strArr = find.split(":")[1].split("+");
66 | if (strArr.length > 0 && strArr[0] !== "") {
67 | let keys = strArr.map((item) => {
68 | return parseInt(item);
69 | });
70 | short_cuts.push({
71 | func: item.func,
72 | keys,
73 | });
74 | }
75 | } else {
76 | short_cuts.push(item);
77 | }
78 | });
79 | this.short_cut = short_cuts;
80 | },
81 | // 持久化
82 | async saveCommonConfig() {
83 | await setCommonConfig(this.common_config);
84 | },
85 | },
86 | });
87 |
88 | // modify like this:
89 | // store.$patch((state) => {
90 | // state.common_config.language = 'en'
91 | // })
92 |
--------------------------------------------------------------------------------
/src/service/windowUtil.js:
--------------------------------------------------------------------------------
1 | import { appWindow } from "@tauri-apps/api/window";
2 |
3 | let closeWindowTimer = null;
4 |
5 | let skipNextClose = false;
6 |
7 | export function keepWindowOpen() {
8 | skipNextClose = true;
9 | }
10 |
11 | export async function closeWindowLater(delay) {
12 | if (skipNextClose) {
13 | skipNextClose = false;
14 | return;
15 | }
16 | if (closeWindowTimer) {
17 | clearTimeout(closeWindowTimer);
18 | closeWindowTimer = null;
19 | }
20 | await appWindow.hide();
21 | // 延迟5秒如果未重新打开window则close
22 | closeWindowTimer = setTimeout(async () => {
23 | // 等待关闭window
24 | let visible = await appWindow.isVisible();
25 | if (!visible) {
26 | await appWindow.close();
27 | }
28 | }, delay);
29 | }
30 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | /* ./src/index.css */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
6 | :root {
7 | background-color: transparent;
8 | box-sizing: border-box;
9 | font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
10 | font-size: 16px;
11 | line-height: 24px;
12 | font-weight: 400;
13 | font-synthesis: none;
14 | text-rendering: optimizeLegibility;
15 | -webkit-font-smoothing: antialiased;
16 | -moz-osx-font-smoothing: grayscale;
17 | -webkit-text-size-adjust: 100%;
18 | --docsearch-muted-color: #909399;
19 | --docsearch-modal-background: #fafafa;
20 | --docsearch-highlight-color: #409eff;
21 | --docsearch-spacing: 12px;
22 | --docsearch-icon-stroke-width: 1.4;
23 | --docsearch-container-background: rgba(101, 108, 133, 0.8);
24 | --docsearch-logo-color: #5468ff;
25 | --docsearch-modal-width: 560px;
26 | --docsearch-modal-height: 600px;
27 | --docsearch-searchbox-height: 56px;
28 | --docsearch-hit-height: 56px;
29 | --docsearch-hit-max-height: 112px;
30 | --docsearch-hit-color: #444950;
31 | --docsearch-hit-active-color: #fff;
32 | --docsearch-hit-background: #fff;
33 | --docsearch-hit-shadow: 0 1px 3px 0 #d4d9e1;
34 | --docsearch-key-shadow: inset 0 -2px 0 0 #cdcde6, inset 0 0 1px 1px #fff,
35 | 0 1px 2px 1px rgba(30, 35, 90, 0.4);
36 | }
37 |
38 | html,
39 | body {
40 | margin: 0;
41 | padding: 0;
42 | width: 100%;
43 | overflow: hidden;
44 | -webkit-touch-callout: none;
45 | -webkit-user-select: none;
46 | -khtml-user-select: none;
47 | -moz-user-select: none;
48 | -ms-user-select: none;
49 | user-select: none;
50 | scroll-behavior: smooth;
51 | /* font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; */
52 | }
53 |
54 | img {
55 | -webkit-user-drag: none;
56 | -khtml-user-drag: none;
57 | -moz-user-drag: none;
58 | -o-user-drag: none;
59 | }
60 |
61 | input {
62 | background: none;
63 | outline: none;
64 | border: 0;
65 | }
66 |
67 | @media (prefers-color-scheme: light) {
68 | :root {
69 | }
70 | }
71 |
72 | @media (prefers-color-scheme: dark) {
73 | :root {
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/views/Config.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
23 |
24 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
54 |
55 |
80 |
--------------------------------------------------------------------------------
/src/views/Main.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
14 |
15 |
16 |
17 |
314 |
315 |
316 |
--------------------------------------------------------------------------------
/tailwind.config.cjs:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./index.html", "./src/**/*.{vue,js}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [require("daisyui")],
8 | // daisyUI config (optional)
9 | daisyui: {
10 | styled: true,
11 | themes: false,
12 | base: true,
13 | utils: true,
14 | logs: true,
15 | rtl: false,
16 | prefix: "",
17 | darkTheme: "halloween",
18 | },
19 | };
20 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import path from "path";
3 | import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
4 | import vue from "@vitejs/plugin-vue";
5 |
6 | export default defineConfig({
7 | plugins: [
8 | vue(),
9 | VueI18nPlugin({
10 | include: path.resolve(__dirname, "./src/i18n/locales/**"),
11 | compositionOnly: false,
12 | }),
13 | ],
14 | // 防止 vite 输出复杂的 rust 错误
15 | clearScreen: false,
16 | // Tauri 使用固定端口,若此端口不可用将会导致程序错误
17 | server: {
18 | strictPort: true,
19 | },
20 | // 使用 `TAURI_PLATFORM`、`TAURI_ARCH`、`TAURI_FAMILY`,
21 | // `TAURI_PLATFORM_VERSION`、`TAURI_PLATFORM_TYPE` 和 `TAURI_DEBUG` 环境变量
22 | envPrefix: ["VITE_", "TAURI_"],
23 | resolve: {
24 | alias: {
25 | "@": path.resolve("./src"),
26 | "@root": path.resolve("."),
27 | },
28 | },
29 | build: {
30 | // Tauri 支持 es2021
31 | target: ["es2021", "chrome100", "safari13"],
32 | // 不为调试构建压缩构建体积
33 | minify: !process.env.TAURI_DEBUG ? "esbuild" : false,
34 | // 为调试构建生成源代码映射 (sourcemap)
35 | sourcemap: !!process.env.TAURI_DEBUG,
36 | },
37 | });
38 |
--------------------------------------------------------------------------------