diff --git a/src/components/FitButtonBar/RenameButton.tsx b/src/components/FitButtonBar/RenameButton.tsx index dc8086c..a1046d5 100644 --- a/src/components/FitButtonBar/RenameButton.tsx +++ b/src/components/FitButtonBar/RenameButton.tsx @@ -23,10 +23,10 @@ export const RenameButton = () => { }, [fitManager]); const openRename = React.useCallback(() => { - if (currentFit.fit === null) return; - setName(currentFit.fit.name); + if (currentFit.currentFit === null) return; + setName(currentFit.currentFit.name); setIsRenameOpen(true); - }, [currentFit.fit]); + }, [currentFit.currentFit]); return ( <> diff --git a/src/components/FitButtonBar/SaveButton.tsx b/src/components/FitButtonBar/SaveButton.tsx index 5ffe317..af2ca24 100644 --- a/src/components/FitButtonBar/SaveButton.tsx +++ b/src/components/FitButtonBar/SaveButton.tsx @@ -16,13 +16,13 @@ export const SaveButton = () => { const saveBrowser = React.useCallback( (force?: boolean) => { - if (currentFit.fit === null) return; + if (currentFit.currentFit === null) return; setIsPopupOpen(false); if (!force) { for (const fit of localFits.fittings) { - if (fit.name === currentFit.fit.name) { + if (fit.name === currentFit.currentFit.name) { setIsAlreadyExistsOpen(true); return; } @@ -30,9 +30,9 @@ export const SaveButton = () => { } setIsAlreadyExistsOpen(false); - localFits.addFit(currentFit.fit); + localFits.addFit(currentFit.currentFit); }, - [localFits, currentFit.fit], + [localFits, currentFit.currentFit], ); return ( @@ -60,7 +60,7 @@ export const SaveButton = () => { >
- You have a local fitting with the name "{currentFit.fit?.name}"; do you want to update it? + You have a local fitting with the name "{currentFit.currentFit?.name}"; do you want to update it?
saveBrowser(true)}> diff --git a/src/components/FitHistory/FitHistory.stories.tsx b/src/components/FitHistory/FitHistory.stories.tsx index b0e5c43..3ce8de4 100644 --- a/src/components/FitHistory/FitHistory.stories.tsx +++ b/src/components/FitHistory/FitHistory.stories.tsx @@ -21,7 +21,7 @@ type Story = StoryObj; const TestStory = () => { const currentFit = useCurrentFit(); - return
Current Fit: {currentFit.fit?.name}
; + return
Current Fit: {currentFit.currentFit?.name}
; }; export const Default: Story = { diff --git a/src/components/FitHistory/FitHistory.tsx b/src/components/FitHistory/FitHistory.tsx index b0021ec..a1be1dc 100644 --- a/src/components/FitHistory/FitHistory.tsx +++ b/src/components/FitHistory/FitHistory.tsx @@ -27,12 +27,14 @@ export const FitHistory = (props: FitHistoryProps) => { historySizeRef.current = props.historySize; React.useEffect(() => { - const fit = currentFit.fit; - if (fit === null) return; + const fit = currentFit.currentFit; + if (fit === null || currentFit.isPreview) return; /* Store the fit as a JSON string, to ensure that any modifications * to the current doesn't impact the history. */ const fitString = JSON.stringify(fit); + /* Do not store fits in the history that have no changes. */ if (currentIndexRef.current !== -1 && historyRef.current[currentIndexRef.current] === fitString) return; + if (historyRef.current.length > 0 && historyRef.current[historyRef.current.length - 1] === fitString) return; setHistory((prev) => { if (prev.length >= historySizeRef.current) { @@ -42,7 +44,7 @@ export const FitHistory = (props: FitHistoryProps) => { return [...prev, fitString]; }); setCurrentIndex(-1); - }, [currentFit.fit]); + }, [currentFit.currentFit, currentFit.isPreview]); React.useEffect(() => { if (currentIndex === -1) return; diff --git a/src/components/HardwareListing/HardwareListing.tsx b/src/components/HardwareListing/HardwareListing.tsx index 742198d..7afec2a 100644 --- a/src/components/HardwareListing/HardwareListing.tsx +++ b/src/components/HardwareListing/HardwareListing.tsx @@ -56,16 +56,21 @@ const OnItemDragStart = ( }; }; -const PreloadImage = (typeId: number): ((e: React.MouseEvent) => void) => { - return () => { - const img = new Image(); - img.src = `https://images.evetech.net/types/${typeId}/icon?size=64`; - }; -}; - const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: boolean }) => { const fitManager = useFitManager(); + const PreviewStart = React.useCallback( + (typeId: number, slotType: CalculationSlotType): void => { + if (slotType === "DroneBay") return; + fitManager.addItem(typeId, slotType, true); + }, + [fitManager], + ); + + const PreviewEnd = React.useCallback((): void => { + fitManager.removePreview(); + }, [fitManager]); + const getChildren = React.useCallback(() => { return ( <> @@ -78,9 +83,13 @@ const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: bo key={item.typeId} level={2} content={item.name} - onDoubleClick={() => fitManager.addItem(item.typeId, slotType)} + onDoubleClick={() => { + PreviewEnd(); + fitManager.addItem(item.typeId, slotType); + }} onDragStart={OnItemDragStart(item.typeId, slotType)} - onMouseEnter={PreloadImage(item.typeId)} + onMouseEnter={() => PreviewStart(item.typeId, slotType)} + onMouseLeave={() => PreviewEnd()} /> ); })} @@ -95,7 +104,7 @@ const ModuleGroup = (props: { level: number; group: ListingGroup; hideGroup?: bo })} ); - }, [fitManager, props.group, props.level]); + }, [fitManager, props.group, props.level, PreviewStart, PreviewEnd]); if (props.hideGroup) { return ; diff --git a/src/components/HullListing/HullListing.tsx b/src/components/HullListing/HullListing.tsx index 1470a4a..96c393d 100644 --- a/src/components/HullListing/HullListing.tsx +++ b/src/components/HullListing/HullListing.tsx @@ -213,7 +213,7 @@ export const HullListing = () => { if (hull.marketGroupID === undefined) continue; if (!hull.published) continue; - if (filter.currentHull && currentFit.fit?.shipTypeId !== parseInt(typeId)) continue; + if (filter.currentHull && currentFit.currentFit?.shipTypeId !== parseInt(typeId)) continue; const fits: ListingFit[] = []; if (anyFilter) { @@ -221,7 +221,7 @@ export const HullListing = () => { if (filter.characterFits && Object.keys(characterFitsGrouped).includes(typeId)) fits.push(...characterFitsGrouped[typeId]); if (fits.length == 0) { - if (!filter.currentHull || currentFit.fit?.shipTypeId !== parseInt(typeId)) continue; + if (!filter.currentHull || currentFit.currentFit?.shipTypeId !== parseInt(typeId)) continue; } } else { if (Object.keys(localFitsGrouped).includes(typeId)) fits.push(...localFitsGrouped[typeId]); diff --git a/src/components/ShipAttribute/ShipAttribute.module.css b/src/components/ShipAttribute/ShipAttribute.module.css new file mode 100644 index 0000000..12c81f1 --- /dev/null +++ b/src/components/ShipAttribute/ShipAttribute.module.css @@ -0,0 +1,7 @@ +.increase { + color: #8dc169; +} + +.decrease { + color: #ff454b; +} diff --git a/src/components/ShipAttribute/ShipAttribute.tsx b/src/components/ShipAttribute/ShipAttribute.tsx index 9305dda..3f19258 100644 --- a/src/components/ShipAttribute/ShipAttribute.tsx +++ b/src/components/ShipAttribute/ShipAttribute.tsx @@ -1,11 +1,16 @@ +import clsx from "clsx"; import React from "react"; import { useEveData } from "@/providers/EveDataProvider"; -import { useStatistics } from "@/providers/StatisticsProvider"; +import { useCurrentStatistics, useStatistics } from "@/providers/StatisticsProvider"; + +import styles from "./ShipAttribute.module.css"; export interface AttributeProps { /** Name of the attribute. */ name: string; + /** The unit of the attribute, used as postfix. */ + unit?: string; /** How many decimals to render. */ fixed: number; /** Whether this is a resistance attribute. */ @@ -16,28 +21,55 @@ export interface AttributeProps { roundDown?: boolean; } +export enum AttributeChange { + Increase = "Increase", + Decrease = "Decrease", + Unchanged = "Unchanged", + Unknown = "Unknown", +} + /** * Return the value of a ship's attribute. */ -export function useAttribute(type: "Ship" | "Char", props: AttributeProps) { +export function useAttribute(type: "Ship" | "Char", props: AttributeProps): { value: string; change: AttributeChange } { const eveData = useEveData(); const statistics = useStatistics(); + const currentStatistics = useCurrentStatistics(); + + const attributeId = eveData?.attributeMapping[props.name] ?? 0; let value; + let currentValue; if (eveData === null || statistics === null) { value = 0; + currentValue = 0; } else { - const attributeId = eveData.attributeMapping[props.name] ?? 0; - if (type === "Ship") { value = statistics.hull.attributes.get(attributeId)?.value; + currentValue = currentStatistics?.hull.attributes.get(attributeId)?.value; } else { value = statistics.char.attributes.get(attributeId)?.value; + currentValue = currentStatistics?.char.attributes.get(attributeId)?.value; } } if (value === undefined) { - return "?"; + return { + value: "?", + change: AttributeChange.Unknown, + }; + } + + let change = AttributeChange.Unchanged; + if (currentValue !== undefined && currentValue !== value) { + const highIsGood = + props.isResistance || props.name === "mass" ? false : eveData?.dogmaAttributes[attributeId]?.highIsGood; + + if (currentValue < value) { + change = highIsGood ? AttributeChange.Increase : AttributeChange.Decrease; + } else { + change = highIsGood ? AttributeChange.Decrease : AttributeChange.Increase; + } } if (props.isResistance) { @@ -65,26 +97,61 @@ export function useAttribute(type: "Ship" | "Char", props: AttributeProps) { value = 0; } - return value.toLocaleString("en", { - minimumFractionDigits: props.fixed, - maximumFractionDigits: props.fixed, - }); + return { + value: value.toLocaleString("en", { + minimumFractionDigits: props.fixed, + maximumFractionDigits: props.fixed, + }), + change, + }; } /** * Render a single ship attribute of a ship's snapshot. */ export const ShipAttribute = (props: AttributeProps) => { - const stringValue = useAttribute("Ship", props); + const { value, change } = useAttribute("Ship", props); + const prefix = + props.unit === undefined + ? "" + : props.unit === "s" || props.unit === "x" || props.unit === "%" + ? props.unit + : ` ${props.unit}`; - return {stringValue}; + return ( + + {value} + {prefix} + + ); }; /** * Render a single character attribute of a ship's snapshot. */ export const CharAttribute = (props: AttributeProps) => { - const stringValue = useAttribute("Char", props); + const { value, change } = useAttribute("Char", props); + const prefix = + props.unit === undefined + ? "" + : props.unit === "s" || props.unit === "x" || props.unit === "%" + ? props.unit + : ` ${props.unit}`; - return {stringValue}; + return ( + + {value} + {prefix} + + ); }; diff --git a/src/components/ShipFit/Hull.tsx b/src/components/ShipFit/Hull.tsx index 8b792b1..a512280 100644 --- a/src/components/ShipFit/Hull.tsx +++ b/src/components/ShipFit/Hull.tsx @@ -10,11 +10,11 @@ export interface ShipFitProps { export const Hull = () => { const currentFit = useCurrentFit(); - if (currentFit.fit === null) { + if (currentFit.currentFit === null) { return <>; } - const shipTypeId = currentFit.fit.shipTypeId; + const shipTypeId = currentFit.currentFit.shipTypeId; if (shipTypeId === undefined) { return <>; } diff --git a/src/components/ShipFit/ShipFit.module.css b/src/components/ShipFit/ShipFit.module.css index 2f71bbc..7d03e95 100644 --- a/src/components/ShipFit/ShipFit.module.css +++ b/src/components/ShipFit/ShipFit.module.css @@ -209,3 +209,7 @@ transform: rotate(var(--reverse-rotation)); stroke: #ffffff; } + +.preview { + filter: sepia(100%) hue-rotate(190deg) saturate(200%); +} diff --git a/src/components/ShipFit/Slot.tsx b/src/components/ShipFit/Slot.tsx index 739c38b..04e651d 100644 --- a/src/components/ShipFit/Slot.tsx +++ b/src/components/ShipFit/Slot.tsx @@ -1,3 +1,4 @@ +import clsx from "clsx"; import React from "react"; import { Icon, IconName } from "@/components/Icon"; @@ -5,7 +6,7 @@ import { useEveData } from "@/providers/EveDataProvider"; import { useStatistics } from "@/providers/StatisticsProvider"; import { useFitManager } from "@/providers/FitManagerProvider"; import { CalculationSlot } from "@/providers/DogmaEngineProvider"; -import { EsfSlot, EsfSlotType, EsfState } from "@/providers/CurrentFitProvider"; +import { EsfSlot, EsfSlotType, EsfState, useCurrentFit } from "@/providers/CurrentFitProvider"; import styles from "./ShipFit.module.css"; @@ -20,8 +21,12 @@ export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolea const eveData = useEveData(); const statistics = useStatistics(); const fitManager = useFitManager(); + const currentFit = useCurrentFit(); const module = statistics?.items.find((item) => item.slot.type === props.type && item.slot.index === props.index); + const fitModule = currentFit?.fit?.modules.find( + (item) => item.slot.type === props.type && item.slot.index === props.index, + ); const active = module?.max_state !== "Passive" && module?.max_state !== "Online"; const offlineState = React.useCallback( @@ -199,8 +204,12 @@ export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolea preserveAspectRatio="xMidYMin slice" > - {props.fittable && module !== undefined && active && } - {props.fittable && module !== undefined && !active && } + {props.fittable && fitModule?.state !== "Preview" && module !== undefined && active && ( + + )} + {props.fittable && fitModule?.state !== "Preview" && module !== undefined && !active && ( + + )} ); @@ -233,6 +242,7 @@ export const Slot = (props: { type: EsfSlotType; index: number; fittable: boolea title={eveData.typeIDs[module.type_id].name} draggable={true} onDragStart={onDragStart} + className={clsx({ [styles.preview]: fitModule?.state === "Preview" })} /> ); } diff --git a/src/components/ShipFitExtended/ShipFitExtended.stories.tsx b/src/components/ShipFitExtended/ShipFitExtended.stories.tsx index 5bb60dc..1e5a1e8 100644 --- a/src/components/ShipFitExtended/ShipFitExtended.stories.tsx +++ b/src/components/ShipFitExtended/ShipFitExtended.stories.tsx @@ -6,6 +6,7 @@ import { useFitSelection, withDecoratorFull } from "../../../.storybook/helpers" import { HardwareListing } from "@/components/HardwareListing"; import { HullListing } from "@/components/HullListing"; +import { ShipStatistics } from "@/components/ShipStatistics"; import { EsfFit } from "@/providers/CurrentFitProvider"; import { ShipFitExtended } from "./"; @@ -52,7 +53,7 @@ export const WithHardwareListing: Story = { useFitSelection(fit); return ( -
+
@@ -60,6 +61,10 @@ export const WithHardwareListing: Story = {
+
+
+ +
); }, @@ -77,7 +82,7 @@ export const WithHullListing: Story = { useFitSelection(fit); return ( -
+
@@ -85,6 +90,10 @@ export const WithHullListing: Story = {
+
+
+ +
); }, diff --git a/src/components/ShipFitExtended/ShipFitExtended.tsx b/src/components/ShipFitExtended/ShipFitExtended.tsx index 7a2e4c2..140a6a1 100644 --- a/src/components/ShipFitExtended/ShipFitExtended.tsx +++ b/src/components/ShipFitExtended/ShipFitExtended.tsx @@ -36,7 +36,7 @@ const ShipDroneBay = () => { if (eveData === null) return <>; - const isStructure = eveData.typeIDs[currentFit.fit?.shipTypeId ?? 0]?.categoryID === 65; + const isStructure = eveData.typeIDs[currentFit.currentFit?.shipTypeId ?? 0]?.categoryID === 65; return ( <> @@ -77,7 +77,7 @@ const FitName = () => { return ( <>
Name
-
{currentFit.fit?.name}
+
{currentFit.currentFit?.name}
); }; @@ -118,7 +118,7 @@ export const ShipFitExtended = () => {
- {currentFit.fit === null &&
To start, select a hull on the left.
} + {currentFit.currentFit === null &&
To start, select a hull on the left.
}
); }; diff --git a/src/components/ShipStatistics/RechargeRate.tsx b/src/components/ShipStatistics/RechargeRate.tsx index 4bada50..13230ca 100644 --- a/src/components/ShipStatistics/RechargeRate.tsx +++ b/src/components/ShipStatistics/RechargeRate.tsx @@ -2,17 +2,17 @@ import clsx from "clsx"; import React from "react"; import { IconName, Icon } from "@/components/Icon"; -import { useAttribute } from "@/components/ShipAttribute"; +import { ShipAttribute, useAttribute } from "@/components/ShipAttribute"; import styles from "./ShipStatistics.module.css"; export const RechargeRateItem = (props: { name: string; icon: IconName }) => { - const stringValue = useAttribute("Ship", { + const { value } = useAttribute("Ship", { name: props.name, fixed: 1, }); - if (stringValue == "0.0") { + if (value == "0.0") { return ( @@ -28,7 +28,7 @@ export const RechargeRateItem = (props: { name: string; icon: IconName }) => { - {stringValue} hp/s + ); }; diff --git a/src/components/ShipStatistics/Resistance.tsx b/src/components/ShipStatistics/Resistance.tsx index d7a2ddb..5e0aa84 100644 --- a/src/components/ShipStatistics/Resistance.tsx +++ b/src/components/ShipStatistics/Resistance.tsx @@ -1,11 +1,11 @@ import React from "react"; -import { useAttribute } from "@/components/ShipAttribute"; +import { ShipAttribute, useAttribute } from "@/components/ShipAttribute"; import styles from "./ShipStatistics.module.css"; export const Resistance = (props: { name: string }) => { - const stringValue = useAttribute("Ship", { + const { value } = useAttribute("Ship", { name: props.name, fixed: 0, isResistance: true, @@ -25,8 +25,8 @@ export const Resistance = (props: { name: string }) => { return ( - - {stringValue} % + + ); }; diff --git a/src/components/ShipStatistics/ShipStatistics.tsx b/src/components/ShipStatistics/ShipStatistics.tsx index 0ab4ae6..c29913b 100644 --- a/src/components/ShipStatistics/ShipStatistics.tsx +++ b/src/components/ShipStatistics/ShipStatistics.tsx @@ -22,7 +22,7 @@ export const ShipStatistics = () => { const statistics = useStatistics(); let capacitorState = "Stable"; - const isStructure = eveData?.typeIDs[currentFit.fit?.shipTypeId ?? 0]?.categoryID === 65; + const isStructure = eveData?.typeIDs[currentFit.currentFit?.shipTypeId ?? 0]?.categoryID === 65; const attributeId = eveData?.attributeMapping.capacitorDepletesIn ?? 0; const capacitorDepletesIn = statistics?.hull.attributes.get(attributeId)?.value; @@ -55,15 +55,14 @@ export const ShipStatistics = () => { > - GJ /{" "} - s + /{" "} + - Δ GJ/s ( - - %) + Δ ( + ) @@ -72,7 +71,7 @@ export const ShipStatistics = () => { headerLabel="Offense" headerContent={ - dps + } > @@ -82,8 +81,8 @@ export const ShipStatistics = () => { - dps ( - dps) + ( + )
@@ -91,7 +90,7 @@ export const ShipStatistics = () => { - HP + @@ -101,7 +100,7 @@ export const ShipStatistics = () => { headerLabel="Defense" headerContent={ - ehp + } > @@ -128,9 +127,10 @@ export const ShipStatistics = () => { - hp + +
+
- s
@@ -146,7 +146,7 @@ export const ShipStatistics = () => { - hp + @@ -162,7 +162,7 @@ export const ShipStatistics = () => { - hp + @@ -178,7 +178,7 @@ export const ShipStatistics = () => { headerLabel="Targeting" headerContent={ - km + } > @@ -188,7 +188,7 @@ export const ShipStatistics = () => { - points + @@ -196,7 +196,7 @@ export const ShipStatistics = () => { - mm + @@ -206,7 +206,7 @@ export const ShipStatistics = () => { - m + @@ -214,7 +214,7 @@ export const ShipStatistics = () => { - x + @@ -225,7 +225,7 @@ export const ShipStatistics = () => { headerLabel="Navigation" headerContent={ - m/s + } > @@ -235,7 +235,7 @@ export const ShipStatistics = () => { - t + @@ -243,7 +243,7 @@ export const ShipStatistics = () => { - x + @@ -253,7 +253,7 @@ export const ShipStatistics = () => { - AU/s + @@ -261,7 +261,7 @@ export const ShipStatistics = () => { - s + @@ -273,7 +273,7 @@ export const ShipStatistics = () => { headerLabel="Drones" headerContent={ - dps + } > @@ -292,7 +292,7 @@ export const ShipStatistics = () => { - km + diff --git a/src/components/TreeListing/TreeListing.tsx b/src/components/TreeListing/TreeListing.tsx index fd25494..556c9d5 100644 --- a/src/components/TreeListing/TreeListing.tsx +++ b/src/components/TreeListing/TreeListing.tsx @@ -56,6 +56,7 @@ export const TreeLeaf = (props: { onDoubleClick?: (e: React.MouseEvent) => void; onDragStart?: (e: React.DragEvent) => void; onMouseEnter?: (e: React.MouseEvent) => void; + onMouseLeave?: (e: React.MouseEvent) => void; }) => { const stylesHeader = styles[`header${props.level}`]; @@ -76,6 +77,7 @@ export const TreeLeaf = (props: { draggable={!!props.onDragStart} onDragStart={props.onDragStart} onMouseEnter={props.onMouseEnter} + onMouseLeave={props.onMouseLeave} > {props.icon !== undefined && ( diff --git a/src/hooks/ExportEft/ExportEft.tsx b/src/hooks/ExportEft/ExportEft.tsx index cbd01a1..4b24669 100644 --- a/src/hooks/ExportEft/ExportEft.tsx +++ b/src/hooks/ExportEft/ExportEft.tsx @@ -22,7 +22,7 @@ export function useExportEft() { const statistics = useStatistics(); return (): string | null => { - const fit = currentFit.fit; + const fit = currentFit.currentFit; if (eveData === null || fit === null || statistics === null) return null; diff --git a/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx b/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx index 0abb028..e8138d3 100644 --- a/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx +++ b/src/hooks/ExportEveShipFitHash/ExportEveShipFitHash.tsx @@ -55,8 +55,8 @@ export function useExportEveShipFitHash(hashOnly?: boolean) { setFitHash((hashOnly ? "" : "https://eveship.fit/") + `#fit:${newFitHash}`); } - createHash(currentFit.fit); - }, [currentFit.fit, hashOnly]); + createHash(currentFit.currentFit); + }, [currentFit.currentFit, hashOnly]); return fitHash; } diff --git a/src/providers/CurrentFitProvider/CurrentFitProvider.stories.tsx b/src/providers/CurrentFitProvider/CurrentFitProvider.stories.tsx index 469d723..19004cb 100644 --- a/src/providers/CurrentFitProvider/CurrentFitProvider.stories.tsx +++ b/src/providers/CurrentFitProvider/CurrentFitProvider.stories.tsx @@ -24,7 +24,7 @@ const TestStory = ({ fit }: { fit: EsfFit | null }) => { currentFit.setFit(fit ?? null); }); - return
{JSON.stringify(currentFit.fit, null, 2)}
; + return
{JSON.stringify(currentFit.currentFit, null, 2)}
; }; export const Default: Story = { diff --git a/src/providers/CurrentFitProvider/CurrentFitProvider.tsx b/src/providers/CurrentFitProvider/CurrentFitProvider.tsx index 961b3c7..383d65a 100644 --- a/src/providers/CurrentFitProvider/CurrentFitProvider.tsx +++ b/src/providers/CurrentFitProvider/CurrentFitProvider.tsx @@ -15,7 +15,7 @@ export interface EsfSlot { export interface EsfModule { typeId: number; slot: EsfSlot; - state: EsfState; + state: EsfState | "Preview"; charge?: EsfCharge; } @@ -42,13 +42,24 @@ export interface EsfFit { } interface CurrentFit { + /** The current fit to render. */ fit: EsfFit | null; + /** The current fit, regardless of preview state. */ + currentFit: EsfFit | null; + /** Whether the fit is a preview. */ + isPreview: boolean; + /** Set the current fit. */ setFit: React.Dispatch>; + /** Set a (temporary) preview fit, for the over-over effect on modules. */ + setPreview: React.Dispatch>; } const CurrentFitContext = React.createContext({ fit: null, + currentFit: null, + isPreview: false, setFit: () => {}, + setPreview: () => {}, }); export const useCurrentFit = () => { @@ -73,13 +84,17 @@ interface CurrentFitProps { */ export const CurrentFitProvider = (props: CurrentFitProps) => { const [currentFit, setCurrentFit] = React.useState(props.initialFit ?? null); + const [previewFit, setPreviewFit] = React.useState(null); const contextValue = React.useMemo(() => { return { - fit: currentFit, + fit: previewFit ?? currentFit, + currentFit: currentFit, + isPreview: previewFit !== null, setFit: setCurrentFit, + setPreview: setPreviewFit, }; - }, [currentFit, setCurrentFit]); + }, [previewFit, currentFit]); return {props.children}; }; diff --git a/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx b/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx index d8349e7..ce21b38 100644 --- a/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx +++ b/src/providers/DogmaEngineProvider/DogmaEngineProvider.tsx @@ -103,7 +103,7 @@ export const DogmaEngineProvider = (props: DogmaEngineProps) => { modules: fit.modules.map((module) => ({ type_id: module.typeId, slot: module.slot, - state: module.state, + state: module.state === "Preview" ? "Active" : module.state, charge: module.charge === undefined ? undefined diff --git a/src/providers/FitManagerProvider/FitManagerProvider.tsx b/src/providers/FitManagerProvider/FitManagerProvider.tsx index 265244e..fd2c2ce 100644 --- a/src/providers/FitManagerProvider/FitManagerProvider.tsx +++ b/src/providers/FitManagerProvider/FitManagerProvider.tsx @@ -7,13 +7,15 @@ import { useEveData } from "@/providers/EveDataProvider"; interface FitManager { /** Set the current fit. */ setFit: (fit: EsfFit) => void; + /** Remove the preview fit, reverting back to the actual fit. */ + removePreview: () => void; /** Create a new fit of the given ship type. */ createNewFit: (typeId: number) => void; /** Set the name of the current fit. */ setName: (name: string) => void; /** Add an item (module, charge, drone) to the fit. */ - addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge") => void; + addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge", preview?: boolean) => void; /** Set a module in a slot. */ setModule: (slot: EsfSlot, typeId: number) => void; @@ -37,6 +39,7 @@ interface FitManager { const FitManagerContext = React.createContext({ setFit: () => {}, + removePreview: () => {}, createNewFit: () => {}, setName: () => {}, @@ -71,11 +74,16 @@ export const FitManagerProvider = (props: FitManagerProps) => { const currentFit = useCurrentFit(); const statistics = useStatistics(); const setFit = currentFit.setFit; + const setPreview = currentFit.setPreview; + + const currentFitRef = React.useRef(currentFit.currentFit); + currentFitRef.current = currentFit.currentFit; const contextValue = React.useMemo(() => { if (eveData === null) { return { setFit: () => {}, + removePreview: () => {}, createNewFit: () => {}, setName: () => {}, @@ -98,6 +106,9 @@ export const FitManagerProvider = (props: FitManagerProps) => { setFit: (fit: EsfFit) => { setFit(fit); }, + removePreview: () => { + setPreview(null); + }, createNewFit: (typeId: number) => { setFit({ name: "Unnamed Fit", @@ -119,8 +130,11 @@ export const FitManagerProvider = (props: FitManagerProps) => { }); }, - addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge") => { - setFit((oldFit: EsfFit | null): EsfFit | null => { + addItem: (typeId: number, slot: EsfSlotType | "DroneBay" | "Charge", preview?: boolean) => { + const setFitOrPreview = preview ? setPreview : setFit; + setFitOrPreview((oldFit: EsfFit | null): EsfFit | null => { + /* Previews are always based on the current fit. */ + if (preview) oldFit = currentFitRef.current; if (oldFit === null) return null; if (slot === "Charge") { @@ -217,7 +231,7 @@ export const FitManagerProvider = (props: FitManagerProps) => { }, typeId: typeId, charge: undefined, - state: "Active", + state: preview ? "Preview" : "Active", }, ], }; @@ -414,7 +428,7 @@ export const FitManagerProvider = (props: FitManagerProps) => { }); }, }; - }, [eveData, statistics?.slots, setFit]); + }, [eveData, statistics?.slots, setFit, setPreview]); return {props.children}; }; diff --git a/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx b/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx index fa0ad52..87da62a 100644 --- a/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx +++ b/src/providers/StatisticsProvider/StatisticsProvider.stories.tsx @@ -25,11 +25,11 @@ const TestStory = ({ fit }: { fit: EsfFit | null }) => { const currentFit = useCurrentFit(); const statistics = useStatistics(); - if (fit != currentFit.fit) { + if (fit != currentFit.currentFit) { currentFit.setFit(fit); } - if (currentFit.fit === null) { + if (currentFit.currentFit === null) { return
No fit selected
; } if (statistics === null) { diff --git a/src/providers/StatisticsProvider/StatisticsProvider.tsx b/src/providers/StatisticsProvider/StatisticsProvider.tsx index 0dbaf05..4c0812a 100644 --- a/src/providers/StatisticsProvider/StatisticsProvider.tsx +++ b/src/providers/StatisticsProvider/StatisticsProvider.tsx @@ -26,10 +26,18 @@ interface Statistics extends Calculation { slots: StatisticsSlots; } -const StatisticsContext = React.createContext(null); +const StatisticsContext = React.createContext<{ statistics: Statistics | null; current: Statistics | null } | null>( + null, +); export const useStatistics = () => { - return React.useContext(StatisticsContext); + const statistics = React.useContext(StatisticsContext); + return statistics === null ? null : statistics.statistics; +}; + +export const useCurrentStatistics = () => { + const statistics = React.useContext(StatisticsContext); + return statistics === null ? null : statistics.current ?? statistics.statistics; }; interface StatisticsProps { @@ -78,6 +86,10 @@ export const StatisticsProvider = (props: StatisticsProps) => { const currentCharacter = useCurrentCharacter(); const dogmaEngine = useDogmaEngine(); + const [lastStatistics, setLastStatistics] = React.useState(null); + const lastStatisticsRef = React.useRef(lastStatistics); + lastStatisticsRef.current = lastStatistics; + const contextValue = React.useMemo(() => { const fit = currentFit.fit; const skills = currentCharacter.character?.skills; @@ -101,8 +113,15 @@ export const StatisticsProvider = (props: StatisticsProps) => { CalculateSlots(eveData, statistics); - return statistics; - }, [eveData, dogmaEngine, currentFit.fit, currentCharacter.character?.skills]); + if (!currentFit.isPreview) { + setLastStatistics(statistics); + } + + return { + statistics: statistics, + current: currentFit.isPreview ? lastStatisticsRef.current : statistics, + }; + }, [eveData, dogmaEngine, currentFit.fit, currentFit.isPreview, currentCharacter.character?.skills]); return {props.children}; }; diff --git a/src/providers/StatisticsProvider/index.ts b/src/providers/StatisticsProvider/index.ts index 6ecb754..a6fea0c 100644 --- a/src/providers/StatisticsProvider/index.ts +++ b/src/providers/StatisticsProvider/index.ts @@ -1,2 +1,2 @@ -export { StatisticsProvider, useStatistics } from "./StatisticsProvider"; +export { StatisticsProvider, useStatistics, useCurrentStatistics } from "./StatisticsProvider"; export type { StatisticsSlots, StatisticsSlotType } from "./StatisticsProvider";