diff --git a/README-CN.md b/README-CN.md
index 9b03ed5..9fe4dd3 100644
--- a/README-CN.md
+++ b/README-CN.md
@@ -174,8 +174,8 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
10. 对深色模式的支持 ✅
11. 添加 Gnirehtet 反向供网功能 ✅
12. 添加新的相机镜像相关功能 ✅
-13. 添加独立的剪切板同步功能 🚧
-14. 更好的多屏协同 🚧
+13. 更好的多屏协同 ✅
+14. 添加独立的剪切板同步功能 🚧
15. 添加 Scrcpy 快捷键查询页面 🚧
16. 添加对游戏的增强功能,如游戏键位映射 🚧
diff --git a/README.md b/README.md
index d75e196..fde878b 100644
--- a/README.md
+++ b/README.md
@@ -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 🚧
diff --git a/electron/exposes/adbkit/index.js b/electron/exposes/adbkit/index.js
index faf8b17..d0d86a7 100644
--- a/electron/exposes/adbkit/index.js
+++ b/electron/exposes/adbkit/index.js
@@ -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,
}
}
diff --git a/electron/exposes/index.js b/electron/exposes/index.js
index 0ebc81d..c203d94 100644
--- a/electron/exposes/index.js
+++ b/electron/exposes/index.js
@@ -28,7 +28,7 @@ export default {
expose('adbkit', adbkitExecute)
- expose('scrcpy', scrcpy())
+ expose('scrcpy', scrcpy({ adbkit: adbkitExecute }))
expose('gnirehtet', gnirehtet({ adbkit: adbkitExecute }))
},
diff --git a/electron/exposes/scrcpy/index.js b/electron/exposes/scrcpy/index.js
index fb7296b..e124baf 100644
--- a/electron/exposes/scrcpy/index.js
+++ b/electron/exposes/scrcpy/index.js
@@ -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 () => ({
- shell,
- execShell,
- getEncoders,
- mirror,
- record,
-})
+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,
+ }
+}
diff --git a/jsconfig.json b/jsconfig.json
index bc7e623..6e35d74 100644
--- a/jsconfig.json
+++ b/jsconfig.json
@@ -4,7 +4,8 @@
"paths": {
"@/*": ["src/*"],
"@root/*": ["*"],
- "@electron/*": ["electron/*"]
+ "@electron/*": ["electron/*"],
+ "@renderer/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist", "dist-electron", "dist-release"],
diff --git a/src/components/Device/components/ControlBar/MirrorGroup/index.vue b/src/components/Device/components/ControlBar/MirrorGroup/index.vue
new file mode 100644
index 0000000..cf6be0a
--- /dev/null
+++ b/src/components/Device/components/ControlBar/MirrorGroup/index.vue
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+ {{ $t("device.control.mirror-group.open", { num: item }) }}
+
+
+
+
+
+
+
+
+
diff --git a/src/components/Device/components/ControlBar/index.vue b/src/components/Device/components/ControlBar/index.vue
index fe9bea4..7d91096 100644
--- a/src/components/Device/components/ControlBar/index.vue
+++ b/src/components/Device/components/ControlBar/index.vue
@@ -18,22 +18,29 @@
: {}),
}"
>
-
-
-
-
-
-
-
- {{ $t(item.label) }}
-
+
+
+
+
+
+
+
+
+ {{ $t(item.label) }}
+
+
@@ -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',
+ },
],
}
},
diff --git a/src/icons/svg/multi-screen.svg b/src/icons/svg/multi-screen.svg
new file mode 100644
index 0000000..555fe26
--- /dev/null
+++ b/src/icons/svg/multi-screen.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/locales/languages/en_US.json b/src/locales/languages/en_US.json
index 2225845..c7ba07d 100644
--- a/src/locales/languages/en_US.json
+++ b/src/locales/languages/en_US.json
@@ -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",
diff --git a/src/locales/languages/zh_CN.json b/src/locales/languages/zh_CN.json
index 0c847ef..74eb4f3 100644
--- a/src/locales/languages/zh_CN.json
+++ b/src/locales/languages/zh_CN.json
@@ -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": "无边框模式",
diff --git a/src/store/preference/index.js b/src/store/preference/index.js
index a954c7d..ea12f90 100644
--- a/src/store/preference/index.js
+++ b/src/store/preference/index.js
@@ -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
diff --git a/src/store/preference/model/device/index.js b/src/store/preference/model/device/index.js
index 9d939f9..013cae6 100644
--- a/src/store/preference/model/device/index.js
+++ b/src/store/preference/model/device/index.js
@@ -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',
+ },
},
}
diff --git a/vite.config.js b/vite.config.js
index 8d6a560..d77dedb 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -17,6 +17,7 @@ const merge = (config, { command = '' } = {}) =>
alias: {
'@root': resolve(),
'@electron': resolve('electron'),
+ '@renderer': resolve('src'),
},
},
plugins: [...(command === 'serve' ? [notBundle()] : [])],