feat: 🚀 新增支持添加设备备注

This commit is contained in:
viarotel 2023-10-19 11:44:17 +08:00
parent 326a133460
commit 43f15be265
17 changed files with 301 additions and 154 deletions

View File

@ -4,7 +4,11 @@ module.exports = {
'no-unused-vars': 'off', 'no-unused-vars': 'off',
'eqeqeq': 'off', 'eqeqeq': 'off',
'prefer-promise-reject-errors': 'off', 'prefer-promise-reject-errors': 'off',
'antfu/top-level-function': 'off', 'antfu/top-level-function': 'off',
'import/default': 'off', 'import/default': 'off',
'vue/no-mutating-props': 'off',
}, },
} }

View File

@ -66,7 +66,7 @@
- Adb 路径 - Adb 路径
- Scrcpy 路径 - Scrcpy 路径
- 文件存储路径(音视频及截图保存在这里) - 文件存储路径(音视频录制设备截图保存在这里)
### 视频控制 ### 视频控制
@ -98,7 +98,6 @@
### 音视频录制 ### 音视频录制
- 文件保存路径
- 录制视频格式 - 录制视频格式
### 音频控制 ### 音频控制

View File

@ -6,12 +6,18 @@ export default () => {
'show-open-dialog', 'show-open-dialog',
async (event, { preset = '', ...options } = {}) => { async (event, { preset = '', ...options } = {}) => {
// console.log('options', options) // console.log('options', options)
try { const res = await dialog
const res = await dialog.showOpenDialog(options) .showOpenDialog(options)
// console.log('showOpenDialog.res', res) .catch(e => console.warn(e))
if (res.canceled) { if (res.canceled) {
return false throw new Error('用户取消操作')
} }
if (!res.filePaths.length) {
throw new Error('获取目录或文件路径失败')
}
const filePaths = res.filePaths const filePaths = res.filePaths
switch (preset) { switch (preset) {
@ -21,57 +27,37 @@ export default () => {
} }
return filePaths return filePaths
}
catch (error) {
console.warn(error?.message || error)
return false
}
}, },
) )
ipcMain.handle('open-path', async (event, pathValue) => { ipcMain.handle('open-path', async (event, pathValue) => {
try { return shell.openPath(pathValue)
await shell.openPath(pathValue)
return true
}
catch (error) {
console.warn(error?.message || error)
return false
}
}) })
ipcMain.handle('show-item-in-folder', async (event, filePath) => { ipcMain.handle('show-item-in-folder', async (event, filePath) => {
try { return shell.showItemInFolder(filePath)
await shell.showItemInFolder(filePath)
return true
}
catch (error) {
console.warn(error?.message || error)
return false
}
}) })
ipcMain.handle( ipcMain.handle(
'show-save-dialog', 'show-save-dialog',
async (event, { filePath = '', ...options } = {}) => { async (event, { filePath = '', ...options } = {}) => {
try { const res = await dialog
const result = await dialog.showSaveDialog({ .showSaveDialog({
...options, ...options,
}) })
if (!result || result.canceled || !result.filePath) { .catch(e => console.warn(e))
return false
if (res.canceled) {
throw new Error('用户取消操作')
} }
const destinationPath = result.filePath if (!res.filePath) {
throw new Error('获取文件路径失败')
}
const destinationPath = res.filePath
await fs.copy(filePath, destinationPath) await fs.copy(filePath, destinationPath)
return true
}
catch (error) {
console.error(error?.message || error)
return false
}
}, },
) )
} }

View File

