5 Commits

Author SHA1 Message Date
Gato 61fb56fa14 chore: icônes de bundle, packaging Flatpak et bump version 0.1.2
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:17:56 +02:00
Gato 8039a3545d feat: rechargement automatique des documents et sidebar redimensionnable
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:17:12 +02:00
Gato fe0476218d feat: export systematique de pena.flatpak lors du build
Le mode par defaut installe (--user) ET exporte le bundle pena.flatpak a cote
du script. Le flag --bundle devient --no-install (bundle seul, alias conserve).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 08:11:27 +02:00
Gato 1220ce1bb0 fix: tri naturel de la sidebar et police uniforme dossiers/fichiers
Les entrees a prefixe numerique etaient triees lexicographiquement (10 avant 2).
Passage a un tri naturel via Intl.Collator numeric. Les libelles de dossiers
adoptent desormais la meme taille et graisse que les fichiers (chevron seul
marqueur visuel).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 07:54:19 +02:00
Gato 4bb2c6cc77 feat: build flatpak dans distrobox ubuntu-rust + sync version metainfo
Le build cargo tauri echouait sur l hote atomique (libs GTK/WebKit absentes) ;
il tourne desormais dans le distrobox ubuntu-rust. La version applicative est
lue depuis tauri.conf.json et repercutee dans le metainfo AppStream.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 07:37:17 +02:00
69 changed files with 288 additions and 64 deletions
+1
View File
@@ -6,4 +6,5 @@ flatpak/.build/
flatpak/.repo/ flatpak/.repo/
flatpak/.flatpak-builder/ flatpak/.flatpak-builder/
flatpak/pena.deb flatpak/pena.deb
flatpak/icon.png
flatpak/*.flatpak flatpak/*.flatpak
+79 -27
View File
@@ -6,17 +6,20 @@
# 1. `cargo tauri build --bundles deb` -> produit le .deb autonome # 1. `cargo tauri build --bundles deb` -> produit le .deb autonome
# 2. Copie du .deb à côté du manifeste sous le nom `pena.deb` # 2. Copie du .deb à côté du manifeste sous le nom `pena.deb`
# 3. `flatpak-builder` construit le bundle dans la sandbox GNOME # 3. `flatpak-builder` construit le bundle dans la sandbox GNOME
# 4. Installation locale (--user) et/ou export en fichier .flatpak # 4. Installation locale (--user) ET export du fichier pena.flatpak
#
# Le fichier `pena.flatpak` est TOUJOURS exporté à côté du script.
# #
# Usage : # Usage :
# ./build-flatpak.sh # build + installation locale (--user) # ./build-flatpak.sh # build + installation (--user) + pena.flatpak
# ./build-flatpak.sh --bundle # build + export d'un fichier Pena.flatpak # ./build-flatpak.sh --no-install # build + pena.flatpak seul (pas d'installation)
# ./build-flatpak.sh --no-deb # réutilise le .deb déjà présent (build rapide) # ./build-flatpak.sh --no-deb # réutilise le .deb déjà présent (build rapide)
# #
set -euo pipefail set -euo pipefail
APP_ID="com.pena.app" APP_ID="com.pena.app"
RUNTIME_VERSION="48" RUNTIME_VERSION="48"
RUST_CONTAINER="ubuntu-rust" # distrobox contenant cargo-tauri + libs GTK/WebKit
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # .../Pena-tauri PROJECT_DIR="$(dirname "$SCRIPT_DIR")" # .../Pena-tauri
@@ -24,14 +27,16 @@ MANIFEST="$SCRIPT_DIR/${APP_ID}.yml"
BUILD_DIR="$SCRIPT_DIR/.build" # arbre de build flatpak-builder BUILD_DIR="$SCRIPT_DIR/.build" # arbre de build flatpak-builder
REPO_DIR="$SCRIPT_DIR/.repo" # dépôt ostree temporaire REPO_DIR="$SCRIPT_DIR/.repo" # dépôt ostree temporaire
DEB_TARGET="$SCRIPT_DIR/pena.deb" DEB_TARGET="$SCRIPT_DIR/pena.deb"
TAURI_CONF="$PROJECT_DIR/src-tauri/tauri.conf.json"
METAINFO="$SCRIPT_DIR/${APP_ID}.metainfo.xml"
DO_BUNDLE=0 DO_INSTALL=1
BUILD_DEB=1 BUILD_DEB=1
for arg in "$@"; do for arg in "$@"; do
case "$arg" in case "$arg" in
--bundle) DO_BUNDLE=1 ;; --no-install|--bundle) DO_INSTALL=0 ;; # --bundle : alias rétrocompatible
--no-deb) BUILD_DEB=0 ;; --no-deb) BUILD_DEB=0 ;;
-h|--help) sed -n '2,16p' "$0"; exit 0 ;; -h|--help) sed -n '2,17p' "$0"; exit 0 ;;
*) echo "Argument inconnu : $arg" >&2; exit 1 ;; *) echo "Argument inconnu : $arg" >&2; exit 1 ;;
esac esac
done done
@@ -39,26 +44,61 @@ done
# --- Vérification des outils ------------------------------------------------- # --- Vérification des outils -------------------------------------------------
need() { command -v "$1" >/dev/null 2>&1 || { echo "Outil manquant : $1" >&2; exit 1; }; } need() { command -v "$1" >/dev/null 2>&1 || { echo "Outil manquant : $1" >&2; exit 1; }; }
need flatpak need flatpak
if ! command -v flatpak-builder >/dev/null 2>&1; then
# flatpak-builder : commande native, sinon le Flatpak org.flatpak.Builder
# (cas des distributions atomiques type Bazzite/Silverblue).
if command -v flatpak-builder >/dev/null 2>&1; then
FLATPAK_BUILDER=(flatpak-builder)
elif flatpak info org.flatpak.Builder >/dev/null 2>&1; then
FLATPAK_BUILDER=(flatpak run org.flatpak.Builder)
else
echo "flatpak-builder est requis. Installez-le :" >&2 echo "flatpak-builder est requis. Installez-le :" >&2
echo " flatpak install -y flathub org.flatpak.Builder" >&2 echo " flatpak install -y flathub org.flatpak.Builder" >&2
echo " (puis lancez ce script via : flatpak run org.flatpak.Builder ...)" >&2 echo " ou sur Fedora classique : sudo dnf install flatpak-builder" >&2
echo " ou sur Fedora : sudo dnf install flatpak-builder" >&2
exit 1 exit 1
fi fi
# --- Runtime / SDK GNOME ----------------------------------------------------- # --- Remote flathub (scope --user) -------------------------------------------
echo ">> Vérification du runtime GNOME ${RUNTIME_VERSION}" # flatpak-builder est lancé en --user et installe les dépendances (runtime/SDK
if ! flatpak info "org.gnome.Platform//${RUNTIME_VERSION}" >/dev/null 2>&1; then # via --install-deps-from=flathub) dans cette même installation. Le remote
flatpak install -y --user flathub \ # flathub doit donc exister au niveau --user, ce qui n'est pas le cas par
"org.gnome.Platform//${RUNTIME_VERSION}" \ # défaut sur les distributions atomiques (où flathub est un remote système).
"org.gnome.Sdk//${RUNTIME_VERSION}" echo ">> Vérification du remote flathub (--user)…"
flatpak remote-add --if-not-exists --user flathub \
https://flathub.org/repo/flathub.flatpakrepo
# --- 0. Version : source de vérité = tauri.conf.json -------------------------
# On lit la version applicative depuis tauri.conf.json et on la répercute dans
# le metainfo AppStream (release) afin que le Flatpak porte toujours la
# dernière version déclarée dans les sources.
need jq
VERSION="$(jq -r '.version' "$TAURI_CONF")"
if [[ -z "$VERSION" || "$VERSION" == "null" ]]; then
echo "Impossible de lire la version dans $TAURI_CONF" >&2
exit 1
fi
TODAY="$(date -u +%Y-%m-%d)"
echo ">> Version applicative : $VERSION (build du $TODAY)"
# Met à jour (ou insère) l'entrée <release> du metainfo pour cette version.
if grep -q "version=\"$VERSION\"" "$METAINFO"; then
echo ">> Release $VERSION déjà présente dans le metainfo."
else
echo ">> Ajout de la release $VERSION au metainfo…"
sed -i \
"s#<releases>#<releases>\n <release version=\"$VERSION\" date=\"$TODAY\"/>#" \
"$METAINFO"
fi fi
# --- 1. Build du .deb via Tauri --------------------------------------------- # --- 1. Build du .deb via Tauri ---------------------------------------------
if [[ "$BUILD_DEB" -eq 1 ]]; then if [[ "$BUILD_DEB" -eq 1 ]]; then
echo ">> Build du paquet .deb (cargo tauri build --bundles deb)…" # La compilation Tauri exige les libs de dev GTK/WebKit, absentes de l'hôte
( cd "$PROJECT_DIR" && cargo tauri build --bundles deb ) # (distribution atomique). On build donc dans le distrobox `$RUST_CONTAINER`,
# qui embarque cargo-tauri et ces libs. flatpak-builder reste sur l'hôte.
need distrobox
echo ">> Build du paquet .deb dans le distrobox $RUST_CONTAINER"
distrobox enter "$RUST_CONTAINER" -- \
bash -lc "cd '$PROJECT_DIR' && cargo tauri build --bundles deb"
fi fi
DEB_SRC="$(find "$PROJECT_DIR/src-tauri/target/release/bundle/deb" -name '*.deb' 2>/dev/null | sort | tail -n1 || true)" DEB_SRC="$(find "$PROJECT_DIR/src-tauri/target/release/bundle/deb" -name '*.deb' 2>/dev/null | sort | tail -n1 || true)"
@@ -69,24 +109,36 @@ fi
echo ">> Utilisation du .deb : $DEB_SRC" echo ">> Utilisation du .deb : $DEB_SRC"
cp -f "$DEB_SRC" "$DEB_TARGET" cp -f "$DEB_SRC" "$DEB_TARGET"
# Icône installée dans le Flatpak (128x128) depuis les sources du projet.
cp -f "$PROJECT_DIR/src-tauri/icons/128x128.png" "$SCRIPT_DIR/icon.png"
# --- 2. Build Flatpak -------------------------------------------------------- # --- 2. Build Flatpak --------------------------------------------------------
# Construit l'app dans un dépôt OSTree local ($REPO_DIR) ; les dépendances
# manquantes (runtime/SDK) sont récupérées depuis flathub.
echo ">> Construction du Flatpak…" echo ">> Construction du Flatpak…"
rm -rf "$BUILD_DIR" rm -rf "$BUILD_DIR"
FB_ARGS=(--force-clean --user --repo="$REPO_DIR") FB_ARGS=(--force-clean --user --install-deps-from=flathub --repo="$REPO_DIR")
if [[ "$DO_BUNDLE" -eq 0 ]]; then if [[ "$DO_INSTALL" -eq 1 ]]; then
FB_ARGS+=(--install) FB_ARGS+=(--install)
fi fi
flatpak-builder "${FB_ARGS[@]}" "$BUILD_DIR" "$MANIFEST" "${FLATPAK_BUILDER[@]}" "${FB_ARGS[@]}" "$BUILD_DIR" "$MANIFEST"
# --- 3. Export d'un fichier .flatpak (optionnel) ----------------------------- # --- 3. Export du fichier .flatpak (toujours) --------------------------------
if [[ "$DO_BUNDLE" -eq 1 ]]; then # Le bundle autonome est systématiquement exporté à côté du manifeste.
OUT="$SCRIPT_DIR/Pena.flatpak" OUT="$SCRIPT_DIR/pena.flatpak"
echo ">> Export du bundle : $OUT" echo ">> Export du bundle : $OUT"
flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" "$RUNTIME_VERSION" 2>/dev/null \ flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" \
|| flatpak build-bundle "$REPO_DIR" "$OUT" "$APP_ID" --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo
echo ">> Installer avec : flatpak install --user $OUT"
echo ">> Nettoyage du dépôt OSTree temporaire…"
rm -rf "$REPO_DIR"
if [[ "$DO_INSTALL" -eq 1 ]]; then
echo ">> Pena $VERSION installé (--user) et bundle exporté : $OUT"
echo ">> Lancer avec : flatpak run $APP_ID"
else else
echo ">> Installé. Lancer avec : flatpak run $APP_ID" echo ">> Bundle exporté : $OUT"
echo ">> Installer avec : flatpak install --user $OUT"
fi fi
echo ">> Terminé." echo ">> Terminé."
+1
View File
@@ -19,6 +19,7 @@
</categories> </categories>
<content_rating type="oars-1.1"/> <content_rating type="oars-1.1"/>
<releases> <releases>
<release version="0.1.2" date="2026-07-05"/>
<release version="0.1.0" date="2026-06-23"/> <release version="0.1.0" date="2026-06-23"/>
</releases> </releases>
</component> </component>
+4 -3
View File
@@ -21,6 +21,8 @@ modules:
path: pena.deb path: pena.deb
- type: file - type: file
path: com.pena.app.metainfo.xml path: com.pena.app.metainfo.xml
- type: file
path: icon.png
build-commands: build-commands:
- | - |
set -eux set -eux
@@ -40,9 +42,8 @@ modules:
-e 's/^Icon=.*/Icon=com.pena.app/' \ -e 's/^Icon=.*/Icon=com.pena.app/' \
/app/share/applications/com.pena.app.desktop /app/share/applications/com.pena.app.desktop
# Icône : on prend la plus grande disponible # Icône (128x128) fournie séparément à côté du manifeste.
icon="$(find usr/share/icons -name '*.png' | sort | tail -n1)" install -Dm644 icon.png /app/share/icons/hicolor/128x128/apps/com.pena.app.png
install -Dm644 "$icon" /app/share/icons/hicolor/128x128/apps/com.pena.app.png
# Métadonnées AppStream # Métadonnées AppStream
install -Dm644 com.pena.app.metainfo.xml /app/share/metainfo/com.pena.app.metainfo.xml install -Dm644 com.pena.app.metainfo.xml /app/share/metainfo/com.pena.app.metainfo.xml
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

