mirror of
https://github.com/chrislicodes/obsidian-chess-study.git
synced 2026-07-22 07:50:30 +00:00
feat: basic setup with pgn viewer, obsidian import and interactive chess board
This commit is contained in:
parent
7fd6afbf8f
commit
b9bf576025
18 changed files with 1796 additions and 121 deletions
|
|
@ -4,6 +4,8 @@
|
|||
"env": { "node": true },
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"plugin:react/recommended",
|
||||
"plugin:react-hooks/recommended",
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
|
|
|
|||
47
.gitignore
vendored
47
.gitignore
vendored
|
|
@ -1,22 +1,25 @@
|
|||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
# vscode
|
||||
.vscode
|
||||
|
||||
# Intellij
|
||||
*.iml
|
||||
.idea
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
|
||||
# Don't include the compiled main.js file in the repo.
|
||||
# They should be uploaded to GitHub releases instead.
|
||||
main.js
|
||||
|
||||
# Exclude sourcemaps
|
||||
*.map
|
||||
|
||||
# obsidian
|
||||
data.json
|
||||
|
||||
# Exclude macOS Finder (System Explorer) View States
|
||||
.DS_Store
|
||||
|
||||
storage/*
|
||||
!storage/.keep
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import builtins from "builtin-modules";
|
||||
import esbuild from "esbuild";
|
||||
import process from "process";
|
||||
import builtins from "builtin-modules";
|
||||
import { renameStyles } from "./rename-styles.mjs";
|
||||
|
||||
const banner = `/*
|
||||
|
|
|
|||
1041
package-lock.json
generated
1041
package-lock.json
generated
File diff suppressed because it is too large
Load diff
17
package.json
17
package.json
|
|
@ -19,21 +19,26 @@
|
|||
"dependencies": {
|
||||
"chess.js": "1.0.0-beta.5",
|
||||
"chessground": "8.3.7",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
"lucide-react": "0.216.0",
|
||||
"nanoid": "4.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.1.0",
|
||||
"@types/react": "^18.2.6",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "^18.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.2",
|
||||
"@typescript-eslint/parser": "5.59.2",
|
||||
"builtin-modules": "3.3.0",
|
||||
"esbuild": "0.17.18",
|
||||
"husky": "^8.0.3",
|
||||
"eslint-plugin-react": "7.32.2",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"husky": "8.0.3",
|
||||
"obsidian": "latest",
|
||||
"prettier": "^2.8.8",
|
||||
"pretty-quick": "^3.1.3",
|
||||
"prettier": "2.8.8",
|
||||
"prettier-plugin-organize-imports": "3.2.2",
|
||||
"pretty-quick": "3.1.3",
|
||||
"tslib": "2.5.0",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
|
|
|
|||
47
reset.css
Normal file
47
reset.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Josh's Custom CSS Reset
|
||||
https://www.joshwcomeau.com/css/custom-css-reset/
|
||||
*/
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
img,
|
||||
picture,
|
||||
video,
|
||||
canvas,
|
||||
svg {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
}
|
||||
input,
|
||||
button,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
#root,
|
||||
#__next {
|
||||
isolation: isolate;
|
||||
}
|
||||
46
src/components/ReactView.tsx
Normal file
46
src/components/ReactView.tsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { App, MarkdownRenderChild } from "obsidian";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import { ChessifyFileData } from "src/utils";
|
||||
import { Chessify } from "../components/react/Chessify";
|
||||
import { ChessifyPluginSettings } from "./obsidian/SettingsTab";
|
||||
|
||||
export class ReactView extends MarkdownRenderChild {
|
||||
root: ReactDOM.Root;
|
||||
source: string;
|
||||
app: App;
|
||||
settings: ChessifyPluginSettings;
|
||||
data: ChessifyFileData;
|
||||
|
||||
constructor(
|
||||
containerEL: HTMLElement,
|
||||
source: string,
|
||||
app: App,
|
||||
settings: ChessifyPluginSettings,
|
||||
data: ChessifyFileData
|
||||
) {
|
||||
super(containerEL);
|
||||
this.source = source;
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.root = ReactDOM.createRoot(this.containerEl);
|
||||
this.root.render(
|
||||
<React.StrictMode>
|
||||
<Chessify
|
||||
source={this.source}
|
||||
app={this.app}
|
||||
pluginSettings={this.settings}
|
||||
chessifyData={this.data}
|
||||
/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
42
src/components/obsidian/PgnModal.ts
Normal file
42
src/components/obsidian/PgnModal.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { App, Modal, Setting } from "obsidian";
|
||||
|
||||
export class PgnModal extends Modal {
|
||||
pgn: string;
|
||||
onSubmit: (pgn: string) => void;
|
||||
|
||||
constructor(app: App, onSubmit: (pgn: string) => void) {
|
||||
super(app);
|
||||
this.onSubmit = onSubmit;
|
||||
}
|
||||
|
||||
onOpen() {
|
||||
const { contentEl } = this;
|
||||
|
||||
contentEl.createEl("h1", {
|
||||
text: "Paste the full PGN (leave empty for a new game):",
|
||||
});
|
||||
|
||||
new Setting(contentEl).setName("PGN").addTextArea((text) =>
|
||||
text
|
||||
.onChange((value) => {
|
||||
this.pgn = value;
|
||||
})
|
||||
.inputEl.setCssStyles({ width: "100%", height: "250px" })
|
||||
);
|
||||
|
||||
new Setting(contentEl).addButton((btn) =>
|
||||
btn
|
||||
.setButtonText("Submit")
|
||||
.setCta()
|
||||
.onClick(() => {
|
||||
this.close();
|
||||
this.onSubmit(this.pgn);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
onClose() {
|
||||
const { contentEl } = this;
|
||||
contentEl.empty();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { PluginSettingTab, App, Setting } from "obsidian";
|
||||
import { App, PluginSettingTab, Setting } from "obsidian";
|
||||
import ChessifyPlugin from "src/main";
|
||||
|
||||
export interface ChessifyPluginSettings {
|
||||
|
|
|
|||
|
|
@ -1,70 +1,98 @@
|
|||
import * as React from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Chessground as ChessgroundApi } from "chessground";
|
||||
import * as React from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
import { Chess, Move } from "chess.js";
|
||||
import { Api } from "chessground/api";
|
||||
import { Config } from "chessground/config";
|
||||
import { Chess } from "chess.js";
|
||||
import { playOtherSide, toColor, toDests } from "src/utils";
|
||||
|
||||
export interface ChessGroundSettings {
|
||||
api: Api | null;
|
||||
setApi: React.Dispatch<React.SetStateAction<Api>>;
|
||||
chessifyId: string;
|
||||
config?: Config;
|
||||
boardColor?: "brown" | "green";
|
||||
chess: Chess;
|
||||
setHistory: React.Dispatch<React.SetStateAction<Move[]>>;
|
||||
setMoveNumber: React.Dispatch<React.SetStateAction<number>>;
|
||||
isViewOnly: boolean;
|
||||
}
|
||||
|
||||
export function ChessgroundWrapper({
|
||||
api,
|
||||
setApi,
|
||||
chessifyId,
|
||||
boardColor = "green",
|
||||
config = {},
|
||||
chess,
|
||||
setHistory,
|
||||
setMoveNumber,
|
||||
isViewOnly,
|
||||
}: ChessGroundSettings) {
|
||||
const [api, setApi] = useState<Api | null>(null);
|
||||
const [chess, setChess] = useState<Chess | null>(null);
|
||||
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
//Chessground Init
|
||||
useEffect(() => {
|
||||
if (ref.current && !api) {
|
||||
const chess = config.fen?.length
|
||||
? new Chess(config.fen)
|
||||
: new Chess();
|
||||
const chessgroundApi = ChessgroundApi(ref.current, {
|
||||
fen: chess.fen(),
|
||||
animation: { enabled: true, duration: 100 },
|
||||
check: chess.isCheck(),
|
||||
movable: {
|
||||
free: false,
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
events: {
|
||||
//Hook up the Chessground UI changes to our App State
|
||||
after: (orig, dest, _metadata) => {
|
||||
const handler = playOtherSide(
|
||||
chessgroundApi,
|
||||
chess
|
||||
);
|
||||
|
||||
setHistory(handler(orig, dest));
|
||||
setMoveNumber((state) => state + 1);
|
||||
},
|
||||
},
|
||||
},
|
||||
highlight: {
|
||||
check: true,
|
||||
},
|
||||
turnColor: toColor(chess),
|
||||
drawable: {
|
||||
onChange(shapes) {},
|
||||
},
|
||||
...config,
|
||||
});
|
||||
|
||||
chessgroundApi.set({
|
||||
movable: {
|
||||
events: { after: playOtherSide(chessgroundApi, chess) },
|
||||
},
|
||||
});
|
||||
|
||||
setHistory(chess.history({ verbose: true }));
|
||||
setApi(chessgroundApi);
|
||||
setChess(chess);
|
||||
setMoveNumber(chess.history().length - 1);
|
||||
} else if (ref.current && api) {
|
||||
api.set(config);
|
||||
}
|
||||
}, [ref, chess]);
|
||||
}, [
|
||||
ref,
|
||||
chess,
|
||||
api,
|
||||
chessifyId,
|
||||
config,
|
||||
setApi,
|
||||
setHistory,
|
||||
setMoveNumber,
|
||||
]);
|
||||
|
||||
//Sync Is View Only
|
||||
useEffect(() => {
|
||||
api?.set(config);
|
||||
if (config.fen?.length) {
|
||||
chess?.load(config.fen);
|
||||
}
|
||||
}, [api, config]);
|
||||
api?.set({ viewOnly: isViewOnly });
|
||||
}, [isViewOnly, api]);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: "450px",
|
||||
width: "450px",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
}}
|
||||
className={`${boardColor}-board`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
import { Chess, Move } from "chess.js";
|
||||
import { Api } from "chessground/api";
|
||||
import { App } from "obsidian";
|
||||
import * as React from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { ChessifyPluginSettings } from "src/components/obsidian/SettingsTab";
|
||||
import { ChessgroundWrapper, ChessGroundSettings } from "./ChessgroundWrapper";
|
||||
import { parseUserConfig } from "src/utils";
|
||||
import { ChessifyFileData, parseUserConfig } from "src/utils";
|
||||
import { ChessGroundSettings, ChessgroundWrapper } from "./ChessgroundWrapper";
|
||||
import { PgnViewer } from "./PgnViewer";
|
||||
|
||||
export type ChessifyConfig = ChessGroundSettings;
|
||||
|
||||
|
|
@ -10,21 +14,129 @@ interface AppProps {
|
|||
source: string;
|
||||
app: App;
|
||||
pluginSettings: ChessifyPluginSettings;
|
||||
chessifyData: ChessifyFileData;
|
||||
}
|
||||
|
||||
export const Chessify = ({ source, pluginSettings }: AppProps) => {
|
||||
const { fen, boardColor, boardOrientation } = parseUserConfig(
|
||||
export const Chessify = ({
|
||||
source,
|
||||
pluginSettings,
|
||||
chessifyData,
|
||||
}: AppProps) => {
|
||||
// Parse Obsidian / Code Block Settings
|
||||
const { boardColor, boardOrientation, chessifyId } = parseUserConfig(
|
||||
pluginSettings,
|
||||
source
|
||||
);
|
||||
|
||||
// Setup Chessboard and Chess.js APIs
|
||||
const [chessView, setChessView] = useState<Api | null>(null);
|
||||
const chessLogic = useMemo(() => {
|
||||
const chess = new Chess();
|
||||
|
||||
chessifyData.moves.forEach((move) => {
|
||||
chess.move({
|
||||
from: move.from,
|
||||
to: move.to,
|
||||
promotion: move.promotion,
|
||||
});
|
||||
});
|
||||
return chess;
|
||||
}, [chessifyData.moves]);
|
||||
|
||||
// Track Application State
|
||||
const [history, setHistory] = useState<Move[]>([]);
|
||||
const [isViewOnly, setIsViewOnly] = useState<boolean>(false);
|
||||
const [currentMove, setCurrentMove] = useState<number>(0);
|
||||
|
||||
//PgnViewer Functions
|
||||
const onBackButtonClick = useCallback(() => {
|
||||
if (currentMove >= 0) {
|
||||
setCurrentMove((currentMove) => {
|
||||
const move = history[currentMove];
|
||||
const tempChess = new Chess(move.before);
|
||||
chessView?.set({
|
||||
fen: move.before,
|
||||
check: tempChess.isCheck(),
|
||||
});
|
||||
setIsViewOnly(true);
|
||||
return currentMove - 1;
|
||||
});
|
||||
}
|
||||
}, [chessView, currentMove, history]);
|
||||
|
||||
const onForwardButtonClick = useCallback(() => {
|
||||
if (currentMove < history.length - 1) {
|
||||
setCurrentMove((currentMove) => {
|
||||
const move = history[currentMove + 1];
|
||||
const tempChess = new Chess(move.after);
|
||||
chessView?.set({
|
||||
fen: move.after,
|
||||
check: tempChess.isCheck(),
|
||||
});
|
||||
if (currentMove + 1 === history.length - 1)
|
||||
setIsViewOnly(false);
|
||||
return currentMove + 1;
|
||||
});
|
||||
}
|
||||
}, [currentMove, chessView, history]);
|
||||
|
||||
const onMoveItemClick = useCallback(
|
||||
(moveIndex: number) => {
|
||||
setCurrentMove(() => {
|
||||
const move = history[moveIndex];
|
||||
const tempChess = new Chess(move.after);
|
||||
chessView?.set({
|
||||
fen: move.after,
|
||||
check: tempChess.isCheck(),
|
||||
});
|
||||
if (moveIndex + 1 !== history.length - 1) {
|
||||
setIsViewOnly(false);
|
||||
} else {
|
||||
setIsViewOnly(true);
|
||||
}
|
||||
|
||||
return moveIndex;
|
||||
});
|
||||
},
|
||||
[chessView, history]
|
||||
);
|
||||
|
||||
return (
|
||||
<ChessgroundWrapper
|
||||
boardColor={boardColor}
|
||||
config={{
|
||||
fen: fen,
|
||||
orientation: boardOrientation,
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
height: "450px",
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: "0 0 450px",
|
||||
height: "100%",
|
||||
}}
|
||||
>
|
||||
<ChessgroundWrapper
|
||||
api={chessView}
|
||||
setApi={setChessView}
|
||||
chessifyId={chessifyId}
|
||||
config={{
|
||||
orientation: boardOrientation,
|
||||
}}
|
||||
boardColor={boardColor}
|
||||
chess={chessLogic}
|
||||
setHistory={setHistory}
|
||||
setMoveNumber={setCurrentMove}
|
||||
isViewOnly={isViewOnly}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flex: 1, height: "100%" }}>
|
||||
<PgnViewer
|
||||
history={history}
|
||||
currentMove={currentMove}
|
||||
onBackButtonClick={onBackButtonClick}
|
||||
onForwardButtonClick={onForwardButtonClick}
|
||||
onMoveItemClick={onMoveItemClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
129
src/components/react/PgnViewer/index.tsx
Normal file
129
src/components/react/PgnViewer/index.tsx
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
import { Move } from "chess.js";
|
||||
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
|
||||
const chunkArray = <T,>(array: T[], chunkSize: number) => {
|
||||
return array.reduce((resultArray, item, index) => {
|
||||
const chunkIndex = Math.floor(index / chunkSize);
|
||||
|
||||
if (!resultArray[chunkIndex]) {
|
||||
resultArray[chunkIndex] = [];
|
||||
}
|
||||
|
||||
resultArray[chunkIndex].push(item);
|
||||
|
||||
return resultArray;
|
||||
}, [] as T[][]);
|
||||
};
|
||||
|
||||
const MoveItem = ({
|
||||
isCurrentMove,
|
||||
san,
|
||||
onMoveItemClick,
|
||||
}: {
|
||||
isCurrentMove: boolean;
|
||||
san: string;
|
||||
onMoveItemClick: () => void;
|
||||
}) => {
|
||||
const ref = useRef<HTMLParagraphElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isCurrentMove) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
inline: "end",
|
||||
});
|
||||
}
|
||||
}, [isCurrentMove]);
|
||||
|
||||
return (
|
||||
<p
|
||||
className={`move-item ${
|
||||
(isCurrentMove && "active") || ""
|
||||
} vertical-align`}
|
||||
ref={ref}
|
||||
onClick={() => onMoveItemClick()}
|
||||
>
|
||||
{san}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
export const PgnViewer = ({
|
||||
history,
|
||||
currentMove,
|
||||
onBackButtonClick,
|
||||
onForwardButtonClick,
|
||||
onMoveItemClick,
|
||||
}: {
|
||||
history: Move[];
|
||||
currentMove: number;
|
||||
onBackButtonClick: () => void;
|
||||
onForwardButtonClick: () => void;
|
||||
onMoveItemClick: (moveIndex: number) => void;
|
||||
}) => {
|
||||
const movePairs = useMemo(() => chunkArray(history, 2), [history]);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100%", height: "100%" }}>
|
||||
<div style={{ height: "415px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "0.15fr 0.425fr 0.425fr",
|
||||
gridAutoRows: "30px",
|
||||
width: "100%",
|
||||
maxHeight: "415px",
|
||||
overflowY: "scroll",
|
||||
}}
|
||||
>
|
||||
{movePairs.map((pair, i) => {
|
||||
const [wMove, bMove] = pair;
|
||||
return (
|
||||
<React.Fragment key={wMove.san + bMove?.san}>
|
||||
<p className="move-indicator center">{i + 1}</p>
|
||||
<MoveItem
|
||||
san={wMove.san}
|
||||
isCurrentMove={i * 2 === currentMove}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(i * 2)
|
||||
}
|
||||
/>
|
||||
{bMove && (
|
||||
<MoveItem
|
||||
san={bMove.san}
|
||||
isCurrentMove={
|
||||
i * 2 + 1 === currentMove
|
||||
}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(i * 2 + 1)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
width: "100%",
|
||||
gap: "4px",
|
||||
height: "35px",
|
||||
}}
|
||||
>
|
||||
<button onClick={() => onBackButtonClick()}>
|
||||
<ArrowLeft />
|
||||
</button>
|
||||
<button onClick={() => onForwardButtonClick()}>
|
||||
<ArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
47
src/main.css
Normal file
47
src/main.css
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.vertical-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.move-item {
|
||||
padding-left: 8px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.move-item:hover {
|
||||
background-color: hsl(
|
||||
var(--accent-h),
|
||||
var(--accent-s),
|
||||
calc(var(--accent-l))
|
||||
);
|
||||
color: var(--text-on-accent);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.move-item.active {
|
||||
background-color: hsl(
|
||||
var(--accent-h),
|
||||
var(--accent-s),
|
||||
calc(var(--accent-l) - 10%)
|
||||
);
|
||||
color: var(--text-on-accent);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.move-indicator {
|
||||
color: var(--color-base-60);
|
||||
text-align: center;
|
||||
background-color: var(--color-base-10);
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
}
|
||||
136
src/main.tsx
136
src/main.tsx
|
|
@ -1,36 +1,116 @@
|
|||
import { App, MarkdownRenderChild, Plugin } from "obsidian";
|
||||
import { Chess } from "chess.js";
|
||||
import { Editor, Notice, Plugin } from "obsidian";
|
||||
import { ReactView } from "./components/ReactView";
|
||||
import { PgnModal } from "./components/obsidian/PgnModal";
|
||||
import {
|
||||
ChessifyPluginSettings,
|
||||
DEFAULT_SETTINGS,
|
||||
SettingsTab,
|
||||
} from "./components/obsidian/SettingsTab";
|
||||
import * as ReactDOM from "react-dom/client";
|
||||
import * as React from "react";
|
||||
import { Chessify } from "./components/react/Chessify";
|
||||
import {
|
||||
ChessifyDataAdapter,
|
||||
ChessifyFileData,
|
||||
parseUserConfig,
|
||||
} from "./utils";
|
||||
|
||||
// these styles must be imported somewhere
|
||||
import "assets/board/green.css";
|
||||
import "chessground/assets/chessground.base.css";
|
||||
import "chessground/assets/chessground.brown.css";
|
||||
import "chessground/assets/chessground.cburnett.css";
|
||||
import "assets/board/green.css";
|
||||
import "../reset.css";
|
||||
import "./main.css";
|
||||
|
||||
export default class ChessifyPlugin extends Plugin {
|
||||
settings: ChessifyPluginSettings;
|
||||
dataAdapter: ChessifyDataAdapter;
|
||||
storagePath = `${this.app.vault.configDir}/plugins/${this.manifest.id}/storage/`;
|
||||
|
||||
async onload() {
|
||||
// Load the settings
|
||||
// Load Settings
|
||||
await this.loadSettings();
|
||||
|
||||
// Register Data Adapter
|
||||
this.dataAdapter = new ChessifyDataAdapter(
|
||||
this.app.vault.adapter,
|
||||
this.storagePath
|
||||
);
|
||||
|
||||
// Add settings tab
|
||||
this.addSettingTab(new SettingsTab(this.app, this));
|
||||
|
||||
// Add command
|
||||
this.addCommand({
|
||||
id: "insert-chesser",
|
||||
name: "Insert PGN-Editor at cursor position",
|
||||
editorCallback: (editor: Editor) => {
|
||||
const cursorPosition = editor.getCursor();
|
||||
|
||||
const onSubmit = async (pgn: string) => {
|
||||
try {
|
||||
const chess = new Chess();
|
||||
|
||||
if (pgn) {
|
||||
//Try to parse the PGN
|
||||
chess.loadPgn(pgn, {
|
||||
strict: false,
|
||||
});
|
||||
}
|
||||
|
||||
const chessifyFileData: ChessifyFileData = {
|
||||
header: {
|
||||
title: chess.header()["opening"] || null,
|
||||
},
|
||||
moves: chess
|
||||
.history({ verbose: true })
|
||||
.map((move) => ({
|
||||
...move,
|
||||
subMoves: [],
|
||||
shapes: [],
|
||||
})),
|
||||
};
|
||||
|
||||
const id = await this.dataAdapter.saveFile(
|
||||
chessifyFileData
|
||||
);
|
||||
|
||||
editor.replaceRange(
|
||||
`\`\`\`chessify\nchessifyId: ${id}\n\`\`\``,
|
||||
cursorPosition
|
||||
);
|
||||
} catch (e) {
|
||||
new Notice("There was an error during PGN parsing.", 0);
|
||||
}
|
||||
};
|
||||
|
||||
new PgnModal(this.app, onSubmit).open();
|
||||
},
|
||||
});
|
||||
|
||||
// Add chessify code block processor
|
||||
this.registerMarkdownCodeBlockProcessor(
|
||||
"chessify",
|
||||
(source, el, ctx) => {
|
||||
ctx.addChild(
|
||||
new ReactView(el, source, this.app, this.settings)
|
||||
);
|
||||
async (source, el, ctx) => {
|
||||
const { chessifyId } = parseUserConfig(this.settings, source);
|
||||
|
||||
if (!chessifyId.trim().length)
|
||||
return new Notice(
|
||||
"No chessifyId parameter found, please add one manually if the file already exists or add it via the 'Insert PGN-Editor at cursor position' command.",
|
||||
0
|
||||
);
|
||||
|
||||
try {
|
||||
const data = await this.dataAdapter.loadFile(chessifyId);
|
||||
|
||||
ctx.addChild(
|
||||
new ReactView(el, source, this.app, this.settings, data)
|
||||
);
|
||||
} catch (e) {
|
||||
new Notice(
|
||||
`There was an error while trying to load ${chessifyId}.json. You can check the plugin folder if the file exist and if not add one via the 'Insert PGN-Editor at cursor position' command.`,
|
||||
0
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
|
@ -53,39 +133,3 @@ export default class ChessifyPlugin extends Plugin {
|
|||
await this.saveData(this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
class ReactView extends MarkdownRenderChild {
|
||||
root: ReactDOM.Root;
|
||||
source: string;
|
||||
app: App;
|
||||
settings: ChessifyPluginSettings;
|
||||
|
||||
constructor(
|
||||
containerEL: HTMLElement,
|
||||
source: string,
|
||||
app: App,
|
||||
settings: ChessifyPluginSettings
|
||||
) {
|
||||
super(containerEL);
|
||||
this.source = source;
|
||||
this.app = app;
|
||||
this.settings = settings;
|
||||
}
|
||||
|
||||
onload() {
|
||||
this.root = ReactDOM.createRoot(this.containerEl);
|
||||
this.root.render(
|
||||
<React.StrictMode>
|
||||
<Chessify
|
||||
source={this.source}
|
||||
app={this.app}
|
||||
pluginSettings={this.settings}
|
||||
/>
|
||||
</React.StrictMode>
|
||||
);
|
||||
}
|
||||
|
||||
onunload() {
|
||||
this.root.unmount();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
import { parseYaml } from "obsidian";
|
||||
import { ChessifyPluginSettings } from "src/components/obsidian/SettingsTab";
|
||||
import { Chess, QUEEN, SQUARES, Square } from "chess.js";
|
||||
import { Chess, Move, QUEEN, SQUARES, Square } from "chess.js";
|
||||
import { Api } from "chessground/api";
|
||||
import { Config } from "chessground/config";
|
||||
import { DrawShape } from "chessground/draw";
|
||||
import { nanoid } from "nanoid";
|
||||
import { DataAdapter, parseYaml } from "obsidian";
|
||||
import { ChessifyPluginSettings } from "src/components/obsidian/SettingsTab";
|
||||
|
||||
//Chess Logic
|
||||
type ChessifyAppConfig = ChessifyPluginSettings & {
|
||||
fen: string;
|
||||
chessifyId: string;
|
||||
};
|
||||
|
||||
export const parseUserConfig = (
|
||||
|
|
@ -14,7 +17,7 @@ export const parseUserConfig = (
|
|||
): ChessifyAppConfig => {
|
||||
const chessifyConfig: ChessifyAppConfig = {
|
||||
...settings,
|
||||
fen: "",
|
||||
chessifyId: "",
|
||||
};
|
||||
|
||||
try {
|
||||
|
|
@ -66,5 +69,44 @@ export function playOtherSide(cg: Api, chess: Chess) {
|
|||
} else {
|
||||
cg.set(commonTurnProperties);
|
||||
}
|
||||
|
||||
return chess.history({ verbose: true });
|
||||
};
|
||||
}
|
||||
|
||||
//Storage Logic
|
||||
|
||||
interface ChessifyMove extends Move {
|
||||
subMoves: Move[];
|
||||
shapes: DrawShape[];
|
||||
}
|
||||
export interface ChessifyFileData {
|
||||
header: { title: string | null };
|
||||
moves: ChessifyMove[];
|
||||
}
|
||||
|
||||
export class ChessifyDataAdapter {
|
||||
adapter: DataAdapter;
|
||||
storagePath: string;
|
||||
|
||||
constructor(adapter: DataAdapter, storagePath: string) {
|
||||
this.adapter = adapter;
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
|
||||
async saveFile(data: ChessifyFileData) {
|
||||
const id = nanoid();
|
||||
await this.adapter.write(
|
||||
`${this.storagePath}/${id}.json`,
|
||||
JSON.stringify(data, null, 2),
|
||||
{}
|
||||
);
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async loadFile(id: string): Promise<ChessifyFileData> {
|
||||
const data = await this.adapter.read(`${this.storagePath}/${id}.json`);
|
||||
return JSON.parse(data);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
storage/.keep
Normal file
0
storage/.keep
Normal file
96
styles.css
96
styles.css
File diff suppressed because one or more lines are too long
|
|
@ -14,5 +14,6 @@
|
|||
"jsx": "react",
|
||||
"lib": ["DOM", "ES5", "ES6", "ES7"]
|
||||
},
|
||||
"include": ["**/*.ts", "src/main.tsx"]
|
||||
"include": ["**/*.ts", "src/**/*.tsx"],
|
||||
"exclude": ["node-modules"]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue