mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 23:50:12 +01:00
229 lines
6.8 KiB
Python
229 lines
6.8 KiB
Python
"""Session helpers for managing supplemental user themes in the web UI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from dataclasses import asdict
|
|
from typing import Any, Dict, Iterable, List, Tuple
|
|
|
|
from deck_builder.theme_resolution import (
|
|
ThemeResolutionInfo,
|
|
clean_theme_inputs,
|
|
normalize_theme_match_mode,
|
|
resolve_additional_theme_inputs,
|
|
)
|
|
|
|
DEFAULT_THEME_LIMIT = 8
|
|
ADDITION_COOLDOWN_SECONDS = 0.75
|
|
|
|
_INPUTS_KEY = "custom_theme_inputs"
|
|
_RESOLUTION_KEY = "user_theme_resolution"
|
|
_MODE_KEY = "theme_match_mode"
|
|
_LAST_ADD_KEY = "custom_theme_last_add_ts"
|
|
_CATALOG_VERSION_KEY = "theme_catalog_version"
|
|
|
|
|
|
def _sanitize_single(value: str | None) -> str | None:
|
|
for item in clean_theme_inputs([value] if value is not None else []):
|
|
return item
|
|
return None
|
|
|
|
|
|
def _store_inputs(sess: Dict[str, Any], inputs: List[str]) -> None:
|
|
sess[_INPUTS_KEY] = list(inputs)
|
|
|
|
|
|
def _current_inputs(sess: Dict[str, Any]) -> List[str]:
|
|
values = sess.get(_INPUTS_KEY)
|
|
if isinstance(values, list):
|
|
return [str(v) for v in values if isinstance(v, str)]
|
|
return []
|
|
|
|
|
|
def _store_resolution(sess: Dict[str, Any], info: ThemeResolutionInfo) -> None:
|
|
info_dict = asdict(info)
|
|
sess[_RESOLUTION_KEY] = info_dict
|
|
sess[_CATALOG_VERSION_KEY] = info.catalog_version
|
|
sess[_MODE_KEY] = info.mode
|
|
sess["additional_themes"] = list(info.resolved)
|
|
|
|
|
|
def _default_resolution(mode: str) -> Dict[str, Any]:
|
|
return {
|
|
"requested": [],
|
|
"mode": normalize_theme_match_mode(mode),
|
|
"catalog_version": "unknown",
|
|
"resolved": [],
|
|
"matches": [],
|
|
"unresolved": [],
|
|
"fuzzy_corrections": {},
|
|
}
|
|
|
|
|
|
def _resolve_and_store(
|
|
sess: Dict[str, Any],
|
|
inputs: List[str],
|
|
mode: str,
|
|
commander_tags: Iterable[str],
|
|
) -> ThemeResolutionInfo:
|
|
info = resolve_additional_theme_inputs(inputs, mode, commander_tags=commander_tags)
|
|
_store_inputs(sess, inputs)
|
|
_store_resolution(sess, info)
|
|
return info
|
|
|
|
|
|
def get_view_state(sess: Dict[str, Any], *, default_mode: str) -> Dict[str, Any]:
|
|
inputs = _current_inputs(sess)
|
|
mode = sess.get(_MODE_KEY, default_mode)
|
|
resolution = sess.get(_RESOLUTION_KEY)
|
|
if not isinstance(resolution, dict):
|
|
resolution = _default_resolution(mode)
|
|
remaining = max(0, int(sess.get("custom_theme_limit", DEFAULT_THEME_LIMIT)) - len(inputs))
|
|
return {
|
|
"inputs": inputs,
|
|
"mode": normalize_theme_match_mode(mode),
|
|
"resolution": resolution,
|
|
"limit": int(sess.get("custom_theme_limit", DEFAULT_THEME_LIMIT)),
|
|
"remaining": remaining,
|
|
}
|
|
|
|
|
|
def set_limit(sess: Dict[str, Any], limit: int) -> None:
|
|
sess["custom_theme_limit"] = max(1, int(limit))
|
|
|
|
|
|
def add_theme(
|
|
sess: Dict[str, Any],
|
|
value: str | None,
|
|
*,
|
|
commander_tags: Iterable[str],
|
|
mode: str | None,
|
|
limit: int = DEFAULT_THEME_LIMIT,
|
|
) -> Tuple[ThemeResolutionInfo | None, str, str]:
|
|
normalized_mode = normalize_theme_match_mode(mode)
|
|
inputs = _current_inputs(sess)
|
|
sanitized = _sanitize_single(value)
|
|
if not sanitized:
|
|
return None, "Enter a theme to add.", "error"
|
|
lower_inputs = {item.casefold() for item in inputs}
|
|
if sanitized.casefold() in lower_inputs:
|
|
return None, "That theme is already listed.", "info"
|
|
if len(inputs) >= limit:
|
|
return None, f"You can only add up to {limit} themes.", "warning"
|
|
last_ts = float(sess.get(_LAST_ADD_KEY, 0.0) or 0.0)
|
|
now = time.time()
|
|
if now - last_ts < ADDITION_COOLDOWN_SECONDS:
|
|
return None, "Please wait a moment before adding another theme.", "warning"
|
|
proposed = inputs + [sanitized]
|
|
try:
|
|
info = _resolve_and_store(sess, proposed, normalized_mode, commander_tags)
|
|
sess[_LAST_ADD_KEY] = now
|
|
return info, f"Added theme '{sanitized}'.", "success"
|
|
except ValueError as exc:
|
|
# Revert when strict mode rejects unresolved entries.
|
|
_resolve_and_store(sess, inputs, normalized_mode, commander_tags)
|
|
return None, str(exc), "error"
|
|
|
|
|
|
def remove_theme(
|
|
sess: Dict[str, Any],
|
|
value: str | None,
|
|
*,
|
|
commander_tags: Iterable[str],
|
|
mode: str | None,
|
|
) -> Tuple[ThemeResolutionInfo | None, str, str]:
|
|
normalized_mode = normalize_theme_match_mode(mode)
|
|
inputs = _current_inputs(sess)
|
|
if not inputs:
|
|
return None, "No themes to remove.", "info"
|
|
key = (value or "").strip().casefold()
|
|
if not key:
|
|
return None, "Select a theme to remove.", "error"
|
|
filtered = [item for item in inputs if item.casefold() != key]
|
|
if len(filtered) == len(inputs):
|
|
return None, "Theme not found in your list.", "warning"
|
|
info = _resolve_and_store(sess, filtered, normalized_mode, commander_tags)
|
|
return info, "Theme removed.", "success"
|
|
|
|
|
|
def choose_suggestion(
|
|
sess: Dict[str, Any],
|
|
original: str,
|
|
selection: str,
|
|
*,
|
|
commander_tags: Iterable[str],
|
|
mode: str | None,
|
|
) -> Tuple[ThemeResolutionInfo | None, str, str]:
|
|
normalized_mode = normalize_theme_match_mode(mode)
|
|
inputs = _current_inputs(sess)
|
|
orig_key = (original or "").strip().casefold()
|
|
if not orig_key:
|
|
return None, "Original theme missing.", "error"
|
|
sanitized = _sanitize_single(selection)
|
|
if not sanitized:
|
|
return None, "Select a suggestion to apply.", "error"
|
|
try:
|
|
index = next(i for i, item in enumerate(inputs) if item.casefold() == orig_key)
|
|
except StopIteration:
|
|
return None, "Original theme not found.", "warning"
|
|
replacement_key = sanitized.casefold()
|
|
if replacement_key in {item.casefold() for i, item in enumerate(inputs) if i != index}:
|
|
# Duplicate suggestion: simply drop the original.
|
|
updated = [item for i, item in enumerate(inputs) if i != index]
|
|
message = f"Removed duplicate theme '{original}'."
|
|
else:
|
|
updated = list(inputs)
|
|
updated[index] = sanitized
|
|
message = f"Updated '{original}' to '{sanitized}'."
|
|
info = _resolve_and_store(sess, updated, normalized_mode, commander_tags)
|
|
return info, message, "success"
|
|
|
|
|
|
def set_mode(
|
|
sess: Dict[str, Any],
|
|
mode: str,
|
|
*,
|
|
commander_tags: Iterable[str],
|
|
) -> Tuple[ThemeResolutionInfo | None, str, str]:
|
|
new_mode = normalize_theme_match_mode(mode)
|
|
current_inputs = _current_inputs(sess)
|
|
previous_mode = sess.get(_MODE_KEY)
|
|
try:
|
|
info = _resolve_and_store(sess, current_inputs, new_mode, commander_tags)
|
|
return info, f"Theme matching set to {new_mode} mode.", "success"
|
|
except ValueError as exc:
|
|
if previous_mode is not None:
|
|
sess[_MODE_KEY] = previous_mode
|
|
return None, str(exc), "error"
|
|
|
|
|
|
def clear_all(sess: Dict[str, Any]) -> None:
|
|
for key in (_INPUTS_KEY, _RESOLUTION_KEY, "additional_themes", _LAST_ADD_KEY):
|
|
if key in sess:
|
|
del sess[key]
|
|
|
|
|
|
def refresh_resolution(
|
|
sess: Dict[str, Any],
|
|
*,
|
|
commander_tags: Iterable[str],
|
|
mode: str | None = None,
|
|
) -> ThemeResolutionInfo | None:
|
|
inputs = _current_inputs(sess)
|
|
normalized_mode = normalize_theme_match_mode(mode or sess.get(_MODE_KEY))
|
|
if not inputs:
|
|
empty = ThemeResolutionInfo(
|
|
requested=[],
|
|
mode=normalized_mode,
|
|
catalog_version=sess.get(_CATALOG_VERSION_KEY, "unknown"),
|
|
resolved=[],
|
|
matches=[],
|
|
unresolved=[],
|
|
fuzzy_corrections={},
|
|
)
|
|
_store_inputs(sess, [])
|
|
_store_resolution(sess, empty)
|
|
return empty
|
|
info = _resolve_and_store(sess, inputs, normalized_mode, commander_tags)
|
|
return info
|
|
|