feat: Support floating control bar

This commit is contained in:
viarotel 2024-09-12 19:11:46 +08:00
parent 47ae53d623
commit 8807e50413
29 changed files with 406 additions and 95 deletions

118
control/App.vue Normal file
View File

@ -0,0 +1,118 @@
<template>
<el-config-provider :locale="locale">
<div
class="flex items-center bg-primary-100 dark:bg-gray-800 absolute inset-0 h-full"
>
<div class="flex-none h-full">
<el-button
type="primary"
class="!px-3 bg-transparent !border-none !h-full"
plain
@click="handleClose"
>
<el-icon class="">
<ElIconCircleCloseFilled />
</el-icon>
</el-button>
</div>
<div
class="h-4 w-px mx-1 bg-primary-200 dark:bg-primary-800 flex-none"
></div>
<div class="flex-none h-full">
<el-button
type="primary"
text
class="!px-2 !h-full"
icon="Switch"
@click="switchDevice"
>
<span class="mr-2">{{ deviceInfo.$remark || deviceInfo.$name }}</span>
</el-button>
</div>
<div
class="h-4 w-px mx-1 bg-primary-200 dark:bg-primary-800 flex-none"
></div>
<div class="flex-1 w-0 overflow-hidden h-full">
<ControlBar class="!h-full" :device="deviceInfo" />
</div>
<div
class="h-4 w-px mx-1 bg-primary-200 dark:bg-primary-800 flex-none"
></div>
<div class="flex-none h-full app-region-drag">
<el-button type="primary" text class="!px-3 !h-full">
<el-icon class="">
<ElIconRank />
</el-icon>
</el-button>
</div>
</div>
</el-config-provider>
</template>
<script setup>
import ControlBar from '$/components/Device/components/ControlBar/index.vue'
import { i18n } from '$/locales/index.js'
import localeModel from '$/plugins/element-plus/locale.js'
import { useDeviceStore, useThemeStore } from '$/store/index.js'
import { ElMessage } from 'element-plus'
const themeStore = useThemeStore()
const deviceStore = useDeviceStore()
themeStore.init()
onMounted(() => {
window.electron.ipcRenderer.send('control-mounted')
})
const locale = computed(() => {
const i18nLocale = i18n.global.locale.value
const value = localeModel[i18nLocale]
return value
})
const deviceInfo = ref({})
window.electron.ipcRenderer.on('device-change', (event, data) => {
deviceInfo.value = data
})
window.electron.ipcRenderer.on('language-change', (event, data) => {
i18n.global.locale.value = data
})
window.electron.ipcRenderer.on('theme-change', (event, data) => {
themeStore.update(data)
})
function handleClose() {
window.electron.ipcRenderer.send('hide-active-window')
}
const deviceList = ref([])
async function switchDevice(e) {
e.preventDefault()
const data = await deviceStore.getList()
window.electron.ipcRenderer.send('show-device-list', data)
}
</script>
<style lang="postcss">
.app-region-drag {
-webkit-app-region: drag;
}
</style>

View File

@ -0,0 +1,44 @@
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { BrowserWindow } from 'electron'
import { getLogoPath } from '$electron/configs/index.js'
import { sleep } from '$renderer/utils/index.js'
import { loadPage } from '$electron/helpers/index.js'
export function initControlWindow(mainWindow) {
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const controlWindow = new BrowserWindow({
icon: getLogoPath(),
width: 500,
minWidth: 500,
height: 30,
maxHeight: 30,
frame: false,
show: false,
autoHideMenuBar: true,
alwaysOnTop: true,
skipTaskbar: true,
webPreferences: {
preload: path.join(__dirname, 'preload.mjs'),
nodeIntegration: true,
sandbox: false,
spellcheck: false,
},
})
controlWindow.customId = 'control'
loadPage(controlWindow, 'control/')
return controlWindow
}
export async function openControlWindow(win, data, args = {}) {
if (args.sleep) {
await sleep(args.sleep)
}
win.show()
win.webContents.send('device-change', data)
}

