Skip to content
This repository was archived by the owner on May 2, 2026. It is now read-only.

Commit b8f77d8

Browse files
committed
feat: implement restore mode for dynamic search with last query and cached results
1 parent 6192252 commit b8f77d8

4 files changed

Lines changed: 74 additions & 2 deletions

File tree

viu_media/assets/scripts/fzf/search.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
# --- Template Variables (Injected by Python) ---
3131
GRAPHQL_ENDPOINT = "{GRAPHQL_ENDPOINT}"
3232
SEARCH_RESULTS_FILE = Path("{SEARCH_RESULTS_FILE}")
33+
LAST_QUERY_FILE = Path("{LAST_QUERY_FILE}")
3334
AUTH_HEADER = "{AUTH_HEADER}"
3435

3536
# The GraphQL query is injected as a properly escaped JSON string
@@ -176,6 +177,9 @@ def main():
176177
try:
177178
with open(SEARCH_RESULTS_FILE, "w", encoding="utf-8") as f:
178179
json.dump(response, f, ensure_ascii=False, indent=2)
180+
# Also save the raw query so it can be restored when going back
181+
with open(LAST_QUERY_FILE, "w", encoding="utf-8") as f:
182+
f.write(RAW_QUERY)
179183
except IOError as e:
180184
print(f"❌ Failed to save results: {e}")
181185
sys.exit(1)

viu_media/cli/interactive/menu/media/dynamic_search.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,38 @@
1313

1414
SEARCH_CACHE_DIR = APP_CACHE_DIR / "previews" / "dynamic-search"
1515
SEARCH_RESULTS_FILE = SEARCH_CACHE_DIR / "current_search_results.json"
16+
LAST_QUERY_FILE = SEARCH_CACHE_DIR / "last_query.txt"
17+
RESTORE_MODE_FILE = SEARCH_CACHE_DIR / ".restore_mode"
1618
FZF_SCRIPTS_DIR = SCRIPTS_DIR / "fzf"
1719
SEARCH_TEMPLATE_SCRIPT = (FZF_SCRIPTS_DIR / "search.py").read_text(encoding="utf-8")
1820
FILTER_PARSER_SCRIPT = FZF_SCRIPTS_DIR / "_filter_parser.py"
1921

2022

23+
def _load_cached_titles() -> list[str]:
24+
"""Load titles from cached search results for display in fzf."""
25+
if not SEARCH_RESULTS_FILE.exists():
26+
return []
27+
28+
try:
29+
with open(SEARCH_RESULTS_FILE, "r", encoding="utf-8") as f:
30+
data = json.load(f)
31+
32+
media_list = data.get("data", {}).get("Page", {}).get("media", [])
33+
titles = []
34+
for media in media_list:
35+
title_obj = media.get("title", {})
36+
title = (
37+
title_obj.get("english")
38+
or title_obj.get("romaji")
39+
or title_obj.get("native")
40+
or "Unknown"
41+
)
42+
titles.append(title)
43+
return titles
44+
except (IOError, json.JSONDecodeError):
45+
return []
46+
47+
2148
@session.menu
2249
def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
2350
"""Dynamic search menu that provides real-time search results."""
@@ -27,6 +54,12 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
2754
# Ensure cache directory exists
2855
SEARCH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
2956

57+
# Check if we're in restore mode (coming back from media_actions)
58+
restore_mode = RESTORE_MODE_FILE.exists()
59+
if restore_mode:
60+
# Clear the restore flag
61+
RESTORE_MODE_FILE.unlink(missing_ok=True)
62+
3063
# Read the GraphQL search query
3164
from .....libs.media_api.anilist import gql
3265

@@ -46,6 +79,7 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
4679
"GRAPHQL_ENDPOINT": "https://graphql.anilist.co",
4780
"GRAPHQL_QUERY": search_query_json,
4881
"SEARCH_RESULTS_FILE": SEARCH_RESULTS_FILE.as_posix(),
82+
"LAST_QUERY_FILE": LAST_QUERY_FILE.as_posix(),
4983
"AUTH_HEADER": auth_header,
5084
}
5185

