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