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

View file

@ -1,6 +1,6 @@
{
"singleQuote": true,
"tabWidth": 2,
"tabWidth": 1,
"proseWrap": "preserve",
"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)
- [Usage](#usage)
- [Features](#features)
- [1.0.0](#100)
- [1.1.0](#110)
- [1.0.0](#100)
- [1.1.0](#110)
- [1.2.0](#120)
- [Settings](#settings)
- [Roadmap](#roadmap)
- [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)
### 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
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 |
| `boardOrientation` | `white` \| `black` | Orientation 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 |
You can permanently set some settings in the [Obsidian](https://obsidian.md/) plugin settings for Obsidian Chess Study.
## 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 more styles
- [ ] Add more settings
- [ ] Support canvas view
- [ ] Mobile support
## Tools Used

View file

@ -1,7 +1,8 @@
import { App, Modal, Setting } from 'obsidian';
import { ChessString } from 'src/main';
export class PgnModal extends Modal {
pgn: string;
export class ChessStringModal extends Modal {
chessString: ChessString;
onSubmit: (pgn: string) => void;
constructor(app: App, onSubmit: (pgn: string) => void) {
@ -13,13 +14,13 @@ export class PgnModal extends Modal {
const { contentEl } = this;
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
.onChange((value) => {
this.pgn = value;
this.chessString = value;
})
.inputEl.setCssStyles({ width: '100%', height: '250px' })
);
@ -30,7 +31,7 @@ export class PgnModal extends Modal {
.setCta()
.onClick(() => {
this.close();
this.onSubmit(this.pgn);
this.onSubmit(this.chessString);
})
);
}

View file

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

View file

@ -35,7 +35,7 @@ interface AppProps {
}
export interface GameState {
currentMove: ChessStudyMove | VariantMove;
currentMove: ChessStudyMove | VariantMove | null;
isViewOnly: boolean;
study: ChessStudyFileData;
}
@ -56,16 +56,15 @@ export const ChessStudy = ({
dataAdapter,
}: AppProps) => {
// Parse Obsidian / Code Block Settings
const { boardColor, boardOrientation, fen, viewComments, chessStudyId } =
const { boardColor, boardOrientation, viewComments, chessStudyId } =
parseUserConfig(pluginSettings, source);
// Setup Chessground API
const [chessView, setChessView] = useState<Api | null>(null);
// Setup Chess.js API
const [firstPlayer, initialMoveNumber, initialChessLogic] = useMemo(() => {
const chess = (fen) ? new Chess(fen) : new Chess();
const [initialChessLogic, firstPlayer, initialMoveNumber] = useMemo(() => {
const chess = new Chess(chessStudyData.rootFEN);
const firstPlayer = chess.turn();
const initialMoveNumber = chess.moveNumber();
@ -78,16 +77,17 @@ export const ChessStudy = ({
});
});
return [firstPlayer, initialMoveNumber, chess];
}, [chessStudyData.moves]);
return [chess, firstPlayer, initialMoveNumber];
}, [chessStudyData.moves, chessStudyData.rootFEN]);
const [chessLogic, setChessLogic] = useState(initialChessLogic);
const [gameState, dispatch] = useImmerReducer<GameState, GameActions>(
(draft, action) => {
const hasNoMoves = draft.study.moves.length === 0;
switch (action.type) {
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, {
offset: 1,
@ -97,7 +97,7 @@ export const ChessStudy = ({
return draft;
}
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, {
offset: -1,
@ -107,56 +107,61 @@ export const ChessStudy = ({
return draft;
}
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 { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
if (currentMoveId) {
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
if (variant) {
const parent = moves[variant.parentMoveIndex];
const variantMoves = parent.variants[variant.variantIndex].moves;
if (variant) {
const parent = moves[variant.parentMoveIndex];
const variantMoves = parent.variants[variant.variantIndex].moves;
const isLastMove = moveIndex === variantMoves.length - 1;
const isLastMove = moveIndex === variantMoves.length - 1;
if (isLastMove) {
displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1,
selectedMoveId: currentMoveId,
});
}
if (isLastMove) {
displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1,
selectedMoveId: currentMoveId,
});
}
variantMoves.pop();
if (variantMoves.length == 0) {
parent.variants.splice(variant.variantIndex, 1);
}
variantMoves.pop();
if (variantMoves.length === 0) {
parent.variants.splice(variant.variantIndex, 1);
}
if (isLastMove) {
draft.currentMove = (variantMoves.length > 0) ? variantMoves[variantMoves.length - 1] : moves[variant.parentMoveIndex];
}
} else {
const isLastMove = moveIndex === moves.length - 1;
if (isLastMove) {
draft.currentMove =
variantMoves.length > 0
? variantMoves[variantMoves.length - 1]
: moves[variant.parentMoveIndex];
}
} else {
const isLastMove = moveIndex === moves.length - 1;
if (isLastMove) {
displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1,
selectedMoveId: currentMoveId,
});
}
if (isLastMove) {
displayMoveInHistory(draft, chessView, setChessLogic, {
offset: -1,
selectedMoveId: currentMoveId,
});
}
moves.pop();
moves.pop();
if (isLastMove) {
draft.currentMove = (moves.length > 0) ? moves[moves.length - 1] : null;
if (isLastMove) {
draft.currentMove = moves.length > 0 ? moves[moves.length - 1] : null;
}
}
}
return draft;
}
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;
@ -168,22 +173,26 @@ export const ChessStudy = ({
return draft;
}
case 'SYNC_SHAPES': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft;
if (!chessView || hasNoMoves) return draft;
const move = getCurrentMove(draft);
move.shapes = action.shapes;
draft.currentMove = move;
if (move) {
move.shapes = action.shapes;
draft.currentMove = move;
}
return draft;
}
case 'SYNC_COMMENT': {
if (!chessView || !draft || draft.study.moves.length == 0) return draft;
if (!chessView || hasNoMoves) return draft;
const move = getCurrentMove(draft);
move.comment = action.comment;
draft.currentMove = move;
if (move) {
move.comment = action.comment;
draft.currentMove = move;
}
return draft;
}
@ -193,80 +202,94 @@ export const ChessStudy = ({
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;
if (currentMoveId) {
const currentMoveIndex = moves.findIndex(
(move) => move.moveId === currentMoveId
);
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 (isLastMove) {
const move = {
...newMove,
moveId: moveId,
shapes: [],
comment: null,
};
variantMoves.push(move);
if (variant) {
//handle variant
const parent = moves[variant.parentMoveIndex];
const variantMoves = parent.variants[variant.variantIndex].moves;
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({
fen: newMove.after,
check: tempChess.isCheck(),
});
const tempChess = new Chess(newMove.after);
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 {
//handle main line
const isLastMove = currentMoveIndex === moves.length - 1;
const move = {
...newMove,
moveId: moveId,
variants: [],
shapes: [],
comment: null,
};
moves.push(move);
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;
}
draft.currentMove = move;
}
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,
study: chessStudyData,
}
@ -310,14 +333,14 @@ export const ChessStudy = ({
syncShapes={(shapes: DrawShape[]) =>
dispatch({ type: 'SYNC_SHAPES', shapes })
}
shapes={gameState.currentMove?.shapes}
shapes={gameState.currentMove?.shapes || []}
/>
</div>
<div className="pgn-container">
<PgnViewer
history={gameState.study.moves}
currentMoveId={gameState.currentMove?.moveId}
currentMoveId={gameState.currentMove?.moveId ?? null}
firstPlayer={firstPlayer}
initialMoveNumber={initialMoveNumber}
onUndoButtonClick={() =>
@ -338,7 +361,7 @@ export const ChessStudy = ({
onSaveButtonClick={onSaveButtonClick}
onCopyButtonClick={() => {
try {
navigator.clipboard.writeText(chessLogic.fen())
navigator.clipboard.writeText(chessLogic.fen());
new Notice('Copied to clipboard!');
} catch (e) {
new Notice('Could not copy to clipboard:', e);
@ -350,7 +373,7 @@ export const ChessStudy = ({
{viewComments && (
<div className="CommentSection">
<CommentSection
currentComment={gameState.currentMove?.comment}
currentComment={gameState.currentMove?.comment ?? null}
setComments={(comment: JSONContent) =>
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 { ReactNode, useEffect, useMemo, useRef } from 'react';
import { useMemo } from 'react';
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) => {
const chunkIndex = Math.floor((index + ((offsetByOne) ? 1 : 0)) / chunkSize);
const chunkIndex = Math.floor((index + (offsetByOne ? 1 : 0)) / chunkSize);
if (!resultArray[chunkIndex]) {
resultArray[chunkIndex] = [];
@ -17,307 +18,187 @@ const chunkArray = <T,>(array: T[], chunkSize: number, offsetByOne: boolean = fa
}, [] as T[][]);
};
const MoveItem = ({
isCurrentMove,
san,
onMoveItemClick,
export const VariantMoveItemContainer = ({
children,
}: {
isCurrentMove: boolean;
san: string;
onMoveItemClick: () => void;
children: React.ReactNode;
}) => {
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>;
};
const VariantContainer = ({ children }: { children: ReactNode }) => {
export const VariantContainer = ({
children,
}: {
children: React.ReactNode;
}) => {
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>;
};
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,
currentMoveId,
firstPlayer,
initialMoveNumber,
onUndoButtonClick,
onBackButtonClick,
onForwardButtonClick,
onMoveItemClick,
onSaveButtonClick,
onCopyButtonClick,
}: {
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]);
...controlActions
} = props;
return (
<div className="height-width-100">
<div className="move-item-section">
<div className="move-item-container">
{movePairs.map((pair, currentMoveIndex) => {
const [wMove, bMove] = pair;
const movePairs = useMemo(
() => chunkArray(history, 2, firstPlayer === 'b'),
[firstPlayer, history]
);
return (
<React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}>
<p className="move-indicator center">
{currentMoveIndex + initialMoveNumber}
</p>
{(firstPlayer === 'b' && bMove === undefined && currentMoveIndex === 0) && (
<MoveItem
san={'...'}
isCurrentMove={false}
onMoveItemClick={() => {}}
/>
)}
return (
<div className="height-width-100">
<div className="move-item-section">
<div className="move-item-container">
{movePairs.map((pair, currentMoveIndex) => {
const [wMove, bMove] = pair;
return (
<React.Fragment key={wMove.san + bMove?.san + currentMoveIndex}>
<p className="move-indicator center">
{currentMoveIndex + initialMoveNumber}
</p>
{firstPlayer === 'b' && !bMove && currentMoveIndex === 0 && (
<MoveItem
san={wMove.san}
isCurrentMove={wMove.moveId === currentMoveId}
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
san={'...'}
isCurrentMove={false}
onMoveItemClick={() => {}}
/>
{bMove && (
<MoveItem
san={bMove.san}
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;
)}
<MoveItem
san={wMove.san}
isCurrentMove={wMove.moveId === currentMoveId}
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
/>
{bMove && (
<MoveItem
san={bMove.san}
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 &&
(firstPlayer === 'w'
|| currentMoveIndex > 0) &&
`${
currentMoveIndex +
initialMoveNumber +
wMoveVarianti
}... `) ||
(firstPlayer === 'b' &&
currentMoveIndex === 0 &&
`${
currentMoveIndex +
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={
return (
<React.Fragment
key={bMove.san + wMove?.san + currentMoveIndex}
>
<VariantMoveItem
isCurrentMove={bMove.moveId === currentMoveId}
san={bMove.san}
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
moveIndicator={
(wMoveVarianti === 0 &&
(firstPlayer === 'w' || currentMoveIndex > 0) &&
`${
currentMoveIndex + initialMoveNumber + wMoveVarianti
}... `) ||
(firstPlayer === 'b' &&
currentMoveIndex === 0 &&
`${
currentMoveIndex + initialMoveNumber + wMoveVarianti
}. `) ||
null
}
/>
{wMove && (
<VariantMoveItem
isCurrentMove={wMove.moveId === currentMoveId}
san={wMove.san}
onMoveItemClick={() => onMoveItemClick(wMove.moveId)}
moveIndicator={
((firstPlayer === 'w' || currentMoveIndex > 0) &&
`${
currentMoveIndex
+ initialMoveNumber + 1
+ bMoveVarianti
}. `
}
/>
{bMove && (
<VariantMoveItem
isCurrentMove={
bMove.moveId === currentMoveId
}
san={bMove.san}
onMoveItemClick={() =>
onMoveItemClick(bMove.moveId)
}
/>
)}
</React.Fragment>
);
}
)}
</VariantMoveItemContainer>
);
})}
</VariantContainer>
)}
</VariantsContainer>
)}
</React.Fragment>
);
})}
</div>
</div>
<div className="button-section">
<button onClick={() => onUndoButtonClick()}>
<Undo2 />
</button>
<button onClick={() => onBackButtonClick()}>
<ArrowLeft />
</button>
<button onClick={() => onForwardButtonClick()}>
<ArrowRight />
</button>
</div>
<div className="button-section">
<button onClick={() => onSaveButtonClick()}>
<Save strokeWidth={'1px'} />
</button>
<button onClick={() => onCopyButtonClick()}>
<Copy strokeWidth={'1px'} />
</button>
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 + initialMoveNumber + 1 + bMoveVarianti
}. `}
/>
{bMove && (
<VariantMoveItem
isCurrentMove={bMove.moveId === currentMoveId}
san={bMove.san}
onMoveItemClick={() => onMoveItemClick(bMove.moveId)}
/>
)}
</React.Fragment>
);
})}
</VariantMoveItemContainer>
);
})}
</VariantContainer>
)}
</VariantsContainer>
)}
</React.Fragment>
);
})}
</div>
</div>
);
}
);
<Controls {...controlActions} />
</div>
);
});
PgnViewer.displayName = 'PgnViewer';

View file

@ -3,14 +3,9 @@ import { Move } from 'chess.js';
import { DrawShape } from 'chessground/draw';
import { nanoid } from 'nanoid';
import { DataAdapter, normalizePath } from 'obsidian';
import { ROOT_FEN } from 'src/main';
export const CURRENT_STORAGE_VERSION = '0.0.1';
export interface VariantMove extends Move {
moveId: string;
shapes: DrawShape[];
comment: JSONContent | null;
}
export const CURRENT_STORAGE_VERSION = '0.0.2';
export interface Variant {
variantId: string;
@ -18,6 +13,12 @@ export interface Variant {
moves: VariantMove[];
}
export interface VariantMove extends Move {
moveId: string;
shapes: DrawShape[];
comment: JSONContent | null;
}
export interface ChessStudyMove extends Move {
moveId: string;
variants: Variant[];
@ -29,6 +30,7 @@ export interface ChessStudyFileData {
version: string;
header: { title: string | null };
moves: ChessStudyMove[];
rootFEN: string;
}
export class ChessStudyDataAdapter {
@ -42,6 +44,13 @@ export class ChessStudyDataAdapter {
async saveFile(data: ChessStudyFileData, id?: string) {
const chessStudyId = id || nanoid();
console.log(
`Writing file to ${normalizePath(
`${this.storagePath}/${chessStudyId}.json`
)}`
);
await this.adapter.write(
normalizePath(`${this.storagePath}/${chessStudyId}.json`),
JSON.stringify(data, null, 2),
@ -52,16 +61,29 @@ export class ChessStudyDataAdapter {
}
async loadFile(id: string): Promise<ChessStudyFileData> {
console.log(
`Reading file from ${normalizePath(`${this.storagePath}/${id}.json`)}`
);
const data = await this.adapter.read(
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() {
const folderExists = await this.adapter.exists(this.storagePath);
if (!folderExists) {
console.log(`Creating storage folder at: ${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 };
for (const [iVariant, variant] of move.variants.entries()) {
const moveIndex = variant.moves.findIndex(
(move) => move.moveId === moveId
);
const moveIndex = variant.moves.findIndex((move) => move.moveId === moveId);
if (moveIndex >= 0)
return {
@ -48,64 +46,74 @@ export const displayMoveInHistory = (
//Figure out where we are
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 baseMoveId = selectedMoveId || currentMoveId;
const moves = draft.study.moves;
const { variant, moveIndex } = findMoveIndex(moves, baseMoveId);
//Are we in a variant? Are we not? Decide which move to display
//If we pass a moveId, find out where that is and offset from there, otherwise take current moveId
const baseMoveId = selectedMoveId || currentMoveId;
if (variant) {
const variantMoves =
moves[variant.parentMoveIndex].variants[variant.variantIndex].moves;
const { variant, moveIndex } = findMoveIndex(moves, baseMoveId);
//Are we in a variant? Are we not? Decide which move to display
if (typeof variantMoves[moveIndex + offset] !== 'undefined') {
moveToDisplay = variantMoves[moveIndex + offset];
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];
}
}
} else {
if (typeof moves[moveIndex + offset] !== 'undefined') {
moveToDisplay = moves[moveIndex + offset];
if (!moveToDisplay) {
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;
};
export const getCurrentMove = (
draft: Draft<GameState>
): Draft<ChessStudyMove> | Draft<VariantMove> => {
): Draft<ChessStudyMove> | Draft<VariantMove> | null => {
const currentMoveId = draft.currentMove?.moveId;
const moves = draft.study.moves;
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
if (currentMoveId) {
const { variant, moveIndex } = findMoveIndex(moves, currentMoveId);
if (variant) {
return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[
moveIndex
];
} else {
return moves[moveIndex];
if (variant) {
return moves[variant.parentMoveIndex].variants[variant.variantIndex].moves[
moveIndex
];
} else {
return moves[moveIndex];
}
}
return null;
};

View file

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

View file

@ -6,7 +6,7 @@ import {
ChessStudyFileData,
} from 'src/lib/storage';
import { ReactView } from './components/ReactView';
import { PgnModal } from './components/obsidian/PgnModal';
import { ChessStringModal } from './components/obsidian/ChessStringModal';
import {
ChessStudyPluginSettings,
DEFAULT_SETTINGS,
@ -22,6 +22,17 @@ import { nanoid } from 'nanoid';
import { parseUserConfig } from './lib/obsidian';
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 {
settings: ChessStudyPluginSettings;
dataAdapter: ChessStudyDataAdapter;
@ -39,7 +50,7 @@ export default class ChessStudyPlugin extends Plugin {
this.storagePath
);
this.dataAdapter.createStorageFolderIfNotExists();
await this.dataAdapter.createStorageFolderIfNotExists();
// Add settings tab
this.addSettingTab(new SettingsTab(this.app, this));
@ -51,22 +62,17 @@ export default class ChessStudyPlugin extends Plugin {
editorCallback: (editor: Editor) => {
const cursorPosition = editor.getCursor();
const onSubmit = async (pgn_or_fen: string) => {
const onSubmit = async (chessString: ChessString | undefined) => {
try {
let pgn = '', fen = '';
if (pgn_or_fen) {
if (pgn_or_fen.includes('/')) {
fen = pgn_or_fen.trim();
} else {
pgn = pgn_or_fen.trim();
}
}
const chessStringTrimmed = chessString?.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
chess.loadPgn(pgn, {
chess.loadPgn(chessStringTrimmed, {
strict: false,
});
}
@ -83,18 +89,15 @@ export default class ChessStudyPlugin extends Plugin {
shapes: [],
comment: null,
})),
rootFEN: isFen ? chessStringTrimmed : ROOT_FEN,
};
this.dataAdapter.createStorageFolderIfNotExists();
const id = await this.dataAdapter.saveFile(chessStudyFileData);
const blockStr = (fen)
? `\`\`\`chessStudy\nchessStudyId: ${id}\nfen: ${fen}\n\`\`\``
: `\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``;
editor.replaceRange(
blockStr,
`\`\`\`chessStudy\nchessStudyId: ${id}\n\`\`\``,
cursorPosition
);
} 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);
ctx.addChild(
new ReactView(
el,
source,
this.app,
this.settings,
data,
this.dataAdapter
)
new ReactView(el, source, this.app, this.settings, data, this.dataAdapter)
);
} catch (e) {
new Notice(

File diff suppressed because one or more lines are too long