上篇手把手教你实现一个支持插件化的 uTools 工具箱我们介绍过了如何通过 electron
实现 utools
的插件功能体系,并按照 utools
的交互和设计做出了一套可以支持插件化的桌面端工具箱 Rubick
。
本篇将继续为大家介绍如何再基于 electron
实现 utools
的搜索能力。
utools
搜索核心分为系统命令、插件命令、系统 app 功能搜索等几大类,下面我们来一一实现这 3 类功能的检索能力。
由于这 3 类搜索搜索出来的内容点击触发的交互不一样,所以我们设计了一个枚举类型来标记这三种检索内容,用于点击后触发不同的行为。
const SEARCH_TYPE = {
DEV: 'dev', // 测试插件
PROD: 'prod', // 已安装的插件
SYSTEM: 'system', // 系统插件
APP: 'app' // 应用 app
}
开发者插件分为已安装
和本地开发
2 种类型,分别根据 SEARCH_TYPE.PROD
和 SEARCH_TYPE.DEV
来进行区分。
搜索内容的基础数据结构如下:
const item = {
name: '搜索的 title',
icon: '插件的 icon',
desc: '插件的描述信息',
type: 'SEARCH_TYPE 对应的插件类型',
click: '点击事件'
}
拿一个具体的搜索插件举例:
const item = {
// 搜索插件功能对应的 cmd
name: cmd,
// 插件 icon 地址
icon: plugin.sourceFile ? 'image://' + path.join(plugin.sourceFile, `../${plugin.logo}`) : plugin.logo,
// 功能描述
desc: fe.explain,
// 类型
type: plugin.type,
// 点击后的动作
click: (router) => {
actions.openPlugin({commit}, {cmd, plugin, feature: fe, router});
}
}
整体来看已安装插件和本地插件交互展示上唯一需要区分的是本地插件需要打上一个 tag
用于标记,这样才不至于混淆线上插件和本地插件:
<a-tag v-show="item.type === 'dev'">开发者</a-tag>
<a-tag v-show="item.type === 'system'">系统</a-tag>
我们来看一下完成后的效果:
会带有开发者标记。
接下来就是实现openPlugin
点击效果,点击核心能力是需要对 plugin
进行 webview 渲染。上篇问诊已经介绍过了如何实现这个 webview
这里就不在赘述,说一下点击逻辑:
openPlugin() {
commit('commonUpdate', {
// 点击后设置标签 tag 为搜索词
selected: {
key: 'plugin-container',
name: cmd,
icon: 'image://' + path.join(plugin.sourceFile, `../${plugin.logo}`),
},
// 清空搜索内容
searchValue: '',
// 展示 plugin webview
showMain: true
});
// 计算 webview 内容高度
ipcRenderer.send('changeWindowSize-rubick', {
height: getWindowHeight(),
});
}
再说一下点击后触发的逻辑步骤:
webview
加载插件,并动态计算插件高度最后效果如下:
系统插件是 utools
内置的,所以我们也需要将系统插件内置到 Rubick
中,这里我拿实现一个取色器来举例,去实现一个系统插件。首先先定义好系统插件的数据结构:
const SYSTEM_PLUFINS = [
{
"pluginName": "屏幕颜色拾取",
"logo": "https://alicdn.com/img/6a1b4b8a17da45d680ea30b53a91aca8.png",
"features": [
{
"code": "pick",
"explain": "rubick 帮助文档",
"cmds": [ "取色", "拾色", 'Pick color' ]
},
],
"tag": 'rubick-color',
}
]
字段说明:
系统插件的交互展示和开发者插件本无太大的差异,核心较大的差异在于点击后的功能和开发者插件不太一样,我们来看看系统插件的点击交互逻辑:
opnPlugin() {
// 如果点击的是系统插件
if (plugin.type === 'system') {
// 调用系统函数
systemMethod[plugin.tag][feature.code]();
// 清空选择
commit('commonUpdate', {
selected: null,
showMain: false,
options: [],
});
// 设置高度为初始高度
ipcRenderer.send('changeWindowSize-rubick', {
height: getWindowHeight([]),
});
// 跳转到首页
router.push({
path: '/home',
});
}
}
所以对系统插件来说,由于系统插件本身并无 webview
所以不需要打开 webview
来承载插件,而是调用系统函数,比如 color-pick
调用的对应系统函数如下:
export default {
'rubick-color': {
pick() {
ipcRenderer.send('start-picker')
}
},
}
向 main
进程发送取色能力。如何取色将在后面章节介绍。实现后的交互如下:
针对于 macos
用户,所安装的系统 App 都放在了 /System/Applications
和 /Applications
下,所以要实现 app 搜索,就是需要对 /System/Applications
和 /Applications
目录下的 app 进行检索。但有的时候除了 app 需要搜索,一些系统功能也需要搜索,比如偏好设置之类的。偏好设置一般存方的路径在 /System/Library/PreferencePanes
中。
接下来第一步需要做的是检束所有 app 和 PreferencePanes:
const APP_FINDER_PATH = [
'/System/Applications',
'/Applications',
'/System/Library/PreferencePanes',
];
APP_FINDER_PATH.forEach((searchPath) => {
// 搜索对应目录
fs.readdir(searchPath, (err, files) => {
// 查询所有 app 和 PreferencePanes
try {
for (let i = 0; i < files.length; i++) {
const appName = files[i];
const extname = path.extname(appName);
const appSubStr = appName.split(extname)[0];
if ((extname === '.app' || extname === '.prefPane') >= 0 ) {
// 查找 应用程序的 icon
try {
const path1 = path.join(searchPath, `${appName}/Contents/Resources/App.icns`);
const path2 = path.join(searchPath, `${appName}/Contents/Resources/AppIcon.icns`);
const path3 = path.join(searchPath, `${appName}/Contents/Resources/${appSubStr}.icns`);
const path4 = path.join(searchPath, `${appName}/Contents/Resources/${appSubStr.replace(' ', '')}.icns`);
let iconPath = path1;
if (fs.existsSync(path1)) {
iconPath = path1;
} else if (fs.existsSync(path2)) {
iconPath = path2;
} else if (fs.existsSync(path3)) {
iconPath = path3;
} else if (fs.existsSync(path4)) {
iconPath = path4;
} else {
// 性能最低的方式
const resourceList = fs.readdirSync(path.join(searchPath, `${appName}/Contents/Resources`));
const iconName = resourceList.filter(file => path.extname(file) === '.icns')[0];
iconPath = path.join(searchPath, `${appName}/Contents/Resources/${iconName}`);
}
// 创建图片
nativeImage.createThumbnailFromPath(iconPath, {width: 64, height: 64}).then(img => {
// 创建搜索项
fileLists.push({
name: appSubStr,
value: 'plugin',
icon: img.toDataURL(),
desc: path.join(searchPath, appName),
type: 'app',
action: `open ${path.join(searchPath, appName).replace(' ', '\\ ')}`
})
})
} catch (e) {
}
}
}
} catch (e) {
console.log(e);
}
});
});
代码看的有点多,其实很简单,主要也是几步走:
最后一步就是点击呼出了:
openPlugin() {
if (plugin.type === 'app') {
// 呼出 app
execSync(plugin.action);
commit('commonUpdate', {
selected: null,
showMain: false,
options: [],
searchValue: '',
});
ipcRenderer.send('changeWindowSize-rubick', {
height: getWindowHeight([]),
});
return;
}
}
最后来看一下系统 app 检索效果:
本篇主要介绍如何实现一个类似于 utools 的插件搜索功能,当然这远远不是 utools 的全部,下期我们再继续介绍如何实现 utools 其他能力。欢迎大家前往体验 Rubick 有问题可以随时提 issue 我们会及时反馈。
另外,如果觉得设计实现思路对你有用,也欢迎给个 Star:https://github.com/clouDr-f2e/rubick
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.