feat: implement restore mode for dynamic search with last query and cached results

This commit is contained in:
benexl
2025-12-31 18:43:59 +03:00
parent 6192252d10
commit b8f77d80e9
4 changed files with 74 additions and 2 deletions

View File

@@ -30,6 +30,7 @@ from _filter_parser import parse_filters
# --- Template Variables (Injected by Python) ---
GRAPHQL_ENDPOINT = "{GRAPHQL_ENDPOINT}"
SEARCH_RESULTS_FILE = Path("{SEARCH_RESULTS_FILE}")
LAST_QUERY_FILE = Path("{LAST_QUERY_FILE}")
AUTH_HEADER = "{AUTH_HEADER}"
# The GraphQL query is injected as a properly escaped JSON string
@@ -176,6 +177,9 @@ def main():
try:
with open(SEARCH_RESULTS_FILE, "w", encoding="utf-8") as f:
json.dump(response, f, ensure_ascii=False, indent=2)
# Also save the raw query so it can be restored when going back
with open(LAST_QUERY_FILE, "w", encoding="utf-8") as f:
f.write(RAW_QUERY)
except IOError as e:
print(f"❌ Failed to save results: {e}")
sys.exit(1)

View File

@@ -13,11 +13,38 @@ logger = logging.getLogger(__name__)
SEARCH_CACHE_DIR = APP_CACHE_DIR / "previews" / "dynamic-search"
SEARCH_RESULTS_FILE = SEARCH_CACHE_DIR / "current_search_results.json"
LAST_QUERY_FILE = SEARCH_CACHE_DIR / "last_query.txt"
RESTORE_MODE_FILE = SEARCH_CACHE_DIR / ".restore_mode"
FZF_SCRIPTS_DIR = SCRIPTS_DIR / "fzf"
SEARCH_TEMPLATE_SCRIPT = (FZF_SCRIPTS_DIR / "search.py").read_text(encoding="utf-8")
FILTER_PARSER_SCRIPT = FZF_SCRIPTS_DIR / "_filter_parser.py"
def _load_cached_titles() -> list[str]:
"""Load titles from cached search results for display in fzf."""
if not SEARCH_RESULTS_FILE.exists():
return []
try:
with open(SEARCH_RESULTS_FILE, "r", encoding="utf-8") as f:
data = json.load(f)
media_list = data.get("data", {}).get("Page", {}).get("media", [])
titles = []
for media in media_list:
title_obj = media.get("title", {})
title = (
title_obj.get("english")
or title_obj.get("romaji")
or title_obj.get("native")
or "Unknown"
)
titles.append(title)
return titles
except (IOError, json.JSONDecodeError):
return []
@session.menu
def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
"""Dynamic search menu that provides real-time search results."""
@@ -27,6 +54,12 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
# Ensure cache directory exists
SEARCH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
# Check if we're in restore mode (coming back from media_actions)
restore_mode = RESTORE_MODE_FILE.exists()
if restore_mode:
# Clear the restore flag
RESTORE_MODE_FILE.unlink(missing_ok=True)
# Read the GraphQL search query
from .....libs.media_api.anilist import gql
@@ -46,6 +79,7 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
"GRAPHQL_ENDPOINT": "https://graphql.anilist.co",
"GRAPHQL_QUERY": search_query_json,
"SEARCH_RESULTS_FILE": SEARCH_RESULTS_FILE.as_posix(),
"LAST_QUERY_FILE": LAST_QUERY_FILE.as_posix(),
"AUTH_HEADER": auth_header,
}
@@ -71,6 +105,19 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
# Header hint for filter syntax
filter_hint = "💡 Filters: @genre:action @status:airing @year:2024 @sort:score (type @help for more)"
# Only load previous query if we're in restore mode (coming back from media_actions)
initial_query = None
cached_results = None
if restore_mode:
# Load previous query
if LAST_QUERY_FILE.exists():
try:
initial_query = LAST_QUERY_FILE.read_text(encoding="utf-8").strip()
except IOError:
pass
# Load cached results to display immediately without network request
cached_results = _load_cached_titles()
try:
# Prepare preview functionality
preview_command = None
@@ -85,12 +132,16 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
search_command=search_command_final,
preview=preview_command,
header=filter_hint,
initial_query=initial_query,
initial_results=cached_results,
)
else:
choice = ctx.selector.search(
prompt="Search Anime",
search_command=search_command_final,
header=filter_hint,
initial_query=initial_query,
initial_results=cached_results,
)
except NotImplementedError:
feedback.error("Dynamic search is not supported by your current selector")
@@ -129,6 +180,9 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
logger.error(f"Could not find selected media for choice: {choice}")
return InternalDirective.MAIN
# Set restore mode flag so we can restore state when user goes back
RESTORE_MODE_FILE.touch()
# Navigate to media actions with the selected item
return State(
menu_name=MenuName.MEDIA_ACTIONS,

View File

@@ -88,6 +88,8 @@ class BaseSelector(ABC):
*,
preview: Optional[str] = None,
header: Optional[str] = None,
initial_query: Optional[str] = None,
initial_results: Optional[List[str]] = None,
) -> str | None:
"""
Provides dynamic search functionality that reloads results based on user input.
@@ -97,6 +99,8 @@ class BaseSelector(ABC):
search_command: The command to execute for searching/reloading results.
preview: An optional command or string for a preview window.
header: An optional header to display above the choices.
initial_query: An optional initial query to pre-populate the search.
initial_results: Optional list of results to display initially (avoids network request).
Returns:
The string of the chosen item.

View File

@@ -117,7 +117,7 @@ class FzfSelector(BaseSelector):
lines = result.stdout.strip().splitlines()
return lines[-1] if lines else (default or "")
def search(self, prompt, search_command, *, preview=None, header=None):
def search(self, prompt, search_command, *, preview=None, header=None, initial_query=None, initial_results=None):
"""Enhanced search using fzf's --reload flag for dynamic search."""
# Build the header with optional custom header line
display_header = self.header
@@ -137,12 +137,22 @@ class FzfSelector(BaseSelector):
"--ansi",
]
# If there's an initial query, set it
if initial_query:
commands.extend(["--query", initial_query])
# Only trigger reload on start if we don't have cached results
if not initial_results:
commands.extend(["--bind", f"start:reload({search_command})"])
if preview:
commands.extend(["--preview", preview])
# Use cached results as initial input if provided (avoids network request)
fzf_input = "\n".join(initial_results) if initial_results else ""
result = subprocess.run(
commands,
input="",
input=fzf_input,
stdout=subprocess.PIPE,
text=True,
encoding="utf-8",