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:
@@ -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
@@ -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
@@ -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
@@ -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: [],
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user