1
0
Fork 0

Merge branch 'main' into sup39

This commit is contained in:
sup39 2022-10-21 13:50:35 +09:00
commit 964994eef9
14 changed files with 355 additions and 39 deletions

View file

@ -297,9 +297,9 @@ module.exports = function (webpackEnv) {
],
},
resolve: {
// fallback: {
// buffer: require.resolve("buffer/")
// },
fallback: {
buffer: require.resolve("buffer/")
},
// This allows you to set a fallback for where webpack should look for modules.
// We placed these paths second because we want `node_modules` to "win"
// if there are any conflicts. This matches Node resolution mechanism.
@ -579,6 +579,11 @@ module.exports = function (webpackEnv) {
].filter(Boolean),
},
plugins: [
// Work around for Buffer is undefined:
// https://github.com/webpack/changelog-v5/issues/10
new webpack.ProvidePlugin({
Buffer: ['buffer', 'Buffer'],
}),
// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(

97
package-lock.json generated
View file

@ -1,13 +1,12 @@
{
"name": "botw-hundo-dupl",
"version": "2.1.3",
"version": "2.1.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "botw-hundo-dupl",
"version": "2.1.3",
"license": "MIT",
"version": "2.1.5",
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@ -44,12 +43,14 @@
"jest-resolve": "^27.4.2",
"jest-watch-typeahead": "^1.0.0",
"mini-css-extract-plugin": "^2.4.5",
"pako": "^2.0.4",
"postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1",
"postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2",
"query-string": "^7.1.1",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
@ -73,6 +74,7 @@
"yaml-loader": "^0.8.0"
},
"devDependencies": {
"@types/pako": "^2.0.0",
"webpack-bundle-analyzer": "^4.5.0"
}
},
@ -3828,6 +3830,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz",
"integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ=="
},
"node_modules/@types/pako": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz",
"integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -7653,6 +7661,14 @@
"node": ">=8"
}
},
"node_modules/filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@ -11963,6 +11979,11 @@
"node": ">=6"
}
},
"node_modules/pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"node_modules/param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@ -13456,6 +13477,23 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/query-string": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
"integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
"dependencies": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -14581,6 +14619,14 @@
"wbuf": "^1.7.3"
}
},
"node_modules/split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==",
"engines": {
"node": ">=6"
}
},
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -14623,6 +14669,14 @@
"node": ">= 0.8"
}
},
"node_modules/strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==",
"engines": {
"node": ">=4"
}
},
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@ -19049,6 +19103,12 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.41.tgz",
"integrity": "sha512-mqoYK2TnVjdkGk8qXAVGc/x9nSaTpSrFaGFm43BUH3IdoBV0nta6hYaGmdOvIMlbHJbUEVen3gvwpwovAZKNdQ=="
},
"@types/pako": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.0.tgz",
"integrity": "sha512-10+iaz93qR5WYxTo+PMifD5TSxiOtdRaxBf7INGGXMQgTCu8Z/7GYWYFUOS3q/G0nE5boj1r4FEB+WSy7s5gbA==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
@ -21851,6 +21911,11 @@
"to-regex-range": "^5.0.1"
}
},
"filter-obj": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz",
"integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ=="
},
"finalhandler": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
@ -24930,6 +24995,11 @@
"resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz",
"integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="
},
"pako": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/pako/-/pako-2.0.4.tgz",
"integrity": "sha512-v8tweI900AUkZN6heMU/4Uy4cXRc2AYNRggVmTR+dEncawDJgCdLMximOVA2p4qO57WMynangsfGRb5WD6L1Bg=="
},
"param-case": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
@ -25848,6 +25918,17 @@
"side-channel": "^1.0.4"
}
},
"query-string": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.1.tgz",
"integrity": "sha512-MplouLRDHBZSG9z7fpuAAcI7aAYjDLhtsiVZsevsfaHWDS2IDdORKbSd1kWUA+V4zyva/HZoSfpwnYMMQDhb0w==",
"requires": {
"decode-uri-component": "^0.2.0",
"filter-obj": "^1.1.0",
"split-on-first": "^1.0.0",
"strict-uri-encode": "^2.0.0"
}
},
"queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@ -26680,6 +26761,11 @@
"wbuf": "^1.7.3"
}
},
"split-on-first": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz",
"integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw=="
},
"sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@ -26715,6 +26801,11 @@
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
"integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="
},
"strict-uri-encode": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
"integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ=="
},
"string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",

View file

