Fixed: fixed bug scrcpy and shell is disconnect when switch page

This commit is contained in:
Kenta420 2024-02-19 14:24:05 +07:00
parent 9543d4541c
commit 0fe469b5c6
43 changed files with 1378 additions and 1366 deletions

View 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)
}

View file

@ -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)
})
}

View file

@ -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)
})

View file

@ -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",

View file

@ -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",

View file

@ -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>

View 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 }

View 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

View 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

View file

@ -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
})
}

View 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

View 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

View file

@ -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>

View file

@ -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)

View file

@ -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) => {

View file

@ -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'
}
]

View file

@ -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>

View file

@ -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"
}
]

View file

@ -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>

View file

@ -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>
)
})

View file

@ -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) {

View file

@ -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>
)

View file

@ -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} />
}
]

View file

@ -0,0 +1,9 @@
const RecipeForm: React.FC = () => {
return (
<div>
<h1>Recipe Form</h1>
</div>
)
}
export default RecipeForm

View file

@ -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} />
}
]

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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

View file

@ -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>
) : (

View 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

View 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

View file

@ -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

View 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