├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── docs ├── CONTRIBUTE.md ├── img1.png └── logo │ ├── assets │ ├── clipboard.png │ ├── clipboard.svg │ ├── format-list-bulleted-type.png │ └── format-list-bulleted-type.svg │ ├── logo-high.psd │ ├── logo-medium.psd │ ├── logo-old.psd │ └── logo.png ├── package.json ├── pnpm-lock.yaml ├── public ├── index.html ├── listener.js ├── logo.png ├── plugin.json ├── preload.js ├── time.js └── time.worker.js ├── src ├── App.vue ├── cpns │ ├── ClipFloatBtn.vue │ ├── ClipFullData.vue │ ├── ClipItemList.vue │ ├── ClipOperate.vue │ ├── ClipSearch.vue │ ├── ClipSwitch.vue │ ├── ClipWordBreak.vue │ └── FileList.vue ├── data │ ├── notify.json │ ├── operation.json │ └── setting.json ├── global │ ├── initPlugin.js │ ├── readSetting.js │ ├── registerElement.js │ └── restoreSetting.js ├── hooks │ └── useClipOperate.js ├── main.js ├── style │ ├── cpns │ │ ├── clip-float-btn.less │ │ ├── clip-full-data.less │ │ ├── clip-item-list.less │ │ ├── clip-operate.less │ │ ├── clip-search.less │ │ ├── clip-switch.less │ │ ├── clip-word-break.less │ │ ├── file-list.less │ │ └── setting.less │ └── index.less ├── utils │ └── index.js └── views │ ├── Main.vue │ └── Setting.vue └── vue.config.js /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw? 22 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 | [官网 | Site](https://ziuchen.gitee.io/project/ClipboardManager/) 2 | 3 | [贡献 | Contribute](./docs/CONTRIBUTE.md) 4 | 5 | ![img1](./docs/img1.png) -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | # 参与贡献 | Contribute 2 | 3 | ## 🧱 基本流程 4 | 5 | Issue => Project => Develop => Pull Request => Merge 6 | 7 | 提出Issue确定需求,在Project中排入发布的版本号,贡献者fork指定版本号的分支,完成开发后提交PR 8 | 9 | ## 🔰 贡献指南 10 | 11 | ```sh 12 | # 安装依赖 13 | pnpm i 14 | # 开发热更新 (仅视图, preload需要在开发者工具中手动重启插件) 15 | pnpm run serve 16 | # 构建应用 17 | pnpm run build 18 | ``` 19 | 20 | ## 📌 注意事项 21 | 22 | - **请务必按照流程参与贡献,以统一开发进度** 23 | - **请按照[Commit规范](https://github.com/angular/angular/blob/main/CONTRIBUTING.md#commit)提交代码** -------------------------------------------------------------------------------- /docs/img1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/img1.png -------------------------------------------------------------------------------- /docs/logo/assets/clipboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/logo/assets/clipboard.png -------------------------------------------------------------------------------- /docs/logo/assets/clipboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/logo/assets/format-list-bulleted-type.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/logo/assets/format-list-bulleted-type.png -------------------------------------------------------------------------------- /docs/logo/assets/format-list-bulleted-type.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/logo/logo-high.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/logo/logo-high.psd -------------------------------------------------------------------------------- /docs/logo/logo-medium.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/logo/logo-medium.psd -------------------------------------------------------------------------------- /docs/logo/logo-old.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/logo/logo-old.psd -------------------------------------------------------------------------------- /docs/logo/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/docs/logo/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "serve": "vue-cli-service serve", 4 | "build": "vue-cli-service build" 5 | }, 6 | "dependencies": { 7 | "@element-plus/icons-vue": "^2.0.9", 8 | "element-plus": "^2.2.17", 9 | "less": "^4.1.3", 10 | "vue": "^3.2.37", 11 | "webpack": "4.37.0" 12 | }, 13 | "devDependencies": { 14 | "@vue/cli-service": "^5.0.8", 15 | "less-loader": "^11.0.0", 16 | "uglifyjs-webpack-plugin": "^2.2.0", 17 | "vue-template-compiler": "^2.7.8" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= htmlWebpackPlugin.options.title %> 8 | 14 | 15 | 16 | 19 |
20 | 21 | 22 | -------------------------------------------------------------------------------- /public/listener.js: -------------------------------------------------------------------------------- 1 | const { chmodSync, existsSync } = require('fs') 2 | const { EventEmitter } = require('events') 3 | const path = require('path') 4 | const { execFile } = require('child_process') 5 | 6 | class ClipboardEventListener extends EventEmitter { 7 | constructor() { 8 | super() 9 | this.child = null 10 | this.listening = false 11 | } 12 | startListening(dbPath) { 13 | const targetMap = { 14 | win32: 'clipboard-event-handler-win32.exe', 15 | linux: 'clipboard-event-handler-linux', 16 | darwin: 'clipboard-event-handler-mac' 17 | } 18 | const { platform } = process 19 | const target = path.resolve( 20 | dbPath.split('_utools_clipboard_manager_storage')[0], 21 | targetMap[platform] 22 | ) 23 | if (!existsSync(target)) { 24 | this.emit('error', '剪贴板监听程序不存在') 25 | return 26 | } 27 | if (platform === 'win32') { 28 | this.child = execFile(target) 29 | } else if (platform === 'linux' || platform === 'darwin') { 30 | chmodSync(target, 0o755) 31 | this.child = execFile(target) 32 | } else { 33 | throw 'Not yet supported' 34 | } 35 | this.child.stdout.on('data', (data) => { 36 | if (data.trim() === 'CLIPBOARD_CHANGE') { 37 | this.emit('change') 38 | } 39 | }) 40 | this.child.stdout.on('close', () => { 41 | this.emit('close') 42 | this.listening = false 43 | }) 44 | this.child.stdout.on('exit', () => { 45 | this.emit('exit') 46 | this.listening = false 47 | }) 48 | this.listening = true 49 | } 50 | stopListening() { 51 | const res = this.child.kill() 52 | this.listening = false 53 | return res 54 | } 55 | } 56 | 57 | module.exports = new ClipboardEventListener() 58 | -------------------------------------------------------------------------------- /public/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiuChen/ClipboardManager/55fac27d917a0a00baced89f52da53b15f929154/public/logo.png -------------------------------------------------------------------------------- /public/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginName": "超级剪贴板", 3 | "description": "强大的剪贴板管理工具", 4 | "author": "ZiuChen", 5 | "homepage": "https://github.com/ZiuChen", 6 | "main": "index.html", 7 | "preload": "preload.js", 8 | "development": { 9 | "main": "http://localhost:8081/" 10 | }, 11 | "logo": "logo.png", 12 | "platform": ["win32", "darwin", "linux"], 13 | "features": [ 14 | { 15 | "code": "clipboard", 16 | "explain": "剪切板历史、剪贴板快速粘贴", 17 | "cmds": ["剪切板", "剪贴板", "Clipboard"] 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /public/preload.js: -------------------------------------------------------------------------------- 1 | const { existsSync, readFileSync, writeFileSync, mkdirSync, watch } = require('fs') 2 | const { sep } = require('path') 3 | const crypto = require('crypto') 4 | const listener = require('./listener') 5 | const { clipboard } = require('electron') 6 | const time = require('./time') 7 | 8 | window.exports = { 9 | utools, 10 | existsSync, 11 | readFileSync, 12 | writeFileSync, 13 | mkdirSync, 14 | watch, 15 | sep, 16 | crypto, 17 | listener, 18 | clipboard, 19 | time, 20 | Buffer 21 | } 22 | -------------------------------------------------------------------------------- /public/time.js: -------------------------------------------------------------------------------- 1 | // time.js author: inu1255 2 | const path=require("path");function newPromise(fn){let a,b;var tmp={resolve(x){if(this.pending){a(x);this.resolved=true;this.pending=false}},reject(e){if(this.pending){b(e);this.rejectd=true;this.pending=false}},pending:true,resolved:false,rejected:false};var pms=new Promise(function(resolve,reject){a=resolve;b=reject;if(fn)fn(tmp.resolve,tmp.reject)});return Object.assign(pms,tmp)}let cbIdx=1;const cbMap=new Map;function getWorker(){if(getWorker.worker)return getWorker.worker;const worker=new Worker(path.join(__dirname,"time.worker.js"));getWorker.worker=worker;worker.onmessage=e=>{if(e.data&&cbMap.has(e.data.cb)){cbMap.get(e.data.cb).apply(null,e.data.args)}};return worker}function call(method,args){const cb=cbIdx++;let pms=newPromise();cbMap.set(cb,function(err,data){if(err)pms.reject(err);else pms.resolve(data)});getWorker().postMessage({method:method,args:args,cb:cb});return pms}function sleep(ms){return call("sleep",[ms])}exports.sleep=sleep; -------------------------------------------------------------------------------- /public/time.worker.js: -------------------------------------------------------------------------------- 1 | // time.worker.js author: inu1255 2 | const apis={sleep(ms){return new Promise(resolve=>setTimeout(resolve,ms))}};onmessage=event=>{const data=event.data;if(!data)return;const{cb,method,args}=data;if(!apis[method]){postMessage({cb:cb,err:"no such method"});return}apis[method].apply(null,args).then(res=>postMessage({cb:cb,data:res}),err=>postMessage({cb:cb,err:err}))}; -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | -------------------------------------------------------------------------------- /src/cpns/ClipFloatBtn.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /src/cpns/ClipFullData.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 69 | 70 | 82 | -------------------------------------------------------------------------------- /src/cpns/ClipItemList.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 297 | 298 | 301 | -------------------------------------------------------------------------------- /src/cpns/ClipOperate.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 38 | 39 | 42 | -------------------------------------------------------------------------------- /src/cpns/ClipSearch.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 57 | 58 | 61 | -------------------------------------------------------------------------------- /src/cpns/ClipSwitch.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 37 | 38 | 41 | -------------------------------------------------------------------------------- /src/cpns/ClipWordBreak.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 29 | 32 | -------------------------------------------------------------------------------- /src/cpns/FileList.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 29 | -------------------------------------------------------------------------------- /src/data/notify.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "重要版本更新提示", 3 | "content": "1. 如果你是第一次使用此插件, 请务必设置跟随主程序启动选项, 否则可能导致剪贴板记录丢失
2. MacOS也支持使用剪贴板监听程序了, 请手动安装`clipboard-event-handler`以获得最佳剪贴板监听性能, 安装方法见插件主页
3. 插件使用过程中遇到任何问题, 请到论坛发布页回帖或加入QQ群反馈", 4 | "version": 4 5 | } 6 | -------------------------------------------------------------------------------- /src/data/operation.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "id": "copy", "title": "复制", "icon": "📄" }, 3 | { "id": "view", "title": "查看全部", "icon": "💬" }, 4 | { "id": "open-folder", "title": "打开文件夹", "icon": "📁" }, 5 | { "id": "collect", "title": "收藏", "icon": "⭐" }, 6 | { "id": "un-collect", "title": "移出收藏", "icon": "📤" }, 7 | { "id": "remove", "title": "删除", "icon": "❌" }, 8 | { "id": "word-break", "title": "分词", "icon": "💣" }, 9 | { "id": "save-file", "title": "保存", "icon": "💾" } 10 | ] 11 | -------------------------------------------------------------------------------- /src/data/setting.json: -------------------------------------------------------------------------------- 1 | { 2 | "database.path": {}, 3 | "database.maxsize": 800, 4 | "database.maxage": 14, 5 | "operation.shown": ["copy", "view", "collect", "un-collect", "remove"], 6 | "operation.custom": [ 7 | { 8 | "id": "custom.1663583455000", 9 | "title": "收藏到备忘快贴", 10 | "icon": "📌", 11 | "match": ["text", "image"], 12 | "command": "redirect:添加到「备忘快贴」" 13 | }, 14 | { 15 | "id": "custom.1663490858", 16 | "title": "讯飞OCR识别", 17 | "icon": "⚡", 18 | "match": ["image", { "type": "file", "regex": ".(?:jpg|jpeg|png)$" }], 19 | "command": "redirect:讯飞ocr" 20 | }, 21 | { 22 | "id": "custom.1663490864", 23 | "title": "上传到图床", 24 | "icon": "🚀", 25 | "match": ["image", { "type": "file", "regex": ".(?:jpg|jpeg|png)$" }], 26 | "command": "redirect:上传到图床" 27 | }, 28 | { 29 | "id": "custom.1663490859", 30 | "title": "百度搜索", 31 | "icon": "🔍", 32 | "match": ["text"], 33 | "command": "redirect:百度一下" 34 | }, 35 | { 36 | "id": "custom.1663490860", 37 | "title": "统计文本字数", 38 | "icon": "🧮", 39 | "match": ["text"], 40 | "command": "redirect:统计文本次数" 41 | }, 42 | { 43 | "id": "custom.1663490861", 44 | "title": "颜色管理", 45 | "icon": "🎨", 46 | "match": [ 47 | { 48 | "type": "text", 49 | "regex": "^(?:#?[a-f0-9]{6}|(?:(?:25[0-5]|2[0-4]\\d|1?\\d{1,2}), ?){2}(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})|rgba?\\((?:(?:25[0-5]|2[0-4]\\d|1?\\d{1,2}), ?){2}(?:25[0-5]|2[0-4]\\d|1?\\d{1,2})(?:, ?(?:1|0|0\\.\\d{1,2}))?\\)|hs[liv]a?\\((?:360|(?:(?:3[0-5]\\d|[1-2]\\d{2}|\\d{1,2})(?:\\.\\d{1,15})?))(?:deg)?(?:, |,| )(?:100|\\d{1,2}(?:\\.\\d{1,15})?)%?(?:, |,| )(?:100|\\d{1,2}(?:\\.\\d{1,15})?)%?(?:, ?(?:1|0|0\\.\\d{1,2}))?\\))$" 50 | } 51 | ], 52 | "command": "redirect:颜色信息" 53 | }, 54 | { 55 | "id": "custom.1663490862", 56 | "title": "百度识图", 57 | "icon": "🌄", 58 | "match": ["image"], 59 | "command": "redirect:百度识图" 60 | }, 61 | { 62 | "id": "custom.1663490863", 63 | "title": "识别图片中二维码", 64 | "icon": "📜", 65 | "match": ["image"], 66 | "command": "redirect:识别图片中二维码" 67 | }, 68 | { 69 | "id": "custom.1663490865", 70 | "title": "翻译", 71 | "icon": "🌎", 72 | "match": ["text"], 73 | "command": "redirect:翻译" 74 | } 75 | ] 76 | } 77 | -------------------------------------------------------------------------------- /src/global/initPlugin.js: -------------------------------------------------------------------------------- 1 | const { 2 | utools, 3 | existsSync, 4 | readFileSync, 5 | writeFileSync, 6 | watch, 7 | crypto, 8 | listener, 9 | clipboard, 10 | time 11 | } = window.exports 12 | import { copy, paste, createFile, getNativeId } from '../utils' 13 | import setting from './readSetting' 14 | 15 | export default function initPlugin() { 16 | class DB { 17 | constructor(path) { 18 | const d = new Date() 19 | this.path = path 20 | this.dataBase = {} 21 | this.createTime = d.getTime() 22 | this.updateTime = d.getTime() 23 | this.defaultDB = { 24 | data: [], 25 | createTime: this.createTime, 26 | updateTime: this.updateTime 27 | } 28 | } 29 | init() { 30 | const isExist = existsSync(this.path) 31 | if (isExist) { 32 | const data = readFileSync(this.path, { 33 | encoding: 'utf8' 34 | }) 35 | try { 36 | // 读取磁盘记录到内存 37 | const dataBase = JSON.parse(data) 38 | this.dataBase = dataBase 39 | // 将超过14天的数据删除 排除掉收藏 40 | const now = new Date().getTime() 41 | const deleteTime = now - setting.database.maxage * 24 * 60 * 60 * 1000 // unicode 42 | this.dataBase.data = this.dataBase.data?.filter( 43 | (item) => item.updateTime > deleteTime || item.collect 44 | ) 45 | this.updateDataBaseLocal() 46 | this.watchDataBaseUpdate() 47 | } catch (err) { 48 | utools.showNotification('读取剪切板出错: ' + err) 49 | return 50 | } 51 | return 52 | } 53 | this.dataBase = this.defaultDB 54 | this.updateDataBaseLocal(this.defaultDB) 55 | } 56 | watchDataBaseUpdate() { 57 | watch(this.path, (eventType, filename) => { 58 | if (eventType === 'change') { 59 | // 更新内存中的数据 60 | const data = readFileSync(this.path, { 61 | encoding: 'utf8' 62 | }) 63 | try { 64 | const dataBase = JSON.parse(data) 65 | this.dataBase = dataBase 66 | window.db.dataBase = dataBase // 更新内存中数据 67 | listener.emit('view-change') // 触发视图更新 68 | } catch (err) { 69 | utools.showNotification('读取剪切板出错: ' + err) 70 | return 71 | } 72 | } 73 | }) 74 | } 75 | updateDataBase() { 76 | // 更新内存数据 77 | this.dataBase.updateTime = new Date().getTime() 78 | } 79 | updateDataBaseLocal(dataBase) { 80 | // 更新文件数据 81 | writeFileSync(this.path, JSON.stringify(dataBase || this.dataBase), (err) => { 82 | if (err) { 83 | utools.showNotification('写入剪切板出错: ' + err) 84 | return 85 | } 86 | }) 87 | } 88 | addItem(cItem) { 89 | this.dataBase.data.unshift(cItem) 90 | this.updateDataBase() 91 | const exceedCount = this.dataBase.data.length - setting.database.maxsize 92 | if (exceedCount > 0) { 93 | // 达到条数限制 在收藏条数限制内遍历非收藏历史并删除 94 | // 所有被移除的 item都存入tempList 95 | const tmpList = [] 96 | for (let i = 0; i < exceedCount; i++) { 97 | const item = this.dataBase.data.pop() 98 | tmpList.push(item) 99 | } 100 | tmpList.forEach((item) => !item.collect || this.dataBase.data.push(item)) // 收藏内容 重新入栈 101 | } 102 | this.updateDataBaseLocal() 103 | } 104 | emptyDataBase() { 105 | this.dataBase.data = [] 106 | window.db.dataBase.data = [] 107 | this.updateDataBaseLocal(this.defaultDB) 108 | listener.emit('view-change') 109 | } 110 | filterDataBaseViaId(id) { 111 | return this.dataBase.data.filter((item) => item.id === id) 112 | } 113 | updateItemViaId(id) { 114 | for (const item of this.dataBase.data) { 115 | if (item.id === id) { 116 | item.updateTime = new Date().getTime() 117 | this.sortDataBaseViaTime() 118 | return true 119 | } 120 | } 121 | return false 122 | } 123 | sortDataBaseViaTime() { 124 | this.dataBase.data = this.dataBase.data.sort((a, b) => { 125 | return b.updateTime - a.updateTime 126 | }) 127 | this.updateDataBaseLocal() 128 | } 129 | removeItemViaId(id) { 130 | for (const item of this.dataBase.data) { 131 | if (item.id === id) { 132 | this.dataBase.data.splice(this.dataBase.data.indexOf(item), 1) 133 | this.updateDataBaseLocal() 134 | return true 135 | } 136 | } 137 | return false 138 | } 139 | } 140 | 141 | const pbpaste = () => { 142 | // file 143 | const files = utools.getCopyedFiles() // null | Array 144 | if (files) { 145 | return { 146 | type: 'file', 147 | data: JSON.stringify(files) 148 | } 149 | } 150 | // text 151 | const text = clipboard.readText() 152 | if (text.trim()) return { type: 'text', data: text } 153 | // image 154 | const image = clipboard.readImage() // 大图卡顿来源 155 | const data = image.toDataURL() 156 | if (!image.isEmpty()) { 157 | return { 158 | type: 'image', 159 | data: data 160 | } 161 | } 162 | } 163 | 164 | // 根据当前设备id读取不同路径 若为旧版本则迁移数据 165 | const nativeId = getNativeId() 166 | console.log(setting.database.path[nativeId]) 167 | const db = new DB(setting.database.path[nativeId] || setting.database.path) 168 | db.init() 169 | 170 | const remove = (item) => db.removeItemViaId(item.id) 171 | 172 | const focus = (isBlur = false) => { 173 | return document.querySelector('.clip-search').style.display !== 'none' 174 | ? isBlur 175 | ? document.querySelector('.clip-search-input')?.blur() 176 | : document.querySelector('.clip-search-input')?.focus() 177 | : (document.querySelector('.clip-search-btn')?.click(), 178 | document.querySelector('.clip-search-input')?.focus()) 179 | } 180 | const toTop = () => (document.scrollingElement.scrollTop = 0) 181 | const resetNav = () => document.querySelectorAll('.clip-switch-item')[0]?.click() 182 | 183 | const handleClipboardChange = (item = pbpaste()) => { 184 | if (!item) return 185 | item.id = crypto.createHash('md5').update(item.data).digest('hex') 186 | if (db.updateItemViaId(item.id)) { 187 | // 在库中 由 updateItemViaId 更新 updateTime 188 | return 189 | } 190 | // 不在库中 由 addItem 添加 191 | item.createTime = new Date().getTime() 192 | item.updateTime = new Date().getTime() 193 | db.addItem(item) 194 | } 195 | 196 | const addCommonListener = () => { 197 | let prev = db.dataBase.data[0] || {} 198 | function loop() { 199 | time.sleep(300).then(loop) 200 | const item = pbpaste() 201 | if (!item) return 202 | item.id = crypto.createHash('md5').update(item.data).digest('hex') 203 | if (item && prev.id != item.id) { 204 | // 剪切板元素 与最近一次复制内容不同 205 | prev = item 206 | handleClipboardChange(item) 207 | } else { 208 | // 剪切板元素 与上次复制内容相同 209 | } 210 | } 211 | loop() 212 | } 213 | 214 | const registerClipEvent = (listener) => { 215 | const exitHandler = () => { 216 | utools.showNotification('剪贴板监听异常退出 请重启插件以开启监听') 217 | utools.outPlugin() 218 | } 219 | const errorHandler = (error) => { 220 | // const info = '请到设置页检查剪贴板监听程序状态' 221 | // utools.showNotification('启动剪贴板监听程序启动出错: ' + error + info) 222 | addCommonListener() 223 | } 224 | listener 225 | .on('change', handleClipboardChange) 226 | .on('close', exitHandler) 227 | .on('exit', exitHandler) 228 | .on('error', (error) => errorHandler(error)) 229 | } 230 | 231 | // 首次启动插件 即开启监听 232 | // 如果监听程序异常退出 则会在errorHandler中开启常规监听 233 | registerClipEvent(listener) 234 | listener.startListening(setting.database.path[nativeId]) 235 | 236 | utools.onPluginEnter(() => { 237 | if (!listener.listening) { 238 | // 进入插件后 如果监听已关闭 则重新开启监听 239 | registerClipEvent(listener) 240 | listener.startListening(setting.database.path[nativeId]) 241 | } 242 | toTop() 243 | resetNav() 244 | }) 245 | 246 | window.db = db 247 | window.copy = copy 248 | window.paste = paste 249 | window.remove = remove 250 | window.createFile = createFile 251 | window.focus = focus 252 | window.toTop = toTop 253 | window.listener = listener 254 | } 255 | -------------------------------------------------------------------------------- /src/global/readSetting.js: -------------------------------------------------------------------------------- 1 | import restoreSetting from './restoreSetting' 2 | import { defaultPath } from './restoreSetting' 3 | import { getNativeId } from '../utils' 4 | 5 | const setting = utools.dbStorage.getItem('setting') || restoreSetting() 6 | const nativeId = getNativeId() 7 | 8 | // 旧版本的setting中path是字符串,新版本的path是对象 9 | if (typeof setting.database.path === 'string') { 10 | setting.database.path = { 11 | [nativeId]: setting.database.path 12 | } 13 | } else { 14 | // 新版本的setting中path是对象,但是没有当前平台的路径 15 | if (!setting.database.path[nativeId]) { 16 | setting.database.path[nativeId] = defaultPath 17 | } 18 | } 19 | 20 | // 将设置更新到数据库 21 | utools.dbStorage.setItem('setting', setting) 22 | 23 | export default setting 24 | -------------------------------------------------------------------------------- /src/global/registerElement.js: -------------------------------------------------------------------------------- 1 | import 'element-plus/theme-chalk/base.css' 2 | import 'element-plus/theme-chalk/dark/css-vars.css' 3 | import 'element-plus/theme-chalk/el-overlay.css' 4 | import 'element-plus/theme-chalk/el-button.css' 5 | import 'element-plus/theme-chalk/el-message-box.css' 6 | import 'element-plus/theme-chalk/el-message.css' 7 | import 'element-plus/theme-chalk/el-card.css' 8 | import 'element-plus/theme-chalk/el-input.css' 9 | import 'element-plus/theme-chalk/el-select.css' 10 | import 'element-plus/theme-chalk/el-option.css' 11 | import 'element-plus/theme-chalk/el-scrollbar.css' 12 | import 'element-plus/theme-chalk/el-tag.css' 13 | import { 14 | ElButton, 15 | ElMessageBox, 16 | ElMessage, 17 | ElCard, 18 | ElInput, 19 | ElSelect, 20 | ElOption, 21 | ElScrollbar, 22 | ElTag 23 | } from 'element-plus' 24 | 25 | const components = [ 26 | ElButton, 27 | ElMessageBox, 28 | ElMessage, 29 | ElCard, 30 | ElInput, 31 | ElSelect, 32 | ElOption, 33 | ElScrollbar, 34 | ElTag 35 | ] 36 | 37 | document.querySelector('html').className = utools.isDarkColors() ? 'dark' : '' 38 | 39 | export default function registerElement(app) { 40 | components.forEach((c) => { 41 | let name = transferCamel(c.name) 42 | app.component(name, c) 43 | }) 44 | } 45 | 46 | function transferCamel(camel) { 47 | return camel 48 | .replace(/([A-Z])/g, '-$1') 49 | .toLowerCase() 50 | .slice(1) 51 | } 52 | -------------------------------------------------------------------------------- /src/global/restoreSetting.js: -------------------------------------------------------------------------------- 1 | import defaultSetting from '../data/setting.json' 2 | import { pointToObj, getNativeId } from '../utils' 3 | const { utools } = window.exports 4 | 5 | const defaultPath = `${utools.isMacOs() ? utools.getPath('userData') : utools.getPath('home')}${ 6 | window.exports.sep 7 | }_utools_clipboard_manager_storage` 8 | 9 | const nativeId = getNativeId() 10 | 11 | export default function restoreSetting() { 12 | // 将defaultSetting的key点语法转换为对象 13 | const setting = pointToObj(defaultSetting) 14 | setting.database.path[nativeId] = defaultPath // 根据不同设备设置不同的默认路径 15 | utools.dbStorage.setItem('setting', setting) 16 | return setting 17 | } 18 | 19 | export { defaultPath } 20 | -------------------------------------------------------------------------------- /src/hooks/useClipOperate.js: -------------------------------------------------------------------------------- 1 | import { ElMessage } from 'element-plus' 2 | import setting from '../global/readSetting' 3 | 4 | export default function useClipOperate({ emit }) { 5 | return { 6 | handleOperateClick: (operation, item) => { 7 | const { id } = operation 8 | const typeMap = { 9 | text: 'text', 10 | file: 'files', 11 | image: 'img' 12 | } 13 | if (id === 'copy') { 14 | window.copy(item, false) 15 | ElMessage({ 16 | message: '复制成功', 17 | type: 'success' 18 | }) 19 | } else if (id === 'view') { 20 | emit('onDataChange', item) 21 | } else if (id === 'open-folder') { 22 | const { data } = item 23 | const fl = JSON.parse(data) 24 | utools.shellShowItemInFolder(fl[0].path) // 取第一个文件的路径打开 25 | } else if (id === 'collect') { 26 | item.collect = true 27 | window.db.updateDataBaseLocal() 28 | } else if (id === 'un-collect') { 29 | item.collect = undefined 30 | window.db.updateDataBaseLocal() 31 | } else if (id === 'word-break') { 32 | utools.redirect('超级分词', item.data) 33 | } else if (id === 'save-file') { 34 | utools.redirect('收集文件', { 35 | type: typeMap[item.type], 36 | data: item.type === 'file' ? JSON.parse(item.data).map((f) => f.path) : item.data 37 | }) 38 | } else if (id === 'remove') { 39 | window.remove(item) 40 | emit('onDataRemove') 41 | } else if (id.indexOf('custom') !== -1) { 42 | const a = operation.command.split(':') 43 | if (a[0] === 'redirect') { 44 | utools.redirect(a[1], { 45 | type: typeMap[item.type], 46 | data: item.type === 'file' ? JSON.parse(item.data).map((f) => f.path) : item.data 47 | }) 48 | } 49 | } 50 | emit('onOperateExecute') 51 | }, 52 | filterOperate: (operation, item, isFullData) => { 53 | const { id } = operation 54 | if (!isFullData) { 55 | // 在非预览页 只展示setting.operation.shown中的功能按钮 56 | if (!setting.operation.shown.includes(id)) { 57 | return false 58 | } 59 | } 60 | if (id === 'copy') { 61 | return true 62 | } else if (id === 'view') { 63 | return !isFullData 64 | } else if (id === 'open-folder') { 65 | return item.type === 'file' 66 | } else if (id === 'collect') { 67 | return item.type !== 'file' && !item.collect 68 | } else if (id === 'un-collect') { 69 | return item.type !== 'file' && item.collect 70 | } else if (id === 'word-break') { 71 | return item.type === 'text' && item.data.length <= 500 && item.data.length >= 2 72 | } else if (id === 'save-file') { 73 | return true 74 | } else if (id === 'remove') { 75 | return true 76 | } else if (id.indexOf('custom') !== -1) { 77 | // 如果匹配到了自定义的操作 则展示 78 | for (const m of operation.match) { 79 | if (typeof m === 'string') { 80 | if (item.type === m) { 81 | return true 82 | } 83 | } else if (typeof m === 'object') { 84 | // 根据正则匹配内容 85 | const r = new RegExp(m.regex) 86 | if (item.type === 'file') { 87 | const fl = JSON.parse(item.data) 88 | for (const f of fl) { 89 | if (r.test(f.name)) { 90 | return true 91 | } 92 | } 93 | } else { 94 | return r.test(item.data) 95 | } 96 | } 97 | } 98 | return false 99 | } 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import initPlugin from './global/initPlugin' 2 | import { createApp } from 'vue' 3 | import App from './App.vue' 4 | import less from 'less' 5 | import registerElement from './global/registerElement' 6 | 7 | initPlugin() 8 | const app = createApp(App) 9 | app.use(less).use(registerElement) 10 | 11 | app.mount('#app') 12 | -------------------------------------------------------------------------------- /src/style/cpns/clip-float-btn.less: -------------------------------------------------------------------------------- 1 | .clip-float-btn { 2 | display: flex; 3 | justify-content: center; 4 | align-items: center; 5 | position: fixed; 6 | z-index: 100; 7 | bottom: 15px; 8 | right: 15px; 9 | height: 50px; 10 | width: 50px; 11 | cursor: pointer; 12 | border-radius: 50%; 13 | font-size: 20px; 14 | background-color: @text-bg-color-lighter; 15 | user-select: none; 16 | &:hover { 17 | font-size: 25px; 18 | color: @bg-color; 19 | background-color: @primary-color; 20 | transition: all 0.15s; 21 | } 22 | div { 23 | margin-bottom: 5px; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/style/cpns/clip-full-data.less: -------------------------------------------------------------------------------- 1 | .clip-full-wrapper { 2 | z-index: 102; 3 | position: fixed; 4 | top: 0; 5 | height: -webkit-fill-available; 6 | width: 80%; 7 | color: @text-color; 8 | background: @bg-color; 9 | margin: 0px 0px; 10 | padding: 10px 20px; 11 | overflow-y: scroll; 12 | word-break: break-all; 13 | white-space: pre-wrap; 14 | .clip-full-operate-list { 15 | position: fixed; 16 | right: 20%; 17 | z-index: 102; 18 | box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.2); 19 | display: flex; 20 | align-items: center; 21 | justify-content: center; 22 | color: @text-color; 23 | background-color: @text-bg-color; 24 | padding: 5px 5px; 25 | margin-bottom: 5px; 26 | border-radius: 5px; 27 | } 28 | .clip-full-content { 29 | display: flex; 30 | flex-direction: column; 31 | justify-content: center; 32 | position: relative; 33 | top: 50px; 34 | padding: 20px; 35 | margin-bottom: 55px; 36 | border-radius: 5px; 37 | background-color: @text-bg-color; 38 | img { 39 | max-width: 100%; 40 | } 41 | } 42 | &::-webkit-scrollbar { 43 | width: 10px; 44 | height: 10px; 45 | } 46 | &::-webkit-scrollbar-track { 47 | border-radius: 2px; 48 | background-color: @text-bg-color; 49 | &-piece { 50 | border-radius: 2px; 51 | background-color: @text-bg-color; 52 | } 53 | } 54 | &::-webkit-scrollbar-thumb { 55 | background: @text-color-lighter; 56 | border-radius: 5px; 57 | &:hover { 58 | background: @text-color; 59 | } 60 | } 61 | } 62 | .clip-overlay { 63 | z-index: 101; 64 | position: fixed; 65 | top: 0; 66 | height: 100%; 67 | width: 100%; 68 | background: rgba(0, 0, 0, 0.5); 69 | } 70 | -------------------------------------------------------------------------------- /src/style/cpns/clip-item-list.less: -------------------------------------------------------------------------------- 1 | .clip-item-list { 2 | background: @bg-color; 3 | user-select: none; 4 | .clip-item { 5 | display: flex; 6 | justify-content: space-between; 7 | margin: 0px 2px; 8 | /* 占用px 但是不显示(与背景同色) */ 9 | border-style: solid hidden hidden hidden; 10 | border-width: 1px 0px 0px 0px; 11 | border-color: @text-bg-color-lighter @bg-color @bg-color @bg-color; 12 | cursor: pointer; 13 | &.active { 14 | border-style: solid hidden hidden solid; 15 | border-width: 1px 0px 0px 6px; 16 | border-color: @text-bg-color-lighter @bg-color @bg-color @primary-color; 17 | background-color: @text-bg-color-lighter; 18 | transition: all 0.15s; 19 | } 20 | &.multi-active { 21 | border-style: solid hidden hidden solid; // TODO: 会导致左侧提示块上部被切下去一块 22 | border-width: 1px 0px 0px 6px; 23 | border-color: @text-bg-color-lighter @bg-color @bg-color @primary-color; 24 | transition: all 0.15s; 25 | } 26 | &.select { 27 | background-color: @text-bg-color-lighter; 28 | } 29 | .clip-info { 30 | display: flex; 31 | justify-content: center; 32 | align-items: center; 33 | font-size: 14px; 34 | padding: 10px 10px 10px 5px; 35 | .clip-time { 36 | display: flex; 37 | flex-direction: column; 38 | justify-content: center; 39 | align-items: center; 40 | min-width: 95px; 41 | max-width: 100px; 42 | border: 0px solid @text-bg-color-lighter; 43 | border-color: @text-bg-color-lighter; 44 | border-style: solid; 45 | border-width: 0px 2px 0px 0px; 46 | margin: 0px 5px 0px 0px; 47 | .relative-date { 48 | display: flex; 49 | justify-content: center; 50 | align-items: center; 51 | font-size: 13px; 52 | color: @text-color; 53 | background-color: @text-bg-color; 54 | border-radius: 5px; 55 | min-width: 50px; 56 | padding: 5px 7px; 57 | } 58 | } 59 | .clip-data { 60 | display: flex; 61 | overflow: hidden; 62 | word-break: break-all; 63 | max-height: 120px; 64 | max-width: 460px; 65 | padding: 5px 5px 5px 10px; 66 | white-space: pre-wrap; 67 | flex-direction: column; 68 | color: @text-color; 69 | img.clip-data-image { 70 | // 此 class用于区分 file的 image 71 | max-height: 100px; // 比外框 max-height少一点 因为有 5px的边框 72 | max-width: 85%; 73 | box-shadow: 0px 0px 3px @text-color; 74 | } 75 | .clip-over-sized-content { 76 | color: @primary-color; 77 | } 78 | } 79 | } 80 | .clip-count { 81 | display: flex; 82 | align-items: center; 83 | justify-content: center; 84 | min-width: 50px; 85 | padding: 10px; 86 | font-size: 13px; 87 | color: @text-color-lighter; 88 | padding: 10px; 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/style/cpns/clip-operate.less: -------------------------------------------------------------------------------- 1 | .clip-operate { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | flex-wrap: wrap; 6 | min-width: 180px; 7 | flex-wrap: wrap; 8 | & div { 9 | display: flex; 10 | align-items: center; 11 | justify-content: center; 12 | font-size: 13px; 13 | color: @primary-color; 14 | background: @text-bg-color; 15 | cursor: pointer; 16 | margin: 0px 2px; 17 | border-radius: 5px; 18 | width: 30px; 19 | height: 30px; 20 | transition: all 0.15s; 21 | &:hover { 22 | color: @bg-color; 23 | background: @primary-color; 24 | transition: all 0.15s; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/style/cpns/clip-search.less: -------------------------------------------------------------------------------- 1 | .clip-search { 2 | display: flex; 3 | align-items: center; 4 | justify-content: flex-end; 5 | min-width: 300px; 6 | margin-right: 10px; 7 | .clip-search-input { 8 | width: 90%; 9 | /* normalize */ 10 | background: none; 11 | outline: none; 12 | border: none; 13 | /* custom */ 14 | color: @text-color; 15 | background-color: @bg-color; 16 | height: fit-content; 17 | font-size: 15px; 18 | padding: 10px; 19 | border-radius: 5px; 20 | &::placeholder { 21 | color: @text-color-lighter; 22 | } 23 | } 24 | .clip-search-suffix { 25 | text-align: center; 26 | position: fixed; 27 | right: 20px; 28 | top: 18px; 29 | font-size: 13px; 30 | padding: 2px; 31 | width: 20px; 32 | height: 20px; 33 | background-color: @nav-bg-color; 34 | border-radius: 5px; 35 | cursor: pointer; 36 | &:hover { 37 | background-color: @nav-hover-bg-color; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/style/cpns/clip-switch.less: -------------------------------------------------------------------------------- 1 | .clip-switch { 2 | z-index: 100; 3 | position: fixed; 4 | top: 0px; 5 | display: flex; 6 | justify-content: space-between; 7 | align-items: center; 8 | flex-direction: row; 9 | width: 100%; 10 | color: @text-color; 11 | background-color: @nav-bg-color; 12 | transition: all 0.15s; 13 | .active { 14 | color: @primary-color; 15 | background-color: @bg-color; 16 | transition: all 0.15s ease-in-out; 17 | svg { 18 | fill: @primary-color; 19 | } 20 | } 21 | .clip-switch-items { 22 | display: flex; 23 | justify-content: left; 24 | align-items: center; 25 | flex-direction: row; 26 | margin-left: 5px; 27 | .clip-switch-item { 28 | display: flex; 29 | align-items: center; 30 | justify-content: center; 31 | min-width: 58px; 32 | padding: 10px 15px 10px 15px; 33 | margin: 10px 0px 10px 5px; 34 | cursor: pointer; 35 | border-radius: 5px; 36 | font-size: 14px; 37 | transition: all 0.15s; 38 | &:hover { 39 | background-color: @nav-hover-bg-color; 40 | transition: all 0.15s ease-in-out; 41 | } 42 | svg { 43 | margin-right: 5px; 44 | max-width: 30%; 45 | fill: @text-color-lighter; 46 | } 47 | } 48 | } 49 | .clip-switch-btn-list { 50 | margin-right: 5px; 51 | .clip-switch-btn { 52 | width: 25px; 53 | height: 25px; 54 | padding: 10px; 55 | border-radius: 5px; 56 | margin: 5px 2px; 57 | cursor: pointer; 58 | color: @text-color; 59 | background-color: @nav-bg-color; 60 | &:hover { 61 | background-color: @nav-hover-bg-color; 62 | transition: all 0.15s ease-in-out; 63 | } 64 | &.clip-select-count { 65 | background-color: @primary-color; 66 | color: @bg-color; 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/style/cpns/clip-word-break.less: -------------------------------------------------------------------------------- 1 | .clip-word-break { 2 | display: flex; 3 | align-items: center; 4 | padding: 5px; 5 | background-color: @text-bg-color; 6 | padding: 20px; 7 | border-radius: 5px; 8 | margin: 5px 0px; 9 | &-content-item { 10 | display: inline-block; 11 | vertical-align: middle; 12 | white-space: normal; 13 | word-break: break-all; 14 | word-wrap: break-word; 15 | padding: 5px; 16 | margin: 2px 5px; 17 | cursor: pointer; 18 | user-select: none; 19 | background-color: @bg-color; 20 | border-radius: 5px; 21 | transition: all 0.15s; 22 | &.active { 23 | color: @bg-color; 24 | background-color: @primary-color; 25 | transition: all 0.15s; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/style/cpns/file-list.less: -------------------------------------------------------------------------------- 1 | .clip-file { 2 | display: flex; 3 | justify-content: flex-start; 4 | align-items: center; 5 | cursor: pointer; 6 | font-size: 13px; 7 | width: fit-content; 8 | &:hover { 9 | text-decoration: underline; 10 | &::after { 11 | content: '📝'; 12 | } 13 | } 14 | .clip-file-icon { 15 | width: 15px; 16 | height: 15px; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/style/cpns/setting.less: -------------------------------------------------------------------------------- 1 | .setting-card { 2 | margin: 10px; 3 | &-content { 4 | padding: 10px; 5 | &-item { 6 | display: flex; 7 | justify-content: flex-start; 8 | align-items: center; 9 | margin: 10px; 10 | .el-select { 11 | &.number-select { 12 | width: 85px; 13 | } 14 | &.operation-select { 15 | width: 520px; 16 | } 17 | } 18 | .el-textarea { 19 | width: 70%; 20 | } 21 | .el-tag { 22 | margin: 0 10px; 23 | cursor: pointer; 24 | } 25 | .path { 26 | width: 65%; 27 | } 28 | & div { 29 | padding: 5px 10px; 30 | } 31 | } 32 | } 33 | &-footer { 34 | display: flex; 35 | justify-content: flex-end; 36 | align-items: center; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/style/index.less: -------------------------------------------------------------------------------- 1 | .import() { 2 | /* 导入全部涉及到变量的样式文件 */ 3 | @import (multiple) './cpns/clip-float-btn.less'; 4 | @import (multiple) './cpns/clip-full-data.less'; 5 | @import (multiple) './cpns/clip-item-list.less'; 6 | @import (multiple) './cpns/clip-operate.less'; 7 | @import (multiple) './cpns/clip-search.less'; 8 | @import (multiple) './cpns/clip-switch.less'; 9 | @import (multiple) './cpns/clip-word-break.less'; 10 | @import (multiple) './cpns/file-list.less'; 11 | @import (multiple) './cpns/setting.less'; 12 | } 13 | 14 | @media (prefers-color-scheme: dark) { 15 | #app { 16 | @primary-color: #448bd2; 17 | @primary-color-lighter: #4997e1; 18 | @text-color: #e8e6e3; 19 | @text-color-lighter: rgb(181, 181, 181); 20 | @text-bg-color: #656565; 21 | @text-bg-color-lighter: #4e4e4e; 22 | @nav-bg-color: #222426; 23 | @nav-hover-bg-color: #2b2e30; 24 | @bg-color: #181a1b; 25 | .import(); 26 | } 27 | } 28 | 29 | @media (prefers-color-scheme: light) { 30 | #app { 31 | @primary-color: #3271ae; 32 | @primary-color-lighter: #4997e1; 33 | @text-color: #333; 34 | @text-color-lighter: rgb(138, 138, 138); 35 | @text-bg-color: #f2f2f2; 36 | @text-bg-color-lighter: #eeeaf3; 37 | @nav-bg-color: #eeeeee; 38 | @nav-hover-bg-color: #dedede; 39 | @bg-color: #fff; 40 | .import(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | const { utools, existsSync, writeFileSync, mkdirSync, sep, Buffer } = window.exports 2 | 3 | const dateFormat = (timeStamp) => { 4 | const startTime = new Date(timeStamp) // 开始时间 5 | const endTime = new Date() // 结束时间 6 | const gaps = [ 7 | Math.floor((endTime - startTime) / 1000 / 60), // 分钟 8 | Math.floor((endTime - startTime) / 1000 / 60 / 60), // 小时 9 | Math.floor((endTime - startTime) / 1000 / 60 / 60 / 24) // 天 10 | ] 11 | let info = '' 12 | if (gaps[2] > 0) { 13 | info = `${gaps[2]}天前` 14 | } else if (gaps[1] > 0) { 15 | info = `${gaps[1]}小时前` 16 | } else if (gaps[0] > 0) { 17 | info = `${gaps[0]}分钟前` 18 | } else { 19 | info = '刚刚' 20 | } 21 | return info 22 | } 23 | 24 | const pointToObj = (objWithPointKey) => { 25 | let rtnObj = {} 26 | for (const key in objWithPointKey) { 27 | const keys = key.split('.') 28 | let obj = rtnObj 29 | for (let i = 0; i < keys.length; i++) { 30 | if (i === keys.length - 1) { 31 | obj[keys[i]] = objWithPointKey[key] 32 | } else { 33 | if (!obj[keys[i]]) obj[keys[i]] = {} 34 | obj = obj[keys[i]] 35 | } 36 | } 37 | } 38 | return rtnObj 39 | } 40 | 41 | const copy = (item, isHideMainWindow = true) => { 42 | switch (item.type) { 43 | case 'text': 44 | utools.copyText(item.data) 45 | break 46 | case 'image': 47 | utools.copyImage(item.data) 48 | break 49 | case 'file': 50 | const paths = JSON.parse(item.data).map((file) => file.path) 51 | utools.copyFile(paths) 52 | break 53 | } 54 | isHideMainWindow && utools.hideMainWindow() 55 | } 56 | 57 | const paste = () => { 58 | if (utools.isMacOs()) utools.simulateKeyboardTap('v', 'command') 59 | else utools.simulateKeyboardTap('v', 'ctrl') 60 | } 61 | 62 | const createFile = (item) => { 63 | const tempPath = utools.getPath('temp') 64 | const folderPath = tempPath + sep + 'utools-clipboard-manager' 65 | if (!existsSync(folderPath)) { 66 | try { 67 | mkdirSync(folderPath) 68 | } catch (err) { 69 | utools.showNotification('创建临时文件夹出错: ' + err) 70 | } 71 | } 72 | const { type } = item 73 | if (type === 'image') { 74 | const base64Data = item.data.replace(/^data:image\/\w+;base64,/, '') // remove the prefix 75 | const buffer = Buffer.from(base64Data, 'base64') // to Buffer 76 | const filePath = folderPath + sep + new Date().valueOf() + '.png' 77 | writeFileSync(filePath, buffer) 78 | return filePath 79 | } else if (type === 'text') { 80 | const filePath = folderPath + sep + new Date().valueOf() + '.txt' 81 | writeFileSync(filePath, item.data) 82 | return filePath 83 | } 84 | } 85 | 86 | const getNativeId = () => { 87 | return utools.getNativeId() 88 | } 89 | 90 | export { dateFormat, pointToObj, copy, paste, createFile, getNativeId } 91 | -------------------------------------------------------------------------------- /src/views/Main.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 361 | 362 | 376 | -------------------------------------------------------------------------------- /src/views/Setting.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 210 | 211 | 214 | -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | const UglifyJsPlugin = require('uglifyjs-webpack-plugin') 2 | 3 | module.exports = { 4 | publicPath: './', 5 | productionSourceMap: false, 6 | chainWebpack: (config) => { 7 | config.optimization.minimizer('uglify-plugin').use(UglifyJsPlugin, [ 8 | { 9 | uglifyOptions: { 10 | drop_console: false, 11 | drop_debugger: false, 12 | pure_funcs: ['console.log'] 13 | } 14 | } 15 | ]) 16 | } 17 | } 18 | --------------------------------------------------------------------------------