@ -4,10 +4,9 @@ import path from 'node:path'
import fs from 'node:fs' import fs from 'node:fs'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { Adb } from '@devicefarmer/adbkit' import { Adb } from '@devicefarmer/adbkit'
import appStore from '@electron/helpers/store.js'
import { adbPath } from '@electron/configs/index.js' import { adbPath } from '@electron/configs/index.js'
// console.log('adbPath', adbPath)
const exec = util.promisify(child_process.exec) const exec = util.promisify(child_process.exec)
let client = null let client = null
@ -18,6 +17,15 @@ window.addEventListener('beforeunload', () => {
} }
}) })
appStore.onDidChange('scrcpy.adbPath', async (value) => {
console.log('onDidChange.scrcpy.adbPath.value', value)
if (client) {
await client.kill().catch(e => console.warn(e))
client = null
}
client = Adb.createClient({ bin: value })
})
const shell = async command => exec(`${adbPath} ${command}`) const shell = async command => exec(`${adbPath} ${command}`)
const getDevices = async () => client.listDevicesWithPaths() const getDevices = async () => client.listDevicesWithPaths()
const deviceShell = async (id, command) => client.getDevice(id).shell(command) const deviceShell = async (id, command) => client.getDevice(id).shell(command)
@ -100,8 +108,8 @@ const watch = async (callback) => {
} }
export default () => { export default () => {
client = Adb.createClient({ bin: adbPath }) client = Adb.createClient({ bin: appStore.get('scrcpy.adbPath') || adbPath })
// console.log('client', client) console.log('client', client)
return { return {
shell, shell,

View File

@ -1,7 +1,7 @@
import path from 'node:path' import path from 'node:path'
import store from '@electron/helpers/store.js'
import * as configs from '@electron/configs/index.js' import * as configs from '@electron/configs/index.js'
import store from './store/index.js'
import electron from './electron/index.js' import electron from './electron/index.js'
import adbkit from './adbkit/index.js' import adbkit from './adbkit/index.js'
import scrcpy from './scrcpy/index.js' import scrcpy from './scrcpy/index.js'
@ -10,7 +10,7 @@ export default {
init(expose) { init(expose) {
expose('nodePath', path) expose('nodePath', path)
expose('appStore', store()) expose('appStore', store)
expose('electron', { expose('electron', {
...electron(), ...electron(),

View File

@ -1,12 +1,17 @@
import { spawn } from 'node:child_process' import { spawn } from 'node:child_process'
import appStore from '@electron/helpers/store.js'
import { adbPath, scrcpyPath } from '@electron/configs/index.js' import { adbPath, scrcpyPath } from '@electron/configs/index.js'
const shell = async (command, { stdout, stderr } = {}) => { const shell = async (command, { stdout, stderr } = {}) => {
const args = command.split(' ') const args = command.split(' ')
const scrcpyProcess = spawn(scrcpyPath, args, { const scrcpyProcess = spawn(
appStore.get('scrcpy.scrcpyPath') || scrcpyPath,
args,
{
env: { ...process.env, ADB: adbPath }, env: { ...process.env, ADB: adbPath },
shell: true, shell: true,
}) },
)
scrcpyProcess.stdout.on('data', (data) => { scrcpyProcess.stdout.on('data', (data) => {
const stringData = data.toString() const stringData = data.toString()

View File

@ -1,27 +0,0 @@
import Store from 'electron-store'
import { createProxy } from '@electron/helpers/index'
export default () => {
const appStore = new Store()
appStore.onDidAnyChange(() => {
console.log('reload.appStore.onDidAnyChange', appStore.store)
})
return {
...createProxy(appStore, [
'set',
'get',
'delete',
'clear',
'reset',
'has',
'onDidChange',
'onDidAnyChange',
'openInEditor',
]),
...appStore,
getAll: () => appStore.store,
setAll: value => (appStore.store = value),
}
}

25
electron/helpers/store.js Normal file
View File

@ -0,0 +1,25 @@
import Store from 'electron-store'
import { createProxy } from './index.js'
const appStore = new Store()
appStore.onDidAnyChange(() => {
console.log('appStore.onDidAnyChange', appStore.store)
})
export default {
...createProxy(appStore, [
'set',
'get',
'delete',
'clear',
'reset',
'has',
'onDidChange',
'onDidAnyChange',
'openInEditor',
]),
...appStore,
getAll: () => appStore.store,
setAll: value => (appStore.store = value),
}

View File

@ -1,17 +1,15 @@
import path from 'node:path' import path from 'node:path'
import { BrowserWindow, app, shell } from 'electron' import { BrowserWindow, app, shell } from 'electron'
import Store from 'electron-store'
import { electronApp, optimizer } from '@electron-toolkit/utils' import { electronApp, optimizer } from '@electron-toolkit/utils'
// packaged.js 必须位于非依赖项的顶部 // packaged.js 必须位于非依赖项的顶部
import './helpers/packaged.js' import './helpers/packaged.js'
import './helpers/store.js'
import { icnsLogoPath, icoLogoPath, logoPath } from './configs/index.js' import { icnsLogoPath, icoLogoPath, logoPath } from './configs/index.js'
import events from './events/index.js' import events from './events/index.js'
const appStore = new Store()
// The built directory structure // The built directory structure
// //
// ├─┬─┬ dist // ├─┬─┬ dist

View File

@ -8,7 +8,7 @@
:name="item.prop" :name="item.prop"
lazy lazy
> >
<component :is="item.prop" :ref="item.prop" /> <component :is="item.prop" v-if="isRender(item)" :ref="item.prop" />
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
@ -42,13 +42,31 @@ export default {
}, },
], ],
activeTab: 'Device', activeTab: 'Device',
rendered: true,
} }
}, },
created() { created() {
this.$store.scrcpy.init() this.$store.scrcpy.init()
}, },
methods: { methods: {
onTabChange(prop) {}, isRender(item) {
if (this.activeTab === item.prop) {
return this.rendered
}
return true
},
async reRender() {
this.rendered = false
await this.$nextTick()
this.rendered = true
},
async onTabChange(prop) {
switch (prop) {
case 'Device':
this.reRender()
break
}
},
}, },
} }
</script> </script>

View File

@ -84,13 +84,19 @@ export default {
}, },
methods: { methods: {
async handleInstall(device) { async handleInstall(device) {
const files = await this.$electron.ipcRenderer.invoke( let files = null
'show-open-dialog',
{ try {
files = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
properties: ['openFile', 'multiSelections'], properties: ['openFile', 'multiSelections'],
filters: [{ name: '请选择要安装的应用', extensions: ['apk'] }], filters: [{ name: '请选择要安装的应用', extensions: ['apk'] }],
}, })
) }
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
if (!files) { if (!files) {
return false return false
@ -174,7 +180,10 @@ export default {
closeOnClickModal: false, closeOnClickModal: false,
type: 'success', type: 'success',
}) })
this.$electron.ipcRenderer.invoke('show-item-in-folder', savePath) await this.$electron.ipcRenderer.invoke(
'show-item-in-folder',
savePath,
)
} }
catch (error) { catch (error) {
if (error.message) { if (error.message) {

View File

@ -0,0 +1,55 @@
<template>
<el-popover
placement="bottom-start"
:width="250"
trigger="hover"
@show="handleFocus"
>
<template #reference>
<el-tag effect="light" class="ml-2 cursor-pointer">
<div class="flex items-center">
<el-icon>
<EditPen />
</el-icon>
<span class="pl-1">{{ device.$remark || "备注" }}</span>
</div>
</el-tag>
</template>
<el-input
ref="elInput"
v-model="device.$remark"
class=""
placeholder="请输入设备备注"
clearable
@change="onChange"
></el-input>
</el-popover>
</template>
<script>
export default {
props: {
device: {
type: Object,
default: () => ({}),
},
},
data() {
return {}
},
created() {},
methods: {
onChange(value) {
// console.log('onChange', value)
this.$store.device.setRemark(this.device.id, value)
},
async handleFocus() {
console.log('handleFocus')
this.$refs.elInput.focus()
},
},
}
</script>
<style></style>

View File

@ -63,6 +63,7 @@
label="设备 ID" label="设备 ID"
show-overflow-tooltip show-overflow-tooltip
align="left" align="left"
width="200"
/> />
<el-table-column <el-table-column
prop="name" prop="name"
@ -84,13 +85,15 @@
{{ row.name }} {{ row.name }}
<el-tag v-if="row.$wireless" effect="light" class="ml-2"> <el-tag v-if="row.$wifi" effect="light" class="ml-2">
WIFI WIFI
</el-tag> </el-tag>
<Remark :device="row" />
</div> </div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="500" align="left"> <el-table-column label="操作" width="400" align="left">
<template #default="{ row }"> <template #default="{ row }">
<el-button <el-button
type="primary" type="primary"
@ -115,7 +118,7 @@
</el-button> </el-button>
<el-button <el-button
v-if="!row.$wireless" v-if="!row.$wifi"
type="primary" type="primary"
text text
:disabled=" :disabled="
@ -130,7 +133,7 @@
</el-button> </el-button>
<el-button <el-button
v-if="row.$wireless" v-if="row.$wifi"
type="danger" type="danger"
text text
:loading="row.$stopLoading" :loading="row.$stopLoading"
@ -162,6 +165,7 @@
import dayjs from 'dayjs' import dayjs from 'dayjs'
import PairDialog from './PairDialog/index.vue' import PairDialog from './PairDialog/index.vue'
import ControlBar from './ControlBar/index.vue' import ControlBar from './ControlBar/index.vue'
import Remark from './Remark/index.vue'
import storage from '@/utils/storages' import storage from '@/utils/storages'
import { isIPWithPort, sleep } from '@/utils/index.js' import { isIPWithPort, sleep } from '@/utils/index.js'
@ -169,6 +173,7 @@ export default {
components: { components: {
PairDialog, PairDialog,
ControlBar, ControlBar,
Remark,
}, },
data() { data() {
const adbCache = storage.get('adbCache') || {} const adbCache = storage.get('adbCache') || {}
@ -191,13 +196,15 @@ export default {
return this.$store.scrcpy.stringConfig return this.$store.scrcpy.stringConfig
}, },
}, },
created() { async created() {
this.getDeviceData() this.getDeviceData()
this.$adb.watch(async (type, ret) => { this.unAdbWatch = await this.$adb.watch(async (type, ret) => {
console.log('adb.watch.ret', ret) console.log('adb.watch.ret', ret)
if (ret && ret.id) {
this.getDeviceData() this.getDeviceData()
}
if (type === 'add' && !isIPWithPort(ret.id) && ret.$host) { if (type === 'add' && !isIPWithPort(ret.id) && ret.$host) {
this.formData = { this.formData = {
@ -207,6 +214,11 @@ export default {
} }
}) })
}, },
beforeUnmount() {
if (this.unAdbWatch) {
this.unAdbWatch()
}
},
methods: { methods: {
onStdout() {}, onStdout() {},
toggleRowExpansion(...params) { toggleRowExpansion(...params) {
@ -242,7 +254,10 @@ export default {
type: 'success', type: 'success',
}) })
this.$electron.ipcRenderer.invoke('show-item-in-folder', savePath) await this.$electron.ipcRenderer.invoke(
'show-item-in-folder',
savePath,
)
} }
catch (error) { catch (error) {
if (error.message) { if (error.message) {
@ -362,15 +377,16 @@ export default {
$recordLoading: false, $recordLoading: false,
$stopLoading: false, $stopLoading: false,
$unauthorized: item.type === 'unauthorized', $unauthorized: item.type === 'unauthorized',
$wireless: isIPWithPort(item.id), $wifi: isIPWithPort(item.id),
$remark: this.$store.device.getRemark(item.id),
})) || [] })) || []
console.log('getDeviceData.data', this.deviceList) console.log('getDeviceData.data', this.deviceList)
} }
catch (error) { catch (error) {
console.log(error) console.warn(error)
if (error.message) { if (error?.message || error?.cause?.message) {
this.$message.warning(error.message) this.$message.warning(error?.message || error?.cause?.message)
} }
this.deviceList = [] this.deviceList = []
} }

View File

@ -90,19 +90,24 @@
<el-input <el-input
v-if="item_1.type === 'input.path'" v-if="item_1.type === 'input.path'"
v-bind="item_1.props || {}" v-bind="item_1.props || {}"
:value="scrcpyForm[item_1.field]" v-model="scrcpyForm[item_1.field]"
readonly
class="!w-full" class="!w-full"
clearable clearable
:placeholder="item_1.placeholder" :placeholder="item_1.placeholder"
:title="item_1.placeholder" :title="item_1.placeholder"
>
<template #append>
<el-button
icon="Search"
@click=" @click="
handleSelect(item_1, { handleSelect(item_1, {
properties: item_1.properties, properties: item_1.properties,
filters: item_1.filters, filters: item_1.filters,
}) })
" "
></el-input> />
</template>
</el-input>
<el-switch <el-switch
v-if="item_1.type === 'switch'" v-if="item_1.type === 'switch'"
v-bind="item_1.props || {}" v-bind="item_1.props || {}"
@ -193,22 +198,22 @@ export default {
}, },
methods: { methods: {
async handleImport() { async handleImport() {
const result = await this.$electron.ipcRenderer.invoke( try {
'show-open-dialog', await this.$electron.ipcRenderer.invoke('show-open-dialog', {
{
preset: 'replaceFile', preset: 'replaceFile',
filePath: this.$appStore.path, filePath: this.$appStore.path,
filters: [{ name: '请选择要导入的配置文件', extensions: ['json'] }], filters: [{ name: '请选择要导入的配置文件', extensions: ['json'] }],
}, })
)
if (!result) {
this.$message.warning('导入偏好配置失败')
return
}
this.$message.success('导入偏好配置成功') this.$message.success('导入偏好配置成功')
this.scrcpyForm = this.$store.scrcpy.init() this.scrcpyForm = this.$store.scrcpy.init()
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
}, },
handleEdit() { handleEdit() {
this.$appStore.openInEditor() this.$appStore.openInEditor()
@ -220,40 +225,47 @@ export default {
duration: 0, duration: 0,
}) })
const result = await this.$electron.ipcRenderer.invoke( try {
'show-save-dialog', await this.$electron.ipcRenderer.invoke('show-save-dialog', {
{
defaultPath: 'escrcpy-configs.json', defaultPath: 'escrcpy-configs.json',
filePath: this.$appStore.path, filePath: this.$appStore.path,
filters: [ filters: [
{ name: '请选择配置文件要保存的位置', extensions: ['json'] }, { name: '请选择配置文件要保存的位置', extensions: ['json'] },
], ],
}, })
)
messageEl.close()
if (result) {
this.$message.success('导出偏好配置成功') this.$message.success('导出偏好配置成功')
} }
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
messageEl.close()
}, },
async handleSelect({ field }, { properties, filters } = {}) { async handleSelect({ field }, { properties, filters } = {}) {
const res = await this.$electron.ipcRenderer.invoke('show-open-dialog', { try {
const files = await this.$electron.ipcRenderer.invoke(
'show-open-dialog',
{
properties, properties,
filters, filters,
defaultPath: this.scrcpyForm[field], defaultPath: this.scrcpyForm[field],
}) },
)
if (!res) { const value = files[0]
return false
}
const value = res[0]
this.scrcpyForm[field] = value this.scrcpyForm[field] = value
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
}, },
handleSave() { handleSave() {
this.$store.scrcpy.updateConfig(this.scrcpyForm) this.$store.scrcpy.setConfig(this.scrcpyForm)
this.$message.success('保存配置成功,将在下一次控制设备时生效') this.$message.success('保存配置成功,将在下一次控制设备时生效')
}, },
getSubModel(type) { getSubModel(type) {
@ -265,7 +277,7 @@ export default {
...this.scrcpyForm, ...this.scrcpyForm,
...this.$store.scrcpy.getDefaultConfig(type), ...this.$store.scrcpy.getDefaultConfig(type),
} }
this.$store.scrcpy.updateConfig(this.scrcpyForm) this.$store.scrcpy.setConfig(this.scrcpyForm)
}, },
}, },
} }

37
src/store/device/index.js Normal file
View File

@ -0,0 +1,37 @@
import { defineStore } from 'pinia'
const $appStore = window.appStore
const removeDot = value => value.replaceAll('.', '_')
export const useDeviceStore = defineStore({
id: 'app-device',
state() {
return {
config: {},
}
},
getters: {},
actions: {
removeDot,
init() {
this.config = {
...($appStore.get('device') || {}),
}
return this.config
},
setConfig(value, key = 'device') {
$appStore.set(key, value)
this.init()
},
setRemark(deviceId, value) {
$appStore.set(`device.${removeDot(deviceId)}.remark`, value)
this.init()
},
getRemark(deviceId) {
const value = $appStore.get(`device.${removeDot(deviceId)}.remark`)
return value
},
},
})

View File

@ -1,7 +1,8 @@
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
import { useScrcpyStore } from './scrcpy/index.js' import { useScrcpyStore } from './scrcpy/index.js'
import { useDeviceStore } from './device/index.js'
export { useScrcpyStore } export { useScrcpyStore, useDeviceStore }
export default { export default {
install(app) { install(app) {
@ -10,6 +11,7 @@ export default {
app.use(store) app.use(store)
app.config.globalProperties.$store = { app.config.globalProperties.$store = {
scrcpy: useScrcpyStore(), scrcpy: useScrcpyStore(),
device: useDeviceStore(),
} }
}, },
} }

View File

@ -64,7 +64,7 @@ export const useScrcpyStore = defineStore({
}, []) }, [])
.join(' ') .join(' ')
console.log('stringifyConfig.value', value) // console.log('stringifyConfig.value', value)
return value return value
}, },
@ -79,10 +79,10 @@ export const useScrcpyStore = defineStore({
return this.config return this.config
}, },
updateConfig(data) { setConfig(data) {
const pickConfig = pickBy(data, value => !!value) const pickConfig = pickBy(data, value => !!value)
console.log('pickConfig', pickConfig) // console.log('pickConfig', pickConfig)
$appStore.set('scrcpy', pickConfig) $appStore.set('scrcpy', pickConfig)