diff --git a/electron/exposes/adbkit/index.js b/electron/exposes/adbkit/index.js
index f98a911..680afae 100644
--- a/electron/exposes/adbkit/index.js
+++ b/electron/exposes/adbkit/index.js
@@ -6,7 +6,7 @@ import dayjs from 'dayjs'
import { Adb } from '@devicefarmer/adbkit'
import { adbPath } from '@electron/configs/index.js'
-console.log('adbPath', adbPath)
+// console.log('adbPath', adbPath)
const exec = util.promisify(child_process.exec)
@@ -19,12 +19,11 @@ window.addEventListener('beforeunload', () => {
})
const shell = async command => exec(`${adbPath} ${command}`)
-const getDevices = async () => await client.listDevicesWithPaths()
-const deviceShell = async (id, command) =>
- await client.getDevice(id).shell(command)
-const kill = async (...params) => await client.kill(...params)
-const connect = async (...params) => await client.connect(...params)
-const disconnect = async (...params) => await client.disconnect(...params)
+const getDevices = async () => client.listDevicesWithPaths()
+const deviceShell = async (id, command) => client.getDevice(id).shell(command)
+const kill = async (...params) => client.kill(...params)
+const connect = async (...params) => client.connect(...params)
+const disconnect = async (...params) => client.disconnect(...params)
const getDeviceIP = async (id) => {
try {
@@ -40,7 +39,7 @@ const getDeviceIP = async (id) => {
}
}
-const tcpip = async (id, port = 5555) => await client.getDevice(id).tcpip(port)
+const tcpip = async (id, port = 5555) => client.getDevice(id).tcpip(port)
const screencap = async (deviceId, options = {}) => {
let fileStream = null
@@ -74,6 +73,8 @@ const screencap = async (deviceId, options = {}) => {
})
}
+const install = async (id, path) => client.getDevice(id).install(path)
+
const watch = async (callback) => {
const tracker = await client.trackDevices()
tracker.on('add', async (ret) => {
@@ -112,6 +113,7 @@ export default () => {
getDeviceIP,
tcpip,
screencap,
+ install,
watch,
}
}
diff --git a/electron/exposes/scrcpy/index.js b/electron/exposes/scrcpy/index.js
index 5152830..0d430f4 100644
--- a/electron/exposes/scrcpy/index.js
+++ b/electron/exposes/scrcpy/index.js
@@ -1,11 +1,48 @@
-import util from 'node:util'
-import child_process from 'node:child_process'
+import { spawn } from 'node:child_process'
import { adbPath, scrcpyPath } from '@electron/configs/index.js'
-const exec = util.promisify(child_process.exec)
+const shell = async (command, { stdout, stderr } = {}) => {
+ const args = command.split(' ')
+ const scrcpyProcess = spawn(scrcpyPath, args, {
+ env: { ...process.env, ADB: adbPath },
+ shell: true,
+ })
-const shell = command =>
- exec(`${scrcpyPath} ${command}`, { env: { ...process.env, ADB: adbPath } })
+ scrcpyProcess.stdout.on('data', (data) => {
+ const stringData = data.toString()
+
+ console.log('scrcpyProcess.stdout.data:', stringData)
+
+ if (stdout) {
+ stdout(stringData)
+ }
+ })
+
+ scrcpyProcess.stderr.on('data', (data) => {
+ const stringData = data.toString()
+
+ console.error('scrcpyProcess.stderr.data:', stringData)
+
+ if (stderr) {
+ stderr(stringData)
+ }
+ })
+
+ return new Promise((resolve, reject) => {
+ scrcpyProcess.on('close', (code) => {
+ if (code === 0) {
+ resolve()
+ }
+ else {
+ reject(new Error(`Command failed with code ${code}`))
+ }
+ })
+
+ scrcpyProcess.on('error', (err) => {
+ reject(err)
+ })
+ })
+}
export default () => ({
shell,
diff --git a/package.json b/package.json
index 8abf3a1..7c9a2da 100644
--- a/package.json
+++ b/package.json
@@ -14,6 +14,7 @@
"build:linux": "vite build && electron-builder --linux",
"preview": "vite preview",
"lint": "eslint . --ext .md,.vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --ignore-path .eslintignore --fix",
+ "svgo": "svgo -f src/icons/svg --config=src/icons/svgo.config.js",
"postinstall": "electron-builder install-app-deps"
},
"dependencies": {
@@ -40,6 +41,7 @@
"vite-plugin-electron": "^0.14.0",
"vite-plugin-electron-renderer": "^0.14.5",
"vite-plugin-eslint": "^1.8.1",
+ "vite-svg-loader": "^4.0.0",
"vue-tsc": "^1.8.8"
}
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ef220d5..a73b72c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -70,6 +70,9 @@ devDependencies:
vite-plugin-eslint:
specifier: ^1.8.1
version: 1.8.1(eslint@8.51.0)(vite@4.4.11)
+ vite-svg-loader:
+ specifier: ^4.0.0
+ version: 4.0.0
vue-tsc:
specifier: ^1.8.8
version: 1.8.19(typescript@5.2.2)
@@ -781,6 +784,11 @@ packages:
engines: {node: '>= 10'}
dev: true
+ /@trysound/sax@0.2.0:
+ resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
+ engines: {node: '>=10.13.0'}
+ dev: true
+
/@types/cacheable-request@6.0.3:
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
dependencies:
@@ -2035,6 +2043,11 @@ packages:
engines: {node: '>= 6'}
dev: true
+ /commander@7.2.0:
+ resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==}
+ engines: {node: '>= 10'}
+ dev: true
+
/commander@9.5.0:
resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==}
engines: {node: ^12.20.0 || >=14}
@@ -2084,6 +2097,16 @@ packages:
which: 2.0.2
dev: true
+ /css-select@5.1.0:
+ resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==}
+ dependencies:
+ boolbase: 1.0.0
+ css-what: 6.1.0
+ domhandler: 5.0.3
+ domutils: 3.1.0
+ nth-check: 2.1.1
+ dev: true
+
/css-selector-tokenizer@0.8.0:
resolution: {integrity: sha512-Jd6Ig3/pe62/qe5SBPTN8h8LeUg/pT4lLgtavPf7updwwHpvFzxvOQBHYj2LZDMjUnBzgvIUSjRcf6oT5HzHFg==}
dependencies:
@@ -2091,6 +2114,14 @@ packages:
fastparse: 1.1.2
dev: true
+ /css-tree@2.2.1:
+ resolution: {integrity: sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+ dependencies:
+ mdn-data: 2.0.28
+ source-map-js: 1.0.2
+ dev: true
+
/css-tree@2.3.1:
resolution: {integrity: sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0}
@@ -2099,12 +2130,24 @@ packages:
source-map-js: 1.0.2
dev: true
+ /css-what@6.1.0:
+ resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==}
+ engines: {node: '>= 6'}
+ dev: true
+
/cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
hasBin: true
dev: true
+ /csso@5.0.5:
+ resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
+ engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
+ dependencies:
+ css-tree: 2.2.1
+ dev: true
+
/csstype@3.1.2:
resolution: {integrity: sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==}
@@ -3696,6 +3739,10 @@ packages:
resolution: {integrity: sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==}
dev: true
+ /mdn-data@2.0.28:
+ resolution: {integrity: sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==}
+ dev: true
+
/mdn-data@2.0.30:
resolution: {integrity: sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==}
dev: true
@@ -4644,6 +4691,19 @@ packages:
engines: {node: '>= 0.4'}
dev: true
+ /svgo@3.0.2:
+ resolution: {integrity: sha512-Z706C1U2pb1+JGP48fbazf3KxHrWOsLme6Rv7imFBn5EnuanDW1GPaA/P1/dvObE670JDePC3mnj0k0B7P0jjQ==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+ dependencies:
+ '@trysound/sax': 0.2.0
+ commander: 7.2.0
+ css-select: 5.1.0
+ css-tree: 2.3.1
+ csso: 5.0.5
+ picocolors: 1.0.0
+ dev: true
+
/tailwindcss@3.3.3:
resolution: {integrity: sha512-A0KgSkef7eE4Mf+nKJ83i75TMyq8HqY3qmFIJSWy8bNt0v1lG7jUcpGpoTFxAwYcWOphcTBLPPJg+bDfhDf52w==}
engines: {node: '>=14.0.0'}
@@ -4991,6 +5051,13 @@ packages:
vite: 4.4.11
dev: true
+ /vite-svg-loader@4.0.0:
+ resolution: {integrity: sha512-0MMf1yzzSYlV4MGePsLVAOqXsbF5IVxbn4EEzqRnWxTQl8BJg/cfwIzfQNmNQxZp5XXwd4kyRKF1LytuHZTnqA==}
+ dependencies:
+ '@vue/compiler-sfc': 3.3.4
+ svgo: 3.0.2
+ dev: true
+
/vite@4.4.11:
resolution: {integrity: sha512-ksNZJlkcU9b0lBwAGZGGaZHCMqHsc8OpgtoYhsQ4/I2v5cnpmmmqe5pM4nv/4Hn6G/2GhTdj0DhZh2e+Er1q5A==}
engines: {node: ^14.18.0 || >=16.0.0}
diff --git a/src/components/Devices/ControlBar/index.vue b/src/components/Devices/ControlBar/index.vue
index bfdd4bb..b39f17a 100644
--- a/src/components/Devices/ControlBar/index.vue
+++ b/src/components/Devices/ControlBar/index.vue
@@ -6,11 +6,16 @@
type="primary"
plain
class="!border-none !mx-0 bg-transparent !rounded-0"
- :icon="item.icon"
:disabled="device.$unauthorized"
:title="item.tips"
@click="handleClick(item)"
>
+
+
+
+
+
+
{{ item.label }}
@@ -32,37 +37,43 @@ export default {
controlModel: [
{
label: '切换键',
- icon: 'Switch',
+ elIcon: 'Switch',
command: 'input keyevent KEYCODE_APP_SWITCH',
},
{
label: '主屏幕键',
- icon: 'HomeFilled',
+ elIcon: 'HomeFilled',
command: 'input keyevent KEYCODE_HOME',
},
{
label: '返回键',
- icon: 'Back',
+ elIcon: 'Back',
command: 'input keyevent KEYCODE_BACK',
},
{
label: '菜单键',
- icon: 'Menu',
+ elIcon: 'Menu',
command: 'input keyevent KEYCODE_MENU',
tips: '不要和切换键搞错啦',
},
{
label: '电源键',
- icon: 'SwitchButton',
+ elIcon: 'SwitchButton',
command: 'input keyevent KEYCODE_POWER',
tips: '可以用来开启或关闭屏幕',
},
{
label: '截取屏幕',
- icon: 'Crop',
+ elIcon: 'Crop',
handle: this.handleScreenCap,
tips: '',
},
+ {
+ label: '安装应用',
+ svgIcon: 'install',
+ handle: this.handleInstall,
+ tips: '',
+ },
],
}
},
@@ -72,6 +83,54 @@ export default {
},
},
methods: {
+ async handleInstall(device) {
+ const files = await this.$electron.ipcRenderer.invoke(
+ 'show-open-dialog',
+ {
+ properties: ['openFile', 'multiSelections'],
+ filters: [{ name: '请选择要安装的应用', extensions: ['apk'] }],
+ },
+ )
+
+ if (!files) {
+ return false
+ }
+
+ const messageEl = this.$message({
+ message: ` 正在为 ${device.name} 安装应用中...`,
+ icon: LoadingIcon,
+ duration: 0,
+ })
+
+ let failCount = 0
+
+ for (let index = 0; index < files.length; index++) {
+ const item = files[index]
+ await this.$adb.install(device.id, item).catch((e) => {
+ console.warn(e)
+ ++failCount
+ })
+ }
+
+ messageEl.close()
+
+ const totalCount = files.length
+ const successCount = totalCount - failCount
+
+ if (successCount) {
+ if (totalCount > 1) {
+ this.$message.success(
+ `已成功将应用安装到 ${device.name} 中,共 ${totalCount}个,成功 ${successCount}个,失败 ${failCount}个`,
+ )
+ }
+ else {
+ this.$message.success(`已成功将应用安装到 ${device.name} 中`)
+ }
+ return
+ }
+
+ this.$message.warning('安装应用失败,请检查安装包后重试')
+ },
handleClick(row) {
if (row.command) {
this.$adb.deviceShell(this.device.id, row.command)
@@ -84,16 +143,19 @@ export default {
}
},
async handleScreenCap(device) {
- const deviceName = device.name || device.id
-
const messageEl = this.$message({
- message: ` 正在截取 ${deviceName} 的屏幕快照...`,
+ message: ` 正在截取 ${device.name} 的屏幕快照...`,
icon: LoadingIcon,
duration: 0,
})
- const fileName = `${deviceName}-screencap-${dayjs().format('YYYY-MM-DD-HH-mm-ss')}.png`
- const savePath = this.$path.resolve(this.scrcpyConfig['--record'], fileName)
+ const fileName = `${device.name}-screencap-${dayjs().format(
+ 'YYYY-MM-DD-HH-mm-ss',
+ )}.png`
+ const savePath = this.$path.resolve(
+ this.scrcpyConfig['--record'],
+ fileName,
+ )
try {
await this.$adb.screencap(device.id, { savePath })
diff --git a/src/components/Devices/index.vue b/src/components/Devices/index.vue
index 0e4d1f4..f0aa582 100644
--- a/src/components/Devices/index.vue
+++ b/src/components/Devices/index.vue
@@ -1,7 +1,12 @@
-
+
无线连接
@@ -53,8 +58,18 @@
-
-
+
+
- {{ row.$loading ? '正在镜像' : '开始镜像' }}
+ {{ row.$loading ? "正在镜像" : "开始镜像" }}
- {{ row.$recordLoading ? '正在录制' : '开始录制' }}
+ {{ row.$recordLoading ? "正在录制" : "开始录制" }}
+
+
+
无线模式
@@ -119,7 +138,7 @@
:icon="row.$stopLoading ? '' : 'CircleClose'"
@click="handleStop(row)"
>
- {{ row.$stopLoading ? '正在断开' : '断开连接' }}
+ {{ row.$stopLoading ? "正在断开" : "断开连接" }}
@@ -189,6 +208,7 @@ export default {
})
},
methods: {
+ onStdout() {},
toggleRowExpansion(...params) {
this.$refs.elTable.toggleRowExpansion(...params)
},
@@ -213,7 +233,7 @@ export default {
console.log('handleRecord.command', command)
- await this.$scrcpy.shell(command)
+ await this.$scrcpy.shell(command, { stdout: this.onStdout })
await this.$confirm('是否前往录制位置进行查看?', '录制成功', {
confirmButtonText: '确定',
@@ -239,6 +259,7 @@ export default {
try {
await this.$scrcpy.shell(
`--serial=${row.id} --window-title=${row.name}-${row.id} ${this.stringScrcpyConfig}`,
+ { stdout: this.onStdout },
)
}
catch (error) {
diff --git a/src/icons/components/SvgIcon.vue b/src/icons/components/SvgIcon.vue
new file mode 100644
index 0000000..17f9a55
--- /dev/null
+++ b/src/icons/components/SvgIcon.vue
@@ -0,0 +1,75 @@
+
+
+
diff --git a/src/icons/index.js b/src/icons/index.js
new file mode 100644
index 0000000..a66ca0b
--- /dev/null
+++ b/src/icons/index.js
@@ -0,0 +1,7 @@
+import SvgIcon from './components/SvgIcon.vue'
+
+export default {
+ install(app) {
+ app.component('SvgIcon', SvgIcon)
+ },
+}
diff --git a/src/icons/svg/install.svg b/src/icons/svg/install.svg
new file mode 100644
index 0000000..43c1261
--- /dev/null
+++ b/src/icons/svg/install.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/svg/wifi.svg b/src/icons/svg/wifi.svg
new file mode 100644
index 0000000..f600ff7
--- /dev/null
+++ b/src/icons/svg/wifi.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/icons/svgo.config.js b/src/icons/svgo.config.js
new file mode 100644
index 0000000..4184764
--- /dev/null
+++ b/src/icons/svgo.config.js
@@ -0,0 +1,10 @@
+module.exports = {
+ plugins: [
+ {
+ name: 'removeAttrs',
+ params: {
+ attrs: '(fill|fill-rule)',
+ },
+ },
+ ],
+}
diff --git a/src/main.js b/src/main.js
index 3a46bba..e7502b9 100644
--- a/src/main.js
+++ b/src/main.js
@@ -4,6 +4,7 @@ import App from './App.vue'
import store from './store/index.js'
import plugins from './plugins/index.js'
+import icons from './icons/index.js'
import 'virtual:uno.css'
import './styles/index.js'
@@ -13,6 +14,7 @@ const app = createApp(App)
app.use(store)
app.use(plugins)
+app.use(icons)
app.config.globalProperties.$electron = window.electron
app.config.globalProperties.$adb = window.adbkit
diff --git a/vite.config.js b/vite.config.js
index 98f577b..7bd9f2d 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -6,6 +6,7 @@ import useRenderer from 'vite-plugin-electron-renderer'
import useVue from '@vitejs/plugin-vue'
import useEslint from 'vite-plugin-eslint'
import useUnoCSS from 'unocss/vite'
+import useSvg from 'vite-svg-loader'
const merge = config =>
mergeConfig(
@@ -32,6 +33,7 @@ export default merge(
plugins: [
useEslint(),
useUnoCSS(),
+ useSvg(),
useVue(),
useElectron([
{