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

muwooo發表於2021-06-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": "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

相關文章