diff --git a/client-electron/electron/adb.ts b/client-electron/electron/adb.ts
new file mode 100644
index 0000000..16bebb6
--- /dev/null
+++ b/client-electron/electron/adb.ts
@@ -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)
+}
diff --git a/client-electron/electron/keychain.ts b/client-electron/electron/keychain.ts
index aab8305..a814723 100644
--- a/client-electron/electron/keychain.ts
+++ b/client-electron/electron/keychain.ts
@@ -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)
})
}
diff --git a/client-electron/electron/main.ts b/client-electron/electron/main.ts
index 0b0d1d5..45bb7ba 100644
--- a/client-electron/electron/main.ts
+++ b/client-electron/electron/main.ts
@@ -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)
})
diff --git a/client-electron/package-lock.json b/client-electron/package-lock.json
index 5423393..472edc3 100644
--- a/client-electron/package-lock.json
+++ b/client-electron/package-lock.json
@@ -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",
diff --git a/client-electron/package.json b/client-electron/package.json
index f43262e..4b16abc 100644
--- a/client-electron/package.json
+++ b/client-electron/package.json
@@ -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",
diff --git a/client-electron/src/App.tsx b/client-electron/src/App.tsx
index bdaf30e..2c09dc3 100644
--- a/client-electron/src/App.tsx
+++ b/client-electron/src/App.tsx
@@ -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:
},
{
- path: 'recipes',
- element:
+ path: 'recipes/select-country',
+ element:
+ },
+ {
+ path: 'recipes/:country_id/:filename',
+ element:
},
{
path: 'android',
@@ -68,7 +73,7 @@ function router() {
{
path: '*',
element: (
-
+
diff --git a/client-electron/src/components/ui/calendar.tsx b/client-electron/src/components/ui/calendar.tsx
new file mode 100644
index 0000000..66a23cc
--- /dev/null
+++ b/client-electron/src/components/ui/calendar.tsx
@@ -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
+
+function Calendar({
+ className,
+ classNames,
+ showOutsideDays = true,
+ ...props
+}: CalendarProps) {
+ return (
+ .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 }) => ,
+ IconRight: ({ ...props }) => ,
+ }}
+ {...props}
+ />
+ )
+}
+Calendar.displayName = "Calendar"
+
+export { Calendar }
diff --git a/client-electron/src/hooks/localStorage.ts b/client-electron/src/hooks/localStorage.ts
new file mode 100644
index 0000000..549dffd
--- /dev/null
+++ b/client-electron/src/hooks/localStorage.ts
@@ -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(
+ set => ({
+ recipeQuery: undefined,
+ setRecipeQuery(query) {
+ set({ recipeQuery: query })
+ }
+ }),
+ {
+ name: 'local-storage',
+ storage: createJSONStorage(() => localStorage)
+ }
+ )
+)
+
+export default useLocalStorage
diff --git a/client-electron/src/hooks/recipe-dashboard.ts b/client-electron/src/hooks/recipe-dashboard.ts
new file mode 100644
index 0000000..f3e2ea8
--- /dev/null
+++ b/client-electron/src/hooks/recipe-dashboard.ts
@@ -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
+ getMaterials: (filter?: RecipeDashboardFilterQuery) => Promise
+}
+
+const useRecipeDashboard = create(() => ({
+ async getRecipesDashboard(filter) {
+ return customAxios
+ .get('/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('/v2/materials/dashboard', {
+ params: filter
+ ? {
+ country_id: filter.countryID,
+ filename: filter.filename
+ }
+ : undefined
+ })
+ .then(res => {
+ return res.data
+ })
+ }
+}))
+
+export default useRecipeDashboard
diff --git a/client-electron/src/hooks/recipe/get-recipe-overview.ts b/client-electron/src/hooks/recipe/get-recipe-overview.ts
deleted file mode 100644
index 741790e..0000000
--- a/client-electron/src/hooks/recipe/get-recipe-overview.ts
+++ /dev/null
@@ -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 => {
- return customAxios
- .get(import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/recipe/overview', { params: query })
- .then(res => {
- return res.data
- })
-}
diff --git a/client-electron/src/hooks/scrcpy-android.ts b/client-electron/src/hooks/scrcpy-android.ts
new file mode 100644
index 0000000..1086c7e
--- /dev/null
+++ b/client-electron/src/hooks/scrcpy-android.ts
@@ -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((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
diff --git a/client-electron/src/hooks/shell-android.ts b/client-electron/src/hooks/shell-android.ts
new file mode 100644
index 0000000..961deeb
--- /dev/null
+++ b/client-electron/src/hooks/shell-android.ts
@@ -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> | undefined
+ writerProcess: WritableStream | undefined
+ startTerminal: (adb: Adb | undefined) => void
+ killTerminal: () => void
+}
+
+const useShellAndroid = create((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({
+ 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
diff --git a/client-electron/src/layouts/MainLayout.tsx b/client-electron/src/layouts/MainLayout.tsx
index ccf1709..83c193b 100644
--- a/client-electron/src/layouts/MainLayout.tsx
+++ b/client-electron/src/layouts/MainLayout.tsx
@@ -9,7 +9,6 @@ import { Toaster } from '@/components/ui/toaster'
interface MainLayoutProps {
sidebarMenu: MenuList
}
-
const MainLayout: React.FC = ({ sidebarMenu }) => {
const { userInfo, getUserInfo } = userAuthStore(
useShallow(state => ({
@@ -38,7 +37,7 @@ const MainLayout: React.FC = ({ sidebarMenu }) => {
<>
-
+
diff --git a/client-electron/src/lib/customAxios.ts b/client-electron/src/lib/customAxios.ts
index 752e54d..f2c68db 100644
--- a/client-electron/src/lib/customAxios.ts
+++ b/client-electron/src/lib/customAxios.ts
@@ -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)
diff --git a/client-electron/src/main.tsx b/client-electron/src/main.tsx
index ff86c06..d2bb9b5 100644
--- a/client-electron/src/main.tsx
+++ b/client-electron/src/main.tsx
@@ -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) => {
diff --git a/client-electron/src/models/data.ts b/client-electron/src/models/data.ts
index 589c148..e33a955 100644
--- a/client-electron/src/models/data.ts
+++ b/client-electron/src/models/data.ts
@@ -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'
+ }
+]
diff --git a/client-electron/src/models/recipe/schema.ts b/client-electron/src/models/recipe/schema.ts
index 38d2bcd..b71f56d 100644
--- a/client-electron/src/models/recipe/schema.ts
+++ b/client-electron/src/models/recipe/schema.ts
@@ -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
-
-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
+export type RecipeDashboard = z.infer
diff --git a/client-electron/src/models/recipe/tasks.json b/client-electron/src/models/recipe/tasks.json
deleted file mode 100644
index 1a2132f..0000000
--- a/client-electron/src/models/recipe/tasks.json
+++ /dev/null
@@ -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"
- }
-]
\ No newline at end of file
diff --git a/client-electron/src/pages/android/components/file-manager-tab.tsx b/client-electron/src/pages/android/components/file-manager-tab.tsx
index 132bf4c..669dd2d 100644
--- a/client-electron/src/pages/android/components/file-manager-tab.tsx
+++ b/client-electron/src/pages/android/components/file-manager-tab.tsx
@@ -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 = ({ adb }) => {
const [path, setPath] = useState('')
+ const [pushPath, setPushPath] = useState('')
+ const [pushFile, setPushFile] = useState()
const [files, setFiles] = useState()
+ 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 = ({ adb }) => {
}
}, [adb, path])
- const refresh = useCallback(() => {
- console.log('Refreshing...')
- }, [])
-
return (
@@ -143,19 +161,29 @@ export const FileManagerTab: React.FC = ({ adb }) => {
Manage files in Android
-
-
-
+
+
-
- setPath(e.target.value)} />
-
-
-
{files && }
+
{files && }
diff --git a/client-electron/src/pages/android/components/scrcpy-tab.tsx b/client-electron/src/pages/android/components/scrcpy-tab.tsx
index 5c49323..9120d0e 100644
--- a/client-electron/src/pages/android/components/scrcpy-tab.tsx
+++ b/client-electron/src/pages/android/components/scrcpy-tab.tsx
@@ -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
= memo(({ adb }) => {
- const logcatRef = useRef(null)
const scrcpyScreenRef = useRef(null)
- const [process, setProcess] = useState()
- const [client, setClient] = useState()
- const [decoder, setDecoder] = useState()
-
- 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({
- 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 (
@@ -271,54 +40,44 @@ export const ScrcpyTab: React.FC = memo(({ adb }) => {
Scrcpy
Stream and control your Android device from your computer
-
+
-
-
-
+
Control
-
- {client ? (
+
+ {scrcpyClient ? (
) : (
-
-
- {/* logcat card */}
-
-
- Logcat
-
-
-
-
-
-
- Save changes
-
)
})
diff --git a/client-electron/src/pages/android/components/shell-tab.tsx b/client-electron/src/pages/android/components/shell-tab.tsx
index e2c961a..099d545 100644
--- a/client-electron/src/pages/android/components/shell-tab.tsx
+++ b/client-electron/src/pages/android/components/shell-tab.tsx
@@ -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
= memo(({ adb }) => {
- const [process, setProcess] = useState()
- const [terminal, setTerminal] = useState()
-
- const [reader, setReader] = useState | 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({
- 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 (
@@ -88,10 +31,16 @@ export const ShellTab: React.FC = memo(({ adb }) => {
-
-
- Kill Shell
-
+
+ {terminal ? (
+
+ Terminate
+
+ ) : (
+ startTerminal(adb)}>
+ Start
+
+ )}
{terminal ? (
@@ -114,7 +63,6 @@ const ShellTerminal: React.FC
= ({ terminal }) => {
const shellRef = useRef(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) {
diff --git a/client-electron/src/pages/android/components/tool-bar.tsx b/client-electron/src/pages/android/components/tool-bar.tsx
index ec5c6a9..e81628c 100644
--- a/client-electron/src/pages/android/components/tool-bar.tsx
+++ b/client-electron/src/pages/android/components/tool-bar.tsx
@@ -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 = ({ manager, adb, device, setAdb,
const [version, setVersion] = useState('')
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 = ({ 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 = ({ 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 = ({ manager, adb, device, setAdb,
Connect
)}
+
+
+ Connect Adb Daemon
+
)
diff --git a/client-electron/src/pages/recipes/components/columns.tsx b/client-electron/src/pages/recipes/components/columns.tsx
deleted file mode 100644
index 4eef8ab..0000000
--- a/client-electron/src/pages/recipes/components/columns.tsx
+++ /dev/null
@@ -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
[] = [
- {
- id: 'select',
- header: ({ table }) => (
- table.toggleAllPageRowsSelected(!!value)}
- aria-label="Select all"
- className="translate-y-[2px]"
- />
- ),
- cell: ({ row }) => (
- row.toggleSelected(!!value)}
- aria-label="Select row"
- className="translate-y-[2px]"
- />
- ),
- enableSorting: false,
- enableHiding: false
- },
- {
- accessorKey: 'productCode',
- header: ({ column }) => ,
- cell: ({ row }) => {row.getValue('productCode')}
,
- enableSorting: false,
- enableHiding: false
- },
- {
- accessorKey: 'name',
- header: ({ column }) => ,
- cell: ({ row }) => {
- const label = { label: 'Test Label' } // labels.find(label => label.value === row.getValue('label'))
-
- return (
-
- {label && {label.label}}
- {row.getValue('name')}
-
- )
- }
- },
- {
- accessorKey: 'status',
- header: ({ column }) => ,
- cell: ({ row }) => {
- const status: RecipeStatus = row.getValue('status')
- const StatusIcon = getRecipeStatusIcon(status)
- if (!status) {
- return null
- }
-
- return (
-
- {}
- {}
-
- )
- },
- filterFn: (row, id, value) => {
- return value.includes(row.getValue(id))
- }
- },
- {
- accessorKey: 'lastUpdated',
- header: ({ column }) => ,
- cell: ({ row }) => {
- return (
-
- {row.getValue('lastUpdated')}
-
- )
- },
- filterFn: (row, id, value) => {
- return value.includes(row.getValue(id))
- }
- },
- {
- id: 'actions',
- cell: ({ row }) =>
- }
-]
diff --git a/client-electron/src/pages/recipes/components/recipe-edit-components/recipe-form.tsx b/client-electron/src/pages/recipes/components/recipe-edit-components/recipe-form.tsx
new file mode 100644
index 0000000..99ba5f4
--- /dev/null
+++ b/client-electron/src/pages/recipes/components/recipe-edit-components/recipe-form.tsx
@@ -0,0 +1,9 @@
+const RecipeForm: React.FC = () => {
+ return (
+
+
Recipe Form
+
+ )
+}
+
+export default RecipeForm
diff --git a/client-electron/src/pages/recipes/components/recipe-table-components/columns.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/columns.tsx
new file mode 100644
index 0000000..0371790
--- /dev/null
+++ b/client-electron/src/pages/recipes/components/recipe-table-components/columns.tsx
@@ -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[] = [
+ {
+ id: 'select',
+ header: ({ table }) => (
+ table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ className="translate-y-[2px]"
+ />
+ ),
+ cell: ({ row }) => (
+ row.toggleSelected(!!value)}
+ aria-label="Select row"
+ className="translate-y-[2px]"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false
+ },
+ {
+ accessorKey: 'productCode',
+ header: ({ column }) => ,
+ cell: ({ row }) => {row.getValue('productCode')}
,
+ enableHiding: false,
+ enableGlobalFilter: true,
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ }
+ },
+ {
+ accessorKey: 'name',
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ //const label = { label: 'Test Label' } // labels.find(label => label.value === row.getValue('label'))
+
+ return (
+
+ {/* {label && {label.label}} */}
+ {row.getValue('name')}
+
+ )
+ },
+ enableGlobalFilter: true,
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ }
+ },
+ {
+ accessorKey: 'nameEng',
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ return (
+
+ {row.getValue('nameEng')}
+
+ )
+ },
+ enableGlobalFilter: true,
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ }
+ },
+ {
+ accessorKey: 'inUse',
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ return (
+
+
+ {row.getValue('inUse') ? (
+
+ ) : (
+
+ )}
+
+
+ )
+ },
+ filterFn: (row, id, value) => {
+ return value.includes(row.getValue(id))
+ }
+ },
+ {
+ accessorKey: 'lastUpdated',
+ header: ({ column }) => ,
+ cell: ({ row }) => {
+ return (
+
+ {dateFormat.format(row.getValue('lastUpdated'), 'dd-MM-yyyy HH:mm:ss')}
+
+ )
+ },
+ 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 }) =>
+ }
+]
diff --git a/client-electron/src/pages/recipes/components/data-table-column-header.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx
similarity index 52%
rename from client-electron/src/pages/recipes/components/data-table-column-header.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx
index 4535b97..3369fe9 100644
--- a/client-electron/src/pages/recipes/components/data-table-column-header.tsx
+++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-column-header.tsx
@@ -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 extends React.HTMLAttributes {
column: Column
title: string
+ isDate?: boolean
}
const DataTableColumnHeader = ({
column,
title,
+ isDate,
className
}: DataTableColumnHeaderProps) => {
- if (!column.getCanSort()) {
+ if (!column.getCanSort() && !isDate) {
return {title}
}
+ if (isDate) {
+ return (
+
+
+
+
+ {title}
+
+
+
+
+ column.setFilterValue(date)}
+ numberOfMonths={2}
+ disabled={date => date > new Date() || date < new Date('1900-01-01')}
+ initialFocus
+ />
+
+
+
+ )
+ }
+
return (
-
+
{title}
{column.getIsSorted() === 'desc' ? (
@@ -41,16 +71,16 @@ const DataTableColumnHeader = ({
column.toggleSorting(false)}>
-
+
Asc
column.toggleSorting(true)}>
-
+
Desc
column.toggleVisibility(false)}>
-
+
Hide
diff --git a/client-electron/src/pages/recipes/components/data-table-faceted-filter.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-faceted-filter.tsx
similarity index 88%
rename from client-electron/src/pages/recipes/components/data-table-faceted-filter.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table-faceted-filter.tsx
index 6a733d4..f7efb65 100644
--- a/client-electron/src/pages/recipes/components/data-table-faceted-filter.tsx
+++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-faceted-filter.tsx
@@ -15,21 +15,19 @@ import {
CommandSeparator
} from '@/components/ui/command'
-interface DataTableFacetedFilterProps {
+interface DataTableFacetedFilterProps {
column?: Column
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 = ({
+const DataTableFacetedFilter = ({
column,
title,
options
-}: DataTableFacetedFilterProps) => {
+}: DataTableFacetedFilterProps) => {
const facets = column?.getFacetedUniqueValues()
const selectedValues = new Set(column?.getFilterValue() as string[])
@@ -87,13 +85,13 @@ const DataTableFacetedFilter = ({
>
- {option.icon && }
+ {option.icon && }
{option.label}
{facets?.get(option.value) && (
diff --git a/client-electron/src/pages/recipes/components/data-table-pagination.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-pagination.tsx
similarity index 100%
rename from client-electron/src/pages/recipes/components/data-table-pagination.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table-pagination.tsx
diff --git a/client-electron/src/pages/recipes/components/data-table-row-actions.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx
similarity index 92%
rename from client-electron/src/pages/recipes/components/data-table-row-actions.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx
index 73bd66e..6897f20 100644
--- a/client-electron/src/pages/recipes/components/data-table-row-actions.tsx
+++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-row-actions.tsx
@@ -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 {
}
const DataTableRowActions = ({ row }: DataTableRowActionsProps) => {
- const task = taskSchema.parse(row.original)
+ const task = recipeDashboardSchema.parse(row.original)
return (
@@ -35,7 +35,6 @@ const DataTableRowActions = ({ row }: DataTableRowActionsProps) =
Edit
Make a copy
- Favorite
Labels
diff --git a/client-electron/src/pages/recipes/components/data-table-toolbar.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-toolbar.tsx
similarity index 52%
rename from client-electron/src/pages/recipes/components/data-table-toolbar.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table-toolbar.tsx
index cc3e2d4..ac8c77c 100644
--- a/client-electron/src/pages/recipes/components/data-table-toolbar.tsx
+++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-toolbar.tsx
@@ -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 {
table: Table
+ globalFilter: string
+ setGlobalFilter: (value: string) => void
}
-const DataTableToolbar = ({ table }: DataTableToolbarProps) => {
+const DataTableToolbar = ({ table, globalFilter, setGlobalFilter }: DataTableToolbarProps) => {
const isFiltered = table.getState().columnFilters.length > 0
+ const getMaterial = useRecipeDashboard(state => state.getMaterials)
+
+ const {
+ data: materials,
+ isLoading,
+ isError
+ } = useQuery({
+ queryKey: ['materials'],
+ queryFn: () => getMaterial()
+ })
+
return (
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 ? (
+
}[]
+ }
+ />
+ ) : !isError ? (
+
+ ) : (
+ Error loading materials
+ )}
+ {/* {table.getColumn('status') && (
)}
{table.getColumn('priority') && (
- )}
+ )} */}
{isFiltered && (
table.resetColumnFilters()} className="h-8 px-2 lg:px-3">
Reset
diff --git a/client-electron/src/pages/recipes/components/data-table-view-options.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table-view-options.tsx
similarity index 100%
rename from client-electron/src/pages/recipes/components/data-table-view-options.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table-view-options.tsx
diff --git a/client-electron/src/pages/recipes/components/data-table.tsx b/client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx
similarity index 76%
rename from client-electron/src/pages/recipes/components/data-table.tsx
rename to client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx
index d890b52..1e74aae 100644
--- a/client-electron/src/pages/recipes/components/data-table.tsx
+++ b/client-electron/src/pages/recipes/components/recipe-table-components/data-table.tsx
@@ -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 = (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 {
columns: ColumnDef[]
@@ -26,15 +41,23 @@ const DataTable = ({ columns, data, isLoading }: DataTableProps([])
const [sorting, setSorting] = useState([])
+ const [globalFilter, setGlobalFilter] = useState('')
+
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 = ({ columns, data, isLoading }: DataTableProps
-
+
@@ -77,8 +100,9 @@ const DataTable = ({ columns, data, isLoading }: DataTableProps
-
- Loading...
+
+ Loading...
+
) : (
diff --git a/client-electron/src/pages/recipes/recipe-edit.tsx b/client-electron/src/pages/recipes/recipe-edit.tsx
new file mode 100644
index 0000000..784f45d
--- /dev/null
+++ b/client-electron/src/pages/recipes/recipe-edit.tsx
@@ -0,0 +1,12 @@
+import RecipeForm from './components/recipe-edit-components/recipe-form'
+
+const RecipeEditPage: React.FC = () => {
+ return (
+
+
Edit Recipe
+
+
+ )
+}
+
+export default RecipeEditPage
diff --git a/client-electron/src/pages/recipes/recipe-table.tsx b/client-electron/src/pages/recipes/recipe-table.tsx
new file mode 100644
index 0000000..26618ca
--- /dev/null
+++ b/client-electron/src/pages/recipes/recipe-table.tsx
@@ -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 (
+
+
+
+
+ )
+}
+
+export default RecipesTablePage
diff --git a/client-electron/src/pages/recipes/recipes.tsx b/client-electron/src/pages/recipes/recipes.tsx
deleted file mode 100644
index e69ef3c..0000000
--- a/client-electron/src/pages/recipes/recipes.tsx
+++ /dev/null
@@ -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 (
-
-
-
-
- )
-}
-
-export default RecipesPage
diff --git a/client-electron/src/pages/recipes/select-country.tsx b/client-electron/src/pages/recipes/select-country.tsx
new file mode 100644
index 0000000..2903d8c
--- /dev/null
+++ b/client-electron/src/pages/recipes/select-country.tsx
@@ -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 (
+
+
SelectContryPage
+ setRecipeQuery({ countryID: 'tha', filename: 'coffeethai02_635.json' })}>
+ Thai
+
+ {recipeQuery && Recipes}
+
+ )
+}
+
+export default SelectCountryPage
diff --git a/server/models/v2/material.go b/server/models/v2/material.go
new file mode 100644
index 0000000..d8f3802
--- /dev/null
+++ b/server/models/v2/material.go
@@ -0,0 +1,6 @@
+package v2
+
+type MaterialDashboard struct {
+ Lebel string `json:"lebel"`
+ Value string `json:"value"`
+}
diff --git a/server/models/v2/recipe.go b/server/models/v2/recipe.go
new file mode 100644
index 0000000..031c3bd
--- /dev/null
+++ b/server/models/v2/recipe.go
@@ -0,0 +1,16 @@
+package v2
+
+/*
+ This is the recipe model that specificly for API v2 version.
+ But some of the model of recipe can be used in the main version of the API.
+*/
+
+type DashboardRecipe struct {
+ ProductCode string `json:"productCode"`
+ InUse bool `json:"inUse"`
+ Name string `json:"name"`
+ NameENG string `json:"nameEng"`
+ Image string `json:"image"`
+ LastUpdated string `json:"lastUpdated"`
+ SubRecipe []*DashboardRecipe `json:"subRecipe,omitempty"`
+}
diff --git a/server/routers/auth.go b/server/routers/auth.go
index fd9c7f3..0c60a49 100644
--- a/server/routers/auth.go
+++ b/server/routers/auth.go
@@ -185,10 +185,25 @@ func (ar *AuthRouter) Route(r chi.Router) {
json.NewEncoder(w).Encode(value)
})
- r.Get("/refresh", func(w http.ResponseWriter, r *http.Request) {
+ r.Post("/refresh", func(w http.ResponseWriter, r *http.Request) {
// get refresh token from query string
- refreshToken := r.URL.Query().Get("refresh_token")
- redirectTo := r.URL.Query().Get("redirect_to")
+
+ // get refresh token from body and redirect_to from body and mashal it to struct
+ var refreshToken string
+ var redirectTo string
+
+ err := json.NewDecoder(r.Body).Decode(&struct {
+ RefreshToken string `json:"refresh_token"`
+ RedirectTo string `json:"redirect_to"`
+ }{refreshToken, redirectTo})
+
+ if err != nil {
+ http.Error(w, "Error decoding body", http.StatusBadRequest)
+ return
+ }
+
+ // refreshToken := r.URL.Query().Get("refresh_token")
+ // redirectTo := r.URL.Query().Get("redirect_to")
if refreshToken == "" {
http.Error(w, "Refresh token not found", http.StatusBadRequest)
return
diff --git a/server/routers/recipe.go b/server/routers/recipe.go
index 5681898..be8964b 100644
--- a/server/routers/recipe.go
+++ b/server/routers/recipe.go
@@ -55,8 +55,8 @@ func (rr *RecipeRouter) Route(r chi.Router) {
r.Get("/{country}/{filename}/all", func(w http.ResponseWriter, r *http.Request) {
w.Header().Add("Content-Type", "application/json")
- country := r.URL.Query().Get("country")
- filename := r.URL.Query().Get("filename")
+ country := chi.URLParam(r, "country")
+ filename := chi.URLParam(r, "filename")
rr.taoLogger.Log.Debug("RecipeRouter.GetAll", zap.Any("country", country), zap.Any("filename", filename))
diff --git a/server/routers/v2/material.go b/server/routers/v2/material.go
new file mode 100644
index 0000000..76eaec8
--- /dev/null
+++ b/server/routers/v2/material.go
@@ -0,0 +1,52 @@
+package v2
+
+import (
+ "encoding/json"
+ "net/http"
+ "recipe-manager/data"
+ modelsV2 "recipe-manager/models/v2"
+ "recipe-manager/services/logger"
+ "strconv"
+
+ "github.com/go-chi/chi/v5"
+)
+
+type materialRouter struct {
+ data *data.Data
+ taoLogger *logger.TaoLogger
+}
+
+func NewMaterialRouter(data *data.Data, taoLogger *logger.TaoLogger) *materialRouter {
+ return &materialRouter{
+ data: data,
+ taoLogger: taoLogger,
+ }
+}
+
+func (mr *materialRouter) Route(r chi.Router) {
+ r.Route("/materials", func(r chi.Router) {
+ r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+
+ countryID := r.URL.Query().Get("country_id")
+ filename := r.URL.Query().Get("filename")
+
+ materials := mr.data.GetMaterialSetting(countryID, filename)
+
+ result := make([]modelsV2.MaterialDashboard, 0, len(materials))
+ for _, material := range materials {
+
+ dashboardMaterial := modelsV2.MaterialDashboard{
+ Lebel: material.MaterialName,
+ Value: strconv.Itoa(int(material.ID)),
+ }
+
+ result = append(result, dashboardMaterial)
+ }
+
+ if err := json.NewEncoder(w).Encode(result); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ })
+ })
+}
diff --git a/server/routers/v2/recipe.go b/server/routers/v2/recipe.go
new file mode 100644
index 0000000..98b26fd
--- /dev/null
+++ b/server/routers/v2/recipe.go
@@ -0,0 +1,69 @@
+package v2
+
+import (
+ "encoding/json"
+ "net/http"
+ "recipe-manager/data"
+ modelsV2 "recipe-manager/models/v2"
+ "recipe-manager/services/logger"
+
+ "github.com/go-chi/chi/v5"
+)
+
+type recipeRouter struct {
+ data *data.Data
+ taoLogger *logger.TaoLogger
+}
+
+func NewRecipeRouter(data *data.Data, taoLogger *logger.TaoLogger) *recipeRouter {
+ return &recipeRouter{
+ data: data,
+ taoLogger: taoLogger,
+ }
+}
+
+func (rr *recipeRouter) Route(r chi.Router) {
+ r.Route("/recipes", func(r chi.Router) {
+ r.Get("/dashboard", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Add("Content-Type", "application/json")
+
+ countryID := r.URL.Query().Get("country_id")
+ filename := r.URL.Query().Get("filename")
+
+ recipes := rr.data.GetRecipe(countryID, filename)
+
+ result := make([]modelsV2.DashboardRecipe, 0, len(recipes.Recipe01))
+ for _, recipe := range recipes.Recipe01 {
+
+ dashboardRecipe := modelsV2.DashboardRecipe{
+ ProductCode: recipe.ProductCode,
+ Name: recipe.Name,
+ NameENG: recipe.OtherName,
+ InUse: recipe.IsUse,
+ LastUpdated: recipe.LastChange,
+ Image: "",
+ }
+
+ if recipe.SubMenu != nil && len(recipe.SubMenu) > 0 {
+ dashboardRecipe.SubRecipe = make([]*modelsV2.DashboardRecipe, 0, len(recipe.SubMenu))
+ for _, subMenu := range recipe.SubMenu {
+ dashboardRecipe.SubRecipe = append(dashboardRecipe.SubRecipe, &modelsV2.DashboardRecipe{
+ ProductCode: subMenu.ProductCode,
+ Name: subMenu.Name,
+ NameENG: subMenu.OtherName,
+ InUse: subMenu.IsUse,
+ LastUpdated: recipe.LastChange,
+ Image: "",
+ })
+ }
+ }
+
+ result = append(result, dashboardRecipe)
+ }
+
+ if err := json.NewEncoder(w).Encode(result); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+ })
+ })
+}
diff --git a/server/server.go b/server/server.go
index fd961f9..52feb83 100644
--- a/server/server.go
+++ b/server/server.go
@@ -12,6 +12,7 @@ import (
"recipe-manager/middlewares"
"recipe-manager/models"
"recipe-manager/routers"
+ routersV2 "recipe-manager/routers/v2"
"recipe-manager/services/logger"
"recipe-manager/services/oauth"
"recipe-manager/services/recipe"
@@ -142,6 +143,19 @@ func (s *Server) createHandler() {
})
+ // Protected Group V2
+ r.Group(func(r chi.Router) {
+ r.Route("/v2", func(r chi.Router) {
+ r.Use(func(next http.Handler) http.Handler {
+ return middlewares.Authorize(s.oauth, userService, next)
+ })
+
+ // Recipe Router
+ rr := routersV2.NewRecipeRouter(s.data, s.taoLogger)
+ rr.Route(r)
+ })
+ })
+
// routers.NewToppingRouter(s.data, s.taoLogger).Route(r)
r.NotFound(func(w http.ResponseWriter, r *http.Request) {