mirror of
https://github.com/chrislicodes/obsidian-chess-study.git
synced 2026-07-22 07:50:30 +00:00
feat: support variants of depth 1 (#1)
feat: support variants of depth 1
This commit is contained in:
parent
26321747a4
commit
15b5a3de40
19 changed files with 849 additions and 381 deletions
36
README.md
36
README.md
|
|
@ -10,15 +10,19 @@ With this plugin, you can either import PGNs or simply start a fresh new game. I
|
|||
|
||||
## Table of contents
|
||||
|
||||
- [Motivation](#motivation)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Features](#features)
|
||||
- [Settings](#settings)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Tools Used](#tools-used)
|
||||
- [Alternatives](#alternatives)
|
||||
- [License](#license)
|
||||
- [Obsidian Chess Study](#obsidian-chess-study)
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Motivation](#motivation)
|
||||
- [Installation](#installation)
|
||||
- [Usage](#usage)
|
||||
- [Features](#features)
|
||||
- [1.0.0](#100)
|
||||
- [1.0.1](#101)
|
||||
- [Settings](#settings)
|
||||
- [Roadmap](#roadmap)
|
||||
- [Tools Used](#tools-used)
|
||||
- [Alternatives](#alternatives)
|
||||
- [License](#license)
|
||||
|
||||
## Motivation
|
||||
|
||||
|
|
@ -40,12 +44,14 @@ Once you click `Submit`, Obsidian Chess Study will parse the PGN, generate a new
|
|||
|
||||

|
||||
|
||||
After that the PGN viewer/editor will render and you are good to go:
|
||||
After that the PGN viewer/editor will render and you are good to go (styles are aligned with your theme and accent color):
|
||||
|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
### 1.0.0
|
||||
|
||||
- [x] Import PGNs
|
||||
- [x] Store game state in JSON
|
||||
- [x] Add custom PGN viewer
|
||||
|
|
@ -54,6 +60,12 @@ After that the PGN viewer/editor will render and you are good to go:
|
|||
- [x] Draw and sync shapes
|
||||
- [x] Add and sync comments with Markdown support
|
||||
|
||||
### 1.0.1
|
||||
|
||||
- [x] Add support for variants (depth 1)
|
||||
|
||||

|
||||
|
||||
## Settings
|
||||
|
||||
Here are the available settings for a `chessStudy` code block:
|
||||
|
|
@ -68,7 +80,7 @@ You can permanently set some settings in the [Obsidian](https://obsidian.md/) pl
|
|||
|
||||
## Roadmap
|
||||
|
||||
- [ ] Add support for variants
|
||||
- [ ] Add undo button
|
||||
- [ ] Add view to manage stored games
|
||||
- [ ] Add more styles
|
||||
- [ ] Add more settings
|
||||
|
|
|
|||
BIN
imgs/chess-study-variants.png
Normal file
BIN
imgs/chess-study-variants.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 67 KiB |
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"id": "chess-study",
|
||||
"name": "Chess Study",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"minAppVersion": "0.15.0",
|
||||
"description": "A chess study helper and PGN viewer/editor.",
|
||||
"author": "Christoph Lindstädt",
|
||||
|
|
|
|||
46
package-lock.json
generated
46
package-lock.json
generated
|
|
@ -14,13 +14,15 @@
|
|||
"@tiptap/starter-kit": "2.0.3",
|
||||
"chess.js": "1.0.0-beta.6",
|
||||
"chessground": "8.3.7",
|
||||
"lucide-react": "0.217.0",
|
||||
"immer": "^10.0.2",
|
||||
"lucide-react": "0.220.0",
|
||||
"nanoid": "4.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"react-dom": "18.2.0",
|
||||
"use-immer": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.1.7",
|
||||
"@types/node": "20.2.1",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.6",
|
||||
|
|
@ -34,7 +36,7 @@
|
|||
"prettier": "2.8.8",
|
||||
"prettier-plugin-organize-imports": "3.2.2",
|
||||
"pretty-quick": "3.1.3",
|
||||
"tslib": "2.5.0",
|
||||
"tslib": "2.5.2",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
},
|
||||
|
|
@ -1528,9 +1530,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.1.7.tgz",
|
||||
"integrity": "sha512-WCuw/o4GSwDGMoonES8rcvwsig77dGCMbZDrZr2x4ZZiNW4P/gcoZXe/0twgtobcTkmg9TuKflxYL/DuwDyJzg==",
|
||||
"version": "20.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.1.tgz",
|
||||
"integrity": "sha512-DqJociPbZP1lbZ5SQPk4oag6W7AyaGMO6gSfRwq3PWl4PXTwJpRQJhDq4W0kzrg3w6tJ1SwlvGZ5uKFHY13LIg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/object.omit": {
|
||||
|
|
@ -3147,6 +3149,15 @@
|
|||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.0.2",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.0.2.tgz",
|
||||
"integrity": "sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA==",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||
|
|
@ -3638,9 +3649,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/lucide-react": {
|
||||
"version": "0.217.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.217.0.tgz",
|
||||
"integrity": "sha512-I4DHIW9EDYuAFNFpkYHZw7lh++BM9Vim86L+gLdSAnb7WwAB2JpGrOl9j8a6c8PSw67UXc3gxj3c26smyZ+7mg==",
|
||||
"version": "0.220.0",
|
||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.220.0.tgz",
|
||||
"integrity": "sha512-bYtGUsLAWBvZu+BzAU/ziP1gzE4LwMEXLnlgSr1yUKEPPalLG77JLd5GdYebOVkpm+GtqRqnp6tEKDX7Bm8ZlQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.5.1 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
|
|
@ -4863,9 +4874,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.0.tgz",
|
||||
"integrity": "sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg==",
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz",
|
||||
"integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
|
|
@ -5001,6 +5012,15 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-immer": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/use-immer/-/use-immer-0.9.0.tgz",
|
||||
"integrity": "sha512-/L+enLi0nvuZ6j4WlyK0US9/ECUtV5v9RUbtxnn5+WbtaXYUaOBoKHDNL9I5AETdurQ4rIFIj/s+Z5X80ATyKw==",
|
||||
"peerDependencies": {
|
||||
"immer": ">=2.0.0",
|
||||
"react": "^16.8.0 || ^17.0.1 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-keyname": {
|
||||
"version": "2.2.6",
|
||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
|
||||
|
|
|
|||
12
package.json
12
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "chess-study",
|
||||
"version": "1.0.0",
|
||||
"version": "1.0.1",
|
||||
"description": "A chess study helper and PGN viewer/editor.",
|
||||
"keywords": [
|
||||
"chess",
|
||||
|
|
@ -22,13 +22,15 @@
|
|||
"@tiptap/starter-kit": "2.0.3",
|
||||
"chess.js": "1.0.0-beta.6",
|
||||
"chessground": "8.3.7",
|
||||
"lucide-react": "0.217.0",
|
||||
"immer": "^10.0.2",
|
||||
"lucide-react": "0.220.0",
|
||||
"nanoid": "4.0.2",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0"
|
||||
"react-dom": "18.2.0",
|
||||
"use-immer": "^0.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "20.1.7",
|
||||
"@types/node": "20.2.1",
|
||||
"@types/react": "18.2.6",
|
||||
"@types/react-dom": "18.2.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.6",
|
||||
|
|
@ -42,7 +44,7 @@
|
|||
"prettier": "2.8.8",
|
||||
"prettier-plugin-organize-imports": "3.2.2",
|
||||
"pretty-quick": "3.1.3",
|
||||
"tslib": "2.5.0",
|
||||
"tslib": "2.5.2",
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { App, MarkdownRenderChild } from 'obsidian';
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom/client';
|
||||
import { ChessStudyDataAdapter, ChessStudyFileData } from 'src/utils';
|
||||
import { ChessStudyDataAdapter, ChessStudyFileData } from 'src/lib/storage';
|
||||
import { ChessStudyPluginSettings } from './obsidian/SettingsTab';
|
||||
import { ChessStudy } from './react/ChessStudy';
|
||||
|
||||
|
|
|
|||
|
|
@ -2,20 +2,29 @@ import { JSONContent } from '@tiptap/react';
|
|||
import { Chess, Move } from 'chess.js';
|
||||
import { Api } from 'chessground/api';
|
||||
import { DrawShape } from 'chessground/draw';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { App, Notice } from 'obsidian';
|
||||
import * as React from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { ChessStudyPluginSettings } from 'src/components/obsidian/SettingsTab';
|
||||
import { parseUserConfig } from 'src/lib/obsidian';
|
||||
import {
|
||||
ChessStudyDataAdapter,
|
||||
ChessStudyFileData,
|
||||
parseUserConfig,
|
||||
} from 'src/utils';
|
||||
import { ChessGroundSettings, ChessgroundWrapper } from './ChessgroundWrapper';
|
||||
ChessStudyMove,
|
||||
VariantMove,
|
||||
} from 'src/lib/storage';
|
||||
import {
|
||||
displayMoveInHistory,
|
||||
findMoveIndex,
|
||||
getCurrentMove,
|
||||
} from 'src/lib/ui-state';
|
||||
import { useImmerReducer } from 'use-immer';
|
||||
import { ChessgroundProps, ChessgroundWrapper } from './ChessgroundWrapper';
|
||||
import { CommentSection } from './CommentSection';
|
||||
import { PgnViewer } from './PgnViewer';
|
||||
|
||||
export type ChessStudyConfig = ChessGroundSettings;
|
||||
export type ChessStudyConfig = ChessgroundProps;
|
||||
|
||||
interface AppProps {
|
||||
source: string;
|
||||
|
|
@ -25,6 +34,20 @@ interface AppProps {
|
|||
dataAdapter: ChessStudyDataAdapter;
|
||||
}
|
||||
|
||||
export interface GameState {
|
||||
currentMove: ChessStudyMove | VariantMove;
|
||||
isViewOnly: boolean;
|
||||
study: ChessStudyFileData;
|
||||
}
|
||||
|
||||
export type GameActions =
|
||||
| { type: 'ADD_MOVE_TO_HISTORY'; move: Move }
|
||||
| { type: 'DISPLAY_NEXT_MOVE_IN_HISTORY' }
|
||||
| { type: 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY' }
|
||||
| { type: 'DISPLAY_SELECTED_MOVE_IN_HISTORY'; moveId: string }
|
||||
| { type: 'SYNC_SHAPES'; shapes: DrawShape[] }
|
||||
| { type: 'SYNC_COMMENT'; comment: JSONContent | null };
|
||||
|
||||
export const ChessStudy = ({
|
||||
source,
|
||||
pluginSettings,
|
||||
|
|
@ -37,11 +60,11 @@ export const ChessStudy = ({
|
|||
source
|
||||
);
|
||||
|
||||
const [chessStudyDataModified] = useState(chessStudyData);
|
||||
|
||||
// Setup Chessboard and Chess.js APIs
|
||||
// Setup Chessground API
|
||||
const [chessView, setChessView] = useState<Api | null>(null);
|
||||
const chessLogic = useMemo(() => {
|
||||
|
||||
// Setup Chess.js API
|
||||
const initialChessLogic = useMemo(() => {
|
||||
const chess = new Chess();
|
||||
|
||||
chessStudyData.moves.forEach((move) => {
|
||||
|
|
@ -54,131 +77,211 @@ export const ChessStudy = ({
|
|||
return chess;
|
||||
}, [chessStudyData.moves]);
|
||||
|
||||
// Track Application State
|
||||
const [history, setHistory] = useState<Move[]>([]);
|
||||
const [isViewOnly, setIsViewOnly] = useState<boolean>(false);
|
||||
const [currentMove, setCurrentMove] = useState<number>(0);
|
||||
const [shapes, setShapes] = useState<DrawShape[][]>(
|
||||
chessStudyData.moves.map((data) => data.shapes)
|
||||
);
|
||||
const [comments, setComments] = useState<(JSONContent | null)[]>(
|
||||
chessStudyData.moves.map((data) => data.comment)
|
||||
);
|
||||
const [chessLogic, setChessLogic] = useState(initialChessLogic);
|
||||
|
||||
//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 [gameState, dispatch] = useImmerReducer<GameState, GameActions>(
|
||||
(draft, action) => {
|
||||
switch (action.type) {
|
||||
case 'DISPLAY_NEXT_MOVE_IN_HISTORY': {
|
||||
if (!chessView) return draft;
|
||||
|
||||
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) => {
|
||||
if (moveIndex !== currentMove) {
|
||||
setCurrentMove(() => {
|
||||
const move = history[moveIndex];
|
||||
const tempChess = new Chess(move.after);
|
||||
chessView?.set({
|
||||
fen: move.after,
|
||||
check: tempChess.isCheck(),
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: 1,
|
||||
selectedMoveId: null,
|
||||
});
|
||||
if (moveIndex !== history.length - 1) {
|
||||
setIsViewOnly(true);
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY': {
|
||||
if (!chessView) return draft;
|
||||
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: -1,
|
||||
selectedMoveId: null,
|
||||
});
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'DISPLAY_SELECTED_MOVE_IN_HISTORY': {
|
||||
if (!chessView) return draft;
|
||||
|
||||
const selectedMoveId = action.moveId;
|
||||
|
||||
displayMoveInHistory(draft, chessView, setChessLogic, {
|
||||
offset: 0,
|
||||
selectedMoveId: selectedMoveId,
|
||||
});
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'SYNC_SHAPES': {
|
||||
const move = getCurrentMove(draft);
|
||||
|
||||
move.shapes = action.shapes;
|
||||
draft.currentMove = move;
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'SYNC_COMMENT': {
|
||||
const move = getCurrentMove(draft);
|
||||
|
||||
move.comment = action.comment;
|
||||
draft.currentMove = move;
|
||||
|
||||
return draft;
|
||||
}
|
||||
case 'ADD_MOVE_TO_HISTORY': {
|
||||
const newMove = action.move;
|
||||
|
||||
const moves = draft.study.moves;
|
||||
const currentMoveId = draft.currentMove?.moveId;
|
||||
|
||||
const currentMoveIndex = moves.findIndex(
|
||||
(move) => move.moveId === currentMoveId
|
||||
);
|
||||
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
const moveId = nanoid();
|
||||
|
||||
if (variant) {
|
||||
//handle variant
|
||||
const parent = moves[variant.parentMoveIndex];
|
||||
const variantMoves = parent.variants[variant.variantIndex].moves;
|
||||
|
||||
const isLastMove = moveIndex === variantMoves.length - 1;
|
||||
|
||||
//Only push if its the last move in the variant because depth can only be 1
|
||||
if (isLastMove) {
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
variantMoves.push(move);
|
||||
|
||||
const tempChess = new Chess(newMove.after);
|
||||
|
||||
draft.currentMove = move;
|
||||
|
||||
chessView?.set({
|
||||
fen: newMove.after,
|
||||
check: tempChess.isCheck(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setIsViewOnly(false);
|
||||
//handle main line
|
||||
const isLastMove = currentMoveIndex === moves.length - 1;
|
||||
|
||||
if (isLastMove) {
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
variants: [],
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
moves.push(move);
|
||||
|
||||
draft.currentMove = move;
|
||||
} else {
|
||||
const currentMove = moves[moveIndex];
|
||||
|
||||
// check if the next move is the same move
|
||||
const nextMove = moves[moveIndex + 1];
|
||||
|
||||
if (nextMove.san === newMove.san) {
|
||||
draft.currentMove = nextMove;
|
||||
return draft;
|
||||
}
|
||||
|
||||
const move = {
|
||||
...newMove,
|
||||
moveId: moveId,
|
||||
shapes: [],
|
||||
comment: null,
|
||||
};
|
||||
|
||||
currentMove.variants.push({
|
||||
parentMoveId: currentMove.moveId,
|
||||
variantId: nanoid(),
|
||||
moves: [move],
|
||||
});
|
||||
|
||||
draft.currentMove = move;
|
||||
}
|
||||
}
|
||||
|
||||
return moveIndex;
|
||||
});
|
||||
return draft;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
[chessView, currentMove, history]
|
||||
{
|
||||
currentMove: chessStudyData.moves[chessStudyData.moves.length - 1],
|
||||
isViewOnly: false,
|
||||
study: chessStudyData,
|
||||
}
|
||||
);
|
||||
|
||||
const onSaveButtonClick = useCallback(async () => {
|
||||
const chessStudyData: ChessStudyFileData = {
|
||||
header: chessStudyDataModified.header,
|
||||
moves: chessLogic.history({ verbose: true }).map((move, index) => ({
|
||||
...move,
|
||||
variants: [],
|
||||
shapes: shapes[index],
|
||||
comment: comments[index],
|
||||
})),
|
||||
};
|
||||
|
||||
await dataAdapter.saveFile(chessStudyData, chessStudyId);
|
||||
|
||||
new Notice('Save successfull!');
|
||||
}, [
|
||||
chessLogic,
|
||||
chessStudyDataModified.header,
|
||||
chessStudyId,
|
||||
comments,
|
||||
dataAdapter,
|
||||
shapes,
|
||||
]);
|
||||
try {
|
||||
await dataAdapter.saveFile(gameState.study, chessStudyId);
|
||||
new Notice('Save successfull!');
|
||||
} catch (e) {
|
||||
new Notice('Something went wrong during saving:', e);
|
||||
}
|
||||
}, [chessStudyId, dataAdapter, gameState.study]);
|
||||
|
||||
return (
|
||||
<div className="chess-study border">
|
||||
<div className="chess-study">
|
||||
<div className="chessground-pgn-container">
|
||||
<div className="chessground-container">
|
||||
<ChessgroundWrapper
|
||||
api={chessView}
|
||||
setApi={setChessView}
|
||||
chessStudyId={chessStudyId}
|
||||
config={{
|
||||
orientation: boardOrientation,
|
||||
}}
|
||||
boardColor={boardColor}
|
||||
chess={chessLogic}
|
||||
setHistory={setHistory}
|
||||
setMoveNumber={setCurrentMove}
|
||||
isViewOnly={isViewOnly}
|
||||
setShapes={setShapes}
|
||||
currentMoveNumber={currentMove}
|
||||
currentMoveShapes={shapes[currentMove]}
|
||||
addMoveToHistory={(move: Move) =>
|
||||
dispatch({ type: 'ADD_MOVE_TO_HISTORY', move })
|
||||
}
|
||||
isViewOnly={gameState.isViewOnly}
|
||||
syncShapes={(shapes: DrawShape[]) =>
|
||||
dispatch({ type: 'SYNC_SHAPES', shapes })
|
||||
}
|
||||
shapes={gameState.currentMove?.shapes}
|
||||
/>
|
||||
</div>
|
||||
<div className="pgn-container">
|
||||
<PgnViewer
|
||||
history={history}
|
||||
currentMove={currentMove}
|
||||
onBackButtonClick={onBackButtonClick}
|
||||
onForwardButtonClick={onForwardButtonClick}
|
||||
onMoveItemClick={onMoveItemClick}
|
||||
history={gameState.study.moves}
|
||||
currentMoveId={gameState.currentMove?.moveId}
|
||||
onBackButtonClick={() =>
|
||||
dispatch({ type: 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY' })
|
||||
}
|
||||
onForwardButtonClick={() =>
|
||||
dispatch({ type: 'DISPLAY_NEXT_MOVE_IN_HISTORY' })
|
||||
}
|
||||
onMoveItemClick={(moveId: string) =>
|
||||
dispatch({
|
||||
type: 'DISPLAY_SELECTED_MOVE_IN_HISTORY',
|
||||
moveId: moveId,
|
||||
})
|
||||
}
|
||||
onSaveButtonClick={onSaveButtonClick}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="CommentSection border-top">
|
||||
<div className="CommentSection">
|
||||
<CommentSection
|
||||
currentMove={currentMove}
|
||||
currentComment={comments[currentMove]}
|
||||
setComments={setComments}
|
||||
currentComment={gameState.currentMove?.comment}
|
||||
setComments={(comment: JSONContent) =>
|
||||
dispatch({ type: 'SYNC_COMMENT', comment: comment })
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,43 +1,36 @@
|
|||
import { Chessground as ChessgroundApi } from 'chessground';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { Chess, Move } from 'chess.js';
|
||||
import { Chessground as ChessgroundApi } from 'chessground';
|
||||
import { Api } from 'chessground/api';
|
||||
import { Config } from 'chessground/config';
|
||||
import { DrawShape } from 'chessground/draw';
|
||||
import { playOtherSide, toColor, toDests } from 'src/utils';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { playOtherSide, toColor, toDests } from 'src/lib/chess-logic';
|
||||
|
||||
export interface ChessGroundSettings {
|
||||
export interface ChessgroundProps {
|
||||
api: Api | null;
|
||||
setApi: React.Dispatch<React.SetStateAction<Api>>;
|
||||
chessStudyId: string;
|
||||
chess: Chess;
|
||||
addMoveToHistory: (move: Move) => void;
|
||||
syncShapes: (shapes: DrawShape[]) => void;
|
||||
isViewOnly: boolean;
|
||||
shapes: DrawShape[];
|
||||
config?: Config;
|
||||
boardColor?: 'brown' | 'green';
|
||||
chess: Chess;
|
||||
setHistory: React.Dispatch<React.SetStateAction<Move[]>>;
|
||||
setShapes: React.Dispatch<React.SetStateAction<DrawShape[][]>>;
|
||||
setMoveNumber: React.Dispatch<React.SetStateAction<number>>;
|
||||
currentMoveNumber: number;
|
||||
isViewOnly: boolean;
|
||||
currentMoveShapes: DrawShape[];
|
||||
}
|
||||
|
||||
export const ChessgroundWrapper = React.memo(
|
||||
({
|
||||
api,
|
||||
setApi,
|
||||
chessStudyId,
|
||||
chess,
|
||||
addMoveToHistory,
|
||||
syncShapes: setShapes,
|
||||
isViewOnly,
|
||||
shapes,
|
||||
boardColor = 'green',
|
||||
config = {},
|
||||
chess,
|
||||
setHistory,
|
||||
setMoveNumber,
|
||||
currentMoveNumber,
|
||||
setShapes,
|
||||
isViewOnly,
|
||||
currentMoveShapes,
|
||||
}: ChessGroundSettings) => {
|
||||
}: ChessgroundProps) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
//Chessground Init
|
||||
|
|
@ -51,67 +44,52 @@ export const ChessgroundWrapper = React.memo(
|
|||
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,
|
||||
},
|
||||
drawable: {
|
||||
onChange: (shapes) => {
|
||||
setShapes(shapes);
|
||||
},
|
||||
},
|
||||
turnColor: toColor(chess),
|
||||
...config,
|
||||
});
|
||||
|
||||
setHistory(chess.history({ verbose: true }));
|
||||
setApi(chessgroundApi);
|
||||
setMoveNumber(chess.history().length - 1);
|
||||
} else if (ref.current && api) {
|
||||
api.set(config);
|
||||
}
|
||||
}, [
|
||||
ref,
|
||||
chess,
|
||||
api,
|
||||
chessStudyId,
|
||||
config,
|
||||
setApi,
|
||||
setHistory,
|
||||
setMoveNumber,
|
||||
setShapes,
|
||||
currentMoveNumber,
|
||||
]);
|
||||
}, [addMoveToHistory, api, chess, config, setApi, setShapes]);
|
||||
|
||||
//Sync Chess Logic
|
||||
useEffect(() => {
|
||||
api?.set({
|
||||
movable: {
|
||||
events: {
|
||||
//Hook up the Chessground UI changes to our App State
|
||||
after: (orig, dest, _metadata) => {
|
||||
const handler = playOtherSide(api, chess);
|
||||
|
||||
addMoveToHistory(handler(orig, dest));
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [addMoveToHistory, api, chess]);
|
||||
|
||||
//Sync View Only
|
||||
useEffect(() => {
|
||||
api?.set({ viewOnly: isViewOnly });
|
||||
}, [isViewOnly, api]);
|
||||
|
||||
//Sync Shapes To State
|
||||
// Load Shapes
|
||||
useEffect(() => {
|
||||
api?.set({
|
||||
drawable: {
|
||||
onChange(shapes) {
|
||||
setShapes((currentShapes) => {
|
||||
const shapesModified = [...currentShapes];
|
||||
shapesModified[currentMoveNumber] = shapes;
|
||||
|
||||
return shapesModified;
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [api, currentMoveNumber, setShapes]);
|
||||
|
||||
//Load Shapes
|
||||
useEffect(() => {
|
||||
if (currentMoveShapes) api?.setShapes(currentMoveShapes);
|
||||
}, [api, currentMoveNumber, currentMoveShapes, setShapes]);
|
||||
if (shapes) {
|
||||
api?.setShapes([...shapes]);
|
||||
}
|
||||
}, [api, shapes]);
|
||||
|
||||
return (
|
||||
<div className={`${boardColor}-board height-width-100 table`}>
|
||||
|
|
|
|||
|
|
@ -4,21 +4,17 @@ import * as React from 'react';
|
|||
import { useEffect } from 'react';
|
||||
|
||||
interface CommentSectionProps {
|
||||
currentMove: number;
|
||||
currentComment: JSONContent | null;
|
||||
setComments: React.Dispatch<React.SetStateAction<(JSONContent | null)[]>>;
|
||||
setComments: (comment: JSONContent) => void;
|
||||
}
|
||||
|
||||
export const CommentSection = React.memo(
|
||||
({ currentMove, currentComment, setComments }: CommentSectionProps) => {
|
||||
({ currentComment, setComments }: CommentSectionProps) => {
|
||||
const editor = useEditor({
|
||||
extensions: [StarterKit],
|
||||
onUpdate: (state) => {
|
||||
setComments((currentComments) => {
|
||||
const currentCommentModified = [...currentComments];
|
||||
currentCommentModified[currentMove] = state.editor.getJSON();
|
||||
return currentCommentModified;
|
||||
});
|
||||
const comment = state.editor.getJSON();
|
||||
if (comment) setComments(comment);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Move } from 'chess.js';
|
||||
import { ArrowLeft, ArrowRight, Save } from 'lucide-react';
|
||||
import * as React from 'react';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { ReactNode, useEffect, useMemo, useRef } from 'react';
|
||||
import { ChessStudyMove } from 'src/lib/storage';
|
||||
|
||||
const chunkArray = <T,>(array: T[], chunkSize: number) => {
|
||||
return array.reduce((resultArray, item, index) => {
|
||||
|
|
@ -54,20 +54,70 @@ const MoveItem = ({
|
|||
);
|
||||
};
|
||||
|
||||
const VariantMoveItem = ({
|
||||
isCurrentMove,
|
||||
san,
|
||||
onMoveItemClick,
|
||||
moveIndicator = null,
|
||||
}: {
|
||||
isCurrentMove: boolean;
|
||||
san: string;
|
||||
onMoveItemClick: () => void;
|
||||
moveIndicator?: string | null;
|
||||
}) => {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current && isCurrentMove) {
|
||||
ref.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
inline: 'end',
|
||||
});
|
||||
}
|
||||
}, [isCurrentMove]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`variant-move-item ${(isCurrentMove && 'active') || ''}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onMoveItemClick();
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<span className={'variant-move-indicator'}>{moveIndicator}</span>
|
||||
{san}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const VariantMoveItemContainer = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="variant-move-item-container">{children}</div>;
|
||||
};
|
||||
|
||||
const VariantContainer = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="variant-container">{children}</div>;
|
||||
};
|
||||
|
||||
const VariantsContainer = ({ children }: { children: ReactNode }) => {
|
||||
return <div className="variants-container">{children}</div>;
|
||||
};
|
||||
|
||||
export const PgnViewer = React.memo(
|
||||
({
|
||||
history,
|
||||
currentMove,
|
||||
currentMoveId,
|
||||
onBackButtonClick,
|
||||
onForwardButtonClick,
|
||||
onMoveItemClick,
|
||||
onSaveButtonClick,
|
||||
}: {
|
||||
history: Move[];
|
||||
currentMove: number;
|
||||
history: ChessStudyMove[];
|
||||
currentMoveId: string;
|
||||
onBackButtonClick: () => void;
|
||||
onForwardButtonClick: () => void;
|
||||
onMoveItemClick: (moveIndex: number) => void;
|
||||
onMoveItemClick: (moveId: string) => void;
|
||||
onSaveButtonClick: () => void;
|
||||
}) => {
|
||||
const movePairs = useMemo(() => chunkArray(history, 2), [history]);
|
||||
|
|
@ -76,23 +126,142 @@ export const PgnViewer = React.memo(
|
|||
<div className="height-width-100">
|
||||
<div className="move-item-section">
|
||||
<div className="move-item-container">
|
||||
{movePairs.map((pair, i) => {
|
||||
{movePairs.map((pair, currentMoveIndex) => {
|
||||
const [wMove, bMove] = pair;
|
||||
|
||||
return (
|
||||
<React.Fragment key={wMove.san + bMove?.san}>
|
||||
<p className="move-indicator center">{i + 1}</p>
|
||||
<React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}>
|
||||
<p className="move-indicator center">
|
||||
{currentMoveIndex + 1}
|
||||
</p>
|
||||
<MoveItem
|
||||
san={wMove.san}
|
||||
isCurrentMove={i * 2 === currentMove}
|
||||
onMoveItemClick={() => onMoveItemClick(i * 2)}
|
||||
isCurrentMove={wMove.moveId === currentMoveId}
|
||||
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
|
||||
/>
|
||||
{bMove && (
|
||||
<MoveItem
|
||||
san={bMove.san}
|
||||
isCurrentMove={i * 2 + 1 === currentMove}
|
||||
onMoveItemClick={() => onMoveItemClick(i * 2 + 1)}
|
||||
isCurrentMove={bMove.moveId === currentMoveId}
|
||||
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
|
||||
/>
|
||||
)}
|
||||
{!!wMove.variants.concat(bMove?.variants || []).length && (
|
||||
<VariantsContainer>
|
||||
{!!wMove.variants.length && (
|
||||
<VariantContainer>
|
||||
{wMove.variants.map((variant) => {
|
||||
return (
|
||||
<VariantMoveItemContainer key={variant.variantId}>
|
||||
{chunkArray(variant.moves, 2).map(
|
||||
(pair, wMoveVarianti) => {
|
||||
const [bMove, wMove] = pair;
|
||||
|
||||
return (
|
||||
<React.Fragment
|
||||
key={
|
||||
bMove.san +
|
||||
wMove?.san +
|
||||
currentMoveIndex
|
||||
}
|
||||
>
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
bMove.moveId === currentMoveId
|
||||
}
|
||||
san={bMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(bMove.moveId)
|
||||
}
|
||||
moveIndicator={
|
||||
(wMoveVarianti === 0 &&
|
||||
`${
|
||||
currentMoveIndex +
|
||||
1 +
|
||||
wMoveVarianti
|
||||
}... `) ||
|
||||
null
|
||||
}
|
||||
/>
|
||||
{wMove && (
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
wMove.moveId === currentMoveId
|
||||
}
|
||||
san={wMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(wMove.moveId)
|
||||
}
|
||||
moveIndicator={
|
||||
(wMoveVarianti % 2 === 0 &&
|
||||
null) ||
|
||||
`${
|
||||
currentMoveIndex +
|
||||
2 +
|
||||
wMoveVarianti
|
||||
}. `
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</VariantMoveItemContainer>
|
||||
);
|
||||
})}
|
||||
</VariantContainer>
|
||||
)}
|
||||
{!!bMove?.variants.length && (
|
||||
<VariantContainer>
|
||||
{bMove.variants.map((variant) => {
|
||||
return (
|
||||
<VariantMoveItemContainer key={variant.variantId}>
|
||||
{chunkArray(variant.moves, 2).map(
|
||||
(pair, bMoveVarianti) => {
|
||||
const [wMove, bMove] = pair;
|
||||
return (
|
||||
<React.Fragment
|
||||
key={
|
||||
wMove.san +
|
||||
bMove?.san +
|
||||
currentMoveIndex
|
||||
}
|
||||
>
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
wMove.moveId === currentMoveId
|
||||
}
|
||||
san={wMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(wMove.moveId)
|
||||
}
|
||||
moveIndicator={`${
|
||||
currentMoveIndex + 2 + bMoveVarianti
|
||||
}. `}
|
||||
/>
|
||||
{bMove && (
|
||||
<VariantMoveItem
|
||||
isCurrentMove={
|
||||
bMove.moveId === currentMoveId
|
||||
}
|
||||
san={bMove.san}
|
||||
onMoveItemClick={() =>
|
||||
onMoveItemClick(bMove.moveId)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</VariantMoveItemContainer>
|
||||
);
|
||||
})}
|
||||
</VariantContainer>
|
||||
)}
|
||||
</VariantsContainer>
|
||||
)}
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
|
|
|
|||
47
src/lib/chess-logic/index.ts
Normal file
47
src/lib/chess-logic/index.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Chess, QUEEN, SQUARES, Square } from 'chess.js';
|
||||
import { Api } from 'chessground/api';
|
||||
import { Config } from 'chessground/config';
|
||||
|
||||
export function toColor(chess: Chess) {
|
||||
return chess.turn() === 'w' ? 'white' : 'black';
|
||||
}
|
||||
|
||||
export function toDests(chess: Chess): Map<Square, Square[]> {
|
||||
const dests = new Map();
|
||||
SQUARES.forEach((s) => {
|
||||
const ms = chess.moves({ square: s, verbose: true });
|
||||
if (ms.length)
|
||||
dests.set(
|
||||
s,
|
||||
ms.map((m) => m.to)
|
||||
);
|
||||
});
|
||||
return dests;
|
||||
}
|
||||
|
||||
export function playOtherSide(cg: Api, chess: Chess) {
|
||||
return (orig: string, dest: string) => {
|
||||
const move = chess.move({ from: orig, to: dest, promotion: QUEEN });
|
||||
|
||||
const commonTurnProperties: Partial<Config> = {
|
||||
turnColor: toColor(chess),
|
||||
movable: {
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
},
|
||||
check: chess.isCheck(),
|
||||
};
|
||||
|
||||
if (move.flags === 'e' || move.promotion) {
|
||||
//Handle En Passant && Promote to Queen by default
|
||||
cg.set({
|
||||
fen: chess.fen(),
|
||||
...commonTurnProperties,
|
||||
});
|
||||
} else {
|
||||
cg.set(commonTurnProperties);
|
||||
}
|
||||
|
||||
return move;
|
||||
};
|
||||
}
|
||||
25
src/lib/obsidian/index.ts
Normal file
25
src/lib/obsidian/index.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { parseYaml } from 'obsidian';
|
||||
import { ChessStudyPluginSettings } from 'src/components/obsidian/SettingsTab';
|
||||
|
||||
type ChessStudyAppConfig = ChessStudyPluginSettings & {
|
||||
chessStudyId: string;
|
||||
};
|
||||
|
||||
export const parseUserConfig = (
|
||||
settings: ChessStudyPluginSettings,
|
||||
content: string
|
||||
): ChessStudyAppConfig => {
|
||||
const chessStudyConfig: ChessStudyAppConfig = {
|
||||
...settings,
|
||||
chessStudyId: '',
|
||||
};
|
||||
|
||||
try {
|
||||
return {
|
||||
...chessStudyConfig,
|
||||
...parseYaml(content),
|
||||
};
|
||||
} catch (e) {
|
||||
throw Error('Something went wrong during parsing. :(');
|
||||
}
|
||||
};
|
||||
68
src/lib/storage/index.ts
Normal file
68
src/lib/storage/index.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
import { JSONContent } from '@tiptap/react';
|
||||
import { Move } from 'chess.js';
|
||||
import { DrawShape } from 'chessground/draw';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { DataAdapter, normalizePath } from 'obsidian';
|
||||
|
||||
export const CURRENT_STORAGE_VERSION = '0.0.1';
|
||||
|
||||
export interface VariantMove extends Move {
|
||||
moveId: string;
|
||||
shapes: DrawShape[];
|
||||
comment: JSONContent | null;
|
||||
}
|
||||
|
||||
export interface Variant {
|
||||
variantId: string;
|
||||
parentMoveId: string;
|
||||
moves: VariantMove[];
|
||||
}
|
||||
|
||||
export interface ChessStudyMove extends Move {
|
||||
moveId: string;
|
||||
variants: Variant[];
|
||||
shapes: DrawShape[];
|
||||
comment: JSONContent | null;
|
||||
}
|
||||
|
||||
export interface ChessStudyFileData {
|
||||
version: string;
|
||||
header: { title: string | null };
|
||||
moves: ChessStudyMove[];
|
||||
}
|
||||
|
||||
export class ChessStudyDataAdapter {
|
||||
adapter: DataAdapter;
|
||||
storagePath: string;
|
||||
|
||||
constructor(adapter: DataAdapter, storagePath: string) {
|
||||
this.adapter = adapter;
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
|
||||
async saveFile(data: ChessStudyFileData, id?: string) {
|
||||
const chessStudyId = id || nanoid();
|
||||
await this.adapter.write(
|
||||
normalizePath(`${this.storagePath}/${chessStudyId}.json`),
|
||||
JSON.stringify(data, null, 2),
|
||||
{}
|
||||
);
|
||||
|
||||
return chessStudyId;
|
||||
}
|
||||
|
||||
async loadFile(id: string): Promise<ChessStudyFileData> {
|
||||
const data = await this.adapter.read(
|
||||
normalizePath(`${this.storagePath}/${id}.json`)
|
||||
);
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
async createStorageFolderIfNotExists() {
|
||||
const folderExists = await this.adapter.exists(this.storagePath);
|
||||
|
||||
if (!folderExists) {
|
||||
this.adapter.mkdir(this.storagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
111
src/lib/ui-state/index.ts
Normal file
111
src/lib/ui-state/index.ts
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
import { Chess } from 'chess.js';
|
||||
import { Api as ChessgroundApi } from 'chessground/api';
|
||||
import { Draft } from 'immer';
|
||||
import { GameState } from 'src/components/react/ChessStudy';
|
||||
import { toColor, toDests } from '../chess-logic';
|
||||
import { ChessStudyMove, VariantMove } from '../storage';
|
||||
|
||||
interface MovePosition {
|
||||
variant: { parentMoveIndex: number; variantIndex: number } | null;
|
||||
moveIndex: number;
|
||||
}
|
||||
|
||||
export const findMoveIndex = (
|
||||
moves: ChessStudyMove[],
|
||||
moveId: string
|
||||
): MovePosition => {
|
||||
for (const [iMainLine, move] of moves.entries()) {
|
||||
if (move.moveId === moveId) return { variant: null, moveIndex: iMainLine };
|
||||
|
||||
for (const [iVariant, variant] of move.variants.entries()) {
|
||||
const moveIndex = variant.moves.findIndex(
|
||||
(move) => move.moveId === moveId
|
||||
);
|
||||
|
||||
if (moveIndex >= 0)
|
||||
return {
|
||||
variant: { parentMoveIndex: iMainLine, variantIndex: iVariant },
|
||||
moveIndex: moveIndex,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { variant: null, moveIndex: -1 };
|
||||
};
|
||||
|
||||
export const displayMoveInHistory = (
|
||||
draft: Draft<GameState>,
|
||||
chessView: ChessgroundApi,
|
||||
setChessLogic: React.Dispatch<React.SetStateAction<Chess>>,
|
||||
options: { offset: number; selectedMoveId: string | null } = {
|
||||
offset: 0,
|
||||
selectedMoveId: null,
|
||||
}
|
||||
): Draft<GameState> => {
|
||||
let moveToDisplay: ChessStudyMove | VariantMove | null = null;
|
||||
|
||||
const { offset, selectedMoveId } = options;
|
||||
|
||||
//Figure out where we are
|
||||
const currentMove = draft.currentMove;
|
||||
const currentMoveId = currentMove.moveId;
|
||||
|
||||
const moves = draft.study.moves;
|
||||
|
||||
//If we pass a moveId, find out where that is and offset from there, otherwise take current moveId
|
||||
const baseMoveId = selectedMoveId || currentMoveId;
|
||||
|
||||
const { variant, moveIndex } = findMoveIndex(moves, baseMoveId);
|
||||
//Are we in a variant? Are we not? Decide which move to display
|
||||
|
||||
if (variant) {
|
||||
const variantMoves =
|
||||
moves[variant.parentMoveIndex].variants[variant.variantIndex].moves;
|
||||
|
||||
if (typeof variantMoves[moveIndex + offset] !== 'undefined') {
|
||||
moveToDisplay = variantMoves[moveIndex + offset];
|
||||
}
|
||||
} else {
|
||||
if (typeof moves[moveIndex + offset] !== 'undefined') {
|
||||
moveToDisplay = moves[moveIndex + offset];
|
||||
}
|
||||
}
|
||||
|
||||
if (!moveToDisplay) return draft;
|
||||
|
||||
const chess = new Chess(moveToDisplay.after);
|
||||
|
||||
chessView.set({
|
||||
fen: moveToDisplay.after,
|
||||
check: chess.isCheck(),
|
||||
movable: {
|
||||
free: false,
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
},
|
||||
turnColor: toColor(chess),
|
||||
});
|
||||
|
||||
draft.currentMove = moveToDisplay;
|
||||
|
||||
setChessLogic(chess);
|
||||
|
||||
return draft;
|
||||
};
|
||||
|
||||
export const getCurrentMove = (
|
||||
draft: Draft<GameState>
|
||||
): Draft<ChessStudyMove> | Draft<VariantMove> => {
|
||||
const currentMoveId = draft.currentMove?.moveId;
|
||||
const moves = draft.study.moves;
|
||||
|
||||
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
|
||||
|
||||
if (variant) {
|
||||
return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[
|
||||
moveIndex
|
||||
];
|
||||
} else {
|
||||
return moves[moveIndex];
|
||||
}
|
||||
};
|
||||
66
src/main.css
66
src/main.css
|
|
@ -1,18 +1,8 @@
|
|||
/* General */
|
||||
|
||||
.chess-study button:active {
|
||||
transform: translateY(1px);
|
||||
}
|
||||
|
||||
.chess-study .border {
|
||||
border: 1px var(--color-base-10) solid;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.chess-study .border-top {
|
||||
border: 1px var(--color-base-10) solid;
|
||||
}
|
||||
|
||||
.chess-study .vertical-align {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -34,12 +24,13 @@
|
|||
}
|
||||
|
||||
.chess-study {
|
||||
height: 100%;
|
||||
max-width: 750px;
|
||||
line-height: 1.5;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.chess-study {
|
||||
height: 100%;
|
||||
background-color: var(--background-secondary);
|
||||
border: 1px solid var(--background-secondary-alt);
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.chess-study *,
|
||||
|
|
@ -85,6 +76,7 @@
|
|||
}
|
||||
|
||||
/* PGN Viewer */
|
||||
|
||||
.chess-study .move-item-section {
|
||||
height: 415px;
|
||||
}
|
||||
|
|
@ -98,10 +90,12 @@
|
|||
height: 35px;
|
||||
}
|
||||
|
||||
/* Move Items */
|
||||
|
||||
.chess-study .move-item-container {
|
||||
display: grid;
|
||||
grid-template-columns: 0.15fr 0.425fr 0.425fr;
|
||||
grid-auto-rows: 30px;
|
||||
grid-auto-rows: minmax(30px, auto);
|
||||
width: 100%;
|
||||
max-height: 415px;
|
||||
overflow-y: scroll;
|
||||
|
|
@ -132,6 +126,21 @@
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Variant Move Items */
|
||||
|
||||
.chess-study .variants-container {
|
||||
grid-column: span 3 / auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-bottom: 1px solid var(--color-base-50);
|
||||
}
|
||||
|
||||
.chess-study .variant-container {
|
||||
grid-column: span 3 / auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chess-study .move-indicator {
|
||||
color: var(--color-base-60);
|
||||
text-align: center;
|
||||
|
|
@ -140,6 +149,28 @@
|
|||
cursor: default;
|
||||
}
|
||||
|
||||
.chess-study .variant-move-item {
|
||||
padding-left: 6px;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.chess-study .variant-move-item-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 4px 0;
|
||||
border-top: 1px solid var(--color-base-50);
|
||||
}
|
||||
|
||||
.chess-study .variant-move-item.active,
|
||||
.chess-study .variant-move-item:hover {
|
||||
color: hsl(var(--accent-h), var(--accent-s), calc(var(--accent-l) - 10%));
|
||||
}
|
||||
|
||||
.chess-study .variant-move-indicator {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Comment Section */
|
||||
|
||||
.chess-study .editor-input {
|
||||
|
|
@ -158,10 +189,11 @@
|
|||
height: 250px;
|
||||
padding-top: 5px;
|
||||
overflow: scroll;
|
||||
border-top: 1px solid var(--background-secondary-alt);
|
||||
}
|
||||
|
||||
.chess-study.ProseMirror,
|
||||
.chess-study.CommentSection > div {
|
||||
.chess-study .ProseMirror,
|
||||
.chess-study .CommentSection > div {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
|
|
|||
16
src/main.tsx
16
src/main.tsx
|
|
@ -1,5 +1,10 @@
|
|||
import { Chess } from 'chess.js';
|
||||
import { Editor, Notice, Plugin, normalizePath } from 'obsidian';
|
||||
import {
|
||||
CURRENT_STORAGE_VERSION,
|
||||
ChessStudyDataAdapter,
|
||||
ChessStudyFileData,
|
||||
} from 'src/lib/storage';
|
||||
import { ReactView } from './components/ReactView';
|
||||
import { PgnModal } from './components/obsidian/PgnModal';
|
||||
import {
|
||||
|
|
@ -7,17 +12,14 @@ import {
|
|||
DEFAULT_SETTINGS,
|
||||
SettingsTab,
|
||||
} from './components/obsidian/SettingsTab';
|
||||
import {
|
||||
ChessStudyDataAdapter,
|
||||
ChessStudyFileData,
|
||||
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 { nanoid } from 'nanoid';
|
||||
import { parseUserConfig } from './lib/obsidian';
|
||||
import './main.css';
|
||||
|
||||
export default class ChessStudyPlugin extends Plugin {
|
||||
|
|
@ -61,11 +63,13 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
}
|
||||
|
||||
const chessStudyFileData: ChessStudyFileData = {
|
||||
version: CURRENT_STORAGE_VERSION,
|
||||
header: {
|
||||
title: chess.header()['opening'] || null,
|
||||
},
|
||||
moves: chess.history({ verbose: true }).map((move) => ({
|
||||
...move,
|
||||
moveId: nanoid(),
|
||||
variants: [],
|
||||
shapes: [],
|
||||
comment: null,
|
||||
|
|
@ -75,7 +79,7 @@ export default class ChessStudyPlugin extends Plugin {
|
|||
const id = await this.dataAdapter.saveFile(chessStudyFileData);
|
||||
|
||||
editor.replaceRange(
|
||||
`\`\`\`chessStudy\nchessStudyId: ${id}\nboardOrientation: white\nboardColor: green\n\`\`\``,
|
||||
`\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``,
|
||||
cursorPosition
|
||||
);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -1,124 +0,0 @@
|
|||
import { JSONContent } from '@tiptap/react';
|
||||
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, normalizePath, parseYaml } from 'obsidian';
|
||||
import { ChessStudyPluginSettings } from 'src/components/obsidian/SettingsTab';
|
||||
|
||||
//Chess Logic
|
||||
type ChessStudyAppConfig = ChessStudyPluginSettings & {
|
||||
chessStudyId: string;
|
||||
};
|
||||
|
||||
export const parseUserConfig = (
|
||||
settings: ChessStudyPluginSettings,
|
||||
content: string
|
||||
): ChessStudyAppConfig => {
|
||||
const chessStudyConfig: ChessStudyAppConfig = {
|
||||
...settings,
|
||||
chessStudyId: '',
|
||||
};
|
||||
|
||||
try {
|
||||
return {
|
||||
...chessStudyConfig,
|
||||
...parseYaml(content),
|
||||
};
|
||||
} catch (e) {
|
||||
throw Error('Something went wrong during parsing. :(');
|
||||
}
|
||||
};
|
||||
|
||||
export function toColor(chess: Chess) {
|
||||
return chess.turn() === 'w' ? 'white' : 'black';
|
||||
}
|
||||
|
||||
export function toDests(chess: Chess): Map<Square, Square[]> {
|
||||
const dests = new Map();
|
||||
SQUARES.forEach((s) => {
|
||||
const ms = chess.moves({ square: s, verbose: true });
|
||||
if (ms.length)
|
||||
dests.set(
|
||||
s,
|
||||
ms.map((m) => m.to)
|
||||
);
|
||||
});
|
||||
return dests;
|
||||
}
|
||||
|
||||
export function playOtherSide(cg: Api, chess: Chess) {
|
||||
return (orig: string, dest: string) => {
|
||||
const move = chess.move({ from: orig, to: dest, promotion: QUEEN });
|
||||
|
||||
const commonTurnProperties: Partial<Config> = {
|
||||
turnColor: toColor(chess),
|
||||
movable: {
|
||||
color: toColor(chess),
|
||||
dests: toDests(chess),
|
||||
},
|
||||
check: chess.isCheck(),
|
||||
};
|
||||
|
||||
if (move.flags === 'e' || move.promotion) {
|
||||
//Handle En Passant && Promote to Queen by default
|
||||
cg.set({
|
||||
fen: chess.fen(),
|
||||
...commonTurnProperties,
|
||||
});
|
||||
} else {
|
||||
cg.set(commonTurnProperties);
|
||||
}
|
||||
|
||||
return chess.history({ verbose: true });
|
||||
};
|
||||
}
|
||||
|
||||
//Storage Logic
|
||||
|
||||
interface ChessStudyMove extends Move {
|
||||
variants: Move[][];
|
||||
shapes: DrawShape[];
|
||||
comment: JSONContent | null;
|
||||
}
|
||||
export interface ChessStudyFileData {
|
||||
header: { title: string | null };
|
||||
moves: ChessStudyMove[];
|
||||
}
|
||||
|
||||
export class ChessStudyDataAdapter {
|
||||
adapter: DataAdapter;
|
||||
storagePath: string;
|
||||
|
||||
constructor(adapter: DataAdapter, storagePath: string) {
|
||||
this.adapter = adapter;
|
||||
this.storagePath = storagePath;
|
||||
}
|
||||
|
||||
async saveFile(data: ChessStudyFileData, id?: string) {
|
||||
const chessStudyId = id || nanoid();
|
||||
await this.adapter.write(
|
||||
normalizePath(`${this.storagePath}/${chessStudyId}.json`),
|
||||
JSON.stringify(data, null, 2),
|
||||
{}
|
||||
);
|
||||
|
||||
return chessStudyId;
|
||||
}
|
||||
|
||||
async loadFile(id: string): Promise<ChessStudyFileData> {
|
||||
const data = await this.adapter.read(
|
||||
normalizePath(`${this.storagePath}/${id}.json`)
|
||||
);
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
async createStorageFolderIfNotExists() {
|
||||
const folderExists = await this.adapter.exists(this.storagePath);
|
||||
|
||||
if (!folderExists) {
|
||||
this.adapter.mkdir(this.storagePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
52
styles.css
52
styles.css
File diff suppressed because one or more lines are too long
|
|
@ -1,3 +1,4 @@
|
|||
{
|
||||
"1.0.0": "0.15.0"
|
||||
"1.0.0": "0.15.0",
|
||||
"1.0.1": "0.15.0"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue