fix: solve conflict

This commit is contained in:
徐志鹏 2021-07-06 11:26:36 +08:00
commit af5c033abe
25 changed files with 883 additions and 192 deletions

24
.github/ISSUE_TEMPLATE/bug-report.md vendored Normal file
View File

@ -0,0 +1,24 @@
---
name: Bug report
about: 报告一个bug
title: ''
labels: bug
assignees: ''
---
**描述一下这个bug**
清楚而简洁地描述了错误是什么
**复现方式**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**预期行为**
清晰简明地描述了您预期的发生。
**截图**
如果可以,请添加屏幕截图以帮助解释您的问题。

View File

@ -0,0 +1,10 @@
---
name: Development Problem
about: 任何开发建议、使用问题、交流学习都可以
title: ''
labels: help wanted
assignees: ''
---
## 任何开发建议、交流学习都可以

View File

@ -0,0 +1,17 @@
---
name: Feature request
about: 提交一个新特性/功能
title: ''
labels: feature
assignees: ''
---
**您的功能请求是否与问题相关? 请简单描述.**
清晰简明地描述问题是什么. Ex. I'm always frustrated when [...]
**请描述一下您想要的解决方案**
清晰简明地描述您想要发生的事情。
**描述你考虑过的替代方案**
清晰简洁地描述您所考虑的任何替代解决方案或功能。

View File

@ -2,12 +2,45 @@
<img align="center" src="https://user-images.githubusercontent.com/21073039/123022701-c3848180-d408-11eb-86ec-7727f355ea96.png" /> <img align="center" src="https://user-images.githubusercontent.com/21073039/123022701-c3848180-d408-11eb-86ec-7727f355ea96.png" />
</div> </div>
<h1 align= "center">Rubick</h1> # Rubick
<p>
<a href="https://github.com/clouDr-f2e/rubick/releases/tag/v0.0.1-beta">
<img alt="release" src="https://img.shields.io/badge/release-v0.0.1-brightgreen" />
</a>
<a href="https://github.com/clouDr-f2e/rubick/blob/master/LICENSE">
<img alt="npm" src="https://img.shields.io/github/license/clouDr-f2e/rubick" />
</a>
</p>
基于 electron 的工具箱,媲美 utools的开源插件已实现 utools 大部分的 API 能力,所以可以做到无缝适配 utools 开源的插件。 基于 electron 的工具箱,媲美 utools的开源插件已实现 utools 大部分的 API 能力,所以可以做到无缝适配 utools 开源的插件。
之所以做这个工具箱一方面是 utools 本身并未开源,但是公司内部的工具库又无法发布到 utools 插件中,所以为了既要享受 utools 生态又要有定制化需求,我们自己参考 utools 设计,做了 Rubick 之所以做这个工具箱一方面是 utools 本身并未开源,但是公司内部的工具库又无法发布到 utools 插件中,所以为了既要享受 utools 生态又要有定制化需求,我们自己参考 utools 设计,做了 Rubick
![image](https://user-images.githubusercontent.com/21073039/122888869-d6e60d00-d374-11eb-9fb9-2a6e541e389e.png) ## 安装包
* [Rubick Mac OS V0.0.1](https://github.com/clouDr-f2e/rubick/releases/download/v0.0.1-beta/rubick2-0.0.1.pkg)
## 支持能力
- [x] 支持 uTools 官方文档 90% API还在更新中很快可以做到 100%
- [x] 插件化支持 uTools 所有开源插件
- [x] 支持远程下载安装插件,支持插件开发者模式
- [x] 支持插件分离
- [x] 支持系统命令取色、截屏、帮助
- [x] 支持超级面板,长按右击呼出
- [x] 支持全局快捷键设置
- [x] 支持搜索本地已安装 app 或 偏好设置
- [ ] 支持 Windows
- [ ] 支持 Linux
![QQ20210705-210753](https://user-images.githubusercontent.com/21073039/124477360-8770f980-ddd6-11eb-8dc3-ba318223697f.gif)
## 使用问题
1. 目前 `Rubick` 插件市场 server 端还没有部署,所以目前看不到插件市场的插件。
2. 依赖于 `robotjs` dev 环境运行请在 `install` 后执行 `npm run rebuild`
## utools 插件支持 ## utools 插件支持
### plugin.json ### plugin.json

View File

@ -21,6 +21,7 @@
"asar": false, "asar": false,
"productName": "rubick2", "productName": "rubick2",
"appId": "com.example.yourapp2", "appId": "com.example.yourapp2",
"compression":"maximum",
"directories": { "directories": {
"output": "build" "output": "build"
}, },
@ -62,14 +63,20 @@
"download": "^8.0.0", "download": "^8.0.0",
"download-git-repo": "^3.0.2", "download-git-repo": "^3.0.2",
"electron-store": "^8.0.0", "electron-store": "^8.0.0",
"iohook": "^0.9.3",
"is-chinese": "^1.4.2",
"keycode": "^2.2.0", "keycode": "^2.2.0",
"marked": "^2.0.7", "marked": "^2.0.7",
"md5": "^2.3.0",
"mime-types": "^2.1.31",
"node-fetch": "^2.6.1", "node-fetch": "^2.6.1",
"osx-mouse": "git+https://github.com/Toinane/osx-mouse.git", "osx-mouse": "^2.0.0",
"puppeteer-core": "^10.0.0", "puppeteer-core": "^10.0.0",
"puppeteer-in-electron": "^3.0.3", "puppeteer-in-electron": "^3.0.3",
"query-string": "^7.0.0", "query-string": "^7.0.0",
"robotjs": "git+https://github.com/Toinane/robotjs.git", "robotjs": "git+https://github.com/Toinane/robotjs.git",
"request": "^2.88.2",
"request-promise": "^4.2.6",
"semver": "^7.3.5", "semver": "^7.3.5",
"sudo-prompt": "^9.2.1", "sudo-prompt": "^9.2.1",
"unzip": "^0.1.11", "unzip": "^0.1.11",
@ -108,6 +115,7 @@
"listr": "^0.14.3", "listr": "^0.14.3",
"mini-css-extract-plugin": "0.4.0", "mini-css-extract-plugin": "0.4.0",
"multispinner": "^0.2.1", "multispinner": "^0.2.1",
"node-abi": "^2.30.0",
"node-loader": "^0.6.0", "node-loader": "^0.6.0",
"react": "^17.0.2", "react": "^17.0.2",
"style-loader": "^0.21.0", "style-loader": "^0.21.0",
@ -121,5 +129,18 @@
"webpack-dev-server": "^3.1.4", "webpack-dev-server": "^3.1.4",
"webpack-hot-middleware": "^2.22.2", "webpack-hot-middleware": "^2.22.2",
"webpack-merge": "^4.1.3" "webpack-merge": "^4.1.3"
},
"iohook": {
"targets": [
"node-83",
"electron-85"
],
"platforms": [
"darwin"
],
"arches": [
"x64",
"ia32"
]
} }
} }

