fix: revue complète obsigate-desktop — 6 correctifs pour le prochain build
CI / lint (push) Successful in 24s
CI / security (push) Successful in 13s
CI / test (push) Successful in 27s
CI / build (push) Successful in 9s
CI / e2e (push) Failing after 26s
Desktop Build / build-windows (push) Has been cancelled
Desktop Build / build-linux (push) Has been cancelled

1. main.rs: .current_dir(exe_dir) pour que Python trouve backend/ (pas le cwd hérité)
2. build-windows.bat: dir bundles pointe vers target/x86_64-.../release (était target/release)
3. build-windows.bat: supprimer étape 3 inutile (frontend-static) — Tauri utilise ../frontend
4. capabilities/default.json: requis par Tauri v2 (permissions plugins)
5. tauri.conf.json + Cargo.toml: retirer updater (placeholder sans clé)
6. .gitignore: ajouter desktop/target/, python-embed/, frontend-static/
7. build-linux.sh: mêmes corrections (bundles path, simplification)
This commit is contained in:
2026-07-26 11:06:53 -04:00
parent fc93f550e0
commit fd41175ad8
92 changed files with 46 additions and 48673 deletions
+6
View File
@@ -19,3 +19,9 @@ odysseus/
node_modules/
test-results/
playwright-report/
# Desktop build artifacts
desktop/target/
desktop/python-embed/
desktop/frontend-static/
-1
View File
@@ -15,7 +15,6 @@ tauri-plugin-dialog = "2"
tauri-plugin-notification = "2"
tauri-plugin-fs = "2"
tauri-plugin-process = "2"
tauri-plugin-updater = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
reqwest = { version = "0.12", features = ["json"] }
+10 -20
View File
@@ -11,7 +11,7 @@ echo "========================================"
echo ""
# ── 1. Vérifier les prérequis ─────────────────────────────────
echo "[1/6] Verifying prerequisites..."
echo "[1/4] Verifying prerequisites..."
command -v cargo >/dev/null 2>&1 || { echo "❌ Rust/Cargo not found. Install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh"; exit 1; }
command -v tauri >/dev/null 2>&1 || cargo install tauri-cli
@@ -22,32 +22,22 @@ fi
echo "✅ Dependencies OK"
# ── 2. Préparer Python 3.11 ────────────────────────────────────
echo "[2/6] Preparing Python environment..."
echo "[2/4] Preparing Python environment..."
python3 -m venv python-embed/venv
source python-embed/venv/bin/activate
pip install --upgrade pip -q
pip install -r ../backend/requirements.txt -q
echo "✅ Python environment ready"
# ── 3. Copier le frontend ─────────────────────────────────────
echo "[3/6] Copying frontend..."
rm -rf frontend-static
cp -r ../frontend frontend-static
echo "✅ Frontend copied"
# ── 4. Icônes ──────────────────────────────────────────────────
echo "[4/6] Generating icons..."
# Placeholder — tauri icon generator
echo "✅ Icons OK"
# ── 5. Build Tauri ────────────────────────────────────────────
echo "[5/6] Building Tauri application..."
# ── 3. Build Tauri ────────────────────────────────────────────
echo "[3/4] Building Tauri application..."
cargo tauri build --target x86_64-unknown-linux-gnu --bundles deb,appimage
echo "✅ Build successful"
# ── 6. Résultats ───────────────────────────────────────────────
echo "[6/6] Results:"
ls -lh target/release/bundle/deb/*.deb 2>/dev/null
ls -lh target/release/bundle/appimage/*.AppImage 2>/dev/null
# ── 4. Résultats ───────────────────────────────────────────────
OUTDIR="target/x86_64-unknown-linux-gnu/release"
echo "[4/4] Results:"
ls -lh "$OUTDIR/bundle/deb/"*.deb 2>/dev/null
ls -lh "$OUTDIR/bundle/appimage/"*.AppImage 2>/dev/null
echo ""
echo "✅ Done! Check target/release/bundle/"
echo "✅ Done! Check $OUTDIR/bundle/"
+14 -26
View File
@@ -11,41 +11,30 @@ echo ========================================
echo.
REM ── 1. Vérifier les prérequis ─────────────────────────────────
echo [1/6] Verifying prerequisites...
echo [1/5] Verifying prerequisites...
where cargo >nul 2>&1 || (echo ❌ Rust/Cargo not found. Install from https://rustup.rs & exit /b 1)
where tauri >nul 2>&1 || cargo install tauri-cli
echo ✅ Rust + Tauri OK
REM ── 2. Préparer Python 3.11 embed ─────────────────────────────
echo [2/6] Preparing Python embed...
echo [2/5] Preparing Python embed...
if not exist "python-embed\python.exe" (
echo 📥 Downloading Python 3.11 embed...
if exist "python-embed" rmdir /s /q "python-embed"
curl -L -o python-embed.zip "https://www.python.org/ftp/python/3.11.9/python-3.11.9-embed-amd64.zip"
powershell Expand-Archive -Path python-embed.zip -DestinationPath python-embed\
del python-embed.zip
REM Enable pip in embedded Python
echo import site >> python-embed\python311._pth
REM Install dependencies
python-embed\python.exe -m pip install --no-cache-dir -r ..\backend\requirements.txt
)
echo ✅ Python embed ready
REM ── 3. Copier le frontend ─────────────────────────────────────
echo [3/6] Copying frontend...
if exist "frontend-static" rmdir /s /q "frontend-static"
xcopy /E /I /Q "..\frontend" "frontend-static"
echo ✅ Frontend copied
REM ── 4. Mettre à jour les icônes ───────────────────────────────
echo [4/6] Generating icons...
REM Placeholder — will use tauri icon generator when icons are ready
echo ✅ Icons OK (using defaults)
REM ── 5. Build Tauri ────────────────────────────────────────────
echo [5/6] Building Tauri application...
REM ── 3. Build Tauri ────────────────────────────────────────────
echo [3/5] Building Tauri application...
cargo tauri build --target x86_64-pc-windows-msvc --bundles msi,nsis
if errorlevel 1 (
echo ❌ Build failed!
@@ -53,20 +42,19 @@ if errorlevel 1 (
)
echo ✅ Build successful
REM ── 5b. Copier python-embed à côté de l'exécutable pour le dev ─────
echo [5b/6] Copying python-embed to output dir...
REM ── 4. Copier le runtime à côté de l'exécutable ───────────────
echo [4/5] Copying runtime to output dir...
set "OUTDIR=target\x86_64-pc-windows-msvc\release"
if exist "%OUTDIR%\python-embed" rmdir /s /q "%OUTDIR%\python-embed"
if exist "%OUTDIR%\backend" rmdir /s /q "%OUTDIR%\backend"
xcopy /E /I /Q "python-embed" "%OUTDIR%\python-embed"
REM Copier aussi le backend Python
if not exist "%OUTDIR%\backend" mkdir "%OUTDIR%\backend"
xcopy /E /I /Q "..\backend" "%OUTDIR%\backend"
echo ✅ Runtime copied
REM ── 6. Résultats ───────────────────────────────────────────────────
echo [6/6] Results:
dir /b target\release\bundle\msi\*.msi 2>nul
dir /b target\release\bundle\nsis\*.exe 2>nul
REM ── 5. Résultats ───────────────────────────────────────────────
echo [5/5] Results:
dir /b "%OUTDIR%\bundle\msi\*.msi" 2>nul
dir /b "%OUTDIR%\bundle\nsis\*.exe" 2>nul
echo.
echo ✅ Done! Check target\release\bundle\
echo ✅ Done! Check %OUTDIR%\bundle\
endlocal
+14
View File
@@ -0,0 +1,14 @@
{
"identifier": "default",
"description": "Default capabilities for ObsiGate Desktop",
"windows": ["main"],
"permissions": [
"core:default",
"shell:allow-open",
"shell:allow-execute",
"dialog:default",
"notification:default",
"fs:default",
"process:default"
]
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
# ObsiGate PWA Icons
## Generated Icons
This directory contains PWA icons in SVG format.
### Converting to PNG
For production, convert these SVG files to PNG:
**Using ImageMagick:**
```bash
for file in *.svg; do
size=$(echo $file | grep -oP '\d+x\d+' | head -1 | cut -d'x' -f1)
convert -background none -resize ${size}x${size} "$file" "${file%.svg}.png"
done
```
**Using Inkscape:**
```bash
for file in *.svg; do
size=$(echo $file | grep -oP '\d+x\d+' | head -1 | cut -d'x' -f1)
inkscape "$file" --export-filename="${file%.svg}.png" --export-width=$size
done
```
**Online tools:**
- https://cloudconvert.com/svg-to-png
- https://convertio.co/svg-png/
### Icon Types
- **Regular icons**: Standard app icons with rounded corners
- **Maskable icons**: Icons with safe zone padding for adaptive icons
- **Search icon**: Icon for the search shortcut
### Sizes
- 72x72, 96x96, 128x128, 144x144, 152x152: Mobile devices
- 192x192: Android home screen
- 384x384: High-res displays
- 512x512: Splash screens and high-DPI displays
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="128" height="128" rx="19.2" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(32.0, 25.6)">
<!-- Book cover -->
<rect x="0" y="0" width="64.0" height="76.8" rx="2.56" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="6.4" y="0" width="2.56" height="76.8" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="12.8" y1="19.2" x2="57.6" y2="19.2" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
<line x1="12.8" y1="32.0" x2="57.6" y2="32.0" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
<line x1="12.8" y1="44.8" x2="57.6" y2="44.8" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
<line x1="12.8" y1="57.6" x2="44.8" y2="57.6" stroke="#1a1a1a" stroke-width="1.28" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="144" height="144" viewBox="0 0 144 144" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="144" height="144" rx="21.599999999999998" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(36.0, 28.8)">
<!-- Book cover -->
<rect x="0" y="0" width="72.0" height="86.39999999999999" rx="2.88" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="7.2" y="0" width="2.88" height="86.39999999999999" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="14.4" y1="21.599999999999998" x2="64.8" y2="21.599999999999998" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
<line x1="14.4" y1="36.0" x2="64.8" y2="36.0" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
<line x1="14.4" y1="50.4" x2="64.8" y2="50.4" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
<line x1="14.4" y1="64.8" x2="50.4" y2="64.8" stroke="#1a1a1a" stroke-width="1.44" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="152" height="152" viewBox="0 0 152 152" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="152" height="152" rx="22.8" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(38.0, 30.400000000000002)">
<!-- Book cover -->
<rect x="0" y="0" width="76.0" height="91.2" rx="3.04" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="7.6000000000000005" y="0" width="3.04" height="91.2" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="15.200000000000001" y1="22.8" x2="68.4" y2="22.8" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
<line x1="15.200000000000001" y1="38.0" x2="68.4" y2="38.0" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
<line x1="15.200000000000001" y1="53.199999999999996" x2="68.4" y2="53.199999999999996" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
<line x1="15.200000000000001" y1="68.4" x2="53.199999999999996" y2="68.4" stroke="#1a1a1a" stroke-width="1.52" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Full background for maskable -->
<rect width="192" height="192" fill="url(#grad1)"/>
<!-- Book icon (centered with padding) -->
<g transform="translate(57.25, 49.6)">
<!-- Book cover -->
<rect x="0" y="0" width="76.5" height="91.8" rx="3.06" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="7.65" y="0" width="3.06" height="91.8" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="15.3" y1="22.95" x2="68.85000000000001" y2="22.95" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
<line x1="15.3" y1="38.25" x2="68.85000000000001" y2="38.25" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
<line x1="15.3" y1="53.55" x2="68.85000000000001" y2="53.55" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
<line x1="15.3" y1="68.85000000000001" x2="53.55" y2="68.85000000000001" stroke="#1a1a1a" stroke-width="1.53" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="192" height="192" viewBox="0 0 192 192" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="192" height="192" rx="28.799999999999997" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(48.0, 38.400000000000006)">
<!-- Book cover -->
<rect x="0" y="0" width="96.0" height="115.19999999999999" rx="3.84" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="9.600000000000001" y="0" width="3.84" height="115.19999999999999" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="19.200000000000003" y1="28.799999999999997" x2="86.4" y2="28.799999999999997" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
<line x1="19.200000000000003" y1="48.0" x2="86.4" y2="48.0" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
<line x1="19.200000000000003" y1="67.19999999999999" x2="86.4" y2="67.19999999999999" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
<line x1="19.200000000000003" y1="86.4" x2="67.19999999999999" y2="86.4" stroke="#1a1a1a" stroke-width="1.92" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="384" height="384" viewBox="0 0 384 384" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="384" height="384" rx="57.599999999999994" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(96.0, 76.80000000000001)">
<!-- Book cover -->
<rect x="0" y="0" width="192.0" height="230.39999999999998" rx="7.68" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="19.200000000000003" y="0" width="7.68" height="230.39999999999998" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="38.400000000000006" y1="57.599999999999994" x2="172.8" y2="57.599999999999994" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
<line x1="38.400000000000006" y1="96.0" x2="172.8" y2="96.0" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
<line x1="38.400000000000006" y1="134.39999999999998" x2="172.8" y2="134.39999999999998" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
<line x1="38.400000000000006" y1="172.8" x2="134.39999999999998" y2="172.8" stroke="#1a1a1a" stroke-width="3.84" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Full background for maskable -->
<rect width="512" height="512" fill="url(#grad1)"/>
<!-- Book icon (centered with padding) -->
<g transform="translate(153.25, 132.8)">
<!-- Book cover -->
<rect x="0" y="0" width="204.5" height="245.39999999999998" rx="8.18" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="20.450000000000003" y="0" width="8.18" height="245.39999999999998" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="40.900000000000006" y1="61.349999999999994" x2="184.05" y2="61.349999999999994" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
<line x1="40.900000000000006" y1="102.25" x2="184.05" y2="102.25" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
<line x1="40.900000000000006" y1="143.14999999999998" x2="184.05" y2="143.14999999999998" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
<line x1="40.900000000000006" y1="184.05" x2="143.14999999999998" y2="184.05" stroke="#1a1a1a" stroke-width="4.09" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="512" height="512" rx="76.8" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(128.0, 102.4)">
<!-- Book cover -->
<rect x="0" y="0" width="256.0" height="307.2" rx="10.24" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="25.6" y="0" width="10.24" height="307.2" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="51.2" y1="76.8" x2="230.4" y2="76.8" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
<line x1="51.2" y1="128.0" x2="230.4" y2="128.0" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
<line x1="51.2" y1="179.2" x2="230.4" y2="179.2" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
<line x1="51.2" y1="230.4" x2="179.2" y2="230.4" stroke="#1a1a1a" stroke-width="5.12" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="72" height="72" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="72" height="72" rx="10.799999999999999" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(18.0, 14.4)">
<!-- Book cover -->
<rect x="0" y="0" width="36.0" height="43.199999999999996" rx="1.44" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="3.6" y="0" width="1.44" height="43.199999999999996" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="7.2" y1="10.799999999999999" x2="32.4" y2="10.799999999999999" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
<line x1="7.2" y1="18.0" x2="32.4" y2="18.0" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
<line x1="7.2" y1="25.2" x2="32.4" y2="25.2" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
<line x1="7.2" y1="32.4" x2="25.2" y2="32.4" stroke="#1a1a1a" stroke-width="0.72" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="96" height="96" rx="14.399999999999999" fill="url(#grad1)"/>
<!-- Book icon -->
<g transform="translate(24.0, 19.200000000000003)">
<!-- Book cover -->
<rect x="0" y="0" width="48.0" height="57.599999999999994" rx="1.92" fill="white" opacity="0.95"/>
<!-- Book spine -->
<rect x="4.800000000000001" y="0" width="1.92" height="57.599999999999994" fill="#1a1a1a" opacity="0.2"/>
<!-- Book pages lines -->
<line x1="9.600000000000001" y1="14.399999999999999" x2="43.2" y2="14.399999999999999" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
<line x1="9.600000000000001" y1="24.0" x2="43.2" y2="24.0" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
<line x1="9.600000000000001" y1="33.599999999999994" x2="43.2" y2="33.599999999999994" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
<line x1="9.600000000000001" y1="43.2" x2="33.599999999999994" y2="43.2" stroke="#1a1a1a" stroke-width="0.96" opacity="0.3"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,18 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="96" height="96" viewBox="0 0 96 96" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="grad1" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#2563eb;stop-opacity:1" />
<stop offset="100%" style="stop-color:#1d4ed8;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background -->
<rect width="96" height="96" rx="14.399999999999999" fill="url(#grad1)"/>
<!-- Search icon -->
<g transform="translate(19.200000000000003, 19.200000000000003)">
<circle cx="24.0" cy="24.0" r="17.28" fill="none" stroke="white" stroke-width="4.800000000000001" opacity="0.95"/>
<line x1="36.480000000000004" y1="36.480000000000004" x2="48.0" y2="48.0" stroke="white" stroke-width="4.800000000000001" stroke-linecap="round" opacity="0.95"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 865 B

File diff suppressed because it is too large Load Diff
-457
View File
@@ -1,457 +0,0 @@
/* ObsiGate — AI Editor Toolbar
Provides AI-powered text editing features:
- Inline completion (Ctrl+J)
- Edit (improve, fix spelling, shorter, longer, simplify)
- Tone (professional, casual)
- Translate (multiple languages)
- Generate (explain, summarize, continue)
- AI Custom Rewrite
- AI Toolbox (to list, to table, frontmatter, to canvas)
*/
import { api } from './auth.js';
import { t } from './i18n.js';
// ── API call helper ──
async function aiAction(endpoint, text, extra = {}) {
const body = { text, ...extra };
const data = await api(`/api/ai/${endpoint}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
return data.result;
}
// ── Get selected text from CodeMirror ──
function getSelection(editorView) {
if (!editorView) return '';
const { from, to } = editorView.state.selection.main;
if (from === to) {
// No selection — get entire document
return editorView.state.doc.toString();
}
return editorView.state.doc.sliceString(from, to);
}
// ── Replace or insert text ──
function replaceSelection(editorView, newText, mode = 'replace') {
if (!editorView) return;
const { from, to } = editorView.state.selection.main;
if (mode === 'append') {
// Insert at end of selection
editorView.dispatch({
changes: { from: to, insert: '\n' + newText },
selection: { anchor: to + 1 + newText.length },
});
} else if (mode === 'before') {
editorView.dispatch({
changes: { from: from, insert: newText + '\n' },
});
} else {
// Replace selection
editorView.dispatch({
changes: { from, to, insert: newText },
selection: { anchor: from + newText.length },
});
}
}
// ── Show toast notification ──
function showToast(msg, type = 'info') {
// Use global showToast if available, otherwise console
if (typeof window._obsigateShowToast === 'function') {
window._obsigateShowToast(msg, type);
}
}
// ── Dropdown menu builder ──
function createMenu(items, parentEl) {
// Remove any existing menu
const existing = document.querySelector('.ai-dropdown-menu');
if (existing) existing.remove();
const menu = document.createElement('div');
menu.className = 'ai-dropdown-menu';
// Position relative to parent, but attached to body to avoid
// being clipped by overflow:hidden on the editor container.
const rect = parentEl.getBoundingClientRect();
const menuTop = rect.bottom + 4;
const menuLeft = rect.left;
Object.assign(menu.style, {
position: 'fixed',
top: menuTop + 'px',
left: menuLeft + 'px',
background: 'var(--bg-primary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
padding: '4px 0',
minWidth: '200px',
maxHeight: '60vh',
overflowY: 'auto',
zIndex: '10000',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
});
// If menu overflows bottom of viewport, flip above the button
requestAnimationFrame(() => {
const menuRect = menu.getBoundingClientRect();
if (menuRect.bottom > window.innerHeight - 8) {
menu.style.top = Math.max(8, rect.top - menuRect.height - 4) + 'px';
}
// If menu overflows right edge, align right
if (menuRect.right > window.innerWidth - 8) {
menu.style.left = Math.max(8, window.innerWidth - menuRect.width - 8) + 'px';
}
});
items.forEach(item => {
const el = document.createElement('div');
el.className = 'ai-menu-item';
Object.assign(el.style, {
padding: '6px 12px',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '0.8rem',
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
background: 'transparent',
transition: 'background 0.1s',
});
el.innerHTML = item.label;
if (item.hint) {
const hint = document.createElement('span');
hint.style.cssText = 'margin-left:auto;font-size:0.65rem;color:var(--text-muted)';
hint.textContent = item.hint;
el.appendChild(hint);
}
if (item.children) {
const arrow = document.createElement('span');
arrow.style.cssText = 'margin-left:auto;font-size:0.7rem;color:var(--text-muted)';
arrow.textContent = '▶';
el.appendChild(arrow);
}
el.addEventListener('mouseenter', () => {
el.style.background = 'rgba(255,255,255,0.06)';
// Show submenu
if (item.children) {
const sub = el.querySelector('.ai-submenu');
if (!sub) {
const subMenu = createSubMenu(item.children, el);
el.appendChild(subMenu);
}
}
});
el.addEventListener('mouseleave', (e) => {
el.style.background = 'transparent';
});
el.addEventListener('click', (e) => {
e.stopPropagation();
if (item.action) item.action();
menu.remove();
});
menu.appendChild(el);
});
document.body.appendChild(menu);
// Close on outside click
const closeHandler = (e) => {
if (!menu.contains(e.target)) {
menu.remove();
document.removeEventListener('click', closeHandler);
}
};
setTimeout(() => document.addEventListener('click', closeHandler), 0);
}
function createSubMenu(items, parentEl) {
const menu = document.createElement('div');
menu.className = 'ai-submenu';
Object.assign(menu.style, {
position: 'absolute',
left: '100%',
top: '0',
background: 'var(--bg-primary)',
border: '1px solid var(--border-color)',
borderRadius: '6px',
padding: '4px 0',
minWidth: '180px',
zIndex: '101',
boxShadow: '0 4px 16px rgba(0,0,0,0.4)',
});
items.forEach(item => {
const el = document.createElement('div');
el.className = 'ai-menu-item';
Object.assign(el.style, {
padding: '6px 12px',
cursor: 'pointer',
fontSize: '0.78rem',
color: 'var(--text-primary)',
whiteSpace: 'nowrap',
});
el.innerHTML = item.label;
el.addEventListener('mouseenter', () => { el.style.background = 'rgba(255,255,255,0.06)'; });
el.addEventListener('mouseleave', () => { el.style.background = 'transparent'; });
el.addEventListener('click', (e) => {
e.stopPropagation();
if (item.action) item.action();
// Close all menus
document.querySelectorAll('.ai-dropdown-menu, .ai-submenu').forEach(m => m.remove());
});
menu.appendChild(el);
});
return menu;
}
// ── Main AI Toolbar ──
export async function createAIToolbar(container, getEditorView) {
// Check if AI is configured
let aiConfigured = false;
try {
const status = await api('/api/ai/status');
aiConfigured = status.configured;
} catch { aiConfigured = false; }
if (!aiConfigured) {
const hint = document.createElement('div');
hint.style.cssText = 'padding:6px 12px;font-size:0.7rem;color:var(--text-muted);border-bottom:1px solid var(--border-color)';
hint.textContent = t('ai.not_configured');
container.appendChild(hint);
return;
}
const toolbar = document.createElement('div');
toolbar.className = 'ai-toolbar';
Object.assign(toolbar.style, {
display: 'flex',
alignItems: 'center',
gap: '2px',
padding: '4px 8px',
borderBottom: '1px solid var(--border-color)',
background: 'var(--bg-secondary)',
flexWrap: 'wrap',
position: 'sticky',
top: '0',
zIndex: '5',
});
function ev() { return getEditorView(); }
// ── Inline completion button ──
const aiBtn = document.createElement('button');
aiBtn.className = 'ai-toolbar-btn';
aiBtn.innerHTML = '✦ AI';
Object.assign(aiBtn.style, {
color: '#60a5fa',
fontWeight: '600',
fontSize: '0.75rem',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '3px 8px',
borderRadius: '4px',
});
aiBtn.title = t('ai.inline_completion_title');
aiBtn.addEventListener('click', async () => {
const v = ev();
if (!v) return;
const text = getSelection(v);
if (!text.trim()) return;
const origHTML = aiBtn.innerHTML;
aiBtn.innerHTML = '⏳ AI...';
aiBtn.disabled = true;
try {
const result = await aiAction('inline-complete', text);
replaceSelection(v, result, 'append');
showToast(t('ai.completion_added'), 'success');
} catch (e) {
showToast('AI: ' + (String(e.message || e).includes('401') ? t('ai.error_invalid_key') : e.message), 'error');
} finally {
aiBtn.innerHTML = origHTML;
aiBtn.disabled = false;
}
});
// ── Edit menu ──
const editBtn = createDropdownBtn(t('ai.edit'), [
{ label: '🪄 Improve writing', action: () => action('improve'), hint: t('ai.hint_improve') },
{ label: '🔤 Fix spelling & grammar', action: () => action('fix-spelling'), hint: 'Corrige les fautes' },
{ label: '📏 Make shorter', action: () => action('make-shorter'), hint: 'Rend le texte plus concis' },
{ label: '📐 Make longer', action: () => action('make-longer'), hint: t('ai.hint_add_details') },
{ label: '📋 Simplify language', action: () => action('simplify'), hint: 'Simplifie le langage' },
], t('ai.hint_edit_selection'));
// ── Tone menu ──
const toneBtn = createDropdownBtn('Ton', [
{ label: '💼 Professional tone', action: () => action('tone', { tone: 'professional' }) },
{ label: '💬 Casual tone', action: () => action('tone', { tone: 'casual' }) },
], 'Change le ton du texte');
// ── Translate menu ──
const translateBtn = createDropdownBtn('Traduire', [
{ label: '🇬🇧 English', action: () => action('translate', { target_lang: 'English' }) },
{ label: '🇨🇳 Chinese', action: () => action('translate', { target_lang: 'Chinese' }) },
{ label: '🇯🇵 Japanese', action: () => action('translate', { target_lang: 'Japanese' }) },
{ label: '🇩🇪 German', action: () => action('translate', { target_lang: 'German' }) },
{ label: '🇫🇷 French', action: () => action('translate', { target_lang: 'French' }) },
{ label: '🇪🇸 Spanish', action: () => action('translate', { target_lang: 'Spanish' }) },
], t('ai.hint_translate'));
// ── Generate menu ──
const genBtn = createDropdownBtn(t('ai.generate'), [
{ label: 'ℹ️ Explain this', action: () => action('explain', {}, 'append') },
{ label: '📝 Summarize', action: () => action('summarize', {}, 'append') },
{ label: '✏️ Continue writing', action: () => action('continue', {}, 'append') },
], t('ai.hint_generate'));
// ── Custom Rewrite ──
const rewriteBtn = document.createElement('button');
rewriteBtn.className = 'ai-toolbar-btn';
rewriteBtn.innerHTML = t('ai.rewrite');
rewriteBtn.title = t('ai.rewrite_title');
Object.assign(rewriteBtn.style, {
fontSize: '0.7rem',
background: 'transparent',
border: 'none',
cursor: 'pointer',
padding: '3px 6px',
borderRadius: '4px',
color: 'var(--text-primary)',
});
rewriteBtn.addEventListener('click', async () => {
const v = ev();
if (!v) return;
const text = getSelection(v);
const instruction = prompt(t('ai.rewrite_prompt'), '');
if (!instruction) return;
try {
const result = await aiAction('rewrite', text, { instruction });
replaceSelection(v, result);
showToast(t('ai.text_rewritten'), 'success');
} catch (e) {
showToast('AI: ' + (String(e.message || e).includes('401') ? t('ai.error_invalid_key') : e.message), 'error');
}
});
// ── Toolbox menu ──
const toolboxBtn = createDropdownBtn(t('ai.toolbox'), [
{ label: '📋 Convert to list', action: () => action('to-list') },
{ label: '📊 Convert to table', action: () => action('to-table') },
{ label: '⚙️ Generate frontmatter', action: () => action('frontmatter', {}, 'before') },
{ label: '🔷 Convert to canvas', action: () => action('to-canvas', {}, 'append') },
], 'Outils de conversion');
toolbar.appendChild(aiBtn);
toolbar.appendChild(createSeparator());
toolbar.appendChild(editBtn);
toolbar.appendChild(toneBtn);
toolbar.appendChild(translateBtn);
toolbar.appendChild(createSeparator());
toolbar.appendChild(genBtn);
toolbar.appendChild(rewriteBtn);
toolbar.appendChild(toolboxBtn);
container.insertBefore(toolbar, container.firstChild);
// ── Action helper ──
async function action(endpoint, extra = {}, mode = 'replace') {
const v = ev();
if (!v) return;
const text = getSelection(v);
if (!text.trim()) {
showToast(t('ai.select_text'), 'warning');
return;
}
showToast(t('ai.processing'), 'info');
// Show processing indicator in the editor header
_showProcessingIndicator(true);
// Dispatch AI processing start event
document.dispatchEvent(new CustomEvent('ai-processing-start', { detail: { endpoint, mode } }));
try {
const result = await aiAction(endpoint, text, extra);
replaceSelection(v, result, mode);
showToast(t('ai.text_processed'), 'success');
} catch (e) {
const msg = String(e.message || e);
if (msg.includes('401')) {
showToast(t('ai.invalid_api_key'), 'error');
} else if (msg.includes('402') || msg.includes('429')) {
showToast(t('ai.quota_exceeded'), 'error');
} else {
showToast('AI: ' + msg, 'error');
}
} finally {
_showProcessingIndicator(false);
// Dispatch AI processing end event
document.dispatchEvent(new CustomEvent('ai-processing-end'));
}
}
// ── In-editor processing indicator ──
function _showProcessingIndicator(show) {
let el = document.getElementById('ai-processing-indicator');
if (show) {
if (!el) {
el = document.createElement('span');
el.id = 'ai-processing-indicator';
el.className = 'ai-processing-indicator';
el.innerHTML = '<span class="ai-spinner"></span> AI...';
el.title = 'Traitement AI en cours';
const header = document.querySelector('#editor-modal .editor-header');
if (header) header.appendChild(el);
}
el.style.display = 'inline-flex';
} else if (el) {
el.style.display = 'none';
}
}
// ── Keyboard shortcut Ctrl+J for inline completion ──
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key === 'j') {
const modal = document.getElementById('editor-modal');
if (!modal || !modal.classList.contains('active')) return;
e.preventDefault();
aiBtn.click();
}
});
return toolbar;
}
function createDropdownBtn(text, items, tooltip = '') {
const btn = document.createElement('button');
btn.className = 'ai-toolbar-btn';
btn.innerHTML = text + ' ▾';
btn.title = tooltip || text;
Object.assign(btn.style, {
position: 'relative',
fontSize: '0.7rem',
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '3px 6px',
borderRadius: '4px',
color: 'var(--text-primary)',
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
createMenu(items, btn);
});
btn.addEventListener('mouseenter', () => { btn.style.background = 'rgba(255,255,255,0.08)'; });
btn.addEventListener('mouseleave', () => { btn.style.background = 'transparent'; });
return btn;
}
function createSeparator() {
const sep = document.createElement('span');
sep.style.cssText = 'width:1px;height:16px;background:var(--border-color);margin:0 2px';
return sep;
}
-107
View File
@@ -1,107 +0,0 @@
/* ObsiGate — Module orchestrator (ES modules).
Replaces the monolithic frontend/app.js IIFE.
All code lives in frontend/js/*.js modules. */
import * as UI from './ui.js';
import * as Utils from './utils.js';
import { initI18n, t } from './i18n.js';
// Wire up AI toolbar toast (avoids circular import in utils.js)
window._obsigateShowToast = UI.showToast;
import * as Sidebar from './sidebar.js';
import * as Auth from './auth.js';
import * as Viewer from './viewer.js';
import * as Sync from './sync.js';
import { initGraphView } from './graph.js';
import * as Legacy from './legacy.js';
import { initCommandPalette } from './palette.js';
import { initMobileToolbar } from './mobile-toolbar.js';
import { initDragDrop } from './dragdrop.js';
import { initMermaid, setupFocusMode } from './mermaid-viewer.js';
import PaneManager from './pane-manager.js';
// =========================================================================
// Initialization — mirrors the original app.js init() ordering
// =========================================================================
async function init() {
// 1. Init i18n FIRST — loads fallback (FR) + selected locale
// Server language check may fail (no auth yet), falls to localStorage
// LocalStorage has correct language if user set it previously
await initI18n();
// 2. Init auth — now t() works inside showApp()/renderUserMenu()
const authOk = await Auth.AuthManager.initAuth();
UI.initTheme();
UI.initHeaderMenu();
UI.initCustomDropdowns();
document.getElementById("theme-toggle").addEventListener("click", UI.toggleTheme);
document.getElementById("header-logo").addEventListener("click", Legacy.goHome);
const refreshBtn = document.getElementById("header-refresh-btn");
if (refreshBtn) refreshBtn.addEventListener("click", Legacy.goHome);
Legacy.initSearch();
UI.initSidebarToggle();
UI.initMobile();
Sidebar.initVaultContext();
Legacy.initSidebarTabs();
Legacy.initHelpModal();
Legacy.initConfigModal();
Legacy.initAboutModal();
Sidebar.initSidebarFilter();
UI.initSidebarResize();
Utils.initEditor();
Auth.initLoginForm();
Legacy.initRecentTab();
UI.RightSidebarManager.init();
UI.FindInPageManager.init();
UI.ContextMenuManager.init();
UI.TabManager.init();
PaneManager.init();
initCommandPalette();
initMobileToolbar();
if (authOk) {
Legacy.initSyncStatus();
try {
await Promise.all([Legacy.loadVaultSettings(), Sidebar.loadVaults(), Sidebar.loadTags()]);
initDragDrop();
Legacy.DashboardRecentWidget.init();
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("popup") === "true") {
document.body.classList.add("popup-mode");
}
if (window.location.hash && window.location.hash.startsWith("#file=")) {
const hashVal = window.location.hash.substring(6);
const sepIndex = hashVal.indexOf(":");
if (sepIndex > -1) {
const vault = decodeURIComponent(hashVal.substring(0, sepIndex));
const path = decodeURIComponent(hashVal.substring(sepIndex + 1));
Viewer.openFile(vault, path);
}
} else if (urlParams.get("popup") !== "true") {
Legacy.showWelcome();
}
} catch (err) {
console.error("Failed to initialize ObsiGate:", err);
UI.showToast(t("error.initialization"), "error");
}
}
initGraphView();
initMermaid();
setupFocusMode();
Utils.safeCreateIcons();
}
document.addEventListener("DOMContentLoaded", () => {
init();
Sync.init();
});
-579
View File
@@ -1,579 +0,0 @@
/* ObsiGate — Authentication: API helper, AuthManager, login form, AdminPanel */
import { state } from './state.js';
import { safeCreateIcons } from './utils.js';
import { showToast, closeHeaderMenu } from './ui.js';
import { t, getLocale, setLocale } from './i18n.js';
import { showWelcome } from './viewer.js';
// ── Global logout handler for header dropdown onclick ────────────────────
// Defined at module top level so it's always available, even if
// renderUserMenu() hasn't been called yet.
// Uses a getter to lazy-resolve AuthManager (defined later in this module).
window.handleLogout = () => {
AuthManager.logout();
};
// ---------------------------------------------------------------------------
// API helpers
// ---------------------------------------------------------------------------
/**
* Fetch JSON from an API endpoint with optional AbortSignal support.
* Surfaces errors to the user via toast instead of silently failing.
*
* @param {string} path - API URL path.
* @param {object} [opts] - Fetch options (may include signal).
* @returns {Promise<any>} Parsed JSON response.
*/
async function api(path, opts) {
var res;
try {
// Inject auth header if authenticated
const authHeaders = AuthManager.getAuthHeaders();
const mergedOpts = opts || {};
// Auto-set Content-Type for JSON bodies
if (mergedOpts.body && typeof mergedOpts.body === "string" && !mergedOpts.headers?.["Content-Type"]) {
mergedOpts.headers = { ...mergedOpts.headers, "Content-Type": "application/json" };
}
if (authHeaders) {
mergedOpts.headers = { ...mergedOpts.headers, ...authHeaders };
}
mergedOpts.credentials = "include";
res = await fetch(path, mergedOpts);
} catch (err) {
if (err.name === "AbortError") throw err; // let callers handle abort
showToast(t("auth.network_error"), "error");
throw err;
}
if (res.status === 401 && AuthManager._authEnabled) {
// Token expired — try refresh
try {
await AuthManager.refreshAccessToken();
// Retry the request with new token
const retryHeaders = AuthManager.getAuthHeaders();
const retryOpts = opts || {};
retryOpts.headers = { ...retryOpts.headers, ...retryHeaders };
retryOpts.credentials = "include";
res = await fetch(path, retryOpts);
} catch (refreshErr) {
AuthManager.clearSession();
AuthManager.showLoginScreen();
throw new Error("Session expirée");
}
}
if (!res.ok) {
var detail = "";
try {
var body = await res.json();
detail = body.detail || "";
} catch (_) {
/* no json body */
}
showToast(detail || "Erreur API : " + res.status, "error");
throw new Error(detail || "API error: " + res.status);
}
return res.json();
}
// ---------------------------------------------------------------------------
// AuthManager — Authentication state & token management
// ---------------------------------------------------------------------------
const AuthManager = {
ACCESS_TOKEN_KEY: "obsigate_access_token",
TOKEN_EXPIRY_KEY: "obsigate_token_expiry",
USER_KEY: "obsigate_user",
_authEnabled: false,
// ── Token storage (sessionStorage) ─────────────────────────────
saveToken(tokenData) {
const expiresAt = Date.now() + tokenData.expires_in * 1000;
sessionStorage.setItem(this.ACCESS_TOKEN_KEY, tokenData.access_token);
sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiresAt.toString());
if (tokenData.user) {
sessionStorage.setItem(this.USER_KEY, JSON.stringify(tokenData.user));
}
},
getToken() {
return sessionStorage.getItem(this.ACCESS_TOKEN_KEY);
},
getUser() {
const raw = sessionStorage.getItem(this.USER_KEY);
return raw ? JSON.parse(raw) : null;
},
isTokenExpired() {
const expiry = sessionStorage.getItem(this.TOKEN_EXPIRY_KEY);
if (!expiry) return true;
// Renew 60s before expiration
return Date.now() > parseInt(expiry) - 60000;
},
clearSession() {
sessionStorage.removeItem(this.ACCESS_TOKEN_KEY);
sessionStorage.removeItem(this.TOKEN_EXPIRY_KEY);
sessionStorage.removeItem(this.USER_KEY);
},
getAuthHeaders() {
const token = this.getToken();
if (!token || !this._authEnabled) return null;
return { Authorization: "Bearer " + token };
},
// ── API calls ──────────────────────────────────────────────────
async login(username, password, rememberMe) {
const response = await fetch("/api/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
credentials: "include",
body: JSON.stringify({ username, password, remember_me: rememberMe || false }),
});
if (!response.ok) {
const err = await response.json();
throw new Error(err.detail || "Erreur de connexion");
}
const data = await response.json();
this.saveToken(data);
return data.user;
},
async logout() {
try {
const token = this.getToken();
await fetch("/api/auth/logout", {
method: "POST",
headers: token ? { Authorization: "Bearer " + token } : {},
credentials: "include",
});
} catch (e) {
/* continue even if API fails */
}
this.clearSession();
this.showLoginScreen();
},
async refreshAccessToken() {
const response = await fetch("/api/auth/refresh", {
method: "POST",
credentials: "include",
});
if (!response.ok) {
this.clearSession();
throw new Error("Session expirée");
}
const data = await response.json();
const expiry = Date.now() + data.expires_in * 1000;
sessionStorage.setItem(this.ACCESS_TOKEN_KEY, data.access_token);
sessionStorage.setItem(this.TOKEN_EXPIRY_KEY, expiry.toString());
return data.access_token;
},
// ── UI controls ────────────────────────────────────────────────
showLoginScreen() {
const app = document.getElementById("app");
const login = document.getElementById("login-screen");
if (app) app.classList.add("hidden");
if (login) {
login.classList.remove("hidden");
const usernameInput = document.getElementById("login-username");
if (usernameInput) usernameInput.focus();
}
},
showApp() {
const login = document.getElementById("login-screen");
const app = document.getElementById("app");
if (login) login.classList.add("hidden");
if (app) app.classList.remove("hidden");
this.renderUserMenu();
},
renderUserMenu() {
const user = this.getUser();
const userMenu = document.getElementById("user-menu");
if (!userMenu) return;
if (!user || !this._authEnabled) {
userMenu.innerHTML = "";
return;
}
userMenu.innerHTML = '<span class="user-display-name">' + (user.display_name || user.username) + "</span>" + '<button class="btn-logout" id="logout-btn" title="' + t('auth.logout_title') + '" onclick="window.handleLogout()"><i data-lucide="log-out" style="width:14px;height:14px"></i></button>';
safeCreateIcons();
const logoutBtn = document.getElementById("logout-btn");
if (logoutBtn) logoutBtn.addEventListener("click", () => AuthManager.logout());
const adminRow = document.getElementById("admin-menu-row");
if (adminRow) {
if (user.role === "admin") {
adminRow.classList.remove("hidden");
// Important: use an inline function to ensure we don't bind multiple identical listeners on rerenders, or clean up before
adminRow.onclick = () => {
closeHeaderMenu();
AdminPanel.show();
};
} else {
adminRow.classList.add("hidden");
}
}
},
// ── Initialization ──────────────────────────────────────────────
async checkAuthStatus() {
try {
const res = await fetch("/api/auth/status");
const data = await res.json();
this._authEnabled = data.auth_enabled;
return data;
} catch (e) {
this._authEnabled = false;
return { auth_enabled: false };
}
},
async initAuth() {
const status = await this.checkAuthStatus();
if (!status.auth_enabled) {
// Auth disabled — show app immediately
this.showApp();
return true;
}
// Auth enabled — check for existing session
if (this.getToken() && !this.isTokenExpired()) {
this.showApp();
return true;
}
// Try silent refresh
try {
await this.refreshAccessToken();
// Fetch user info
const token = this.getToken();
const res = await fetch("/api/auth/me", {
headers: { Authorization: "Bearer " + token },
credentials: "include",
});
if (res.ok) {
const user = await res.json();
sessionStorage.setItem(this.USER_KEY, JSON.stringify(user));
this.showApp();
return true;
}
} catch (e) {
/* silent refresh failed */
}
// No valid session — show login
this.showLoginScreen();
return false;
},
};
// ---------------------------------------------------------------------------
// Login form handler
// ---------------------------------------------------------------------------
function initLoginForm() {
const form = document.getElementById("login-form");
if (!form) return;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const username = document.getElementById("login-username").value;
const password = document.getElementById("login-password").value;
const rememberMe = document.getElementById("remember-me").checked;
const errorEl = document.getElementById("login-error");
const btn = document.getElementById("login-btn");
btn.disabled = true;
btn.querySelector(".btn-spinner").classList.remove("hidden");
btn.querySelector(".btn-text").textContent = "Connexion...";
errorEl.classList.add("hidden");
try {
await AuthManager.login(username, password, rememberMe);
AuthManager.showApp();
// Re-sync language from server now that we're authenticated
try {
const resp = await fetch('/api/auth/me', { credentials: 'include' });
if (resp.ok) {
const user = await resp.json();
if (user.language && user.language !== getLocale()) {
await setLocale(user.language);
}
}
} catch (e) { /* non-bloquant */ }
// Load app data after successful login
try {
// Dynamic imports to avoid circular dependency with sidebar.js
const { loadVaults, loadTags } = await import('./sidebar.js');
await Promise.all([loadVaults(), loadTags()]);
// Start SSE sync now that auth cookie is set (dynamic import to avoid circular dep)
const { IndexUpdateManager } = await import('./sync.js');
IndexUpdateManager.connect();
// Sync offline file index now that we're authenticated
const { syncFileIndexFromServer } = await import('./offline.js');
syncFileIndexFromServer();
// Show dashboard
showWelcome();
} catch (err) {
console.error("Failed to load data after login:", err);
}
safeCreateIcons();
} catch (err) {
errorEl.textContent = err.message;
errorEl.classList.remove("hidden");
document.getElementById("login-password").value = "";
document.getElementById("login-password").focus();
} finally {
btn.disabled = false;
btn.querySelector(".btn-spinner").classList.add("hidden");
btn.querySelector(".btn-text").textContent = "Se connecter";
}
});
// Toggle password visibility
const toggleBtn = document.getElementById("toggle-password");
if (toggleBtn) {
toggleBtn.addEventListener("click", () => {
const input = document.getElementById("login-password");
input.type = input.type === "password" ? "text" : "password";
});
}
}
// ---------------------------------------------------------------------------
// Admin Panel — User management (admin only)
// ---------------------------------------------------------------------------
const AdminPanel = {
_modal: null,
_allVaults: [],
show() {
this._createModal();
this._modal.classList.add("active");
this._loadUsers();
},
hide() {
if (this._modal) this._modal.classList.remove("active");
},
_createModal() {
if (this._modal) return;
this._modal = document.createElement("div");
this._modal.className = "editor-modal";
this._modal.id = "admin-modal";
this._modal.innerHTML = `
<div class="editor-container">
<div class="editor-header">
<div class="editor-title">⚙️ ${t('admin.title')}</div>
<div class="editor-actions">
<button class="editor-btn" id="admin-close" title="Fermer">
<i data-lucide="x" style="width:16px;height:16px"></i>
</button>
</div>
</div>
<div class="editor-body" id="admin-body">
<div class="admin-toolbar">
<button class="btn-login" id="admin-add-user" style="font-size:0.85rem;padding:6px 16px;">${t('admin.new_user')}</button>
</div>
<div id="admin-users-list" class="admin-users-list"></div>
</div>
</div>
`;
document.body.appendChild(this._modal);
safeCreateIcons();
document.getElementById("admin-close").addEventListener("click", () => this.hide());
document.getElementById("admin-add-user").addEventListener("click", () => this._showUserForm(null));
},
async _loadUsers() {
try {
const users = await api("/api/auth/admin/users");
// Also load available vaults
try {
const vaultsData = await api("/api/vaults");
this._allVaults = vaultsData.map((v) => v.name);
} catch (e) {
this._allVaults = [];
}
this._renderUsers(users);
} catch (err) {
document.getElementById("admin-users-list").innerHTML = '<p style="color:var(--danger);padding:16px;">Erreur : ' + err.message + "</p>";
}
},
_renderUsers(users) {
const container = document.getElementById("admin-users-list");
if (!users.length) {
container.innerHTML = '<p style="padding:16px;color:var(--text-muted);">Aucun utilisateur.</p>';
return;
}
let html = '<table class="admin-table"><thead><tr>' +
'<th>' + t('admin.col_user') + '</th><th>' + t('admin.col_role') + '</th><th>' + t('admin.col_vaults') + '</th><th>' + t('admin.col_status') + '</th><th>' + t('admin.col_last_login') + '</th><th>' + t('admin.col_actions') + '</th>' +
'</tr></thead><tbody>';
users.forEach((u) => {
const vaults = u.vaults.includes("*") ? t('common.all') : u.vaults.join(", ") || t('common.none');
const status = u.active ? "✅" : "🔴";
const lastLogin = u.last_login ? new Date(u.last_login).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US', { day: "numeric", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit" }) : t('common.never');
html +=
"<tr>" +
"<td><strong>" +
u.username +
"</strong>" +
(u.display_name && u.display_name !== u.username ? "<br><small>" + u.display_name + "</small>" : "") +
"</td>" +
'<td><span class="admin-role-badge admin-role-' +
u.role +
'">' +
u.role +
"</span></td>" +
'<td><span class="admin-vaults-text">' +
vaults +
"</span></td>" +
"<td>" +
status +
"</td>" +
"<td><small>" +
lastLogin +
"</small></td>" +
'<td class="admin-actions">' +
'<button class="admin-action-btn" data-action="edit" data-username="' +
u.username +
'" title="' + t('common.edit') + '">✏️</button>' +
'<button class="admin-action-btn danger" data-action="delete" data-username="' +
u.username +
'" title="' + t('common.delete') + '">🗑️</button>' +
"</td></tr>";
});
html += "</tbody></table>";
container.innerHTML = html;
// Bind action buttons
container.querySelectorAll('[data-action="edit"]').forEach((btn) => {
btn.addEventListener("click", () => {
const user = users.find((u) => u.username === btn.dataset.username);
if (user) this._showUserForm(user);
});
});
container.querySelectorAll('[data-action="delete"]').forEach((btn) => {
btn.addEventListener("click", () => this._deleteUser(btn.dataset.username));
});
},
_showUserForm(user) {
const isEdit = !!user;
const title = isEdit ? "Modifier : " + user.username : "Nouvel utilisateur";
const vaultCheckboxes = this._allVaults
.map((v) => {
const checked = user && (user.vaults.includes(v) || user.vaults.includes("*")) ? "checked" : "";
return '<label class="checkbox-label"><input type="checkbox" name="vault" value="' + v + '" ' + checked + "><span>" + v + "</span></label>";
})
.join("");
const allVaultsChecked = user && user.vaults.includes("*") ? "checked" : "";
// Create form modal overlay
const overlay = document.createElement("div");
overlay.className = "admin-form-overlay";
overlay.innerHTML = `
<div class="admin-form-card">
<h3>${title}</h3>
<form id="admin-user-form">
${!isEdit ? '<div class="form-group"><label>Nom d\'utilisateur</label><input type="text" name="username" required pattern="[a-zA-Z0-9_\-]{2,32}" placeholder="username"></div>' : ""}
<div class="form-group"><label>Nom affiché</label><input type="text" name="display_name" value="${isEdit ? user.display_name || "" : ""}"></div>
<div class="form-group"><label>${isEdit ? "Nouveau mot de passe (vide = inchangé)" : "Mot de passe"}</label><input type="password" name="password" ${!isEdit ? 'required minlength="8"' : ""} placeholder="${isEdit ? "Laisser vide pour ne pas changer" : "Min. 8 caractères"}"></div>
<div class="form-group"><label>Rôle</label><select name="role"><option value="user" ${isEdit && user.role === "user" ? "selected" : ""}>Utilisateur</option><option value="admin" ${isEdit && user.role === "admin" ? "selected" : ""}>Admin</option></select></div>
<div class="form-group">
<label>Vaults autorisées</label>
<div class="admin-vault-list">${vaultCheckboxes}</div>
<label class="checkbox-label" style="margin-top:8px;border-top:1px solid var(--border);padding-top:8px;"><input type="checkbox" id="admin-all-vaults" ${allVaultsChecked}><span><strong>Accès total</strong> (toutes les vaults, y compris futures)</span></label>
</div>
${isEdit ? '<div class="form-group"><label>Compte actif</label><label class="checkbox-label"><input type="checkbox" name="active" ' + (user.active ? "checked" : "") + "><span>Actif</span></label></div>" : ""}
<div class="admin-form-actions">
<button type="button" class="config-btn-secondary" id="admin-form-cancel">Annuler</button>
<button type="submit" class="btn-login" style="font-size:0.85rem;padding:6px 20px;">Enregistrer</button>
</div>
</form>
</div>
`;
this._modal.appendChild(overlay);
document.getElementById("admin-form-cancel").addEventListener("click", () => overlay.remove());
document.getElementById("admin-user-form").addEventListener("submit", async (e) => {
e.preventDefault();
const form = e.target;
const allVaults = document.getElementById("admin-all-vaults").checked;
const selectedVaults = allVaults ? ["*"] : Array.from(form.querySelectorAll('input[name="vault"]:checked')).map((cb) => cb.value);
try {
if (isEdit) {
const updates = {
display_name: form.display_name.value || null,
role: form.role.value,
vaults: selectedVaults,
};
if (form.password.value) updates.password = form.password.value;
const activeCheckbox = form.querySelector('input[name="active"]');
if (activeCheckbox) updates.active = activeCheckbox.checked;
await api("/api/auth/admin/users/" + user.username, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(updates),
});
} else {
await api("/api/auth/admin/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
username: form.username.value,
password: form.password.value,
display_name: form.display_name.value || null,
role: form.role.value,
vaults: selectedVaults,
}),
});
}
overlay.remove();
this._loadUsers();
showToast(isEdit ? "Utilisateur modifié" : "Utilisateur créé", "success");
} catch (err) {
showToast(err.message, "error");
}
});
},
async _deleteUser(username) {
const currentUser = AuthManager.getUser();
if (currentUser && currentUser.username === username) {
showToast(t("auth.cannot_delete_self"), "error");
return;
}
if (!confirm("Supprimer l'utilisateur \"" + username + '" ?')) return;
try {
await api("/api/auth/admin/users/" + username, { method: "DELETE" });
this._loadUsers();
showToast(t("auth.user_deleted"), "success");
} catch (err) {
showToast(err.message, "error");
}
},
};
export { api, AuthManager, initLoginForm, AdminPanel };
-350
View File
@@ -1,350 +0,0 @@
/* ObsiGate — Autocomplete intelligent
* Partagé entre éditeur Forge (textarea) et CodeMirror.
* Détecte le contexte autour du curseur et suggère des complétions.
*
* Contexte supporté : mermaid, code fence, frontmatter, markdown, wikilink, table, texte libre.
*
* API :
* suggest(text, cursorPos) → { context, items: [{label, detail, insert, from, to}] }
* CompletionSource pour CodeMirror : codeMirrorCompletionSource(context)
*/
// ── Suggestion Data ──────────────────────────────────────────────────
var MERMAID_TYPES = [
{ label: 'flowchart TD', detail: 'Diagramme de flux (top-down)', insert: 'flowchart TD\n ' },
{ label: 'flowchart LR', detail: 'Diagramme de flux (gauche-droite)', insert: 'flowchart LR\n ' },
{ label: 'sequenceDiagram', detail: 'Sequence (acteurs + messages)', insert: 'sequenceDiagram\n ' },
{ label: 'classDiagram', detail: 'Diagramme de classes UML', insert: 'classDiagram\n ' },
{ label: 'stateDiagram-v2', detail: 'Diagramme d\'etats', insert: 'stateDiagram-v2\n ' },
{ label: 'erDiagram', detail: 'Diagramme Entite-Relation', insert: 'erDiagram\n ' },
{ label: 'journey', detail: 'Parcours utilisateur', insert: 'journey\n ' },
{ label: 'gantt', detail: 'Diagramme de Gantt', insert: 'gantt\n ' },
{ label: 'pie', detail: 'Diagramme circulaire', insert: 'pie\n ' },
{ label: 'gitGraph', detail: 'Historique Git', insert: 'gitGraph\n ' },
{ label: 'mindmap', detail: 'Carte mentale', insert: 'mindmap\n ' },
{ label: 'timeline', detail: 'Ligne du temps', insert: 'timeline\n ' },
{ label: 'quadrantChart', detail: 'Graphique quadrants', insert: 'quadrantChart\n ' },
{ label: 'xychart-beta', detail: 'Graphique XY', insert: 'xychart-beta\n ' },
{ label: 'block-beta', detail: 'Diagramme de blocs', insert: 'block-beta\n ' }
];
var MERMAID_SYNTAX = {
'flowchart': [
{ label: 'A-->B', detail: 'Fleche', insert: '-->' },
{ label: 'A--text-->B', detail: 'Fleche avec texte', insert: '-- texte -->' },
{ label: 'A-.->B', detail: 'Fleche pointillee', insert: '-.->' },
{ label: 'A==>B', detail: 'Fleche epaisse', insert: '==>' },
{ label: 'subgraph', detail: 'Sous-graphe', insert: 'subgraph titre\n \nend' },
{ label: 'style A fill:#xxx', detail: 'Style de noeud', insert: 'style A fill:#333,stroke:#58a6ff' },
{ label: 'classDef', detail: 'Classe CSS', insert: 'classDef default fill:#333,stroke:#58a6ff' }
],
'sequenceDiagram': [
{ label: 'participant X', detail: 'Declarer acteur', insert: 'participant Nom' },
{ label: 'A->>B: msg', detail: 'Message', insert: '->> : ' },
{ label: 'A-->>B: msg', detail: 'Message asynchrone', insert: '-->> : ' },
{ label: 'Note over A,B', detail: 'Note', insert: 'Note over A,B: ' },
{ label: 'loop text', detail: 'Boucle', insert: 'loop description\n \nend' },
{ label: 'alt/else', detail: 'Alternative', insert: 'alt condition\n \nelse\n \nend' },
{ label: 'activate A', detail: 'Activer ligne de vie', insert: 'activate A' }
],
'classDiagram': [
{ label: 'class Nom {', detail: 'Declarer classe', insert: 'class Nom {\n \n}' },
{ label: '+methode()', detail: 'Methode publique', insert: '+nom() Type' },
{ label: '-attribut', detail: 'Attribut prive', insert: '-nom Type' },
{ label: 'Heritage', detail: 'Relation heritage', insert: ' <|-- ' },
{ label: 'Composition', detail: 'Relation composition', insert: ' *-- ' }
],
'gantt': [
{ label: 'dateFormat', detail: 'Format de date', insert: 'dateFormat YYYY-MM-DD' },
{ label: 'section', detail: 'Section Gantt', insert: 'section Nom' },
{ label: 'tache : apres', detail: 'Tache avec dependance', insert: ' : after tache1, 3d' }
],
'pie': [
{ label: 'title', detail: 'Titre du pie', insert: 'title Titre du graphique' },
{ label: '"Label" : val', detail: 'Valeur', insert: '"Label" : 50' }
],
'mindmap': [
{ label: 'root', detail: 'Racine', insert: 'root((Sujet))' },
{ label: 'branche', detail: 'Sous-sujet', insert: '\n Sous-sujet' },
{ label: 'leaf', detail: 'Feuille', insert: '\n Detail' }
]
};
var CODE_LANGUAGES = [
{ label: 'python', detail: 'Python', insert: 'python' },
{ label: 'javascript', detail: 'JavaScript', insert: 'javascript' },
{ label: 'typescript', detail: 'TypeScript', insert: 'typescript' },
{ label: 'bash', detail: 'Bash / Shell', insert: 'bash' },
{ label: 'sql', detail: 'SQL', insert: 'sql' },
{ label: 'json', detail: 'JSON', insert: 'json' },
{ label: 'yaml', detail: 'YAML', insert: 'yaml' },
{ label: 'html', detail: 'HTML', insert: 'html' },
{ label: 'css', detail: 'CSS', insert: 'css' },
{ label: 'dockerfile', detail: 'Dockerfile', insert: 'dockerfile' },
{ label: 'mermaid', detail: 'Mermaid (diagrammes)', insert: 'mermaid' },
{ label: 'markdown', detail: 'Markdown', insert: 'markdown' },
{ label: 'rust', detail: 'Rust', insert: 'rust' },
{ label: 'go', detail: 'Go', insert: 'go' },
{ label: 'java', detail: 'Java', insert: 'java' }
];
var CODE_SNIPPETS = {
python: [
{ label: 'def', detail: 'Fonction', insert: 'def nom(params):\n ' },
{ label: 'class', detail: 'Classe', insert: 'class Nom:\n def __init__(self):\n ' },
{ label: 'if/else', detail: 'Condition', insert: 'if condition:\n \nelse:\n ' },
{ label: 'for', detail: 'Boucle for', insert: 'for item in iterable:\n ' },
{ label: 'try/except', detail: 'Try-except', insert: 'try:\n \nexcept Exception as e:\n ' },
{ label: 'import', detail: 'Import', insert: 'import ' },
{ label: 'print', detail: 'Print', insert: 'print()' },
{ label: 'lambda', detail: 'Lambda', insert: 'lambda x: ' }
],
javascript: [
{ label: 'function', detail: 'Fonction', insert: 'function nom(params) {\n \n}' },
{ label: 'const', detail: 'Constante', insert: 'const nom = ' },
{ label: '=>', detail: 'Fleche', insert: '(params) => {\n \n}' },
{ label: 'if/else', detail: 'Condition', insert: 'if (cond) {\n \n} else {\n \n}' },
{ label: 'for', detail: 'Boucle for', insert: 'for (let i = 0; i < n; i++) {\n \n}' },
{ label: 'try/catch', detail: 'Try-catch', insert: 'try {\n \n} catch (e) {\n \n}' },
{ label: 'console.log', detail: 'Console log', insert: 'console.log()' },
{ label: 'async/await', detail: 'Async function', insert: 'async function nom() {\n await \n}' }
],
bash: [
{ label: '#!/bin/bash', detail: 'Shebang', insert: '#!/bin/bash' },
{ label: 'if/fi', detail: 'Condition', insert: 'if [ condition ]; then\n \nfi' },
{ label: 'for', detail: 'Boucle for', insert: 'for item in list; do\n \ndone' },
{ label: 'case/esac', detail: 'Case', insert: 'case var in\n pattern)\n ;;\nesac' },
{ label: 'function', detail: 'Fonction', insert: 'nom() {\n \n}' }
],
sql: [
{ label: 'SELECT', detail: 'Select', insert: 'SELECT colonnes\nFROM table' },
{ label: 'JOIN', detail: 'Join', insert: 'JOIN table2 ON table1.id = table2.id' },
{ label: 'WHERE', detail: 'Where', insert: 'WHERE condition' },
{ label: 'GROUP BY', detail: 'Group by', insert: 'GROUP BY colonne' },
{ label: 'ORDER BY', detail: 'Order by', insert: 'ORDER BY colonne DESC' },
{ label: 'INSERT INTO', detail: 'Insert', insert: 'INSERT INTO table (cols) VALUES (vals)' },
{ label: 'CREATE TABLE', detail: 'Create table', insert: 'CREATE TABLE nom (\n id INTEGER PRIMARY KEY,\n \n)' }
]
};
var FRONTMATTER_FIELDS = [
{ label: 'title:', detail: 'Titre du document', insert: 'title: ' },
{ label: 'tags:', detail: 'Tags (liste)', insert: 'tags:\n - ' },
{ label: 'date:', detail: 'Date', insert: 'date: ' + new Date().toISOString().slice(0,10) },
{ label: 'status:', detail: 'Statut', insert: 'status: en cours' },
{ label: 'category:', detail: 'Categorie', insert: 'category: ' },
{ label: 'publish:', detail: 'Publier (true/false)', insert: 'publish: true' },
{ label: 'favoris:', detail: 'Favoris (true/false)', insert: 'favoris: true' },
{ label: 'author:', detail: 'Auteur', insert: 'author: ' },
{ label: 'description:', detail: 'Description', insert: 'description: ' }
];
var MARKDOWN_FORMAT = [
{ label: '# H1', detail: 'Titre niveau 1', insert: '# ' },
{ label: '## H2', detail: 'Titre niveau 2', insert: '## ' },
{ label: '### H3', detail: 'Titre niveau 3', insert: '### ' },
{ label: '**gras**', detail: 'Texte en gras', insert: '****' },
{ label: '*italique*', detail: 'Texte en italique', insert: '**' },
{ label: '`code`', detail: 'Code inline', insert: '``' },
{ label: '[lien](url)', detail: 'Lien', insert: '[]()' },
{ label: '![image](url)', detail: 'Image', insert: '![]()' },
{ label: '- liste', detail: 'Liste a puces', insert: '- ' },
{ label: '1. liste', detail: 'Liste numerotee', insert: '1. ' },
{ label: '> citation', detail: 'Citation', insert: '> ' },
{ label: '---', detail: 'Separateur horizontal', insert: '\n---\n' },
{ label: '> [!note]', detail: 'Callout Note', insert: '> [!note] Titre\n> Contenu' },
{ label: '> [!warning]', detail: 'Callout Warning', insert: '> [!warning] Titre\n> Contenu' },
{ label: '> [!tip]', detail: 'Callout Tip', insert: '> [!tip] Titre\n> Contenu' },
{ label: '> [!danger]', detail: 'Callout Danger', insert: '> [!danger] Titre\n> Contenu' }
];
// ── Context Detection ─────────────────────────────────────────────────
function detectContext(text, cursorPos) {
var before = text.slice(0, cursorPos);
var after = text.slice(cursorPos);
var lines = before.split('\n');
var currentLine = lines[lines.length - 1] || '';
var lineNum = lines.length;
// Check for code fence context (look backwards for unclosed ```)
var inFence = null;
var fenceLang = '';
var fenceStart = 0;
for (var i = lines.length - 1; i >= 0; i--) {
var l = lines[i].trim();
var m = l.match(/^```(\w*)/);
if (m) {
if (inFence === null) {
inFence = true;
fenceLang = m[1] || '';
fenceStart = i + 1;
} else {
inFence = false;
break;
}
}
}
if (inFence) {
if (fenceLang === 'mermaid') return { type: 'mermaid', lang: '', line: currentLine };
return { type: 'code', lang: fenceLang, line: currentLine };
}
// Check for frontmatter context (between first and second ---)
if (lineNum >= 2) {
var firstSep = -1, secondSep = -1;
for (var j = 0; j < lines.length; j++) {
if (lines[j].trim() === '---') {
if (firstSep === -1) firstSep = j;
else if (secondSep === -1) { secondSep = j; break; }
}
}
if (firstSep === 0 && secondSep === -1) {
return { type: 'frontmatter', line: currentLine };
}
}
// Check for wikilink
if (/\[\[$/.test(currentLine) || /\[\[[^\]]*$/.test(currentLine)) {
var partial = (currentLine.match(/\[\[([^\]]*)$/) || ['',''])[1];
return { type: 'wikilink', partial: partial, line: currentLine };
}
// Check for markdown link
if (/\[.*\]\($/.test(currentLine)) {
return { type: 'link', line: currentLine };
}
// Check for triple backtick (about to open fence)
if (/```$/.test(before) || /^```\s*$/.test(currentLine)) {
return { type: 'fence_open', line: currentLine };
}
// Check if inside a markdown table
if (/^\|.*\|$/.test(currentLine.trim())) {
return { type: 'table', line: currentLine };
}
// Plain text / markdown
return { type: 'markdown', line: currentLine };
}
// ── Suggestion Engine ─────────────────────────────────────────────────
function suggest(text, cursorPos) {
var ctx = detectContext(text, cursorPos);
var items = [];
var prefix = '';
var currentLine = ctx.line;
switch (ctx.type) {
case 'mermaid':
// Try to match mermaid type already declared
var mermaidMatch = text.match(/```mermaid\s*\n(\w+)/);
if (mermaidMatch) {
var mmType = mermaidMatch[1];
items = MERMAID_SYNTAX[mmType] || [];
}
if (items.length === 0) {
items = MERMAID_TYPES;
}
break;
case 'code':
items = CODE_SNIPPETS[ctx.lang] || [];
break;
case 'fence_open':
items = CODE_LANGUAGES;
break;
case 'frontmatter':
prefix = currentLine.trim();
items = FRONTMATTER_FIELDS.filter(function(f) {
return f.label.startsWith(prefix) || prefix === '';
}).map(function(f) {
return { label: f.label, detail: f.detail, insert: prefix ? f.insert : f.insert };
});
return {
context: ctx.type,
items: items.length > 0 ? items : FRONTMATTER_FIELDS.map(function(f) {
return { label: f.label, detail: f.detail, insert: f.insert };
})
};
case 'wikilink':
return {
context: 'wikilink',
items: [],
partial: ctx.partial,
async: true // requires file index lookup
};
case 'markdown':
default:
// At start of line: markdown formatting
if (currentLine.trim().length < 3) {
items = MARKDOWN_FORMAT;
} else {
items = [];
}
break;
}
var filtered = items.filter(function(item) {
return !prefix || item.label.toLowerCase().indexOf(prefix.toLowerCase()) !== -1;
});
return {
context: ctx.type,
items: filtered.length > 0 ? filtered : items
};
}
// ── Wikilink async completion (fetches file list from server) ─────────
var _fileCache = null;
var _fileCacheTime = 0;
function searchWikilinks(partial, callback) {
var now = Date.now();
// Use cached file list if fresh (< 30s)
if (_fileCache && (now - _fileCacheTime) < 30000) {
callback(filterFiles(partial));
return;
}
try {
fetch('/api/search/advanced?q=**&vault=all&limit=500')
.then(function(r) { return r.json(); })
.then(function(data) {
_fileCache = (data.results || []).map(function(r) {
return {
path: r.path,
title: r.title || r.path.replace(/\.md$/i, ''),
vault: r.vault,
name: r.path.split('/').pop().replace(/\.md$/i, '')
};
});
_fileCacheTime = now;
callback(filterFiles(partial));
})
.catch(function() { callback([]); });
} catch(e) { callback([]); }
}
function filterFiles(partial) {
if (!_fileCache) return [];
var q = (partial || '').toLowerCase();
return _fileCache.filter(function(f) {
return f.name.toLowerCase().indexOf(q) !== -1 || f.path.toLowerCase().indexOf(q) !== -1;
}).slice(0, 20).map(function(f) {
return {
label: f.name,
detail: f.path,
insert: f.path + ']]',
vault: f.vault
};
});
}
export { suggest, searchWikilinks, detectContext, MERMAID_TYPES, CODE_LANGUAGES, FRONTMATTER_FIELDS, MARKDOWN_FORMAT };
@@ -1,324 +0,0 @@
/**
* Backup Manager module for ObsiGate.
*
* Full-page view to browse, preview, restore, and delete backups across vaults.
* Accessible from the backup/diff viewer footer.
*/
import { api } from './auth.js';
import { showToast } from './ui.js';
import { safeCreateIcons } from './utils.js';
var state = { backups: [], filter: '', preview: null };
export function openBackupManager() {
var area = document.getElementById('content-area');
if (!area) return;
area.innerHTML = '';
var container = document.createElement('div');
container.className = 'backup-manager';
container.id = 'backup-manager';
container.innerHTML =
'<div class="backup-manager-header">' +
'<h2><i data-lucide="archive" style="width:20px;height:20px"></i> Gestion des backups</h2>' +
'<div class="backup-manager-actions">' +
'<input type="text" id="bm-filter" class="backup-manager-filter" placeholder="Filtrer par fichier...">' +
'<button class="backup-manager-btn" id="bm-refresh" title="Rafraichir"><i data-lucide="refresh-cw" style="width:14px;height:14px"></i></button>' +
'</div>' +
'</div>' +
'<div class="backup-manager-stats" id="bm-stats"></div>' +
'<div class="backup-manager-body">' +
'<div class="backup-manager-list" id="bm-list"></div>' +
'<div class="backup-manager-preview" id="bm-preview" style="display:none">' +
'<div class="bm-preview-header">' +
'<h3 id="bm-preview-title">Apercu</h3>' +
'<button class="backup-manager-btn" id="bm-preview-close" title="Fermer"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
'</div>' +
'<pre class="bm-preview-content" id="bm-preview-content"></pre>' +
'</div>' +
'</div>';
area.appendChild(container);
safeCreateIcons();
document.getElementById('bm-filter').addEventListener('input', function() {
state.filter = this.value.toLowerCase();
render();
});
document.getElementById('bm-refresh').addEventListener('click', load);
document.getElementById('bm-preview-close').addEventListener('click', function() {
document.getElementById('bm-preview').style.display = 'none';
});
state.preview = null;
load();
}
async function load() {
var list = document.getElementById('bm-list');
list.innerHTML = '<div class="tree-loading" style="padding:2rem;text-align:center"><div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div></div>';
try {
var data = await api('/api/backups');
state.backups = data.backups || [];
updateStats(data);
render();
} catch (err) {
list.innerHTML = '<div class="backup-manager-error">Erreur: ' + escapeHtml(err.message || 'Inconnue') + '</div>';
}
}
function updateStats(data) {
var stats = document.getElementById('bm-stats');
if (!stats) return;
var totalSize = formatSize(data.total_size_bytes || 0);
stats.innerHTML =
'<span class="bm-stat"><strong>' + data.total + '</strong> backups</span>' +
'<span class="bm-stat"><strong>' + totalSize + '</strong> total</span>';
}
function render() {
var list = document.getElementById('bm-list');
if (!list) return;
var filtered = state.backups;
if (state.filter) {
filtered = state.backups.filter(function(b) {
return b.file.toLowerCase().indexOf(state.filter) !== -1 || b.vault.toLowerCase().indexOf(state.filter) !== -1;
});
}
if (filtered.length === 0) {
list.innerHTML = '<div class="backup-manager-empty">Aucun backup trouve.</div>';
return;
}
var grouped = {};
filtered.forEach(function(b) {
if (!grouped[b.vault]) grouped[b.vault] = [];
grouped[b.vault].push(b);
});
var html = '';
var vaultNames = Object.keys(grouped).sort();
vaultNames.forEach(function(vault) {
var vaultBackups = grouped[vault];
var vaultSize = vaultBackups.reduce(function(s, b) { return s + b.size; }, 0);
html += '<div class="bm-vault-group">';
html += '<div class="bm-vault-header">';
html += '<h3 class="bm-vault-name"><i data-lucide="database" style="width:14px;height:14px"></i> ' + escapeHtml(vault) + '</h3>';
html += '<span class="bm-vault-meta">' + vaultBackups.length + ' backups &middot; ' + formatSize(vaultSize) + '</span>';
html += '<button class="bm-vault-purge" data-vault="' + escapeHtml(vault) + '" title="Supprimer tous les backups de ce vault"><i data-lucide="trash-2" style="width:14px;height:14px"></i> Vider</button>';
html += '</div>';
var byFile = {};
vaultBackups.forEach(function(b) {
if (!byFile[b.file]) byFile[b.file] = [];
byFile[b.file].push(b);
});
var fileNames = Object.keys(byFile).sort();
fileNames.forEach(function(file) {
var backups = byFile[file];
backups.sort(function(a, b) { return b.timestamp - a.timestamp; });
var totalFileSize = backups.reduce(function(s, b) { return s + b.size; }, 0);
html += '<div class="bm-file-group">';
html += '<div class="bm-file-header">';
html += '<span class="bm-file-name"><i data-lucide="file-text" style="width:14px;height:14px"></i> ' + escapeHtml(file) + '</span>';
html += '<span class="bm-file-meta">' + backups.length + ' versions &middot; ' + formatSize(totalFileSize) + '</span>';
html += '<button class="bm-file-purge" data-vault="' + escapeHtml(vault) + '" data-file="' + escapeHtml(file) + '" title="Supprimer tous les backups de ce fichier"><i data-lucide="trash-2" style="width:12px;height:12px"></i></button>';
html += '</div>';
html += '<div class="bm-backup-list">';
backups.forEach(function(b) {
var d = new Date(b.timestamp * 1000);
var ds = d.getFullYear() + '-' + pad(d.getMonth()+1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
html += '<div class="bm-backup-item' + (state.preview === b.full_path ? ' active' : '') + '" data-path="' + escapeHtml(b.full_path) + '" data-vault="' + escapeHtml(b.vault) + '" data-file="' + escapeHtml(b.file) + '">';
html += '<span class="bm-backup-date">' + ds + '</span>';
html += '<span class="bm-backup-size">' + formatSize(b.size) + '</span>';
html += '<div class="bm-backup-actions">';
html += '<button class="bm-backup-preview" title="Apercu"><i data-lucide="eye" style="width:12px;height:12px"></i></button>';
html += '<button class="bm-backup-restore" title="Restaurer cette version"><i data-lucide="rotate-ccw" style="width:12px;height:12px"></i></button>';
html += '<button class="bm-backup-delete" data-path="' + escapeHtml(b.full_path) + '" title="Supprimer"><i data-lucide="x" style="width:12px;height:12px"></i></button>';
html += '</div>';
html += '</div>';
});
html += '</div>';
html += '</div>';
});
html += '</div>';
});
list.innerHTML = html;
safeCreateIcons();
// Bind vault purge
list.querySelectorAll('.bm-vault-purge').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
if (confirm('Supprimer TOUS les backups du vault "' + btn.dataset.vault + '" ?')) {
purgeVault(btn.dataset.vault);
}
});
});
// Bind file purge
list.querySelectorAll('.bm-file-purge').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
if (confirm('Supprimer TOUS les backups de "' + btn.dataset.file + '" ?')) {
purgeFile(btn.dataset.vault, btn.dataset.file);
}
});
});
// Bind backup item clicks (preview) and action buttons
list.querySelectorAll('.bm-backup-item').forEach(function(item) {
item.addEventListener('click', function(e) {
// Don't trigger preview if clicking an action button
if (e.target.closest('button')) return;
state.preview = item.dataset.path;
render();
previewBackup(item.dataset.path);
});
});
// Bind action buttons
list.querySelectorAll('.bm-backup-preview').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var item = btn.closest('.bm-backup-item');
state.preview = item.dataset.path;
render();
previewBackup(item.dataset.path);
});
});
list.querySelectorAll('.bm-backup-restore').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
var item = btn.closest('.bm-backup-item');
restoreBackup(item.dataset.vault, item.dataset.file);
});
});
list.querySelectorAll('.bm-backup-delete').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
if (confirm('Supprimer ce backup ?')) {
deleteBackups([btn.dataset.path]);
}
});
});
}
async function previewBackup(fpath) {
var panel = document.getElementById('bm-preview');
var content = document.getElementById('bm-preview-content');
var title = document.getElementById('bm-preview-title');
panel.style.display = 'block';
content.textContent = t("common.loading");
try {
var data = await api('/api/backups/content?path=' + encodeURIComponent(fpath));
content.textContent = data.content || '(fichier vide)';
title.textContent = 'Apercu - ' + (data.name || '');
} catch (err) {
content.textContent = 'Erreur: ' + (err.message || 'Inconnue');
}
}
async function restoreBackup(vault, file) {
// Find the backup timestamp from the currently selected backup
var item = document.querySelector('.bm-backup-item.active');
if (!item) { showToast(t('backup.select_to_restore'), 'error'); return; }
var backupPath = item.dataset.path;
// Extract timestamp from the filename
var parts = backupPath.replace(/\\/g, '/').split('/').pop().split('.');
// The timestamp is the second-to-last component before .bak
var ts = null;
for (var i = parts.length - 1; i >= 0; i--) {
if (parts[i] === 'bak' && i > 0 && /^\d+$/.test(parts[i-1])) {
ts = parseInt(parts[i-1]);
break;
}
}
if (!ts) { showToast(t('backup.timestamp_not_found'), 'error'); return; }
if (!confirm('Restaurer "' + file + '" a la version du backup selectionne ?\nLa version actuelle sera sauvegardee.')) return;
try {
var resp = await api('/api/file/' + encodeURIComponent(vault) + '/restore?path=' + encodeURIComponent(file), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: ts }),
});
if (resp && resp.success) {
showToast(t('backup.file_restored'), 'success');
load();
}
} catch (err) {
showToast('Erreur: ' + (err.message || 'Inconnue'), 'error');
}
}
async function deleteBackups(paths) {
try {
var resp = await api('/api/backups/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ paths: paths }),
});
showToast(resp.deleted + ' backup(s) supprime(s)', 'success');
load();
} catch (err) {
showToast('Erreur: ' + err.message, 'error');
}
}
async function purgeFile(vault, file) {
try {
var resp = await api('/api/backups/purge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault: vault, file: file }),
});
showToast(resp.deleted + ' backup(s) supprime(s)', 'success');
load();
} catch (err) {
showToast('Erreur: ' + err.message, 'error');
}
}
async function purgeVault(vault) {
try {
var resp = await api('/api/backups/purge', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ vault: vault }),
});
showToast(resp.deleted + ' backup(s) supprime(s)', 'success');
load();
} catch (err) {
showToast('Erreur: ' + err.message, 'error');
}
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' o';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
}
function pad(n) { return String(n).padStart(2, '0'); }
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
-501
View File
@@ -1,501 +0,0 @@
/**
* Backup & Diff viewer module for ObsiGate.
*
* Provides a modal UI to browse file backups, compare versions with
* a unified diff view, and restore previous versions.
*
* Usage:
* import { openBackupViewer } from './backups.js';
* openBackupViewer(vaultName, filePath);
*/
import { api } from './auth.js';
import { showToast } from './ui.js';
import { refreshSidebarTreePreservingState } from './sidebar.js';
import { safeCreateIcons } from './utils.js';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const state = {
vault: null,
path: null,
backups: [],
overlay: null,
leftVersion: null,
rightVersion: null,
diffText: '',
diffLeft: '',
diffRight: '',
diffMode: 'unified',
};
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
export async function openBackupViewer(vault, path) {
state.vault = vault;
state.path = path;
state.backups = [];
state.leftVersion = null;
state.rightVersion = null;
state.diffText = '';
state.diffLeft = '';
state.diffRight = '';
const overlay = createOverlay();
document.body.appendChild(overlay);
state.overlay = overlay;
requestAnimationFrame(function() { overlay.classList.add('active'); });
setBody(renderLoading());
try {
var url = '/api/file/' + encodeURIComponent(vault) + '/backups?path=' + encodeURIComponent(path);
var data = await api(url);
state.backups = data.backups || [];
} catch (err) {
var msg = err && err.detail || err && err.message || 'Erreur inconnue';
setBody(renderError('Erreur API: ' + msg));
return;
}
if (state.backups.length === 0) {
setBody(renderEmpty());
return;
}
state.leftVersion = state.backups[0].timestamp;
state.rightVersion = null;
renderMainView();
await loadDiff();
}
// ---------------------------------------------------------------------------
// DOM helpers
// ---------------------------------------------------------------------------
var _escHandler = null;
function createOverlay() {
var overlay = document.createElement('div');
overlay.className = 'obsigate-modal-overlay backup-viewer-overlay';
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close();
});
var panel = document.createElement('div');
panel.className = 'backup-viewer-panel';
panel.id = 'backup-viewer-panel';
overlay.appendChild(panel);
// Close button -- static, never replaced by innerHTML
var closeBtn = document.createElement('button');
closeBtn.className = 'editor-btn backup-close-btn';
closeBtn.id = 'backup-close';
closeBtn.title = 'Fermer';
closeBtn.style.cssText = 'position:absolute;top:12px;right:12px;z-index:1002;flex-shrink:0';
closeBtn.innerHTML = '<i data-lucide="x" style="width:15px;height:15px"></i>';
closeBtn.addEventListener('click', function(e) {
e.stopPropagation();
close();
});
overlay.appendChild(closeBtn);
// Escape key
_escHandler = function(e) {
if (e.key === 'Escape' && state.overlay) close();
};
document.addEventListener('keydown', _escHandler);
return overlay;
}
function setBody(html) {
var panel = document.getElementById('backup-viewer-panel');
if (panel) panel.innerHTML = html;
}
function close() {
if (!state.overlay) return;
if (_escHandler) {
document.removeEventListener('keydown', _escHandler);
_escHandler = null;
}
state.overlay.classList.remove('active');
setTimeout(function() {
if (state.overlay) state.overlay.remove();
state.overlay = null;
}, 200);
}
// Global click handler for close button
document.addEventListener('click', function(e) {
var btn = e.target.closest ? e.target.closest('#backup-close') : null;
if (btn && state.overlay) close();
});
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
function renderLoading() {
return '<div class="backup-header"><h3>Backups</h3></div>' +
'<div class="backup-body">' +
'<div class="tree-loading" style="padding:2rem;text-align:center">' +
'<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>' +
'<p style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">Chargement des backups...</p>' +
'</div></div>';
}
function renderError(msg) {
return '<div class="backup-header"><h3>Backups</h3></div>' +
'<div class="backup-body" style="text-align:center;padding:2rem;color:var(--text-muted)">' +
'<p>' + escapeHtml(msg) + '</p></div>';
}
function renderEmpty() {
return '<div class="backup-header"><h3>Backups - ' + escapeHtml(state.path) + '</h3></div>' +
'<div class="backup-body" style="text-align:center;padding:2rem;color:var(--text-muted)">' +
'<p>Aucun backup disponible pour ce fichier.</p>' +
'<p style="font-size:0.8rem;margin-top:0.5rem">Les backups sont crees automatiquement avant chaque modification.</p>' +
'</div>';
}
function renderMainView() {
var b = state.backups;
var leftOpts = b.map(function(bk) {
var sel = bk.timestamp === state.leftVersion ? ' selected' : '';
return '<option value="' + bk.timestamp + '"' + sel + '>' + formatTimestamp(bk.timestamp) + ' (' + formatSize(bk.size) + ')</option>';
}).join('');
var rightOpts = ['<option value="current"' + (state.rightVersion === null ? ' selected' : '') + '>Version actuelle</option>']
.concat(b.map(function(bk) {
var sel = bk.timestamp === state.rightVersion ? ' selected' : '';
return '<option value="' + bk.timestamp + '"' + sel + '>' + formatTimestamp(bk.timestamp) + ' (' + formatSize(bk.size) + ')</option>';
})).join('');
var html =
'<div class="backup-header">' +
'<div class="backup-header-left">' +
'<h3>'+t('backups.title')+'</h3>' +
'<span class="backup-file-path">' + escapeHtml(state.path) + '</span>' +
'</div>' +
'<div class="backup-header-right">' +
'<span class="backup-count">' + b.length + ' backup' + (b.length > 1 ? 's' : '') + '</span>' +
'</div>' +
'</div>' +
'<div class="backup-toolbar">' +
'<div class="backup-selector-group">' +
'<label>Comparer :</label>' +
'<select class="backup-select" id="backup-left-select">' + leftOpts + '</select>' +
'</div>' +
'<span class="backup-vs-label">vs</span>' +
'<div class="backup-selector-group">' +
'<select class="backup-select" id="backup-right-select">' + rightOpts + '</select>' +
'</div>' +
'<button class="backup-restore-btn" id="backup-restore-btn" title="Restaurer la version de gauche">' +
'<i data-lucide="rotate-ccw" class="icon-sm"></i> Restaurer' +
'</button>' +
'<button class="backup-diff-mode-btn" id="backup-diff-mode-btn" title="Vue cote a cote">' +
'<i data-lucide="columns-2" class="icon-sm"></i> Cote-a-cote' +
'</button>' +
'</div>' +
'<div class="backup-diff-container" id="backup-diff-container">' +
'<div class="backup-diff-loading">' +
'<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>' +
'</div>' +
'</div>' +
'<div class="backup-footer">' +
'<span class="backup-footer-hint">Backups crees lors de la sauvegarde manuelle (Ctrl+S)</span>' +
'<button class="backup-footer-manage-btn" id="backup-manage-btn">Gerer tous les backups</button>' +
'</div>';
setBody(html);
bindEvents();
}
// ---------------------------------------------------------------------------
// Event bindings
// ---------------------------------------------------------------------------
function bindEvents() {
var leftSelect = document.getElementById('backup-left-select');
if (leftSelect) {
leftSelect.addEventListener('change', async function() {
state.leftVersion = parseInt(leftSelect.value);
await loadDiff();
});
}
var rightSelect = document.getElementById('backup-right-select');
if (rightSelect) {
rightSelect.addEventListener('change', async function() {
var val = rightSelect.value;
state.rightVersion = val === 'current' ? null : parseInt(val);
await loadDiff();
});
}
var restoreBtn = document.getElementById('backup-restore-btn');
if (restoreBtn) {
restoreBtn.addEventListener('click', async function() {
await restoreBackup();
});
}
var modeBtn = document.getElementById('backup-diff-mode-btn');
var manageBtn = document.getElementById('backup-manage-btn');
if (manageBtn) {
manageBtn.addEventListener('click', function() {
close();
import('./backup-manager.js').then(function(m) { m.openBackupManager(); });
});
}
if (modeBtn) {
modeBtn.addEventListener('click', function() {
state.diffMode = state.diffMode === 'sidebyside' ? 'unified' : 'sidebyside';
var container = document.getElementById('backup-diff-container');
if (container) {
container.innerHTML = state.diffMode === 'sidebyside'
? renderDiffSideBySide(state.diffLeft, state.diffRight)
: renderDiff(state.diffText);
}
var icon = modeBtn.querySelector('i');
if (icon) icon.setAttribute('data-lucide', state.diffMode === 'sidebyside' ? 'columns-2' : 'align-justify');
var textNodes = [];
for (var i = 0; i < modeBtn.childNodes.length; i++) {
if (modeBtn.childNodes[i].nodeType === 3) textNodes.push(modeBtn.childNodes[i]);
}
textNodes.forEach(function(n) { n.remove(); });
modeBtn.appendChild(document.createTextNode(state.diffMode === 'sidebyside' ? ' Unifiee' : ' Cote-a-cote'));
safeCreateIcons();
});
}
}
// ---------------------------------------------------------------------------
// Data loading
// ---------------------------------------------------------------------------
async function loadDiff() {
var container = document.getElementById('backup-diff-container');
if (!container) return;
container.innerHTML =
'<div class="backup-diff-loading">' +
'<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>' +
'</div>';
var url = '/api/file/' + encodeURIComponent(state.vault) + '/diff?path=' + encodeURIComponent(state.path) + '&version=' + state.leftVersion;
if (state.rightVersion !== null) {
url += '&compare_with=' + state.rightVersion;
}
try {
var data = await api(url);
state.diffText = data.diff || '';
state.diffLeft = data.left_content || '';
state.diffRight = data.right_content || '';
container.innerHTML = state.diffMode === 'sidebyside'
? renderDiffSideBySide(state.diffLeft, state.diffRight)
: renderDiff(state.diffText);
} catch (err) {
container.innerHTML = '<div class="backup-diff-error">Erreur: ' + escapeHtml((err && err.detail) || (err && err.message) || 'Unknown') + '</div>';
}
}
async function restoreBackup() {
if (!state.leftVersion) return;
var confirmed = confirm(
'Restaurer "' + state.path + '" a la version du ' + formatTimestamp(state.leftVersion) + ' ?\n\n' +
'La version actuelle sera sauvegardee avant la restauration.'
);
if (!confirmed) return;
var restoreBtn = document.getElementById('backup-restore-btn');
if (restoreBtn) {
restoreBtn.disabled = true;
restoreBtn.textContent = 'Restauration...';
}
try {
var resp = await api(
'/api/file/' + encodeURIComponent(state.vault) + '/restore?path=' + encodeURIComponent(state.path),
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ version: state.leftVersion }),
}
);
if (resp && resp.success) {
showToast(t('backup.file_restored'), 'success');
await refreshSidebarTreePreservingState();
close();
if (typeof TabManager !== 'undefined' && TabManager.reloadCurrent) {
TabManager.reloadCurrent();
}
} else {
showToast(t('backup.restore_failed'), 'error');
}
} catch (err) {
showToast('Erreur: ' + ((err && err.detail) || (err && err.message) || 'Unknown'), 'error');
} finally {
if (restoreBtn) {
restoreBtn.disabled = false;
restoreBtn.innerHTML = '<i data-lucide="rotate-ccw" class="icon-sm"></i> Restaurer';
safeCreateIcons();
}
}
}
// ---------------------------------------------------------------------------
// Diff rendering
// ---------------------------------------------------------------------------
function renderDiff(diffText) {
if (!diffText) {
return '<div class="backup-diff-empty">Aucune difference - les versions sont identiques.</div>';
}
var html = '<div class="backup-diff-output"><table class="backup-diff-table">';
var lines = diffText.split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i];
var cssClass = 'diff-context';
var prefix = ' ';
if (line.indexOf('@@') === 0) { cssClass = 'diff-hunk'; prefix = ''; }
else if (line.indexOf('+') === 0) { cssClass = 'diff-add'; prefix = '+'; }
else if (line.indexOf('-') === 0) { cssClass = 'diff-del'; prefix = '-'; }
else if (line.indexOf('---') === 0 || line.indexOf('+++') === 0) { cssClass = 'diff-header-line'; prefix = ''; }
html += '<tr class="' + cssClass + '"><td class="diff-prefix">' + prefix + '</td><td class="diff-content"><pre>' + escapeHtml(line) + '</pre></td></tr>';
}
html += '</table></div>';
return html;
}
// ---------------------------------------------------------------------------
// Side-by-side diff rendering
// ---------------------------------------------------------------------------
function renderDiffSideBySide(leftText, rightText) {
if (!leftText && !rightText) {
return '<div class="backup-diff-empty">Aucune difference - les versions sont identiques.</div>';
}
var leftLines = (leftText || '').split('\n');
var rightLines = (rightText || '').split('\n');
var sm = new DifflibSeqMatcher(leftLines, rightLines);
var ops = sm.getOpcodes();
var html = '<div class="backup-diff-output backup-diff-sidebyside"><table class="backup-diff-table sidebyside"><colgroup><col style="width:50%"><col style="width:50%"></colgroup><tbody>';
for (var oi = 0; oi < ops.length; oi++) {
var op = ops[oi];
var tag = op[0], i1 = op[1], i2 = op[2], j1 = op[3], j2 = op[4];
if (tag === 'equal') {
for (var k = 0; k < (i2 - i1); k++) {
html += '<tr class="diff-context"><td class="diff-content sidebyside-left"><pre>' + escapeHtml(leftLines[i1 + k]) + '</pre></td><td class="diff-content sidebyside-right"><pre>' + escapeHtml(rightLines[j1 + k]) + '</pre></td></tr>';
}
} else if (tag === 'delete') {
for (var k2 = i1; k2 < i2; k2++) {
html += '<tr class="diff-del"><td class="diff-content sidebyside-left"><pre>' + escapeHtml(leftLines[k2]) + '</pre></td><td class="diff-content sidebyside-right diff-empty-cell"></td></tr>';
}
} else if (tag === 'insert') {
for (var k3 = j1; k3 < j2; k3++) {
html += '<tr class="diff-add"><td class="diff-content sidebyside-left diff-empty-cell"></td><td class="diff-content sidebyside-right"><pre>' + escapeHtml(rightLines[k3]) + '</pre></td></tr>';
}
} else if (tag === 'replace') {
var maxLen = Math.max(i2 - i1, j2 - j1);
for (var k4 = 0; k4 < maxLen; k4++) {
var leftLine = (i1 + k4 < i2) ? escapeHtml(leftLines[i1 + k4]) : '';
var rightLine = (j1 + k4 < j2) ? escapeHtml(rightLines[j1 + k4]) : '';
var leftClass = (i1 + k4 < i2) ? 'diff-del' : 'diff-empty-cell';
var rightClass = (j1 + k4 < j2) ? 'diff-add' : 'diff-empty-cell';
html += '<tr><td class="diff-content sidebyside-left ' + leftClass + '"><pre>' + leftLine + '</pre></td><td class="diff-content sidebyside-right ' + rightClass + '"><pre>' + rightLine + '</pre></td></tr>';
}
}
}
html += '</tbody></table></div>';
return html;
}
// Minimal SequenceMatcher for line-based diff
function DifflibSeqMatcher(a, b) {
this.a = a;
this.b = b;
}
DifflibSeqMatcher.prototype.getOpcodes = function() {
var m = this.a.length, n = this.b.length;
var dp = [];
for (var i = 0; i <= m; i++) { dp[i] = []; for (var j = 0; j <= n; j++) dp[i][j] = 0; }
for (var i = 1; i <= m; i++) {
for (var j = 1; j <= n; j++) {
if (this.a[i - 1] === this.b[j - 1]) dp[i][j] = dp[i - 1][j - 1] + 1;
else dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
var ops = [];
var i = m, j = n;
while (i > 0 || j > 0) {
if (i > 0 && j > 0 && this.a[i - 1] === this.b[j - 1]) {
var ei = i - 1, ej = j - 1;
while (ei > 0 && ej > 0 && this.a[ei - 1] === this.b[ej - 1]) { ei--; ej--; }
if (ei < i) ops.unshift(['equal', ei, i, ej, j]);
i = ei; j = ej;
} else if (j > 0 && (i === 0 || dp[i][j - 1] >= dp[i - 1][j])) {
ops.unshift(['insert', i, i, j - 1, j]);
j--;
} else if (i > 0 && (j === 0 || dp[i][j - 1] < dp[i - 1][j])) {
ops.unshift(['delete', i - 1, i, j, j]);
i--;
}
}
var merged = [];
for (var k = 0; k < ops.length; k++) {
if (merged.length > 0 && merged[merged.length - 1][0] === 'delete' && ops[k][0] === 'insert' &&
merged[merged.length - 1][4] === ops[k][3]) {
var del = merged.pop();
merged.push(['replace', del[1], del[2], ops[k][3], ops[k][4]]);
} else {
merged.push(ops[k]);
}
}
return merged;
};
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
function formatTimestamp(ts) {
var d = new Date(ts * 1000);
var pad = function(n) { return String(n).padStart(2, '0'); };
return d.getFullYear() + '-' + pad(d.getMonth() + 1) + '-' + pad(d.getDate()) + ' ' + pad(d.getHours()) + ':' + pad(d.getMinutes());
}
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' o';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' Ko';
return (bytes / (1024 * 1024)).toFixed(1) + ' Mo';
}
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
File diff suppressed because it is too large Load Diff
-484
View File
@@ -1,484 +0,0 @@
// dashboard.js — extracted from app.js (3414-3806) + DashboardBookmarkWidget (3810-3870)
import { api } from './auth.js';
import { state } from './state.js';
import { escapeHtml, safeCreateIcons, getFileIcon } from './utils.js';
import { icon, openFile } from './viewer.js';
import { TabManager, showToast } from './ui.js';
import { t, getLocale } from './i18n.js';
// ---------------------------------------------------------------------------
// Recent files
// ---------------------------------------------------------------------------
let _recentRefreshTimer = null;
let _recentTimestampTimer = null;
let _recentFilesCache = [];
// ---------------------------------------------------------------------------
// Dashboard Recent Files Widget
// ---------------------------------------------------------------------------
// ── Dashboard Stats Widget ──
const DashboardStatsWidget = {
async load() {
const grid = document.getElementById("dashboard-stats-grid");
if (!grid) return;
grid.innerHTML = `<div class="dashboard-stats-loading">${t('common.loading')}</div>`;
try {
const data = await api("/api/dashboard");
this.render(data);
} catch (err) {
grid.innerHTML = `<div class="dashboard-recent-empty">${t('dashboard.error_prefix')}${escapeHtml(err.message)}</div>`;
}
},
render(data) {
const grid = document.getElementById("dashboard-stats-grid");
if (!grid) return;
const fmtSize = (bytes) => bytes < 1024 ? `${bytes}${t('dashboard.size_bytes')}` : bytes < 1048576 ? `${(bytes/1024).toFixed(1)}${t('dashboard.size_kb')}` : bytes < 1073741824 ? `${(bytes/1048576).toFixed(1)}${t('dashboard.size_mb')}` : `${(bytes/1073741824).toFixed(1)}${t('dashboard.size_gb')}`;
const items = [
{ icon: "files", label: t('dashboard.stats_files'), value: data.total_files.toLocaleString() },
{ icon: "tags", label: t('dashboard.stats_tags'), value: data.total_tags.toLocaleString() },
{ icon: "hard-drive", label: t('dashboard.stats_size'), value: fmtSize(data.total_size_bytes) },
{ icon: "folder-open", label: t('dashboard.stats_vaults'), value: data.vaults.length.toString() },
];
grid.innerHTML = items.map(i => `
<div class="stat-card">
<i data-lucide="${i.icon}" class="stat-icon"></i>
<span class="stat-value">${i.value}</span>
<span class="stat-label">${i.label}</span>
</div>
`).join("");
safeCreateIcons();
}
};
// ── Dashboard Shared Widget ──
const DashboardSharedWidget = {
async load() {
const grid = document.getElementById("dashboard-shared-grid");
const empty = document.getElementById("dashboard-shared-empty");
if (!grid) return;
try {
const shares = await api("/api/shares");
if (!shares.length) { if (empty) empty.style.display = ""; grid.innerHTML = ""; return; }
if (empty) empty.style.display = "none";
grid.innerHTML = shares.map(s => `
<div class="shared-card" data-vault="${escapeHtml(s.vault)}" data-path="${escapeHtml(s.path)}">
<div class="shared-card-header">
<i data-lucide="file-text" style="width:14px;height:14px"></i>
<span class="shared-card-title">${escapeHtml(s.path.split("/").pop().replace(/\.md$/i, ""))}</span>
<span class="shared-card-vault">${escapeHtml(s.vault)}</span>
</div>
<div class="shared-card-meta">
<span>${s.access_count || 0} ${t('dashboard.views', {count: s.access_count || 0})}</span>
${s.expires_at ? `<span>${t('dashboard.expires', {date: new Date(s.expires_at).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US')})}</span>` : ""}
</div>
<div class="shared-card-actions">
<button class="shared-copy-btn" data-url="${window.location.origin}/s/${s.token}">${t('dashboard.copy_btn')}</button>
<button class="shared-open-btn">${t('dashboard.open_btn')}</button>
<button class="shared-revoke-btn" data-id="${s.id}">🗑</button>
</div>
</div>
`).join("");
lucide.createIcons();
grid.querySelectorAll(".shared-copy-btn").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
const url = b.dataset.url;
try { await navigator.clipboard.writeText(url); } catch { const ta = document.createElement("textarea"); ta.value=url; ta.style.position="fixed"; ta.style.left="-9999px"; document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta); }
showToast(t("config.link_copied"), "success");
}));
grid.querySelectorAll(".shared-open-btn").forEach(b => b.addEventListener("click", (e) => {
e.stopPropagation();
const card = b.closest(".shared-card");
if (card) TabManager.openPreview(card.dataset.vault, card.dataset.path);
}));
grid.querySelectorAll(".shared-revoke-btn").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
await api(`/api/share/${b.dataset.id}`, { method: "DELETE" });
showToast(t("config.share_revoked"), "success");
this.load();
}));
grid.querySelectorAll(".shared-card").forEach(card => card.addEventListener("click", () => {
TabManager.openPreview(card.dataset.vault, card.dataset.path);
}));
} catch (err) { if (empty) empty.style.display = ""; }
}
};
// ── Dashboard Conflicts Widget ──
const DashboardConflictsWidget = {
async load() {
const container = document.getElementById("dashboard-conflicts-container");
if (!container) return;
try {
const data = await api("/api/conflicts");
if (data.total === 0) { container.innerHTML = ""; return; }
this.render(data.conflicts, container);
} catch (err) { container.innerHTML = ""; }
},
render(conflicts, container) {
container.innerHTML = `
<div class="dashboard-section">
<div class="dashboard-header">
<div class="dashboard-title-row">
<i data-lucide="alert-triangle" class="dashboard-icon" style="color:var(--accent-orange)"></i>
<h2>${t('dashboard.conflicts_title')}</h2>
<span class="dashboard-badge" style="background:var(--accent-orange)">${conflicts.length}</span>
</div>
</div>
<div class="dashboard-conflicts-grid">
${conflicts.map(c => `
<div class="conflict-card">
<div class="conflict-info">
<span class="conflict-vault">${escapeHtml(c.vault)}</span>
<span class="conflict-name">${escapeHtml(c.conflict_path.split("/").pop())}</span>
<span class="conflict-date">${t('dashboard.conflict_date')}${c.conflict_date.replace(/(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})/, "$3/$2/$1 $4:$5")}</span>
</div>
<div class="conflict-actions">
<button class="conflict-btn keep-local" data-vault="${escapeHtml(c.vault)}" data-conflict="${escapeHtml(c.conflict_path)}" data-original="${escapeHtml(c.original_path)}">${t('dashboard.keep_original')}</button>
<button class="conflict-btn keep-conflict" data-vault="${escapeHtml(c.vault)}" data-conflict="${escapeHtml(c.conflict_path)}" data-original="${escapeHtml(c.original_path)}">${t('dashboard.keep_conflict')}</button>
</div>
</div>
`).join("")}
</div>
</div>`;
lucide.createIcons();
container.querySelectorAll(".keep-local").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_local")));
container.querySelectorAll(".keep-conflict").forEach(btn => btn.addEventListener("click", () => this._resolve(btn.dataset, "keep_conflict")));
},
async _resolve(d, action) {
try {
await api("/api/conflicts/resolve", { method: "POST", body: JSON.stringify({ vault: d.vault, conflict_path: d.conflict, original_path: d.original, action }) });
showToast(t("dashboard.conflict_resolved"), "success");
this.load();
} catch (err) { showToast(t('dashboard.error_prefix') + err.message, "error"); }
}
};
const DashboardRecentWidget = {
_cache: [],
_currentFilter: "",
async load(vaultFilter = "") {
const v = vaultFilter || state.selectedContextVault || "all";
this._currentFilter = v;
this.showLoading();
let url = "/api/recent?mode=opened";
if (v !== "all") url += `&vault=${encodeURIComponent(v)}`;
try {
const data = await api(url);
this._cache = data.files || [];
this.render();
} catch (err) {
console.error("Dashboard: Failed to load recent files:", err);
this.showError();
}
},
async toggleBookmark(vault, path, title, card) {
try {
const data = await api("/api/bookmarks/toggle", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ vault, path, title }),
});
// Refresh both widgets to keep sync
DashboardBookmarkWidget.load();
// Update current card icon if it exists
if (card) {
const btn = card.querySelector(".dashboard-card-bookmark-btn");
if (btn) {
btn.classList.toggle("active", data.bookmarked);
const icon = btn.querySelector("i");
if (icon) icon.setAttribute("data-lucide", data.bookmarked ? "bookmark" : "bookmark-plus");
safeCreateIcons();
}
}
// Check if we need to refresh the current list to reflect bookmark status across all cards
// To avoid flickering, just update the cache and re-render if needed or do a silent refresh
this._cache.forEach(f => {
if (f.vault === vault && f.path === path) f.bookmarked = data.bookmarked;
});
} catch (err) {
console.error("Failed to toggle bookmark:", err);
showToast(t("dashboard.pin_error"), "error");
}
},
showLoading() {
const grid = document.getElementById("dashboard-recent-grid");
const loading = document.getElementById("dashboard-loading");
const empty = document.getElementById("dashboard-recent-empty");
const count = document.getElementById("dashboard-count");
if (grid) grid.innerHTML = "";
if (loading) loading.classList.add("active");
if (empty) empty.classList.add("hidden");
if (count) count.textContent = "";
},
render() {
const grid = document.getElementById("dashboard-recent-grid");
const loading = document.getElementById("dashboard-loading");
const empty = document.getElementById("dashboard-recent-empty");
const count = document.getElementById("dashboard-count");
if (loading) loading.classList.remove("active");
if (!this._cache || this._cache.length === 0) {
this.showEmpty();
return;
}
if (empty) empty.classList.add("hidden");
if (count) count.textContent = `${this._cache.length} ${this._cache.length > 1 ? t('dashboard.count_files_plural') : t('dashboard.count_files')}`;
if (!grid) return;
grid.innerHTML = "";
this._cache.forEach((f, index) => {
const card = this._createCard(f, index);
grid.appendChild(card);
});
safeCreateIcons();
},
_createCard(file, index) {
const card = document.createElement("div");
card.className = "dashboard-card";
card.setAttribute("data-vault", file.vault);
card.setAttribute("data-path", file.path);
card.style.animationDelay = `${Math.min(index * 50, 400)}ms`;
// Header with icon and vault badge
const header = document.createElement("div");
header.className = "dashboard-card-header";
const iconContainer = document.createElement("div");
iconContainer.className = "dashboard-card-icon";
const fileIconName = getFileIcon(file.path);
try {
iconContainer.appendChild(icon(fileIconName, 24));
} catch (e) {
console.error("Error creating icon:", fileIconName, e);
// Fallback to default file icon
iconContainer.appendChild(icon("file", 24));
}
const badge = document.createElement("span");
badge.className = "dashboard-vault-badge";
badge.textContent = file.vault;
const bookmarkBtn = document.createElement("button");
bookmarkBtn.className = `dashboard-card-bookmark-btn ${file.bookmarked ? "active" : ""}`;
bookmarkBtn.title = file.bookmarked ? t('dashboard.bookmark_remove') : t('dashboard.bookmark_add');
bookmarkBtn.innerHTML = `<i data-lucide="${file.bookmarked ? "bookmark" : "bookmark-plus"}" style="width:14px;height:14px"></i>`;
bookmarkBtn.addEventListener("click", (e) => {
e.stopPropagation();
this.toggleBookmark(file.vault, file.path, file.title, card);
});
header.appendChild(iconContainer);
header.appendChild(badge);
header.appendChild(bookmarkBtn);
card.appendChild(header);
// Title
const title = document.createElement("h3");
title.className = "dashboard-card-title";
title.textContent = file.title || file.path.split("/").pop();
title.title = file.title || file.path;
card.appendChild(title);
// Path (compact)
const pathParts = file.path.split("/");
if (pathParts.length > 1) {
const path = document.createElement("div");
path.className = "dashboard-card-path";
path.textContent = pathParts.slice(0, -1).join(" / ");
path.title = file.path;
card.appendChild(path);
}
// Footer with time and tags
const footer = document.createElement("div");
footer.className = "dashboard-card-footer";
const time = document.createElement("span");
time.className = "dashboard-card-time";
time.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> ${file.mtime_human || this._humanizeDelta(file.mtime)}`;
footer.appendChild(time);
// Tags
if (file.tags && file.tags.length > 0) {
const tags = document.createElement("div");
tags.className = "dashboard-card-tags";
file.tags.slice(0, 3).forEach((tag) => {
const tagEl = document.createElement("span");
tagEl.className = "tag-pill";
tagEl.textContent = tag;
tags.appendChild(tagEl);
});
footer.appendChild(tags);
}
card.appendChild(footer);
// Click handler
card.addEventListener("click", () => {
openFile(file.vault, file.path);
});
return card;
},
showEmpty() {
const grid = document.getElementById("dashboard-recent-grid");
const loading = document.getElementById("dashboard-loading");
const empty = document.getElementById("dashboard-recent-empty");
const count = document.getElementById("dashboard-count");
if (grid) grid.innerHTML = "";
if (loading) loading.classList.remove("active");
if (empty) empty.classList.remove("hidden");
if (count) count.textContent = t('dashboard.zero_files');
safeCreateIcons();
},
showError() {
this.showEmpty();
const empty = document.getElementById("dashboard-recent-empty");
if (empty) {
const msg = empty.querySelector("span");
if (msg) msg.textContent = t('dashboard.load_error');
}
},
_humanizeDelta(mtime) {
const delta = Date.now() / 1000 - mtime;
if (delta < 60) return t('dashboard.just_now');
if (delta < 3600) return t('dashboard.min_ago', { n: Math.floor(delta / 60) });
if (delta < 86400) return t('dashboard.hour_ago', { n: Math.floor(delta / 3600) });
if (delta < 604800) return t('dashboard.day_ago', { n: Math.floor(delta / 86400) });
return new Date(mtime * 1000).toLocaleDateString(getLocale() === 'fr' ? 'fr-FR' : 'en-US', { day: "numeric", month: "short", year: "numeric" });
},
populateVaultFilter() {
const select = document.getElementById("dashboard-vault-filter");
if (!select) return;
// Keep first option "Tous les vaults"
while (select.options.length > 1) select.remove(1);
if (typeof state.allVaults !== "undefined" && Array.isArray(state.allVaults)) {
state.allVaults.forEach((v) => {
const opt = document.createElement("option");
opt.value = v.name;
opt.textContent = v.name;
select.appendChild(opt);
});
}
syncVaultSelectors();
},
init() {
const select = document.getElementById("dashboard-vault-filter");
if (select) {
select.addEventListener("change", async () => {
await setSelectedVaultContext(select.value, { focusVault: select.value !== "all" });
});
}
this.populateVaultFilter();
// Quick Help toggle (with localStorage persistence)
var qhToggle = document.getElementById('qh-toggle');
var qhBody = document.getElementById('qh-body');
if (qhToggle && qhBody) {
// Restore saved state
if (localStorage.getItem('obsigate-qh-collapsed') === '1') {
qhBody.classList.add('collapsed');
qhToggle.textContent = '+';
qhToggle.title = t('dashboard.show');
}
qhToggle.addEventListener('click', function() {
var collapsed = qhBody.classList.toggle('collapsed');
qhToggle.textContent = collapsed ? '+' : '\u2212';
qhToggle.title = collapsed ? t('dashboard.show') : t('dashboard.hide');
localStorage.setItem('obsigate-qh-collapsed', collapsed ? '1' : '0');
});
}
},
};
// ── Dashboard Bookmarks Widget ──
// (moved from app.js 3810-3870; logically belongs with dashboard widgets)
const DashboardBookmarkWidget = {
_cache: [],
_currentFilter: "",
async load(vaultFilter = "") {
const v = vaultFilter || state.selectedContextVault || "all";
this._currentFilter = v;
this.showLoading();
let url = "/api/bookmarks";
if (v !== "all") url += `?vault=${encodeURIComponent(v)}`;
try {
const data = await api(url);
this._cache = data.files || [];
this.render();
} catch (err) {
console.error("Dashboard: Failed to load bookmarks:", err);
this.showEmpty();
}
},
showLoading() {
const grid = document.getElementById("dashboard-bookmarks-grid");
const empty = document.getElementById("dashboard-bookmarks-empty");
const section = document.getElementById("dashboard-bookmarks-section");
if (grid) grid.innerHTML = "";
if (empty) empty.classList.add("hidden");
},
render() {
const grid = document.getElementById("dashboard-bookmarks-grid");
const empty = document.getElementById("dashboard-bookmarks-empty");
const section = document.getElementById("dashboard-bookmarks-section");
if (!this._cache || this._cache.length === 0) {
if (grid) grid.innerHTML = "";
if (empty) empty.classList.remove("hidden");
return;
}
if (empty) empty.classList.add("hidden");
if (!grid) return;
grid.innerHTML = "";
this._cache.forEach((f, idx) => {
const card = DashboardRecentWidget._createCard(f, idx);
grid.appendChild(card);
});
safeCreateIcons();
},
showEmpty() {
const grid = document.getElementById("dashboard-bookmarks-grid");
const empty = document.getElementById("dashboard-bookmarks-empty");
if (grid) grid.innerHTML = "";
if (empty) empty.classList.remove("hidden");
}
};
export { DashboardRecentWidget, DashboardStatsWidget, DashboardBookmarkWidget, DashboardSharedWidget, DashboardConflictsWidget };
-329
View File
@@ -1,329 +0,0 @@
/**
* Drag & Drop module for ObsiGate sidebar tree.
*
* Allows drag-and-drop of tree items (files and directories) onto
* target directories or vault roots to reorganize the vault.
*
* Usage: import { initDragDrop } from './dragdrop.js';
* initDragDrop() after the vault tree is ready.
*/
import { api } from './auth.js';
import { showToast } from './ui.js';
import { refreshSidebarTreePreservingState } from './sidebar.js';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
/** Currently dragged item: { vault, path, type, element } */
let dragState = null;
// ---------------------------------------------------------------------------
// CSS classes applied during drag operations
// ---------------------------------------------------------------------------
const DRAG_OVER_CLASS = 'drag-over';
const DRAG_SOURCE_CLASS = 'drag-source';
const DRAG_VALID_TARGET_CLASS = 'drag-valid-target';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
/**
* Walk up the DOM to find the closest ancestor matching the selector.
*/
function closest(selector, el) {
while (el && el !== document) {
if (el.matches && el.matches(selector)) return el;
el = el.parentNode;
}
return null;
}
/**
* Determine if an element is a valid drop target (directory or vault root).
* Returns { vault, targetDir } or null.
*/
function getDropTarget(el) {
// Directory tree item
const dirItem = closest('.tree-item[data-path]', el);
if (dirItem) {
// Only directories are valid targets
const icon = dirItem.querySelector('[data-lucide]');
const isDir = icon && (icon.getAttribute('data-lucide') === 'folder'
|| icon.getAttribute('data-lucide') === 'chevron-right'
|| icon.getAttribute('data-lucide') === 'chevron-down');
// Heuristic: if the tree item has a child container sibling, it's a directory
const hasChildren = dirItem.nextElementSibling
&& dirItem.nextElementSibling.classList.contains('tree-children');
if (isDir || hasChildren) {
return {
vault: dirItem.dataset.vault,
targetDir: dirItem.dataset.path,
};
}
return null;
}
// Vault item (root of a vault)
const vaultItem = closest('.tree-item.vault-item[data-vault]', el);
if (vaultItem) {
return {
vault: vaultItem.dataset.vault,
targetDir: '', // empty = vault root
};
}
return null;
}
/**
* Highlight drop target and its parent chain.
*/
function highlightDropTarget(el) {
const target = getDropTarget(el);
if (!target) return;
if (target.targetDir) {
// Highlight the directory
const dirEl = document.querySelector(
`.tree-item[data-vault="${CSS.escape(target.vault)}"][data-path="${CSS.escape(target.targetDir)}"]`
);
if (dirEl) dirEl.classList.add(DRAG_OVER_CLASS);
} else {
// Highlight the vault
const vaultEl = document.querySelector(`.tree-item.vault-item[data-vault="${CSS.escape(target.vault)}"]`);
if (vaultEl) vaultEl.classList.add(DRAG_OVER_CLASS);
}
}
/**
* Remove all drop target highlights.
*/
function clearAllHighlights() {
document.querySelectorAll(`.${DRAG_OVER_CLASS}`).forEach(el => el.classList.remove(DRAG_OVER_CLASS));
}
/**
* Check if the source item is being dropped on itself or inside itself.
*/
function isInvalidDrop(source, target) {
if (source.vault !== target.vault) return true; // cross-vault not supported
if (!target.targetDir) {
// Target is vault root — check if source is already at root
const sourceParent = source.path.includes('/')
? source.path.substring(0, source.path.lastIndexOf('/'))
: '';
return sourceParent === '';
}
// Can't drop a directory into itself or its children
if (source.type === 'directory') {
if (source.path === target.targetDir) return true; // dropping on itself
if (target.targetDir.startsWith(source.path + '/')) return true; // dropping into a child
}
// Check if source is already in this directory
const sourceParent = source.path.includes('/')
? source.path.substring(0, source.path.lastIndexOf('/'))
: '';
if (sourceParent === target.targetDir) return true; // same directory
return false;
}
// ---------------------------------------------------------------------------
// Event handlers
// ---------------------------------------------------------------------------
function onDragStart(e) {
const treeItem = closest('.tree-item', e.target);
if (!treeItem) return;
// Don't allow dragging vault items themselves
if (treeItem.classList.contains('vault-item')) return;
const vault = treeItem.dataset.vault;
const path = treeItem.dataset.path;
if (!vault || path === undefined) return;
// Determine type: directory or file
const hasChildren = treeItem.nextElementSibling
&& treeItem.nextElementSibling.classList.contains('tree-children');
const type = hasChildren ? 'directory' : 'file';
dragState = { vault, path, type, element: treeItem };
// Set drag data and visual feedback
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', path);
// Visual: reduce opacity of source
treeItem.classList.add(DRAG_SOURCE_CLASS);
// Add valid-target indicator to all directories and vaults
setTimeout(() => {
document.querySelectorAll('.tree-item').forEach(item => {
if (item === treeItem) return;
if (item.classList.contains('vault-item')) {
item.classList.add(DRAG_VALID_TARGET_CLASS);
} else {
const icon = item.querySelector('[data-lucide]');
const hasChildContainer = item.nextElementSibling
&& item.nextElementSibling.classList.contains('tree-children');
if (icon && (icon.getAttribute('data-lucide') === 'folder' || hasChildContainer)) {
item.classList.add(DRAG_VALID_TARGET_CLASS);
}
}
});
}, 0);
}
function onDragOver(e) {
if (!dragState) return;
e.preventDefault(); // Required to allow drop
e.dataTransfer.dropEffect = 'move';
clearAllHighlights();
const target = getDropTarget(e.target);
if (!target || isInvalidDrop(dragState, target)) {
e.dataTransfer.dropEffect = 'none';
return;
}
highlightDropTarget(e.target);
}
function onDragLeave(e) {
// Only clear when actually leaving the container, not entering a child
const container = document.getElementById('vault-tree');
if (!container) return;
// Use a small delay to avoid flickering when moving between children
clearAllHighlights();
}
function onDragEnd() {
clearAllHighlights();
// Remove drag source visual
if (dragState && dragState.element) {
dragState.element.classList.remove(DRAG_SOURCE_CLASS);
}
// Remove valid-target indicators
document.querySelectorAll(`.${DRAG_VALID_TARGET_CLASS}`).forEach(el => {
el.classList.remove(DRAG_VALID_TARGET_CLASS);
});
dragState = null;
}
async function onDrop(e) {
e.preventDefault();
e.stopPropagation();
if (!dragState) return;
const target = getDropTarget(e.target);
if (!target || isInvalidDrop(dragState, target)) {
onDragEnd();
return;
}
const { vault, path, type } = dragState;
const { vault: targetVault, targetDir } = target;
// Don't move if source and target are the same vault
if (vault !== targetVault) {
showToast(t('dragdrop.cross_vault_unsupported'), 'error');
onDragEnd();
return;
}
// Save drag state before cleanup
const savedState = { ...dragState };
// Clean up visual state immediately
onDragEnd();
// Call API
try {
const response = await api(`/api/move/${encodeURIComponent(vault)}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source_path: path,
destination_dir: targetDir,
}),
});
if (response && response.success) {
showToast(`Moved ${type} to ${targetDir || 'vault root'}`, 'success');
// Refresh the tree preserving expanded state
await refreshSidebarTreePreservingState();
} else {
showToast(t('dragdrop.move_failed'), 'error');
}
} catch (err) {
console.error('Drag & drop move failed:', err);
const detail = err?.detail || err?.message || 'Unknown error';
showToast(`Move failed: ${detail}`, 'error');
// Refresh anyway in case the filesystem state changed
try { await refreshSidebarTreePreservingState(); } catch (_) { /* ignore */ }
}
}
// ---------------------------------------------------------------------------
// Initialization
// ---------------------------------------------------------------------------
/**
* Attach drag-and-drop event listeners to the vault tree container.
* Uses event delegation on the #vault-tree element.
*/
export function initDragDrop() {
const container = document.getElementById('vault-tree');
if (!container) {
console.warn('DragDrop: vault-tree container not found');
return;
}
// Remove previous listeners if any (idempotent)
container.removeEventListener('dragstart', onDragStart);
container.removeEventListener('dragover', onDragOver);
container.removeEventListener('dragleave', onDragLeave);
container.removeEventListener('dragend', onDragEnd);
container.removeEventListener('drop', onDrop);
// Attach listeners with delegation
container.addEventListener('dragstart', onDragStart);
container.addEventListener('dragover', onDragOver);
container.addEventListener('dragleave', onDragLeave);
container.addEventListener('dragend', onDragEnd);
container.addEventListener('drop', onDrop);
// Make all existing tree items draggable
makeAllDraggable();
// Re-apply draggable after any DOM mutation in the tree
const observer = new MutationObserver(() => {
makeAllDraggable();
});
observer.observe(container, { childList: true, subtree: true });
}
/**
* Make all tree items (files and directories) draggable.
* Vault items themselves are NOT draggable.
*/
function makeAllDraggable() {
const container = document.getElementById('vault-tree');
if (!container) return;
container.querySelectorAll('.tree-item').forEach(item => {
if (item.classList.contains('vault-item')) return;
if (!item.hasAttribute('draggable')) {
item.setAttribute('draggable', 'true');
}
});
}
-949
View File
@@ -1,949 +0,0 @@
/* ObsiGate — Graph View: interactive file/folder relationship visualization.
Phase 2: theme colors, tooltips, depth slider, full-vault, search, backlinks. */
import { api } from './auth.js';
import { t } from './i18n.js';
import { safeCreateIcons } from './utils.js';
import { openFile } from './viewer.js';
// ---------------------------------------------------------------------------
// Theme-aware color helpers
// ---------------------------------------------------------------------------
function _cssVar(name, fallback) {
return getComputedStyle(document.body).getPropertyValue(name).trim() || fallback;
}
const COLORS = {
get bg() { return _cssVar('--bg-primary', '#1e1e1e'); },
get text() { return _cssVar('--text-primary', '#ddd'); },
get accent() { return _cssVar('--accent-color', '#2563eb'); },
get muted() { return _cssVar('--text-muted', '#888'); },
get border() { return _cssVar('--border-color', '#333'); },
dir: '#5b9bd5',
md: '#70ad47',
other: '#999',
vault: '#ffc000',
backlink: '#e74c3c',
highlight: '#ff6b6b',
};
// ---------------------------------------------------------------------------
// GraphViewManager
// ---------------------------------------------------------------------------
export const GraphViewManager = {
_canvas: null,
_ctx: null,
_nodes: [],
_edges: [],
_offsetX: 0,
_offsetY: 0,
_zoom: 1,
_dragging: false,
_dragNode: null,
_panning: false,
_panStartX: 0,
_panStartY: 0,
_animFrame: null,
_vault: null,
_path: null,
_nodePositions: {},
_width: 0,
_height: 0,
_scope: 'directory',
_depth: 1,
_searchTerm: '',
_tagFilter: '',
_hoveredNode: null,
_infoPanel: null,
_infoContent: null,
_previewPanel: null,
_previewContent: null,
_previewCache: {},
_previewPending: null,
_ctrlHoverNode: null,
_navHistory: [],
_navIndex: -1,
async open(vault, path, type) {
// Push current state to history before navigating (unless navigating via back/fwd)
if (this._vault && !this._navNavigating) {
this._pushHistory(this._vault, this._path || '');
}
this._navNavigating = false;
this._vault = vault;
this._path = path;
const modal = document.getElementById('graph-modal');
const title = document.getElementById('graph-title');
const statusEl = document.getElementById('graph-status');
const canvas = document.getElementById('graph-canvas');
const depthSlider = document.getElementById('graph-depth');
if (!modal || !canvas) return;
this._infoPanel = document.getElementById('graph-info-panel');
this._infoContent = document.getElementById('graph-info-content');
this._previewPanel = document.getElementById('graph-preview-panel');
this._previewContent = document.getElementById('graph-preview-content');
this._depth = depthSlider ? parseInt(depthSlider.value) : 1;
this._scope = 'directory';
title.textContent = t('graph.title', { vault, path: path ? '/' + path : '' });
statusEl.textContent = t('graph.loading');
modal.classList.add('active');
this._canvas = canvas;
this._ctx = canvas.getContext('2d');
this._resetView();
await this._fetchAndRender();
this._updateNavButtons();
safeCreateIcons();
},
_cache: {},
_cacheKey: '',
_getCacheKey() {
return `${this._vault}|${this._path}|${this._depth}|${this._scope}|${this._searchTerm}|${this._tagFilter}`;
},
async _fetchAndRender() {
const statusEl = document.getElementById('graph-status');
if (!statusEl) return;
const cacheKey = this._getCacheKey();
// Use cache if same parameters
if (this._cache[cacheKey]) {
const cached = this._cache[cacheKey];
this._nodes = cached.nodes;
this._edges = cached.edges;
this._scope = cached.scope;
const scopeLabel = this._scope === 'full' ? t('graph.scope_full') : t('graph.scope_directory');
statusEl.textContent = `${t('graph.nodes_count', { count: this._nodes.length })}, ${t('graph.edges_count', { count: this._edges.length })} · ${scopeLabel} · ${t('graph.depth_label', { depth: this._depth })} ${t('graph.cache_label')}`;
this._initLayout();
this._startRender();
return;
}
const params = new URLSearchParams();
if (this._path) params.set('path', this._path);
params.set('depth', String(this._depth));
params.set('scope', this._scope);
if (this._tagFilter) params.set('tag', this._tagFilter);
try {
const data = await api(
`/api/graph/${encodeURIComponent(this._vault)}?${params.toString()}`
);
this._nodes = data.nodes || [];
this._edges = data.edges || [];
this._scope = data.scope || 'directory';
// Cache the result
this._cache[cacheKey] = {
nodes: this._nodes,
edges: this._edges,
scope: this._scope,
};
this._cacheKey = cacheKey;
const scopeLabel = this._scope === 'full' ? t('graph.scope_full') : t('graph.scope_directory');
statusEl.textContent = `${t('graph.nodes_count', { count: this._nodes.length })}, ${t('graph.edges_count', { count: this._edges.length })} · ${scopeLabel} · ${t('graph.depth_label', { depth: this._depth })}`;
this._initLayout();
this._startRender();
} catch (err) {
statusEl.textContent = t('graph.error_loading');
console.error('Graph error:', err);
}
},
reload() {
// Called when depth slider or full-vault button changes
this._resetView();
this._fetchAndRender();
},
setDepth(depth) {
this._depth = depth;
this.reload();
},
toggleScope() {
this._scope = this._scope === 'full' ? 'directory' : 'full';
const btn = document.getElementById('graph-full-vault');
if (btn) btn.textContent = this._scope === 'full' ? t('graph.btn_directory') : t('graph.btn_full');
this.reload();
},
setSearch(term) {
this._searchTerm = term;
// Parse unified search: #tag → tag filter, otherwise → node search
if (term.startsWith('#')) {
const tag = term.substring(1).trim();
if (tag && tag !== this._tagFilter) {
this._tagFilter = tag;
this.reload();
} else if (!tag && this._tagFilter) {
this._tagFilter = '';
this.reload();
}
} else {
if (this._tagFilter) {
this._tagFilter = '';
this.reload();
return;
}
if (term && term.length >= 2) {
this.reload();
} else if (!term && this._searchTerm !== term) {
this.reload();
}
}
},
close() {
const modal = document.getElementById('graph-modal');
if (modal) modal.classList.remove('active');
this._navHistory = [];
this._navIndex = -1;
if (this._animFrame) {
cancelAnimationFrame(this._animFrame);
this._animFrame = null;
}
this._hideTooltip();
},
_resetView() {
this._offsetX = 0;
this._offsetY = 0;
this._zoom = 1;
this._nodePositions = {};
},
_pushHistory(vault, path) {
// Truncate forward history if we navigated back then took a new path
if (this._navIndex < this._navHistory.length - 1) {
this._navHistory = this._navHistory.slice(0, this._navIndex + 1);
}
this._navHistory.push({ vault, path });
this._navIndex = this._navHistory.length - 1;
// Keep max 50 entries
if (this._navHistory.length > 50) {
this._navHistory.shift();
this._navIndex--;
}
this._updateNavButtons();
},
_updateNavButtons() {
const backBtn = document.getElementById('graph-nav-back');
const fwdBtn = document.getElementById('graph-nav-fwd');
const upBtn = document.getElementById('graph-nav-up');
if (backBtn) {
backBtn.disabled = this._navIndex <= 0;
backBtn.style.opacity = this._navIndex > 0 ? '1' : '0.3';
}
if (fwdBtn) {
fwdBtn.disabled = this._navIndex >= this._navHistory.length - 1;
fwdBtn.style.opacity = this._navIndex < this._navHistory.length - 1 ? '1' : '0.3';
}
// Show "↑ Parent" if we have a path (subfolder) or history
if (upBtn) {
const hasParent = !!(this._path && this._path.includes('/'));
const hasHistory = this._navIndex > 0;
upBtn.style.display = (hasParent || hasHistory) ? '' : 'none';
}
},
goBack() {
if (this._navIndex <= 0) return;
this._navIndex--;
const entry = this._navHistory[this._navIndex];
this._navNavigating = true;
this.open(entry.vault, entry.path);
},
goForward() {
if (this._navIndex >= this._navHistory.length - 1) return;
this._navIndex++;
const entry = this._navHistory[this._navIndex];
this._navNavigating = true;
this.open(entry.vault, entry.path);
},
goUp() {
if (!this._path || !this._path.includes('/')) {
// No subfolder — go to previous history entry
this.goBack();
return;
}
// Go to parent directory
const parentPath = this._path.split('/').slice(0, -1).join('/');
this.open(this._vault, parentPath);
},
_initLayout() {
const w = this._canvas.parentElement.clientWidth;
const h = this._canvas.parentElement.clientHeight;
this._canvas.width = w * devicePixelRatio;
this._canvas.height = h * devicePixelRatio;
this._canvas.style.width = w + 'px';
this._canvas.style.height = h + 'px';
this._width = w;
this._height = h;
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
this._ctx.scale(devicePixelRatio, devicePixelRatio);
const cx = w / 2;
const cy = h / 2;
const radius = Math.min(w, h) * 0.35;
this._nodes.forEach((node, i) => {
const angle = (2 * Math.PI * i) / Math.max(this._nodes.length, 1);
this._nodePositions[node.id] = {
x: cx + radius * Math.cos(angle),
y: cy + radius * Math.sin(angle),
vx: 0,
vy: 0,
};
});
},
_startRender() {
const self = this;
let lastTime = 0;
const loop = (time) => {
const dt = Math.min((time - lastTime) / 1000, 0.1);
lastTime = time;
self._simulate(dt);
self._draw();
self._animFrame = requestAnimationFrame(loop);
};
this._animFrame = requestAnimationFrame(loop);
},
_simulate(dt) {
if (this._dragging || this._dragNode) return;
const positions = this._nodePositions;
const cx = this._width / 2;
const cy = this._height / 2;
// Spring forces (edges) — unchanged
for (const edge of this._edges) {
const a = positions[edge.source];
const b = positions[edge.target];
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const targetLen = 80;
const force = (dist - targetLen) * 0.01;
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx += fx;
a.vy += fy;
b.vx -= fx;
b.vy -= fy;
}
// Repulsion: Barnes-Hut for >200 nodes, naive O(n²) otherwise
if (this._nodes.length > 200) {
this._barnesHutRepulsion(positions);
} else {
this._naiveRepulsion(positions);
}
// Center gravity
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
p.vx += (cx - p.x) * 0.001;
p.vy += (cy - p.y) * 0.001;
}
// Apply velocities with damping
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
p.vx *= 0.9;
p.vy *= 0.9;
p.x += p.vx * dt * 60;
p.y += p.vy * dt * 60;
}
},
_naiveRepulsion(positions) {
for (const n1 of this._nodes) {
for (const n2 of this._nodes) {
if (n1.id === n2.id) continue;
const a = positions[n1.id];
const b = positions[n2.id];
if (!a || !b) continue;
const dx = b.x - a.x;
const dy = b.y - a.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const force = 2000 / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
a.vx -= fx;
a.vy -= fy;
}
}
},
// Barnes-Hut quadtree for O(n log n) repulsion
_barnesHutRepulsion(positions) {
// Build quadtree
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
if (p.x < minX) minX = p.x;
if (p.y < minY) minY = p.y;
if (p.x > maxX) maxX = p.x;
if (p.y > maxY) maxY = p.y;
}
const root = this._buildQuadTree(positions, minX, minY, maxX, maxY);
// Apply forces from quadtree
const theta = 0.9; // Barnes-Hut opening angle
for (const node of this._nodes) {
const p = positions[node.id];
if (!p) continue;
this._applyQuadTreeForce(root, p, theta);
}
},
_buildQuadTree(positions, x0, y0, x1, y1) {
const cx = (x0 + x1) / 2;
const cy = (y0 + y1) / 2;
const cell = { x0, y0, x1, y1, cx, cy, mass: 0, mx: 0, my: 0, children: null };
// Collect nodes in this cell
const contained = [];
for (const node of this._nodes) {
const p = positions[node.id];
if (p && p.x >= x0 && p.x < x1 && p.y >= y0 && p.y < y1) {
contained.push(p);
}
}
if (contained.length === 0) return cell;
if (contained.length === 1) {
cell.mass = 1;
cell.mx = contained[0].x;
cell.my = contained[0].y;
return cell;
}
// Subdivide
const midX = cx;
const midY = cy;
cell.children = [
this._buildQuadTree(positions, x0, y0, midX, midY), // NW
this._buildQuadTree(positions, midX, y0, x1, midY), // NE
this._buildQuadTree(positions, x0, midY, midX, y1), // SW
this._buildQuadTree(positions, midX, midY, x1, y1), // SE
];
// Compute center of mass
let totalMass = 0;
let sumX = 0, sumY = 0;
for (const child of cell.children) {
if (child.mass > 0) {
totalMass += child.mass;
sumX += child.mx * child.mass;
sumY += child.my * child.mass;
}
}
cell.mass = totalMass;
cell.mx = sumX / totalMass;
cell.my = sumY / totalMass;
return cell;
},
_applyQuadTreeForce(cell, p, theta) {
if (cell.mass === 0) return;
const dx = cell.mx - p.x;
const dy = cell.my - p.y;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
const size = cell.x1 - cell.x0;
// If cell is far enough or is a leaf, apply force from center of mass
if (!cell.children || size / dist < theta) {
const force = 2000 * cell.mass / (dist * dist);
const fx = (dx / dist) * force;
const fy = (dy / dist) * force;
p.vx -= fx;
p.vy -= fy;
return;
}
// Otherwise recurse into children
for (const child of cell.children) {
this._applyQuadTreeForce(child, p, theta);
}
},
_draw() {
const ctx = this._ctx;
const w = this._width;
const h = this._height;
ctx.save();
ctx.clearRect(0, 0, w, h);
ctx.translate(this._offsetX, this._offsetY);
ctx.scale(this._zoom, this._zoom);
// Draw edges
for (const edge of this._edges) {
const a = this._nodePositions[edge.source];
const b = this._nodePositions[edge.target];
if (!a || !b) continue;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
if (edge.relation === 'backlink') {
ctx.strokeStyle = COLORS.backlink;
ctx.lineWidth = 1.5;
ctx.setLineDash([3, 3]);
} else if (edge.relation === 'wikilink') {
ctx.strokeStyle = COLORS.accent;
ctx.lineWidth = 2;
ctx.setLineDash([4, 4]);
} else {
ctx.strokeStyle = COLORS.muted;
ctx.lineWidth = 1;
ctx.setLineDash([]);
}
ctx.stroke();
}
ctx.setLineDash([]);
// Draw nodes
const searchLower = this._searchTerm.toLowerCase();
for (const node of this._nodes) {
if (!this._isNodeVisible(node)) continue;
const p = this._nodePositions[node.id];
if (!p) continue;
const links = (node.incoming_count || 0) + (node.outgoing_count || 0);
const r = Math.max(5, Math.min(22, 6 + Math.sqrt(Math.max(node.size || 100, links * 200)) / 100));
// Highlight if search match
const isHighlighted = searchLower && (
node.name.toLowerCase().includes(searchLower) ||
(node.tags || []).some(t => t.toLowerCase().includes(searchLower))
);
ctx.beginPath();
ctx.arc(p.x, p.y, r, 0, Math.PI * 2);
switch (node.type) {
case 'directory':
ctx.fillStyle = COLORS.dir;
break;
case 'file':
ctx.fillStyle = (node.path || '').endsWith('.md') ? COLORS.md : COLORS.other;
break;
case 'vault':
ctx.fillStyle = COLORS.vault;
break;
default:
ctx.fillStyle = COLORS.other;
}
if (isHighlighted) {
ctx.shadowColor = COLORS.highlight;
ctx.shadowBlur = 12;
}
ctx.fill();
if (isHighlighted) {
ctx.shadowBlur = 0;
ctx.strokeStyle = COLORS.highlight;
ctx.lineWidth = 2.5;
} else {
ctx.strokeStyle = COLORS.bg;
ctx.lineWidth = 1.5;
}
ctx.stroke();
// Node label
const label = node.name.length > 20 ? node.name.slice(0, 18) + '...' : node.name;
ctx.font = `${isHighlighted ? 12 : 11} / ${this._zoom}px -apple-system, sans-serif`;
ctx.fillStyle = isHighlighted ? COLORS.highlight : COLORS.text;
ctx.textAlign = 'center';
ctx.fillText(label, p.x, p.y + r + 12 / this._zoom);
}
ctx.restore();
},
_getNodeAt(screenX, screenY) {
const x = (screenX - this._offsetX) / this._zoom;
const y = (screenY - this._offsetY) / this._zoom;
for (const node of this._nodes) {
const p = this._nodePositions[node.id];
if (!p) continue;
const links = (node.incoming_count || 0) + (node.outgoing_count || 0);
const r = Math.max(5, Math.min(22, 6 + Math.sqrt(Math.max(node.size || 100, links * 200)) / 100));
const dx = x - p.x;
const dy = y - p.y;
if (dx * dx + dy * dy <= r * r + 100) {
return { node, pos: p };
}
}
return null;
},
_showTooltip(node, screenX, screenY, isPreview) {
if (isPreview) return; // Handled by _showPreviewTooltip
if (!this._infoContent) return;
const cacheKey = `${this._vault}:${node.path}`;
const cached = this._previewCache[cacheKey];
const meta = cached?.meta;
// Build metadata summary
let metaRows = '';
if (meta?.frontmatter && Object.keys(meta.frontmatter).length > 0) {
const fm = meta.frontmatter;
const rows = [];
if (fm.auteur) rows.push(`<div><span style="color:var(--text-muted)">✍️ Auteur</span> <span>${fm.auteur}</span></div>`);
if (fm.date || fm.creation_date) rows.push(`<div><span style="color:var(--text-muted)">📅 Date</span> <span>${fm.date || fm.creation_date}</span></div>`);
if (fm.statut) rows.push(`<div><span style="color:var(--text-muted)">📌 Statut</span> <span>${fm.statut}</span></div>`);
if (fm.catégorie || fm.categorie) rows.push(`<div><span style="color:var(--text-muted)">${t('graph.category')}</span> <span>${fm.catégorie || fm.categorie}</span></div>`);
if (fm.publish) rows.push(`<div><span style="color:var(--text-muted)">✅</span> <span>${t('graph.published')}</span></div>`);
if (fm.favoris) rows.push('<div><span style="color:var(--text-muted)">⭐</span> <span>' + t('graph.favorites') + '</span></div>');
if (rows.length > 0) metaRows = rows.join('');
}
const tags = (meta?.tags || node.tags || []).slice(0, 6);
const inc = node.incoming_count || 0;
const out = node.outgoing_count || 0;
const typeIcon = node.type === 'directory' ? '📁' : '📄';
const typeLabel = node.type === 'directory' ? t('graph.type_directory') : (node.path || '').endsWith('.md') ? t('graph.type_markdown') : t('graph.type_file');
this._infoContent.innerHTML = `
<div style="display:flex;align-items:flex-start;gap:8px;margin-bottom:8px;padding-right:16px">
<span style="font-size:1.2rem">${typeIcon}</span>
<div style="flex:1;min-width:0">
<div style="font-weight:600;font-size:0.85rem;word-break:break-word">${meta?.title || node.name}</div>
<div style="color:var(--text-muted);font-size:0.7rem;word-break:break-all">${node.path}</div>
</div>
</div>
<div style="display:flex;gap:6px;flex-wrap:wrap;margin-bottom:6px">
<span style="font-size:0.6rem;background:var(--bg-secondary);padding:1px 6px;border-radius:3px;color:var(--text-muted)">${typeLabel}</span>
${inc + out > 0 ? `<span style="font-size:0.6rem;background:var(--bg-secondary);padding:1px 6px;border-radius:3px;color:var(--text-muted)">🔗 ${out}→ · ${inc}←</span>` : ''}
</div>
${metaRows ? `<div style="font-size:0.72rem;display:flex;flex-direction:column;gap:2px;padding:6px 0;border-top:1px solid var(--border-color);border-bottom:1px solid var(--border-color);margin-bottom:6px">${metaRows}</div>` : ''}
${tags.length > 0 ? `<div style="display:flex;gap:4px;flex-wrap:wrap">${tags.map(t => `<span style="font-size:0.65rem;background:var(--accent-color);color:#fff;padding:1px 6px;border-radius:3px;opacity:0.8">#${t}</span>`).join('')}</div>` : ''}
${node.type === 'file' ? '<div style="font-size:0.6rem;color:var(--text-muted);margin-top:6px;opacity:0.6">' + t('graph.ctrl_hover_preview') + '</div>' : ''}
`;
this._infoPanel.style.display = 'block';
},
_hideTooltip() {
// Panels are now sticky — only hide via close button
},
_showPreviewTooltip(node, preview, screenX, screenY) {
if (!this._previewContent || this._ctrlHoverNode !== node) return;
const cacheKey = `${this._vault}:${node.path}`;
const cached = this._previewCache[cacheKey];
const meta = cached?.meta;
// Format frontmatter summary
let fmSummary = '';
if (meta?.frontmatter && Object.keys(meta.frontmatter).length > 0) {
const fm = meta.frontmatter;
const items = [];
if (fm.auteur) items.push(`✍️ ${fm.auteur}`);
if (fm.date || fm.creation_date) items.push(`📅 ${fm.date || fm.creation_date}`);
if (fm.statut) items.push(`📌 ${fm.statut}`);
if (fm.publish) items.push(t('graph.published_badge'));
if (fm.favoris) items.push('⭐ favoris');
if (items.length > 0) fmSummary = `<div style="font-size:0.7rem;color:var(--text-muted);margin:4px 0">${items.join(' · ')}</div>`;
}
const tags = (meta?.tags || node.tags || []).slice(0, 8);
this._previewContent.innerHTML = `
<div style="font-weight:700;font-size:0.95rem;margin-bottom:4px;word-break:break-word">${meta?.title || node.name}</div>
<div style="color:var(--text-muted);font-size:0.7rem;margin-bottom:6px;word-break:break-all">${node.path}</div>
${fmSummary}
${tags.length > 0 ? `<div style="display:flex;gap:4px;flex-wrap:wrap;margin-bottom:10px">${tags.map(t => `<span style="font-size:0.65rem;background:var(--accent-color);color:#fff;padding:1px 6px;border-radius:3px;opacity:0.8">#${t}</span>`).join('')}</div>` : ''}
<div style="border-top:1px solid var(--border-color);padding-top:10px;font-size:0.82rem;line-height:1.6;white-space:pre-wrap;word-break:break-word">${preview}</div>
`;
this._previewPanel.style.display = 'block';
},
// --- Advanced controls ---
_isNodeVisible(node) {
const showDir = document.getElementById('graph-filter-dir')?.checked ?? true;
const showMd = document.getElementById('graph-filter-md')?.checked ?? true;
const showOther = document.getElementById('graph-filter-other')?.checked ?? true;
if (node.type === 'directory') return showDir;
if (node.type === 'file' && (node.path || '').endsWith('.md')) return showMd;
if (node.type === 'file') return showOther;
return true;
},
applyTypeFilter() {
// Filters are applied during _draw() — nodes/edges not in visible set are skipped
},
exportPNG() {
const link = document.createElement('a');
link.download = `obsigate-graph-${this._vault}-${Date.now()}.png`;
link.href = this._canvas.toDataURL('image/png');
link.click();
},
toggleFullscreen() {
const container = this._canvas?.parentElement?.parentElement; // editor-container
if (!container) return;
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
container.requestFullscreen();
}
// Re-init layout after fullscreen change
setTimeout(() => this._onResize(), 300);
},
focusNode(nodeId) {
const pos = this._nodePositions[nodeId];
if (!pos) return;
this._offsetX = this._width / 2 - pos.x * this._zoom;
this._offsetY = this._height / 2 - pos.y * this._zoom;
},
_onMouseDown(e) {
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const hit = this._getNodeAt(mx, my);
if (hit) {
this._dragging = true;
this._dragNode = hit;
this._canvas.style.cursor = 'grabbing';
} else {
this._panning = true;
this._panStartX = e.clientX - this._offsetX;
this._panStartY = e.clientY - this._offsetY;
this._canvas.style.cursor = 'grabbing';
}
},
_onMouseMove(e) {
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (this._dragging && this._dragNode) {
this._dragNode.pos.x = (mx - this._offsetX) / this._zoom;
this._dragNode.pos.y = (my - this._offsetY) / this._zoom;
this._dragNode.pos.vx = 0;
this._dragNode.pos.vy = 0;
} else if (this._panning) {
this._offsetX = e.clientX - this._panStartX;
this._offsetY = e.clientY - this._panStartY;
} else {
const hit = this._getNodeAt(mx, my);
if (hit) {
this._canvas.style.cursor = 'pointer';
this._canvas.title = hit.node.type === 'file'
? `📄 ${hit.node.name} (${t('graph.click_to_open')}${e.ctrlKey ? ', ' + t('graph.ctrl_hover_preview_short') : ''})`
: `📁 ${hit.node.name} (cliquer pour explorer)`;
// Ctrl+hover: fetch content preview for file nodes
if (e.ctrlKey && hit.node.type === 'file') {
if (this._ctrlHoverNode !== hit.node) {
this._ctrlHoverNode = hit.node;
this._fetchPreview(hit.node, e.clientX, e.clientY);
}
this._showTooltip(hit.node, e.clientX, e.clientY, true);
} else {
this._ctrlHoverNode = null;
this._showTooltip(hit.node, e.clientX, e.clientY, false);
}
} else {
this._canvas.style.cursor = 'grab';
this._canvas.title = '';
this._ctrlHoverNode = null;
this._hideTooltip();
}
}
},
async _fetchPreview(node, screenX, screenY) {
const cacheKey = `${this._vault}:${node.path}`;
if (this._previewCache[cacheKey] !== undefined) {
const cached = this._previewCache[cacheKey];
this._showPreviewTooltip(node, cached.preview, screenX, screenY);
return;
}
// Show loading indicator
this._showPreviewTooltip(node, '<em>' + t('common.loading') + '</em>', screenX, screenY);
try {
const data = await api(`/api/file/${encodeURIComponent(this._vault)}?path=${encodeURIComponent(node.path)}`);
// Strip HTML tags, keep line breaks, limit to 600 chars
const text = (data.html || '').replace(/<[^>]*>/g, '').replace(/\n+/g, '\n').trim();
const preview = text.substring(0, 600) + (text.length > 600 ? '…' : '');
// Cache both preview text and full metadata for tooltips
this._previewCache[cacheKey] = {
preview,
meta: {
title: data.title,
frontmatter: data.frontmatter || {},
tags: data.tags || [],
path: node.path,
}
};
this._showPreviewTooltip(node, preview, screenX, screenY);
} catch {
this._previewCache[cacheKey] = { preview: '(impossible de charger)', meta: null };
this._showPreviewTooltip(node, '(impossible de charger le contenu)', screenX, screenY);
}
},
_onMouseUp(e) {
if (this._dragging && this._dragNode) {
const node = this._dragNode.node;
this._dragging = false;
this._dragNode = null;
this._canvas.style.cursor = 'grab';
if (node.type === 'file') {
this.close();
openFile(this._vault, node.path);
} else if (node.type === 'directory' || node.type === 'vault') {
this.close();
this._path = node.path || '';
this.open(this._vault, this._path, node.type);
}
}
this._panning = false;
},
_onWheel(e) {
e.preventDefault();
const rect = this._canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
const newZoom = Math.max(0.2, Math.min(3, this._zoom * zoomFactor));
this._offsetX = mx - (mx - this._offsetX) * (newZoom / this._zoom);
this._offsetY = my - (my - this._offsetY) * (newZoom / this._zoom);
this._zoom = newZoom;
},
_onResize() {
if (!this._canvas || !this._nodes.length) return;
const w = this._canvas.parentElement.clientWidth;
const h = this._canvas.parentElement.clientHeight;
this._canvas.width = w * devicePixelRatio;
this._canvas.height = h * devicePixelRatio;
this._canvas.style.width = w + 'px';
this._canvas.style.height = h + 'px';
this._width = w;
this._height = h;
this._ctx.setTransform(1, 0, 0, 1, 0, 0);
this._ctx.scale(devicePixelRatio, devicePixelRatio);
},
};
// ---------------------------------------------------------------------------
// Init graph view event listeners
// ---------------------------------------------------------------------------
export function initGraphView() {
const gm = GraphViewManager;
const closeBtn = document.getElementById('graph-close');
const zoomIn = document.getElementById('graph-zoom-in');
const zoomOut = document.getElementById('graph-zoom-out');
const reset = document.getElementById('graph-reset');
const fullVault = document.getElementById('graph-full-vault');
const depthSlider = document.getElementById('graph-depth');
const searchInput = document.getElementById('graph-search');
const modal = document.getElementById('graph-modal');
const canvas = document.getElementById('graph-canvas');
if (closeBtn) closeBtn.addEventListener('click', () => gm.close());
if (modal) modal.addEventListener('click', (e) => { if (e.target === modal) gm.close(); });
// Navigation buttons
const navBack = document.getElementById('graph-nav-back');
if (navBack) navBack.addEventListener('click', () => gm.goBack());
const navFwd = document.getElementById('graph-nav-fwd');
if (navFwd) navFwd.addEventListener('click', () => gm.goForward());
const navUp = document.getElementById('graph-nav-up');
if (navUp) navUp.addEventListener('click', () => gm.goUp());
if (zoomIn) zoomIn.addEventListener('click', () => { gm._zoom = Math.min(3, gm._zoom * 1.2); });
if (zoomOut) zoomOut.addEventListener('click', () => { gm._zoom = Math.max(0.2, gm._zoom * 0.8); });
if (reset) reset.addEventListener('click', () => { gm._offsetX = 0; gm._offsetY = 0; gm._zoom = 1; });
if (fullVault) fullVault.addEventListener('click', () => gm.toggleScope());
if (depthSlider) depthSlider.addEventListener('input', () => gm.setDepth(parseInt(depthSlider.value)));
if (searchInput) {
let debounce;
searchInput.addEventListener('input', () => {
clearTimeout(debounce);
debounce = setTimeout(() => gm.setSearch(searchInput.value.trim()), 300);
});
}
// Export PNG
const exportBtn = document.getElementById('graph-export');
if (exportBtn) exportBtn.addEventListener('click', () => gm.exportPNG());
// Fullscreen
const fullscreenBtn = document.getElementById('graph-fullscreen');
if (fullscreenBtn) fullscreenBtn.addEventListener('click', () => gm.toggleFullscreen());
// Type filters
['dir', 'md', 'other'].forEach(type => {
const cb = document.getElementById(`graph-filter-${type}`);
if (cb) cb.addEventListener('change', () => gm.applyTypeFilter());
});
if (canvas) {
canvas.addEventListener('mousedown', (e) => gm._onMouseDown(e));
canvas.addEventListener('mousemove', (e) => gm._onMouseMove(e));
canvas.addEventListener('mouseup', (e) => gm._onMouseUp(e));
canvas.addEventListener('mouseleave', () => {
gm._dragging = false;
gm._dragNode = null;
gm._panning = false;
canvas.style.cursor = 'grab';
// Panels stay visible until closed via button
});
canvas.addEventListener('wheel', (e) => gm._onWheel(e), { passive: false });
window.addEventListener('resize', () => gm._onResize());
}
document.addEventListener('keydown', (e) => {
if (modal && modal.classList.contains('active') && e.key === 'Escape') {
gm.close();
}
});
// Close buttons for sticky panels
const infoClose = document.getElementById('graph-info-close');
if (infoClose) infoClose.addEventListener('click', () => { if (gm._infoPanel) gm._infoPanel.style.display = 'none'; });
const previewClose = document.getElementById('graph-preview-close');
if (previewClose) previewClose.addEventListener('click', () => { if (gm._previewPanel) gm._previewPanel.style.display = 'none'; });
}
-193
View File
@@ -1,193 +0,0 @@
/* ObsiGate — i18n (internationalisation).
*
* Architecture:
* Flat key→string JSON locale files in /static/locales/{fr,en,...}.json
* Static DOM: data-i18n="key" → textContent
* data-i18n-placeholder="key" → placeholder
* data-i18n-attr:title="key" → title attribute
* data-i18n-html="key" → innerHTML (use sparingly)
* Dynamic JS: import { t } from './i18n.js'; t('key', {param: 'val'})
* Live reload: setLocale('en') updates every data-i18n element instantly.
*
* Key naming: section.what — lowercase, dot-separated, descriptive.
* e.g. header.search_placeholder button.save error.network
*
* Usage:
* import { t, setLocale, getLocale, initI18n } from './i18n.js';
* await initI18n(); // early, before any rendering
* el.textContent = t('button.cancel'); // dynamic string
* setLocale('en'); // user switches language
*/
// ── State ──────────────────────────────────────────────────────────────────
let _currentLocale = 'fr';
let _strings = {}; // { key: translated_string }
let _fallbackStrings = {}; // FR fallback, loaded once
let _loaded = false;
// ── Public helpers ─────────────────────────────────────────────────────────
/** Return the currently active locale code ('fr', 'en', ...). */
export function getLocale() {
return _currentLocale;
}
/** Translate a key. If params provided, replace {name} placeholders.
* Falls back: key → French → key itself. Never throws. */
export function t(key, params) {
let s = _strings[key];
if (s === undefined) {
s = _fallbackStrings[key];
if (s === undefined) {
console.warn('obsigate i18n: missing key', key);
s = key; // last resort — show the key so the dev notices
}
}
if (params) {
for (const [k, v] of Object.entries(params)) {
s = s.replace(new RegExp('\\{' + k + '\\}', 'g'), String(v));
}
}
return s;
}
// ── Reverse-lookup cache (French text → key) ────────────────────────────
let _reverseCache = null;
function _buildReverseCache() {
_reverseCache = {};
for (const [k, v] of Object.entries(_fallbackStrings)) {
if (k.startsWith('_')) continue;
// Prefer shorter keys when multiple keys map to the same French text
if (!_reverseCache[v] || k.length < _reverseCache[v].length) {
_reverseCache[v] = k;
}
}
}
/** Translate a French string to the current locale by reverse lookup.
* "Paramètres client sauvegardés" → fr: same, en: "Client settings saved".
* If the string is not found in the FR locale, returns it unchanged. */
export function tr(frenchText) {
if (!frenchText || _currentLocale === 'fr') return frenchText;
if (!_reverseCache) _buildReverseCache();
var key = _reverseCache[frenchText];
return key ? t(key) : frenchText;
}
/** Change locale and re-translate the entire DOM.
* Persists to localStorage. */
export async function setLocale(locale) {
locale = locale || 'fr';
if (!_loaded) {
// Too early — store intent and apply after init
try { localStorage.setItem('obsigate-lang', locale); } catch (e) { /* */ }
return;
}
await _loadLocaleStrings(locale);
_applyDOM();
document.documentElement.lang = locale;
try { localStorage.setItem('obsigate-lang', locale); } catch (e) { /* */ }
}
/** Initialise the i18n system. Call once, early, before any rendering.
* Priority: server profile > localStorage > navigator.language > 'fr'. */
export async function initI18n() {
// 1. Determine locale: server profile > localStorage > navigator.language > 'fr'
let locale = null;
// Try server-side language preference (survives clear site data)
try {
const resp = await fetch('/api/auth/me', { credentials: 'include' });
if (resp.ok) {
const user = await resp.json();
if (user.language === 'en' || user.language === 'fr') {
locale = user.language;
}
}
} catch (e) { /* not logged in or network error — fall through */ }
// Fall back to localStorage
if (!locale) {
try { locale = localStorage.getItem('obsigate-lang'); } catch (e) { /* */ }
}
// Fall back to navigator.language
if (!locale) {
const nav = (navigator.language || '').toLowerCase();
locale = nav.startsWith('fr') ? 'fr'
: nav.startsWith('en') ? 'en'
: 'fr';
}
// 2. Load fallback (FR) — always needed
_fallbackStrings = await _fetchLocale('fr');
// 3. Load selected locale
await _loadLocaleStrings(locale);
// 4. Apply to DOM
_applyDOM();
document.documentElement.lang = locale;
_loaded = true;
}
// ── Internal ───────────────────────────────────────────────────────────────
async function _fetchLocale(locale) {
try {
const resp = await fetch('/static/locales/' + locale + '.json');
if (!resp.ok) throw new Error('HTTP ' + resp.status);
return await resp.json();
} catch (err) {
console.warn('obsigate i18n: failed to load locale ' + locale, err);
return {};
}
}
async function _loadLocaleStrings(locale) {
_currentLocale = locale;
if (locale === 'fr') {
_strings = _fallbackStrings;
} else {
_strings = await _fetchLocale(locale);
}
}
// ── Global exports for non-module scripts (legacy app.js) ──────────────────
window.t = t;
window.tr = tr;
window.getLocale = getLocale;
window.setLocale = setLocale;
window.initI18n = initI18n;
/** Walk the DOM and update every element with a data-i18n* attribute. */
function _applyDOM() {
// textContent
document.querySelectorAll('[data-i18n]').forEach(function (el) {
const key = el.getAttribute('data-i18n');
el.textContent = t(key);
});
// placeholder
document.querySelectorAll('[data-i18n-placeholder]').forEach(function (el) {
const key = el.getAttribute('data-i18n-placeholder');
el.setAttribute('placeholder', t(key));
});
// innerHTML (use sparingly — XSS risk if keys ever hold user content)
document.querySelectorAll('[data-i18n-html]').forEach(function (el) {
const key = el.getAttribute('data-i18n-html');
el.innerHTML = t(key);
});
// data-i18n-attr:TITLE → sets any attribute
document.querySelectorAll('[data-i18n-attr]').forEach(function (el) {
const raw = el.getAttribute('data-i18n-attr');
const colon = raw.indexOf(':');
if (colon === -1) return;
const attr = raw.substring(0, colon);
const key = raw.substring(colon + 1);
el.setAttribute(attr, t(key));
});
}
-417
View File
@@ -1,417 +0,0 @@
/* ObsiGate — Legacy module: remaining functions for the orchestrator
Extracted from the monolithic frontend/app.js IIFE.
Functions already in other modules are re-exported. */
import { api } from './auth.js';
import { state } from './state.js';
import { escapeHtml, safeCreateIcons } from './utils.js';
import { TabManager, closeMobileSidebar } from './ui.js';
import { showProgressBar, hideProgressBar, showWelcome } from './viewer.js';
// --- Search imports ---
import {
AutocompleteDropdown,
SearchChips,
SearchHistory,
performAdvancedSearch,
} from './search.js';
// --- Dashboard imports ---
import {
DashboardRecentWidget,
DashboardStatsWidget,
DashboardBookmarkWidget,
DashboardSharedWidget,
} from './dashboard.js';
// --- Re-exports from modules that already have these functions ---
export { initSidebarTabs, initConfigModal, initHelpModal, initRecentTab, initAboutModal } from './config.js';
export { initSyncStatus } from './sync.js';
export { DashboardRecentWidget } from './dashboard.js';
// =========================================================================
// Helpers
// =========================================================================
async function loadVaultSettings() {
try {
const settings = await api("/api/vaults/settings/all");
state.vaultSettings = settings;
} catch (err) {
console.error("Failed to load vault settings:", err);
state.vaultSettings = {};
}
}
// =========================================================================
// Config helpers (needed by initSearch)
// =========================================================================
const _FRONTEND_CONFIG_KEY = "obsigate-perf-config";
function _getFrontendConfig() {
try {
return JSON.parse(localStorage.getItem(_FRONTEND_CONFIG_KEY) || "{}");
} catch {
return {};
}
}
function _getEffective(key, fallback) {
const cfg = _getFrontendConfig();
return cfg[key] !== undefined ? cfg[key] : fallback;
}
/** Check if user is focused on an input/textarea/contenteditable */
function _isInputFocused() {
const tag = document.activeElement?.tagName;
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
return document.activeElement?.isContentEditable === true;
}
// =========================================================================
// initSearch
// =========================================================================
function initSearch() {
const input = document.getElementById("search-input");
if (!input) return;
const caseBtn = document.getElementById("rb-case");
const wordBtn = document.getElementById("rb-word");
const regexBtn = document.getElementById("rb-regex");
const filterBtn = document.getElementById("search-filter-btn");
const clearBtn = document.getElementById("search-clear-btn");
const filterRow = document.getElementById("search-filter-row");
const prevBtn = document.getElementById("rb-prev");
const nextBtn = document.getElementById("rb-next");
const countEl = document.getElementById("rb-count");
const resultsBar = document.getElementById("results-bar");
function _showResultsBar() { if (resultsBar) resultsBar.style.display = 'flex'; }
function _hideResultsBar() { if (resultsBar) resultsBar.style.display = 'none'; }
function _updateToggleUI() {
if (caseBtn) caseBtn.classList.toggle("active", state.searchCaseSensitive);
if (wordBtn) wordBtn.classList.toggle("active", state.searchWholeWord);
if (regexBtn) regexBtn.classList.toggle("active", state.searchRegex);
if (filterBtn) filterBtn.classList.toggle("active", state.searchFilterVisible);
}
// Toggle buttons
if (caseBtn) caseBtn.addEventListener("click", () => { state.searchCaseSensitive = !state.searchCaseSensitive; _updateToggleUI(); _research(); });
if (wordBtn) wordBtn.addEventListener("click", () => { state.searchWholeWord = !state.searchWholeWord; _updateToggleUI(); _research(); });
if (regexBtn) regexBtn.addEventListener("click", () => { state.searchRegex = !state.searchRegex; _updateToggleUI(); _research(); });
if (filterBtn) filterBtn.addEventListener("click", () => { state.searchFilterVisible = !state.searchFilterVisible; if (filterRow) filterRow.style.display = state.searchFilterVisible ? "flex" : "none"; _updateToggleUI(); });
// Result navigation (up/down arrows + Enter)
let _searchResultIdx = -1;
let _searchResultItems = [];
function _updateResultHighlight() {
_searchResultItems.forEach((el, i) => {
el.classList.toggle("search-result-active", i === _searchResultIdx);
});
if (_searchResultIdx >= 0 && _searchResultIdx < _searchResultItems.length) {
_searchResultItems[_searchResultIdx].scrollIntoView({ block: "nearest", behavior: "smooth" });
}
const countEl = document.getElementById("rb-count");
if (countEl) countEl.textContent = _searchResultIdx >= 0 ? `${_searchResultIdx + 1}/${_searchResultItems.length}` : `0/${_searchResultItems.length}`;
}
function _refreshResultItems() {
_searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
_searchResultIdx = _searchResultItems.length > 0 ? 0 : -1;
_updateResultHighlight();
if (_searchResultItems.length > 0) _showResultsBar(); else _hideResultsBar();
}
window.navigateSearchResults = function(delta) {
_searchResultItems = Array.from(document.querySelectorAll(".search-result-item"));
if (_searchResultItems.length === 0) return;
_searchResultIdx = Math.max(0, Math.min(_searchResultItems.length - 1, _searchResultIdx + delta));
_updateResultHighlight();
};
if (prevBtn) prevBtn.addEventListener("click", () => navigateSearchResults(-1));
if (nextBtn) nextBtn.addEventListener("click", () => navigateSearchResults(1));
function _research() {
const q = input.value.trim();
if (q.length >= _getEffective("min_query_length", 2)) {
clearTimeout(state.searchTimeout);
state.searchTimeout = setTimeout(() => {
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
state.advancedSearchOffset = 0;
performAdvancedSearch(q, vault, tagFilter);
}, _getEffective("debounce_ms", 300));
}
}
// Keyboard shortcuts
document.addEventListener("keydown", (e) => {
if (e.altKey && !e.ctrlKey && !e.metaKey) {
if (e.key === "c" || e.key === "C") { e.preventDefault(); caseBtn.click(); }
else if (e.key === "w" || e.key === "W") { e.preventDefault(); if (wordBtn) wordBtn.click(); }
else if (e.key === "r" || e.key === "R") { e.preventDefault(); if (regexBtn) regexBtn.click(); }
else if (e.key === "f" || e.key === "F") { e.preventDefault(); if (filterBtn) filterBtn.click(); input.focus(); }
}
});
// Initialize sub-controllers
AutocompleteDropdown.init();
SearchChips.init();
// Initially hide clear button
if (clearBtn) clearBtn.style.display = "none";
// Input handler: autocomplete dropdown only (search triggered on Enter or selection)
input.addEventListener("input", () => {
const hasText = input.value.length > 0;
clearBtn.style.display = hasText ? "flex" : "none";
// Show autocomplete dropdown while typing
AutocompleteDropdown.populate(input.value, input.selectionStart);
// Clear results when input becomes empty
if (!hasText) {
SearchChips.clear();
showWelcome();
}
});
// Focus handler: show history dropdown
input.addEventListener("focus", () => {
if (input.value.length === 0) {
const historyItems = SearchHistory.filter("").slice(0, 5);
if (historyItems.length > 0) {
AutocompleteDropdown.populate("", 0);
}
}
});
// Keyboard navigation in dropdown
input.addEventListener("keydown", (e) => {
if (AutocompleteDropdown.isVisible()) {
if (e.key === "ArrowDown") {
e.preventDefault();
AutocompleteDropdown.navigateDown();
} else if (e.key === "ArrowUp") {
e.preventDefault();
AutocompleteDropdown.navigateUp();
} else if (e.key === "Enter") {
// First: check dropdown suggestions (higher priority than search results)
if (AutocompleteDropdown.selectActive()) {
e.preventDefault();
return;
}
// Second: navigate search results if visible
const results = document.querySelectorAll(".search-result-item");
if (results.length > 0 && _searchResultIdx >= 0) {
const el = results[_searchResultIdx];
if (el) {
const vault = el.dataset.vault;
const path = el.dataset.path;
if (vault && path) { TabManager.openPreview(vault, path); e.preventDefault(); return; }
}
}
// Third: execute search
AutocompleteDropdown.hide();
const q = input.value.trim();
if (q) {
SearchHistory.add(q);
clearTimeout(state.searchTimeout);
state.advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
performAdvancedSearch(q, vault, tagFilter);
}
e.preventDefault();
} else if (e.key === "Escape") {
AutocompleteDropdown.hide();
e.stopPropagation();
}
} else {
// Dropdown not visible: navigate search results or execute search
if (e.key === "ArrowDown") {
if (window.navigateSearchResults) { window.navigateSearchResults(1); e.preventDefault(); }
} else if (e.key === "ArrowUp") {
if (window.navigateSearchResults) { window.navigateSearchResults(-1); e.preventDefault(); }
} else if (e.key === "Enter") {
const q = input.value.trim();
if (q) {
SearchHistory.add(q);
clearTimeout(state.searchTimeout);
state.advancedSearchOffset = 0;
const vault = document.getElementById("vault-filter").value;
const tagFilter = state.selectedTags.length > 0 ? state.selectedTags.join(",") : null;
performAdvancedSearch(q, vault, tagFilter);
}
e.preventDefault();
}
}
});
clearBtn.addEventListener("click", () => {
input.value = "";
if (clearBtn) clearBtn.style.display = "none";
state.searchCaseSensitive = false;
state.searchWholeWord = false;
state.searchRegex = false;
_updateToggleUI();
_hideResultsBar();
SearchChips.clear();
AutocompleteDropdown.hide();
showWelcome();
});
// Global keyboard shortcuts
document.addEventListener("keydown", (e) => {
// Ctrl+K or Cmd+K: focus search
if ((e.ctrlKey || e.metaKey) && e.key === "k") {
e.preventDefault();
input.focus();
input.select();
}
// "/" key: focus search (when not in an input/textarea)
if (e.key === "/" && !_isInputFocused()) {
e.preventDefault();
input.focus();
}
// Escape: blur search input and close dropdown
if (e.key === "Escape" && document.activeElement === input) {
AutocompleteDropdown.hide();
input.blur();
}
});
}
function goHome() {
const searchInput = document.getElementById("search-input");
if (searchInput) searchInput.value = "";
document.querySelectorAll(".tree-item.active").forEach((el) => el.classList.remove("active"));
state.currentVault = null;
state.currentPath = null;
state.showingSource = false;
state.cachedRawSource = null;
closeMobileSidebar();
showWelcome();
}
// =========================================================================
// loadSavedSearches (needed by showWelcome)
// =========================================================================
async function loadSavedSearches() {
const list = document.getElementById("saved-searches-list");
const empty = document.getElementById("saved-searches-empty");
if (!list) return;
try {
const searches = await api("/api/saved-searches");
if (!searches.length) {
list.innerHTML = "";
if (empty) empty.style.display = "";
return;
}
if (empty) empty.style.display = "none";
list.innerHTML = searches.map(s => {
var isDirSearch = (!s.query || s.query.trim() === '') && s.include_paths;
var displayQuery = isDirSearch ? (s.include_paths || 'Dossier') : escapeHtml(s.query);
var typeIcon = isDirSearch
? '<i data-lucide="folder-search" style="width:14px;height:14px;color:var(--accent)" class="saved-search-type-icon"></i>'
: '<i data-lucide="search" style="width:14px;height:14px;color:var(--text-muted)" class="saved-search-type-icon"></i>';
const badges = [];
if (s.case_sensitive) badges.push('<span class="search-filter-badge">Aa</span>');
if (s.whole_word) badges.push('<span class="search-filter-badge">wd</span>');
if (s.regex) badges.push('<span class="search-filter-badge">.*</span>');
const pathFilters = [];
if (s.include_paths) pathFilters.push(`<span class="saved-search-path" title="Inclure: ${escapeHtml(s.include_paths)}">\u{1F4E5} ${escapeHtml(s.include_paths)}</span>`);
if (s.exclude_paths) pathFilters.push(`<span class="saved-search-path" title="Exclure: ${escapeHtml(s.exclude_paths)}">\u{1F4E4} ${escapeHtml(s.exclude_paths)}</span>`);
const vaultStr = s.vault && s.vault !== "all" ? `<span class="saved-search-vault">\u{1F4C1} ${escapeHtml(s.vault)}</span>` : "";
return `
<div class="saved-search-item${isDirSearch ? ' saved-search-dir' : ''}" data-type="${isDirSearch ? 'directory' : 'search'}">
<div class="saved-search-query">${displayQuery}</div>
<div class="saved-search-meta">
<span class="saved-search-badges">${badges.join("")} ${vaultStr}</span>
${typeIcon}
</div>
${pathFilters.length ? '<div class="saved-search-filters">' + pathFilters.join(" ") + '</div>' : ""}
<button class="saved-search-delete" data-id="${s.id}" title="Supprimer">✕</button>
</div>
`}).join("");
// Apply saved filter + init filter bar
if (typeof _applySavedFilter === 'function') _applySavedFilter();
if (typeof _initSavedFilter === 'function') _initSavedFilter();
list.querySelectorAll(".saved-search-item").forEach(item => {
item.addEventListener("click", (e) => {
if (e.target.classList.contains("saved-search-delete")) return;
const idx = Array.from(list.children).indexOf(item);
const s = searches[idx];
if (!s) return;
// Apply the saved search
const input = document.getElementById("search-input");
if (input) input.value = s.query;
state.searchCaseSensitive = s.case_sensitive || false;
state.searchWholeWord = s.whole_word || false;
state.searchRegex = s.regex || false;
if (typeof _updateToggleUI === "function") _updateToggleUI();
if (s.include_paths) {
const incl = document.getElementById("search-include-input");
if (incl) incl.value = s.include_paths;
}
if (s.exclude_paths) {
const excl = document.getElementById("search-exclude-input");
if (excl) excl.value = s.exclude_paths;
}
// Execute the search
AutocompleteDropdown.hide();
AutocompleteDropdown._suppressNext = true;
const vault = s.vault || "all";
if (input) { input.dispatchEvent(new Event("input")); }
clearTimeout(state.searchTimeout);
state.advancedSearchOffset = 0;
performAdvancedSearch(s.query, vault, null);
});
});
list.querySelectorAll(".saved-search-delete").forEach(b => b.addEventListener("click", async (e) => {
e.stopPropagation();
await api(`/api/saved-searches/${b.dataset.id}`, { method: "DELETE" });
loadSavedSearches();
}));
safeCreateIcons();
} catch (err) { /* silently ignore */ }
}
// =========================================================================
// initDashboardTabs (needed by showWelcome)
// =========================================================================
function initDashboardTabs() {
document.querySelectorAll(".dashboard-tab").forEach(tab => {
// Remove existing listeners by cloning
const newTab = tab.cloneNode(true);
tab.parentNode.replaceChild(newTab, tab);
newTab.addEventListener("click", function() {
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
this.classList.add("active");
const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
if (panel) panel.classList.add("active");
});
});
}
// =========================================================================
// Exports
// =========================================================================
export {
loadVaultSettings,
initSearch,
showWelcome,
goHome,
};
@@ -1,845 +0,0 @@
/**
* Mermaid.js integration for ObsiGate.
*
* Handles initialization, rendering, templates, zoom/pan,
* fullscreen, focus mode, theme sync, and Obsidian compatibility.
*
* Usage: import { initMermaid, renderMermaidBlocks } from './mermaid-viewer.js';
*/
import { safeCreateIcons } from './utils.js';
// ---------------------------------------------------------------------------
// Configuration (#20)
// ---------------------------------------------------------------------------
var _mermaidReady = false;
var _currentTheme = 'default';
export function initMermaid() {
if (typeof mermaid === 'undefined') {
console.warn('[Mermaid] library not loaded');
return;
}
mermaid.initialize(_getMermaidConfig(_currentTheme === 'dark'));
_mermaidReady = true;
}
export function updateMermaidTheme(isDark) {
_currentTheme = isDark ? 'dark' : 'default';
if (_mermaidReady) {
mermaid.initialize(_getMermaidConfig(isDark));
}
// Re-render all existing Mermaid containers with the new theme
var containers = document.querySelectorAll('.mermaid-container');
for (var i = 0; i < containers.length; i++) {
reRenderContainer(containers[i]);
}
}
// Unified theme config — used by both init and theme toggle
function _getMermaidConfig(isDark) {
return {
startOnLoad: false,
securityLevel: 'strict',
theme: 'base',
fontFamily: 'inherit',
themeVariables: isDark ? DARK_THEME : LIGHT_THEME,
};
}
// Dark theme — optimized for readability on dark backgrounds
var DARK_THEME = {
// Flowchart / general shapes
primaryColor: '#1e3a5f',
primaryTextColor: '#c8daf0',
primaryBorderColor: '#4a7ab5',
lineColor: '#5a7a9a',
secondaryColor: '#2a4a6a',
tertiaryColor: '#3a6090',
// Background & containers
background: '#0f1117',
mainBkg: '#1a2030',
nodeBorder: '#4a7ab5',
clusterBkg: '#1a2235',
clusterBorder: '#3a5a80',
titleColor: '#a0c0e0',
edgeLabelBackground: '#1a2d47',
nodeTextColor: '#c8daf0',
// Sequence diagram
actorBkg: '#1e3a5f',
actorBorder: '#4a7ab5',
actorTextColor: '#c8daf0',
actorLineColor: '#4a7ab5',
signalColor: '#c8daf0',
signalTextColor: '#c8daf0',
labelBoxBkgColor: '#1e3a5f',
labelBoxBorderColor: '#4a7ab5',
labelTextColor: '#c8daf0',
loopTextColor: '#c8daf0',
noteBkgColor: '#2a3550',
noteBorderColor: '#4a7ab5',
noteTextColor: '#c8daf0',
activationBkgColor: '#3a5a80',
// Gantt
sectionBkgColor: '#1a2d47',
altSectionBkgColor: '#1e3350',
sectionBkgColor2: '#1a2d47',
taskBkgColor: '#1e3a5f',
taskBorderColor: '#4a7ab5',
taskTextColor: '#c8daf0',
taskTextOutsideColor: '#8aacca',
taskTextLightColor: '#e0e8f0',
gridColor: '#2a3a55',
activeTaskBkgColor: '#2a5a8a',
activeTaskBorderColor: '#6a9ad0',
doneTaskBkgColor: '#1a3a4f',
doneTaskBorderColor: '#3a7a9a',
critBkgColor: '#5a2a3a',
critBorderColor: '#a04050',
todayLineColor: '#4a7ab5',
// Git graph
git0: '#4a7ab5',
git1: '#3a6a9a',
git2: '#5a8ac0',
git3: '#2a5a8a',
git4: '#6a9ad0',
git5: '#3a7aaa',
git6: '#4a8abf',
git7: '#5a9acf',
gitInv0: '#c8daf0',
gitInv1: '#b0c8e0',
gitInv2: '#d0e0f0',
gitInv3: '#a0b8d0',
gitBranchLabel0: '#c8daf0',
gitBranchLabel1: '#b0c8e0',
gitBranchLabel2: '#d0e0f0',
commitLabelColor: '#c8daf0',
commitLabelBackground: '#1e3a5f',
commitLabelFontSize: '11px',
tagLabelColor: '#a0c0e0',
tagLabelBackground: '#1a2d47',
tagLabelBorder: '#4a7ab5',
// Pie chart
pie1: '#1e3a5f',
pie2: '#2a5a8a',
pie3: '#153050',
pie4: '#3a6a9a',
pie5: '#204070',
pie6: '#4a7ab5',
pie7: '#255080',
pie8: '#5a8ac0',
pieTitleTextColor: '#c8daf0',
pieSectionTextColor: '#c8daf0',
pieLegendTextColor: '#a0c0e0',
pieStrokeColor: '#0f1117',
// Class diagram
classText: '#c8daf0',
// State diagram
labelColor: '#c8daf0',
// ER diagram — dark alternating rows for readability
fillType0: '#111d30',
fillType1: '#152238',
fillType2: '#111d30',
fillType3: '#152238',
fillType4: '#111d30',
fillType5: '#152238',
fillType6: '#111d30',
fillType7: '#152238',
entityBorder: '#4a7ab5',
entityBackground: '#0d1520',
attributeEvenColor: '#ffffff',
attributeOddColor: '#c8daf0',
// Timeline — section colors
cScale0: '#1e3a5f',
cScale1: '#2a5a8a',
cScale2: '#153050',
cScale3: '#3a6a9a',
cScale4: '#204070',
cScale5: '#4a7ab5',
cScaleLabel0: '#c8daf0',
cScaleLabel1: '#c8daf0',
cScaleLabel2: '#c8daf0',
cScaleLabel3: '#c8daf0',
cScaleLabel4: '#c8daf0',
cScaleLabel5: '#c8daf0',
// XY Chart — bar and line colors
xyChartBackgroundColor: '#0f1117',
xyChartTitleColor: '#c8daf0',
xyChartXaxisTitleColor: '#8aacca',
xyChartYaxisTitleColor: '#8aacca',
plotColor1: '#1e3a5f',
plotColor2: '#4a7ab5',
plotColor3: '#2a5a8a',
plotColor4: '#5a8ac0',
plotColor5: '#3a6a9a',
plotColor6: '#6a9ad0',
plotColor7: '#153050',
plotColor8: '#204070',
// General text
textColor: '#c8daf0',
fontSize: '14px',
};
// Light theme — clean, minimal
var LIGHT_THEME = {
primaryColor: '#f0f0f5',
primaryTextColor: '#1a1a2e',
primaryBorderColor: '#ccc',
lineColor: '#666',
secondaryColor: '#e8e8f0',
tertiaryColor: '#fafafa',
background: '#ffffff',
mainBkg: '#f5f5fa',
nodeBorder: '#ccc',
clusterBkg: '#f0f0f5',
clusterBorder: '#ddd',
titleColor: '#333',
edgeLabelBackground: '#f5f5fa',
nodeTextColor: '#1a1a2e',
fontSize: '14px',
};
// ---------------------------------------------------------------------------
// Templates for all diagram types (#22)
// ---------------------------------------------------------------------------
export var MERMAID_TEMPLATES = {
flowchart: 'flowchart TD\n A[Start] --> B{Decision?}\n B -->|Yes| C[OK]\n B -->|No| D[Cancel]',
sequenceDiagram: 'sequenceDiagram\n Alice->>Bob: Hello\n Bob-->>Alice: Hi!',
classDiagram: 'classDiagram\n class Animal {\n +name: string\n +age: int\n +makeSound()\n }\n class Dog {\n +bark()\n }\n Animal <|-- Dog',
'stateDiagram-v2': 'stateDiagram-v2\n [*] --> Idle\n Idle --> Running : start\n Running --> Idle : stop\n Running --> [*] : done',
erDiagram: 'erDiagram\n CUSTOMER ||--o{ ORDER : places\n ORDER ||--|{ LINE-ITEM : contains',
journey: 'journey\n title My day\n section Morning\n Wake up: 5: Me\n Breakfast: 3: Me\n section Work\n Code: 5: Me, Cat',
gantt: 'gantt\n title Project Plan\n dateFormat YYYY-MM-DD\n section Design\n Mockups :a1, 2024-01-01, 7d\n section Dev\n Backend :a2, after a1, 14d',
pie: 'pie title Languages\n "Python" : 45\n "JavaScript" : 30\n "Go" : 15\n "Rust" : 10',
gitGraph: 'gitGraph\n commit\n branch feature\n checkout feature\n commit\n checkout main\n merge feature',
requirementDiagram: 'requirementDiagram\n requirement req1 {\n id: 1\n text: System shall do X\n }\n element Component {\n type: package\n }\n Component - satisfies -> req1',
mindmap: 'mindmap\n root((Project))\n Frontend\n HTML\n CSS\n JavaScript\n Backend\n API\n Database',
timeline: 'timeline\n title History\n 2020 : Started\n 2022 : v1.0\n 2024 : v2.0',
quadrantChart: 'quadrantChart\n title Products\n x-axis Low Reach --> High Reach\n y-axis Low Value --> High Value\n quadrant-1 Stars\n quadrant-2 Question Marks\n Product A: [0.8, 0.6]\n Product B: [0.3, 0.8]',
C4Context: 'C4Context\n title System Context\n Person(user, "User")\n System(app, "My App")\n System_Ext(db, "Database")\n Rel(user, app, "Uses")\n Rel(app, db, "Reads/Writes")',
C4Container: 'C4Container\n title Containers\n Container(web, "Web App", "React")\n Container(api, "API", "FastAPI")\n ContainerDb(db, "DB", "PostgreSQL")\n Rel(web, api, "REST")\n Rel(api, db, "SQL")',
C4Component: 'C4Component\n title API Components\n Container_Boundary(api, "API") {\n Component(auth, "Auth Service")\n Component(search, "Search Engine")\n }',
C4Dynamic: 'C4Dynamic\n title Login Flow\n Person(user, "User")\n Container(web, "Web App")\n Container(api, "API")\n Rel(user, web, "POST /login")\n Rel(web, api, "authenticate")',
C4Deployment: 'C4Deployment\n title Deployment\n Deployment_Node(cloud, "Cloud") {\n Container(web, "Web")\n Container(api, "API")\n }',
'xychart-beta': 'xychart-beta\n title "Sales"\n x-axis [Jan, Feb, Mar, Apr]\n y-axis "Revenue" 0 --> 100\n bar [45, 60, 75, 90]\n line [30, 45, 55, 70]',
'block-beta': 'block-beta\n columns 3\n A B C\n D E F',
'sankey-beta': 'sankey-beta\n A, B, 50\n A, C, 30\n B, D, 20\n C, D, 30',
};
// ---------------------------------------------------------------------------
// Rendering engine (#21)
// ---------------------------------------------------------------------------
var _renderCounter = 0;
export async function renderMermaidBlocks(container) {
if (!_mermaidReady || typeof mermaid === 'undefined') return;
var blocks = container.querySelectorAll('pre code.language-mermaid, .mermaid-code');
if (blocks.length === 0) return;
for (var i = 0; i < blocks.length; i++) {
var codeBlock = blocks[i];
var originalCode = codeBlock.textContent || codeBlock.innerText || '';
if (!originalCode.trim()) continue;
var wrapper = codeBlock.closest('pre') || codeBlock.parentElement;
if (!wrapper) continue;
// Check if already rendered
if (wrapper.querySelector('.mermaid-rendered')) continue;
try {
// Preprocess Obsidian syntax before Mermaid parsing (#24)
var code = preprocessObsidian(originalCode);
await mermaid.parse(code);
var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now();
var result = await mermaid.render(id, code);
// Extract diagram type from first non-comment, non-empty line
var diagramType = extractDiagramType(code);
// Build container with header + body
var containerDiv = document.createElement('div');
containerDiv.className = 'mermaid-container';
containerDiv.setAttribute('data-mermaid-code', originalCode);
// Header bar
var header = document.createElement('div');
header.className = 'mermaid-header';
header.innerHTML =
'<span class="mermaid-diagram-type">' + escapeHtml(diagramType) + '</span>' +
'<div class="mermaid-header-actions">' +
'<button class="mermaid-btn mermaid-zoom-in" title="Zoom +"><i data-lucide="zoom-in" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-zoom-out" title="Zoom -"><i data-lucide="zoom-out" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fullscreen" title="Plein ecran"><i data-lucide="expand" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-copy-svg" title="Copier SVG"><i data-lucide="copy" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-copy-code" title="Copier le code Mermaid"><i data-lucide="file-code-2" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-download" title="Telecharger PNG"><i data-lucide="download" style="width:14px;height:14px"></i></button>' +
'<div class="mermaid-toggle">' +
'<button class="mermaid-toggle-btn active" data-mode="preview">Preview</button>' +
'<button class="mermaid-toggle-btn" data-mode="code">Code</button>' +
'</div>' +
'</div>';
// Body
var body = document.createElement('div');
body.className = 'mermaid-body';
body.innerHTML =
'<div class="mermaid-rendered">' + result.svg + '</div>' +
'<div class="mermaid-source" style="display:none"><pre><code class="language-mermaid">' + escapeHtml(originalCode) + '</code></pre></div>';
containerDiv.appendChild(header);
containerDiv.appendChild(body);
wrapper.parentNode.replaceChild(containerDiv, wrapper);
// Wire up header buttons
wireHeaderButtons(containerDiv, originalCode);
setupZoomPan(containerDiv);
setupCodePreviewToggle(containerDiv);
// Patch beta diagram colors for dark mode
if (_currentTheme === 'dark') patchBetaColors(containerDiv, diagramType);
safeCreateIcons();
} catch (err) {
var errorDiv = document.createElement('div');
errorDiv.className = 'mermaid-error';
errorDiv.innerHTML = '<strong>Erreur Mermaid</strong><br>' + escapeHtml(err.message || String(err));
wrapper.parentNode.replaceChild(errorDiv, wrapper);
}
}
}
// ---------------------------------------------------------------------------
// Header button wiring
// ---------------------------------------------------------------------------
function wireHeaderButtons(container, originalCode) {
// Copy SVG
var copySvgBtn = container.querySelector('.mermaid-copy-svg');
if (copySvgBtn) {
copySvgBtn.addEventListener('click', function() {
copySvgToClipboard(container);
});
}
// Copy code
var copyCodeBtn = container.querySelector('.mermaid-copy-code');
if (copyCodeBtn) {
copyCodeBtn.addEventListener('click', function() {
copyToClipboard(originalCode);
});
}
// Download PNG
var downloadBtn = container.querySelector('.mermaid-download');
if (downloadBtn) {
downloadBtn.addEventListener('click', function() {
exportMermaidPNG(container);
});
}
}
// ---------------------------------------------------------------------------
// Code / Preview toggle
// ---------------------------------------------------------------------------
function setupCodePreviewToggle(container) {
var toggleBtns = container.querySelectorAll('.mermaid-toggle-btn');
var rendered = container.querySelector('.mermaid-rendered');
var source = container.querySelector('.mermaid-source');
var zoomBtns = container.querySelectorAll('.mermaid-zoom-in, .mermaid-zoom-out');
var fullscreenBtn = container.querySelector('.mermaid-fullscreen');
for (var i = 0; i < toggleBtns.length; i++) {
toggleBtns[i].addEventListener('click', function() {
var mode = this.getAttribute('data-mode');
// Update active state
for (var j = 0; j < toggleBtns.length; j++) {
toggleBtns[j].classList.remove('active');
}
this.classList.add('active');
if (mode === 'preview') {
rendered.style.display = '';
source.style.display = 'none';
// Show zoom/fullscreen buttons in preview mode
for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = '';
if (fullscreenBtn) fullscreenBtn.style.display = '';
} else {
rendered.style.display = 'none';
source.style.display = '';
// Hide zoom/fullscreen in code mode
for (var k = 0; k < zoomBtns.length; k++) zoomBtns[k].style.display = 'none';
if (fullscreenBtn) fullscreenBtn.style.display = 'none';
}
});
}
}
// ---------------------------------------------------------------------------
// Diagram type extraction
// ---------------------------------------------------------------------------
function extractDiagramType(code) {
var lines = code.trim().split('\n');
for (var i = 0; i < lines.length; i++) {
var line = lines[i].trim();
// Skip comments and empty lines
if (!line || line.indexOf('%%') === 0) continue;
// Return the first meaningful word(s)
var words = line.split(/\s+/);
if (words.length >= 2) {
return words[0] + ' ' + words[1];
}
return words[0] || 'mermaid';
}
return 'mermaid';
}
// ---------------------------------------------------------------------------
// Re-render a container after theme change
// ---------------------------------------------------------------------------
async function reRenderContainer(container) {
var code = container.getAttribute('data-mermaid-code');
if (!code) return;
var rendered = container.querySelector('.mermaid-rendered');
if (!rendered) return;
try {
var id = 'mermaid-' + (++_renderCounter) + '-' + Date.now();
var result = await mermaid.render(id, preprocessObsidian(code));
rendered.innerHTML = result.svg;
// Reset zoom/pan state on the SVG
var svg = rendered.querySelector('svg');
if (svg) {
svg.style.transform = '';
svg.style.cursor = 'grab';
}
// Patch beta diagram colors for dark mode
if (_currentTheme === 'dark') {
var diagramType = extractDiagramType(code);
patchBetaColors(container, diagramType);
}
} catch (err) {
// Keep old rendering on error
console.warn('[Mermaid] Re-render failed:', err.message);
}
}
// ---------------------------------------------------------------------------
// Patch beta diagram colors for dark mode (inline SVG attributes beat CSS)
// ---------------------------------------------------------------------------
function patchBetaColors(container, diagramType) {
var svg = container.querySelector('.mermaid-rendered svg');
if (!svg) return;
// packet-beta — override white block backgrounds
if (diagramType.indexOf('packet') !== -1) {
var rects = svg.querySelectorAll('rect');
for (var i = 0; i < rects.length; i++) {
var r = rects[i];
var fill = r.getAttribute('fill');
// Only override if the fill is light/white (preserve dark fills)
if (!fill || fill === '#f4f4f4' || fill === '#fff' || fill === '#ffffff' || fill === 'white' || fill === '#e8e8e8') {
r.setAttribute('fill', '#1e3a5f');
r.setAttribute('stroke', '#4a7ab5');
}
}
// Text color
var texts = svg.querySelectorAll('text');
for (var j = 0; j < texts.length; j++) {
var t = texts[j];
var tfill = t.getAttribute('fill');
if (!tfill || tfill === '#000' || tfill === '#333' || tfill === 'black') {
t.setAttribute('fill', '#c8daf0');
}
}
}
// xychart-beta — override cream bars and orange line
if (diagramType.indexOf('xychart') !== -1) {
var allRects = svg.querySelectorAll('rect');
for (var k = 0; k < allRects.length; k++) {
var rr = allRects[k];
var f = rr.getAttribute('fill');
if (!f || f === '#f4f4f4' || f === '#fff' || f === '#ffffff' || f === 'white' || f.indexOf('cream') !== -1 || f === '#ececec') {
rr.setAttribute('fill', '#1e3a5f');
rr.setAttribute('stroke', '#4a7ab5');
}
}
// Line color
var paths = svg.querySelectorAll('path');
for (var m = 0; m < paths.length; m++) {
var p = paths[m];
var ps = p.getAttribute('stroke');
if (!ps || ps === '#ffa500' || ps === 'orange' || ps === '#ff8c00') {
p.setAttribute('stroke', '#6a9ad0');
}
}
}
}
// ---------------------------------------------------------------------------
// Zoom & Pan (#27)
// ---------------------------------------------------------------------------
function setupZoomPan(container) {
var svgWrapper = container.querySelector('.mermaid-rendered');
if (!svgWrapper) return;
var svg = svgWrapper.querySelector('svg');
if (!svg) return;
var scale = 1;
var panX = 0, panY = 0;
var dragging = false, startX, startY;
svg.style.transformOrigin = '0 0';
svg.style.cursor = 'grab';
function update() {
svg.style.transform = 'translate(' + panX + 'px, ' + panY + 'px) scale(' + scale + ')';
}
svg.addEventListener('mousedown', function(e) {
dragging = true;
startX = e.clientX - panX;
startY = e.clientY - panY;
svg.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!dragging) return;
panX = e.clientX - startX;
panY = e.clientY - startY;
update();
});
document.addEventListener('mouseup', function() {
dragging = false;
if (svg) svg.style.cursor = 'grab';
});
svg.addEventListener('wheel', function(e) {
e.preventDefault();
var delta = e.deltaY > 0 ? 0.9 : 1.1;
scale = Math.max(0.2, Math.min(5, scale * delta));
update();
}, { passive: false });
var zoomIn = container.querySelector('.mermaid-zoom-in');
var zoomOut = container.querySelector('.mermaid-zoom-out');
if (zoomIn) zoomIn.addEventListener('click', function() { scale = Math.min(5, scale * 1.3); update(); });
if (zoomOut) zoomOut.addEventListener('click', function() { scale = Math.max(0.2, scale * 0.7); update(); });
// Fullscreen (#28)
var fsBtn = container.querySelector('.mermaid-fullscreen');
if (fsBtn) {
fsBtn.addEventListener('click', function() {
openFullscreen(container);
});
}
}
// ---------------------------------------------------------------------------
// Fullscreen mode (#28) — with header bar + full-size diagram
// ---------------------------------------------------------------------------
function openFullscreen(container) {
var svgWrapper = container.querySelector('.mermaid-rendered');
var diagramType = container.querySelector('.mermaid-diagram-type');
var typeLabel = diagramType ? diagramType.textContent : 'mermaid';
var overlay = document.createElement('div');
overlay.className = 'mermaid-fullscreen-overlay';
// Fullscreen header bar
overlay.innerHTML =
'<div class="mermaid-fullscreen-header">' +
'<span class="mermaid-diagram-type">' + escapeHtml(typeLabel) + '</span>' +
'<div class="mermaid-header-actions">' +
'<button class="mermaid-btn mermaid-fs-zoom-in" title="Zoom +"><i data-lucide="zoom-in" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-zoom-out" title="Zoom -"><i data-lucide="zoom-out" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-copy-svg" title="Copier SVG"><i data-lucide="copy" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-copy-code" title="Copier le code Mermaid"><i data-lucide="file-code-2" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-download" title="Telecharger PNG"><i data-lucide="download" style="width:14px;height:14px"></i></button>' +
'<button class="mermaid-btn mermaid-fs-close" title="Fermer (Echap)"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
'</div>' +
'</div>' +
'<div class="mermaid-fullscreen-content"></div>';
var content = overlay.querySelector('.mermaid-fullscreen-content');
content.innerHTML = svgWrapper.innerHTML;
var svg = content.querySelector('svg');
if (svg) {
svg.style.cursor = 'default';
}
document.body.appendChild(overlay);
// Fullscreen zoom
var fsScale = 1, fsPanX = 0, fsPanY = 0, fsDragging = false, fsStartX, fsStartY;
if (svg) {
svg.style.transformOrigin = '0 0';
function fsUpdate() {
svg.style.transform = 'translate(' + fsPanX + 'px, ' + fsPanY + 'px) scale(' + fsScale + ')';
}
svg.addEventListener('mousedown', function(e) {
fsDragging = true;
fsStartX = e.clientX - fsPanX;
fsStartY = e.clientY - fsPanY;
svg.style.cursor = 'grabbing';
e.preventDefault();
});
document.addEventListener('mousemove', function(e) {
if (!fsDragging) return;
fsPanX = e.clientX - fsStartX;
fsPanY = e.clientY - fsStartY;
fsUpdate();
});
document.addEventListener('mouseup', function() {
fsDragging = false;
if (svg) svg.style.cursor = 'grab';
});
svg.addEventListener('wheel', function(e) {
e.preventDefault();
var delta = e.deltaY > 0 ? 0.9 : 1.1;
fsScale = Math.max(0.2, Math.min(5, fsScale * delta));
fsUpdate();
}, { passive: false });
var fsZoomIn = overlay.querySelector('.mermaid-fs-zoom-in');
var fsZoomOut = overlay.querySelector('.mermaid-fs-zoom-out');
if (fsZoomIn) fsZoomIn.addEventListener('click', function() { fsScale = Math.min(5, fsScale * 1.3); fsUpdate(); });
if (fsZoomOut) fsZoomOut.addEventListener('click', function() { fsScale = Math.max(0.2, fsScale * 0.7); fsUpdate(); });
}
// Copy SVG in fullscreen
var fsCopySvg = overlay.querySelector('.mermaid-fs-copy-svg');
if (fsCopySvg) {
fsCopySvg.addEventListener('click', function() {
copySvgFromElement(content);
});
}
// Copy code in fullscreen
var fsCopyCode = overlay.querySelector('.mermaid-fs-copy-code');
if (fsCopyCode) {
fsCopyCode.addEventListener('click', function() {
var code = container.getAttribute('data-mermaid-code') || '';
copyToClipboard(code);
});
}
// Download PNG in fullscreen
var fsDownload = overlay.querySelector('.mermaid-fs-download');
if (fsDownload) {
fsDownload.addEventListener('click', function() {
exportMermaidPNGFromElement(svg);
});
}
safeCreateIcons();
function close() {
overlay.remove();
document.removeEventListener('keydown', escHandler);
}
var escHandler = function(e) {
if (e.key === 'Escape') close();
};
document.addEventListener('keydown', escHandler);
overlay.addEventListener('click', function(e) {
if (e.target === overlay) close();
});
overlay.querySelector('.mermaid-fs-close').addEventListener('click', close);
}
// ---------------------------------------------------------------------------
// Copy SVG to clipboard
// ---------------------------------------------------------------------------
function copySvgToClipboard(container) {
copySvgFromElement(container.querySelector('.mermaid-rendered'));
}
function copySvgFromElement(el) {
if (!el) return;
var svg = el.querySelector('svg');
if (!svg) return;
var data = new XMLSerializer().serializeToString(svg);
copyToClipboard(data);
}
function fallbackCopy(text) {
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.style.position = 'fixed';
textarea.style.opacity = '0';
document.body.appendChild(textarea);
textarea.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(textarea);
}
// ---------------------------------------------------------------------------
// Export SVG/PNG (#24)
// ---------------------------------------------------------------------------
export function exportMermaidSVG(container) {
var svg = container.querySelector('.mermaid-rendered svg');
if (!svg) return;
var data = new XMLSerializer().serializeToString(svg);
var blob = new Blob([data], { type: 'image/svg+xml' });
downloadBlob(blob, 'diagram.svg');
}
export function exportMermaidPNG(container) {
var svg = container.querySelector('.mermaid-rendered svg');
exportMermaidPNGFromElement(svg);
}
function exportMermaidPNGFromElement(svg) {
if (!svg) return;
var data = new XMLSerializer().serializeToString(svg);
var canvas = document.createElement('canvas');
var ctx = canvas.getContext('2d');
var img = new Image();
var blob = new Blob([data], { type: 'image/svg+xml' });
var url = URL.createObjectURL(blob);
img.onload = function() {
canvas.width = img.width * 2;
canvas.height = img.height * 2;
ctx.scale(2, 2);
ctx.drawImage(img, 0, 0);
URL.revokeObjectURL(url);
canvas.toBlob(function(pngBlob) {
downloadBlob(pngBlob, 'diagram.png');
}, 'image/png');
};
img.src = url;
}
function downloadBlob(blob, filename) {
var a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
// ---------------------------------------------------------------------------
// Copy text to clipboard
// ---------------------------------------------------------------------------
function copyToClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).catch(function() {
fallbackCopy(text);
});
} else {
fallbackCopy(text);
}
}
// ---------------------------------------------------------------------------
// Focus mode (#29) — click diagram to enlarge in a side panel
// ---------------------------------------------------------------------------
var _focusPanel = null;
export function setupFocusMode() {
document.addEventListener('click', function(e) {
var container = e.target.closest('.mermaid-container');
if (!container) return;
// Don't trigger focus if clicking header or controls
if (e.target.closest('.mermaid-header') || e.target.closest('button')) return;
var svgWrapper = container.querySelector('.mermaid-rendered');
if (!svgWrapper) return;
if (_focusPanel && _focusPanel.style.display !== 'none') {
updateFocusPanel(svgWrapper);
} else {
showFocusPanel(svgWrapper);
}
});
}
function showFocusPanel(svgWrapper) {
if (!_focusPanel) {
_focusPanel = document.createElement('div');
_focusPanel.className = 'mermaid-focus-panel';
_focusPanel.innerHTML =
'<div class="mermaid-focus-header">' +
'<span>Diagramme</span>' +
'<button class="mermaid-focus-close" title="Fermer"><i data-lucide="x" style="width:14px;height:14px"></i></button>' +
'</div>' +
'<div class="mermaid-focus-content"></div>';
document.body.appendChild(_focusPanel);
_focusPanel.querySelector('.mermaid-focus-close').addEventListener('click', function() {
_focusPanel.style.display = 'none';
});
}
_focusPanel.style.display = 'block';
updateFocusPanel(svgWrapper);
safeCreateIcons();
}
function updateFocusPanel(svgWrapper) {
if (!_focusPanel) return;
var content = _focusPanel.querySelector('.mermaid-focus-content');
content.innerHTML = svgWrapper.innerHTML;
var svg = content.querySelector('svg');
if (svg) {
svg.style.width = '100%';
svg.style.height = 'auto';
svg.style.maxHeight = '80vh';
}
}
// ---------------------------------------------------------------------------
// Obsidian compatibility pre-processor (#24)
// ---------------------------------------------------------------------------
export function preprocessObsidian(code) {
return code
.replace(/!\[\[([^\]]+?)\]\]/g, function(_, m) {
var parts = m.split('|');
var src = parts[0].trim();
var width = parts[1] ? parts[1].trim() : null;
if (width && /^\d+$/.test(width)) {
return '<img src="' + src + '" width="' + width + '" />';
}
return '<img src="' + src + '" />';
})
.replace(/\[\[([^\]]+?)\]\]/g, '<a href="$1">$1</a>')
.replace(/==([^=]+)==/g, '<mark>$1</mark>');
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
function escapeHtml(str) {
var div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
@@ -1,149 +0,0 @@
/* ObsiGate — Mobile Bottom Toolbar (Obsidian-style)
Phases 1-5: fixed bar, vault home, file palette, commands, tab switcher */
import { state } from './state.js';
import { TabManager } from './ui.js';
// ── Open file palette (search files) ──
function openFilePalette() {
import('./palette.js').then(m => { m.open(''); });
}
// ── Open command palette ──
function openCommandPalette() {
import('./palette.js').then(m => { m.open('> '); });
}
// ── Tab switcher ──
function openTabSwitcher() {
const sheet = document.getElementById('mt-tab-switcher');
const overlay = document.getElementById('mt-overlay');
if (!sheet || !overlay) return;
renderTabList();
sheet.classList.add('open');
overlay.classList.add('visible');
}
function closeTabSwitcher() {
const sheet = document.getElementById('mt-tab-switcher');
const overlay = document.getElementById('mt-overlay');
if (sheet) sheet.classList.remove('open');
if (overlay) overlay.classList.remove('visible');
}
function renderTabList() {
const list = document.getElementById('mt-tab-list');
if (!list) return;
const tabs = TabManager._tabs || [];
const activeId = TabManager._activeTabId;
if (tabs.length === 0) {
list.innerHTML = '<div class="cp-empty" style="padding:20px;text-align:center">Aucun fichier ouvert</div>';
return;
}
list.innerHTML = tabs.map(t => {
const isActive = t.id === activeId;
const icon = t.path.endsWith('.md') ? '📄' : '📝';
return `<div class="mt-tab-item${isActive ? ' active' : ''}" data-tab-id="${t.id}">
<span class="mt-tab-item-icon">${icon}</span>
<span class="mt-tab-item-name">${t.title || t.path.split('/').pop()}</span>
<button class="mt-tab-item-close" data-close="${t.id}" aria-label="Fermer">✕</button>
</div>`;
}).join('');
list.querySelectorAll('.mt-tab-item').forEach(el => {
el.addEventListener('click', (e) => {
if (e.target.closest('.mt-tab-item-close')) return;
const tabId = el.dataset.tabId;
if (tabId) {
TabManager.activate(tabId);
closeTabSwitcher();
}
});
});
list.querySelectorAll('.mt-tab-item-close').forEach(btn => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const tabId = btn.dataset.close;
if (tabId) {
TabManager.close(tabId);
setTimeout(() => renderTabList(), 100);
}
});
});
}
function updateTabCount() {
const badge = document.getElementById('mt-tab-count');
if (!badge) return;
const count = (TabManager._tabs || []).length;
if (count > 0) {
badge.style.display = 'flex';
badge.textContent = count;
} else {
badge.style.display = 'none';
}
}
// ── Vault home / sidebar toggle ──
function toggleExplorer() {
const sidebar = document.getElementById('sidebar');
if (sidebar) {
if (sidebar.classList.contains('mobile-open')) {
sidebar.classList.remove('mobile-open');
} else {
sidebar.classList.add('mobile-open');
}
}
if (!state.currentVault && state.allVaults && state.allVaults.length > 0) {
import('./sidebar.js').then(m => {
if (m.setSelectedVaultContext) m.setSelectedVaultContext(state.allVaults[0].name);
});
}
}
// ── Init ──
export function initMobileToolbar() {
const explorerBtn = document.getElementById('mt-explorer');
const searchBtn = document.getElementById('mt-search');
const paletteBtn = document.getElementById('mt-palette');
const tabsBtn = document.getElementById('mt-tabs');
const sheetClose = document.getElementById('mt-sheet-close');
const closeAll = document.getElementById('mt-close-all');
const overlay = document.getElementById('mt-overlay');
if (explorerBtn) explorerBtn.addEventListener('click', toggleExplorer);
if (searchBtn) searchBtn.addEventListener('click', openFilePalette);
if (paletteBtn) paletteBtn.addEventListener('click', openCommandPalette);
if (tabsBtn) tabsBtn.addEventListener('click', openTabSwitcher);
if (sheetClose) sheetClose.addEventListener('click', closeTabSwitcher);
if (closeAll) closeAll.addEventListener('click', () => {
const tabs = [...(TabManager._tabs || [])];
tabs.forEach(t => TabManager.close(t.id));
updateTabCount();
renderTabList();
});
if (overlay) overlay.addEventListener('click', closeTabSwitcher);
setInterval(updateTabCount, 2000);
updateTabCount();
// ── Vault context indicator ──
function updateVaultPill() {
const pill = document.getElementById('vault-context-pill');
if (!pill) return;
const vaultName = state.selectedContextVault || 'all';
if (vaultName === 'all') {
pill.textContent = t("mobile.all_vaults");
} else {
pill.textContent = '📂 ' + vaultName;
}
}
updateVaultPill();
setInterval(updateVaultPill, 1500);
window._mtUpdateTabs = updateTabCount;
}
-377
View File
@@ -1,377 +0,0 @@
/* ObsiGate — Offline IndexedDB Storage
* Stores: file index, file contents, pending changes queue
* Schema v1:
* files store: { path, vault, title, tags[], snippet, modified, tokens[], weight }
* content store: { key: "vault::path", content: string, modified }
* pending store: { id, vault, path, content, action, timestamp }
*/
const DB_NAME = 'obsigate-offline';
const DB_VERSION = 1;
let _db = null;
let _dbPromise = null;
export const OfflineDB = {
async open() {
if (_db) return _db;
if (_dbPromise) return _dbPromise;
_dbPromise = new Promise((resolve, reject) => {
const req = indexedDB.open(DB_NAME, DB_VERSION);
req.onupgradeneeded = (e) => {
const db = e.target.result;
console.log('[OfflineDB] Upgrading to v' + DB_VERSION);
// File index store — keyed by composite "vault::path"
if (!db.objectStoreNames.contains('files')) {
const store = db.createObjectStore('files', { keyPath: 'key' });
store.createIndex('vault', 'vault', { unique: false });
store.createIndex('modified', 'modified', { unique: false });
store.createIndex('title', 'title', { unique: false });
}
// File contents store — keyed by "vault::path"
if (!db.objectStoreNames.contains('content')) {
db.createObjectStore('content', { keyPath: 'key' });
}
// Pending changes queue — auto-increment key
if (!db.objectStoreNames.contains('pending')) {
const store = db.createObjectStore('pending', { keyPath: 'id', autoIncrement: true });
store.createIndex('timestamp', 'timestamp', { unique: false });
}
};
req.onsuccess = (e) => {
_db = e.target.result;
console.log('[OfflineDB] Database opened');
resolve(_db);
};
req.onerror = () => {
_dbPromise = null;
reject(req.error);
};
});
return _dbPromise;
},
/* ── File Index ── */
async putFileIndex(file) {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction('files', 'readwrite');
const store = tx.objectStore('files');
const key = file.vault + '::' + file.path;
const doc = { key: key, vault: file.vault, path: file.path,
title: file.title, tags: file.tags || [], snippet: file.snippet || '',
modified: file.modified || '', tokens: file.tokens || [], weight: file.score || 0 };
store.put(doc);
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
},
async putFileIndexBatch(files) {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction('files', 'readwrite');
const store = tx.objectStore('files');
for (const file of files) {
const key = file.vault + '::' + file.path;
store.put({
key: key, vault: file.vault, path: file.path,
title: file.title, tags: file.tags || [], snippet: file.snippet || '',
modified: file.modified || '', tokens: file.tokens || [], score: file.score || 0
});
}
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
},
async getFileIndex(vault, path) {
const db = await this.open();
const key = vault + '::' + path;
return new Promise((resolve) => {
const tx = db.transaction('files', 'readonly');
const store = tx.objectStore('files');
const req = store.get(key);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => resolve(null);
});
},
/* Search in file index — title matching and prefix */
async searchFileIndex(query, vaultFilter) {
const db = await this.open();
const lowerQ = query.toLowerCase();
const words = lowerQ.split(/\s+/).filter(Boolean);
return new Promise((resolve) => {
const tx = db.transaction('files', 'readonly');
const store = tx.objectStore('files');
const results = [];
store.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor) {
// Sort by relevance: title match > path match > snippet match
results.sort((a, b) => b._score - a._score);
resolve(results.slice(0, 50));
return;
}
const doc = cursor.value;
// Vault filter
if (vaultFilter && vaultFilter !== 'all' && doc.vault !== vaultFilter) {
cursor.continue();
return;
}
// Score based on match quality
let score = 0;
const titleLower = doc.title.toLowerCase();
const pathLower = doc.path.toLowerCase();
const snippetLower = (doc.snippet || '').toLowerCase();
for (const word of words) {
if (titleLower.includes(word)) score += 10;
else if (pathLower.includes(word)) score += 5;
else if (snippetLower.includes(word)) score += 2;
}
// Full query match bonus
if (titleLower.includes(lowerQ)) score += 5;
if (pathLower === lowerQ) score += 8;
if (score > 0) {
results.push({
vault: doc.vault,
path: doc.path,
title: doc.title,
tags: doc.tags,
snippet: doc.snippet,
score: score,
modified: doc.modified,
_score: score
});
}
cursor.continue();
};
});
},
async getAllFileIndex(vaultFilter) {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('files', 'readonly');
const store = tx.objectStore('files');
const results = [];
store.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor) { resolve(results); return; }
if (!vaultFilter || vaultFilter === 'all' || cursor.value.vault === vaultFilter) {
results.push(cursor.value);
}
cursor.continue();
};
});
},
async clearFileIndex(vaultFilter) {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('files', 'readwrite');
const store = tx.objectStore('files');
if (!vaultFilter || vaultFilter === 'all') {
store.clear().onsuccess = () => resolve();
} else {
store.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor) { resolve(); return; }
if (cursor.value.vault === vaultFilter) cursor.delete();
cursor.continue();
};
}
});
},
/* ── File Contents ── */
async putFileContent(vault, path, content, modified) {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction('content', 'readwrite');
const store = tx.objectStore('content');
const key = vault + '::' + path;
store.put({ key: key, content: content, modified: modified || '' });
tx.oncomplete = () => resolve();
tx.onerror = () => reject(tx.error);
});
},
async getFileContent(vault, path) {
const db = await this.open();
const key = vault + '::' + path;
return new Promise((resolve) => {
const tx = db.transaction('content', 'readonly');
const store = tx.objectStore('content');
const req = store.get(key);
req.onsuccess = () => resolve(req.result || null);
req.onerror = () => resolve(null);
});
},
async removeFileContent(vault, path) {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('content', 'readwrite');
const store = tx.objectStore('content');
const key = vault + '::' + path;
store.delete(key);
tx.oncomplete = () => resolve();
});
},
/* ── Pending Changes Queue ── */
async addPending(entry) {
const db = await this.open();
return new Promise((resolve, reject) => {
const tx = db.transaction('pending', 'readwrite');
const store = tx.objectStore('pending');
const doc = {
vault: entry.vault,
path: entry.path,
content: entry.content || '',
action: entry.action || 'save',
timestamp: Date.now()
};
const req = store.add(doc);
req.onsuccess = () => resolve(req.result);
tx.onerror = () => reject(tx.error);
});
},
async getPendingCount() {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('pending', 'readonly');
const store = tx.objectStore('pending');
store.count().onsuccess = (e) => resolve(e.target.result);
});
},
async getAllPending() {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('pending', 'readonly');
const store = tx.objectStore('pending');
const results = [];
const index = store.index('timestamp');
index.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor) { resolve(results); return; }
results.push(cursor.value);
cursor.continue();
};
});
},
async clearPending() {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('pending', 'readwrite');
tx.objectStore('pending').clear().onsuccess = () => resolve();
});
},
async removePendingById(id) {
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('pending', 'readwrite');
tx.objectStore('pending').delete(id);
tx.oncomplete = () => resolve();
});
},
/* ── Stats ── */
async getStats() {
const db = await this.open();
const fileCount = await new Promise(r => {
db.transaction('files','readonly').objectStore('files').count().onsuccess = e => r(e.target.result);
});
const contentCount = await new Promise(r => {
db.transaction('content','readonly').objectStore('content').count().onsuccess = e => r(e.target.result);
});
const pendingCount = await this.getPendingCount();
return { fileCount, contentCount, pendingCount };
},
/* ── Full Sync ── */
async syncFileIndex(serverFiles) {
// serverFiles: array of {vault,path,title,tags,snippet,modified,tokens}
await this.clearFileIndex(null);
await this.putFileIndexBatch(serverFiles);
console.log('[OfflineDB] File index synced:', serverFiles.length, 'files');
},
async pruneContentCache(maxEntries) {
maxEntries = maxEntries || 50;
const db = await this.open();
return new Promise((resolve) => {
const tx = db.transaction('content', 'readwrite');
const store = tx.objectStore('content');
const keys = [];
store.openCursor().onsuccess = (e) => {
const cursor = e.target.result;
if (!cursor) {
// Delete oldest if over maxEntries
if (keys.length > maxEntries) {
const toDelete = keys.slice(0, keys.length - maxEntries);
for (const k of toDelete) store.delete(k);
}
resolve();
return;
}
keys.push(cursor.value.key);
cursor.continue();
};
});
}
};
/* ── Network Status ── */
let _online = navigator.onLine;
let _listeners = [];
export const NetworkStatus = {
get isOnline() { return _online; },
init() {
window.addEventListener('online', () => {
_online = true;
_listeners.forEach(fn => fn(true));
console.log('[Network] Online');
});
window.addEventListener('offline', () => {
_online = false;
_listeners.forEach(fn => fn(false));
console.log('[Network] Offline');
});
},
onChange(fn) {
_listeners.push(fn);
return () => { _listeners = _listeners.filter(f => f !== fn); };
}
};
-209
View File
@@ -1,209 +0,0 @@
/* ObsiGate — Offline Integration Module
* Ties together: IndexedDB, offline search, sync queue, UI indicator
*/
import { OfflineDB, NetworkStatus } from './offline-db.js';
import { state } from './state.js';
import { showToast } from './ui.js';
/* ── Init ── */
export async function initOffline() {
NetworkStatus.init();
// Open DB early
try {
await OfflineDB.open();
console.log('[Offline] IndexedDB ready');
} catch (e) {
console.warn('[Offline] IndexedDB not available:', e);
}
NetworkStatus.onChange((online) => {
updateOfflineIndicator();
if (online) {
syncPendingChanges();
showToast(t('offline.reconnected'), 'info');
} else {
showToast(t('offline.mode_offline'), 'warning');
}
});
updateOfflineIndicator();
}
/* ── UI Indicator ── */
function updateOfflineIndicator() {
const badge = document.getElementById('offline-badge');
if (!badge) return;
if (!NetworkStatus.isOnline) {
// Build or show indicator
badge.innerHTML = replaceLucideIcon(badge, 'wifi-off',
'<i data-lucide="wifi-off" style="width:14px;height:14px"></i>');
badge.title = 'Mode hors-ligne';
badge.style.display = 'flex';
badge.className = 'header-icon offline-indicator offline';
// Add pending count if any
OfflineDB.getPendingCount().then(count => {
if (count > 0) {
const span = badge.querySelector('.offline-pending') || document.createElement('span');
span.className = 'offline-pending';
span.textContent = String(count);
if (!badge.contains(span)) badge.appendChild(span);
}
});
} else {
badge.style.display = 'none';
badge.className = 'header-icon offline-indicator';
}
}
function replaceLucideIcon(container, iconName, html) {
const existing = container.querySelector('i[data-lucide]');
if (existing && existing.getAttribute('data-lucide') === iconName) return container.innerHTML;
return html;
}
/* ── Sync File Index from Server ── */
export async function syncFileIndexFromServer() {
if (!NetworkStatus.isOnline) {
console.log('[Offline] Skipping index sync — offline');
return;
}
try {
// Use a wildcard query to get recent files and populate offline index
const resp = await fetch('/api/search/advanced?q=**&vault=all&limit=200');
if (!resp.ok) {
console.warn('[Offline] Index sync failed — API returned', resp.status);
return;
}
const data = await resp.json();
const files = (data.results || []).map(r => ({
vault: r.vault,
path: r.path,
title: r.title,
tags: r.tags || [],
snippet: r.snippet || '',
modified: r.modified || '',
tokens: []
}));
if (files.length > 0) {
await OfflineDB.syncFileIndex(files);
console.log('[Offline] Index synced:', files.length, 'files');
}
} catch (e) {
console.warn('[Offline] Index sync failed:', e.message);
}
}
/* ── Cache File When Viewed ── */
export async function cacheViewedFile(vault, path, content, modified) {
if (!vault || !path) return;
try {
await OfflineDB.putFileContent(vault, path, content, modified);
await OfflineDB.pruneContentCache(50);
} catch (e) {
console.warn('[Offline] Cache file failed:', e);
}
}
/* ── Offline Search ── */
export async function searchOffline(query, vaultFilter) {
try {
return await OfflineDB.searchFileIndex(query, vaultFilter || state.currentVault);
} catch (e) {
console.error('[Offline] Search failed:', e);
return [];
}
}
/* ── Get Cached File ── */
export async function getCachedFile(vault, path) {
try {
return await OfflineDB.getFileContent(vault, path);
} catch (e) {
return null;
}
}
/* ── Pending Changes Queue ── */
let _syncing = false;
export async function queueOfflineSave(vault, path, content) {
try {
const id = await OfflineDB.addPending({ vault, path, content, action: 'save' });
updateOfflineIndicator();
console.log('[Offline] Queued save:', vault, path, 'id=', id);
return id;
} catch (e) {
console.error('[Offline] Queue save failed:', e);
return null;
}
}
export async function syncPendingChanges() {
if (!NetworkStatus.isOnline || _syncing) return;
_syncing = true;
try {
const pending = await OfflineDB.getAllPending();
if (pending.length === 0) {
_syncing = false;
return;
}
console.log('[Offline] Syncing', pending.length, 'pending changes...');
for (const entry of pending) {
try {
const resp = await fetch('/api/file/' + encodeURIComponent(entry.vault) + '/write', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ path: entry.path, content: entry.content })
});
if (resp.ok) {
await OfflineDB.removePendingById(entry.id);
console.log('[Offline] Synced:', entry.vault, entry.path);
} else if (resp.status === 409) {
// Conflict — keep in queue for manual resolution
console.warn('[Offline] Conflict on:', entry.vault, entry.path);
} else {
console.error('[Offline] Sync failed for:', entry.vault, entry.path, resp.status);
}
} catch (e) {
console.error('[Offline] Sync error for:', entry.vault, entry.path, e);
break; // Stop on network error
}
}
const remaining = await OfflineDB.getPendingCount();
console.log('[Offline] Sync complete. Remaining:', remaining);
updateOfflineIndicator();
if (remaining === 0) {
showToast(t('offline.all_synced'), 'success');
} else {
showToast(remaining + ' modification(s) en attente de resolution', 'warning');
}
} finally {
_syncing = false;
}
}
/* ── Conflict Detection ── */
export async function checkConflict(vault, path, localModified) {
if (!NetworkStatus.isOnline) return null;
try {
const resp = await fetch('/api/file/' + encodeURIComponent(vault) + '/info?path=' + encodeURIComponent(path));
if (!resp.ok) return null;
const data = await resp.json();
if (data.modified && data.modified !== localModified) {
return { serverModified: data.modified, localModified };
}
return null;
} catch {
return null;
}
}
-1
View File
@@ -1 +0,0 @@
{"type": "module"}
-590
View File
@@ -1,590 +0,0 @@
/* ObsiGate — Command Palette (Ctrl+Shift+Espace / Ctrl+Alt+Espace) */
import { api } from './auth.js';
import { showToast, toggleTheme, TabManager } from './ui.js';
import { openEditor } from './utils.js';
import { state } from './state.js';
import { t } from './i18n.js';
function triggerGoHome() { const e = document.getElementById('header-logo'); if (e) e.click(); }
function getCurrentFile() {
const id = TabManager._activeTabId;
if (id) { const t = TabManager._tabs.find(x => x.id === id); if (t) return { vault: t.vault, path: t.path }; }
if (state.currentVault && state.currentPath) return { vault: state.currentVault, path: state.currentPath };
return null;
}
// ── Query parser: +word = required, -word = exclude, word = required ──
function parseQuery(raw) {
const positive = [];
const negative = [];
const parts = (raw || '').split(/\s+/).filter(Boolean);
for (const p of parts) {
if (p.startsWith('-') && p.length > 1) negative.push(p.slice(1).toLowerCase());
else if (p.startsWith('+')) positive.push(p.slice(1).toLowerCase());
else positive.push(p.toLowerCase());
}
return { positive, negative };
}
// ── DOM / state ──
let _overlay = null, _input = null, _results = null, _mode = 'files', _sel = 0, _to = null, _items = [];
let _browse = { action: null, vault: null, path: '' };
let _browseFilterEl = null, _browseFilter = '', _allItems = [];
let _browseFilterTimer = null; // debounce for global search
// ── Commands ── (lazy: built on first access so i18n is ready)
let _cmdsCache = null;
function getCMDS() {
if (_cmdsCache) return _cmdsCache;
_cmdsCache = [
{ id:'home', label:t('palette.cmd_back_home'), desc:t('palette.cmd_back_home_desc'), cat:'Navigation', act:()=>{close();triggerGoHome();} },
{ id:'edit-current', label:'✏️ '+t('editor.edit'), desc:t('editor.edit_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)openEditor(f.vault,f.path);else showToast(t('toast.no_open_file'),'error');} },
{ id:'popout', label:'🪟 Pop-out', desc:t('palette.popout_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f)window.open(`/popout/${encodeURIComponent(f.vault)}/${encodeURIComponent(f.path)}?theme=${document.documentElement.getAttribute('data-theme')||'dark'}`,'popout','width=1000,height=700');else showToast(t('toast.no_open_file'),'error');} },
{ id:'create-file', label:t('palette.cmd_create_file'), desc:t('palette.cmd_create_file_desc'), cat:'Fichiers', act:()=>startBrowse('create-file') },
{ id:'delete-file', label:'🗑️ '+t('file.delete'), desc:t('palette.delete_file_desc'), cat:'Fichiers', act:()=>startBrowse('delete-file') },
{ id:'delete-cur', label:'🗑️ '+t('palette.delete_current'), desc:t('palette.delete_current_desc'), cat:'Fichiers', act:async()=>{close();await delCurFile();} },
{ id:'close-cur', label:'✕ '+t('palette.close_current'), desc:t('palette.close_current_desc'), cat:'Fichiers', act:()=>{close();const f=getCurrentFile();if(f){const tab=TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path);if(tab)TabManager.close(tab.id);else triggerGoHome();}else{showToast(t('toast.no_open_file'),'error');}} },
{ id:'create-dir', label:t('palette.cmd_create_dir'), desc:t('palette.cmd_create_dir_desc'), cat:'Répertoires', act:()=>startBrowse('create-dir') },
{ id:'delete-dir', label:t('palette.cmd_delete_dir'), desc:t('palette.cmd_delete_dir_desc'), cat:'Répertoires', act:()=>startBrowse('delete-dir') },
{ id:'theme', label:t('palette.cmd_theme'), desc:t('palette.cmd_theme_desc'), cat:'Actions', act:()=>{close();toggleTheme();} },
{ id:'reindex', label:'🔄 '+t('vault.reindex'), desc:t('palette.reindex_desc'), cat:'Actions', act:async()=>{close();try{await api('/api/index/reload');showToast(t('sync.index_reloaded'),'success');}catch{showToast(t('common.error'),'error');}} },
{ id:'help', label:'❓ '+t('help.help_title'), desc:t('palette.help_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('help-open-btn');if(b)b.click();} },
{ id:'config', label:t('palette.cmd_config'), desc:t('palette.cmd_config_desc'), cat:'Système', act:()=>{close();const b=document.getElementById('config-open-btn');if(b)b.click();} },
];
return _cmdsCache;
}
// ── Browse: vault picker ──
function startBrowse(action) {
_browse = { action, vault: null, path: '' };
_browseFilter = ''; _allItems = [];
switchMode('browse-vault');
renderVaultPicker();
}
async function renderVaultPicker() {
try {
const data = await api('/api/vaults');
const vaults = Array.isArray(data) ? data : [];
_items = vaults.map(v => ({ type:'_vault', vault: v.name, label: (v.type==='DIR'?'📂':'🗂️')+' '+v.name, sublabel: t('palette.vault_count', {count: v.file_count}) }));
if (_items.length === 0) {
_items = [{ type:'_empty', label: t('palette.no_vaults'), sublabel: '' }];
}
_allItems = _items;
render();
} catch { _items = [{ type:'_error', label: t('palette.error_loading'), sublabel: '' }]; _allItems = _items; render(); }
}
// ── Browse: directory navigation ──
async function renderBrowse() {
const { vault, path } = _browse;
_items = [];
const bc = document.getElementById('cp-breadcrumb');
if (bc) {
bc.style.display = 'flex';
const segs = path ? path.split('/') : [];
let html = `<span class="cp-bc-item cp-bc-root" data-vault="1">${esc(vault)}</span>`;
let acc = '';
for (const s of segs) { acc = acc ? acc+'/'+s : s; html += `<span class="cp-bc-sep">/</span><span class="cp-bc-item" data-path="${esc(acc)}">${esc(s)}</span>`; }
bc.innerHTML = html;
bc.querySelectorAll('.cp-bc-item[data-path]').forEach(el => el.addEventListener('click', ()=>{ _browse.path=el.dataset.path; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); }));
bc.querySelector('.cp-bc-root').addEventListener('click', ()=>{ _browse.path=''; _browseFilter=''; if(_browseFilterEl)_browseFilterEl.value=''; renderBrowse(); });
}
try {
const data = await api(`/api/browse/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`);
const items = data.items || [];
const dirs = items.filter(i => i.type === 'directory' || i.is_dir);
const files = items.filter(i => i.type === 'file' && !i.is_dir);
// Action row
const actLabel = {
'create-file': t('palette.cmd_create_file'),
'create-dir': t('palette.cmd_create_dir'),
'delete-dir': t('palette.cmd_delete_dir'),
'delete-file': t('palette.delete_file_desc'),
'open-file': t('palette.browse_open_file')
};
const isDeleteDir = _browse.action === 'delete-dir' && path;
const isDeleteFile = _browse.action === 'delete-file';
const isOpenFile = _browse.action === 'open-file';
const needsName = _browse.action === 'create-file' || _browse.action === 'create-dir';
if (isDeleteDir) {
_items.push({ type:'_action', label: '🗑️ '+t('file.delete')+' '+(path.split('/').pop()||path), sublabel: t('palette.dir_delete_confirm'), act:()=>execDelDir(path) });
} else if (needsName) {
_items.push({ type:'_action', label: actLabel[_browse.action]||'', sublabel: t('palette.browse_in', {path: path||t('palette.browse_root')}), act:()=>askName() });
}
// Parent
if (path) {
_items.push({ type:'_nav', label: '📂 .. (parent)', go: path.split('/').slice(0,-1).join('/') });
}
// Subdirs
for (const d of dirs) {
const fp = path ? path+'/'+d.name : d.name;
_items.push({ type:'_nav', label: '📁 '+d.name, go: fp });
}
// Files
for (const f of files) {
const fp = path ? path+'/'+f.name : f.name;
if (isDeleteFile) {
_items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>execDelFile(fp) });
} else if (isOpenFile) {
_items.push({ type:'_file', label: '📄 '+f.name, sublabel: f.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(vault, fp); } });
} else {
_items.push({ type:'_info', label: '📄 '+f.name, sublabel: fp });
}
}
if (_items.length === 0) _items.push({ type:'_empty', label: t('palette.empty_dir'), sublabel: '' });
} catch { _items = [{ type:'_error', label: t('common.error'), sublabel: '' }]; }
_allItems = _items;
applyBrowseFilter();
}
// ── Browse filter: local when empty, global suggest when text ──
function applyBrowseFilter() {
const q = _browseFilter.trim();
if (!q) {
// Empty filter: show local directory items
_items = _allItems;
render();
return;
}
// Has filter text: debounce and search globally
if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
_browseFilterTimer = setTimeout(() => searchBrowseGlobal(q), 200);
}
async function searchBrowseGlobal(rawQ) {
const { positive, negative } = parseQuery(rawQ);
const action = _browse.action;
const isDeleteFile = action === 'delete-file';
const isOpenFile = action === 'open-file';
const isDeleteDir = action === 'delete-dir';
const needsName = action === 'create-file' || action === 'create-dir';
try {
const q = positive.join(' ');
const vault = _browse.vault || 'all';
const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=${encodeURIComponent(vault)}`);
const sug = (d.suggestions || []).filter(s => {
const fullPath = (s.vault+'/'+s.path).toLowerCase();
// Client-side negative filter
for (const n of negative) {
if (fullPath.includes(n)) return false;
}
return true;
});
if (sug.length === 0) {
_items = [{ type:'_empty', label: t('palette.no_results', {query: rawQ}), sublabel: '' }];
render();
return;
}
_items = [];
// Extract unique directories from result paths
const dirSet = new Set();
for (const s of sug) {
const parts = s.path.split('/');
// Build parent directories
for (let i = 0; i < parts.length - 1; i++) {
dirSet.add(s.vault + '|' + parts.slice(0, i + 1).join('/'));
}
}
// Show directories as navigable
const dirs = [...dirSet].map(d => {
const [v, p] = d.split('|');
const name = p.split('/').pop();
return { type:'_nav', label:'📁 '+name, sublabel:v+'/'+p, go:p, vault:v };
}).sort((a,b) => a.sublabel.localeCompare(b.sublabel));
for (const d of dirs) _items.push(d);
// Show files
for (const s of sug) {
const fp = s.path;
const name = fp.split('/').pop();
if (isDeleteFile) {
_items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelFileBrowsed(s.vault, fp) });
} else if (isOpenFile) {
_items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } });
} else if (isDeleteDir) {
_items.push({ type:'_file', label:'📁 '+name, sublabel:s.vault+'/'+fp, act:()=>execDelDirBrowsed(s.vault, fp) });
} else {
_items.push({ type:'_file', label:'📄 '+name, sublabel:s.vault+'/'+fp, act:()=>{ close(); TabManager.openPersistent(s.vault, fp); } });
}
}
render();
} catch { _items = [{ type:'_error', label: t('palette.search_error'), sublabel: '' }]; render(); }
}
// Navigate to a directory from global search
function execNavGlobal(vault, goPath) {
_browse.vault = vault;
_browse.path = goPath;
_browseFilter = '';
if (_browseFilterEl) _browseFilterEl.value = '';
renderBrowse();
}
async function execDelFileBrowsed(vault, path) {
if (!confirm(t("confirm.delete_file_named", {path: vault + "/" + path}))) return;
try {
const d = await api(`/api/file/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
else showToast(t('common.error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
async function execDelDirBrowsed(vault, path) {
if (!confirm(t("confirm.delete_dir_named", {path: path}))) return;
try {
const d = await api(`/api/directory/${encodeURIComponent(vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
else showToast(t('common.error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
function onBrowseFilterInput() {
_browseFilter = _browseFilterEl ? _browseFilterEl.value : '';
applyBrowseFilter();
}
function onBrowseFilterKey(e) {
if (e.key === 'Escape') {
e.preventDefault();
if (_browseFilter) {
_browseFilter = '';
if (_browseFilterEl) _browseFilterEl.value = '';
applyBrowseFilter();
} else {
close();
}
} else if (e.key === 'ArrowDown') {
e.preventDefault();
_sel = 0;
updSel(_results.querySelectorAll('.cp-item'));
_overlay.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const items = _results.querySelectorAll('.cp-item');
_sel = Math.max(items.length - 1, 0);
updSel(items);
_overlay.focus();
} else if (e.key === 'Enter') {
e.preventDefault();
if (_items.length > 0) exec(_sel);
} else if (e.key === 'Backspace' && !_browseFilter && (!_browseFilterEl || !_browseFilterEl.value)) {
e.preventDefault();
const segs = _browse.path ? _browse.path.split('/') : [];
if (segs.length > 0) { _browse.path = segs.slice(0, -1).join('/'); renderBrowse(); }
else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); }
}
}
// ── Name input modal ──
function askName() {
const label = _browse.action === 'create-file' ? 'Nom du fichier' : 'Nom du dossier';
const ext = _browse.action === 'create-file' ? '(.md ajouté si besoin)' : '';
const existingInput = document.getElementById('cp-name-input');
if (existingInput) { existingInput.focus(); return; }
_input.style.display = 'none';
const bc = document.getElementById('cp-breadcrumb');
if (bc) bc.style.display = 'none';
const nameDiv = document.createElement('div');
nameDiv.id = 'cp-name-input';
nameDiv.className = 'cp-name-input';
nameDiv.innerHTML = `<input type="text" id="cp-name-field" class="cp-name-field" placeholder="${label} ${ext}" autocomplete="off" spellcheck="false">
<span class="cp-name-hint">Enter confirmer · Esc annuler</span>`;
_results.before(nameDiv);
const field = document.getElementById('cp-name-field');
field.focus();
field.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const name = field.value.trim();
if (!name) return;
cleanupNameInput();
if (_browse.action === 'create-file') execCreateFile(name);
else execCreateDir(name);
} else if (e.key === 'Escape') {
e.preventDefault();
cleanupNameInput();
_overlay.focus();
renderBrowse();
}
});
}
function cleanupNameInput() {
const el = document.getElementById('cp-name-input');
if (el) el.remove();
const bc = document.getElementById('cp-breadcrumb');
if (bc) bc.style.display = 'flex';
}
// ── Execute actions ──
async function execCreateFile(name) {
let fp = _browse.path ? _browse.path+'/'+name : name;
if (!fp.includes('.')) fp += '.md';
try {
const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:fp, content:`# ${name.replace('.md','')}\n\n`}) });
if (d?.status==='ok' || d?.success) { showToast(`✅ ${fp} créé`,'success'); close(); openEditor(_browse.vault, fp); }
else showToast(t('toast.create_error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
async function execCreateDir(name) {
const dp = _browse.path ? _browse.path+'/'+name : name;
try {
const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}`, { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({path:dp}) });
if (d?.status==='ok' || d?.success) { showToast(`📁 ${dp} créé`,'success'); close(); refreshSidebar(); }
else showToast(t('common.error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
async function execDelDir(path) {
if (!confirm(t("confirm.delete_dir_named", {path: path}))) return;
try {
const d = await api(`/api/directory/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
else showToast(t('common.error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
async function execDelFile(path) {
if (!confirm(t("confirm.delete_file_named", {path: path}))) return;
try {
const d = await api(`/api/file/${encodeURIComponent(_browse.vault)}?path=${encodeURIComponent(path)}`, { method:'DELETE' });
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${path} supprimé`,'success'); close(); refreshSidebar(); }
else showToast(t('common.error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
async function delCurFile() {
const f = getCurrentFile();
if (!f) { showToast(t('toast.no_open_file'),'error'); return; }
if (!confirm(t("confirm.delete_file_named", {path: f.path}))) return;
try {
const d = await api(`/api/file/${encodeURIComponent(f.vault)}?path=${encodeURIComponent(f.path)}`, { method:'DELETE' });
if (d?.status==='ok' || d?.success) { showToast(`🗑️ ${f.path} supprimé`,'success');
const tab = TabManager._tabs.find(t=>t.vault===f.vault&&t.path===f.path); if(tab) TabManager.close(tab.id); triggerGoHome(); refreshSidebar(); }
else showToast(t('common.error'),'error');
} catch(e) { showToast('Erreur : '+(e.message||e),'error'); }
}
function refreshSidebar() {
import('./sidebar.js').then(m => { if (m.loadVaults) m.loadVaults(); }).catch(() => {});
}
// ── DOM ──
function createDOM() {
if (_overlay) return;
_overlay = document.createElement('div');
_overlay.className = 'command-palette-overlay';
_overlay.tabIndex = -1;
_overlay.innerHTML = `<div class="command-palette">
<div class="cp-header"><input type="text" class="cp-input" placeholder="Rechercher un fichier... (+inclure -exclure)" spellcheck="false" autocomplete="off"><div class="cp-breadcrumb" id="cp-breadcrumb"></div><span class="cp-hint">Ctrl+Shift+Espace</span></div>
<input type="text" class="cp-browse-filter" id="cp-browse-filter" placeholder="Filtrer (+mot -mot) — recherche globale..." spellcheck="false" autocomplete="off" style="display:none">
<div class="cp-results"></div>
<div class="cp-footer"><span>↑↓ naviguer</span><span>↵ ouvrir/entrer</span><span>⌫ monter</span><span>Esc fermer</span></div></div>`;
_input = _overlay.querySelector('.cp-input');
_browseFilterEl = _overlay.querySelector('#cp-browse-filter');
_results = _overlay.querySelector('.cp-results');
_overlay.addEventListener('click', (e) => { if (e.target === _overlay) close(); });
_input.addEventListener('keydown', onKeyDown);
_input.addEventListener('input', onInput);
if (_browseFilterEl) {
_browseFilterEl.addEventListener('input', onBrowseFilterInput);
_browseFilterEl.addEventListener('keydown', onBrowseFilterKey);
}
_overlay.addEventListener('keydown', onOverlayKeyDown);
document.body.appendChild(_overlay);
}
export function open(q = '') {
createDOM();
_overlay.classList.add('active');
_input.value = q;
_mode = q.startsWith('>') ? 'commands' : 'files';
_input.disabled = false; _input.style.display = '';
_sel = 0; _items = []; _results.innerHTML = '';
_input.focus();
onInput();
}
export function close() {
if (!_overlay) return;
_overlay.classList.remove('active');
if (_to) clearTimeout(_to); _to = null;
if (_browseFilterTimer) clearTimeout(_browseFilterTimer);
_mode = 'files';
_browseFilter = '';
_allItems = [];
cleanupNameInput();
if (_browseFilterEl) { _browseFilterEl.style.display = 'none'; _browseFilterEl.value = ''; }
const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
const si = document.querySelector('.search-input'); if (si) si.focus();
}
function isOpen() { return _overlay && _overlay.classList.contains('active'); }
function switchMode(m) {
_mode = m;
if (m === 'browse' || m === 'browse-vault') {
_input.style.display = 'none'; _input.disabled = true;
_overlay.focus();
if (m === 'browse' && _browseFilterEl) {
_browseFilterEl.style.display = '';
_browseFilterEl.value = _browseFilter;
_browseFilterEl.focus();
} else if (_browseFilterEl) {
_browseFilterEl.style.display = 'none';
}
} else {
_input.style.display = ''; _input.disabled = false;
_input.focus();
if (_browseFilterEl) _browseFilterEl.style.display = 'none';
const bc = document.getElementById('cp-breadcrumb'); if (bc) bc.style.display = 'none';
}
}
function onInput() {
if (_mode === 'browse' || _mode === 'browse-vault') return;
const v = _input.value;
const nm = v.startsWith('>') ? 'commands' : 'files';
if (nm !== _mode) { _mode = nm; _input.placeholder = _mode==='commands'?'Tapez une commande...':'Rechercher un fichier... (+inclure -exclure)'; }
if (_to) clearTimeout(_to);
_to = setTimeout(() => { _to = null; if (_mode==='commands') searchCmd(v.slice(1).trim()); else searchFiles(v.trim()); }, 150);
}
async function searchFiles(rawQ) {
if (!rawQ) {
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Naviguer dans les vaults pour trouver un fichier', act:()=>startBrowse('open-file') }];
render();
return;
}
const { positive, negative } = parseQuery(rawQ);
try {
const q = positive.join(' ');
const d = await api(`/api/suggest?q=${encodeURIComponent(q)}&vault=all`);
let sug = d.suggestions || [];
// Client-side negative filter
if (negative.length > 0) {
sug = sug.filter(s => {
const fullPath = (s.vault+'/'+s.path).toLowerCase();
for (const n of negative) {
if (fullPath.includes(n)) return false;
}
return true;
});
}
if (sug.length === 0) {
_items = [{ type:'_browse', label:'📂 Parcourir les répertoires...', sublabel:'Aucun résultat — naviguer manuellement', act:()=>startBrowse('open-file') }];
} else {
_items = sug.map(s => ({ type:'file', vault:s.vault, path:s.path, label:s.title||s.path.split('/').pop(), sublabel:`${s.vault}/${s.path}` }));
}
render();
} catch { _results.innerHTML = '<div class="cp-empty cp-error">Erreur</div>'; }
}
function searchCmd(q) {
if (!q) { _items = getCMDS().map(c => ({ type:'cmd', ...c })); render(); return; }
const lq = q.toLowerCase();
_items = getCMDS().filter(c => c.label.toLowerCase().includes(lq)||c.desc.toLowerCase().includes(lq)||(c.cat&&c.cat.toLowerCase().includes(lq))).map(c => ({ type:'cmd', ...c }));
render();
}
function render() {
_sel = 0;
if (!_items.length) { _results.innerHTML = '<div class="cp-empty">Aucun résultat</div>'; return; }
_results.innerHTML = _items.map((it, i) => `<div class="cp-item ${i===0?'cp-selected':''}" data-idx="${i}"><div class="cp-item-label">${esc(it.label)}</div><div class="cp-item-sublabel">${esc(it.sublabel||'')}</div></div>`).join('');
_results.querySelectorAll('.cp-item').forEach(el => {
el.addEventListener('click', () => exec(parseInt(el.dataset.idx)));
el.addEventListener('mouseenter', () => { document.querySelectorAll('.cp-item').forEach(e=>e.classList.remove('cp-selected')); el.classList.add('cp-selected'); _sel = parseInt(el.dataset.idx); });
});
scroll();
}
function onKeyDown(e) {
const items = _results.querySelectorAll('.cp-item');
if (_mode === 'browse' || _mode === 'browse-vault') {
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'Backspace') {
e.preventDefault();
if (_mode === 'browse-vault') { _mode='commands'; _input.style.display=''; _input.disabled=false; _input.focus(); searchCmd(''); return; }
const segs = _browse.path ? _browse.path.split('/') : [];
if (segs.length > 0) { _browse.path = segs.slice(0,-1).join('/'); renderBrowse(); }
else if (_browse.vault) { switchMode('browse-vault'); renderVaultPicker(); }
}
else if (e.key === 'Enter') { e.preventDefault(); exec(_sel); }
else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = Math.min(_sel+1, _items.length-1); updSel(items); }
else if (e.key === 'ArrowUp') { e.preventDefault(); _sel = Math.max(_sel-1, 0); updSel(items); }
return;
}
if (e.key === 'Escape') { e.preventDefault(); close(); }
else if (e.key === 'Enter') { e.preventDefault(); exec(_sel); }
else if (e.key === 'ArrowDown') { e.preventDefault(); _sel = Math.min(_sel+1, _items.length-1); updSel(items); }
else if (e.key === 'ArrowUp') { e.preventDefault(); _sel = Math.max(_sel-1, 0); updSel(items); }
else if (e.key === 'Tab') { e.preventDefault(); if (_mode==='files') { _input.value='> '; setTimeout(()=>_input.setSelectionRange(2,2),0); } else _input.value=''; onInput(); }
}
function updSel(items) { items.forEach((el,i)=>el.classList.toggle('cp-selected',i===_sel)); scroll(); }
function scroll() { const s = _results.querySelector('.cp-selected'); if (s) s.scrollIntoView({block:'nearest'}); }
// ── Overlay keydown handler (for browse modes where input is hidden) ──
function onOverlayKeyDown(e) {
if (_mode !== 'browse' && _mode !== 'browse-vault') return;
if (document.getElementById('cp-name-input')) return;
if (document.activeElement === _browseFilterEl) return;
onKeyDown(e);
}
function exec(idx) {
const it = _items[idx]; if (!it) return;
if (it.type === 'file') {
close();
if (window.innerWidth <= 768) {
TabManager.openPersistent(it.vault, it.path);
} else {
TabManager.openPreview(it.vault, it.path);
}
}
else if (it.type === 'cmd' || it.type === '_browse') { it.act(); }
else if (it.type === '_vault') { _browse.vault = it.vault; switchMode('browse'); renderBrowse(); }
else if (it.type === '_nav') {
// Check if this is a global search result with vault info
if (it.vault && it.vault !== _browse.vault) { _browse.vault = it.vault; }
_browse.path = it.go;
_browseFilter='';
if(_browseFilterEl) _browseFilterEl.value='';
renderBrowse();
}
else if (it.type === '_action' || it.type === '_file') { it.act(); }
}
function esc(s) { if (!s) return ''; return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/\"/g,'&quot;'); }
export function initCommandPalette() {
document.addEventListener('keydown', (e) => {
if ((e.ctrlKey||e.metaKey) && e.shiftKey && !e.altKey && (e.key===' '||e.code==='Space')) { e.preventDefault(); if (isOpen()) close(); else open(''); }
if ((e.ctrlKey||e.metaKey) && e.altKey && !e.shiftKey && (e.key===' '||e.code==='Space')) { e.preventDefault(); if (isOpen()) close(); else open('> '); }
});
}
-263
View File
@@ -1,263 +0,0 @@
/**
* PaneManager — Multi-pane split view for ObsiGate (v2.0.0-dev)
* Supports 1–4 panes in CSS grid, split right/down, resize, drag tabs between panes.
* Integrates with existing TabManager singleton.
*/
const PANE_MAX = 4;
const PaneManager = {
panes: [],
activePaneId: 0,
_paneCounter: 1,
layout: 'horizontal',
init() {
const saved = localStorage.getItem('obsigate-panes');
if (saved) {
try {
const data = JSON.parse(saved);
if (data.panes && data.panes.length >= 2) {
this.layout = data.layout || 'horizontal';
this._paneCounter = data.panes.length;
this._buildGrid(data.panes.length);
data.panes.forEach((p, i) => {
this.panes[i].activeTab = p.activeTab;
this.panes[i].width = p.width;
});
this.activePaneId = data.panes[0].id;
this._applyResize();
return;
}
} catch (e) { /* ignore */ }
}
},
_buildGrid(n) {
const wrapper = document.querySelector('.content-wrapper');
const origTabBar = document.getElementById('tab-bar');
const origContent = document.getElementById('content-area');
if (!wrapper || !origContent || n < 2) return;
// Create grid container
const grid = document.createElement('div');
grid.className = 'pane-grid';
if (n === 2) grid.style.gridTemplateColumns = this.layout === 'horizontal' ? '1fr 4px 1fr' : '1fr';
if (n >= 3) grid.style.gridTemplateColumns = '1fr 4px 1fr';
if (n === 4) grid.style.gridTemplateRows = '1fr 4px 1fr';
wrapper.innerHTML = '';
wrapper.appendChild(grid);
for (let i = 0; i < Math.min(n, PANE_MAX); i++) {
const pane = this._createPane(i, grid, i === 0 ? origTabBar : null, i === 0 ? origContent : null);
this.panes.push(pane);
if (i < n - 1) {
const handle = document.createElement('div');
handle.className = 'pane-resize-handle';
if (this.layout === 'horizontal') handle.style.cursor = 'col-resize';
else handle.style.cursor = 'row-resize';
handle.addEventListener('mousedown', (e) => this._onResizeStart(e, i, i + 1));
handle.addEventListener('dblclick', () => this._resetResize(i, i + 1));
grid.appendChild(handle);
}
}
this.activePaneId = 0;
this._highlightActive();
},
_createPane(idx, grid, reuseTabBar, reuseContent) {
const container = document.createElement('div');
container.className = 'pane-container';
container.dataset.pane = idx;
if (reuseTabBar && reuseContent) {
// Pane 0 reuses existing DOM
container.appendChild(reuseTabBar);
container.appendChild(reuseContent);
reuseContent.id = 'content-area-0';
} else {
const tabBar = document.createElement('div');
tabBar.className = 'tab-bar pane-tab-bar';
tabBar.id = 'tab-bar-' + idx;
tabBar.innerHTML = '<div class="tab-list" id="tab-list-' + idx + '"></div>';
const content = document.createElement('main');
content.className = 'content-area pane-content';
content.id = 'content-area-' + idx;
container.appendChild(tabBar);
container.appendChild(content);
}
container.addEventListener('click', () => {
this.activePaneId = idx;
this._highlightActive();
});
grid.appendChild(container);
return { id: idx, element: container, width: 100 / this.panes.length + '%' };
},
_highlightActive() {
this.panes.forEach((p, i) => {
p.element.classList.toggle('pane-active', i === this.activePaneId);
});
},
splitRight() {
if (this.panes.length >= PANE_MAX) return;
if (this.panes.length === 0) {
// First split: promote single pane to grid
this._buildGrid(2);
} else {
this._addPane();
}
this._persist();
},
splitDown() {
if (this.panes.length >= PANE_MAX) return;
this.layout = 'vertical';
if (this.panes.length === 0) {
this._buildGrid(2);
} else {
this._addPane();
}
this._persist();
},
_addPane() {
const grid = document.querySelector('.pane-grid');
if (!grid) return;
const n = this.panes.length + 1;
const handle = document.createElement('div');
handle.className = 'pane-resize-handle';
handle.addEventListener('mousedown', (e) => this._onResizeStart(e, n - 2, n - 1));
grid.appendChild(handle);
const pane = this._createPane(n - 1, grid, null, null);
this.panes.push(pane);
this.activePaneId = n - 1;
this._highlightActive();
if (n <= 2) grid.style.gridTemplateColumns = '1fr 4px 1fr';
else if (n === 4) grid.style.gridTemplateRows = '1fr 4px 1fr';
},
closePane(paneId) {
if (this.panes.length <= 1) return;
const idx = this.panes.findIndex(p => p.id === paneId);
if (idx < 0) return;
// Remove pane and its resize handles
this.panes[idx].element.remove();
const handles = document.querySelectorAll('.pane-resize-handle');
if (handles[idx]) handles[idx].remove();
else if (handles[idx - 1]) handles[idx - 1].remove();
this.panes.splice(idx, 1);
if (this.panes.length <= 1) {
this._collapseToSingle();
} else {
this.activePaneId = Math.min(this.activePaneId, this.panes.length - 1);
this._highlightActive();
}
this._persist();
},
_collapseToSingle() {
const grid = document.querySelector('.pane-grid');
if (!grid) return;
const wrapper = grid.parentElement;
const pane0 = this.panes[0];
wrapper.innerHTML = '';
const tabBar = pane0.element.querySelector('.tab-bar') || document.getElementById('tab-bar-0');
const content = pane0.element.querySelector('.content-area') || document.getElementById('content-area-0');
if (tabBar) wrapper.appendChild(tabBar);
if (content) wrapper.appendChild(content);
this.panes = [];
this.activePaneId = 0;
},
_onResizeStart(e, leftIdx, rightIdx) {
e.preventDefault();
const grid = document.querySelector('.pane-grid');
const startX = e.clientX;
const startY = e.clientY;
const totalSize = this.layout === 'horizontal' ? grid.offsetWidth : grid.offsetHeight;
const onMove = (ev) => {
const delta = this.layout === 'horizontal' ? ev.clientX - startX : ev.clientY - startY;
const pct = (delta / totalSize) * 100;
const leftEl = this.panes[leftIdx].element;
const rightEl = this.panes[rightIdx].element;
const curLeft = parseFloat(leftEl.style.flexBasis || leftEl.style.width || '50');
const curRight = parseFloat(rightEl.style.flexBasis || rightEl.style.width || '50');
const newLeft = Math.max(20, Math.min(80, curLeft + pct));
const newRight = 100 - newLeft;
leftEl.style.flexBasis = newLeft + '%';
rightEl.style.flexBasis = newRight + '%';
this.panes[leftIdx].width = newLeft + '%';
this.panes[rightIdx].width = newRight + '%';
};
const onUp = () => {
document.removeEventListener('mousemove', onMove);
document.removeEventListener('mouseup', onUp);
this._persist();
};
document.addEventListener('mousemove', onMove);
document.addEventListener('mouseup', onUp);
},
_resetResize(leftIdx, rightIdx) {
this.panes[leftIdx].element.style.flexBasis = '50%';
this.panes[rightIdx].element.style.flexBasis = '50%';
this._persist();
},
_applyResize() {
this.panes.forEach(p => {
if (p.width) p.element.style.flexBasis = p.width;
});
},
_persist() {
if (this.panes.length <= 1) {
localStorage.removeItem('obsigate-panes');
return;
}
const data = {
layout: this.layout,
panes: this.panes.map(p => ({ id: p.id, activeTab: p.activeTab, width: p.width })),
};
localStorage.setItem('obsigate-panes', JSON.stringify(data));
},
getActiveContentArea() {
if (this.panes.length < 2) return document.getElementById('content-area');
return document.getElementById('content-area-' + this.activePaneId) || document.getElementById('content-area-0');
},
isSplit() {
return this.panes.length >= 2;
},
};
// Export and auto-init
if (typeof window !== 'undefined') {
window.PaneManager = PaneManager;
document.addEventListener('DOMContentLoaded', () => {
// Register keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.altKey && !e.shiftKey && e.key === '\\') {
e.preventDefault();
PaneManager.splitRight();
}
if (e.ctrlKey && e.altKey && e.shiftKey && e.key === '\\') {
e.preventDefault();
PaneManager.splitDown();
}
});
// Initialize if saved state exists
setTimeout(() => PaneManager.init(), 500);
});
}
export default PaneManager;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-59
View File
@@ -1,59 +0,0 @@
/* ObsiGate — Shared mutable application state.
Use state.xxx to read/write any value. ES module imports are read-only,
so we export a single mutable object instead of individual let bindings. */
export const state = {
APP_VERSION: "1.5.0",
// Core navigation
currentVault: null,
currentPath: null,
allVaults: [],
selectedContextVault: "all",
// Search
searchTimeout: null,
searchAbortController: null,
advancedSearchOffset: 0,
advancedSearchTotal: 0,
advancedSearchSort: "relevance",
advancedSearchLastQuery: "",
suggestAbortController: null,
dropdownActiveIndex: -1,
dropdownItems: [],
currentSearchId: 0,
selectedTags: [],
searchCaseSensitive: false,
searchWholeWord: false,
searchRegex: false,
searchFilterVisible: false,
// Search constants
SEARCH_HISTORY_KEY: "obsigate_search_history",
MAX_HISTORY_ENTRIES: 50,
SUGGEST_DEBOUNCE_MS: 150,
ADVANCED_SEARCH_LIMIT: 50,
MIN_SEARCH_LENGTH: 2,
SEARCH_TIMEOUT_MS: 30000,
// Viewer state
showingSource: false,
cachedRawSource: null,
editorView: null,
editorVault: null,
editorPath: null,
fallbackEditorEl: null,
_iconDebounceTimer: null,
// Outline/TOC
outlineObserver: null,
activeHeadingId: null,
headingsCache: [],
rightSidebarVisible: true,
rightSidebarWidth: 280,
// Sidebar
sidebarFilterCaseSensitive: false,
activeSidebarTab: "vaults",
filterDebounce: null,
vaultSettings: {},
};
-643
View File
@@ -1,643 +0,0 @@
/* ObsiGate — Sync: SSE client + PWA registration + Offline mode */
import { state } from './state.js';
import { t } from './i18n.js';
import { showToast } from './ui.js';
import { api, AuthManager } from './auth.js';
import { loadVaults, loadTags, refreshTagsForContext, refreshSidebarTreePreservingState } from './sidebar.js';
import { loadRecentFiles, initAboutModal } from './config.js';
import { openFile } from './viewer.js';
import { initOffline, syncFileIndexFromServer } from './offline.js';
import { initThemes } from './themes.js';
// ---------------------------------------------------------------------------
// SSE Client — IndexUpdateManager
// ---------------------------------------------------------------------------
export const IndexUpdateManager = (() => {
let eventSource = null;
let reconnectTimer = null;
let reconnectDelay = 1000;
const MAX_RECONNECT_DELAY = 30000;
let recentEvents = [];
const MAX_RECENT_EVENTS = 20;
let connectionState = "disconnected"; // disconnected | connecting | connected
function connect() {
if (eventSource) {
eventSource.close();
}
connectionState = "connecting";
_updateBadge();
eventSource = new EventSource("/api/events");
eventSource.addEventListener("connected", (e) => {
connectionState = "connected";
reconnectDelay = 1000;
_updateBadge();
});
eventSource.addEventListener("index_updated", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_updated", data);
_onIndexUpdated(data);
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_reloaded", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_reloaded", data);
_onIndexReloaded(data);
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("vault_added", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("vault_added", data);
showToast(t('sync.vault_added', { vault: data.vault, count: data.stats.file_count }), "info");
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("vault_removed", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("vault_removed", data);
showToast(t('sync.vault_removed', { vault: data.vault }), "info");
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_start", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_start", data);
connectionState = "syncing";
_updateBadge();
showToast(t('sync.index_started', { count: data.total_vaults }), "info");
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_progress", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_progress", data);
connectionState = "syncing";
_updateBadge();
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.addEventListener("index_complete", (e) => {
try {
const data = JSON.parse(e.data);
_addEvent("index_complete", data);
connectionState = "connected";
_updateBadge();
showToast(t('sync.index_complete', { count: data.total_files }), "success");
loadVaults();
loadTags();
} catch (err) {
console.error("SSE parse error:", err);
}
});
eventSource.onerror = () => {
connectionState = "disconnected";
_updateBadge();
eventSource.close();
eventSource = null;
_scheduleReconnect();
};
}
function _scheduleReconnect() {
if (reconnectTimer) clearTimeout(reconnectTimer);
reconnectTimer = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
connect();
}, reconnectDelay);
}
function _addEvent(type, data) {
recentEvents.unshift({
type,
data,
timestamp: new Date().toISOString(),
});
if (recentEvents.length > MAX_RECENT_EVENTS) {
recentEvents = recentEvents.slice(0, MAX_RECENT_EVENTS);
}
}
async function _onIndexUpdated(data) {
// Brief syncing state
connectionState = "syncing";
_updateBadge();
const n = data.total_changes || 0;
const vaults = (data.vaults || []).join(", ");
// Toast removed: silent auto-indexing — no notification needed
// Refresh sidebar and tags if affected vault matches current context
const affectsCurrentVault = state.selectedContextVault === "all" || (data.vaults || []).includes(state.selectedContextVault);
if (affectsCurrentVault) {
try {
await Promise.all([refreshSidebarTreePreservingState(), refreshTagsForContext()]);
// Refresh current file if it was updated
if (state.currentVault && state.currentPath) {
const changed = (data.changes || []).some((c) => c.vault === state.currentVault && c.path === state.currentPath);
if (changed) {
openFile(state.currentVault, state.currentPath);
}
}
} catch (err) {
console.error("Error refreshing after index update:", err);
}
}
// Refresh recent tab if it is active
if (state.activeSidebarTab === "recent") {
const vaultFilter = document.getElementById("recent-vault-filter");
loadRecentFiles(vaultFilter ? vaultFilter.value || null : null);
}
setTimeout(() => {
connectionState = "connected";
_updateBadge();
}, 1500);
}
async function _onIndexReloaded(data) {
connectionState = "syncing";
_updateBadge();
showToast(t("sync.index_reloaded"), "info");
try {
await Promise.all([loadVaults(), loadTags()]);
} catch (err) {
console.error("Error refreshing after full reload:", err);
}
setTimeout(() => {
connectionState = "connected";
_updateBadge();
}, 1500);
}
function _updateBadge() {
const badge = document.getElementById("sync-badge");
if (!badge) return;
badge.className = "sync-badge sync-badge--" + connectionState;
const labels = {
disconnected: t('sync.disconnected'),
connecting: "Connexion...",
connected: t('sync.connected'),
syncing: t('sync.updating'),
};
badge.title = labels[connectionState] || connectionState;
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
connectionState = "disconnected";
_updateBadge();
}
function getState() {
return connectionState;
}
function getRecentEvents() {
return recentEvents;
}
return { connect, disconnect, getState, getRecentEvents };
})();
// ---------------------------------------------------------------------------
// Sync status badge and panel
// ---------------------------------------------------------------------------
function initSyncStatus() {
const badge = document.getElementById("sync-badge");
if (!badge) return;
badge.addEventListener("click", (e) => {
e.stopPropagation();
toggleSyncPanel();
});
IndexUpdateManager.connect();
// Initialize AI status tracking
_initAIStatus();
}
function toggleSyncPanel() {
let panel = document.getElementById("sync-panel");
if (panel) {
panel.remove();
return;
}
// Auto reconnect if disconnected when user opens the panel
if (IndexUpdateManager.getState() === "disconnected") {
IndexUpdateManager.connect();
}
panel = document.createElement("div");
panel.id = "sync-panel";
panel.className = "sync-panel";
_renderSyncPanel(panel);
document.body.appendChild(panel);
// Close on outside click
setTimeout(() => {
document.addEventListener("click", _closeSyncPanelOutside, { once: true });
}, 0);
}
function _closeSyncPanelOutside(e) {
const panel = document.getElementById("sync-panel");
if (panel && !panel.contains(e.target) && e.target.id !== "sync-badge") {
panel.remove();
}
}
function _renderSyncPanel(panel) {
const state = IndexUpdateManager.getState();
const events = IndexUpdateManager.getRecentEvents();
const stateLabels = {
disconnected: t('sync.disconnected'),
connecting: "Connexion...",
connected: t('sync.connected_short'),
syncing: "Synchronisation...",
};
let html = `<div class="sync-panel__header">
<span class="sync-panel__title">Synchronisation</span>
<span class="sync-panel__state sync-panel__state--${state}">${stateLabels[state] || state}</span>
</div>`;
if (events.length === 0) {
html += `<div class="sync-panel__empty">${t('sync.no_recent_events')}</div>`;
} else {
html += `<div class="sync-panel__events">`;
events.slice(0, 10).forEach((ev) => {
const time = new Date(ev.timestamp).toLocaleTimeString();
const typeLabels = {
index_updated: t('sync.event_index_updated'),
index_reloaded: "Rechargement",
vault_added: t('sync.event_vault_added'),
vault_removed: t('sync.event_vault_removed'),
index_start: t('sync.event_index_start'),
index_progress: t('sync.event_index_progress'),
index_complete: "Indexation tech.",
};
const label = typeLabels[ev.type] || ev.type;
let detail = ev.data.vaults ? ev.data.vaults.join(", ") : ev.data.vault || "";
if (ev.type === "index_start") detail = `${ev.data.total_vaults} vaults à traiter`;
if (ev.type === "index_progress") detail = t("sync.vault_files", {vault: ev.data.vault, files: ev.data.files});
if (ev.type === "index_complete" && ev.data.total_files !== undefined) detail = `${ev.data.total_files} fichiers total`;
html += `<div class="sync-panel__event">
<span class="sync-panel__event-type">${label}</span>
<span class="sync-panel__event-detail">${detail}</span>
<span class="sync-panel__event-time">${time}</span>
</div>`;
});
html += `</div>`;
}
panel.innerHTML = html;
// ── AI Status section ──
_renderAIStatusSection(panel);
}
// ---------------------------------------------------------------------------
// AI Status Indicator (inside sync panel + header badge animation)
// ---------------------------------------------------------------------------
let _aiProcessing = false;
let _aiConfigured = null; // null = unknown, true/false
let _aiAutocomplete = null; // null = unknown, {available, server_ok, model_loaded, model, error}
/** Check if AI is configured by calling the /api/ai/status endpoint.
* Updates _aiConfigured, _aiAutocomplete and refreshes the panel if open. */
async function _checkAIStatus() {
try {
const status = await api('/api/ai/status');
_aiConfigured = status.configured === true;
_aiAutocomplete = status.autocomplete || null;
} catch {
_aiConfigured = false;
_aiAutocomplete = null;
}
_updateAIStatusInPanel();
}
function _renderAIStatusSection(panel) {
const section = document.createElement('div');
section.className = 'sync-panel__ai-section';
section.id = 'sync-panel-ai';
// ---- External provider status ----
let extIcon, extText, extClass;
if (_aiProcessing) {
extIcon = 'loader';
extText = t('sync.processing');
extClass = 'sync-panel__ai--processing';
} else if (_aiConfigured === true) {
extIcon = 'check-circle';
extText = 'Fournisseur externe actif';
extClass = 'sync-panel__ai--active';
} else if (_aiConfigured === false) {
extIcon = 'x-circle';
extText = t('sync.no_provider');
extClass = 'sync-panel__ai--inactive';
} else {
extIcon = 'help-circle';
extText = t('sync.verifying');
extClass = 'sync-panel__ai--unknown';
}
// ---- Autocomplete (Ollama) status ----
let acIcon, acText, acClass;
if (_aiAutocomplete && _aiAutocomplete.available) {
acIcon = 'check-circle';
acText = t('sync.autocomplete_active');
acClass = 'sync-panel__ai--active';
} else if (_aiAutocomplete && _aiAutocomplete.server_ok && !_aiAutocomplete.model_loaded) {
acIcon = 'alert-triangle';
acText = t('sync.model_not_loaded');
acClass = 'sync-panel__ai--inactive';
} else if (_aiAutocomplete && _aiAutocomplete.error) {
acIcon = 'x-circle';
acText = 'Serveur Ollama injoignable';
acClass = 'sync-panel__ai--inactive';
} else if (_aiAutocomplete === null) {
acIcon = 'help-circle';
acText = t('sync.verifying');
acClass = 'sync-panel__ai--unknown';
} else {
acIcon = 'x-circle';
acText = t('sync.not_configured');
acClass = 'sync-panel__ai--inactive';
}
section.innerHTML = '' +
'<div class="sync-panel__ai-title">Intelligence Artificielle</div>' +
'<div class="sync-panel__ai-row">' +
'<span class="sync-panel__ai-label">Fournisseur</span>' +
'<span class="sync-panel__ai-state ' + extClass + '" data-lucide-icon="' + extIcon + '">' +
'<i data-lucide="' + extIcon + '" style="width:14px;height:14px"></i>' +
extText +
'</span>' +
'</div>' +
'<div class="sync-panel__ai-row">' +
'<span class="sync-panel__ai-label">' + t('sync.autocomplete') + '</span>' +
'<span class="sync-panel__ai-state ' + acClass + '" data-lucide-icon="' + acIcon + '">' +
'<i data-lucide="' + acIcon + '" style="width:14px;height:14px"></i>' +
acText +
'</span>' +
'</div>';
panel.appendChild(section);
// Trigger icon refresh via global
if (window._obsigateRefreshIcons) window._obsigateRefreshIcons();
}
/** Update the AI section in the open sync panel. */
function _updateAIStatusInPanel() {
const section = document.getElementById('sync-panel-ai');
if (!section) return;
section.innerHTML = '';
const panel = document.getElementById('sync-panel');
if (panel) {
// Re-render just the AI section
const oldSection = panel.querySelector('.sync-panel__ai-section');
if (oldSection) oldSection.remove();
_renderAIStatusSection(panel);
}
}
/** Flash the sync badge white during AI processing.
* NOTE: now handled via CSS class on #sync-badge.
* The visual flash in the editor itself is handled by ai.js
* which adds a processing indicator inline. */
function _setBadgeAIProcessing(active) {
const badge = document.getElementById('sync-badge');
if (!badge) return;
if (active) {
badge.classList.add('sync-badge--ai-processing');
} else {
badge.classList.remove('sync-badge--ai-processing');
}
}
/** Initialize AI status tracking. */
function _initAIStatus() {
// Check AI status immediately at startup
_checkAIStatus();
// Listen for AI processing events from ai.js
document.addEventListener('ai-processing-start', () => {
_aiProcessing = true;
_setBadgeAIProcessing(true);
_updateAIStatusInPanel();
});
document.addEventListener('ai-processing-end', () => {
_aiProcessing = false;
_setBadgeAIProcessing(false);
_updateAIStatusInPanel();
});
// Re-check AI status when sync panel opens (in case config changed)
const badge = document.getElementById('sync-badge');
if (badge) {
badge.addEventListener('click', () => {
_checkAIStatus();
}, { once: false });
}
}
// ---------------------------------------------------------------------------
// PWA Service Worker Registration
// ---------------------------------------------------------------------------
function registerServiceWorker() {
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {
navigator.serviceWorker
.register("/sw.js")
.then((registration) => {
console.log("[PWA] Service Worker registered successfully:", registration.scope);
// Check for updates periodically
setInterval(() => {
registration.update();
}, 60000); // Check every minute
// Handle service worker updates
registration.addEventListener("updatefound", () => {
const newWorker = registration.installing;
newWorker.addEventListener("statechange", () => {
if (newWorker.state === "installed" && navigator.serviceWorker.controller) {
// New service worker available
showUpdateNotification();
}
});
});
})
.catch((error) => {
console.log("[PWA] Service Worker registration failed:", error);
});
});
}
}
function showUpdateNotification() {
const message = document.createElement("div");
message.className = "pwa-update-notification";
message.innerHTML = `
<div class="pwa-update-content">
<span>Une nouvelle version d'ObsiGate est disponible !</span>
<button class="pwa-update-btn" onclick="window.location.reload()">Mettre à jour</button>
<button class="pwa-update-dismiss" onclick="this.parentElement.parentElement.remove()">×</button>
</div>
`;
document.body.appendChild(message);
// Auto-dismiss after 30 seconds
setTimeout(() => {
if (message.parentElement) {
message.remove();
}
}, 30000);
}
// Handle install prompt
let deferredPrompt;
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault();
deferredPrompt = e;
// Show install button if desired
const installBtn = document.getElementById("pwa-install-btn");
if (installBtn) {
installBtn.style.display = "block";
installBtn.addEventListener("click", async () => {
if (deferredPrompt) {
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
console.log(`[PWA] User response to install prompt: ${outcome}`);
deferredPrompt = null;
installBtn.style.display = "none";
}
});
}
});
// Log when app is installed
window.addEventListener("appinstalled", () => {
console.log("[PWA] ObsiGate has been installed");
showToast(t("sync.install_success"), "success");
});
// ── Dashboard tab switching (runs on page load and after rebuild) ──
function initDashboardTabs() {
document.querySelectorAll(".dashboard-tab").forEach(tab => {
// Remove existing listeners by cloning
const newTab = tab.cloneNode(true);
tab.parentNode.replaceChild(newTab, tab);
newTab.addEventListener("click", function() {
document.querySelectorAll(".dashboard-tab").forEach(t => t.classList.remove("active"));
document.querySelectorAll(".dashboard-panel").forEach(p => p.classList.remove("active"));
this.classList.add("active");
const panel = document.getElementById("dashboard-panel-" + this.dataset.tab);
if (panel) panel.classList.add("active");
});
});
}
// ---------------------------------------------------------------------------
// Init — called by app.js orchestrator
// ---------------------------------------------------------------------------
export { initSyncStatus, initDashboardTabs };
export function init() {
registerServiceWorker();
initOffline();
initThemes();
initAboutModal();
// Sync file index after a short delay (let vaults load first).
// Gate on auth at execution time (not scheduling time) so that
// initAuth() has a chance to complete and set _authEnabled / token.
// The login handler triggers syncFileIndexFromServer explicitly on
// fresh login, so we only do it here for already-authenticated or
// auth-disabled sessions.
setTimeout(() => {
if (!AuthManager._authEnabled || AuthManager.getToken()) {
syncFileIndexFromServer();
}
}, 3000);
initDashboardTabs();
// Listen for Forge editor rename events
window.addEventListener('message', function(e) {
if (!e.data || !e.data.type) return;
if (e.data.type === 'forge-renamed') {
var vault = e.data.vault;
var oldPath = e.data.oldPath;
var newPath = e.data.newPath;
console.log('[Forge] File renamed:', vault, oldPath, '->', newPath);
var titleEl = document.getElementById('editor-title');
if (titleEl && titleEl.textContent.indexOf('Forge:') === 0) {
titleEl.textContent = 'Forge: ' + newPath.split('/').pop();
}
refreshSidebarTreePreservingState(vault);
if (state.currentVault === vault && state.currentPath === oldPath) {
import('./viewer.js').then(function(m) { m.openFile(vault, newPath); });
}
}
if (e.data.type === 'forge-close') {
var modal = document.getElementById('editor-modal');
var forgeFrame = document.getElementById('forge-iframe');
if (forgeFrame) forgeFrame.remove();
if (modal) modal.classList.remove('active');
// Restore brand text
var brand = modal.querySelector('.editor-brand');
if (brand) brand.textContent = 'ObsiGate';
// Restore header/footer
var headerEl = modal.querySelector('.editor-header');
var footerEl = modal.querySelector('.editor-footer');
if (headerEl) headerEl.style.display = '';
if (footerEl) footerEl.style.display = '';
// Refresh current file content
if (state.currentVault && state.currentPath) {
import('./viewer.js').then(function(m) { m.openFile(state.currentVault, state.currentPath); });
}
}
});
}
-566
View File
@@ -1,566 +0,0 @@
/* ObsiGate — Theme System
* 15 themes x 2 modes (dark/light)
* Persisted in localStorage as 'obsigate-theme'
* Usage: import { initThemes, applyTheme, getCurrentTheme, THEMES } from './themes.js'
*/
// ── Theme definitions ────────────────────────────────────────────────
// Each theme has 'dark' and 'light' variants.
// CSS variables are applied to :root via setProperty.
var THEMES = {
'defaut-obsigate': {
name: 'Defaut ObsiGate',
desc: 'Theme original',
modes: {
dark: {
'--bg-primary': '#0f1117', '--bg-secondary': '#161b22', '--bg-sidebar': '#13161d',
'--bg-hover': '#1f2430', '--border': '#21262d', '--text-primary': '#e6edf3',
'--text-secondary': '#8b949e', '--text-muted': '#484f58', '--accent': '#58a6ff',
'--accent-green': '#3fb950', '--tag-bg': '#1f2a3a', '--tag-text': '#79c0ff',
'--code-bg': '#161b22', '--search-bg': '#21262d', '--scrollbar': '#30363d',
'--resize-handle': '#30363d', '--overlay-bg': 'rgba(0,0,0,0.5)',
'--danger': '#ff7b72', '--danger-bg': '#3d1a18', '--success': '#3fb950', '--success-bg': '#1a3d1f',
'--accent-card': '#58a6ff', '--accent-bg': '#1f2a3a', '--accent-text': '#79c0ff',
'--green': '#3fb950', '--green-bg': '#1a3d1f', '--purple': '#a371f7', '--purple-bg': '#2d1b4e',
'--surface': '#161b22', '--surface2': '#0f1117', '--surface3': '#0d0f14',
'--border-md': '#30363d', '--text': '#e6edf3', '--text-2': '#8b949e', '--text-3': '#484f58',
'--ok': '#34d399', '--warn': '#fbbf24', '--ai': '#8b5cf6', '--err': '#f87171'
},
light: {
'--bg-primary': '#ffffff', '--bg-secondary': '#f6f8fa', '--bg-sidebar': '#f0f2f5',
'--bg-hover': '#e8ecf0', '--border': '#d0d7de', '--text-primary': '#1f2328',
'--text-secondary': '#57606a', '--text-muted': '#9198a1', '--accent': '#0969da',
'--accent-green': '#1a7f37', '--tag-bg': '#ddf4ff', '--tag-text': '#0969da',
'--code-bg': '#f6f8fa', '--search-bg': '#ffffff', '--scrollbar': '#d0d7de',
'--resize-handle': '#d0d7de', '--overlay-bg': 'rgba(0,0,0,0.3)',
'--danger': '#cf222e', '--danger-bg': '#ffebe9', '--success': '#1a7f37', '--success-bg': '#dafbe1',
'--accent-card': '#0969da', '--accent-bg': '#ddf4ff', '--accent-text': '#0969da',
'--green': '#1a7f37', '--green-bg': '#dafbe1', '--purple': '#8250df', '--purple-bg': '#fbefff',
'--surface': '#f6f8fa', '--surface2': '#ffffff', '--surface3': '#f0f2f5',
'--border-md': '#d0d7de', '--text': '#1f2328', '--text-2': '#57606a', '--text-3': '#9198a1',
'--ok': '#1a7f37', '--warn': '#9a6700', '--ai': '#8250df', '--err': '#cf222e'
}
}
},
'github': {
name: 'GitHub', desc: 'Contraste doux',
modes: {
dark: {
'--bg-primary': '#0d1117', '--bg-secondary': '#161b22', '--bg-sidebar': '#0d1117',
'--bg-hover': '#1c2128', '--border': '#30363d', '--text-primary': '#f0f6fc',
'--text-secondary': '#8b949e', '--text-muted': '#6e7681', '--accent': '#58a6ff',
'--accent-green': '#3fb950', '--tag-bg': '#13233a', '--tag-text': '#58a6ff',
'--code-bg': '#161b22', '--search-bg': '#0d1117', '--scrollbar': '#30363d',
'--resize-handle': '#30363d', '--overlay-bg': 'rgba(0,0,0,0.6)',
'--danger': '#f85149', '--danger-bg': '#490202', '--success': '#3fb950', '--success-bg': '#04260f',
'--accent-card': '#58a6ff', '--accent-bg': '#13233a', '--accent-text': '#58a6ff',
'--green': '#3fb950', '--green-bg': '#04260f', '--purple': '#a371f7', '--purple-bg': '#1c1130',
'--surface': '#161b22', '--surface2': '#0d1117', '--surface3': '#010409',
'--border-md': '#30363d', '--text': '#f0f6fc', '--text-2': '#8b949e', '--text-3': '#6e7681',
'--ok': '#3fb950', '--warn': '#d29922', '--ai': '#a371f7', '--err': '#f85149'
},
light: {
'--bg-primary': '#ffffff', '--bg-secondary': '#f6f8fa', '--bg-sidebar': '#f6f8fa',
'--bg-hover': '#eaeef2', '--border': '#d0d7de', '--text-primary': '#1f2328',
'--text-secondary': '#656d76', '--text-muted': '#8c959f', '--accent': '#0969da',
'--accent-green': '#1a7f37', '--tag-bg': '#ddf4ff', '--tag-text': '#0969da',
'--code-bg': '#f6f8fa', '--search-bg': '#ffffff', '--scrollbar': '#d0d7de',
'--resize-handle': '#d0d7de', '--overlay-bg': 'rgba(0,0,0,0.25)',
'--danger': '#cf222e', '--danger-bg': '#ffebe9', '--success': '#1a7f37', '--success-bg': '#dafbe1',
'--accent-card': '#0969da', '--accent-bg': '#ddf4ff', '--accent-text': '#0969da',
'--green': '#1a7f37', '--green-bg': '#dafbe1', '--purple': '#8250df', '--purple-bg': '#fbefff',
'--surface': '#f6f8fa', '--surface2': '#ffffff', '--surface3': '#ebedf0',
'--border-md': '#d0d7de', '--text': '#1f2328', '--text-2': '#656d76', '--text-3': '#8c959f',
'--ok': '#1a7f37', '--warn': '#9a6700', '--ai': '#8250df', '--err': '#cf222e'
}
}
},
'linear': {
name: 'Linear', desc: 'Dégradés subtils',
modes: {
dark: {
'--bg-primary': '#1a1a2e', '--bg-secondary': '#16162a', '--bg-sidebar': '#12121f',
'--bg-hover': '#222240', '--border': '#2a2a45', '--text-primary': '#e1e1f5',
'--text-secondary': '#8888aa', '--text-muted': '#555577', '--accent': '#5e6ad2',
'--accent-green': '#2ecc71', '--tag-bg': '#222050', '--tag-text': '#8b95e0',
'--code-bg': '#16162a', '--search-bg': '#1e1e38', '--scrollbar': '#2a2a45',
'--resize-handle': '#2a2a45', '--overlay-bg': 'rgba(0,0,0,0.6)',
'--danger': '#e0556a', '--danger-bg': '#361520', '--success': '#2ecc71', '--success-bg': '#0d2818',
'--accent-card': '#5e6ad2', '--accent-bg': '#222050', '--accent-text': '#8b95e0',
'--green': '#2ecc71', '--green-bg': '#0d2818', '--purple': '#9b6bff', '--purple-bg': '#25183d',
'--surface': '#16162a', '--surface2': '#1a1a2e', '--surface3': '#12121f',
'--border-md': '#2a2a45', '--text': '#e1e1f5', '--text-2': '#8888aa', '--text-3': '#555577',
'--ok': '#2ecc71', '--warn': '#f0b429', '--ai': '#9b6bff', '--err': '#e0556a'
},
light: {
'--bg-primary': '#fafafa', '--bg-secondary': '#f0f0f5', '--bg-sidebar': '#e8e8f0',
'--bg-hover': '#e0e0ea', '--border': '#d5d5e0', '--text-primary': '#1a1a2e',
'--text-secondary': '#5c5c7a', '--text-muted': '#9090a8', '--accent': '#5e6ad2',
'--accent-green': '#1a8b4a', '--tag-bg': '#e8e8ff', '--tag-text': '#5e6ad2',
'--code-bg': '#f0f0f5', '--search-bg': '#fafafa', '--scrollbar': '#d5d5e0',
'--resize-handle': '#d5d5e0', '--overlay-bg': 'rgba(0,0,0,0.2)',
'--danger': '#c92a3e', '--danger-bg': '#ffeaea', '--success': '#1a8b4a', '--success-bg': '#e0ffe0',
'--accent-card': '#5e6ad2', '--accent-bg': '#e8e8ff', '--accent-text': '#5e6ad2',
'--green': '#1a8b4a', '--green-bg': '#e0ffe0', '--purple': '#7c5ce0', '--purple-bg': '#efe8ff',
'--surface': '#f0f0f5', '--surface2': '#fafafa', '--surface3': '#e8e8f0',
'--border-md': '#d5d5e0', '--text': '#1a1a2e', '--text-2': '#5c5c7a', '--text-3': '#9090a8',
'--ok': '#1a8b4a', '--warn': '#9a6700', '--ai': '#7c5ce0', '--err': '#c92a3e'
}
}
},
'spotify': {
name: 'Spotify', desc: 'Noir pur & vert',
modes: {
dark: {
'--bg-primary': '#000000', '--bg-secondary': '#121212', '--bg-sidebar': '#000000',
'--bg-hover': '#1a1a1a', '--border': '#282828', '--text-primary': '#ffffff',
'--text-secondary': '#b3b3b3', '--text-muted': '#727272', '--accent': '#1db954',
'--accent-green': '#1db954', '--tag-bg': '#0d3317', '--tag-text': '#1db954',
'--code-bg': '#121212', '--search-bg': '#242424', '--scrollbar': '#333333',
'--resize-handle': '#282828', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#e22134', '--danger-bg': '#2a0a0e', '--success': '#1db954', '--success-bg': '#0d3317',
'--accent-card': '#1db954', '--accent-bg': '#0d3317', '--accent-text': '#1db954',
'--green': '#1db954', '--green-bg': '#0d3317', '--purple': '#af2896', '--purple-bg': '#2a0d33',
'--surface': '#121212', '--surface2': '#000000', '--surface3': '#000000',
'--border-md': '#282828', '--text': '#ffffff', '--text-2': '#b3b3b3', '--text-3': '#727272',
'--ok': '#1db954', '--warn': '#f59b23', '--ai': '#af2896', '--err': '#e22134'
},
light: {
'--bg-primary': '#ffffff', '--bg-secondary': '#f5f5f5', '--bg-sidebar': '#f0f0f0',
'--bg-hover': '#e5e5e5', '--border': '#d5d5d5', '--text-primary': '#121212',
'--text-secondary': '#575757', '--text-muted': '#8c8c8c', '--accent': '#1db954',
'--accent-green': '#1db954', '--tag-bg': '#e0ffe0', '--tag-text': '#128a3c',
'--code-bg': '#f5f5f5', '--search-bg': '#ffffff', '--scrollbar': '#d5d5d5',
'--resize-handle': '#d5d5d5', '--overlay-bg': 'rgba(0,0,0,0.2)',
'--danger': '#c92a2a', '--danger-bg': '#ffe3e3', '--success': '#1db954', '--success-bg': '#e0ffe0',
'--accent-card': '#1db954', '--accent-bg': '#e0ffe0', '--accent-text': '#128a3c',
'--green': '#1db954', '--green-bg': '#e0ffe0', '--purple': '#8a2be2', '--purple-bg': '#f3e0ff',
'--surface': '#f5f5f5', '--surface2': '#ffffff', '--surface3': '#f0f0f0',
'--border-md': '#d5d5d5', '--text': '#121212', '--text-2': '#575757', '--text-3': '#8c8c8c',
'--ok': '#1db954', '--warn': '#9a6700', '--ai': '#8a2be2', '--err': '#c92a2a'
}
}
},
'notion': {
name: 'Notion', desc: 'Lecture propre',
modes: {
dark: {
'--bg-primary': '#191919', '--bg-secondary': '#202020', '--bg-sidebar': '#191919',
'--bg-hover': '#262626', '--border': '#333333', '--text-primary': '#e6e6e6',
'--text-secondary': '#999999', '--text-muted': '#666666', '--accent': '#4b85e8',
'--accent-green': '#0ca16a', '--tag-bg': '#1c2744', '--tag-text': '#6ba1f0',
'--code-bg': '#202020', '--search-bg': '#262626', '--scrollbar': '#333333',
'--resize-handle': '#333333', '--overlay-bg': 'rgba(0,0,0,0.6)',
'--danger': '#e03e3e', '--danger-bg': '#2a1010', '--success': '#0ca16a', '--success-bg': '#0d2818',
'--accent-card': '#4b85e8', '--accent-bg': '#1c2744', '--accent-text': '#6ba1f0',
'--green': '#0ca16a', '--green-bg': '#0d2818', '--purple': '#9065e0', '--purple-bg': '#24183d',
'--surface': '#202020', '--surface2': '#191919', '--surface3': '#141414',
'--border-md': '#333333', '--text': '#e6e6e6', '--text-2': '#999999', '--text-3': '#666666',
'--ok': '#0ca16a', '--warn': '#dfab3e', '--ai': '#9065e0', '--err': '#e03e3e'
},
light: {
'--bg-primary': '#ffffff', '--bg-secondary': '#f7f6f3', '--bg-sidebar': '#f7f6f3',
'--bg-hover': '#edece9', '--border': '#e3e2e0', '--text-primary': '#37352f',
'--text-secondary': '#787774', '--text-muted': '#abaaa7', '--accent': '#4b85e8',
'--accent-green': '#0ca16a', '--tag-bg': '#e8f0fe', '--tag-text': '#4b85e8',
'--code-bg': '#f7f6f3', '--search-bg': '#ffffff', '--scrollbar': '#e3e2e0',
'--resize-handle': '#e3e2e0', '--overlay-bg': 'rgba(0,0,0,0.15)',
'--danger': '#e03e3e', '--danger-bg': '#fdebec', '--success': '#0ca16a', '--success-bg': '#e6f7ec',
'--accent-card': '#4b85e8', '--accent-bg': '#e8f0fe', '--accent-text': '#4b85e8',
'--green': '#0ca16a', '--green-bg': '#e6f7ec', '--purple': '#9065e0', '--purple-bg': '#f3effc',
'--surface': '#f7f6f3', '--surface2': '#ffffff', '--surface3': '#edece9',
'--border-md': '#e3e2e0', '--text': '#37352f', '--text-2': '#787774', '--text-3': '#abaaa7',
'--ok': '#0ca16a', '--warn': '#dfab3e', '--ai': '#9065e0', '--err': '#e03e3e'
}
}
},
'discord': {
name: 'Discord', desc: 'Standard communautaire',
modes: {
dark: {
'--bg-primary': '#313338', '--bg-secondary': '#2b2d31', '--bg-sidebar': '#1e1f22',
'--bg-hover': '#3c3e45', '--border': '#3f4147', '--text-primary': '#f2f3f5',
'--text-secondary': '#b5bac1', '--text-muted': '#80848e', '--accent': '#5865f2',
'--accent-green': '#23a559', '--tag-bg': '#28315c', '--tag-text': '#8891f2',
'--code-bg': '#2b2d31', '--search-bg': '#383a40', '--scrollbar': '#1a1b1e',
'--resize-handle': '#3f4147', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#da373c', '--danger-bg': '#3a1518', '--success': '#23a559', '--success-bg': '#122b1a',
'--accent-card': '#5865f2', '--accent-bg': '#28315c', '--accent-text': '#8891f2',
'--green': '#23a559', '--green-bg': '#122b1a', '--purple': '#9c5bf0', '--purple-bg': '#2c1a46',
'--surface': '#2b2d31', '--surface2': '#313338', '--surface3': '#1e1f22',
'--border-md': '#3f4147', '--text': '#f2f3f5', '--text-2': '#b5bac1', '--text-3': '#80848e',
'--ok': '#23a559', '--warn': '#f0b232', '--ai': '#9c5bf0', '--err': '#da373c'
},
light: {
'--bg-primary': '#ffffff', '--bg-secondary': '#f2f3f5', '--bg-sidebar': '#e3e5e8',
'--bg-hover': '#e3e5e8', '--border': '#d4d7dc', '--text-primary': '#313338',
'--text-secondary': '#5c5e66', '--text-muted': '#91949e', '--accent': '#5865f2',
'--accent-green': '#248046', '--tag-bg': '#e8eafe', '--tag-text': '#4752c4',
'--code-bg': '#f2f3f5', '--search-bg': '#ffffff', '--scrollbar': '#d4d7dc',
'--resize-handle': '#d4d7dc', '--overlay-bg': 'rgba(0,0,0,0.2)',
'--danger': '#d92028', '--danger-bg': '#fde8e8', '--success': '#248046', '--success-bg': '#e6f7e8',
'--accent-card': '#5865f2', '--accent-bg': '#e8eafe', '--accent-text': '#4752c4',
'--green': '#248046', '--green-bg': '#e6f7e8', '--purple': '#8b5cf6', '--purple-bg': '#f0ebff',
'--surface': '#f2f3f5', '--surface2': '#ffffff', '--surface3': '#e3e5e8',
'--border-md': '#d4d7dc', '--text': '#313338', '--text-2': '#5c5e66', '--text-3': '#91949e',
'--ok': '#248046', '--warn': '#9a6700', '--ai': '#8b5cf6', '--err': '#d92028'
}
}
},
'dracula': {
name: 'Dracula', desc: 'Accents rose & violet',
modes: {
dark: {
'--bg-primary': '#282a36', '--bg-secondary': '#21222c', '--bg-sidebar': '#1d1e26',
'--bg-hover': '#343746', '--border': '#3b3d54', '--text-primary': '#f8f8f2',
'--text-secondary': '#b2b2b2', '--text-muted': '#6272a4', '--accent': '#bd93f9',
'--accent-green': '#50fa7b', '--tag-bg': '#2a2240', '--tag-text': '#bd93f9',
'--code-bg': '#21222c', '--search-bg': '#343746', '--scrollbar': '#44475a',
'--resize-handle': '#3b3d54', '--overlay-bg': 'rgba(0,0,0,0.6)',
'--danger': '#ff5555', '--danger-bg': '#3a1a1a', '--success': '#50fa7b', '--success-bg': '#0d2818',
'--accent-card': '#bd93f9', '--accent-bg': '#2a2240', '--accent-text': '#bd93f9',
'--green': '#50fa7b', '--green-bg': '#0d2818', '--purple': '#bd93f9', '--purple-bg': '#2a2240',
'--surface': '#21222c', '--surface2': '#282a36', '--surface3': '#1d1e26',
'--border-md': '#3b3d54', '--text': '#f8f8f2', '--text-2': '#b2b2b2', '--text-3': '#6272a4',
'--ok': '#50fa7b', '--warn': '#f1fa8c', '--ai': '#bd93f9', '--err': '#ff5555'
},
light: {
'--bg-primary': '#f8f8f2', '--bg-secondary': '#f0f0ea', '--bg-sidebar': '#e8e8e0',
'--bg-hover': '#e0e0d8', '--border': '#d5d5cc', '--text-primary': '#282a36',
'--text-secondary': '#4d5066', '--text-muted': '#8b8fa8', '--accent': '#8a5cf6',
'--accent-green': '#3d8f54', '--tag-bg': '#ede0ff', '--tag-text': '#8a5cf6',
'--code-bg': '#f0f0ea', '--search-bg': '#f8f8f2', '--scrollbar': '#d5d5cc',
'--resize-handle': '#d5d5cc', '--overlay-bg': 'rgba(0,0,0,0.15)',
'--danger': '#d7373f', '--danger-bg': '#fde8e8', '--success': '#3d8f54', '--success-bg': '#e6f7e8',
'--accent-card': '#8a5cf6', '--accent-bg': '#ede0ff', '--accent-text': '#8a5cf6',
'--green': '#3d8f54', '--green-bg': '#e6f7e8', '--purple': '#8a5cf6', '--purple-bg': '#ede0ff',
'--surface': '#f0f0ea', '--surface2': '#f8f8f2', '--surface3': '#e8e8e0',
'--border-md': '#d5d5cc', '--text': '#282a36', '--text-2': '#4d5066', '--text-3': '#8b8fa8',
'--ok': '#3d8f54', '--warn': '#8b7500', '--ai': '#8a5cf6', '--err': '#d7373f'
}
}
},
'one-dark': {
name: 'One Dark', desc: 'Inspire d\'Atom',
modes: {
dark: {
'--bg-primary': '#282c34', '--bg-secondary': '#21252b', '--bg-sidebar': '#1c1f26',
'--bg-hover': '#333842', '--border': '#3e4452', '--text-primary': '#abb2bf',
'--text-secondary': '#828997', '--text-muted': '#5c6370', '--accent': '#61afef',
'--accent-green': '#98c379', '--tag-bg': '#1e3350', '--tag-text': '#61afef',
'--code-bg': '#21252b', '--search-bg': '#333842', '--scrollbar': '#3e4452',
'--resize-handle': '#3e4452', '--overlay-bg': 'rgba(0,0,0,0.6)',
'--danger': '#e06c75', '--danger-bg': '#351b1e', '--success': '#98c379', '--success-bg': '#1a2e1a',
'--accent-card': '#61afef', '--accent-bg': '#1e3350', '--accent-text': '#61afef',
'--green': '#98c379', '--green-bg': '#1a2e1a', '--purple': '#c678dd', '--purple-bg': '#2c1a3a',
'--surface': '#21252b', '--surface2': '#282c34', '--surface3': '#1c1f26',
'--border-md': '#3e4452', '--text': '#abb2bf', '--text-2': '#828997', '--text-3': '#5c6370',
'--ok': '#98c379', '--warn': '#e5c07b', '--ai': '#c678dd', '--err': '#e06c75'
},
light: {
'--bg-primary': '#fafafa', '--bg-secondary': '#f0f0f0', '--bg-sidebar': '#e8e8e8',
'--bg-hover': '#e0e0e0', '--border': '#d4d4d4', '--text-primary': '#383a42',
'--text-secondary': '#696c77', '--text-muted': '#9ca0a4', '--accent': '#4078f2',
'--accent-green': '#50a14f', '--tag-bg': '#e0e8ff', '--tag-text': '#4078f2',
'--code-bg': '#f0f0f0', '--search-bg': '#fafafa', '--scrollbar': '#d4d4d4',
'--resize-handle': '#d4d4d4', '--overlay-bg': 'rgba(0,0,0,0.15)',
'--danger': '#e45649', '--danger-bg': '#fde8e8', '--success': '#50a14f', '--success-bg': '#e6f7e6',
'--accent-card': '#4078f2', '--accent-bg': '#e0e8ff', '--accent-text': '#4078f2',
'--green': '#50a14f', '--green-bg': '#e6f7e6', '--purple': '#a626a4', '--purple-bg': '#f8e0f8',
'--surface': '#f0f0f0', '--surface2': '#fafafa', '--surface3': '#e8e8e8',
'--border-md': '#d4d4d4', '--text': '#383a42', '--text-2': '#696c77', '--text-3': '#9ca0a4',
'--ok': '#50a14f', '--warn': '#986801', '--ai': '#a626a4', '--err': '#e45649'
}
}
},
'tokyo-night': {
name: 'Tokyo Night', desc: 'Bleu neon',
modes: {
dark: {
'--bg-primary': '#1a1b26', '--bg-secondary': '#16161e', '--bg-sidebar': '#13131a',
'--bg-hover': '#24283b', '--border': '#2f3545', '--text-primary': '#c0caf5',
'--text-secondary': '#a9b1d6', '--text-muted': '#565f89', '--accent': '#7aa2f7',
'--accent-green': '#9ece6a', '--tag-bg': '#1f2845', '--tag-text': '#7aa2f7',
'--code-bg': '#16161e', '--search-bg': '#24283b', '--scrollbar': '#2f3545',
'--resize-handle': '#2f3545', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#f7768e', '--danger-bg': '#361a22', '--success': '#9ece6a', '--success-bg': '#1a2e1a',
'--accent-card': '#7aa2f7', '--accent-bg': '#1f2845', '--accent-text': '#7aa2f7',
'--green': '#9ece6a', '--green-bg': '#1a2e1a', '--purple': '#bb9af7', '--purple-bg': '#241a3a',
'--surface': '#16161e', '--surface2': '#1a1b26', '--surface3': '#13131a',
'--border-md': '#2f3545', '--text': '#c0caf5', '--text-2': '#a9b1d6', '--text-3': '#565f89',
'--ok': '#9ece6a', '--warn': '#e0af68', '--ai': '#bb9af7', '--err': '#f7768e'
},
light: {
'--bg-primary': '#d5d6db', '--bg-secondary': '#c8c9d0', '--bg-sidebar': '#bfc0c8',
'--bg-hover': '#bbbbc4', '--border': '#a9aab3', '--text-primary': '#343b58',
'--text-secondary': '#565a6e', '--text-muted': '#848cb0', '--accent': '#34548a',
'--accent-green': '#485e30', '--tag-bg': '#dfe5f5', '--tag-text': '#34548a',
'--code-bg': '#c8c9d0', '--search-bg': '#d5d6db', '--scrollbar': '#a9aab3',
'--resize-handle': '#a9aab3', '--overlay-bg': 'rgba(0,0,0,0.15)',
'--danger': '#8c4351', '--danger-bg': '#f8e0e0', '--success': '#485e30', '--success-bg': '#e0f0d8',
'--accent-card': '#34548a', '--accent-bg': '#dfe5f5', '--accent-text': '#34548a',
'--green': '#485e30', '--green-bg': '#e0f0d8', '--purple': '#5a4a9a', '--purple-bg': '#e8e0f8',
'--surface': '#c8c9d0', '--surface2': '#d5d6db', '--surface3': '#bfc0c8',
'--border-md': '#a9aab3', '--text': '#343b58', '--text-2': '#565a6e', '--text-3': '#848cb0',
'--ok': '#485e30', '--warn': '#8f5e15', '--ai': '#5a4a9a', '--err': '#8c4351'
}
}
},
'nord': {
name: 'Nord', desc: 'Style arctique',
modes: {
dark: {
'--bg-primary': '#2e3440', '--bg-secondary': '#3b4252', '--bg-sidebar': '#242933',
'--bg-hover': '#434c5e', '--border': '#4c566a', '--text-primary': '#eceff4',
'--text-secondary': '#d8dee9', '--text-muted': '#7b88a1', '--accent': '#88c0d0',
'--accent-green': '#a3be8c', '--tag-bg': '#2a3a48', '--tag-text': '#88c0d0',
'--code-bg': '#3b4252', '--search-bg': '#434c5e', '--scrollbar': '#4c566a',
'--resize-handle': '#4c566a', '--overlay-bg': 'rgba(0,0,0,0.6)',
'--danger': '#bf616a', '--danger-bg': '#3a2025', '--success': '#a3be8c', '--success-bg': '#1a2e1a',
'--accent-card': '#88c0d0', '--accent-bg': '#2a3a48', '--accent-text': '#88c0d0',
'--green': '#a3be8c', '--green-bg': '#1a2e1a', '--purple': '#b48ead', '--purple-bg': '#2a2030',
'--surface': '#3b4252', '--surface2': '#2e3440', '--surface3': '#242933',
'--border-md': '#4c566a', '--text': '#eceff4', '--text-2': '#d8dee9', '--text-3': '#7b88a1',
'--ok': '#a3be8c', '--warn': '#ebcb8b', '--ai': '#b48ead', '--err': '#bf616a'
},
light: {
'--bg-primary': '#eceff4', '--bg-secondary': '#e5e9f0', '--bg-sidebar': '#d8dee9',
'--bg-hover': '#d8dee9', '--border': '#c8cfdb', '--text-primary': '#2e3440',
'--text-secondary': '#4c566a', '--text-muted': '#7b88a1', '--accent': '#5e81ac',
'--accent-green': '#719350', '--tag-bg': '#dfe8f0', '--tag-text': '#5e81ac',
'--code-bg': '#e5e9f0', '--search-bg': '#eceff4', '--scrollbar': '#c8cfdb',
'--resize-handle': '#c8cfdb', '--overlay-bg': 'rgba(0,0,0,0.12)',
'--danger': '#bf616a', '--danger-bg': '#f8e0e0', '--success': '#719350', '--success-bg': '#e0f0d8',
'--accent-card': '#5e81ac', '--accent-bg': '#dfe8f0', '--accent-text': '#5e81ac',
'--green': '#719350', '--green-bg': '#e0f0d8', '--purple': '#7b5e95', '--purple-bg': '#e8dff0',
'--surface': '#e5e9f0', '--surface2': '#eceff4', '--surface3': '#d8dee9',
'--border-md': '#c8cfdb', '--text': '#2e3440', '--text-2': '#4c566a', '--text-3': '#7b88a1',
'--ok': '#719350', '--warn': '#8f6b20', '--ai': '#7b5e95', '--err': '#bf616a'
}
}
},
'night-owl': {
name: 'Night Owl', desc: 'Optimise pour la nuit',
modes: {
dark: {
'--bg-primary': '#011627', '--bg-secondary': '#01111d', '--bg-sidebar': '#010d18',
'--bg-hover': '#0b2942', '--border': '#1d3b53', '--text-primary': '#d6deeb',
'--text-secondary': '#7e97b0', '--text-muted': '#4b6479', '--accent': '#82aaff',
'--accent-green': '#c3e88d', '--tag-bg': '#152d45', '--tag-text': '#82aaff',
'--code-bg': '#01111d', '--search-bg': '#0b2942', '--scrollbar': '#1d3b53',
'--resize-handle': '#1d3b53', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#ef5350', '--danger-bg': '#2a1018', '--success': '#c3e88d', '--success-bg': '#0d2818',
'--accent-card': '#82aaff', '--accent-bg': '#152d45', '--accent-text': '#82aaff',
'--green': '#c3e88d', '--green-bg': '#0d2818', '--purple': '#c792ea', '--purple-bg': '#201838',
'--surface': '#01111d', '--surface2': '#011627', '--surface3': '#010d18',
'--border-md': '#1d3b53', '--text': '#d6deeb', '--text-2': '#7e97b0', '--text-3': '#4b6479',
'--ok': '#c3e88d', '--warn': '#ffcb8b', '--ai': '#c792ea', '--err': '#ef5350'
},
light: {
'--bg-primary': '#fbfbfb', '--bg-secondary': '#f0f0f0', '--bg-sidebar': '#e8e8e8',
'--bg-hover': '#e0e0e0', '--border': '#d4d4d4', '--text-primary': '#403f53',
'--text-secondary': '#5f6c7a', '--text-muted': '#8e99a8', '--accent': '#4876d6',
'--accent-green': '#2d8a4e', '--tag-bg': '#e0e8ff', '--tag-text': '#4876d6',
'--code-bg': '#f0f0f0', '--search-bg': '#fbfbfb', '--scrollbar': '#d4d4d4',
'--resize-handle': '#d4d4d4', '--overlay-bg': 'rgba(0,0,0,0.12)',
'--danger': '#c92a2a', '--danger-bg': '#ffe3e3', '--success': '#2d8a4e', '--success-bg': '#e0ffe0',
'--accent-card': '#4876d6', '--accent-bg': '#e0e8ff', '--accent-text': '#4876d6',
'--green': '#2d8a4e', '--green-bg': '#e0ffe0', '--purple': '#7c3aed', '--purple-bg': '#ede0ff',
'--surface': '#f0f0f0', '--surface2': '#fbfbfb', '--surface3': '#e8e8e8',
'--border-md': '#d4d4d4', '--text': '#403f53', '--text-2': '#5f6c7a', '--text-3': '#8e99a8',
'--ok': '#2d8a4e', '--warn': '#8f6b20', '--ai': '#7c3aed', '--err': '#c92a2a'
}
}
},
'anthracite': {
name: 'Anthracite', desc: 'Material dark',
modes: {
dark: {
'--bg-primary': '#1e1e1e', '--bg-secondary': '#252526', '--bg-sidebar': '#1a1a1a',
'--bg-hover': '#2d2d2d', '--border': '#3e3e3e', '--text-primary': '#d4d4d4',
'--text-secondary': '#9d9d9d', '--text-muted': '#6e6e6e', '--accent': '#9c7aef',
'--accent-green': '#6a9955', '--tag-bg': '#2a2250', '--tag-text': '#9c7aef',
'--code-bg': '#252526', '--search-bg': '#2d2d2d', '--scrollbar': '#3e3e3e',
'--resize-handle': '#3e3e3e', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#f44747', '--danger-bg': '#3a1a1a', '--success': '#6a9955', '--success-bg': '#1a2e1a',
'--accent-card': '#9c7aef', '--accent-bg': '#2a2250', '--accent-text': '#9c7aef',
'--green': '#6a9955', '--green-bg': '#1a2e1a', '--purple': '#9c7aef', '--purple-bg': '#2a2250',
'--surface': '#252526', '--surface2': '#1e1e1e', '--surface3': '#1a1a1a',
'--border-md': '#3e3e3e', '--text': '#d4d4d4', '--text-2': '#9d9d9d', '--text-3': '#6e6e6e',
'--ok': '#6a9955', '--warn': '#d7ba7d', '--ai': '#9c7aef', '--err': '#f44747'
},
light: {
'--bg-primary': '#fafafa', '--bg-secondary': '#f5f5f5', '--bg-sidebar': '#eeeeee',
'--bg-hover': '#e5e5e5', '--border': '#d5d5d5', '--text-primary': '#212121',
'--text-secondary': '#616161', '--text-muted': '#9e9e9e', '--accent': '#7c3aed',
'--accent-green': '#388e3c', '--tag-bg': '#ede0ff', '--tag-text': '#7c3aed',
'--code-bg': '#f5f5f5', '--search-bg': '#fafafa', '--scrollbar': '#d5d5d5',
'--resize-handle': '#d5d5d5', '--overlay-bg': 'rgba(0,0,0,0.12)',
'--danger': '#d32f2f', '--danger-bg': '#fde8e8', '--success': '#388e3c', '--success-bg': '#e8f5e9',
'--accent-card': '#7c3aed', '--accent-bg': '#ede0ff', '--accent-text': '#7c3aed',
'--green': '#388e3c', '--green-bg': '#e8f5e9', '--purple': '#7c3aed', '--purple-bg': '#ede0ff',
'--surface': '#f5f5f5', '--surface2': '#fafafa', '--surface3': '#eeeeee',
'--border-md': '#d5d5d5', '--text': '#212121', '--text-2': '#616161', '--text-3': '#9e9e9e',
'--ok': '#388e3c', '--warn': '#8f6b20', '--ai': '#7c3aed', '--err': '#d32f2f'
}
}
},
'cyberpunk': {
name: 'Cyberpunk', desc: 'Neon vibrant',
modes: {
dark: {
'--bg-primary': '#0a0a0f', '--bg-secondary': '#111122', '--bg-sidebar': '#08080d',
'--bg-hover': '#1a1a33', '--border': '#2a2a55', '--text-primary': '#e0e0ff',
'--text-secondary': '#8888cc', '--text-muted': '#555599', '--accent': '#00ffff',
'--accent-green': '#00ff88', '--tag-bg': '#0a2a3a', '--tag-text': '#00ffff',
'--code-bg': '#111122', '--search-bg': '#1a1a33', '--scrollbar': '#2a2a55',
'--resize-handle': '#2a2a55', '--overlay-bg': 'rgba(0,0,0,0.8)',
'--danger': '#ff3366', '--danger-bg': '#331122', '--success': '#00ff88', '--success-bg': '#003322',
'--accent-card': '#00ffff', '--accent-bg': '#0a2a3a', '--accent-text': '#00ffff',
'--green': '#00ff88', '--green-bg': '#003322', '--purple': '#ff00ff', '--purple-bg': '#220033',
'--surface': '#111122', '--surface2': '#0a0a0f', '--surface3': '#08080d',
'--border-md': '#2a2a55', '--text': '#e0e0ff', '--text-2': '#8888cc', '--text-3': '#555599',
'--ok': '#00ff88', '--warn': '#ffaa00', '--ai': '#ff00ff', '--err': '#ff3366'
},
light: {
'--bg-primary': '#f0f0ff', '--bg-secondary': '#e8e8ff', '--bg-sidebar': '#e0e0f8',
'--bg-hover': '#d8d8f0', '--border': '#c0c0e0', '--text-primary': '#1a1a33',
'--text-secondary': '#4a4a6a', '--text-muted': '#8080a0', '--accent': '#0077cc',
'--accent-green': '#008844', '--tag-bg': '#ddeeff', '--tag-text': '#0077cc',
'--code-bg': '#e8e8ff', '--search-bg': '#f0f0ff', '--scrollbar': '#c0c0e0',
'--resize-handle': '#c0c0e0', '--overlay-bg': 'rgba(0,0,0,0.1)',
'--danger': '#cc2244', '--danger-bg': '#ffe0e8', '--success': '#008844', '--success-bg': '#d8ffe0',
'--accent-card': '#0077cc', '--accent-bg': '#ddeeff', '--accent-text': '#0077cc',
'--green': '#008844', '--green-bg': '#d8ffe0', '--purple': '#8800cc', '--purple-bg': '#f0d8ff',
'--surface': '#e8e8ff', '--surface2': '#f0f0ff', '--surface3': '#e0e0f8',
'--border-md': '#c0c0e0', '--text': '#1a1a33', '--text-2': '#4a4a6a', '--text-3': '#8080a0',
'--ok': '#008844', '--warn': '#996600', '--ai': '#8800cc', '--err': '#cc2244'
}
}
},
'navy-elegance': {
name: 'Navy Elegance', desc: 'Bleu profond & or',
modes: {
dark: {
'--bg-primary': '#0a1628', '--bg-secondary': '#0d1f3c', '--bg-sidebar': '#081220',
'--bg-hover': '#162d50', '--border': '#1e3a5f', '--text-primary': '#d4dde8',
'--text-secondary': '#8899b4', '--text-muted': '#556688', '--accent': '#c9a84c',
'--accent-green': '#4caf7c', '--tag-bg': '#1e2840', '--tag-text': '#c9a84c',
'--code-bg': '#0d1f3c', '--search-bg': '#162d50', '--scrollbar': '#1e3a5f',
'--resize-handle': '#1e3a5f', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#d94a5a', '--danger-bg': '#351a22', '--success': '#4caf7c', '--success-bg': '#12281e',
'--accent-card': '#c9a84c', '--accent-bg': '#282218', '--accent-text': '#c9a84c',
'--green': '#4caf7c', '--green-bg': '#12281e', '--purple': '#8b6fc0', '--purple-bg': '#201a3a',
'--surface': '#0d1f3c', '--surface2': '#0a1628', '--surface3': '#081220',
'--border-md': '#1e3a5f', '--text': '#d4dde8', '--text-2': '#8899b4', '--text-3': '#556688',
'--ok': '#4caf7c', '--warn': '#c9a84c', '--ai': '#8b6fc0', '--err': '#d94a5a'
},
light: {
'--bg-primary': '#f5f0e8', '--bg-secondary': '#ede6d8', '--bg-sidebar': '#e8dfd0',
'--bg-hover': '#e0d8c8', '--border': '#c8bda8', '--text-primary': '#2a3a50',
'--text-secondary': '#5a6a80', '--text-muted': '#8a9aa8', '--accent': '#8b6914',
'--accent-green': '#3d7a4c', '--tag-bg': '#f0e8d0', '--tag-text': '#8b6914',
'--code-bg': '#ede6d8', '--search-bg': '#f5f0e8', '--scrollbar': '#c8bda8',
'--resize-handle': '#c8bda8', '--overlay-bg': 'rgba(0,0,0,0.1)',
'--danger': '#b8383a', '--danger-bg': '#f8e0e0', '--success': '#3d7a4c', '--success-bg': '#e0f0e0',
'--accent-card': '#8b6914', '--accent-bg': '#f0e8d0', '--accent-text': '#8b6914',
'--green': '#3d7a4c', '--green-bg': '#e0f0e0', '--purple': '#6a4c8a', '--purple-bg': '#e8dff0',
'--surface': '#ede6d8', '--surface2': '#f5f0e8', '--surface3': '#e8dfd0',
'--border-md': '#c8bda8', '--text': '#2a3a50', '--text-2': '#5a6a80', '--text-3': '#8a9aa8',
'--ok': '#3d7a4c', '--warn': '#8b6914', '--ai': '#6a4c8a', '--err': '#b8383a'
}
}
},
'tons-terreux': {
name: 'Tons Terreux', desc: 'Sensation organique',
modes: {
dark: {
'--bg-primary': '#1c1a16', '--bg-secondary': '#252218', '--bg-sidebar': '#161410',
'--bg-hover': '#302b20', '--border': '#3d3628', '--text-primary': '#e8e0d0',
'--text-secondary': '#a89880', '--text-muted': '#6b5d48', '--accent': '#c8a050',
'--accent-green': '#6b9b50', '--tag-bg': '#2a2418', '--tag-text': '#c8a050',
'--code-bg': '#252218', '--search-bg': '#302b20', '--scrollbar': '#3d3628',
'--resize-handle': '#3d3628', '--overlay-bg': 'rgba(0,0,0,0.7)',
'--danger': '#c86050', '--danger-bg': '#352018', '--success': '#6b9b50', '--success-bg': '#1a2a14',
'--accent-card': '#c8a050', '--accent-bg': '#2a2418', '--accent-text': '#c8a050',
'--green': '#6b9b50', '--green-bg': '#1a2a14', '--purple': '#9070b0', '--purple-bg': '#221a30',
'--surface': '#252218', '--surface2': '#1c1a16', '--surface3': '#161410',
'--border-md': '#3d3628', '--text': '#e8e0d0', '--text-2': '#a89880', '--text-3': '#6b5d48',
'--ok': '#6b9b50', '--warn': '#c8a050', '--ai': '#9070b0', '--err': '#c86050'
},
light: {
'--bg-primary': '#f8f4ec', '--bg-secondary': '#f0e8d8', '--bg-sidebar': '#e8e0d0',
'--bg-hover': '#e0d8c8', '--border': '#c8bda0', '--text-primary': '#3d3028',
'--text-secondary': '#6b5d48', '--text-muted': '#9a8a70', '--accent': '#8b6914',
'--accent-green': '#5a8440', '--tag-bg': '#f0e0c8', '--tag-text': '#8b6914',
'--code-bg': '#f0e8d8', '--search-bg': '#f8f4ec', '--scrollbar': '#c8bda0',
'--resize-handle': '#c8bda0', '--overlay-bg': 'rgba(0,0,0,0.1)',
'--danger': '#a84040', '--danger-bg': '#f8e0e0', '--success': '#5a8440', '--success-bg': '#e0f0d8',
'--accent-card': '#8b6914', '--accent-bg': '#f0e0c8', '--accent-text': '#8b6914',
'--green': '#5a8440', '--green-bg': '#e0f0d8', '--purple': '#7058a0', '--purple-bg': '#e8dff8',
'--surface': '#f0e8d8', '--surface2': '#f8f4ec', '--surface3': '#e8e0d0',
'--border-md': '#c8bda0', '--text': '#3d3028', '--text-2': '#6b5d48', '--text-3': '#9a8a70',
'--ok': '#5a8440', '--warn': '#8b6914', '--ai': '#7058a0', '--err': '#a84040'
}
}
}
};
// ── Theme Engine ─────────────────────────────────────────────────────
var _currentTheme = 'defaut-obsigate';
var _currentMode = 'dark';
function applyTheme(themeKey, mode) {
var theme = THEMES[themeKey];
if (!theme) return;
mode = mode || _currentMode || 'dark';
var vars = theme.modes[mode];
if (!vars) return;
var root = document.documentElement;
Object.keys(vars).forEach(function(key) {
root.style.setProperty(key, vars[key]);
});
_currentTheme = themeKey;
_currentMode = mode;
try { localStorage.setItem('obsigate-theme', themeKey); } catch(e) {}
try { localStorage.setItem('obsigate-theme-mode', mode); } catch(e) {}
}
function toggleThemeMode() {
var newMode = _currentMode === 'dark' ? 'light' : 'dark';
applyTheme(_currentTheme, newMode);
return newMode;
}
function getCurrentTheme() { return _currentTheme; }
function getCurrentMode() { return _currentMode; }
function initThemes() {
var saved = null;
var savedMode = null;
try { saved = localStorage.getItem('obsigate-theme'); } catch(e) {}
try { savedMode = localStorage.getItem('obsigate-theme-mode'); } catch(e) {}
if (saved && THEMES[saved]) _currentTheme = saved;
if (savedMode) _currentMode = savedMode;
applyTheme(_currentTheme, _currentMode);
}
export { THEMES, applyTheme, toggleThemeMode, getCurrentTheme, getCurrentMode, initThemes };
File diff suppressed because it is too large Load Diff
-679
View File
@@ -1,679 +0,0 @@
import { state } from './state.js';
import { api } from './auth.js';
import { openFile, showWelcome } from './viewer.js';
import { refreshSidebarForContext, refreshTagsForContext } from './sidebar.js';
import { createAIToolbar } from './ai.js';
// ---------------------------------------------------------------------------
// File extension → Lucide icon mapping
// ---------------------------------------------------------------------------
const EXT_ICONS = {
// Text files
".md": "file-text",
".txt": "file-text",
".log": "file-text",
".readme": "file-text",
".rst": "file-text",
".adoc": "file-text",
// Web development
".html": "file-code",
".htm": "file-code",
".css": "file-code",
".scss": "file-code",
".sass": "file-code",
".less": "file-code",
".js": "file-code",
".jsx": "file-code",
".ts": "file-code",
".tsx": "file-code",
".vue": "file-code",
".svelte": "file-code",
// Programming languages
".py": "file-code",
".java": "file-code",
".c": "file-code",
".cpp": "file-code",
".cc": "file-code",
".cxx": "file-code",
".h": "file-code",
".hpp": "file-code",
".cs": "file-code",
".go": "file-code",
".rs": "file-code",
".rb": "file-code",
".php": "file-code",
".swift": "file-code",
".kt": "file-code",
".scala": "file-code",
".r": "file-code",
".m": "file-code",
".pl": "file-code",
".lua": "file-code",
".dart": "file-code",
".nim": "file-code",
".zig": "file-code",
".odin": "file-code",
".v": "file-code",
".cr": "file-code",
".ex": "file-code",
".exs": "file-code",
".elm": "file-code",
".purs": "file-code",
".hs": "file-code",
".ml": "file-code",
".ocaml": "file-code",
".fs": "file-code",
".fsx": "file-code",
".vb": "file-code",
".pas": "file-code",
".pp": "file-code",
".inc": "file-code",
// Data formats
".json": "file-json",
".yaml": "file-cog",
".yml": "file-cog",
".toml": "file-cog",
".xml": "file-code",
".csv": "table",
".tsv": "table",
".sql": "database",
".db": "database",
".sqlite": "database",
".sqlite3": "database",
".parquet": "database",
".avro": "database",
// Configuration files
".ini": "file-cog",
".cfg": "file-cog",
".conf": "file-cog",
".env": "file-cog",
".dockerfile": "file-cog",
".gitignore": "file-cog",
".gitattributes": "file-cog",
".editorconfig": "file-cog",
".eslintrc": "file-cog",
".prettierrc": "file-cog",
".babelrc": "file-cog",
".tsconfig": "file-cog",
"package.json": "file-cog",
"package-lock.json": "file-cog",
"yarn.lock": "file-cog",
"composer.json": "file-cog",
"requirements.txt": "file-cog",
"pipfile": "file-cog",
"gemfile": "file-cog",
"cargo.toml": "file-cog",
"go.mod": "file-cog",
"go.sum": "file-cog",
"pom.xml": "file-cog",
"build.gradle": "file-cog",
"cmakelists.txt": "file-cog",
"makefile": "file-cog",
// Shell scripts
".sh": "terminal",
".bash": "terminal",
".zsh": "terminal",
".fish": "terminal",
".bat": "terminal",
".cmd": "terminal",
".ps1": "terminal",
".psm1": "terminal",
".psd1": "terminal",
// Document formats
".pdf": "file-text",
".doc": "file-text",
".docx": "file-text",
".rtf": "file-text",
".odt": "file-text",
".tex": "file-text",
".latex": "file-text",
// Image files
".png": "file-image",
".jpg": "file-image",
".jpeg": "file-image",
".gif": "file-image",
".svg": "file-image",
".webp": "file-image",
".bmp": "file-image",
".ico": "file-image",
".tiff": "file-image",
".tif": "file-image",
// Audio files
".mp3": "file-music",
".wav": "file-music",
".flac": "file-music",
".aac": "file-music",
".ogg": "file-music",
".m4a": "file-music",
".wma": "file-music",
// Video files
".mp4": "play",
".avi": "play",
".mov": "play",
".wmv": "play",
".flv": "play",
".webm": "play",
".mkv": "play",
".m4v": "play",
".3gp": "play",
// Archive files
".zip": "file-archive",
".rar": "file-archive",
".7z": "file-archive",
".tar": "file-archive",
".gz": "file-archive",
".tgz": "file-archive",
".bz2": "file-archive",
".xz": "file-archive",
".deb": "file-archive",
".rpm": "file-archive",
".dmg": "file-archive",
".pkg": "file-archive",
".msi": "file-archive",
".exe": "file-archive",
// Font files
".ttf": "file-type",
".otf": "file-type",
".woff": "file-type",
".woff2": "file-type",
".eot": "file-type",
// Other common files
".key": "file-cog",
".pem": "file-cog",
".crt": "file-cog",
".cert": "file-cog",
".p12": "file-cog",
".pfx": "file-cog",
".lock": "file-cog",
".tmp": "file",
".bak": "file",
".old": "file",
".orig": "file",
".save": "file",
};
function getFileIcon(name) {
const ext = "." + name.split(".").pop().toLowerCase();
return EXT_ICONS[ext] || "file";
}
// ---------------------------------------------------------------------------
// Safe CDN helpers
// ---------------------------------------------------------------------------
let _iconDebounceTimer = null;
/**
* Debounced icon creation — batches multiple rapid calls into one
* DOM scan to avoid excessive reflows when building large trees.
*/
function safeCreateIcons() {
if (typeof lucide === "undefined" || !lucide.createIcons) return;
if (state._iconDebounceTimer) return; // already scheduled
state._iconDebounceTimer = requestAnimationFrame(() => {
state._iconDebounceTimer = null;
try {
lucide.createIcons();
} catch (e) {
/* CDN not loaded */
}
});
}
/** Force-flush icon creation immediately (use sparingly). */
function flushIcons() {
if (state._iconDebounceTimer) {
cancelAnimationFrame(state._iconDebounceTimer);
state._iconDebounceTimer = null;
}
if (typeof lucide !== "undefined" && lucide.createIcons) {
try {
lucide.createIcons();
} catch (e) {
/* CDN not loaded */
}
}
}
function safeHighlight(block) {
// Skip mermaid blocks — they're rendered by Mermaid.js, not highlighted
if (block.className && block.className.indexOf('language-mermaid') !== -1) return;
if (typeof hljs !== "undefined" && hljs.highlightElement) {
try {
hljs.highlightElement(block);
} catch (e) {
/* CDN not loaded */
}
}
}
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function escapeHtml(str) {
if (!str) return "";
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
// ---------------------------------------------------------------------------
// Editor (CodeMirror)
// ---------------------------------------------------------------------------
async function openEditor(vaultName, filePath) {
state.editorVault = vaultName;
state.editorPath = filePath;
const modal = document.getElementById("editor-modal");
const titleInput = document.getElementById("editor-title-input");
const filenameInput = document.getElementById("editor-filename-input");
const statFile = document.getElementById("editor-stat-file");
const statWords = document.getElementById("editor-stat-words");
const bodyEl = document.getElementById("editor-body");
// Restore header/footer (may be hidden by Forge)
var headerEl = modal.querySelector(".editor-header");
var footerEl = modal.querySelector(".editor-footer");
var container = modal.querySelector(".editor-container");
if (headerEl) headerEl.style.display = "";
if (footerEl) footerEl.style.display = "";
if (container) container.style.maxHeight = "";
filenameInput.value = filePath.split("/").pop();
statFile.textContent = vaultName + "/" + filePath;
// Fetch raw content
const rawUrl = `/api/file/${encodeURIComponent(vaultName)}/raw?path=${encodeURIComponent(filePath)}`;
const rawData = await api(rawUrl);
// Clear previous editor
bodyEl.innerHTML = "";
if (state.editorView) {
state.editorView.destroy();
state.editorView = null;
}
state.fallbackEditorEl = null;
// Re-create AI toolbar container (was cleared by innerHTML above)
const aiContainer = document.createElement('div');
aiContainer.id = 'ai-toolbar-container';
bodyEl.appendChild(aiContainer);
try {
await waitForCodeMirror();
const { EditorView, EditorState, basicSetup, markdown, python, javascript, html, css, json, xml, sql, php, cpp, java, rust, oneDark, keymap } = window.CodeMirror;
const currentTheme = document.documentElement.getAttribute("data-theme");
const fileExt = filePath.split(".").pop().toLowerCase();
const extensions = [
basicSetup,
keymap.of([
{
key: "Mod-s",
run: () => {
saveFile();
return true;
},
},
]),
EditorView.lineWrapping,
// Auto-save: debounced save after 2s of inactivity
EditorView.updateListener.of((update) => {
if (update.docChanged) {
clearTimeout(window._obsigateAutoSaveTimer);
window._obsigateAutoSaveTimer = setTimeout(() => saveFile(true), 2000);
// Mark dirty
var sd = document.getElementById("editor-save-dot");
var sl = document.getElementById("editor-save-label");
if (sd) sd.className = "editor-save-dot dirty";
if (sl) sl.textContent = "Unsaved";
}
}),
];
// Add language support based on file extension
const langMap = {
md: markdown,
markdown: markdown,
py: python,
js: javascript,
jsx: javascript,
ts: javascript,
tsx: javascript,
mjs: javascript,
cjs: javascript,
html: html,
htm: html,
css: css,
scss: css,
less: css,
json: json,
xml: xml,
svg: xml,
sql: sql,
php: php,
cpp: cpp,
cc: cpp,
cxx: cpp,
c: cpp,
h: cpp,
hpp: cpp,
java: java,
rs: rust,
sh: javascript, // Using javascript for shell scripts as fallback
bash: javascript,
zsh: javascript,
};
const langMode = langMap[fileExt];
if (langMode) {
extensions.push(langMode());
}
if (currentTheme === "dark") {
extensions.push(oneDark);
}
const cmState = EditorState.create({
doc: rawData.raw,
extensions: extensions,
});
state.editorView = new EditorView({
state: cmState,
parent: bodyEl,
});
} catch (err) {
console.error("CodeMirror init failed, falling back to textarea:", err);
state.fallbackEditorEl = document.createElement("textarea");
state.fallbackEditorEl.className = "fallback-editor";
state.fallbackEditorEl.value = rawData.raw;
bodyEl.appendChild(state.fallbackEditorEl);
}
// Set up AI toolbar (recreate each time editor opens)
var container = document.getElementById('ai-toolbar-container');
if (container && !container.querySelector('.ai-toolbar') && !container.querySelector('div')) {
createAIToolbar(container, function() { return state.editorView || null; });
}
// Mermaid live preview panel (#26)
var existingPreview = document.getElementById('mermaid-live-preview');
if (existingPreview) existingPreview.remove();
var previewPanel = document.createElement('div');
previewPanel.id = 'mermaid-live-preview';
previewPanel.className = 'mermaid-live-preview';
previewPanel.style.display = 'none';
bodyEl.appendChild(previewPanel);
// Debounced live preview on content change
var mermaidTimer = null;
var updateMermaidPreview = function() {
clearTimeout(mermaidTimer);
mermaidTimer = setTimeout(function() {
var content = state.editorView ? state.editorView.state.doc.toString() : (state.fallbackEditorEl ? state.fallbackEditorEl.value : '');
var match = content.match(/```mermaid\n([\s\S]*?)```/);
if (match) {
previewPanel.style.display = 'block';
previewPanel.innerHTML = '';
var code = match[1];
import('./mermaid-viewer.js').then(function(m) {
// Create a temp container and render
var tmp = document.createElement('div');
tmp.innerHTML = '<pre><code class="language-mermaid">' + code.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;') + '</code></pre>';
previewPanel.appendChild(tmp);
m.renderMermaidBlocks(previewPanel);
});
} else {
previewPanel.style.display = 'none';
}
}, 500);
};
if (state.editorView) {
// Attach to CodeMirror
var extUpdate = state.editorView.state.updateListener;
// Use the existing update listener approach - just add debounce on any change
}
modal.classList.add("active");
safeCreateIcons();
// Initial preview if content has mermaid
updateMermaidPreview();
}
async function waitForCodeMirror() {
let attempts = 0;
while (!window.CodeMirror && attempts < 50) {
await new Promise((resolve) => setTimeout(resolve, 100));
attempts++;
}
if (!window.CodeMirror) {
throw new Error("CodeMirror failed to load");
}
}
function closeEditor() {
const vault = state.editorVault;
const path = state.editorPath;
const modal = document.getElementById("editor-modal");
modal.classList.remove("active");
// Restore brand text
var brand = modal.querySelector('.editor-brand');
if (brand) brand.textContent = 'ObsiGate';
if (state.editorView) {
state.editorView.destroy();
state.editorView = null;
}
state.fallbackEditorEl = null;
// Remove forge iframe if present + refresh current file
var forgeFrame = document.getElementById("forge-iframe");
if (forgeFrame) {
forgeFrame.remove();
if (state.currentVault && state.currentPath) openFile(state.currentVault, state.currentPath);
}
state.editorVault = null;
state.editorPath = null;
// Refresh file content if we're viewing the edited file
if (vault && path && state.currentVault === vault && state.currentPath === path) {
openFile(vault, path);
}
}
async function saveFile(silent = false) {
// If Forge is open, delegate save to the iframe
var forgeFrame = document.getElementById("forge-iframe");
if (forgeFrame && forgeFrame.contentWindow) {
if (!state.editorVault || !state.editorPath) return;
var fSaveBtn = document.getElementById("editor-save");
var fSaveDot = document.getElementById("editor-save-dot");
var fSaveLabel = document.getElementById("editor-save-label");
if (fSaveDot) { fSaveDot.className = 'editor-save-dot saving'; }
if (fSaveLabel) fSaveLabel.textContent = 'Saving...';
if (fSaveBtn) { fSaveBtn.disabled = true; }
forgeFrame.contentWindow.postMessage({ type: 'parent-save' }, '*');
return;
}
if ((!state.editorView && !state.fallbackEditorEl) || !state.editorVault || !state.editorPath) return;
const content = state.editorView ? state.editorView.state.doc.toString() : state.fallbackEditorEl.value;
const saveBtn = document.getElementById("editor-save");
const saveDot = document.getElementById("editor-save-dot");
const saveLabel = document.getElementById("editor-save-label");
try {
// Update save dot to saving state
if (saveDot) { saveDot.className = 'editor-save-dot saving'; }
if (saveLabel) saveLabel.textContent = 'Saving...';
if (saveBtn && !silent) {
saveBtn.disabled = true;
saveBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
}
const backupParam = silent ? '&backup=false' : '';
const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}/save?path=${encodeURIComponent(state.editorPath)}${backupParam}`, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content }),
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de sauvegarde");
}
// Update save dot to saved state
if (saveDot) { saveDot.className = 'editor-save-dot ok'; }
if (saveLabel) saveLabel.textContent = 'Saved';
if (silent) {
// Auto-save: brief green flash on save button
if (saveBtn) {
saveBtn.style.background = '#22c55e';
saveBtn.style.color = '#fff';
saveBtn.style.borderColor = '#22c55e';
}
setTimeout(() => {
if (saveBtn) {
saveBtn.style.background = '';
saveBtn.style.color = '';
saveBtn.style.borderColor = '';
}
}, 1500);
} else {
closeEditor();
}
} catch (err) {
console.error("Save error:", err);
if (saveDot) { saveDot.className = 'editor-save-dot err'; }
if (saveLabel) saveLabel.textContent = 'Erreur';
// If offline, queue the save for later sync
if (!navigator.onLine) {
try {
const { queueOfflineSave } = await import('./offline.js');
await queueOfflineSave(state.editorVault, state.editorPath, content);
alert('Vous etes hors-ligne. La modification a ete enregistree localement.');
} catch (e) { alert('Erreur: ' + err.message); }
} else {
alert('Erreur: ' + err.message);
}
}
}
async function deleteFile() {
// If Forge is open, delegate delete to the iframe
var forgeFrame = document.getElementById("forge-iframe");
if (forgeFrame && forgeFrame.contentWindow) {
if (!state.editorVault || !state.editorPath) return;
forgeFrame.contentWindow.postMessage({ type: 'parent-delete' }, '*');
return;
}
if (!state.editorVault || !state.editorPath) return;
const deleteBtn = document.getElementById("editor-delete");
const originalHTML = deleteBtn.innerHTML;
let success = false;
try {
deleteBtn.disabled = true;
deleteBtn.innerHTML = '<i data-lucide="loader" style="width:16px;height:16px"></i>';
safeCreateIcons();
const response = await fetch(`/api/file/${encodeURIComponent(state.editorVault)}?path=${encodeURIComponent(state.editorPath)}`, { method: "DELETE" });
if (!response.ok) {
const error = await response.json();
throw new Error(error.detail || "Erreur de suppression");
}
success = true;
closeEditor();
showWelcome();
await refreshSidebarForContext();
await refreshTagsForContext();
} catch (err) {
console.error("Delete error:", err);
alert(`Erreur: ${err.message}`);
} finally {
deleteBtn.innerHTML = originalHTML;
deleteBtn.disabled = false;
if (success) safeCreateIcons();
}
}
function initEditor() {
const cancelBtn = document.getElementById("editor-cancel");
const deleteBtn = document.getElementById("editor-delete");
const saveBtn = document.getElementById("editor-save");
const modal = document.getElementById("editor-modal");
cancelBtn.addEventListener("click", closeEditor);
deleteBtn.addEventListener("click", deleteFile);
saveBtn.addEventListener("click", () => saveFile());
// ESC to close
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && modal.classList.contains("active")) {
closeEditor();
}
});
// Fix mouse wheel scrolling in editor
modal.addEventListener(
"wheel",
(e) => {
const editorBody = document.getElementById("editor-body");
if (editorBody && editorBody.contains(e.target)) {
// Let the editor handle the scroll
return;
}
// Prevent modal from scrolling if not in editor area
e.preventDefault();
},
{ passive: false },
);
}
/** Copy text to clipboard — works on both HTTP and HTTPS */
function copyToClipboard(text) {
// Try modern API first (HTTPS-only)
if (navigator.clipboard && window.isSecureContext) {
navigator.clipboard.writeText(text).catch(() => {});
return true;
}
// Fallback for HTTP contexts
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
ta.style.top = "-9999px";
document.body.appendChild(ta);
ta.focus();
ta.select();
try {
document.execCommand("copy");
return true;
} catch (e) {
return false;
} finally {
document.body.removeChild(ta);
}
}
export { getFileIcon, safeCreateIcons, flushIcons, safeHighlight, escapeHtml, EXT_ICONS, openEditor, closeEditor, saveFile, deleteFile, waitForCodeMirror, initEditor, copyToClipboard };
-384
View File
@@ -1,384 +0,0 @@
/**
* Vault Home Page module for ObsiGate.
*
* When a specific vault is selected (not "All"), replaces the dashboard
* with a non-recursive file listing sorted by modification time (newest first).
* Only files from the selected directory are shown (not subdirectories)
* to keep the listing fast and lightweight.
* The listing updates automatically when browsing into subdirectories.
*
* The visual style mirrors the search results (.search-result-item) for
* consistency.
*
* Usage:
* import { showVaultHome, updateVaultHomeDir, isVaultHomeActive } from './vaulthome.js';
*/
import { api } from './auth.js';
import { el } from './viewer.js';
import { getFileIcon, safeCreateIcons } from './utils.js';
import { TabManager } from './ui.js';
import { t, getLocale } from './i18n.js';
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const state = {
active: false,
currentVault: null,
currentDir: '', // empty = vault root
};
// ---------------------------------------------------------------------------
// Public API
// ---------------------------------------------------------------------------
/** Check if vault home is currently displayed. */
export function isVaultHomeActive() {
return state.active;
}
/**
* Display the vault home page for a vault directory.
* @param {string} vaultName - Vault name
* @param {string} dirPath - Directory path (empty for root)
*/
export async function showVaultHome(vaultName, dirPath = '') {
if (vaultName === 'all' || !vaultName) {
state.active = false;
return;
}
state.active = true;
state.currentVault = vaultName;
state.currentDir = dirPath || '';
const area = document.getElementById('content-area');
if (!area) return;
// Remove any existing dashboard
const home = document.getElementById('dashboard-home');
if (home) home.remove();
// Build vault home container
area.innerHTML = '';
const container = el('div', { class: 'vault-home', id: 'vault-home' });
area.appendChild(container);
// Breadcrumb
renderBreadcrumb(container, vaultName, state.currentDir);
// Results container
const resultsArea = el('div', { class: 'vault-home-results', id: 'vault-home-results' });
container.appendChild(resultsArea);
// Show loading
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:24px;height:24px;border-width:3px;margin:0 auto"></div>
<p data-i18n="vaulthome.loading" style="margin-top:0.75rem;color:var(--text-muted);font-size:0.85rem">${t('vaulthome.loading')}</p>
</div>`;
await loadAndRender(vaultName, state.currentDir, resultsArea);
// Also load recent files for this vault (only at root level)
if (!state.currentDir) {
loadRecentSection(vaultName, container);
}
}
/**
* Update the vault home directory without rebuilding the whole page.
* Called when navigating into a subdirectory.
* @param {string} vaultName
* @param {string} dirPath
*/
export async function updateVaultHomeDir(vaultName, dirPath) {
if (!state.active || state.currentVault !== vaultName) return;
state.currentDir = dirPath || '';
const container = document.getElementById('vault-home');
if (!container) return;
// Update breadcrumb
const bc = container.querySelector('.vault-home-breadcrumb');
if (bc) {
bc.innerHTML = '';
buildBreadcrumbHTML(bc, vaultName, state.currentDir);
safeCreateIcons();
}
// Remove recent section when navigating into subdirectory
const recentSection = container.querySelector('.vault-home-recent');
if (recentSection) recentSection.remove();
// Reload results
const resultsArea = document.getElementById('vault-home-results');
if (resultsArea) {
resultsArea.innerHTML = `
<div class="tree-loading" style="padding:2rem;text-align:center">
<div class="loading-spinner" style="width:20px;height:20px;border-width:2px;margin:0 auto"></div>
</div>`;
await loadAndRender(vaultName, state.currentDir, resultsArea);
}
}
// ---------------------------------------------------------------------------
// Tree click delegation — update vault home on directory clicks
// ---------------------------------------------------------------------------
let _treeDelegationInstalled = false;
function _initTreeDelegation() {
if (_treeDelegationInstalled) return;
const tree = document.getElementById('vault-tree');
if (!tree) {
// Retry once when DOM might not be ready — use MutationObserver as fallback
const observer = new MutationObserver(() => {
const t = document.getElementById('vault-tree');
if (t) {
observer.disconnect();
_attachTreeListener(t);
}
});
observer.observe(document.body, { childList: true, subtree: true });
return;
}
_attachTreeListener(tree);
}
function _attachTreeListener(tree) {
if (_treeDelegationInstalled) return;
_treeDelegationInstalled = true;
tree.addEventListener('click', (e) => {
if (!state.active) return;
// Find the closest tree-item that is a directory
const item = e.target.closest('.tree-item');
if (!item || !item.dataset.vault || item.dataset.path === undefined) return;
const vault = item.dataset.vault;
const path = item.dataset.path;
// Only react to directory clicks (items that have a child container)
const hasChildContainer = item.nextElementSibling
&& item.nextElementSibling.classList.contains('tree-children');
if (!hasChildContainer) return;
// Update vault home dir
updateVaultHomeDir(vault, path);
});
}
// Initialize delegation when module loads
_initTreeDelegation();
// ---------------------------------------------------------------------------
// Rendering
// ---------------------------------------------------------------------------
function renderBreadcrumb(container, vaultName, dirPath) {
const bc = el('div', { class: 'vault-home-breadcrumb' });
buildBreadcrumbHTML(bc, vaultName, dirPath);
container.appendChild(bc);
}
function buildBreadcrumbHTML(parentEl, vaultName, dirPath) {
// Vault icon + name (root)
const vaultLink = el('span', { class: 'vault-home-breadcrumb-item vault-home-breadcrumb-root' }, [
el('i', { 'data-lucide': 'database', style: 'width:14px;height:14px' }),
el('span', {}, [document.createTextNode(vaultName)]),
]);
vaultLink.addEventListener('click', () => updateVaultHomeDir(vaultName, ''));
vaultLink.style.cursor = 'pointer';
parentEl.appendChild(vaultLink);
if (!dirPath) return;
const segments = dirPath.split('/').filter(Boolean);
let cumulative = '';
for (const seg of segments) {
cumulative = cumulative ? cumulative + '/' + seg : seg;
const sep = el('span', { class: 'vault-home-breadcrumb-sep' }, [document.createTextNode(' / ')]);
parentEl.appendChild(sep);
const segLink = el('span', { class: 'vault-home-breadcrumb-item' }, [
el('i', { 'data-lucide': 'folder', style: 'width:14px;height:14px' }),
el('span', {}, [document.createTextNode(seg)]),
]);
segLink.addEventListener('click', () => updateVaultHomeDir(vaultName, cumulative));
segLink.style.cursor = 'pointer';
parentEl.appendChild(segLink);
}
}
async function loadAndRender(vaultName, dirPath, container) {
try {
const url = `/api/vault/${encodeURIComponent(vaultName)}/files?dir=${encodeURIComponent(dirPath)}&limit=200&recursive=false`;
const data = await api(url);
if (!data.files || data.files.length === 0) {
container.innerHTML = `
<div class="vault-home-empty">
<i data-lucide="folder-open" style="width:32px;height:32px;color:var(--text-muted)"></i>
<p data-i18n="vaulthome.empty">${t('vaulthome.empty')}</p>
</div>`;
safeCreateIcons();
return;
}
// Header
const header = el('div', { class: 'vault-home-header' });
const countKey = data.files.length > 1 ? 'vaulthome.file_count_plural' : 'vaulthome.file_count';
const countStr = t(countKey, { count: data.files.length });
const count = el('span', { class: 'vault-home-count', 'data-i18n': countKey }, [
document.createTextNode(countStr),
]);
header.appendChild(count);
// Directory label
const dirLabel = dirPath ? `/${dirPath}` : ` ${t('common.root')}`;
const dirInfo = el('span', { class: 'vault-home-dir-info' }, [
document.createTextNode(dirLabel),
]);
header.appendChild(dirInfo);
container.innerHTML = '';
container.appendChild(header);
// File list — using search-result-item style
const list = el('div', { class: 'search-results' });
const locale = getLocale();
data.files.forEach((f) => {
const fileIconName = getFileIcon ? getFileIcon(f.name) : 'file';
const displayName = f.name.endsWith('.md') ? f.name.slice(0, -3) : f.name;
const ext = f.extension || '';
const titleDiv = el('div', { class: 'search-result-title' }, [
document.createTextNode(displayName),
]);
// Format date using current locale
const modDate = new Date(f.modified * 1000);
const dateStr = modDate.toLocaleDateString(locale === 'fr' ? 'fr-CA' : 'en-CA', {
year: 'numeric', month: 'short', day: 'numeric',
hour: '2-digit', minute: '2-digit',
});
// Build metadata line: date + size
const metaParts = [];
metaParts.push(dateStr);
metaParts.push(formatSize(f.size));
const metaDiv = el('div', { class: 'search-result-vault' }, [
document.createTextNode(metaParts.join(' · ')),
]);
const item = el('div', {
class: 'search-result-item vault-home-item',
'data-vault': f.vault,
'data-path': f.path,
}, [
el('span', { class: 'search-result-ext' }, [document.createTextNode(ext)]),
el('i', { 'data-lucide': fileIconName, style: 'width:14px;height:14px;margin-right:4px;opacity:0.6' }),
titleDiv,
metaDiv,
]);
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
item.addEventListener('dblclick', (e) => {
e.preventDefault();
TabManager.openPersistent(f.vault, f.path);
});
list.appendChild(item);
});
container.appendChild(list);
safeCreateIcons();
} catch (err) {
const errMsg = t('vaulthome.error_loading') + ' ' + escapeHtml(err?.detail || err?.message || t('common.unknown'));
container.innerHTML = `
<div class="vault-home-error">
<p data-i18n="vaulthome.error_loading">${errMsg}</p>
</div>`;
}
}
// ---------------------------------------------------------------------------
// Recent files section (loaded at vault root only)
// ---------------------------------------------------------------------------
async function loadRecentSection(vaultName, container) {
try {
const data = await api(`/api/recent?mode=opened&vault=${encodeURIComponent(vaultName)}&limit=5`);
const files = data.files || [];
if (files.length === 0) return;
// Insert recent section BEFORE the results area
const resultsArea = document.getElementById('vault-home-results');
const section = el('div', { class: 'vault-home-recent' });
const header = el('div', { class: 'vault-home-recent-header' });
header.innerHTML = `
<i data-lucide="clock" style="width:14px;height:14px"></i>
<span data-i18n="dashboard.recent">${t('dashboard.recent')}</span>`;
section.appendChild(header);
const list = el('div', { class: 'vault-home-recent-list' });
files.forEach((f) => {
const title = f.title || f.path.split('/').pop() || '';
const displayTitle = title.endsWith('.md') ? title.slice(0, -3) : title;
const dir = f.path.includes('/') ? f.path.split('/').slice(0, -1).join('/') + '/' : '';
const item = el('div', { class: 'vault-home-recent-item' });
item.innerHTML = `
<i data-lucide="${getFileIcon ? getFileIcon(f.path) : 'file'}" style="width:14px;height:14px;opacity:0.6;flex-shrink:0"></i>
<span class="vault-home-recent-title">${escapeHtml(displayTitle)}</span>
<span class="vault-home-recent-dir">${escapeHtml(dir)}</span>
<span class="vault-home-recent-time">${escapeHtml(f.mtime_human || '')}</span>`;
item.addEventListener('click', () => TabManager.openPreview(f.vault, f.path));
item.addEventListener('dblclick', (e) => {
e.preventDefault();
TabManager.openPersistent(f.vault, f.path);
});
list.appendChild(item);
});
section.appendChild(list);
// Insert before the results area
if (resultsArea) {
container.insertBefore(section, resultsArea);
} else {
container.appendChild(section);
}
safeCreateIcons();
} catch {
// Silently skip if recent files can't be loaded
}
}
// ---------------------------------------------------------------------------
// Utility
// ---------------------------------------------------------------------------
function formatSize(bytes) {
if (bytes < 1024) return bytes + ' ' + t('vaulthome.size_bytes');
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' ' + t('vaulthome.size_kb');
return (bytes / (1024 * 1024)).toFixed(1) + ' ' + t('vaulthome.size_mb');
}
function escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-89
View File
@@ -1,89 +0,0 @@
{
"name": "ObsiGate",
"short_name": "ObsiGate",
"description": "Porte d'entree web pour vos vaults Obsidian - Accedez, naviguez et recherchez dans toutes vos notes",
"start_url": "/",
"scope": "/",
"display": "standalone",
"background_color": "#1a1a1a",
"theme_color": "#2563eb",
"orientation": "any",
"icons": [
{
"src": "/static/icons/icon-72x72.svg",
"sizes": "72x72",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-96x96.svg",
"sizes": "96x96",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-128x128.svg",
"sizes": "128x128",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-144x144.svg",
"sizes": "144x144",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-152x152.svg",
"sizes": "152x152",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-192x192.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-384x384.svg",
"sizes": "384x384",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-512x512.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "any"
},
{
"src": "/static/icons/icon-192x192-maskable.svg",
"sizes": "192x192",
"type": "image/svg+xml",
"purpose": "maskable"
},
{
"src": "/static/icons/icon-512x512-maskable.svg",
"sizes": "512x512",
"type": "image/svg+xml",
"purpose": "maskable"
}
],
"categories": ["productivity", "utilities"],
"shortcuts": [
{
"name": "Recherche",
"short_name": "Recherche",
"description": "Rechercher dans vos vaults",
"url": "/?action=search",
"icons": [
{
"src": "/static/icons/search-96x96.svg",
"sizes": "96x96"
}
]
}
],
"prefer_related_applications": false
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-197
View File
@@ -1,197 +0,0 @@
/* ObsiGate Service Worker - PWA v2 with IndexedDB offline support */
const CACHE_VERSION = 'obsigate-v6-i18n';
const STATIC_CACHE = CACHE_VERSION + '-static';
const DYNAMIC_CACHE = CACHE_VERSION + '-dynamic';
const MAX_DYNAMIC_CACHE_SIZE = 100;
const STATIC_ASSETS = [
'/',
'/static/index.html',
'/static/app.js',
'/static/style.css',
'/static/manifest.json',
'/static/js/viewer.js',
'/static/js/i18n.js',
'/static/js/auth.js',
'/static/js/dashboard.js',
'/static/js/app.js',
'/static/locales/en.json',
'/static/locales/fr.json'
];
/* ── Install ── */
self.addEventListener('install', (event) => {
console.log('[SW] Installing v' + CACHE_VERSION);
event.waitUntil(
caches.open(STATIC_CACHE)
.then((cache) => {
console.log('[SW] Caching static assets');
return cache.addAll(STATIC_ASSETS.map(url => new Request(url, { cache: 'reload' })));
})
.catch((err) => console.error('[SW] Cache error:', err))
.then(() => self.skipWaiting())
);
});
/* ── Activate ── */
self.addEventListener('activate', (event) => {
console.log('[SW] Activating...');
event.waitUntil(
caches.keys()
.then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name.startsWith('obsigate-') && name !== STATIC_CACHE && name !== DYNAMIC_CACHE)
.map((name) => {
console.log('[SW] Deleting old cache:', name);
return caches.delete(name);
})
);
})
.then(() => self.clients.claim())
);
});
/* ── Fetch ── */
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
if (request.method !== 'GET') return;
if (url.pathname === '/api/events') return;
if (url.pathname.startsWith('/api/auth/')) return;
// API requests: Network first, tell client to use IndexedDB if offline
if (url.pathname.startsWith('/api/')) {
event.respondWith(networkFirstAPI(request));
return;
}
// Static assets: Cache first
event.respondWith(cacheFirstStatic(request, url));
});
/* ── Network First for API ── */
async function networkFirstAPI(request) {
try {
const response = await fetch(request);
// Cache successful GETs in dynamic cache
if (response && response.status === 200) {
const cache = await caches.open(DYNAMIC_CACHE);
cache.put(request, response.clone());
limitCacheSize(DYNAMIC_CACHE, MAX_DYNAMIC_CACHE_SIZE);
}
return response;
} catch (error) {
console.log('[SW] Offline — checking cache for:', request.url);
// Try dynamic cache first
const cached = await caches.match(request);
if (cached) {
// Add offline header so the page can detect
const headers = new Headers(cached.headers);
headers.set('X-ObsiGate-Offline', 'true');
return new Response(cached.body, { status: cached.status, statusText: cached.statusText, headers });
}
// Fallback: tell the page to use IndexedDB
return new Response(JSON.stringify({
offline: true,
endpoint: new URL(request.url).pathname,
message: 'Vous etes hors-ligne. Les donnees locales sont utilisees.'
}), {
status: 200,
headers: { 'Content-Type': 'application/json', 'X-ObsiGate-Offline': 'true' }
});
}
}
/* ── Cache First for Static ── */
async function cacheFirstStatic(request, url) {
const cached = await caches.match(request);
if (cached) return cached;
try {
const response = await fetch(request);
if (response && response.status === 200) {
const cache = await caches.open(STATIC_CACHE);
cache.put(request, response.clone());
}
return response;
} catch {
// Offline fallback for HTML navigation
if (request.mode === 'navigate' || url.pathname === '/' || url.pathname.endsWith('/')) {
const offlinePage = await caches.match('/static/index.html');
if (offlinePage) return offlinePage;
}
return new Response('Offline', { status: 503 });
}
}
async function limitCacheSize(cacheName, maxSize) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxSize) {
for (let i = 0; i < keys.length - maxSize; i++) {
await cache.delete(keys[i]);
}
}
}
/* ── Messages ── */
self.addEventListener('message', (event) => {
const data = event.data;
if (!data || !data.type) return;
switch (data.type) {
case 'SKIP_WAITING':
self.skipWaiting();
break;
case 'CLEAR_CACHE':
event.waitUntil(
caches.keys().then(names => Promise.all(names.map(n => caches.delete(n))))
);
break;
case 'CLAIM_CLIENTS':
event.waitUntil(self.clients.claim());
break;
}
});
/* ── Sync ── */
self.addEventListener('sync', (event) => {
console.log('[SW] Background sync:', event.tag);
if (event.tag === 'obsigate-sync') {
event.waitUntil(
self.clients.matchAll().then(clients => {
clients.forEach(client => {
client.postMessage({ type: 'SYNC_TRIGGERED' });
});
})
);
}
});
/* ── Push ── */
self.addEventListener('push', (event) => {
const options = {
body: event.data ? event.data.text() : 'Nouvelle mise a jour',
icon: '/static/icons/icon-192x192.svg',
badge: '/static/icons/icon-72x72.svg',
vibrate: [200, 100, 200],
actions: [
{ action: 'open', title: 'Ouvrir ObsiGate' },
{ action: 'close', title: 'Fermer' }
]
};
event.waitUntil(self.registration.showNotification('ObsiGate', options));
});
self.addEventListener('notificationclick', (event) => {
event.notification.close();
if (event.action === 'open') {
event.waitUntil(clients.openWindow('/'));
}
});
-702
View File
@@ -1,702 +0,0 @@
A. HISTORY OF THE SOFTWARE
==========================
Python was created in the early 1990s by Guido van Rossum at Stichting
Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands
as a successor of a language called ABC. Guido remains Python's
principal author, although it includes many contributions from others.
In 1995, Guido continued his work on Python at the Corporation for
National Research Initiatives (CNRI, see https://www.cnri.reston.va.us)
in Reston, Virginia where he released several versions of the
software.
In May 2000, Guido and the Python core development team moved to
BeOpen.com to form the BeOpen PythonLabs team. In October of the same
year, the PythonLabs team moved to Digital Creations, which became
Zope Corporation. In 2001, the Python Software Foundation (PSF, see
https://www.python.org/psf/) was formed, a non-profit organization
created specifically to own Python-related Intellectual Property.
Zope Corporation was a sponsoring member of the PSF.
All Python releases are Open Source (see https://opensource.org for
the Open Source Definition). Historically, most, but not all, Python
releases have also been GPL-compatible; the table below summarizes
the various releases.
Release Derived Year Owner GPL-
from compatible? (1)
0.9.0 thru 1.2 1991-1995 CWI yes
1.3 thru 1.5.2 1.2 1995-1999 CNRI yes
1.6 1.5.2 2000 CNRI no
2.0 1.6 2000 BeOpen.com no
1.6.1 1.6 2001 CNRI yes (2)
2.1 2.0+1.6.1 2001 PSF no
2.0.1 2.0+1.6.1 2001 PSF yes
2.1.1 2.1+2.0.1 2001 PSF yes
2.1.2 2.1.1 2002 PSF yes
2.1.3 2.1.2 2002 PSF yes
2.2 and above 2.1.1 2001-now PSF yes
Footnotes:
(1) GPL-compatible doesn't mean that we're distributing Python under
the GPL. All Python licenses, unlike the GPL, let you distribute
a modified version without making your changes open source. The
GPL-compatible licenses make it possible to combine Python with
other software that is released under the GPL; the others don't.
(2) According to Richard Stallman, 1.6.1 is not GPL-compatible,
because its license has a choice of law clause. According to
CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1
is "not incompatible" with the GPL.
Thanks to the many outside volunteers who have worked under Guido's
direction to make these releases possible.
B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON
===============================================================
Python software and documentation are licensed under the
Python Software Foundation License Version 2.
Starting with Python 3.8.6, examples, recipes, and other code in
the documentation are dual licensed under the PSF License Version 2
and the Zero-Clause BSD license.
Some software incorporated into Python is under different licenses.
The licenses are listed with code falling under that license.
PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2
--------------------------------------------
1. This LICENSE AGREEMENT is between the Python Software Foundation
("PSF"), and the Individual or Organization ("Licensee") accessing and
otherwise using this software ("Python") in source or binary form and
its associated documentation.
2. Subject to the terms and conditions of this License Agreement, PSF hereby
grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce,
analyze, test, perform and/or display publicly, prepare derivative works,
distribute, and otherwise use Python alone or in any derivative version,
provided, however, that PSF's License Agreement and PSF's notice of copyright,
i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation;
All Rights Reserved" are retained in Python alone or in any derivative version
prepared by Licensee.
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python.
4. PSF is making Python available to Licensee on an "AS IS"
basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. Nothing in this License Agreement shall be deemed to create any
relationship of agency, partnership, or joint venture between PSF and
Licensee. This License Agreement does not grant permission to use PSF
trademarks or trade name in a trademark sense to endorse or promote
products or services of Licensee, or any third party.
8. By copying, installing or otherwise using Python, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0
-------------------------------------------
BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1
1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an
office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the
Individual or Organization ("Licensee") accessing and otherwise using
this software in source or binary form and its associated
documentation ("the Software").
2. Subject to the terms and conditions of this BeOpen Python License
Agreement, BeOpen hereby grants Licensee a non-exclusive,
royalty-free, world-wide license to reproduce, analyze, test, perform
and/or display publicly, prepare derivative works, distribute, and
otherwise use the Software alone or in any derivative version,
provided, however, that the BeOpen Python License is retained in the
Software, alone or in any derivative version prepared by Licensee.
3. BeOpen is making the Software available to Licensee on an "AS IS"
basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE
SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS
AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY
DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
5. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
6. This License Agreement shall be governed by and interpreted in all
respects by the law of the State of California, excluding conflict of
law provisions. Nothing in this License Agreement shall be deemed to
create any relationship of agency, partnership, or joint venture
between BeOpen and Licensee. This License Agreement does not grant
permission to use BeOpen trademarks or trade names in a trademark
sense to endorse or promote products or services of Licensee, or any
third party. As an exception, the "BeOpen Python" logos available at
http://www.pythonlabs.com/logos.html may be used according to the
permissions granted on that web page.
7. By copying, installing or otherwise using the software, Licensee
agrees to be bound by the terms and conditions of this License
Agreement.
CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1
---------------------------------------
1. This LICENSE AGREEMENT is between the Corporation for National
Research Initiatives, having an office at 1895 Preston White Drive,
Reston, VA 20191 ("CNRI"), and the Individual or Organization
("Licensee") accessing and otherwise using Python 1.6.1 software in
source or binary form and its associated documentation.
2. Subject to the terms and conditions of this License Agreement, CNRI
hereby grants Licensee a nonexclusive, royalty-free, world-wide
license to reproduce, analyze, test, perform and/or display publicly,
prepare derivative works, distribute, and otherwise use Python 1.6.1
alone or in any derivative version, provided, however, that CNRI's
License Agreement and CNRI's notice of copyright, i.e., "Copyright (c)
1995-2001 Corporation for National Research Initiatives; All Rights
Reserved" are retained in Python 1.6.1 alone or in any derivative
version prepared by Licensee. Alternately, in lieu of CNRI's License
Agreement, Licensee may substitute the following text (omitting the
quotes): "Python 1.6.1 is made available subject to the terms and
conditions in CNRI's License Agreement. This Agreement together with
Python 1.6.1 may be located on the internet using the following
unique, persistent identifier (known as a handle): 1895.22/1013. This
Agreement may also be obtained from a proxy server on the internet
using the following URL: http://hdl.handle.net/1895.22/1013".
3. In the event Licensee prepares a derivative work that is based on
or incorporates Python 1.6.1 or any part thereof, and wants to make
the derivative work available to others as provided herein, then
Licensee hereby agrees to include in any such work a brief summary of
the changes made to Python 1.6.1.
4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS"
basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR
IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND
DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS
FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT
INFRINGE ANY THIRD PARTY RIGHTS.
5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON
1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS
A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1,
OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF.
6. This License Agreement will automatically terminate upon a material
breach of its terms and conditions.
7. This License Agreement shall be governed by the federal
intellectual property law of the United States, including without
limitation the federal copyright law, and, to the extent such
U.S. federal law does not apply, by the law of the Commonwealth of
Virginia, excluding Virginia's conflict of law provisions.
Notwithstanding the foregoing, with regard to derivative works based
on Python 1.6.1 that incorporate non-separable material that was
previously distributed under the GNU General Public License (GPL), the
law of the Commonwealth of Virginia shall govern this License
Agreement only as to issues arising under or with respect to
Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this
License Agreement shall be deemed to create any relationship of
agency, partnership, or joint venture between CNRI and Licensee. This
License Agreement does not grant permission to use CNRI trademarks or
trade name in a trademark sense to endorse or promote products or
services of Licensee, or any third party.
8. By clicking on the "ACCEPT" button where indicated, or by copying,
installing or otherwise using Python 1.6.1, Licensee agrees to be
bound by the terms and conditions of this License Agreement.
ACCEPT
CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2
--------------------------------------------------
Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam,
The Netherlands. All rights reserved.
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose and without fee is hereby granted,
provided that the above copyright notice appear in all copies and that
both that copyright notice and this permission notice appear in
supporting documentation, and that the name of Stichting Mathematisch
Centrum or CWI not be used in advertising or publicity pertaining to
distribution of the software without specific, written prior
permission.
STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO
THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM BE LIABLE
FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION
----------------------------------------------------------------------
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
Additional Conditions for this Windows binary build
---------------------------------------------------
This program is linked with and uses Microsoft Distributable Code,
copyrighted by Microsoft Corporation. The Microsoft Distributable Code
is embedded in each .exe, .dll and .pyd file as a result of running
the code through a linker.
If you further distribute programs that include the Microsoft
Distributable Code, you must comply with the restrictions on
distribution specified by Microsoft. In particular, you must require
distributors and external end users to agree to terms that protect the
Microsoft Distributable Code at least as much as Microsoft's own
requirements for the Distributable Code. See Microsoft's documentation
(included in its developer tools and on its website at microsoft.com)
for specific details.
Redistribution of the Windows binary build of the Python interpreter
complies with this agreement, provided that you do not:
- alter any copyright, trademark or patent notice in Microsoft's
Distributable Code;
- use Microsoft's trademarks in your programs' names or in a way that
suggests your programs come from or are endorsed by Microsoft;
- distribute Microsoft's Distributable Code to run on a platform other
than Microsoft operating systems, run-time technologies or application
platforms; or
- include Microsoft Distributable Code in malicious, deceptive or
unlawful programs.
These restrictions apply only to the Microsoft Distributable Code as
defined above, not to Python itself or any programs running on the
Python interpreter. The redistribution of the Python interpreter and
libraries is governed by the Python Software License included with this
file, or by other licenses as marked.
--------------------------------------------------------------------------
This program, "bzip2", the associated library "libbzip2", and all
documentation, are copyright (C) 1996-2019 Julian R Seward. All
rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. The origin of this software must not be misrepresented; you must
not claim that you wrote the original software. If you use this
software in a product, an acknowledgment in the product
documentation would be appreciated but is not required.
3. Altered source versions must be plainly marked as such, and must
not be misrepresented as being the original software.
4. The name of the author may not be used to endorse or promote
products derived from this software without specific prior written
permission.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Julian Seward, [email protected]
bzip2/libbzip2 version 1.0.8 of 13 July 2019
--------------------------------------------------------------------------
libffi - Copyright (c) 1996-2022 Anthony Green, Red Hat, Inc and others.
See source files for details.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
``Software''), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED ``AS IS'', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Apache License
Version 2.0, January 2004
https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
This software is copyrighted by the Regents of the University of
California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
Corporation and other parties. The following terms apply to all files
associated with the software unless explicitly disclaimed in
individual files.
The authors hereby grant permission to use, copy, modify, distribute,
and license this software and its documentation for any purpose, provided
that existing copyright notices are retained in all copies and that this
notice is included verbatim in any distributions. No written agreement,
license, or royalty fee is required for any of the authorized uses.
Modifications to this software may be copyrighted by their authors
and need not follow the licensing terms described here, provided that
the new terms are clearly indicated on the first page of each file where
they apply.
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
MODIFICATIONS.
GOVERNMENT USE: If you are acquiring this software on behalf of the
U.S. government, the Government shall have only "Restricted Rights"
in the software and related documentation as defined in the Federal
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
are acquiring the software on behalf of the Department of Defense, the
software shall be classified as "Commercial Computer Software" and the
Government shall have only "Restricted Rights" as defined in Clause
252.227-7014 (b) (3) of DFARs. Notwithstanding the foregoing, the
authors grant the U.S. Government and others acting in its behalf
permission to use and distribute the software in accordance with the
terms specified in this license.
This software is copyrighted by the Regents of the University of
California, Sun Microsystems, Inc., Scriptics Corporation, ActiveState
Corporation, Apple Inc. and other parties. The following terms apply to
all files associated with the software unless explicitly disclaimed in
individual files.
The authors hereby grant permission to use, copy, modify, distribute,
and license this software and its documentation for any purpose, provided
that existing copyright notices are retained in all copies and that this
notice is included verbatim in any distributions. No written agreement,
license, or royalty fee is required for any of the authorized uses.
Modifications to this software may be copyrighted by their authors
and need not follow the licensing terms described here, provided that
the new terms are clearly indicated on the first page of each file where
they apply.
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
MODIFICATIONS.
GOVERNMENT USE: If you are acquiring this software on behalf of the
U.S. government, the Government shall have only "Restricted Rights"
in the software and related documentation as defined in the Federal
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
are acquiring the software on behalf of the Department of Defense, the
software shall be classified as "Commercial Computer Software" and the
Government shall have only "Restricted Rights" as defined in Clause
252.227-7013 (b) (3) of DFARs. Notwithstanding the foregoing, the
authors grant the U.S. Government and others acting in its behalf
permission to use and distribute the software in accordance with the
terms specified in this license.
Copyright (c) 1993-1999 Ioi Kim Lam.
Copyright (c) 2000-2001 Tix Project Group.
Copyright (c) 2004 ActiveState
This software is copyrighted by the above entities
and other parties. The following terms apply to all files associated
with the software unless explicitly disclaimed in individual files.
The authors hereby grant permission to use, copy, modify, distribute,
and license this software and its documentation for any purpose, provided
that existing copyright notices are retained in all copies and that this
notice is included verbatim in any distributions. No written agreement,
license, or royalty fee is required for any of the authorized uses.
Modifications to this software may be copyrighted by their authors
and need not follow the licensing terms described here, provided that
the new terms are clearly indicated on the first page of each file where
they apply.
IN NO EVENT SHALL THE AUTHORS OR DISTRIBUTORS BE LIABLE TO ANY PARTY
FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES
ARISING OUT OF THE USE OF THIS SOFTWARE, ITS DOCUMENTATION, OR ANY
DERIVATIVES THEREOF, EVEN IF THE AUTHORS HAVE BEEN ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
THE AUTHORS AND DISTRIBUTORS SPECIFICALLY DISCLAIM ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, AND NON-INFRINGEMENT. THIS SOFTWARE
IS PROVIDED ON AN "AS IS" BASIS, AND THE AUTHORS AND DISTRIBUTORS HAVE
NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR
MODIFICATIONS.
GOVERNMENT USE: If you are acquiring this software on behalf of the
U.S. government, the Government shall have only "Restricted Rights"
in the software and related documentation as defined in the Federal
Acquisition Regulations (FARs) in Clause 52.227.19 (c) (2). If you
are acquiring the software on behalf of the Department of Defense, the
software shall be classified as "Commercial Computer Software" and the
Government shall have only "Restricted Rights" as defined in Clause
252.227-7013 (c) (1) of DFARs. Notwithstanding the foregoing, the
authors grant the U.S. Government and others acting in its behalf
permission to use and distribute the software in accordance with the
terms specified in this license.
----------------------------------------------------------------------
Parts of this software are based on the Tcl/Tk software copyrighted by
the Regents of the University of California, Sun Microsystems, Inc.,
and other parties. The original license terms of the Tcl/Tk software
distribution is included in the file docs/license.tcltk.
Parts of this software are based on the HTML Library software
copyrighted by Sun Microsystems, Inc. The original license terms of
the HTML Library software distribution is included in the file
docs/license.html_lib.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-2
View File
@@ -1,2 +0,0 @@
import site
import site
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-72
View File
@@ -1,72 +0,0 @@
#!/usr/bin/env python3
"""
ObsiGate Desktop — Sidecar Launcher
====================================
Lancé par le shell Tauri, démarre le backend FastAPI.
Utilise le port 17890 (127.0.0.1 uniquement, pas d'exposition réseau).
"""
import sys
import os
import subprocess
import time
import signal
BACKEND_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
HOST = "127.0.0.1"
PORT = int(os.environ.get("OBSIGATE_PORT", "17890"))
def find_free_port(start: int) -> int:
"""Trouve le premier port libre à partir de `start`."""
import socket
port = start
while port < start + 100:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex((HOST, port)) != 0:
return port
port += 1
raise RuntimeError(f"No free port found in range {start}-{start+99}")
def main():
port = find_free_port(PORT)
# Create log directory
if sys.platform == "win32":
log_dir = os.path.join(os.environ.get("APPDATA", os.path.expanduser("~")), "ObsiGate", "logs")
else:
log_dir = os.path.join(os.path.expanduser("~"), ".local", "share", "obsigate", "logs")
os.makedirs(log_dir, exist_ok=True)
log_file = os.path.join(log_dir, "backend.log")
print(f"ObsiGate backend starting on {HOST}:{port} — logs: {log_file}")
# Launch uvicorn
cmd = [
sys.executable,
"-m", "uvicorn",
"backend.main:app",
"--host", HOST,
"--port", str(port),
"--log-level", "info",
]
with open(log_file, "a") as log:
log.write(f"\n--- ObsiGate started at {time.ctime()} ---\n")
proc = subprocess.Popen(cmd, cwd=BACKEND_DIR, stdout=log, stderr=log)
def handle_signal(signum, frame):
print("Shutting down ObsiGate backend...")
proc.terminate()
proc.wait(timeout=10)
sys.exit(0)
signal.signal(signal.SIGTERM, handle_signal)
signal.signal(signal.SIGINT, handle_signal)
# Wait for backend to finish
proc.wait()
if __name__ == "__main__":
main()
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -1
View File
@@ -23,7 +23,8 @@ fn spawn_backend() -> Result<Child, String> {
exe_dir.join("python-embed").join("bin").join("python3")
};
let child = Command::new(python)
let child = Command::new(&python)
.current_dir(&exe_dir)
.args([
"-m", "uvicorn",
"backend.main:app",
-6
View File
@@ -75,12 +75,6 @@
}
},
"plugins": {
"updater": {
"pubkey": "CHANGE_ME_TO_YOUR_PUBLIC_KEY",
"endpoints": [
"https://git.dracodev.net/api/v1/repos/Projets/ObsiGate/releases/latest"
]
},
"shell": {
"open": true,
"scope": [