mirror of
https://github.com/rubickCenter/rubick
synced 2025-12-18 17:04:41 +08:00
Compare commits
43 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b4ca0e289 | ||
|
|
b75188fdb8 | ||
|
|
bf3ae7c9ba | ||
|
|
b7b16e2f3e | ||
|
|
3fec3665b6 | ||
|
|
b7587a454f | ||
|
|
f5f3f030ce | ||
|
|
0bddb33fde | ||
|
|
9005d070aa | ||
|
|
9adfa84cab | ||
|
|
15c160e45d | ||
|
|
6bf613042a | ||
|
|
04fe2e03a6 | ||
|
|
6c0d34fc4f | ||
|
|
3fa9bb0384 | ||
|
|
791115901a | ||
|
|
a879ed6555 | ||
|
|
28b58e7976 | ||
|
|
dc54b25f84 | ||
|
|
fc51a383bf | ||
|
|
a546bc0d59 | ||
|
|
c732e448c3 | ||
|
|
fbc7da0606 | ||
|
|
6315ec12ae | ||
|
|
3ba8250d7c | ||
|
|
706aa84374 | ||
|
|
f70bf3983e | ||
|
|
47359308fc | ||
|
|
3f03e5578e | ||
|
|
7cabbe26f5 | ||
|
|
5c048c6341 | ||
|
|
e90a30c8a4 | ||
|
|
599538db76 | ||
|
|
e9c41b6bdb | ||
|
|
1e7a8209b7 | ||
|
|
481cd44ab3 | ||
|
|
d41caa742b | ||
|
|
69218a728b | ||
|
|
73cef1512c | ||
|
|
986ad42ed7 | ||
|
|
42aec3403a | ||
|
|
8c15dba68d | ||
|
|
37647de7a8 |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@@ -23,7 +23,7 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest, windows-2019, ubuntu-latest]
|
os: [macos-latest, windows-2022, ubuntu-latest]
|
||||||
|
|
||||||
# create steps
|
# create steps
|
||||||
steps:
|
steps:
|
||||||
@@ -52,7 +52,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
yarn
|
yarn
|
||||||
yarn global add xvfb-maybe
|
yarn global add xvfb-maybe
|
||||||
yarn global add @vue/cli
|
yarn global add @vue/cli@4.5.0 --frozen-lockfile --ignore-engines
|
||||||
- name: Build feature
|
- name: Build feature
|
||||||
run: |
|
run: |
|
||||||
cd ./feature
|
cd ./feature
|
||||||
|
|||||||
30
README.md
30
README.md
@@ -1,36 +1,22 @@
|
|||||||
English | [简体中文](./README.zh-CN.md)
|
English | [简体中文](./README.zh-CN.md)
|
||||||
|
|
||||||
|
|
||||||
<div align= "center">
|
<div align= "center">
|
||||||
<img align="center" width=200 src="./public/logo.png" />
|
<img align="center" width=200 src="./public/logo.png" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
<div align= "center">
|
<h1>Rubick</h1>
|
||||||
<h1>Rubick</h1>
|
<img alt="downloads" src="https://img.shields.io/github/downloads/rubickCenter/rubick/total" />
|
||||||
|
<a href="https://github.com/rubickCenter/rubick/releases"><img alt="latest release" src="https://img.shields.io/github/package-json/v/rubickCenter/rubick" /></a>
|
||||||
<img alt="release" src="https://img.shields.io/github/downloads/rubickCenter/rubick/total" />
|
<a href="https://github.com/rubickCenter/rubick/actions"><img alt="github action building" src="https://img.shields.io/github/actions/workflow/status/rubickCenter/rubick/main.yml" /></a>
|
||||||
<a href="https://github.com/rubickCenter/rubick/releases">
|
<a href="https://github.com/rubickCenter/rubick/blob/master/LICENSE"><img alt="license" src="https://img.shields.io/github/license/rubickCenter/rubick" /></a>
|
||||||
<img alt="release" src="https://img.shields.io/github/package-json/v/rubickCenter/rubick" />
|
<a href="https://github.com/rubickCenter/rubick/stargazers"><img alt="github stars" src="https://img.shields.io/github/stars/rubickCenter/rubick?style=social" /></a>
|
||||||
</a>
|
<a href="https://gitee.com/monkeyWang/rubick"><img alt="gitee mirror" src="https://img.shields.io/badge/Gitee--yellow.svg?style=social&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNTc0ODM3MTM4ODM3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE3NzAiICAgICB3aWR0aD0iMTYiIGhlaWdodD0iMTYiPiAgICA8cGF0aCBkPSJNODkxIDQyOC44SDQ2NS44Yy0yMC40IDAtMzcgMTYuNS0zNyAzN3Y5Mi40YzAgMjAuNCAxNi41IDM3IDM3IDM3aDI1OC45YzIwLjQgMCAzNyAxNi42IDM3IDM3djE4LjRjMCA2MS4zLTQ5LjcgMTEwLjktMTEwLjkgMTEwLjlIMjk5LjRjLTIwLjQgMC0zNy0xNi42LTM3LTM3VjM3My4yYzAtNjEuMyA0OS43LTExMC45IDExMC45LTExMC45aDUxNy42YzIwLjQgMCAzNy0xNi41IDM3LTM3bDAuMS05Mi4zYzAtMjAuNC0xNi41LTM3LTM3LTM3SDM3My4zQzIyMC4yIDk2IDk2IDIyMC4yIDk2IDM3My4zVjg5MWMwIDIwLjQgMTYuNiAzNyAzNyAzN2g1NDUuNEM4MTYuMiA5MjggOTI4IDgxNi4zIDkyOCA2NzguNFY0NjUuOGMwLTIwLjQtMTYuNi0zNy0zNy0zN3oiICAgICAgICAgIGZpbGw9IiNkODFlMDYiIHAtaWQ9IjE3NzEiPjwvcGF0aD48L3N2Zz4=" /></a>
|
||||||
<a href="https://github.com/rubickCenter/rubick/actions">
|
|
||||||
<img alt=building src=https://img.shields.io/github/actions/workflow/status/rubickCenter/rubick/main.yml>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/rubickCenter/rubick/blob/master/LICENSE">
|
|
||||||
<img alt="npm" src="https://img.shields.io/github/license/rubickCenter/rubick" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/rubickCenter/rubick/stargazers">
|
|
||||||
<img alt="star" src="https://img.shields.io/github/stars/rubickCenter/rubick?style=social">
|
|
||||||
</a>
|
|
||||||
<a href="https://gitee.com/monkeyWang/rubick">
|
|
||||||
<img alt="码云" src="https://img.shields.io/badge/Gitee--yellow.svg?style=social&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNTc0ODM3MTM4ODM3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE3NzAiICAgICB3aWR0aD0iMTYiIGhlaWdodD0iMTYiPiAgICA8cGF0aCBkPSJNODkxIDQyOC44SDQ2NS44Yy0yMC40IDAtMzcgMTYuNS0zNyAzN3Y5Mi40YzAgMjAuNCAxNi41IDM3IDM3IDM3aDI1OC45YzIwLjQgMCAzNyAxNi42IDM3IDM3djE4LjRjMCA2MS4zLTQ5LjcgMTEwLjktMTEwLjkgMTEwLjlIMjk5LjRjLTIwLjQgMC0zNy0xNi42LTM3LTM3VjM3My4yYzAtNjEuMyA0OS43LTExMC45IDExMC45LTExMC45aDUxNy42YzIwLjQgMCAzNy0xNi41IDM3LTM3bDAuMS05Mi4zYzAtMjAuNC0xNi41LTM3LTM3LTM3SDM3My4zQzIyMC4yIDk2IDk2IDIyMC4yIDk2IDM3My4zVjg5MWMwIDIwLjQgMTYuNiAzNyAzNyAzN2g1NDUuNEM4MTYuMiA5MjggOTI4IDgxNi4zIDkyOCA2NzguNFY0NjUuOGMwLTIwLjQtMTYuNi0zNy0zNy0zN3oiICAgICAgICAgIGZpbGw9IiNkODFlMDYiIHAtaWQ9IjE3NzEiPjwvcGF0aD48L3N2Zz4="/>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align= "center">
|
<div align= "center">
|
||||||
<img align="center" src="https://picx.zhimg.com/80/v2-f8fe09ef125dac5fdcbef3fe00f92b21_720w.png" />
|
<img align="center" src="https://picx.zhimg.com/80/v2-f8fe09ef125dac5fdcbef3fe00f92b21_720w.png" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
Open-source plugin-based desktop efficiency toolbox. The plugins are installed and uninstalled based on npm, which is very lightweight. The plugin data supports webdav multi-terminal synchronization, which is very secure. It supports internal network deployment and can be customized for further development, which is very flexible.
|
Open-source plugin-based desktop efficiency toolbox. The plugins are installed and uninstalled based on npm, which is very lightweight. The plugin data supports webdav multi-terminal synchronization, which is very secure. It supports internal network deployment and can be customized for further development, which is very flexible.
|
||||||
|
|
||||||
## Get Rubick
|
## Get Rubick
|
||||||
|
|||||||
@@ -5,25 +5,14 @@
|
|||||||
<img align="center" width=200 src="./public/logo.png" />
|
<img align="center" width=200 src="./public/logo.png" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align= "center">
|
<div align="center">
|
||||||
<h1>Rubick</h1>
|
<h1>Rubick</h1>
|
||||||
|
<img alt="累计下载数" src="https://img.shields.io/github/downloads/rubickCenter/rubick/total" />
|
||||||
<img alt="release" src="https://img.shields.io/github/downloads/rubickCenter/rubick/total" />
|
<a href="https://github.com/rubickCenter/rubick/releases"><img alt="最新发布版本" src="https://img.shields.io/github/package-json/v/rubickCenter/rubick" /></a>
|
||||||
<a href="https://github.com/rubickCenter/rubick/releases">
|
<a href="https://github.com/rubickCenter/rubick/actions"><img alt="github action 构建" src="https://img.shields.io/github/actions/workflow/status/rubickCenter/rubick/main.yml" /></a>
|
||||||
<img alt="release" src="https://img.shields.io/github/package-json/v/rubickCenter/rubick" />
|
<a href="https://github.com/rubickCenter/rubick/blob/master/LICENSE"><img alt="许可证" src="https://img.shields.io/github/license/rubickCenter/rubick" /></a>
|
||||||
</a>
|
<a href="https://github.com/rubickCenter/rubick/stargazers"><img alt="github 收藏数" src="https://img.shields.io/github/stars/rubickCenter/rubick?style=social" /></a>
|
||||||
<a href="https://github.com/rubickCenter/rubick/actions">
|
<a href="https://gitee.com/monkeyWang/rubick"><img alt="gitee 镜像源" src="https://img.shields.io/badge/Gitee--yellow.svg?style=social&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNTc0ODM3MTM4ODM3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE3NzAiICAgICB3aWR0aD0iMTYiIGhlaWdodD0iMTYiPiAgICA8cGF0aCBkPSJNODkxIDQyOC44SDQ2NS44Yy0yMC40IDAtMzcgMTYuNS0zNyAzN3Y5Mi40YzAgMjAuNCAxNi41IDM3IDM3IDM3aDI1OC45YzIwLjQgMCAzNyAxNi42IDM3IDM3djE4LjRjMCA2MS4zLTQ5LjcgMTEwLjktMTEwLjkgMTEwLjlIMjk5LjRjLTIwLjQgMC0zNy0xNi42LTM3LTM3VjM3My4yYzAtNjEuMyA0OS43LTExMC45IDExMC45LTExMC45aDUxNy42YzIwLjQgMCAzNy0xNi41IDM3LTM3bDAuMS05Mi4zYzAtMjAuNC0xNi41LTM3LTM3LTM3SDM3My4zQzIyMC4yIDk2IDk2IDIyMC4yIDk2IDM3My4zVjg5MWMwIDIwLjQgMTYuNiAzNyAzNyAzN2g1NDUuNEM4MTYuMiA5MjggOTI4IDgxNi4zIDkyOCA2NzguNFY0NjUuOGMwLTIwLjQtMTYuNi0zNy0zNy0zN3oiICAgICAgICAgIGZpbGw9IiNkODFlMDYiIHAtaWQ9IjE3NzEiPjwvcGF0aD48L3N2Zz4=" /></a>
|
||||||
<img alt=building src=https://img.shields.io/github/actions/workflow/status/rubickCenter/rubick/main.yml>
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/rubickCenter/rubick/blob/master/LICENSE">
|
|
||||||
<img alt="npm" src="https://img.shields.io/github/license/rubickCenter/rubick" />
|
|
||||||
</a>
|
|
||||||
<a href="https://github.com/rubickCenter/rubick/stargazers">
|
|
||||||
<img alt="star" src="https://img.shields.io/github/stars/rubickCenter/rubick?style=social">
|
|
||||||
</a>
|
|
||||||
<a href="https://gitee.com/monkeyWang/rubick">
|
|
||||||
<img alt="码云" src="https://img.shields.io/badge/Gitee--yellow.svg?style=social&logo=data:image/svg+xml;base64,PHN2ZyB0PSIxNTc0ODM3MTM4ODM3IiBjbGFzcz0iaWNvbiIgdmlld0JveD0iMCAwIDEwMjQgMTAyNCIgdmVyc2lvbj0iMS4xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHAtaWQ9IjE3NzAiICAgICB3aWR0aD0iMTYiIGhlaWdodD0iMTYiPiAgICA8cGF0aCBkPSJNODkxIDQyOC44SDQ2NS44Yy0yMC40IDAtMzcgMTYuNS0zNyAzN3Y5Mi40YzAgMjAuNCAxNi41IDM3IDM3IDM3aDI1OC45YzIwLjQgMCAzNyAxNi42IDM3IDM3djE4LjRjMCA2MS4zLTQ5LjcgMTEwLjktMTEwLjkgMTEwLjlIMjk5LjRjLTIwLjQgMC0zNy0xNi42LTM3LTM3VjM3My4yYzAtNjEuMyA0OS43LTExMC45IDExMC45LTExMC45aDUxNy42YzIwLjQgMCAzNy0xNi41IDM3LTM3bDAuMS05Mi4zYzAtMjAuNC0xNi41LTM3LTM3LTM3SDM3My4zQzIyMC4yIDk2IDk2IDIyMC4yIDk2IDM3My4zVjg5MWMwIDIwLjQgMTYuNiAzNyAzNyAzN2g1NDUuNEM4MTYuMiA5MjggOTI4IDgxNi4zIDkyOCA2NzguNFY0NjUuOGMwLTIwLjQtMTYuNi0zNy0zNy0zN3oiICAgICAgICAgIGZpbGw9IiNkODFlMDYiIHAtaWQ9IjE3NzEiPjwvcGF0aD48L3N2Zz4="/>
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div align= "center">
|
<div align= "center">
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
"eslint-plugin-prettier": "^3.3.1",
|
"eslint-plugin-prettier": "^3.3.1",
|
||||||
"eslint-plugin-vue": "^7.0.0",
|
"eslint-plugin-vue": "^7.0.0",
|
||||||
"less": "^4.1.3",
|
"less": "^4.1.3",
|
||||||
"less-loader": "5.0.0",
|
"less-loader": "^6.2.0",
|
||||||
"prettier": "^2.2.1",
|
"prettier": "^2.2.1",
|
||||||
"typescript": "~4.1.5"
|
"typescript": "~4.1.5"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -274,12 +274,16 @@ const state = reactive({
|
|||||||
custom: {},
|
custom: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加lastKeyPressTime变量来跟踪按键时间
|
||||||
|
const lastKeyPressTime = ref(0);
|
||||||
|
const DOUBLE_CLICK_THRESHOLD = 300; // 双击时间阈值(毫秒)
|
||||||
|
|
||||||
const isWindows = window?.rubick?.isWindows();
|
const isWindows = window?.rubick?.isWindows();
|
||||||
const tipText = computed(() => {
|
const tipText = computed(() => {
|
||||||
const optionKeyName = isWindows ? 'Alt' : 'Option、Command';
|
const optionKeyName = isWindows ? 'Alt' : 'Option、Command';
|
||||||
return t('feature.settings.global.addShortcutKeyTips', {
|
return t('feature.settings.global.addShortcutKeyTips', {
|
||||||
optionKeyName: optionKeyName,
|
optionKeyName: optionKeyName,
|
||||||
});
|
}) + `此外你也可以双击修饰键如(Ctrl+Ctrl)`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentSelect = ref(['userInfo']);
|
const currentSelect = ref(['userInfo']);
|
||||||
@@ -314,33 +318,60 @@ watch(state, setConfig);
|
|||||||
|
|
||||||
const changeShortCut = (e, key) => {
|
const changeShortCut = (e, key) => {
|
||||||
let compose = '';
|
let compose = '';
|
||||||
// 添加是否包含功能键的判断
|
const currentTime = Date.now();
|
||||||
let incluFuncKeys = false;
|
const isDoubleClick = currentTime - lastKeyPressTime.value < DOUBLE_CLICK_THRESHOLD;
|
||||||
|
lastKeyPressTime.value = currentTime;
|
||||||
|
|
||||||
|
// 处理 F1-F12 功能键
|
||||||
|
if (e.keyCode >= 112 && e.keyCode <= 123) {
|
||||||
|
state.shortCut[key] = keycodes[e.keyCode].toUpperCase();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理双击功能键的情况
|
||||||
|
if (isDoubleClick) {
|
||||||
|
if (e.keyCode === 17) { // Ctrl
|
||||||
|
state.shortCut[key] = 'Ctrl+Ctrl';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 18) { // Alt
|
||||||
|
state.shortCut[key] = 'Option+Option';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 16) { // Shift
|
||||||
|
state.shortCut[key] = 'Shift+Shift';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.keyCode === 93) { // Command
|
||||||
|
state.shortCut[key] = 'Command+Command';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理功能键+普通键的组合
|
||||||
|
let hasModifierKey = false;
|
||||||
|
|
||||||
if (e.ctrlKey && e.keyCode !== 17) {
|
if (e.ctrlKey && e.keyCode !== 17) {
|
||||||
compose += '+Ctrl';
|
compose += '+Ctrl';
|
||||||
incluFuncKeys = true;
|
hasModifierKey = true;
|
||||||
}
|
}
|
||||||
if (e.shiftKey && e.keyCode !== 16) {
|
if (e.shiftKey && e.keyCode !== 16) {
|
||||||
compose += '+Shift';
|
compose += '+Shift';
|
||||||
incluFuncKeys = true;
|
hasModifierKey = true;
|
||||||
}
|
}
|
||||||
if (e.altKey && e.keyCode !== 18) {
|
if (e.altKey && e.keyCode !== 18) {
|
||||||
compose += '+Option';
|
compose += '+Option';
|
||||||
incluFuncKeys = true;
|
hasModifierKey = true;
|
||||||
}
|
}
|
||||||
if (e.metaKey && e.keyCode !== 93) {
|
if (e.metaKey && e.keyCode !== 93) {
|
||||||
compose += '+Command';
|
compose += '+Command';
|
||||||
incluFuncKeys = true;
|
hasModifierKey = true;
|
||||||
}
|
}
|
||||||
compose += '+' + keycodes[e.keyCode].toUpperCase();
|
|
||||||
compose = compose.substring(1);
|
// 只有当有修饰键时才添加普通键
|
||||||
if (
|
if (hasModifierKey) {
|
||||||
incluFuncKeys &&
|
compose += '+' + keycodes[e.keyCode].toUpperCase();
|
||||||
e.keyCode !== 16 &&
|
compose = compose.substring(1);
|
||||||
e.keyCode !== 17 &&
|
|
||||||
e.keyCode !== 18 &&
|
|
||||||
e.keyCode !== 93
|
|
||||||
) {
|
|
||||||
state.shortCut[key] = compose;
|
state.shortCut[key] = compose;
|
||||||
} else {
|
} else {
|
||||||
// 不做处理
|
// 不做处理
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ module.exports = {
|
|||||||
// 向预处理器 Loader 传递配置选项
|
// 向预处理器 Loader 传递配置选项
|
||||||
less: {
|
less: {
|
||||||
// 配置less(其他样式解析用法一致)
|
// 配置less(其他样式解析用法一致)
|
||||||
javascriptEnabled: true, // 设置为true
|
lessOptions: {
|
||||||
|
javascriptEnabled: true, // 设置为true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "rubick",
|
"name": "rubick",
|
||||||
"version": "4.3.2",
|
"version": "4.3.7",
|
||||||
"author": "muwoo <2424880409@qq.com>",
|
"author": "muwoo <2424880409@qq.com>",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -39,6 +39,7 @@
|
|||||||
"pouchdb-load": "^1.4.6",
|
"pouchdb-load": "^1.4.6",
|
||||||
"pouchdb-replication-stream": "^1.2.9",
|
"pouchdb-replication-stream": "^1.2.9",
|
||||||
"simple-plist": "0.2.1",
|
"simple-plist": "0.2.1",
|
||||||
|
"uiohook-napi": "^1.5.4",
|
||||||
"vue": "^3.0.0",
|
"vue": "^3.0.0",
|
||||||
"vue-router": "^4.0.0-0",
|
"vue-router": "^4.0.0-0",
|
||||||
"vuex": "^4.0.0-0",
|
"vuex": "^4.0.0-0",
|
||||||
|
|||||||
@@ -4,6 +4,6 @@ import path from 'path';
|
|||||||
const appPath = app.getPath('userData');
|
const appPath = app.getPath('userData');
|
||||||
|
|
||||||
const PLUGIN_INSTALL_DIR = path.join(appPath, './rubick-plugins-new');
|
const PLUGIN_INSTALL_DIR = path.join(appPath, './rubick-plugins-new');
|
||||||
const PLUGIN_HISTORY = 'rubick-local-start-app';
|
const PLUGIN_HISTORY = 'rubick-plugin-history';
|
||||||
|
|
||||||
export { PLUGIN_INSTALL_DIR, PLUGIN_HISTORY };
|
export { PLUGIN_INSTALL_DIR, PLUGIN_HISTORY };
|
||||||
|
|||||||
@@ -36,7 +36,14 @@ class AdapterHandler {
|
|||||||
fs.mkdirsSync(options.baseDir);
|
fs.mkdirsSync(options.baseDir);
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
`${options.baseDir}/package.json`,
|
`${options.baseDir}/package.json`,
|
||||||
'{"dependencies":{}}'
|
// '{"dependencies":{}}'
|
||||||
|
// fix 插件安装时node版本问题
|
||||||
|
JSON.stringify({
|
||||||
|
dependencies: {},
|
||||||
|
volta: {
|
||||||
|
node: '16.19.1',
|
||||||
|
},
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.baseDir = options.baseDir;
|
this.baseDir = options.baseDir;
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export default () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const init = (plugin, window: BrowserWindow) => {
|
const init = (plugin, window: BrowserWindow) => {
|
||||||
if (view === null || view === undefined) {
|
if (view === null || view === undefined || view.inDetach) {
|
||||||
createView(plugin, window);
|
createView(plugin, window);
|
||||||
// if (viewInstance.getView(plugin.name) && !commonConst.dev()) {
|
// if (viewInstance.getView(plugin.name) && !commonConst.dev()) {
|
||||||
// view = viewInstance.getView(plugin.name).view;
|
// view = viewInstance.getView(plugin.name).view;
|
||||||
@@ -176,14 +176,29 @@ export default () => {
|
|||||||
const removeView = (window: BrowserWindow) => {
|
const removeView = (window: BrowserWindow) => {
|
||||||
if (!view) return;
|
if (!view) return;
|
||||||
executeHooks('PluginOut', null);
|
executeHooks('PluginOut', null);
|
||||||
|
// 先记住这次要移除的视图,防止后面异步代码里全局引用被换掉
|
||||||
|
const snapshotView = view;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
window.removeBrowserView(view);
|
// 获取当前视图,判断是否已经换成了新视图
|
||||||
if (!view.inDetach) {
|
const currentView = window.getBrowserView?.();
|
||||||
window.setBrowserView(null);
|
window.removeBrowserView(snapshotView);
|
||||||
view.webContents?.destroy();
|
|
||||||
|
// 主窗口的插件视图仍然挂着旧实例时,需要还原主窗口 UI
|
||||||
|
if (!snapshotView.inDetach) {
|
||||||
|
// 如果窗口还挂着旧视图,说明还没换掉,需要把主窗口恢复到初始状态
|
||||||
|
if (currentView === snapshotView) {
|
||||||
|
window.setBrowserView(null);
|
||||||
|
if (view === snapshotView) {
|
||||||
|
window.webContents?.executeJavaScript(`window.initRubick()`);
|
||||||
|
view = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
snapshotView.webContents?.destroy();
|
||||||
|
}
|
||||||
|
// 分离窗口只需释放全局引用,视图由分离窗口继续管理
|
||||||
|
else if (view === snapshotView) {
|
||||||
|
view = undefined;
|
||||||
}
|
}
|
||||||
window.webContents?.executeJavaScript(`window.initRubick()`);
|
|
||||||
view = undefined;
|
|
||||||
}, 0);
|
}, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,30 @@ import DBInstance from './db';
|
|||||||
import getWinPosition from './getWinPosition';
|
import getWinPosition from './getWinPosition';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import commonConst from '@/common/utils/commonConst';
|
import commonConst from '@/common/utils/commonConst';
|
||||||
|
import { copyFilesToWindowsClipboard } from './windowsClipboard';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* sanitize input files 剪贴板文件合法性校验
|
||||||
|
* @param input
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const sanitizeInputFiles = (input: unknown): string[] => {
|
||||||
|
const candidates = Array.isArray(input)
|
||||||
|
? input
|
||||||
|
: typeof input === 'string'
|
||||||
|
? [input]
|
||||||
|
: [];
|
||||||
|
return candidates
|
||||||
|
.map((filePath) => (typeof filePath === 'string' ? filePath.trim() : ''))
|
||||||
|
.filter((filePath) => {
|
||||||
|
if (!filePath) return false;
|
||||||
|
try {
|
||||||
|
return fs.existsSync(filePath);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const runnerInstance = runner();
|
const runnerInstance = runner();
|
||||||
const detachInstance = detach();
|
const detachInstance = detach();
|
||||||
@@ -230,13 +254,28 @@ class API extends DBInstance {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public copyFile({ data }) {
|
public copyFile({ data }) {
|
||||||
if (data.file && fs.existsSync(data.file)) {
|
const targetFiles = sanitizeInputFiles(data?.file);
|
||||||
clipboard.writeBuffer(
|
|
||||||
'NSFilenamesPboardType',
|
if (!targetFiles.length) {
|
||||||
Buffer.from(plist.build([data.file]))
|
return false;
|
||||||
);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
try {
|
||||||
|
clipboard.writeBuffer(
|
||||||
|
'NSFilenamesPboardType',
|
||||||
|
Buffer.from(plist.build(targetFiles))
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
return copyFilesToWindowsClipboard(targetFiles);
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
import screenCapture from '@/core/screen-capture';
|
import screenCapture from '@/core/screen-capture';
|
||||||
import localConfig from '@/main/common/initLocalConfig';
|
import localConfig from '@/main/common/initLocalConfig';
|
||||||
import winPosition from './getWinPosition';
|
import winPosition from './getWinPosition';
|
||||||
|
import { uIOhook, UiohookKey } from 'uiohook-napi';
|
||||||
|
|
||||||
const registerHotKey = (mainWindow: BrowserWindow): void => {
|
const registerHotKey = (mainWindow: BrowserWindow): void => {
|
||||||
// 设置开机启动
|
// 设置开机启动
|
||||||
@@ -57,27 +58,43 @@ const registerHotKey = (mainWindow: BrowserWindow): void => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 显示主窗口
|
||||||
|
function mainWindowPopUp() {
|
||||||
|
const currentShow = mainWindow.isVisible() && mainWindow.isFocused();
|
||||||
|
if (currentShow) return mainWindow.hide();
|
||||||
|
const { x: wx, y: wy } = winPosition.getPosition();
|
||||||
|
mainWindow.setAlwaysOnTop(false);
|
||||||
|
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
||||||
|
mainWindow.focus();
|
||||||
|
mainWindow.setVisibleOnAllWorkspaces(false, {
|
||||||
|
visibleOnFullScreen: true,
|
||||||
|
});
|
||||||
|
mainWindow.setPosition(wx, wy);
|
||||||
|
mainWindow.show();
|
||||||
|
}
|
||||||
|
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
await setAutoLogin();
|
await setAutoLogin();
|
||||||
await setDarkMode();
|
await setDarkMode();
|
||||||
await setTheme();
|
await setTheme();
|
||||||
const config = await localConfig.getConfig();
|
const config = await localConfig.getConfig();
|
||||||
globalShortcut.unregisterAll();
|
globalShortcut.unregisterAll();
|
||||||
// 注册偏好快捷键
|
|
||||||
globalShortcut.register(config.perf.shortCut.showAndHidden, () => {
|
|
||||||
const currentShow = mainWindow.isVisible() && mainWindow.isFocused();
|
|
||||||
if (currentShow) return mainWindow.hide();
|
|
||||||
const { x: wx, y: wy } = winPosition.getPosition();
|
|
||||||
mainWindow.setAlwaysOnTop(false);
|
|
||||||
mainWindow.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
||||||
mainWindow.focus();
|
|
||||||
mainWindow.setVisibleOnAllWorkspaces(false, {
|
|
||||||
visibleOnFullScreen: true,
|
|
||||||
});
|
|
||||||
mainWindow.setPosition(wx, wy);
|
|
||||||
mainWindow.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// 注册偏好快捷键
|
||||||
|
// 处理显示/隐藏快捷键的注册
|
||||||
|
const doublePressShortcuts = ['Ctrl+Ctrl', 'Option+Option', 'Shift+Shift', 'Command+Command'];
|
||||||
|
const isDoublePressShortcut = doublePressShortcuts.includes(config.perf.shortCut.showAndHidden);
|
||||||
|
|
||||||
|
if (isDoublePressShortcut) {
|
||||||
|
// 双击快捷键(如 Ctrl+Ctrl)详见 uIOhookRegister 函数实现
|
||||||
|
} else {
|
||||||
|
// 注册普通快捷键(如 Ctrl+Space、F8 等)
|
||||||
|
globalShortcut.register(config.perf.shortCut.showAndHidden, () => {
|
||||||
|
mainWindowPopUp();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截图快捷键
|
||||||
globalShortcut.register(config.perf.shortCut.capture, () => {
|
globalShortcut.register(config.perf.shortCut.capture, () => {
|
||||||
screenCapture(mainWindow, (data) => {
|
screenCapture(mainWindow, (data) => {
|
||||||
data &&
|
data &&
|
||||||
@@ -93,6 +110,17 @@ const registerHotKey = (mainWindow: BrowserWindow): void => {
|
|||||||
// mainWindow.show();
|
// mainWindow.show();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加局部快捷键监听
|
||||||
|
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||||
|
if (input.key.toLowerCase() === 'w'
|
||||||
|
&& (input.control || input.meta) && !input.alt && !input.shift) {
|
||||||
|
event.preventDefault();
|
||||||
|
if (mainWindow && !mainWindow.isDestroyed()) {
|
||||||
|
mainWindow.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 注册自定义全局快捷键
|
// 注册自定义全局快捷键
|
||||||
config.global.forEach((sc) => {
|
config.global.forEach((sc) => {
|
||||||
if (!sc.key || !sc.value) return;
|
if (!sc.key || !sc.value) return;
|
||||||
@@ -101,9 +129,48 @@ const registerHotKey = (mainWindow: BrowserWindow): void => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
uIOhookRegister(mainWindowPopUp);
|
||||||
init();
|
init();
|
||||||
ipcMain.on('re-register', () => {
|
ipcMain.on('re-register', () => {
|
||||||
init();
|
init();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
export default registerHotKey;
|
export default registerHotKey;
|
||||||
|
|
||||||
|
function uIOhookRegister(callback: () => void) {
|
||||||
|
let lastModifierPress = Date.now();
|
||||||
|
uIOhook.on('keydown', async (uio_event) => {
|
||||||
|
const config = await localConfig.getConfig(); // 此处还有优化空间
|
||||||
|
|
||||||
|
if (
|
||||||
|
![
|
||||||
|
'Ctrl+Ctrl',
|
||||||
|
'Option+Option',
|
||||||
|
'Shift+Shift',
|
||||||
|
'Command+Command',
|
||||||
|
].includes(config.perf.shortCut.showAndHidden)
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 双击快捷键,如 Ctrl+Ctrl
|
||||||
|
const modifers = config.perf.shortCut.showAndHidden.split('+');
|
||||||
|
const showAndHiddenKeyStr = modifers.pop(); // Ctrl
|
||||||
|
const keyStr2uioKeyCode = {
|
||||||
|
Ctrl: UiohookKey.Ctrl,
|
||||||
|
Shift: UiohookKey.Shift,
|
||||||
|
Option: UiohookKey.Alt,
|
||||||
|
Command: UiohookKey.Comma,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (uio_event.keycode === keyStr2uioKeyCode[showAndHiddenKeyStr]) {
|
||||||
|
const currentTime = Date.now();
|
||||||
|
if (currentTime - lastModifierPress < 300) {
|
||||||
|
callback(); // 调用 mainWindowPopUp
|
||||||
|
}
|
||||||
|
lastModifierPress = currentTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
uIOhook.start();
|
||||||
|
}
|
||||||
|
|||||||
133
src/main/common/windowsClipboard.ts
Normal file
133
src/main/common/windowsClipboard.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
import { clipboard } from 'electron';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// 仅在 Windows 平台辅助操作剪贴板多文件格式。
|
||||||
|
type ClipboardExModule = typeof import('electron-clipboard-ex');
|
||||||
|
|
||||||
|
const DROPFILES_HEADER_SIZE = 20;
|
||||||
|
|
||||||
|
let clipboardExModule: ClipboardExModule | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Windows 平台专用:尝试加载第三方库 electron-clipboard-ex。
|
||||||
|
* 这个库能够调用系统底层接口写入“文件复制”数据,成功率更高。
|
||||||
|
* 其他系统无需加载它,因此这里做了“按需加载”的处理。
|
||||||
|
*/
|
||||||
|
const ensureClipboardEx = (): ClipboardExModule | null => {
|
||||||
|
if (process.platform !== 'win32') return null;
|
||||||
|
if (clipboardExModule) return clipboardExModule;
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line global-require, @typescript-eslint/no-var-requires
|
||||||
|
clipboardExModule = require('electron-clipboard-ex');
|
||||||
|
} catch {
|
||||||
|
clipboardExModule = null;
|
||||||
|
}
|
||||||
|
return clipboardExModule;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把一组文件路径变成 Windows 规定的文本格式。
|
||||||
|
* 要求:每个路径之间用单个空字符分隔,最后再额外放两个空字符,表示列表结束。
|
||||||
|
* Windows 资源管理器会按这个格式解析我们复制到剪贴板的文件。
|
||||||
|
*/
|
||||||
|
const buildWindowsFileListPayload = (files: string[]): Buffer =>
|
||||||
|
Buffer.from(`${files.join('\0')}\0\0`, 'utf16le');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造 CF_HDROP 专用的二进制数据。
|
||||||
|
* 这是 Windows 复制文件时的底层格式,前 20 字节是固定的结构头,
|
||||||
|
* 后面紧跟着具体的文件路径(由 buildWindowsFileListPayload 生成)。
|
||||||
|
* 只要把这个内容写入剪贴板,任何支持粘贴文件的程序都能理解。
|
||||||
|
*/
|
||||||
|
const buildWindowsFileDropBuffer = (files: string[]): Buffer => {
|
||||||
|
const payload = buildWindowsFileListPayload(files);
|
||||||
|
const header = Buffer.alloc(DROPFILES_HEADER_SIZE);
|
||||||
|
header.writeUInt32LE(DROPFILES_HEADER_SIZE, 0);
|
||||||
|
header.writeInt32LE(0, 4);
|
||||||
|
header.writeInt32LE(0, 8);
|
||||||
|
header.writeUInt32LE(0, 12);
|
||||||
|
header.writeUInt32LE(1, 16);
|
||||||
|
|
||||||
|
const result = Buffer.alloc(header.length + payload.length);
|
||||||
|
for (let i = 0; i < header.length; i += 1) {
|
||||||
|
result[i] = header[i];
|
||||||
|
}
|
||||||
|
for (let i = 0; i < payload.length; i += 1) {
|
||||||
|
result[header.length + i] = payload[i];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制/移动/创建快捷方式 等不同操作在 Windows 中对应不同的“意图”值。
|
||||||
|
* Preferred DropEffect 告诉系统:当前剪贴板数据应该以何种方式处理。
|
||||||
|
* 我们默认写入“copy”,相当于普通的复制粘贴。
|
||||||
|
*/
|
||||||
|
const buildDropEffectBuffer = (effect: 'copy' | 'move' | 'link' = 'copy') => {
|
||||||
|
const effectMap = {
|
||||||
|
copy: 1,
|
||||||
|
move: 2,
|
||||||
|
link: 4,
|
||||||
|
} as const;
|
||||||
|
const buffer = Buffer.alloc(4);
|
||||||
|
buffer.writeUInt32LE(effectMap[effect], 0);
|
||||||
|
return buffer;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 直接使用 Electron 内置 API 写入多种剪贴板格式。
|
||||||
|
* 步骤:
|
||||||
|
* 1. 写入二进制的 CF_HDROP(含头部与路径列表)
|
||||||
|
* 2. 写入纯文本形式的 FileNameW(备选格式)
|
||||||
|
* 3. 写入 Preferred DropEffect(告诉系统“这是复制”)
|
||||||
|
* 全部成功后,读取一次 CF_HDROP 的长度,确认剪贴板里确实有内容。
|
||||||
|
*/
|
||||||
|
const writeWindowsBuffers = (files: string[]): boolean => {
|
||||||
|
try {
|
||||||
|
clipboard.writeBuffer('CF_HDROP', buildWindowsFileDropBuffer(files));
|
||||||
|
clipboard.writeBuffer('FileNameW', buildWindowsFileListPayload(files));
|
||||||
|
clipboard.writeBuffer('Preferred DropEffect', buildDropEffectBuffer('copy'));
|
||||||
|
return clipboard.readBuffer('CF_HDROP').length > 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 如果项目中安装了 electron-clipboard-ex,我们优先使用它。
|
||||||
|
* 理由:该库通过原生方式与系统交互,兼容性往往优于 Electron 的 JS 层写入。
|
||||||
|
* 调用成功后,必要时读回文件列表做一次数量校验,确保复制的文件数量正确。
|
||||||
|
*/
|
||||||
|
const writeWithClipboardEx = (files: string[]): boolean => {
|
||||||
|
const clipboardEx = ensureClipboardEx();
|
||||||
|
if (!clipboardEx) return false;
|
||||||
|
try {
|
||||||
|
clipboardEx.writeFilePaths(files);
|
||||||
|
if (typeof clipboardEx.readFilePaths === 'function') {
|
||||||
|
const result = clipboardEx.readFilePaths();
|
||||||
|
return Array.isArray(result) && result.length === files.length;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对外暴露的唯一入口。
|
||||||
|
* 1. 先把所有路径换成 Windows 可识别的标准形式(path.normalize)。
|
||||||
|
* 2. 尝试使用 electron-clipboard-ex 写入,如果成功就结束。
|
||||||
|
* 3. 若第三方库不可用或失败,再退回 Electron 原生写入流程。
|
||||||
|
* 这一层屏蔽了所有细节,外部调用者只需传入字符串数组即可。
|
||||||
|
*/
|
||||||
|
export const copyFilesToWindowsClipboard = (files: string[]): boolean => {
|
||||||
|
const normalizedFiles = files
|
||||||
|
.map((filePath) => path.normalize(filePath))
|
||||||
|
.filter(Boolean);
|
||||||
|
if (!normalizedFiles.length) return false;
|
||||||
|
if (writeWithClipboardEx(normalizedFiles)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return writeWindowsBuffers(normalizedFiles);
|
||||||
|
};
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ const createPluginManager = (): any => {
|
|||||||
const initLocalStartPlugin = () => {
|
const initLocalStartPlugin = () => {
|
||||||
const result = ipcRenderer.sendSync('msg-trigger', {
|
const result = ipcRenderer.sendSync('msg-trigger', {
|
||||||
type: 'dbGet',
|
type: 'dbGet',
|
||||||
data: { id: PLUGIN_HISTORY },
|
data: { id: 'rubick-local-start-app' },
|
||||||
});
|
});
|
||||||
if (result && result.value) {
|
if (result && result.value) {
|
||||||
appList.value.push(...result.value);
|
appList.value.push(...result.value);
|
||||||
|
|||||||
Reference in New Issue
Block a user