diff --git a/src/HardwareListing/HardwareListing.module.css b/src/HardwareListing/HardwareListing.module.css index 4c66869..a0021b6 100644 --- a/src/HardwareListing/HardwareListing.module.css +++ b/src/HardwareListing/HardwareListing.module.css @@ -6,9 +6,8 @@ position: relative; width: 100%; } - .listingContent { - height: calc(100% - 42px - 32px - 5px); + height: calc(100% - 42px - 32px - 5px - 24px - 5px); overflow-y: auto; padding-right: 20px; } @@ -29,7 +28,7 @@ .filter { display: flex; - margin-bottom: 5px; + height: 37px; } .filter > span { border-radius: 5px; @@ -55,3 +54,37 @@ .filter > span.disabled:hover { background-color: #111111; } + +.selectionHeader { + display: flex; + margin-bottom: 5px; +} +.selectionHeader > div { + background-color: #4f4f4f; + cursor: pointer; + flex: 1; + line-height: 24px; + text-align: center; +} +.selectionHeader > div:hover { + background-color: #6c6c6c; +} +.selectionHeader > div:first-child { + border-bottom-left-radius: 25px; + margin-right: 5px; +} +.selectionHeader > div:last-child { + border-bottom-right-radius: 25px; + margin-left: 5px; +} +.selectionHeader > div.selected { + background-color: #7a7a7a; +} + +.collapsed { + display: none; +} + +.moduleChargeIcon { + border-top-left-radius: 50%; +} diff --git a/src/HardwareListing/HardwareListing.stories.tsx b/src/HardwareListing/HardwareListing.stories.tsx index ae2b2bb..b02926d 100644 --- a/src/HardwareListing/HardwareListing.stories.tsx +++ b/src/HardwareListing/HardwareListing.stories.tsx @@ -1,7 +1,11 @@ import type { Decorator, Meta, StoryObj } from "@storybook/react"; import React from "react"; +import { DogmaEngineProvider } from "../DogmaEngineProvider"; +import { EsiProvider } from "../EsiProvider"; import { EveDataProvider } from "../EveDataProvider"; +import { fullFit } from "../../.storybook/fits"; +import { ShipSnapshotProvider } from "../ShipSnapshotProvider"; import { HardwareListing } from "./"; @@ -14,16 +18,29 @@ const meta: Meta = { export default meta; type Story = StoryObj; -const withEveDataProvider: Decorator> = (Story) => { +const useShipSnapshotProvider: Decorator> = (Story, context) => { + const [skills, setSkills] = React.useState>({}); + return ( -
- -
+ + + +
+ +
+
+
+
); }; export const Default: Story = { - decorators: [withEveDataProvider], + decorators: [useShipSnapshotProvider], + parameters: { + snapshot: { + fit: fullFit, + }, + }, }; diff --git a/src/HardwareListing/HardwareListing.tsx b/src/HardwareListing/HardwareListing.tsx index 7675be5..2a5aa43 100644 --- a/src/HardwareListing/HardwareListing.tsx +++ b/src/HardwareListing/HardwareListing.tsx @@ -9,11 +9,18 @@ import { TreeListing, TreeHeader, TreeLeaf } from "../TreeListing"; import styles from "./HardwareListing.module.css"; +interface ModuleCharge { + typeId: number; + name: string; + chargeGroupIDs: number[]; + chargeSize: number; +} + interface ListingItem { name: string; meta: number; typeId: number; - slotType: ShipSnapshotSlotsType | "dronebay"; + slotType: ShipSnapshotSlotsType | "dronebay" | "charge"; } interface ListingGroup { @@ -24,7 +31,16 @@ interface ListingGroup { items: ListingItem[]; } -const ModuleGroup = (props: { level: number; group: ListingGroup }) => { +interface Filter { + lowslot: boolean; + medslot: boolean; + hislot: boolean; + rig_subsystem: boolean; + drone: boolean; + moduleWithCharge: ModuleCharge | undefined; +} + +const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: boolean }) => { const shipSnapShot = React.useContext(ShipSnapshotContext); const getChildren = React.useCallback(() => { @@ -33,14 +49,19 @@ const ModuleGroup = (props: { level: number; group: ListingGroup }) => { {props.group.items .sort((a, b) => a.meta - b.meta || a.name.localeCompare(b.name)) .map((item) => { - return ( - shipSnapShot.addModule(item.typeId, item.slotType)} - /> - ); + if (item.slotType === "charge") { + return {}} />; + } else { + const slotType = item.slotType; + return ( + shipSnapShot.addModule(item.typeId, slotType)} + /> + ); + } })} {Object.keys(props.group.groups) .sort( @@ -55,6 +76,10 @@ const ModuleGroup = (props: { level: number; group: ListingGroup }) => { ); }, [props, shipSnapShot]); + if (props.hideGroup) { + return ; + } + const header = ( { */ export const HardwareListing = () => { const eveData = React.useContext(EveDataContext); + const shipSnapShot = React.useContext(ShipSnapshotContext); const [moduleGroups, setModuleGroups] = React.useState({ name: "Modules", @@ -76,14 +102,65 @@ export const HardwareListing = () => { groups: {}, items: [], }); + const [chargeGroups, setChageGroups] = React.useState({ + name: "Charges", + meta: 0, + groups: {}, + items: [], + }); const [search, setSearch] = React.useState(""); - const [filter, setFilter] = React.useState({ + const [filter, setFilter] = React.useState({ lowslot: false, medslot: false, hislot: false, rig_subsystem: false, drone: false, + moduleWithCharge: undefined, }); + const [selection, setSelection] = React.useState<"modules" | "charges">("modules"); + const [modulesWithCharges, setModulesWithCharges] = React.useState([]); + + React.useEffect(() => { + if (!eveData.loaded) return; + if (!shipSnapShot.loaded || shipSnapShot.items === undefined) return; + + /* Iterate all items to check if they have a charge. */ + const newModulesWithCharges: ModuleCharge[] = []; + const seenModules = new Set(); + for (const item of shipSnapShot.items) { + const chargeGroup1 = item.attributes.get(eveData?.attributeMapping?.chargeGroup1 || 0)?.value; + const chargeGroup2 = item.attributes.get(eveData?.attributeMapping?.chargeGroup2 || 0)?.value; + const chargeGroup3 = item.attributes.get(eveData?.attributeMapping?.chargeGroup3 || 0)?.value; + const chargeGroup4 = item.attributes.get(eveData?.attributeMapping?.chargeGroup4 || 0)?.value; + const chargeGroup5 = item.attributes.get(eveData?.attributeMapping?.chargeGroup5 || 0)?.value; + + const chargeGroupIDs: number[] = [chargeGroup1, chargeGroup2, chargeGroup3, chargeGroup4, chargeGroup5].filter( + (x): x is number => x !== undefined, + ); + + if (chargeGroupIDs.length === 0) continue; + if (seenModules.has(item.type_id)) continue; + seenModules.add(item.type_id); + + newModulesWithCharges.push({ + typeId: item.type_id, + name: eveData?.typeIDs?.[item.type_id].name ?? "Unknown", + chargeGroupIDs, + chargeSize: item.attributes.get(eveData?.attributeMapping?.chargeSize || 0)?.value ?? -1, + }); + } + + setModulesWithCharges(newModulesWithCharges); + + /* Reset the filter, as the ship is changed. */ + setFilter({ + ...filter, + moduleWithCharge: undefined, + }); + + /* Filter should not be part of the dependency array. */ + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [shipSnapShot, eveData, setFilter]); React.useEffect(() => { if (!eveData.loaded) return; @@ -94,43 +171,79 @@ export const HardwareListing = () => { groups: {}, items: [], }; + const newChargeGroups: ListingGroup = { + name: "Charges", + meta: 0, + groups: {}, + items: [], + }; for (const typeId in eveData.typeIDs) { const module = eveData.typeIDs[typeId]; - /* Modules (7), Drones (18), Subsystems (32), and Structures (66) */ - if (module.categoryID !== 7 && module.categoryID !== 18 && module.categoryID !== 32 && module.categoryID !== 66) + /* Modules (7), Charges (8), Drones (18), Subsystems (32), and Structures (66) */ + if ( + module.categoryID !== 7 && + module.categoryID !== 8 && + module.categoryID !== 18 && + module.categoryID !== 32 && + module.categoryID !== 66 + ) { continue; + } if (module.marketGroupID === undefined) continue; if (!module.published) continue; - let slotType: ShipSnapshotSlotsType | "dronebay" | undefined = eveData.typeDogma?.[typeId]?.dogmaEffects - .map((effect) => { - switch (effect.effectID) { - case 11: - return "lowslot"; - case 13: - return "medslot"; - case 12: - return "hislot"; - case 2663: - return "rig"; - case 3772: - return "subsystem"; + let slotType: ShipSnapshotSlotsType | "dronebay" | "charge" | undefined; + if (module.categoryID !== 8) { + slotType = eveData.typeDogma?.[typeId]?.dogmaEffects + .map((effect) => { + switch (effect.effectID) { + case 11: + return "lowslot"; + case 13: + return "medslot"; + case 12: + return "hislot"; + case 2663: + return "rig"; + case 3772: + return "subsystem"; + } + }) + .filter((slot) => slot !== undefined)[0]; + if (module.categoryID === 18) { + slotType = "dronebay"; + } + + if (slotType === undefined) continue; + + if (filter.lowslot || filter.medslot || filter.hislot || filter.rig_subsystem || filter.drone) { + if (slotType === "lowslot" && !filter.lowslot) continue; + if (slotType === "medslot" && !filter.medslot) continue; + if (slotType === "hislot" && !filter.hislot) continue; + if ((slotType === "rig" || slotType === "subsystem") && !filter.rig_subsystem) continue; + if (module.categoryID === 18 && !filter.drone) continue; + } + } else { + if (filter.moduleWithCharge !== undefined) { + /* If the module has size restrictions, ensure the charge matches. */ + const chargeSize = + eveData.typeDogma?.[typeId]?.dogmaAttributes.find( + (attr) => attr.attributeID === eveData.attributeMapping?.chargeSize, + )?.value ?? -1; + if (filter.moduleWithCharge.chargeSize !== -1 && chargeSize !== filter.moduleWithCharge.chargeSize) continue; + + for (const chargeGroupID of filter.moduleWithCharge.chargeGroupIDs) { + if (module.groupID !== chargeGroupID) continue; + + slotType = "charge"; + break; } - }) - .filter((slot) => slot !== undefined)[0]; - if (module.categoryID === 18) { - slotType = "dronebay"; - } - if (slotType === undefined) continue; - - if (filter.lowslot || filter.medslot || filter.hislot || filter.rig_subsystem || filter.drone) { - if (slotType === "lowslot" && !filter.lowslot) continue; - if (slotType === "medslot" && !filter.medslot) continue; - if (slotType === "hislot" && !filter.hislot) continue; - if ((slotType === "rig" || slotType === "subsystem") && !filter.rig_subsystem) continue; - if (module.categoryID === 18 && !filter.drone) continue; + if (slotType === undefined) continue; + } else { + slotType = "charge"; + } } if (search !== "" && !module.name.toLowerCase().includes(search.toLowerCase())) continue; @@ -166,8 +279,12 @@ export const HardwareListing = () => { if (module.categoryID === 66) marketGroups.push(477); /* Build up the tree till the find the leaf node. */ - let groupTree = newModuleGroups; + let groupTree = module.categoryID === 8 ? newChargeGroups : newModuleGroups; for (const group of marketGroups.reverse()) { + if (module.categoryID === 8 && filter.moduleWithCharge !== undefined && group >= 0) { + continue; + } + if (groupTree.groups[group] === undefined) { let name; let meta; @@ -219,6 +336,7 @@ export const HardwareListing = () => { } setModuleGroups(newModuleGroups); + setChageGroups(newChargeGroups); }, [eveData, search, filter]); return ( @@ -226,7 +344,7 @@ export const HardwareListing = () => {
setSearch(e.target.value)} />
-
+
setFilter({ ...filter, lowslot: !filter.lowslot })} @@ -258,13 +376,47 @@ export const HardwareListing = () => {
-
- {Object.keys(moduleGroups.groups) - .sort((a, b) => moduleGroups.groups[a].name.localeCompare(moduleGroups.groups[b].name)) - .map((groupId) => { - return ; +
+ {modulesWithCharges + .sort((a, b) => a.name.localeCompare(b.name)) + .map((chargeGroup) => { + return ( + + setFilter({ + ...filter, + moduleWithCharge: filter.moduleWithCharge?.typeId === chargeGroup.typeId ? undefined : chargeGroup, + }) + } + > + + + ); })}
+
+
setSelection("modules")} className={clsx({ [styles.selected]: selection === "modules" })}> + Modules +
+
setSelection("charges")} className={clsx({ [styles.selected]: selection === "charges" })}> + Charges +
+
+
+ +
+
+ +
); }; diff --git a/src/TreeListing/TreeListing.tsx b/src/TreeListing/TreeListing.tsx index 13f3977..41a4515 100644 --- a/src/TreeListing/TreeListing.tsx +++ b/src/TreeListing/TreeListing.tsx @@ -87,11 +87,11 @@ export const TreeLeaf = (props: { */ export const TreeListing = (props: { level: number; - header: React.ReactNode; + header?: React.ReactNode; height?: number; getChildren: () => React.ReactNode; }) => { - const [expanded, setExpanded] = React.useState(false); + const [expanded, setExpanded] = React.useState(props.header === undefined); const stylesHeader = styles[`header${props.level}`]; const stylesContent = styles[`content${props.level}`]; @@ -108,17 +108,19 @@ export const TreeListing = (props: { return (
-
setExpanded((current) => !current)} - > - - - - {props.header} -
-
{children}
+ {props.header !== undefined && ( +
setExpanded((current) => !current)} + > + + + + {props.header} +
+ )} +
{children}
);