diff --git a/src/EveDataProvider/DataTypes.tsx b/src/EveDataProvider/DataTypes.tsx index db693d8..d0266fc 100644 --- a/src/EveDataProvider/DataTypes.tsx +++ b/src/EveDataProvider/DataTypes.tsx @@ -19,6 +19,7 @@ export interface TypeID { groupID: number, categoryID: number, published: boolean, + factionID?: number, marketGroupID?: number, capacity?: number, mass?: number, @@ -26,6 +27,17 @@ export interface TypeID { volume?: number, } +export interface GroupID { + name: string, + categoryID: number, + published: boolean, +} + +export interface MarketGroup { + name: string, + parentGroupID?: number, +} + export interface DogmaAttribute { name: string published: boolean, diff --git a/src/EveDataProvider/EveDataProvider.stories.tsx b/src/EveDataProvider/EveDataProvider.stories.tsx index 431d461..8737b86 100644 --- a/src/EveDataProvider/EveDataProvider.stories.tsx +++ b/src/EveDataProvider/EveDataProvider.stories.tsx @@ -18,6 +18,8 @@ const TestEveData = () => { return (
TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}
+ GroupIDs: {eveData.groupIDs ? Object.keys(eveData.groupIDs).length : "loading"}
+ MarketGroups: {eveData.marketGroups ? Object.keys(eveData.marketGroups).length : "loading"}
TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}
DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}
DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}
diff --git a/src/EveDataProvider/EveDataProvider.tsx b/src/EveDataProvider/EveDataProvider.tsx index bc39291..2d35cd5 100644 --- a/src/EveDataProvider/EveDataProvider.tsx +++ b/src/EveDataProvider/EveDataProvider.tsx @@ -1,6 +1,6 @@ import React from "react"; -import { DogmaAttribute, DogmaEffect, TypeDogma, TypeID } from "./DataTypes"; +import { DogmaAttribute, DogmaEffect, GroupID, MarketGroup, TypeDogma, TypeID } from "./DataTypes"; import { defaultDataUrl } from "../settings"; // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -11,6 +11,8 @@ import * as esf_pb2 from "./esf_pb2.js"; interface DogmaData { loaded?: boolean; typeIDs?: Record; + groupIDs?: Record; + marketGroups?: Record; typeDogma?: Record; dogmaEffects?: Record; dogmaAttributes?: Record; @@ -37,6 +39,8 @@ async function fetchDataFile(dataUrl: string, name: string, pb2: any): Promise { } fetchAndLoadDataFile("typeIDs", esf_pb2.esf.TypeIDs); + fetchAndLoadDataFile("groupIDs", esf_pb2.esf.GroupIDs); + fetchAndLoadDataFile("marketGroups", esf_pb2.esf.MarketGroups); fetchAndLoadDataFile("typeDogma", esf_pb2.esf.TypeDogma); fetchAndLoadDataFile("dogmaEffects", esf_pb2.esf.DogmaEffects); fetchAndLoadDataFile("dogmaAttributes", esf_pb2.esf.DogmaAttributes); diff --git a/src/EveDataProvider/esf_pb2.js b/src/EveDataProvider/esf_pb2.js index 9e8ae12..e73575e 100644 --- a/src/EveDataProvider/esf_pb2.js +++ b/src/EveDataProvider/esf_pb2.js @@ -263,6 +263,7 @@ export const esf = $root.esf = (() => { TypeID.prototype.groupID = 0; TypeID.prototype.categoryID = 0; TypeID.prototype.published = false; + TypeID.prototype.factionID = 0; TypeID.prototype.marketGroupID = 0; TypeID.prototype.capacity = 0; TypeID.prototype.mass = 0; @@ -293,22 +294,26 @@ export const esf = $root.esf = (() => { break; } case 5: { - m.marketGroupID = r.int32(); + m.factionID = r.int32(); break; } case 6: { - m.capacity = r.float(); + m.marketGroupID = r.int32(); break; } case 7: { - m.mass = r.float(); + m.capacity = r.float(); break; } case 8: { - m.radius = r.float(); + m.mass = r.float(); break; } case 9: { + m.radius = r.float(); + break; + } + case 10: { m.volume = r.float(); break; } @@ -334,6 +339,211 @@ export const esf = $root.esf = (() => { return TypeIDs; })(); + esf.GroupIDs = (function() { + + function GroupIDs(p) { + this.entries = {}; + if (p) + for (var ks = Object.keys(p), i = 0; i < ks.length; ++i) + if (p[ks[i]] != null) + this[ks[i]] = p[ks[i]]; + } + + GroupIDs.prototype.entries = emptyObject; + + GroupIDs.decode = async function decode(r, l) { + if (!(r instanceof $Reader)) + r = $Reader.create(r); + var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.GroupIDs(), k, value; + while (r.pos < c) { + if (r.need_data()) { + await r.fetch_data(); + } + if (r.is_eof()) break; + + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + if (m.entries === emptyObject) + m.entries = {}; + var c2 = r.uint32() + r.pos; + k = 0; + value = null; + while (r.pos < c2) { + var tag2 = r.uint32(); + switch (tag2 >>> 3) { + case 1: + k = r.int32(); + break; + case 2: + value = $root.esf.GroupIDs.GroupID.decode(r, r.uint32()); + break; + default: + r.skipType(tag2 & 7); + break; + } + } + m.entries[k] = value; + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + GroupIDs.GroupID = (function() { + + function GroupID(p) { + if (p) + for (var ks = Object.keys(p), i = 0; i < ks.length; ++i) + if (p[ks[i]] != null) + this[ks[i]] = p[ks[i]]; + } + + GroupID.prototype.name = ""; + GroupID.prototype.categoryID = 0; + GroupID.prototype.published = false; + + GroupID.decode = function decode(r, l) { + if (!(r instanceof $Reader)) + r = $Reader.create(r); + var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.GroupIDs.GroupID(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.name = r.string(); + break; + } + case 2: { + m.categoryID = r.int32(); + break; + } + case 3: { + m.published = r.bool(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("name")) + throw Error("missing required 'name'", { instance: m }); + if (!m.hasOwnProperty("categoryID")) + throw Error("missing required 'categoryID'", { instance: m }); + if (!m.hasOwnProperty("published")) + throw Error("missing required 'published'", { instance: m }); + return m; + }; + + return GroupID; + })(); + + return GroupIDs; + })(); + + esf.MarketGroups = (function() { + + function MarketGroups(p) { + this.entries = {}; + if (p) + for (var ks = Object.keys(p), i = 0; i < ks.length; ++i) + if (p[ks[i]] != null) + this[ks[i]] = p[ks[i]]; + } + + MarketGroups.prototype.entries = emptyObject; + + MarketGroups.decode = async function decode(r, l) { + if (!(r instanceof $Reader)) + r = $Reader.create(r); + var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.MarketGroups(), k, value; + while (r.pos < c) { + if (r.need_data()) { + await r.fetch_data(); + } + if (r.is_eof()) break; + + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + if (m.entries === emptyObject) + m.entries = {}; + var c2 = r.uint32() + r.pos; + k = 0; + value = null; + while (r.pos < c2) { + var tag2 = r.uint32(); + switch (tag2 >>> 3) { + case 1: + k = r.int32(); + break; + case 2: + value = $root.esf.MarketGroups.MarketGroup.decode(r, r.uint32()); + break; + default: + r.skipType(tag2 & 7); + break; + } + } + m.entries[k] = value; + break; + } + default: + r.skipType(t & 7); + break; + } + } + return m; + }; + + MarketGroups.MarketGroup = (function() { + + function MarketGroup(p) { + if (p) + for (var ks = Object.keys(p), i = 0; i < ks.length; ++i) + if (p[ks[i]] != null) + this[ks[i]] = p[ks[i]]; + } + + MarketGroup.prototype.name = ""; + MarketGroup.prototype.parentGroupID = 0; + + MarketGroup.decode = function decode(r, l) { + if (!(r instanceof $Reader)) + r = $Reader.create(r); + var c = l === undefined ? r.len : r.pos + l, m = new $root.esf.MarketGroups.MarketGroup(); + while (r.pos < c) { + var t = r.uint32(); + switch (t >>> 3) { + case 1: { + m.name = r.string(); + break; + } + case 2: { + m.parentGroupID = r.int32(); + break; + } + default: + r.skipType(t & 7); + break; + } + } + if (!m.hasOwnProperty("name")) + throw Error("missing required 'name'", { instance: m }); + return m; + }; + + return MarketGroup; + })(); + + return MarketGroups; + })(); + esf.DogmaAttributes = (function() { function DogmaAttributes(p) { diff --git a/src/HullListing/HullListing.module.css b/src/HullListing/HullListing.module.css new file mode 100644 index 0000000..78bd1c2 --- /dev/null +++ b/src/HullListing/HullListing.module.css @@ -0,0 +1,86 @@ +.listing { + background-color: #111111; + color: #c5c5c5; + font-size: 15px; + height: 100%; + position: relative; + width: 100%; +} + +.listingContent { + height: calc(100% - 42px); + padding-right: 20px; + overflow-y: auto; +} + +.header1, .header2, .header3 { + display: flex; + padding-left: 10px; + user-select: none; +} +.header1, .header2 { + height: 25px; + line-height: 25px; +} + +.header1 { + background-color: #1d1d1d; +} + +.header1:hover, .header2:hover, .header3:hover { + background-color: #4f4f4f; +} + +.collapsed { + display: none; +} + +.level1 { + padding-left: 20px; +} +.level2 { + padding-left: 40px; +} +.level3 { + padding-left: 60px; +} + +.hull { + display: flex; +} +.hull > span { + display: inline-block; + height: 32px; + line-height: 32px; + margin-right: 6px; +} +.hull > span:nth-child(2) { + flex: 1; +} +.hull > span:last-child { + text-align: right; +} +.hull > span:last-child > img { + opacity: 0.5; +} +.hull > span:last-child > img:hover { + opacity: 1.0; +} + +.hullSimulate { + cursor: pointer; +} + +.topbar { + display: flex; +} + +.topbar > input { + background-color: #1d1d1d; + color: #c5c5c5; + flex: 1; + height: 24px; + line-height: 24px; + margin: 6px; + padding-left: 6px; +} diff --git a/src/HullListing/HullListing.stories.tsx b/src/HullListing/HullListing.stories.tsx new file mode 100644 index 0000000..94a1806 --- /dev/null +++ b/src/HullListing/HullListing.stories.tsx @@ -0,0 +1,34 @@ +import type { Decorator, Meta, StoryObj } from '@storybook/react'; +import React from "react"; + +import { EsiProvider } from '../EsiProvider'; +import { HullListing } from './'; +import { EveDataProvider } from '../EveDataProvider'; + +const meta: Meta = { + component: HullListing, + tags: ['autodocs'], + title: 'Component/HullListing', +}; + +export default meta; +type Story = StoryObj; + +const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void }> = (Story) => { + return ( + + +
+ +
+
+
+ ); +} + +export const Default: Story = { + args: { + changeHull: (typeId: number) => console.log(`changeHull(${typeId})`), + }, + decorators: [withEsiProvider], +}; diff --git a/src/HullListing/HullListing.tsx b/src/HullListing/HullListing.tsx new file mode 100644 index 0000000..e73f80e --- /dev/null +++ b/src/HullListing/HullListing.tsx @@ -0,0 +1,144 @@ +import clsx from "clsx"; +import React from "react"; + +import { EveDataContext } from "../EveDataProvider"; +import { Icon } from "../Icon"; + +import styles from "./HullListing.module.css"; + +interface ListingHulls { + [typeId: string]: string; +} + +interface ListingGroup { + [raceName: string]: ListingHulls; +} + +interface ListingGroups { + [groupName: string]: ListingGroup; +} + +const factionIdToRace: Record = { + 500001: "Caldari", + 500002: "Minmatar", + 500003: "Amarr", + 500004: "Gallente", +} as const; + +const Hull = (props: { typeId: number, name: string, changeHull: (typeId: number) => void }) => { + const [expanded, setExpanded] = React.useState(false); + + return
+
setExpanded((current) => !current)}> + + + + + {props.name} + + props.changeHull(props.typeId)}> + + +
+
+
+
+} + +const HullRace = (props: { name: string, entries: ListingHulls, changeHull: (typeId: number) => void }) => { + const [expanded, setExpanded] = React.useState(false); + + if (props.entries === undefined) return null; + + let children = <>; + if (expanded) { + children = <>{Object.keys(props.entries).sort((a, b) => props.entries[a].localeCompare(props.entries[b])).map((typeId) => { + const name = props.entries[typeId]; + return + })}; + } + + return
+
setExpanded((current) => !current)}> + {props.name} [{Object.keys(props.entries).length}] +
+
+ {children} +
+
+} + +const HullGroup = (props: { name: string, entries: ListingGroup, changeHull: (typeId: number) => void }) => { + const [expanded, setExpanded] = React.useState(false); + + let children = <>; + if (expanded) { + children = <> + + + + + + + } + + return
+
setExpanded((current) => !current)}> + {props.name} +
+
+ {children} +
+
+}; + +/** + * Show all the fittings for the current ESI character. + */ +export const HullListing = (props: { changeHull: (typeId: number) => void }) => { + const eveData = React.useContext(EveDataContext); + + const [hullGroups, setHullGroups] = React.useState({}); + const [search, setSearch] = React.useState(""); + + React.useEffect(() => { + if (!eveData.loaded) return; + + const newHullGroups: ListingGroups = {}; + + for (const typeId in eveData.typeIDs) { + const hull = eveData.typeIDs[typeId]; + if (hull.categoryID !== 6) continue; + if (hull.marketGroupID === undefined) continue; + if (!hull.published) continue; + + if (search !== "" && !hull.name.toLowerCase().includes(search.toLowerCase())) continue; + + const group = eveData.groupIDs?.[hull.groupID]?.name ?? "Unknown Group"; + const race = factionIdToRace[hull.factionID || 0] ?? "NonEmpire"; + + if (newHullGroups[group] === undefined) { + newHullGroups[group] = {}; + } + if (newHullGroups[group][race] === undefined) { + newHullGroups[group][race] = {}; + } + + newHullGroups[group][race][typeId] = hull.name; + } + + setHullGroups(newHullGroups); + }, [eveData, search]); + + return
+
+ setSearch(e.target.value)} /> +
+
+ {Object.keys(hullGroups).sort().map((groupName) => { + const groupData = hullGroups[groupName]; + return + })} +
+
+}; diff --git a/src/HullListing/index.ts b/src/HullListing/index.ts new file mode 100644 index 0000000..1897994 --- /dev/null +++ b/src/HullListing/index.ts @@ -0,0 +1 @@ +export { HullListing } from "./HullListing"; diff --git a/src/Icon/Icon.tsx b/src/Icon/Icon.tsx index 58f4641..41dddaf 100644 --- a/src/Icon/Icon.tsx +++ b/src/Icon/Icon.tsx @@ -20,6 +20,7 @@ const iconMapping = { "shield-boost-rate": "texture/classes/fitting/statsicons/shieldboostrate.png", "shield-hp": "texture/classes/fitting/statsicons/shieldhp.png", "signature-radius": "texture/classes/fitting/statsicons/signatureradius.png", + "simulate": "texture/classes/fitting/iconsimulatorhover.png", "thermal-resistance": "texture/classes/fitting/statsicons/thermalresistance.png", "warp-speed": "texture/classes/fitting/statsicons/warpspeed.png", } as const; diff --git a/src/index.ts b/src/index.ts index 86007b4..85bbf75 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,7 @@ export * from './EveDataProvider'; export * from './EveShipFitHash'; export * from './EveShipFitLink'; export * from './FormatEftToEsi'; +export * from './HullListing'; export * from './Icon'; export * from './ShipAttribute'; export * from './ShipFit'; diff --git a/src/settings.ts b/src/settings.ts index 2d7a130..7770408 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1 +1 @@ -export const defaultDataUrl = "https://data.eveship.fit/v1.2-20231115/"; +export const defaultDataUrl = "https://data.eveship.fit/v4.0-20231115/";