From 0fe469b5c6c6e9608b862cea093b7b2d300bd0f0 Mon Sep 17 00:00:00 2001 From: Kenta420 Date: Mon, 19 Feb 2024 14:24:05 +0700 Subject: [PATCH] Fixed: fixed bug scrcpy and shell is disconnect when switch page --- client-electron/electron/adb.ts | 62 ++ client-electron/electron/keychain.ts | 17 +- client-electron/electron/main.ts | 4 + client-electron/package-lock.json | 197 +++-- client-electron/package.json | 4 + client-electron/src/App.tsx | 15 +- .../src/components/ui/calendar.tsx | 70 ++ client-electron/src/hooks/localStorage.ts | 25 + client-electron/src/hooks/recipe-dashboard.ts | 52 ++ .../src/hooks/recipe/get-recipe-overview.ts | 16 - client-electron/src/hooks/scrcpy-android.ts | 199 +++++ client-electron/src/hooks/shell-android.ts | 88 +++ client-electron/src/layouts/MainLayout.tsx | 3 +- client-electron/src/lib/customAxios.ts | 25 +- client-electron/src/main.tsx | 52 +- client-electron/src/models/data.ts | 19 + client-electron/src/models/recipe/schema.ts | 21 +- client-electron/src/models/recipe/tasks.json | 702 ------------------ .../android/components/file-manager-tab.tsx | 66 +- .../pages/android/components/scrcpy-tab.tsx | 313 +------- .../pages/android/components/shell-tab.tsx | 96 +-- .../src/pages/android/components/tool-bar.tsx | 66 +- .../src/pages/recipes/components/columns.tsx | 91 --- .../recipe-edit-components/recipe-form.tsx | 9 + .../recipe-table-components/columns.tsx | 119 +++ .../data-table-column-header.tsx | 42 +- .../data-table-faceted-filter.tsx | 18 +- .../data-table-pagination.tsx | 0 .../data-table-row-actions.tsx | 5 +- .../data-table-toolbar.tsx | 43 +- .../data-table-view-options.tsx | 0 .../data-table.tsx | 34 +- .../src/pages/recipes/recipe-edit.tsx | 12 + .../src/pages/recipes/recipe-table.tsx | 28 + client-electron/src/pages/recipes/recipes.tsx | 24 - .../src/pages/recipes/select-country.tsx | 25 + server/models/v2/material.go | 6 + server/models/v2/recipe.go | 16 + server/routers/auth.go | 21 +- server/routers/recipe.go | 4 +- server/routers/v2/material.go | 52 ++ server/routers/v2/recipe.go | 69 ++ server/server.go | 14 + 43 files changed, 1378 insertions(+), 1366 deletions(-) create mode 100644 client-electron/electron/adb.ts create mode 100644 client-electron/src/components/ui/calendar.tsx create mode 100644 client-electron/src/hooks/localStorage.ts create mode 100644 client-electron/src/hooks/recipe-dashboard.ts delete mode 100644 client-electron/src/hooks/recipe/get-recipe-overview.ts create mode 100644 client-electron/src/hooks/scrcpy-android.ts create mode 100644 client-electron/src/hooks/shell-android.ts delete mode 100644 client-electron/src/models/recipe/tasks.json delete mode 100644 client-electron/src/pages/recipes/components/columns.tsx create mode 100644 client-electron/src/pages/recipes/components/recipe-edit-components/recipe-form.tsx create mode 100644 client-electron/src/pages/recipes/components/recipe-table-components/columns.tsx rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table-column-header.tsx (52%) rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table-faceted-filter.tsx (88%) rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table-pagination.tsx (100%) rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table-row-actions.tsx (92%) rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table-toolbar.tsx (52%) rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table-view-options.tsx (100%) rename client-electron/src/pages/recipes/components/{ => recipe-table-components}/data-table.tsx (76%) create mode 100644 client-electron/src/pages/recipes/recipe-edit.tsx create mode 100644 client-electron/src/pages/recipes/recipe-table.tsx delete mode 100644 client-electron/src/pages/recipes/recipes.tsx create mode 100644 client-electron/src/pages/recipes/select-country.tsx create mode 100644 server/models/v2/material.go create mode 100644 server/models/v2/recipe.go create mode 100644 server/routers/v2/material.go create mode 100644 server/routers/v2/recipe.go 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: ( -
+

404

Page Not Found

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)} /> + +
+
+ setPushPath(e.target.value)} /> + setPushFile(e.target.files?.item(0))} /> + +
-
- 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 - - -
-
-
- -
- - -
) }) 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 }) => {
-
- +
+ {terminal ? ( + + ) : ( + + )}
{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 )} + +
) 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 ( +
+ + + + + + column.setFilterValue(date)} + numberOfMonths={2} + disabled={date => date > new Date() || date < new Date('1900-01-01')} + initialFocus + /> + + +
+ ) + } + return (
-