feat: Add persistence for loaded file, scroll position, speed, and font size.

This commit is contained in:
2025-12-12 15:38:33 +01:00
parent c414258ae1
commit d70db77141
3 changed files with 123 additions and 4 deletions
+22
View File
@@ -62,3 +62,25 @@ ipcMain.handle('read-file', async (event, filePath: string) => {
throw err; throw err;
} }
}); });
const configPath = path.join(app.getPath('userData'), 'config.json');
ipcMain.handle('load-settings', async () => {
try {
if (fs.existsSync(configPath)) {
const data = fs.readFileSync(configPath, 'utf-8');
return JSON.parse(data);
}
} catch (error) {
console.error('Failed to load settings:', error);
}
return {};
});
ipcMain.handle('save-settings', async (event, settings) => {
try {
fs.writeFileSync(configPath, JSON.stringify(settings, null, 2));
} catch (error) {
console.error('Failed to save settings:', error);
}
});
+3 -1
View File
@@ -2,5 +2,7 @@ import { contextBridge, ipcRenderer } from 'electron';
contextBridge.exposeInMainWorld('electronAPI', { contextBridge.exposeInMainWorld('electronAPI', {
openFileDialog: () => ipcRenderer.invoke('open-file-dialog'), openFileDialog: () => ipcRenderer.invoke('open-file-dialog'),
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath) readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
saveSettings: (settings: any) => ipcRenderer.invoke('save-settings', settings),
loadSettings: () => ipcRenderer.invoke('load-settings')
}); });
+98 -3
View File
@@ -1,6 +1,19 @@
interface AppSettings {
filePath?: string;
scrollTop?: number;
speed?: number;
fontSize?: number;
windowSize?: { width: number; height: number }; // Optional: we could save window size too, but user asked for "position size and speed" of app logic mostly. But "position size" could mean window. Let's stick to content first as requested "position" usually means scroll position in this context. "Position size" might be a typo for "Position, size". I will assume scroll position + font size + speed.
// Re-reading: "Loaded file, position size and speed". Comma is missing? "Loaded file, position, size and speed".
// I will save: filePath, scrollTop, speed, fontSize.
}
interface ElectronAPI { interface ElectronAPI {
openFileDialog: () => Promise<string | null>; openFileDialog: () => Promise<string | null>;
readFile: (path: string) => Promise<string>; readFile: (path: string) => Promise<string>;
loadSettings: () => Promise<AppSettings>;
saveSettings: (settings: AppSettings) => Promise<void>;
} }
interface Window { interface Window {
@@ -27,11 +40,70 @@ let isScrolling = false;
let scrollAccumulator = 0; let scrollAccumulator = 0;
let animationId: number; let animationId: number;
let scrollSpeed = parseInt(inputSpeed.value, 10); let scrollSpeed = parseInt(inputSpeed.value, 10);
let currentFilePath: string | undefined;
// Debounce helper
function debounce(func: Function, wait: number) {
let timeout: any;
return function (...args: any[]) {
clearTimeout(timeout);
timeout = setTimeout(() => func(...args), wait);
};
}
const saveState = debounce(async () => {
const api = (window as unknown as Window).electronAPI;
const settings: AppSettings = {
filePath: currentFilePath,
scrollTop: scrollContainer.scrollTop,
speed: parseInt(inputSpeed.value, 10),
fontSize: parseInt(inputSize.value, 10)
};
await api.saveSettings(settings);
}, 1000); // Save every 1s max if changing
// Initialize // Initialize
function init() { async function init() {
setupEventListeners(); setupEventListeners();
await restoreSettings();
}
async function restoreSettings() {
const api = (window as unknown as Window).electronAPI;
const settings = await api.loadSettings();
if (settings.speed) {
inputSpeed.value = settings.speed.toString();
scrollSpeed = settings.speed;
}
if (settings.fontSize) {
inputSize.value = settings.fontSize.toString();
// Update immediately
// Note: updateFontSize adds 'px'.
// Logic inside updateSettings handled this but I will call it explicitly.
}
// Apply font size before rendering to ensure correct height calc
updateFontSize(inputSize.value); updateFontSize(inputSize.value);
if (settings.filePath) {
try {
const content = await api.readFile(settings.filePath);
currentFilePath = settings.filePath;
renderMarkdown(content);
// Restore scroll position after render
if (settings.scrollTop) {
// Determine if we need to requestAnimationFrame to wait for layout
setTimeout(() => {
scrollContainer.scrollTop = settings.scrollTop || 0;
}, 100);
}
} catch (e) {
console.warn("Could not load last file:", e);
}
}
} }
function setupEventListeners() { function setupEventListeners() {
@@ -51,16 +123,26 @@ function setupEventListeners() {
btnReset.addEventListener('click', () => { btnReset.addEventListener('click', () => {
stopScrolling(); stopScrolling();
scrollContainer.scrollTo({ top: 0, behavior: 'smooth' }); scrollContainer.scrollTo({ top: 0, behavior: 'smooth' });
saveState();
}); });
inputSpeed.addEventListener('input', (e: Event) => { inputSpeed.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
scrollSpeed = parseInt(target.value, 10); scrollSpeed = parseInt(target.value, 10);
saveState();
}); });
inputSize.addEventListener('input', (e: Event) => { inputSize.addEventListener('input', (e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
updateFontSize(target.value); updateFontSize(target.value);
saveState();
});
// Save scroll position when scrolling manually
scrollContainer.addEventListener('scroll', () => {
if (!isScrolling) {
saveState();
}
}); });
} }
@@ -69,8 +151,10 @@ async function openFile() {
const api = (window as unknown as Window).electronAPI; const api = (window as unknown as Window).electronAPI;
const filePath = await api.openFileDialog(); const filePath = await api.openFileDialog();
if (filePath) { if (filePath) {
currentFilePath = filePath; // Update current file path
const content = await api.readFile(filePath); const content = await api.readFile(filePath);
renderMarkdown(content); renderMarkdown(content);
saveState();
} }
} }
@@ -85,8 +169,12 @@ function renderMarkdown(content: string) {
placeholder.style.display = 'none'; placeholder.style.display = 'none';
markdownOutput.style.display = 'block'; markdownOutput.style.display = 'block';
// Reset scroll // Reset scroll if it's a new open action (handled by resetting scrollTop in openFile if desired,
scrollContainer.scrollTop = 0; // but here we might abuse this for restoreSettings.
// For manual open, we probably want reset. For restore, we don't.
// I moved reset to openFile logic implicitly by not passing scrollTop?
// Actually, generic render shouldn't rely on it.
// Let's rely on caller to set scroll.
} }
function updateFontSize(size: string) { function updateFontSize(size: string) {
@@ -118,6 +206,7 @@ function stopScrolling() {
iconPlay.style.display = 'block'; iconPlay.style.display = 'block';
iconPause.style.display = 'none'; iconPause.style.display = 'none';
cancelAnimationFrame(animationId); cancelAnimationFrame(animationId);
saveState(); // Save on stop
} }
function step() { function step() {
@@ -141,9 +230,15 @@ function step() {
scrollContainer.scrollTop += pixelsToScroll; scrollContainer.scrollTop += pixelsToScroll;
scrollAccumulator -= pixelsToScroll; scrollAccumulator -= pixelsToScroll;
// We could saveState here but it would be too frequent.
// We rely on "stopScrolling" or the periodic saver if we implement it inside loop.
// Actually, let's use the debounced saveState inside the loop to ensure crashes during scroll are saved reasonably often.
saveState();
} }
animationId = requestAnimationFrame(step); animationId = requestAnimationFrame(step);
} }
init(); init();