chore: minor improvements

This commit is contained in:
chrislicodes 2024-02-04 19:02:36 +01:00 committed by Christoph Lindstädt
parent e5381f1cf5
commit 137821c4da
14 changed files with 583 additions and 553 deletions

View file

@ -1,34 +1,34 @@
name: Release Obsidian plugin name: Release Obsidian plugin
on: on:
push: push:
tags: tags:
- '*' - '*'
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Use Node.js - name: Use Node.js
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '18.x' node-version: '18.x'
- name: Build plugin - name: Build plugin
run: | run: |
npm install npm install
npm run build npm run build
- name: Create release - name: Create release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: | run: |
tag="${GITHUB_REF#refs/tags/}" tag="${GITHUB_REF#refs/tags/}"
gh release create "$tag" \ gh release create "$tag" \
--title="$tag" \ --title="$tag" \
--draft \ --draft \
main.js manifest.json styles.css main.js manifest.json styles.css

View file

@ -1,6 +1,6 @@
{ {
"singleQuote": true, "singleQuote": true,
"tabWidth": 2, "tabWidth": 1,
"proseWrap": "preserve", "proseWrap": "preserve",
"semi": true "semi": true
} }

View file

@ -12,8 +12,9 @@ With this plugin, you can either import PGNs or simply start a fresh new game. I
- [Installation](#installation) - [Installation](#installation)
- [Usage](#usage) - [Usage](#usage)
- [Features](#features) - [Features](#features)
- [1.0.0](#100) - [1.0.0](#100)
- [1.1.0](#110) - [1.1.0](#110)
- [1.2.0](#120)
- [Settings](#settings) - [Settings](#settings)
- [Roadmap](#roadmap) - [Roadmap](#roadmap)
- [Tools Used](#tools-used) - [Tools Used](#tools-used)
@ -62,6 +63,14 @@ After that the PGN viewer/editor will render and you are good to go (styles are
![chess-study-variants](imgs/chess-study-variants.png) ![chess-study-variants](imgs/chess-study-variants.png)
### 1.2.0
- [x] Add FEN start position support
- [x] Add undo button
- [x] Copy current FEN string to clipboard
Thanks to @latenitecoding for the contributions
## Settings ## Settings
Here are the available settings for a `chessStudy` code block: Here are the available settings for a `chessStudy` code block:
@ -71,19 +80,17 @@ Here are the available settings for a `chessStudy` code block:
| `chessStudyId` | Valid nanoid | Valid ID for a file stored in the plugin storage | | `chessStudyId` | Valid nanoid | Valid ID for a file stored in the plugin storage |
| `boardOrientation` | `white` \| `black` | Orientation of the board | | `boardOrientation` | `white` \| `black` | Orientation of the board |
| `boardColor` | `green` \| `brown` | Color of the board | | `boardColor` | `green` \| `brown` | Color of the board |
| `fen` | Valid FEN string | FEN string |
| `viewComments` | `true` \| `false` | Whether to display the comments section | | `viewComments` | `true` \| `false` | Whether to display the comments section |
You can permanently set some settings in the [Obsidian](https://obsidian.md/) plugin settings for Obsidian Chess Study. You can permanently set some settings in the [Obsidian](https://obsidian.md/) plugin settings for Obsidian Chess Study.
## Roadmap ## Roadmap
- [ ] Add option to export current FEN
- [ ] Add option to start from a specific position (FEN)
- [ ] Add undo button
- [ ] Add view to manage stored games - [ ] Add view to manage stored games
- [ ] Add more styles - [ ] Add more styles
- [ ] Add more settings - [ ] Add more settings
- [ ] Support canvas view
- [ ] Mobile support
## Tools Used ## Tools Used

View file

@ -1,7 +1,8 @@
import { App, Modal, Setting } from 'obsidian'; import { App, Modal, Setting } from 'obsidian';
import { ChessString } from 'src/main';
export class PgnModal extends Modal { export class ChessStringModal extends Modal {
pgn: string; chessString: ChessString;
onSubmit: (pgn: string) => void; onSubmit: (pgn: string) => void;
constructor(app: App, onSubmit: (pgn: string) => void) { constructor(app: App, onSubmit: (pgn: string) => void) {
@ -13,13 +14,13 @@ export class PgnModal extends Modal {
const { contentEl } = this; const { contentEl } = this;
contentEl.createEl('h1', { contentEl.createEl('h1', {
text: 'Paste the full PGN (leave empty for a new game):', text: 'Paste the full PGN/FEN (leave empty for a new game):',
}); });
new Setting(contentEl).setName('PGN').addTextArea((text) => new Setting(contentEl).setName('PGN/FEN').addTextArea((text) =>
text text
.onChange((value) => { .onChange((value) => {
this.pgn = value; this.chessString = value;
}) })
.inputEl.setCssStyles({ width: '100%', height: '250px' }) .inputEl.setCssStyles({ width: '100%', height: '250px' })
); );
@ -30,7 +31,7 @@ export class PgnModal extends Modal {
.setCta() .setCta()
.onClick(() => { .onClick(() => {
this.close(); this.close();
this.onSubmit(this.pgn); this.onSubmit(this.chessString);
}) })
); );
} }

View file

@ -4,14 +4,12 @@ import ChessStudyPlugin from 'src/main';
export interface ChessStudyPluginSettings { export interface ChessStudyPluginSettings {
boardOrientation: 'white' | 'black'; boardOrientation: 'white' | 'black';
boardColor: 'green' | 'brown'; boardColor: 'green' | 'brown';
fen: string;
viewComments: true | false; viewComments: true | false;
} }
export const DEFAULT_SETTINGS: ChessStudyPluginSettings = { export const DEFAULT_SETTINGS: ChessStudyPluginSettings = {
boardOrientation: 'white', boardOrientation: 'white',
boardColor: 'green', boardColor: 'green',
fen: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
viewComments: true, viewComments: true,
}; };
@ -38,9 +36,7 @@ export class SettingsTab extends PluginSettingTab {
dropdown dropdown
.setValue(this.plugin.settings.boardOrientation) .setValue(this.plugin.settings.boardOrientation)
.onChange((orientation) => { .onChange((orientation) => {
this.plugin.settings.boardOrientation = orientation as this.plugin.settings.boardOrientation = orientation as 'white' | 'black';
| 'white'
| 'black';
this.plugin.saveSettings(); this.plugin.saveSettings();
}); });
}); });
@ -60,32 +56,18 @@ export class SettingsTab extends PluginSettingTab {
}); });
}); });
new Setting(containerEl)
.setName('FEN')
.setDesc('Sets the default board position')
.addText((text) => {
text
.setValue(this.plugin.settings.fen)
.onChange((fen) => {
this.plugin.settings.fen = fen as string;
this.plugin.saveSettings();
});
});
new Setting(containerEl) new Setting(containerEl)
.setName('View Comments') .setName('View Comments')
.setDesc('Sets the default view of the comments') .setDesc('Sets the default view of the comments')
.addDropdown((dropdown) => { .addDropdown((dropdown) => {
dropdown.addOption('true', 'True'); dropdown.addOption('true', 'True');
dropdown.addOption('false', 'False'); dropdown.addOption('false', 'False');
dropdown dropdown
.setValue(this.plugin.settings.viewComments) .setValue(this.plugin.settings.viewComments.toString())
.onChange((viewComments) => { .onChange((viewComments) => {
this.plugin.settings.viewComments = (viewComments == true || viewComments == "true" || viewComments == "True") as true | false; this.plugin.settings.viewComments = viewComments === 'true';
this.plugin.saveSettings(); this.plugin.saveSettings();
}); });
}); });
} }
} }

