aorus
This commit is contained in:
@@ -10,6 +10,7 @@ from sqlmodel import Session, select
|
|||||||
from app.config import UPLOAD_DIR
|
from app.config import UPLOAD_DIR
|
||||||
from app.database import get_session
|
from app.database import get_session
|
||||||
from app.models.media import Attachment, Media
|
from app.models.media import Attachment, Media
|
||||||
|
from app.models.settings import UserSettings
|
||||||
|
|
||||||
|
|
||||||
class MediaPatch(BaseModel):
|
class MediaPatch(BaseModel):
|
||||||
@@ -102,6 +103,20 @@ def _canonicalize_rows(rows: List[Media], session: Session) -> None:
|
|||||||
session.commit()
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
import pillow_heif
|
||||||
|
pillow_heif.register_heif_opener()
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _is_heic(content_type: str, filename: str) -> bool:
|
||||||
|
if content_type.lower() in ("image/heic", "image/heif"):
|
||||||
|
return True
|
||||||
|
fn = (filename or "").lower()
|
||||||
|
return fn.endswith(".heic") or fn.endswith(".heif")
|
||||||
|
|
||||||
|
|
||||||
def _save_webp(data: bytes, max_px: int) -> str:
|
def _save_webp(data: bytes, max_px: int) -> str:
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
@@ -122,12 +137,28 @@ def _save_webp(data: bytes, max_px: int) -> str:
|
|||||||
|
|
||||||
|
|
||||||
@router.post("/upload")
|
@router.post("/upload")
|
||||||
async def upload_file(file: UploadFile = File(...)):
|
async def upload_file(
|
||||||
|
file: UploadFile = File(...),
|
||||||
|
session: Session = Depends(get_session),
|
||||||
|
):
|
||||||
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
os.makedirs(UPLOAD_DIR, exist_ok=True)
|
||||||
data = await file.read()
|
data = await file.read()
|
||||||
ct = file.content_type or ""
|
ct = file.content_type or ""
|
||||||
if ct.startswith("image/"):
|
|
||||||
name = _save_webp(data, 1200)
|
# Lire la largeur max configurée (défaut 1200, 0 = taille originale)
|
||||||
|
setting = session.exec(select(UserSettings).where(UserSettings.cle == "image_max_width")).first()
|
||||||
|
max_px = 1200
|
||||||
|
if setting:
|
||||||
|
try:
|
||||||
|
max_px = int(setting.valeur)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
pass
|
||||||
|
if max_px <= 0:
|
||||||
|
max_px = 99999
|
||||||
|
|
||||||
|
heic = _is_heic(ct, file.filename or "")
|
||||||
|
if heic or ct.startswith("image/"):
|
||||||
|
name = _save_webp(data, max_px)
|
||||||
thumb = _save_webp(data, 300)
|
thumb = _save_webp(data, 300)
|
||||||
return {"url": f"/uploads/{name}", "thumbnail_url": f"/uploads/{thumb}"}
|
return {"url": f"/uploads/{name}", "thumbnail_url": f"/uploads/{thumb}"}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from fastapi.responses import FileResponse
|
from fastapi.responses import FileResponse
|
||||||
from starlette.background import BackgroundTask
|
from starlette.background import BackgroundTask
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
@@ -235,8 +235,8 @@ def get_debug_system_stats() -> dict[str, Any]:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/settings/backup/download")
|
def _create_backup_zip() -> tuple[Path, str]:
|
||||||
def download_backup_zip() -> FileResponse:
|
"""Crée l'archive ZIP de sauvegarde. Retourne (chemin_tmp, nom_fichier)."""
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
ts = now.strftime("%Y%m%d_%H%M%S")
|
ts = now.strftime("%Y%m%d_%H%M%S")
|
||||||
db_path = _resolve_sqlite_db_path()
|
db_path = _resolve_sqlite_db_path()
|
||||||
@@ -247,17 +247,12 @@ def download_backup_zip() -> FileResponse:
|
|||||||
os.close(fd)
|
os.close(fd)
|
||||||
tmp_zip = Path(tmp_zip_path)
|
tmp_zip = Path(tmp_zip_path)
|
||||||
|
|
||||||
stats = {
|
stats = {"database_files": 0, "upload_files": 0, "text_files": 0}
|
||||||
"database_files": 0,
|
|
||||||
"upload_files": 0,
|
|
||||||
"text_files": 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
with zipfile.ZipFile(tmp_zip, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zipf:
|
with zipfile.ZipFile(tmp_zip, mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=6) as zipf:
|
||||||
if db_path and db_path.is_file():
|
if db_path and db_path.is_file():
|
||||||
zipf.write(db_path, arcname=f"db/{db_path.name}")
|
zipf.write(db_path, arcname=f"db/{db_path.name}")
|
||||||
stats["database_files"] = 1
|
stats["database_files"] = 1
|
||||||
|
|
||||||
stats["upload_files"] = _zip_directory(zipf, uploads_dir, "uploads")
|
stats["upload_files"] = _zip_directory(zipf, uploads_dir, "uploads")
|
||||||
stats["text_files"] = _zip_data_text_files(zipf, data_root, db_path, uploads_dir)
|
stats["text_files"] = _zip_data_text_files(zipf, data_root, db_path, uploads_dir)
|
||||||
|
|
||||||
@@ -274,10 +269,66 @@ def download_backup_zip() -> FileResponse:
|
|||||||
}
|
}
|
||||||
zipf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
|
zipf.writestr("manifest.json", json.dumps(manifest, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
download_name = f"jardin_backup_{ts}.zip"
|
return tmp_zip, f"jardin_backup_{ts}.zip"
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/settings/backup/download")
|
||||||
|
def download_backup_zip() -> FileResponse:
|
||||||
|
tmp_zip, download_name = _create_backup_zip()
|
||||||
return FileResponse(
|
return FileResponse(
|
||||||
path=str(tmp_zip),
|
path=str(tmp_zip),
|
||||||
media_type="application/zip",
|
media_type="application/zip",
|
||||||
filename=download_name,
|
filename=download_name,
|
||||||
background=BackgroundTask(_safe_remove, str(tmp_zip)),
|
background=BackgroundTask(_safe_remove, str(tmp_zip)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/settings/backup/samba")
|
||||||
|
def backup_to_samba(session: Session = Depends(get_session)) -> dict[str, Any]:
|
||||||
|
"""Envoie une sauvegarde ZIP vers un partage Samba/CIFS."""
|
||||||
|
|
||||||
|
def _get(key: str, default: str = "") -> str:
|
||||||
|
row = session.exec(select(UserSettings).where(UserSettings.cle == key)).first()
|
||||||
|
return row.valeur if row else default
|
||||||
|
|
||||||
|
server = _get("samba_serveur").strip()
|
||||||
|
share = _get("samba_partage").strip()
|
||||||
|
username = _get("samba_utilisateur").strip()
|
||||||
|
password = _get("samba_motdepasse")
|
||||||
|
subfolder = _get("samba_sous_dossier").strip().strip("/\\")
|
||||||
|
|
||||||
|
if not server or not share:
|
||||||
|
raise HTTPException(400, "Configuration Samba incomplète : serveur et partage requis.")
|
||||||
|
|
||||||
|
try:
|
||||||
|
import smbclient # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
raise HTTPException(500, "Module smbprotocol non installé dans l'environnement.")
|
||||||
|
|
||||||
|
tmp_zip, filename = _create_backup_zip()
|
||||||
|
try:
|
||||||
|
smbclient.register_session(server, username=username or None, password=password or None)
|
||||||
|
|
||||||
|
remote_dir = f"\\\\{server}\\{share}"
|
||||||
|
if subfolder:
|
||||||
|
remote_dir = f"{remote_dir}\\{subfolder}"
|
||||||
|
try:
|
||||||
|
smbclient.makedirs(remote_dir, exist_ok=True)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
remote_path = f"{remote_dir}\\{filename}"
|
||||||
|
|
||||||
|
with open(tmp_zip, "rb") as local_f:
|
||||||
|
data = local_f.read()
|
||||||
|
with smbclient.open_file(remote_path, mode="wb") as smb_f:
|
||||||
|
smb_f.write(data)
|
||||||
|
|
||||||
|
return {"ok": True, "fichier": filename, "chemin": remote_path}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as exc:
|
||||||
|
raise HTTPException(500, f"Erreur Samba : {exc}") from exc
|
||||||
|
finally:
|
||||||
|
_safe_remove(str(tmp_zip))
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ aiofiles==24.1.0
|
|||||||
pytest==8.3.3
|
pytest==8.3.3
|
||||||
httpx==0.28.0
|
httpx==0.28.0
|
||||||
Pillow==11.1.0
|
Pillow==11.1.0
|
||||||
|
pillow-heif==0.21.0
|
||||||
|
smbprotocol==1.15.0
|
||||||
skyfield==1.49
|
skyfield==1.49
|
||||||
pytz==2025.1
|
pytz==2025.1
|
||||||
numpy==2.2.3
|
numpy==2.2.3
|
||||||
|
|||||||
BIN
data/jardin.db
BIN
data/jardin.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -45,4 +45,6 @@ export const settingsApi = {
|
|||||||
}
|
}
|
||||||
return { blob: r.data as Blob, filename }
|
return { blob: r.data as Blob, filename }
|
||||||
}),
|
}),
|
||||||
|
backupSamba: () =>
|
||||||
|
client.post<{ ok: boolean; fichier: string; chemin: string }>('/api/settings/backup/samba').then(r => r.data),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<span class="text-text-muted text-sm">{{ medias.length }} photo(s)</span>
|
<span class="text-text-muted text-sm">{{ medias.length }} photo(s)</span>
|
||||||
<label class="cursor-pointer bg-bg-soft text-text-muted hover:text-text px-3 py-1 rounded-lg text-xs border border-bg-hard transition-colors">
|
<label class="cursor-pointer bg-bg-soft text-text-muted hover:text-text px-3 py-1 rounded-lg text-xs border border-bg-hard transition-colors">
|
||||||
+ Photo
|
+ Photo
|
||||||
<input type="file" accept="image/*" capture="environment" class="hidden" @change="onUpload" />
|
<input type="file" accept="image/*,.heic,.HEIC,.heif,.HEIF" capture="environment" class="hidden" @change="onUpload" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
Choisir / Photographier
|
Choisir / Photographier
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="image/*"
|
accept="image/*,.heic,.HEIC,.heif,.HEIF"
|
||||||
capture="environment"
|
capture="environment"
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="onFileSelect"
|
@change="onFileSelect"
|
||||||
|
|||||||
@@ -174,7 +174,7 @@
|
|||||||
<div class="grid grid-cols-2 gap-4 pt-2">
|
<div class="grid grid-cols-2 gap-4 pt-2">
|
||||||
<label class="btn-outline border-dashed border-bg-soft flex items-center justify-center gap-2 py-3 cursor-pointer hover:border-yellow hover:text-yellow text-[10px] font-black uppercase transition-all">
|
<label class="btn-outline border-dashed border-bg-soft flex items-center justify-center gap-2 py-3 cursor-pointer hover:border-yellow hover:text-yellow text-[10px] font-black uppercase transition-all">
|
||||||
{{ uploadingPhotos ? '...' : '📸 Photos' }}
|
{{ uploadingPhotos ? '...' : '📸 Photos' }}
|
||||||
<input type="file" accept="image/*" multiple class="hidden" @change="uploadFiles($event, 'photo')" />
|
<input type="file" accept="image/*,.heic,.HEIC,.heif,.HEIF" multiple class="hidden" @change="uploadFiles($event, 'photo')" />
|
||||||
</label>
|
</label>
|
||||||
<label class="btn-outline border-dashed border-bg-soft flex items-center justify-center gap-2 py-3 cursor-pointer hover:border-aqua hover:text-aqua text-[10px] font-black uppercase transition-all">
|
<label class="btn-outline border-dashed border-bg-soft flex items-center justify-center gap-2 py-3 cursor-pointer hover:border-aqua hover:text-aqua text-[10px] font-black uppercase transition-all">
|
||||||
{{ uploadingVideos ? '...' : '🎬 Vidéos' }}
|
{{ uploadingVideos ? '...' : '🎬 Vidéos' }}
|
||||||
|
|||||||
@@ -219,7 +219,7 @@
|
|||||||
<div class="bg-bg-soft/30 rounded-3xl p-4 border border-bg-soft">
|
<div class="bg-bg-soft/30 rounded-3xl p-4 border border-bg-soft">
|
||||||
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-3">Photo de l'espace</label>
|
<label class="text-text-muted text-[10px] font-black uppercase tracking-widest block mb-3">Photo de l'espace</label>
|
||||||
<div class="relative group aspect-video rounded-2xl overflow-hidden bg-bg border-2 border-dashed border-bg-soft flex items-center justify-center cursor-pointer hover:border-green transition-all">
|
<div class="relative group aspect-video rounded-2xl overflow-hidden bg-bg border-2 border-dashed border-bg-soft flex items-center justify-center cursor-pointer hover:border-green transition-all">
|
||||||
<input type="file" accept="image/*" @change="onPhotoSelected" class="absolute inset-0 opacity-0 cursor-pointer z-20" />
|
<input type="file" accept="image/*,.heic,.HEIC,.heif,.HEIF" @change="onPhotoSelected" class="absolute inset-0 opacity-0 cursor-pointer z-20" />
|
||||||
<img v-if="photoPreview" :src="photoPreview" class="absolute inset-0 w-full h-full object-cover" />
|
<img v-if="photoPreview" :src="photoPreview" class="absolute inset-0 w-full h-full object-cover" />
|
||||||
<div v-else class="text-center">
|
<div v-else class="text-center">
|
||||||
<span class="text-3xl block mb-2">📸</span>
|
<span class="text-3xl block mb-2">📸</span>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@
|
|||||||
<!-- Upload Photo -->
|
<!-- Upload Photo -->
|
||||||
<div class="bg-bg-soft/30 p-3 rounded-xl border border-bg-soft">
|
<div class="bg-bg-soft/30 p-3 rounded-xl border border-bg-soft">
|
||||||
<label class="text-text-muted text-[9px] font-black uppercase tracking-widest block mb-2">Photo de l'outil</label>
|
<label class="text-text-muted text-[9px] font-black uppercase tracking-widest block mb-2">Photo de l'outil</label>
|
||||||
<input type="file" accept="image/*" @change="onPhotoSelected" class="text-[10px] text-text-muted w-full" />
|
<input type="file" accept="image/*,.heic,.HEIC,.heif,.HEIF" @change="onPhotoSelected" class="text-[10px] text-text-muted w-full" />
|
||||||
<img v-if="photoPreview" :src="photoPreview" class="mt-2 w-full h-24 object-cover rounded border border-bg-hard shadow-lg" />
|
<img v-if="photoPreview" :src="photoPreview" class="mt-2 w-full h-24 object-cover rounded border border-bg-hard shadow-lg" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -495,7 +495,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Upload Photo -->
|
<!-- Upload Photo -->
|
||||||
<input type="file" ref="fileInput" accept="image/*" class="hidden" @change="handleFileUpload" />
|
<input type="file" ref="fileInput" accept="image/*,.heic,.HEIC,.heif,.HEIF" class="hidden" @change="handleFileUpload" />
|
||||||
|
|
||||||
<!-- ====== POPUP FORMULAIRE VARIÉTÉ ====== -->
|
<!-- ====== POPUP FORMULAIRE VARIÉTÉ ====== -->
|
||||||
<div v-if="showFormVariety" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[70] flex items-center justify-center p-4" @click.self="closeFormVariety">
|
<div v-if="showFormVariety" class="fixed inset-0 bg-black/80 backdrop-blur-sm z-[70] flex items-center justify-center p-4" @click.self="closeFormVariety">
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Section Sauvegarde -->
|
<!-- Section Sauvegarde locale -->
|
||||||
<section class="card-jardin flex flex-col h-full border-aqua/20">
|
<section class="card-jardin flex flex-col h-full border-aqua/20">
|
||||||
<div class="flex items-center gap-3 mb-6 border-b border-bg-hard pb-4">
|
<div class="flex items-center gap-3 mb-6 border-b border-bg-hard pb-4">
|
||||||
<span class="text-2xl">📦</span>
|
<span class="text-2xl">📦</span>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<p class="text-xs text-text-muted leading-relaxed">
|
<p class="text-xs text-text-muted leading-relaxed">
|
||||||
Génère une archive complète (.zip) incluant votre base de données SQLite et tous les médias (photos/vidéos) uploadés.
|
Génère une archive complète (.zip) incluant votre base de données SQLite et tous les médias (photos/vidéos) uploadés.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="btn-primary !bg-aqua !text-bg w-full py-4 flex flex-col items-center gap-2 shadow-lg hover:shadow-aqua/20"
|
class="btn-primary !bg-aqua !text-bg w-full py-4 flex flex-col items-center gap-2 shadow-lg hover:shadow-aqua/20"
|
||||||
:disabled="downloadingBackup"
|
:disabled="downloadingBackup"
|
||||||
@@ -135,11 +135,119 @@
|
|||||||
<span class="text-xl">💾</span>
|
<span class="text-xl">💾</span>
|
||||||
<span class="text-[10px] font-black uppercase tracking-widest">{{ downloadingBackup ? 'Préparation...' : 'Télécharger le Pack Complet' }}</span>
|
<span class="text-[10px] font-black uppercase tracking-widest">{{ downloadingBackup ? 'Préparation...' : 'Télécharger le Pack Complet' }}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div v-if="backupMsg" class="text-[10px] text-center font-bold text-aqua animate-bounce">{{ backupMsg }}</div>
|
<div v-if="backupMsg" class="text-[10px] text-center font-bold text-aqua animate-bounce">{{ backupMsg }}</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<!-- Section Images -->
|
||||||
|
<section class="card-jardin flex flex-col h-full border-orange/20">
|
||||||
|
<div class="flex items-center gap-3 mb-6 border-b border-bg-hard pb-4">
|
||||||
|
<span class="text-2xl">🖼️</span>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-text font-bold uppercase tracking-widest text-xs">Images</h2>
|
||||||
|
<p class="text-[10px] text-text-muted font-bold">Qualité et taille des photos importées.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 space-y-6">
|
||||||
|
<div class="space-y-3">
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-text-muted">Largeur max des photos</label>
|
||||||
|
<span class="text-xs font-mono text-orange">{{ imageMaxWidthLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-3 gap-2">
|
||||||
|
<button
|
||||||
|
v-for="opt in imageWidthOptions"
|
||||||
|
:key="opt.value"
|
||||||
|
@click="imageMaxWidth = opt.value"
|
||||||
|
:class="[
|
||||||
|
'py-2 px-1 rounded-xl text-[10px] font-black uppercase tracking-widest border transition-all',
|
||||||
|
imageMaxWidth === opt.value
|
||||||
|
? 'bg-orange/20 border-orange text-orange'
|
||||||
|
: 'border-bg-soft text-text-muted hover:border-orange/40'
|
||||||
|
]"
|
||||||
|
>{{ opt.label }}</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-[10px] text-text-muted leading-relaxed italic">
|
||||||
|
S'applique aux nouvelles photos importées. Les miniatures restent à 300 px.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-8 pt-4 border-t border-bg-hard flex items-center justify-end gap-3">
|
||||||
|
<span v-if="imageSavedMsg" class="text-[10px] font-bold text-aqua">{{ imageSavedMsg }}</span>
|
||||||
|
<button
|
||||||
|
class="btn-primary !bg-orange !text-bg !py-2 !px-6 text-xs"
|
||||||
|
:disabled="savingImage"
|
||||||
|
@click="saveImageSettings"
|
||||||
|
>{{ savingImage ? '...' : 'Enregistrer' }}</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Section Sauvegarde Samba -->
|
||||||
|
<section class="card-jardin flex flex-col h-full border-purple/20">
|
||||||
|
<div class="flex items-center gap-3 mb-6 border-b border-bg-hard pb-4">
|
||||||
|
<span class="text-2xl">🗄️</span>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-text font-bold uppercase tracking-widest text-xs">Sauvegarde Samba / NAS</h2>
|
||||||
|
<p class="text-[10px] text-text-muted font-bold">Envoi automatique vers un partage réseau.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-1 space-y-4">
|
||||||
|
<div class="grid grid-cols-2 gap-3">
|
||||||
|
<div class="space-y-1 col-span-2 sm:col-span-1">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-text-muted">Serveur (IP ou nom)</label>
|
||||||
|
<input v-model="samba.serveur" type="text" placeholder="192.168.1.10"
|
||||||
|
class="input-jardin w-full text-xs" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 col-span-2 sm:col-span-1">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-text-muted">Partage</label>
|
||||||
|
<input v-model="samba.partage" type="text" placeholder="Jardin"
|
||||||
|
class="input-jardin w-full text-xs" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 col-span-2">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-text-muted">Sous-dossier (optionnel)</label>
|
||||||
|
<input v-model="samba.sous_dossier" type="text" placeholder="backups/jardin"
|
||||||
|
class="input-jardin w-full text-xs" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 col-span-2 sm:col-span-1">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-text-muted">Utilisateur</label>
|
||||||
|
<input v-model="samba.utilisateur" type="text" placeholder="user"
|
||||||
|
class="input-jardin w-full text-xs" />
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1 col-span-2 sm:col-span-1">
|
||||||
|
<label class="text-[10px] font-black uppercase tracking-widest text-text-muted">Mot de passe</label>
|
||||||
|
<input v-model="samba.motdepasse" type="password" placeholder="••••••••"
|
||||||
|
class="input-jardin w-full text-xs" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="sambaSendMsg" :class="['text-[10px] text-center font-bold p-2 rounded-lg', sambaError ? 'text-red bg-red/10' : 'text-green bg-green/10']">
|
||||||
|
{{ sambaSendMsg }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-6 pt-4 border-t border-bg-hard flex flex-col gap-3">
|
||||||
|
<div class="flex items-center justify-end gap-3">
|
||||||
|
<span v-if="sambaSavedMsg" class="text-[10px] font-bold text-aqua">{{ sambaSavedMsg }}</span>
|
||||||
|
<button class="btn-outline !py-2 !px-4 text-xs border-purple/40 text-purple hover:bg-purple/10"
|
||||||
|
:disabled="savingSamba" @click="saveSambaSettings">
|
||||||
|
{{ savingSamba ? '...' : 'Enregistrer la config' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="btn-primary w-full py-3 flex items-center justify-center gap-2 text-xs"
|
||||||
|
:disabled="sendingSamba || !samba.serveur || !samba.partage"
|
||||||
|
@click="sendSambaBackup"
|
||||||
|
>
|
||||||
|
<span>{{ sendingSamba ? '⏳' : '📤' }}</span>
|
||||||
|
<span class="font-black uppercase tracking-widest text-[10px]">{{ sendingSamba ? 'Envoi en cours...' : 'Envoyer un backup maintenant' }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<!-- Section API Docs (Largeur double sur XL+) -->
|
<!-- Section API Docs (Largeur double sur XL+) -->
|
||||||
<section class="card-jardin xl:col-span-2 flex flex-col border-bg-soft/50">
|
<section class="card-jardin xl:col-span-2 flex flex-col border-bg-soft/50">
|
||||||
<div class="flex items-center gap-3 mb-6 border-b border-bg-hard pb-4">
|
<div class="flex items-center gap-3 mb-6 border-b border-bg-hard pb-4">
|
||||||
@@ -188,6 +296,32 @@ const downloadingBackup = ref(false)
|
|||||||
const backupMsg = ref('')
|
const backupMsg = ref('')
|
||||||
const apiBaseUrl = detectApiBaseUrl()
|
const apiBaseUrl = detectApiBaseUrl()
|
||||||
|
|
||||||
|
// --- Image max width ---
|
||||||
|
const imageWidthOptions = [
|
||||||
|
{ value: 400, label: '400 px' },
|
||||||
|
{ value: 600, label: '600 px' },
|
||||||
|
{ value: 800, label: '800 px' },
|
||||||
|
{ value: 1200, label: '1200 px' },
|
||||||
|
{ value: 1600, label: '1600 px' },
|
||||||
|
{ value: 2400, label: '2400 px' },
|
||||||
|
{ value: 0, label: 'Originale' },
|
||||||
|
]
|
||||||
|
const imageMaxWidth = ref(1200)
|
||||||
|
const imageMaxWidthLabel = computed(() => {
|
||||||
|
const opt = imageWidthOptions.find(o => o.value === imageMaxWidth.value)
|
||||||
|
return opt ? opt.label : `${imageMaxWidth.value} px`
|
||||||
|
})
|
||||||
|
const savingImage = ref(false)
|
||||||
|
const imageSavedMsg = ref('')
|
||||||
|
|
||||||
|
// --- Samba ---
|
||||||
|
const samba = ref({ serveur: '', partage: '', sous_dossier: '', utilisateur: '', motdepasse: '' })
|
||||||
|
const savingSamba = ref(false)
|
||||||
|
const sambaSavedMsg = ref('')
|
||||||
|
const sendingSamba = ref(false)
|
||||||
|
const sambaSendMsg = ref('')
|
||||||
|
const sambaError = ref(false)
|
||||||
|
|
||||||
// --- UI Size settings ---
|
// --- UI Size settings ---
|
||||||
const uiSizeSettings = [
|
const uiSizeSettings = [
|
||||||
{ key: 'ui_font_size', label: 'Corps de texte', min: 12, max: 24, step: 1, unit: 'px' },
|
{ key: 'ui_font_size', label: 'Corps de texte', min: 12, max: 24, step: 1, unit: 'px' },
|
||||||
@@ -276,11 +410,69 @@ async function loadSettings() {
|
|||||||
if (v != null) uiSizes.value[s.key] = Number(v) || UI_SIZE_DEFAULTS[s.key]
|
if (v != null) uiSizes.value[s.key] = Number(v) || UI_SIZE_DEFAULTS[s.key]
|
||||||
}
|
}
|
||||||
applyUiSizes()
|
applyUiSizes()
|
||||||
|
if (data.image_max_width != null) imageMaxWidth.value = Number(data.image_max_width) || 1200
|
||||||
|
if (data.samba_serveur != null) samba.value.serveur = data.samba_serveur
|
||||||
|
if (data.samba_partage != null) samba.value.partage = data.samba_partage
|
||||||
|
if (data.samba_sous_dossier != null) samba.value.sous_dossier = data.samba_sous_dossier
|
||||||
|
if (data.samba_utilisateur != null) samba.value.utilisateur = data.samba_utilisateur
|
||||||
|
if (data.samba_motdepasse != null) samba.value.motdepasse = data.samba_motdepasse
|
||||||
} catch {
|
} catch {
|
||||||
// Laisse la valeur locale
|
// Laisse la valeur locale
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveImageSettings() {
|
||||||
|
savingImage.value = true
|
||||||
|
imageSavedMsg.value = ''
|
||||||
|
try {
|
||||||
|
await settingsApi.update({ image_max_width: String(imageMaxWidth.value) })
|
||||||
|
imageSavedMsg.value = 'Enregistré'
|
||||||
|
setTimeout(() => { imageSavedMsg.value = '' }, 1800)
|
||||||
|
} catch {
|
||||||
|
imageSavedMsg.value = 'Erreur.'
|
||||||
|
setTimeout(() => { imageSavedMsg.value = '' }, 2200)
|
||||||
|
} finally {
|
||||||
|
savingImage.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSambaSettings() {
|
||||||
|
savingSamba.value = true
|
||||||
|
sambaSavedMsg.value = ''
|
||||||
|
try {
|
||||||
|
await settingsApi.update({
|
||||||
|
samba_serveur: samba.value.serveur,
|
||||||
|
samba_partage: samba.value.partage,
|
||||||
|
samba_sous_dossier: samba.value.sous_dossier,
|
||||||
|
samba_utilisateur: samba.value.utilisateur,
|
||||||
|
samba_motdepasse: samba.value.motdepasse,
|
||||||
|
})
|
||||||
|
sambaSavedMsg.value = 'Enregistré'
|
||||||
|
setTimeout(() => { sambaSavedMsg.value = '' }, 1800)
|
||||||
|
} catch {
|
||||||
|
sambaSavedMsg.value = 'Erreur.'
|
||||||
|
setTimeout(() => { sambaSavedMsg.value = '' }, 2200)
|
||||||
|
} finally {
|
||||||
|
savingSamba.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendSambaBackup() {
|
||||||
|
sendingSamba.value = true
|
||||||
|
sambaSendMsg.value = ''
|
||||||
|
sambaError.value = false
|
||||||
|
try {
|
||||||
|
const res = await settingsApi.backupSamba()
|
||||||
|
sambaSendMsg.value = `Envoyé : ${res.fichier}`
|
||||||
|
} catch (err: any) {
|
||||||
|
sambaError.value = true
|
||||||
|
sambaSendMsg.value = err?.response?.data?.detail || 'Erreur lors de l\'envoi Samba.'
|
||||||
|
} finally {
|
||||||
|
sendingSamba.value = false
|
||||||
|
setTimeout(() => { sambaSendMsg.value = '' }, 4000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function saveSettings() {
|
async function saveSettings() {
|
||||||
saving.value = true
|
saving.value = true
|
||||||
savedMsg.value = ''
|
savedMsg.value = ''
|
||||||
|
|||||||
Reference in New Issue
Block a user