@@ -71,6 +105,19 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
71105
# Header hint for filter syntax
72106
filter_hint = "💡 Filters: @genre:action @status:airing @year:2024 @sort:score (type @help for more)"
73107

108+
# Only load previous query if we're in restore mode (coming back from media_actions)
109+
initial_query = None
110+
cached_results = None
111+
if restore_mode:
112+
# Load previous query
113+
if LAST_QUERY_FILE.exists():
114+
try:
115+
initial_query = LAST_QUERY_FILE.read_text(encoding="utf-8").strip()
116+
except IOError:
117+
pass
118+
# Load cached results to display immediately without network request
119+
cached_results = _load_cached_titles()
120+
74121
try:
75122
# Prepare preview functionality
76123
preview_command = None
@@ -85,12 +132,16 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
85132
search_command=search_command_final,
86133
preview=preview_command,
87134
header=filter_hint,
135+
initial_query=initial_query,
136+
initial_results=cached_results,
88137
)
89138
else:
90139
choice = ctx.selector.search(
91140
prompt="Search Anime",
92141
search_command=search_command_final,
93142
header=filter_hint,
143+
initial_query=initial_query,
144+
initial_results=cached_results,
94145
)
95146
except NotImplementedError:
96147
feedback.error("Dynamic search is not supported by your current selector")
@@ -129,6 +180,9 @@ def dynamic_search(ctx: Context, state: State) -> State | InternalDirective:
129180
logger.error(f"Could not find selected media for choice: {choice}")
130181
return InternalDirective.MAIN
131182

183+
# Set restore mode flag so we can restore state when user goes back
184+
RESTORE_MODE_FILE.touch()
185+
132186
# Navigate to media actions with the selected item
133187
return State(
134188
menu_name=MenuName.MEDIA_ACTIONS,

viu_media/libs/selectors/base.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ def search(
8888
*,
8989
preview: Optional[str] = None,
9090
header: Optional[str] = None,
91+
initial_query: Optional[str] = None,
92+
initial_results: Optional[List[str]] = None,
9193
) -> str | None:
9294
"""
9395
Provides dynamic search functionality that reloads results based on user input.
@@ -97,6 +99,8 @@ def search(
9799
search_command: The command to execute for searching/reloading results.
98100
preview: An optional command or string for a preview window.
99101
header: An optional header to display above the choices.
102+
initial_query: An optional initial query to pre-populate the search.
103+
initial_results: Optional list of results to display initially (avoids network request).
100104
101105
Returns:
102106
The string of the chosen item.

viu_media/libs/selectors/fzf/selector.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ def ask(self, prompt, *, default=None):
117117
lines = result.stdout.strip().splitlines()
118118
return lines[-1] if lines else (default or "")
119119

120-
def search(self, prompt, search_command, *, preview=None, header=None):
120+
def search(self, prompt, search_command, *, preview=None, header=None, initial_query=None, initial_results=None):
121121
"""Enhanced search using fzf's --reload flag for dynamic search."""
122122
# Build the header with optional custom header line
123123
display_header = self.header
@@ -137,12 +137,22 @@ def search(self, prompt, search_command, *, preview=None, header=None):
137137
"--ansi",
138138
]
139139

140+
# If there's an initial query, set it
141+
if initial_query:
142+
commands.extend(["--query", initial_query])
143+
# Only trigger reload on start if we don't have cached results
144+
if not initial_results:
145+
commands.extend(["--bind", f"start:reload({search_command})"])
146+
140147
if preview:
141148
commands.extend(["--preview", preview])
142149

150+
# Use cached results as initial input if provided (avoids network request)
151+
fzf_input = "\n".join(initial_results) if initial_results else ""
152+
143153
result = subprocess.run(
144154
commands,
145-
input="",
155+
input=fzf_input,
146156
stdout=subprocess.PIPE,
147157
text=True,
148158
encoding="utf-8",

0 commit comments

Comments
 (0)