feat: 🚀 Add basic timing task function

This commit is contained in:
viarotel 2024-07-23 19:03:35 +08:00
parent 8e6af2087e
commit 04a760897e
28 changed files with 1107 additions and 538 deletions

2
components.d.ts vendored
View File

@ -13,6 +13,8 @@ declare module 'vue' {
ElCol: typeof import('element-plus/es')['ElCol']
ElCollapse: typeof import('element-plus/es')['ElCollapse']
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElDropdown: typeof import('element-plus/es')['ElDropdown']
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']

View File

@ -23,6 +23,7 @@
},
"dependencies": {
"electron-in-page-search": "^1.3.2",
"nanoid": "^5.0.7",
"vue": "^3.4.26"
},
"devDependencies": {

View File

@ -1,4 +1,5 @@
<template>
<el-config-provider :locale="locale">
<div class="absolute inset-0 px-4 pb-4 h-full">
<el-tabs
v-model="activeTab"
@ -24,6 +25,7 @@
</el-tab-pane>
</el-tabs>
</div>
</el-config-provider>
</template>
<script setup>
@ -34,9 +36,20 @@ import Preference from './components/Preference/index.vue'
import About from './components/About/index.vue'
import AppSearch from './components/Search/index.vue'
import { i18n } from '$/locales/index.js'
import localeModel from '$/plugins/element-plus/locale.js'
import { useThemeStore } from '$/store/theme/index.js'
import { usePreferenceStore } from '$/store/preference/index.js'
const locale = computed(() => {
const i18nLocale = i18n.global.locale.value
const value = localeModel[i18nLocale]
return value
})
const tabsModel = ref([
{
label: 'device.list',

View File

@ -1,76 +1,31 @@
<template>
<div class="" @click="handleClick">
<div class="" @click="handleClick(devices)">
<slot v-bind="{ loading }" />
<ApplicationProxy ref="applicationProxyRef" />
</div>
</template>
<script>
import ApplicationProxy from '$/components/Device/components/ControlBar/Application/index.vue'
import { allSettledWrapper } from '$/utils'
<script setup>
import { useInstallAction } from '$/composables/useInstallAction/index.js'
export default {
components: {
ApplicationProxy,
},
props: {
import { useTaskStore } from '$/store/index.js'
const props = defineProps({
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
const closeMessage = this.$message.loading(
this.$t('device.control.install.progress', {
deviceName: window.t('common.device'),
}),
).close
await allSettledWrapper(this.devices, (item) => {
return this.$refs.applicationProxyRef.invoke(item, {
files,
silent: true,
})
})
closeMessage()
const { loading, invoke: handleClick } = useInstallAction()
ElMessage.success(window.t('common.success.batch'))
const taskStore = useTaskStore()
this.loading = false
},
},
}
taskStore.on('install', (task) => {
taskStore.start({
task,
handler: handleClick,
})
})
</script>
<style></style>

View File

@ -2,7 +2,6 @@
<el-dropdown :hide-on-click="false" :disabled="loading">
<div class="">
<slot :loading="loading" />
<FileManageProxy ref="fileManageProxyRef" />
</div>
<template #dropdown>
<el-dropdown-menu>
@ -17,11 +16,7 @@
</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 { allSettledWrapper } from '$/utils'
import { useFileActions } from '$/composables/useFileActions/index.js'
const props = defineProps({
devices: {
@ -30,48 +25,7 @@ const props = defineProps({
},
})
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
const closeMessage = ElMessage.loading(
window.t('device.control.file.push.loading'),
).close
await allSettledWrapper(devices, (item) => {
return fileManageProxyRef.value.handlePush(item, { files, silent: true })
})
closeMessage()
ElMessage.success(window.t('common.success.batch'))
loading.value = false
}
const { loading, send: handlePush } = useFileActions()
</script>
<style></style>

View File

@ -1,51 +1,31 @@
<template>
<div class="" @click="handleClick">
<div class="" @click="handleClick(devices)">
<slot v-bind="{ loading }" />
<ScreenshotProxy ref="screenshotProxyRef" />
</div>
</template>
<script>
import ScreenshotProxy from '$/components/Device/components/ControlBar/Screenshot/index.vue'
import { allSettledWrapper, sleep } from '$/utils'
<script setup>
import { useTaskStore } from '$/store/index.js'
export default {
components: {
ScreenshotProxy,
},
props: {
import { useScreenshotAction } from '$/composables/useScreenshotAction/index.js'
const props = defineProps({
devices: {
type: Array,
default: () => [],
},
},
data() {
return {
loading: false,
}
},
methods: {
async handleClick() {
this.loading = true
const closeMessage = this.$message.loading(
window.t('device.control.capture.progress', {
deviceName: window.t('common.device'),
}),
).close
await allSettledWrapper(this.devices, (item) => {
return this.$refs.screenshotProxyRef.invoke(item, { silent: true })
})
closeMessage()
const { loading, invoke: handleClick } = useScreenshotAction()
ElMessage.success(window.t('common.success.batch'))
const taskStore = useTaskStore()
this.loading = false
},
},
}
taskStore.on('screenshot', (task) => {
taskStore.start({
task,
handler: handleClick,
})
})
</script>
<style></style>

View File

@ -5,9 +5,8 @@
</template>
<script setup>
import { ElMessage, ElMessageBox } from 'element-plus'
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
import { allSettledWrapper } from '$/utils'
import { useShellAction } from '$/composables/useShellAction/index.js'
import { useTaskStore } from '$/store/index.js'
const props = defineProps({
devices: {
@ -16,73 +15,16 @@ const props = defineProps({
},
})
const loading = ref(false)
const { loading, invoke: handleClick } = useShellAction()
async function handleClick(devices) {
let files = null
const taskStore = useTaskStore()
try {
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
properties: ['openFile'],
filters: [
{
name: window.t('device.control.shell.select'),
extensions: ['sh'],
},
],
taskStore.on('shell', (task) => {
taskStore.start({
task,
handler: handleClick,
})
}
catch (error) {
if (error.message) {
const message = error.message?.match(/Error: (.*)/)?.[1]
ElMessage.warning(message || error.message)
}
return false
}
loading.value = true
const closeLoading = ElMessage.loading(
window.t('device.control.shell.push.loading'),
).close
const failFiles = []
await allSettledWrapper(devices, async (device) => {
const successFiles = await selectAndSendFileToDevice(device.id, {
files,
silent: true,
}).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
}
closeLoading()
await ElMessage.success(window.t('device.control.shell.success'))
loading.value = false
}
</script>
<style></style>

View File

@ -0,0 +1,260 @@
<template>
<el-dialog
v-model="visible"
title="定时任务"
width="60%"
class="el-dialog-beautify"
append-to-body
destroy-on-close
@closed="onClosed"
>
<ele-form-row
ref="formRef"
:model="model"
:rules="rules"
label-width="120px"
class="!pr-[120px] !pt-4"
>
<ele-form-item-col label="任务类型" :span="24" prop="taskType">
<el-select
v-model="model.taskType"
placeholder="请选择任务类型"
clearable
filterable
@change="onTaskChange"
>
<el-option
v-for="item in taskModel"
:key="item.value"
:label="$t(item.label)"
:value="item.value"
>
</el-option>
</el-select>
</ele-form-item-col>
<ele-form-item-col label="执行频率" :span="24" prop="timerType">
<el-radio-group v-model="model.timerType">
<el-radio
v-for="(item, index) of timerModel"
:key="index"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</ele-form-item-col>
<ele-form-item-col
v-if="['timeout'].includes(model.timerType)"
label="执行时间"
:span="24"
prop="timeout"
>
<el-date-picker
v-model="model.timeout"
type="datetime"
placeholder="0000-00-00 00:00:00"
clearable
v-bind="{ disabledDate, defaultTime }"
></el-date-picker>
</ele-form-item-col>
<ele-form-item-col
v-if="['interval'].includes(model.timerType)"
label="重复规则"
:span="24"
prop="interval"
>
<el-input
v-model="model.interval"
type="number"
placeholder="0"
clearable
>
<template #append>
<el-select
v-model="model.intervalType"
placeholder="请选择时间单位"
filterable
class="!w-24"
>
<el-option
v-for="(item, index) of intervalModel"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</template>
</el-input>
</ele-form-item-col>
<ele-form-item-col
v-if="['install'].includes(model.taskType)"
label="选择应用"
:span="24"
prop="extra"
>
<InputPath
v-model="model.extra"
:placeholder="$t('device.control.install.placeholder')"
:data="{
properties: ['openFile', 'multiSelections'],
filters: [
{
name: $t('device.control.install.placeholder'),
extensions: ['apk'],
},
],
}"
/>
</ele-form-item-col>
<ele-form-item-col
v-if="['shell'].includes(model.taskType)"
label="选择脚本"
:span="24"
prop="extra"
>
<InputPath
v-model="model.extra"
:placeholder="$t('device.control.shell.select')"
:data="{
properties: ['openFile'],
filters: [
{
name: $t('device.control.shell.select'),
extensions: ['sh'],
},
],
}"
/>
</ele-form-item-col>
</ele-form-row>
<template #footer>
<el-button @click="close">
取消
</el-button>
<el-button :loading type="primary" @click="submit">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ElMessage } from 'element-plus'
import {
timeUnit as intervalModel,
timerType as timerModel,
} from '$/dicts/index.js'
import { useTaskStore } from '$/store/index.js'
import { sleep } from '$/utils'
import InputPath from '$/components/Preference/components/PreferenceForm/components/InputPath/index.vue'
const taskStore = useTaskStore()
const visible = ref(false)
const loading = ref(false)
const model = ref({
taskType: void 0,
timerType: 'timeout',
timeout: void 0,
interval: void 0,
intervalType: 'second',
extra: void 0,
})
const rules = computed(() =>
Object.keys(model.value).reduce((obj, item) => {
obj[item] = [{ required: true, message: '该选项不能为空', trigger: 'blur' }]
if (item === 'timeout') {
obj[item].push({
trigger: 'blur',
validator: (rule, value, callback) => {
if (value.getTime() <= Date.now()) {
callback(new Error('不能小于当前时间'))
}
else {
callback()
}
},
})
}
return obj
}, {}),
)
const formRef = ref(null)
const taskModel = computed(() => taskStore.model)
const devices = ref(null)
const defaultTime = ref(null)
function open(args) {
visible.value = true
if (args.devices) {
devices.value = args.devices
}
else if (args.device) {
devices.value = [args.device]
}
defaultTime.value = new Date()
}
function close() {
visible.value = false
}
async function submit() {
try {
await formRef.value.validate()
}
catch (error) {
return error.message
}
loading.value = true
await taskStore.add({ ...model.value, devices: devices.value })
await sleep()
await ElMessage.success(window.t('common.success'))
loading.value = false
close()
}
async function onClosed() {
devices.value = null
formRef.value.resetFields()
formRef.value.clearValidate()
}
function disabledDate(time) {
return time.getTime() < Date.now() - 24 * 60 * 60 * 1000
}
function onTaskChange() {
model.value.extra = void 0
}
defineExpose({
open,
close,
})
</script>
<style></style>

View File

@ -1,10 +1,14 @@
<template>
<div class="" @click="handleClick(devices)">
<slot v-bind="{ loading }" />
<TaskDialog ref="taskDialogRef"></TaskDialog>
</div>
</template>
<script setup>
import TaskDialog from './components/TaskDialog/index.vue'
const props = defineProps({
devices: {
type: Array,
@ -13,6 +17,12 @@ const props = defineProps({
})
const loading = ref(false)
const taskDialogRef = ref(null)
function handleClick(devices) {
taskDialogRef.value.open({ devices })
}
</script>
<style></style>

View File

@ -1,7 +1,7 @@
<template>
<el-dropdown :hide-on-click="false">
<el-dropdown :hide-on-click="false" :disabled="loading">
<div class="">
<slot v-bind="{ loading }" />
<slot :loading="loading" />
</div>
<template #dropdown>
<el-dropdown-menu>
@ -16,10 +16,7 @@
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
import { useDeviceStore } from '$/store'
import { allSettledWrapper } from '$/utils'
import { useFileActions } from '$/composables/useFileActions/index.js'
const props = defineProps({
device: {
@ -28,94 +25,7 @@ const props = defineProps({
},
})
const deviceStore = useDeviceStore()
const loading = ref(false)
async function handlePush(device, { files, silent = false } = {}) {
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: ['*'],
},
],
})
}
catch (error) {
if (error.message) {
const message
= error.message?.match(/Error: (.*)/)?.[1] || error.message
ElMessage.warning(message)
}
return false
}
}
loading.value = true
let closeLoading
if (!silent) {
closeLoading = ElMessage.loading(
`${deviceStore.getLabel(device)}: ${window.t(
'device.control.file.push.loading',
)}`,
).close
}
let failCount = 0
await allSettledWrapper(files, (item) => {
return window.adbkit.push(device.id, item).catch(() => {
++failCount
})
})
loading.value = false
if (silent) {
return false
}
const totalCount = files.length
const successCount = totalCount - failCount
if (successCount) {
closeLoading()
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
}
closeLoading()
ElMessage.warning(window.t('device.control.file.push.error'))
}
defineExpose({
handlePush,
})
const { loading, send: handlePush } = useFileActions()
</script>
<style></style>

View File

@ -1,69 +1,20 @@
<template>
<div class="" @click="handleCapture(device)">
<slot />
<div class="" @click="handleClick(device)">
<slot v-bind="{ loading }" />
</div>
</template>
<script>
export default {
props: {
<script setup>
import { useScreenshotAction } from '$/composables/useScreenshotAction/index.js'
const props = defineProps({
device: {
type: Object,
default: () => ({}),
},
},
data() {
return {}
},
methods: {
invoke(...args) {
return this.handleCapture(...args)
},
preferenceData(...args) {
return this.$store.preference.getData(...args)
},
async handleCapture(device, { silent = false } = {}) {
let closeLoading
if (!silent) {
closeLoading = this.$message.loading(
this.$t('device.control.capture.progress', {
deviceName: this.$store.device.getLabel(device),
}),
).close
}
})
const fileName = this.$store.device.getLabel(
device,
({ time }) => `screenshot-${time}.jpg`,
)
const deviceConfig = this.preferenceData(device.id)
const savePath = this.$path.resolve(deviceConfig.savePath, fileName)
try {
await this.$adb.screencap(device.id, { savePath })
}
catch (error) {
if (error.message) {
this.$message.warning(error.message)
}
return false
}
if (silent) {
return false
}
closeLoading()
this.$message.success(
`${this.$t(
'device.control.capture.success.message.title',
)}: ${savePath}`,
)
},
},
}
const { loading, invoke: handleClick } = useScreenshotAction()
</script>
<style></style>

View File

@ -5,8 +5,7 @@
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
import { useShellAction } from '$/composables/useShellAction/index.js'
const props = defineProps({
device: {
@ -15,37 +14,7 @@ const props = defineProps({
},
})
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
}
const { loading, invoke: handleClick } = useShellAction()
</script>
<style></style>

View File

@ -1,97 +0,0 @@
<template>
<el-dialog
v-model="visible"
title="定时任务"
width="60%"
class="el-dialog-beautify"
append-to-body
@closed="onClosed"
>
<ele-form-row :model="model" label-width="120px" class="!pr-[120px] !pt-4">
<ele-form-item-col label="任务类型" :span="24">
<el-select v-model="model.taskType" placeholder="请选择任务类型">
<el-option
v-for="item in taskModel"
:key="item.value"
:label="$t(item.label)"
:value="item.value"
>
</el-option>
</el-select>
</ele-form-item-col>
<ele-form-item-col label="定时器类型" :span="24">
<el-radio-group v-model="model.timerType">
<el-radio
v-for="(item, index) of timerModel"
:key="index"
:value="item.value"
>
{{ item.label }}
</el-radio>
</el-radio-group>
</ele-form-item-col>
</ele-form-row>
<template #footer>
<el-button @click="close">
取消
</el-button>
<el-button type="primary" @click="submit">
确定
</el-button>
</template>
</el-dialog>
</template>
<script setup>
const visible = ref(false)
const model = ref({
taskType: '',
timerType: 'timeout',
})
const taskModel = [
{
label: 'device.control.install',
value: 'install',
},
{
label: 'device.control.capture',
value: 'screenshot',
},
{
label: 'device.control.shell.name',
value: 'shell',
},
]
const timerModel = [
{
label: '单次',
value: 'timeout',
},
{
label: '周期',
value: 'interval',
},
]
function open() {
visible.value = true
}
function close() {
visible.value = false
}
function submit() {}
function onClosed() {}
defineExpose({
open,
close,
})
</script>
<style></style>

View File

@ -7,12 +7,12 @@
</template>
<script setup>
import TaskDialog from './components/TaskDialog/index.vue'
import TaskDialog from '$/components/Device/components/BatchActions/Tasks/components/TaskDialog/index.vue'
const props = defineProps({
device: {
type: Array,
default: () => [],
type: Object,
default: null,
},
})
@ -21,7 +21,7 @@ const loading = ref(false)
const taskDialogRef = ref(null)
function handleClick(device) {
taskDialogRef.value.open(device)
taskDialogRef.value.open({ devices: [device] })
}
</script>

View File

@ -48,9 +48,10 @@ 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'
import { useTaskStore, useThemeStore } from '$/store/index.js'
const themeStore = useThemeStore()
const taskStore = useTaskStore()
const loading = ref(false)
const visible = ref(false)
@ -166,6 +167,10 @@ function onClosed() {
history.value = [createQuery()]
}
taskStore.on('terminal', (task) => {
invoke(task.command)
})
defineExpose({
open,
close,

View File

@ -150,11 +150,6 @@ export default {
WirelessAction,
BatchActions,
},
provide() {
return {
invokeTerminal: (...args) => this.$refs.terminalActionRef.invoke(...args),
}
},
data() {
return {
loading: false,

View File

@ -70,7 +70,7 @@ export default {
},
)
const value = files[0]
const value = files.join(',')
this.pathValue = value
}

View File

@ -0,0 +1,149 @@
import { ElMessage } from 'element-plus'
import { allSettledWrapper } from '$/utils'
import { useDeviceStore } from '$/store'
export function useFileActions() {
const deviceStore = useDeviceStore()
const loading = ref(false)
function send(...args) {
const [devices] = args
if (Array.isArray(devices)) {
return multipleSend(...args)
}
return singleSend(...args)
}
async function selectFiles() {
try {
const files = await window.electron.ipcRenderer.invoke(
'show-open-dialog',
{
properties: ['openFile', 'multiSelections'],
filters: [
{
name: window.t('device.control.file.push.placeholder'),
extensions: ['*'],
},
],
},
)
return files
}
catch (error) {
const message = error.message?.match(/Error: (.*)/)?.[1] || error.message
throw new Error(message)
}
}
async function singleSend(device, { files, silent = false } = {}) {
if (!files) {
try {
files = await selectFiles()
}
catch (error) {
ElMessage.warning(error.message)
return false
}
}
loading.value = true
let closeLoading
if (!silent) {
closeLoading = ElMessage.loading(
`${deviceStore.getLabel(device)}: ${window.t(
'device.control.file.push.loading',
)}`,
).close
}
let failCount = 0
await allSettledWrapper(files, (item) => {
return window.adbkit.push(device.id, item).catch(() => {
++failCount
})
})
loading.value = false
if (silent) {
return false
}
const totalCount = files.length
const successCount = totalCount - failCount
if (successCount) {
closeLoading()
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
}
closeLoading()
ElMessage.warning(window.t('device.control.file.push.error'))
}
async function multipleSend(devices, { files } = {}) {
if (!files) {
try {
files = await selectFiles()
}
catch (error) {
ElMessage.warning(error.message)
return false
}
}
loading.value = true
const closeMessage = ElMessage.loading(
window.t('device.control.file.push.loading'),
).close
await allSettledWrapper(devices, (item) => {
return singleSend(item, { files, silent: true })
})
closeMessage()
ElMessage.success(window.t('common.success.batch'))
loading.value = false
}
return {
loading,
send,
selectFiles,
singleSend,
multipleSend,
}
}

View File

@ -0,0 +1,150 @@
import { ElMessage } from 'element-plus'
import { allSettledWrapper } from '$/utils'
import { useDeviceStore } from '$/store'
export function useInstallAction() {
const deviceStore = useDeviceStore()
const loading = ref(false)
function invoke(...args) {
const [devices] = args
if (Array.isArray(devices)) {
return multipleInvoke(...args)
}
return singleInvoke(...args)
}
async function selectFiles() {
try {
const files = await window.electron.ipcRenderer.invoke(
'show-open-dialog',
{
properties: ['openFile', 'multiSelections'],
filters: [
{
name: window.t('device.control.install.placeholder'),
extensions: ['apk'],
},
],
},
)
return files
}
catch (error) {
const message = error.message?.match(/Error: (.*)/)?.[1] || error.message
throw new Error(message)
}
}
async function singleInvoke(device, { files, silent = false } = {}) {
if (!files) {
try {
files = await selectFiles()
}
catch (error) {
ElMessage.warning(error.message)
return false
}
}
let closeLoading = null
if (!silent) {
closeLoading = ElMessage.loading(
window.t('device.control.install.progress', {
deviceName: deviceStore.getLabel(device),
}),
).close
}
let failCount = 0
await allSettledWrapper(files, (item) => {
return window.adbkit.install(device.id, item).catch((e) => {
console.warn(e)
++failCount
})
})
if (silent) {
return false
}
closeLoading()
const totalCount = files.length
const successCount = totalCount - failCount
if (successCount) {
if (totalCount > 1) {
ElMessage.success(
window.t('device.control.install.success', {
deviceName: deviceStore.getLabel(device),
totalCount,
successCount,
failCount,
}),
)
}
else {
ElMessage.success(
window.t('device.control.install.success.single', {
deviceName: deviceStore.getLabel(device),
}),
)
}
return false
}
ElMessage.warning(window.t('device.control.install.error'))
}
async function multipleInvoke(devices, { files } = {}) {
if (!files) {
try {
files = await selectFiles()
}
catch (error) {
ElMessage.warning(error.message)
return false
}
}
loading.value = true
const closeMessage = ElMessage.loading(
window.t('device.control.install.progress', {
deviceName: window.t('common.device'),
}),
).close
await allSettledWrapper(devices, (item) => {
return singleInvoke(item, {
files,
silent: true,
})
})
closeMessage()
ElMessage.success(window.t('common.success.batch'))
loading.value = false
}
return {
invoke,
loading,
deviceStore,
selectFiles,
multipleInvoke,
singleInvoke,
}
}
export default useInstallAction

View File

@ -0,0 +1,90 @@
import { ElMessage } from 'element-plus'
import { allSettledWrapper } from '$/utils/index.js'
import { useDeviceStore, usePreferenceStore } from '$/store'
export function useScreenshotAction() {
const deviceStore = useDeviceStore()
const preferenceStore = usePreferenceStore()
const loading = ref(false)
function invoke(...args) {
const [devices] = args
if (Array.isArray(devices)) {
return multipleInvoke(...args)
}
return singleInvoke(...args)
}
async function singleInvoke(device, { silent = false } = {}) {
let closeLoading
if (!silent) {
closeLoading = ElMessage.loading(
window.t('device.control.capture.progress', {
deviceName: deviceStore.getLabel(device),
}),
).close
}
const fileName = deviceStore.getLabel(
device,
({ time }) => `screenshot-${time}.jpg`,
)
const deviceConfig = preferenceStore.getData(device.id)
const savePath = window.nodePath.resolve(deviceConfig.savePath, fileName)
try {
await window.adbkit.screencap(device.id, { savePath })
}
catch (error) {
if (error.message) {
ElMessage.warning(error.message)
}
return false
}
if (silent) {
return false
}
closeLoading()
ElMessage.success(
`${window.t('device.control.capture.success.message.title')}: ${savePath}`,
)
}
async function multipleInvoke(devices) {
loading.value = true
const closeMessage = ElMessage.loading(
window.t('device.control.capture.progress', {
deviceName: window.t('common.device'),
}),
).close
await allSettledWrapper(devices, (item) => {
return singleInvoke(item, { silent: true })
})
closeMessage()
ElMessage.success(window.t('common.success.batch'))
loading.value = false
}
return {
invoke,
loading,
singleInvoke,
multipleInvoke,
}
}
export default useScreenshotAction

View File

@ -0,0 +1,124 @@
import { ElMessage, ElMessageBox } from 'element-plus'
import { allSettledWrapper } from '$/utils/index.js'
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
import { useTaskStore } from '$/store'
export function useShellAction() {
const taskStore = useTaskStore()
const loading = ref(false)
async function invoke(...args) {
const [devices] = args
if (Array.isArray(devices)) {
return multipleInvoke(...args)
}
return singleInvoke(...args)
}
async function singleInvoke(device, { files } = {}) {
if (!files) {
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
}
}
loading.value = true
const filePath = files[0]
const command = `adb -s ${device.id} shell sh ${filePath}`
taskStore.emit('terminal', { command })
loading.value = false
}
async function multipleInvoke(devices, { files } = {}) {
if (!files) {
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 closeLoading = ElMessage.loading(
window.t('device.control.shell.push.loading'),
).close
const failFiles = []
await allSettledWrapper(devices, async (device) => {
const successFiles = await selectAndSendFileToDevice(device.id, {
files,
silent: true,
}).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
}
closeLoading()
await ElMessage.success(window.t('device.control.shell.success'))
loading.value = false
}
return {
invoke,
loading,
singleInvoke,
multipleInvoke,
}
}
export default useShellAction

1
src/dicts/index.js Normal file
View File

@ -0,0 +1 @@
export * from './tasks/index'

41
src/dicts/tasks/index.js Normal file
View File

@ -0,0 +1,41 @@
export const timerType = [
{
label: '单次执行',
value: 'timeout',
},
{
label: '周期重复',
value: 'interval',
},
]
export const timeUnit = [
{
label: '月',
value: 'month',
},
{
label: '周',
value: 'week',
},
{
label: '天',
value: 'day',
},
{
label: '小时',
value: 'hour',
},
{
label: '分钟',
value: 'minute',
},
{
label: '秒',
value: 'second',
},
{
label: '毫秒',
value: 'millisecond',
},
]

View File

@ -1,5 +1,5 @@
<template>
<el-form v-bind="{ ...$props }">
<el-form ref="formRef" v-bind="{ ...$props }">
<el-row v-bind="{ ...$attrs, size: $props.size }">
<slot />
</el-row>
@ -8,6 +8,7 @@
<script>
import { ElForm } from 'element-plus'
import { inheritComponentMethods } from '$/utils/index.js'
export default {
name: 'ElFormRow',
@ -15,6 +16,16 @@ export default {
props: {
...ElForm.props,
},
methods: {
...inheritComponentMethods('formRef', [
'validate',
'validateField',
'resetFields',
'scrollToField',
'clearValidate',
'fields',
]),
},
}
</script>

View File

@ -0,0 +1,9 @@
import enUs from 'element-plus/es/locale/lang/en'
import zhCn from 'element-plus/es/locale/lang/zh-cn'
import zhTw from 'element-plus/es/locale/lang/zh-tw'
export default {
'en-Us': enUs,
'zh-CN': zhCn,
'zh-TW': zhTw,
}

View File

@ -2,8 +2,9 @@ import { createPinia } from 'pinia'
import { useDeviceStore } from './device/index.js'
import { usePreferenceStore } from './preference/index.js'
import { useThemeStore } from './theme/index.js'
import { useTaskStore } from './task/index.js'
export { useDeviceStore, usePreferenceStore, useThemeStore }
export { useDeviceStore, usePreferenceStore, useThemeStore, useTaskStore }
export default {
install(app) {

109
src/store/task/index.js Normal file
View File

@ -0,0 +1,109 @@
import { defineStore } from 'pinia'
import dayjs from 'dayjs'
import duration from 'dayjs/plugin/duration'
import { nanoid } from 'nanoid'
import { useEventBus } from '@vueuse/core'
import { clearTimer, isIPWithPort, replaceIP, setTimer } from '$/utils/index.js'
dayjs.extend(duration)
export const useTaskStore = defineStore('app-task', () => {
const event = useEventBus('app-task')
const model = ref([
{
label: 'device.control.install',
value: 'install',
},
{
label: 'device.control.capture',
value: 'screenshot',
},
{
label: 'device.control.shell.name',
value: 'shell',
},
])
const list = ref([])
function add(form) {
const task = {
...form,
timerId: void 0,
id: nanoid(),
}
event.emit(task)
list.value.push(task)
}
function getTimeout(task) {
let value = 0
const { timerType } = task
if (timerType === 'timeout') {
value = dayjs(task.timeout).diff(dayjs())
}
else if (timerType === 'interval') {
value = dayjs.duration(task.interval, task.intervalType).asMilliseconds()
}
return value
}
function start({ task, handler }) {
const { timerType, devices } = task
const files = task.extra ? task.extra.split(',') : void 0
const timeout = getTimeout(task)
task.timerId = setTimer(
timerType,
() => {
handler(devices, { files })
if (['timeout'].includes(timerType)) {
clear(task)
}
},
timeout,
)
}
function clear(task) {
const { timerType, timerId } = task
if (timerId) {
clearTimer(timerType, timerId)
list.value = list.value.filter(item => item.id !== task.id)
}
}
function on(name, callback) {
event.on((...args) => {
const [{ taskType }] = args
if (taskType !== name) {
return false
}
callback(...args)
})
return event
}
function emit(name, args) {
event.emit({ taskType: name, ...args })
return event
}
return { event, on, emit, list, model, add, start, clear }
})

View File

@ -1,4 +1,4 @@
import { cloneDeep, keyBy } from 'lodash-es'
import { camelCase, cloneDeep, keyBy } from 'lodash-es'
/**
* @desc 使用async await 进项进行延时操作
@ -89,3 +89,37 @@ export function allSettledWrapper(list = [], iterator) {
return Promise.allSettled(promises)
}
/**
* @description 继承组件方法
* @param {*} refName ref名称
* @param {*} methodNames 需要继承的方法名列表
* @returns
*/
export function inheritComponentMethods(refName, methodNames) {
const methods = {}
methodNames.forEach((name) => {
methods[name] = function (...params) {
return this.$refs[refName][name](...params)
}
})
return methods
}
/**
* 通用定时器
* @param {string} type
*/
export function setTimer(type, ...args) {
const method = camelCase(`set-${type}`)
return globalThis[method](...args)
}
/**
* 通用清除定时器
* @param {string} type
*/
export function clearTimer(type, ...args) {
const method = camelCase(`clear-${type}`)
return globalThis[method](...args)
}