View File

@ -2,4 +2,5 @@ module.exports = () => ({
picker: require("./picker")(), picker: require("./picker")(),
separator: require("./separate")(), separator: require("./separate")(),
capture: require("./capture")(), capture: require("./capture")(),
superPanel: require("./superPanel")(),
}); });

View File

@ -0,0 +1,54 @@
const { BrowserWindow, ipcMain, app } = require("electron");
module.exports = () => {
let win;
let init = (mainWindow) => {
if (win === null || win === undefined) {
createWindow();
ipcMain.on('superPanel-hidden', () => {
win.hide();
});
ipcMain.on('superPanel-setSize', (e, height) => {
win.setSize(250, height);
});
ipcMain.on('superPanel-openPlugin', (e, args) => {
mainWindow.webContents.send('superPanel-openPlugin', args);
});
}
};
let createWindow = () => {
win = new BrowserWindow({
frame: false,
autoHideMenuBar: true,
width: 250,
height: 50,
show: false,
alwaysOnTop: true,
webPreferences: {
webSecurity: false,
enableRemoteModule: true,
backgroundThrottling: false,
nodeIntegration: true,
devTools: false,
},
});
win.loadURL(`file://${__static}/plugins/superPanel/index.html`);
win.once('ready-to-show', () => win.show());
win.on("closed", () => {
win = undefined;
});
// 打包后,失焦隐藏
win.on('blur', () => {
win.hide();
});
};
let getWindow = () => win;
return {
init: init,
getWindow: getWindow,
};
};

View File

