diff --git a/client-electron/electron/adb-native.ts b/client-electron/electron/adb-native.ts new file mode 100644 index 0000000..70aee24 --- /dev/null +++ b/client-electron/electron/adb-native.ts @@ -0,0 +1,165 @@ +import { Adb, AdbDaemonTransport } from '@yume-chan/adb' +import { type AdbDaemonWebUsbDevice, AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb' +import { type BrowserWindow } from 'electron' +import { WebUSB } from 'usb' + +import { type AdbCredentialStore, adbGeneratePublicKey } from '@yume-chan/adb' +import { webcrypto } from 'node:crypto' +import { readFile, writeFile } from 'node:fs/promises' +import { homedir, hostname, userInfo } from 'node:os' +import { join } from 'node:path' + +class AdbNodeJsCredentialStore implements AdbCredentialStore { + #name: string + + constructor(name: string) { + this.#name = name + } + + #privateKeyPath() { + return join(homedir(), '.android', 'adbkey') + } + + #publicKeyPath() { + return join(homedir(), '.android', 'adbkey.pub') + } + + async generateKey() { + const { privateKey: cryptoKey } = await webcrypto.subtle.generateKey( + { + name: 'RSASSA-PKCS1-v1_5', + modulusLength: 2048, + // 65537 + publicExponent: new Uint8Array([0x01, 0x00, 0x01]), + hash: 'SHA-1' + }, + true, + ['sign', 'verify'] + ) + + const privateKey = new Uint8Array(await crypto.subtle.exportKey('pkcs8', cryptoKey)) + await writeFile(this.#privateKeyPath(), Buffer.from(privateKey).toString('utf8')) + await writeFile( + this.#publicKeyPath(), + `${Buffer.from(adbGeneratePublicKey(privateKey)).toString('base64')} ${this.#name}\n` + ) + + return { + buffer: privateKey, + name: this.#name + } + } + + async #readPubKeyName() { + const content = await readFile(this.#publicKeyPath(), 'utf8') + const pubKeyName = content.split(' ')[1] + return pubKeyName || `${userInfo().username}@${hostname()}` + } + + async *iterateKeys() { + const content = await readFile(this.#privateKeyPath(), 'utf8') + const privateKey = Buffer.from(content.split('\n').slice(1, -2).join(''), 'base64') + yield { + buffer: privateKey, + name: await this.#readPubKeyName() + } + } +} + +type ConnectedDevice = { + [serial: string]: { + adb: Adb + metadata: { + venderId?: number + productId?: number + manufacturerNam?: string + productName?: string + serialNumber?: string + } + } +} + +export class AdbUsbNative { + private _manager: AdbDaemonWebUsbDeviceManager + private _win: BrowserWindow + + private _connectedDevices: ConnectedDevice = {} + + constructor(win: BrowserWindow) { + const webUsb: WebUSB = new WebUSB({ allowedDevices: [{ serialNumber: 'd' }] }) + this._manager = new AdbDaemonWebUsbDeviceManager(webUsb) + this._win = win + } + + getAvailableDevices(responseChannel: string) { + this._manager.getDevices().then(devices => { + this._win.webContents.send( + responseChannel, + devices.map(d => ({ + vendorId: d.raw.vendorId, + productId: d.raw.productId, + manufacturerNam: d.raw.manufacturerName, + productName: d.raw.productName, + serialNumber: d.raw.serialNumber + })) + ) + }) + } + + getConnectedDevices(responseChannel: string) { + this._win.webContents.send( + responseChannel, + Object.entries(this._connectedDevices).map(([serial, { metadata }]) => ({ + serial: serial, + productId: metadata.productId, + venderId: metadata.venderId, + manufacturerNam: metadata.manufacturerNam, + productName: metadata.productName + })) + ) + } + + connectDevice(responseChanel: string, serial: string) { + this._manager.getDevices().then(devices => { + let device: AdbDaemonWebUsbDevice | null = null + for (const d of devices) { + if (d.serial === serial) { + device = d + break + } + } + + if (!device) { + this._win.webContents.send(responseChanel, { + error: 'Device with serialNumber: ' + serial + ' not found' + }) + return + } + + device.connect().then(connection => { + const credentialStore = new AdbNodeJsCredentialStore(`${userInfo().username}@${hostname()}`) + + AdbDaemonTransport.authenticate({ + serial: device!.serial, + connection, + credentialStore + }).then(transport => { + this._connectedDevices[device!.serial] = { + adb: new Adb(transport), + metadata: { + venderId: device!.raw.vendorId, + productId: device!.raw.productId, + manufacturerNam: device!.raw.manufacturerName, + productName: device!.raw.productName, + serialNumber: device!.raw.serialNumber + } + } + + this._win.webContents.send(responseChanel, { + serial: device!.serial + }) + }) + }) + }) + } +} diff --git a/client-electron/package-lock.json b/client-electron/package-lock.json index d2da58b..0f54dff 100644 --- a/client-electron/package-lock.json +++ b/client-electron/package-lock.json @@ -65,7 +65,7 @@ "sonner": "^1.4.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", - "usb": "^2.11.0", + "usb": "^2.12.1", "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0", "zod": "^3.22.4", @@ -13342,9 +13342,9 @@ } }, "node_modules/usb": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", - "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.12.1.tgz", + "integrity": "sha512-hgtoSQUFuMXVJBApelpUTiX7ZB83MQCbYeHTBsHftA2JG7YZ76ycwIgKQhkhKqVY76C8K6xJscHpF7Ep0eG3pQ==", "hasInstallScript": true, "dependencies": { "@types/w3c-web-usb": "^1.0.6", @@ -23341,9 +23341,9 @@ } }, "usb": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/usb/-/usb-2.11.0.tgz", - "integrity": "sha512-u5+NZ6DtoW8TIBtuSArQGAZZ/K15i3lYvZBAYmcgI+RcDS9G50/KPrUd3CrU8M92ahyCvg5e0gc8BDvr5Hwejg==", + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/usb/-/usb-2.12.1.tgz", + "integrity": "sha512-hgtoSQUFuMXVJBApelpUTiX7ZB83MQCbYeHTBsHftA2JG7YZ76ycwIgKQhkhKqVY76C8K6xJscHpF7Ep0eG3pQ==", "requires": { "@types/w3c-web-usb": "^1.0.6", "node-addon-api": "^7.0.0", diff --git a/client-electron/package.json b/client-electron/package.json index d32c546..3e37a2d 100644 --- a/client-electron/package.json +++ b/client-electron/package.json @@ -77,7 +77,7 @@ "sonner": "^1.4.0", "tailwind-merge": "^2.2.1", "tailwindcss-animate": "^1.0.7", - "usb": "^2.11.0", + "usb": "^2.12.1", "xterm": "^5.3.0", "xterm-addon-fit": "^0.8.0", "zod": "^3.22.4", diff --git a/client-electron/src/hooks/recipe-dashboard.ts b/client-electron/src/hooks/recipe-dashboard.ts index ada0ebc..b6f9d52 100644 --- a/client-electron/src/hooks/recipe-dashboard.ts +++ b/client-electron/src/hooks/recipe-dashboard.ts @@ -12,6 +12,26 @@ interface materialDashboard { value: string } +export type ItemMetadata = { + id: string | number + name?: string + lastChange?: Date +} + +export type ListMetadata = { + items: ItemMetadata[] + currentSelectedId: string | number | undefined + onSelectFn: (id: string | number) => void +} + +export enum EditorShowStateEnum { + RECIPES_IN_USE = 0, + RECIPES_NOT_IN_USE = 1, + MATERIALS_SETTING = 2, + TOPPING_GROUPS = 3, + TOPPING_LIST = 4 +} + interface RecipeDashboardHook { selectedRecipe: string setSelectedRecipe: (recipe: string) => void diff --git a/client-electron/src/pages/recipes/components/recipe-editor-components/nav.tsx b/client-electron/src/pages/recipes/components/recipe-editor-components/nav.tsx index b63d918..0265a39 100644 --- a/client-electron/src/pages/recipes/components/recipe-editor-components/nav.tsx +++ b/client-electron/src/pages/recipes/components/recipe-editor-components/nav.tsx @@ -1,12 +1,13 @@ import { Button } from '@/components/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' +import { type EditorShowStateEnum } from '@/hooks/recipe-dashboard' import { cn } from '@/lib/utils' import { type LucideIcon } from 'lucide-react' interface NavProps { isCollapsed: boolean links: { - index: number + index: EditorShowStateEnum title: string label?: string icon: LucideIcon diff --git a/client-electron/src/pages/recipes/components/recipe-editor-components/recipe-display.tsx b/client-electron/src/pages/recipes/components/recipe-editor-components/recipe-display.tsx index 85a2c14..27ffef1 100644 --- a/client-electron/src/pages/recipes/components/recipe-editor-components/recipe-display.tsx +++ b/client-electron/src/pages/recipes/components/recipe-editor-components/recipe-display.tsx @@ -1,16 +1,11 @@ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from '@/components/ui/accordion' -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' -import { Label } from '@/components/ui/label' import { Separator } from '@/components/ui/separator' -import { Switch } from '@/components/ui/switch' import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table' -import { Textarea } from '@/components/ui/textarea' import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import useRecipeDashboard from '@/hooks/recipe-dashboard' import { type Recipes } from '@/models/recipe/schema' -import { format } from 'date-fns' import { Archive, ArchiveX, Forward, MoreVertical, Reply, ReplyAll, Trash2 } from 'lucide-react' import { useMemo } from 'react' @@ -25,25 +20,13 @@ const RecipeDisplay: React.FC = ({ recipes }) => { return recipes.Recipe01.find(recipe => recipe.productCode === selectedRecipe) }, [selectedRecipe]) - const user: - | { - id: string - name: string - email: string - } - | undefined = { - id: '1', - name: 'John Doe', - email: 'john_doe@gmail.com' - } - return (
- @@ -52,7 +35,7 @@ const RecipeDisplay: React.FC = ({ recipes }) => { - @@ -61,7 +44,7 @@ const RecipeDisplay: React.FC = ({ recipes }) => { - @@ -73,7 +56,7 @@ const RecipeDisplay: React.FC = ({ recipes }) => {
- @@ -82,7 +65,7 @@ const RecipeDisplay: React.FC = ({ recipes }) => { - @@ -91,7 +74,7 @@ const RecipeDisplay: React.FC = ({ recipes }) => { - @@ -102,7 +85,7 @@ const RecipeDisplay: React.FC = ({ recipes }) => { - @@ -116,118 +99,80 @@ const RecipeDisplay: React.FC = ({ recipes }) => {
- {user ? ( + {recipe ? (
-
-
- - - - {user.name - .split(' ') - .map(chunk => chunk[0]) - .join('')} - - -
-
{user.name}
-
- ID: {user.id} -
-
- Email: {user.email} -
-
-
+ {/*
{recipe && recipe.LastChange && (
{format(new Date(recipe.LastChange), 'PPpp')}
)} -
- +
*/}
- {recipe && ( +
+
Product Code: {recipe.productCode}
-
Product Code: {recipe.productCode}
-
- Name: {recipe.name} -
-
- Other Name: {recipe.otherName} -
-
- Description: {recipe.Description} -
-
- Other Description: {recipe.otherDescription} -
-
- - { - // list all recipes - recipe.recipes - .filter(r => r.isUse) - .map((recipe, index) => ( - - {recipe.materialPathId} - - - - - Is Use - Powder Gram - Powder Time - Syrup Gram - Syrup Time - Hot Water - Cold Water - Mix Order - String Param - Stir Time - Feed param - Feed pattern - - - - - {JSON.stringify(recipe.isUse)} - {recipe.powderGram} - {recipe.powderTime} - {recipe.syrupGram} - {recipe.syrupTime} - {recipe.waterYield} - {recipe.waterCold} - {recipe.MixOrder} - {recipe.StringParam} - {recipe.stirTime} - {recipe.FeedParameter} - {recipe.FeedPattern} - - -
-
-
- )) - } -
-
+ Name: {recipe.name}
- )} -
- -
-
-
-