diff --git a/README-CN.md b/README-CN.md index c72fbe81..fe239992 100644 --- a/README-CN.md +++ b/README-CN.md @@ -54,12 +54,18 @@ ### macOS && Linux -> 注意:这些平台没有集成 [Adb](https://developer.android.com/studio/releases/platform-tools?hl=zh-cn) 及 [Scrcpy](https://github.com/Genymobile/scrcpy) 需要手动安装 +> 注意:这些平台没有集成 [Scrcpy](https://github.com/Genymobile/scrcpy) 需要手动安装 1. Linux 可参阅的 [安装文档](https://github.com/Genymobile/scrcpy/blob/master/doc/linux.md) 2. macOS 可参阅的 [安装文档](https://github.com/Genymobile/scrcpy/blob/master/doc/macos.md) 3. 安装上述依赖成功后步骤同 USB 连接 和 WIFI 连接 +### Gnirehtet 反向供网 + +> 注意: macOS 内部没有集成如需使用需要手动安装 [安装文档](https://github.com/Genymobile/gnirehtet) + +Windows 及 Linux 端内部集成了 Gnirehtet, 用于提供 PC 到安卓设备的反向供网功能。 + ## 快捷键 请参阅 [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master/doc/shortcuts.md) @@ -135,7 +141,7 @@ 8. 添加 macOS 及 linux 操作系统的支持 ✅ 9. 支持国际化 ✅ 10. 对深色模式的支持 ✅ -11. 添加 Gnirehtet 反向供网功能 🚧 +11. 添加 Gnirehtet 反向供网功能 ✅ 12. 添加对游戏的增强功能,如游戏键位映射 🚧 ## 常见问题 diff --git a/README.md b/README.md index 821c9eaa..82aad97e 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,12 @@ 2. Refer to the [installation document](https://github.com/Genymobile/scrcpy/blob/master/doc/macos.md) for macOS 3. Follow steps in USB Connection and WIFI Connection after dependencies are installed successfully +### Gnirehtet Reverse Tethering + +> Note: macOS does not have Gnirehtet built-in. You need to manually install it to use this feature [Installation Guide](https://github.com/Genymobile/gnirehtet). + +Gnirehtet is built into the Windows and Linux apps to provide reverse tethering from PC to Android devices. + ## Shortcuts Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master/doc/shortcuts.md) @@ -133,7 +139,7 @@ Refer to [scrcpy/doc/shortcuts](https://github.com/Genymobile/scrcpy/blob/master 8. Add support for macOS and linux operating systems ✅ 9. Support internationalization ✅ 10. Support for dark mode ✅ -11. Add Gnirehtet reverse network function 🚧 +11. Add Gnirehtet reverse network function ✅ 12. Add game enhancement features such as game keyboard mapping 🚧 ## FAQ diff --git a/electron/configs/gnirehtet/index.js b/electron/configs/gnirehtet/index.js new file mode 100644 index 00000000..fa05e935 --- /dev/null +++ b/electron/configs/gnirehtet/index.js @@ -0,0 +1,19 @@ +import { extraResolve } from '@electron/helpers/index.js' +import which from 'which' + +export const getGnirehtetPath = () => { + switch (process.platform) { + // case 'darwin': + // return extraResolve('mac/gnirehtet/gnirehtet') + case 'win32': + return extraResolve('win/gnirehtet/gnirehtet.exe') + case 'linux': + return extraResolve('linux/gnirehtet/gnirehtet') + default: + return which.sync('gnirehtet', { nothrow: true }) + } +} + +export const gnirehtetPath = getGnirehtetPath() + +export const gnirehtetApkPath = extraResolve('common/gnirehtet/gnirehtet.apk') diff --git a/electron/configs/index.js b/electron/configs/index.js index 99868ece..03bb8e6e 100644 --- a/electron/configs/index.js +++ b/electron/configs/index.js @@ -6,6 +6,8 @@ export { adbPath } from './android-platform-tools/index.js' export { scrcpyPath } from './scrcpy/index.js' +export { gnirehtetPath, gnirehtetApkPath } from './gnirehtet/index.js' + export const desktopPath = process.env.DESKTOP_PATH export const devPublishPath = resolve('dev-publish.yml') diff --git a/electron/exposes/adbkit/index.js b/electron/exposes/adbkit/index.js index 3450e1e0..faf8b17d 100644 --- a/electron/exposes/adbkit/index.js +++ b/electron/exposes/adbkit/index.js @@ -54,6 +54,8 @@ const getDeviceIP = async (id) => { const reg = /inet ([0-9.]+)\/\d+/ const match = stdout.match(reg) const value = match[1] + + console.log('adbkit.getDeviceIP', value) return value } catch (error) { @@ -97,6 +99,8 @@ const screencap = async (deviceId, options = {}) => { const install = async (id, path) => client.getDevice(id).install(path) +const isInstalled = async (id, pkg) => client.getDevice(id).isInstalled(pkg) + const version = async () => client.version() const display = async (deviceId) => { @@ -167,6 +171,7 @@ export default () => { tcpip, screencap, install, + isInstalled, version, display, watch, diff --git a/electron/exposes/gnirehtet/index.js b/electron/exposes/gnirehtet/index.js new file mode 100644 index 00000000..df8a1ade --- /dev/null +++ b/electron/exposes/gnirehtet/index.js @@ -0,0 +1,139 @@ +import { spawn } from 'node:child_process' +import appStore from '@electron/helpers/store.js' +import { + adbPath, + gnirehtetApkPath, + gnirehtetPath, +} from '@electron/configs/index.js' + +const appDebug = appStore.get('common.debug') || false + +let adbkit = null + +const shell = async (command, { debug = false, stdout, stderr } = {}) => { + const spawnPath = appStore.get('common.gnirehtet') || gnirehtetPath + const ADB = appStore.get('common.adbPath') || adbPath + + const GNIREHTET_APK = gnirehtetApkPath + + const args = command.split(' ') + + console.log('gnirehtet.shell.spawnPath', spawnPath) + console.log('gnirehtet.shell.adbPath', adbPath) + + const gnirehtetProcess = spawn(`"${spawnPath}"`, args, { + env: { ...process.env, ADB, GNIREHTET_APK }, + shell: true, + encoding: 'utf8', + }) + + gnirehtetProcess.stdout.on('data', (data) => { + const stringData = data.toString() + + if (debug) { + console.log('gnirehtetProcess.stdout.data:', stringData) + } + + if (stdout) { + stdout(stringData, gnirehtetProcess) + } + }) + + gnirehtetProcess.stderr.on('data', (data) => { + const stringData = data.toString() + + if (debug) { + console.error('gnirehtetProcess.stderr.data:', stringData) + } + + if (stderr) { + stderr(stringData, gnirehtetProcess) + } + }) + + return new Promise((resolve, reject) => { + gnirehtetProcess.on('close', (code) => { + if (code === 0) { + resolve() + } + else { + reject(new Error(`Command failed with code ${code}`)) + } + }) + + gnirehtetProcess.on('error', (err) => { + reject(err) + }) + }) +} + +let relayProcess = null +const relay = async (args) => { + if (relayProcess) { + return relayProcess + } + + return new Promise((resolve, reject) => { + shell('relay', { + ...args, + debug: appDebug, + stdout: (_, process) => { + if (!relayProcess) { + relayProcess = process + } + resolve(process) + }, + }).catch((error) => { + reject(error) + }) + }) +} + +const install = deviceId => shell(`install ${deviceId}`) +const start = deviceId => shell(`start ${deviceId}`) +const stop = deviceId => shell(`stop ${deviceId}`) +const tunnel = deviceId => shell(`tunnel ${deviceId}`) + +const installed = async (deviceId) => { + const res = await adbkit.isInstalled(deviceId, 'com.genymobile.gnirehtet') + console.log('gnirehtet.apk.installed', res) + return res +} + +const run = async (deviceId) => { + await relay().catch((e) => { + throw new Error('Gnirehtet Relay fail') + }) + console.log('run.relay.success') + await install(deviceId).catch((e) => { + throw new Error('Gnirehtet Install Client fail') + }) + console.log('run.install.success') + await start(deviceId).catch((e) => { + throw new Error('Gnirehtet Start fail') + }) + console.log('run.start.success') +} + +window.addEventListener('beforeunload', () => { + stop() + + if (relayProcess) { + relayProcess.kill() + } +}) + +export default (options = {}) => { + adbkit = options.adbkit + + return { + shell, + relay, + install, + installed, + start, + stop, + tunnel, + run, + } +} diff --git a/electron/exposes/index.js b/electron/exposes/index.js index b796143e..0ebc81d6 100644 --- a/electron/exposes/index.js +++ b/electron/exposes/index.js @@ -1,6 +1,6 @@ import path from 'node:path' -import log from '@electron/helpers/log.js' +import appLog from '@electron/helpers/log.js' import '@electron/helpers/console.js' import store from '@electron/helpers/store.js' @@ -9,12 +9,13 @@ import * as configs from '@electron/configs/index.js' import electron from './electron/index.js' import adbkit from './adbkit/index.js' import scrcpy from './scrcpy/index.js' +import gnirehtet from './gnirehtet/index.js' export default { init(expose) { expose('nodePath', path) - expose('appLog', log) + expose('appLog', appLog) expose('appStore', store) @@ -23,7 +24,12 @@ export default { configs, }) - expose('adbkit', adbkit({ log })) - expose('scrcpy', scrcpy({ log })) + const adbkitExecute = adbkit() + + expose('adbkit', adbkitExecute) + + expose('scrcpy', scrcpy()) + + expose('gnirehtet', gnirehtet({ adbkit: adbkitExecute })) }, } diff --git a/electron/resources/extra/common/gnirehtet/gnirehtet.apk b/electron/resources/extra/common/gnirehtet/gnirehtet.apk new file mode 100644 index 00000000..98ea978e Binary files /dev/null and b/electron/resources/extra/common/gnirehtet/gnirehtet.apk differ diff --git a/electron/resources/extra/linux/gnirehtet/gnirehtet b/electron/resources/extra/linux/gnirehtet/gnirehtet new file mode 100644 index 00000000..fe321830 Binary files /dev/null and b/electron/resources/extra/linux/gnirehtet/gnirehtet differ diff --git a/electron/resources/extra/win/gnirehtet/gnirehtet-run.cmd b/electron/resources/extra/win/gnirehtet/gnirehtet-run.cmd new file mode 100644 index 00000000..ffefea65 --- /dev/null +++ b/electron/resources/extra/win/gnirehtet/gnirehtet-run.cmd @@ -0,0 +1,2 @@ +@gnirehtet.exe run +@pause diff --git a/electron/resources/extra/win/gnirehtet/gnirehtet.apk b/electron/resources/extra/win/gnirehtet/gnirehtet.apk new file mode 100644 index 00000000..98ea978e Binary files /dev/null and b/electron/resources/extra/win/gnirehtet/gnirehtet.apk differ diff --git a/electron/resources/extra/win/gnirehtet/gnirehtet.exe b/electron/resources/extra/win/gnirehtet/gnirehtet.exe new file mode 100644 index 00000000..ccc57824 Binary files /dev/null and b/electron/resources/extra/win/gnirehtet/gnirehtet.exe differ diff --git a/src/App.vue b/src/App.vue index 83349a91..13eceeb1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -53,24 +53,15 @@ export default { }, methods: { async showTips() { - if (this.$electron.process.platform === 'win32') { - return false - } - - const { adbPath, scrcpyPath } = this.$electron?.configs || {} - - if (adbPath) { - return false - } + const { scrcpyPath } = this.$electron?.configs || {} if (scrcpyPath) { return false } this.$alert( - `
该软件依赖与 - adb - 以及 + `
+ 该软件依赖与 scrcpy ,请确保已正确安装所述依赖项,或者在偏好设置中手动配置依赖项所在位置。
`, diff --git a/src/components/Device/ControlBar/index.vue b/src/components/Device/ControlBar/index.vue index 695830cd..0307734e 100644 --- a/src/components/Device/ControlBar/index.vue +++ b/src/components/Device/ControlBar/index.vue @@ -1,13 +1,18 @@ @@ -36,57 +41,88 @@ export default { return { controlModel: [ { - label: this.$t('device.control.switch'), + label: 'device.control.switch', elIcon: 'Switch', command: 'input keyevent KEYCODE_APP_SWITCH', }, { - label: this.$t('device.control.home'), + label: 'device.control.home', elIcon: 'HomeFilled', command: 'input keyevent KEYCODE_HOME', }, { - label: this.$t('device.control.return'), + label: 'device.control.return', elIcon: 'Back', command: 'input keyevent KEYCODE_BACK', }, { - label: this.$t('device.control.notification'), + label: 'device.control.notification', elIcon: 'Notification', command: 'cmd statusbar expand-notifications', - tips: this.$t('device.control.notification.tips'), + tips: 'device.control.notification.tips', }, { - label: this.$t('device.control.power'), + label: 'device.control.power', elIcon: 'SwitchButton', command: 'input keyevent KEYCODE_POWER', - tips: this.$t('device.control.power.tips'), + tips: 'device.control.power.tips', }, { - label: this.$t('device.control.reboot'), + label: 'device.control.reboot', elIcon: 'RefreshLeft', command: 'reboot', }, { - label: this.$t('device.control.capture'), + label: 'device.control.capture', elIcon: 'Crop', handle: this.handleScreenCap, tips: '', }, { - label: this.$t('device.control.install'), + label: 'device.control.install', svgIcon: 'install', handle: this.handleInstall, tips: '', }, + { + label: 'device.control.gnirehtet', + elIcon: 'Link', + handle: this.handleGnirehtet, + tips: 'device.control.gnirehtet.tips', + }, ], } }, computed: {}, methods: { + onWheel(event) { + const container = this.$refs.wheelContainer + container.scrollLeft += event.deltaY + }, preferenceData(...args) { return this.$store.preference.getData(...args) }, + async handleGnirehtet(device) { + const messageEl = this.$message({ + message: this.$t('device.control.gnirehtet.progress', { + deviceName: device.$name, + }), + icon: LoadingIcon, + duration: 0, + }) + + try { + await this.$gnirehtet.run(device.id) + this.$message.success(this.$t('device.control.gnirehtet.success')) + } + catch (error) { + if (error.message) { + this.$message.warning(error.message) + } + } + + messageEl.close() + }, async handleInstall(device) { let files = null diff --git a/src/components/Device/index.vue b/src/components/Device/index.vue index e22d5fde..9d5ea6bf 100644 --- a/src/components/Device/index.vue +++ b/src/components/Device/index.vue @@ -355,16 +355,28 @@ export default { async handleWifi(row) { try { const host = await this.$adb.getDeviceIP(row.id) - const port = await this.$adb.tcpip(row.id, 5555) + + if (!host) { + throw new Error(this.$t('device.wireless.mode.error')) + } + this.formData.host = host + + const port = await this.$adb.tcpip(row.id, 5555) + this.formData.port = port + console.log('host:port', `${host}:${port}`) + await sleep() this.handleConnect() } catch (error) { console.warn(error.message) + if (error?.message || error?.cause?.message) { + this.$message.warning(error?.message || error?.cause?.message) + } } }, onPairSuccess() { diff --git a/src/locales/languages/en_US.json b/src/locales/languages/en_US.json index a84c038e..c003ac45 100644 --- a/src/locales/languages/en_US.json +++ b/src/locales/languages/en_US.json @@ -21,6 +21,7 @@ "device.permission.error": "Device permission error, please reconnect device and allow USB debugging", "device.wireless.name": "Wireless", "device.wireless.mode": "Wireless Mode", + "device.wireless.mode.error": "Do not get the local area network connection address, please check the network", "device.wireless.connect.name": "Connect", "device.wireless.connect.error.title": "Connect failed", "device.wireless.connect.error.detail": "Error details", @@ -83,6 +84,10 @@ "device.control.return": "Return", "device.control.home": "Home", "device.control.switch": "Switch", + "device.control.gnirehtet": "Gnirehtet", + "device.control.gnirehtet.tips": "Gnirehtet provides reverse tethering for Android; Note: Initial connection requires authorization on the device.", + "device.control.gnirehtet.progress": "Starting Gnirehtet reverse tethering service...", + "device.control.gnirehtet.success": "Gnirehtet reverse tethering feature started successfully", "preferences.name": "Preferences", "preferences.reset": "Reset to Default", "preferences.scope.global": "Global", @@ -119,6 +124,9 @@ "preferences.common.scrcpy.name": "Scrcpy Path", "preferences.common.scrcpy.placeholder": "Set scrcpy path", "preferences.common.scrcpy.tips": "scrcpy path to connect device", + "preferences.common.gnirehtet.name": "Gnirehtet Path", + "preferences.common.gnirehtet.placeholder": "Set gnirehtet path", + "preferences.common.gnirehtet.tips": "The path for gnirehtet used to provide reverse tethering for devices.", "preferences.common.language.name": "Language", "preferences.common.language.placeholder": "Select language", "preferences.common.language.chinese": "中文", diff --git a/src/locales/languages/zh_CN.json b/src/locales/languages/zh_CN.json index 5327d035..3339950f 100644 --- a/src/locales/languages/zh_CN.json +++ b/src/locales/languages/zh_CN.json @@ -21,6 +21,7 @@ "device.permission.error": "设备可能未授权成功,请重新插拔设备并点击允许USB调试", "device.wireless.name": "无线连接", "device.wireless.mode": "无线模式", + "device.wireless.mode.error": "没有获取到局域网连接地址,请检查网络", "device.wireless.connect.name": "连接设备", "device.wireless.connect.error.title": "连接设备失败", "device.wireless.connect.error.detail": "错误详情", @@ -83,6 +84,10 @@ "device.control.return": "返回键", "device.control.home": "主屏幕", "device.control.switch": "切换键", + "device.control.gnirehtet": "反向供网", + "device.control.gnirehtet.tips": "使用 Gnirehtet 为 Android 提供反向网络共享;注意:首次连接需要在设备上进行授权", + "device.control.gnirehtet.progress": "正在启动 Gnirehtet 反向供网服务中...", + "device.control.gnirehtet.success": "Gnirehtet 反向网络共享功能启动成功", "preferences.name": "偏好设置", "preferences.reset": "恢复默认值", "preferences.scope.global": "全局", @@ -119,6 +124,9 @@ "preferences.common.scrcpy.name": "scrcpy 路径", "preferences.common.scrcpy.placeholder": "请设置 scrcpy 路径", "preferences.common.scrcpy.tips": "用于连接设备的 scrcpy 地址。", + "preferences.common.gnirehtet.name": "gnirehtet 路径", + "preferences.common.gnirehtet.placeholder": "请设置 gnirehtet 路径", + "preferences.common.gnirehtet.tips": "用于为设备反向供网的 gnirehtet 地址。", "preferences.common.language.name": "语言", "preferences.common.language.placeholder": "选择你需要的语言", "preferences.common.language.chinese": "中文", diff --git a/src/main.js b/src/main.js index dd64d282..d3af3b81 100644 --- a/src/main.js +++ b/src/main.js @@ -26,13 +26,14 @@ app.use(icons) app.use(i18n) window.t = t -app.config.globalProperties.$electron = window.electron -app.config.globalProperties.$adb = window.adbkit -app.config.globalProperties.$scrcpy = window.scrcpy app.config.globalProperties.$path = window.nodePath - app.config.globalProperties.$appStore = window.appStore app.config.globalProperties.$appLog = window.appLog +app.config.globalProperties.$electron = window.electron + +app.config.globalProperties.$adb = window.adbkit +app.config.globalProperties.$scrcpy = window.scrcpy +app.config.globalProperties.$gnirehtet = window.gnirehtet app.config.globalProperties.$replaceIP = replaceIP diff --git a/src/store/preference/model/common/index.js b/src/store/preference/model/common/index.js index 2eff1b85..7b0d151d 100644 --- a/src/store/preference/model/common/index.js +++ b/src/store/preference/model/common/index.js @@ -1,4 +1,5 @@ -const { adbPath, scrcpyPath, desktopPath } = window?.electron?.configs || {} +const { adbPath, scrcpyPath, gnirehtetPath, desktopPath } + = window?.electron?.configs || {} const defaultLanguage = window.electron?.process?.env?.LOCALE @@ -76,6 +77,18 @@ export default { properties: ['openFile'], filters: [{ name: 'preferences.common.scrcpy.name', extensions: ['*'] }], }, + gnirehtetPath: { + label: 'preferences.common.gnirehtet.name', + field: 'gnirehtetPath', + value: gnirehtetPath, + type: 'Input.path', + placeholder: 'preferences.common.gnirehtet.placeholder', + tips: 'preferences.common.gnirehtet.tips', + properties: ['openFile'], + filters: [ + { name: 'preferences.common.gnirehtet.name', extensions: ['*'] }, + ], + }, debug: { label: 'preferences.common.debug.name', field: 'debug',