feat: rechargement automatique des documents et sidebar redimensionnable

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 06:17:12 +02:00
parent fe0476218d
commit 8039a3545d
6 changed files with 124 additions and 4 deletions
+2
View File
@@ -22,6 +22,7 @@
<div id="app-content">
<div id="view-home">
<img class="home-logo" src="assets/icon.png" alt="Pena" />
<h1 class="home-title">Pena</h1>
<div class="home-buttons">
<button id="btn-open-file">Ouvrir un fichier Markdown</button>
@@ -47,6 +48,7 @@
</button>
<button id="btn-back" class="btn-back">← Accueil</button>
</div>
<div id="sidebar-resizer" class="sidebar__resizer" title="Redimensionner"></div>
</div>
<div id="content" class="content">
<!-- contenu généré par JS -->
+4 -1
View File
@@ -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();
+55 -2
View File
@@ -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 = `<p class="error">Impossible d'ouvrir le dossier : ${err}</p>`;
}
}
// 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);
});
}
+12 -1
View File
@@ -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: [],
};
+2
View File
@@ -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();
+49
View File
@@ -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);
});
}