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',
'eqeqeq': 'off',
'prefer-promise-reject-errors': 'off',
'antfu/top-level-function': 'off',
'import/default': 'off',
'vue/no-mutating-props': 'off',
},
}

View File

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

View File

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

View File

@ -4,10 +4,9 @@ import path from 'node:path'
import fs from 'node:fs'
import dayjs from 'dayjs'
import { Adb } from '@devicefarmer/adbkit'
import appStore from '@electron/helpers/store.js'
import { adbPath } from '@electron/configs/index.js'
// console.log('adbPath', adbPath)
const exec = util.promisify(child_process.exec)
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 getDevices = async () => client.listDevicesWithPaths()
const deviceShell = async (id, command) => client.getDevice(id).shell(command)
@ -100,8 +108,8 @@ const watch = async (callback) => {
}
export default () => {
client = Adb.createClient({ bin: adbPath })
// console.log('client', client)
client = Adb.createClient({ bin: appStore.get('scrcpy.adbPath') || adbPath })
console.log('client', client)
return {
shell,

View File

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

View File

@ -1,12 +1,17 @@
import { spawn } from 'node:child_process'
import appStore from '@electron/helpers/store.js'
import { adbPath, scrcpyPath } from '@electron/configs/index.js'
const shell = async (command, { stdout, stderr } = {}) => {
const args = command.split(' ')
const scrcpyProcess = spawn(scrcpyPath, args, {
env: { ...process.env, ADB: adbPath },
shell: true,
})
const scrcpyProcess = spawn(
appStore.get('scrcpy.scrcpyPath') || scrcpyPath,
args,
{
env: { ...process.env, ADB: adbPath },
shell: true,
},
)
scrcpyProcess.stdout.on('data', (data) => {
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 { BrowserWindow, app, shell } from 'electron'
import Store from 'electron-store'
import { electronApp, optimizer } from '@electron-toolkit/utils'
// packaged.js 必须位于非依赖项的顶部
import './helpers/packaged.js'
import './helpers/store.js'
import { icnsLogoPath, icoLogoPath, logoPath } from './configs/index.js'
import events from './events/index.js'
const appStore = new Store()
// The built directory structure
//
// ├─┬─┬ dist

View File

@ -8,7 +8,7 @@
:name="item.prop"
lazy
>
<component :is="item.prop" :ref="item.prop" />
<component :is="item.prop" v-if="isRender(item)" :ref="item.prop" />
</el-tab-pane>
</el-tabs>
</div>
@ -42,13 +42,31 @@ export default {
},
],
activeTab: 'Device',
rendered: true,
}
},
created() {
this.$store.scrcpy.init()
},
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>

View File

@ -84,13 +84,19 @@ export default {
},
methods: {
async handleInstall(device) {
const files = await this.$electron.ipcRenderer.invoke(
'show-open-dialog',
{
let files = null
try {
files = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
properties: ['openFile', 'multiSelections'],
filters: [{ name: '请选择要安装的应用', extensions: ['apk'] }],
},
)
})
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
}
if (!files) {
return false
@ -174,7 +180,10 @@ export default {
closeOnClickModal: false,
type: 'success',
})
this.$electron.ipcRenderer.invoke('show-item-in-folder', savePath)
await this.$electron.ipcRenderer.invoke(
'show-item-in-folder',
savePath,
)
}
catch (error) {
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"
show-overflow-tooltip
align="left"
width="200"
/>
<el-table-column
prop="name"
@ -84,13 +85,15 @@
{{ 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
</el-tag>
<Remark :device="row" />
</div>
</template>
</el-table-column>
<el-table-column label="操作" width="500" align="left">
<el-table-column label="操作" width="400" align="left">
<template #default="{ row }">
<el-button
type="primary"
@ -115,7 +118,7 @@
</el-button>
<el-button
v-if="!row.$wireless"
v-if="!row.$wifi"
type="primary"
text
:disabled="
@ -130,7 +133,7 @@
</el-button>
<el-button
v-if="row.$wireless"
v-if="row.$wifi"
type="danger"
text
:loading="row.$stopLoading"
@ -162,6 +165,7 @@
import dayjs from 'dayjs'
import PairDialog from './PairDialog/index.vue'
import ControlBar from './ControlBar/index.vue'
import Remark from './Remark/index.vue'
import storage from '@/utils/storages'
import { isIPWithPort, sleep } from '@/utils/index.js'
@ -169,6 +173,7 @@ export default {
components: {
PairDialog,
ControlBar,
Remark,
},
data() {
const adbCache = storage.get('adbCache') || {}
@ -191,13 +196,15 @@ export default {
return this.$store.scrcpy.stringConfig
},
},
created() {
async created() {
this.getDeviceData()
this.$adb.watch(async (type, ret) => {
this.unAdbWatch = await this.$adb.watch(async (type, ret) => {
console.log('adb.watch.ret', ret)
this.getDeviceData()
if (ret && ret.id) {
this.getDeviceData()
}
if (type === 'add' && !isIPWithPort(ret.id) && ret.$host) {
this.formData = {
@ -207,6 +214,11 @@ export default {
}
})
},
beforeUnmount() {
if (this.unAdbWatch) {
this.unAdbWatch()
}
},
methods: {
onStdout() {},
toggleRowExpansion(...params) {
@ -242,7 +254,10 @@ export default {
type: 'success',
})
this.$electron.ipcRenderer.invoke('show-item-in-folder', savePath)
await this.$electron.ipcRenderer.invoke(
'show-item-in-folder',
savePath,
)
}
catch (error) {
if (error.message) {
@ -362,15 +377,16 @@ export default {
$recordLoading: false,
$stopLoading: false,
$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)
}
catch (error) {
console.log(error)
if (error.message) {
this.$message.warning(error.message)
console.warn(error)
if (error?.message || error?.cause?.message) {
this.$message.warning(error?.message || error?.cause?.message)
}
this.deviceList = []
}

View File

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

View File

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