Merge branch 'dev'

This commit is contained in:
viarotel 2024-07-13 17:10:43 +08:00
commit 61a0c8d029
62 changed files with 1477 additions and 673 deletions

View File

@ -79,6 +79,7 @@
"watch": true,
"watchEffect": true,
"watchPostEffect": true,
"watchSyncEffect": true
"watchSyncEffect": true,
"ElMessage": true
}
}

View File

@ -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",

View File

@ -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. 支持通过界面从设备下载选中的文件 🚧

View File

@ -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
View File

@ -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
View File

@ -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']

View File

@ -198,8 +198,8 @@ const push = async (
progress?.(stats)
})
res.on('end', (ret) => {
resolve(ret)
res.on('end', () => {
resolve(savePath)
})
res.on('error', (err) => {

View File

@ -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:',

View File

@ -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'

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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'))

View File

@ -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>

View File

@ -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')

View File

@ -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)
}
}
},
},
}

View 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>

View File

@ -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',

View File

@ -45,6 +45,8 @@ export default {
},
)}`
console.log('args', args)
try {
const mirroring = this.$scrcpy.mirror(row.id, {
title: this.$store.device.getLabel(row),

View File

@ -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>

View File

@ -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>

View File

@ -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)
}
},
},
}

View File

@ -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',
},
],
}
},

View File

@ -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>

View File

@ -20,7 +20,10 @@ export default {
},
methods: {
handleShow() {
this.$refs.terminalDialog.show()
this.$refs.terminalDialog.open()
},
invoke(...args) {
this.$refs.terminalDialog.invoke(...args)
},
},
}

View File

@ -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,

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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>

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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>

View File

@ -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,
}

View 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>

View 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>

View File

@ -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>

View File

@ -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?",

View File

@ -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": "是否前往截屏位置进行查看?",

View File

@ -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": "是否前往截圖位置進行檢視?",

View File

@ -16,6 +16,7 @@ export default () => {
}),
useAutoComponents({
resolvers,
dirs: 'none',
}),
]
}

View File

@ -6,7 +6,7 @@
<script>
export default {
name: 'LoadingIcon',
name: 'EleIconLoading',
}
</script>

View File

@ -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)

View File

@ -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;
}
}
}

View File

@ -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
}, {})

View File

@ -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) {

View File

@ -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',

View File

@ -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',

View File

@ -28,6 +28,9 @@ export default {
value: 'system',
},
],
props: {
clearable: false,
},
},
language: {
label: 'common.language.name',

View File

@ -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',

View File

@ -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',

View File

@ -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',
},

View File

@ -7,6 +7,7 @@ html {
@screen sm {
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {

66
src/utils/device/index.js Normal file
View 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
}

View File

@ -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)
}