56
control/electron/main.js Normal file
View File

@ -0,0 +1,56 @@
import { BrowserWindow, ipcMain, Menu } from 'electron'
import { initControlWindow, openControlWindow } from './helpers/index.js'
export default (mainWindow) => {
let controlWindow
ipcMain.on('open-control-window', (event, data) => {
controlWindow = BrowserWindow.getAllWindows().find(
win => win.customId === 'control',
)
if (!controlWindow) {
controlWindow = initControlWindow(mainWindow)
ipcMain.on('control-mounted', () => {
openControlWindow(controlWindow, data)
})
return false
}
openControlWindow(controlWindow, data)
})
ipcMain.on('language-change', (event, data) => {
if (controlWindow) {
controlWindow.webContents.send('language-change', data)
}
})
ipcMain.on('theme-change', (event, data) => {
if (controlWindow) {
controlWindow.webContents.send('theme-change', data)
}
})
ipcMain.on('show-device-list', (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))
})
}

13
control/index.html Normal file
View File

@ -0,0 +1,13 @@
<!DOCTYPE html>
<html lang="en" class="">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/logo.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Escrcpy Control</title>
</head>
<body class="overflow-hidden">
<div id="app"></div>
<script type="module" src="./index.js"></script>
</body>
</html>

5
control/index.js Normal file
View File

@ -0,0 +1,5 @@
import bootstrap from '../src/bootstrap/index.js'
import App from './App.vue'
bootstrap(App)

View File

@ -22,3 +22,16 @@ export const trayPath
: extraResolve('common/tray/icon.png') : extraResolve('common/tray/icon.png')
export const logPath = process.env.LOG_PATH export const logPath = process.env.LOG_PATH
export function getLogoPath() {
let icon = logoPath
if (process.platform === 'win32') {
icon = icoLogoPath
}
else if (process.platform === 'darwin') {
icon = icnsLogoPath
}
return logoPath
}

View File

@ -0,0 +1,19 @@
import { app, BrowserWindow, ipcMain } from 'electron'
export default () => {
ipcMain.on('restart-app', () => {
app.isQuiting = true
app.relaunch()
app.quit()
})
ipcMain.on('close-active-window', (event) => {
const win = BrowserWindow.getFocusedWindow()
win.close()
})
ipcMain.on('hide-active-window', (event) => {
const win = BrowserWindow.getFocusedWindow()
win.hide()
})
}

View File

@ -1,5 +1,4 @@
import { app, ipcMain } from 'electron' import appEvents from './app/index.js'
import handles from './handles/index.js' import handles from './handles/index.js'
import shortcuts from './shortcuts/index.js' import shortcuts from './shortcuts/index.js'
import theme from './theme/index.js' import theme from './theme/index.js'
@ -7,12 +6,7 @@ import tray from './tray/index.js'
import updater from './updater/index.js' import updater from './updater/index.js'
export default (mainWindow) => { export default (mainWindow) => {
ipcMain.on('restart-app', () => { appEvents(mainWindow)
app.isQuiting = true
app.relaunch()
app.quit()
})
handles(mainWindow) handles(mainWindow)
updater(mainWindow) updater(mainWindow)
tray(mainWindow) tray(mainWindow)

View File

@ -1,4 +1,4 @@
import { resolve } from 'node:path' import { join, resolve } from 'node:path'
import { contextBridge } from 'electron' import { contextBridge } from 'electron'
import { cloneDeep } from 'lodash-es' import { cloneDeep } from 'lodash-es'
@ -56,3 +56,15 @@ export async function executeI18n(mainWindow, value) {
return value return value
} }
} }
export function loadPage(win, prefix = '') {
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL
if (VITE_DEV_SERVER_URL) {
win.loadURL(join(VITE_DEV_SERVER_URL, prefix))
}
else {
win.loadFile(join(process.env.DIST, prefix, 'index.html'))
}
}

View File

