add shell
This commit is contained in:
parent
aaa60216b2
commit
be417729ea
10 changed files with 768 additions and 380 deletions
163
client-electron/package-lock.json
generated
163
client-electron/package-lock.json
generated
|
|
@ -21,6 +21,7 @@
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.17.19",
|
||||||
"@tanstack/react-table": "^8.11.7",
|
"@tanstack/react-table": "^8.11.7",
|
||||||
|
|
@ -49,10 +50,12 @@
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"sonner": "^1.3.1",
|
"sonner": "^1.4.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"usb": "^2.11.0",
|
"usb": "^2.11.0",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
|
|
@ -2761,18 +2764,6 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@jridgewell/source-map": {
|
|
||||||
"version": "0.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
|
|
||||||
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/gen-mapping": "^0.3.0",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@jridgewell/sourcemap-codec": {
|
"node_modules/@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||||
|
|
@ -3748,6 +3739,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@radix-ui/react-tabs": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.4",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*",
|
||||||
|
"@types/react-dom": "*",
|
||||||
|
"react": "^16.8 || ^17.0 || ^18.0",
|
||||||
|
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@types/react-dom": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@radix-ui/react-toast": {
|
"node_modules/@radix-ui/react-toast": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
|
||||||
|
|
@ -12152,9 +12173,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/sonner": {
|
"node_modules/sonner": {
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.0.tgz",
|
||||||
"integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==",
|
"integrity": "sha512-nvkTsIuOmi9e5Wz5If8ldasJjZNVfwiXYijBi2dbijvTQnQppvMcXTFNxL/NUFWlI2yJ1JX7TREDsg+gYm9WyA==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"react": "^18.0.0",
|
"react": "^18.0.0",
|
||||||
"react-dom": "^18.0.0"
|
"react-dom": "^18.0.0"
|
||||||
|
|
@ -12649,34 +12670,6 @@
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/terser": {
|
|
||||||
"version": "5.27.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
|
|
||||||
"integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
|
||||||
"acorn": "^8.8.2",
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"terser": "bin/terser"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/terser/node_modules/commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
},
|
|
||||||
"node_modules/text-table": {
|
"node_modules/text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
@ -13358,6 +13351,19 @@
|
||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
|
||||||
|
},
|
||||||
|
"node_modules/xterm-addon-fit": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"xterm": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/y18n": {
|
"node_modules/y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|
@ -15455,18 +15461,6 @@
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
|
||||||
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
|
"integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw=="
|
||||||
},
|
},
|
||||||
"@jridgewell/source-map": {
|
|
||||||
"version": "0.3.5",
|
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz",
|
|
||||||
"integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
|
||||||
"@jridgewell/gen-mapping": "^0.3.0",
|
|
||||||
"@jridgewell/trace-mapping": "^0.3.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"@jridgewell/sourcemap-codec": {
|
"@jridgewell/sourcemap-codec": {
|
||||||
"version": "1.4.15",
|
"version": "1.4.15",
|
||||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
|
||||||
|
|
@ -16013,6 +16007,22 @@
|
||||||
"@radix-ui/react-compose-refs": "1.0.1"
|
"@radix-ui/react-compose-refs": "1.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@radix-ui/react-tabs": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==",
|
||||||
|
"requires": {
|
||||||
|
"@babel/runtime": "^7.13.10",
|
||||||
|
"@radix-ui/primitive": "1.0.1",
|
||||||
|
"@radix-ui/react-context": "1.0.1",
|
||||||
|
"@radix-ui/react-direction": "1.0.1",
|
||||||
|
"@radix-ui/react-id": "1.0.1",
|
||||||
|
"@radix-ui/react-presence": "1.0.1",
|
||||||
|
"@radix-ui/react-primitive": "1.0.3",
|
||||||
|
"@radix-ui/react-roving-focus": "1.0.4",
|
||||||
|
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@radix-ui/react-toast": {
|
"@radix-ui/react-toast": {
|
||||||
"version": "1.1.5",
|
"version": "1.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz",
|
||||||
|
|
@ -22118,9 +22128,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sonner": {
|
"sonner": {
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.0.tgz",
|
||||||
"integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==",
|
"integrity": "sha512-nvkTsIuOmi9e5Wz5If8ldasJjZNVfwiXYijBi2dbijvTQnQppvMcXTFNxL/NUFWlI2yJ1JX7TREDsg+gYm9WyA==",
|
||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"source-map": {
|
"source-map": {
|
||||||
|
|
@ -22503,30 +22513,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"terser": {
|
|
||||||
"version": "5.27.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz",
|
|
||||||
"integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"requires": {
|
|
||||||
"@jridgewell/source-map": "^0.3.3",
|
|
||||||
"acorn": "^8.8.2",
|
|
||||||
"commander": "^2.20.0",
|
|
||||||
"source-map-support": "~0.5.20"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"commander": {
|
|
||||||
"version": "2.20.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
|
||||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"peer": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"text-table": {
|
"text-table": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||||
|
|
@ -23006,6 +22992,17 @@
|
||||||
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
|
"integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"xterm": {
|
||||||
|
"version": "5.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm/-/xterm-5.3.0.tgz",
|
||||||
|
"integrity": "sha512-8QqjlekLUFTrU6x7xck1MsPzPA571K5zNqWm0M0oroYEWVOptZ0+ubQSkQ3uxIEhcIHRujJy6emDWX4A7qyFzg=="
|
||||||
|
},
|
||||||
|
"xterm-addon-fit": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-yj3Np7XlvxxhYF/EJ7p3KHaMt6OdwQ+HDu573Vx1lRXsVxOcnVJs51RgjZOouIZOczTsskaS+CpXspK81/DLqw==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "5.0.8",
|
"version": "5.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@
|
||||||
"@radix-ui/react-select": "^2.0.0",
|
"@radix-ui/react-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@tanstack/react-query": "^5.17.19",
|
"@tanstack/react-query": "^5.17.19",
|
||||||
"@tanstack/react-table": "^8.11.7",
|
"@tanstack/react-table": "^8.11.7",
|
||||||
|
|
@ -61,10 +62,12 @@
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.49.3",
|
"react-hook-form": "^7.49.3",
|
||||||
"react-router-dom": "^6.21.1",
|
"react-router-dom": "^6.21.1",
|
||||||
"sonner": "^1.3.1",
|
"sonner": "^1.4.0",
|
||||||
"tailwind-merge": "^2.2.1",
|
"tailwind-merge": "^2.2.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"usb": "^2.11.0",
|
"usb": "^2.11.0",
|
||||||
|
"xterm": "^5.3.0",
|
||||||
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.22.4",
|
"zod": "^3.22.4",
|
||||||
"zustand": "^4.4.7"
|
"zustand": "^4.4.7"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import AuthCallBack from './AuthCallBack'
|
||||||
import MainLayout from './layouts/MainLayout'
|
import MainLayout from './layouts/MainLayout'
|
||||||
import HomePage from './pages/home'
|
import HomePage from './pages/home'
|
||||||
import LoginPage from './pages/login'
|
import LoginPage from './pages/login'
|
||||||
import AndroidPage from './pages/android'
|
import AndroidPage from './pages/android/android'
|
||||||
import RecipesPage from './pages/recipes/recipes'
|
import RecipesPage from './pages/recipes/recipes'
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import UploadPage from './pages/upload'
|
import UploadPage from './pages/upload'
|
||||||
|
|
|
||||||
29
client-electron/src/components/ui/sonner.tsx
Normal file
29
client-electron/src/components/ui/sonner.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { useTheme } from "next-themes"
|
||||||
|
import { Toaster as Sonner } from "sonner"
|
||||||
|
|
||||||
|
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||||
|
|
||||||
|
const Toaster = ({ ...props }: ToasterProps) => {
|
||||||
|
const { theme = "system" } = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sonner
|
||||||
|
theme={theme as ToasterProps["theme"]}
|
||||||
|
className="toaster group"
|
||||||
|
toastOptions={{
|
||||||
|
classNames: {
|
||||||
|
toast:
|
||||||
|
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||||
|
description: "group-[.toast]:text-muted-foreground",
|
||||||
|
actionButton:
|
||||||
|
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||||
|
cancelButton:
|
||||||
|
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Toaster }
|
||||||
53
client-electron/src/components/ui/tabs.tsx
Normal file
53
client-electron/src/components/ui/tabs.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
|
|
@ -1,295 +0,0 @@
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
|
||||||
import { ADB_DEFAULT_DEVICE_FILTER } from '@yume-chan/adb-daemon-webusb'
|
|
||||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
|
||||||
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
|
||||||
import useAdb from '../hooks/useAdb'
|
|
||||||
import type { AdbScrcpyVideoStream } from '@yume-chan/adb-scrcpy'
|
|
||||||
import { AdbScrcpyClient, AdbScrcpyOptions1_22 } from '@yume-chan/adb-scrcpy'
|
|
||||||
import { WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs'
|
|
||||||
import {
|
|
||||||
AndroidKeyCode,
|
|
||||||
AndroidKeyEventAction,
|
|
||||||
AndroidMotionEventAction,
|
|
||||||
ScrcpyLogLevel1_18,
|
|
||||||
ScrcpyOptions1_25,
|
|
||||||
ScrcpyPointerId,
|
|
||||||
ScrcpyVideoCodecId
|
|
||||||
} from '@yume-chan/scrcpy'
|
|
||||||
import { useCallback, useRef, useState } from 'react'
|
|
||||||
import { ReadableStream, WritableStream, Consumable, DecodeUtf8Stream } from '@yume-chan/stream-extra'
|
|
||||||
import { useBeforeUnload } from 'react-router-dom'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { ArrowLeftIcon, HomeIcon } from '@radix-ui/react-icons'
|
|
||||||
|
|
||||||
const AndroidPage: React.FC = () => {
|
|
||||||
const { adb, manager, device, setAdb, setDevice } = useAdb(
|
|
||||||
useShallow(state => ({
|
|
||||||
adb: state.adb,
|
|
||||||
manager: state.manager,
|
|
||||||
device: state.device,
|
|
||||||
setAdb: state.setAdb,
|
|
||||||
setDevice: state.setDevice
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
async function createNewConnection() {
|
|
||||||
device?.raw.forget()
|
|
||||||
setDevice(undefined)
|
|
||||||
const selectedDevice = await manager?.requestDevice({
|
|
||||||
filters: [
|
|
||||||
{
|
|
||||||
...ADB_DEFAULT_DEVICE_FILTER,
|
|
||||||
serialNumber: 'd'
|
|
||||||
}
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!selectedDevice) {
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
setDevice(selectedDevice)
|
|
||||||
}
|
|
||||||
|
|
||||||
const connection = await selectedDevice.connect()
|
|
||||||
|
|
||||||
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
|
||||||
|
|
||||||
const transport = await AdbDaemonTransport.authenticate({
|
|
||||||
serial: selectedDevice.serial,
|
|
||||||
connection: connection,
|
|
||||||
credentialStore: credentialStore
|
|
||||||
})
|
|
||||||
|
|
||||||
const adb: Adb = new Adb(transport)
|
|
||||||
setAdb(adb)
|
|
||||||
}
|
|
||||||
|
|
||||||
const [client, setClient] = useState<AdbScrcpyClient | undefined>()
|
|
||||||
const [decoder, setDecoder] = useState<WebCodecsDecoder | undefined>()
|
|
||||||
const screenRef = useRef<HTMLDivElement>(null)
|
|
||||||
|
|
||||||
// when user close or refresh the page, close the adb connection
|
|
||||||
useBeforeUnload(
|
|
||||||
useCallback(() => {
|
|
||||||
decoder?.dispose()
|
|
||||||
client?.close()
|
|
||||||
adb?.close()
|
|
||||||
|
|
||||||
setDecoder(undefined)
|
|
||||||
setClient(undefined)
|
|
||||||
setAdb(undefined)
|
|
||||||
}, [])
|
|
||||||
)
|
|
||||||
|
|
||||||
async function scrcpyConnect() {
|
|
||||||
const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
|
|
||||||
res.arrayBuffer()
|
|
||||||
)
|
|
||||||
|
|
||||||
await AdbScrcpyClient.pushServer(
|
|
||||||
adb!,
|
|
||||||
new ReadableStream({
|
|
||||||
start(controller) {
|
|
||||||
controller.enqueue(new Consumable(new Uint8Array(server)))
|
|
||||||
controller.close()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const res = await adb!.subprocess.spawn(
|
|
||||||
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
|
|
||||||
)
|
|
||||||
|
|
||||||
res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
|
||||||
new WritableStream({
|
|
||||||
write(chunk) {
|
|
||||||
console.log(chunk)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
const scrcpyOption = new ScrcpyOptions1_25({
|
|
||||||
maxFps: 60,
|
|
||||||
bitRate: 4000000,
|
|
||||||
stayAwake: true,
|
|
||||||
control: true,
|
|
||||||
logLevel: ScrcpyLogLevel1_18.Debug
|
|
||||||
})
|
|
||||||
const _client = await AdbScrcpyClient.start(
|
|
||||||
adb!,
|
|
||||||
'/data/local/tmp/scrcpy-server.jar',
|
|
||||||
'1.25',
|
|
||||||
new AdbScrcpyOptions1_22(scrcpyOption)
|
|
||||||
)
|
|
||||||
|
|
||||||
const videoStream: AdbScrcpyVideoStream | undefined = await _client?.videoStream
|
|
||||||
|
|
||||||
if (videoStream) {
|
|
||||||
const _decoder = new WebCodecsDecoder(ScrcpyVideoCodecId.H264)
|
|
||||||
|
|
||||||
_decoder.renderer.style.width = '100%'
|
|
||||||
_decoder.renderer.style.height = '100%'
|
|
||||||
|
|
||||||
if (screenRef.current && screenRef.current.firstChild) {
|
|
||||||
screenRef.current.removeChild(screenRef.current.firstChild)
|
|
||||||
}
|
|
||||||
screenRef.current?.appendChild(_decoder.renderer)
|
|
||||||
videoStream?.stream.pipeTo(_decoder.writable)
|
|
||||||
setDecoder(_decoder)
|
|
||||||
|
|
||||||
if (_client.controlMessageWriter) {
|
|
||||||
_decoder.renderer.addEventListener('mousedown', e => {
|
|
||||||
// client width and height 700 x 400
|
|
||||||
const react = _decoder.renderer.getBoundingClientRect()
|
|
||||||
|
|
||||||
// normalize to _decoder.renderer.width and height 1080 x 1920
|
|
||||||
const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
|
|
||||||
const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
|
|
||||||
|
|
||||||
//console.log('mouse down at ' + x + ' ' + y)
|
|
||||||
_client.controlMessageWriter?.injectTouch({
|
|
||||||
action: AndroidMotionEventAction.Down,
|
|
||||||
pointerId: ScrcpyPointerId.Mouse | ScrcpyPointerId.Finger,
|
|
||||||
pointerX: x,
|
|
||||||
pointerY: y,
|
|
||||||
pressure: 1,
|
|
||||||
screenWidth: _decoder.renderer.width,
|
|
||||||
screenHeight: _decoder.renderer.height,
|
|
||||||
buttons: 0,
|
|
||||||
actionButton: 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
_decoder.renderer.addEventListener('mouseup', e => {
|
|
||||||
// client width and height 700 x 400
|
|
||||||
const react = _decoder.renderer.getBoundingClientRect()
|
|
||||||
|
|
||||||
// normalize to _decoder.renderer.width and height 1080 x 1920
|
|
||||||
const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
|
|
||||||
const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
|
|
||||||
|
|
||||||
//console.log('mouse up at ' + x + ' ' + y)
|
|
||||||
_client.controlMessageWriter?.injectTouch({
|
|
||||||
action: AndroidMotionEventAction.Up,
|
|
||||||
pointerId: ScrcpyPointerId.Mouse,
|
|
||||||
pointerX: x,
|
|
||||||
pointerY: y,
|
|
||||||
pressure: 1,
|
|
||||||
screenWidth: _decoder.renderer.width,
|
|
||||||
screenHeight: _decoder.renderer.height,
|
|
||||||
buttons: 0,
|
|
||||||
actionButton: 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
_decoder.renderer.addEventListener('mousemove', e => {
|
|
||||||
// client width and height 700 x 400
|
|
||||||
const react = _decoder.renderer.getBoundingClientRect()
|
|
||||||
|
|
||||||
// normalize to _decoder.renderer.width and height 1080 x 1920
|
|
||||||
const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
|
|
||||||
const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
|
|
||||||
|
|
||||||
//console.log('mouse move at ' + x + ' ' + y)
|
|
||||||
_client.controlMessageWriter?.injectTouch({
|
|
||||||
action: AndroidMotionEventAction.Move,
|
|
||||||
pointerId: ScrcpyPointerId.Mouse,
|
|
||||||
pointerX: x,
|
|
||||||
pointerY: y,
|
|
||||||
pressure: 1,
|
|
||||||
screenWidth: _decoder.renderer.width,
|
|
||||||
screenHeight: _decoder.renderer.height,
|
|
||||||
buttons: 0,
|
|
||||||
actionButton: 0
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setClient(_client)
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnectAdb() {
|
|
||||||
client?.close()
|
|
||||||
adb?.close()
|
|
||||||
setClient(undefined)
|
|
||||||
setAdb(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrcpyDisconnect() {
|
|
||||||
decoder?.dispose()
|
|
||||||
client?.close()
|
|
||||||
if (decoder && screenRef.current) {
|
|
||||||
screenRef.current.removeChild(decoder.renderer)
|
|
||||||
}
|
|
||||||
setClient(undefined)
|
|
||||||
setDecoder(undefined)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rebootDevice() {
|
|
||||||
const res = await adb?.power.reboot()
|
|
||||||
console.log('[rebootDevice] res: ', res)
|
|
||||||
}
|
|
||||||
|
|
||||||
function goHome() {
|
|
||||||
client?.controlMessageWriter?.injectKeyCode({
|
|
||||||
action: AndroidKeyEventAction.Up,
|
|
||||||
keyCode: AndroidKeyCode.AndroidHome,
|
|
||||||
metaState: 0,
|
|
||||||
repeat: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function goBack() {
|
|
||||||
client?.controlMessageWriter?.injectKeyCode({
|
|
||||||
action: AndroidKeyEventAction.Up,
|
|
||||||
keyCode: AndroidKeyCode.AndroidBack,
|
|
||||||
metaState: 0,
|
|
||||||
repeat: 0
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col justify-center items-center mx-auto">
|
|
||||||
<div className="flex justify-around items-start mx-auto w-full h-full">
|
|
||||||
<div>
|
|
||||||
<div ref={screenRef} className="min-h-[700px] min-w-[400px] max-h-[700px] max-w-[400px] bg-gray-700"></div>
|
|
||||||
{screenRef.current?.firstChild ? (
|
|
||||||
<div className="flex justify-around gap-4 mt-3">
|
|
||||||
<Button onClick={goBack} className="flex-grow">
|
|
||||||
<ArrowLeftIcon className="mr-3" />
|
|
||||||
Back
|
|
||||||
</Button>
|
|
||||||
<Button onClick={goHome} className="flex-grow">
|
|
||||||
<HomeIcon className="mr-3" />
|
|
||||||
Home
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<button
|
|
||||||
onClick={adb ? disconnectAdb : createNewConnection}
|
|
||||||
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
|
|
||||||
>
|
|
||||||
{adb ? 'Disconnect' : 'Connect'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={client ? scrcpyDisconnect : scrcpyConnect}
|
|
||||||
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
|
|
||||||
>
|
|
||||||
{client ? 'Disconnect Scrcpy' : 'Connect Scrcpy'}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={rebootDevice}
|
|
||||||
className="bg-white px-4 py-2 border flex gap-2 border-slate-200 rounded-lg text-slate-700 hover:border-slate-400 hover:text-slate-900 hover:shadow transition duration-150"
|
|
||||||
>
|
|
||||||
Reboot Device
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default AndroidPage
|
|
||||||
230
client-electron/src/pages/android/android.tsx
Normal file
230
client-electron/src/pages/android/android.tsx
Normal file
|
|
@ -0,0 +1,230 @@
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
import useAdb from '../../hooks/useAdb'
|
||||||
|
import { type AdbScrcpyClient } from '@yume-chan/adb-scrcpy'
|
||||||
|
import { type WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs'
|
||||||
|
import { useCallback, useState } from 'react'
|
||||||
|
import { useBeforeUnload } from 'react-router-dom'
|
||||||
|
import { ToolBar } from './components/tool-bar'
|
||||||
|
import { ScrcpyTab } from './components/scrcpy-tab'
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
import { Toaster } from '@/components/ui/toaster'
|
||||||
|
import { ShellTab } from './components/shell-tab'
|
||||||
|
|
||||||
|
const AndroidPage: React.FC = () => {
|
||||||
|
const { adb, setAdb } = useAdb(
|
||||||
|
useShallow(state => ({
|
||||||
|
adb: state.adb,
|
||||||
|
manager: state.manager,
|
||||||
|
device: state.device,
|
||||||
|
setAdb: state.setAdb,
|
||||||
|
setDevice: state.setDevice
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const [client, setClient] = useState<AdbScrcpyClient | undefined>()
|
||||||
|
const [decoder, setDecoder] = useState<WebCodecsDecoder | undefined>()
|
||||||
|
|
||||||
|
// when user close or refresh the page, close the adb connection
|
||||||
|
useBeforeUnload(
|
||||||
|
useCallback(() => {
|
||||||
|
decoder?.dispose()
|
||||||
|
client?.close()
|
||||||
|
adb?.close()
|
||||||
|
|
||||||
|
setDecoder(undefined)
|
||||||
|
setClient(undefined)
|
||||||
|
setAdb(undefined)
|
||||||
|
}, [])
|
||||||
|
)
|
||||||
|
|
||||||
|
// async function scrcpyConnect() {
|
||||||
|
// const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
|
||||||
|
// res.arrayBuffer()
|
||||||
|
// )
|
||||||
|
|
||||||
|
// await AdbScrcpyClient.pushServer(
|
||||||
|
// adb!,
|
||||||
|
// new ReadableStream({
|
||||||
|
// start(controller) {
|
||||||
|
// controller.enqueue(new Consumable(new Uint8Array(server)))
|
||||||
|
// controller.close()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const res = await adb!.subprocess.spawn(
|
||||||
|
// 'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
|
||||||
|
// )
|
||||||
|
|
||||||
|
// res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
||||||
|
// new WritableStream({
|
||||||
|
// write(chunk) {
|
||||||
|
// console.log(chunk)
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const scrcpyOption = new ScrcpyOptions1_25({
|
||||||
|
// maxFps: 60,
|
||||||
|
// bitRate: 4000000,
|
||||||
|
// stayAwake: true,
|
||||||
|
// control: true,
|
||||||
|
// logLevel: ScrcpyLogLevel1_18.Debug
|
||||||
|
// })
|
||||||
|
// const _client = await AdbScrcpyClient.start(
|
||||||
|
// adb!,
|
||||||
|
// '/data/local/tmp/scrcpy-server.jar',
|
||||||
|
// '1.25',
|
||||||
|
// new AdbScrcpyOptions1_22(scrcpyOption)
|
||||||
|
// )
|
||||||
|
|
||||||
|
// const videoStream: AdbScrcpyVideoStream | undefined = await _client?.videoStream
|
||||||
|
|
||||||
|
// if (videoStream) {
|
||||||
|
// const _decoder = new WebCodecsDecoder(ScrcpyVideoCodecId.H264)
|
||||||
|
|
||||||
|
// _decoder.renderer.style.width = '100%'
|
||||||
|
// _decoder.renderer.style.height = '100%'
|
||||||
|
|
||||||
|
// if (screenRef.current && screenRef.current.firstChild) {
|
||||||
|
// screenRef.current.removeChild(screenRef.current.firstChild)
|
||||||
|
// }
|
||||||
|
// screenRef.current?.appendChild(_decoder.renderer)
|
||||||
|
// videoStream?.stream.pipeTo(_decoder.writable)
|
||||||
|
// setDecoder(_decoder)
|
||||||
|
|
||||||
|
// if (_client.controlMessageWriter) {
|
||||||
|
// _decoder.renderer.addEventListener('mousedown', e => {
|
||||||
|
// // client width and height 700 x 400
|
||||||
|
// const react = _decoder.renderer.getBoundingClientRect()
|
||||||
|
|
||||||
|
// // normalize to _decoder.renderer.width and height 1080 x 1920
|
||||||
|
// const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
|
||||||
|
// const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
|
||||||
|
|
||||||
|
// //console.log('mouse down at ' + x + ' ' + y)
|
||||||
|
// _client.controlMessageWriter?.injectTouch({
|
||||||
|
// action: AndroidMotionEventAction.Down,
|
||||||
|
// pointerId: ScrcpyPointerId.Mouse | ScrcpyPointerId.Finger,
|
||||||
|
// pointerX: x,
|
||||||
|
// pointerY: y,
|
||||||
|
// pressure: 1,
|
||||||
|
// screenWidth: _decoder.renderer.width,
|
||||||
|
// screenHeight: _decoder.renderer.height,
|
||||||
|
// buttons: 0,
|
||||||
|
// actionButton: 0
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// _decoder.renderer.addEventListener('mouseup', e => {
|
||||||
|
// // client width and height 700 x 400
|
||||||
|
// const react = _decoder.renderer.getBoundingClientRect()
|
||||||
|
|
||||||
|
// // normalize to _decoder.renderer.width and height 1080 x 1920
|
||||||
|
// const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
|
||||||
|
// const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
|
||||||
|
|
||||||
|
// //console.log('mouse up at ' + x + ' ' + y)
|
||||||
|
// _client.controlMessageWriter?.injectTouch({
|
||||||
|
// action: AndroidMotionEventAction.Up,
|
||||||
|
// pointerId: ScrcpyPointerId.Mouse,
|
||||||
|
// pointerX: x,
|
||||||
|
// pointerY: y,
|
||||||
|
// pressure: 1,
|
||||||
|
// screenWidth: _decoder.renderer.width,
|
||||||
|
// screenHeight: _decoder.renderer.height,
|
||||||
|
// buttons: 0,
|
||||||
|
// actionButton: 0
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// _decoder.renderer.addEventListener('mousemove', e => {
|
||||||
|
// // client width and height 700 x 400
|
||||||
|
// const react = _decoder.renderer.getBoundingClientRect()
|
||||||
|
|
||||||
|
// // normalize to _decoder.renderer.width and height 1080 x 1920
|
||||||
|
// const x = ((e.clientX - react.left) * _decoder.renderer.width) / react.width
|
||||||
|
// const y = ((e.clientY - react.top) * _decoder.renderer.height) / react.height
|
||||||
|
|
||||||
|
// //console.log('mouse move at ' + x + ' ' + y)
|
||||||
|
// _client.controlMessageWriter?.injectTouch({
|
||||||
|
// action: AndroidMotionEventAction.Move,
|
||||||
|
// pointerId: ScrcpyPointerId.Mouse,
|
||||||
|
// pointerX: x,
|
||||||
|
// pointerY: y,
|
||||||
|
// pressure: 1,
|
||||||
|
// screenWidth: _decoder.renderer.width,
|
||||||
|
// screenHeight: _decoder.renderer.height,
|
||||||
|
// buttons: 0,
|
||||||
|
// actionButton: 0
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setClient(_client)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function disconnectAdb() {
|
||||||
|
// client?.close()
|
||||||
|
// adb?.close()
|
||||||
|
// setClient(undefined)
|
||||||
|
// setAdb(undefined)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function scrcpyDisconnect() {
|
||||||
|
// decoder?.dispose()
|
||||||
|
// client?.close()
|
||||||
|
// if (decoder && screenRef.current) {
|
||||||
|
// screenRef.current.removeChild(decoder.renderer)
|
||||||
|
// }
|
||||||
|
// setClient(undefined)
|
||||||
|
// setDecoder(undefined)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async function rebootDevice() {
|
||||||
|
// const res = await adb?.power.reboot()
|
||||||
|
// console.log('[rebootDevice] res: ', res)
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function goHome() {
|
||||||
|
// client?.controlMessageWriter?.injectKeyCode({
|
||||||
|
// action: AndroidKeyEventAction.Up,
|
||||||
|
// keyCode: AndroidKeyCode.AndroidHome,
|
||||||
|
// metaState: 0,
|
||||||
|
// repeat: 0
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function goBack() {
|
||||||
|
// client?.controlMessageWriter?.injectKeyCode({
|
||||||
|
// action: AndroidKeyEventAction.Up,
|
||||||
|
// keyCode: AndroidKeyCode.AndroidBack,
|
||||||
|
// metaState: 0,
|
||||||
|
// repeat: 0
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full">
|
||||||
|
<div className="flex w-full p-5">
|
||||||
|
<ToolBar />
|
||||||
|
</div>
|
||||||
|
<Tabs defaultValue="scrcpy" className="w-full">
|
||||||
|
<TabsList className="grid w-full grid-cols-2">
|
||||||
|
<TabsTrigger value="scrcpy">Scrcpy</TabsTrigger>
|
||||||
|
<TabsTrigger value="shell">Shell</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="scrcpy">
|
||||||
|
<ScrcpyTab />
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="shell">
|
||||||
|
<ShellTab />
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
<Toaster />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default AndroidPage
|
||||||
113
client-electron/src/pages/android/components/scrcpy-tab.tsx
Normal file
113
client-electron/src/pages/android/components/scrcpy-tab.tsx
Normal file
|
|
@ -0,0 +1,113 @@
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||||
|
import useAdb from '@/hooks/useAdb'
|
||||||
|
import { type AdbSubprocessProtocol } from '@yume-chan/adb'
|
||||||
|
import { WritableStream } from '@yume-chan/stream-extra'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { Terminal } from 'xterm'
|
||||||
|
import { FitAddon } from 'xterm-addon-fit'
|
||||||
|
|
||||||
|
import 'xterm/css/xterm.css'
|
||||||
|
|
||||||
|
export const ScrcpyTab: React.FC = () => {
|
||||||
|
const adb = useAdb(state => state.adb)
|
||||||
|
|
||||||
|
const logcatRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [process, setProcess] = useState<AdbSubprocessProtocol | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const startTerminal = async () => {
|
||||||
|
if (logcatRef.current && adb) {
|
||||||
|
if (logcatRef.current.children.length > 0) {
|
||||||
|
// remove all children from the logcatRef
|
||||||
|
while (logcatRef.current.firstChild) {
|
||||||
|
logcatRef.current.removeChild(logcatRef.current.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const terminal: Terminal = new Terminal()
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
terminal.loadAddon(fitAddon)
|
||||||
|
|
||||||
|
const process: AdbSubprocessProtocol = await adb.subprocess.shell('logcat')
|
||||||
|
process.stdout.pipeTo(
|
||||||
|
new WritableStream<Uint8Array>({
|
||||||
|
write(chunk) {
|
||||||
|
terminal.write(chunk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
terminal.options.disableStdin = true
|
||||||
|
terminal.options.theme = {
|
||||||
|
background: '#1e1e1e',
|
||||||
|
foreground: '#d4d4d4'
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.open(logcatRef.current)
|
||||||
|
fitAddon.fit()
|
||||||
|
setProcess(process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startTerminal()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
logcatRef.current && logcatRef.current.firstChild && logcatRef.current.removeChild(logcatRef.current.firstChild)
|
||||||
|
process?.stderr.cancel()
|
||||||
|
process?.stdout.cancel()
|
||||||
|
process?.kill()
|
||||||
|
}
|
||||||
|
}, [logcatRef, adb])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Scrcpy</CardTitle>
|
||||||
|
<CardDescription>Stream and control your Android device from your computer</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex w-full justify-around items-start">
|
||||||
|
<div>
|
||||||
|
<div className="w-[450px] h-[800px] bg-slate-700" />
|
||||||
|
<div className="flex pt-3 justify-center items-center space-x-4 w-[450px]">
|
||||||
|
<Button variant={'outline'} className="flex-1">
|
||||||
|
Home
|
||||||
|
</Button>
|
||||||
|
<Button variant={'outline'} className="flex-1">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col space-y-4 w-full px-5">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Control</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex space-x-4 items-center">
|
||||||
|
<Button>Connect</Button>
|
||||||
|
<Button>Power</Button>
|
||||||
|
<Button>Volume Up</Button>
|
||||||
|
<Button>Volume Down</Button>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* logcat card */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Logcat</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex space-x-4 items-center">
|
||||||
|
<div className="w-full h-96 bg-slate-700" ref={logcatRef} />
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<Button>Save changes</Button>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
100
client-electron/src/pages/android/components/shell-tab.tsx
Normal file
100
client-electron/src/pages/android/components/shell-tab.tsx
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||||
|
import useAdb from '@/hooks/useAdb'
|
||||||
|
import { encodeUtf8, type AdbSubprocessProtocol } from '@yume-chan/adb'
|
||||||
|
import { Consumable, WritableStream } from '@yume-chan/stream-extra'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
|
import { Terminal } from 'xterm'
|
||||||
|
import { FitAddon } from 'xterm-addon-fit'
|
||||||
|
|
||||||
|
import 'xterm/css/xterm.css'
|
||||||
|
|
||||||
|
export const ShellTab: React.FC = () => {
|
||||||
|
const adb = useAdb(state => state.adb)
|
||||||
|
|
||||||
|
const shellRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
|
const [process, setProcess] = useState<AdbSubprocessProtocol | undefined>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const startTerminal = async () => {
|
||||||
|
if (shellRef.current && adb) {
|
||||||
|
if (shellRef.current.children.length > 0) {
|
||||||
|
// remove all children from the shellRef
|
||||||
|
while (shellRef.current.firstChild) {
|
||||||
|
shellRef.current.removeChild(shellRef.current.firstChild)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const terminal: Terminal = new Terminal()
|
||||||
|
const fitAddon = new FitAddon()
|
||||||
|
terminal.loadAddon(fitAddon)
|
||||||
|
|
||||||
|
const process: AdbSubprocessProtocol = await adb.subprocess.shell(
|
||||||
|
'/data/data/com.termux/files/usr/bin/telnet localhost 45515'
|
||||||
|
)
|
||||||
|
process.stdout.pipeTo(
|
||||||
|
new WritableStream<Uint8Array>({
|
||||||
|
write(chunk) {
|
||||||
|
terminal.write(chunk)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const writer = process.stdin.getWriter()
|
||||||
|
terminal.onData(data => {
|
||||||
|
const buffer = encodeUtf8(data)
|
||||||
|
const consumable = new Consumable(buffer)
|
||||||
|
writer.write(consumable)
|
||||||
|
})
|
||||||
|
|
||||||
|
terminal.options.cursorBlink = true
|
||||||
|
terminal.options.theme = {
|
||||||
|
background: '#1e1e1e',
|
||||||
|
foreground: '#d4d4d4'
|
||||||
|
}
|
||||||
|
|
||||||
|
terminal.open(shellRef.current)
|
||||||
|
fitAddon.fit()
|
||||||
|
setProcess(process)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
startTerminal()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
console.log('cleaning up shell')
|
||||||
|
shellRef.current && shellRef.current.firstChild && shellRef.current.removeChild(shellRef.current.firstChild)
|
||||||
|
process?.stderr.cancel()
|
||||||
|
process?.stdin.close()
|
||||||
|
process?.stdout.cancel()
|
||||||
|
process?.kill()
|
||||||
|
}
|
||||||
|
}, [shellRef, adb])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Shell</CardTitle>
|
||||||
|
<CardDescription>Access your device's shell using a terminal emulator</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex items-center justify-end py-3 w-full">
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant={'destructive'}
|
||||||
|
onClick={() => {
|
||||||
|
process?.stderr.cancel()
|
||||||
|
process?.stdin.close()
|
||||||
|
process?.stdout.cancel()
|
||||||
|
process?.kill()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Kill Shell
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-full h-[800px] bg-slate-700" ref={shellRef}></div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
158
client-electron/src/pages/android/components/tool-bar.tsx
Normal file
158
client-electron/src/pages/android/components/tool-bar.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
DialogTrigger
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
import { toast } from '@/components/ui/use-toast'
|
||||||
|
import useAdb from '@/hooks/useAdb'
|
||||||
|
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
||||||
|
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||||
|
import { ADB_DEFAULT_DEVICE_FILTER } from '@yume-chan/adb-daemon-webusb'
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { useShallow } from 'zustand/react/shallow'
|
||||||
|
|
||||||
|
export const ToolBar: React.FC = () => {
|
||||||
|
const { manager, device, adb, setDevice, setAdb } = useAdb(
|
||||||
|
useShallow(state => ({
|
||||||
|
manager: state.manager,
|
||||||
|
device: state.device,
|
||||||
|
adb: state.adb,
|
||||||
|
setDevice: state.setDevice,
|
||||||
|
setAdb: state.setAdb
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
const [name, setName] = useState<string>('')
|
||||||
|
const [resolution, setResolution] = useState<string>('')
|
||||||
|
const [version, setVersion] = useState<string>('')
|
||||||
|
|
||||||
|
async function createNewConnection() {
|
||||||
|
let selectedDevice
|
||||||
|
|
||||||
|
if (!device) {
|
||||||
|
selectedDevice = await manager?.requestDevice({
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
...ADB_DEFAULT_DEVICE_FILTER,
|
||||||
|
serialNumber: 'd'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!selectedDevice) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
setDevice(selectedDevice)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
selectedDevice = device
|
||||||
|
}
|
||||||
|
|
||||||
|
let connection
|
||||||
|
try {
|
||||||
|
connection = await selectedDevice.connect()
|
||||||
|
} catch (e) {
|
||||||
|
toast({
|
||||||
|
duration: 5000,
|
||||||
|
variant: 'destructive',
|
||||||
|
title: 'Failed to connect to device',
|
||||||
|
description: (e as Error).message
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||||
|
|
||||||
|
const transport = await AdbDaemonTransport.authenticate({
|
||||||
|
serial: selectedDevice.serial,
|
||||||
|
connection: connection,
|
||||||
|
credentialStore: credentialStore
|
||||||
|
})
|
||||||
|
|
||||||
|
const adb: Adb = new Adb(transport)
|
||||||
|
|
||||||
|
const name = await adb.getProp('ro.product.model')
|
||||||
|
const version = await adb.getProp('ro.build.version.release')
|
||||||
|
|
||||||
|
setName(name)
|
||||||
|
setResolution(resolution)
|
||||||
|
setVersion(version)
|
||||||
|
|
||||||
|
setAdb(adb)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisconnect() {
|
||||||
|
device?.raw.forget()
|
||||||
|
setDevice(undefined)
|
||||||
|
|
||||||
|
adb?.close()
|
||||||
|
setAdb(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTerminate() {
|
||||||
|
adb?.close()
|
||||||
|
setAdb(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between items-center space-x-5 w-full p-4 shadow-lg rounded-lg">
|
||||||
|
{adb ? (
|
||||||
|
<div className="flex flex-col justify-center items-start">
|
||||||
|
<ul className="list-disc pl-4">
|
||||||
|
<li>Name: {name}</li>
|
||||||
|
<li>Version: {version}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col justify-center items-start">
|
||||||
|
<h2>No Device Connected</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
{adb ? (
|
||||||
|
<DisconnectConfirmDialog onDisconnect={onDisconnect} onTerminate={onTerminate} />
|
||||||
|
) : (
|
||||||
|
<Button variant={'default'} onClick={createNewConnection}>
|
||||||
|
Connect
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DisconnectConfirmDialogProps {
|
||||||
|
onDisconnect: () => void
|
||||||
|
onTerminate: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const DisconnectConfirmDialog: React.FC<DisconnectConfirmDialogProps> = ({ onDisconnect, onTerminate }) => {
|
||||||
|
return (
|
||||||
|
<Dialog>
|
||||||
|
<DialogTrigger asChild>
|
||||||
|
<Button variant={'destructive'}>Disconnect</Button>
|
||||||
|
</DialogTrigger>
|
||||||
|
<DialogContent className="sm:max-w-[600px]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Disconnect Device</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Do you want to also declaim device? if so press Disconnect else press Terminate
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="destructive" onClick={onDisconnect}>
|
||||||
|
Disconnect
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={onTerminate}>
|
||||||
|
Terminate
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue