From 8039a3545dfc1606850914db5f1043ef47b50a6e Mon Sep 17 00:00:00 2001 From: Gato Date: Sun, 5 Jul 2026 06:17:12 +0200 Subject: [PATCH] feat: rechargement automatique des documents et sidebar redimensionnable Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.html | 2 ++ src/main.js | 5 +++- src/router.js | 57 ++++++++++++++++++++++++++++++++++++++-- src/state/app-state.js | 13 ++++++++- src/ui/reader.js | 2 ++ src/ui/sidebar-resize.js | 49 ++++++++++++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 4 deletions(-) create mode 100644 src/ui/sidebar-resize.js diff --git a/src/index.html b/src/index.html index c31305a..b52411e 100644 --- a/src/index.html +++ b/src/index.html @@ -22,6 +22,7 @@
+

Pena

@@ -47,6 +48,7 @@
+
diff --git a/src/main.js b/src/main.js index f6182ac..aaa84ba 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,6 @@ -import { showHome, openPath } from './router.js'; +import { showHome, openPath, initWatcher } from './router.js'; import { initCssModal } from './ui/css-modal.js'; +import { initSidebarResize } from './ui/sidebar-resize.js'; const win = window.__TAURI__.window.getCurrentWindow(); @@ -33,4 +34,6 @@ document.getElementById('btn-open-dir').addEventListener('click', async () => { document.getElementById('btn-back').addEventListener('click', showHome); await initCssModal(); +initSidebarResize(); +await initWatcher(); showHome(); diff --git a/src/router.js b/src/router.js index d3f5bad..ba3371f 100644 --- a/src/router.js +++ b/src/router.js @@ -1,5 +1,6 @@ import { appState } from './state/app-state.js'; import { listMdFiles } from './services/files.js'; +import { startWatch, stopWatch, onFileChanged } from './services/watcher.js'; import { renderSidebar } from './ui/sidebar.js'; import { loadPage } from './ui/reader.js'; import { saveRecent, renderRecents } from './ui/home.js'; @@ -11,9 +12,15 @@ const content = document.querySelector('#content'); const win = window.__TAURI__.window.getCurrentWindow(); const titlebarTitle = document.getElementById('titlebar-title'); +const navigate = filePath => loadPage(filePath, appState.currentMode, sidebarEl); + export function showHome() { + stopWatch(); appState.currentPath = null; appState.currentMode = null; + appState.baseDir = null; + appState.currentFile = null; + appState.files = []; sidebarEl.innerHTML = ''; win.setTitle('Pena — Markdown Viewer'); titlebarTitle.textContent = 'Pena — Markdown Viewer'; @@ -38,15 +45,21 @@ export async function openPath(path, mode) { const parentDir = path.split(sep).slice(0, -1).join(sep); try { const files = await listMdFiles(parentDir); + appState.baseDir = parentDir; + appState.files = files; sidebarEl.classList.remove('hidden'); content.style.marginLeft = ''; - renderSidebar(parentDir, files, path, filePath => loadPage(filePath, appState.currentMode, sidebarEl)); + renderSidebar(parentDir, files, path, navigate); } catch { + // Le dossier parent est illisible : on surveille au moins le fichier ouvert. + appState.baseDir = path; + appState.files = []; sidebarEl.innerHTML = ''; sidebarEl.classList.add('hidden'); content.style.marginLeft = '0'; } await loadPage(path, appState.currentMode, sidebarEl); + startWatch(appState.baseDir); return; } @@ -56,6 +69,8 @@ export async function openPath(path, mode) { try { const files = await listMdFiles(path); + appState.baseDir = path; + appState.files = files; showReader(); const sep = path.includes('\\') ? '\\' : '/'; @@ -65,10 +80,48 @@ export async function openPath(path, mode) { }); const firstFile = home ?? files[0]; - renderSidebar(path, files, firstFile, filePath => loadPage(filePath, appState.currentMode, sidebarEl)); + renderSidebar(path, files, firstFile, navigate); await loadPage(firstFile, appState.currentMode, sidebarEl); + startWatch(path); } catch (err) { showReader(); content.innerHTML = `

Impossible d'ouvrir le dossier : ${err}

`; } } + +// Compare deux listes de fichiers indépendamment de l'ordre. +function sameFiles(a, b) { + if (a.length !== b.length) return false; + const key = arr => arr.slice().sort().join('\n'); + return key(a) === key(b); +} + +// Réaction à un changement filesystem signalé par le backend (événement débouncé, +// filtré sur les fichiers .md). +async function handleFileChanged(changedPath) { + if (!appState.baseDir || !appState.currentMode) return; + + // Rafraîchir la sidebar uniquement si l'arborescence a changé (ajout/suppression), + // pour ne pas replier les dossiers ouverts à chaque simple modification de contenu. + try { + const files = await listMdFiles(appState.baseDir); + if (!sameFiles(files, appState.files)) { + appState.files = files; + renderSidebar(appState.baseDir, files, appState.currentFile, navigate); + } + } catch { + // baseDir supprimé/illisible : on garde la sidebar en l'état. + } + + // Recharger le document affiché si c'est lui qui vient d'être modifié. + if (changedPath === appState.currentFile) { + await loadPage(appState.currentFile, appState.currentMode, sidebarEl); + } +} + +// À appeler une fois au démarrage : enregistre le listener global de changements. +export function initWatcher() { + return onFileChanged(event => { + handleFileChanged(event.payload.path); + }); +} diff --git a/src/state/app-state.js b/src/state/app-state.js index a81822b..6458599 100644 --- a/src/state/app-state.js +++ b/src/state/app-state.js @@ -1 +1,12 @@ -export const appState = { currentPath: null, currentMode: null }; +export const appState = { + currentPath: null, + currentMode: null, + // Dossier réellement surveillé (racine de la sidebar) : le dossier ouvert en + // mode « dir », ou le dossier parent du fichier ouvert en mode « file ». + baseDir: null, + // Fichier actuellement affiché dans le lecteur. + currentFile: null, + // Dernière liste de fichiers .md connue de `baseDir`, pour détecter les + // ajouts/suppressions et ne rafraîchir la sidebar que si l'arborescence change. + files: [], +}; diff --git a/src/ui/reader.js b/src/ui/reader.js index f96b9bd..318542c 100644 --- a/src/ui/reader.js +++ b/src/ui/reader.js @@ -1,4 +1,5 @@ import { convertFile } from '../services/markdown.js'; +import { appState } from '../state/app-state.js'; const win = window.__TAURI__.window.getCurrentWindow(); const titlebarTitle = document.getElementById('titlebar-title'); @@ -69,6 +70,7 @@ function addCopyButtons() { export async function loadPage(filePath, mode, sidebarEl) { try { const html = await convertFile(filePath); + appState.currentFile = filePath; content.innerHTML = html; addCopyButtons(); diff --git a/src/ui/sidebar-resize.js b/src/ui/sidebar-resize.js new file mode 100644 index 0000000..1178310 --- /dev/null +++ b/src/ui/sidebar-resize.js @@ -0,0 +1,49 @@ +const STORAGE_KEY = 'pena.sidebarWidth'; +const MIN_WIDTH = 180; +const MAX_WIDTH = 520; + +function clamp(width) { + return Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, width)); +} + +function applyWidth(width) { + document.documentElement.style.setProperty('--sidebar-width', `${width}px`); +} + +export function initSidebarResize() { + const handle = document.getElementById('sidebar-resizer'); + if (!handle) return; + + const stored = parseInt(localStorage.getItem(STORAGE_KEY), 10); + if (!Number.isNaN(stored)) applyWidth(clamp(stored)); + + let dragging = false; + + const onMove = (e) => { + if (!dragging) return; + applyWidth(clamp(e.clientX)); + }; + + const onUp = () => { + if (!dragging) return; + dragging = false; + handle.classList.remove('is-dragging'); + document.body.classList.remove('is-resizing-sidebar'); + const width = parseInt( + getComputedStyle(document.documentElement).getPropertyValue('--sidebar-width'), + 10, + ); + if (!Number.isNaN(width)) localStorage.setItem(STORAGE_KEY, String(width)); + window.removeEventListener('mousemove', onMove); + window.removeEventListener('mouseup', onUp); + }; + + handle.addEventListener('mousedown', (e) => { + e.preventDefault(); + dragging = true; + handle.classList.add('is-dragging'); + document.body.classList.add('is-resizing-sidebar'); + window.addEventListener('mousemove', onMove); + window.addEventListener('mouseup', onUp); + }); +}