feat: Supports starting applications for mirroring

This commit is contained in:
viarotel 2024-11-05 18:34:14 +08:00
parent ce6ea8e39d
commit d19e781471
19 changed files with 260 additions and 123 deletions

View File

@ -28,7 +28,7 @@
icon="ArrowDown" icon="ArrowDown"
@click="switchDevice" @click="switchDevice"
> >
<span class="mr-2">{{ deviceInfo.$remark || deviceInfo.$name }}</span> <span class="mr-2">{{ deviceName }}</span>
</el-button> </el-button>
</div> </div>
@ -79,6 +79,8 @@ const deviceInfo = ref({})
const deviceList = ref([]) const deviceList = ref([])
const deviceName = computed(() => deviceStore.getLabel(deviceInfo.value, ({ deviceName }) => deviceName))
function handleClose() { function handleClose() {
window.electron.ipcRenderer.send('hide-active-window') window.electron.ipcRenderer.send('hide-active-window')
} }
@ -88,7 +90,17 @@ async function switchDevice(e) {
const data = await deviceStore.getList() 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(() => { onMounted(() => {

View File

@ -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))
})
}

View File

@ -1,4 +1 @@
export { default as devices } from './devices/index.js' export { default as menu } from './menu/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'

View File

@ -1,16 +1,16 @@
import { BrowserWindow, ipcMain, Menu } from 'electron' import { BrowserWindow, ipcMain, Menu } from 'electron'
export default function (controlWindow) { export default function (controlWindow) {
ipcMain.on('open-device-gnirehtet-menu', openDeviceGnirehtetMenu) ipcMain.on('open-system-menu', openSystemMenu)
function openDeviceGnirehtetMenu(event, args = {}) { function openSystemMenu(event, args = {}) {
const { options = [] } = args const { options = [], channel = 'system-menu-click' } = args
const template = options.map((item) => { const template = options.map((item) => {
return { return {
label: item.label, label: item.label,
click() { click() {
controlWindow.webContents.send(item.value) controlWindow.webContents.send(channel, item.value)
}, },
} }
}) })

View File

@ -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))
}
}

View File

@ -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))
}
}

View File

