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="app-content">
|
||||||
|
|
||||||
<div id="view-home">
|
<div id="view-home">
|
||||||
|
<img class="home-logo" src="assets/icon.png" alt="Pena" />
|
||||||
<h1 class="home-title">Pena</h1>
|
<h1 class="home-title">Pena</h1>
|
||||||
<div class="home-buttons">
|
<div class="home-buttons">
|
||||||
<button id="btn-open-file">Ouvrir un fichier Markdown</button>
|
<button id="btn-open-file">Ouvrir un fichier Markdown</button>
|
||||||
@@ -47,6 +48,7 @@
|
|||||||
</button>
|
</button>
|
||||||
<button id="btn-back" class="btn-back">← Accueil</button>
|
<button id="btn-back" class="btn-back">← Accueil</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="sidebar-resizer" class="sidebar__resizer" title="Redimensionner"></div>
|
||||||
</div>
|
</div>
|
||||||
<div id="content" class="content">
|
<div id="content" class="content">
|
||||||
<!-- contenu généré par JS -->
|
<!-- 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 { initCssModal } from './ui/css-modal.js';
|
||||||
|
import { initSidebarResize } from './ui/sidebar-resize.js';
|
||||||
|
|
||||||
const win = window.__TAURI__.window.getCurrentWindow();
|
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);
|
document.getElementById('btn-back').addEventListener('click', showHome);
|
||||||
|
|
||||||
await initCssModal();
|
await initCssModal();
|
||||||
|
initSidebarResize();
|
||||||
|
await initWatcher();
|
||||||
showHome();
|
showHome();
|
||||||
|
|||||||
+55
-2
@@ -1,5 +1,6 @@
|
|||||||
import { appState } from './state/app-state.js';
|
import { appState } from './state/app-state.js';
|
||||||
import { listMdFiles } from './services/files.js';
|
import { listMdFiles } from './services/files.js';
|
||||||
|
import { startWatch, stopWatch, onFileChanged } from './services/watcher.js';
|
||||||
import { renderSidebar } from './ui/sidebar.js';
|
import { renderSidebar } from './ui/sidebar.js';
|
||||||
import { loadPage } from './ui/reader.js';
|
import { loadPage } from './ui/reader.js';
|
||||||
import { saveRecent, renderRecents } from './ui/home.js';
|
import { saveRecent, renderRecents } from './ui/home.js';
|
||||||
@@ -11,9 +12,15 @@ const content = document.querySelector('#content');
|
|||||||
const win = window.__TAURI__.window.getCurrentWindow();
|
const win = window.__TAURI__.window.getCurrentWindow();
|
||||||
const titlebarTitle = document.getElementById('titlebar-title');
|
const titlebarTitle = document.getElementById('titlebar-title');
|
||||||
|
|
||||||
|
const navigate = filePath => loadPage(filePath, appState.currentMode, sidebarEl);
|
||||||
|
|
||||||
export function showHome() {
|
export function showHome() {
|
||||||
|
stopWatch();
|
||||||
appState.currentPath = null;
|
appState.currentPath = null;
|
||||||
appState.currentMode = null;
|
appState.currentMode = null;
|
||||||
|
appState.baseDir = null;
|
||||||
|
appState.currentFile = null;
|
||||||
|
appState.files = [];
|
||||||
sidebarEl.innerHTML = '';
|
sidebarEl.innerHTML = '';
|
||||||
win.setTitle('Pena — Markdown Viewer');
|
win.setTitle('Pena — Markdown Viewer');
|
||||||
titlebarTitle.textContent = '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);
|
const parentDir = path.split(sep).slice(0, -1).join(sep);
|
||||||
try {
|
try {
|
||||||
const files = await listMdFiles(parentDir);
|
const files = await listMdFiles(parentDir);
|
||||||
|
appState.baseDir = parentDir;
|
||||||
|
appState.files = files;
|
||||||
sidebarEl.classList.remove('hidden');
|
sidebarEl.classList.remove('hidden');
|
||||||
content.style.marginLeft = '';
|
content.style.marginLeft = '';
|
||||||
renderSidebar(parentDir, files, path, filePath => loadPage(filePath, appState.currentMode, sidebarEl));
|
renderSidebar(parentDir, files, path, navigate);
|
||||||
} catch {
|
} catch {
|
||||||
|
// Le dossier parent est illisible : on surveille au moins le fichier ouvert.
|
||||||
|
appState.baseDir = path;
|
||||||
|
appState.files = [];
|
||||||
sidebarEl.innerHTML = '';
|
sidebarEl.innerHTML = '';
|
||||||
sidebarEl.classList.add('hidden');
|
sidebarEl.classList.add('hidden');
|
||||||
content.style.marginLeft = '0';
|
content.style.marginLeft = '0';
|
||||||
}
|
}
|
||||||
await loadPage(path, appState.currentMode, sidebarEl);
|
await loadPage(path, appState.currentMode, sidebarEl);
|
||||||
|
startWatch(appState.baseDir);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,6 +69,8 @@ export async function openPath(path, mode) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const files = await listMdFiles(path);
|
const files = await listMdFiles(path);
|
||||||
|
appState.baseDir = path;
|
||||||
|
appState.files = files;
|
||||||
showReader();
|
showReader();
|
||||||
|
|
||||||
const sep = path.includes('\\') ? '\\' : '/';
|
const sep = path.includes('\\') ? '\\' : '/';
|
||||||
@@ -65,10 +80,48 @@ export async function openPath(path, mode) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const firstFile = home ?? files[0];
|
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);
|
await loadPage(firstFile, appState.currentMode, sidebarEl);
|
||||||
|
startWatch(path);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
showReader();
|
showReader();
|
||||||
content.innerHTML = `<p class="error">Impossible d'ouvrir le dossier : ${err}</p>`;
|
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 { convertFile } from '../services/markdown.js';
|
||||||
|
import { appState } from '../state/app-state.js';
|
||||||
|
|
||||||
const win = window.__TAURI__.window.getCurrentWindow();
|
const win = window.__TAURI__.window.getCurrentWindow();
|
||||||
const titlebarTitle = document.getElementById('titlebar-title');
|
const titlebarTitle = document.getElementById('titlebar-title');
|
||||||
@@ -69,6 +70,7 @@ function addCopyButtons() {
|
|||||||
export async function loadPage(filePath, mode, sidebarEl) {
|
export async function loadPage(filePath, mode, sidebarEl) {
|
||||||
try {
|
try {
|
||||||
const html = await convertFile(filePath);
|
const html = await convertFile(filePath);
|
||||||
|
appState.currentFile = filePath;
|
||||||
content.innerHTML = html;
|
content.innerHTML = html;
|
||||||
addCopyButtons();
|
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