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:
Patric Stout
2023-11-24 21:07:41 +01:00
committed by GitHub
parent eebb2eba93
commit 36e2827404
12 changed files with 272 additions and 1 deletions

View File

@@ -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,

View 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],
};

View 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>
};

View File

@@ -0,0 +1 @@
export { eveShipFitHash } from "./EveShipFitHash";

View 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: {},
}
},
};

View 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>
};

View File

@@ -0,0 +1 @@
export { useEveShipFitLink } from "./EveShipFitLink";

View File

@@ -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
View 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>
}

View File

@@ -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;
}

View File

@@ -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" />

View File

@@ -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';