Fixed: fixed bug scrcpy and shell is disconnect when switch page
This commit is contained in:
parent
9543d4541c
commit
0fe469b5c6
43 changed files with 1378 additions and 1366 deletions
62
client-electron/electron/adb.ts
Normal file
62
client-electron/electron/adb.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
import { type BrowserWindow } from 'electron/main'
|
||||
import { AdbServerNodeTcpConnector } from '@yume-chan/adb-server-node-tcp'
|
||||
import type { AdbServerDevice, AdbServerTransport } from '@yume-chan/adb'
|
||||
import { Adb, AdbServerClient } from '@yume-chan/adb'
|
||||
import { DecodeUtf8Stream, WritableStream } from '@yume-chan/stream-extra'
|
||||
|
||||
let adb: Adb | undefined
|
||||
|
||||
export function AdbDaemon(_win: BrowserWindow | null, ipcMain: Electron.IpcMain) {
|
||||
ipcMain.handle('adb', async () => {
|
||||
await createConnection()
|
||||
})
|
||||
|
||||
ipcMain.handle('adb:shell', async (event, command: string) => {
|
||||
if (!adb) {
|
||||
return
|
||||
}
|
||||
|
||||
const process = await adb.subprocess.shell(command)
|
||||
let result: string | null = null
|
||||
await process.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
result += chunk
|
||||
}
|
||||
})
|
||||
)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
async function createConnection() {
|
||||
const connector: AdbServerNodeTcpConnector = new AdbServerNodeTcpConnector({
|
||||
host: 'localhost',
|
||||
port: 5037
|
||||
})
|
||||
|
||||
console.log('Connecting to ADB server...')
|
||||
connector.connect()
|
||||
|
||||
const client: AdbServerClient = new AdbServerClient(connector)
|
||||
|
||||
const devices: AdbServerDevice[] = await client.getDevices()
|
||||
if (devices.length === 0) {
|
||||
console.log('No device found')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Devices found:', devices.map(device => device.serial).join(', '))
|
||||
|
||||
const device: AdbServerDevice | undefined = devices.find(device => device.serial === 'd')
|
||||
|
||||
if (!device) {
|
||||
console.log('No device found')
|
||||
return
|
||||
}
|
||||
|
||||
console.log('Device found:', device.serial)
|
||||
|
||||
const transport: AdbServerTransport = await client.createTransport(device)
|
||||
adb = new Adb(transport)
|
||||
}
|
||||
|
|
@ -1,22 +1,19 @@
|
|||
import { findCredentials, getPassword, setPassword, deletePassword } from '@postman/node-keytar'
|
||||
|
||||
export function eventGetKeyChain(icpMain: Electron.IpcMain) {
|
||||
icpMain.on('get-keyChain', (event, serviceName, account) => {
|
||||
getPassword(serviceName, account).then(password => {
|
||||
event.returnValue = password
|
||||
})
|
||||
icpMain.handle('get-keyChain', async (_event, serviceName, account) => {
|
||||
return getPassword(serviceName, account)
|
||||
})
|
||||
|
||||
icpMain.on('set-keyChain', (_event, { serviceName, account, password }) => {
|
||||
setPassword(serviceName, account, password)
|
||||
icpMain.handle('set-keyChain', async (_event, { serviceName, account, password }) => {
|
||||
return setPassword(serviceName, account, password)
|
||||
})
|
||||
|
||||
icpMain.on('delete-keyChain', (_event, { serviceName, account }) => {
|
||||
deletePassword(serviceName, account)
|
||||
icpMain.handle('delete-keyChain', async (_event, { serviceName, account }) => {
|
||||
return deletePassword(serviceName, account)
|
||||
})
|
||||
|
||||
icpMain.handle('keyChainSync', async (_event, serviceName) => {
|
||||
const credentials = await findCredentials(serviceName)
|
||||
return credentials
|
||||
return findCredentials(serviceName)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { app, BrowserWindow, ipcMain, shell } from 'electron'
|
|||
import path from 'node:path'
|
||||
import deeplink from './deeplink'
|
||||
import { eventGetKeyChain } from './keychain'
|
||||
import { AdbDaemon } from './adb'
|
||||
|
||||
// The built directory structure
|
||||
//
|
||||
|
|
@ -122,6 +123,9 @@ app.whenReady().then(() => {
|
|||
// deeplink
|
||||
deeplink(app, win, ipcMain, shell)
|
||||
|
||||
// adb
|
||||
AdbDaemon(win, ipcMain)
|
||||
|
||||
//keychain
|
||||
eventGetKeyChain(ipcMain)
|
||||
})
|
||||
|
|
|
|||
197
client-electron/package-lock.json
generated
197
client-electron/package-lock.json
generated
|
|
@ -23,6 +23,7 @@
|
|||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@tanstack/match-sorter-utils": "^8.11.8",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"@tanstack/react-table": "^8.11.7",
|
||||
"@uppy/core": "^3.8.0",
|
||||
|
|
@ -37,6 +38,7 @@
|
|||
"@yume-chan/adb-credential-web": "^0.0.22",
|
||||
"@yume-chan/adb-daemon-webusb": "^0.0.22",
|
||||
"@yume-chan/adb-scrcpy": "^0.0.22",
|
||||
"@yume-chan/adb-server-node-tcp": "^0.0.22",
|
||||
"@yume-chan/scrcpy": "^0.0.22",
|
||||
"@yume-chan/scrcpy-decoder-webcodecs": "^0.0.22",
|
||||
"@yume-chan/stream-extra": "^0.0.22",
|
||||
|
|
@ -44,9 +46,11 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.49.3",
|
||||
|
|
@ -4137,6 +4141,21 @@
|
|||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/match-sorter-utils": {
|
||||
"version": "8.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz",
|
||||
"integrity": "sha512-3VPh0SYMGCa5dWQEqNab87UpCMk+ANWHDP4ALs5PeEW9EpfTAbrezzaOk/OiM52IESViefkoAOYuxdoa04p6aA==",
|
||||
"dependencies": {
|
||||
"remove-accents": "0.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/tannerlinsley"
|
||||
}
|
||||
},
|
||||
"node_modules/@tanstack/query-core": {
|
||||
"version": "5.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.19.tgz",
|
||||
|
|
@ -4937,6 +4956,17 @@
|
|||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@yume-chan/adb-server-node-tcp": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@yume-chan/adb-server-node-tcp/-/adb-server-node-tcp-0.0.22.tgz",
|
||||
"integrity": "sha512-E2EblLwiY1T6SI2TONuV57izJvW8KBakmgvBCigAbM6I31K3EwqocjnDkH81TQAORO6O6vg7Chv7A5Z8SI28EQ==",
|
||||
"dependencies": {
|
||||
"@yume-chan/adb": "^0.0.22",
|
||||
"@yume-chan/stream-extra": "^0.0.22",
|
||||
"@yume-chan/struct": "^0.0.22",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@yume-chan/async": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@yume-chan/async/-/async-2.2.0.tgz",
|
||||
|
|
@ -6524,6 +6554,22 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/concurrently/node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
}
|
||||
},
|
||||
"node_modules/concurrently/node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -6685,19 +6731,12 @@
|
|||
"integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.11"
|
||||
},
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
|
||||
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/date-fns"
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
|
|
@ -8918,11 +8957,18 @@
|
|||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"dev": true
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
|
|
@ -9275,6 +9321,12 @@
|
|||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
|
|
@ -11389,6 +11441,19 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-day-picker": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz",
|
||||
"integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==",
|
||||
"funding": {
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/gpbl"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"date-fns": "^2.28.0 || ^3.0.0",
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
|
|
@ -11737,6 +11802,11 @@
|
|||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/remove-accents": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||
"integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -12215,16 +12285,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socks": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
|
||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz",
|
||||
"integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"ip": "^2.0.0",
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.13.0",
|
||||
"node": ">= 10.0.0",
|
||||
"npm": ">= 3.0.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -12320,8 +12390,7 @@
|
|||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ssri": {
|
||||
"version": "9.0.1",
|
||||
|
|
@ -16303,6 +16372,14 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"@tanstack/match-sorter-utils": {
|
||||
"version": "8.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.11.8.tgz",
|
||||
"integrity": "sha512-3VPh0SYMGCa5dWQEqNab87UpCMk+ANWHDP4ALs5PeEW9EpfTAbrezzaOk/OiM52IESViefkoAOYuxdoa04p6aA==",
|
||||
"requires": {
|
||||
"remove-accents": "0.4.2"
|
||||
}
|
||||
},
|
||||
"@tanstack/query-core": {
|
||||
"version": "5.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.17.19.tgz",
|
||||
|
|
@ -16887,6 +16964,17 @@
|
|||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"@yume-chan/adb-server-node-tcp": {
|
||||
"version": "0.0.22",
|
||||
"resolved": "https://registry.npmjs.org/@yume-chan/adb-server-node-tcp/-/adb-server-node-tcp-0.0.22.tgz",
|
||||
"integrity": "sha512-E2EblLwiY1T6SI2TONuV57izJvW8KBakmgvBCigAbM6I31K3EwqocjnDkH81TQAORO6O6vg7Chv7A5Z8SI28EQ==",
|
||||
"requires": {
|
||||
"@yume-chan/adb": "^0.0.22",
|
||||
"@yume-chan/stream-extra": "^0.0.22",
|
||||
"@yume-chan/struct": "^0.0.22",
|
||||
"tslib": "^2.6.2"
|
||||
}
|
||||
},
|
||||
"@yume-chan/async": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@yume-chan/async/-/async-2.2.0.tgz",
|
||||
|
|
@ -18065,6 +18153,15 @@
|
|||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
}
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
|
|
@ -18181,13 +18278,9 @@
|
|||
"integrity": "sha512-p6JFxJc3M4OTD2li2qaHkDCw9SfMw82Ldr6OC9Je1aXiGfhx2W8p3GaoeaGrPJTUN9NirTM/KTxHWMUdR1rsUg=="
|
||||
},
|
||||
"date-fns": {
|
||||
"version": "2.30.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
|
||||
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.21.0"
|
||||
}
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.3.1.tgz",
|
||||
"integrity": "sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw=="
|
||||
},
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
|
|
@ -19882,11 +19975,15 @@
|
|||
"loose-envify": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"ip": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz",
|
||||
"integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==",
|
||||
"dev": true
|
||||
"ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
"integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jsbn": "1.1.0",
|
||||
"sprintf-js": "^1.1.3"
|
||||
}
|
||||
},
|
||||
"is-arrayish": {
|
||||
"version": "0.2.1",
|
||||
|
|
@ -20134,6 +20231,12 @@
|
|||
"argparse": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"jsbn": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz",
|
||||
"integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==",
|
||||
"dev": true
|
||||
},
|
||||
"jsesc": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
|
|
@ -21682,6 +21785,12 @@
|
|||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"react-day-picker": {
|
||||
"version": "8.10.0",
|
||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.0.tgz",
|
||||
"integrity": "sha512-mz+qeyrOM7++1NCb1ARXmkjMkzWVh2GL9YiPbRjKe0zHccvekk4HE+0MPOZOrosn8r8zTHIIeOUXTmXRqmkRmg==",
|
||||
"requires": {}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "18.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||
|
|
@ -21920,6 +22029,11 @@
|
|||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"remove-accents": {
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz",
|
||||
"integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA=="
|
||||
},
|
||||
"require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
|
|
@ -22248,12 +22362,12 @@
|
|||
"dev": true
|
||||
},
|
||||
"socks": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz",
|
||||
"integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==",
|
||||
"version": "2.7.3",
|
||||
"resolved": "https://registry.npmjs.org/socks/-/socks-2.7.3.tgz",
|
||||
"integrity": "sha512-vfuYK48HXCTFD03G/1/zkIls3Ebr2YNa4qU9gHDZdblHLiqhJrJGkY3+0Nx0JpN9qBhJbVObc1CNciT1bIZJxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"ip": "^2.0.0",
|
||||
"ip-address": "^9.0.5",
|
||||
"smart-buffer": "^4.2.0"
|
||||
}
|
||||
},
|
||||
|
|
@ -22337,8 +22451,7 @@
|
|||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz",
|
||||
"integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
"dev": true
|
||||
},
|
||||
"ssri": {
|
||||
"version": "9.0.1",
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@
|
|||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@tanstack/match-sorter-utils": "^8.11.8",
|
||||
"@tanstack/react-query": "^5.17.19",
|
||||
"@tanstack/react-table": "^8.11.7",
|
||||
"@uppy/core": "^3.8.0",
|
||||
|
|
@ -49,6 +50,7 @@
|
|||
"@yume-chan/adb-credential-web": "^0.0.22",
|
||||
"@yume-chan/adb-daemon-webusb": "^0.0.22",
|
||||
"@yume-chan/adb-scrcpy": "^0.0.22",
|
||||
"@yume-chan/adb-server-node-tcp": "^0.0.22",
|
||||
"@yume-chan/scrcpy": "^0.0.22",
|
||||
"@yume-chan/scrcpy-decoder-webcodecs": "^0.0.22",
|
||||
"@yume-chan/stream-extra": "^0.0.22",
|
||||
|
|
@ -56,9 +58,11 @@
|
|||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"cmdk": "^0.2.0",
|
||||
"date-fns": "^3.3.1",
|
||||
"jszip": "^3.10.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"react": "^18.2.0",
|
||||
"react-day-picker": "^8.10.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.49.3",
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@ import MainLayout from './layouts/MainLayout'
|
|||
import HomePage from './pages/home'
|
||||
import LoginPage from './pages/login'
|
||||
import AndroidPage from './pages/android/android'
|
||||
import RecipesPage from './pages/recipes/recipes'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import UploadPage from './pages/upload'
|
||||
import { type MenuList } from './components/sidebar'
|
||||
import { DashboardIcon, FileTextIcon, RocketIcon, UploadIcon } from '@radix-ui/react-icons'
|
||||
import SelectCountryPage from './pages/recipes/select-country'
|
||||
import RecipesTablePage from './pages/recipes/recipe-table'
|
||||
|
||||
const sideBarMenuList: MenuList = [
|
||||
{
|
||||
|
|
@ -19,7 +20,7 @@ const sideBarMenuList: MenuList = [
|
|||
{
|
||||
title: 'Recipes',
|
||||
icon: FileTextIcon,
|
||||
link: '/recipes'
|
||||
link: '/recipes/select-country'
|
||||
},
|
||||
{
|
||||
title: 'Android',
|
||||
|
|
@ -44,8 +45,12 @@ function router() {
|
|||
element: <HomePage />
|
||||
},
|
||||
{
|
||||
path: 'recipes',
|
||||
element: <RecipesPage />
|
||||
path: 'recipes/select-country',
|
||||
element: <SelectCountryPage />
|
||||
},
|
||||
{
|
||||
path: 'recipes/:country_id/:filename',
|
||||
element: <RecipesTablePage />
|
||||
},
|
||||
{
|
||||
path: 'android',
|
||||
|
|
@ -68,7 +73,7 @@ function router() {
|
|||
{
|
||||
path: '*',
|
||||
element: (
|
||||
<div className="flex flex-col items-center justify-center h-screen">
|
||||
<div className="flex h-screen flex-col items-center justify-center">
|
||||
<h1 className="text-3xl font-bold text-gray-900">404</h1>
|
||||
<p className="text-gray-500">Page Not Found</p>
|
||||
</div>
|
||||
|
|
|
|||
70
client-electron/src/components/ui/calendar.tsx
Normal file
70
client-electron/src/components/ui/calendar.tsx
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import * as React from "react"
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from "@radix-ui/react-icons"
|
||||
import { DayPicker } from "react-day-picker"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { buttonVariants } from "@/components/ui/button"
|
||||
|
||||
export type CalendarProps = React.ComponentProps<typeof DayPicker>
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
...props
|
||||
}: CalendarProps) {
|
||||
return (
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn("p-3", className)}
|
||||
classNames={{
|
||||
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
|
||||
month: "space-y-4",
|
||||
caption: "flex justify-center pt-1 relative items-center",
|
||||
caption_label: "text-sm font-medium",
|
||||
nav: "space-x-1 flex items-center",
|
||||
nav_button: cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
|
||||
),
|
||||
nav_button_previous: "absolute left-1",
|
||||
nav_button_next: "absolute right-1",
|
||||
table: "w-full border-collapse space-y-1",
|
||||
head_row: "flex",
|
||||
head_cell:
|
||||
"text-muted-foreground rounded-md w-8 font-normal text-[0.8rem]",
|
||||
row: "flex w-full mt-2",
|
||||
cell: cn(
|
||||
"relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-accent [&:has([aria-selected].day-outside)]:bg-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md",
|
||||
props.mode === "range"
|
||||
? "[&:has(>.day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
|
||||
: "[&:has([aria-selected])]:rounded-md"
|
||||
),
|
||||
day: cn(
|
||||
buttonVariants({ variant: "ghost" }),
|
||||
"h-8 w-8 p-0 font-normal aria-selected:opacity-100"
|
||||
),
|
||||
day_range_start: "day-range-start",
|
||||
day_range_end: "day-range-end",
|
||||
day_selected:
|
||||
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
|
||||
day_today: "bg-accent text-accent-foreground",
|
||||
day_outside:
|
||||
"day-outside text-muted-foreground opacity-50 aria-selected:bg-accent/50 aria-selected:text-muted-foreground aria-selected:opacity-30",
|
||||
day_disabled: "text-muted-foreground opacity-50",
|
||||
day_range_middle:
|
||||
"aria-selected:bg-accent aria-selected:text-accent-foreground",
|
||||
day_hidden: "invisible",
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
IconLeft: ({ ...props }) => <ChevronLeftIcon className="h-4 w-4" />,
|
||||
IconRight: ({ ...props }) => <ChevronRightIcon className="h-4 w-4" />,
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
Calendar.displayName = "Calendar"
|
||||
|
||||
export { Calendar }
|
||||
25
client-electron/src/hooks/localStorage.ts
Normal file
25
client-electron/src/hooks/localStorage.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { create } from 'zustand'
|
||||
import { persist, createJSONStorage } from 'zustand/middleware'
|
||||
import { RecipeDashboardFilterQuery } from './recipe-dashboard'
|
||||
|
||||
interface localStorageHook {
|
||||
recipeQuery?: RecipeDashboardFilterQuery
|
||||
setRecipeQuery: (query?: RecipeDashboardFilterQuery) => void
|
||||
}
|
||||
|
||||
const useLocalStorage = create(
|
||||
persist<localStorageHook>(
|
||||
set => ({
|
||||
recipeQuery: undefined,
|
||||
setRecipeQuery(query) {
|
||||
set({ recipeQuery: query })
|
||||
}
|
||||
}),
|
||||
{
|
||||
name: 'local-storage',
|
||||
storage: createJSONStorage(() => localStorage)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
export default useLocalStorage
|
||||
52
client-electron/src/hooks/recipe-dashboard.ts
Normal file
52
client-electron/src/hooks/recipe-dashboard.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import customAxios from '@/lib/customAxios'
|
||||
import { type RecipeDashboard } from '@/models/recipe/schema'
|
||||
import { create } from 'zustand'
|
||||
|
||||
export interface RecipeDashboardFilterQuery {
|
||||
countryID: string
|
||||
filename: string
|
||||
}
|
||||
|
||||
interface materialDashboard {
|
||||
lebel: string
|
||||
value: string
|
||||
}
|
||||
|
||||
interface RecipeDashboardHook {
|
||||
getRecipesDashboard: (filter?: RecipeDashboardFilterQuery) => Promise<RecipeDashboard[] | []>
|
||||
getMaterials: (filter?: RecipeDashboardFilterQuery) => Promise<materialDashboard[] | []>
|
||||
}
|
||||
|
||||
const useRecipeDashboard = create<RecipeDashboardHook>(() => ({
|
||||
async getRecipesDashboard(filter) {
|
||||
return customAxios
|
||||
.get<RecipeDashboard[]>('/v2/recipes/dashboard', {
|
||||
params: filter
|
||||
? {
|
||||
country_id: filter.countryID,
|
||||
filename: filter.filename
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
.then(res => {
|
||||
return res.data
|
||||
})
|
||||
.catch(() => [])
|
||||
},
|
||||
async getMaterials(filter) {
|
||||
return customAxios
|
||||
.get<materialDashboard[]>('/v2/materials/dashboard', {
|
||||
params: filter
|
||||
? {
|
||||
country_id: filter.countryID,
|
||||
filename: filter.filename
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
.then(res => {
|
||||
return res.data
|
||||
})
|
||||
}
|
||||
}))
|
||||
|
||||
export default useRecipeDashboard
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
import customAxios from '@/lib/customAxios'
|
||||
import { type RecipeOverview } from '@/models/recipe/schema'
|
||||
|
||||
interface GetRecipeOverviewFilterQuery {
|
||||
countryID: string
|
||||
filename: string
|
||||
materialIDs: string
|
||||
}
|
||||
|
||||
export const getRecipeOverview = (query?: GetRecipeOverviewFilterQuery): Promise<RecipeOverview[]> => {
|
||||
return customAxios
|
||||
.get<RecipeOverview[]>(import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/recipe/overview', { params: query })
|
||||
.then(res => {
|
||||
return res.data
|
||||
})
|
||||
}
|
||||
199
client-electron/src/hooks/scrcpy-android.ts
Normal file
199
client-electron/src/hooks/scrcpy-android.ts
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
import { toast } from '@/components/ui/use-toast'
|
||||
import { type Adb } from '@yume-chan/adb'
|
||||
import { AdbScrcpyClient, AdbScrcpyOptions1_22 } from '@yume-chan/adb-scrcpy'
|
||||
import {
|
||||
AndroidKeyCode,
|
||||
AndroidKeyEventAction,
|
||||
AndroidMotionEventAction,
|
||||
ScrcpyLogLevel1_18,
|
||||
ScrcpyOptions1_25,
|
||||
ScrcpyPointerId,
|
||||
ScrcpyVideoCodecId
|
||||
} from '@yume-chan/scrcpy'
|
||||
import { WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs'
|
||||
import { Consumable, DecodeUtf8Stream, ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface ScrcpyAndroidHook {
|
||||
scrcpyClient: AdbScrcpyClient | undefined
|
||||
decoder: WebCodecsDecoder | undefined
|
||||
connectScrcpy: (adb: Adb | undefined) => void
|
||||
onHomeClick(): void
|
||||
onBackClick(): void
|
||||
disconnectScrcpy(): void
|
||||
}
|
||||
|
||||
const useScrcpy = create<ScrcpyAndroidHook>((set, get) => ({
|
||||
scrcpyClient: undefined,
|
||||
decoder: undefined,
|
||||
async connectScrcpy(adb) {
|
||||
if (!adb) {
|
||||
toast({
|
||||
duration: 3000,
|
||||
variant: 'destructive',
|
||||
title: 'Failed to connect to device',
|
||||
description: 'Please connect Adb first'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// fetch the scrcpy server binary
|
||||
// TODO: should load from real server instead of local file. Fix this later
|
||||
const server: ArrayBuffer = await fetch(new URL('../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
|
||||
res.arrayBuffer()
|
||||
)
|
||||
|
||||
// push the server binary to the device
|
||||
const sync = await adb.sync()
|
||||
try {
|
||||
await sync.write({
|
||||
filename: '/data/local/tmp/scrcpy-server.jar',
|
||||
file: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Consumable(new Uint8Array(server)))
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
await sync.dispose()
|
||||
}
|
||||
|
||||
// start the scrcpy server
|
||||
const res = await adb.subprocess.spawn(
|
||||
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
|
||||
)
|
||||
|
||||
// pipe the server output to the console
|
||||
res.stdout.pipeThrough(new DecodeUtf8Stream()).pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
console.log(chunk)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const scrcpyOption = new ScrcpyOptions1_25({
|
||||
sendDummyByte: true,
|
||||
maxFps: 60,
|
||||
bitRate: 8000000,
|
||||
stayAwake: true,
|
||||
control: true,
|
||||
logLevel: ScrcpyLogLevel1_18.Debug
|
||||
})
|
||||
|
||||
// start the scrcpy client
|
||||
const client = await AdbScrcpyClient.start(
|
||||
adb,
|
||||
'/data/local/tmp/scrcpy-server.jar',
|
||||
'1.25',
|
||||
new AdbScrcpyOptions1_22(scrcpyOption)
|
||||
)
|
||||
|
||||
// get the video stream
|
||||
const videoStream = await client?.videoStream
|
||||
|
||||
// create a decoder
|
||||
const decoder = new WebCodecsDecoder(ScrcpyVideoCodecId.H264)
|
||||
|
||||
// pipe the video stream to the decoder
|
||||
videoStream?.stream.pipeTo(decoder.writable)
|
||||
|
||||
// if client has controlMessageWriter, Inject mouse and button events
|
||||
if (client.controlMessageWriter) {
|
||||
decoder.renderer.addEventListener('mousedown', e => {
|
||||
// client width and height 450 x 800
|
||||
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 450 x 800
|
||||
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 450 x 800
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
set({ scrcpyClient: client, decoder })
|
||||
},
|
||||
onHomeClick() {
|
||||
const { scrcpyClient } = get()
|
||||
scrcpyClient?.controlMessageWriter?.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Up,
|
||||
keyCode: AndroidKeyCode.AndroidHome,
|
||||
metaState: 0,
|
||||
repeat: 0
|
||||
})
|
||||
},
|
||||
onBackClick() {
|
||||
const { scrcpyClient } = get()
|
||||
scrcpyClient?.controlMessageWriter?.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Up,
|
||||
keyCode: AndroidKeyCode.AndroidBack,
|
||||
metaState: 0,
|
||||
repeat: 0
|
||||
})
|
||||
},
|
||||
disconnectScrcpy() {
|
||||
const { scrcpyClient, decoder } = get()
|
||||
decoder?.dispose()
|
||||
scrcpyClient?.close()
|
||||
set({ scrcpyClient: undefined, decoder: undefined })
|
||||
}
|
||||
}))
|
||||
|
||||
export default useScrcpy
|
||||
88
client-electron/src/hooks/shell-android.ts
Normal file
88
client-electron/src/hooks/shell-android.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { toast } from '@/components/ui/use-toast'
|
||||
import type { Adb, AdbSubprocessProtocol } from '@yume-chan/adb'
|
||||
import { encodeUtf8 } from '@yume-chan/adb'
|
||||
import { Consumable, WritableStream } from '@yume-chan/stream-extra'
|
||||
import { Terminal } from 'xterm'
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface ShellAndroidHook {
|
||||
process: AdbSubprocessProtocol | undefined
|
||||
terminal: Terminal | undefined
|
||||
writerTerminal: WritableStreamDefaultWriter<Consumable<Uint8Array>> | undefined
|
||||
writerProcess: WritableStream<Uint8Array> | undefined
|
||||
startTerminal: (adb: Adb | undefined) => void
|
||||
killTerminal: () => void
|
||||
}
|
||||
|
||||
const useShellAndroid = create<ShellAndroidHook>((set, get) => ({
|
||||
process: undefined,
|
||||
terminal: undefined,
|
||||
writerTerminal: undefined,
|
||||
writerProcess: undefined,
|
||||
async startTerminal(adb) {
|
||||
if (!adb) {
|
||||
toast({
|
||||
duration: 3000,
|
||||
variant: 'destructive',
|
||||
title: 'Failed to connect to device',
|
||||
description: 'Please connect Adb first'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const terminal: Terminal = new Terminal()
|
||||
terminal.options.cursorBlink = true
|
||||
terminal.options.theme = {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4'
|
||||
}
|
||||
|
||||
try {
|
||||
const writerProcess = new WritableStream<Uint8Array>({
|
||||
write(chunk) {
|
||||
terminal.write(chunk)
|
||||
}
|
||||
})
|
||||
|
||||
const process = await adb.subprocess.shell('/data/data/com.termux/files/usr/bin/telnet localhost 45515')
|
||||
process.stdout.pipeTo(writerProcess)
|
||||
|
||||
const writerTerminal = process.stdin.getWriter()
|
||||
terminal.onData(data => {
|
||||
const buffer = encodeUtf8(data)
|
||||
const consumable = new Consumable(buffer)
|
||||
writerTerminal.write(consumable)
|
||||
})
|
||||
|
||||
set({
|
||||
process,
|
||||
terminal,
|
||||
writerProcess,
|
||||
writerTerminal
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error: ', error)
|
||||
}
|
||||
},
|
||||
async killTerminal() {
|
||||
try {
|
||||
const { writerTerminal, process } = get()
|
||||
writerTerminal?.write(new Consumable(encodeUtf8('exit\n')))
|
||||
|
||||
if (process) {
|
||||
await process.kill()
|
||||
}
|
||||
|
||||
set({
|
||||
process: undefined,
|
||||
terminal: undefined,
|
||||
writerTerminal: undefined,
|
||||
writerProcess: undefined
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error: ', error)
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
export default useShellAndroid
|
||||
|
|
@ -9,7 +9,6 @@ import { Toaster } from '@/components/ui/toaster'
|
|||
interface MainLayoutProps {
|
||||
sidebarMenu: MenuList
|
||||
}
|
||||
|
||||
const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
|
||||
const { userInfo, getUserInfo } = userAuthStore(
|
||||
useShallow(state => ({
|
||||
|
|
@ -38,7 +37,7 @@ const MainLayout: React.FC<MainLayoutProps> = ({ sidebarMenu }) => {
|
|||
<>
|
||||
<Navbar />
|
||||
<Sidebar menuList={sidebarMenu} />
|
||||
<main className="p-8 sm:ml-64 mt-20">
|
||||
<main className="mt-20 p-8 sm:ml-64">
|
||||
<Outlet />
|
||||
<Toaster />
|
||||
</main>
|
||||
|
|
|
|||
|
|
@ -7,16 +7,29 @@ const customAxios = axios.create({
|
|||
|
||||
customAxios.interceptors.response.use(
|
||||
res => res,
|
||||
err => {
|
||||
async err => {
|
||||
const originalRequest = err.config
|
||||
|
||||
if (err.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true
|
||||
return customAxios.get('/auth/refresh').then(res => {
|
||||
if (res.status === 200) {
|
||||
return customAxios(originalRequest)
|
||||
}
|
||||
})
|
||||
|
||||
console.log('refreshing token')
|
||||
|
||||
const refreshToken = await window.ipcRenderer.invoke(
|
||||
'get-keyChain',
|
||||
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
||||
)
|
||||
|
||||
return customAxios
|
||||
.post('/auth/refresh', {
|
||||
refresh_token: refreshToken
|
||||
})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
return customAxios(originalRequest)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return Promise.reject(err)
|
||||
|
|
|
|||
|
|
@ -18,37 +18,31 @@ if (window.electronRuntime) {
|
|||
console.log(message)
|
||||
})
|
||||
|
||||
window.ipcRenderer
|
||||
.invoke('keyChainSync', import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME)
|
||||
.then(result => {
|
||||
console.log(result)
|
||||
})
|
||||
// window.ipcRenderer
|
||||
// .invoke('keyChainSync', import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME)
|
||||
// .then(([accessToken, refreshToken]) => {
|
||||
// console.log(accessToken, refreshToken)
|
||||
// })
|
||||
|
||||
// getPassword(
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN
|
||||
// ).then(tokenMaxAge => {
|
||||
// if (tokenMaxAge) {
|
||||
// const [token, max_age] = tokenMaxAge.split(';')
|
||||
// document.cookie =
|
||||
// 'access_token=' +
|
||||
// token +
|
||||
// '; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=' +
|
||||
// max_age
|
||||
// }
|
||||
// })
|
||||
// window.ipcRenderer
|
||||
// .invoke(
|
||||
// 'get-keyChain',
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN
|
||||
// )
|
||||
// .then(token => {
|
||||
// console.log(token)
|
||||
// })
|
||||
|
||||
// getPassword(
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
||||
// ).then(refreshToken => {
|
||||
// if (refreshToken) {
|
||||
// document.cookie =
|
||||
// 'refresh_token=' +
|
||||
// refreshToken +
|
||||
// '; Path=/; HttpOnly; SameSite=None; Secure;'
|
||||
// }
|
||||
// })
|
||||
// window.ipcRenderer
|
||||
// .invoke(
|
||||
// 'get-keyChain',
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
// import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN
|
||||
// )
|
||||
// .then(token => {
|
||||
// console.log(token)
|
||||
// })
|
||||
|
||||
// Use deep link
|
||||
window.ipcRenderer.on('deeplink', (_event, url) => {
|
||||
|
|
|
|||
|
|
@ -69,3 +69,22 @@ export const priorities = [
|
|||
icon: ArrowUpIcon
|
||||
}
|
||||
]
|
||||
|
||||
export const materails = [
|
||||
{
|
||||
lable: 'Syrup',
|
||||
value: 'syrup'
|
||||
},
|
||||
{
|
||||
lable: 'Milk',
|
||||
value: 'milk'
|
||||
},
|
||||
{
|
||||
lable: 'Flour',
|
||||
value: 'flour'
|
||||
},
|
||||
{
|
||||
lable: 'Eggs',
|
||||
value: 'eggs'
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -1,20 +1,11 @@
|
|||
import { RecipeStatus } from '@/constants/recipe'
|
||||
import { z } from 'zod'
|
||||
|
||||
export const taskSchema = z.object({
|
||||
id: z.string(),
|
||||
title: z.string(),
|
||||
status: z.string(),
|
||||
label: z.string(),
|
||||
priority: z.string()
|
||||
})
|
||||
export type Task = z.infer<typeof taskSchema>
|
||||
|
||||
export const recipeOverviewSchema = z.object({
|
||||
export const recipeDashboardSchema = z.object({
|
||||
productCode: z.string(),
|
||||
name: z.string(),
|
||||
otherName: z.string(),
|
||||
status: z.nativeEnum(RecipeStatus),
|
||||
lastUpdated: z.date()
|
||||
nameEng: z.string(),
|
||||
inUse: z.boolean(),
|
||||
lastUpdated: z.string().pipe(z.coerce.date()).nullable(),
|
||||
image: z.string().nullable()
|
||||
})
|
||||
export type RecipeOverview = z.infer<typeof recipeOverviewSchema>
|
||||
export type RecipeDashboard = z.infer<typeof recipeDashboardSchema>
|
||||
|
|
|
|||
|
|
@ -1,702 +0,0 @@
|
|||
[
|
||||
{
|
||||
"id": "TASK-8782",
|
||||
"title": "You can't compress the program without quantifying the open-source SSD pixel!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7878",
|
||||
"title": "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7839",
|
||||
"title": "We need to bypass the neural TCP card!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5562",
|
||||
"title": "The SAS interface is down, bypass the open-source pixel so we can back up the PNG bandwidth!",
|
||||
"status": "backlog",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8686",
|
||||
"title": "I'll parse the wireless SSL protocol, that should driver the API panel!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1280",
|
||||
"title": "Use the digital TLS panel, then you can transmit the haptic system!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7262",
|
||||
"title": "The UTF8 application is down, parse the neural bandwidth so we can back up the PNG firewall!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1138",
|
||||
"title": "Generating the driver won't do anything, we need to quantify the 1080p SMTP bandwidth!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7184",
|
||||
"title": "We need to program the back-end THX pixel!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5160",
|
||||
"title": "Calculating the bus won't do anything, we need to navigate the back-end JSON protocol!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5618",
|
||||
"title": "Generating the driver won't do anything, we need to index the online SSL application!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6699",
|
||||
"title": "I'll transmit the wireless JBOD capacitor, that should hard drive the SSD feed!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-2858",
|
||||
"title": "We need to override the online UDP bus!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9864",
|
||||
"title": "I'll reboot the 1080p FTP panel, that should matrix the HEX hard drive!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8404",
|
||||
"title": "We need to generate the virtual HEX alarm!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5365",
|
||||
"title": "Backing up the pixel won't do anything, we need to transmit the primary IB array!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1780",
|
||||
"title": "The CSS feed is down, index the bluetooth transmitter so we can compress the CLI protocol!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6938",
|
||||
"title": "Use the redundant SCSI application, then you can hack the optical alarm!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9885",
|
||||
"title": "We need to compress the auxiliary VGA driver!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3216",
|
||||
"title": "Transmitting the transmitter won't do anything, we need to compress the virtual HDD sensor!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9285",
|
||||
"title": "The IP monitor is down, copy the haptic alarm so we can generate the HTTP transmitter!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1024",
|
||||
"title": "Overriding the microchip won't do anything, we need to transmit the digital OCR transmitter!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7068",
|
||||
"title": "You can't generate the capacitor without indexing the wireless HEX pixel!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6502",
|
||||
"title": "Navigating the microchip won't do anything, we need to bypass the back-end SQL bus!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5326",
|
||||
"title": "We need to hack the redundant UTF8 transmitter!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6274",
|
||||
"title": "Use the virtual PCI circuit, then you can parse the bluetooth alarm!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1571",
|
||||
"title": "I'll input the neural DRAM circuit, that should protocol the SMTP interface!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9518",
|
||||
"title": "Compressing the interface won't do anything, we need to compress the online SDD matrix!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5581",
|
||||
"title": "I'll synthesize the digital COM pixel, that should transmitter the UTF8 protocol!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-2197",
|
||||
"title": "Parsing the feed won't do anything, we need to copy the bluetooth DRAM bus!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8484",
|
||||
"title": "We need to parse the solid state UDP firewall!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9892",
|
||||
"title": "If we back up the application, we can get to the UDP application through the multi-byte THX capacitor!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9616",
|
||||
"title": "We need to synthesize the cross-platform ASCII pixel!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9744",
|
||||
"title": "Use the back-end IP card, then you can input the solid state hard drive!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1376",
|
||||
"title": "Generating the alarm won't do anything, we need to generate the mobile IP capacitor!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7382",
|
||||
"title": "If we back up the firewall, we can get to the RAM alarm through the primary UTF8 pixel!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-2290",
|
||||
"title": "I'll compress the virtual JSON panel, that should application the UTF8 bus!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1533",
|
||||
"title": "You can't input the firewall without overriding the wireless TCP firewall!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4920",
|
||||
"title": "Bypassing the hard drive won't do anything, we need to input the bluetooth JSON program!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5168",
|
||||
"title": "If we synthesize the bus, we can get to the IP panel through the virtual TLS array!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7103",
|
||||
"title": "We need to parse the multi-byte EXE bandwidth!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4314",
|
||||
"title": "If we compress the program, we can get to the XML alarm through the multi-byte COM matrix!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3415",
|
||||
"title": "Use the cross-platform XML application, then you can quantify the solid state feed!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8339",
|
||||
"title": "Try to calculate the DNS interface, maybe it will input the bluetooth capacitor!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6995",
|
||||
"title": "Try to hack the XSS bandwidth, maybe it will override the bluetooth matrix!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8053",
|
||||
"title": "If we connect the program, we can get to the UTF8 matrix through the digital UDP protocol!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4336",
|
||||
"title": "If we synthesize the microchip, we can get to the SAS sensor through the optical UDP program!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8790",
|
||||
"title": "I'll back up the optical COM alarm, that should alarm the RSS capacitor!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8980",
|
||||
"title": "Try to navigate the SQL transmitter, maybe it will back up the virtual firewall!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7342",
|
||||
"title": "Use the neural CLI card, then you can parse the online port!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5608",
|
||||
"title": "I'll hack the haptic SSL program, that should bus the UDP transmitter!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1606",
|
||||
"title": "I'll generate the bluetooth PNG firewall, that should pixel the SSL driver!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7872",
|
||||
"title": "Transmitting the circuit won't do anything, we need to reboot the 1080p RSS monitor!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4167",
|
||||
"title": "Use the cross-platform SMS circuit, then you can synthesize the optical feed!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9581",
|
||||
"title": "You can't index the port without hacking the cross-platform XSS monitor!",
|
||||
"status": "backlog",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-8806",
|
||||
"title": "We need to bypass the back-end SSL panel!",
|
||||
"status": "done",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6542",
|
||||
"title": "Try to quantify the RSS firewall, maybe it will quantify the open-source system!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6806",
|
||||
"title": "The VGA protocol is down, reboot the back-end matrix so we can parse the CSS panel!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9549",
|
||||
"title": "You can't bypass the bus without connecting the neural JBOD bus!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1075",
|
||||
"title": "Backing up the driver won't do anything, we need to parse the redundant RAM pixel!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1427",
|
||||
"title": "Use the auxiliary PCI circuit, then you can calculate the cross-platform interface!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1907",
|
||||
"title": "Hacking the circuit won't do anything, we need to back up the online DRAM system!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4309",
|
||||
"title": "If we generate the system, we can get to the TCP sensor through the optical GB pixel!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3973",
|
||||
"title": "I'll parse the back-end ADP array, that should bandwidth the RSS bandwidth!",
|
||||
"status": "todo",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7962",
|
||||
"title": "Use the wireless RAM program, then you can hack the cross-platform feed!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3360",
|
||||
"title": "You can't quantify the program without synthesizing the neural OCR interface!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9887",
|
||||
"title": "Use the auxiliary ASCII sensor, then you can connect the solid state port!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3649",
|
||||
"title": "I'll input the virtual USB system, that should circuit the DNS monitor!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3586",
|
||||
"title": "If we quantify the circuit, we can get to the CLI feed through the mobile SMS hard drive!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5150",
|
||||
"title": "I'll hack the wireless XSS port, that should transmitter the IP interface!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3652",
|
||||
"title": "The SQL interface is down, override the optical bus so we can program the ASCII interface!",
|
||||
"status": "backlog",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6884",
|
||||
"title": "Use the digital PCI circuit, then you can synthesize the multi-byte microchip!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1591",
|
||||
"title": "We need to connect the mobile XSS driver!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3802",
|
||||
"title": "Try to override the ASCII protocol, maybe it will parse the virtual matrix!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7253",
|
||||
"title": "Programming the capacitor won't do anything, we need to bypass the neural IB hard drive!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9739",
|
||||
"title": "We need to hack the multi-byte HDD bus!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4424",
|
||||
"title": "Try to hack the HEX alarm, maybe it will connect the optical pixel!",
|
||||
"status": "in progress",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3922",
|
||||
"title": "You can't back up the capacitor without generating the wireless PCI program!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4921",
|
||||
"title": "I'll index the open-source IP feed, that should system the GB application!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5814",
|
||||
"title": "We need to calculate the 1080p AGP feed!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-2645",
|
||||
"title": "Synthesizing the system won't do anything, we need to navigate the multi-byte HDD firewall!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4535",
|
||||
"title": "Try to copy the JSON circuit, maybe it will connect the wireless feed!",
|
||||
"status": "in progress",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4463",
|
||||
"title": "We need to copy the solid state AGP monitor!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9745",
|
||||
"title": "If we connect the protocol, we can get to the GB system through the bluetooth PCI microchip!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-2080",
|
||||
"title": "If we input the bus, we can get to the RAM matrix through the auxiliary RAM card!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3838",
|
||||
"title": "I'll bypass the online TCP application, that should panel the AGP system!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-1340",
|
||||
"title": "We need to navigate the virtual PNG circuit!",
|
||||
"status": "todo",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6665",
|
||||
"title": "If we parse the monitor, we can get to the SSD hard drive through the cross-platform AGP alarm!",
|
||||
"status": "canceled",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7585",
|
||||
"title": "If we calculate the hard drive, we can get to the SSL program through the multi-byte CSS microchip!",
|
||||
"status": "backlog",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6319",
|
||||
"title": "We need to copy the multi-byte SCSI program!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4369",
|
||||
"title": "Try to input the SCSI bus, maybe it will generate the 1080p pixel!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-9035",
|
||||
"title": "We need to override the solid state PNG array!",
|
||||
"status": "canceled",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3970",
|
||||
"title": "You can't index the transmitter without quantifying the haptic ASCII card!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4473",
|
||||
"title": "You can't bypass the protocol without overriding the neural RSS program!",
|
||||
"status": "todo",
|
||||
"label": "documentation",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-4136",
|
||||
"title": "You can't hack the hard drive without hacking the primary JSON program!",
|
||||
"status": "canceled",
|
||||
"label": "bug",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-3939",
|
||||
"title": "Use the back-end SQL firewall, then you can connect the neural hard drive!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "low"
|
||||
},
|
||||
{
|
||||
"id": "TASK-2007",
|
||||
"title": "I'll input the back-end USB protocol, that should bandwidth the PCI system!",
|
||||
"status": "backlog",
|
||||
"label": "bug",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-7516",
|
||||
"title": "Use the primary SQL program, then you can generate the auxiliary transmitter!",
|
||||
"status": "done",
|
||||
"label": "documentation",
|
||||
"priority": "medium"
|
||||
},
|
||||
{
|
||||
"id": "TASK-6906",
|
||||
"title": "Try to back up the DRAM system, maybe it will reboot the online transmitter!",
|
||||
"status": "done",
|
||||
"label": "feature",
|
||||
"priority": "high"
|
||||
},
|
||||
{
|
||||
"id": "TASK-5207",
|
||||
"title": "The SMS interface is down, copy the bluetooth bus so we can quantify the VGA card!",
|
||||
"status": "in progress",
|
||||
"label": "bug",
|
||||
"priority": "low"
|
||||
}
|
||||
]
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { ReloadIcon } from '@radix-ui/react-icons'
|
||||
import { LinuxFileType, type Adb } from '@yume-chan/adb'
|
||||
import { WritableStream } from '@yume-chan/stream-extra'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { Consumable, WritableStream, ReadableStream } from '@yume-chan/stream-extra'
|
||||
import JSZip from 'jszip'
|
||||
import { useCallback, useState } from 'react'
|
||||
|
||||
interface FileManagerTabProps {
|
||||
adb: Adb | undefined
|
||||
|
|
@ -20,9 +19,32 @@ type filesType = {
|
|||
|
||||
export const FileManagerTab: React.FC<FileManagerTabProps> = ({ adb }) => {
|
||||
const [path, setPath] = useState<string>('')
|
||||
const [pushPath, setPushPath] = useState<string>('')
|
||||
const [pushFile, setPushFile] = useState<File | null>()
|
||||
|
||||
const [files, setFiles] = useState<filesType>()
|
||||
|
||||
const pushFiles = async (filename: string, blob: Blob, targetPath: string) => {
|
||||
if (!adb) return
|
||||
|
||||
const buffer = await blob.arrayBuffer()
|
||||
console.log(blob, buffer, targetPath, filename)
|
||||
const sync = await adb.sync()
|
||||
try {
|
||||
await sync.write({
|
||||
filename: targetPath + '/' + filename,
|
||||
file: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Consumable(new Uint8Array(buffer)))
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
await sync.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
const zipFiles = (files: filesType) => {
|
||||
const zip = new JSZip()
|
||||
|
||||
|
|
@ -132,10 +154,6 @@ export const FileManagerTab: React.FC<FileManagerTabProps> = ({ adb }) => {
|
|||
}
|
||||
}, [adb, path])
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
console.log('Refreshing...')
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
|
@ -143,19 +161,29 @@ export const FileManagerTab: React.FC<FileManagerTabProps> = ({ adb }) => {
|
|||
<CardDescription>Manage files in Android</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-end py-3 w-full">
|
||||
<div className="flex w-full items-center justify-end">
|
||||
<Button variant={'outline'} onClick={refresh}>
|
||||
<ReloadIcon />
|
||||
</Button>
|
||||
<div className="flex w-full flex-col items-center justify-end py-3">
|
||||
<div className="flex w-full items-center justify-around space-x-10">
|
||||
<div className="flex space-x-5">
|
||||
<Input placeholder="folder to download" value={path} onChange={e => setPath(e.target.value)} />
|
||||
<Button variant={'default'} onClick={download}>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex space-x-5">
|
||||
<Input placeholder="path to push file" value={pushPath} onChange={e => setPushPath(e.target.value)} />
|
||||
<Input type="file" accept="*" dir="ltr" onChange={e => setPushFile(e.target.files?.item(0))} />
|
||||
<Button
|
||||
variant={'default'}
|
||||
onClick={
|
||||
() => pushFiles(pushFile?.name || 'unname', pushFile as Blob, pushPath)
|
||||
//testPushFile
|
||||
}
|
||||
>
|
||||
Push
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex space-x-5 w-96">
|
||||
<Input placeholder="folder to download" value={path} onChange={e => setPath(e.target.value)} />
|
||||
<Button variant={'default'} onClick={download}>
|
||||
Download
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full max-h-96 overflow-y-auto">{files && <FileTree files={files} />}</div>
|
||||
<div className="max-h-96 w-full overflow-y-auto">{files && <FileTree files={files} />}</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
|
|
|||
|
|
@ -1,269 +1,38 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||
import { toast } from '@/components/ui/use-toast'
|
||||
import { type Adb, type AdbSubprocessProtocol } from '@yume-chan/adb'
|
||||
import { AdbScrcpyClient, AdbScrcpyOptions1_22 } from '@yume-chan/adb-scrcpy'
|
||||
import {
|
||||
AndroidKeyCode,
|
||||
AndroidKeyEventAction,
|
||||
AndroidMotionEventAction,
|
||||
ScrcpyLogLevel1_18,
|
||||
ScrcpyOptions1_25,
|
||||
ScrcpyPointerId,
|
||||
ScrcpyVideoCodecId
|
||||
} from '@yume-chan/scrcpy'
|
||||
import { WebCodecsDecoder } from '@yume-chan/scrcpy-decoder-webcodecs'
|
||||
import { Consumable, WritableStream, ReadableStream, DecodeUtf8Stream } from '@yume-chan/stream-extra'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
|
||||
import { Card, CardHeader, CardTitle, CardDescription, CardContent } from '@/components/ui/card'
|
||||
import useScrcpy from '@/hooks/scrcpy-android'
|
||||
import { type Adb } from '@yume-chan/adb'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import 'xterm/css/xterm.css'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
interface ScrcpyTabProps {
|
||||
adb: Adb | undefined
|
||||
}
|
||||
|
||||
export const ScrcpyTab: React.FC<ScrcpyTabProps> = memo(({ adb }) => {
|
||||
const logcatRef = useRef<HTMLDivElement>(null)
|
||||
const scrcpyScreenRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [process, setProcess] = useState<AdbSubprocessProtocol | undefined>()
|
||||
const [client, setClient] = useState<AdbScrcpyClient | undefined>()
|
||||
const [decoder, setDecoder] = useState<WebCodecsDecoder | undefined>()
|
||||
|
||||
console.log('rendering scrcpy tab')
|
||||
const { scrcpyClient, decoder, connectScrcpy, onHomeClick, onBackClick, disconnectScrcpy } = useScrcpy(
|
||||
useShallow(state => ({
|
||||
scrcpyClient: state.scrcpyClient,
|
||||
decoder: state.decoder,
|
||||
connectScrcpy: state.connectScrcpy,
|
||||
onHomeClick: state.onHomeClick,
|
||||
onBackClick: state.onBackClick,
|
||||
disconnectScrcpy: state.disconnectScrcpy
|
||||
}))
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const startTerminal = async () => {
|
||||
if (logcatRef.current && adb) {
|
||||
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)
|
||||
}
|
||||
if (decoder) {
|
||||
scrcpyScreenRef.current?.appendChild(decoder.renderer)
|
||||
decoder.renderer.style.width = '100%'
|
||||
decoder.renderer.style.height = '100%'
|
||||
} else {
|
||||
if (scrcpyScreenRef.current) scrcpyScreenRef.current.innerHTML = ''
|
||||
}
|
||||
|
||||
startTerminal()
|
||||
|
||||
return () => {
|
||||
console.log('cleaning up logcat')
|
||||
|
||||
for (const child of logcatRef.current?.children || []) {
|
||||
logcatRef.current?.removeChild(child)
|
||||
}
|
||||
|
||||
process?.stderr.cancel()
|
||||
process?.stdout.cancel()
|
||||
process?.kill()
|
||||
}
|
||||
}, [adb])
|
||||
|
||||
const connectScrcpy = useCallback(async () => {
|
||||
if (!adb) {
|
||||
toast({
|
||||
title: 'No ADB connection',
|
||||
description: 'Please connect to a device first',
|
||||
duration: 3000,
|
||||
variant: 'destructive'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// clean up the scrcpy screen
|
||||
if (scrcpyScreenRef.current && scrcpyScreenRef.current.children.length > 0) {
|
||||
while (scrcpyScreenRef.current.firstChild) {
|
||||
scrcpyScreenRef.current.removeChild(scrcpyScreenRef.current.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
// fetch the scrcpy server binary
|
||||
// TODO: should load from real server instead of local file. Fix this later
|
||||
const server: ArrayBuffer = await fetch(new URL('../../../scrcpy/scrcpy_server_v1.25', import.meta.url)).then(res =>
|
||||
res.arrayBuffer()
|
||||
)
|
||||
|
||||
// push the server binary to the device
|
||||
const sync = await adb.sync()
|
||||
try {
|
||||
await sync.write({
|
||||
filename: '/data/local/tmp/scrcpy-server.jar',
|
||||
file: new ReadableStream({
|
||||
start(controller) {
|
||||
controller.enqueue(new Consumable(new Uint8Array(server)))
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
})
|
||||
} finally {
|
||||
await sync.dispose()
|
||||
}
|
||||
|
||||
// start the scrcpy server
|
||||
const res = await adb.subprocess.spawn(
|
||||
'CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 1.25'
|
||||
)
|
||||
|
||||
// pipe the server output to the console
|
||||
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
|
||||
})
|
||||
|
||||
// start the scrcpy client
|
||||
const _client = await AdbScrcpyClient.start(
|
||||
adb,
|
||||
'/data/local/tmp/scrcpy-server.jar',
|
||||
'1.25',
|
||||
new AdbScrcpyOptions1_22(scrcpyOption)
|
||||
)
|
||||
|
||||
// get the video stream
|
||||
const videoStream = await _client?.videoStream
|
||||
|
||||
// create a decoder
|
||||
const _decoder = new WebCodecsDecoder(ScrcpyVideoCodecId.H264)
|
||||
scrcpyScreenRef.current?.appendChild(_decoder.renderer)
|
||||
_decoder.renderer.style.width = '100%'
|
||||
_decoder.renderer.style.height = '100%'
|
||||
|
||||
// pipe the video stream to the decoder
|
||||
videoStream?.stream.pipeTo(_decoder.writable)
|
||||
|
||||
// if client has controlMessageWriter, Inject mouse and button events
|
||||
if (_client.controlMessageWriter) {
|
||||
_decoder.renderer.addEventListener('mousedown', e => {
|
||||
// client width and height 450 x 800
|
||||
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 450 x 800
|
||||
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 450 x 800
|
||||
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
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
setDecoder(_decoder)
|
||||
setClient(_client)
|
||||
}, [adb, scrcpyScreenRef])
|
||||
|
||||
function onHomeClickHandler() {
|
||||
client?.controlMessageWriter?.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Up,
|
||||
keyCode: AndroidKeyCode.AndroidHome,
|
||||
metaState: 0,
|
||||
repeat: 0
|
||||
})
|
||||
}
|
||||
|
||||
function onBackClickHandler() {
|
||||
client?.controlMessageWriter?.injectKeyCode({
|
||||
action: AndroidKeyEventAction.Up,
|
||||
keyCode: AndroidKeyCode.AndroidBack,
|
||||
metaState: 0,
|
||||
repeat: 0
|
||||
})
|
||||
}
|
||||
|
||||
function disconnectScrcpy() {
|
||||
// clean ref
|
||||
if (scrcpyScreenRef.current && scrcpyScreenRef.current.children.length > 0) {
|
||||
while (scrcpyScreenRef.current.firstChild) {
|
||||
scrcpyScreenRef.current.removeChild(scrcpyScreenRef.current.firstChild)
|
||||
}
|
||||
}
|
||||
|
||||
decoder?.dispose()
|
||||
client?.close()
|
||||
|
||||
setClient(undefined)
|
||||
setDecoder(undefined)
|
||||
}
|
||||
}, [decoder])
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -271,54 +40,44 @@ export const ScrcpyTab: React.FC<ScrcpyTabProps> = memo(({ adb }) => {
|
|||
<CardTitle>Scrcpy</CardTitle>
|
||||
<CardDescription>Stream and control your Android device from your computer</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="flex w-full justify-around items-start">
|
||||
<CardContent className="flex w-full items-start justify-around">
|
||||
<div>
|
||||
<div className="w-[450px] max-w-[450px] h-[800px] max-h-[800px] bg-slate-700" ref={scrcpyScreenRef} />
|
||||
<div className="flex pt-3 justify-center items-center space-x-4 w-[450px]">
|
||||
<Button onClick={onHomeClickHandler} variant={'outline'} className="flex-1">
|
||||
<div className="h-[800px] max-h-[800px] w-[450px] max-w-[450px] bg-slate-700" ref={scrcpyScreenRef} />
|
||||
<div className="flex w-[450px] items-center justify-center space-x-4 pt-3">
|
||||
<Button onClick={onHomeClick} variant={'outline'} className="flex-1">
|
||||
Home
|
||||
</Button>
|
||||
<Button onClick={onBackClickHandler} variant={'outline'} className="flex-1">
|
||||
<Button onClick={onBackClick} variant={'outline'} className="flex-1">
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 w-full px-5">
|
||||
<div className="flex w-full flex-col space-y-4 px-5">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Control</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex space-x-4 items-center">
|
||||
{client ? (
|
||||
<div className="flex items-center space-x-4">
|
||||
{scrcpyClient ? (
|
||||
<Button onClick={disconnectScrcpy} variant="destructive">
|
||||
Disconnect
|
||||
</Button>
|
||||
) : (
|
||||
<Button onClick={connectScrcpy} variant="default">
|
||||
<Button
|
||||
onClick={() => {
|
||||
connectScrcpy(adb)
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
Connect
|
||||
</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>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,84 +1,27 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { encodeUtf8, type AdbSubprocessProtocol, type Adb } from '@yume-chan/adb'
|
||||
import { Consumable, WritableStream } from '@yume-chan/stream-extra'
|
||||
import { memo, useCallback, useEffect, useRef, useState } from 'react'
|
||||
import useShellAndroid from '@/hooks/shell-android'
|
||||
import { type Adb } from '@yume-chan/adb'
|
||||
import { memo, useEffect, useRef } from 'react'
|
||||
import { type Terminal } from 'xterm'
|
||||
|
||||
import { Terminal } from 'xterm'
|
||||
import { FitAddon } from 'xterm-addon-fit'
|
||||
|
||||
import 'xterm/css/xterm.css'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
interface ShellTabProps {
|
||||
adb: Adb | undefined
|
||||
}
|
||||
|
||||
export const ShellTab: React.FC<ShellTabProps> = memo(({ adb }) => {
|
||||
const [process, setProcess] = useState<AdbSubprocessProtocol | undefined>()
|
||||
const [terminal, setTerminal] = useState<Terminal | undefined>()
|
||||
|
||||
const [reader, setReader] = useState<WritableStream<Uint8Array> | undefined>()
|
||||
|
||||
useEffect(() => {
|
||||
if (adb) {
|
||||
console.log('adb is connected')
|
||||
|
||||
console.log('creating terminal')
|
||||
const terminal: Terminal = new Terminal()
|
||||
terminal.options.cursorBlink = true
|
||||
terminal.options.theme = {
|
||||
background: '#1e1e1e',
|
||||
foreground: '#d4d4d4'
|
||||
}
|
||||
|
||||
console.log('creating process')
|
||||
|
||||
const _reader = new WritableStream<Uint8Array>({
|
||||
write(chunk) {
|
||||
terminal.write(chunk)
|
||||
}
|
||||
})
|
||||
|
||||
adb.subprocess.shell('/data/data/com.termux/files/usr/bin/telnet localhost 45515').then(_process => {
|
||||
_process.stdout.pipeTo(_reader)
|
||||
|
||||
const writer = _process.stdin.getWriter()
|
||||
terminal.onData(data => {
|
||||
const buffer = encodeUtf8(data)
|
||||
const consumable = new Consumable(buffer)
|
||||
writer.write(consumable)
|
||||
})
|
||||
|
||||
setReader(_reader)
|
||||
setProcess(_process)
|
||||
setTerminal(terminal)
|
||||
})
|
||||
} else {
|
||||
console.log('adb is not connected')
|
||||
if (process) {
|
||||
process?.stdout.cancel()
|
||||
process?.stderr.cancel()
|
||||
process?.stdin.close()
|
||||
process?.kill()
|
||||
}
|
||||
|
||||
setProcess(undefined)
|
||||
setTerminal(undefined)
|
||||
}
|
||||
}, [adb])
|
||||
|
||||
const killProcess = useCallback(() => {
|
||||
console.log('killing shell')
|
||||
console.log(process)
|
||||
if (process && terminal) {
|
||||
terminal.write('exit\n')
|
||||
reader?.close()
|
||||
process.stderr.cancel()
|
||||
process.stdin.close()
|
||||
process.stdout.cancel()
|
||||
process.kill()
|
||||
}
|
||||
}, [process])
|
||||
const { terminal, startTerminal, killTerminal } = useShellAndroid(
|
||||
useShallow(state => ({
|
||||
terminal: state.terminal,
|
||||
startTerminal: state.startTerminal,
|
||||
killTerminal: state.killTerminal
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<Card>
|
||||
|
|
@ -88,10 +31,16 @@ export const ShellTab: React.FC<ShellTabProps> = memo(({ adb }) => {
|
|||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-end py-3 w-full">
|
||||
<div>
|
||||
<Button variant={'destructive'} onClick={killProcess}>
|
||||
Kill Shell
|
||||
</Button>
|
||||
<div className="space-x-5">
|
||||
{terminal ? (
|
||||
<Button variant={'destructive'} onClick={killTerminal}>
|
||||
Terminate
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant={'default'} onClick={() => startTerminal(adb)}>
|
||||
Start
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{terminal ? (
|
||||
|
|
@ -114,7 +63,6 @@ const ShellTerminal: React.FC<ShellTerminalProps> = ({ terminal }) => {
|
|||
const shellRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
console.log(shellRef.current)
|
||||
// check if shellRef is have child remove all
|
||||
if (shellRef.current && shellRef.current.children.length > 0) {
|
||||
for (const child of shellRef.current.children) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
|||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||
import {
|
||||
ADB_DEFAULT_DEVICE_FILTER,
|
||||
type AdbDaemonWebUsbDevice,
|
||||
AdbDaemonWebUsbDevice,
|
||||
type AdbDaemonWebUsbDeviceManager
|
||||
} from '@yume-chan/adb-daemon-webusb'
|
||||
import { useState } from 'react'
|
||||
|
|
@ -32,9 +32,11 @@ export const ToolBar: React.FC<ToolBarProps> = ({ manager, adb, device, setAdb,
|
|||
const [version, setVersion] = useState<string>('')
|
||||
|
||||
async function createNewConnection() {
|
||||
let selectedDevice
|
||||
|
||||
console.log(device)
|
||||
let selectedDevice: AdbDaemonWebUsbDevice | undefined = undefined
|
||||
if (!device) {
|
||||
console.log('no device')
|
||||
|
||||
selectedDevice = await manager?.requestDevice({
|
||||
filters: [
|
||||
{
|
||||
|
|
@ -53,9 +55,13 @@ export const ToolBar: React.FC<ToolBarProps> = ({ manager, adb, device, setAdb,
|
|||
selectedDevice = device
|
||||
}
|
||||
|
||||
// create transport and connect to device
|
||||
let adb: Adb | null = null
|
||||
let connection
|
||||
try {
|
||||
connection = await selectedDevice.connect()
|
||||
if (selectedDevice instanceof AdbDaemonWebUsbDevice) {
|
||||
connection = await selectedDevice.connect()
|
||||
}
|
||||
} catch (e) {
|
||||
toast({
|
||||
duration: 5000,
|
||||
|
|
@ -66,24 +72,46 @@ export const ToolBar: React.FC<ToolBarProps> = ({ manager, adb, device, setAdb,
|
|||
return
|
||||
}
|
||||
|
||||
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||
if (connection) {
|
||||
const credentialStore: AdbWebCredentialStore = new AdbWebCredentialStore()
|
||||
|
||||
const transport = await AdbDaemonTransport.authenticate({
|
||||
serial: selectedDevice.serial,
|
||||
connection: connection,
|
||||
credentialStore: credentialStore
|
||||
})
|
||||
const transport = await AdbDaemonTransport.authenticate({
|
||||
serial: selectedDevice.serial,
|
||||
connection: connection,
|
||||
credentialStore: credentialStore
|
||||
})
|
||||
|
||||
const adb: Adb = new Adb(transport)
|
||||
adb = new Adb(transport)
|
||||
}
|
||||
|
||||
const name = await adb.getProp('ro.product.model')
|
||||
const version = await adb.getProp('ro.build.version.release')
|
||||
if (adb) {
|
||||
const name = await adb.getProp('ro.product.model')
|
||||
const version = await adb.getProp('ro.build.version.release')
|
||||
|
||||
setName(name)
|
||||
setResolution(resolution)
|
||||
setVersion(version)
|
||||
setName(name)
|
||||
setResolution(resolution)
|
||||
setVersion(version)
|
||||
|
||||
setAdb(adb)
|
||||
setAdb(adb)
|
||||
}
|
||||
}
|
||||
|
||||
async function connectAdbDaemon() {
|
||||
if (!window.electronRuntime) {
|
||||
toast({
|
||||
duration: 5000,
|
||||
variant: 'destructive',
|
||||
title: 'Failed to connect to adb daemon',
|
||||
description: 'This feature is only available in the desktop app'
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// create connection
|
||||
await window.ipcRenderer.invoke('adb')
|
||||
|
||||
const result = await window.ipcRenderer.invoke('adb:shell', 'ls')
|
||||
console.log(result)
|
||||
}
|
||||
|
||||
function onDisconnect() {
|
||||
|
|
@ -121,6 +149,10 @@ export const ToolBar: React.FC<ToolBarProps> = ({ manager, adb, device, setAdb,
|
|||
Connect
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button variant={'default'} onClick={connectAdbDaemon}>
|
||||
Connect Adb Daemon
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,91 +0,0 @@
|
|||
import { type ColumnDef } from '@tanstack/react-table'
|
||||
import { type RecipeOverview } from '@/models/recipe/schema'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import DataTableColumnHeader from './data-table-column-header'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import DataTableRowActions from './data-table-row-actions'
|
||||
import { type RecipeStatus, getRecipeStatusIcon } from '@/constants/recipe'
|
||||
|
||||
export const columns: ColumnDef<RecipeOverview>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
|
||||
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={value => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorKey: 'productCode',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="ProductCode" />,
|
||||
cell: ({ row }) => <div className="w-[80px]">{row.getValue('productCode')}</div>,
|
||||
enableSorting: false,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
||||
cell: ({ row }) => {
|
||||
const label = { label: 'Test Label' } // labels.find(label => label.value === row.getValue('label'))
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{label && <Badge variant="outline">{label.label}</Badge>}
|
||||
<span className="max-w-[500px] truncate font-medium">{row.getValue('name')}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'status',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Status" />,
|
||||
cell: ({ row }) => {
|
||||
const status: RecipeStatus = row.getValue('status')
|
||||
const StatusIcon = getRecipeStatusIcon(status)
|
||||
if (!status) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex w-[100px] items-center">
|
||||
{<StatusIcon className="text-muted-foreground mr-2 h-4 w-4" />}
|
||||
<span>{}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastUpdated',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Last Updated" />,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span>{row.getValue('lastUpdated')}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => <DataTableRowActions row={row} />
|
||||
}
|
||||
]
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
const RecipeForm: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Recipe Form</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeForm
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
import { type ColumnDef } from '@tanstack/react-table'
|
||||
import { type RecipeDashboard } from '@/models/recipe/schema'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import DataTableColumnHeader from './data-table-column-header'
|
||||
// import { Badge } from '@/components/ui/badge'
|
||||
import DataTableRowActions from './data-table-row-actions'
|
||||
import { CheckCircledIcon, CrossCircledIcon } from '@radix-ui/react-icons'
|
||||
import * as dateFormat from 'date-fns'
|
||||
import { DateRange } from 'react-day-picker'
|
||||
|
||||
export const columns: ColumnDef<RecipeDashboard>[] = [
|
||||
{
|
||||
id: 'select',
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected() || (table.getIsSomePageRowsSelected() && 'indeterminate')}
|
||||
onCheckedChange={value => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={value => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableHiding: false
|
||||
},
|
||||
{
|
||||
accessorKey: 'productCode',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="ProductCode" />,
|
||||
cell: ({ row }) => <div>{row.getValue('productCode')}</div>,
|
||||
enableHiding: false,
|
||||
enableGlobalFilter: true,
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'name',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name" />,
|
||||
cell: ({ row }) => {
|
||||
//const label = { label: 'Test Label' } // labels.find(label => label.value === row.getValue('label'))
|
||||
|
||||
return (
|
||||
<div className="flex space-x-2">
|
||||
{/* {label && <Badge variant="outline">{label.label}</Badge>} */}
|
||||
<span className="max-w-[500px] truncate font-medium">{row.getValue('name')}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableGlobalFilter: true,
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'nameEng',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Name ENG" />,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span>{row.getValue('nameEng')}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
enableGlobalFilter: true,
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'inUse',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Active" />,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span>
|
||||
{row.getValue('inUse') ? (
|
||||
<CheckCircledIcon className="h-6 w-6 text-green-700" />
|
||||
) : (
|
||||
<CrossCircledIcon className="h-6 w-6 text-red-700" />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (row, id, value) => {
|
||||
return value.includes(row.getValue(id))
|
||||
}
|
||||
},
|
||||
{
|
||||
accessorKey: 'lastUpdated',
|
||||
header: ({ column }) => <DataTableColumnHeader column={column} title="Last Updated" isDate />,
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span>{dateFormat.format(row.getValue('lastUpdated'), 'dd-MM-yyyy HH:mm:ss')}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
filterFn: (_row, _id, _value) => {
|
||||
const value = _value as DateRange
|
||||
const rowValue = _row.getValue(_id) as Date
|
||||
return (
|
||||
dateFormat.isAfter(rowValue, value.from || dateFormat.add(rowValue, { days: 1 })) &&
|
||||
dateFormat.isBefore(rowValue, value.to || dateFormat.sub(rowValue, { days: 1 }))
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
cell: ({ row }) => <DataTableRowActions row={row} />
|
||||
}
|
||||
]
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Calendar } from '@/components/ui/calendar'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -7,28 +8,57 @@ import {
|
|||
DropdownMenuTrigger
|
||||
} from '@/components/ui/dropdown-menu'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ArrowDownIcon, ArrowUpIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
|
||||
import { ArrowDownIcon, ArrowUpIcon, CalendarIcon, CaretSortIcon, EyeNoneIcon } from '@radix-ui/react-icons'
|
||||
import { type Column } from '@tanstack/react-table'
|
||||
import { DateRange } from 'react-day-picker'
|
||||
|
||||
interface DataTableColumnHeaderProps<TData, TValue> extends React.HTMLAttributes<HTMLDivElement> {
|
||||
column: Column<TData, TValue>
|
||||
title: string
|
||||
isDate?: boolean
|
||||
}
|
||||
|
||||
const DataTableColumnHeader = <TData, TValue>({
|
||||
column,
|
||||
title,
|
||||
isDate,
|
||||
className
|
||||
}: DataTableColumnHeaderProps<TData, TValue>) => {
|
||||
if (!column.getCanSort()) {
|
||||
if (!column.getCanSort() && !isDate) {
|
||||
return <div className={cn(className)}>{title}</div>
|
||||
}
|
||||
|
||||
if (isDate) {
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-2', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="data-[state=open]:bg-accent -ml-3 h-8">
|
||||
<span>{title}</span>
|
||||
<CalendarIcon className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="center">
|
||||
<Calendar
|
||||
mode="range"
|
||||
defaultMonth={new Date('2022-01-01')}
|
||||
selected={column.getFilterValue() as DateRange}
|
||||
onSelect={date => column.setFilterValue(date)}
|
||||
numberOfMonths={2}
|
||||
disabled={date => date > new Date() || date < new Date('1900-01-01')}
|
||||
initialFocus
|
||||
/>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex items-center space-x-2', className)}>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="sm" className="-ml-3 h-8 data-[state=open]:bg-accent">
|
||||
<Button variant="ghost" size="sm" className="data-[state=open]:bg-accent -ml-3 h-8">
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === 'desc' ? (
|
||||
<ArrowDownIcon className="ml-2 h-4 w-4" />
|
||||
|
|
@ -41,16 +71,16 @@ const DataTableColumnHeader = <TData, TValue>({
|
|||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="start">
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(false)}>
|
||||
<ArrowUpIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
||||
<ArrowUpIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
||||
Asc
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => column.toggleSorting(true)}>
|
||||
<ArrowDownIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
||||
<ArrowDownIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
||||
Desc
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={() => column.toggleVisibility(false)}>
|
||||
<EyeNoneIcon className="mr-2 h-3.5 w-3.5 text-muted-foreground/70" />
|
||||
<EyeNoneIcon className="text-muted-foreground/70 mr-2 h-3.5 w-3.5" />
|
||||
Hide
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
|
|
@ -15,21 +15,19 @@ import {
|
|||
CommandSeparator
|
||||
} from '@/components/ui/command'
|
||||
|
||||
interface DataTableFacetedFilterProps<TData, TValue> {
|
||||
interface DataTableFacetedFilterProps<TData, TValue, TOption> {
|
||||
column?: Column<TData, TValue>
|
||||
title?: string
|
||||
options: {
|
||||
label: string
|
||||
value: string
|
||||
icon?: React.ComponentType<{ className?: string }>
|
||||
}[]
|
||||
options: TOption extends { value: string; label: string; icon?: React.ComponentType<{ className?: string }> }
|
||||
? TOption[]
|
||||
: never
|
||||
}
|
||||
|
||||
const DataTableFacetedFilter = <TData, TValue>({
|
||||
const DataTableFacetedFilter = <TData, TValue, TOption>({
|
||||
column,
|
||||
title,
|
||||
options
|
||||
}: DataTableFacetedFilterProps<TData, TValue>) => {
|
||||
}: DataTableFacetedFilterProps<TData, TValue, TOption>) => {
|
||||
const facets = column?.getFacetedUniqueValues()
|
||||
const selectedValues = new Set(column?.getFilterValue() as string[])
|
||||
|
||||
|
|
@ -87,13 +85,13 @@ const DataTableFacetedFilter = <TData, TValue>({
|
|||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mr-2 flex h-4 w-4 items-center justify-center rounded-sm border border-primary',
|
||||
'border-primary mr-2 flex h-4 w-4 items-center justify-center rounded-sm border',
|
||||
isSelected ? 'bg-primary text-primary-foreground' : 'opacity-50 [&_svg]:invisible'
|
||||
)}
|
||||
>
|
||||
<CheckIcon className={cn('h-4 w-4')} />
|
||||
</div>
|
||||
{option.icon && <option.icon className="mr-2 h-4 w-4 text-muted-foreground" />}
|
||||
{option.icon && <option.icon className="text-muted-foreground mr-2 h-4 w-4" />}
|
||||
<span>{option.label}</span>
|
||||
{facets?.get(option.value) && (
|
||||
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { type Row } from '@tanstack/react-table'
|
||||
import { taskSchema } from '@/models/recipe/schema'
|
||||
import { recipeDashboardSchema } from '@/models/recipe/schema'
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
|
|
@ -22,7 +22,7 @@ interface DataTableRowActionsProps<TData> {
|
|||
}
|
||||
|
||||
const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) => {
|
||||
const task = taskSchema.parse(row.original)
|
||||
const task = recipeDashboardSchema.parse(row.original)
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
|
|
@ -35,7 +35,6 @@ const DataTableRowActions = <TData,>({ row }: DataTableRowActionsProps<TData>) =
|
|||
<DropdownMenuContent align="end" className="w-[160px]">
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Make a copy</DropdownMenuItem>
|
||||
<DropdownMenuItem>Favorite</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>Labels</DropdownMenuSubTrigger>
|
||||
|
|
@ -1,35 +1,62 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { type Table } from '@tanstack/react-table'
|
||||
import { statuses, priorities } from '@/models/data'
|
||||
// import { statuses, priorities } from '@/models/data'
|
||||
import DataTableFacetedFilter from './data-table-faceted-filter'
|
||||
import DataTableViewOptions from './data-table-view-options'
|
||||
import { Cross2Icon } from '@radix-ui/react-icons'
|
||||
import { Cross2Icon, ReloadIcon } from '@radix-ui/react-icons'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
|
||||
interface DataTableToolbarProps<TData> {
|
||||
table: Table<TData>
|
||||
globalFilter: string
|
||||
setGlobalFilter: (value: string) => void
|
||||
}
|
||||
|
||||
const DataTableToolbar = <TData,>({ table }: DataTableToolbarProps<TData>) => {
|
||||
const DataTableToolbar = <TData,>({ table, globalFilter, setGlobalFilter }: DataTableToolbarProps<TData>) => {
|
||||
const isFiltered = table.getState().columnFilters.length > 0
|
||||
|
||||
const getMaterial = useRecipeDashboard(state => state.getMaterials)
|
||||
|
||||
const {
|
||||
data: materials,
|
||||
isLoading,
|
||||
isError
|
||||
} = useQuery({
|
||||
queryKey: ['materials'],
|
||||
queryFn: () => getMaterial()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex w-full flex-col space-y-4 rounded-lg bg-white p-3 shadow-md">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-1 items-center space-x-2">
|
||||
<Input
|
||||
placeholder="Filter tasks..."
|
||||
value={(table.getColumn('title')?.getFilterValue() as string) ?? ''}
|
||||
onChange={event => table.getColumn('title')?.setFilterValue(event.target.value)}
|
||||
placeholder="Filter recipe..."
|
||||
value={globalFilter}
|
||||
onChange={event => setGlobalFilter(event.target.value)}
|
||||
className="h-8 w-[150px] lg:w-[250px]"
|
||||
/>
|
||||
{table.getColumn('status') && (
|
||||
{!isLoading && !isError ? (
|
||||
<DataTableFacetedFilter
|
||||
title="Material"
|
||||
options={
|
||||
materials as { value: string; label: string; icon?: React.ComponentType<{ className?: string }> }[]
|
||||
}
|
||||
/>
|
||||
) : !isError ? (
|
||||
<ReloadIcon className="text-muted-foreground mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<span className="text-muted-foreground">Error loading materials</span>
|
||||
)}
|
||||
{/* {table.getColumn('status') && (
|
||||
<DataTableFacetedFilter column={table.getColumn('status')} title="Status" options={statuses} />
|
||||
)}
|
||||
{table.getColumn('priority') && (
|
||||
<DataTableFacetedFilter column={table.getColumn('priority')} title="Priority" options={priorities} />
|
||||
)}
|
||||
)} */}
|
||||
{isFiltered && (
|
||||
<Button variant="ghost" onClick={() => table.resetColumnFilters()} className="h-8 px-2 lg:px-3">
|
||||
Reset
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import type { VisibilityState, ColumnFiltersState, SortingState, ColumnDef } from '@tanstack/react-table'
|
||||
import type { VisibilityState, ColumnFiltersState, SortingState, ColumnDef, FilterFn } from '@tanstack/react-table'
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
|
|
@ -9,10 +9,25 @@ import {
|
|||
getFacetedUniqueValues,
|
||||
flexRender
|
||||
} from '@tanstack/react-table'
|
||||
import { rankItem } from '@tanstack/match-sorter-utils'
|
||||
import { useState } from 'react'
|
||||
import DataTableToolbar from './data-table-toolbar'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import DataTablePagination from './data-table-pagination'
|
||||
import { ReloadIcon } from '@radix-ui/react-icons'
|
||||
|
||||
const fuzzyFilter: FilterFn<any> = (row, columnId, value, addMeta) => {
|
||||
// Rank the item
|
||||
const itemRank = rankItem(row.getValue(columnId), value)
|
||||
|
||||
// Store the itemRank info
|
||||
addMeta({
|
||||
itemRank
|
||||
})
|
||||
|
||||
// Return if the item should be filtered in/out
|
||||
return itemRank.passed
|
||||
}
|
||||
|
||||
interface DataTableProps<TData, TValue> {
|
||||
columns: ColumnDef<TData, TValue>[]
|
||||
|
|
@ -26,15 +41,23 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
|||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
|
||||
const [sorting, setSorting] = useState<SortingState>([])
|
||||
|
||||
const [globalFilter, setGlobalFilter] = useState<string>('')
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
filterFns: {
|
||||
fuzzy: fuzzyFilter
|
||||
},
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters
|
||||
columnFilters,
|
||||
globalFilter
|
||||
},
|
||||
globalFilterFn: fuzzyFilter,
|
||||
onGlobalFilterChange: setGlobalFilter,
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
|
|
@ -50,7 +73,7 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
|||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<DataTableToolbar table={table} />
|
||||
<DataTableToolbar table={table} globalFilter={globalFilter} setGlobalFilter={setGlobalFilter} />
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
|
|
@ -77,8 +100,9 @@ const DataTable = <TData, TValue>({ columns, data, isLoading }: DataTableProps<T
|
|||
))
|
||||
) : isLoading ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
Loading...
|
||||
<TableCell colSpan={columns.length} className="flex h-24 space-x-5 text-center">
|
||||
<span>Loading...</span>
|
||||
<ReloadIcon className="h-5 w-5 animate-spin" />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
12
client-electron/src/pages/recipes/recipe-edit.tsx
Normal file
12
client-electron/src/pages/recipes/recipe-edit.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import RecipeForm from './components/recipe-edit-components/recipe-form'
|
||||
|
||||
const RecipeEditPage: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Edit Recipe</h1>
|
||||
<RecipeForm />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeEditPage
|
||||
28
client-electron/src/pages/recipes/recipe-table.tsx
Normal file
28
client-electron/src/pages/recipes/recipe-table.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { columns } from './components/recipe-table-components/columns'
|
||||
import DataTable from './components/recipe-table-components/data-table'
|
||||
import useRecipeDashboard from '@/hooks/recipe-dashboard'
|
||||
import useLocalStorage from '@/hooks/localStorage'
|
||||
|
||||
const RecipesTablePage = () => {
|
||||
const recipeQuery = useLocalStorage(state => state.recipeQuery)
|
||||
const getRecipesDashboard = useRecipeDashboard(state => state.getRecipesDashboard)
|
||||
|
||||
const { data: recipeDashboardList, isLoading } = useQuery({
|
||||
queryKey: ['recipe-overview'],
|
||||
queryFn: () => getRecipesDashboard(recipeQuery)
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<section>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
|
||||
</section>
|
||||
<section>
|
||||
<DataTable data={recipeDashboardList ?? []} columns={columns} isLoading={isLoading} />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipesTablePage
|
||||
|
|
@ -1,24 +0,0 @@
|
|||
import { useQuery } from '@tanstack/react-query'
|
||||
import { columns } from './components/columns'
|
||||
import DataTable from './components/data-table'
|
||||
import { getRecipeOverview } from '@/hooks/recipe/get-recipe-overview'
|
||||
|
||||
const RecipesPage = () => {
|
||||
const { data: recipeOverviewList, isLoading } = useQuery({
|
||||
queryKey: ['recipe-overview'],
|
||||
queryFn: () => getRecipeOverview()
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-3">
|
||||
<section>
|
||||
<h1 className="text-3xl font-bold text-gray-900">Recipes</h1>
|
||||
</section>
|
||||
<section>
|
||||
<DataTable data={recipeOverviewList ?? []} columns={columns} isLoading={isLoading} />
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipesPage
|
||||
25
client-electron/src/pages/recipes/select-country.tsx
Normal file
25
client-electron/src/pages/recipes/select-country.tsx
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { Button } from '@/components/ui/button'
|
||||
import useLocalStorage from '@/hooks/localStorage'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
|
||||
const SelectCountryPage: React.FC = () => {
|
||||
const { recipeQuery, setRecipeQuery } = useLocalStorage(
|
||||
useShallow(state => ({
|
||||
recipeQuery: state.recipeQuery,
|
||||
setRecipeQuery: state.setRecipeQuery
|
||||
}))
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>SelectContryPage</h1>
|
||||
<Button variant={'link'} onClick={() => setRecipeQuery({ countryID: 'tha', filename: 'coffeethai02_635.json' })}>
|
||||
Thai
|
||||
</Button>
|
||||
{recipeQuery && <Link to={`/recipes/${recipeQuery?.countryID}/${recipeQuery?.filename}`}>Recipes</Link>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SelectCountryPage
|
||||
Loading…
Add table
Add a link
Reference in a new issue