feat: 🎉 Add mirror group function

This commit is contained in:
viarotel 2023-11-07 14:09:09 +08:00
parent df1d9da65e
commit 0c9d36fddb
14 changed files with 218 additions and 32 deletions

View File

@ -174,8 +174,8 @@ Windows 及 Linux 端内部集成了 Gnirehtet 用于提供 PC 到安卓设
10. 对深色模式的支持 ✅ 10. 对深色模式的支持 ✅
11. 添加 Gnirehtet 反向供网功能 ✅ 11. 添加 Gnirehtet 反向供网功能 ✅
12. 添加新的相机镜像相关功能 ✅ 12. 添加新的相机镜像相关功能 ✅
13. 添加独立的剪切板同步功能 🚧 13. 更好的多屏协同 ✅
14. 更好的多屏协同 🚧 14. 添加独立的剪切板同步功能 🚧
15. 添加 Scrcpy 快捷键查询页面 🚧 15. 添加 Scrcpy 快捷键查询页面 🚧
16. 添加对游戏的增强功能,如游戏键位映射 🚧 16. 添加对游戏的增强功能,如游戏键位映射 🚧

View File

@ -172,8 +172,8 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
10. Support for dark mode ✅ 10. Support for dark mode ✅
11. Add Gnirehtet reverse network function ✅ 11. Add Gnirehtet reverse network function ✅
12. Add new camera mirror related features ✅ 12. Add new camera mirror related features ✅
13. Add an clipboard synchronization function 🚧 13. Better multi -screen collaboration ✅
14. Better multi -screen collaboration 🚧 14. Add an clipboard synchronization function 🚧
15. Add Scrcpy shortcut key query page 🚧 15. Add Scrcpy shortcut key query page 🚧
16. Add game enhancement features such as game keyboard mapping 🚧 16. Add game enhancement features such as game keyboard mapping 🚧

View File

@ -125,6 +125,13 @@ const display = async (deviceId) => {
return value return value
} }
const clearOverlayDisplayDevices = async (deviceId) => {
return deviceShell(
deviceId,
'settings put global overlay_display_devices none',
)
}
const watch = async (callback) => { const watch = async (callback) => {
const tracker = await client.trackDevices() const tracker = await client.trackDevices()
tracker.on('add', async (ret) => { tracker.on('add', async (ret) => {
@ -174,6 +181,7 @@ export default () => {
isInstalled, isInstalled,
version, version,
display, display,
clearOverlayDisplayDevices,
watch, watch,
} }
} }

View File

@ -28,7 +28,7 @@ export default {
expose('adbkit', adbkitExecute) expose('adbkit', adbkitExecute)
expose('scrcpy', scrcpy()) expose('scrcpy', scrcpy({ adbkit: adbkitExecute }))
expose('gnirehtet', gnirehtet({ adbkit: adbkitExecute })) expose('gnirehtet', gnirehtet({ adbkit: adbkitExecute }))
}, },

View File

@ -2,6 +2,9 @@ import util from 'node:util'
import { exec as _exec, spawn } from 'node:child_process' import { exec as _exec, spawn } from 'node:child_process'
import appStore from '@electron/helpers/store.js' import appStore from '@electron/helpers/store.js'
import { adbPath, scrcpyPath } from '@electron/configs/index.js' import { adbPath, scrcpyPath } from '@electron/configs/index.js'
import { replaceIP, sleep } from '@renderer/utils/index.js'
let adbkit
const exec = util.promisify(_exec) const exec = util.promisify(_exec)
@ -102,8 +105,13 @@ const getEncoders = async (serial) => {
return value return value
} }
const mirror = async (serial, { title, args = '', ...options } = {}) => { const mirror = async (
return shell( serial,
{ title, args = '', exec = false, ...options } = {},
) => {
const mirrorShell = exec ? execShell : shell
return mirrorShell(
`--serial="${serial}" --window-title="${title}" ${args}`, `--serial="${serial}" --window-title="${title}" ${args}`,
options, options,
) )
@ -119,10 +127,74 @@ const record = async (
) )
} }
export default () => ({ const mirrorGroup = async (serial, { open = 1, ...options } = {}) => {
shell, const overlayDisplay
execShell, = appStore.get(`scrcpy.${replaceIP(serial)}.--display-overlay`)
getEncoders, || appStore.get('scrcpy.global.--display-overlay')
mirror, || '1080x1920/320,secure'
record,
}) const command = `settings put global overlay_display_devices "${[
...Array.from({ length: open }).keys(),
]
.map(() => overlayDisplay)
.join(';')}"`
await adbkit.deviceShell(serial, command)
await sleep()
const displayList = await adbkit.display(serial, command)
const filterList = displayList.filter(item => item !== '0')
console.log('filterList', filterList)
const results = []
for (let index = 0; index < filterList.length; index++) {
const displayId = filterList[index]
let args = options.args || ''
if (args.includes('--display-id')) {
args = args.replace(/(--display-id=)"[^"]*"/, `$1"${displayId}"`)
}
else {
args += ` --display-id="${displayId}"`
}
const title = options?.title?.({ displayId, index }) || options?.title
const promise = mirror(serial, {
...options,
title,
args,
exec: true,
}).catch((error) => {
console.warn(
'error',
error?.message
|| error?.cause?.message
|| `display-id-${displayId}: Open failed`,
)
})
results.push(promise)
await sleep(1500)
}
return Promise.allSettled(results)
}
export default (options = {}) => {
adbkit = options.adbkit
return {
shell,
execShell,
getEncoders,
mirror,
record,
mirrorGroup,
}
}