@ -10,6 +10,7 @@ if (isEqual(appStore.store, {})) {
} }
export default { export default {
...appStore,
...createProxy(appStore, [ ...createProxy(appStore, [
'set', 'set',
'get', 'get',
@ -21,7 +22,6 @@ export default {
'onDidAnyChange', 'onDidAnyChange',
'openInEditor', 'openInEditor',
]), ]),
...appStore,
getAll: () => appStore.store, getAll: () => appStore.store,
setAll: value => (appStore.store = value), setAll: value => (appStore.store = value),
} }

View File

@ -12,10 +12,14 @@ import log from './helpers/log.js'
import './helpers/console.js' import './helpers/console.js'
import appStore from './helpers/store.js' import appStore from './helpers/store.js'
import { icnsLogoPath, icoLogoPath, logoPath } from './configs/index.js' import { getLogoPath } from './configs/index.js'
import events from './events/index.js' import events from './events/index.js'
import control from '$control/electron/main.js'
import { loadPage } from './helpers/index.js'
const require = createRequire(import.meta.url) const require = createRequire(import.meta.url)
const __dirname = path.dirname(fileURLToPath(import.meta.url)) const __dirname = path.dirname(fileURLToPath(import.meta.url))
@ -51,25 +55,10 @@ contextMenu({
process.env.DIST = path.join(__dirname, '../dist') process.env.DIST = path.join(__dirname, '../dist')
let mainWindow let mainWindow
// 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x
const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL
function createWindow() { function createWindow() {
let icon = logoPath
if (process.platform === 'win32') {
icon = icoLogoPath
} else if (process.platform === 'darwin') {
icon = icnsLogoPath
}
mainWindow = new BrowserWindow({ mainWindow = new BrowserWindow({
// 这里设置的图标仅在开发模式生效,打包后将使用应用程序图标 icon: getLogoPath(),
...(!isPackaged
? {
icon,
}
: {}),
show: false, show: false,
width: 1200, width: 1200,
height: 800, height: 800,
@ -96,13 +85,11 @@ function createWindow() {
return { action: 'deny' } return { action: 'deny' }
}) })
if (VITE_DEV_SERVER_URL) { loadPage(mainWindow)
mainWindow.loadURL(VITE_DEV_SERVER_URL)
} else {
mainWindow.loadFile(path.join(process.env.DIST, 'index.html'))
}
events(mainWindow) events(mainWindow)
control(mainWindow)
} }
app.whenReady().then(() => { app.whenReady().then(() => {

View File

@ -63,6 +63,7 @@ export default antfu(
'unicorn/consistent-function-scoping': 'off', 'unicorn/consistent-function-scoping': 'off',
'regexp/no-unused-capturing-group': 'off', 'regexp/no-unused-capturing-group': 'off',
'regexp/no-dupe-disjunctions': 'off', 'regexp/no-dupe-disjunctions': 'off',
'perfectionist/sort-imports': 'off',
}, },
}, },
) )

View File

@ -5,7 +5,8 @@
"$/*": ["src/*"], "$/*": ["src/*"],
"$root/*": ["*"], "$root/*": ["*"],
"$electron/*": ["electron/*"], "$electron/*": ["electron/*"],
"$renderer/*": ["src/*"] "$renderer/*": ["src/*"],
"$control/*": ["control/*"]
} }
}, },
"exclude": ["node_modules", "dist", "dist-electron", "dist-release"], "exclude": ["node_modules", "dist", "dist-electron", "dist-release"],

View File