@ -1,7 +1,7 @@
import { BrowserWindow, ipcMain } from 'electron' import { BrowserWindow, ipcMain } from 'electron'
import { initControlWindow, openControlWindow } from './helpers/index.js' 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) { function onControlMounted(controlWindow) {
ipcMain.on('language-change', (event, data) => { ipcMain.on('language-change', (event, data) => {
@ -12,10 +12,7 @@ function onControlMounted(controlWindow) {
controlWindow.webContents.send('theme-change', data) controlWindow.webContents.send('theme-change', data)
}) })
rotation(controlWindow) menu(controlWindow)
devices(controlWindow)
volume(controlWindow)
gnirehtet(controlWindow)
} }
export default (mainWindow) => { export default (mainWindow) => {

View 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 []
}
}

View File

@ -5,11 +5,13 @@ import appStore from '$electron/helpers/store.js'
import { replaceIP, sleep } from '$renderer/utils/index.js' import { replaceIP, sleep } from '$renderer/utils/index.js'
import commandHelper from '$renderer/utils/command/index.js' import commandHelper from '$renderer/utils/command/index.js'
import { parseScrcpyAppList } from './helper.js'
let adbkit let adbkit
const exec = util.promisify(_exec) 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 spawnPath = appStore.get('common.scrcpyPath') || scrcpyPath
const ADB = appStore.get('common.adbPath') || adbPath const ADB = appStore.get('common.adbPath') || adbPath
const args = command.split(' ') const args = command.split(' ')
@ -21,28 +23,35 @@ async function shell(command, { stdout, stderr, ...options } = {}) {
...options, ...options,
}) })
scrcpyProcess.stdout.on('data', (data) => {
const stringData = data.toString()
if (stdout) {
stdout(stringData, scrcpyProcess)
}
})
const stderrList = [] const stderrList = []
scrcpyProcess.stderr.on('data', (data) => {
const stringData = data.toString()
stderrList.push(stringData)
console.error('scrcpyProcess.stderr.data:', stringData)
if (stderr) {
stderr(stringData, scrcpyProcess)
}
})
return new Promise((resolve, reject) => { 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)
}
})
scrcpyProcess.stderr.on('data', (data) => {
const stringData = data.toString()
stderrList.push(stringData)
console.error('scrcpyProcess.stderr.data:', stringData)
if (stderr) {
stderr(stringData, scrcpyProcess)
}
})
scrcpyProcess.on('close', (code) => { scrcpyProcess.on('close', (code) => {
if (code === 0) { if (code === 0) {
resolve() 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 = {}) => { export default (options = {}) => {
adbkit = options.adbkit adbkit = options.adbkit
@ -195,5 +229,7 @@ export default (options = {}) => {
record, record,
mirrorGroup, mirrorGroup,
helper, helper,
getAppList,
startApp,
} }
} }

View File

@ -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>

View File

@ -55,8 +55,10 @@ export default {
return false return false
} }
const channel = 'stop-device-gnirehtet'
window.electron.ipcRenderer.once( window.electron.ipcRenderer.once(
'stop-device-gnirehtet', channel,
(event, data) => { (event, data) => {
this.handleStop() this.handleStop()
}, },
@ -65,11 +67,11 @@ export default {
const options = [ const options = [
{ {
label: window.t('device.control.gnirehtet.stop'), 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, options,
}) })
}, },

View File

@ -73,8 +73,10 @@ export default {
return false return false
} }
const channel = 'rotationScreen'
window.electron.ipcRenderer.once( window.electron.ipcRenderer.once(
'execute-device-rotation-shell', channel,
(event, data) => { (event, data) => {
this.handleCommand(data) this.handleCommand(data)
}, },
@ -82,7 +84,8 @@ export default {
const options = toRaw(this.options) const options = toRaw(this.options)
window.electron.ipcRenderer.send('open-device-rotation-menu', { window.electron.ipcRenderer.send('open-system-menu', {
channel,
options, options,
}) })
}, },
@ -97,6 +100,8 @@ export default {
await sleep(500) await sleep(500)
} }
console.log('command', command)
this.$adb.deviceShell(this.device.id, command) this.$adb.deviceShell(this.device.id, command)
this.loading = false this.loading = false

View File

@ -67,16 +67,20 @@ export default {
return false return false
} }
const channel = 'changeVolume'
window.electron.ipcRenderer.once( window.electron.ipcRenderer.once(
'execute-device-volume-shell', channel,
(event, data) => { (event, data) => {
console.log('data')
this.handleCommand(data) this.handleCommand(data)
}, },
) )
const options = toRaw(this.options) const options = toRaw(this.options)
window.electron.ipcRenderer.send('open-device-volume-menu', { window.electron.ipcRenderer.send('open-system-menu', {
channel,
options, options,
}) })
}, },

View File

@ -64,10 +64,11 @@
</template> </template>
<script> <script>
// import Synergy from './Synergy/index.vue'
import Application from './Application/index.vue' import Application from './Application/index.vue'
import ApplicationStart from './ApplicationStart/index.vue'
import FileManage from './FileManage/index.vue' import FileManage from './FileManage/index.vue'
import Gnirehtet from './Gnirehtet/index.vue' import Gnirehtet from './Gnirehtet/index.vue'
import Synergy from './Synergy/index.vue'
import Rotation from './Rotation/index.vue' import Rotation from './Rotation/index.vue'
import Screenshot from './Screenshot/index.vue' import Screenshot from './Screenshot/index.vue'
import Shell from './Shell/index.vue' import Shell from './Shell/index.vue'
@ -76,10 +77,11 @@ import Volume from './Volume/index.vue'
export default { export default {
components: { components: {
// Synergy,
Screenshot, Screenshot,
Application, Application,
ApplicationStart,
Gnirehtet, Gnirehtet,
Synergy,
Rotation, Rotation,
Volume, Volume,
FileManage, FileManage,
@ -125,6 +127,11 @@ export default {
window.scrcpy.helper(this.device.id, '--turn-screen-off') window.scrcpy.helper(this.device.id, '--turn-screen-off')
}, },
}, },
{
label: 'device.control.startApp',
elIcon: 'Files',
component: 'ApplicationStart',
},
{ {
label: 'device.control.notification', label: 'device.control.notification',
elIcon: 'Notification', elIcon: 'Notification',
@ -187,14 +194,14 @@ export default {
component: 'Gnirehtet', component: 'Gnirehtet',
tips: 'device.control.gnirehtet.tips', tips: 'device.control.gnirehtet.tips',
}, },
{ // {
label: 'device.control.mirror-group.name', // label: 'device.control.mirror-group.name',
svgIcon: 'multi-screen', // svgIcon: 'multi-screen',
iconClass: '', // iconClass: '',
component: 'Synergy', // component: 'Synergy',
tips: 'device.control.mirror-group.tips', // tips: 'device.control.mirror-group.tips',
hiddenKeys: ['floating'], // hiddenKeys: ['floating'],
}, // },
] ]
const handler = item => const handler = item =>

