From fd207364a4cb68e352b594bf2d55905e53949a33 Mon Sep 17 00:00:00 2001 From: viarotel Date: Fri, 13 Oct 2023 17:01:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=F0=9F=9A=80=20=E5=A2=9E=E5=8A=A0?= =?UTF-8?q?=E4=BA=86=E5=AF=B9=E8=AE=BE=E5=A4=87=E4=BA=A4=E4=BA=92=E6=8E=A7?= =?UTF-8?q?=E5=88=B6=E6=A0=8F=E7=9A=84=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 ++- package.json | 1 + pnpm-lock.yaml | 12 ++ src/preload/plugins/adbkit/index.js | 38 ++++- .../Devices/ControlBar/LoadingIcon/index.vue | 13 ++ .../components/Devices/ControlBar/index.vue | 130 ++++++++++++++++++ src/renderer/src/components/Devices/index.vue | 70 ++++++---- src/renderer/src/store/scrcpy/index.js | 73 ++++++---- .../src/store/scrcpy/model/record/index.js | 4 +- 9 files changed, 292 insertions(+), 66 deletions(-) create mode 100644 src/renderer/src/components/Devices/ControlBar/LoadingIcon/index.vue create mode 100644 src/renderer/src/components/Devices/ControlBar/index.vue diff --git a/README.md b/README.md index 9be83941..cfb7d9c6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ 📱 Use Scrcpy with a graphical interface to display and control your Android device, driven by Electron
- viarotel-escrcpy + viarotel-escrcpy
## 特点 @@ -37,6 +37,8 @@ ### WIFI 连接 +> 注意:如果首次无线连接失败,你可能需要无线配对请参阅 [常见问题](#常见问题) +> > 注意:需同时开启无线调试功能,并在无线调试页面中获取你的当前设备的无线地址(通常为你连接WIFI时分配的IP地址)及端口号(默认为 5555) 1. 同 USB 连接中的 1-2 步骤 @@ -82,10 +84,11 @@ 1. 用户界面进行优化,制作合适的 Logo ✅ 2. 内置的软件更新功能 ✅ 3. 录制和保存音视频 ✅ -4. 添加设备交互控制栏 🚧 -5. 添加 macOS 及 linux 操作系统的支持 🚧 -6. 支持语言国际化功能 🚧 -7. 添加对游戏的增强功能 如游戏键位映射 🚧 +4. 添加设备快捷交互控制栏 ✅ +5. 支持自定义 Adb 及 Scrcpy 依赖,并支持生成精简版本和完整版本以满足不同用户需求 +6. 添加 macOS 及 linux 操作系统的支持 🚧 +7. 支持语言国际化功能 🚧 +8. 添加对游戏的增强功能,如游戏键位映射 🚧 ## 常见问题 @@ -118,6 +121,10 @@ 请再点一次,或点击刷新设备,一般不会超过两次,如果还不行,请提供机型和安卓版本信息到 [Issues](https://github.com/viarotel-org/escrcpy/issues) +### 设备交互控制栏为什么不设计为自动跟踪吸附的悬浮菜单? + +采用悬浮菜单方案不可避免地会增加对 Scrcpy 的耦合性,并增加与 Scrcpy 同步更新的难度。许多类似的 ScrcpyGUI 软件在使用此方案后不得不投入大量精力,最终因难以维护而放弃开发。因此,综合考虑,我们决定采用现有的方案,并期待 Scrcpy 未来能够增加原生交互控制栏的支持。 + ## 获得帮助 > 因为是开源项目 全靠爱发电 所以支持有限 更新节奏不固定 diff --git a/package.json b/package.json index f305cfcd..91a04af1 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "dayjs": "^1.11.10", "electron-updater": "^6.1.1", "element-plus": "^2.3.14", + "fs-extra": "^11.1.1", "lodash-es": "^4.17.21", "pinia": "^2.1.6", "ufo": "^1.3.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8abc2ded..55ec212c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -26,6 +26,9 @@ dependencies: element-plus: specifier: ^2.3.14 version: 2.3.14(vue@3.3.4) + fs-extra: + specifier: ^11.1.1 + version: 11.1.1 lodash-es: specifier: ^4.17.21 version: 4.17.21 @@ -3276,6 +3279,15 @@ packages: jsonfile: 6.1.0 universalify: 2.0.0 + /fs-extra@11.1.1: + resolution: {integrity: sha512-MGIE4HOvQCeUCzmlHs0vXpih4ysz4wg9qiSAu6cd42lVwPbTM1TjV7RusoyQqMmk/95gdQZX72u+YW+c3eEpFQ==} + engines: {node: '>=14.14'} + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.1.0 + universalify: 2.0.0 + dev: false + /fs-extra@8.1.0: resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} engines: {node: '>=6 <7 || >=8'} diff --git a/src/preload/plugins/adbkit/index.js b/src/preload/plugins/adbkit/index.js index 672196e0..4272724c 100644 --- a/src/preload/plugins/adbkit/index.js +++ b/src/preload/plugins/adbkit/index.js @@ -1,5 +1,8 @@ import util from 'node:util' import child_process from 'node:child_process' +import path from 'node:path' +import fs from 'node:fs' +import dayjs from 'dayjs' import { Adb } from '@devicefarmer/adbkit' import adbPath from '@resources/core/adb.exe?asset&asarUnpack' @@ -36,6 +39,38 @@ const getDeviceIP = async (id) => { const tcpip = async (id, port = 5555) => await client.getDevice(id).tcpip(port) +const screencap = async (deviceId, options = {}) => { + let fileStream = null + try { + const device = client.getDevice(deviceId) + fileStream = await device.screencap() + console.log('fileStream', fileStream) + } + catch (error) { + console.warn(error?.message || error) + return false + } + + if (!fileStream) { + return false + } + + const fileName = `Screencap-${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.png` + const savePath = options.savePath || path.resolve('../', fileName) + + return new Promise((resolve, reject) => { + fileStream + .pipe(fs.createWriteStream(savePath)) + .on('finish', () => { + resolve(true) + }) + .on('error', (error) => { + console.warn(error?.message || error) + reject(false) + }) + }) +} + const watch = async (callback) => { const tracker = await client.trackDevices() tracker.on('add', async (ret) => { @@ -71,8 +106,9 @@ export default () => { kill, connect, disconnect, - watch, getDeviceIP, tcpip, + screencap, + watch, } } diff --git a/src/renderer/src/components/Devices/ControlBar/LoadingIcon/index.vue b/src/renderer/src/components/Devices/ControlBar/LoadingIcon/index.vue new file mode 100644 index 00000000..074ba74e --- /dev/null +++ b/src/renderer/src/components/Devices/ControlBar/LoadingIcon/index.vue @@ -0,0 +1,13 @@ + + + + + diff --git a/src/renderer/src/components/Devices/ControlBar/index.vue b/src/renderer/src/components/Devices/ControlBar/index.vue new file mode 100644 index 00000000..cab15334 --- /dev/null +++ b/src/renderer/src/components/Devices/ControlBar/index.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/src/renderer/src/components/Devices/index.vue b/src/renderer/src/components/Devices/index.vue index b01ba62b..1136f55f 100644 --- a/src/renderer/src/components/Devices/index.vue +++ b/src/renderer/src/components/Devices/index.vue @@ -41,12 +41,14 @@ @@ -142,10 +144,12 @@ import { isIPWithPort, sleep } from '@renderer/utils/index.js' import storage from '@renderer/utils/storages' import dayjs from 'dayjs' import PairDialog from './PairDialog/index.vue' +import ControlBar from './ControlBar/index.vue' export default { components: { PairDialog, + ControlBar, }, data() { const adbCache = storage.get('adbCache') || {} @@ -185,12 +189,13 @@ export default { }) }, methods: { + toggleRowExpansion(...params) { + this.$refs.elTable.toggleRowExpansion(...params) + }, getRecordPath(row) { - const defaultPath = this.$path.resolve('../') - // console.log('defaultPath', defaultPath) - const basePath = this.scrcpyConfig['--record'] || defaultPath - const recordFormat = this.scrcpyConfig['--record-format'] || 'mp4' - const fileName = `${row.name || row.id}-${dayjs().format( + const basePath = this.scrcpyConfig['--record'] + const recordFormat = this.scrcpyConfig['--record-format'] + const fileName = `${row.name || row.id}-recording-${dayjs().format( 'YYYY-MM-DD-HH-mm-ss', )}.${recordFormat}` const joinValue = this.$path.join(basePath, fileName) @@ -199,9 +204,12 @@ export default { }, async handleRecord(row) { row.$recordLoading = true - const recordPath = this.getRecordPath(row) + + this.toggleRowExpansion(row, true) + + const savePath = this.getRecordPath(row) try { - const command = `--serial=${row.id} --window-title=${row.name}-${row.id}-🎥录制中... --record=${recordPath} ${this.stringScrcpyConfig}` + const command = `--serial=${row.id} --window-title=${row.name}-${row.id}-🎥录制中... --record=${savePath} ${this.stringScrcpyConfig}` console.log('handleRecord.command', command) @@ -214,7 +222,7 @@ export default { type: 'success', }) - this.$electron.ipcRenderer.invoke('show-item-in-folder', recordPath) + this.$electron.ipcRenderer.invoke('show-item-in-folder', savePath) } catch (error) { if (error.message) { @@ -225,6 +233,9 @@ export default { }, async handleMirror(row) { row.$loading = true + + this.toggleRowExpansion(row, true) + try { await this.$scrcpy.shell( `--serial=${row.id} --window-title=${row.name}-${row.id} ${this.stringScrcpyConfig}`, @@ -255,9 +266,6 @@ export default { onPairSuccess() { this.handleConnect() }, - handleScreenUp(row) { - this.$adb.deviceShell(row.id, 'input keyevent KEYCODE_POWER') - }, handleReset() { this.$electron.ipcRenderer.send('restart-app') }, @@ -324,15 +332,17 @@ export default { await sleep() try { const data = await this.$adb.getDevices() - this.deviceList = (data || []).map(item => ({ - ...item, - name: item.model ? item.model.split(':')[1] : '未授权设备', - $loading: false, - $recordLoading: false, - $stopLoading: false, - $unauthorized: item.type === 'unauthorized', - $wireless: isIPWithPort(item.id), - })) + this.deviceList + = data?.map(item => ({ + ...item, + id: item.id, + name: item.model ? item.model.split(':')[1] : '未授权设备', + $loading: false, + $recordLoading: false, + $stopLoading: false, + $unauthorized: item.type === 'unauthorized', + $wireless: isIPWithPort(item.id), + })) || [] console.log('getDeviceData.data', this.deviceList) } diff --git a/src/renderer/src/store/scrcpy/index.js b/src/renderer/src/store/scrcpy/index.js index e9331651..f023597d 100644 --- a/src/renderer/src/store/scrcpy/index.js +++ b/src/renderer/src/store/scrcpy/index.js @@ -1,14 +1,39 @@ import { defineStore } from 'pinia' import storage from '@renderer/utils/storages' -import { cloneDeep } from 'lodash-es' -import * as model from './model/index.js' +import { pickBy } from 'lodash-es' +import * as scrcpyModel from './model/index.js' + +/** + * 获取 Scrcpy 默认配置 + */ +function getDefaultConfig(type) { + const model = [] + if (type) { + const handler = scrcpyModel[type] + model.push(...handler()) + } + else { + // console.log('scrcpyModel', scrcpyModel) + const values = Object.values(scrcpyModel) + model.push(...values.flatMap(handler => handler())) + } + + const value = model.reduce((obj, item) => { + const { field, value } = item + obj[field] = value + return obj + }, {}) + + return value +} export const useScrcpyStore = defineStore({ id: 'app-scrcpy', state() { return { - model, - config: storage.get('scrcpyConfig'), + model: scrcpyModel, + defaultConfig: getDefaultConfig(), + config: {}, excludeKeys: ['--record', '--record-format'], } }, @@ -44,37 +69,27 @@ export const useScrcpyStore = defineStore({ }, }, actions: { + getDefaultConfig, init() { - this.config = this.config || this.getDefaultConfig() + this.config = { + ...this.defaultConfig, + ...(storage.get('scrcpyConfig') || {}), + } + return this.config }, - updateConfig(value) { - this.config = cloneDeep(value) - storage.set('scrcpyConfig', this.config) + updateConfig(data) { + const pickConfig = pickBy(data, value => !!value) + + console.log('pickConfig', pickConfig) + + storage.set('scrcpyConfig', pickConfig) + + this.init() }, getModel(key, params) { - const handler = this.model[key] + const handler = scrcpyModel[key] return handler(params) }, - getDefaultConfig(type) { - const model = [] - if (type) { - const handler = this.model[type] - model.push(...handler()) - } - else { - // console.log('scrcpyModel', scrcpyModel) - const values = Object.values(this.model) - model.push(...values.flatMap(handler => handler())) - } - - const value = model.reduce((obj, item) => { - const { field, value } = item - obj[field] = value - return obj - }, {}) - - return value - }, }, }) diff --git a/src/renderer/src/store/scrcpy/model/record/index.js b/src/renderer/src/store/scrcpy/model/record/index.js index e8c23ce8..6a7b1373 100644 --- a/src/renderer/src/store/scrcpy/model/record/index.js +++ b/src/renderer/src/store/scrcpy/model/record/index.js @@ -1,10 +1,12 @@ export default () => { + const $path = window.nodePath + return [ { label: '录制存储路径', type: 'input.directory', field: '--record', - value: '', + value: $path.resolve('../'), placeholder: '默认值为执行应用的同级目录', }, {