View File

@ -4,7 +4,8 @@
"paths": { "paths": {
"@/*": ["src/*"], "@/*": ["src/*"],
"@root/*": ["*"], "@root/*": ["*"],
"@electron/*": ["electron/*"] "@electron/*": ["electron/*"],
"@renderer/*": ["src/*"]
} }
}, },
"exclude": ["node_modules", "dist", "dist-electron", "dist-release"], "exclude": ["node_modules", "dist", "dist-electron", "dist-release"],

View File

@ -0,0 +1,66 @@
<template>
<el-dropdown :disabled="loading" @command="handleMirror">
<div class="">
<slot :loading="loading" />
</div>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="item of 4" :key="item" :command="item">
{{ $t("device.control.mirror-group.open", { num: item }) }}
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</template>
<script>
export default {
props: {
device: {
type: Object,
default: () => ({}),
},
},
data() {
return {
loading: false,
}
},
methods: {
scrcpyArgs(...args) {
return this.$store.preference.getScrcpyArgs(...args)
},
preferenceData(...args) {
return this.$store.preference.getData(...args)
},
async handleMirror(open) {
console.log('handleMirror.open', open)
this.loading = true
try {
await this.$scrcpy.mirrorGroup(this.device.id, {
open,
title: ({ displayId }) =>
`${this.device.$remark ? `${this.device.$remark}-` : ''}${
this.device.$name
}-${this.device.id}-display-${displayId}`,
args: this.scrcpyArgs(this.device.id),
})
}
catch (error) {
console.warn(error.message)
if (error?.message || error?.cause?.message) {
this.$message.warning(error?.message || error?.cause?.message)
}
}
this.$adb.clearOverlayDisplayDevices(this.device.id)
this.loading = false
},
},
}
</script>
<style></style>

View File