View file

@ -35,7 +35,7 @@ interface AppProps {
} }
export interface GameState { export interface GameState {
currentMove: ChessStudyMove | VariantMove; currentMove: ChessStudyMove | VariantMove | null;
isViewOnly: boolean; isViewOnly: boolean;
study: ChessStudyFileData; study: ChessStudyFileData;
} }
@ -56,16 +56,15 @@ export const ChessStudy = ({
dataAdapter, dataAdapter,
}: AppProps) => { }: AppProps) => {
// Parse Obsidian / Code Block Settings // Parse Obsidian / Code Block Settings
const { boardColor, boardOrientation, fen, viewComments, chessStudyId } = const { boardColor, boardOrientation, viewComments, chessStudyId } =
parseUserConfig(pluginSettings, source); parseUserConfig(pluginSettings, source);
// Setup Chessground API // Setup Chessground API
const [chessView, setChessView] = useState<Api | null>(null); const [chessView, setChessView] = useState<Api | null>(null);
// Setup Chess.js API // Setup Chess.js API
const [firstPlayer, initialMoveNumber, initialChessLogic] = useMemo(() => { const [initialChessLogic, firstPlayer, initialMoveNumber] = useMemo(() => {
const chess = (fen) ? new Chess(fen) : new Chess(); const chess = new Chess(chessStudyData.rootFEN);
const firstPlayer = chess.turn(); const firstPlayer = chess.turn();
const initialMoveNumber = chess.moveNumber(); const initialMoveNumber = chess.moveNumber();
@ -78,16 +77,17 @@ export const ChessStudy = ({
}); });
}); });
return [firstPlayer, initialMoveNumber, chess]; return [chess, firstPlayer, initialMoveNumber];
}, [chessStudyData.moves]); }, [chessStudyData.moves, chessStudyData.rootFEN]);
const [chessLogic, setChessLogic] = useState(initialChessLogic); const [chessLogic, setChessLogic] = useState(initialChessLogic);
const [gameState, dispatch] = useImmerReducer<GameState, GameActions>( const [gameState, dispatch] = useImmerReducer<GameState, GameActions>(
(draft, action) => { (draft, action) => {
const hasNoMoves = draft.study.moves.length === 0;
switch (action.type) { switch (action.type) {
case 'DISPLAY_NEXT_MOVE_IN_HISTORY': { case 'DISPLAY_NEXT_MOVE_IN_HISTORY': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft; if (!chessView || hasNoMoves) return draft;
displayMoveInHistory(draft, chessView, setChessLogic, { displayMoveInHistory(draft, chessView, setChessLogic, {
offset: 1, offset: 1,
@ -97,7 +97,7 @@ export const ChessStudy = ({
return draft; return draft;
} }
case 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY': { case 'DISPLAY_PREVIOUS_MOVE_IN_HISTORY': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft; if (!chessView || hasNoMoves) return draft;
displayMoveInHistory(draft, chessView, setChessLogic, { displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1, offset: -1,
@ -107,56 +107,61 @@ export const ChessStudy = ({
return draft; return draft;
} }
case 'REMOVE_LAST_MOVE_FROM_HISTORY': { case 'REMOVE_LAST_MOVE_FROM_HISTORY': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft; if (!chessView || hasNoMoves) return draft;
let moves = draft.study.moves; const moves = draft.study.moves;
const currentMoveId = draft.currentMove?.moveId; const currentMoveId = draft.currentMove?.moveId;
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId); if (currentMoveId) {
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
if (variant) { if (variant) {
const parent = moves[variant.parentMoveIndex]; const parent = moves[variant.parentMoveIndex];
const variantMoves = parent.variants[variant.variantIndex].moves; const variantMoves = parent.variants[variant.variantIndex].moves;
const isLastMove = moveIndex === variantMoves.length - 1; const isLastMove = moveIndex === variantMoves.length - 1;
if (isLastMove) { if (isLastMove) {
displayMoveInHistory(draft, chessView, setChessLogic, { displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1, offset: -1,
selectedMoveId: currentMoveId, selectedMoveId: currentMoveId,
}); });
} }
variantMoves.pop(); variantMoves.pop();
if (variantMoves.length == 0) { if (variantMoves.length === 0) {
parent.variants.splice(variant.variantIndex, 1); parent.variants.splice(variant.variantIndex, 1);
} }
if (isLastMove) { if (isLastMove) {
draft.currentMove = (variantMoves.length > 0) ? variantMoves[variantMoves.length - 1] : moves[variant.parentMoveIndex]; draft.currentMove =
} variantMoves.length > 0
} else { ? variantMoves[variantMoves.length - 1]
const isLastMove = moveIndex === moves.length - 1; : moves[variant.parentMoveIndex];
}
} else {
const isLastMove = moveIndex === moves.length - 1;
if (isLastMove) { if (isLastMove) {
displayMoveInHistory(draft, chessView, setChessLogic, { displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1, offset: -1,
selectedMoveId: currentMoveId, selectedMoveId: currentMoveId,
}); });
} }
moves.pop(); moves.pop();
if (isLastMove) { if (isLastMove) {
draft.currentMove = (moves.length > 0) ? moves[moves.length - 1] : null; draft.currentMove = moves.length > 0 ? moves[moves.length - 1] : null;
}
} }
} }
return draft; return draft;
} }
case 'DISPLAY_SELECTED_MOVE_IN_HISTORY': { case 'DISPLAY_SELECTED_MOVE_IN_HISTORY': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft; if (!chessView || hasNoMoves) return draft;
const selectedMoveId = action.moveId; const selectedMoveId = action.moveId;
@ -168,22 +173,26 @@ export const ChessStudy = ({
return draft; return draft;
} }
case 'SYNC_SHAPES': { case 'SYNC_SHAPES': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft; if (!chessView || hasNoMoves) return draft;
const move = getCurrentMove(draft); const move = getCurrentMove(draft);
move.shapes = action.shapes; if (move) {
draft.currentMove = move; move.shapes = action.shapes;
draft.currentMove = move;
}
return draft; return draft;
} }
case 'SYNC_COMMENT': { case 'SYNC_COMMENT': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft; if (!chessView || hasNoMoves) return draft;
const move = getCurrentMove(draft); const move = getCurrentMove(draft);
move.comment = action.comment; if (move) {
draft.currentMove = move; move.comment = action.comment;
draft.currentMove = move;
}
return draft; return draft;
} }
@ -193,80 +202,94 @@ export const ChessStudy = ({
const moves = draft.study.moves; const moves = draft.study.moves;
const currentMoveId = draft.currentMove?.moveId; const currentMoveId = draft.currentMove?.moveId;
const currentMoveIndex = moves.findIndex(
(move) => move.moveId === currentMoveId
);
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
const moveId = nanoid(); const moveId = nanoid();
if (variant) { if (currentMoveId) {
//handle variant const currentMoveIndex = moves.findIndex(
const parent = moves[variant.parentMoveIndex]; (move) => move.moveId === currentMoveId
const variantMoves = parent.variants[variant.variantIndex].moves; );
const isLastMove = moveIndex === variantMoves.length - 1; const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
//Only push if its the last move in the variant because depth can only be 1 if (variant) {
if (isLastMove) { //handle variant
const move = { const parent = moves[variant.parentMoveIndex];
...newMove, const variantMoves = parent.variants[variant.variantIndex].moves;
moveId: moveId,
shapes: [],
comment: null,
};
variantMoves.push(move);
const tempChess = new Chess(newMove.after); const isLastMove = moveIndex === variantMoves.length - 1;
draft.currentMove = move; //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);
chessView?.set({ const tempChess = new Chess(newMove.after);
fen: newMove.after,
check: tempChess.isCheck(), draft.currentMove = move;
});
chessView?.set({
fen: newMove.after,
check: tempChess.isCheck(),
});
}
} else {
//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;
}
} }
} else { } else {
//handle main line const move = {
const isLastMove = currentMoveIndex === moves.length - 1; ...newMove,
moveId: moveId,
variants: [],
shapes: [],
comment: null,
};
moves.push(move);
if (isLastMove) { draft.currentMove = move;
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 draft; return draft;
@ -276,7 +299,7 @@ export const ChessStudy = ({
} }
}, },
{ {
currentMove: chessStudyData.moves[chessStudyData.moves.length - 1], currentMove: chessStudyData.moves[chessStudyData.moves.length - 1] ?? null,
isViewOnly: false, isViewOnly: false,
study: chessStudyData, study: chessStudyData,
} }
@ -310,14 +333,14 @@ export const ChessStudy = ({
syncShapes={(shapes: DrawShape[]) => syncShapes={(shapes: DrawShape[]) =>
dispatch({ type: 'SYNC_SHAPES', shapes }) dispatch({ type: 'SYNC_SHAPES', shapes })
} }
shapes={gameState.currentMove?.shapes} shapes={gameState.currentMove?.shapes || []}
/> />
</div> </div>
<div className="pgn-container"> <div className="pgn-container">
<PgnViewer <PgnViewer
history={gameState.study.moves} history={gameState.study.moves}
currentMoveId={gameState.currentMove?.moveId} currentMoveId={gameState.currentMove?.moveId ?? null}
firstPlayer={firstPlayer} firstPlayer={firstPlayer}
initialMoveNumber={initialMoveNumber} initialMoveNumber={initialMoveNumber}
onUndoButtonClick={() => onUndoButtonClick={() =>
@ -338,7 +361,7 @@ export const ChessStudy = ({
onSaveButtonClick={onSaveButtonClick} onSaveButtonClick={onSaveButtonClick}
onCopyButtonClick={() => { onCopyButtonClick={() => {
try { try {
navigator.clipboard.writeText(chessLogic.fen()) navigator.clipboard.writeText(chessLogic.fen());
new Notice('Copied to clipboard!'); new Notice('Copied to clipboard!');
} catch (e) { } catch (e) {
new Notice('Could not copy to clipboard:', e); new Notice('Could not copy to clipboard:', e);
@ -350,7 +373,7 @@ export const ChessStudy = ({
{viewComments && ( {viewComments && (
<div className="CommentSection"> <div className="CommentSection">
<CommentSection <CommentSection
currentComment={gameState.currentMove?.comment} currentComment={gameState.currentMove?.comment ?? null}
setComments={(comment: JSONContent) => setComments={(comment: JSONContent) =>
dispatch({ type: 'SYNC_COMMENT', comment: comment }) dispatch({ type: 'SYNC_COMMENT', comment: comment })
} }

View file

@ -0,0 +1,36 @@
import { ArrowLeft, ArrowRight, Copy, Save, Undo2 } from 'lucide-react';
import * as React from 'react';
export interface ControlActions {
onUndoButtonClick: () => void;
onBackButtonClick: () => void;
onForwardButtonClick: () => void;
onSaveButtonClick: () => void;
onCopyButtonClick: () => void;
}
export const Controls = (props: ControlActions) => {
return (
<div>
<div className="button-section">
<button onClick={() => props.onBackButtonClick()}>
<ArrowLeft />
</button>
<button onClick={() => props.onForwardButtonClick()}>
<ArrowRight />
</button>
<button onClick={() => props.onSaveButtonClick()}>
<Save strokeWidth={'1px'} />
</button>
</div>
<div className="button-section">
<button onClick={() => props.onCopyButtonClick()}>
<Copy strokeWidth={'1px'} />
</button>
<button onClick={() => props.onUndoButtonClick()}>
<Undo2 />
</button>
</div>
</div>
);
};

View file

@ -0,0 +1,74 @@
import * as React from 'react';
export const MoveItem = ({
isCurrentMove,
san,
onMoveItemClick,
}: {
isCurrentMove: boolean;
san: string;
onMoveItemClick: () => void;
}) => {
const ref = React.useRef<HTMLParagraphElement>(null);
React.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={(e) => {
e.stopPropagation();
onMoveItemClick();
}}
>
{san}
</p>
);
};
export const VariantMoveItem = ({
isCurrentMove,
san,
onMoveItemClick,
moveIndicator = null,
}: {
isCurrentMove: boolean;
san: string;
onMoveItemClick: () => void;
moveIndicator?: string | null;
}) => {
const ref = React.useRef<HTMLDivElement>(null);
React.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>
);
};

View file

@ -1,11 +1,12 @@
import { ArrowLeft, ArrowRight, Save, Undo2 } from 'lucide-react';
import * as React from 'react'; import * as React from 'react';
import { ReactNode, useEffect, useMemo, useRef } from 'react'; import { useMemo } from 'react';
import { ChessStudyMove } from 'src/lib/storage'; import { ChessStudyMove } from 'src/lib/storage';
import { Controls } from './Controls';
import { MoveItem, VariantMoveItem } from './MoveItems';
const chunkArray = <T,>(array: T[], chunkSize: number, offsetByOne: boolean = false) => { const chunkArray = <T,>(array: T[], chunkSize: number, offsetByOne = false) => {
return array.reduce((resultArray, item, index) => { return array.reduce((resultArray, item, index) => {
const chunkIndex = Math.floor((index + ((offsetByOne) ? 1 : 0)) / chunkSize); const chunkIndex = Math.floor((index + (offsetByOne ? 1 : 0)) / chunkSize);
if (!resultArray[chunkIndex]) { if (!resultArray[chunkIndex]) {
resultArray[chunkIndex] = []; resultArray[chunkIndex] = [];
@ -17,307 +18,187 @@ const chunkArray = <T,>(array: T[], chunkSize: number, offsetByOne: boolean = fa
}, [] as T[][]); }, [] as T[][]);
}; };
const MoveItem = ({ export const VariantMoveItemContainer = ({
isCurrentMove, children,
san,
onMoveItemClick,
}: { }: {
isCurrentMove: boolean; children: React.ReactNode;
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={(e) => {
e.stopPropagation();
onMoveItemClick();
}}
>
{san}
</p>
);
};
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>; return <div className="variant-move-item-container">{children}</div>;
}; };
const VariantContainer = ({ children }: { children: ReactNode }) => { export const VariantContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
return <div className="variant-container">{children}</div>; return <div className="variant-container">{children}</div>;
}; };
const VariantsContainer = ({ children }: { children: ReactNode }) => { export const VariantsContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
return <div className="variants-container">{children}</div>; return <div className="variants-container">{children}</div>;
}; };
export const PgnViewer = React.memo( interface PgnViewerProps {
({ history: ChessStudyMove[];
currentMoveId: string | null;
firstPlayer: string;
initialMoveNumber: number;
onMoveItemClick: (moveId: string) => void;
onUndoButtonClick: () => void;
onBackButtonClick: () => void;
onForwardButtonClick: () => void;
onSaveButtonClick: () => void;
onCopyButtonClick: () => void;
}
export const PgnViewer = React.memo((props: PgnViewerProps) => {
const {
history, history,
currentMoveId, currentMoveId,
firstPlayer, firstPlayer,
initialMoveNumber, initialMoveNumber,
onUndoButtonClick,
onBackButtonClick,
onForwardButtonClick,
onMoveItemClick, onMoveItemClick,
onSaveButtonClick, ...controlActions
onCopyButtonClick, } = props;
}: {
history: ChessStudyMove[];
currentMoveId: string;
firstPlayer: string;
initialMoveNumber: number;
onUndoButtonClick: () => void;
onBackButtonClick: () => void;
onForwardButtonClick: () => void;
onMoveItemClick: (moveId: string) => void;
onSaveButtonClick: () => void;
onCopyButtonClick: () => void;
}) => {
const movePairs = useMemo(() => chunkArray(history, 2, firstPlayer === 'b'), [history]);
return ( const movePairs = useMemo(
<div className="height-width-100"> () => chunkArray(history, 2, firstPlayer === 'b'),
<div className="move-item-section"> [firstPlayer, history]
<div className="move-item-container"> );
{movePairs.map((pair, currentMoveIndex) => {
const [wMove, bMove] = pair;
return ( return (
<React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}> <div className="height-width-100">
<p className="move-indicator center"> <div className="move-item-section">
{currentMoveIndex + initialMoveNumber} <div className="move-item-container">
</p> {movePairs.map((pair, currentMoveIndex) => {
{(firstPlayer === 'b' && bMove === undefined && currentMoveIndex === 0) && ( const [wMove, bMove] = pair;
<MoveItem
san={'...'} return (
isCurrentMove={false} <React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}>
onMoveItemClick={() => {}} <p className="move-indicator center">
/> {currentMoveIndex + initialMoveNumber}
)} </p>
{firstPlayer === 'b' && !bMove && currentMoveIndex === 0 && (
<MoveItem <MoveItem
san={wMove.san} san={'...'}
isCurrentMove={wMove.moveId === currentMoveId} isCurrentMove={false}
onMoveItemClick={() => onMoveItemClick(wMove.moveId)} onMoveItemClick={() => {}}
/> />
{bMove && ( )}
<MoveItem <MoveItem
san={bMove.san} san={wMove.san}
isCurrentMove={bMove.moveId === currentMoveId} isCurrentMove={wMove.moveId === currentMoveId}
onMoveItemClick={() => onMoveItemClick(bMove.moveId)} onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
/> />
)} {bMove && (
{!!wMove.variants.concat(bMove?.variants || []).length && ( <MoveItem
<VariantsContainer> san={bMove.san}
{!!wMove.variants.length && ( isCurrentMove={bMove.moveId === currentMoveId}
<VariantContainer> onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
{wMove.variants.map((variant) => { />
return ( )}
<VariantMoveItemContainer key={variant.variantId}> {!!wMove.variants.concat(bMove?.variants || []).length && (
{chunkArray(variant.moves, 2).map( <VariantsContainer>
(pair, wMoveVarianti) => { {!!wMove.variants.length && (
const [bMove, wMove] = pair; <VariantContainer>
{wMove.variants.map((variant) => {
return (
<VariantMoveItemContainer key={variant.variantId}>
{chunkArray(variant.moves, 2).map((pair, wMoveVarianti) => {
const [bMove, wMove] = pair;
return ( return (
<React.Fragment <React.Fragment
key={ key={bMove.san + wMove?.san + currentMoveIndex}
bMove.san + >
wMove?.san + <VariantMoveItem
currentMoveIndex isCurrentMove={bMove.moveId === currentMoveId}
} san={bMove.san}
> onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
<VariantMoveItem moveIndicator={
isCurrentMove={ (wMoveVarianti === 0 &&
bMove.moveId === currentMoveId (firstPlayer === 'w' || currentMoveIndex > 0) &&
} `${
san={bMove.san} currentMoveIndex + initialMoveNumber + wMoveVarianti
onMoveItemClick={() => }... `) ||
onMoveItemClick(bMove.moveId) (firstPlayer === 'b' &&
} currentMoveIndex === 0 &&
moveIndicator={ `${
(wMoveVarianti === 0 && currentMoveIndex + initialMoveNumber + wMoveVarianti
(firstPlayer === 'w' }. `) ||
|| currentMoveIndex > 0) && null
`${ }
currentMoveIndex + />
initialMoveNumber + {wMove && (
wMoveVarianti <VariantMoveItem
}... `) || isCurrentMove={wMove.moveId === currentMoveId}
(firstPlayer === 'b' && san={wMove.san}
currentMoveIndex === 0 && onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
`${ moveIndicator={
currentMoveIndex + ((firstPlayer === 'w' || currentMoveIndex > 0) &&
initialMoveNumber +
wMoveVarianti
}. `) ||
null
}
/>
{wMove && (
<VariantMoveItem
isCurrentMove={
wMove.moveId === currentMoveId
}
san={wMove.san}
onMoveItemClick={() =>
onMoveItemClick(wMove.moveId)
}
moveIndicator={
((firstPlayer === 'w' ||
currentMoveIndex > 0) &&
`${
currentMoveIndex +
initialMoveNumber + 1 +
wMoveVarianti
}. `) ||
null
}
/>
)}
</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 currentMoveIndex + initialMoveNumber + 1 + wMoveVarianti
+ initialMoveNumber + 1 }. `) ||
+ bMoveVarianti null
}. ` }
} />
/> )}
{bMove && ( </React.Fragment>
<VariantMoveItem );
isCurrentMove={ })}
bMove.moveId === currentMoveId </VariantMoveItemContainer>
} );
san={bMove.san} })}
onMoveItemClick={() => </VariantContainer>
onMoveItemClick(bMove.moveId) )}
} {!!bMove?.variants.length && (
/> <VariantContainer>
)} {bMove.variants.map((variant) => {
</React.Fragment> return (
); <VariantMoveItemContainer key={variant.variantId}>
} {chunkArray(variant.moves, 2).map((pair, bMoveVarianti) => {
)} const [wMove, bMove] = pair;
</VariantMoveItemContainer> return (
); <React.Fragment
})} key={wMove.san + bMove?.san + currentMoveIndex}
</VariantContainer> >
)} <VariantMoveItem
</VariantsContainer> isCurrentMove={wMove.moveId === currentMoveId}
)} san={wMove.san}
</React.Fragment> onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
); moveIndicator={`${
})} currentMoveIndex + initialMoveNumber + 1 + bMoveVarianti
</div> }. `}
</div> />
<div className="button-section"> {bMove && (
<button onClick={() => onUndoButtonClick()}> <VariantMoveItem
<Undo2 /> isCurrentMove={bMove.moveId === currentMoveId}
</button> san={bMove.san}
<button onClick={() => onBackButtonClick()}> onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
<ArrowLeft /> />
</button> )}
<button onClick={() => onForwardButtonClick()}> </React.Fragment>
<ArrowRight /> );
</button> })}
</div> </VariantMoveItemContainer>
<div className="button-section"> );
<button onClick={() => onSaveButtonClick()}> })}
<Save strokeWidth={'1px'} /> </VariantContainer>
</button> )}
<button onClick={() => onCopyButtonClick()}> </VariantsContainer>
<Copy strokeWidth={'1px'} /> )}
</button> </React.Fragment>
);
})}
</div> </div>
</div> </div>
); <Controls {...controlActions} />
} </div>
); );
});
PgnViewer.displayName = 'PgnViewer'; PgnViewer.displayName = 'PgnViewer';

View file

@ -3,14 +3,9 @@ import { Move } from 'chess.js';
import { DrawShape } from 'chessground/draw'; import { DrawShape } from 'chessground/draw';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { DataAdapter, normalizePath } from 'obsidian'; import { DataAdapter, normalizePath } from 'obsidian';
import { ROOT_FEN } from 'src/main';
export const CURRENT_STORAGE_VERSION = '0.0.1'; export const CURRENT_STORAGE_VERSION = '0.0.2';
export interface VariantMove extends Move {
moveId: string;
shapes: DrawShape[];
comment: JSONContent | null;
}
export interface Variant { export interface Variant {
variantId: string; variantId: string;
@ -18,6 +13,12 @@ export interface Variant {
moves: VariantMove[]; moves: VariantMove[];
} }
export interface VariantMove extends Move {
moveId: string;
shapes: DrawShape[];
comment: JSONContent | null;
}
export interface ChessStudyMove extends Move { export interface ChessStudyMove extends Move {
moveId: string; moveId: string;
variants: Variant[]; variants: Variant[];
@ -29,6 +30,7 @@ export interface ChessStudyFileData {
version: string; version: string;
header: { title: string | null }; header: { title: string | null };
moves: ChessStudyMove[]; moves: ChessStudyMove[];
rootFEN: string;
} }
export class ChessStudyDataAdapter { export class ChessStudyDataAdapter {
@ -42,6 +44,13 @@ export class ChessStudyDataAdapter {
async saveFile(data: ChessStudyFileData, id?: string) { async saveFile(data: ChessStudyFileData, id?: string) {
const chessStudyId = id || nanoid(); const chessStudyId = id || nanoid();
console.log(
`Writing file to ${normalizePath(
`${this.storagePath}/${chessStudyId}.json`
)}`
);
await this.adapter.write( await this.adapter.write(
normalizePath(`${this.storagePath}/${chessStudyId}.json`), normalizePath(`${this.storagePath}/${chessStudyId}.json`),
JSON.stringify(data, null, 2), JSON.stringify(data, null, 2),
@ -52,16 +61,29 @@ export class ChessStudyDataAdapter {
} }
async loadFile(id: string): Promise<ChessStudyFileData> { async loadFile(id: string): Promise<ChessStudyFileData> {
console.log(
`Reading file from ${normalizePath(`${this.storagePath}/${id}.json`)}`
);
const data = await this.adapter.read( const data = await this.adapter.read(
normalizePath(`${this.storagePath}/${id}.json`) normalizePath(`${this.storagePath}/${id}.json`)
); );
return JSON.parse(data);
const jsonData = JSON.parse(data);
//Make sure data is compatible with storage version 0.0.1.
if (!jsonData.rootFEN) {
return { ...jsonData, rootFEN: ROOT_FEN };
}
return jsonData;
} }
async createStorageFolderIfNotExists() { async createStorageFolderIfNotExists() {
const folderExists = await this.adapter.exists(this.storagePath); const folderExists = await this.adapter.exists(this.storagePath);
if (!folderExists) { if (!folderExists) {
console.log(`Creating storage folder at: ${this.storagePath}`);
this.adapter.mkdir(this.storagePath); this.adapter.mkdir(this.storagePath);
} }
} }

View file

@ -18,9 +18,7 @@ export const findMoveIndex = (
if (move.moveId === moveId) return { variant: null, moveIndex: iMainLine }; if (move.moveId === moveId) return { variant: null, moveIndex: iMainLine };
for (const [iVariant, variant] of move.variants.entries()) { for (const [iVariant, variant] of move.variants.entries()) {
const moveIndex = variant.moves.findIndex( const moveIndex = variant.moves.findIndex((move) => move.moveId === moveId);
(move) => move.moveId === moveId
);
if (moveIndex >= 0) if (moveIndex >= 0)
return { return {
@ -48,64 +46,74 @@ export const displayMoveInHistory = (
//Figure out where we are //Figure out where we are
const currentMove = draft.currentMove; const currentMove = draft.currentMove;
const currentMoveId = currentMove.moveId;
const moves = draft.study.moves; if (currentMove) {
const currentMoveId = currentMove.moveId;
//If we pass a moveId, find out where that is and offset from there, otherwise take current moveId const moves = draft.study.moves;
const baseMoveId = selectedMoveId || currentMoveId;
const { variant, moveIndex } = findMoveIndex(moves, baseMoveId); //If we pass a moveId, find out where that is and offset from there, otherwise take current moveId
//Are we in a variant? Are we not? Decide which move to display const baseMoveId = selectedMoveId || currentMoveId;
if (variant) { const { variant, moveIndex } = findMoveIndex(moves, baseMoveId);
const variantMoves = //Are we in a variant? Are we not? Decide which move to display
moves[variant.parentMoveIndex].variants[variant.variantIndex].moves;
if (typeof variantMoves[moveIndex + offset] !== 'undefined') { if (variant) {
moveToDisplay = variantMoves[moveIndex + offset]; 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];
}
} }
} else {
if (typeof moves[moveIndex + offset] !== 'undefined') { if (!moveToDisplay) {
moveToDisplay = moves[moveIndex + offset]; console.log(`No move to display found`);
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);
} }
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; return draft;
}; };
export const getCurrentMove = ( export const getCurrentMove = (
draft: Draft<GameState> draft: Draft<GameState>
): Draft<ChessStudyMove> | Draft<VariantMove> => { ): Draft<ChessStudyMove> | Draft<VariantMove> | null => {
const currentMoveId = draft.currentMove?.moveId; const currentMoveId = draft.currentMove?.moveId;
const moves = draft.study.moves; const moves = draft.study.moves;
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId); if (currentMoveId) {
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
if (variant) { if (variant) {
return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[ return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[
moveIndex moveIndex
]; ];
} else { } else {
return moves[moveIndex]; return moves[moveIndex];
}
} }
return null;
}; };

View file

@ -87,7 +87,7 @@
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: 4px; gap: 4px;
height: 35px; height: 32px;
} }
/* Move Items */ /* Move Items */
@ -95,23 +95,23 @@
.chess-study .move-item-container { .chess-study .move-item-container {
display: grid; display: grid;
grid-template-columns: 0.15fr 0.425fr 0.425fr; grid-template-columns: 0.15fr 0.425fr 0.425fr;
grid-auto-rows: minmax(30px, auto); grid-auto-rows: minmax(35px, auto);
width: 100%; width: 100%;
max-height: 380px; max-height: 380px;
overflow-y: scroll; overflow-y: scroll;
} }
.chess-study .move-item-container > * {
margin: 0;
}
.chess-study .move-item { .chess-study .move-item {
padding-left: 8px; padding-left: 8px;
user-select: none; user-select: none;
} }
.chess-study .move-item:hover { .chess-study .move-item:hover {
background-color: hsl( background-color: hsl(var(--accent-h), var(--accent-s), calc(var(--accent-l)));
var(--accent-h),
var(--accent-s),
calc(var(--accent-l))
);
color: var(--text-on-accent); color: var(--text-on-accent);
cursor: pointer; cursor: pointer;
} }

View file

@ -6,7 +6,7 @@ import {
ChessStudyFileData, ChessStudyFileData,
} from 'src/lib/storage'; } from 'src/lib/storage';
import { ReactView } from './components/ReactView'; import { ReactView } from './components/ReactView';
import { PgnModal } from './components/obsidian/PgnModal'; import { ChessStringModal } from './components/obsidian/ChessStringModal';
import { import {
ChessStudyPluginSettings, ChessStudyPluginSettings,
DEFAULT_SETTINGS, DEFAULT_SETTINGS,
@ -22,6 +22,17 @@ import { nanoid } from 'nanoid';
import { parseUserConfig } from './lib/obsidian'; import { parseUserConfig } from './lib/obsidian';
import './main.css'; import './main.css';
type FEN = string;
type PGN = string;
export type ChessString = FEN | PGN;
export const ROOT_FEN =
'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
// TODO:
// 1) Allow to show the root position
// 2) Display correct move after removing the last move
export default class ChessStudyPlugin extends Plugin { export default class ChessStudyPlugin extends Plugin {
settings: ChessStudyPluginSettings; settings: ChessStudyPluginSettings;
dataAdapter: ChessStudyDataAdapter; dataAdapter: ChessStudyDataAdapter;
@ -39,7 +50,7 @@ export default class ChessStudyPlugin extends Plugin {
this.storagePath this.storagePath
); );
this.dataAdapter.createStorageFolderIfNotExists(); await this.dataAdapter.createStorageFolderIfNotExists();
// Add settings tab // Add settings tab
this.addSettingTab(new SettingsTab(this.app, this)); this.addSettingTab(new SettingsTab(this.app, this));
@ -51,22 +62,17 @@ export default class ChessStudyPlugin extends Plugin {
editorCallback: (editor: Editor) => { editorCallback: (editor: Editor) => {
const cursorPosition = editor.getCursor(); const cursorPosition = editor.getCursor();
const onSubmit = async (pgn_or_fen: string) => { const onSubmit = async (chessString: ChessString | undefined) => {
try { try {
let pgn = '', fen = ''; const chessStringTrimmed = chessString?.trim() ?? '';
if (pgn_or_fen) {
if (pgn_or_fen.includes('/')) {
fen = pgn_or_fen.trim();
} else {
pgn = pgn_or_fen.trim();
}
}
const chess = (fen) ? new Chess(fen) : new Chess(); const isFen = chessStringTrimmed.includes('/');
if (pgn) { const chess = isFen ? new Chess(chessStringTrimmed) : new Chess();
if (!isFen) {
//Try to parse the PGN //Try to parse the PGN
chess.loadPgn(pgn, { chess.loadPgn(chessStringTrimmed, {
strict: false, strict: false,
}); });
} }
@ -83,18 +89,15 @@ export default class ChessStudyPlugin extends Plugin {
shapes: [], shapes: [],
comment: null, comment: null,
})), })),
rootFEN: isFen ? chessStringTrimmed : ROOT_FEN,
}; };
this.dataAdapter.createStorageFolderIfNotExists(); this.dataAdapter.createStorageFolderIfNotExists();
const id = await this.dataAdapter.saveFile(chessStudyFileData); const id = await this.dataAdapter.saveFile(chessStudyFileData);
const blockStr = (fen)
? `\`\`\`chessStudy\nchessStudyId: ${id}\nfen: ${fen}\n\`\`\``
: `\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``;
editor.replaceRange( editor.replaceRange(
blockStr, `\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``,
cursorPosition cursorPosition
); );
} catch (e) { } catch (e) {
@ -103,7 +106,7 @@ export default class ChessStudyPlugin extends Plugin {
} }
}; };
new PgnModal(this.app, onSubmit).open(); new ChessStringModal(this.app, onSubmit).open();
}, },
}); });
@ -123,14 +126,7 @@ export default class ChessStudyPlugin extends Plugin {
const data = await this.dataAdapter.loadFile(chessStudyId); const data = await this.dataAdapter.loadFile(chessStudyId);
ctx.addChild( ctx.addChild(
new ReactView( new ReactView(el, source, this.app, this.settings, data, this.dataAdapter)
el,
source,
this.app,
this.settings,
data,
this.dataAdapter
)
); );
} catch (e) { } catch (e) {
new Notice( new Notice(

File diff suppressed because one or more lines are too long