mirror of
https://github.com/viarotel-org/escrcpy.git
synced 2025-01-19 17:28:47 +01:00
Merge branch 'dev'
This commit is contained in:
commit
61a0c8d029
@ -79,6 +79,7 @@
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
"watchSyncEffect": true,
|
||||
"ElMessage": true
|
||||
}
|
||||
}
|
||||
|
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -30,7 +30,7 @@
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"i18n-ally.localesPaths": ["src/locales/index.js", "src/locales/languages"],
|
||||
"i18n-ally.sourceLanguage": "en",
|
||||
"i18n-ally.sourceLanguage": "zh-CN",
|
||||
"i18n-ally.keystyle": "nested",
|
||||
"i18n-ally.extract.ignored": [
|
||||
"Switch",
|
||||
|
@ -76,7 +76,10 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
|
||||
### 批量处理
|
||||
|
||||
- 批量截取屏幕
|
||||
- 批量安装应用
|
||||
- 批量文件管理
|
||||
- 批量执行脚本
|
||||
|
||||
### 控制模式
|
||||
|
||||
@ -84,6 +87,7 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
- 录制
|
||||
- OTG
|
||||
- 摄像
|
||||
- 灵活启动
|
||||
|
||||
### 设备交互栏
|
||||
|
||||
@ -98,6 +102,7 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
- 重启设备
|
||||
- 安装应用
|
||||
- 文件管理
|
||||
- 执行脚本
|
||||
- 反向供网(Gnirehtet)
|
||||
- 多屏协同
|
||||
|
||||
@ -203,8 +208,8 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
15. 支持批量连接历史设备功能 ✅
|
||||
16. 支持使用内置终端执行自定义命令 ✅
|
||||
17. 支持设备自动执行镜像 ✅
|
||||
18. 添加批量安装应用功能 ✅
|
||||
19. 支持更多批量处理功能 🚧
|
||||
18. 支持灵活启动镜像 ✅
|
||||
19. 支持常用批量功能 ✅
|
||||
20. 支持对设备进行分组 🚧
|
||||
21. 添加文件传输助手功能 🚧
|
||||
22. 支持通过界面从设备下载选中的文件 🚧
|
||||
|
13
README.md
13
README.md
@ -74,7 +74,10 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
|
||||
|
||||
### Batch Processing
|
||||
|
||||
- Batch installation application
|
||||
- Batch Interception Screen
|
||||
- Batch Installation Application
|
||||
- Batch File Management
|
||||
- Batch Execution Script
|
||||
|
||||
### Control Model
|
||||
|
||||
@ -82,6 +85,7 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
|
||||
- Recording
|
||||
- OTG
|
||||
- Camera
|
||||
- Custom
|
||||
|
||||
### Device Interaction Bar
|
||||
|
||||
@ -96,6 +100,7 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
|
||||
- Reboot
|
||||
- Install APP
|
||||
- File Manager
|
||||
- Execution Script
|
||||
- Gnirehtet
|
||||
- Mirror Group
|
||||
|
||||
@ -200,9 +205,9 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master
|
||||
14. Add more features to device interaction bar: file push, screen rotation, audio control etc ✅
|
||||
15. Support bulk connecting to historical devices ✅
|
||||
16. Support to use built-in terminals to execute custom commands ✅
|
||||
17. Supports automatic execution of mirror on devices ✅
|
||||
18. Add batch installation application function ✅
|
||||
19. Support more batch processing functions 🚧
|
||||
17. Support automatic execution of mirror on devices ✅
|
||||
18. Support for custom startup mirroring ✅
|
||||
19. Support common batch processing function ✅
|
||||
20. Support the device to group 🚧
|
||||
21. Add file transmission assistant function 🚧
|
||||
22. Support GUI-based selective file downloads from devices 🚧
|
||||
|
1
auto-imports.d.ts
vendored
1
auto-imports.d.ts
vendored
@ -6,6 +6,7 @@
|
||||
export {}
|
||||
declare global {
|
||||
const EffectScope: typeof import('vue')['EffectScope']
|
||||
const ElMessage: typeof import('element-plus/es')['ElMessage']
|
||||
const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate']
|
||||
const computed: typeof import('vue')['computed']
|
||||
const createApp: typeof import('vue')['createApp']
|
||||
|
31
components.d.ts
vendored
31
components.d.ts
vendored
@ -7,15 +7,6 @@ export {}
|
||||
/* prettier-ignore */
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
About: typeof import('./src/components/About/index.vue')['default']
|
||||
AppInstall: typeof import('./src/components/Device/components/BatchActions/AppInstall/index.vue')['default']
|
||||
AppSearch: typeof import('./src/components/AppSearch/index.vue')['default']
|
||||
AudioCodecSelect: typeof import('./src/components/Preference/components/AudioCodecSelect/index.vue')['default']
|
||||
BatchActions: typeof import('./src/components/Device/components/BatchActions/index.vue')['default']
|
||||
Camera: typeof import('./src/components/Device/components/MoreDropdown/components/Camera/index.vue')['default']
|
||||
ControlBar: typeof import('./src/components/Device/components/ControlBar/index.vue')['default']
|
||||
Device: typeof import('./src/components/Device/index.vue')['default']
|
||||
DisplaySelect: typeof import('./src/components/Preference/components/DisplaySelect/index.vue')['default']
|
||||
ElAutocomplete: typeof import('element-plus/es')['ElAutocomplete']
|
||||
ElButton: typeof import('element-plus/es')['ElButton']
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
@ -43,28 +34,6 @@ declare module 'vue' {
|
||||
ElTabs: typeof import('element-plus/es')['ElTabs']
|
||||
ElTag: typeof import('element-plus/es')['ElTag']
|
||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
||||
FileManage: typeof import('./src/components/Device/components/ControlBar/FileManage/index.vue')['default']
|
||||
Gnirehtet: typeof import('./src/components/Device/components/ControlBar/Gnirehtet/index.vue')['default']
|
||||
KeyboardInjectSelect: typeof import('./src/components/Preference/components/KeyboardInjectSelect/index.vue')['default']
|
||||
LanguageSelect: typeof import('./src/components/Preference/components/LanguageSelect/index.vue')['default']
|
||||
LoadingIcon: typeof import('./src/components/Device/components/LoadingIcon/index.vue')['default']
|
||||
MirrorAction: typeof import('./src/components/Device/components/MirrorAction/index.vue')['default']
|
||||
MirrorGroup: typeof import('./src/components/Device/components/ControlBar/MirrorGroup/index.vue')['default']
|
||||
MoreDropdown: typeof import('./src/components/Device/components/MoreDropdown/index.vue')['default']
|
||||
Otg: typeof import('./src/components/Device/components/MoreDropdown/components/Otg/index.vue')['default']
|
||||
PairDialog: typeof import('./src/components/Device/components/Wireless/PairDialog/index.vue')['default']
|
||||
PathInput: typeof import('./src/components/Preference/components/PathInput/index.vue')['default']
|
||||
Preference: typeof import('./src/components/Preference/index.vue')['default']
|
||||
Record: typeof import('./src/components/Device/components/MoreDropdown/components/Record/index.vue')['default']
|
||||
Remark: typeof import('./src/components/Device/components/Remark/index.vue')['default']
|
||||
Rotation: typeof import('./src/components/Device/components/ControlBar/Rotation/index.vue')['default']
|
||||
Screenshot: typeof import('./src/components/Device/components/ControlBar/Screenshot/index.vue')['default']
|
||||
TerminalAction: typeof import('./src/components/Device/components/TerminalAction/index.vue')['default']
|
||||
TerminalDialog: typeof import('./src/components/Device/components/TerminalAction/components/TerminalDialog/index.vue')['default']
|
||||
VideoCodecSelect: typeof import('./src/components/Preference/components/VideoCodecSelect/index.vue')['default']
|
||||
Volume: typeof import('./src/components/Device/components/ControlBar/Volume/index.vue')['default']
|
||||
Wireless: typeof import('./src/components/Device/components/Wireless/index.vue')['default']
|
||||
WirelessAction: typeof import('./src/components/Device/components/WirelessAction/index.vue')['default']
|
||||
}
|
||||
export interface ComponentCustomProperties {
|
||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
||||
|
@ -198,8 +198,8 @@ const push = async (
|
||||
progress?.(stats)
|
||||
})
|
||||
|
||||
res.on('end', (ret) => {
|
||||
resolve(ret)
|
||||
res.on('end', () => {
|
||||
resolve(savePath)
|
||||
})
|
||||
|
||||
res.on('error', (err) => {
|
||||
|
@ -27,8 +27,6 @@ log.initialize({ preload: true })
|
||||
|
||||
const debug = !!appStore.get('common.debug')
|
||||
|
||||
log.info('Debug Status:', debug)
|
||||
|
||||
if (!debug) {
|
||||
log.warn(
|
||||
'Debug Tips:',
|
||||
|
@ -33,7 +33,7 @@ import { ElMessageBox } from 'element-plus'
|
||||
import Device from './components/Device/index.vue'
|
||||
import Preference from './components/Preference/index.vue'
|
||||
import About from './components/About/index.vue'
|
||||
import AppSearch from './components/AppSearch/index.vue'
|
||||
import AppSearch from './components/Search/index.vue'
|
||||
|
||||
import { useThemeStore } from '$/store/theme/index.js'
|
||||
import { usePreferenceStore } from '$/store/preference/index.js'
|
||||
|
@ -1,34 +0,0 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick">
|
||||
<slot />
|
||||
<AppInstallProxy ref="appInstallProxyRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppInstallProxy from '$/components/Device/components/ControlBar/AppInstall/index.vue'
|
||||
import { sleep } from '$/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AppInstallProxy,
|
||||
},
|
||||
props: {
|
||||
devices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
for (let index = 0; index < this.devices.length; index++) {
|
||||
const item = this.devices[index]
|
||||
await this.$refs.appInstallProxyRef.handleInstall(item)
|
||||
await sleep(2 * 1000)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick">
|
||||
<slot v-bind="{ loading }" />
|
||||
<ApplicationProxy ref="applicationProxyRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApplicationProxy from '$/components/Device/components/ControlBar/Application/index.vue'
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ApplicationProxy,
|
||||
},
|
||||
props: {
|
||||
devices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
let files = null
|
||||
|
||||
try {
|
||||
files = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
name: this.$t('device.control.install.placeholder'),
|
||||
extensions: ['apk'],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message
|
||||
= error.message?.match(/Error: (.*)/)?.[1] || error.message
|
||||
this.$message.warning(message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
this.loading = true
|
||||
|
||||
await allSettled(this.devices, (item) => {
|
||||
return this.$refs.applicationProxyRef.invoke(item, { files })
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,71 @@
|
||||
<template>
|
||||
<el-dropdown :hide-on-click="false">
|
||||
<div class="">
|
||||
<slot :loading="loading" />
|
||||
<FileManageProxy ref="fileManageProxyRef" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handlePush(devices)">
|
||||
<span class="" title="/sdcard/Download/">
|
||||
{{ $t('device.control.file.push') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import FileManageProxy from '$/components/Device/components/ControlBar/FileManage/index.vue'
|
||||
|
||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
const props = defineProps({
|
||||
devices: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const fileManageProxyRef = ref(null)
|
||||
|
||||
async function handlePush(devices) {
|
||||
let files = null
|
||||
|
||||
try {
|
||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
name: window.t('device.control.file.push.placeholder'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message = error.message?.match(/Error: (.*)/)?.[1] || error.message
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
await allSettled(devices, (item) => {
|
||||
return fileManageProxyRef.value.handlePush(item, { files })
|
||||
})
|
||||
|
||||
ElMessage.success(window.t('common.success.batch'))
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,41 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick">
|
||||
<slot v-bind="{ loading }" />
|
||||
<ScreenshotProxy ref="screenshotProxyRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScreenshotProxy from '$/components/Device/components/ControlBar/Screenshot/index.vue'
|
||||
import { allSettled, sleep } from '$/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ScreenshotProxy,
|
||||
},
|
||||
props: {
|
||||
devices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
this.loading = true
|
||||
|
||||
await allSettled(this.devices, (item) => {
|
||||
return this.$refs.screenshotProxyRef.invoke(item)
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick(devices)">
|
||||
<slot v-bind="{ loading }" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
const props = defineProps({
|
||||
devices: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function handleClick(devices) {
|
||||
let files = null
|
||||
|
||||
try {
|
||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: window.t('device.control.shell.select'),
|
||||
extensions: ['sh'],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message = error.message?.match(/Error: (.*)/)?.[1]
|
||||
ElMessage.warning(message || error.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
loading.value = true
|
||||
|
||||
const failFiles = []
|
||||
|
||||
await allSettled(devices, async (device) => {
|
||||
const successFiles = await selectAndSendFileToDevice(device.id, {
|
||||
files,
|
||||
loadingText: window.t('device.control.shell.push.loading'),
|
||||
successText: window.t('device.control.shell.push.success'),
|
||||
}).catch((e) => {
|
||||
console.warn(e.message)
|
||||
failFiles.push(e.message)
|
||||
})
|
||||
|
||||
const filePath = successFiles?.[0]
|
||||
|
||||
if (filePath) {
|
||||
window.adbkit.deviceShell(device.id, `sh ${filePath}`)
|
||||
}
|
||||
})
|
||||
|
||||
if (failFiles.length) {
|
||||
ElMessageBox.alert(
|
||||
`<div>${failFiles.map(text => `${text}<br/>`).join('')}</div>`,
|
||||
window.t('common.tips'),
|
||||
{
|
||||
type: 'warning',
|
||||
dangerouslyUseHTMLString: true,
|
||||
},
|
||||
)
|
||||
loading.value = false
|
||||
return false
|
||||
}
|
||||
|
||||
await ElMessage.success(window.t('device.control.shell.success'))
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="flex items-center">
|
||||
<div class="flex items-center space-x-2">
|
||||
<component
|
||||
:is="item.component || 'div'"
|
||||
v-for="(item, index) in actionModel"
|
||||
@ -38,34 +38,41 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import AppInstall from './AppInstall/index.vue'
|
||||
<script setup>
|
||||
import Application from './Application/index.vue'
|
||||
import Screenshot from './Screenshot/index.vue'
|
||||
import FileManage from './FileManage/index.vue'
|
||||
import Shell from './Shell/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
AppInstall,
|
||||
},
|
||||
props: {
|
||||
const props = defineProps({
|
||||
devices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const actionModel = [
|
||||
{
|
||||
label: 'device.control.capture',
|
||||
elIcon: 'Crop',
|
||||
component: Screenshot,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actionModel: [
|
||||
{
|
||||
label: 'device.control.install',
|
||||
svgIcon: 'install',
|
||||
component: 'AppInstall',
|
||||
component: Application,
|
||||
},
|
||||
],
|
||||
}
|
||||
{
|
||||
label: 'device.control.file.name',
|
||||
svgIcon: 'file-send',
|
||||
component: FileManage,
|
||||
},
|
||||
methods: {
|
||||
handleShell() {},
|
||||
{
|
||||
label: 'device.control.shell.name',
|
||||
svgIcon: 'command',
|
||||
component: Shell,
|
||||
},
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@ -5,7 +5,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIcon from '$/components/Device/components/LoadingIcon/index.vue'
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@ -18,12 +18,14 @@ export default {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
invoke(...args) {
|
||||
return this.handleInstall(...args)
|
||||
},
|
||||
preferenceData(...args) {
|
||||
return this.$store.preference.getData(...args)
|
||||
},
|
||||
async handleInstall(device) {
|
||||
let files = null
|
||||
|
||||
async handleInstall(device, { files } = {}) {
|
||||
if (!files) {
|
||||
try {
|
||||
files = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
@ -37,32 +39,28 @@ export default {
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message = error.message?.match(/Error: (.*)/)?.[1]
|
||||
this.$message.warning(message || error.message)
|
||||
const message
|
||||
= error.message?.match(/Error: (.*)/)?.[1] || error.message
|
||||
this.$message.warning(message)
|
||||
}
|
||||
}
|
||||
|
||||
if (!files) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const messageEl = this.$message({
|
||||
message: this.$t('device.control.install.progress', {
|
||||
const messageEl = this.$message.loading(
|
||||
this.$t('device.control.install.progress', {
|
||||
deviceName: this.$store.device.getLabel(device),
|
||||
}),
|
||||
icon: LoadingIcon,
|
||||
duration: 0,
|
||||
})
|
||||
)
|
||||
|
||||
let failCount = 0
|
||||
|
||||
for (let index = 0; index < files.length; index++) {
|
||||
const item = files[index]
|
||||
await this.$adb.install(device.id, item).catch((e) => {
|
||||
await allSettled(files, (item) => {
|
||||
return this.$adb.install(device.id, item).catch((e) => {
|
||||
console.warn(e)
|
||||
++failCount
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
messageEl.close()
|
||||
|
||||
@ -87,7 +85,7 @@ export default {
|
||||
}),
|
||||
)
|
||||
}
|
||||
return
|
||||
return false
|
||||
}
|
||||
|
||||
this.$message.warning(this.$t('device.control.install.error'))
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<el-dropdown :hide-on-click="false">
|
||||
<div class="">
|
||||
<slot :loading="loading" />
|
||||
<slot v-bind="{ loading }" />
|
||||
</div>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handlePush">
|
||||
<el-dropdown-item @click="handlePush(device)">
|
||||
<span class="" title="/sdcard/Download/">
|
||||
{{ $t("device.control.file.push") }}
|
||||
{{ $t('device.control.file.push') }}
|
||||
</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
@ -15,30 +15,31 @@
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
||||
import { useDeviceStore } from '$/store'
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
default: () => null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handlePush() {
|
||||
this.loading = true
|
||||
let files = null
|
||||
})
|
||||
|
||||
const deviceStore = useDeviceStore()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function handlePush(device, { files } = {}) {
|
||||
if (!files) {
|
||||
try {
|
||||
files = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
name: this.$t('device.control.file.push.placeholder'),
|
||||
name: window.t('device.control.file.push.placeholder'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
@ -46,36 +47,42 @@ export default {
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message = error.message?.match(/Error: (.*)/)?.[1]
|
||||
this.$message.warning(message || error.message)
|
||||
const message
|
||||
= error.message?.match(/Error: (.*)/)?.[1] || error.message
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
if (!files) {
|
||||
this.loading = false
|
||||
return false
|
||||
}
|
||||
loading.value = true
|
||||
|
||||
const closeMessage = ElMessage.loading(
|
||||
window.t('device.control.file.push.loading'),
|
||||
{ grouping: true },
|
||||
).close
|
||||
|
||||
let failCount = 0
|
||||
|
||||
for (let index = 0; index < files.length; index++) {
|
||||
const item = files[index]
|
||||
await this.$adb.push(this.device.id, item).catch((e) => {
|
||||
console.warn(e)
|
||||
await allSettled(files, (item) => {
|
||||
return window.adbkit.push(device.id, item).catch(() => {
|
||||
++failCount
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
loading.value = false
|
||||
|
||||
const totalCount = files.length
|
||||
const successCount = totalCount - failCount
|
||||
|
||||
if (successCount) {
|
||||
closeMessage()
|
||||
|
||||
if (totalCount > 1) {
|
||||
this.$message.success(
|
||||
this.$t('device.control.file.push.success', {
|
||||
deviceName: this.$store.device.getLabel(this.device),
|
||||
ElMessage.success(
|
||||
window.t('device.control.file.push.success', {
|
||||
deviceName: deviceStore.getLabel(device),
|
||||
totalCount,
|
||||
successCount,
|
||||
failCount,
|
||||
@ -83,19 +90,23 @@ export default {
|
||||
)
|
||||
}
|
||||
else {
|
||||
this.$message.success(
|
||||
this.$t('device.control.file.push.success.single', {
|
||||
deviceName: this.$store.device.getLabel(this.device),
|
||||
ElMessage.success(
|
||||
window.t('device.control.file.push.success.single', {
|
||||
deviceName: deviceStore.getLabel(device),
|
||||
}),
|
||||
)
|
||||
}
|
||||
return
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
this.$message.warning(this.$t('device.control.file.push.error'))
|
||||
},
|
||||
},
|
||||
closeMessage()
|
||||
ElMessage.warning(window.t('device.control.file.push.error'))
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
handlePush,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@ -7,7 +7,7 @@
|
||||
<template v-if="device.$gnirehtetLoading" #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="handleStop">
|
||||
{{ $t("device.control.gnirehtet.stop") }}
|
||||
{{ $t('device.control.gnirehtet.stop') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
@ -48,9 +48,7 @@ export default {
|
||||
try {
|
||||
await this.$gnirehtet.run(this.device.id)
|
||||
await sleep()
|
||||
this.$message.success(
|
||||
this.$t('device.control.gnirehtet.start.success'),
|
||||
)
|
||||
this.$message.success(this.$t('device.control.gnirehtet.start.success'))
|
||||
}
|
||||
catch (error) {
|
||||
this.$message.warning(error.message || 'Start service failure')
|
||||
|
@ -1,12 +1,10 @@
|
||||
<template>
|
||||
<div class="" @click="handleScreenCap(device)">
|
||||
<div class="" @click="handleCapture(device)">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import LoadingIcon from '$/components/Device/components/LoadingIcon/index.vue'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
device: {
|
||||
@ -18,21 +16,22 @@ export default {
|
||||
return {}
|
||||
},
|
||||
methods: {
|
||||
invoke(...args) {
|
||||
return this.handleCapture(...args)
|
||||
},
|
||||
preferenceData(...args) {
|
||||
return this.$store.preference.getData(...args)
|
||||
},
|
||||
async handleScreenCap(device) {
|
||||
const messageEl = this.$message({
|
||||
message: this.$t('device.control.capture.progress', {
|
||||
async handleCapture(device) {
|
||||
const messageEl = this.$message.loading(
|
||||
this.$t('device.control.capture.progress', {
|
||||
deviceName: this.$store.device.getLabel(device),
|
||||
}),
|
||||
icon: LoadingIcon,
|
||||
duration: 0,
|
||||
})
|
||||
)
|
||||
|
||||
const fileName = this.$store.device.getLabel(
|
||||
device,
|
||||
({ time }) => `screenshot-${time}.png`,
|
||||
({ time }) => `screenshot-${time}.jpg`,
|
||||
)
|
||||
|
||||
const deviceConfig = this.preferenceData(device.id)
|
||||
@ -40,7 +39,7 @@ export default {
|
||||
|
||||
try {
|
||||
await this.$adb.screencap(device.id, { savePath })
|
||||
this.handleScreencapSuccess(savePath)
|
||||
await this.handleSuccess(savePath)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
@ -50,29 +49,12 @@ export default {
|
||||
|
||||
messageEl.close()
|
||||
},
|
||||
async handleScreencapSuccess(savePath) {
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t('device.control.capture.success.message'),
|
||||
this.$t('device.control.capture.success.message.title'),
|
||||
{
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
closeOnClickModal: false,
|
||||
type: 'success',
|
||||
},
|
||||
async handleSuccess(savePath) {
|
||||
return this.$message.success(
|
||||
`${this.$t(
|
||||
'device.control.capture.success.message.title',
|
||||
)}: ${savePath}`,
|
||||
)
|
||||
|
||||
await this.$electron.ipcRenderer.invoke(
|
||||
'show-item-in-folder',
|
||||
savePath,
|
||||
)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
this.$message.warning(error.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
51
src/components/Device/components/ControlBar/Shell/index.vue
Normal file
51
src/components/Device/components/ControlBar/Shell/index.vue
Normal file
@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick(device)">
|
||||
<slot :loading="loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const invokeTerminal = inject('invokeTerminal')
|
||||
|
||||
async function handleClick(device) {
|
||||
let files = null
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
files = await selectAndSendFileToDevice(device.id, {
|
||||
extensions: ['sh'],
|
||||
selectText: window.t('device.control.shell.select'),
|
||||
loadingText: window.t('device.control.shell.push.loading'),
|
||||
successText: window.t('device.control.shell.push.success'),
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
loading.value = false
|
||||
ElMessage.warning(error.message)
|
||||
return false
|
||||
}
|
||||
|
||||
const filePath = files[0]
|
||||
|
||||
const command = `adb -s ${device.id} shell sh ${filePath}`
|
||||
|
||||
invokeTerminal(command)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -66,22 +66,24 @@
|
||||
|
||||
<script>
|
||||
import Screenshot from './Screenshot/index.vue'
|
||||
import AppInstall from './AppInstall/index.vue'
|
||||
import Application from './Application/index.vue'
|
||||
import Gnirehtet from './Gnirehtet/index.vue'
|
||||
import MirrorGroup from './MirrorGroup/index.vue'
|
||||
import Rotation from './Rotation/index.vue'
|
||||
import Volume from './Volume/index.vue'
|
||||
import FileManage from './FileManage/index.vue'
|
||||
import Shell from './Shell/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Screenshot,
|
||||
AppInstall,
|
||||
Application,
|
||||
Gnirehtet,
|
||||
MirrorGroup,
|
||||
Rotation,
|
||||
Volume,
|
||||
FileManage,
|
||||
Shell,
|
||||
},
|
||||
props: {
|
||||
device: {
|
||||
@ -142,13 +144,19 @@ export default {
|
||||
{
|
||||
label: 'device.control.install',
|
||||
svgIcon: 'install',
|
||||
component: 'AppInstall',
|
||||
component: 'Application',
|
||||
},
|
||||
{
|
||||
label: 'device.control.file.name',
|
||||
svgIcon: 'file-send',
|
||||
component: 'FileManage',
|
||||
},
|
||||
{
|
||||
label: 'device.control.shell.name',
|
||||
svgIcon: 'command',
|
||||
component: 'Shell',
|
||||
tips: 'device.control.shell.tips',
|
||||
},
|
||||
{
|
||||
label: 'device.control.gnirehtet',
|
||||
elIcon: 'Link',
|
||||
|
@ -45,6 +45,8 @@ export default {
|
||||
},
|
||||
)}`
|
||||
|
||||
console.log('args', args)
|
||||
|
||||
try {
|
||||
const mirroring = this.$scrcpy.mirror(row.id, {
|
||||
title: this.$store.device.getLabel(row),
|
||||
|
@ -0,0 +1,94 @@
|
||||
<template>
|
||||
<TemplatePromise v-slot="{ resolve, reject }">
|
||||
<el-dialog
|
||||
v-model="visible"
|
||||
:title="$t('device.actions.more.custom.name')"
|
||||
class="w-[98%] el-dialog-flex el-dialog-beautify"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
@close="close(reject)"
|
||||
>
|
||||
<div class="h-full overflow-auto -mx-2 pr-2">
|
||||
<PreferenceForm
|
||||
ref="preferenceFormRef"
|
||||
v-model="preferenceData"
|
||||
tag="el-collapse-item"
|
||||
v-bind="{
|
||||
collapseProps: { accordion: true },
|
||||
excludes: ['common'],
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<el-button @click="close(reject)">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit(resolve)">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</TemplatePromise>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { createTemplatePromise } from '@vueuse/core'
|
||||
|
||||
import { nextTick } from 'vue'
|
||||
import { usePreferenceStore } from '$/store/index.js'
|
||||
|
||||
import PreferenceForm from '$/components/Preference/components/PreferenceForm/index.vue'
|
||||
|
||||
const TemplatePromise = createTemplatePromise()
|
||||
|
||||
const preferenceStore = usePreferenceStore()
|
||||
|
||||
const visible = ref(false)
|
||||
|
||||
const preferenceFormRef = ref(null)
|
||||
|
||||
const preferenceData = ref({
|
||||
...getDefaultData(),
|
||||
})
|
||||
|
||||
const collapseValue = ref([])
|
||||
|
||||
const device = ref(null)
|
||||
|
||||
async function open(row) {
|
||||
device.value = row
|
||||
visible.value = true
|
||||
|
||||
return TemplatePromise.start()
|
||||
}
|
||||
|
||||
async function submit(resolve) {
|
||||
const data = await preferenceFormRef.value.generateCommand()
|
||||
|
||||
visible.value = false
|
||||
|
||||
resolve(data)
|
||||
}
|
||||
|
||||
async function close(reject) {
|
||||
visible.value = false
|
||||
|
||||
await nextTick()
|
||||
|
||||
preferenceData.value = { ...getDefaultData() }
|
||||
|
||||
reject(new Error('User cancel operation'))
|
||||
}
|
||||
|
||||
function getDefaultData() {
|
||||
return preferenceStore.getDefaultData('global', () => void 0)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,84 @@
|
||||
<template>
|
||||
<slot :loading="loading" :trigger="handleClick" />
|
||||
|
||||
<DeployDialog ref="deployDialogRef" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DeployDialog from './components/DeployDialog/index.vue'
|
||||
import { sleep } from '$/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DeployDialog,
|
||||
},
|
||||
props: {
|
||||
row: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
toggleRowExpansion: {
|
||||
type: Function,
|
||||
default: () => () => false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
const row = this.row
|
||||
|
||||
this.loading = true
|
||||
|
||||
let args = ''
|
||||
|
||||
try {
|
||||
args = await this.$refs.deployDialogRef.open(row)
|
||||
}
|
||||
catch (error) {
|
||||
this.loading = false
|
||||
this.$message.warning(error.message)
|
||||
return false
|
||||
}
|
||||
|
||||
/** TODO */
|
||||
const isCamera = ['--camera-facing'].some(key => args.includes(key))
|
||||
if (isCamera) {
|
||||
args += ' --video-source=camera'
|
||||
}
|
||||
|
||||
this.toggleRowExpansion(row, true)
|
||||
|
||||
try {
|
||||
const mirroring = this.$scrcpy.mirror(row.id, {
|
||||
title: this.$store.device.getLabel(row),
|
||||
args,
|
||||
stdout: this.onStdout,
|
||||
stderr: this.onStderr,
|
||||
})
|
||||
|
||||
await sleep(1 * 1000)
|
||||
|
||||
this.loading = false
|
||||
|
||||
await mirroring
|
||||
}
|
||||
catch (error) {
|
||||
console.error('mirror.args', args)
|
||||
console.error('mirror.error', error)
|
||||
|
||||
if (error.message) {
|
||||
this.$message.warning(error.message)
|
||||
}
|
||||
}
|
||||
},
|
||||
onStdout() {},
|
||||
onStderr() {},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -51,7 +51,7 @@ export default {
|
||||
|
||||
await recording
|
||||
|
||||
this.onRecordSuccess(savePath)
|
||||
await this.handleSuccess(savePath)
|
||||
}
|
||||
catch (error) {
|
||||
console.error('record.args', args)
|
||||
@ -79,24 +79,10 @@ export default {
|
||||
|
||||
return value
|
||||
},
|
||||
async onRecordSuccess(savePath) {
|
||||
try {
|
||||
await this.$confirm(
|
||||
this.$t('device.record.success.message'),
|
||||
this.$t('device.record.success.title'),
|
||||
{
|
||||
confirmButtonText: this.$t('common.confirm'),
|
||||
cancelButtonText: this.$t('common.cancel'),
|
||||
closeOnClickModal: false,
|
||||
type: 'success',
|
||||
},
|
||||
async handleSuccess(savePath) {
|
||||
return this.$message.success(
|
||||
`${this.$t('device.record.success.title')}: ${savePath}`,
|
||||
)
|
||||
|
||||
await this.$electron.ipcRenderer.invoke('show-item-in-folder', savePath)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -41,17 +41,20 @@
|
||||
import Record from './components/Record/index.vue'
|
||||
import Otg from './components/Otg/index.vue'
|
||||
import Camera from './components/Camera/index.vue'
|
||||
import Custom from './components/Custom/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Record,
|
||||
Otg,
|
||||
Camera,
|
||||
Custom,
|
||||
},
|
||||
props: {
|
||||
...Record.props,
|
||||
...Otg.props,
|
||||
...Camera.props,
|
||||
...Custom.props,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@ -68,6 +71,10 @@ export default {
|
||||
label: 'device.actions.more.camera.name',
|
||||
component: 'Camera',
|
||||
},
|
||||
{
|
||||
label: 'device.actions.more.custom.name',
|
||||
component: 'Custom',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
@ -4,27 +4,26 @@
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
:destroy-on-close="true"
|
||||
class="overflow-hidden !rounded-md el-dialog-headless dark:border dark:border-gray-700"
|
||||
@open="onOpen"
|
||||
@closed="onClosed"
|
||||
>
|
||||
<el-icon
|
||||
class="cursor-pointer absolute top-3 right-3 w-8 h-8 flex items-center justify-center hover:bg-gray-200 dark:text-gray-200 dark:hover:bg-gray-700 !active:bg-red-600 !active:text-gray-200 rounded-md"
|
||||
@click="hide"
|
||||
class="cursor-pointer absolute top-3 right-3 w-8 h-8 flex items-center justify-center bg-white hover:bg-gray-200 dark:text-gray-200 dark:hover:bg-gray-700 !active:bg-red-600 !active:text-gray-200 rounded-md"
|
||||
@click="close"
|
||||
>
|
||||
<CloseBold />
|
||||
</el-icon>
|
||||
|
||||
<VueCommand
|
||||
v-if="renderShell"
|
||||
:ref="(value) => (vShell = value)"
|
||||
ref="vShell"
|
||||
v-model:history="history"
|
||||
:dispatched-queries="dispatchedQueries"
|
||||
:commands="commands"
|
||||
:invert="invert"
|
||||
hide-bar
|
||||
show-help
|
||||
help-text="Type in help"
|
||||
:help-timeout="3000"
|
||||
:invert="invert"
|
||||
class=""
|
||||
@update:dispatched-queries="onDispatchedQueriesUpdate"
|
||||
>
|
||||
@ -37,34 +36,36 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, shallowRef } from 'vue'
|
||||
<script setup>
|
||||
import VueCommand, {
|
||||
createQuery,
|
||||
createStdout,
|
||||
listFormatter,
|
||||
} from 'vue-command'
|
||||
import 'vue-command/dist/vue-command.css'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useAdb } from './composables/adb-async.js'
|
||||
import { useScrcpy } from './composables/scrcpy.js'
|
||||
import { useGnirehtet } from './composables/gnirehtet.js'
|
||||
import { sleep } from '$/utils/index.js'
|
||||
import { useThemeStore } from '$/store/index.js'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
VueCommand,
|
||||
},
|
||||
setup() {
|
||||
const vShell = ref(null)
|
||||
const history = shallowRef([createQuery()])
|
||||
const loading = ref(false)
|
||||
const renderShell = ref(false)
|
||||
const dispatchedQueries = ref(new Set([]))
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const { adb } = useAdb({ vShell, history, loading })
|
||||
const { scrcpy } = useScrcpy({ vShell, history, loading })
|
||||
const { gnirehtet } = useGnirehtet({ vShell, history, loading })
|
||||
const loading = ref(false)
|
||||
const visible = ref(false)
|
||||
|
||||
const commands = ref({
|
||||
const vShell = ref(null)
|
||||
const history = shallowRef([createQuery()])
|
||||
const dispatchedQueries = ref(new Set([]))
|
||||
|
||||
const { adb } = useAdb({ vShell, history, loading })
|
||||
const { scrcpy } = useScrcpy({ vShell, history, loading })
|
||||
const { gnirehtet } = useGnirehtet({ vShell, history, loading })
|
||||
|
||||
const invert = computed(() => !themeStore.isDark)
|
||||
|
||||
const commands = ref({
|
||||
adb,
|
||||
scrcpy,
|
||||
gnirehtet,
|
||||
@ -72,73 +73,104 @@ export default {
|
||||
history.value = []
|
||||
return createQuery()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
commands.value.help = () => {
|
||||
commands.value.help = () => {
|
||||
const commandList = Object.keys(commands.value)
|
||||
return createStdout(listFormatter('Supported Commands:', ...commandList))
|
||||
}
|
||||
}
|
||||
|
||||
dispatchedQueries.value = new Set([
|
||||
dispatchedQueries.value = new Set([
|
||||
...(window.appStore.get('terminal.dispatchedQueries') || []),
|
||||
...Object.keys(commands.value),
|
||||
])
|
||||
])
|
||||
|
||||
const onOpen = () => {
|
||||
renderShell.value = true
|
||||
}
|
||||
function getShell() {
|
||||
let unwatch = null
|
||||
|
||||
return {
|
||||
vShell,
|
||||
loading,
|
||||
history,
|
||||
commands,
|
||||
dispatchedQueries,
|
||||
onOpen,
|
||||
renderShell,
|
||||
return new Promise((resolve) => {
|
||||
unwatch = watch(
|
||||
() => vShell.value,
|
||||
(value) => {
|
||||
if (value) {
|
||||
unwatch?.()
|
||||
resolve(value)
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
invert() {
|
||||
return !this.$store.theme.isDark
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'vShell.signals': {
|
||||
handler(value) {
|
||||
value.off('SIGINT')
|
||||
|
||||
value.on('SIGINT', () => {
|
||||
this.onCtrlC()
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
onDispatchedQueriesUpdate(value) {
|
||||
this.$appStore.set('terminal.dispatchedQueries', Array.from(value))
|
||||
|
||||
this.dispatchedQueries = value
|
||||
},
|
||||
|
||||
onCtrlC() {
|
||||
window.gnirehtet.shell('stop')
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
;(async () => {
|
||||
const shell = await getShell()
|
||||
|
||||
shell.signals.off('SIGINT')
|
||||
|
||||
shell.signals.on('SIGINT', () => {
|
||||
onCtrlC()
|
||||
})
|
||||
})()
|
||||
|
||||
async function open() {
|
||||
visible.value = true
|
||||
await focus()
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
async function invoke(command) {
|
||||
visible.value = true
|
||||
|
||||
const shell = await getShell()
|
||||
|
||||
shell.setQuery(command)
|
||||
|
||||
await focus()
|
||||
|
||||
ElMessage.info(window.t('device.control.shell.enter'))
|
||||
}
|
||||
|
||||
async function focus() {
|
||||
await nextTick()
|
||||
|
||||
const shell = await getShell()
|
||||
|
||||
const targetRefs = shell.$refs.vueCommandHistoryEntryComponentRefs || []
|
||||
|
||||
const targetRef = targetRefs[targetRefs.length - 1]
|
||||
|
||||
if (!targetRef) {
|
||||
return false
|
||||
}
|
||||
|
||||
await sleep()
|
||||
|
||||
targetRef.focus()
|
||||
}
|
||||
|
||||
function onDispatchedQueriesUpdate(value) {
|
||||
window.appStore.set('terminal.dispatchedQueries', Array.from(value))
|
||||
|
||||
dispatchedQueries.value = value
|
||||
}
|
||||
|
||||
function onCtrlC() {
|
||||
window.gnirehtet.shell('stop')
|
||||
}
|
||||
|
||||
function onClosed() {
|
||||
vShell.value.dispatch('clear')
|
||||
history.value = [createQuery()]
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open,
|
||||
close,
|
||||
invoke,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
@ -20,7 +20,10 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleShow() {
|
||||
this.$refs.terminalDialog.show()
|
||||
this.$refs.terminalDialog.open()
|
||||
},
|
||||
invoke(...args) {
|
||||
this.$refs.terminalDialog.invoke(...args)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="h-full flex flex-col">
|
||||
<div class="flex items-center flex-none space-x-2 pt-1">
|
||||
<div class="flex items-center flex-none space-x-2 py-1 overflow-x-auto">
|
||||
<Wireless ref="wireless" :reload="getDeviceData" />
|
||||
|
||||
<div class="w-px h-7 !ml-4 !mr-2 bg-gray-200"></div>
|
||||
@ -28,12 +28,12 @@
|
||||
{{ $t('device.log.name') }}
|
||||
</el-button>
|
||||
|
||||
<TerminalAction />
|
||||
<TerminalAction ref="terminalActionRef" />
|
||||
</div>
|
||||
|
||||
<BatchActions
|
||||
class="overflow-hidden transition-all"
|
||||
:class="isMultipleRow ? 'h-12 opacity-100 mt-4' : 'h-0 opacity-0 mt-0'"
|
||||
:class="isMultipleRow ? 'h-12 opacity-100 mt-3' : 'h-0 opacity-0 mt-0'"
|
||||
:devices="selectionRows"
|
||||
/>
|
||||
|
||||
@ -61,13 +61,14 @@
|
||||
sortable
|
||||
show-overflow-tooltip
|
||||
align="left"
|
||||
width="200"
|
||||
min-width="100"
|
||||
/>
|
||||
<el-table-column
|
||||
:label="$t('device.name')"
|
||||
sortable
|
||||
show-overflow-tooltip
|
||||
align="left"
|
||||
min-width="150"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex items-center">
|
||||
@ -96,7 +97,7 @@
|
||||
<el-table-column
|
||||
v-slot="{ row, $index }"
|
||||
:label="$t('device.control.name')"
|
||||
width="450"
|
||||
min-width="200"
|
||||
align="left"
|
||||
>
|
||||
<MirrorAction
|
||||
@ -147,6 +148,11 @@ export default {
|
||||
WirelessAction,
|
||||
BatchActions,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
invokeTerminal: (...args) => this.$refs.terminalActionRef.invoke(...args),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<el-input
|
||||
class="!w-full"
|
||||
v-bind="{
|
||||
clearable: true,
|
||||
...(data.props || {}),
|
||||
}"
|
||||
>
|
||||
<template v-if="data.append" #append>
|
||||
{{ data.append }}
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<el-input
|
||||
class="!w-full"
|
||||
v-bind="{
|
||||
type: 'number',
|
||||
clearable: true,
|
||||
...(data.props || {}),
|
||||
}"
|
||||
>
|
||||
<template v-if="data.append" #append>
|
||||
{{ data.append }}
|
||||
</template>
|
||||
</el-input>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -1,11 +1,11 @@
|
||||
<template>
|
||||
<el-input
|
||||
v-bind="data.props || {}"
|
||||
v-bind="{
|
||||
clearable: true,
|
||||
...(data.props || {}),
|
||||
}"
|
||||
v-model="pathValue"
|
||||
clearable
|
||||
class="!w-full"
|
||||
:title="$t(data.placeholder)"
|
||||
:placeholder="$t(data.placeholder)"
|
||||
>
|
||||
<template #append>
|
||||
<el-button
|
@ -0,0 +1,31 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-bind="{
|
||||
clearable: true,
|
||||
...(data.props || {}),
|
||||
}"
|
||||
class="!w-full"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in data.options"
|
||||
:key="index"
|
||||
:label="$t(item.label)"
|
||||
:value="item.value"
|
||||
:title="$t(item.placeholder || item.label)"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-bind="data.props || {}"
|
||||
v-bind="{ ...(data.props || {}) }"
|
||||
v-model="selectValue"
|
||||
class="!w-full"
|
||||
:title="$t(data.placeholder)"
|
||||
:placeholder="$t(data.placeholder)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in options"
|
@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-bind="data.props || {}"
|
||||
v-bind="{
|
||||
...(data.props || {}),
|
||||
}"
|
||||
v-model="selectValue"
|
||||
class="!w-full"
|
||||
:title="$t(data.placeholder)"
|
||||
:placeholder="$t(data.placeholder)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in options"
|
@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-bind="data.props || {}"
|
||||
v-bind="{ clearable: true, ...(data.props || {}) }"
|
||||
v-model="selectValue"
|
||||
class="!w-full"
|
||||
clearable
|
||||
:title="$t(data.placeholder)"
|
||||
:placeholder="$t(data.placeholder)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in options"
|
@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-bind="data.props || {}"
|
||||
v-bind="{ ...(data.props || {}) }"
|
||||
v-model="inputValue"
|
||||
class="!w-full"
|
||||
:title="$t(data.placeholder)"
|
||||
:placeholder="$t(data.placeholder)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in data.options"
|
@ -1,10 +1,8 @@
|
||||
<template>
|
||||
<el-select
|
||||
v-bind="data.props || {}"
|
||||
v-bind="{ ...(data.props || {}) }"
|
||||
v-model="selectValue"
|
||||
class="!w-full"
|
||||
:title="$t(data.placeholder)"
|
||||
:placeholder="$t(data.placeholder)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item, index) in options"
|
@ -0,0 +1,16 @@
|
||||
<template>
|
||||
<el-switch class="!w-full" v-bind="{ ...(data.props || {}) }"></el-switch>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
data: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -0,0 +1,30 @@
|
||||
import Input from './Input/index.vue'
|
||||
import InputNumber from './InputNumber/index.vue'
|
||||
import InputPath from './InputPath/index.vue'
|
||||
import Select from './Select/index.vue'
|
||||
import SelectAudioCodec from './SelectAudioCodec/index.vue'
|
||||
import SelectDisplay from './SelectDisplay/index.vue'
|
||||
import SelectKeyboardInject from './SelectKeyboardInject/index.vue'
|
||||
import SelectLanguage from './SelectLanguage/index.vue'
|
||||
import SelectVideoCodec from './SelectVideoCodec/index.vue'
|
||||
import Switch from './Switch/index.vue'
|
||||
|
||||
export const inputModel = {
|
||||
PathInput: InputPath,
|
||||
AudioCodecSelect: SelectAudioCodec,
|
||||
VideoCodecSelect: SelectVideoCodec,
|
||||
DisplaySelect: SelectDisplay,
|
||||
KeyboardInjectSelect: SelectKeyboardInject,
|
||||
LanguageSelect: SelectLanguage,
|
||||
|
||||
Input,
|
||||
InputNumber,
|
||||
InputPath,
|
||||
Select,
|
||||
SelectAudioCodec,
|
||||
SelectDisplay,
|
||||
SelectKeyboardInject,
|
||||
SelectLanguage,
|
||||
SelectVideoCodec,
|
||||
Switch,
|
||||
}
|
183
src/components/Preference/components/PreferenceForm/index.vue
Normal file
183
src/components/Preference/components/PreferenceForm/index.vue
Normal file
@ -0,0 +1,183 @@
|
||||
<template>
|
||||
<el-form ref="elForm" :model="preferenceData" label-width="225px" class="">
|
||||
<el-collapse
|
||||
v-model="collapseValue"
|
||||
v-bind="{
|
||||
accordion: false,
|
||||
...collapseProps,
|
||||
}"
|
||||
class="space-y-4 borderless"
|
||||
>
|
||||
<el-collapse-item
|
||||
v-for="(item, name) of preferenceModel"
|
||||
:key="name"
|
||||
:name="name"
|
||||
class="!border dark:border-gray-700 rounded-[5px] overflow-hidden shadow-el-lighter"
|
||||
>
|
||||
<template #title>
|
||||
<div
|
||||
class="flex items-center w-full text-left -mr-10 overflow-hidden dark:border-gray-700"
|
||||
:class="{
|
||||
'!border-b': collapseValue.includes(name),
|
||||
}"
|
||||
>
|
||||
<div class="flex-1 w-0 truncate pl-4 text-base">
|
||||
{{ $t(item.label) }}
|
||||
</div>
|
||||
<div class="flex-none pl-4 pr-12" @click.stop>
|
||||
<el-button type="primary" text @click="handleReset(name)">
|
||||
{{ $t('preferences.reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="pt-4">
|
||||
<el-form
|
||||
ref="elForm"
|
||||
:model="preferenceData"
|
||||
label-width="225px"
|
||||
class="pr-8 pt-4"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="(item_1, name_1) of subModel(item)"
|
||||
:key="name_1"
|
||||
:span="item_1.span || 12"
|
||||
:offset="item_1.offset || 0"
|
||||
>
|
||||
<el-form-item :label="$t(item_1.label)" :prop="item_1.field">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<el-tooltip
|
||||
v-if="item_1.tips"
|
||||
popper-class="max-w-96"
|
||||
effect="dark"
|
||||
:content="$t(item_1.tips)"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-link
|
||||
class="mr-1 !text-base"
|
||||
icon="InfoFilled"
|
||||
type="warning"
|
||||
:underline="false"
|
||||
>
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
<span class="" :title="$t(item_1.placeholder)">{{
|
||||
$t(item_1.label)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<component
|
||||
:is="inputModel[item_1.type]"
|
||||
v-model="preferenceData[item_1.field]"
|
||||
v-bind="{
|
||||
preferenceData,
|
||||
deviceScope,
|
||||
title: $t(item_1.placeholder),
|
||||
placeholder: $t(item_1.placeholder),
|
||||
data: item_1,
|
||||
}"
|
||||
></component>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { omit } from 'lodash-es'
|
||||
|
||||
import { inputModel } from './components/index.js'
|
||||
|
||||
import { usePreferenceStore } from '$/store/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
deviceScope: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
collapseProps: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
excludes: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const preferenceData = defineModel('modelValue', {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
})
|
||||
|
||||
const preferenceStore = usePreferenceStore()
|
||||
|
||||
const preferenceModel = computed(() =>
|
||||
omit(preferenceStore.model, props.excludes),
|
||||
)
|
||||
|
||||
const preferenceModelKeys = Object.keys(preferenceModel.value ?? {})
|
||||
|
||||
const collapseValue = ref([])
|
||||
|
||||
if (preferenceModelKeys.length) {
|
||||
if (props.collapseProps.accordion) {
|
||||
collapseValue.value = preferenceModelKeys[0]
|
||||
}
|
||||
else {
|
||||
collapseValue.value = preferenceModelKeys
|
||||
}
|
||||
}
|
||||
|
||||
function subModel(item) {
|
||||
const children = item?.children || {}
|
||||
|
||||
const value = {}
|
||||
|
||||
Object.entries(children).forEach(([key, data]) => {
|
||||
if (!data.hidden) {
|
||||
value[key] = data
|
||||
}
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
function handleReset(type) {
|
||||
preferenceData.value = {
|
||||
...preferenceData.value,
|
||||
...preferenceStore.getDefaultData(type),
|
||||
}
|
||||
}
|
||||
|
||||
async function generateCommand() {
|
||||
const value = await preferenceStore.getScrcpyArgs(preferenceData.value, {
|
||||
isRecord: true,
|
||||
isCamera: true,
|
||||
isOtg: true,
|
||||
})
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
generateCommand,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="postcss">
|
||||
:deep(.el-collapse-item__header) {
|
||||
@apply h-13 leading-13;
|
||||
}
|
||||
|
||||
:deep(.el-collapse-item__arrow) {
|
||||
@apply w-2em;
|
||||
}
|
||||
</style>
|
70
src/components/Preference/components/ScopeSelect/index.vue
Normal file
70
src/components/Preference/components/ScopeSelect/index.vue
Normal file
@ -0,0 +1,70 @@
|
||||
<template>
|
||||
<el-select
|
||||
:placeholder="$t('preferences.scope.placeholder')"
|
||||
:no-data-text="$t('preferences.scope.no-data')"
|
||||
filterable
|
||||
class="!w-90"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-tooltip class="" effect="dark" placement="bottom-start">
|
||||
<el-icon class="text-primary-300 hover:text-primary-500">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
<template #content>
|
||||
<div class="space-y-1">
|
||||
<div class="pb-1">
|
||||
{{ $t('preferences.scope.details[0]') }}
|
||||
</div>
|
||||
<div class="">
|
||||
{{ $t('preferences.scope.details[1]') }}
|
||||
</div>
|
||||
<div class="">
|
||||
{{ $t('preferences.scope.details[2]') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in options"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDeviceStore } from '$/store/index.js'
|
||||
|
||||
const emit = defineEmits(['device-change'])
|
||||
|
||||
const deviceStore = useDeviceStore()
|
||||
|
||||
const options = computed(() => {
|
||||
const value = deviceStore.list.map(item => ({
|
||||
...item,
|
||||
label: `${item.id}(${item.$name}${
|
||||
item.$remark ? `,${item.$remark}` : ''
|
||||
})`,
|
||||
value: item.id,
|
||||
}))
|
||||
|
||||
value.unshift({
|
||||
label: `Global(${window.t('preferences.scope.global')})`,
|
||||
value: 'global',
|
||||
})
|
||||
|
||||
return value
|
||||
})
|
||||
|
||||
watch(
|
||||
() => deviceStore.list.length,
|
||||
() => {
|
||||
emit('device-change', options.value)
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<style></style>
|
@ -4,43 +4,11 @@
|
||||
class="mr-4 pb-4 flex items-center justify-between flex-none border-b border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
<div class="">
|
||||
<el-select
|
||||
<ScopeSelect
|
||||
v-model="deviceScope"
|
||||
value-key=""
|
||||
:placeholder="$t('preferences.scope.placeholder')"
|
||||
filterable
|
||||
:no-data-text="$t('preferences.scope.no-data')"
|
||||
class="!w-90"
|
||||
@change="onScopeChange"
|
||||
>
|
||||
<template #prefix>
|
||||
<el-tooltip class="" effect="dark" placement="bottom-start">
|
||||
<el-icon class="text-primary-300 hover:text-primary-500">
|
||||
<QuestionFilled />
|
||||
</el-icon>
|
||||
<template #content>
|
||||
<div class="space-y-1">
|
||||
<div class="pb-1">
|
||||
{{ $t('preferences.scope.details[0]') }}
|
||||
</div>
|
||||
<div class="">
|
||||
{{ $t('preferences.scope.details[1]') }}
|
||||
</div>
|
||||
<div class="">
|
||||
{{ $t('preferences.scope.details[2]') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in scopeList"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
@device-change="onDeviceChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="">
|
||||
<el-button type="" icon="Upload" plain @click="handleImport">
|
||||
@ -52,208 +20,53 @@
|
||||
<el-button type="" icon="Edit" plain @click="handleEdit">
|
||||
{{ $t('preferences.config.edit.name') }}
|
||||
</el-button>
|
||||
<el-button type="" icon="RefreshRight" plain @click="handleResetAll">
|
||||
<el-button type="" icon="RefreshRight" plain @click="handleReset">
|
||||
{{ $t('preferences.config.reset.name') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-6 pr-2 pt-4 flex-1 h-0 overflow-auto">
|
||||
<el-collapse v-model="collapseValues" class="space-y-4 borderless">
|
||||
<el-collapse-item
|
||||
v-for="(item, name) of preferenceModel"
|
||||
:key="name"
|
||||
:name="name"
|
||||
class="!border dark:border-gray-700 rounded-[5px] overflow-hidden shadow-el-lighter"
|
||||
>
|
||||
<template #title>
|
||||
<div
|
||||
class="flex items-center w-full text-left -mr-10 overflow-hidden dark:border-gray-700"
|
||||
:class="{
|
||||
'!border-b': collapseValues.includes(name),
|
||||
<div class="pr-2 pt-4 flex-1 h-0 overflow-auto">
|
||||
<PreferenceForm
|
||||
v-model="preferenceData"
|
||||
v-bind="{
|
||||
deviceScope,
|
||||
}"
|
||||
>
|
||||
<div class="flex-1 w-0 truncate pl-4 text-base">
|
||||
{{ $t(item.label) }}
|
||||
</div>
|
||||
<div class="flex-none pl-4 pr-12" @click.stop>
|
||||
<el-button type="primary" text @click="handleReset(name)">
|
||||
{{ $t('preferences.reset') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="pt-4">
|
||||
<el-form
|
||||
ref="elForm"
|
||||
:model="preferenceData"
|
||||
label-width="225px"
|
||||
class="pr-8 pt-4"
|
||||
>
|
||||
<el-row :gutter="20">
|
||||
<el-col
|
||||
v-for="(item_1, name_1) of subModel(item)"
|
||||
:key="name_1"
|
||||
:span="item_1.span || 12"
|
||||
:offset="item_1.offset || 0"
|
||||
>
|
||||
<el-form-item :label="$t(item_1.label)" :prop="item_1.field">
|
||||
<template #label>
|
||||
<div class="flex items-center">
|
||||
<el-tooltip
|
||||
v-if="item_1.tips"
|
||||
popper-class="max-w-96"
|
||||
effect="dark"
|
||||
:content="$t(item_1.tips)"
|
||||
placement="bottom"
|
||||
>
|
||||
<el-link
|
||||
class="mr-1 !text-base"
|
||||
icon="InfoFilled"
|
||||
type="warning"
|
||||
:underline="false"
|
||||
>
|
||||
</el-link>
|
||||
</el-tooltip>
|
||||
<span class="" :title="$t(item_1.placeholder)">{{
|
||||
$t(item_1.label)
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-input
|
||||
v-if="item_1.type === 'Input'"
|
||||
v-bind="item_1.props || {}"
|
||||
v-model="preferenceData[item_1.field]"
|
||||
class="!w-full"
|
||||
:title="$t(item_1.placeholder)"
|
||||
:placeholder="$t(item_1.placeholder)"
|
||||
clearable
|
||||
>
|
||||
<template v-if="item_1.append" #append>
|
||||
{{ item_1.append }}
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-input
|
||||
v-else-if="item_1.type === 'Input.number'"
|
||||
v-bind="item_1.props || {}"
|
||||
v-model.number="preferenceData[item_1.field]"
|
||||
class="!w-full"
|
||||
:title="$t(item_1.placeholder)"
|
||||
:placeholder="$t(item_1.placeholder)"
|
||||
clearable
|
||||
>
|
||||
<template v-if="item_1.append" #append>
|
||||
{{ item_1.append }}
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<el-switch
|
||||
v-else-if="item_1.type === 'Switch'"
|
||||
v-bind="item_1.props || {}"
|
||||
v-model="preferenceData[item_1.field]"
|
||||
class="!w-full"
|
||||
:title="$t(item_1.placeholder)"
|
||||
></el-switch>
|
||||
|
||||
<el-select
|
||||
v-else-if="item_1.type === 'Select'"
|
||||
v-bind="item_1.props || {}"
|
||||
v-model="preferenceData[item_1.field]"
|
||||
class="!w-full"
|
||||
:title="$t(item_1.placeholder)"
|
||||
:placeholder="$t(item_1.placeholder)"
|
||||
>
|
||||
<el-option
|
||||
v-for="(item_2, index_2) in item_1.options"
|
||||
:key="index_2"
|
||||
:label="$t(item_2.label)"
|
||||
:value="item_2.value"
|
||||
:title="$t(item_2.placeholder || item_2.label)"
|
||||
>
|
||||
</el-option>
|
||||
</el-select>
|
||||
|
||||
<component
|
||||
:is="item_1.type"
|
||||
v-else
|
||||
v-model="preferenceData[item_1.field]"
|
||||
:data="item_1"
|
||||
:device-scope="deviceScope"
|
||||
:preference-data="preferenceData"
|
||||
></component>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-collapse-item>
|
||||
</el-collapse>
|
||||
</PreferenceForm>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { debounce } from 'lodash-es'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { useOtg } from './composables/otg/index.js'
|
||||
|
||||
import LanguageSelect from './components/LanguageSelect/index.vue'
|
||||
import PathInput from './components/PathInput/index.vue'
|
||||
import VideoCodecSelect from './components/VideoCodecSelect/index.vue'
|
||||
import AudioCodecSelect from './components/AudioCodecSelect/index.vue'
|
||||
import DisplaySelect from './components/DisplaySelect/index.vue'
|
||||
import KeyboardInjectSelect from './components/KeyboardInjectSelect/index.vue'
|
||||
import ScopeSelect from './components/ScopeSelect/index.vue'
|
||||
import PreferenceForm from './components/PreferenceForm/index.vue'
|
||||
|
||||
import { usePreferenceStore } from '$/store/index.js'
|
||||
import LoadingIcon from '$/components/Device/components/LoadingIcon/index.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LanguageSelect,
|
||||
PathInput,
|
||||
VideoCodecSelect,
|
||||
AudioCodecSelect,
|
||||
DisplaySelect,
|
||||
KeyboardInjectSelect,
|
||||
ScopeSelect,
|
||||
PreferenceForm,
|
||||
},
|
||||
setup() {
|
||||
const preferenceStore = usePreferenceStore()
|
||||
|
||||
const preferenceData = ref(preferenceStore.data)
|
||||
|
||||
const deviceScope = ref(preferenceStore.deviceScope)
|
||||
|
||||
const collapseValues = ref(Object.keys(preferenceStore.model))
|
||||
|
||||
useOtg(preferenceData)
|
||||
|
||||
return {
|
||||
preferenceStore,
|
||||
preferenceData,
|
||||
deviceScope,
|
||||
collapseValues,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
preferenceModel() {
|
||||
return this.$store.preference.model || {}
|
||||
},
|
||||
scopeList() {
|
||||
const value = this.$store.device.list.map(item => ({
|
||||
...item,
|
||||
label: `${item.id}(${item.$name}${
|
||||
item.$remark ? `,${item.$remark}` : ''
|
||||
})`,
|
||||
value: item.id,
|
||||
}))
|
||||
|
||||
value.unshift({
|
||||
label: `Global(${this.$t('preferences.scope.global')})`,
|
||||
value: 'global',
|
||||
})
|
||||
|
||||
return value
|
||||
return this.preferenceStore.model || {}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
@ -273,50 +86,36 @@ export default {
|
||||
this.handleDevices()
|
||||
},
|
||||
},
|
||||
// 列表设备发生变化后如果没有匹配到则默认选中 global
|
||||
'scopeList': {
|
||||
handler(value) {
|
||||
const someValue = value.some(
|
||||
item => this.$replaceIP(item.value) === this.deviceScope,
|
||||
)
|
||||
|
||||
if (someValue) {
|
||||
return
|
||||
}
|
||||
|
||||
this.deviceScope = 'global'
|
||||
this.$store.preference.setScope(this.deviceScope)
|
||||
this.preferenceData = this.$store.preference.data
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.handleSave = debounce(this.handleSave, 1000)
|
||||
this.handleDevices = debounce(this.handleDevices, 1000)
|
||||
},
|
||||
methods: {
|
||||
onDeviceChange(options) {
|
||||
const device = options.some(
|
||||
item => this.$replaceIP(item.value) === this.deviceScope,
|
||||
)
|
||||
|
||||
if (device) {
|
||||
return false
|
||||
}
|
||||
|
||||
this.deviceScope = 'global'
|
||||
this.preferenceStore.setScope(this.deviceScope)
|
||||
this.preferenceData = this.preferenceStore.data
|
||||
},
|
||||
handleDevices() {
|
||||
this.$root.reRenderPost()
|
||||
},
|
||||
subModel(item) {
|
||||
const children = item?.children || {}
|
||||
const value = {}
|
||||
Object.entries(children).forEach(([key, data]) => {
|
||||
if (!data.hidden) {
|
||||
value[key] = data
|
||||
}
|
||||
})
|
||||
return value
|
||||
},
|
||||
handleResetAll() {
|
||||
this.$store.preference.reset(this.deviceScope)
|
||||
this.preferenceData = this.$store.preference.data
|
||||
handleReset() {
|
||||
this.preferenceStore.reset(this.deviceScope)
|
||||
this.preferenceData = this.preferenceStore.data
|
||||
},
|
||||
|
||||
onScopeChange(value) {
|
||||
this.$store.preference.setScope(value)
|
||||
this.preferenceData = this.$store.preference.data
|
||||
this.preferenceStore.setScope(value)
|
||||
this.preferenceData = this.preferenceStore.data
|
||||
},
|
||||
|
||||
async handleImport() {
|
||||
@ -334,7 +133,7 @@ export default {
|
||||
|
||||
this.$message.success(this.$t('preferences.config.import.success'))
|
||||
|
||||
this.preferenceData = this.$store.preference.init()
|
||||
this.preferenceData = this.preferenceStore.init()
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
@ -349,11 +148,9 @@ export default {
|
||||
},
|
||||
|
||||
async handleExport() {
|
||||
const messageEl = this.$message({
|
||||
message: this.$t('preferences.config.export.message'),
|
||||
icon: LoadingIcon,
|
||||
duration: 0,
|
||||
})
|
||||
const messageEl = this.$message.loading(
|
||||
this.$t('preferences.config.export.message'),
|
||||
)
|
||||
|
||||
try {
|
||||
await this.$electron.ipcRenderer.invoke('show-save-dialog', {
|
||||
@ -379,17 +176,9 @@ export default {
|
||||
},
|
||||
|
||||
handleSave() {
|
||||
this.$store.preference.setData(this.preferenceData)
|
||||
this.preferenceStore.setData(this.preferenceData)
|
||||
this.$message.success(this.$t('preferences.config.save.placeholder'))
|
||||
},
|
||||
|
||||
handleReset(type) {
|
||||
this.preferenceData = {
|
||||
...this.preferenceData,
|
||||
...this.$store.preference.getDefaultData(type),
|
||||
}
|
||||
this.$store.preference.setData(this.preferenceData)
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
@ -7,6 +7,7 @@
|
||||
"common.open": "Open",
|
||||
"common.input.placeholder": "Please input",
|
||||
"common.success": "Operation successful",
|
||||
"common.success.batch": "Batch operation success",
|
||||
"common.progress": "Starting",
|
||||
"common.loading": "Loading",
|
||||
"common.search": "Search",
|
||||
@ -86,6 +87,7 @@
|
||||
"device.actions.more.record.name": "Start Recording",
|
||||
"device.actions.more.otg.name": "Startup OTG",
|
||||
"device.actions.more.camera.name": "Startup Camera",
|
||||
"device.actions.more.custom.name": "Custom Startup",
|
||||
|
||||
"device.control.name": "Control",
|
||||
"device.control.more": "More Controls",
|
||||
@ -98,9 +100,18 @@
|
||||
"device.control.file.name": "File Manager",
|
||||
"device.control.file.push": "Push File",
|
||||
"device.control.file.push.placeholder": "Please select the file to push",
|
||||
"device.control.file.push.loading": "Push file",
|
||||
"device.control.file.push.success.name": "Push files successfully",
|
||||
"device.control.file.push.success": "Successfully pushed {totalCount} files to the /sdcard/Download/ directory of {deviceName}, {successCount} succeeded, and {failCount} failed",
|
||||
"device.control.file.push.success.single": "Files successfully pushed to the /sdcard/Download/ directory of {deviceName}",
|
||||
"device.control.file.push.error": "Failed to push the file, please check the file and try again",
|
||||
"device.control.shell.name": "Execute Script",
|
||||
"device.control.shell.tips": "Perform custom script through the ADB command",
|
||||
"device.control.shell.select": "Please select the script you want to execute",
|
||||
"device.control.shell.push.loading": "Push script",
|
||||
"device.control.shell.push.success": "Push script success",
|
||||
"device.control.shell.enter": "Please enter the Enter key to confirm the execution of the script",
|
||||
"device.control.shell.success": "Script execution successfully",
|
||||
"device.control.capture": "Screenshot",
|
||||
"device.control.capture.progress": "Capturing screenshot for {deviceName}...",
|
||||
"device.control.capture.success.message": "Open screenshot location?",
|
||||
|
@ -7,6 +7,7 @@
|
||||
"common.open": "打开",
|
||||
"common.input.placeholder": "请填写",
|
||||
"common.success": "操作成功",
|
||||
"common.success.batch": "批量操作成功",
|
||||
"common.progress": "启动中",
|
||||
"common.loading": "加载中",
|
||||
"common.search": "搜索",
|
||||
@ -86,6 +87,7 @@
|
||||
"device.actions.more.record.name": "开始录制",
|
||||
"device.actions.more.otg.name": "启动OTG",
|
||||
"device.actions.more.camera.name": "启动摄像",
|
||||
"device.actions.more.custom.name": "灵活启动",
|
||||
|
||||
"device.control.name": "操作",
|
||||
"device.control.more": "设备交互",
|
||||
@ -98,9 +100,18 @@
|
||||
"device.control.file.name": "文件管理",
|
||||
"device.control.file.push": "推送文件",
|
||||
"device.control.file.push.placeholder": "请选择要推送的文件",
|
||||
"device.control.file.push.loading": "推送文件中",
|
||||
"device.control.file.push.success.name": "推送文件成功",
|
||||
"device.control.file.push.success": "已成功将 {totalCount} 个文件推送到 {deviceName} 的 /sdcard/Download/ 目录,{successCount} 成功,{failCount} 失败。",
|
||||
"device.control.file.push.success.single": "文件已成功推送到 {deviceName} 的 /sdcard/Download/ 目录",
|
||||
"device.control.file.push.error": "推送文件失败,请检查文件后重试",
|
||||
"device.control.shell.name": "执行脚本",
|
||||
"device.control.shell.tips": "通过 ADB 命令执行自定义脚本",
|
||||
"device.control.shell.select": "请选择要执行的脚本",
|
||||
"device.control.shell.push.loading": "推送脚本中",
|
||||
"device.control.shell.push.success": "推送脚本成功",
|
||||
"device.control.shell.enter": "请输入回车键确认执行该脚本",
|
||||
"device.control.shell.success": "脚本执行成功",
|
||||
"device.control.capture": "截取屏幕",
|
||||
"device.control.capture.progress": "正在截取 {deviceName} 的屏幕快照...",
|
||||
"device.control.capture.success.message": "是否前往截屏位置进行查看?",
|
||||
|
@ -7,6 +7,7 @@
|
||||
"common.open": "開啟",
|
||||
"common.input.placeholder": "請輸入",
|
||||
"common.success": "操作成功",
|
||||
"common.success.batch": "批量操作成功",
|
||||
"common.progress": "啟動中",
|
||||
"common.loading": "載入中",
|
||||
"common.search": "搜尋",
|
||||
@ -86,6 +87,7 @@
|
||||
"device.actions.more.record.name": "開始錄製",
|
||||
"device.actions.more.otg.name": "啟動 OTG",
|
||||
"device.actions.more.camera.name": "啟動鏡頭",
|
||||
"device.actions.more.custom.name": "靈活啟動",
|
||||
|
||||
"device.control.name": "操作",
|
||||
"device.control.more": "裝置互動",
|
||||
@ -98,9 +100,18 @@
|
||||
"device.control.file.name": "檔案管理",
|
||||
"device.control.file.push": "推送檔案",
|
||||
"device.control.file.push.placeholder": "請選擇要推送的檔案",
|
||||
"device.control.file.push.loading": "推送檔案中",
|
||||
"device.control.file.push.success.name": "推送檔案成功",
|
||||
"device.control.file.push.success": "已成功將 {totalCount} 個檔案推送到 {deviceName} 的 /sdcard/Download/ 目錄,{successCount} 成功,{failCount} 失敗。",
|
||||
"device.control.file.push.success.single": "檔案已成功推送到 {deviceName} 的 /sdcard/Download/ 目錄",
|
||||
"device.control.file.push.error": "推送檔案失敗,請檢查檔案後重試",
|
||||
"device.control.shell.name": "執行腳本",
|
||||
"device.control.shell.tips": "透過 ADB 命令執行自訂腳本",
|
||||
"device.control.shell.select": "請選擇要執行的腳本",
|
||||
"device.control.shell.push.loading": "推送腳本中",
|
||||
"device.control.shell.push.success": "推送腳本成功",
|
||||
"device.control.shell.enter": "請輸入回車鍵確認執行該腳本",
|
||||
"device.control.shell.success": "腳本執行成功",
|
||||
"device.control.capture": "擷取螢幕",
|
||||
"device.control.capture.progress": "正在擷取 {deviceName} 的螢幕快照...",
|
||||
"device.control.capture.success.message": "是否前往截圖位置進行檢視?",
|
||||
|
@ -16,6 +16,7 @@ export default () => {
|
||||
}),
|
||||
useAutoComponents({
|
||||
resolvers,
|
||||
dirs: 'none',
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'LoadingIcon',
|
||||
name: 'EleIconLoading',
|
||||
}
|
||||
</script>
|
||||
|
@ -1,19 +1,31 @@
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
import 'element-plus/theme-chalk/el-loading.css'
|
||||
import 'element-plus/theme-chalk/el-message.css'
|
||||
import 'element-plus/theme-chalk/el-message-box.css'
|
||||
import 'element-plus/theme-chalk/el-badge.css'
|
||||
|
||||
import 'element-plus/theme-chalk/dark/css-vars.css'
|
||||
import './restyle.css'
|
||||
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
|
||||
import { ElLoading, ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
import EleIconLoading from './components/EleIconLoading/index.vue'
|
||||
|
||||
export default {
|
||||
install(app) {
|
||||
for (const [key, component] of Object.entries(ElementPlusIcons)) {
|
||||
app.component(key, component)
|
||||
}
|
||||
|
||||
ElMessage.loading = (message, options = {}) =>
|
||||
ElMessage({
|
||||
duration: 0,
|
||||
...options,
|
||||
message,
|
||||
icon: EleIconLoading,
|
||||
})
|
||||
|
||||
app.use(ElMessage)
|
||||
app.use(ElMessageBox)
|
||||
app.use(ElLoading)
|
||||
|
@ -59,3 +59,16 @@
|
||||
@apply !p-0;
|
||||
}
|
||||
}
|
||||
|
||||
.el-dialog-beautify {
|
||||
@apply !rounded-lg;
|
||||
|
||||
.el-dialog__title {
|
||||
@apply relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
@apply absolute inset-x-0 bottom-0 h-2 bg-primary-500/30;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -30,12 +30,14 @@ export function getModelMap(data = model) {
|
||||
return value
|
||||
}
|
||||
|
||||
export function getDefaultData(parentId) {
|
||||
export function getDefaultData(parentId, iteratee) {
|
||||
const modelMap = getModelMap()
|
||||
|
||||
iteratee = iteratee ?? (value => value)
|
||||
|
||||
const value = Object.entries(modelMap).reduce((obj, [key, data]) => {
|
||||
if (!parentId || data.parentId === parentId) {
|
||||
obj[key] = data.value
|
||||
obj[key] = iteratee(data.value)
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
|
@ -54,15 +54,9 @@ export const usePreferenceStore = defineStore({
|
||||
getters: {},
|
||||
actions: {
|
||||
getDefaultData,
|
||||
|
||||
init(scope = this.deviceScope) {
|
||||
let data = mergeConfig(getDefaultData(), getStoreData())
|
||||
|
||||
if (scope !== 'global') {
|
||||
data = mergeConfig(data, getStoreData(replaceIP(scope)))
|
||||
}
|
||||
|
||||
this.data = data
|
||||
|
||||
this.data = this.getData(scope)
|
||||
return this.data
|
||||
},
|
||||
setScope(value) {
|
||||
@ -127,7 +121,12 @@ export const usePreferenceStore = defineStore({
|
||||
this.init()
|
||||
},
|
||||
getData(scope = this.deviceScope) {
|
||||
const value = this.init(scope)
|
||||
let value = mergeConfig(getDefaultData(), getStoreData())
|
||||
|
||||
if (scope !== 'global') {
|
||||
value = mergeConfig(value, getStoreData(replaceIP(scope)))
|
||||
}
|
||||
|
||||
return value
|
||||
},
|
||||
|
||||
@ -135,7 +134,7 @@ export const usePreferenceStore = defineStore({
|
||||
scope = this.deviceScope,
|
||||
{ isRecord = false, isCamera = false, isOtg = false, excludes = [] } = {},
|
||||
) {
|
||||
const data = this.getData(scope)
|
||||
const data = typeof scope === 'object' ? scope : this.getData(scope)
|
||||
|
||||
if (!data) {
|
||||
return ''
|
||||
@ -182,12 +181,14 @@ export const usePreferenceStore = defineStore({
|
||||
return arr
|
||||
}, [])
|
||||
|
||||
if (this.data.scrcpyAppend) {
|
||||
valueList.push(...this.data.scrcpyAppend.split(' '))
|
||||
if (data.scrcpyAppend) {
|
||||
valueList.push(...data.scrcpyAppend.split(' '))
|
||||
}
|
||||
|
||||
const value = valueList.join(' ')
|
||||
|
||||
// console.log('value', value)
|
||||
|
||||
return value
|
||||
},
|
||||
getModel(path) {
|
||||
|
@ -66,7 +66,7 @@ export default {
|
||||
audioBuffer: {
|
||||
label: 'preferences.audio.audio-buffer.name',
|
||||
field: '--audio-buffer',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.audio.audio-buffer.placeholder',
|
||||
append: 'ms',
|
||||
@ -74,7 +74,7 @@ export default {
|
||||
audioOutputBuffer: {
|
||||
label: 'preferences.audio.audio-output-buffer.name',
|
||||
field: '--audio-output-buffer',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.audio.audio-output-buffer.placeholder',
|
||||
append: 'ms',
|
||||
|
@ -34,7 +34,7 @@ export default {
|
||||
cameraFps: {
|
||||
label: 'preferences.camera.camera-fps.name',
|
||||
field: '--camera-fps',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.camera.camera-fps.placeholder',
|
||||
append: 'fps',
|
||||
|
@ -28,6 +28,9 @@ export default {
|
||||
value: 'system',
|
||||
},
|
||||
],
|
||||
props: {
|
||||
clearable: false,
|
||||
},
|
||||
},
|
||||
language: {
|
||||
label: 'common.language.name',
|
||||
|
@ -35,7 +35,7 @@ export default {
|
||||
timeLimit: {
|
||||
label: 'preferences.record.time-limit.name',
|
||||
field: '--time-limit',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.record.time-limit.placeholder',
|
||||
append: 's',
|
||||
|
@ -31,7 +31,7 @@ export default {
|
||||
maxSize: {
|
||||
label: 'preferences.video.resolution.name',
|
||||
field: '--max-size',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.video.resolution.placeholder',
|
||||
},
|
||||
@ -46,7 +46,7 @@ export default {
|
||||
maxFps: {
|
||||
label: 'preferences.video.refresh-rate.name',
|
||||
field: '--max-fps',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.video.refresh-rate.placeholder',
|
||||
append: 'fps',
|
||||
@ -133,7 +133,7 @@ export default {
|
||||
displayBuffer: {
|
||||
label: 'preferences.video.video-buffer.name',
|
||||
field: '--display-buffer',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.video.video-buffer.placeholder',
|
||||
append: 'ms',
|
||||
@ -141,7 +141,7 @@ export default {
|
||||
v4l2Buffer: {
|
||||
label: 'preferences.video.receiver-buffer.name',
|
||||
field: '--v4l2-buffer',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.video.receiver-buffer.placeholder',
|
||||
append: 'ms',
|
||||
|
@ -6,7 +6,7 @@ export default {
|
||||
windowWidth: {
|
||||
label: 'preferences.window.size.width',
|
||||
field: '--window-width',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.window.size.width.placeholder',
|
||||
tips: 'preferences.window.size.width.tips',
|
||||
@ -14,7 +14,7 @@ export default {
|
||||
windowHeight: {
|
||||
label: 'preferences.window.size.height',
|
||||
field: '--window-height',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.window.size.height.placeholder',
|
||||
tips: 'preferences.window.size.height.tips',
|
||||
@ -22,14 +22,14 @@ export default {
|
||||
windowX: {
|
||||
label: 'preferences.window.position.x',
|
||||
field: '--window-x',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.window.position.x.placeholder',
|
||||
},
|
||||
windowY: {
|
||||
label: 'preferences.window.position.y',
|
||||
field: '--window-y',
|
||||
type: 'Input.number',
|
||||
type: 'InputNumber',
|
||||
value: undefined,
|
||||
placeholder: 'preferences.window.position.y.placeholder',
|
||||
},
|
||||
|
@ -7,6 +7,7 @@ html {
|
||||
@screen sm {
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
|
66
src/utils/device/index.js
Normal file
66
src/utils/device/index.js
Normal file
@ -0,0 +1,66 @@
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { allSettled } from '$/utils'
|
||||
/**
|
||||
* 选择并将文件发送到设备
|
||||
*/
|
||||
export async function selectAndSendFileToDevice(
|
||||
deviceId,
|
||||
{
|
||||
files,
|
||||
multiSelections = false,
|
||||
extensions = ['*'],
|
||||
selectText = window.t('device.control.file.push.placeholder'),
|
||||
loadingText = window.t('device.control.file.push.loading'),
|
||||
successText = window.t('device.control.file.push.success.name'),
|
||||
} = {},
|
||||
) {
|
||||
if (!files) {
|
||||
try {
|
||||
const properties = ['openFile']
|
||||
|
||||
if (multiSelections) {
|
||||
properties.push('multiSelections')
|
||||
}
|
||||
|
||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties,
|
||||
filters: [
|
||||
{
|
||||
name: selectText,
|
||||
extensions,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
throw new Error(error.message?.match(/Error: (.*)/)?.[1] || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
const closeMessage = ElMessage.loading(loadingText).close
|
||||
|
||||
const successFiles = []
|
||||
const failFiles = []
|
||||
|
||||
await allSettled(files, async (item) => {
|
||||
const ret = await window.adbkit.push(deviceId, item).catch((e) => {
|
||||
console.warn(e?.message)
|
||||
failFiles.push(`${deviceId}-${item}`)
|
||||
})
|
||||
|
||||
if (ret) {
|
||||
successFiles.push(ret)
|
||||
}
|
||||
})
|
||||
|
||||
if (failFiles.length) {
|
||||
closeMessage()
|
||||
throw new Error(`Push file failed: ${failFiles.join(',')}`)
|
||||
}
|
||||
|
||||
closeMessage()
|
||||
|
||||
ElMessage.success({ message: successText, grouping: true })
|
||||
|
||||
return successFiles
|
||||
}
|
@ -52,3 +52,40 @@ export function keyByValue(data, key = 'key', valueKey = 'value') {
|
||||
|
||||
return value
|
||||
}
|
||||
|
||||
/**
|
||||
* 对列表中的每个项目执行给定的迭代器函数,并返回一个 Promise,
|
||||
* 该 Promise 在所有迭代完成时解决,无论它们是成功还是失败。
|
||||
*
|
||||
* @param {Array} list - 要迭代的项目数组。
|
||||
* @param {Function} iterator - 对列表中每个项目执行的函数。
|
||||
* 它应该返回一个 Promise 或者可以是一个异步函数。
|
||||
* @param {*} iterator.item - 当前正在处理的列表项。
|
||||
* @param {number} iterator.index - 当前正在处理的项目的索引。
|
||||
* @param {Array} iterator.array - 正在处理的原始数组。
|
||||
* @returns {Promise<Array<PromiseSettledResult>>} 一个 Promise,解析为一个对象数组,
|
||||
* 描述输入数组中每个 promise 的结果。
|
||||
* @throws {TypeError} 如果第一个参数不是数组或第二个参数不是函数。
|
||||
*
|
||||
* @example
|
||||
* const list = [1, 2, 3, 4, 5];
|
||||
* const iterator = async (item) => {
|
||||
* if (item % 2 === 0) {
|
||||
* return item * 2;
|
||||
* } else {
|
||||
* throw new Error('奇数');
|
||||
* }
|
||||
* };
|
||||
* allSettled(list, iterator).then(console.log);
|
||||
*/
|
||||
export function allSettled(list = [], iterator) {
|
||||
const promises = []
|
||||
|
||||
for (let index = 0; index < list.length; index++) {
|
||||
const item = list[index]
|
||||
|
||||
promises.push(iterator(item))
|
||||
}
|
||||
|
||||
return Promise.allSettled(promises)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user