feat: embed a link to eveship.fit in every fit overview (#20)
Also the tools to find back what the fit in the link was.
This commit is contained in:
@@ -31,6 +31,8 @@ Loki Propulsion - Wake Limiter
|
||||
Hammerhead II x1
|
||||
`;
|
||||
|
||||
export const hashFit = "fit:v1:H4sIAAAAAAAAClXOMQ7CMAwF0L2n6AE8xD92bLMiRhi4AQMSrND7i7itGrG9JHb+R4QLnet8fyzL8zOf5tv7+7pcaWIoiTY04u7WrcGrLe/LZk+zppkA7CODdTBIGOydKFTVoiRzzfMdIFZrmYdKHOyWlEEdW0ZQW3N7hYNxsJZBHsRgL+ammRagypZBU9RO3yid1bKuEkRyylDy0CxIxTBthXQdScmmIt7I/+72D6JNPx7qugdyAQAA";
|
||||
|
||||
export const fullFit = {
|
||||
"name": "C3 Ratter : NishEM",
|
||||
"ship_type_id": 29984,
|
||||
|
||||
32
src/EveShipFitHash/EveShipFitHash.stories.tsx
Normal file
32
src/EveShipFitHash/EveShipFitHash.stories.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import React from "react";
|
||||
|
||||
import { hashFit } from '../../.storybook/fits';
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { EveShipFitHash } from './EveShipFitHash';
|
||||
|
||||
const meta: Meta<typeof EveShipFitHash> = {
|
||||
component: EveShipFitHash,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/EveShipFitHash',
|
||||
};
|
||||
|
||||
const withEveDataProvider: Decorator<{fitHash: string}> = (Story) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<Story />
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EveShipFitHash>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
"fitHash": hashFit,
|
||||
},
|
||||
decorators: [withEveDataProvider],
|
||||
};
|
||||
84
src/EveShipFitHash/EveShipFitHash.tsx
Normal file
84
src/EveShipFitHash/EveShipFitHash.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import React from "react";
|
||||
|
||||
import { EsiFit } from "../ShipSnapshotProvider";
|
||||
|
||||
async function decompress(base64compressedBytes: string): Promise<string> {
|
||||
const stream = new Blob([Uint8Array.from(atob(base64compressedBytes), c => c.charCodeAt(0))]).stream();
|
||||
const decompressedStream = stream.pipeThrough(new DecompressionStream("gzip"));
|
||||
const reader = decompressedStream.getReader();
|
||||
|
||||
let result = "";
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
result += String.fromCharCode.apply(null, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async function decodeEsiFit(fitVersion: string, fitCompressed: string): Promise<EsiFit | undefined> {
|
||||
if (fitVersion != "v1") return undefined;
|
||||
|
||||
const fitEncoded = await decompress(fitCompressed);
|
||||
|
||||
const fitLines = fitEncoded.trim().split("\n");
|
||||
const fitHeader = fitLines[0].split(",");
|
||||
|
||||
const fitItems = fitLines.slice(1).map((line) => {
|
||||
const item = line.split(",");
|
||||
return {
|
||||
flag: parseInt(item[0]),
|
||||
type_id: parseInt(item[1]),
|
||||
quantity: parseInt(item[2]),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
ship_type_id: parseInt(fitHeader[0]),
|
||||
name: fitHeader[1],
|
||||
description: fitHeader[2],
|
||||
items: fitItems,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a hash from window.location.hash to an ESI fit.
|
||||
*/
|
||||
export async function eveShipFitHash(fitHash: string): Promise<EsiFit | undefined> {
|
||||
if (!fitHash) return undefined;
|
||||
|
||||
const fitPrefix = fitHash.split(":")[0];
|
||||
const fitVersion = fitHash.split(":")[1];
|
||||
const fitEncoded = fitHash.split(":")[2];
|
||||
|
||||
if (fitPrefix !== "fit") return undefined;
|
||||
|
||||
const esiFit = await decodeEsiFit(fitVersion, fitEncoded);
|
||||
return esiFit;
|
||||
}
|
||||
|
||||
export interface EveShipFitHashProps {
|
||||
/** The hash of the fit string. */
|
||||
fitHash: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* eveShipFitHash(fitHash) converts a hash from window.location.hash to an ESI fit.
|
||||
*
|
||||
* Note: do not use this React component itself, but the eveShipFitHash() function instead.
|
||||
*/
|
||||
export const EveShipFitHash = (props: EveShipFitHashProps) => {
|
||||
const [esiFit, setEsiFit] = React.useState<EsiFit | undefined>(undefined);
|
||||
|
||||
React.useEffect(() => {
|
||||
async function getFit(fitHash: string) {
|
||||
setEsiFit(await eveShipFitHash(fitHash));
|
||||
}
|
||||
|
||||
getFit(props.fitHash);
|
||||
}, [props.fitHash]);
|
||||
|
||||
return <pre>{JSON.stringify(esiFit, null, 2)}</pre>
|
||||
};
|
||||
1
src/EveShipFitHash/index.ts
Normal file
1
src/EveShipFitHash/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { eveShipFitHash } from "./EveShipFitHash";
|
||||
43
src/EveShipFitLink/EveShipFitLink.stories.tsx
Normal file
43
src/EveShipFitLink/EveShipFitLink.stories.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { EveShipFitLink } from './EveShipFitLink';
|
||||
|
||||
const meta: Meta<typeof EveShipFitLink> = {
|
||||
component: EveShipFitLink,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/EveShipFitLink',
|
||||
};
|
||||
|
||||
const withShipSnapshotProvider: Decorator<{ radius?: number }> = (Story, context) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<Story />
|
||||
</ShipSnapshotProvider>
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof EveShipFitLink>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
},
|
||||
decorators: [withShipSnapshotProvider],
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
};
|
||||
65
src/EveShipFitLink/EveShipFitLink.tsx
Normal file
65
src/EveShipFitLink/EveShipFitLink.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import React from "react";
|
||||
|
||||
import { EsiFit, ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
async function compress(str: string): Promise<string> {
|
||||
const stream = new Blob([str]).stream();
|
||||
const compressedStream = stream.pipeThrough(new CompressionStream("gzip"));
|
||||
const reader = compressedStream.getReader();
|
||||
|
||||
let result = "";
|
||||
while (true) {
|
||||
const {done, value} = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
result += String.fromCharCode.apply(null, value);
|
||||
}
|
||||
|
||||
return btoa(result);
|
||||
}
|
||||
|
||||
async function encodeEsiFit(esiFit: EsiFit): Promise<string> {
|
||||
let result = `${esiFit.ship_type_id},${esiFit.name},${esiFit.description}\n`;
|
||||
|
||||
for (const item of esiFit.items) {
|
||||
result += `${item.flag},${item.type_id},${item.quantity}\n`;
|
||||
}
|
||||
|
||||
return "v1:" + await compress(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a link to https://eveship.fit that contains the current fit.
|
||||
*/
|
||||
export function useEveShipFitLink() {
|
||||
const [fitLink, setFitLink] = React.useState("");
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!shipSnapshot?.loaded) return;
|
||||
|
||||
async function doCreateLink() {
|
||||
if (!shipSnapshot?.fit) {
|
||||
setFitLink("");
|
||||
return;
|
||||
}
|
||||
|
||||
const fitHash = await encodeEsiFit(shipSnapshot.fit);
|
||||
setFitLink(`https://eveship.fit/#fit:${fitHash}`);
|
||||
}
|
||||
doCreateLink();
|
||||
}, [shipSnapshot?.loaded, shipSnapshot?.fit]);
|
||||
|
||||
return fitLink;
|
||||
};
|
||||
|
||||
/**
|
||||
* useEveShipFitLink() converts the current fit into a link to https://eveship.fit.
|
||||
*
|
||||
* Note: do not use this React component itself, but the useEveShipFitLink() React hook instead.
|
||||
*/
|
||||
export const EveShipFitLink = () => {
|
||||
const eveShipFitLink = useEveShipFitLink();
|
||||
|
||||
return <pre>{eveShipFitLink}</pre>
|
||||
};
|
||||
1
src/EveShipFitLink/index.ts
Normal file
1
src/EveShipFitLink/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useEveShipFitLink } from "./EveShipFitLink";
|
||||
@@ -124,7 +124,9 @@ export interface FormatEftToEsiProps {
|
||||
}
|
||||
|
||||
/**
|
||||
* Use useFormatEftToEsi() instead of this component.
|
||||
* useFormatEftToEsi() converts an EFT string to an ESI JSON object.
|
||||
*
|
||||
* Note: do not use this React component itself, but the useFormatEftToEsi() React hook instead.
|
||||
*/
|
||||
export const FormatEftToEsi = (props: FormatEftToEsiProps) => {
|
||||
const esiFit = useFormatEftToEsi();
|
||||
|
||||
28
src/ShipFit/FitLink.tsx
Normal file
28
src/ShipFit/FitLink.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
import { useEveShipFitLink } from "../EveShipFitLink";
|
||||
|
||||
import styles from "./ShipFit.module.css";
|
||||
|
||||
export const FitLink = () => {
|
||||
const link = useEveShipFitLink();
|
||||
|
||||
/* Detect if the fit is loaded on https://eveship.fit */
|
||||
const isEveShipFit = window.location.hostname === "eveship.fit";
|
||||
const linkText = isEveShipFit ? "link to fit" : "open on eveship.fit";
|
||||
|
||||
return <div className={styles.fitlink}>
|
||||
<svg viewBox="0 0 730 730" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
id="fitlink"
|
||||
fill="none"
|
||||
d="M18,365 A25,25 0 0,1 712,365" />
|
||||
|
||||
<a href={link} target="_new">
|
||||
<text textAnchor="middle">
|
||||
<textPath startOffset="50%" href="#fitlink" fill="#cdcdcd">{linkText}</textPath>
|
||||
</text>
|
||||
</a>
|
||||
</svg>
|
||||
</div>
|
||||
}
|
||||
@@ -114,3 +114,12 @@
|
||||
.slotItem > img {
|
||||
border-top-left-radius: 32px;
|
||||
}
|
||||
|
||||
.fitlink {
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import React from "react";
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { ShipSnapshotContext } from '../ShipSnapshotProvider';
|
||||
|
||||
import { FitLink } from './FitLink';
|
||||
import { Hull } from './Hull';
|
||||
import { Slot } from './Slot';
|
||||
|
||||
@@ -54,6 +55,7 @@ export const ShipFit = (props: ShipFitProps) => {
|
||||
<div className={styles.innerBand} />
|
||||
|
||||
<Hull />
|
||||
<FitLink />
|
||||
|
||||
<div className={styles.slots}>
|
||||
<Slot type="subsystem" index={1} fittable={slots.subsystem >= 1} rotation="-125deg" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export * from './DogmaEngineProvider';
|
||||
export * from './EveDataProvider';
|
||||
export * from './EveShipFitHash';
|
||||
export * from './EveShipFitLink';
|
||||
export * from './FormatEftToEsi';
|
||||
export * from './Icon';
|
||||
export * from './ShipAttribute';
|
||||
|
||||
Reference in New Issue
Block a user