WIP
This commit is contained in:
@@ -331,6 +331,10 @@ class Board {
|
||||
}
|
||||
|
||||
toImageData() {
|
||||
return this.ctx.getImageData(0, 0, this.width, this.height);
|
||||
}
|
||||
|
||||
toClampedArray() {
|
||||
return this.ctx.getImageData(0, 0, this.width, this.height).data;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Coords } from "../../types";
|
||||
|
||||
const BASE_FONT_SIZE = 20;
|
||||
const BASE_FONT_SIZE = 24;
|
||||
const FONT_FAMILY = "Fira Mono";
|
||||
|
||||
const drawCoords = (
|
||||
|
||||
@@ -8,9 +8,9 @@ import MP4 from "./MP4";
|
||||
|
||||
const getData = (board: Board, encoder: GIF | WebM | MP4) => {
|
||||
return encoder instanceof GIF
|
||||
? board.toImgElement()
|
||||
: encoder instanceof MP4
|
||||
? board.toImageData()
|
||||
: encoder instanceof MP4
|
||||
? board.toClampedArray()
|
||||
: board.canvas;
|
||||
};
|
||||
|
||||
@@ -18,7 +18,8 @@ const createAnimation = async (
|
||||
pgn: string,
|
||||
boardConfig: BoardConfig,
|
||||
format: "GIF" | "WebM" | "MP4",
|
||||
size: Size
|
||||
size: Size,
|
||||
includeTitleScreen: boolean
|
||||
) => {
|
||||
const game = new Game().loadPGN(pgn);
|
||||
const board = new Board({ ...boardConfig, size: sizeToPX[size] });
|
||||
@@ -31,11 +32,13 @@ const createAnimation = async (
|
||||
|
||||
const header = game.header;
|
||||
|
||||
await board.titleFrame(header);
|
||||
board.render();
|
||||
if (includeTitleScreen) {
|
||||
await board.titleFrame(header);
|
||||
board.render();
|
||||
|
||||
// @ts-ignore
|
||||
await encoder.add(getData(board, encoder), 4);
|
||||
// @ts-ignore
|
||||
await encoder.add(getData(board, encoder), 4);
|
||||
}
|
||||
|
||||
for (let ply = 0; ply < game.length; ply++) {
|
||||
const position = game.getPosition(ply);
|
||||
|
||||
@@ -179,6 +179,19 @@ class Game {
|
||||
};
|
||||
}
|
||||
|
||||
getTitle({ anonymous }: { anonymous: boolean }) {
|
||||
const header = this.header;
|
||||
const w = anonymous ? "Anonymous" : header.WhitePretty;
|
||||
const b = anonymous ? "Anonymous" : header.BlackPretty;
|
||||
|
||||
return (
|
||||
`${w} vs ${b}` +
|
||||
(header.Event ? ` | ${header.Event}` : "") +
|
||||
(header.Round ? `, Round ${header.Round}` : "") +
|
||||
(header.DatePretty ? ` | ${header.DatePretty}` : "")
|
||||
);
|
||||
}
|
||||
|
||||
get pgn() {
|
||||
return this.game.pgn();
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ type Result =
|
||||
| { error: false; pgn: string; side: "w" | "b" }
|
||||
| { error: true; errorType: "INCORRECT_LINK" | "SERVER_ERROR" };
|
||||
|
||||
const importFromLichess = async (link: string): Promise<Result> => {
|
||||
const [first, second] = link
|
||||
.replace(/^https:\/\/(www\.)*lichess\.org\/*/, "")
|
||||
const importFromLichess = async (url: URL): Promise<Result> => {
|
||||
const [first, second] = url.pathname
|
||||
.replace(/^\//, "")
|
||||
.split("/")
|
||||
.map((x) => x.trim());
|
||||
|
||||
@@ -24,7 +24,10 @@ const importFromLichess = async (link: string): Promise<Result> => {
|
||||
return {
|
||||
error: false,
|
||||
pgn,
|
||||
side: String(second).startsWith("black") ? "b" : "w",
|
||||
side:
|
||||
String(second).startsWith("black") || url.hash.startsWith("black")
|
||||
? "b"
|
||||
: "w",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,8 +35,16 @@ const importFromLichess = async (link: string): Promise<Result> => {
|
||||
};
|
||||
|
||||
const importFromLink = async (link: string): Promise<Result> => {
|
||||
if (/^https:\/\/(www\.)*lichess\.org/.test(link)) {
|
||||
return importFromLichess(link);
|
||||
let url;
|
||||
|
||||
try {
|
||||
url = new URL(link);
|
||||
} catch {
|
||||
return { error: true, errorType: "INCORRECT_LINK" };
|
||||
}
|
||||
|
||||
if (/^(www\.)*lichess\.org/.test(url.hostname)) {
|
||||
return importFromLichess(url);
|
||||
}
|
||||
|
||||
return { error: true, errorType: "INCORRECT_LINK" };
|
||||
|
||||
103
src/main.tsx
103
src/main.tsx
@@ -103,11 +103,15 @@ const main = async () => {
|
||||
|
||||
await player.load(game);
|
||||
setState("activeTab", "game");
|
||||
document.title = `SHORTCASTLE - ${game.getTitle({ anonymous: false })}`;
|
||||
},
|
||||
async loadFEN(fen: string) {
|
||||
const game = new Game().loadFEN(fen);
|
||||
setState({ pgn: "", fen, moves: game.getMoves(), ply: 0, game });
|
||||
window.location.hash = `v1/fen/${state.fen}`;
|
||||
await player.load(game);
|
||||
|
||||
document.title = `SHORTCASTLE - FEN ${fen}`;
|
||||
},
|
||||
async importPGN(link: string) {
|
||||
const result = await importFromLink(link);
|
||||
@@ -135,7 +139,8 @@ const main = async () => {
|
||||
state.pgn,
|
||||
state.boardConfig,
|
||||
state.gameConfig.format,
|
||||
state.gameConfig.animationSize
|
||||
state.gameConfig.animationSize,
|
||||
state.gameConfig.titleScreen
|
||||
);
|
||||
download(data, "game", state.gameConfig.format.toLowerCase());
|
||||
},
|
||||
@@ -165,59 +170,61 @@ const main = async () => {
|
||||
|
||||
/* Register events */
|
||||
|
||||
const keyMapping: { [key: string]: () => void } = {
|
||||
ArrowLeft: handlers.prev,
|
||||
ArrowRight: handlers.next,
|
||||
ArrowUp: handlers.first,
|
||||
ArrowDown: handlers.last,
|
||||
" ": handlers.togglePlay,
|
||||
b: handlers.toggleBorder,
|
||||
f: handlers.flip,
|
||||
e: handlers.toggleExtraInfo,
|
||||
};
|
||||
if (!state.mobile) {
|
||||
const keyMapping: { [key: string]: () => void } = {
|
||||
ArrowLeft: handlers.prev,
|
||||
ArrowRight: handlers.next,
|
||||
ArrowUp: handlers.first,
|
||||
ArrowDown: handlers.last,
|
||||
" ": handlers.togglePlay,
|
||||
b: handlers.toggleBorder,
|
||||
f: handlers.flip,
|
||||
e: handlers.toggleExtraInfo,
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
document.addEventListener("keydown", (e) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
|
||||
if (
|
||||
keyMapping[e.key] &&
|
||||
target?.nodeName !== "INPUT" &&
|
||||
target?.nodeName !== "TEXTAREA"
|
||||
) {
|
||||
keyMapping[e.key]();
|
||||
}
|
||||
});
|
||||
if (
|
||||
keyMapping[e.key] &&
|
||||
target?.nodeName !== "INPUT" &&
|
||||
target?.nodeName !== "TEXTAREA"
|
||||
) {
|
||||
keyMapping[e.key]();
|
||||
}
|
||||
});
|
||||
|
||||
const preventDefaults = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
const preventDefaults = (e: Event) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||
document.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
["dragenter", "dragover", "dragleave", "drop"].forEach((eventName) => {
|
||||
document.addEventListener(eventName, preventDefaults, false);
|
||||
});
|
||||
|
||||
document.addEventListener("drop", async (e) => {
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
const content = await readFile(e.dataTransfer.files[0]);
|
||||
handlers.loadPGN(content);
|
||||
}
|
||||
});
|
||||
document.addEventListener("drop", async (e) => {
|
||||
if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) {
|
||||
const content = await readFile(e.dataTransfer.files[0]);
|
||||
handlers.loadPGN(content);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const hammer = new Hammer.Manager(board.canvas);
|
||||
hammer.add(new Hammer.Swipe());
|
||||
hammer.add(new Hammer.Pinch());
|
||||
hammer.add(new Hammer.Press({ time: 500 }));
|
||||
hammer.add(new Hammer.Tap({ taps: 1 }));
|
||||
|
||||
const hammer = new Hammer.Manager(board.canvas);
|
||||
hammer.add(new Hammer.Swipe());
|
||||
hammer.add(new Hammer.Pinch());
|
||||
hammer.add(new Hammer.Press({ time: 500 }));
|
||||
hammer.add(new Hammer.Tap({ taps: 1 }));
|
||||
|
||||
hammer.on("swiperight", handlers.next);
|
||||
hammer.on("swipeleft", handlers.prev);
|
||||
hammer.on("swipeup", handlers.first);
|
||||
hammer.on("swipedown", handlers.last);
|
||||
hammer.on("pinchin", handlers.showBorder);
|
||||
hammer.on("pinchout", handlers.hideBorder);
|
||||
hammer.on("tap", handlers.next);
|
||||
hammer.on("press", handlers.flip);
|
||||
hammer.on("swiperight", handlers.next);
|
||||
hammer.on("swipeleft", handlers.prev);
|
||||
hammer.on("swipeup", handlers.first);
|
||||
hammer.on("swipedown", handlers.last);
|
||||
hammer.on("pinchin", handlers.showBorder);
|
||||
hammer.on("pinchout", handlers.hideBorder);
|
||||
hammer.on("tap", handlers.next);
|
||||
hammer.on("press", handlers.flip);
|
||||
}
|
||||
};
|
||||
|
||||
/* Boot */
|
||||
|
||||
@@ -3,12 +3,14 @@ import { createStore } from "solid-js/store";
|
||||
import Game from "./game/Game";
|
||||
import { BoardConfig, GameConfig } from "./types";
|
||||
|
||||
const mobile = isMobile();
|
||||
|
||||
const boardConfig: BoardConfig = {
|
||||
size: 1024,
|
||||
tiles: 8,
|
||||
boardStyle: "calm",
|
||||
piecesStyle: "tatiana",
|
||||
showBorder: true,
|
||||
showBorder: !mobile,
|
||||
showExtraInfo: true,
|
||||
showMaterial: true,
|
||||
showMoveIndicator: true,
|
||||
@@ -48,7 +50,7 @@ const initialState: State = {
|
||||
fen: "",
|
||||
moves: [],
|
||||
ply: 0,
|
||||
mobile: isMobile(),
|
||||
mobile,
|
||||
activeTab: "load",
|
||||
};
|
||||
|
||||
|
||||
@@ -26,16 +26,17 @@ body {
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: #232831;
|
||||
background-color: #313742;
|
||||
background-image: url(/img/pattern.png);
|
||||
color: #ddd;
|
||||
background-size: 12rem;
|
||||
color: rgb(212, 221, 224);
|
||||
--logo-url: url(/img/logo.svg);
|
||||
|
||||
--color-btn: rgb(0, 173, 136);
|
||||
--color-btn-light: rgb(0, 207, 162);
|
||||
--color-tab: #899399;
|
||||
--color-tab-light: #a9b4bd;
|
||||
--color-bg-block: #0e0e13;
|
||||
--color-bg-block: #17171f;
|
||||
--color-bg-input: #20242a;
|
||||
--color-border-input: #2d323a;
|
||||
--color-highlight: #ffffff22;
|
||||
@@ -43,26 +44,30 @@ body {
|
||||
--color-text-contrast: #0e0e13;
|
||||
--color-text-input: #acbddb;
|
||||
--color-text-dimmed: #677794;
|
||||
--color-scrollbar: rgb(0, 59, 47);
|
||||
--color-scrollbar-track: #ffffff22;
|
||||
}
|
||||
|
||||
.light {
|
||||
background-color: #c1ced4;
|
||||
background-color: #b2bcc0;
|
||||
background-image: url(/img/pattern-light.png);
|
||||
color: #222;
|
||||
color: rgb(29, 31, 32);
|
||||
--logo-url: url(/img/logo-dark.svg);
|
||||
|
||||
--color-btn: rgb(0, 148, 116);
|
||||
--color-btn-light: rgb(0, 114, 89);
|
||||
--color-tab: #5d6468;
|
||||
--color-tab-light: #3e4346;
|
||||
--color-bg-block: #f1f1f1;
|
||||
--color-bg-input: #fcfcfc;
|
||||
--color-bg-block: #dddddd;
|
||||
--color-bg-input: #eeeeee;
|
||||
--color-border-input: #7f8999;
|
||||
--color-highlight: #00000022;
|
||||
--color-text: rgb(46, 54, 58);
|
||||
--color-text-contrast: #fff;
|
||||
--color-text-input: #46494e;
|
||||
--color-text-dimmed: #767980;
|
||||
--color-scrollbar: rgb(133, 184, 173);
|
||||
--color-scrollbar-track: #00000022;
|
||||
}
|
||||
|
||||
.upload {
|
||||
@@ -106,6 +111,7 @@ textarea {
|
||||
border: solid 1px var(--color-border-input);
|
||||
color: var(--color-text-input);
|
||||
outline: none;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
@@ -116,7 +122,7 @@ textarea:focus {
|
||||
h2 {
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
font-size: 1.8rem;
|
||||
font-size: 1.5rem;
|
||||
margin: 2.5rem 0 1.5rem 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
@@ -134,8 +140,29 @@ hr {
|
||||
border-top: solid 1px var(--color-highlight);
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited,
|
||||
a:active {
|
||||
color: var(--color-btn);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.board-box {
|
||||
height: 100vh;
|
||||
grid-area: board;
|
||||
padding: var(--header-margin) 0 2rem 0;
|
||||
}
|
||||
|
||||
.board {
|
||||
/* box-shadow: 0 0 30px rgba(0, 0, 0, 0.5); */
|
||||
border: solid 1rem var(--color-bg-block);
|
||||
border-radius: 5px;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
@@ -159,13 +186,6 @@ hr {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.board-box {
|
||||
/* background: rgba(255, 166, 0, 0.1); */
|
||||
height: 100vh;
|
||||
grid-area: board;
|
||||
padding: var(--header-margin) 0 2rem 0;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
@@ -2,14 +2,20 @@
|
||||
grid-area: moves;
|
||||
padding: 20px;
|
||||
padding-top: var(--header-margin);
|
||||
min-width: 360px;
|
||||
min-width: 375px;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.game-box {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.game-tabs {
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 38px 1fr 84px;
|
||||
grid-template-rows: 38px 195px 1fr 84px;
|
||||
}
|
||||
|
||||
.game-tabs__btn {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Component, Switch, Match } from "solid-js";
|
||||
import Moves from "./Moves";
|
||||
import Controls from "./Controls";
|
||||
import Info from "./Info";
|
||||
import Load from "./Load";
|
||||
import { Handlers } from "../../types";
|
||||
import "./GameTabs.css";
|
||||
@@ -33,6 +34,7 @@ const GameTabs: Component<{ moves: readonly string[]; handlers: Handlers }> = (
|
||||
</div>
|
||||
<Switch>
|
||||
<Match when={state.activeTab === "game"}>
|
||||
<Info handlers={props.handlers}></Info>
|
||||
<Moves moves={props.moves} handlers={props.handlers} />
|
||||
<Controls handlers={props.handlers} />
|
||||
</Match>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
top: 0;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-columns: 2fr 1fr;
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
|
||||
54
src/ui/components/Info.css
Normal file
54
src/ui/components/Info.css
Normal file
@@ -0,0 +1,54 @@
|
||||
.info-box {
|
||||
grid-area: controls;
|
||||
padding: 0 20px 20px 20px;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: var(--color-bg-block);
|
||||
padding: 30px 20px;
|
||||
font-size: 1.5rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.info__players {
|
||||
position: relative;
|
||||
line-height: 2rem;
|
||||
margin-bottom: 2rem;
|
||||
padding: 0 1rem;
|
||||
}
|
||||
|
||||
.info__rating {
|
||||
font-family: "Fira Code", monospace;
|
||||
color: var(--color-text-dimmed);
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.info__color {
|
||||
border-radius: 1rem;
|
||||
padding: 0;
|
||||
border: solid 2px var(--color-tab);
|
||||
width: 1.4rem;
|
||||
height: 1.4rem;
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
|
||||
.info__color--white {
|
||||
background-color: #eee;
|
||||
}
|
||||
|
||||
.info__color--black {
|
||||
background-color: #111;
|
||||
}
|
||||
|
||||
.info__event {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.info__event,
|
||||
.info__site {
|
||||
font-size: 1.3rem;
|
||||
line-height: 1.5rem;
|
||||
color: var(--color-text);
|
||||
padding: 0 1rem;
|
||||
}
|
||||
55
src/ui/components/Info.tsx
Normal file
55
src/ui/components/Info.tsx
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Component, Show } from "solid-js";
|
||||
import { Handlers } from "../../types";
|
||||
import { state } from "../../state";
|
||||
import "./Info.css";
|
||||
import isSafeLink from "../../utils/isSafeLink";
|
||||
|
||||
const Info: Component<{ handlers: Handlers }> = () => {
|
||||
return (
|
||||
<div class="info">
|
||||
<div className="info__players">
|
||||
<p>
|
||||
<button className="info__color info__color--white"></button>
|
||||
{state.game.header.WhitePretty}{" "}
|
||||
<span className="info__rating">
|
||||
{state.game.header.WhiteElo ?? "????"}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<button className="info__color info__color--black"></button>
|
||||
{state.game.header.BlackPretty}{" "}
|
||||
<span className="info__rating">
|
||||
{state.game.header.BlackElo ?? "????"}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="info__event">
|
||||
<Show when={state.game.header.Event}>
|
||||
<p>{state.game.header.Event}</p>
|
||||
</Show>
|
||||
<Show when={state.game.header.Round}>
|
||||
<p>Round {state.game.header.Round}</p>
|
||||
</Show>
|
||||
</div>
|
||||
<div className="info__site">
|
||||
<Show when={state.game.header.Site}>
|
||||
<p>
|
||||
<Show
|
||||
when={isSafeLink(state.game.header.Site)}
|
||||
fallback={state.game.header.Site}
|
||||
>
|
||||
<a href={state.game.header.Site ?? ""}>
|
||||
{state.game.header.Site?.replace(/^https:\/\//, "")}
|
||||
</a>
|
||||
</Show>
|
||||
</p>
|
||||
</Show>
|
||||
<Show when={state.game.header.DatePretty}>
|
||||
<p>{state.game.header.DatePretty}</p>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Info;
|
||||
@@ -3,7 +3,7 @@
|
||||
padding: 20px;
|
||||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
grid-row-end: span 2;
|
||||
grid-row-end: span 3;
|
||||
}
|
||||
|
||||
.load__pgn-input {
|
||||
|
||||
@@ -2,6 +2,11 @@
|
||||
font-size: 1.4rem;
|
||||
font-family: "Fira Mono";
|
||||
text-align: left;
|
||||
background-color: var(--color-bg-input);
|
||||
}
|
||||
|
||||
.moves__turn {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.move {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, For, createEffect } from "solid-js";
|
||||
import { Component, For, Show, createEffect } from "solid-js";
|
||||
import chunk_ from "@arrows/array/chunk_";
|
||||
import { Handlers } from "../../types";
|
||||
import Scrollable from "./reusable/Scrollable";
|
||||
@@ -16,6 +16,11 @@ const Moves: Component<{ moves: readonly string[]; handlers: Handlers }> = (
|
||||
|
||||
return (
|
||||
<Scrollable class="moves">
|
||||
<Show when={props.moves.length === 0}>
|
||||
<p class="moves__turn">
|
||||
{state.game.getPosition(0).turn === "w" ? "White" : "Black"} to move.
|
||||
</p>
|
||||
</Show>
|
||||
<For each={chunk_(2, props.moves as string[])}>
|
||||
{(move, i) => {
|
||||
const [white, black] = move as [string, string];
|
||||
|
||||
@@ -3,7 +3,13 @@
|
||||
grid-area: setup;
|
||||
padding: 20px;
|
||||
padding-top: var(--header-margin);
|
||||
min-width: 360px;
|
||||
min-width: 375px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) {
|
||||
.setup-box {
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.setup {
|
||||
|
||||
@@ -43,7 +43,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
"options__button--active": state.boardConfig.showBorder,
|
||||
}}
|
||||
onClick={props.handlers.toggleBorder}
|
||||
title="BORDER"
|
||||
title={state.boardConfig.showBorder ? "HIDE BORDER" : "SHOW BORDER"}
|
||||
>
|
||||
<i class="las la-expand"></i>
|
||||
</button>
|
||||
@@ -53,7 +53,11 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
"options__button--active": state.boardConfig.showExtraInfo,
|
||||
}}
|
||||
onClick={props.handlers.toggleExtraInfo}
|
||||
title="EXTRA INFO"
|
||||
title={
|
||||
state.boardConfig.showExtraInfo
|
||||
? "HIDE EXTRA INFO"
|
||||
: "SHOW EXTRA INFO"
|
||||
}
|
||||
>
|
||||
<i class="las la-info-circle"></i>
|
||||
</button>
|
||||
@@ -63,7 +67,11 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
"options__button--active": state.gameConfig.titleScreen,
|
||||
}}
|
||||
onClick={props.handlers.toggleTitleScreen}
|
||||
title="TITLE SCREEN"
|
||||
title={
|
||||
state.gameConfig.titleScreen
|
||||
? "EXCLUDE TITLE SCREEN"
|
||||
: "INCLUDE TITLE SCREEN"
|
||||
}
|
||||
>
|
||||
<i class="las la-heading"></i>
|
||||
</button>
|
||||
@@ -74,7 +82,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
"options__button--active": state.boardConfig.anonymous,
|
||||
}}
|
||||
onClick={props.handlers.toggleAnonymous}
|
||||
title="ANONYMOUS"
|
||||
title="TOGGLE ANONYMOUS"
|
||||
>
|
||||
<i class="las la-user-secret"></i>
|
||||
</button>
|
||||
@@ -116,7 +124,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
</button>
|
||||
</div>
|
||||
<Show when={!state.mobile}>
|
||||
<h3>Image</h3>
|
||||
<hr class="invisible" />
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
@@ -215,19 +223,9 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
<button
|
||||
class="share__btn"
|
||||
onClick={() => {
|
||||
const header = state.game.header;
|
||||
const w = state.boardConfig.anonymous
|
||||
? "Anonymous"
|
||||
: header.WhitePretty;
|
||||
const b = state.boardConfig.anonymous
|
||||
? "Anonymous"
|
||||
: header.BlackPretty;
|
||||
|
||||
const title =
|
||||
`${w} vs ${b}` +
|
||||
(header.Event ? ` | ${header.Event}` : "") +
|
||||
(header.Round ? `, Round ${header.Round}` : "") +
|
||||
(header.DatePretty ? ` | ${header.DatePretty}` : "");
|
||||
const title = state.game.getTitle({
|
||||
anonymous: state.boardConfig.anonymous,
|
||||
});
|
||||
|
||||
const md = `[${title}](${window.location.href})`;
|
||||
|
||||
@@ -241,7 +239,7 @@ const Share: Component<{ handlers: Handlers }> = (props) => {
|
||||
</div>
|
||||
<Show when={!state.mobile}>
|
||||
<div class="share__animation">
|
||||
<h3>Animation</h3>
|
||||
<hr className="invisible" />
|
||||
<button
|
||||
classList={{
|
||||
share__size: true,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.scrollable {
|
||||
background: var(--color-bg-block);
|
||||
height: auto;
|
||||
padding: 40px 20px;
|
||||
padding: 20px 10px 20px 20px;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
overflow: auto;
|
||||
@@ -11,6 +11,7 @@
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 0;
|
||||
padding-right: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -20,10 +21,11 @@
|
||||
}
|
||||
|
||||
.scrollable__content::-webkit-scrollbar-track {
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5);
|
||||
background-color: var(--color-scrollbar-track);
|
||||
/* box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.5); */
|
||||
}
|
||||
|
||||
.scrollable__content::-webkit-scrollbar-thumb {
|
||||
background-color: rgb(0, 59, 47);
|
||||
outline: 1px solid rgb(0, 59, 47);
|
||||
background-color: var(--color-scrollbar);
|
||||
outline: 1px solid var(--color-scrollbar);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ const download = (data: string | Blob, name: string, ext: string) => {
|
||||
link.download = `${name}_${Date.now()}.${ext}`;
|
||||
link.target = "_blank";
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
};
|
||||
|
||||
export default download;
|
||||
|
||||
14
src/utils/isSafeLink.ts
Normal file
14
src/utils/isSafeLink.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
const isSafeLink = (text: string | null) => {
|
||||
if (text === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const url = new URL(text);
|
||||
return url.protocol === "https:";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export default isSafeLink;
|
||||
Reference in New Issue
Block a user