@ -0,0 +1,48 @@
import { createApp, toRaw } from 'vue'
import icons from '$/icons/index.js'
import { i18n, t } from '$/locales/index.js'
import plugins from '$/plugins/index.js'
import store from '$/store/index.js'
import { replaceIP, restoreIP } from '$/utils/index.js'
import '$/utils/console.js'
import 'virtual:uno.css'
import '$/styles/index.js'
export default (App) => {
const app = createApp(App)
app.use(store)
app.use(plugins)
app.use(icons)
app.use(i18n)
window.t = t
app.config.globalProperties.$path = window.nodePath
app.config.globalProperties.$appStore = window.appStore
app.config.globalProperties.$appLog = window.appLog
app.config.globalProperties.$electron = window.electron
app.config.globalProperties.$adb = window.adbkit
app.config.globalProperties.$scrcpy = window.scrcpy
app.config.globalProperties.$gnirehtet = window.gnirehtet
app.config.globalProperties.$replaceIP = replaceIP
app.config.globalProperties.$restoreIP = restoreIP
app.config.globalProperties.$toRaw = toRaw
app.mount('#app').$nextTick(() => {
// Remove Preload scripts loading
postMessage({ payload: 'removeLoading' }, '*')
})
}

3
src/bootstrap/index.js Normal file
View File

@ -0,0 +1,3 @@
import bootstrap from './default/index.js'
export default bootstrap

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
class="bg-primary-100 dark:bg-gray-800 flex items-center group -my-[8px] h-9 overflow-hidden" class="bg-primary-100 dark:bg-gray-800 flex items-center group h-9 overflow-hidden"
> >
<el-button <el-button
type="primary" type="primary"

View File

@ -13,6 +13,7 @@
<script> <script>
import { sleep } from '$/utils' import { sleep } from '$/utils'
import { openFloatControl } from '$/utils/device/index.js'
export default { export default {
props: { props: {
@ -52,6 +53,8 @@ export default {
this.loading = false this.loading = false
openFloatControl(toRaw(this.row))
await mirroring await mirroring
} }
catch (error) { catch (error) {
@ -63,6 +66,7 @@ export default {
} }
} }
}, },
onStdout() {}, onStdout() {},
onStderr() {}, onStderr() {},
}, },

View File

@ -6,6 +6,8 @@
<script> <script>
import { sleep } from '$/utils' import { sleep } from '$/utils'
import { openFloatControl } from '$/utils/device/index.js'
import DeployDialog from './components/DeployDialog/index.vue' import DeployDialog from './components/DeployDialog/index.vue'
export default { export default {
@ -58,6 +60,8 @@ export default {
this.loading = false this.loading = false
openFloatControl(toRaw(this.row))
await mirroring await mirroring
} }
catch (error) { catch (error) {
@ -69,6 +73,7 @@ export default {
} }
} }
}, },
onStdout() {}, onStdout() {},
onStderr() {}, onStderr() {},
}, },

View File

