大型情感类技术连续剧-徒手撸一个 uTools(二)

2021-06-29 11:01:13 +08:00
 muwoo

前言

上篇手把手教你实现一个支持插件化的 uTools 工具箱我们介绍过了如何通过 electron 实现 utools 的插件功能体系,并按照 utools 的交互和设计做出了一套可以支持插件化的桌面端工具箱 Rubick

Rubick 源码

本篇将继续为大家介绍如何再基于 electron 实现 utools 的搜索能力。

搜索能力实现

utools 搜索核心分为系统命令、插件命令、系统 app 功能搜索等几大类,下面我们来一一实现这 3 类功能的检索能力。

由于这 3 类搜索搜索出来的内容点击触发的交互不一样,所以我们设计了一个枚举类型来标记这三种检索内容,用于点击后触发不同的行为。

const SEARCH_TYPE = {
  DEV: 'dev', // 测试插件
  PROD: 'prod', // 已安装的插件
  SYSTEM: 'system', // 系统插件
  APP: 'app' // 应用 app
}

开发者插件

开发者插件分为已安装本地开发 2 种类型,分别根据 SEARCH_TYPE.PRODSEARCH_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(),
    });
}

再说一下点击后触发的逻辑步骤:

  1. 设置左上角标签内容为搜索关键词,并设置右上角 icon
  2. 清空搜索内容
  3. 打开 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 进程发送取色能力。如何取色将在后面章节介绍。实现后的交互如下:

系统 app 功能搜索

针对于 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);
    }
  });
});

代码看的有点多,其实很简单,主要也是几步走:

  1. 根据定义好的路径查找所有 app 和 PreferencePanes
  2. 应为下拉选项需要展示插件的 icon 所以对于 app 和 PreferencePanes 需要查找 icns
  3. 根据默认规则查找 icns 如果找不到再用性能较低的方式模糊匹配
  4. 检索成功后设置好下拉选项

最后一步就是点击呼出了:

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

1213 次点击
所在节点    Vue.js
1 条回复
wjx0912
2021-07-01 20:15:20 +08:00
好东东啊,居然没人 up

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/786416

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX