From 9667f90e27ac00d4a5ad3a60e22161eba4ebb64c Mon Sep 17 00:00:00 2001 From: TimLbhnschl Date: Mon, 12 Jan 2026 14:48:42 +0100 Subject: [PATCH 01/21] Save Game MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit you can save games, but sometimes can“t access input field --- src/renderer/components/PgnBrowser.vue | 24 +- src/renderer/components/SaveGameModal.vue | 382 ++++++++++++++++++++++ 2 files changed, 405 insertions(+), 1 deletion(-) create mode 100644 src/renderer/components/SaveGameModal.vue diff --git a/src/renderer/components/PgnBrowser.vue b/src/renderer/components/PgnBrowser.vue index 8bcc5278..ee32846a 100644 --- a/src/renderer/components/PgnBrowser.vue +++ b/src/renderer/components/PgnBrowser.vue @@ -24,6 +24,12 @@ class="icon mdi mdi-plus-box-outline" @click="openAddPgnModal()" /> +
+
+ + + + From 20fc7a5165a9293f8e3dd7ecc9347bc1022f2c16 Mon Sep 17 00:00:00 2001 From: TimLbhnschl Date: Tue, 20 Jan 2026 13:59:44 +0100 Subject: [PATCH 02/21] Visual update --- src/renderer/components/SaveGameModal.vue | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/SaveGameModal.vue b/src/renderer/components/SaveGameModal.vue index 21742814..6ccba0ec 100644 --- a/src/renderer/components/SaveGameModal.vue +++ b/src/renderer/components/SaveGameModal.vue @@ -306,7 +306,7 @@ export default { border: 1px solid var(--main-border-color); border-radius: 4px; background-color: var(--button-color); - color: var(--main-text-color); + color: white; font-size: 14px; box-sizing: border-box; } @@ -319,6 +319,7 @@ export default { .inputGroup input::placeholder { color: var(--light-text-color); + opacity: 0.5; } .infoText { From de2144dc8f04af4f9311d26a4f186d537b644a7a Mon Sep 17 00:00:00 2001 From: TimLbhnschl Date: Tue, 20 Jan 2026 14:07:20 +0100 Subject: [PATCH 03/21] lint update --- src/renderer/components/PgnBrowser.vue | 2 +- src/renderer/components/SaveGameModal.vue | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/PgnBrowser.vue b/src/renderer/components/PgnBrowser.vue index ee32846a..b5783881 100644 --- a/src/renderer/components/PgnBrowser.vue +++ b/src/renderer/components/PgnBrowser.vue @@ -27,8 +27,8 @@
diff --git a/src/renderer/components/SaveGameModal.vue b/src/renderer/components/SaveGameModal.vue index 6ccba0ec..fb14c65a 100644 --- a/src/renderer/components/SaveGameModal.vue +++ b/src/renderer/components/SaveGameModal.vue @@ -118,7 +118,7 @@ export default { } }, mounted () { - //gotta look into this again later dosen't seem to work + // gotta look into this again later dosen't seem to work // Ensure the input field is focused when modal opens this.$nextTick(() => { const input = document.getElementById('gameName') From 55b1f19edf300b7b303b7abc4a26642887879525 Mon Sep 17 00:00:00 2001 From: TimLbhnschl Date: Mon, 26 Jan 2026 11:09:47 +0100 Subject: [PATCH 04/21] Save to local disk Games now get saved on your local disk instead of only storing it temporarily --- src/main/index.js | 22 +++++++++++ src/renderer/components/SaveGameModal.vue | 48 +++++++++++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/src/main/index.js b/src/main/index.js index d75136d3..097f42bc 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -66,6 +66,28 @@ ipcMain.handle('show-open-dialog', async (event, options) => { } }) +// IPC handler for save-as dialog +ipcMain.handle('show-save-dialog', async (event, options) => { + const browserWindow = mainWindow || null + try { + const res = await dialog.showSaveDialog(browserWindow, options || {}) + return res + } catch (err) { + return { canceled: true, filePath: '' } + } +}) + +// IPC handler for writing files +ipcMain.handle('write-file', async (event, filePath, content) => { + const fs = require('fs') + try { + fs.writeFileSync(filePath, content, 'utf8') + return { success: true } + } catch (err) { + return { success: false, error: err.message } + } +}) + // Build and show a context menu requested from renderer. The renderer // sends a simplified template (no functions) and we build native menu // items whose click handlers forward a message back to the renderer. diff --git a/src/renderer/components/SaveGameModal.vue b/src/renderer/components/SaveGameModal.vue index fb14c65a..df9e7328 100644 --- a/src/renderer/components/SaveGameModal.vue +++ b/src/renderer/components/SaveGameModal.vue @@ -131,7 +131,7 @@ export default { cancel () { this.$emit('close') }, - save () { + async save () { if (this.gameName.trim() === '') { alert('Please enter a game name') return @@ -160,14 +160,54 @@ export default { // Update store this.$store.dispatch('loadedGames', games) - // Close modal - this.$emit('close') - alert(`Game "${this.gameName}" saved successfully!`) + // Ask user if they want to save to file + const saveToFile = confirm('Do you want to save this game to a file on your computer?') + if (saveToFile) { + await this.saveToFile(pgn) + } else { + this.$emit('close') + alert(`Game "${this.gameName}" saved successfully!`) + } } catch (error) { console.error('Error saving game:', error) alert('Error saving game: ' + error.message) } }, + async saveToFile (pgn) { + const { ipcRenderer } = require('electron') + + try { + // Show save dialog + const result = await ipcRenderer.invoke('show-save-dialog', { + title: 'Save Game', + defaultPath: `${this.gameName}.pgn`, + filters: [ + { name: 'PGN Files', extensions: ['pgn'] }, + { name: 'All Files', extensions: ['*'] } + ] + }) + + if (result.canceled) { + // User canceled, just close the modal + this.$emit('close') + alert(`Game "${this.gameName}" saved to library!`) + return + } + + // Write the file + const writeResult = await ipcRenderer.invoke('write-file', result.filePath, pgn) + + if (writeResult.success) { + this.$emit('close') + alert(`Game "${this.gameName}" saved successfully to:\n${result.filePath}`) + } else { + alert(`Error writing file: ${writeResult.error}`) + } + } catch (error) { + console.error('Error in saveToFile:', error) + alert('Error saving file: ' + error.message) + } + }, generatePGN () { // Build the header section let pgn = '' From 8ad2b11157db17b411711a00cdc4580f84c6ce7b Mon Sep 17 00:00:00 2001 From: TimLbhnschl Date: Mon, 26 Jan 2026 11:43:07 +0100 Subject: [PATCH 05/21] Saving gamePaths to JSON works but everything else gets saved in browser cach of app --- src/main/gameStorage.js | 79 +++++++++++++++++++++++ src/main/index.js | 43 ++++++++++++ src/renderer/components/LandingPage.vue | 38 +++++++++++ src/renderer/components/SaveGameModal.vue | 3 + 4 files changed, 163 insertions(+) create mode 100644 src/main/gameStorage.js diff --git a/src/main/gameStorage.js b/src/main/gameStorage.js new file mode 100644 index 00000000..6f3977aa --- /dev/null +++ b/src/main/gameStorage.js @@ -0,0 +1,79 @@ +import fs from 'fs' +import path from 'path' +import { app } from 'electron' + +const appDataPath = app.getPath('userData') +const savedGamesPath = path.join(appDataPath, 'savedGames.json') + +/** + * Load all saved game file paths + */ +export function loadSavedGamePaths () { + try { + if (fs.existsSync(savedGamesPath)) { + const data = fs.readFileSync(savedGamesPath, 'utf8') + return JSON.parse(data) + } + } catch (error) { + console.error('Error loading saved game paths:', error) + } + return { games: [] } +} + +/** + * Save a game file path to the registry + */ +export function addGamePath (filePath) { + try { + const data = loadSavedGamePaths() + + // Check if path already exists + if (!data.games.includes(filePath)) { + data.games.push(filePath) + fs.writeFileSync(savedGamesPath, JSON.stringify(data, null, 2), 'utf8') + } + return true + } catch (error) { + console.error('Error adding game path:', error) + return false + } +} + +/** + * Remove a game file path from the registry + */ +export function removeGamePath (filePath) { + try { + const data = loadSavedGamePaths() + data.games = data.games.filter(p => p !== filePath) + fs.writeFileSync(savedGamesPath, JSON.stringify(data, null, 2), 'utf8') + return true + } catch (error) { + console.error('Error removing game path:', error) + return false + } +} + +/** + * Get all saved game file paths + */ +export function getAllSavedGamePaths () { + const data = loadSavedGamePaths() + const existingPaths = [] + + for (const filePath of data.games) { + try { + if (fs.existsSync(filePath)) { + existingPaths.push(filePath) + } else { + // File no longer exists, remove it from the list + removeGamePath(filePath) + } + } catch (error) { + console.error(`Error checking game file ${filePath}:`, error) + } + } + + return existingPaths +} + diff --git a/src/main/index.js b/src/main/index.js index 097f42bc..08a9b397 100644 --- a/src/main/index.js +++ b/src/main/index.js @@ -1,6 +1,7 @@ 'use strict' import { app, BrowserWindow, dialog, ipcMain, Menu } from 'electron' +import { loadSavedGamePaths, addGamePath, removeGamePath, getAllSavedGamePaths } from './gameStorage' /** * Set `__static` path to static files in production @@ -118,6 +119,48 @@ ipcMain.handle('show-context-menu', async (event, template) => { return false } }) + +// IPC handler to load all saved game paths +ipcMain.handle('load-saved-games', async (event) => { + try { + const paths = getAllSavedGamePaths() + return { success: true, paths } + } catch (err) { + return { success: false, error: err.message } + } +}) + +// IPC handler to add a game path to the saved games list +ipcMain.handle('add-game-path', async (event, filePath) => { + try { + const success = addGamePath(filePath) + return { success } + } catch (err) { + return { success: false, error: err.message } + } +}) + +// IPC handler to remove a game path from the saved games list +ipcMain.handle('remove-game-path', async (event, filePath) => { + try { + const success = removeGamePath(filePath) + return { success } + } catch (err) { + return { success: false, error: err.message } + } +}) + +// IPC handler to read a PGN file content +ipcMain.handle('read-pgn-file', async (event, filePath) => { + const fs = require('fs') + try { + const content = fs.readFileSync(filePath, 'utf8') + return { success: true, content } + } catch (err) { + return { success: false, error: err.message } + } +}) + /** * Auto Updater * diff --git a/src/renderer/components/LandingPage.vue b/src/renderer/components/LandingPage.vue index 34de9183..700e9b5f 100644 --- a/src/renderer/components/LandingPage.vue +++ b/src/renderer/components/LandingPage.vue @@ -6,6 +6,7 @@ + + diff --git a/src/renderer/components/AddPgnModal.vue b/src/renderer/components/AddPgnModal.vue index b09729f2..ce1f82c1 100644 --- a/src/renderer/components/AddPgnModal.vue +++ b/src/renderer/components/AddPgnModal.vue @@ -158,6 +158,8 @@ export default { return } currentGameCount++ + // Store the original PGN text for comment extraction + game.originalPGN = match games.push(game) }) } diff --git a/src/renderer/components/LandingPage.vue b/src/renderer/components/LandingPage.vue index 700e9b5f..2a5d81ad 100644 --- a/src/renderer/components/LandingPage.vue +++ b/src/renderer/components/LandingPage.vue @@ -38,6 +38,8 @@ export default { game.id = gameId++ game.supported = true game.filePath = filePath + // Store the original PGN for comment extraction + game.originalPGN = fileResult.content games.push(game) } } catch (error) { diff --git a/src/renderer/components/MenuBar.vue b/src/renderer/components/MenuBar.vue index 2d85b9ba..e25c1b78 100644 --- a/src/renderer/components/MenuBar.vue +++ b/src/renderer/components/MenuBar.vue @@ -137,6 +137,8 @@ export default { return } currentGameCount++ + // Store the original PGN text for comment extraction + game.originalPGN = match games.push(game) }) } diff --git a/src/renderer/components/MoveHistoryNode.vue b/src/renderer/components/MoveHistoryNode.vue index 609300d9..65d2d28d 100644 --- a/src/renderer/components/MoveHistoryNode.vue +++ b/src/renderer/components/MoveHistoryNode.vue @@ -20,17 +20,29 @@ {{ checkCheckmate }} + + {{ '{' + move.comment + '}' }} + +
  • + {{ move.comment ? 'Edit Comment' : 'Add Comment' }} +
  • Delete Variation
  • + t && t.trim()) + + let moveIndex = 0 + let lastWasMove = false + + for (let i = 0; i < tokens.length; i++) { + const token = tokens[i] + + // Skip move numbers and empty tokens + if (token.match(/^\d+\.\.?$/) || !token.trim() || token === '*') { + continue + } + + // Check if this is a comment + if (token.match(/^\{[^}]*\}$/)) { + // Extract comment text without braces + const commentText = token.replace(/^\{/, '').replace(/\}$/, '') + // Associate comment with the current move (just played) + if (lastWasMove) { + commentMap[moveIndex - 1] = commentText + lastWasMove = false + } + } else if (token.trim()) { + // This is a move + moveIndex++ + lastWasMove = true + } + } + + return commentMap +} + export const store = new Vuex.Store({ state: { engineIndex: 1, @@ -453,7 +503,12 @@ export const store = new Vuex.Store({ const sanMove = state.board.sanMove(curVal) state.board.push(curVal) this.commit('playAudio', sanMove) - return { ply: ply, name: sanMove, fen: state.board.fen(), uci: curVal, whitePocket: state.board.pocket(true), blackPocket: state.board.pocket(false), main: undefined, next: [], prev: prev } + const moveObj = { ply: ply, name: sanMove, fen: state.board.fen(), uci: curVal, whitePocket: state.board.pocket(true), blackPocket: state.board.pocket(false), main: undefined, next: [], prev: prev } + // Add comment if provided (only for the first move in the sequence) + if (idx === 0 && payload.comment) { + moveObj.comment = payload.comment + } + return moveObj })) if (payload.prev) { // if the move is not a starting move prev.next.push(state.moves[state.moves.length - 1]) // the last entry in moves is the move object of the current move @@ -1166,11 +1221,18 @@ export const store = new Vuex.Store({ context.commit('selectedGame', payload.game) context.commit('gameInfo', gameInfo) const moves = payload.game.mainlineMoves().split(' ') + + // Parse comments from the original PGN if available + let commentMap = {} + if (payload.game.originalPGN) { + commentMap = extractCommentsFromPGN(payload.game.originalPGN) + } + for (const num in moves) { if (num === 0) { - context.commit('appendMoves', { move: moves[num], prev: undefined }) + context.commit('appendMoves', { move: moves[num], prev: undefined, comment: commentMap[0] }) } else { - context.commit('appendMoves', { move: moves[num], prev: context.state.moves[num - 1] }) // TODO differentiate between alternative lines + context.commit('appendMoves', { move: moves[num], prev: context.state.moves[num - 1], comment: commentMap[num] }) // TODO differentiate between alternative lines } } context.dispatch('updateBoard') From bd3fe2b91b9c7b200b41aec0c5824089d13898cb Mon Sep 17 00:00:00 2001 From: TimLbhnschl Date: Mon, 26 Jan 2026 13:16:11 +0100 Subject: [PATCH 07/21] now can use letters n/p in comment --- src/renderer/components/AddCommentModal.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/renderer/components/AddCommentModal.vue b/src/renderer/components/AddCommentModal.vue index 652fd4a0..a2f7e7fa 100644 --- a/src/renderer/components/AddCommentModal.vue +++ b/src/renderer/components/AddCommentModal.vue @@ -1,5 +1,5 @@