feat: 🎉 Support batch execution script function

This commit is contained in:
viarotel 2024-07-13 17:05:17 +08:00
parent 2013413611
commit 8097022798
21 changed files with 635 additions and 326 deletions

View File

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

View File

@ -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. 添加对游戏的增强功能,如游戏键位映射 🚧
## 常见问题

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

View File

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

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

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

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

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

View File

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

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

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

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

View File

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

View File

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

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

View File

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

View File

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

View File

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