1
+ import { GridCard } from "@/components/Card" ;
2
+ import { useCallback , useEffect , useState } from "react" ;
3
+ import { Button } from "@components/Button" ;
4
+ import LogoBlueIcon from "@/assets/logo-blue.svg" ;
5
+ import LogoWhiteIcon from "@/assets/logo-white.svg" ;
6
+ import Modal from "@components/Modal" ;
7
+ import { InputFieldWithLabel } from "./InputField" ;
8
+ import { useJsonRpc } from "@/hooks/useJsonRpc" ;
9
+ import { useUsbConfigModalStore } from "@/hooks/stores" ;
10
+ import ExtLink from "@components/ExtLink" ;
11
+ import { UsbConfigState } from "@/hooks/stores"
12
+
13
+ export default function USBConfigDialog ( {
14
+ open,
15
+ setOpen,
16
+ } : {
17
+ open : boolean ;
18
+ setOpen : ( open : boolean ) => void ;
19
+ } ) {
20
+ return (
21
+ < Modal open = { open } onClose = { ( ) => setOpen ( false ) } >
22
+ < Dialog setOpen = { setOpen } />
23
+ </ Modal >
24
+ ) ;
25
+ }
26
+
27
+ export function Dialog ( { setOpen } : { setOpen : ( open : boolean ) => void } ) {
28
+ const { modalView, setModalView } = useUsbConfigModalStore ( ) ;
29
+ const [ error , setError ] = useState < string | null > ( null ) ;
30
+
31
+ const [ send ] = useJsonRpc ( ) ;
32
+
33
+ const handleUsbConfigChange = useCallback ( ( usbConfig : object ) => {
34
+ send ( "setUsbConfig" , { usbConfig } , resp => {
35
+ if ( "error" in resp ) {
36
+ setError ( `Failed to update USB Config: ${ resp . error . data || "Unknown error" } ` ) ;
37
+ return ;
38
+ }
39
+ setModalView ( "updateUsbConfigSuccess" ) ;
40
+ } ) ;
41
+ } , [ send , setModalView ] ) ;
42
+
43
+ return (
44
+ < GridCard cardClassName = "relative max-w-lg mx-auto text-left pointer-events-auto dark:bg-slate-800" >
45
+ < div className = "p-10" >
46
+ { modalView === "updateUsbConfig" && (
47
+ < UpdateUsbConfigModal
48
+ onSetUsbConfig = { handleUsbConfigChange }
49
+ onCancel = { ( ) => setOpen ( false ) }
50
+ error = { error }
51
+ />
52
+ ) }
53
+ { modalView === "updateUsbConfigSuccess" && (
54
+ < SuccessModal
55
+ headline = "USB Configuration Updated Successfully"
56
+ description = "You've successfully updated the USB Configuration"
57
+ onClose = { ( ) => setOpen ( false ) }
58
+ />
59
+ ) }
60
+ </ div >
61
+ </ GridCard >
62
+ ) ;
63
+ }
64
+
65
+ function UpdateUsbConfigModal ( {
66
+ onSetUsbConfig,
67
+ onCancel,
68
+ error,
69
+ } : {
70
+ onSetUsbConfig : ( usb_config : object ) => void ;
71
+ onCancel : ( ) => void ;
72
+ error : string | null ;
73
+ } ) {
74
+ const [ usbConfigState , setUsbConfigState ] = useState < UsbConfigState > ( {
75
+ vendor_id : '' ,
76
+ product_id : '' ,
77
+ serial_number : '' ,
78
+ manufacturer : '' ,
79
+ product : ''
80
+ } ) ;
81
+ const [ send ] = useJsonRpc ( ) ;
82
+
83
+ const syncUsbConfig = useCallback ( ( ) => {
84
+ send ( "getUsbConfig" , { } , resp => {
85
+ if ( "error" in resp ) {
86
+ console . error ( "Failed to load USB Config:" , resp . error ) ;
87
+ } else {
88
+ setUsbConfigState ( resp . result as UsbConfigState ) ;
89
+ }
90
+ } ) ;
91
+ } , [ send , setUsbConfigState ] ) ;
92
+
93
+ // Load stored usb config from the backend
94
+ useEffect ( ( ) => {
95
+ syncUsbConfig ( ) ;
96
+ } , [ syncUsbConfig ] ) ;
97
+
98
+ const handleUsbVendorIdChange = ( value : string ) => {
99
+ setUsbConfigState ( { ... usbConfigState , vendor_id : value } )
100
+ } ;
101
+
102
+ const handleUsbProductIdChange = ( value : string ) => {
103
+ setUsbConfigState ( { ... usbConfigState , product_id : value } )
104
+ } ;
105
+
106
+ const handleUsbSerialChange = ( value : string ) => {
107
+ setUsbConfigState ( { ... usbConfigState , serial_number : value } )
108
+ } ;
109
+
110
+ const handleUsbManufacturer = ( value : string ) => {
111
+ setUsbConfigState ( { ... usbConfigState , manufacturer : value } )
112
+ } ;
113
+
114
+ const handleUsbProduct = ( value : string ) => {
115
+ setUsbConfigState ( { ... usbConfigState , product : value } )
116
+ } ;
117
+
118
+ return (
119
+ < div className = "flex flex-col items-start justify-start space-y-4 text-left" >
120
+ < div >
121
+ < img src = { LogoWhiteIcon } alt = "" className = "h-[24px] hidden dark:block" />
122
+ < img src = { LogoBlueIcon } alt = "" className = "h-[24px] dark:hidden" />
123
+ </ div >
124
+ < div className = "space-y-4" >
125
+ < div >
126
+ < h2 className = "text-lg font-semibold dark:text-white" > USB Emulation Configuration</ h2 >
127
+ < p className = "text-sm text-slate-600 dark:text-slate-400" >
128
+ Set custom USB parameters to control how the USB device is emulated.
129
+ The device will rebind once the parameters are updated.
130
+ </ p >
131
+ < div className = "flex justify-start mt-4 text-xs text-slate-500 dark:text-slate-400" >
132
+ < ExtLink
133
+ href = { `https://the-sz.com/products/usbid/index.php` }
134
+ className = "hover:underline"
135
+ >
136
+ Look up USB Device IDs here
137
+ </ ExtLink >
138
+ </ div >
139
+ </ div >
140
+ < InputFieldWithLabel
141
+ required
142
+ label = "Vendor ID"
143
+ placeholder = "Enter Vendor ID"
144
+ pattern = "^0[xX][\da-fA-F]{4}$"
145
+ defaultValue = { usbConfigState ?. vendor_id }
146
+ onChange = { e => handleUsbVendorIdChange ( e . target . value ) }
147
+ />
148
+ < InputFieldWithLabel
149
+ required
150
+ label = "Product ID"
151
+ placeholder = "Enter Product ID"
152
+ pattern = "^0[xX][\da-fA-F]{4}$"
153
+ defaultValue = { usbConfigState ?. product_id }
154
+ onChange = { e => handleUsbProductIdChange ( e . target . value ) }
155
+ />
156
+ < InputFieldWithLabel
157
+ required
158
+ label = "Serial Number"
159
+ placeholder = "Enter Serial Number"
160
+ defaultValue = { usbConfigState ?. serial_number }
161
+ onChange = { e => handleUsbSerialChange ( e . target . value ) }
162
+ />
163
+ < InputFieldWithLabel
164
+ required
165
+ label = "Manufacturer"
166
+ placeholder = "Enter Manufacturer"
167
+ defaultValue = { usbConfigState ?. manufacturer }
168
+ onChange = { e => handleUsbManufacturer ( e . target . value ) }
169
+ />
170
+ < InputFieldWithLabel
171
+ required
172
+ label = "Product Name"
173
+ placeholder = "Enter Product Name"
174
+ defaultValue = { usbConfigState ?. product }
175
+ onChange = { e => handleUsbProduct ( e . target . value ) }
176
+ />
177
+ < div className = "flex gap-x-2" >
178
+ < Button
179
+ size = "SM"
180
+ theme = "primary"
181
+ text = "Update USB Config"
182
+ onClick = { ( ) => onSetUsbConfig ( usbConfigState ) }
183
+ />
184
+ < Button size = "SM" theme = "light" text = "Not Now" onClick = { onCancel } />
185
+ </ div >
186
+ { error && < p className = "text-sm text-red-500" > { error } </ p > }
187
+ </ div >
188
+ </ div >
189
+ ) ;
190
+ }
191
+
192
+ function SuccessModal ( {
193
+ headline,
194
+ description,
195
+ onClose,
196
+ } : {
197
+ headline : string ;
198
+ description : string ;
199
+ onClose : ( ) => void ;
200
+ } ) {
201
+ return (
202
+ < div className = "flex flex-col items-start justify-start w-full max-w-lg space-y-4 text-left" >
203
+ < div >
204
+ < img src = { LogoWhiteIcon } alt = "" className = "h-[24px] hidden dark:block" />
205
+ < img src = { LogoBlueIcon } alt = "" className = "h-[24px] dark:hidden" />
206
+ </ div >
207
+ < div className = "space-y-4" >
208
+ < div >
209
+ < h2 className = "text-lg font-semibold dark:text-white" > { headline } </ h2 >
210
+ < p className = "text-sm text-slate-600 dark:text-slate-400" > { description } </ p >
211
+ </ div >
212
+ < Button size = "SM" theme = "primary" text = "Close" onClick = { onClose } />
213
+ </ div >
214
+ </ div >
215
+ ) ;
216
+ }
0 commit comments