mirror of
https://git.hiabuto.net/C3MA/CCMA.git
synced 2026-08-25 23:15:18 +02:00
AppConfig now keeps a gnucash_last_accounts map (file path -> account guid). When the import dialog opens a file it already knows, it auto-selects whichever account was picked last time for that specific file instead of always defaulting to the first one alphabetically; picking a different account updates and persists the mapping right away. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
138 lines
5.8 KiB
Python
138 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import os
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import TYPE_CHECKING
|
|
|
|
from ccma.domain.models import DEFAULT_OPTIONAL_MEMBER_FIELDS, normalize_optional_member_fields
|
|
from ccma.storage.atomic import write_json_atomic
|
|
|
|
if TYPE_CHECKING:
|
|
from ccma.services.housekeeper import HousekeeperSettings
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class AppConfig:
|
|
store_path: str = ""
|
|
gnucash_path: str = ""
|
|
gnucash_last_accounts: dict[str, str] = field(default_factory=dict)
|
|
theme_mode: str = "dark"
|
|
run_housekeeper_on_startup: bool = True
|
|
splash_minimum_seconds: float = 5.0
|
|
birthday_days_before: int = 7
|
|
birthday_days_after: int = 2
|
|
anniversary_days_before: int = 14
|
|
anniversary_days_after: int = 7
|
|
anniversary_intervals: str = "1Y;5Y;10Y;25Y;50Y"
|
|
retroactive_claims: bool = False
|
|
optional_member_fields: tuple[str, ...] = DEFAULT_OPTIONAL_MEMBER_FIELDS
|
|
window_geometry: str = ""
|
|
window_state: str = "normal"
|
|
monitor_bounds: tuple[int, int, int, int] | None = None
|
|
|
|
@property
|
|
def path(self) -> Path:
|
|
return config_directory() / "config.json"
|
|
|
|
def save(self) -> None:
|
|
write_json_atomic(
|
|
self.path,
|
|
{
|
|
"schema_version": 1,
|
|
"store_path": self.store_path,
|
|
"gnucash_path": self.gnucash_path,
|
|
"gnucash_last_accounts": self.gnucash_last_accounts,
|
|
"theme_mode": self.theme_mode,
|
|
"run_housekeeper_on_startup": self.run_housekeeper_on_startup,
|
|
"splash_minimum_seconds": _non_negative_float(self.splash_minimum_seconds, 5.0),
|
|
"birthday_days_before": self.birthday_days_before,
|
|
"birthday_days_after": self.birthday_days_after,
|
|
"anniversary_days_before": self.anniversary_days_before,
|
|
"anniversary_days_after": self.anniversary_days_after,
|
|
"anniversary_intervals": self.anniversary_intervals,
|
|
"retroactive_claims": self.retroactive_claims,
|
|
"optional_member_fields": list(normalize_optional_member_fields(self.optional_member_fields)),
|
|
"window_geometry": self.window_geometry,
|
|
"window_state": self.window_state,
|
|
"monitor_bounds": list(self.monitor_bounds) if self.monitor_bounds else None,
|
|
},
|
|
)
|
|
|
|
def housekeeper_settings(self) -> HousekeeperSettings:
|
|
from ccma.services.housekeeper import HousekeeperSettings
|
|
from ccma.services.intervals import IntervalValidationError
|
|
|
|
try:
|
|
return HousekeeperSettings.from_values(
|
|
birthday_days_before=self.birthday_days_before,
|
|
birthday_days_after=self.birthday_days_after,
|
|
anniversary_days_before=self.anniversary_days_before,
|
|
anniversary_days_after=self.anniversary_days_after,
|
|
anniversary_intervals=self.anniversary_intervals,
|
|
retroactive_claims=self.retroactive_claims,
|
|
optional_member_fields=self.optional_member_fields,
|
|
)
|
|
except IntervalValidationError:
|
|
return HousekeeperSettings()
|
|
|
|
|
|
def config_directory() -> Path:
|
|
override = os.environ.get("CCMA_CONFIG_DIR")
|
|
if override:
|
|
return Path(override).expanduser()
|
|
if os.name == "nt":
|
|
return Path(os.environ.get("APPDATA", Path.home())) / "CCMA"
|
|
return Path(os.environ.get("XDG_CONFIG_HOME", Path.home() / ".config")) / "ccma"
|
|
|
|
|
|
def load_config() -> AppConfig:
|
|
path = config_directory() / "config.json"
|
|
store_override = os.environ.get("CCMA_STORE", "")
|
|
if not path.exists():
|
|
return AppConfig(store_path=store_override)
|
|
try:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
monitor_raw = data.get("monitor_bounds")
|
|
monitor_bounds = None
|
|
if isinstance(monitor_raw, list) and len(monitor_raw) == 4:
|
|
monitor_bounds = tuple(int(value) for value in monitor_raw)
|
|
last_accounts_raw = data.get("gnucash_last_accounts")
|
|
gnucash_last_accounts = (
|
|
{str(key): str(value) for key, value in last_accounts_raw.items()}
|
|
if isinstance(last_accounts_raw, dict)
|
|
else {}
|
|
)
|
|
return AppConfig(
|
|
store_path=store_override or str(data.get("store_path", "")),
|
|
gnucash_path=str(data.get("gnucash_path", "")),
|
|
gnucash_last_accounts=gnucash_last_accounts,
|
|
theme_mode=str(data.get("theme_mode", "dark")),
|
|
run_housekeeper_on_startup=bool(data.get("run_housekeeper_on_startup", True)),
|
|
splash_minimum_seconds=_non_negative_float(data.get("splash_minimum_seconds", 5.0), 5.0),
|
|
birthday_days_before=int(data.get("birthday_days_before", 7)),
|
|
birthday_days_after=int(data.get("birthday_days_after", 2)),
|
|
anniversary_days_before=int(data.get("anniversary_days_before", 14)),
|
|
anniversary_days_after=int(data.get("anniversary_days_after", 7)),
|
|
anniversary_intervals=str(data.get("anniversary_intervals", "1Y;5Y;10Y;25Y;50Y")),
|
|
retroactive_claims=bool(data.get("retroactive_claims", False)),
|
|
optional_member_fields=normalize_optional_member_fields(
|
|
data.get("optional_member_fields", DEFAULT_OPTIONAL_MEMBER_FIELDS)
|
|
),
|
|
window_geometry=str(data.get("window_geometry", "")),
|
|
window_state=str(data.get("window_state", "normal")),
|
|
monitor_bounds=monitor_bounds,
|
|
)
|
|
except (OSError, ValueError, TypeError):
|
|
return AppConfig(store_path=store_override)
|
|
|
|
|
|
def _non_negative_float(value: object, default: float) -> float:
|
|
try:
|
|
parsed = float(value)
|
|
except (TypeError, ValueError):
|
|
return default
|
|
return max(0.0, parsed) if math.isfinite(parsed) else default
|