mirror of
https://github.com/viarotel-org/escrcpy.git
synced 2024-11-27 17:00:53 +01:00
feat: ✨ Supports starting applications for mirroring
This commit is contained in:
parent
ce6ea8e39d
commit
d19e781471
@ -28,7 +28,7 @@
|
||||
icon="ArrowDown"
|
||||
@click="switchDevice"
|
||||
>
|
||||
<span class="mr-2">{{ deviceInfo.$remark || deviceInfo.$name }}</span>
|
||||
<span class="mr-2">{{ deviceName }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
@ -79,6 +79,8 @@ const deviceInfo = ref({})
|
||||
|
||||
const deviceList = ref([])
|
||||
|
||||
const deviceName = computed(() => deviceStore.getLabel(deviceInfo.value, ({ deviceName }) => deviceName))
|
||||
|
||||
function handleClose() {
|
||||
window.electron.ipcRenderer.send('hide-active-window')
|
||||
}
|
||||
@ -88,7 +90,17 @@ async function switchDevice(e) {
|
||||
|
||||
const data = await deviceStore.getList()
|
||||
|
||||
window.electron.ipcRenderer.send('open-control-device-menu', data)
|
||||
const options = data.map((item) => {
|
||||
return {
|
||||
label: deviceStore.getLabel(item, ({ deviceName }) => deviceName),
|
||||
value: item,
|
||||
}
|
||||
})
|
||||
|
||||
window.electron.ipcRenderer.send('open-system-menu', {
|
||||
channel: 'device-change',
|
||||
options,
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -1,24 +0,0 @@
|
||||
import { BrowserWindow, ipcMain, Menu } from 'electron'
|
||||
import { openControlWindow } from '$control/electron/helpers/index.js'
|
||||
|
||||
export default function (controlWindow) {
|
||||
ipcMain.on('open-control-device-menu', (event, deviceList) => {
|
||||
const template = deviceList.map((item) => {
|
||||
let label = item.$remark || item.$name
|
||||
|
||||
if (item.$wifi) {
|
||||
label += ` (WIFI)`
|
||||
}
|
||||
|
||||
return {
|
||||
label,
|
||||
click: () => {
|
||||
openControlWindow(controlWindow, item)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
menu.popup(BrowserWindow.fromWebContents(event.sender))
|
||||
})
|
||||
}
|
@ -1,4 +1 @@
|
||||
export { default as devices } from './devices/index.js'
|
||||
export { default as gnirehtet } from './gnirehtet/index.js'
|
||||
export { default as rotation } from './rotation/index.js'
|
||||
export { default as volume } from './volume/index.js'
|
||||
export { default as menu } from './menu/index.js'
|
||||
|
@ -1,16 +1,16 @@
|
||||
import { BrowserWindow, ipcMain, Menu } from 'electron'
|
||||
|
||||
export default function (controlWindow) {
|
||||
ipcMain.on('open-device-gnirehtet-menu', openDeviceGnirehtetMenu)
|
||||
ipcMain.on('open-system-menu', openSystemMenu)
|
||||
|
||||
function openDeviceGnirehtetMenu(event, args = {}) {
|
||||
const { options = [] } = args
|
||||
function openSystemMenu(event, args = {}) {
|
||||
const { options = [], channel = 'system-menu-click' } = args
|
||||
|
||||
const template = options.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
click() {
|
||||
controlWindow.webContents.send(item.value)
|
||||
controlWindow.webContents.send(channel, item.value)
|
||||
},
|
||||
}
|
||||
})
|
@ -1,24 +0,0 @@
|
||||
import { BrowserWindow, ipcMain, Menu } from 'electron'
|
||||
|
||||
export default function (controlWindow) {
|
||||
ipcMain.on('open-device-rotation-menu', openDeviceRotationMenu)
|
||||
|
||||
function openDeviceRotationMenu(event, args = {}) {
|
||||
const { options = [] } = args
|
||||
|
||||
const template = options.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
click: () => {
|
||||
controlWindow.webContents.send(
|
||||
'execute-device-rotation-shell',
|
||||
item.value,
|
||||
)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
menu.popup(BrowserWindow.fromWebContents(event.sender))
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import { BrowserWindow, ipcMain, Menu } from 'electron'
|
||||
|
||||
export default function (controlWindow) {
|
||||
ipcMain.on('open-device-volume-menu', openDeviceVolumeMenu)
|
||||
|
||||
function openDeviceVolumeMenu(event, args = {}) {
|
||||
const { options = [] } = args
|
||||
|
||||
const template = options.map((item) => {
|
||||
return {
|
||||
label: item.label,
|
||||
click() {
|
||||
controlWindow.webContents.send('execute-device-volume-shell', item.value)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
const menu = Menu.buildFromTemplate(template)
|
||||
menu.popup(BrowserWindow.fromWebContents(event.sender))
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import { BrowserWindow, ipcMain } from 'electron'
|
||||
import { initControlWindow, openControlWindow } from './helpers/index.js'
|
||||
|
||||
import { devices, gnirehtet, rotation, volume } from './events/index.js'
|
||||
import { menu } from './events/index.js'
|
||||
|
||||
function onControlMounted(controlWindow) {
|
||||
ipcMain.on('language-change', (event, data) => {
|
||||
@ -12,10 +12,7 @@ function onControlMounted(controlWindow) {
|
||||
controlWindow.webContents.send('theme-change', data)
|
||||
})
|
||||
|
||||
rotation(controlWindow)
|
||||
devices(controlWindow)
|
||||
volume(controlWindow)
|
||||
gnirehtet(controlWindow)
|
||||
menu(controlWindow)
|
||||
}
|
||||
|
||||
export default (mainWindow) => {
|
||||
|
43
electron/exposes/scrcpy/helper.js
Normal file
43
electron/exposes/scrcpy/helper.js
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Parse scrcpy app list output into structured data
|
||||
* @param {string} rawText - Raw text output from scrcpy --list-apps command
|
||||
* @returns {Array<{
|
||||
* name: string,
|
||||
* packageName: string,
|
||||
* isSystemApp: boolean
|
||||
* }>} Array of parsed app objects
|
||||
*/
|
||||
export function parseScrcpyAppList(rawText) {
|
||||
try {
|
||||
// Split by lines and filter out non-app lines
|
||||
const lines = rawText.split('\n').filter((line) => {
|
||||
const trimmed = line.trim()
|
||||
return trimmed.startsWith('*') || trimmed.startsWith('-')
|
||||
})
|
||||
|
||||
return lines.map((line) => {
|
||||
// Remove leading * or - and trim
|
||||
const cleanLine = line.trim().replace(/^[*\-]\s+/, '')
|
||||
|
||||
// Extract app name and package name using a more precise regex
|
||||
// Matches any characters up to the last [ followed by package name and ]
|
||||
const match = cleanLine.match(/^([^[]+)\[([^\]]+)\]$/)
|
||||
|
||||
if (!match) {
|
||||
return null
|
||||
}
|
||||
|
||||
const [, name, packageName] = match
|
||||
|
||||
return {
|
||||
name: name.trim(),
|
||||
packageName: packageName.trim(),
|
||||
isSystemApp: line.trim().startsWith('*'),
|
||||
}
|
||||
}).filter(item => item !== null)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error parsing scrcpy app list:', error)
|
||||
return []
|
||||
}
|
||||
}
|
@ -5,11 +5,13 @@ import appStore from '$electron/helpers/store.js'
|
||||
import { replaceIP, sleep } from '$renderer/utils/index.js'
|
||||
import commandHelper from '$renderer/utils/command/index.js'
|
||||
|
||||
import { parseScrcpyAppList } from './helper.js'
|
||||
|
||||
let adbkit
|
||||
|
||||
const exec = util.promisify(_exec)
|
||||
|
||||
async function shell(command, { stdout, stderr, ...options } = {}) {
|
||||
async function shell(command, { stdout, stderr, signal, ...options } = {}) {
|
||||
const spawnPath = appStore.get('common.scrcpyPath') || scrcpyPath
|
||||
const ADB = appStore.get('common.adbPath') || adbPath
|
||||
const args = command.split(' ')
|
||||
@ -21,15 +23,23 @@ async function shell(command, { stdout, stderr, ...options } = {}) {
|
||||
...options,
|
||||
})
|
||||
|
||||
const stderrList = []
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
scrcpyProcess.stdout.on('data', (data) => {
|
||||
const stringData = data.toString()
|
||||
|
||||
if (stdout) {
|
||||
stdout(stringData, scrcpyProcess)
|
||||
}
|
||||
|
||||
const matchList = stringData.match(signal)
|
||||
|
||||
if (matchList) {
|
||||
resolve(matchList, stringData, scrcpyProcess)
|
||||
}
|
||||
})
|
||||
|
||||
const stderrList = []
|
||||
scrcpyProcess.stderr.on('data', (data) => {
|
||||
const stringData = data.toString()
|
||||
|
||||
@ -42,7 +52,6 @@ async function shell(command, { stdout, stderr, ...options } = {}) {
|
||||
}
|
||||
})
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
scrcpyProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve()
|
||||
@ -184,6 +193,31 @@ async function helper(
|
||||
)
|
||||
}
|
||||
|
||||
async function getAppList(serial) {
|
||||
const res = await execShell(`--serial="${serial}" --list-apps`)
|
||||
|
||||
const stdout = res.stdout
|
||||
const value = parseScrcpyAppList(stdout)
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
async function startApp(serial, args = {}) {
|
||||
let { commands, packageName, ...options } = args
|
||||
|
||||
commands += ` --new-display --start-app=${packageName}`
|
||||
|
||||
const res = await mirror(serial, { ...options, args: commands, signal: /display id: (\d+)/i })
|
||||
|
||||
const displayId = res?.[1]
|
||||
|
||||
if (!displayId) {
|
||||
throw new Error('The display ID was not obtained.')
|
||||
}
|
||||
|
||||
return displayId
|
||||
}
|
||||
|
||||
export default (options = {}) => {
|
||||
adbkit = options.adbkit
|
||||
|
||||
@ -195,5 +229,7 @@ export default (options = {}) => {
|
||||
record,
|
||||
mirrorGroup,
|
||||
helper,
|
||||
getAppList,
|
||||
startApp,
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,99 @@
|
||||
<template>
|
||||
<el-dropdown
|
||||
:hide-on-click="false"
|
||||
:disabled="loading || floating"
|
||||
max-height="300px"
|
||||
@command="handleCommand"
|
||||
@mouseenter="getAppData"
|
||||
>
|
||||
<slot :loading :trigger="handleTrigger" />
|
||||
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-for="item of options"
|
||||
:key="item.value"
|
||||
:command="item.value"
|
||||
>
|
||||
{{ $t(item.label) }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
floating: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
appList: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
options() {
|
||||
const value = this.appList.map(item => ({
|
||||
...item,
|
||||
label: item.name,
|
||||
value: item.packageName,
|
||||
}))
|
||||
return value
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.getAppData()
|
||||
},
|
||||
methods: {
|
||||
async getAppData() {
|
||||
const data = await window.scrcpy.getAppList(this.device.id)
|
||||
|
||||
this.appList = data || []
|
||||
},
|
||||
handleTrigger() {
|
||||
if (!this.floating) {
|
||||
return false
|
||||
}
|
||||
const channel = 'startApp'
|
||||
|
||||
window.electron.ipcRenderer.once(
|
||||
channel,
|
||||
(event, data) => {
|
||||
this.handleCommand(data)
|
||||
},
|
||||
)
|
||||
|
||||
const options = toRaw(this.options)
|
||||
|
||||
window.electron.ipcRenderer.send('open-system-menu', {
|
||||
channel,
|
||||
options,
|
||||
})
|
||||
},
|
||||
async handleCommand(value) {
|
||||
this.loading = true
|
||||
|
||||
const title = this.$store.device.getLabel(this.device, 'mirror')
|
||||
|
||||
const commands = this.$store.preference.scrcpyParameter(this.device.id, {
|
||||
excludes: ['--otg', '--mouse=aoa', '--keyboard=aoa'],
|
||||
})
|
||||
|
||||
await window.scrcpy.startApp(this.device.id, { title, commands, packageName: value })
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -55,8 +55,10 @@ export default {
|
||||
return false
|
||||
}
|
||||
|
||||
const channel = 'stop-device-gnirehtet'
|
||||
|
||||
window.electron.ipcRenderer.once(
|
||||
'stop-device-gnirehtet',
|
||||
channel,
|
||||
(event, data) => {
|
||||
this.handleStop()
|
||||
},
|
||||
@ -65,11 +67,11 @@ export default {
|
||||
const options = [
|
||||
{
|
||||
label: window.t('device.control.gnirehtet.stop'),
|
||||
value: 'stop-device-gnirehtet',
|
||||
},
|
||||
]
|
||||
|
||||
window.electron.ipcRenderer.send('open-device-gnirehtet-menu', {
|
||||
window.electron.ipcRenderer.send('open-system-menu', {
|
||||
channel,
|
||||
options,
|
||||
})
|
||||
},
|
||||
|
@ -73,8 +73,10 @@ export default {
|
||||
return false
|
||||
}
|
||||
|
||||
const channel = 'rotationScreen'
|
||||
|
||||
window.electron.ipcRenderer.once(
|
||||
'execute-device-rotation-shell',
|
||||
channel,
|
||||
(event, data) => {
|
||||
this.handleCommand(data)
|
||||
},
|
||||
@ -82,7 +84,8 @@ export default {
|
||||
|
||||
const options = toRaw(this.options)
|
||||
|
||||
window.electron.ipcRenderer.send('open-device-rotation-menu', {
|
||||
window.electron.ipcRenderer.send('open-system-menu', {
|
||||
channel,
|
||||
options,
|
||||
})
|
||||
},
|
||||
@ -97,6 +100,8 @@ export default {
|
||||
await sleep(500)
|
||||
}
|
||||
|
||||
console.log('command', command)
|
||||
|
||||
this.$adb.deviceShell(this.device.id, command)
|
||||
|
||||
this.loading = false
|
||||
|
@ -67,16 +67,20 @@ export default {
|
||||
return false
|
||||
}
|
||||
|
||||
const channel = 'changeVolume'
|
||||
|
||||
window.electron.ipcRenderer.once(
|
||||
'execute-device-volume-shell',
|
||||
channel,
|
||||
(event, data) => {
|
||||
console.log('data')
|
||||
this.handleCommand(data)
|
||||
},
|
||||
)
|
||||
|
||||
const options = toRaw(this.options)
|
||||
|
||||
window.electron.ipcRenderer.send('open-device-volume-menu', {
|
||||
window.electron.ipcRenderer.send('open-system-menu', {
|
||||
channel,
|
||||
options,
|
||||
})
|
||||
},
|
||||
|
@ -64,10 +64,11 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import Synergy from './Synergy/index.vue'
|
||||
import Application from './Application/index.vue'
|
||||
import ApplicationStart from './ApplicationStart/index.vue'
|
||||
import FileManage from './FileManage/index.vue'
|
||||
import Gnirehtet from './Gnirehtet/index.vue'
|
||||
import Synergy from './Synergy/index.vue'
|
||||
import Rotation from './Rotation/index.vue'
|
||||
import Screenshot from './Screenshot/index.vue'
|
||||
import Shell from './Shell/index.vue'
|
||||
@ -76,10 +77,11 @@ import Volume from './Volume/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
// Synergy,
|
||||
Screenshot,
|
||||
Application,
|
||||
ApplicationStart,
|
||||
Gnirehtet,
|
||||
Synergy,
|
||||
Rotation,
|
||||
Volume,
|
||||
FileManage,
|
||||
@ -125,6 +127,11 @@ export default {
|
||||
window.scrcpy.helper(this.device.id, '--turn-screen-off')
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'device.control.startApp',
|
||||
elIcon: 'Files',
|
||||
component: 'ApplicationStart',
|
||||
},
|
||||
{
|
||||
label: 'device.control.notification',
|
||||
elIcon: 'Notification',
|
||||
@ -187,14 +194,14 @@ export default {
|
||||
component: 'Gnirehtet',
|
||||
tips: 'device.control.gnirehtet.tips',
|
||||
},
|
||||
{
|
||||
label: 'device.control.mirror-group.name',
|
||||
svgIcon: 'multi-screen',
|
||||
iconClass: '',
|
||||
component: 'Synergy',
|
||||
tips: 'device.control.mirror-group.tips',
|
||||
hiddenKeys: ['floating'],
|
||||
},
|
||||
// {
|
||||
// label: 'device.control.mirror-group.name',
|
||||
// svgIcon: 'multi-screen',
|
||||
// iconClass: '',
|
||||
// component: 'Synergy',
|
||||
// tips: 'device.control.mirror-group.tips',
|
||||
// hiddenKeys: ['floating'],
|
||||
// },
|
||||
]
|
||||
|
||||
const handler = item =>
|
||||
|
@ -169,6 +169,7 @@
|
||||
"device.control.reboot": "Reboot",
|
||||
"device.control.turnScreenOff": "Turn screen off",
|
||||
"device.control.turnScreenOff.tips": "Turn off the screen while maintaining control (Experimental): This action will create an EscrcpyHelper process; manually closing this process will reopen the screen.",
|
||||
"device.control.startApp": "Start APP",
|
||||
"device.control.power": "Power",
|
||||
"device.control.power.tips": "Turn screen on/off",
|
||||
"device.control.notification": "Notification",
|
||||
|
@ -169,6 +169,7 @@
|
||||
"device.control.reboot": "Перезагрузить",
|
||||
"device.control.turnScreenOff": "Выключить экран",
|
||||
"device.control.turnScreenOff.tips": "Отключение экрана с сохранением контроля (экспериментально): это действие создаст процесс EscrcpyHelper; при ручном завершении этого процесса экран снова включится.",
|
||||
"device.control.startApp": "Start APP",
|
||||
"device.control.power": "Питание",
|
||||
"device.control.power.tips": "Включить/выключить экран",
|
||||
"device.control.notification": "Уведомление",
|
||||
|
@ -169,6 +169,7 @@
|
||||
"device.control.reboot": "重启设备",
|
||||
"device.control.turnScreenOff": "关闭屏幕",
|
||||
"device.control.turnScreenOff.tips": "关闭屏幕且保持控制(实验功能):此操作将创建一个 EscrcpyHelper 进程,手动关闭进程将重新打开屏幕。",
|
||||
"device.control.startApp": "启动应用",
|
||||
"device.control.power": "电源键",
|
||||
"device.control.power.tips": "可以用来开启或关闭屏幕",
|
||||
"device.control.notification": "通知栏",
|
||||
|
@ -169,6 +169,7 @@
|
||||
"device.control.reboot": "重啟裝置",
|
||||
"device.control.turnScreenOff": "關閉螢幕",
|
||||
"device.control.turnScreenOff.tips": "關閉螢幕且保持控制(實驗功能):此操作將創建一個 EscrcpyHelper 進程,手動關閉該進程將重新打開螢幕。",
|
||||
"device.control.startApp": "Start APP",
|
||||
"device.control.power": "電源鍵",
|
||||
"device.control.power.tips": "可以用來開啟或關閉螢幕",
|
||||
"device.control.notification": "通知欄",
|
||||
|
@ -26,7 +26,7 @@ export const useDeviceStore = defineStore({
|
||||
return this.config
|
||||
},
|
||||
getLabel(device, params) {
|
||||
if (!device) {
|
||||
if (!device.id) {
|
||||
return ''
|
||||
}
|
||||
|
||||
@ -36,7 +36,7 @@ export const useDeviceStore = defineStore({
|
||||
|
||||
const appName = capitalize(packageName)
|
||||
|
||||
const deviceName = `${data.$remark || data.$name}${data.$wifi ? '(WIFI)' : ''}`
|
||||
const deviceName = `${data?.$remark || data.$name}${data.$wifi ? '(WIFI)' : ''}`
|
||||
|
||||
const currentTime = dayjs().format('YYYYMMDDHHmmss')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user