feat: import/export current fit as EFT string (#51)
Sadly, due to browser protection, we cannot just read the clipboard at will. So instead, you get a textarea where you have to paste your fit in.
This commit is contained in:
@@ -25,6 +25,10 @@
|
||||
height: 20px;
|
||||
line-height: 20px;
|
||||
}
|
||||
.buttonMax {
|
||||
text-align: center;
|
||||
width: calc(100% - 40px - 2px);
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background-color: #864735;
|
||||
@@ -67,21 +71,41 @@
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.popup > div > .button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
.popup > div > .button:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.renameEdit {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.alreadyExists {
|
||||
text-align: center;
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.alreadyExistsButtons {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
.alreadyExistsButtons .button {
|
||||
margin-right: 10px;
|
||||
width: 35%;
|
||||
text-align: center;
|
||||
width: calc(50% - 48px);
|
||||
}
|
||||
.alreadyExistsButtons .button:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.paste .button {
|
||||
text-align: center;
|
||||
width: calc(100% - 42px);
|
||||
}
|
||||
.pasteTextarea {
|
||||
height: 60px;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 4px;
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ const withEveDataProvider: Decorator<Record<string, never>> = (Story, context) =
|
||||
<DogmaEngineProvider>
|
||||
<LocalFitProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<div style={{marginTop: "50px"}}>
|
||||
<div style={{marginTop: "100px"}}>
|
||||
<ModalDialogAnchor />
|
||||
<Story />
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { ModalDialog } from "../ModalDialog";
|
||||
import { ShipSnapshotContext } from "../ShipSnapshotProvider";
|
||||
|
||||
import styles from "./FitButtonBar.module.css";
|
||||
import { useFormatAsEft } from "../FormatAsEft";
|
||||
import { useFormatEftToEsi } from "../FormatEftToEsi";
|
||||
|
||||
const SaveButton = () => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
@@ -66,6 +68,75 @@ const SaveButton = () => {
|
||||
</>
|
||||
}
|
||||
|
||||
const ClipboardButton = () => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
const toEft = useFormatAsEft();
|
||||
const eftToEsiFit = useFormatEftToEsi();
|
||||
|
||||
const [isPopupOpen, setIsPopupOpen] = React.useState(false);
|
||||
const [isPasteOpen, setIsPasteOpen] = React.useState(false);
|
||||
const textAreaRef = React.useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
const copyToClipboard = React.useCallback(() => {
|
||||
const eft = toEft();
|
||||
if (eft === undefined) return;
|
||||
|
||||
navigator.clipboard.writeText(eft);
|
||||
|
||||
setIsPopupOpen(false);
|
||||
}, [toEft]);
|
||||
|
||||
const importFromClipboard = React.useCallback(() => {
|
||||
if (!shipSnapshot.loaded) return;
|
||||
|
||||
const textArea = textAreaRef.current;
|
||||
if (textArea === null) return;
|
||||
|
||||
const eft = textArea.value;
|
||||
if (eft === "") return;
|
||||
|
||||
const fit = eftToEsiFit(eft);
|
||||
if (fit === undefined) return;
|
||||
|
||||
shipSnapshot.changeFit(fit);
|
||||
|
||||
setIsPasteOpen(false);
|
||||
setIsPopupOpen(false);
|
||||
}, [eftToEsiFit, shipSnapshot]);
|
||||
|
||||
return <>
|
||||
<div className={styles.popupButton} onMouseOver={() => setIsPopupOpen(true)} onMouseOut={() => setIsPopupOpen(false)}>
|
||||
<div className={styles.button}>
|
||||
Clipboard
|
||||
</div>
|
||||
<div className={clsx(styles.popup, {[styles.collapsed]: !isPopupOpen})}>
|
||||
<div>
|
||||
<div className={styles.button} onClick={() => setIsPasteOpen(true)}>
|
||||
Import from Clipboard
|
||||
</div>
|
||||
<div className={clsx(styles.button, styles.buttonMax)} onClick={() => copyToClipboard()}>
|
||||
Copy to Clipboard
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ModalDialog visible={isPasteOpen} onClose={() => setIsPasteOpen(false)} className={styles.paste} title="Import from Clipboard">
|
||||
<div>
|
||||
<div>
|
||||
Paste the EFT fit here
|
||||
</div>
|
||||
<div>
|
||||
<textarea autoFocus className={styles.pasteTextarea} ref={textAreaRef} />
|
||||
</div>
|
||||
<span className={clsx(styles.button, styles.buttonSmall)} onClick={() => importFromClipboard()}>
|
||||
Import
|
||||
</span>
|
||||
</div>
|
||||
</ModalDialog>
|
||||
</>
|
||||
}
|
||||
|
||||
const RenameButton = () => {
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
@@ -106,6 +177,7 @@ const RenameButton = () => {
|
||||
export const FitButtonBar = () => {
|
||||
return <div className={styles.fitButtonBar}>
|
||||
<SaveButton />
|
||||
<ClipboardButton />
|
||||
<RenameButton />
|
||||
</div>
|
||||
};
|
||||
|
||||
40
src/FormatAsEft/FormatAsEft.stories.tsx
Normal file
40
src/FormatAsEft/FormatAsEft.stories.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import type { Decorator, Meta, StoryObj } from '@storybook/react';
|
||||
import React from "react";
|
||||
|
||||
import { fullFit } from '../../.storybook/fits';
|
||||
|
||||
import { EveDataProvider } from '../EveDataProvider';
|
||||
import { FormatAsEft } from './FormatAsEft';
|
||||
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
|
||||
import { DogmaEngineProvider } from '../DogmaEngineProvider';
|
||||
|
||||
const meta: Meta<typeof FormatAsEft> = {
|
||||
component: FormatAsEft,
|
||||
tags: ['autodocs'],
|
||||
title: 'Function/FormatAsEft',
|
||||
};
|
||||
|
||||
const withEveDataProvider: Decorator<Record<string, never>> = (Story, context) => {
|
||||
return (
|
||||
<EveDataProvider>
|
||||
<DogmaEngineProvider>
|
||||
<ShipSnapshotProvider {...context.parameters.snapshot}>
|
||||
<Story />
|
||||
</ShipSnapshotProvider>
|
||||
</DogmaEngineProvider>
|
||||
</EveDataProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FormatAsEft>;
|
||||
|
||||
export const Default: Story = {
|
||||
decorators: [withEveDataProvider],
|
||||
parameters: {
|
||||
snapshot: {
|
||||
fit: fullFit,
|
||||
skills: {},
|
||||
}
|
||||
},
|
||||
};
|
||||
90
src/FormatAsEft/FormatAsEft.tsx
Normal file
90
src/FormatAsEft/FormatAsEft.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import React from "react";
|
||||
|
||||
import { EveDataContext } from '../EveDataProvider';
|
||||
import { ShipSnapshotContext, ShipSnapshotSlotsType } from "../ShipSnapshotProvider";
|
||||
|
||||
/** Mapping between slot types and ESI flags (for first slot in the type). */
|
||||
const esiFlagMapping: Record<ShipSnapshotSlotsType, number[]> = {
|
||||
"lowslot": [
|
||||
11, 12, 13, 14, 15, 16, 17, 18
|
||||
],
|
||||
"medslot": [
|
||||
19, 20, 21, 22, 23, 24, 25, 26
|
||||
],
|
||||
"hislot": [
|
||||
27, 28, 29, 30, 31, 32, 33, 34
|
||||
],
|
||||
"rig": [
|
||||
92, 93, 94
|
||||
],
|
||||
"subsystem": [
|
||||
125, 126, 127, 128
|
||||
],
|
||||
};
|
||||
|
||||
/** Mapping between slot-type and the EFT string name. */
|
||||
const slotToEft: Record<ShipSnapshotSlotsType, string> = {
|
||||
"lowslot": "Low Slot",
|
||||
"medslot": "Mid Slot",
|
||||
"hislot": "High Slot",
|
||||
"rig": "Rig Slot",
|
||||
"subsystem": "Subsystem Slot",
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert current fit to an EFT string.
|
||||
*/
|
||||
export function useFormatAsEft() {
|
||||
const eveData = React.useContext(EveDataContext);
|
||||
const shipSnapshot = React.useContext(ShipSnapshotContext);
|
||||
|
||||
return (): string | undefined => {
|
||||
if (!eveData?.loaded) return undefined;
|
||||
if (!shipSnapshot?.loaded || shipSnapshot.fit == undefined) return undefined;
|
||||
|
||||
let eft = "";
|
||||
|
||||
const shipType = eveData.typeIDs?.[shipSnapshot.fit.ship_type_id];
|
||||
if (!shipType) return undefined;
|
||||
|
||||
eft += `[${shipType.name}, ${shipSnapshot.fit.name}]\n`;
|
||||
|
||||
for (const slotType of Object.keys(esiFlagMapping) as ShipSnapshotSlotsType[]) {
|
||||
let index = 1;
|
||||
|
||||
for (const flag of esiFlagMapping[slotType]) {
|
||||
if (index > shipSnapshot.slots[slotType]) break;
|
||||
index += 1;
|
||||
|
||||
const module = shipSnapshot.fit.items.find((item) => item.flag === flag);
|
||||
if (module === undefined) {
|
||||
eft += "[Empty " + slotToEft[slotType] + "]\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
const moduleType = eveData.typeIDs?.[module.type_id];
|
||||
if (moduleType === undefined) {
|
||||
eft += "[Empty " + slotToEft[slotType] + "]\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
eft += `${moduleType.name}\n`;
|
||||
}
|
||||
|
||||
eft += "\n";
|
||||
}
|
||||
|
||||
return eft;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* useFormatAsEft() converts the current fit to an EFT string.
|
||||
*
|
||||
* Note: do not use this React component itself, but the useFormatAsEft() React hook instead.
|
||||
*/
|
||||
export const FormatAsEft = () => {
|
||||
const toEft = useFormatAsEft();
|
||||
|
||||
return <pre>{toEft()}</pre>
|
||||
};
|
||||
1
src/FormatAsEft/index.ts
Normal file
1
src/FormatAsEft/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { useFormatAsEft } from "./FormatAsEft";
|
||||
@@ -7,6 +7,7 @@ export * from './EveShipFitHash';
|
||||
export * from './EveShipFitLink';
|
||||
export * from './FitButtonBar';
|
||||
export * from './FormatEftToEsi';
|
||||
export * from './FormatAsEft';
|
||||
export * from './HardwareListing';
|
||||
export * from './HullListing';
|
||||
export * from './Icon';
|
||||
|
||||
Reference in New Issue
Block a user