+1 -1
View File
@@ -2445,7 +2445,7 @@ dependencies = [
[[package]] [[package]]
name = "pena-taury" name = "pena-taury"
version = "0.1.0" version = "0.1.2"
dependencies = [ dependencies = [
"comrak", "comrak",
"notify", "notify",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pena-taury" name = "pena-taury"
version = "0.1.0" version = "0.1.2"
edition = "2021" edition = "2021"
[lib] [lib]
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
<background android:drawable="@color/ic_launcher_background"/>
</adaptive-icon>
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#fff</color>
</resources>
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 B

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 928 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

+8 -2
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Pena", "productName": "Pena",
"version": "0.1.0", "version": "0.1.2",
"identifier": "com.pena.app", "identifier": "com.pena.app",
"build": { "build": {
"frontendDist": "../src" "frontendDist": "../src"
@@ -24,6 +24,12 @@
"bundle": { "bundle": {
"active": false, "active": false,
"targets": "all", "targets": "all",
"icon": [] "icon": [
"icons/32x32.png",
"icons/128x128.png",
"icons/128x128@2x.png",
"icons/icon.icns",
"icons/icon.ico"
]
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

+2
View File
@@ -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
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 { 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
View File
@@ -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
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: [],
};
+42 -19
View File
@@ -1,4 +1,5 @@
/* ── Reset ── */ /* ── Reset ── */
:root { --sidebar-width: 264px; }
*, *::before, *::after { box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
::selection { background: #ffc3c3; } ::selection { background: #ffc3c3; }
html, body { height: 100%; margin: 0; } html, body { height: 100%; margin: 0; }
@@ -91,6 +92,13 @@ body {
background: #1a1b2e; background: #1a1b2e;
} }
.home-logo {
width: 160px;
height: 160px;
object-fit: contain;
margin: 0 0 16px;
}
.home-title { .home-title {
color: #fff; color: #fff;
font-size: 4rem; font-size: 4rem;
@@ -206,7 +214,7 @@ body {
position: fixed; position: fixed;
top: 36px; top: 36px;
left: 0; left: 0;
width: 264px; width: var(--sidebar-width);
height: calc(100vh - 36px); height: calc(100vh - 36px);
background: #1a1b2e; background: #1a1b2e;
display: flex; display: flex;
@@ -215,6 +223,22 @@ body {
overflow: hidden; overflow: hidden;
} }
/* Poignée de redimensionnement */
.sidebar__resizer {
position: absolute;
top: 0;
right: 0;
width: 6px;
height: 100%;
cursor: col-resize;
z-index: 100;
background: transparent;
transition: background 0.15s;
}
.sidebar__resizer:hover,
.sidebar__resizer.is-dragging { background: rgba(129, 140, 248, 0.4); }
body.is-resizing-sidebar { cursor: col-resize; user-select: none; }
.sidebar__scroll { .sidebar__scroll {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -315,10 +339,10 @@ body {
.nav-root-link { .nav-root-link {
display: block; display: block;
padding: 7px 24px; padding: 6px 24px;
color: rgba(255,255,255,0.85); color: rgba(255,255,255,0.85);
text-decoration: none; text-decoration: none;
font-size: 16px; font-size: 14px;
font-weight: 400; font-weight: 400;
line-height: 1.4; line-height: 1.4;
transition: color 0.2s; transition: color 0.2s;
@@ -358,33 +382,32 @@ body {
.nav-arrow:hover { opacity: 1; background: rgba(255,255,255,0.08); } .nav-arrow:hover { opacity: 1; background: rgba(255,255,255,0.08); }
.nav-folder[open] > summary .nav-arrow { transform: rotate(90deg); opacity: 0.8; } .nav-folder[open] > summary .nav-arrow { transform: rotate(90deg); opacity: 0.8; }
/* Les libellés de dossiers partagent la même police que les fichiers :
même taille (14px), même graisse (400), sans majuscules. Seule la flèche ▶
distingue visuellement un dossier d'un fichier. */
.nav-folder-link, .nav-folder-name { .nav-folder-link, .nav-folder-name {
flex: 1; flex: 1;
display: block; display: block;
padding: 6px 24px 6px 0; padding: 6px 24px 6px 0;
font-size: 11px; font-size: 14px;
font-weight: 700; font-weight: 400;
text-transform: uppercase; line-height: 1.4;
letter-spacing: 0.08em; color: rgba(255,255,255,0.85);
color: rgba(255,255,255,0.35);
text-decoration: none; text-decoration: none;
transition: color 0.2s; transition: color 0.2s;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.nav-folder-link:hover { color: rgba(255,255,255,0.85); } .nav-folder-link:hover { color: #ff5577; }
.nav-folder-link.active { color: #ff5577; } .nav-folder-link.active { color: #ff5577; }
/* Dossiers imbriqués : même style que les fichiers imbriqués (couleur atténuée). */
.nav-folder .nav-folder > summary .nav-folder-link, .nav-folder .nav-folder > summary .nav-folder-link,
.nav-folder .nav-folder > summary .nav-folder-name { .nav-folder .nav-folder > summary .nav-folder-name {
font-size: 13px; color: rgba(255,255,255,0.55);
font-weight: 500;
text-transform: none;
letter-spacing: 0;
color: rgba(255,255,255,0.45);
} }
.nav-folder .nav-folder > summary .nav-folder-link:hover { color: rgba(255,255,255,0.9); } .nav-folder .nav-folder > summary .nav-folder-link:hover { color: #ff5577; }
.nav-file-link { .nav-file-link {
display: block; display: block;
@@ -402,16 +425,16 @@ body {
font-weight: 500; font-weight: 500;
} }
.nav-depth-1 > .nav-file-link { padding: 5px 24px 5px 52px; } .nav-depth-1 > .nav-file-link { padding: 6px 24px 6px 52px; }
.nav-depth-1 > .nav-folder > summary { padding-left: 24px; } .nav-depth-1 > .nav-folder > summary { padding-left: 24px; }
.nav-depth-2 > .nav-file-link { padding: 4px 24px 4px 68px; } .nav-depth-2 > .nav-file-link { padding: 6px 24px 6px 68px; }
.nav-depth-2 > .nav-folder > summary { padding-left: 40px; } .nav-depth-2 > .nav-folder > summary { padding-left: 40px; }
.nav-depth-3 > .nav-file-link { padding: 4px 24px 4px 84px; } .nav-depth-3 > .nav-file-link { padding: 6px 24px 6px 84px; }
.nav-depth-3 > .nav-folder > summary { padding-left: 56px; } .nav-depth-3 > .nav-folder > summary { padding-left: 56px; }
/* ── Content ── */ /* ── Content ── */
.content { .content {
margin-left: 264px; margin-left: var(--sidebar-width);
padding-top: 48px; padding-top: 48px;
padding-right: 72px; padding-right: 72px;
padding-bottom: 96px; padding-bottom: 96px;
+2
View File
@@ -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();
+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);
});
}
+16 -5
View File
@@ -1,5 +1,13 @@
import { escapeHtml } from '../utils.js'; import { escapeHtml } from '../utils.js';
// Tri « naturel » : les préfixes numériques sont comparés comme des nombres
// (10 vient après 9, pas après 1).
const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
function cleanLabel(name) {
return name.replace(/\.md$/i, '').replace(/-/g, ' ');
}
function buildTree(relPaths, sep) { function buildTree(relPaths, sep) {
const tree = { _files: [], _dirs: {} }; const tree = { _files: [], _dirs: {} };
relPaths.forEach((rel) => { relPaths.forEach((rel) => {
@@ -19,8 +27,11 @@ function renderTree(node, prefix, sep, currentRel, depth) {
let html = ''; let html = '';
const depthClass = depth > 0 ? ` class="nav-depth-${Math.min(depth, 3)}"` : ''; const depthClass = depth > 0 ? ` class="nav-depth-${Math.min(depth, 3)}"` : '';
node._files.slice().sort().forEach((rel) => { node._files
const label = rel.split(sep).pop().replace(/\.md$/i, '').replace(/-/g, ' '); .slice()
.sort((a, b) => collator.compare(a.split(sep).pop(), b.split(sep).pop()))
.forEach((rel) => {
const label = cleanLabel(rel.split(sep).pop());
const abs = prefix + rel; const abs = prefix + rel;
const cls = rel === currentRel ? ' active' : ''; const cls = rel === currentRel ? ' active' : '';
if (depth === 0) { if (depth === 0) {
@@ -30,7 +41,7 @@ function renderTree(node, prefix, sep, currentRel, depth) {
} }
}); });
Object.keys(node._dirs).sort().forEach((dirName) => { Object.keys(node._dirs).sort((a, b) => collator.compare(a, b)).forEach((dirName) => {
const child = node._dirs[dirName]; const child = node._dirs[dirName];
const isOpen = currentRel && currentRel.split(sep).includes(dirName); const isOpen = currentRel && currentRel.split(sep).includes(dirName);
const openAttr = isOpen ? ' open' : ''; const openAttr = isOpen ? ' open' : '';
@@ -40,9 +51,9 @@ function renderTree(node, prefix, sep, currentRel, depth) {
if (homeFile) { if (homeFile) {
const abs = prefix + homeFile; const abs = prefix + homeFile;
const cls = homeFile === currentRel ? ' active' : ''; const cls = homeFile === currentRel ? ' active' : '';
folderLabel = `<a href="#" class="nav-folder-link${cls}" data-path="${escapeHtml(abs)}" onclick="event.stopPropagation()">${escapeHtml(dirName)}</a>`; folderLabel = `<a href="#" class="nav-folder-link${cls}" data-path="${escapeHtml(abs)}" onclick="event.stopPropagation()">${escapeHtml(cleanLabel(dirName))}</a>`;
} else { } else {
folderLabel = `<span class="nav-folder-name">${escapeHtml(dirName)}</span>`; folderLabel = `<span class="nav-folder-name">${escapeHtml(cleanLabel(dirName))}</span>`;
} }
const arrow = `<span class="nav-arrow" onclick="event.preventDefault();event.stopPropagation();var d=this.closest('details');d.open=!d.open">▶</span>`; const arrow = `<span class="nav-arrow" onclick="event.preventDefault();event.stopPropagation();var d=this.closest('details');d.open=!d.open">▶</span>`;