mirror of
https://github.com/viarotel-org/escrcpy.git
synced 2024-11-14 18:57:40 +01:00
feat: 🚀 添加音视频录制功能以及更多的高级选项
This commit is contained in:
parent
7616242744
commit
b6986d14de
2
.github/workflows/release-assets.yml
vendored
2
.github/workflows/release-assets.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: 18
|
||||
|
||||
- name: Install Dependencies
|
||||
run: npm install
|
||||
|
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: release-please
|
||||
name: release
|
||||
|
||||
on:
|
||||
push:
|
||||
|
@ -81,10 +81,11 @@
|
||||
|
||||
1. 用户界面进行优化,制作合适的 Logo ✅
|
||||
2. 内置的软件更新功能 ✅
|
||||
3. 添加 macOS 及 linux 操作系统的支持 🚧
|
||||
4. 添加外部控制栏 🚧
|
||||
5. 支持语言国际化功能 🚧
|
||||
6. 添加对游戏的增强功能 如游戏键位映射 🚧
|
||||
3. 录制和保存音视频 ✅
|
||||
4. 添加 macOS 及 linux 操作系统的支持 🚧
|
||||
5. 添加外部控制栏 🚧
|
||||
6. 支持语言国际化功能 🚧
|
||||
7. 添加对游戏的增强功能 如游戏键位映射 🚧
|
||||
|
||||
## 常见问题
|
||||
|
||||
|
@ -21,8 +21,12 @@
|
||||
"@electron-toolkit/preload": "^2.0.0",
|
||||
"@electron-toolkit/utils": "^2.0.0",
|
||||
"@viarotel-org/design": "^0.7.0",
|
||||
"dayjs": "^1.11.10",
|
||||
"electron-updater": "^6.1.1",
|
||||
"element-plus": "^2.3.14"
|
||||
"element-plus": "^2.3.14",
|
||||
"lodash-es": "^4.17.21",
|
||||
"pinia": "^2.1.6",
|
||||
"ufo": "^1.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config": "^1.0.1",
|
||||
|
@ -17,12 +17,24 @@ dependencies:
|
||||
'@viarotel-org/design':
|
||||
specifier: ^0.7.0
|
||||
version: 0.7.0
|
||||
dayjs:
|
||||
specifier: ^1.11.10
|
||||
version: 1.11.10
|
||||
electron-updater:
|
||||
specifier: ^6.1.1
|
||||
version: 6.1.4
|
||||
element-plus:
|
||||
specifier: ^2.3.14
|
||||
version: 2.3.14(vue@3.3.4)
|
||||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
pinia:
|
||||
specifier: ^2.1.6
|
||||
version: 2.1.6(typescript@5.2.2)(vue@3.3.4)
|
||||
ufo:
|
||||
specifier: ^1.3.1
|
||||
version: 1.3.1
|
||||
|
||||
devDependencies:
|
||||
'@electron-toolkit/eslint-config':
|
||||
@ -1595,6 +1607,10 @@ packages:
|
||||
'@vue/compiler-dom': 3.3.4
|
||||
'@vue/shared': 3.3.4
|
||||
|
||||
/@vue/devtools-api@6.5.1:
|
||||
resolution: {integrity: sha512-+KpckaAQyfbvshdDW5xQylLni1asvNSGme1JFs8I1+/H5pHEhqUKMEQD/qn3Nx5+/nycBq11qAEi8lk+LXI2dA==}
|
||||
dev: false
|
||||
|
||||
/@vue/eslint-config-prettier@8.0.0(eslint@8.49.0)(prettier@3.0.3):
|
||||
resolution: {integrity: sha512-55dPqtC4PM/yBjhAr+yEw6+7KzzdkBuLmnhBrDfp4I48+wy+Giqqj9yUr5T2uD/BkBROjjmqnLZmXRdOx/VtQg==}
|
||||
peerDependencies:
|
||||
@ -2286,8 +2302,8 @@ packages:
|
||||
- ts-node
|
||||
dev: true
|
||||
|
||||
/dayjs@1.11.9:
|
||||
resolution: {integrity: sha512-QvzAURSbQ0pKdIye2txOzNaHmxtUBXerpY0FJsFXUMKbIZeFm5ht1LS/jFsrncjnmtv8HsG0W2g6c0zUjZWmpA==}
|
||||
/dayjs@1.11.10:
|
||||
resolution: {integrity: sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==}
|
||||
dev: false
|
||||
|
||||
/debug@3.2.7:
|
||||
@ -2604,7 +2620,7 @@ packages:
|
||||
'@types/lodash-es': 4.17.9
|
||||
'@vueuse/core': 9.13.0(vue@3.3.4)
|
||||
async-validator: 4.2.5
|
||||
dayjs: 1.11.9
|
||||
dayjs: 1.11.10
|
||||
escape-html: 1.0.3
|
||||
lodash: 4.17.21
|
||||
lodash-es: 4.17.21
|
||||
@ -4135,7 +4151,7 @@ packages:
|
||||
acorn: 8.10.0
|
||||
pathe: 1.1.1
|
||||
pkg-types: 1.0.3
|
||||
ufo: 1.3.0
|
||||
ufo: 1.3.1
|
||||
dev: true
|
||||
|
||||
/mrmime@1.0.1:
|
||||
@ -4269,7 +4285,7 @@ packages:
|
||||
dependencies:
|
||||
destr: 2.0.1
|
||||
node-fetch-native: 1.4.0
|
||||
ufo: 1.3.0
|
||||
ufo: 1.3.1
|
||||
dev: true
|
||||
|
||||
/once@1.4.0:
|
||||
@ -4447,6 +4463,24 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/pinia@2.1.6(typescript@5.2.2)(vue@3.3.4):
|
||||
resolution: {integrity: sha512-bIU6QuE5qZviMmct5XwCesXelb5VavdOWKWaB17ggk++NUwQWWbP5YnsONTk3b752QkW9sACiR81rorpeOMSvQ==}
|
||||
peerDependencies:
|
||||
'@vue/composition-api': ^1.4.0
|
||||
typescript: '>=4.4.4'
|
||||
vue: ^2.6.14 || ^3.3.0
|
||||
peerDependenciesMeta:
|
||||
'@vue/composition-api':
|
||||
optional: true
|
||||
typescript:
|
||||
optional: true
|
||||
dependencies:
|
||||
'@vue/devtools-api': 6.5.1
|
||||
typescript: 5.2.2
|
||||
vue: 3.3.4
|
||||
vue-demi: 0.14.6(vue@3.3.4)
|
||||
dev: false
|
||||
|
||||
/pirates@4.0.6:
|
||||
resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==}
|
||||
engines: {node: '>= 6'}
|
||||
@ -5193,11 +5227,9 @@ packages:
|
||||
resolution: {integrity: sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
dev: true
|
||||
|
||||
/ufo@1.3.0:
|
||||
resolution: {integrity: sha512-bRn3CsoojyNStCZe0BG0Mt4Nr/4KF+rhFlnNXybgqt5pXHNFRlqinSoQaTrGyzE4X8aHplSb+TorH+COin9Yxw==}
|
||||
dev: true
|
||||
/ufo@1.3.1:
|
||||
resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==}
|
||||
|
||||
/unconfig@0.3.10:
|
||||
resolution: {integrity: sha512-tj317lhIq2iZF/NXrJnU1t2UaGUKKz1eL1sK2t63Oq66V9BxqvZV12m55fp/fpQJ+DDmVlLgo7cnLVOZkhlO/A==}
|
||||
|
@ -1,10 +1,13 @@
|
||||
import { join } from 'node:path'
|
||||
|
||||
import { BrowserWindow, app, shell } from 'electron'
|
||||
import { electronApp, is, optimizer } from '@electron-toolkit/utils'
|
||||
|
||||
import iconPath from '../../resources/icons/icon.png?asset'
|
||||
import winIconPath from '../../resources/icons/icon.ico?asset'
|
||||
import macIconPath from '../../resources/icons/icon.icns?asset'
|
||||
import ipcEvent from './ipc/index.js'
|
||||
|
||||
import ipcManage from './ipcManage/index.js'
|
||||
|
||||
function createWindow() {
|
||||
let icon = iconPath
|
||||
@ -46,7 +49,7 @@ function createWindow() {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
|
||||
ipcEvent(mainWindow)
|
||||
ipcManage(mainWindow)
|
||||
}
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
|
41
src/main/ipcManage/handles/index.js
Normal file
41
src/main/ipcManage/handles/index.js
Normal file
@ -0,0 +1,41 @@
|
||||
import { dialog, ipcMain, shell } from 'electron'
|
||||
|
||||
export default () => {
|
||||
ipcMain.handle('show-open-dialog', async (event, params) => {
|
||||
// console.log('params', params)
|
||||
try {
|
||||
const res = await dialog.showOpenDialog(params)
|
||||
// console.log('showOpenDialog.res', res)
|
||||
if (res.canceled) {
|
||||
return false
|
||||
}
|
||||
return res.filePaths
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(error?.message || error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('open-path', async (event, pathValue) => {
|
||||
try {
|
||||
await shell.openPath(pathValue)
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(error?.message || error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('show-item-in-folder', async (event, filePath) => {
|
||||
try {
|
||||
await shell.showItemInFolder(filePath)
|
||||
return true
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(error?.message || error)
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
import { app, ipcMain } from 'electron'
|
||||
import updaterEvents from './updater/index.js'
|
||||
|
||||
import updater from './updater/index.js'
|
||||
import handles from './handles/index.js'
|
||||
|
||||
export default (mainWindow) => {
|
||||
handles(mainWindow)
|
||||
updater(mainWindow)
|
||||
|
||||
ipcMain.on('restart-app', () => {
|
||||
app.relaunch()
|
||||
app.quit()
|
||||
})
|
||||
updaterEvents(mainWindow)
|
||||
}
|
@ -1,9 +1,8 @@
|
||||
import path from 'node:path'
|
||||
import { app, ipcMain } from 'electron'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import { autoUpdater } from 'electron-updater'
|
||||
|
||||
const path = require('node:path')
|
||||
|
||||
export default (mainWindow) => {
|
||||
// dev-start, 这里是为了在本地做应用升级测试使用,正式环境请务必删除
|
||||
if (is.dev && process.env.ELECTRON_RENDERER_URL) {
|
@ -1,8 +1,9 @@
|
||||
import util from 'node:util'
|
||||
import child_process from 'node:child_process'
|
||||
import { Adb } from '@devicefarmer/adbkit'
|
||||
import adbPath from '@resources/core/adb.exe?asset&asarUnpack'
|
||||
|
||||
const util = require('node:util')
|
||||
const exec = util.promisify(require('node:child_process').exec)
|
||||
const exec = util.promisify(child_process.exec)
|
||||
|
||||
let client = null
|
||||
|
||||
|
@ -1,9 +1,12 @@
|
||||
import path from 'node:path'
|
||||
import electron from './electron/index.js'
|
||||
import adbkit from './adbkit/index.js'
|
||||
import scrcpy from './scrcpy/index.js'
|
||||
|
||||
export default {
|
||||
install(expose) {
|
||||
expose('nodePath', path)
|
||||
|
||||
expose('electron', electron())
|
||||
expose('adbkit', adbkit())
|
||||
expose('scrcpy', scrcpy())
|
||||
|
@ -1,8 +1,9 @@
|
||||
import util from 'node:util'
|
||||
import child_process from 'node:child_process'
|
||||
import adbPath from '@resources/core/adb.exe?asset&asarUnpack'
|
||||
import scrcpyPath from '@resources/core/scrcpy.exe?asset&asarUnpack'
|
||||
|
||||
const util = require('node:util')
|
||||
const exec = util.promisify(require('node:child_process').exec)
|
||||
const exec = util.promisify(child_process.exec)
|
||||
|
||||
const shell = command =>
|
||||
exec(`${scrcpyPath} ${command}`, { env: { ...process.env, ADB: adbPath } })
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="absolute inset-0 px-4 pb-4 h-full overflow-hidden">
|
||||
<el-tabs v-model="activeTab" class="el-tabs-flex" @tab-click="handleClick">
|
||||
<el-tabs v-model="activeTab" class="el-tabs-flex" @tab-change="onTabChange">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) of tabsModel"
|
||||
:key="index"
|
||||
@ -8,7 +8,14 @@
|
||||
:name="item.prop"
|
||||
lazy
|
||||
>
|
||||
<component :is="item.prop" ref="component" />
|
||||
<component
|
||||
:is="item.prop"
|
||||
:ref="item.prop"
|
||||
:scrcpy-cache="scrcpyCache"
|
||||
:get-scrcpy-cache="getScrcpyCache"
|
||||
:set-scrcpy-cache="setScrcpyCache"
|
||||
:get-scrcpy-map="getScrcpyMap"
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
@ -44,8 +51,12 @@ export default {
|
||||
activeTab: 'Devices',
|
||||
}
|
||||
},
|
||||
mounted() {},
|
||||
methods: {},
|
||||
created() {
|
||||
this.$store.scrcpy.init()
|
||||
},
|
||||
methods: {
|
||||
onTabChange(prop) {},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
<a class="hover:underline text-primary-500" :href="escrcpyURL" target="_blank">Scrcpy</a>
|
||||
显示和控制您的 Android 设备,由 Electron 驱动
|
||||
</div>
|
||||
<div class="pt-16 pb-4">
|
||||
<div class="pt-12 pb-4">
|
||||
<el-button :loading="loading" type="primary" size="large" @click="handleUpdate">
|
||||
{{ loading && percent ? `正在更新中...(${percent.toFixed(1)}%)` : '版本检测更新' }}
|
||||
</el-button>
|
||||
@ -41,6 +41,10 @@ export default {
|
||||
this.onUpdateError()
|
||||
},
|
||||
methods: {
|
||||
handleUpdate() {
|
||||
this.loading = true
|
||||
this.$electron.ipcRenderer.send('check-for-update')
|
||||
},
|
||||
onUpdateNotAvailable() {
|
||||
this.$electron.ipcRenderer.on('update-not-available', () => {
|
||||
this.loading = false
|
||||
@ -69,10 +73,6 @@ export default {
|
||||
}
|
||||
})
|
||||
},
|
||||
handleUpdate() {
|
||||
this.loading = true
|
||||
this.$electron.ipcRenderer.send('check-for-update')
|
||||
},
|
||||
onDownloadProgress() {
|
||||
this.$electron.ipcRenderer.on('download-progress', async (event, ret) => {
|
||||
console.log('ret', ret)
|
||||
|
@ -14,21 +14,42 @@
|
||||
</div>
|
||||
</template>
|
||||
<div class="">
|
||||
<el-form ref="elForm" :model="scrcpyForm" label-width="120px" class="pr-8 pt-4">
|
||||
<el-form ref="elForm" :model="scrcpyForm" label-width="125px" class="pr-8 pt-4">
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="(item_1, index_1) of getScrcpyConfig(item.type)"
|
||||
v-for="(item_1, index_1) of getSubModel(item.type)"
|
||||
:key="index_1"
|
||||
:span="12"
|
||||
:offset="0"
|
||||
>
|
||||
<el-form-item :label="item_1.label" :prop="item_1.field">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<el-tooltip
|
||||
v-if="item_1.tips"
|
||||
class=""
|
||||
effect="dark"
|
||||
:content="item_1.tips"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-link
|
||||
class="mr-1 !text-base"
|
||||
icon="InfoFilled"
|
||||
type="warning"
|
||||
:underline="false"
|
||||
>
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
<span class="">{{ item_1.label }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-if="item_1.type === 'input'"
|
||||
v-bind="item_1.props || {}"
|
||||
v-model="scrcpyForm[item_1.field]"
|
||||
class="!w-full"
|
||||
:placeholder="item_1.placeholder"
|
||||
clearable
|
||||
></el-input>
|
||||
<el-input
|
||||
v-if="item_1.type === 'input.number'"
|
||||
@ -36,6 +57,17 @@
|
||||
v-model.number="scrcpyForm[item_1.field]"
|
||||
class="!w-full"
|
||||
:placeholder="item_1.placeholder"
|
||||
clearable
|
||||
></el-input>
|
||||
<el-input
|
||||
v-if="item_1.type === 'input.directory'"
|
||||
v-bind="item_1.props || {}"
|
||||
:value="scrcpyForm[item_1.field]"
|
||||
readonly
|
||||
class="!w-full"
|
||||
clearable
|
||||
:placeholder="item_1.placeholder"
|
||||
@click="handleDirectory(item_1)"
|
||||
></el-input>
|
||||
<el-switch
|
||||
v-if="item_1.type === 'switch'"
|
||||
@ -43,6 +75,7 @@
|
||||
v-model="scrcpyForm[item_1.field]"
|
||||
class="!w-full"
|
||||
:title="item_1.placeholder"
|
||||
clearable
|
||||
></el-switch>
|
||||
<el-select
|
||||
v-if="item_1.type === 'select'"
|
||||
@ -50,6 +83,7 @@
|
||||
v-model="scrcpyForm[item_1.field]"
|
||||
:placeholder="item_1.placeholder"
|
||||
class="!w-full"
|
||||
clearable
|
||||
>
|
||||
<el-option
|
||||
v-for="(item_2, index_2) in item_1.options"
|
||||
@ -69,14 +103,12 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import storage from '@renderer/utils/storages'
|
||||
import * as scrcpyConfigs from './configs/index.js'
|
||||
import { debounce } from 'lodash-es'
|
||||
import { useScrcpyStore } from '@renderer/store/index.js'
|
||||
|
||||
export default {
|
||||
emits: ['change'],
|
||||
data() {
|
||||
const scrcpyCache = storage.get('scrcpyCache') || {}
|
||||
// console.log('scrcpyCache', scrcpyCache)
|
||||
const scrcpyStore = useScrcpyStore()
|
||||
|
||||
return {
|
||||
scrcpyModel: [
|
||||
@ -88,54 +120,58 @@ export default {
|
||||
label: '设备控制',
|
||||
type: 'device',
|
||||
},
|
||||
{
|
||||
label: '音频控制',
|
||||
type: 'audio',
|
||||
},
|
||||
{
|
||||
label: '窗口控制',
|
||||
type: 'window',
|
||||
},
|
||||
{
|
||||
label: '录制音视频',
|
||||
type: 'record',
|
||||
},
|
||||
{
|
||||
label: '音频控制',
|
||||
type: 'audio',
|
||||
},
|
||||
],
|
||||
scrcpyForm: { ...this.getDefaultValues(), ...scrcpyCache },
|
||||
scrcpyForm: { ...scrcpyStore.config },
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
scrcpyForm: {
|
||||
handler() {
|
||||
storage.set('scrcpyCache', this.scrcpyForm)
|
||||
this.$message.success('保存配置成功,将在下一次控制设备时生效')
|
||||
this.handleSave()
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.handleSave = debounce(this.handleSave, 1000, { leading: false, trailing: true })
|
||||
},
|
||||
methods: {
|
||||
getScrcpyConfig(type) {
|
||||
const value = scrcpyConfigs[type]()
|
||||
return value
|
||||
async handleDirectory({ field }) {
|
||||
const res = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openDirectory'],
|
||||
})
|
||||
|
||||
if (!res) {
|
||||
return false
|
||||
}
|
||||
|
||||
const value = this.$path.normalize(res[0])
|
||||
|
||||
this.scrcpyForm[field] = value
|
||||
},
|
||||
getDefaultValues(type) {
|
||||
const model = []
|
||||
if (type) {
|
||||
model.push(...this.getScrcpyConfig(type))
|
||||
}
|
||||
else {
|
||||
// console.log('scrcpyConfigs', scrcpyConfigs)
|
||||
const values = Object.values(scrcpyConfigs)
|
||||
model.push(...values.flatMap(handler => handler()))
|
||||
}
|
||||
|
||||
const value = model.reduce((obj, item) => {
|
||||
const { field, value } = item
|
||||
obj[field] = value
|
||||
return obj
|
||||
}, {})
|
||||
|
||||
handleSave() {
|
||||
this.$store.scrcpy.updateConfig(this.scrcpyForm)
|
||||
this.$message.success('保存配置成功,将在下一次控制设备时生效')
|
||||
},
|
||||
getSubModel(type) {
|
||||
const value = this.$store.scrcpy.getModel(type)
|
||||
return value
|
||||
},
|
||||
handleReset(type) {
|
||||
this.scrcpyForm = { ...this.scrcpyForm, ...this.getDefaultValues(type) }
|
||||
storage.set('scrcpyCache', this.scrcpyForm)
|
||||
this.scrcpyForm = { ...this.scrcpyForm, ...this.$store.scrcpy.getDefaultConfig(type) }
|
||||
this.$store.scrcpy.updateConfig(this.scrcpyForm)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -41,8 +41,8 @@
|
||||
<template #empty>
|
||||
<el-empty description="设备列表为空" />
|
||||
</template>
|
||||
<el-table-column prop="id" label="设备 ID" show-overflow-tooltip />
|
||||
<el-table-column prop="name" label="设备名称" show-overflow-tooltip>
|
||||
<el-table-column prop="id" label="设备 ID" show-overflow-tooltip align="left" />
|
||||
<el-table-column prop="name" label="设备名称" show-overflow-tooltip align="left">
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
<el-tooltip
|
||||
@ -63,7 +63,7 @@
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" width="350" align="left">
|
||||
<el-table-column label="操作" width="450" align="left">
|
||||
<template #default="{ row }">
|
||||
<el-button
|
||||
type="primary"
|
||||
@ -73,6 +73,14 @@
|
||||
>
|
||||
{{ row.$loading ? '镜像中' : '开始镜像' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
:loading="row.$recordLoading"
|
||||
:disabled="row.$unauthorized"
|
||||
@click="handleRecord(row)"
|
||||
>
|
||||
{{ row.$recordLoading ? '录制中' : '开始录制' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
v-if="!row.$wireless"
|
||||
type="primary"
|
||||
@ -104,6 +112,7 @@
|
||||
<script>
|
||||
import { isIPWithPort, sleep } from '@renderer/utils/index.js'
|
||||
import storage from '@renderer/utils/storages'
|
||||
import dayjs from 'dayjs'
|
||||
import PairDialog from './PairDialog/index.vue'
|
||||
|
||||
export default {
|
||||
@ -123,8 +132,17 @@ export default {
|
||||
},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
scrcpyConfig() {
|
||||
return this.$store.scrcpy.config
|
||||
},
|
||||
stringScrcpyConfig() {
|
||||
return this.$store.scrcpy.stringConfig
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getDeviceData()
|
||||
|
||||
this.$adb.watch(async (type, ret) => {
|
||||
console.log('adb.watch.ret', ret)
|
||||
|
||||
@ -139,6 +157,58 @@ export default {
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
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(
|
||||
'YYYY-MM-DD-HH-mm-ss',
|
||||
)}.${recordFormat}`
|
||||
const joinValue = this.$path.join(basePath, fileName)
|
||||
const value = this.$path.normalize(joinValue)
|
||||
return value
|
||||
},
|
||||
async handleRecord(row) {
|
||||
row.$recordLoading = true
|
||||
const recordPath = this.getRecordPath(row)
|
||||
try {
|
||||
const command = `--serial=${row.id} --window-title=${row.name}-${row.id} --record=${recordPath} ${this.stringScrcpyConfig}`
|
||||
|
||||
console.log('handleRecord.command', command)
|
||||
|
||||
await this.$scrcpy.shell(command)
|
||||
|
||||
await this.$confirm('是否前往录制位置进行查看?', '录制成功', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
closeOnClickModal: false,
|
||||
type: 'success',
|
||||
})
|
||||
|
||||
this.$electron.ipcRenderer.invoke('show-item-in-folder', recordPath)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
this.$message.warning(error.message)
|
||||
}
|
||||
}
|
||||
row.$recordLoading = false
|
||||
},
|
||||
async handleMirror(row) {
|
||||
row.$loading = true
|
||||
try {
|
||||
await this.$scrcpy.shell(
|
||||
`--serial=${row.id} --window-title=${row.name}-${row.id} ${this.stringScrcpyConfig}`,
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
this.$message.warning(error.message)
|
||||
}
|
||||
}
|
||||
row.$loading = false
|
||||
},
|
||||
async handleWifi(row) {
|
||||
try {
|
||||
const host = await this.$adb.getDeviceIP(row.id)
|
||||
@ -193,6 +263,7 @@ export default {
|
||||
'连接设备失败',
|
||||
{
|
||||
dangerouslyUseHTMLString: true,
|
||||
closeOnClickModal: false,
|
||||
confirmButtonText: '无线配对',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
@ -218,41 +289,7 @@ export default {
|
||||
}
|
||||
row.$stopLoading = false
|
||||
},
|
||||
async handleMirror(row) {
|
||||
row.$loading = true
|
||||
try {
|
||||
await this.$scrcpy.shell(
|
||||
`--serial=${row.id} --window-title=${row.name}-${row.id} ${this.addScrcpyConfigs()}`,
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
this.$message.warning(error.message)
|
||||
}
|
||||
}
|
||||
row.$loading = false
|
||||
},
|
||||
addScrcpyConfigs() {
|
||||
const configs = storage.get('scrcpyCache') || {}
|
||||
const value = Object.entries(configs)
|
||||
.reduce((arr, [key, value]) => {
|
||||
if (!value) {
|
||||
return arr
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
arr.push(key)
|
||||
}
|
||||
else {
|
||||
arr.push(`${key}=${value}`)
|
||||
}
|
||||
return arr
|
||||
}, [])
|
||||
.join(' ')
|
||||
|
||||
console.log('addScrcpyConfigs.value', value)
|
||||
|
||||
return value
|
||||
},
|
||||
async getDeviceData() {
|
||||
this.loading = true
|
||||
await sleep()
|
||||
@ -262,6 +299,7 @@ export default {
|
||||
...item,
|
||||
name: item.model ? item.model.split(':')[1] : '未授权设备',
|
||||
$loading: false,
|
||||
$recordLoading: false,
|
||||
$stopLoading: false,
|
||||
$unauthorized: item.type === 'unauthorized',
|
||||
$wireless: isIPWithPort(item.id),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { createApp } from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
import store from './store/index.js'
|
||||
|
||||
import plugins from './plugins/index.js'
|
||||
|
||||
import 'virtual:uno.css'
|
||||
@ -8,10 +10,13 @@ import './styles/index.js'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.use(store)
|
||||
|
||||
app.use(plugins)
|
||||
|
||||
app.config.globalProperties.$electron = window.electron
|
||||
app.config.globalProperties.$adb = window.adbkit
|
||||
app.config.globalProperties.$scrcpy = window.scrcpy
|
||||
app.config.globalProperties.$path = window.nodePath
|
||||
|
||||
app.mount('#app')
|
||||
|
15
src/renderer/src/store/index.js
Normal file
15
src/renderer/src/store/index.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { createPinia } from 'pinia'
|
||||
import { useScrcpyStore } from './scrcpy/index.js'
|
||||
|
||||
export { useScrcpyStore }
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
const store = createPinia()
|
||||
|
||||
app.use(store)
|
||||
app.config.globalProperties.$store = {
|
||||
scrcpy: useScrcpyStore(),
|
||||
}
|
||||
},
|
||||
}
|
80
src/renderer/src/store/scrcpy/index.js
Normal file
80
src/renderer/src/store/scrcpy/index.js
Normal file
@ -0,0 +1,80 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import storage from '@renderer/utils/storages'
|
||||
import { cloneDeep } from 'lodash-es'
|
||||
import * as model from './model/index.js'
|
||||
|
||||
export const useScrcpyStore = defineStore({
|
||||
id: 'app-scrcpy',
|
||||
state() {
|
||||
return {
|
||||
model,
|
||||
config: storage.get('scrcpyConfig'),
|
||||
excludeKeys: ['--record', '--record-format'],
|
||||
}
|
||||
},
|
||||
getters: {
|
||||
stringConfig() {
|
||||
if (!this.config) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const value = Object.entries(this.config)
|
||||
.reduce((arr, [key, value]) => {
|
||||
if (!value) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if (this.excludeKeys.includes(key)) {
|
||||
return arr
|
||||
}
|
||||
|
||||
if (typeof value === 'boolean') {
|
||||
arr.push(key)
|
||||
}
|
||||
else {
|
||||
arr.push(`${key}=${value}`)
|
||||
}
|
||||
return arr
|
||||
}, [])
|
||||
.join(' ')
|
||||
|
||||
console.log('stringifyConfig.value', value)
|
||||
|
||||
return value
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
init() {
|
||||
this.config = this.config || this.getDefaultConfig()
|
||||
return this.config
|
||||
},
|
||||
updateConfig(value) {
|
||||
this.config = cloneDeep(value)
|
||||
storage.set('scrcpyConfig', this.config)
|
||||
},
|
||||
getModel(key, params) {
|
||||
const handler = this.model[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
|
||||
},
|
||||
},
|
||||
})
|
@ -5,14 +5,16 @@ export default () => {
|
||||
type: 'switch',
|
||||
field: '--show-touches',
|
||||
value: false,
|
||||
placeholder: '开启后将打开开发者选项中的显示点按触摸反馈(仅在物理设备上展示)',
|
||||
placeholder: '开启后将打开开发者选项中的显示点按触摸反馈',
|
||||
tips: '仅在物理设备上展示',
|
||||
},
|
||||
{
|
||||
label: '保持清醒',
|
||||
type: 'switch',
|
||||
field: '--stay-awake',
|
||||
value: false,
|
||||
placeholder: '开启以防止设备进入睡眠状态(仅有线方式连接时有效)',
|
||||
placeholder: '开启以防止设备进入睡眠状态',
|
||||
tips: '仅有线方式连接时有效',
|
||||
},
|
||||
{
|
||||
label: '关闭屏幕',
|
@ -2,3 +2,4 @@ export { default as video } from './video/index.js'
|
||||
export { default as device } from './device/index.js'
|
||||
export { default as window } from './window/index.js'
|
||||
export { default as audio } from './audio/index.js'
|
||||
export { default as record } from './record/index.js'
|
28
src/renderer/src/store/scrcpy/model/record/index.js
Normal file
28
src/renderer/src/store/scrcpy/model/record/index.js
Normal file
@ -0,0 +1,28 @@
|
||||
export default () => {
|
||||
return [
|
||||
{
|
||||
label: '录制存储路径',
|
||||
type: 'input.directory',
|
||||
field: '--record',
|
||||
value: '',
|
||||
placeholder: '默认值为执行应用的同级目录',
|
||||
},
|
||||
{
|
||||
label: '录制视频格式',
|
||||
type: 'select',
|
||||
field: '--record-format',
|
||||
value: 'mp4',
|
||||
placeholder: '默认值为 mp4',
|
||||
options: [
|
||||
{
|
||||
label: 'mp4',
|
||||
value: 'mp4',
|
||||
},
|
||||
{
|
||||
label: 'mkv',
|
||||
value: 'mkv',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
@ -22,24 +22,11 @@ export default () => {
|
||||
placeholder: '默认值为 60',
|
||||
},
|
||||
{
|
||||
label: '屏幕旋转',
|
||||
type: 'select',
|
||||
field: '--rotation',
|
||||
value: '',
|
||||
placeholder: '默认值为设备屏幕旋转角度',
|
||||
options: [
|
||||
{ label: '0°', value: '0' },
|
||||
{ label: '-90°', value: '1' },
|
||||
{ label: '180°', value: '2' },
|
||||
{ label: '90°', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '解码器',
|
||||
label: '视频解码器',
|
||||
type: 'select',
|
||||
field: '--video-codec',
|
||||
value: '',
|
||||
placeholder: '解码器默认值为 h264',
|
||||
placeholder: '默认值为 h264',
|
||||
options: [
|
||||
{
|
||||
label: 'h264',
|
||||
@ -56,11 +43,11 @@ export default () => {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '编码器',
|
||||
label: '视频编码器',
|
||||
type: 'select',
|
||||
field: '--video-encoder',
|
||||
value: '',
|
||||
placeholder: '编码器默认值为 h264',
|
||||
placeholder: '默认值为设备默认编码器',
|
||||
// "[server] INFO: List of video encoders:"
|
||||
// "--video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'"
|
||||
// "--video-codec=h264 --video-encoder='c2.android.avc.encoder'"
|
||||
@ -69,26 +56,86 @@ export default () => {
|
||||
// "--video-codec=h265 --video-encoder='c2.android.hevc.encoder'"
|
||||
options: [
|
||||
{
|
||||
label: 'OMX.qcom.video.encoder.avc',
|
||||
label: 'Qualcomm AVC(H.264) 视频编码器',
|
||||
value: 'OMX.qcom.video.encoder.avc',
|
||||
},
|
||||
{
|
||||
label: 'c2.android.avc.encoder',
|
||||
label: 'Android AVC(H.264) 视频编码器',
|
||||
value: 'c2.android.avc.encoder',
|
||||
},
|
||||
{
|
||||
label: 'OMX.google.h264.encoder',
|
||||
label: 'Google H.264(AVC) 视频编码器',
|
||||
value: 'OMX.google.h264.encoder',
|
||||
},
|
||||
{
|
||||
label: 'OMX.qcom.video.encoder.hevc',
|
||||
label: 'Qualcomm HEVC(H.265) 视频编码器',
|
||||
value: 'OMX.qcom.video.encoder.hevc',
|
||||
},
|
||||
{
|
||||
label: 'c2.android.hevc.encoder',
|
||||
label: 'Android HEVC(H.265) 视频编码器',
|
||||
value: 'c2.android.hevc.encoder',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '屏幕旋转',
|
||||
type: 'select',
|
||||
field: '--rotation',
|
||||
value: '',
|
||||
placeholder: '默认值为设备屏幕旋转角度',
|
||||
options: [
|
||||
{ label: '0°', value: '0' },
|
||||
{ label: '-90°', value: '1' },
|
||||
{ label: '180°', value: '2' },
|
||||
{ label: '90°', value: '3' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '屏幕裁剪',
|
||||
type: 'input',
|
||||
field: '--crop',
|
||||
value: '',
|
||||
placeholder: '默认不裁剪,格式为 1224:1440:0:0',
|
||||
},
|
||||
{
|
||||
label: '多显示器',
|
||||
type: 'select',
|
||||
field: '--display',
|
||||
value: '',
|
||||
placeholder: '默认值为 0(主屏幕)',
|
||||
options: [
|
||||
{ label: '0', value: '0' },
|
||||
{ label: '1', value: '1' },
|
||||
{ label: '2', value: '2' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: '视频缓冲',
|
||||
type: 'input.number',
|
||||
field: '--display-buffer',
|
||||
value: '',
|
||||
placeholder: '单位为 ms,默认值为 0ms',
|
||||
},
|
||||
{
|
||||
label: '音频缓冲',
|
||||
type: 'input.number',
|
||||
field: '--audio-buffer',
|
||||
value: '',
|
||||
placeholder: '单位为 ms,默认值为 0ms',
|
||||
},
|
||||
{
|
||||
label: '接收器(v4l2)缓冲',
|
||||
type: 'input.number',
|
||||
field: '--v4l2-buffer',
|
||||
value: '',
|
||||
placeholder: '单位为 ms,默认值为 0ms',
|
||||
},
|
||||
{
|
||||
label: '禁用视频',
|
||||
type: 'switch',
|
||||
field: '--no-video',
|
||||
value: false,
|
||||
placeholder: '开启后将禁用视频',
|
||||
},
|
||||
]
|
||||
}
|
Loading…
Reference in New Issue
Block a user