-
+
+
-
diff --git a/src/components/Device/components/ControlBar/Shell/index.vue b/src/components/Device/components/ControlBar/Shell/index.vue
index 646ad01..f442708 100644
--- a/src/components/Device/components/ControlBar/Shell/index.vue
+++ b/src/components/Device/components/ControlBar/Shell/index.vue
@@ -5,8 +5,7 @@
diff --git a/src/components/Device/components/ControlBar/Tasks/components/TaskDialog/index.vue b/src/components/Device/components/ControlBar/Tasks/components/TaskDialog/index.vue
deleted file mode 100644
index 28da8c0..0000000
--- a/src/components/Device/components/ControlBar/Tasks/components/TaskDialog/index.vue
+++ /dev/null
@@ -1,97 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
- {{ item.label }}
-
-
-
-
-
-
- 取消
-
-
- 确定
-
-
-
-
-
-
-
-
diff --git a/src/components/Device/components/ControlBar/Tasks/index.vue b/src/components/Device/components/ControlBar/Tasks/index.vue
index 395c198..820e301 100644
--- a/src/components/Device/components/ControlBar/Tasks/index.vue
+++ b/src/components/Device/components/ControlBar/Tasks/index.vue
@@ -7,12 +7,12 @@
diff --git a/src/components/Device/components/TerminalAction/components/TerminalDialog/index.vue b/src/components/Device/components/TerminalAction/components/TerminalDialog/index.vue
index d7028cd..abb85c0 100644
--- a/src/components/Device/components/TerminalAction/components/TerminalDialog/index.vue
+++ b/src/components/Device/components/TerminalAction/components/TerminalDialog/index.vue
@@ -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,
diff --git a/src/components/Device/index.vue b/src/components/Device/index.vue
index b2656aa..66d6573 100644
--- a/src/components/Device/index.vue
+++ b/src/components/Device/index.vue
@@ -150,11 +150,6 @@ export default {
WirelessAction,
BatchActions,
},
- provide() {
- return {
- invokeTerminal: (...args) => this.$refs.terminalActionRef.invoke(...args),
- }
- },
data() {
return {
loading: false,
diff --git a/src/components/Preference/components/PreferenceForm/components/InputPath/index.vue b/src/components/Preference/components/PreferenceForm/components/InputPath/index.vue
index 3771acd..6288ea8 100644
--- a/src/components/Preference/components/PreferenceForm/components/InputPath/index.vue
+++ b/src/components/Preference/components/PreferenceForm/components/InputPath/index.vue
@@ -70,7 +70,7 @@ export default {
},
)
- const value = files[0]
+ const value = files.join(',')
this.pathValue = value
}
diff --git a/src/composables/useFileActions/index.js b/src/composables/useFileActions/index.js
new file mode 100644
index 0000000..174ab60
--- /dev/null
+++ b/src/composables/useFileActions/index.js
@@ -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,
+ }
+}
diff --git a/src/composables/useInstallAction/index.js b/src/composables/useInstallAction/index.js
new file mode 100644
index 0000000..ba5bdbc
--- /dev/null
+++ b/src/composables/useInstallAction/index.js
@@ -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
diff --git a/src/composables/useScreenshotAction/index.js b/src/composables/useScreenshotAction/index.js
new file mode 100644
index 0000000..8f5cbcf
--- /dev/null
+++ b/src/composables/useScreenshotAction/index.js
@@ -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
diff --git a/src/composables/useShellAction/index.js b/src/composables/useShellAction/index.js
new file mode 100644
index 0000000..d7645f7
--- /dev/null
+++ b/src/composables/useShellAction/index.js
@@ -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(
+ `
${failFiles.map(text => `${text}
`).join('')}
`,
+ 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
diff --git a/src/dicts/index.js b/src/dicts/index.js
new file mode 100644
index 0000000..b540e0b
--- /dev/null
+++ b/src/dicts/index.js
@@ -0,0 +1 @@
+export * from './tasks/index'
diff --git a/src/dicts/tasks/index.js b/src/dicts/tasks/index.js
new file mode 100644
index 0000000..5c00ef1
--- /dev/null
+++ b/src/dicts/tasks/index.js
@@ -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',
+ },
+]
diff --git a/src/plugins/element-plus/components/EleFormRow/index.vue b/src/plugins/element-plus/components/EleFormRow/index.vue
index 572f59d..319496f 100644
--- a/src/plugins/element-plus/components/EleFormRow/index.vue
+++ b/src/plugins/element-plus/components/EleFormRow/index.vue
@@ -1,5 +1,5 @@
-
+
@@ -8,6 +8,7 @@
diff --git a/src/plugins/element-plus/locale.js b/src/plugins/element-plus/locale.js
new file mode 100644
index 0000000..552a690
--- /dev/null
+++ b/src/plugins/element-plus/locale.js
@@ -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,
+}
diff --git a/src/store/index.js b/src/store/index.js
index 7934292..0476531 100644
--- a/src/store/index.js
+++ b/src/store/index.js
@@ -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) {
diff --git a/src/store/task/index.js b/src/store/task/index.js
new file mode 100644
index 0000000..05f167d
--- /dev/null
+++ b/src/store/task/index.js
@@ -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 }
+})
diff --git a/src/utils/index.js b/src/utils/index.js
index 100ae15..eb47f33 100644
--- a/src/utils/index.js
+++ b/src/utils/index.js
@@ -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)
+}