@ -4,6 +4,7 @@
<script> <script>
import { sleep } from '$/utils' import { sleep } from '$/utils'
import { openFloatControl } from '$/utils/device/index.js'
export default { export default {
props: { props: {
@ -49,6 +50,8 @@ export default {
this.loading = false this.loading = false
openFloatControl(toRaw(this.row))
await recording await recording
await this.handleSuccess(savePath) await this.handleSuccess(savePath)

View File

@ -106,7 +106,7 @@
</template> </template>
<template #default="{ row }"> <template #default="{ row }">
<ControlBar :device="row" /> <ControlBar :device="row" class="-my-[8px]" />
</template> </template>
</el-table-column> </el-table-column>
</el-table> </el-table>

View File

@ -50,6 +50,7 @@ export default {
set(value) { set(value) {
this.locale = value this.locale = value
this.$emit('update:model-value', value) this.$emit('update:model-value', value)
window.electron.ipcRenderer.send('language-change', value)
}, },
}, },
}, },

View File

@ -74,6 +74,7 @@ export default {
'preferenceData.theme': { 'preferenceData.theme': {
handler(value) { handler(value) {
this.$store.theme.update(value) this.$store.theme.update(value)
window.electron.ipcRenderer.send('theme-change', value)
}, },
}, },
'preferenceData.adbPath': { 'preferenceData.adbPath': {

View File

@ -237,6 +237,8 @@
"preferences.common.gnirehtet.fix.name": "Gnirehtet Fix", "preferences.common.gnirehtet.fix.name": "Gnirehtet Fix",
"preferences.common.gnirehtet.fix.placeholder": "Turning this on will try to fix reverse tethering issues on some devices", "preferences.common.gnirehtet.fix.placeholder": "Turning this on will try to fix reverse tethering issues on some devices",
"preferences.common.gnirehtet.fix.tips": "Note: May cause duplicate gnirehtet client installations", "preferences.common.gnirehtet.fix.tips": "Note: May cause duplicate gnirehtet client installations",
"preferences.common.floatControl.name": "Floating Control Bar",
"preferences.common.floatControl.placeholder": "Once enabled, the device floating control bar will automatically open during mirroring",
"preferences.common.auto-connect.name": "Auto Connect", "preferences.common.auto-connect.name": "Auto Connect",
"preferences.common.auto-connect.placeholder": "When enabled, the software will attempt to automatically connect to historical devices upon startup.", "preferences.common.auto-connect.placeholder": "When enabled, the software will attempt to automatically connect to historical devices upon startup.",
"preferences.common.auto-mirror.name": "Auto Mirror", "preferences.common.auto-mirror.name": "Auto Mirror",

View File

@ -237,6 +237,8 @@
"preferences.common.gnirehtet.fix.name": "gnirehtet 修复", "preferences.common.gnirehtet.fix.name": "gnirehtet 修复",
"preferences.common.gnirehtet.fix.placeholder": "开启将尝试修复某些机型二次开启反向供网功能失败的问题", "preferences.common.gnirehtet.fix.placeholder": "开启将尝试修复某些机型二次开启反向供网功能失败的问题",
"preferences.common.gnirehtet.fix.tips": "注意:可能导致 gnirehtet 客户端重复安装", "preferences.common.gnirehtet.fix.tips": "注意:可能导致 gnirehtet 客户端重复安装",
"preferences.common.floatControl.name": "浮动操控栏",
"preferences.common.floatControl.placeholder": "启用后,镜像时将会自动打开设备浮动操控栏",
"preferences.common.auto-connect.name": "自动连接设备", "preferences.common.auto-connect.name": "自动连接设备",
"preferences.common.auto-connect.placeholder": "启用后,该软件将在启动时尝试自动连接到历史无线设备", "preferences.common.auto-connect.placeholder": "启用后,该软件将在启动时尝试自动连接到历史无线设备",
"preferences.common.auto-mirror.name": "自动执行镜像", "preferences.common.auto-mirror.name": "自动执行镜像",

View File

@ -237,6 +237,8 @@
"preferences.common.gnirehtet.fix.name": "gnirehtet 修復", "preferences.common.gnirehtet.fix.name": "gnirehtet 修復",
"preferences.common.gnirehtet.fix.placeholder": "開啟將嘗試修復某些機型二次開啟反向網路分享功能失敗的問題", "preferences.common.gnirehtet.fix.placeholder": "開啟將嘗試修復某些機型二次開啟反向網路分享功能失敗的問題",
"preferences.common.gnirehtet.fix.tips": "注意:可能導致 gnirehtet 客戶端重複安裝", "preferences.common.gnirehtet.fix.tips": "注意:可能導致 gnirehtet 客戶端重複安裝",
"preferences.common.floatControl.name": "浮動操控欄",
"preferences.common.floatControl.placeholder": "啟用後,鏡像時將會自動開啟設備浮動操控欄",
"preferences.common.auto-connect.name": "自動連接裝置", "preferences.common.auto-connect.name": "自動連接裝置",
"preferences.common.auto-connect.placeholder": "啟用後,該軟體將在啟動時嘗試自動連接到歷史無線裝置", "preferences.common.auto-connect.placeholder": "啟用後,該軟體將在啟動時嘗試自動連接到歷史無線裝置",
"preferences.common.auto-mirror.name": "自動執行鏡像", "preferences.common.auto-mirror.name": "自動執行鏡像",

View File

@ -1,46 +1,5 @@
import { replaceIP, restoreIP } from '$/utils/index.js' import bootstrap from './bootstrap/index.js'
import { createApp, toRaw } from 'vue'
import App from './App.vue' import App from './App.vue'
import icons from './icons/index.js' bootstrap(App)
import { i18n, t } from './locales/index.js'
import plugins from './plugins/index.js'
import store from './store/index.js'
import '$/utils/console.js'
import 'virtual:uno.css'
import './styles/index.js'
const app = createApp(App)
app.use(store)
app.use(plugins)
app.use(icons)
app.use(i18n)
window.t = t
app.config.globalProperties.$path = window.nodePath
app.config.globalProperties.$appStore = window.appStore
app.config.globalProperties.$appLog = window.appLog
app.config.globalProperties.$electron = window.electron
app.config.globalProperties.$adb = window.adbkit
app.config.globalProperties.$scrcpy = window.scrcpy
app.config.globalProperties.$gnirehtet = window.gnirehtet
app.config.globalProperties.$replaceIP = replaceIP
app.config.globalProperties.$restoreIP = restoreIP
app.config.globalProperties.$toRaw = toRaw
app.mount('#app').$nextTick(() => {
// Remove Preload scripts loading
postMessage({ payload: 'removeLoading' }, '*')
})

View File

@ -129,6 +129,13 @@ export default {
placeholder: 'preferences.common.gnirehtet.fix.placeholder', placeholder: 'preferences.common.gnirehtet.fix.placeholder',
tips: 'preferences.common.gnirehtet.fix.tips', tips: 'preferences.common.gnirehtet.fix.tips',
}, },
floatControl: {
label: 'preferences.common.floatControl.name',
field: 'floatControl',
type: 'Switch',
value: undefined,
placeholder: 'preferences.common.floatControl.placeholder',
},
debug: { debug: {
label: 'preferences.common.debug.name', label: 'preferences.common.debug.name',
field: 'debug', field: 'debug',

View File

@ -72,3 +72,13 @@ export async function selectAndSendFileToDevice(
return successFiles return successFiles
} }
export function openFloatControl(deviceInfo) {
const floatControl = window.appStore.get('common.floatControl')
if (!floatControl) {
return false
}
window.electron.ipcRenderer.send('open-control-window', deviceInfo)
}

View File

@ -13,37 +13,37 @@ import postcssConfig from './postcss.config.js'
import useAutoImports from './src/plugins/auto.js' import useAutoImports from './src/plugins/auto.js'
const merge = (config, { command = '' } = {}) => const alias = {
mergeConfig( $: resolve('src'),
$root: resolve(),
$renderer: resolve('src'),
$electron: resolve('electron'),
$control: resolve('control'),
}
function mergeCommon(config, { command = '' } = {}) {
return mergeConfig(
{ {
resolve: { resolve: {
alias: { alias,
$root: resolve(),
$electron: resolve('electron'),
$renderer: resolve('src'),
},
}, },
plugins: [...(command === 'serve' ? [notBundle()] : [])], plugins: [...(command === 'serve' ? [notBundle()] : [])],
}, },
config, config,
) )
}
export default args => export default function (args) {
merge( return mergeCommon(
defineConfig({ defineConfig({
build: { build: {
rollupOptions: { rollupOptions: {
input: { input: {
main: resolve('index.html'), main: resolve('index.html'),
control: resolve('control/index.html'),
}, },
}, },
}, },
resolve: {
alias: {
$: resolve('src'),
$electron: resolve('electron'),
},
},
plugins: [ plugins: [
useUnoCSS(), useUnoCSS(),
useSvg(), useSvg(),
@ -54,11 +54,11 @@ export default args =>
useElectron({ useElectron({
main: { main: {
entry: 'electron/main.js', entry: 'electron/main.js',
vite: merge({}, args), vite: mergeCommon({}, args),
}, },
preload: { preload: {
input: 'electron/preload.js', input: 'electron/preload.js',
vite: merge({}, args), vite: mergeCommon({}, args),
}, },
}), }),
useRenderer(), useRenderer(),
@ -69,3 +69,4 @@ export default args =>
}, },
}), }),
) )
}