From 3bd20753242c3f8b218bc23c4077495e0a1ecd7f Mon Sep 17 00:00:00 2001 From: viarotel Date: Tue, 17 Oct 2023 17:54:29 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=F0=9F=9A=80=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E9=80=9A=E8=BF=87=E6=93=8D=E4=BD=9C=E6=A0=8F=E5=AE=89=E8=A3=85?= =?UTF-8?q?=E5=BA=94=E7=94=A8=E5=B9=B6=E6=8F=90=E4=BE=9B=E7=9B=B8=E5=BA=94?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E5=8F=8D=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/exposes/adbkit/index.js | 18 +++-- electron/exposes/scrcpy/index.js | 47 +++++++++-- package.json | 2 + pnpm-lock.yaml | 67 ++++++++++++++++ src/components/Devices/ControlBar/index.vue | 86 ++++++++++++++++++--- src/components/Devices/index.vue | 39 +++++++--- src/icons/components/SvgIcon.vue | 75 ++++++++++++++++++ src/icons/index.js | 7 ++ src/icons/svg/install.svg | 1 + src/icons/svg/wifi.svg | 1 + src/icons/svgo.config.js | 10 +++ src/main.js | 2 + vite.config.js | 2 + 13 files changed, 323 insertions(+), 34 deletions(-) create mode 100644 src/icons/components/SvgIcon.vue create mode 100644 src/icons/index.js create mode 100644 src/icons/svg/install.svg create mode 100644 src/icons/svg/wifi.svg create mode 100644 src/icons/svgo.config.js diff --git a/electron/exposes/adbkit/index.js b/electron/exposes/adbkit/index.js index f98a9118..680afae8 100644 --- a/electron/exposes/adbkit/index.js +++ b/electron/exposes/adbkit/index.js @@ -6,7 +6,7 @@ import dayjs from 'dayjs' import { Adb } from '@devicefarmer/adbkit' import { adbPath } from '@electron/configs/index.js' -console.log('adbPath', adbPath) +// console.log('adbPath', adbPath) const exec = util.promisify(child_process.exec) @@ -19,12 +19,11 @@ window.addEventListener('beforeunload', () => { }) const shell = async command => exec(`${adbPath} ${command}`) -const getDevices = async () => await client.listDevicesWithPaths() -const deviceShell = async (id, command) => - await client.getDevice(id).shell(command) -const kill = async (...params) => await client.kill(...params) -const connect = async (...params) => await client.connect(...params) -const disconnect = async (...params) => await client.disconnect(...params) +const getDevices = async () => client.listDevicesWithPaths() +const deviceShell = async (id, command) => client.getDevice(id).shell(command) +const kill = async (...params) => client.kill(...params) +const connect = async (...params) => client.connect(...params) +const disconnect = async (...params) => client.disconnect(...params) const getDeviceIP = async (id) => { try { @@ -40,7 +39,7 @@ const getDeviceIP = async (id) => { } } -const tcpip = async (id, port = 5555) => await client.getDevice(id).tcpip(port) +const tcpip = async (id, port = 5555) => client.getDevice(id).tcpip(port) const screencap = async (deviceId, options = {}) => { let fileStream = null @@ -74,6 +73,8 @@ const screencap = async (deviceId, options = {}) => { }) } +const install = async (id, path) => client.getDevice(id).install(path) + const watch = async (callback) => { const tracker = await client.trackDevices() tracker.on('add', async (ret) => { @@ -112,6 +113,7 @@ export default () => { getDeviceIP, tcpip, screencap, + install, watch, } } diff --git a/electron/exposes/scrcpy/index.js b/electron/exposes/scrcpy/index.js index 51528302..0d430f4c 100644 --- a/electron/exposes/scrcpy/index.js +++ b/electron/exposes/scrcpy/index.js @@ -1,11 +1,48 @@ -import util from 'node:util' -import child_process from 'node:child_process' +import { spawn } from 'node:child_process' import { adbPath, scrcpyPath } from '@electron/configs/index.js' -const exec = util.promisify(child_process.exec) +const shell = async (command, { stdout, stderr } = {}) => { + const args = command.split(' ') + const scrcpyProcess = spawn(scrcpyPath, args, { + env: { ...process.env, ADB: adbPath }, + shell: true, + }) -const shell = command => - exec(`${scrcpyPath} ${command}`, { env: { ...process.env, ADB: adbPath } }) + scrcpyProcess.stdout.on('data', (data) => { + const stringData = data.toString() + + console.log('scrcpyProcess.stdout.data:', stringData) + + if (stdout) { + stdout(stringData) + } + }) + + scrcpyProcess.stderr.on('data', (data) => { + const stringData = data.toString() + + console.error('scrcpyProcess.stderr.data:', stringData) + + if (stderr) { + stderr(stringData) + } + }) + + return new Promise((resolve, reject) => { + scrcpyProcess.on('close', (code) => { + if (code === 0) { + resolve() + } + else { + reject(new Error(`Command failed with code ${code}`)) + } + }) + + scrcpyProcess.on('error', (err) => { + reject(err) + }) + }) +} export default () => ({ shell, diff --git a/package.json b/package.json index 8abf3a18..7c9a2da3 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "build:linux": "vite build && electron-builder --linux", "preview": "vite preview", "lint": "eslint . --ext .md,.vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .eslintignore --fix", + "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.config.js", "postinstall": "electron-builder install-app-deps" }, "dependencies": { @@ -40,6 +41,7 @@ "vite-plugin-electron": "^0.14.0", "vite-plugin-electron-renderer": "^0.14.5", "vite-plugin-eslint": "^1.8.1", + "vite-svg-loader": "^4.0.0", "vue-tsc": "^1.8.8" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ef220d56..a73b72c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ devDependencies: vite-plugin-eslint: specifier: ^1.8.1 version: 1.8.1(eslint@8.51.0)(vite@4.4.11) + vite-svg-loader: + specifier: ^4.0.0 + version: 4.0.0 vue-tsc: specifier: ^1.8.8 version: 1.8.19(typescript@5.2.2) @@ -781,6 +784,11 @@ packages: engines: {node: '>= 10'} dev: true + /@trysound/sax@0.2.0: + resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} + engines: {node: '>=10.13.0'} + dev: true + /@types/cacheable-request@6.0.3: resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} dependencies: @@ -2035,6 +2043,11 @@ packages: engines: {node: '>= 6'} dev: true + /commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + dev: true + /commander@9.5.0: resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} engines: {node: ^12.20.0 || >=14} @@ -2084,6 +2097,16 @@ packages: which: 2.0.2 dev: true + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: true + /css-selector-tokenizer@0.8.0: resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==} dependencies: @@ -2091,6 +2114,14 @@ packages: fastparse: 1.1.2 dev: true + /css-tree@2.2.1: + resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + mdn-data: 2.0.28 + source-map-js: 1.0.2 + dev: true + /css-tree@2.3.1: resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==} engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} @@ -2099,12 +2130,24 @@ packages: source-map-js: 1.0.2 dev: true + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: true + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} hasBin: true dev: true + /csso@5.0.5: + resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'} + dependencies: + css-tree: 2.2.1 + dev: true + /csstype@3.1.2: resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==} @@ -3696,6 +3739,10 @@ packages: resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==} dev: true + /mdn-data@2.0.28: + resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==} + dev: true + /mdn-data@2.0.30: resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==} dev: true @@ -4644,6 +4691,19 @@ packages: engines: {node: '>= 0.4'} dev: true + /svgo@3.0.2: + resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==} + engines: {node: '>=14.0.0'} + hasBin: true + dependencies: + '@trysound/sax': 0.2.0 + commander: 7.2.0 + css-select: 5.1.0 + css-tree: 2.3.1 + csso: 5.0.5 + picocolors: 1.0.0 + dev: true + /tailwindcss@3.3.3: resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==} engines: {node: '>=14.0.0'} @@ -4991,6 +5051,13 @@ packages: vite: 4.4.11 dev: true + /vite-svg-loader@4.0.0: + resolution: {integrity: sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==} + dependencies: + '@vue/compiler-sfc': 3.3.4 + svgo: 3.0.2 + dev: true + /vite@4.4.11: resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==} engines: {node: ^14.18.0 || >=16.0.0} diff --git a/src/components/Devices/ControlBar/index.vue b/src/components/Devices/ControlBar/index.vue index bfdd4bb4..b39f17a5 100644 --- a/src/components/Devices/ControlBar/index.vue +++ b/src/components/Devices/ControlBar/index.vue @@ -6,11 +6,16 @@ type="primary" plain class="!border-none !mx-0 bg-transparent !rounded-0" - :icon="item.icon" :disabled="device.$unauthorized" :title="item.tips" @click="handleClick(item)" > + {{ item.label }} @@ -32,37 +37,43 @@ export default { controlModel: [ { label: '切换键', - icon: 'Switch', + elIcon: 'Switch', command: 'input keyevent KEYCODE_APP_SWITCH', }, { label: '主屏幕键', - icon: 'HomeFilled', + elIcon: 'HomeFilled', command: 'input keyevent KEYCODE_HOME', }, { label: '返回键', - icon: 'Back', + elIcon: 'Back', command: 'input keyevent KEYCODE_BACK', }, { label: '菜单键', - icon: 'Menu', + elIcon: 'Menu', command: 'input keyevent KEYCODE_MENU', tips: '不要和切换键搞错啦', }, { label: '电源键', - icon: 'SwitchButton', + elIcon: 'SwitchButton', command: 'input keyevent KEYCODE_POWER', tips: '可以用来开启或关闭屏幕', }, { label: '截取屏幕', - icon: 'Crop', + elIcon: 'Crop', handle: this.handleScreenCap, tips: '', }, + { + label: '安装应用', + svgIcon: 'install', + handle: this.handleInstall, + tips: '', + }, ], } }, @@ -72,6 +83,54 @@ export default { }, }, methods: { + async handleInstall(device) { + const files = await this.$electron.ipcRenderer.invoke( + 'show-open-dialog', + { + properties: ['openFile', 'multiSelections'], + filters: [{ name: '请选择要安装的应用', extensions: ['apk'] }], + }, + ) + + if (!files) { + return false + } + + const messageEl = this.$message({ + message: ` 正在为 ${device.name} 安装应用中...`, + icon: LoadingIcon, + duration: 0, + }) + + let failCount = 0 + + for (let index = 0; index < files.length; index++) { + const item = files[index] + await this.$adb.install(device.id, item).catch((e) => { + console.warn(e) + ++failCount + }) + } + + messageEl.close() + + const totalCount = files.length + const successCount = totalCount - failCount + + if (successCount) { + if (totalCount > 1) { + this.$message.success( + `已成功将应用安装到 ${device.name} 中,共 ${totalCount}个,成功 ${successCount}个,失败 ${failCount}个`, + ) + } + else { + this.$message.success(`已成功将应用安装到 ${device.name} 中`) + } + return + } + + this.$message.warning('安装应用失败,请检查安装包后重试') + }, handleClick(row) { if (row.command) { this.$adb.deviceShell(this.device.id, row.command) @@ -84,16 +143,19 @@ export default { } }, async handleScreenCap(device) { - const deviceName = device.name || device.id - const messageEl = this.$message({ - message: ` 正在截取 ${deviceName} 的屏幕快照...`, + message: ` 正在截取 ${device.name} 的屏幕快照...`, icon: LoadingIcon, duration: 0, }) - const fileName = `${deviceName}-screencap-${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.png` - const savePath = this.$path.resolve(this.scrcpyConfig['--record'], fileName) + const fileName = `${device.name}-screencap-${dayjs().format( + 'YYYY-MM-DD-HH-mm-ss', + )}.png` + const savePath = this.$path.resolve( + this.scrcpyConfig['--record'], + fileName, + ) try { await this.$adb.screencap(device.id, { savePath }) diff --git a/src/components/Devices/index.vue b/src/components/Devices/index.vue index 0e4d1f47..f0aa5824 100644 --- a/src/components/Devices/index.vue +++ b/src/components/Devices/index.vue @@ -1,7 +1,12 @@