From d19e78147152550b0d9f006765e09c955ab0951e Mon Sep 17 00:00:00 2001 From: viarotel Date: Tue, 5 Nov 2024 18:34:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20Supports=20starting=20appli?= =?UTF-8?q?cations=20for=20mirroring?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- control/App.vue | 16 ++- control/electron/events/devices/index.js | 24 ----- control/electron/events/index.js | 5 +- .../events/{gnirehtet => menu}/index.js | 8 +- control/electron/events/rotation/index.js | 24 ----- control/electron/events/volume/index.js | 21 ---- control/electron/main.js | 7 +- electron/exposes/scrcpy/helper.js | 43 ++++++++ electron/exposes/scrcpy/index.js | 76 ++++++++++---- .../ControlBar/ApplicationStart/index.vue | 99 +++++++++++++++++++ .../components/ControlBar/Gnirehtet/index.vue | 8 +- .../components/ControlBar/Rotation/index.vue | 9 +- .../components/ControlBar/Volume/index.vue | 8 +- .../Device/components/ControlBar/index.vue | 27 +++-- src/locales/languages/en-US.json | 1 + src/locales/languages/ru-RU.json | 1 + src/locales/languages/zh-CN.json | 1 + src/locales/languages/zh-TW.json | 1 + src/store/device/index.js | 4 +- 19 files changed, 260 insertions(+), 123 deletions(-) delete mode 100644 control/electron/events/devices/index.js rename control/electron/events/{gnirehtet => menu}/index.js (61%) delete mode 100644 control/electron/events/rotation/index.js delete mode 100644 control/electron/events/volume/index.js create mode 100644 electron/exposes/scrcpy/helper.js create mode 100644 src/components/Device/components/ControlBar/ApplicationStart/index.vue diff --git a/control/App.vue b/control/App.vue index 844676f..75db695 100644 --- a/control/App.vue +++ b/control/App.vue @@ -28,7 +28,7 @@ icon="ArrowDown" @click="switchDevice" > - {{ deviceInfo.$remark || deviceInfo.$name }} + {{ deviceName }} @@ -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(() => { diff --git a/control/electron/events/devices/index.js b/control/electron/events/devices/index.js deleted file mode 100644 index 092418e..0000000 --- a/control/electron/events/devices/index.js +++ /dev/null @@ -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)) - }) -} diff --git a/control/electron/events/index.js b/control/electron/events/index.js index 63255b2..98db7c1 100644 --- a/control/electron/events/index.js +++ b/control/electron/events/index.js @@ -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' diff --git a/control/electron/events/gnirehtet/index.js b/control/electron/events/menu/index.js similarity index 61% rename from control/electron/events/gnirehtet/index.js rename to control/electron/events/menu/index.js index d77e9e9..fc32f11 100644 --- a/control/electron/events/gnirehtet/index.js +++ b/control/electron/events/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) }, } }) diff --git a/control/electron/events/rotation/index.js b/control/electron/events/rotation/index.js deleted file mode 100644 index 857e6db..0000000 --- a/control/electron/events/rotation/index.js +++ /dev/null @@ -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)) - } -} diff --git a/control/electron/events/volume/index.js b/control/electron/events/volume/index.js deleted file mode 100644 index 8cb325f..0000000 --- a/control/electron/events/volume/index.js +++ /dev/null @@ -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)) - } -} diff --git a/control/electron/main.js b/control/electron/main.js index 6d10b98..a2814dd 100644 --- a/control/electron/main.js +++ b/control/electron/main.js @@ -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) => { diff --git a/electron/exposes/scrcpy/helper.js b/electron/exposes/scrcpy/helper.js new file mode 100644 index 0000000..71a2a0a --- /dev/null +++ b/electron/exposes/scrcpy/helper.js @@ -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 [] + } +} diff --git a/electron/exposes/scrcpy/index.js b/electron/exposes/scrcpy/index.js index 18aea9d..8988db9 100644 --- a/electron/exposes/scrcpy/index.js +++ b/electron/exposes/scrcpy/index.js @@ -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,28 +23,35 @@ async function shell(command, { stdout, stderr, ...options } = {}) { ...options, }) - scrcpyProcess.stdout.on('data', (data) => { - const stringData = data.toString() - - if (stdout) { - stdout(stringData, scrcpyProcess) - } - }) - 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) => { + 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) => { 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, } } diff --git a/src/components/Device/components/ControlBar/ApplicationStart/index.vue b/src/components/Device/components/ControlBar/ApplicationStart/index.vue new file mode 100644 index 0000000..af9e212 --- /dev/null +++ b/src/components/Device/components/ControlBar/ApplicationStart/index.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/src/components/Device/components/ControlBar/Gnirehtet/index.vue b/src/components/Device/components/ControlBar/Gnirehtet/index.vue index 62b45f8..d55d78f 100644 --- a/src/components/Device/components/ControlBar/Gnirehtet/index.vue +++ b/src/components/Device/components/ControlBar/Gnirehtet/index.vue @@ -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, }) }, diff --git a/src/components/Device/components/ControlBar/Rotation/index.vue b/src/components/Device/components/ControlBar/Rotation/index.vue index 4ce9471..23b2198 100644 --- a/src/components/Device/components/ControlBar/Rotation/index.vue +++ b/src/components/Device/components/ControlBar/Rotation/index.vue @@ -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 diff --git a/src/components/Device/components/ControlBar/Volume/index.vue b/src/components/Device/components/ControlBar/Volume/index.vue index b94e41b..76c4633 100644 --- a/src/components/Device/components/ControlBar/Volume/index.vue +++ b/src/components/Device/components/ControlBar/Volume/index.vue @@ -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, }) }, diff --git a/src/components/Device/components/ControlBar/index.vue b/src/components/Device/components/ControlBar/index.vue index 8eeed6a..16d5e63 100644 --- a/src/components/Device/components/ControlBar/index.vue +++ b/src/components/Device/components/ControlBar/index.vue @@ -64,10 +64,11 @@