Update Electron
This commit is contained in:
parent
cae6d582ac
commit
c84ee948f5
22 changed files with 763 additions and 152 deletions
|
|
@ -10,7 +10,7 @@ import {
|
|||
DialogTrigger
|
||||
} from './ui/dialog'
|
||||
import { Button } from './ui/button'
|
||||
import { CaretSortIcon, CheckIcon, PlusCircledIcon } from '@radix-ui/react-icons'
|
||||
import { CaretSortIcon, PlusCircledIcon } from '@radix-ui/react-icons'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
|
|
@ -27,9 +27,11 @@ import useAdb from '@/hooks/useAdb'
|
|||
import { useShallow } from 'zustand/react/shallow'
|
||||
import type { AdbDaemonWebUsbConnection } from '@yume-chan/adb-daemon-webusb'
|
||||
import { ADB_DEFAULT_DEVICE_FILTER, AdbDaemonWebUsbDevice } from '@yume-chan/adb-daemon-webusb'
|
||||
import { Adb, AdbDaemonTransport } from '@yume-chan/adb'
|
||||
import { Adb, type AdbDaemonDevice, AdbDaemonTransport } from '@yume-chan/adb'
|
||||
import { toast } from './ui/use-toast'
|
||||
import AdbWebCredentialStore from '@yume-chan/adb-credential-web'
|
||||
import { Input } from './ui/input'
|
||||
import { IpcTcpTransport } from '@/lib/adb-tcp'
|
||||
|
||||
type PopoverTriggerProps = React.ComponentPropsWithoutRef<typeof PopoverTrigger>
|
||||
|
||||
|
|
@ -38,7 +40,8 @@ interface TeamSwitcherProps extends PopoverTriggerProps {}
|
|||
const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [showNewDeviceDialog, setShowNewDeviceDialog] = useState(false)
|
||||
const [connectedDevices, setConnectedDevices] = useState<AdbDaemonWebUsbDevice[]>([])
|
||||
const [connectedUsbDevices, setConnectedUsbDevices] = useState<AdbDaemonDevice[]>([])
|
||||
const [connectedTcpDevices, setConnectedTcpDevices] = useState<{ name: string; serial: string }[]>([])
|
||||
|
||||
const [newConnectionState, setNewConnectionState] = useState<'connection' | 'connecting'>('connection')
|
||||
const [selectedConnectionType, setSelectedConnectionType] = useState<string | undefined>()
|
||||
|
|
@ -56,9 +59,11 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
useEffect(() => {
|
||||
if (open) {
|
||||
const getDevices = async () => {
|
||||
const devices = await manager?.getDevices()
|
||||
console.log(devices)
|
||||
setConnectedDevices(devices || [])
|
||||
const usbDevices = await manager?.getDevices()
|
||||
const tcpDevices = await window.adbNativeTcpSocket.getDevices()
|
||||
|
||||
setConnectedUsbDevices(usbDevices || [])
|
||||
setConnectedTcpDevices(tcpDevices || [])
|
||||
}
|
||||
getDevices()
|
||||
}
|
||||
|
|
@ -94,7 +99,7 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
}
|
||||
|
||||
async function createNewUsbConnection() {
|
||||
let selectedDevice: AdbDaemonWebUsbDevice | undefined = undefined
|
||||
let selectedDevice: AdbDaemonDevice | undefined = undefined
|
||||
if (!device) {
|
||||
console.log('no device')
|
||||
|
||||
|
|
@ -149,6 +154,24 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
}
|
||||
}
|
||||
|
||||
async function createNewTCPConnection() {
|
||||
const device = await window.adbNativeTcpSocket.connect('192.168.11.196', 5555)
|
||||
|
||||
const transport = new IpcTcpTransport(device.serial, { product: device.name, model: device.name })
|
||||
|
||||
const adb = new Adb(transport)
|
||||
|
||||
setAdb(adb)
|
||||
}
|
||||
|
||||
function connectDeviceAdbTcp(device: { name: string; serial: string }) {
|
||||
const transport = new IpcTcpTransport(device.serial, { product: device.name, model: device.name })
|
||||
|
||||
const adb = new Adb(transport)
|
||||
|
||||
setAdb(adb)
|
||||
}
|
||||
|
||||
// async function connectAdbDaemon() {
|
||||
// if (!window.electronRuntime) {
|
||||
// toast({
|
||||
|
|
@ -168,7 +191,9 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
// }
|
||||
|
||||
function onDisconnect() {
|
||||
device?.raw.forget()
|
||||
if (device instanceof AdbDaemonWebUsbDevice) {
|
||||
device?.raw.forget()
|
||||
}
|
||||
setDevice(undefined)
|
||||
|
||||
adb?.close()
|
||||
|
|
@ -195,6 +220,12 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
<span className="font-medium">USB</span> -{' '}
|
||||
<span className="text-muted-foreground">Connect device via USB</span>
|
||||
</SelectItem>
|
||||
{window.electronRuntime && import.meta.env.DEV && (
|
||||
<SelectItem value="tcp">
|
||||
<span className="font-medium">TCP</span> -{' '}
|
||||
<span className="text-muted-foreground">Connect device via TCP</span>
|
||||
</SelectItem>
|
||||
)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
@ -220,23 +251,27 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
<DialogDescription>Connect a new device to manage your recipes or control your devices. </DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<div className="space-y-4 py-2 pb-4">
|
||||
<div className="space-y-2">
|
||||
{/* <Label htmlFor="plan">Connection type</Label>
|
||||
<Select onValueChange={setSelectedConnectionType}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select connection type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="usb">
|
||||
<span className="font-medium">USB</span> -{' '}
|
||||
<span className="text-muted-foreground">Connect device via USB</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
<span className="text-sm">Please connect your device via USB</span>
|
||||
{selectedConnectionType === 'usb' ? (
|
||||
<div className="space-y-4 py-2 pb-4">
|
||||
<div className="space-y-2">
|
||||
<span className="text-sm">Please plug in your device and click Connect.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : selectedConnectionType === 'tcp' ? (
|
||||
<div className="space-y-4 py-2 pb-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="plan">TCP Connection</Label>
|
||||
<div className="flex gap-3">
|
||||
<Input type="text" placeholder="IP Address" className="flex-1" />
|
||||
<Input type="text" placeholder="Port" className="flex-[0.5]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<span className="text-sm">Unknown connection type.</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setNewConnectionState('connection')}>
|
||||
|
|
@ -245,7 +280,7 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
<Button
|
||||
type="submit"
|
||||
onClick={() => {
|
||||
createNewUsbConnection()
|
||||
selectedConnectionType === 'usb' ? createNewUsbConnection() : createNewTCPConnection()
|
||||
}}
|
||||
>
|
||||
Connect
|
||||
|
|
@ -267,7 +302,7 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
aria-label="Select a Device"
|
||||
className={cn('w-[400px] justify-between', className)}
|
||||
>
|
||||
{device ? device.name : 'Select a Device'}
|
||||
{device ? device.name + ' [' + device.serial + ']' : 'Select a Device'}
|
||||
<CaretSortIcon className="ml-auto h-4 w-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
|
|
@ -276,30 +311,50 @@ const DeviceSwitcher = ({ className }: TeamSwitcherProps) => {
|
|||
<CommandList>
|
||||
<CommandInput placeholder="Search device..." />
|
||||
<CommandEmpty>No device found.</CommandEmpty>
|
||||
<CommandGroup heading={'Devices'}>
|
||||
{connectedDevices.length > 0 ? (
|
||||
connectedDevices.map(device => (
|
||||
<CommandGroup heading={'Devices [USB]'}>
|
||||
{connectedUsbDevices.length > 0 ? (
|
||||
connectedUsbDevices.map(device => (
|
||||
<CommandItem
|
||||
key={device.serial}
|
||||
onSelect={() => {
|
||||
connectDeviceAdbUsb(device)
|
||||
connectDeviceAdbUsb(device as AdbDaemonWebUsbDevice)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
{device.name}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
device?.serial === device.serial ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm ml-2">Not found device connected.</span>
|
||||
)}
|
||||
</CommandGroup>
|
||||
{window.electronRuntime && import.meta.env.DEV && (
|
||||
<CommandGroup heading={'Devices [TCP]'}>
|
||||
{connectedTcpDevices.length > 0 ? (
|
||||
connectedTcpDevices.map(device => (
|
||||
<CommandItem
|
||||
key={device.serial}
|
||||
onSelect={() => {
|
||||
connectDeviceAdbTcp(device)
|
||||
setOpen(false)
|
||||
}}
|
||||
className="text-sm"
|
||||
>
|
||||
{device.name}
|
||||
{/* <CheckIcon
|
||||
className={cn(
|
||||
'ml-auto h-4 w-4',
|
||||
device?.serial === device.serial ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
/> */}
|
||||
</CommandItem>
|
||||
))
|
||||
) : (
|
||||
<span className="text-sm ml-2">Not found device connected.</span>
|
||||
)}
|
||||
</CommandGroup>
|
||||
)}
|
||||
</CommandList>
|
||||
<CommandSeparator />
|
||||
<CommandList>
|
||||
|
|
|
|||
55
client-electron/src/components/ui/accordion.tsx
Normal file
55
client-electron/src/components/ui/accordion.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import * as React from "react"
|
||||
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
||||
import { ChevronDownIcon } from "@radix-ui/react-icons"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const Accordion = AccordionPrimitive.Root
|
||||
|
||||
const AccordionItem = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Item>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<AccordionPrimitive.Item
|
||||
ref={ref}
|
||||
className={cn("border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
))
|
||||
AccordionItem.displayName = "AccordionItem"
|
||||
|
||||
const AccordionTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Header className="flex">
|
||||
<AccordionPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline [&[data-state=open]>svg]:rotate-180",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<ChevronDownIcon className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
||||
</AccordionPrimitive.Trigger>
|
||||
</AccordionPrimitive.Header>
|
||||
))
|
||||
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
||||
|
||||
const AccordionContent = React.forwardRef<
|
||||
React.ElementRef<typeof AccordionPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<AccordionPrimitive.Content
|
||||
ref={ref}
|
||||
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
||||
{...props}
|
||||
>
|
||||
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
||||
</AccordionPrimitive.Content>
|
||||
))
|
||||
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
||||
|
||||
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|
||||
|
|
@ -155,31 +155,7 @@ const useFileManager = create<FileManagerAndroidHook>((set, get) => ({
|
|||
return
|
||||
}
|
||||
|
||||
const process = await adb.subprocess.spawn('rm ' + get().rootPath + '/' + filename)
|
||||
process.stderr.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
console.error(chunk)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
process.stdout.pipeTo(
|
||||
new WritableStream({
|
||||
write(chunk) {
|
||||
console.log(chunk)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
if ((await process.exit) != 0) {
|
||||
toast({
|
||||
duration: 3000,
|
||||
variant: 'destructive',
|
||||
title: 'Failed to delete file',
|
||||
description: 'Please try again'
|
||||
})
|
||||
}
|
||||
await adb.rm(filename, { recursive: true })
|
||||
},
|
||||
async rename(filename, newName) {
|
||||
const adb = useAdb.getState().adb
|
||||
|
|
|
|||
|
|
@ -1,16 +1,13 @@
|
|||
import { type Adb } from '@yume-chan/adb'
|
||||
import {
|
||||
type AdbDaemonWebUsbDevice,
|
||||
AdbDaemonWebUsbDeviceManager
|
||||
} from '@yume-chan/adb-daemon-webusb'
|
||||
import { type AdbDaemonDevice, type Adb } from '@yume-chan/adb'
|
||||
import { AdbDaemonWebUsbDeviceManager } from '@yume-chan/adb-daemon-webusb'
|
||||
import { create } from 'zustand'
|
||||
|
||||
interface ADB {
|
||||
adb: Adb | undefined
|
||||
manager: AdbDaemonWebUsbDeviceManager | undefined
|
||||
device: AdbDaemonWebUsbDevice | undefined
|
||||
device: AdbDaemonDevice | undefined
|
||||
setAdb: (adb: Adb | undefined) => void
|
||||
setDevice: (device: AdbDaemonWebUsbDevice | undefined) => void
|
||||
setDevice: (device: AdbDaemonDevice | undefined) => void
|
||||
}
|
||||
|
||||
const useAdb = create<ADB>(set => ({
|
||||
|
|
|
|||
129
client-electron/src/lib/adb-tcp.ts
Normal file
129
client-electron/src/lib/adb-tcp.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import type { AdbSocket, AdbTransport } from '@yume-chan/adb'
|
||||
import { AdbBanner, encodeUtf8 } from '@yume-chan/adb'
|
||||
import { PromiseResolver } from '@yume-chan/async'
|
||||
import type { Consumable } from '@yume-chan/stream-extra'
|
||||
import { ReadableStream, WritableStream } from '@yume-chan/stream-extra'
|
||||
import type { ValueOrPromise } from '@yume-chan/struct'
|
||||
|
||||
export class IpcTcpSocket implements AdbSocket {
|
||||
service: string
|
||||
readable: ReadableStream<Uint8Array>
|
||||
writable: WritableStream<Consumable<Uint8Array>>
|
||||
|
||||
_closed = false
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
constructor(serial: string, service: string, ...args: string[]) {
|
||||
this.service = service
|
||||
|
||||
switch (service) {
|
||||
case 'getprop':
|
||||
this.readable = new ReadableStream({
|
||||
async start(controller) {
|
||||
const result = await window.adbNativeTcpSocket.getProp(serial, args[0])
|
||||
controller.enqueue(encodeUtf8(result))
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
break
|
||||
case 'shell':
|
||||
this.readable = new ReadableStream({
|
||||
async start(controller) {
|
||||
await window.adbNativeTcpSocket.shell(serial, args[0], chunk => {
|
||||
controller.enqueue(chunk)
|
||||
})
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
break
|
||||
default:
|
||||
this.readable = new ReadableStream({
|
||||
start(controller) {
|
||||
controller.close()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
this.writable = new WritableStream({
|
||||
write(chunk) {
|
||||
if (closed) {
|
||||
throw new Error('Socket closed')
|
||||
} else {
|
||||
console.log(chunk)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
close(): ValueOrPromise<void> {
|
||||
this._closed = true
|
||||
}
|
||||
get closed(): Promise<void> {
|
||||
this._closed = true
|
||||
return Promise.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
export class IpcTcpTransport implements AdbTransport {
|
||||
constructor(
|
||||
private _serial: string,
|
||||
private _banner?: {
|
||||
product?: string
|
||||
model?: string
|
||||
device?: string
|
||||
}
|
||||
) {}
|
||||
|
||||
get serial(): string {
|
||||
return this._serial
|
||||
}
|
||||
|
||||
get maxPayloadSize(): number {
|
||||
return 4 * 1024
|
||||
}
|
||||
|
||||
get banner(): AdbBanner {
|
||||
if (this._banner) {
|
||||
return new AdbBanner(this._banner.product, this._banner.model, this._banner.device, [])
|
||||
}
|
||||
return new AdbBanner('undefined', 'undefined', 'undefined', [])
|
||||
}
|
||||
|
||||
#disconnected = new PromiseResolver<void>()
|
||||
get disconnected(): Promise<void> {
|
||||
return this.#disconnected.promise
|
||||
}
|
||||
|
||||
connect(service: string): AdbSocket {
|
||||
const serviceSpl = service.split(':', 2)
|
||||
console.log(service)
|
||||
console.log(serviceSpl)
|
||||
switch (serviceSpl[0]) {
|
||||
case 'exec':
|
||||
if (serviceSpl[1].startsWith('getprop')) {
|
||||
const [func, key] = serviceSpl[1].split(' ', 2)
|
||||
return new IpcTcpSocket(this._serial, func, key)
|
||||
}
|
||||
break
|
||||
case 'shell':
|
||||
return new IpcTcpSocket(this._serial, serviceSpl[0], serviceSpl[1])
|
||||
}
|
||||
|
||||
throw new Error(`Unknown service: ${service}`)
|
||||
}
|
||||
|
||||
addReverseTunnel(): ValueOrPromise<string> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
removeReverseTunnel(): ValueOrPromise<void> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
clearReverseTunnels(): ValueOrPromise<void> {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
close(): ValueOrPromise<void> {
|
||||
this.#disconnected.resolve()
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,9 @@ import axios, { type AxiosResponse } from 'axios'
|
|||
|
||||
const taoAxios = axios.create({
|
||||
baseURL: import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL ?? 'http://localhost:8080',
|
||||
headers: {
|
||||
'Taobin-Client-Kind': window.electronRuntime ? 'electron' : 'web'
|
||||
},
|
||||
withCredentials: true
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -18,18 +18,18 @@ const LoginPage: React.FC = () => {
|
|||
import.meta.env.TAOBIN_RECIPE_MANAGER_SERVER_URL + '/auth/google?redirect_to=' + redirectUrl + '&kind=electron'
|
||||
)
|
||||
|
||||
window.ipcRenderer.on('loginSuccess', (_event, data) => {
|
||||
window.ipcRenderer.on('loginSuccess', async (_event, data) => {
|
||||
console.log(data)
|
||||
|
||||
const { accessToken, maxAge, refreshToken } = data
|
||||
const { accessToken, refreshToken } = data
|
||||
|
||||
window.ipcRenderer.send('set-keyChain', {
|
||||
await window.ipcRenderer.invoke('set-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_ACCESS_TOKEN,
|
||||
password: accessToken + ';' + maxAge
|
||||
password: accessToken
|
||||
})
|
||||
|
||||
window.ipcRenderer.send('set-keyChain', {
|
||||
await window.ipcRenderer.invoke('set-keyChain', {
|
||||
serviceName: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_SERVICE_NAME,
|
||||
account: import.meta.env.TAOBIN_RECIPE_MANAGER_KEY_CHAIN_ACCOUNT_REFRESH_TOKEN,
|
||||
password: refreshToken
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import DataTableRowActions from './filemanager-table/data-table-row-actions'
|
|||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { type AndroidFile } from '@/models/android/schema'
|
||||
import { File, Folder } from 'lucide-react'
|
||||
import useAdb from '@/hooks/useAdb'
|
||||
|
||||
export const FileManagerTab: React.FC = () => {
|
||||
const { currentPath, pushPath, scanPath } = useFileManager(
|
||||
|
|
@ -23,6 +24,8 @@ export const FileManagerTab: React.FC = () => {
|
|||
}))
|
||||
)
|
||||
|
||||
const adb = useAdb(state => state.adb)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('scanning path', currentPath)
|
||||
scanPath(currentPath).then(files => {
|
||||
|
|
@ -30,7 +33,7 @@ export const FileManagerTab: React.FC = () => {
|
|||
setFiles(files)
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [currentPath])
|
||||
}, [currentPath, adb])
|
||||
|
||||
const [files, setFiles] = useState<AndroidFile[] | undefined>(undefined)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
const RecipeForm: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Recipe Form</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default RecipeForm
|
||||
|
|
@ -1,9 +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'
|
||||
|
|
@ -36,7 +38,7 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-full flex-col overflow-y-auto">
|
||||
<div className="flex items-center p-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Tooltip>
|
||||
|
|
@ -159,16 +161,54 @@ const RecipeDisplay: React.FC<RecipeDisplayProps> = ({ recipes }) => {
|
|||
<span className="font-semibold">Other Description:</span> {recipe.otherDescription}
|
||||
</div>
|
||||
<div>
|
||||
{
|
||||
// list all recipes
|
||||
recipe.recipes
|
||||
.filter(r => r.isUse)
|
||||
.map((recipe, index) => (
|
||||
<div key={index}>
|
||||
<span className="font-semibold">Recipe {index + 1}:</span> {recipe.materialPathId}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
<Accordion type="single" collapsible>
|
||||
{
|
||||
// list all recipes
|
||||
recipe.recipes
|
||||
.filter(r => r.isUse)
|
||||
.map((recipe, index) => (
|
||||
<AccordionItem key={index} value={'item-' + index}>
|
||||
<AccordionTrigger>{recipe.materialPathId}</AccordionTrigger>
|
||||
<AccordionContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Is Use</TableHead>
|
||||
<TableHead>Powder Gram</TableHead>
|
||||
<TableHead>Powder Time</TableHead>
|
||||
<TableHead>Syrup Gram</TableHead>
|
||||
<TableHead>Syrup Time</TableHead>
|
||||
<TableHead>Hot Water</TableHead>
|
||||
<TableHead>Cold Water</TableHead>
|
||||
<TableHead>Mix Order</TableHead>
|
||||
<TableHead>String Param</TableHead>
|
||||
<TableHead>Stir Time</TableHead>
|
||||
<TableHead>Feed param</TableHead>
|
||||
<TableHead>Feed pattern</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell className="font-medium">{JSON.stringify(recipe.isUse)}</TableCell>
|
||||
<TableCell>{recipe.powderGram}</TableCell>
|
||||
<TableCell>{recipe.powderTime}</TableCell>
|
||||
<TableCell>{recipe.syrupGram}</TableCell>
|
||||
<TableCell>{recipe.syrupTime}</TableCell>
|
||||
<TableCell>{recipe.waterYield}</TableCell>
|
||||
<TableCell>{recipe.waterCold}</TableCell>
|
||||
<TableCell>{recipe.MixOrder}</TableCell>
|
||||
<TableCell>{recipe.StringParam}</TableCell>
|
||||
<TableCell>{recipe.stirTime}</TableCell>
|
||||
<TableCell>{recipe.FeedParameter}</TableCell>
|
||||
<TableCell>{recipe.FeedPattern}</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))
|
||||
}
|
||||
</Accordion>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -202,7 +202,7 @@ export const RecipeEditor: React.FC<RecipeEditorProps> = ({
|
|||
onLayout={(sizes: number[]) => {
|
||||
document.cookie = `react-resizable-panels:layout=${JSON.stringify(sizes)}`
|
||||
}}
|
||||
className="h-full max-h-[900px] items-stretch"
|
||||
className="h-full max-h-screen items-stretch"
|
||||
>
|
||||
<ResizablePanel
|
||||
defaultSize={defaultLayout[0]}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue