feat: list hulls for creating new empty fits (#28)
This list can be searched and navigated pretty much like you can in-game.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -18,6 +18,8 @@ const TestEveData = () => {
|
||||
return (
|
||||
<div>
|
||||
TypeIDs: {eveData.typeIDs ? Object.keys(eveData.typeIDs).length : "loading"}<br/>
|
||||
GroupIDs: {eveData.groupIDs ? Object.keys(eveData.groupIDs).length : "loading"}<br/>
|
||||
MarketGroups: {eveData.marketGroups ? Object.keys(eveData.marketGroups).length : "loading"}<br/>
|
||||
TypeDogma: {eveData.typeDogma ? Object.keys(eveData.typeDogma).length : "loading"}<br/>
|
||||
DogmaEffects: {eveData.dogmaEffects ? Object.keys(eveData.dogmaEffects).length : "loading"}<br/>
|
||||
DogmaAttributes: {eveData.dogmaAttributes ? Object.keys(eveData.dogmaAttributes).length : "loading"}<br/>
|
||||
|
||||
@@ -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<string, TypeID>;
|
||||
groupIDs?: Record<string, GroupID>;
|
||||
marketGroups?: Record<string, MarketGroup>;
|
||||
typeDogma?: Record<string, TypeDogma>;
|
||||
dogmaEffects?: Record<string, DogmaEffect>;
|
||||
dogmaAttributes?: Record<string, DogmaAttribute>;
|
||||
@@ -37,6 +39,8 @@ async function fetchDataFile(dataUrl: string, name: string, pb2: any): Promise<o
|
||||
|
||||
function isLoaded(dogmaData: DogmaData): boolean | undefined {
|
||||
if (dogmaData.typeIDs === undefined) return undefined;
|
||||
if (dogmaData.groupIDs === undefined) return undefined;
|
||||
if (dogmaData.marketGroups === undefined) return undefined;
|
||||
if (dogmaData.typeDogma === undefined) return undefined;
|
||||
if (dogmaData.dogmaEffects === undefined) return undefined;
|
||||
if (dogmaData.dogmaAttributes === undefined) return undefined;
|
||||
@@ -77,6 +81,8 @@ export const EveDataProvider = (props: DogmaDataProps) => {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
86
src/HullListing/HullListing.module.css
Normal file
86
src/HullListing/HullListing.module.css
Normal file
@@ -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;
|
||||
}
|
||||
34
src/HullListing/HullListing.stories.tsx
Normal file
34
src/HullListing/HullListing.stories.tsx
Normal file
@@ -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<typeof HullListing> = {
|
||||
component: HullListing,
|
||||
tags: ['autodocs'],
|
||||
title: 'Component/HullListing',
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HullListing>;
|
||||
|
||||
const withEsiProvider: Decorator<{ changeHull: (typeId: number) => void }> = (Story) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<EsiProvider>
|
||||
<div style={{height: "400px"}}>
|
||||
<Story />
|
||||
</div>
|
||||
</EsiProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
changeHull: (typeId: number) => console.log(`changeHull(${typeId})`),
|
||||
},
|
||||
decorators: [withEsiProvider],
|
||||
};
|
||||
144
src/HullListing/HullListing.tsx
Normal file
144
src/HullListing/HullListing.tsx
Normal file
@@ -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<number, string> = {
|
||||
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 <div>
|
||||
<div className={clsx(styles.header3, styles.hull)} onClick={() => setExpanded((current) => !current)}>
|
||||
<span>
|
||||
<img src={`https://images.evetech.net/types/${props.typeId}/icon?size=32`} alt="" />
|
||||
</span>
|
||||
<span>
|
||||
{props.name}
|
||||
</span>
|
||||
<span className={styles.hullSimulate} onClick={() => props.changeHull(props.typeId)}>
|
||||
<Icon name="simulate" size={32} />
|
||||
</span>
|
||||
</div>
|
||||
<div className={clsx(styles.level3, {[styles.collapsed]: !expanded})}>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
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 <Hull key={typeId} typeId={parseInt(typeId)} name={name} changeHull={props.changeHull} />
|
||||
})}</>;
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className={styles.header2} onClick={() => setExpanded((current) => !current)}>
|
||||
{props.name} [{Object.keys(props.entries).length}]
|
||||
</div>
|
||||
<div className={clsx(styles.level2, {[styles.collapsed]: !expanded})}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const HullGroup = (props: { name: string, entries: ListingGroup, changeHull: (typeId: number) => void }) => {
|
||||
const [expanded, setExpanded] = React.useState(false);
|
||||
|
||||
let children = <></>;
|
||||
if (expanded) {
|
||||
children = <>
|
||||
<HullRace name="Amarr" entries={props.entries.Amarr} changeHull={props.changeHull} />
|
||||
<HullRace name="Caldari" entries={props.entries.Caldari} changeHull={props.changeHull} />
|
||||
<HullRace name="Gallente" entries={props.entries.Gallente} changeHull={props.changeHull} />
|
||||
<HullRace name="Minmatar" entries={props.entries.Minmatar} changeHull={props.changeHull} />
|
||||
<HullRace name="Non-Empire" entries={props.entries.NonEmpire} changeHull={props.changeHull} />
|
||||
</>
|
||||
}
|
||||
|
||||
return <div>
|
||||
<div className={styles.header1} onClick={() => setExpanded((current) => !current)}>
|
||||
{props.name}
|
||||
</div>
|
||||
<div className={clsx(styles.level1, {[styles.collapsed]: !expanded})}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<ListingGroups>({});
|
||||
const [search, setSearch] = React.useState<string>("");
|
||||
|
||||
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 <div className={styles.listing}>
|
||||
<div className={styles.topbar}>
|
||||
<input type="text" placeholder="Search" value={search} onChange={(e) => setSearch(e.target.value)} />
|
||||
</div>
|
||||
<div className={styles.listingContent}>
|
||||
{Object.keys(hullGroups).sort().map((groupName) => {
|
||||
const groupData = hullGroups[groupName];
|
||||
return <HullGroup key={groupName} name={groupName} entries={groupData} changeHull={props.changeHull} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
};
|
||||
1
src/HullListing/index.ts
Normal file
1
src/HullListing/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { HullListing } from "./HullListing";
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1 +1 @@
|
||||
export const defaultDataUrl = "https://data.eveship.fit/v1.2-20231115/";
|
||||
export const defaultDataUrl = "https://data.eveship.fit/v4.0-20231115/";
|
||||
|
||||
Reference in New Issue
Block a user