mirror of
https://github.com/viarotel-org/escrcpy.git
synced 2025-01-31 11:53:50 +01:00
feat: 🎉 Support batch execution script function
This commit is contained in:
parent
2013413611
commit
8097022798
@ -79,6 +79,7 @@
|
||||
"watch": true,
|
||||
"watchEffect": true,
|
||||
"watchPostEffect": true,
|
||||
"watchSyncEffect": true
|
||||
"watchSyncEffect": true,
|
||||
"ElMessage": true
|
||||
}
|
||||
}
|
||||
|
17
README-CN.md
17
README-CN.md
@ -76,7 +76,10 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
|
||||
### 批量处理
|
||||
|
||||
- 批量截取屏幕
|
||||
- 批量安装应用
|
||||
- 批量文件管理
|
||||
- 批量执行脚本
|
||||
|
||||
### 控制模式
|
||||
|
||||
@ -99,6 +102,7 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
- 重启设备
|
||||
- 安装应用
|
||||
- 文件管理
|
||||
- 执行脚本
|
||||
- 反向供网(Gnirehtet)
|
||||
- 多屏协同
|
||||
|
||||
@ -204,13 +208,12 @@ Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设
|
||||
15. 支持批量连接历史设备功能 ✅
|
||||
16. 支持使用内置终端执行自定义命令 ✅
|
||||
17. 支持设备自动执行镜像 ✅
|
||||
18. 支持常用批量功能 ✅
|
||||
19. 支持灵活启动镜像 ✅
|
||||
20. 支持更多批量处理功能 🚧
|
||||
21. 支持对设备进行分组 🚧
|
||||
22. 添加文件传输助手功能 🚧
|
||||
23. 支持通过界面从设备下载选中的文件 🚧
|
||||
24. 添加对游戏的增强功能,如游戏键位映射 🚧
|
||||
18. 支持灵活启动镜像 ✅
|
||||
19. 支持常用批量功能 ✅
|
||||
20. 支持对设备进行分组 🚧
|
||||
21. 添加文件传输助手功能 🚧
|
||||
22. 支持通过界面从设备下载选中的文件 🚧
|
||||
23. 添加对游戏的增强功能,如游戏键位映射 🚧
|
||||
|
||||
## 常见问题
|
||||
|
||||
|
21
README.md
21
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
|
||||
|
||||
@ -97,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
|
||||
|
||||
@ -201,14 +205,13 @@ 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. Support common batch processing function ✅
|
||||
19. Support for custom startup mirroring ✅
|
||||
20. Support more batch processing functions 🚧
|
||||
21. Support the device to group 🚧
|
||||
22. Add file transmission assistant function 🚧
|
||||
23. Support GUI-based selective file downloads from devices 🚧
|
||||
24. Add game enhancement features such as game keyboard mapping 🚧
|
||||
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 🚧
|
||||
23. Add game enhancement features such as game keyboard mapping 🚧
|
||||
|
||||
## FAQ
|
||||
|
||||
|
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']
|
||||
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick">
|
||||
<slot />
|
||||
<slot v-bind="{ loading }" />
|
||||
<ApplicationProxy ref="applicationProxyRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApplicationProxy from '$/components/Device/components/ControlBar/Application/index.vue'
|
||||
import { sleep } from '$/utils'
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -19,13 +19,42 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
for (let index = 0; index < this.devices.length; index++) {
|
||||
const item = this.devices[index]
|
||||
await this.$refs.applicationProxyRef.invoke(item)
|
||||
await sleep(1 * 1000)
|
||||
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
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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>
|
@ -1,13 +1,13 @@
|
||||
<template>
|
||||
<div class="" @click="handleClick">
|
||||
<slot />
|
||||
<slot v-bind="{ loading }" />
|
||||
<ScreenshotProxy ref="screenshotProxyRef" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ScreenshotProxy from '$/components/Device/components/ControlBar/Screenshot/index.vue'
|
||||
import { sleep } from '$/utils'
|
||||
import { allSettled, sleep } from '$/utils'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
@ -19,13 +19,20 @@ export default {
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handleClick() {
|
||||
for (let index = 0; index < this.devices.length; index++) {
|
||||
const item = this.devices[index]
|
||||
await this.$refs.screenshotProxyRef.invoke(item)
|
||||
await sleep(1 * 1000)
|
||||
}
|
||||
this.loading = true
|
||||
|
||||
await allSettled(this.devices, (item) => {
|
||||
return this.$refs.screenshotProxyRef.invoke(item)
|
||||
})
|
||||
|
||||
this.loading = false
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -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>
|
@ -38,41 +38,41 @@
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<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: {
|
||||
Application,
|
||||
Screenshot,
|
||||
const props = defineProps({
|
||||
devices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
props: {
|
||||
devices: {
|
||||
type: Array,
|
||||
default: () => [],
|
||||
},
|
||||
})
|
||||
|
||||
const actionModel = [
|
||||
{
|
||||
label: 'device.control.capture',
|
||||
elIcon: 'Crop',
|
||||
component: Screenshot,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
actionModel: [
|
||||
{
|
||||
label: 'device.control.capture',
|
||||
elIcon: 'Crop',
|
||||
component: 'Screenshot',
|
||||
},
|
||||
{
|
||||
label: 'device.control.install',
|
||||
svgIcon: 'install',
|
||||
component: 'Application',
|
||||
},
|
||||
],
|
||||
}
|
||||
{
|
||||
label: 'device.control.install',
|
||||
svgIcon: 'install',
|
||||
component: Application,
|
||||
},
|
||||
methods: {
|
||||
handleShell() {},
|
||||
{
|
||||
label: 'device.control.file.name',
|
||||
svgIcon: 'file-send',
|
||||
component: FileManage,
|
||||
},
|
||||
}
|
||||
{
|
||||
label: 'device.control.shell.name',
|
||||
svgIcon: 'command',
|
||||
component: Shell,
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<style></style>
|
||||
|
@ -5,6 +5,8 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { allSettled } from '$/utils'
|
||||
|
||||
export default {
|
||||
props: {
|
||||
device: {
|
||||
@ -22,29 +24,27 @@ export default {
|
||||
preferenceData(...args) {
|
||||
return this.$store.preference.getData(...args)
|
||||
},
|
||||
async handleInstall(device) {
|
||||
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]
|
||||
this.$message.warning(message || error.message)
|
||||
}
|
||||
}
|
||||
|
||||
async handleInstall(device, { files } = {}) {
|
||||
if (!files) {
|
||||
return false
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
const messageEl = this.$message.loading(
|
||||
@ -55,13 +55,12 @@ export default {
|
||||
|
||||
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()
|
||||
|
||||
@ -86,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,87 +15,98 @@
|
||||
</el-dropdown>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
<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: () => null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const deviceStore = useDeviceStore()
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
async function handlePush(device, { files } = {}) {
|
||||
if (!files) {
|
||||
try {
|
||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
name: window.t('device.control.file.push.placeholder'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async handlePush() {
|
||||
this.loading = true
|
||||
let files = null
|
||||
|
||||
try {
|
||||
files = await this.$electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile', 'multiSelections'],
|
||||
filters: [
|
||||
{
|
||||
name: this.$t('device.control.file.push.placeholder'),
|
||||
extensions: ['*'],
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message = error.message?.match(/Error: (.*)/)?.[1]
|
||||
this.$message.warning(message || error.message)
|
||||
}
|
||||
catch (error) {
|
||||
if (error.message) {
|
||||
const message
|
||||
= error.message?.match(/Error: (.*)/)?.[1] || error.message
|
||||
ElMessage.warning(message)
|
||||
}
|
||||
|
||||
if (!files) {
|
||||
this.loading = false
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
let failCount = 0
|
||||
loading.value = true
|
||||
|
||||
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)
|
||||
++failCount
|
||||
})
|
||||
}
|
||||
const closeMessage = ElMessage.loading(
|
||||
window.t('device.control.file.push.loading'),
|
||||
{ grouping: true },
|
||||
).close
|
||||
|
||||
this.loading = false
|
||||
let failCount = 0
|
||||
|
||||
const totalCount = files.length
|
||||
const successCount = totalCount - failCount
|
||||
await allSettled(files, (item) => {
|
||||
return window.adbkit.push(device.id, item).catch(() => {
|
||||
++failCount
|
||||
})
|
||||
})
|
||||
|
||||
if (successCount) {
|
||||
if (totalCount > 1) {
|
||||
this.$message.success(
|
||||
this.$t('device.control.file.push.success', {
|
||||
deviceName: this.$store.device.getLabel(this.device),
|
||||
totalCount,
|
||||
successCount,
|
||||
failCount,
|
||||
}),
|
||||
)
|
||||
}
|
||||
else {
|
||||
this.$message.success(
|
||||
this.$t('device.control.file.push.success.single', {
|
||||
deviceName: this.$store.device.getLabel(this.device),
|
||||
}),
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
loading.value = false
|
||||
|
||||
this.$message.warning(this.$t('device.control.file.push.error'))
|
||||
},
|
||||
},
|
||||
const totalCount = files.length
|
||||
const successCount = totalCount - failCount
|
||||
|
||||
if (successCount) {
|
||||
closeMessage()
|
||||
|
||||
if (totalCount > 1) {
|
||||
ElMessage.success(
|
||||
window.t('device.control.file.push.success', {
|
||||
deviceName: deviceStore.getLabel(device),
|
||||
totalCount,
|
||||
successCount,
|
||||
failCount,
|
||||
}),
|
||||
)
|
||||
}
|
||||
else {
|
||||
ElMessage.success(
|
||||
window.t('device.control.file.push.success.single', {
|
||||
deviceName: deviceStore.getLabel(device),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
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')
|
||||
|
@ -6,74 +6,45 @@
|
||||
|
||||
<script setup>
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
||||
|
||||
const props = defineProps({
|
||||
device: {
|
||||
type: Object,
|
||||
default: () => null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const invokeTerminal = inject('invokeTerminal')
|
||||
|
||||
async function handleClick(device) {
|
||||
let files = null
|
||||
|
||||
loading.value = true
|
||||
|
||||
try {
|
||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
||||
properties: ['openFile'],
|
||||
filters: [
|
||||
{
|
||||
name: window.t('device.control.shell.select'),
|
||||
extensions: ['sh'],
|
||||
},
|
||||
],
|
||||
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) {
|
||||
if (error.message) {
|
||||
const message = error.message?.match(/Error: (.*)/)?.[1]
|
||||
ElMessage.warning(message || error.message)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const closeMessage = ElMessage.loading(
|
||||
window.t('device.control.shell.pushing'),
|
||||
).close
|
||||
|
||||
const filePath = files[0]
|
||||
|
||||
let pushFilePath = ''
|
||||
|
||||
try {
|
||||
pushFilePath = await window.adbkit.push(device.id, filePath)
|
||||
}
|
||||
catch (error) {
|
||||
closeMessage()
|
||||
loading.value = false
|
||||
ElMessage.warning(error.message)
|
||||
return false
|
||||
}
|
||||
|
||||
if (!pushFilePath) {
|
||||
closeMessage()
|
||||
ElMessage.warning('Push script failed')
|
||||
return false
|
||||
}
|
||||
const filePath = files[0]
|
||||
|
||||
await window.adbkit.deviceShell(device.id, `sh ${pushFilePath}`)
|
||||
|
||||
closeMessage()
|
||||
|
||||
await ElMessage.success(window.t('device.control.shell.success'))
|
||||
|
||||
const command = `adb -s ${device.id} shell sh ${pushFilePath}`
|
||||
const command = `adb -s ${device.id} shell sh ${filePath}`
|
||||
|
||||
invokeTerminal(command)
|
||||
|
||||
loading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -4,27 +4,26 @@
|
||||
width="80%"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="true"
|
||||
destroy-on-close
|
||||
:destroy-on-close="true"
|
||||
class="overflow-hidden !rounded-md el-dialog-headless dark:border dark:border-gray-700"
|
||||
@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
|
||||
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,125 +36,141 @@
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<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,
|
||||
const themeStore = useThemeStore()
|
||||
|
||||
const loading = ref(false)
|
||||
const visible = ref(false)
|
||||
|
||||
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,
|
||||
clear() {
|
||||
history.value = []
|
||||
return createQuery()
|
||||
},
|
||||
setup() {
|
||||
const vShell = ref(null)
|
||||
const history = shallowRef([createQuery()])
|
||||
const loading = ref(false)
|
||||
const dispatchedQueries = ref(new Set([]))
|
||||
})
|
||||
|
||||
const { adb } = useAdb({ vShell, history, loading })
|
||||
const { scrcpy } = useScrcpy({ vShell, history, loading })
|
||||
const { gnirehtet } = useGnirehtet({ vShell, history, loading })
|
||||
|
||||
const commands = ref({
|
||||
adb,
|
||||
scrcpy,
|
||||
gnirehtet,
|
||||
clear() {
|
||||
history.value = []
|
||||
return createQuery()
|
||||
},
|
||||
})
|
||||
|
||||
commands.value.help = () => {
|
||||
const commandList = Object.keys(commands.value)
|
||||
return createStdout(listFormatter('Supported Commands:', ...commandList))
|
||||
}
|
||||
|
||||
dispatchedQueries.value = new Set([
|
||||
...(window.appStore.get('terminal.dispatchedQueries') || []),
|
||||
...Object.keys(commands.value),
|
||||
])
|
||||
|
||||
return {
|
||||
vShell,
|
||||
loading,
|
||||
history,
|
||||
commands,
|
||||
dispatchedQueries,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
visible: false,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
invert() {
|
||||
return !this.$store.theme.isDark
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
'vShell.signals': {
|
||||
handler(value) {
|
||||
value.off('SIGINT')
|
||||
|
||||
value.on('SIGINT', () => {
|
||||
this.onCtrlC()
|
||||
})
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
show() {
|
||||
this.visible = true
|
||||
},
|
||||
|
||||
hide() {
|
||||
this.visible = false
|
||||
},
|
||||
|
||||
async invoke(command) {
|
||||
this.visible = true
|
||||
|
||||
if (!this.vShell) {
|
||||
await this.$nextTick()
|
||||
await sleep(1000)
|
||||
}
|
||||
|
||||
await this.vShell.setQuery(command)
|
||||
|
||||
const vueCommandHistoryEntryComponentRefs
|
||||
= this.vShell.$refs.vueCommandHistoryEntryComponentRefs
|
||||
|
||||
const queryLength = vueCommandHistoryEntryComponentRefs.length
|
||||
|
||||
vueCommandHistoryEntryComponentRefs[queryLength - 1].focus()
|
||||
|
||||
this.$message.info(this.$t('device.control.shell.enter'))
|
||||
},
|
||||
|
||||
onDispatchedQueriesUpdate(value) {
|
||||
this.$appStore.set('terminal.dispatchedQueries', Array.from(value))
|
||||
|
||||
this.dispatchedQueries = value
|
||||
},
|
||||
|
||||
onCtrlC() {
|
||||
window.gnirehtet.shell('stop')
|
||||
},
|
||||
|
||||
onClosed() {
|
||||
this.vShell.dispatch('clear')
|
||||
},
|
||||
},
|
||||
commands.value.help = () => {
|
||||
const commandList = Object.keys(commands.value)
|
||||
return createStdout(listFormatter('Supported Commands:', ...commandList))
|
||||
}
|
||||
|
||||
dispatchedQueries.value = new Set([
|
||||
...(window.appStore.get('terminal.dispatchedQueries') || []),
|
||||
...Object.keys(commands.value),
|
||||
])
|
||||
|
||||
function getShell() {
|
||||
let unwatch = null
|
||||
|
||||
return new Promise((resolve) => {
|
||||
unwatch = watch(
|
||||
() => vShell.value,
|
||||
(value) => {
|
||||
if (value) {
|
||||
unwatch?.()
|
||||
resolve(value)
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
;(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,7 @@ export default {
|
||||
},
|
||||
methods: {
|
||||
handleShow() {
|
||||
this.$refs.terminalDialog.show()
|
||||
this.$refs.terminalDialog.open()
|
||||
},
|
||||
invoke(...args) {
|
||||
this.$refs.terminalDialog.invoke(...args)
|
||||
|
@ -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",
|
||||
@ -99,15 +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.pushing": "Push script",
|
||||
"device.control.shell.success": "Push script successfully",
|
||||
"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": "搜索",
|
||||
@ -99,15 +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.pushing": "推送脚本中",
|
||||
"device.control.shell.success": "推送脚本成功",
|
||||
"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": "搜尋",
|
||||
@ -99,15 +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.pushing": "推送腳本中",
|
||||
"device.control.shell.success": "推送腳本成功",
|
||||
"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": "是否前往截圖位置進行檢視?",
|
||||
|
@ -1,13 +1,15 @@
|
||||
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 {
|
||||
|
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