Files

127 lines
4.1 KiB
Python

"""Sauvegarde / chargement de l'état complet de l'application (extraits +
réglages de style + page en cours) dans un fichier "projet" JSON.
Utilisé aussi bien pour la sauvegarde manuelle (Enregistrer / Enregistrer
sous / Ouvrir) que pour la sauvegarde automatique périodique et à la
fermeture — voir main_window.py, qui est seul responsable de décider
*quel* fichier cible (projet explicite ou fichier de récupération par
défaut).
"""
from __future__ import annotations
import json
from dataclasses import asdict, dataclass, fields
from datetime import datetime
from pathlib import Path
from PySide6.QtCore import QSettings
from .ai_config import APP_NAME, ORG_NAME
from .models import Clip
from .pdf_style import PdfStyle
PROJECT_FILE_SUFFIX = ".pdfscrap"
PROJECT_FILE_FILTER = "Projet PdfScrapper (*.pdfscrap)"
_FORMAT_VERSION = 1
_SETTINGS_GROUP = "session"
_SETTINGS_KEY_LAST_PATH = "last_path"
@dataclass
class ProjectState:
"""Tout ce qui définit une session de capture : les extraits, le
style d'export PDF, le réglage d'insertion, et la page en cours (pour
reprendre la navigation là où elle s'était arrêtée)."""
clips: list[Clip]
pdf_style: PdfStyle
insert_above: bool
current_url: str
def save_project(path: str | Path, state: ProjectState) -> None:
"""Écrit `state` en JSON dans `path`, en créant le dossier parent si
besoin. Écriture atomique (fichier temporaire puis remplacement) pour
ne jamais laisser un fichier à moitié écrit si l'appli est
interrompue en plein autosave."""
data = {
"format_version": _FORMAT_VERSION,
"insert_above": state.insert_above,
"current_url": state.current_url,
"pdf_style": asdict(state.pdf_style),
"clips": [
{
"text": clip.text,
"title": clip.title,
"url": clip.url,
"captured_at": clip.captured_at.isoformat(),
"linked_to_next": clip.linked_to_next,
}
for clip in state.clips
],
}
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
tmp_path = path.with_name(path.name + ".tmp")
tmp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
tmp_path.replace(path)
def load_project(path: str | Path) -> ProjectState:
"""Reconstruit un `ProjectState` depuis un fichier écrit par
save_project(). Tolérant aux champs manquants/inconnus (filtre les
clés de pdf_style sur les champs actuels de PdfStyle, valeurs par
défaut pour les champs de Clip absents) — robustesse si le format
évolue dans une version future de l'appli."""
data = json.loads(Path(path).read_text(encoding="utf-8"))
known_style_fields = {f.name for f in fields(PdfStyle)}
style_data = {
key: value
for key, value in (data.get("pdf_style") or {}).items()
if key in known_style_fields
}
pdf_style = PdfStyle(**style_data)
clips = [
Clip(
text=raw["text"],
title=raw["title"],
url=raw["url"],
captured_at=datetime.fromisoformat(raw["captured_at"]),
linked_to_next=raw.get("linked_to_next", False),
)
for raw in data.get("clips", [])
]
return ProjectState(
clips=clips,
pdf_style=pdf_style,
insert_above=data.get("insert_above", True),
current_url=data.get("current_url", ""),
)
def load_last_session_path() -> Path | None:
"""Chemin du dernier fichier actif (projet explicite ou fichier de
récupération par défaut), pour la reprise automatique au démarrage."""
settings = QSettings(ORG_NAME, APP_NAME)
settings.beginGroup(_SETTINGS_GROUP)
try:
raw = settings.value(_SETTINGS_KEY_LAST_PATH, "", type=str)
finally:
settings.endGroup()
return Path(raw) if raw else None
def save_last_session_path(path: str | Path) -> None:
settings = QSettings(ORG_NAME, APP_NAME)
settings.beginGroup(_SETTINGS_GROUP)
try:
settings.setValue(_SETTINGS_KEY_LAST_PATH, str(path))
finally:
settings.endGroup()