大型情感類技術連續劇-徒手擼一個 uTools(二)

  • 2021 年 6 月 30 日
  • 筆記

前言

上篇手把手教你實現一個支持插件化的 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": "//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://github.com/clouDr-f2e/rubick