View File

@ -169,6 +169,7 @@
"device.control.reboot": "Reboot", "device.control.reboot": "Reboot",
"device.control.turnScreenOff": "Turn screen off", "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.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": "Power",
"device.control.power.tips": "Turn screen on/off", "device.control.power.tips": "Turn screen on/off",
"device.control.notification": "Notification", "device.control.notification": "Notification",

View File

@ -169,6 +169,7 @@
"device.control.reboot": "Перезагрузить", "device.control.reboot": "Перезагрузить",
"device.control.turnScreenOff": "Выключить экран", "device.control.turnScreenOff": "Выключить экран",
"device.control.turnScreenOff.tips": "Отключение экрана с сохранением контроля (экспериментально): это действие создаст процесс EscrcpyHelper; при ручном завершении этого процесса экран снова включится.", "device.control.turnScreenOff.tips": "Отключение экрана с сохранением контроля (экспериментально): это действие создаст процесс EscrcpyHelper; при ручном завершении этого процесса экран снова включится.",
"device.control.startApp": "Start APP",
"device.control.power": "Питание", "device.control.power": "Питание",
"device.control.power.tips": "Включить/выключить экран", "device.control.power.tips": "Включить/выключить экран",
"device.control.notification": "Уведомление", "device.control.notification": "Уведомление",

View File

@ -169,6 +169,7 @@
"device.control.reboot": "重启设备", "device.control.reboot": "重启设备",
"device.control.turnScreenOff": "关闭屏幕", "device.control.turnScreenOff": "关闭屏幕",
"device.control.turnScreenOff.tips": "关闭屏幕且保持控制(实验功能):此操作将创建一个 EscrcpyHelper 进程,手动关闭进程将重新打开屏幕。", "device.control.turnScreenOff.tips": "关闭屏幕且保持控制(实验功能):此操作将创建一个 EscrcpyHelper 进程,手动关闭进程将重新打开屏幕。",
"device.control.startApp": "启动应用",
"device.control.power": "电源键", "device.control.power": "电源键",
"device.control.power.tips": "可以用来开启或关闭屏幕", "device.control.power.tips": "可以用来开启或关闭屏幕",
"device.control.notification": "通知栏", "device.control.notification": "通知栏",

View File

@ -169,6 +169,7 @@
"device.control.reboot": "重啟裝置", "device.control.reboot": "重啟裝置",
"device.control.turnScreenOff": "關閉螢幕", "device.control.turnScreenOff": "關閉螢幕",
"device.control.turnScreenOff.tips": "關閉螢幕且保持控制(實驗功能):此操作將創建一個 EscrcpyHelper 進程,手動關閉該進程將重新打開螢幕。", "device.control.turnScreenOff.tips": "關閉螢幕且保持控制(實驗功能):此操作將創建一個 EscrcpyHelper 進程,手動關閉該進程將重新打開螢幕。",
"device.control.startApp": "Start APP",
"device.control.power": "電源鍵", "device.control.power": "電源鍵",
"device.control.power.tips": "可以用來開啟或關閉螢幕", "device.control.power.tips": "可以用來開啟或關閉螢幕",
"device.control.notification": "通知欄", "device.control.notification": "通知欄",

View File

@ -26,7 +26,7 @@ export const useDeviceStore = defineStore({
return this.config return this.config
}, },
getLabel(device, params) { getLabel(device, params) {
if (!device) { if (!device.id) {
return '' return ''
} }
@ -36,7 +36,7 @@ export const useDeviceStore = defineStore({
const appName = capitalize(packageName) 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') const currentTime = dayjs().format('YYYYMMDDHHmmss')