diff --git a/README.md b/README.md
index 9be8394..cfb7d9c 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
-
+
## 特点
@@ -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 f305cfc..91a04af 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 8abc2de..55ec212 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 672196e..4272724 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 0000000..074ba74
--- /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 0000000..cab1533
--- /dev/null
+++ b/src/renderer/src/components/Devices/ControlBar/index.vue
@@ -0,0 +1,130 @@
+
+
+
+ {{ item.label }}
+
+
+
+
+
+
+
diff --git a/src/renderer/src/components/Devices/index.vue b/src/renderer/src/components/Devices/index.vue
index b01ba62..1136f55 100644
--- a/src/renderer/src/components/Devices/index.vue
+++ b/src/renderer/src/components/Devices/index.vue
@@ -41,12 +41,14 @@
@@ -97,16 +99,6 @@
{{ row.$recordLoading ? '录制中' : '开始录制' }}
-
- 点亮屏幕
-
-
+
+
+
+
+
+
+
+
+
+
@@ -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 e933165..f023597 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 e8c23ce..6a7b137 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: '默认值为执行应用的同级目录',
},
{