@ -4,6 +4,7 @@ import {
BrowserWindow, BrowserWindow,
clipboard, clipboard,
Notification, Notification,
app,
} from 'electron'; } from 'electron';
import Api from './api'; import Api from './api';
import robot from 'robotjs'; import robot from 'robotjs';
@ -11,7 +12,9 @@ import './config';
const browsers = require("../browsers")(); const browsers = require("../browsers")();
const mouseEvents = require("osx-mouse"); const mouseEvents = require("osx-mouse");
const {picker, separator} = browsers; const {picker, separator, superPanel} = browsers;
// 需要在超级面板展示的插件
let optionPlugin = [];
let closePicker = (newColor) => { let closePicker = (newColor) => {
if (picker.getWindow()) { if (picker.getWindow()) {
@ -34,18 +37,75 @@ function registerShortCut(mainWindow) {
}); });
} }
export default function init(mainWindow) { const getSelectedText = () => {
const mouseTrack = mouseEvents(); return new Promise((resolve) => {
let down_time = 0; const lastText = clipboard.readText('clipboard');
mouseTrack.on('right-down', () => {
down_time = Date.now(); const platform = process.platform;
if (platform === 'darwin') {
robot.keyTap('c', 'command');
} else {
robot.keyTap('c', 'control');
}
setTimeout(() => {
const text = clipboard.readText('clipboard') || ''
const fileUrl = clipboard.read('public.file-url');
clipboard.writeText(lastText);
resolve({
text,
fileUrl
})
}, 300);
}) })
mouseTrack.on('right-up', () => { }
if ((Date.now() - down_time) > 1000) {
new Notification({ title: 'Rubick 通知', body: '长按了' }).show(); export default function init(mainWindow) {
ipcMain.on('optionPlugin', (e, args) => {
optionPlugin = args;
});
ipcMain.on('right-down', async (e) => {
const copyResult = await getSelectedText();
let win = superPanel.getWindow();
if (win) {
win.webContents.send('trigger-super-panel', {
...copyResult,
optionPlugin: optionPlugin.plugins,
});
} else {
superPanel.init(mainWindow);
win = superPanel.getWindow();
win.once('ready-to-show', () => {
win.webContents.send('trigger-super-panel', {
...copyResult,
optionPlugin: optionPlugin.plugins,
});
});
}
const pos = robot.getMousePos();
win.setPosition(parseInt(pos.x), parseInt(pos.y));
win.show();
});
// 注册快捷键
registerShortCut(mainWindow);
// 设置开机启动
const config = global.opConfig.get();
app.setLoginItemSettings({
openAtLogin: config.perf.common.start,
openAsHidden: true,
});
mainWindow.once("ready-to-show", () => {
// 非隐藏式启动需要显示主窗口
if (!app.getLoginItemSettings().wasOpenedAsHidden) {
mainWindow.show();
} }
}); });
registerShortCut(mainWindow);
ipcMain.on('re-register', (event, arg) => { ipcMain.on('re-register', (event, arg) => {
registerShortCut(mainWindow); registerShortCut(mainWindow);
@ -55,10 +115,12 @@ export default function init(mainWindow) {
mainWindow.setSize(arg.width || 800, arg.height); mainWindow.setSize(arg.width || 800, arg.height);
}); });
// 打包后,失焦隐藏
mainWindow.on('blur', () => { mainWindow.on('blur', () => {
mainWindow.hide(); app.isPackaged && mainWindow.hide();
}); });
// 响应 preload.js 事件
ipcMain.on('msg-trigger', async (event, arg) => { ipcMain.on('msg-trigger', async (event, arg) => {
const window = arg.winId ? BrowserWindow.fromId(arg.winId) : mainWindow const window = arg.winId ? BrowserWindow.fromId(arg.winId) : mainWindow
const operators = arg.type.split('.'); const operators = arg.type.split('.');
@ -70,6 +132,7 @@ export default function init(mainWindow) {
event.sender.send(`msg-back-${arg.type}`, data); event.sender.send(`msg-back-${arg.type}`, data);
}); });
// 窗口分离
ipcMain.on('new-window', (event, arg) => { ipcMain.on('new-window', (event, arg) => {
const opts = { const opts = {
...arg, ...arg,
@ -78,6 +141,7 @@ export default function init(mainWindow) {
separator.init(opts); separator.init(opts);
}); });
// 拾色器
ipcMain.on('start-picker', () => { ipcMain.on('start-picker', () => {
const mouseTrack = mouseEvents(); const mouseTrack = mouseEvents();
picker.init(); picker.init();

View File

@ -20,9 +20,16 @@ let defaultConfig = {
search: true, search: true,
} }
}, },
superPanel: {
baiduAPI: {
key: '',
appid: '',
},
mouseDownTime: 500
},
global: []
} }
} }
global.opConfig = { global.opConfig = {
config: null, config: null,
get() { get() {
@ -31,6 +38,11 @@ global.opConfig = {
if (!opConfig.config) { if (!opConfig.config) {
opConfig.config = JSON.parse(fs.readFileSync(configPath) || JSON.stringify(defaultConfig[platform])); opConfig.config = JSON.parse(fs.readFileSync(configPath) || JSON.stringify(defaultConfig[platform]));
} }
// 重置
if (!opConfig.config.perf || !opConfig.config.superPanel || !opConfig.config.global) {
opConfig.config = defaultConfig[platform];
fs.writeFileSync(configPath, JSON.stringify(opConfig.config));
}
return opConfig.config; return opConfig.config;
} catch (e) { } catch (e) {
opConfig.config = defaultConfig[platform] opConfig.config = defaultConfig[platform]
@ -38,7 +50,6 @@ global.opConfig = {
} }
}, },
set(key, value) { set(key, value) {
console.log(opConfig.config);
opConfig.config[key] = value; opConfig.config[key] = value;
fs.writeFileSync(configPath, JSON.stringify(opConfig.config)); fs.writeFileSync(configPath, JSON.stringify(opConfig.config));
} }

View File

@ -11,7 +11,8 @@ const {capture} = require("./browsers")();
if (process.env.NODE_ENV !== 'development') { if (process.env.NODE_ENV !== 'development') {
global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\') global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
} }
// to fix https://github.com/electron/electron/issues/18397
app.allowRendererProcessReuse = false;
let mainWindow let mainWindow
const winURL = process.env.NODE_ENV === 'development' const winURL = process.env.NODE_ENV === 'development'
? `http://localhost:9080` ? `http://localhost:9080`

View File

@ -19,6 +19,10 @@
@change="e => search({value: e.target.value})" @change="e => search({value: e.target.value})"
:value="searchValue" :value="searchValue"
:maxLength="selected && selected.key !== 'plugin-container' ? 0 : 1000" :maxLength="selected && selected.key !== 'plugin-container' ? 0 : 1000"
@keydown.down="(e) => changeCurrent(1)"
@keydown.up="() => changeCurrent(-1)"
@keypress.enter="(e) => targetSearch({value: e.target.value, type: 'enter'})"
@keypress.space="(e) => targetSearch({value: e.target.value, type: 'space'})"
> >
<div @click="goMenu" class="suffix-tool" slot="suffix"> <div @click="goMenu" class="suffix-tool" slot="suffix">
<a-icon v-show="selected && selected.key === 'plugin-container'" class="icon-more" type="more" /> <a-icon v-show="selected && selected.key === 'plugin-container'" class="icon-more" type="more" />
@ -29,7 +33,12 @@
</a-input> </a-input>
<div class="options" v-show="showOptions"> <div class="options" v-show="showOptions">
<a-list item-layout="horizontal" :data-source="options"> <a-list item-layout="horizontal" :data-source="options">
<a-list-item @click="() => item.click($router)" class="op-item" slot="renderItem" slot-scope="item, index"> <a-list-item
@click="() => item.click($router)"
:class="currentSelect === index ? 'active op-item' : 'op-item'"
slot="renderItem"
slot-scope="item, index"
>
<a-list-item-meta <a-list-item-meta
:description="item.desc" :description="item.desc"
> >
@ -51,8 +60,10 @@
<a-input <a-input
:placeholder="subPlaceHolder" :placeholder="subPlaceHolder"
class="sub-input" class="sub-input"
@change="(e) => onSearch({value: e.target.value, searchType: $route.query.searchType})" @change="(e) => search({value: e.target.value, searchType: $route.query.searchType})"
:value="searchValue" :value="searchValue"
@keypress.enter="(e) => targetSearch({value: e.target.value, type: 'enter'})"
@keypress.space="(e) => targetSearch({value: e.target.value, type: 'space'})"
></a-input> ></a-input>
</div> </div>
@ -68,7 +79,7 @@
import {mapActions, mapMutations, mapState} from "vuex"; import {mapActions, mapMutations, mapState} from "vuex";
import {ipcRenderer, remote} from "electron"; import {ipcRenderer, remote} from "electron";
import {getWindowHeight, debounce} from "./assets/common/utils"; import {getWindowHeight, debounce} from "./assets/common/utils";
const opConfig = remote.getGlobal('opConfig');
const {Menu, MenuItem} = remote; const {Menu, MenuItem} = remote;
export default { export default {
@ -77,18 +88,28 @@ export default {
searchType: this.$route.query.searchType ? 'subWindow' : '', searchType: this.$route.query.searchType ? 'subWindow' : '',
query: this.$route.query, query: this.$route.query,
searchFn: null, searchFn: null,
config: opConfig.get(),
currentSelect: 0,
} }
}, },
mounted() { mounted() {
//
ipcRenderer.send('init-shortcut');
ipcRenderer.on('init-rubick', this.closeTag); ipcRenderer.on('init-rubick', this.closeTag);
ipcRenderer.on('new-window', this.newWindow); ipcRenderer.on('new-window', this.newWindow);
//
ipcRenderer.on('superPanel-openPlugin', (e, args) => {
this.openPlugin({
cmd: args.cmd,
plugin: args.plugin,
feature: args.feature,
router: this.$router,
payload: args.data,
})
});
const searchNd = document.getElementById('search'); const searchNd = document.getElementById('search');
searchNd && searchNd.addEventListener('keydown', this.checkNeedInit) searchNd && searchNd.addEventListener('keydown', this.checkNeedInit)
}, },
methods: { methods: {
...mapActions('main', ['onSearch', 'showMainUI']), ...mapActions('main', ['onSearch', 'showMainUI', 'openPlugin']),
...mapMutations('main', ['commonUpdate']), ...mapMutations('main', ['commonUpdate']),
search(v) { search(v) {
if (!this.searchFn) { if (!this.searchFn) {
@ -96,6 +117,26 @@ export default {
} }
this.searchFn(v); this.searchFn(v);
}, },
targetSearch(action) {
//
if((this.selected && this.selected.key === 'plugin-container') || this.searchType === 'subWindow') {
const webview = document.getElementById('webview');
if (action.type === 'space') {
if (this.config.perf.common.space) {
webview.send('msg-back-setSubInput', this.searchValue);
}
return;
}
webview.send('msg-back-setSubInput', this.searchValue);
} else if (this.showOptions) {
const item = this.options[this.currentSelect]
item.click(this.$router);
}
},
changeCurrent(index) {
this.currentSelect = this.currentSelect + index;
},
renderTitle(title) { renderTitle(title) {
const result = title.split(this.searchValue); const result = title.split(this.searchValue);
return `<div>${result[0]}<span style="color: red">${this.searchValue}</span>${result[1]}</div>` return `<div>${result[0]}<span style="color: red">${this.searchValue}</span>${result[1]}</div>`
@ -234,6 +275,9 @@ export default {
max-height: 500px; max-height: 500px;
overflow: auto; overflow: auto;
background: #fafafa; background: #fafafa;
&.active {
background: #DEE2E8;
}
} }
} }
} }

View File

@ -4,7 +4,7 @@ import fs from 'fs';
import process from 'child_process'; import process from 'child_process';
import Store from 'electron-store'; import Store from 'electron-store';
import downloadFile from 'download'; import downloadFile from 'download';
import {nativeImage} from 'electron'; import {nativeImage, ipcRenderer} from 'electron';
import {APP_FINDER_PATH} from './constans'; import {APP_FINDER_PATH} from './constans';
import {getlocalDataFile} from "../../../main/common/utils"; import {getlocalDataFile} from "../../../main/common/utils";
@ -57,6 +57,19 @@ async function downloadZip(downloadRepoUrl, name) {
const sysFile = { const sysFile = {
savePlugins(plugins) { savePlugins(plugins) {
ipcRenderer.send('optionPlugin', {
plugins: plugins.filter((plugin) => {
let hasOption = false;
plugin.features.forEach(fe => {
fe.cmds.forEach(cmd => {
if (cmd.type) {
hasOption = true;
}
})
});
return hasOption;
})
});
store.set('user-plugins', plugins); store.set('user-plugins', plugins);
}, },
getUserPlugins() { getUserPlugins() {
@ -72,7 +85,7 @@ const sysFile = {
} }
function mergePlugins(plugins) { function mergePlugins(plugins) {
return [ const result = [
...plugins, ...plugins,
...SYSTEM_PLUGINS.map(plugin => { ...SYSTEM_PLUGINS.map(plugin => {
return { return {
@ -81,8 +94,18 @@ function mergePlugins(plugins) {
sourceFile: '', sourceFile: '',
type: 'system', type: 'system',
} }
}), })
] ]
return result.filter((item, i) => {
let targetIndex;
result.forEach((tg, j) => {
if (tg.tag === item.tag && tg.type === 'system') {
targetIndex = j
}
});
return i === targetIndex;
});
} }
function find(p, target = 'plugin.json') { function find(p, target = 'plugin.json') {

View File

@ -18,19 +18,14 @@ export default {
32: 'space', 32: 'space',
33: 'page up', 33: 'page up',
34: 'page down', 34: 'page down',
35: 'end', 35: 'End',
36: 'home', 36: 'Home',
37: 'left arrow', 37: 'Left',
38: 'up arrow', 38: 'Up',
39: 'right arrow', 39: 'Right',
40: 'down arrow', 40: 'Down',
41: 'select', 45: 'Insert',
42: 'print', 46: 'Delete',
43: 'execute',
44: 'Print Screen',
45: 'insert',
46: 'delete',
47: 'help',
48: '0', 48: '0',
49: '1', 49: '1',
50: '2', 50: '2',
@ -41,130 +36,55 @@ export default {
55: '7', 55: '7',
56: '8', 56: '8',
57: '9', 57: '9',
58: ':', 65: 'A',
59: 'semicolon (firefox), equals', 66: 'B',
60: '<', 67: 'C',
61: 'equals (firefox)', 68: 'D',
63: 'ß', 69: 'E',
64: '@ (firefox)', 70: 'F',
65: 'a', 71: 'G',
66: 'b', 72: 'H',
67: 'c', 73: 'I',
68: 'd', 74: 'J',
69: 'e', 75: 'K',
70: 'f', 76: 'L',
71: 'g', 77: 'M',
72: 'h', 78: 'N',
73: 'i', 79: 'O',
74: 'j', 80: 'P',
75: 'k', 81: 'Q',
76: 'l', 82: 'R',
77: 'm', 83: 'S',
78: 'n', 84: 'T',
79: 'o', 85: 'U',
80: 'p', 86: 'V',
81: 'q', 87: 'W',
82: 'r', 88: 'X',
83: 's', 89: 'Y',
84: 't', 90: 'Z',
85: 'u', 112: 'F1',
86: 'v', 113: 'F2',
87: 'w', 114: 'F3',
88: 'x', 115: 'F4',
89: 'y', 116: 'F5',
90: 'z', 117: 'F6',
91: 'Windows Key / Left ⌘ / Chromebook Search key', 118: 'F7',
92: 'right window key', 119: 'F8',
93: 'Windows Menu / Right ⌘', 120: 'F9',
95: 'sleep', 121: 'F10',
96: 'numpad 0', 122: 'F11',
97: 'numpad 1', 123: 'F12',
98: 'numpad 2', 186: ';',
99: 'numpad 3', 187: '=',
100: 'numpad 4', 188: ',',
101: 'numpad 5', 189: '-',
102: 'numpad 6', 190: '.',
103: 'numpad 7', 191: '/',
104: 'numpad 8', 192: '`',
105: 'numpad 9', 219: '[',
106: 'multiply', 220: '\\',
107: 'add', 221: ']',
108: 'numpad period (firefox)', 222: "'",
109: 'subtract',
110: 'decimal point',
111: 'divide',
112: 'f1',
113: 'f2',
114: 'f3',
115: 'f4',
116: 'f5',
117: 'f6',
118: 'f7',
119: 'f8',
120: 'f9',
121: 'f10',
122: 'f11',
123: 'f12',
124: 'f13',
125: 'f14',
126: 'f15',
127: 'f16',
128: 'f17',
129: 'f18',
130: 'f19',
131: 'f20',
132: 'f21',
133: 'f22',
134: 'f23',
135: 'f24',
136: 'f25',
137: 'f26',
138: 'f27',
139: 'f28',
140: 'f29',
141: 'f30',
142: 'f31',
143: 'f32',
144: 'num lock',
145: 'scroll lock',
151: 'airplane mode',
160: '^',
161: '!',
162: '؛ (arabic semicolon)',
163: '#',
164: '$',
165: 'ù',
166: 'page backward',
167: 'page forward',
168: 'refresh',
169: 'closing paren (AZERTY)',
170: '*',
171: '~ + * key',
172: 'home key',
173: 'minus (firefox), mute/unmute',
174: 'decrease volume level',
175: 'increase volume level',
176: 'next',
177: 'previous',
178: 'stop',
179: 'play/pause',
180: 'e-mail',
181: 'mute/unmute (firefox)',
182: 'decrease volume level (firefox)',
183: 'increase volume level (firefox)',
186: 'semi-colon / ñ',
187: 'equal sign',
188: 'comma',
189: 'dash',
190: 'period',
191: 'forward slash / ç',
192: 'grave accent / ñ / æ / ö',
193: '?, / or °',
194: 'numpad period (chrome)',
219: 'open bracket',
220: 'back slash',
221: 'close bracket / å',
222: 'single quote / ø / ä',
223: '`', 223: '`',
224: 'left or right ⌘ key (firefox)', 224: 'left or right ⌘ key (firefox)',
225: 'altgr', 225: 'altgr',

View File

@ -1,6 +1,7 @@
import Vue from 'vue' import Vue from 'vue'
import axios from 'axios' import axios from 'axios'
import ioHook from 'iohook';
import {ipcRenderer, remote} from 'electron';
import App from './App' import App from './App'
import router from './router' import router from './router'
import store from './store' import store from './store'
@ -8,9 +9,11 @@ import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/antd.css'; import 'ant-design-vue/dist/antd.css';
const opConfig = remote.getGlobal('opConfig');
if (!process.env.IS_WEB) Vue.use(require('vue-electron')) if (!process.env.IS_WEB) Vue.use(require('vue-electron'))
Vue.http = Vue.prototype.$http = axios Vue.http = Vue.prototype.$http = axios
Vue.config.productionTip = false Vue.config.productionTip = false;
Vue.use(Antd); Vue.use(Antd);
@ -20,4 +23,24 @@ new Vue({
router, router,
store, store,
template: '<App/>' template: '<App/>'
}).$mount('#app') }).$mount('#app');
ioHook.start(false);
let down_time = 0;
let isPress = false;
ioHook.on('mousedown', (e) => {
if (e.button === 1) return;
isPress = true;
down_time = Date.now();
const config = opConfig.get();
setTimeout(async () => {
if (isPress) {
ipcRenderer.send('right-down');
}
}, config.superPanel.mouseDownTime);
})
ioHook.on('mouseup', (e) => {
if(e.button === 1) return;
isPress = false;
});

View File

@ -30,8 +30,8 @@ export default {
mounted() { mounted() {
this.webview = document.querySelector('webview'); this.webview = document.querySelector('webview');
this.webview.addEventListener('dom-ready', () => { this.webview.addEventListener('dom-ready', () => {
this.webview.send('onPluginReady', this.$route.query); this.webview.send('onPluginReady', this.pluginInfo);
this.webview.send('onPluginEnter', this.$route.query); this.webview.send('onPluginEnter', this.pluginInfo);
}); });
this.setSubPlaceHolder('Hi, Rubick'); this.setSubPlaceHolder('Hi, Rubick');
this.webview.addEventListener('ipc-message', (event) => { this.webview.addEventListener('ipc-message', (event) => {
@ -74,22 +74,19 @@ export default {
...mapMutations('main', ['setSubPlaceHolder', 'commonUpdate']), ...mapMutations('main', ['setSubPlaceHolder', 'commonUpdate']),
}, },
beforeRouteUpdate() { beforeRouteUpdate() {
this.path = `File://${this.$route.query.sourceFile}` this.path = `File://${this.$route.query.sourceFile}`;
console.log(this.pluginInfo)
this.webview.send('onPluginEnter', this.pluginInfo);
}, },
beforeDestroy() { beforeDestroy() {
const webview = document.querySelector('webview'); const webview = document.querySelector('webview');
webview && webview.send('onPluginOut', this.$route.query) webview && webview.send('onPluginOut', this.$route.query)
}, },
computed: { computed: {
...mapState('main', ['searchValue', 'devPlugins']), ...mapState('main', ['searchValue', 'devPlugins', 'pluginInfo']),
pluginDetail() { pluginDetail() {
return (this.devPlugins.filter(plugin => plugin.name === this.query.name)[0] || {}).features return (this.devPlugins.filter(plugin => plugin.name === this.query.name)[0] || {}).features
}, },
},
watch: {
searchValue() {
this.webview.send('msg-back-setSubInput', this.searchValue);
}
} }
} }
</script> </script>

View File

@ -6,14 +6,11 @@
偏好设置 偏好设置
</a-menu-item> </a-menu-item>
<a-menu-item :key="1"> <a-menu-item :key="1">
本地启动文件 超级面板
</a-menu-item> </a-menu-item>
<a-menu-item :key="2"> <a-menu-item :key="2">
全局快捷键 全局快捷键
</a-menu-item> </a-menu-item>
<a-menu-item :key="3">
所有关键字
</a-menu-item>
</a-menu> </a-menu>
<div class="settings-detail"> <div class="settings-detail">
<div v-if="currentSelect[0] === 0"> <div v-if="currentSelect[0] === 0">
@ -47,6 +44,78 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="currentSelect[0] === 1">
<div class="setting-item">
<div class="title">弹出面板</div>
<a-select value="mouseRight" style="width: 200px" disabled>
<a-select-option value="mouseRight">长按鼠标右键</a-select-option>
</a-select>
</div>
<div class="setting-item">
<div class="title">长按以下设置的毫秒响应</div>
<a-slider :step="100" v-model:value="config.superPanel.mouseDownTime" :min="200" :max="1000" />
</div>
<div class="setting-item">
<div class="title">百度搜索配置</div>
<a-form :label-col="{ span: 3 }" :wrapper-col="{ span: 14 }">
<a-form-item label="appid">
<a-input v-model:value="config.superPanel.baiduAPI.appid" />
</a-form-item>
<a-form-item label="key">
<a-input v-model:value="config.superPanel.baiduAPI.key" />
</a-form-item>
</a-form>
</div>
<img width="100%" src="https://static.91jkys.com/upload/202107/02/fa4a5c614234409fb32ddda70cb900aa.jpg" />
</div>
<div v-if="currentSelect[0] === 2">
<a-collapse>
<a-collapse-panel key="1" header="说明及示例">
<div>按下快捷键自动搜索对应关键字当关键字结果完全匹配且结果唯一时会直接指向该功能</div>
<h3 style="margin-top: 10px;">示例</h3>
<a-divider style="margin: 5px 0;" />
<a-list item-layout="horizontal" :data-source="examples">
<a-list-item slot="renderItem" slot-scope="item, index">
<a-list-item-meta
:description="item.desc"
>
<div slot="title">{{ item.title }}</div>
</a-list-item-meta>
</a-list-item>
</a-list>
</a-collapse-panel>
</a-collapse>
<div class="feature-container">
<div class="keywords item">
<div>快捷键</div>
<a-tooltip placement="top" trigger="click">
<template slot="title">
<span>先按功能键CtrlShiftAltOptionCommand再按其他普通键或按 F1-F12 单键</span>
</template>
<div
v-for="(index, item) in config.global"
class="value"
tabIndex=-1
@keydown="(e) => changeGlobalKey(e, index)"
>
{{ item.key }}
</div>
</a-tooltip>
</div>
<div class="short-cut item">
<div>功能关键字</div>
<a-input
:value="item.value"
v-for="(index, item) in config.global"
class="value"
:disabled="!item.key"
@change="(e) => changeGlobalValue(index, e.target.value)"
/>
</div>
</div>
<div @click="addConfig" class="add-global"> + 新增全局快捷功能</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@ -63,7 +132,17 @@ export default {
data() { data() {
return { return {
currentSelect: [0], currentSelect: [0],
config: JSON.parse(JSON.stringify(opConfig.get())) config: JSON.parse(JSON.stringify(opConfig.get())),
examples: [
{
title: '快捷键 「 Alt + W」 关键字 「 微信」',
desc: '按下Alt + W 直接打开本地微信应用'
},
{
title: '快捷键 「 Alt + Q」 关键字 「 取色」',
desc: '按下Alt + Q 直接打开屏幕取色功能'
}
]
} }
}, },
methods: { methods: {
@ -89,6 +168,34 @@ export default {
this.config.perf.shortCut[key] = compose; this.config.perf.shortCut[key] = compose;
change = true; change = true;
} }
},
addConfig() {
this.config.global.push({
key: '',
value: ''
});
},
changeGlobalKey(e, index) {
let compose;
if(e.altKey && e.keyCode !== 18){
compose = `Option+${keycodes[e.keyCode].toUpperCase()}`;
}
if(e.ctrlKey && e.keyCode !== 17){
compose = `Ctrl+${keycodes[e.keyCode].toUpperCase()}`;
}
if(e.shiftKey && e.keyCode !== 16){
compose = `Shift+${keycodes[e.keyCode].toUpperCase()}`;
}
if(e.metaKey && e.keyCode !== 93){
compose = `Command+${keycodes[e.keyCode].toUpperCase()}`;
}
if (compose) {
this.$set(this.config.global[index], 'key', compose);
}
},
changeGlobalValue(index, value) {
this.$set(this.config.global[index], 'value', value);
} }
}, },
watch: { watch: {
@ -96,6 +203,7 @@ export default {
deep: true, deep: true,
handler() { handler() {
opConfig.set('perf', this.config.perf); opConfig.set('perf', this.config.perf);
opConfig.set('superPanel', this.config.superPanel);
ipcRenderer.send('re-register'); ipcRenderer.send('re-register');
} }
} }
@ -111,14 +219,19 @@ export default {
height: 100%; height: 100%;
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
background: #F8FAFC; background: #fff;
} }
.settings-detail { .settings-detail {
padding: 20px; padding: 20px;
box-sizing: border-box; box-sizing: border-box;
flex: 1; flex: 1;
overflow: auto;
height: 100%;
.setting-item { .setting-item {
margin-bottom: 20px; margin-bottom: 20px;
.ant-form-item {
margin-bottom: 0;
}
.title { .title {
color: #6C9FE2; color: #6C9FE2;
font-size: 15px; font-size: 15px;
@ -146,5 +259,35 @@ export default {
} }
} }
} }
.feature-container {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
font-size: 14px;
.item {
flex: 1;
}
.short-cut {
margin-left: 20px;
}
.value {
text-align: center;
border: 1px solid #ddd;
color: #6C9FE2;
font-size: 14px;
height: 24px;
font-weight: lighter;
margin-top: 10px;
}
}
.add-global {
color: #6C9FE2;
margin-top: 20px;
width: 100%;
text-align: center;
cursor: pointer;
}
} }
</style> </style>

View File

@ -22,6 +22,7 @@ const state = {
searchValue: '', searchValue: '',
devPlugins: mergePlugins(sysFile.getUserPlugins() || []), devPlugins: mergePlugins(sysFile.getUserPlugins() || []),
subPlaceHolder: '', subPlaceHolder: '',
pluginInfo: {},
} }
const mutations = { const mutations = {
@ -29,7 +30,7 @@ const mutations = {
Object.keys(payload).forEach((key) => { Object.keys(payload).forEach((key) => {
state[key] = payload[key]; state[key] = payload[key];
if (key === 'devPlugins') { if (key === 'devPlugins') {
sysFile.savePlugins(payload) sysFile.savePlugins(payload[key])
} }
}); });
}, },
@ -96,9 +97,9 @@ const actions = {
return; return;
} }
const value = paylpad.value; const value = paylpad.value;
// 在插件界面 // 在插件界面不触发其他功能
if((state.selected && state.selected.key === 'plugin-container') || paylpad.searchType === 'subWindow') { if((state.selected && state.selected.key === 'plugin-container') || paylpad.searchType === 'subWindow') {
commit('commonUpdate', {searchValue: value}) commit('commonUpdate', {searchValue: value});
return; return;
} }
const fileUrl = clipboard.read('public.file-url').replace('file://', ''); const fileUrl = clipboard.read('public.file-url').replace('file://', '');
@ -236,7 +237,7 @@ const actions = {
devPlugins: [pluginConfig, ...state.devPlugins], devPlugins: [pluginConfig, ...state.devPlugins],
}); });
}, },
openPlugin({commit}, {cmd, plugin, feature, router}) { openPlugin({commit}, {cmd, plugin, feature, router, payload}) {
if (plugin.type === 'app') { if (plugin.type === 'app') {
execSync(plugin.action); execSync(plugin.action);
commit('commonUpdate', { commit('commonUpdate', {
@ -253,7 +254,7 @@ const actions = {
commit('commonUpdate', { commit('commonUpdate', {
selected: { selected: {
key: 'plugin-container', key: 'plugin-container',
name: cmd, name: cmd.label ? cmd.label : cmd,
icon: 'image://' + path.join(plugin.sourceFile, `../${plugin.logo}`), icon: 'image://' + path.join(plugin.sourceFile, `../${plugin.logo}`),
}, },
searchValue: '', searchValue: '',
@ -277,10 +278,20 @@ const actions = {
}); });
return; return;
} }
commit('commonUpdate', {
pluginInfo: {
cmd,
...plugin,
detail: feature,
payload,
}
});
router.push({ router.push({
path: '/plugin', path: '/plugin',
query: { query: {
...plugin, ...plugin,
_modify: Date.now(),
detail: JSON.stringify(feature) detail: JSON.stringify(feature)
}, },
}) })

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 345 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 309 B

View File

@ -0,0 +1,104 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<script src="./vue.min.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
[v-cloak]{
display: none;
}
#app {
-webkit-app-region: drag;
width: 100%;
box-sizing: border-box;
background: #fff;
overflow: hidden;
}
.top {
width: 100%;
height: 50px;
display: flex;
align-items: center;
justify-content: space-between;
border-bottom: 1px #ddd dashed;
padding: 0 10px;
box-sizing: border-box;
}
.top .img {
width: 32px;
height: 32px;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #314659;
}
.top .img img {
width: 22px;
height: 22px;
}
.top .text {
color: #999;
font-size: 14px;
}
.translate {
width: 100%;
background: #F7F7F9;
font-size: 12px;
color: #555;
box-sizing: border-box;
padding: 5px;
}
.trans-item {
line-height: 20px;
}
.options-item {
border-bottom: 1px #ddd dashed;
height: 35px;
line-height: 35px;
padding: 0 10px;
color: #333;
font-size: 14px;
cursor: pointer;
}
.options-item:last-child {
border-bottom: none !important;
}
.icon {
width: 20px;
height: 20px;
margin-right: 5px;
}
.select-item {
display: flex;
align-items: center;
}
</style>
<script src="./index.js" type="module"></script>
<body>
<div id="app" v-cloak>
<div class="top" @click="openMainWindow">
<span class="img"><img src="./assets/logo.png" /></span>
<span class="text" v-if="selectData.text && selectData.text.length">选择的文本 {{selectData.text.length}} 个</span>
</div>
<div class="translate" v-if="selectData.translate">
<div class="trans-item" v-for="trans in selectData.translate">
<div>{{trans.src}}</div>
<div>n. {{trans.dst}}</div>
</div>
</div>
<div @click="() => commonClick(op, selectData.fileUrl)" class="options-item" v-for="op in targetOptions">
<div class="select-item">
<img class="icon" :src="op.icon" />
{{op.name}}
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,185 @@
const {ipcRenderer, nativeImage, remote, clipboard} = require('electron')
const md5 = require("md5");
const rp = require("request-promise");
const isChinese = require('is-chinese');
const path = require('path');
const fs = require('fs');
const { spawn } = require ('child_process');
const mineType = require("mime-types");
const opConfig = remote.getGlobal('opConfig');
new Vue({
el: '#app',
data: {
code: '',
current: {},
selectData: {
translate: {},
optionPlugin: [],
},
options: {
translate: [],
common: [
{
type: 'default',
name: '终端中打开',
icon: './assets/terminal.png',
click: (fileUrl) => {
spawn('open', [ '-a', 'Terminal', fileUrl ]);
}
},
{
type: 'default',
name: '新建文件',
icon: './assets/new.png',
click: (fileUrl) => {
remote.dialog.showSaveDialog({
title: "请选择要保存的文件名",
buttonLabel: "保存",
defaultPath: fileUrl.replace('file://', ''),
showsTagField: false,
nameFieldLabel: '',
}).then(result => {
fs.writeFileSync(result.filePath, '');
});
}
},
{
type: 'default',
name: '复制当前路径',
icon: './assets/link.png',
click: (fileUrl) => {
clipboard.writeText(fileUrl.replace('file://', ''))
}
}
],
selected: [
{
type: 'default',
name: '复制当前路径',
icon: './assets/link.png',
click: (fileUrl) => {
clipboard.writeText(fileUrl.replace('file://', ''))
}
}
]
},
targetOptions: [],
},
created() {
// 简单唤起超级面板
ipcRenderer.on('trigger-super-panel', (e, args) => {
this.selectData = args;
const ext = path.extname(this.selectData.fileUrl);
// 剪切板只有文本时,显示翻译
if (!this.selectData.fileUrl) {
const word = this.selectData.text;
const isCh = isChinese(word);
this.translate(word, isCh ? 'en' : 'zh');
this.targetOptions = this.options.translate;
} else if (!ext || path.parse(this.selectData.fileUrl).base === 'Desktop') {
// 如果在桌面上或者没有选择任何文件,则展示通用选项
this.targetOptions = this.options.common;
} else {
// 有文件选择
this.targetOptions = JSON.parse(JSON.stringify(this.options.selected));
// 检测上传
(this.selectData.optionPlugin || []).forEach(plugin => {
plugin.features.forEach(fe => {
fe.cmds.forEach(cmd => {
// 如果是图片,则唤起图片选项
const regImg = /\.(png|jpg|gif|jpeg|webp)$/;
if (cmd.type === 'img' && regImg.test(ext)) {
console.log(plugin);
this.targetOptions.push({
type: 'ext',
name: cmd.label,
icon: plugin.icon,
click: (fileUrl) => {
const base64 = this.fileToBase64(fileUrl);
ipcRenderer.send('superPanel-openPlugin', {
cmd: cmd,
plugin: plugin,
feature: fe,
data: base64,
});
}
})
}
// 如果是文件,且符合文件正则类型
if (cmd.type === 'file' && new RegExp(cmd.match).test(ext)) {
this.targetOptions.push({
type: 'ext',
name: cmd.label,
icon: '',
click: () => {
ipcRenderer.send('superPanel-openPlugin', {
cmd: cmd,
plugin: plugin,
feature: fe,
data: {
isFile: true,
isDirectory: false,
name: path.basename(this.selectData.fileUrl),
path: this.selectData.fileUrl
}
})
}
})
}
})
});
});
}
});
},
methods: {
translate(msg, to) {
const {appid, key} = opConfig.get().superPanel.baiduAPI;
if (!appid || !key) return;
const q = msg;
const salt = parseInt(Math.random() * 1000000000); //加盐
const sign = md5(appid + q + salt + key); //生成签名
const params = encodeURI(
`q=${q}&from=auto&to=${to}&appid=${appid}&salt=${salt}&sign=${sign}`
);
const options = {
uri: `https://fanyi-api.baidu.com/api/trans/vip/translate?${params}`,
};
return rp(options).then((res) => {
this.$set(this.selectData, 'translate', JSON.parse(res).trans_result)
})
},
commonClick(item, fileUrl) {
ipcRenderer.send('superPanel-hidden')
item.click(fileUrl);
},
fileToBase64 (filePath) {
let data = fs.readFileSync(filePath.replace('file://', ''));
data = new Buffer(data).toString("base64");
let base64 = "data:" + mineType.lookup(filePath) + ";base64," + data;
return base64;
},
openMainWindow() {
ipcRenderer.send('msg-trigger', {
type: 'showMainWindow',
});
}
},
watch: {
selectData: {
deep: true,
handler() {
this.$nextTick(() => {
ipcRenderer.send('superPanel-setSize', parseInt(getComputedStyle(document.getElementById('app')).height))
})
}
}
}
})

6
static/plugins/superPanel/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -43,16 +43,15 @@ function convertImgToBase64(url, callback, outputFormat){
window.utools = window.rubick = { window.utools = window.rubick = {
// 事件 // 事件
onPluginEnter(cb) { onPluginEnter(cb) {
ipcRenderer.once('onPluginEnter', (e, message) => { ipcRenderer.on('onPluginEnter', (e, message) => {
const feature = JSON.parse(message.detail) const feature = message.detail;
console.log(feature) cb({...feature, type: message.cmd.type ? message.cmd.type : 'text', payload: message.payload})
cb({...feature, type: 'text'})
}) })
}, },
onPluginReady(cb) { onPluginReady(cb) {
ipcRenderer.once('onPluginReady', (e, message) => { ipcRenderer.once('onPluginReady', (e, message) => {
const feature = JSON.parse(message.detail) const feature = message.detail
cb({...feature, type: 'text'}) cb({...feature, type: message.cmd.type ? message.cmd.type : 'text', payload: message.payload})
}) })
}, },
onPluginOut(cb) { onPluginOut(cb) {