|
| 1 | +import { useEffect, useState } from 'react'; |
| 2 | +import { Icon } from '@iconify/react'; |
| 3 | +import { useDispatch, useSelector } from 'react-redux'; |
| 4 | +import { DndProvider } from 'react-dnd'; |
| 5 | +import { HTML5Backend } from 'react-dnd-html5-backend'; |
| 6 | +import { |
| 7 | + selectActiveSuite, |
| 8 | + setDevices, |
| 9 | + setSuiteDevices, |
| 10 | +} from 'renderer/store/features/device-manager'; |
| 11 | +import { APP_VIEWS, setAppView } from 'renderer/store/features/ui'; |
| 12 | +import { defaultDevices, Device, getDevicesMap } from 'common/deviceList'; |
| 13 | + |
| 14 | +import cx from 'classnames'; |
| 15 | +import Button from '../Button'; |
| 16 | +import DeviceLabel from './DeviceLabel'; |
| 17 | +import DeviceDetailsModal from './DeviceDetailsModal'; |
| 18 | +import { PreviewSuites } from './PreviewSuites'; |
| 19 | +import { ManageSuitesTool } from './PreviewSuites/ManageSuitesTool/ManageSuitesTool'; |
| 20 | +import { Divider } from '../Divider'; |
| 21 | +import { AccordionItem, Accordion } from '../Accordion'; |
| 22 | +import { DropDown } from '../DropDown'; |
| 23 | +import DeviceManagerLabel from './DeviceManagerLabel'; |
| 24 | + |
| 25 | +const filterDevices = (devices: Device[], filter: string) => { |
| 26 | + const sanitizedFilter = filter.trim().toLowerCase(); |
| 27 | + |
| 28 | + return devices.filter((device: Device) => |
| 29 | + `${device.name.toLowerCase()}${device.width}x${device.height}`.includes( |
| 30 | + sanitizedFilter |
| 31 | + ) |
| 32 | + ); |
| 33 | +}; |
| 34 | + |
| 35 | +const DeviceManager = () => { |
| 36 | + const [isDetailsModalOpen, setIsDetailsModalOpen] = useState<boolean>(false); |
| 37 | + const [selectedDevice, setSelectedDevice] = useState<Device | undefined>( |
| 38 | + undefined |
| 39 | + ); |
| 40 | + const dispatch = useDispatch(); |
| 41 | + const activeSuite = useSelector(selectActiveSuite); |
| 42 | + const devices = activeSuite.devices?.map((id) => getDevicesMap()[id]); |
| 43 | + const [searchText, setSearchText] = useState<string>(''); |
| 44 | + const [filteredType, setFilteredType] = useState<string | null>(null); |
| 45 | + const [filteredDevices, setFilteredDevices] = |
| 46 | + useState<Device[]>(defaultDevices); |
| 47 | + const [customDevices, setCustomDevices] = useState<Device[]>( |
| 48 | + window.electron.store.get('deviceManager.customDevices') |
| 49 | + ); |
| 50 | + const [filteredCustomDevices, setFilteredCustomDevices] = |
| 51 | + useState<Device[]>(customDevices); |
| 52 | + const deviceTypes = Array.from( |
| 53 | + new Set(defaultDevices.map((device) => device.type)) |
| 54 | + ); |
| 55 | + |
| 56 | + useEffect(() => { |
| 57 | + const filtered = filterDevices(defaultDevices, searchText).filter( |
| 58 | + (device) => (filteredType ? device.type === filteredType : true) |
| 59 | + ); |
| 60 | + const filteredCustom = filterDevices(customDevices, searchText).filter( |
| 61 | + (device) => (filteredType ? device.type === filteredType : true) |
| 62 | + ); |
| 63 | + setFilteredDevices(filtered); |
| 64 | + setFilteredCustomDevices(filteredCustom); |
| 65 | + }, [customDevices, searchText, filteredType]); |
| 66 | + |
| 67 | + const saveCustomDevices = (newCustomDevices: Device[]) => { |
| 68 | + setCustomDevices(newCustomDevices); |
| 69 | + window.electron.store.set('deviceManager.customDevices', newCustomDevices); |
| 70 | + setFilteredCustomDevices(filterDevices(newCustomDevices, searchText)); |
| 71 | + }; |
| 72 | + |
| 73 | + const onSaveDevice = async (device: Device, isNew: boolean) => { |
| 74 | + const newCustomDevices = isNew |
| 75 | + ? [...customDevices, device] |
| 76 | + : customDevices.map((d) => (d.id === device.id ? device : d)); |
| 77 | + saveCustomDevices(newCustomDevices); |
| 78 | + if (isNew) { |
| 79 | + dispatch( |
| 80 | + setSuiteDevices({ |
| 81 | + suite: activeSuite.id, |
| 82 | + devices: [...activeSuite.devices, device.id], |
| 83 | + }) |
| 84 | + ); |
| 85 | + } |
| 86 | + }; |
| 87 | + |
| 88 | + const onRemoveDevice = (device: Device) => { |
| 89 | + const newCustomDevices = customDevices.filter((d) => d.id !== device.id); |
| 90 | + saveCustomDevices(newCustomDevices); |
| 91 | + dispatch( |
| 92 | + setSuiteDevices({ |
| 93 | + suite: activeSuite.id, |
| 94 | + devices: activeSuite.devices.filter((d) => d !== device.id), |
| 95 | + }) |
| 96 | + ); |
| 97 | + }; |
| 98 | + |
| 99 | + const onShowDeviceDetails = (device: Device) => { |
| 100 | + setSelectedDevice(device); |
| 101 | + setIsDetailsModalOpen(true); |
| 102 | + }; |
| 103 | + |
| 104 | + return ( |
| 105 | + <div className="mx-auto flex w-4/5 flex-col gap-4 rounded-lg p-8"> |
| 106 | + <div className="flex w-full justify-end text-3xl"> |
| 107 | + <Button onClick={() => dispatch(setAppView(APP_VIEWS.BROWSER))}> |
| 108 | + <Icon icon="ic:round-close" fontSize={18} /> |
| 109 | + </Button> |
| 110 | + </div> |
| 111 | + <div> |
| 112 | + <div className="flex items-center justify-end justify-between "> |
| 113 | + <h2 className="text-2xl font-bold">Device Manager</h2> |
| 114 | + <ManageSuitesTool setCustomDevicesState={setCustomDevices} /> |
| 115 | + </div> |
| 116 | + <Divider /> |
| 117 | + <Accordion> |
| 118 | + <AccordionItem title="MANAGE SUITES"> |
| 119 | + <PreviewSuites /> |
| 120 | + </AccordionItem> |
| 121 | + </Accordion> |
| 122 | + <Divider /> |
| 123 | + <div className="my-4 flex items-start justify-end justify-between"> |
| 124 | + <div className="flex w-fit flex-col items-start px-1"> |
| 125 | + <h2 className="text-2xl font-bold">Manage Devices</h2> |
| 126 | + </div> |
| 127 | + <div> |
| 128 | + <DropDown |
| 129 | + label={ |
| 130 | + <div className="flex items-center gap-2"> |
| 131 | + <Icon icon="mdi:devices" fontSize={18} /> |
| 132 | + <span>Device Type</span> |
| 133 | + </div> |
| 134 | + } |
| 135 | + options={[ |
| 136 | + { |
| 137 | + label: ( |
| 138 | + <DeviceManagerLabel |
| 139 | + type={null} |
| 140 | + filteredType={filteredType} |
| 141 | + label="All Device Types" |
| 142 | + /> |
| 143 | + ), |
| 144 | + onClick: () => setFilteredType(null), |
| 145 | + }, |
| 146 | + ...deviceTypes.map((type) => ({ |
| 147 | + label: ( |
| 148 | + <DeviceManagerLabel |
| 149 | + type={type} |
| 150 | + filteredType={filteredType} |
| 151 | + label={type} |
| 152 | + /> |
| 153 | + ), |
| 154 | + onClick: () => setFilteredType(type), |
| 155 | + })), |
| 156 | + ]} |
| 157 | + /> |
| 158 | + </div> |
| 159 | + <div className="flex w-fit items-center bg-white px-1 dark:bg-slate-900"> |
| 160 | + <Icon icon="ic:outline-search" height={24} /> |
| 161 | + <input |
| 162 | + className="w-60 rounded bg-inherit px-2 py-1 focus:outline-none" |
| 163 | + placeholder="Search ..." |
| 164 | + value={searchText} |
| 165 | + onChange={(e) => setSearchText(e.target.value)} |
| 166 | + /> |
| 167 | + </div> |
| 168 | + </div> |
| 169 | + <Accordion> |
| 170 | + <> |
| 171 | + <AccordionItem title="DEFAULT DEVICES"> |
| 172 | + <div className="ml-4 flex flex-row flex-wrap gap-4"> |
| 173 | + {filteredDevices.map((device) => ( |
| 174 | + <DeviceLabel |
| 175 | + device={device} |
| 176 | + key={device.id} |
| 177 | + onShowDeviceDetails={onShowDeviceDetails} |
| 178 | + disableSelectionControls={ |
| 179 | + devices.find((d) => d.id === device.id) != null && |
| 180 | + devices.length === 1 |
| 181 | + } |
| 182 | + /> |
| 183 | + ))} |
| 184 | + {filteredDevices.length === 0 ? ( |
| 185 | + <div className="m-10 flex w-full items-center justify-center"> |
| 186 | + Sorry, no matching devices found. |
| 187 | + <Icon icon="mdi:emoticon-sad-outline" className="ml-1" /> |
| 188 | + </div> |
| 189 | + ) : null} |
| 190 | + </div> |
| 191 | + </AccordionItem> |
| 192 | + <AccordionItem title="CUSTOM DEVICES"> |
| 193 | + <div className="ml-4 flex flex-row flex-wrap gap-4"> |
| 194 | + {filteredCustomDevices.map((device) => ( |
| 195 | + <DeviceLabel |
| 196 | + device={device} |
| 197 | + key={device.id} |
| 198 | + onShowDeviceDetails={onShowDeviceDetails} |
| 199 | + /> |
| 200 | + ))} |
| 201 | + {customDevices.length === 0 ? ( |
| 202 | + <div className="m-10 flex w-full flex-col items-center justify-center"> |
| 203 | + <span>No custom devices added yet!</span> |
| 204 | + <Button |
| 205 | + className="m-4 rounded-l" |
| 206 | + onClick={() => setIsDetailsModalOpen(true)} |
| 207 | + isActive |
| 208 | + > |
| 209 | + <Icon icon="ic:baseline-add" /> |
| 210 | + <span className="pr-2 pl-2">Add Custom Device</span> |
| 211 | + </Button> |
| 212 | + </div> |
| 213 | + ) : null} |
| 214 | + {customDevices.length > 0 && |
| 215 | + filteredCustomDevices.length === 0 ? ( |
| 216 | + <div className="m-10 flex w-full items-center justify-center"> |
| 217 | + Sorry, no matching devices found. |
| 218 | + <Icon icon="mdi:emoticon-sad-outline" className="ml-1" /> |
| 219 | + </div> |
| 220 | + ) : null} |
| 221 | + <Button |
| 222 | + className={ |
| 223 | + customDevices.length < 1 || filteredCustomDevices.length < 1 |
| 224 | + ? 'hidden' |
| 225 | + : 'rounded-l' |
| 226 | + } |
| 227 | + onClick={() => setIsDetailsModalOpen(true)} |
| 228 | + isActive |
| 229 | + > |
| 230 | + <Icon icon="ic:baseline-add" /> |
| 231 | + <span className="pr-2 pl-2">Add Custom Device</span> |
| 232 | + </Button> |
| 233 | + </div> |
| 234 | + </AccordionItem> |
| 235 | + </> |
| 236 | + </Accordion> |
| 237 | + </div> |
| 238 | + <DeviceDetailsModal |
| 239 | + onSaveDevice={onSaveDevice} |
| 240 | + existingDevices={[...defaultDevices, ...customDevices]} |
| 241 | + isCustom |
| 242 | + isOpen={isDetailsModalOpen} |
| 243 | + onClose={() => { |
| 244 | + setSelectedDevice(undefined); |
| 245 | + setIsDetailsModalOpen(false); |
| 246 | + }} |
| 247 | + device={selectedDevice} |
| 248 | + onRemoveDevice={onRemoveDevice} |
| 249 | + /> |
| 250 | + </div> |
| 251 | + ); |
| 252 | +}; |
| 253 | + |
| 254 | +export default () => ( |
| 255 | + <DndProvider backend={HTML5Backend}> |
| 256 | + <DeviceManager /> |
| 257 | + </DndProvider> |
| 258 | +); |
0 commit comments