mirror of
https://gitee.com/viarotel-org/escrcpy.git
synced 2024-12-04 20:29:35 +08:00
feat: 🚀 Add basic timing task function
This commit is contained in:
parent
8e6af2087e
commit
04a760897e
2
components.d.ts
vendored
2
components.d.ts
vendored
@ -13,6 +13,8 @@ declare module 'vue' {
|
|||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
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']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
ElDropdown: typeof import('element-plus/es')['ElDropdown']
|
||||||
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem']
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"electron-in-page-search": "^1.3.2",
|
"electron-in-page-search": "^1.3.2",
|
||||||
|
"nanoid": "^5.0.7",
|
||||||
"vue": "^3.4.26"
|
"vue": "^3.4.26"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
13
src/App.vue
13
src/App.vue
@ -1,4 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<el-config-provider :locale="locale">
|
||||||
<div class="absolute inset-0 px-4 pb-4 h-full">
|
<div class="absolute inset-0 px-4 pb-4 h-full">
|
||||||
<el-tabs
|
<el-tabs
|
||||||
v-model="activeTab"
|
v-model="activeTab"
|
||||||
@ -24,6 +25,7 @@
|
|||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</div>
|
</div>
|
||||||
|
</el-config-provider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
@ -34,9 +36,20 @@ import Preference from './components/Preference/index.vue'
|
|||||||
import About from './components/About/index.vue'
|
import About from './components/About/index.vue'
|
||||||
import AppSearch from './components/Search/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 { useThemeStore } from '$/store/theme/index.js'
|
||||||
import { usePreferenceStore } from '$/store/preference/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([
|
const tabsModel = ref([
|
||||||
{
|
{
|
||||||
label: 'device.list',
|
label: 'device.list',
|
||||||
|
@ -1,76 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="" @click="handleClick">
|
<div class="" @click="handleClick(devices)">
|
||||||
<slot v-bind="{ loading }" />
|
<slot v-bind="{ loading }" />
|
||||||
<ApplicationProxy ref="applicationProxyRef" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import ApplicationProxy from '$/components/Device/components/ControlBar/Application/index.vue'
|
import { useInstallAction } from '$/composables/useInstallAction/index.js'
|
||||||
import { allSettledWrapper } from '$/utils'
|
|
||||||
|
|
||||||
export default {
|
import { useTaskStore } from '$/store/index.js'
|
||||||
components: {
|
|
||||||
ApplicationProxy,
|
const props = defineProps({
|
||||||
},
|
|
||||||
props: {
|
|
||||||
devices: {
|
devices: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
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>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
<el-dropdown :hide-on-click="false" :disabled="loading">
|
<el-dropdown :hide-on-click="false" :disabled="loading">
|
||||||
<div class="">
|
<div class="">
|
||||||
<slot :loading="loading" />
|
<slot :loading="loading" />
|
||||||
<FileManageProxy ref="fileManageProxyRef" />
|
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@ -17,11 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ElMessage } from 'element-plus'
|
import { useFileActions } from '$/composables/useFileActions/index.js'
|
||||||
import FileManageProxy from '$/components/Device/components/ControlBar/FileManage/index.vue'
|
|
||||||
|
|
||||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
|
||||||
import { allSettledWrapper } from '$/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
devices: {
|
devices: {
|
||||||
@ -30,48 +25,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const { loading, send: handlePush } = useFileActions()
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -1,51 +1,31 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="" @click="handleClick">
|
<div class="" @click="handleClick(devices)">
|
||||||
<slot v-bind="{ loading }" />
|
<slot v-bind="{ loading }" />
|
||||||
<ScreenshotProxy ref="screenshotProxyRef" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
import ScreenshotProxy from '$/components/Device/components/ControlBar/Screenshot/index.vue'
|
import { useTaskStore } from '$/store/index.js'
|
||||||
import { allSettledWrapper, sleep } from '$/utils'
|
|
||||||
|
|
||||||
export default {
|
import { useScreenshotAction } from '$/composables/useScreenshotAction/index.js'
|
||||||
components: {
|
|
||||||
ScreenshotProxy,
|
const props = defineProps({
|
||||||
},
|
|
||||||
props: {
|
|
||||||
devices: {
|
devices: {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: () => [],
|
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>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -5,9 +5,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useShellAction } from '$/composables/useShellAction/index.js'
|
||||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
import { useTaskStore } from '$/store/index.js'
|
||||||
import { allSettledWrapper } from '$/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
devices: {
|
devices: {
|
||||||
@ -16,73 +15,16 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const { loading, invoke: handleClick } = useShellAction()
|
||||||
|
|
||||||
async function handleClick(devices) {
|
const taskStore = useTaskStore()
|
||||||
let files = null
|
|
||||||
|
|
||||||
try {
|
taskStore.on('shell', (task) => {
|
||||||
files = await window.electron.ipcRenderer.invoke('show-open-dialog', {
|
taskStore.start({
|
||||||
properties: ['openFile'],
|
task,
|
||||||
filters: [
|
handler: handleClick,
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -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>
|
@ -1,10 +1,14 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="" @click="handleClick(devices)">
|
<div class="" @click="handleClick(devices)">
|
||||||
<slot v-bind="{ loading }" />
|
<slot v-bind="{ loading }" />
|
||||||
|
|
||||||
|
<TaskDialog ref="taskDialogRef"></TaskDialog>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import TaskDialog from './components/TaskDialog/index.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
devices: {
|
devices: {
|
||||||
type: Array,
|
type: Array,
|
||||||
@ -13,6 +17,12 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
|
|
||||||
|
const taskDialogRef = ref(null)
|
||||||
|
|
||||||
|
function handleClick(devices) {
|
||||||
|
taskDialogRef.value.open({ devices })
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dropdown :hide-on-click="false">
|
<el-dropdown :hide-on-click="false" :disabled="loading">
|
||||||
<div class="">
|
<div class="">
|
||||||
<slot v-bind="{ loading }" />
|
<slot :loading="loading" />
|
||||||
</div>
|
</div>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<el-dropdown-menu>
|
<el-dropdown-menu>
|
||||||
@ -16,10 +16,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ElMessage } from 'element-plus'
|
import { useFileActions } from '$/composables/useFileActions/index.js'
|
||||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
|
||||||
import { useDeviceStore } from '$/store'
|
|
||||||
import { allSettledWrapper } from '$/utils'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
device: {
|
device: {
|
||||||
@ -28,94 +25,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const deviceStore = useDeviceStore()
|
const { loading, send: handlePush } = useFileActions()
|
||||||
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -1,69 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="" @click="handleCapture(device)">
|
<div class="" @click="handleClick(device)">
|
||||||
<slot />
|
<slot v-bind="{ loading }" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script setup>
|
||||||
export default {
|
import { useScreenshotAction } from '$/composables/useScreenshotAction/index.js'
|
||||||
props: {
|
|
||||||
|
const props = defineProps({
|
||||||
device: {
|
device: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => ({}),
|
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(
|
const { loading, invoke: handleClick } = useScreenshotAction()
|
||||||
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}`,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ElMessage } from 'element-plus'
|
import { useShellAction } from '$/composables/useShellAction/index.js'
|
||||||
import { selectAndSendFileToDevice } from '$/utils/device/index.js'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
device: {
|
device: {
|
||||||
@ -15,37 +14,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const loading = ref(false)
|
const { loading, invoke: handleClick } = useShellAction()
|
||||||
|
|
||||||
const invokeTerminal = inject('invokeTerminal')
|
|
||||||
|
|
||||||
async function handleClick(device) {
|
|
||||||
let files = null
|
|
||||||
|
|
||||||
loading.value = true
|
|
||||||
|
|
||||||
try {
|
|
||||||
files = await selectAndSendFileToDevice(device.id, {
|
|
||||||
extensions: ['sh'],
|
|
||||||
selectText: window.t('device.control.shell.select'),
|
|
||||||
loadingText: window.t('device.control.shell.push.loading'),
|
|
||||||
successText: window.t('device.control.shell.push.success'),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
loading.value = false
|
|
||||||
ElMessage.warning(error.message)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const filePath = files[0]
|
|
||||||
|
|
||||||
const command = `adb -s ${device.id} shell sh ${filePath}`
|
|
||||||
|
|
||||||
invokeTerminal(command)
|
|
||||||
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style></style>
|
<style></style>
|
||||||
|
@ -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>
|
|
@ -7,12 +7,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import TaskDialog from './components/TaskDialog/index.vue'
|
import TaskDialog from '$/components/Device/components/BatchActions/Tasks/components/TaskDialog/index.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
device: {
|
device: {
|
||||||
type: Array,
|
type: Object,
|
||||||
default: () => [],
|
default: null,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -21,7 +21,7 @@ const loading = ref(false)
|
|||||||
const taskDialogRef = ref(null)
|
const taskDialogRef = ref(null)
|
||||||
|
|
||||||
function handleClick(device) {
|
function handleClick(device) {
|
||||||
taskDialogRef.value.open(device)
|
taskDialogRef.value.open({ devices: [device] })
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -48,9 +48,10 @@ import { useAdb } from './composables/adb-async.js'
|
|||||||
import { useScrcpy } from './composables/scrcpy.js'
|
import { useScrcpy } from './composables/scrcpy.js'
|
||||||
import { useGnirehtet } from './composables/gnirehtet.js'
|
import { useGnirehtet } from './composables/gnirehtet.js'
|
||||||
import { sleep } from '$/utils/index.js'
|
import { sleep } from '$/utils/index.js'
|
||||||
import { useThemeStore } from '$/store/index.js'
|
import { useTaskStore, useThemeStore } from '$/store/index.js'
|
||||||
|
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
|
const taskStore = useTaskStore()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
@ -166,6 +167,10 @@ function onClosed() {
|
|||||||
history.value = [createQuery()]
|
history.value = [createQuery()]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
taskStore.on('terminal', (task) => {
|
||||||
|
invoke(task.command)
|
||||||
|
})
|
||||||
|
|
||||||
defineExpose({
|
defineExpose({
|
||||||
open,
|
open,
|
||||||
close,
|
close,
|
||||||
|
@ -150,11 +150,6 @@ export default {
|
|||||||
WirelessAction,
|
WirelessAction,
|
||||||
BatchActions,
|
BatchActions,
|
||||||
},
|
},
|
||||||
provide() {
|
|
||||||
return {
|
|
||||||
invokeTerminal: (...args) => this.$refs.terminalActionRef.invoke(...args),
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -70,7 +70,7 @@ export default {
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
const value = files[0]
|
const value = files.join(',')
|
||||||
|
|
||||||
this.pathValue = value
|
this.pathValue = value
|
||||||
}
|
}
|
||||||
|
149
src/composables/useFileActions/index.js
Normal file
149
src/composables/useFileActions/index.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
150
src/composables/useInstallAction/index.js
Normal file
150
src/composables/useInstallAction/index.js
Normal 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
|
90
src/composables/useScreenshotAction/index.js
Normal file
90
src/composables/useScreenshotAction/index.js
Normal 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
|
124
src/composables/useShellAction/index.js
Normal file
124
src/composables/useShellAction/index.js
Normal 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
1
src/dicts/index.js
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './tasks/index'
|
41
src/dicts/tasks/index.js
Normal file
41
src/dicts/tasks/index.js
Normal 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',
|
||||||
|
},
|
||||||
|
]
|
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-form v-bind="{ ...$props }">
|
<el-form ref="formRef" v-bind="{ ...$props }">
|
||||||
<el-row v-bind="{ ...$attrs, size: $props.size }">
|
<el-row v-bind="{ ...$attrs, size: $props.size }">
|
||||||
<slot />
|
<slot />
|
||||||
</el-row>
|
</el-row>
|
||||||
@ -8,6 +8,7 @@
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ElForm } from 'element-plus'
|
import { ElForm } from 'element-plus'
|
||||||
|
import { inheritComponentMethods } from '$/utils/index.js'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ElFormRow',
|
name: 'ElFormRow',
|
||||||
@ -15,6 +16,16 @@ export default {
|
|||||||
props: {
|
props: {
|
||||||
...ElForm.props,
|
...ElForm.props,
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
...inheritComponentMethods('formRef', [
|
||||||
|
'validate',
|
||||||
|
'validateField',
|
||||||
|
'resetFields',
|
||||||
|
'scrollToField',
|
||||||
|
'clearValidate',
|
||||||
|
'fields',
|
||||||
|
]),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
9
src/plugins/element-plus/locale.js
Normal file
9
src/plugins/element-plus/locale.js
Normal 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,
|
||||||
|
}
|
@ -2,8 +2,9 @@ import { createPinia } from 'pinia'
|
|||||||
import { useDeviceStore } from './device/index.js'
|
import { useDeviceStore } from './device/index.js'
|
||||||
import { usePreferenceStore } from './preference/index.js'
|
import { usePreferenceStore } from './preference/index.js'
|
||||||
import { useThemeStore } from './theme/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 {
|
export default {
|
||||||
install(app) {
|
install(app) {
|
||||||
|
109
src/store/task/index.js
Normal file
109
src/store/task/index.js
Normal 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 }
|
||||||
|
})
|
@ -1,4 +1,4 @@
|
|||||||
import { cloneDeep, keyBy } from 'lodash-es'
|
import { camelCase, cloneDeep, keyBy } from 'lodash-es'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @desc 使用async await 进项进行延时操作
|
* @desc 使用async await 进项进行延时操作
|
||||||
@ -89,3 +89,37 @@ export function allSettledWrapper(list = [], iterator) {
|
|||||||
|
|
||||||
return Promise.allSettled(promises)
|
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)
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user