@ -18,22 +18,29 @@
: {}), : {}),
}" }"
> >
<el-button <template #default="{ loading = false } = {}">
type="primary" <el-button
plain type="primary"
class="!border-none !mx-0 bg-transparent !rounded-0" plain
:disabled="device.$unauthorized" class="!border-none !mx-0 bg-transparent !rounded-0"
:title="item.tips ? $t(item.tips) : ''" :disabled="device.$unauthorized"
@wheel.prevent="onWheel" :title="item.tips ? $t(item.tips) : ''"
> :loading="loading"
<template #icon> @wheel.prevent="onWheel"
<svg-icon v-if="item.svgIcon" :name="item.svgIcon"></svg-icon> >
<el-icon v-else-if="item.elIcon" class=""> <template #icon>
<component :is="item.elIcon" /> <svg-icon
</el-icon> v-if="item.svgIcon"
</template> :name="item.svgIcon"
{{ $t(item.label) }} :class="item.iconClass"
</el-button> ></svg-icon>
<el-icon v-else-if="item.elIcon" :class="item.iconClass">
<component :is="item.elIcon" />
</el-icon>
</template>
{{ $t(item.label) }}
</el-button>
</template>
</component> </component>
</div> </div>
</template> </template>
@ -42,12 +49,14 @@
import Screenshot from './Screenshot/index.vue' import Screenshot from './Screenshot/index.vue'
import AppInstall from './AppInstall/index.vue' import AppInstall from './AppInstall/index.vue'
import Gnirehtet from './Gnirehtet/index.vue' import Gnirehtet from './Gnirehtet/index.vue'
import MirrorGroup from './MirrorGroup/index.vue'
export default { export default {
components: { components: {
Screenshot, Screenshot,
AppInstall, AppInstall,
Gnirehtet, Gnirehtet,
MirrorGroup,
}, },
props: { props: {
device: { device: {
@ -108,6 +117,13 @@ export default {
component: 'Gnirehtet', component: 'Gnirehtet',
tips: 'device.control.gnirehtet.tips', tips: 'device.control.gnirehtet.tips',
}, },
{
label: 'device.control.mirror-group.name',
svgIcon: 'multi-screen',
iconClass: '',
component: 'MirrorGroup',
tips: 'device.control.mirror-group.tips',
},
], ],
} }
}, },

View File

@ -0,0 +1 @@
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1699319726512" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="20556" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M787.692308 669.538462h-118.153846v118.153846a118.153846 118.153846 0 0 1-118.153847 118.153846H196.923077a118.153846 118.153846 0 0 1-118.153846-118.153846v-354.461539a118.153846 118.153846 0 0 1 118.153846-118.153846h118.153846V196.923077a118.153846 118.153846 0 0 1 118.153846-118.153846h354.461539a118.153846 118.153846 0 0 1 118.153846 118.153846v354.461538a118.153846 118.153846 0 0 1-118.153846 118.153847zM196.923077 374.153846A59.076923 59.076923 0 0 0 137.846154 433.230769v354.461539A59.076923 59.076923 0 0 0 196.923077 846.769231h354.461538a59.076923 59.076923 0 0 0 59.076923-59.076923v-354.461539A59.076923 59.076923 0 0 0 551.384615 374.153846H196.923077zM846.769231 196.923077A59.076923 59.076923 0 0 0 787.692308 137.846154h-354.461539A59.076923 59.076923 0 0 0 374.153846 196.923077v118.153846H551.384615a118.153846 118.153846 0 0 1 118.153847 118.153846v177.230769h118.153846a59.076923 59.076923 0 0 0 59.076923-59.076923V196.923077z" p-id="20557"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -97,6 +97,9 @@
"device.control.gnirehtet.tips": "Gnirehtet provides reverse tethering for Android; Note: Initial connection requires authorization on the device.", "device.control.gnirehtet.tips": "Gnirehtet provides reverse tethering for Android; Note: Initial connection requires authorization on the device.",
"device.control.gnirehtet.progress": "Starting Gnirehtet reverse tethering service...", "device.control.gnirehtet.progress": "Starting Gnirehtet reverse tethering service...",
"device.control.gnirehtet.success": "Gnirehtet reverse tethering feature started successfully", "device.control.gnirehtet.success": "Gnirehtet reverse tethering feature started successfully",
"device.control.mirror-group.name": "Mirror Group",
"device.control.mirror-group.tips": "When enabled, can mirror multiple simulated secondary displays and achieve multi-screen collaboration by operating each mirrored window. Note this requires ROM support and desktop mode enabled.",
"device.control.mirror-group.open": "Open {num} windows",
"preferences.name": "Preferences", "preferences.name": "Preferences",
"preferences.reset": "Reset to Default", "preferences.reset": "Reset to Default",
@ -187,6 +190,9 @@
"preferences.device.control-in-stop-charging.name": "Stop Charging", "preferences.device.control-in-stop-charging.name": "Stop Charging",
"preferences.device.control-in-stop-charging.placeholder": "Stop charging when controlling", "preferences.device.control-in-stop-charging.placeholder": "Stop charging when controlling",
"preferences.device.control-in-stop-charging.tips": "May not work on some models", "preferences.device.control-in-stop-charging.tips": "May not work on some models",
"preferences.device.display-overlay.name": "Simulated Display",
"preferences.device.display-overlay.placeholder": "Size and resolution of simulated secondary display, default 1080x1920/320,secure",
"preferences.device.display-overlay.tips": "Mirroring group relies on this option",
"preferences.window.name": "Window", "preferences.window.name": "Window",
"preferences.window.borderless.name": "Borderless", "preferences.window.borderless.name": "Borderless",

View File

@ -95,6 +95,9 @@
"device.control.gnirehtet.tips": "使用 Gnirehtet 为 Android 提供反向网络共享;注意:首次连接需要在设备上进行授权", "device.control.gnirehtet.tips": "使用 Gnirehtet 为 Android 提供反向网络共享;注意:首次连接需要在设备上进行授权",
"device.control.gnirehtet.progress": "正在启动 Gnirehtet 反向供网服务中...", "device.control.gnirehtet.progress": "正在启动 Gnirehtet 反向供网服务中...",
"device.control.gnirehtet.success": "Gnirehtet 反向网络共享功能启动成功", "device.control.gnirehtet.success": "Gnirehtet 反向网络共享功能启动成功",
"device.control.mirror-group.name": "多屏协同",
"device.control.mirror-group.tips": "开启后,可以同时镜像多个模拟辅助显示设备,并通过操作各个镜像窗口实现多屏协同功能。请注意,此功能需要手机 ROM 支持,并且必须开启强制使用桌面模式选项。",
"device.control.mirror-group.open": "开启 {num} 个窗口",
"preferences.name": "偏好设置", "preferences.name": "偏好设置",
"preferences.reset": "恢复默认值", "preferences.reset": "恢复默认值",
@ -185,6 +188,9 @@
"preferences.device.control-in-stop-charging.name": "控制时停止充电", "preferences.device.control-in-stop-charging.name": "控制时停止充电",
"preferences.device.control-in-stop-charging.placeholder": "开启后控制设备时将停止充电", "preferences.device.control-in-stop-charging.placeholder": "开启后控制设备时将停止充电",
"preferences.device.control-in-stop-charging.tips": "某些机型上似乎不起作用", "preferences.device.control-in-stop-charging.tips": "某些机型上似乎不起作用",
"preferences.device.display-overlay.name": "模拟辅助显示器",
"preferences.device.display-overlay.placeholder": "用于调整模拟辅助显示器的大小和分辨率,默认值为 1080x1920/320,secure",
"preferences.device.display-overlay.tips": "多屏协同(镜像组)依赖于此选项",
"preferences.window.name": "窗口控制", "preferences.window.name": "窗口控制",
"preferences.window.borderless.name": "无边框模式", "preferences.window.borderless.name": "无边框模式",

View File

@ -39,6 +39,7 @@ export const usePreferenceStore = defineStore({
data: { ...getDefaultData() }, data: { ...getDefaultData() },
deviceScope, deviceScope,
excludeKeys: [ excludeKeys: [
'--display-overlay',
'--camera', '--camera',
'--video-code', '--video-code',
'--audio-code', '--audio-code',
@ -169,7 +170,7 @@ export const usePreferenceStore = defineStore({
arr.push(key) arr.push(key)
} }
else { else {
arr.push(`${key}=${value}`) arr.push(`${key}="${value}"`)
} }
return arr return arr

View File

@ -40,5 +40,13 @@ export default {
placeholder: 'preferences.device.control-in-stop-charging.placeholder', placeholder: 'preferences.device.control-in-stop-charging.placeholder',
tips: 'preferences.device.control-in-stop-charging.tips', tips: 'preferences.device.control-in-stop-charging.tips',
}, },
overlayDisplay: {
label: 'preferences.device.display-overlay.name',
field: '--display-overlay',
type: 'Input',
value: '',
placeholder: 'preferences.device.display-overlay.placeholder',
tips: 'preferences.device.display-overlay.tips',
},
}, },
} }

View File

@ -17,6 +17,7 @@ const merge = (config, { command = '' } = {}) =>
alias: { alias: {
'@root': resolve(), '@root': resolve(),
'@electron': resolve('electron'), '@electron': resolve('electron'),
'@renderer': resolve('src'),
}, },
}, },
plugins: [...(command === 'serve' ? [notBundle()] : [])], plugins: [...(command === 'serve' ? [notBundle()] : [])],