V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
muwoo
V2EX  ›  Vue.js

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

  •  2
     
  •   muwoo · 2021-06-29 11:01:13 +08:00 · 1359 次点击
    这是一个创建于 1278 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

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

    image.png

    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>
    

    我们来看一下完成后的效果:

    image.png

    会带有开发者标记。

    接下来就是实现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 加载插件,并动态计算插件高度

    最后效果如下:

    image.png

    系统插件

    系统插件是 utools 内置的,所以我们也需要将系统插件内置到 Rubick 中,这里我拿实现一个取色器来举例,去实现一个系统插件。首先先定义好系统插件的数据结构:

    const SYSTEM_PLUFINS = [
      {
        "pluginName": "屏幕颜色拾取",
        "logo": "https://alicdn.com/img/6a1b4b8a17da45d680ea30b53a91aca8.png",
        "features": [
          {
            "code": "pick",
            "explain": "rubick 帮助文档",
            "cmds": [ "取色", "拾色", 'Pick color' ]
          },
        ],
        "tag": 'rubick-color',
      }
    ]
    

    字段说明:

    • pluginName:系统插件展示的名称
    • logo: 系统插件展示的 logo
    • features: 系统插件的功能列表
    • feature.code: 系统插件执行的 code 码
    • tag: 系统插件唯一标记

    系统插件的交互展示和开发者插件本无太大的差异,核心较大的差异在于点击后的功能和开发者插件不太一样,我们来看看系统插件的点击交互逻辑:

    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 进程发送取色能力。如何取色将在后面章节介绍。实现后的交互如下:

    QQ20210628-210345-HD.gif

    系统 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 检索效果:

    image.png

    结语

    本篇主要介绍如何实现一个类似于 utools 的插件搜索功能,当然这远远不是 utools 的全部,下期我们再继续介绍如何实现 utools 其他能力。欢迎大家前往体验 Rubick 有问题可以随时提 issue 我们会及时反馈。

    另外,如果觉得设计实现思路对你有用,也欢迎给个 Star:https://github.com/clouDr-f2e/rubick

    wjx0912
        1
    wjx0912  
       2021-07-01 20:15:20 +08:00
    好东东啊,居然没人 up
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2071 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 01:05 · PVG 09:05 · LAX 17:05 · JFK 20:05
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.