@ -1,8 +1,8 @@
{
"name": "botw-hundo-dupl",
"version": "2.1.3",
"version": "2.1.5",
"homepage": "https://ist.botw.sup39.dev/",
"license": "MIT",
"private": true,
"dependencies": {
"@babel/core": "^7.16.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.3",
@ -39,12 +39,14 @@
"jest-resolve": "^27.4.2",
"jest-watch-typeahead": "^1.0.0",
"mini-css-extract-plugin": "^2.4.5",
"pako": "^2.0.4",
"postcss": "^8.4.4",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^6.2.1",
"postcss-normalize": "^10.0.1",
"postcss-preset-env": "^7.0.1",
"prompts": "^2.4.2",
"query-string": "^7.1.1",
"react": "^18.2.0",
"react-app-polyfill": "^3.0.0",
"react-dev-utils": "^12.0.1",
@ -152,6 +154,7 @@
]
},
"devDependencies": {
"@types/pako": "^2.0.0",
"webpack-bundle-analyzer": "^4.5.0"
}
}

View file

@ -14,7 +14,7 @@
<meta property="og:url" content="https://ist.botw.sup39.dev/#/">
<meta property="og:description" content="for Breath of the Wild">
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>IST Sim</title>
<title>IST Simulator</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View file

@ -176,6 +176,13 @@ button.MainButton:active {
background-color: #888888;
}
button.MainButton:disabled{
color: #888888;
background-color: #333333;
border-color: #888888;
box-shadow: none;
}
.FullWidth {
width: 100% !important;
}
@ -188,6 +195,7 @@ div.OtherPage {
div.OtherPageContent{
padding: 10px;
overflow-wrap: break-word;
}
.MainInput {

View file

@ -1,7 +1,12 @@
import { PropsWithChildren } from "react";
type Props = {
multiLine?: boolean,
hasError?: boolean,
}
// an over-engineered loading screen
export const LoadingScreen: React.FC<PropsWithChildren> = ({children})=>{
export const LoadingScreen: React.FC<PropsWithChildren<Props>> = ({multiLine, hasError, children})=>{
return (
<div style={{
textAlign: "center",
@ -13,9 +18,9 @@ export const LoadingScreen: React.FC<PropsWithChildren> = ({children})=>{
backgroundColor: "#262626"
}}>
<span style={{
color: "#00ffcc",
color: hasError? "#ee7777":"#00ffcc",
lineHeight: "100vh",
lineHeight: multiLine?"default":"100vh",
height: "100vh",
}}>
{children}

View file

@ -2,7 +2,7 @@ import { ItemStack, ItemType, createMaterialStack, createEquipmentStack } from "
import { Slots } from "./Slots";
import { createArrowMockItem, createEquipmentMockItem, createFoodMockItem, createKeyMockItem, createMaterialMockItem, equalsExceptEquip } from "./SlotsTestHelpers";
describe("Slots.add", ()=>{
describe("core/Slots.add", ()=>{
describe("sorted", ()=>{
describe("reloading = true", ()=>{
it("should add new stack when empty", ()=>{

View file

@ -2,7 +2,7 @@ import { createEquipmentStack, createMaterialStack, ItemStack, ItemType } from "
import { Slots } from "./Slots";
import { createEquipmentMockItem, createMaterialMockItem } from "./SlotsTestHelpers";
describe("Slots.remove", ()=>{
describe("core/Slots.remove", ()=>{
it("Does nothing if item doesn't exist", ()=>{
const mockItem1 = createMaterialMockItem("MaterialA");
const stackToRemove = createMaterialStack(mockItem1, 1);

View file

@ -2,7 +2,7 @@ import { createEquipmentStack, createMaterialStack, ItemStack, ItemType } from "
import { Slots } from "./Slots";
import { createArrowMockItem, createEquipmentMockItem, createFoodMockItem, createKeyMockItemStackable, createMaterialMockItem } from "./SlotsTestHelpers";
describe.only("Slots.updateLife", ()=>{
describe.only("core/Slots.updateLife", ()=>{
it("should update life", ()=>{
const mockItem1 = createMaterialMockItem("MaterialA");
const slot = createMaterialStack(mockItem1, 1);

View file

@ -0,0 +1,33 @@
import { compressString, decompressString } from "./serialize";
const runCompressDecompressTest = (input: string) => {
const compressed = compressString(input);
expect(compressed).not.toEqual(input);
expect(decompressString(compressed)).toEqual(input);
};
describe.only("data/serialize.compress", ()=>{
it("Should compress and decompress empty string", ()=>{
runCompressDecompressTest("");
});
it("Should compress and decompress one character", ()=>{
runCompressDecompressTest("a");
});
it("Should compress and decompress single command", ()=>{
const input = "Break 5 Slots";
runCompressDecompressTest(input);
});
it("Should compress and decompress large command", ()=>{
const input = "Initialize 1 Tree Branch[equip] 1 Hammer 1 Travel Bow[Equip] 3 NormalArrow[Equip] 1 potlid 1 potlid[equip] 1 Fairy 1 SpeedFood 3 EnduraFood 1 Slate 9 SpiritOrb 1 Glider";
runCompressDecompressTest(input);
});
it("Should compress and decompress multiple commands", ()=>{
const inputArray = [
"Break 5 Slots",
"Save",
"Eat 3 EnduraFood",
"Eat SpeedFood"
];
runCompressDecompressTest(inputArray.join("\n"));
});
});

40
src/data/serialize.ts Normal file
View file

@ -0,0 +1,40 @@
import { gzip, ungzip } from "pako";
const ZLIB_OPTIONS = {
level: 9
} as const;
type SerializedCommands = { r: string } | { c: string }; // r for raw and c for compressed
export const serialize = (commandsString: string): SerializedCommands => {
const compressed = compressString(commandsString);
if (commandsString.length < compressed.length){
return { r: commandsString };
}
return { c: compressed };
};
export const deserialize = (serializedCommands: Partial<SerializedCommands>): string | null => {
if ( "r" in serializedCommands && serializedCommands.r){
return serializedCommands.r;
}
if ( "c" in serializedCommands && serializedCommands.c){
return decompressString(serializedCommands.c);
}
return null;
};
export const compressString = (uncompressedString: string): string => {
const uncompressedBytes = Buffer.from(uncompressedString, "utf8");
const compressedBytes = gzip(uncompressedBytes, ZLIB_OPTIONS);
return Buffer.from(compressedBytes).toString("base64");
};
export const decompressString = (decompressedString: string): string => {
const compressedBytes = Buffer.from(decompressedString, "base64");
const uncompressedBytes = ungzip(compressedBytes, {
to: "string",
...ZLIB_OPTIONS
});
return Buffer.from(uncompressedBytes).toString("utf8");
};

View file

@ -5,6 +5,7 @@ import {App} from "./App";
import reportWebVitals from "./reportWebVitals";
import { LanguageProvider } from "data/i18n";
import { ItemProvider } from "data/item";
import { DirectLoadPage } from "surfaces/DirectLoadPage";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement
@ -12,12 +13,12 @@ const root = ReactDOM.createRoot(
root.render(
<React.StrictMode>
<LanguageProvider>
<DirectLoadPage>
<ItemProvider>
<App />
</ItemProvider>
</DirectLoadPage>
</LanguageProvider>
</React.StrictMode>
);

View file

@ -0,0 +1,72 @@
import { PropsWithChildren, useMemo } from "react";
import { parse } from "query-string";
import { deserialize } from "data/serialize";
import { LoadingScreen } from "components/LoadingScreen";
import { BodyText, Header, SubHeader } from "components/Text";
const redirectToMainApp = ()=>{
window.location.href = window.location.origin;
};
export const DirectLoadPage: React.FC<PropsWithChildren> = ({children}) => {
const query = parse(window.location.search);
const [commandTextToLoad, errorText] = useMemo(()=>{
try {
return [deserialize(query), false];
} catch (e) {
console.error(e);
return [null, "Fail to deserialize. The URL may be corrupted."];
}
}, [query]);
if(errorText){
return (
<LoadingScreen hasError multiLine>
<Header>Error loading direct URL</Header>
<SubHeader>{errorText}</SubHeader>
<BodyText>The browser console may have useful information for debugging</BodyText>
<BodyText emphasized>Press Continue to load existing data in the simulator instead</BodyText>
<button className="MainButton" onClick={()=>{
redirectToMainApp();
}}>Continue</button>
</LoadingScreen>
);
}
if (!commandTextToLoad){
return <>{children}</>;
}
if (!localStorage.getItem("HDS.CurrentCommandsText")){
// If no data is in simulator (i.e. first time use), load data without warning
localStorage.setItem("HDS.CurrentCommandsText", commandTextToLoad);
redirectToMainApp();
}
return <LoadingScreen multiLine>
<div className="OtherPageContent">
<Header>Open Direct URL?</Header>
<SubHeader>You are trying to open a direct URL. This will automatically load data into the simulator.</SubHeader>
<BodyText emphasized>This will override existing data and cannot be reversed</BodyText>
<button className="MainButton" onClick={()=>{
localStorage.setItem("HDS.CurrentCommandsText", commandTextToLoad);
redirectToMainApp();
}}>Yes</button>
<button className="MainButton" onClick={()=>{
redirectToMainApp();
}}>No</button>
<div style={{marginTop: "50px", marginLeft: "10%", marginRight: "10%"}}>
<BodyText>(Below is what the incoming data looks like)</BodyText>
<textarea
className="MainInput"
spellCheck={false}
value={commandTextToLoad}
/>
</div>
</div>
</LoadingScreen>;
};

View file

@ -1,8 +1,12 @@
import { BodyText, SubHeader, SubTitle } from "components/Text";
import { TitledList } from "components/TitledList";
import { saveAs } from "data/FileSaver";
import { useRef, useState } from "react";
import { serialize } from "data/serialize";
import { useEffect, useMemo, useRef, useState } from "react";
import license from "./License";
const URL_MAX = 2048;
type OptionPageProps = {
interlaceInventory: boolean,
setInterlaceInventory: (value: boolean)=>void,
@ -22,8 +26,23 @@ export const OptionPage: React.FC<OptionPageProps> = ({
}) => {
const [currentText, setCurrentText] = useState<string>(commandText);
const [fileName, setFileName] = useState<string>("");
const [showDirectUrl, setShowDirectUrl] = useState<boolean>(false);
const [showCopiedMessage, setShowCopiedMessage] = useState<boolean>(false);
const uploadRef = useRef<HTMLInputElement>(null);
const directUrl = useMemo(()=>{
const serializedCommands = serialize(commandText);
const query = new URLSearchParams(serializedCommands).toString();
return `${window.location.origin}/?${query}`;
}, [commandText]);
const directUrlLength = directUrl.length;
useEffect(()=>{
setShowCopiedMessage(false);
}, [currentText]);
return (
<div className="OtherPage">
@ -42,36 +61,29 @@ export const OptionPage: React.FC<OptionPageProps> = ({
<TitledList title="Options">
<div className="OtherPageContent">
<h3 className="Reference">
<SubHeader>
Interlace Inventory with GameData
<button className="MainButton" onClick={()=>{
setInterlaceInventory(!interlaceInventory);
}}>
{interlaceInventory ? "ON" : "OFF"}
</button>
</h3>
<h4 className="Reference">
Toggle whether Visible Inventory should be displayed separetely from Game Data or interlaced.
</h4>
</SubHeader>
<SubTitle>Toggle whether Visible Inventory should be displayed separetely from Game Data or interlaced.</SubTitle>
<h3 className="Reference">
<SubHeader>
Enable Animated Item Icons
<button className="MainButton" onClick={()=>{
setIsIconAnimated(!isIconAnimated);
}}>
{isIconAnimated ? "ON" : "OFF"}
</button>
</h3>
<h4 className="Reference">
Toggle whether items such as the champion abilities or Travel Medallion use animated or still icons.
</h4>
</SubHeader>
<SubTitle>Toggle whether items such as the champion abilities or Travel Medallion use animated or still icons.</SubTitle>
<h3 className="Reference">Import / Export</h3>
<h4 className="Reference">
You can also directly copy, paste, or edit the commands here
</h4>
<p className="Reference">
<SubHeader>Text Import / Export</SubHeader>
<SubTitle>You can also directly copy, paste, or edit the commands here</SubTitle>
<BodyText>
<button className="MainButton" onClick={()=>{
if(uploadRef.current){
uploadRef.current.click();
@ -113,9 +125,55 @@ export const OptionPage: React.FC<OptionPageProps> = ({
<span className="Example">Don't forget to save changes</span>
</>
}
</BodyText>
<SubHeader>Direct URL</SubHeader>
<SubTitle>Use this to open the simulator with the steps automatically loaded.</SubTitle>
<div>
{
currentText !== commandText ?
<BodyText emphasized>
You must save the changes above to access the updated URL
</BodyText>
:
<>
{
directUrlLength > URL_MAX && <BodyText emphasized>
Warning: The URL is too long ({directUrlLength} characters) and may not work in certain browsers. Export as file instead if you encounter any problems.
</BodyText>
}
<p className="Reference" style={{
fontSize: "10pt",
color: "#aaaaaa",
...!showDirectUrl && {
textOverflow: "ellipsis",
overflowX: "hidden",
whiteSpace: "nowrap",
}
}}>
{directUrl}
</p>
<BodyText>
<button className="MainButton" onClick={()=>{
setShowDirectUrl(!showDirectUrl);
}}>{showDirectUrl ? "Hide" : "Expand"}</button>
<button className="MainButton" disabled={currentText !== commandText} onClick={()=>{
window.navigator.clipboard.writeText(directUrl);
setShowCopiedMessage(true);
}}>
Copy
</button>
{
showCopiedMessage && <span className="Example">Link copied!</span>
}
</BodyText>
</>
}
</div>
<h3 className="Reference">Credits</h3>
<p className="Reference">
This app is a fork of <a href="https://github.com/iTNTPiston">iTNTPiston</a>'s <a href="https://dupl.itntpiston.app/">dupl.itntpiston.app</a>, and is modified by <a href="https://github.com/sup39">sup39</a>. This app is released under MIT license: