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. 对深色模式的支持 ✅
11. 添加 Gnirehtet 反向供网功能 ✅
12. 添加新的相机镜像相关功能 ✅
13. 添加独立的剪切板同步功能 🚧
14. 更好的多屏协同 🚧
13. 更好的多屏协同 ✅
14. 添加独立的剪切板同步功能 🚧
15. 添加 Scrcpy 快捷键查询页面 🚧
16. 添加对游戏的增强功能,如游戏键位映射 🚧

View File

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

View File

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

View File

@ -28,7 +28,7 @@ export default {
expose('adbkit', adbkitExecute)
expose('scrcpy', scrcpy())
expose('scrcpy', scrcpy({ 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 appStore from '@electron/helpers/store.js'
import { adbPath, scrcpyPath } from '@electron/configs/index.js'
import { replaceIP, sleep } from '@renderer/utils/index.js'
let adbkit
const exec = util.promisify(_exec)
@ -102,8 +105,13 @@ const getEncoders = async (serial) => {
return value
}
const mirror = async (serial, { title, args = '', ...options } = {}) => {
return shell(
const mirror = async (
serial,
{ title, args = '', exec = false, ...options } = {},
) => {
const mirrorShell = exec ? execShell : shell
return mirrorShell(
`--serial="${serial}" --window-title="${title}" ${args}`,
options,
)
@ -119,10 +127,74 @@ const record = async (
)
}
export default () => ({
const mirrorGroup = async (serial, { open = 1, ...options } = {}) => {
const overlayDisplay
= appStore.get(`scrcpy.${replaceIP(serial)}.--display-overlay`)
|| appStore.get('scrcpy.global.--display-overlay')
|| '1080x1920/320,secure'
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": {
"@/*": ["src/*"],
"@root/*": ["*"],
"@electron/*": ["electron/*"]
"@electron/*": ["electron/*"],
"@renderer/*": ["src/*"]
}
},
"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 @@
: {}),
}"
>
<template #default="{ loading = false } = {}">
<el-button
type="primary"
plain
class="!border-none !mx-0 bg-transparent !rounded-0"
:disabled="device.$unauthorized"
:title="item.tips ? $t(item.tips) : ''"
:loading="loading"
@wheel.prevent="onWheel"
>
<template #icon>
<svg-icon v-if="item.svgIcon" :name="item.svgIcon"></svg-icon>
<el-icon v-else-if="item.elIcon" class="">
<svg-icon
v-if="item.svgIcon"
:name="item.svgIcon"
:class="item.iconClass"
></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>
</div>
</template>
@ -42,12 +49,14 @@
import Screenshot from './Screenshot/index.vue'
import AppInstall from './AppInstall/index.vue'
import Gnirehtet from './Gnirehtet/index.vue'
import MirrorGroup from './MirrorGroup/index.vue'
export default {
components: {
Screenshot,
AppInstall,
Gnirehtet,
MirrorGroup,
},
props: {
device: {
@ -108,6 +117,13 @@ export default {
component: 'Gnirehtet',
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.progress": "Starting Gnirehtet reverse tethering service...",
"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.reset": "Reset to Default",
@ -187,6 +190,9 @@
"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.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.borderless.name": "Borderless",

View File

@ -95,6 +95,9 @@
"device.control.gnirehtet.tips": "使用 Gnirehtet 为 Android 提供反向网络共享;注意:首次连接需要在设备上进行授权",
"device.control.gnirehtet.progress": "正在启动 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.reset": "恢复默认值",
@ -185,6 +188,9 @@
"preferences.device.control-in-stop-charging.name": "控制时停止充电",
"preferences.device.control-in-stop-charging.placeholder": "开启后控制设备时将停止充电",
"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.borderless.name": "无边框模式",

View File

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

View File

@ -40,5 +40,13 @@ export default {
placeholder: 'preferences.device.control-in-stop-charging.placeholder',
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: {
'@root': resolve(),
'@electron': resolve('electron'),
'@renderer': resolve('src'),
},
},
plugins: [...(command === 'serve' ? [notBundle()] : [])],