diff --git a/fastanime/cli/interactive/menus/main.py b/fastanime/cli/interactive/menus/main.py index 9f00ebc..fdef453 100644 --- a/fastanime/cli/interactive/menus/main.py +++ b/fastanime/cli/interactive/menus/main.py @@ -1,199 +1,220 @@ import logging import random -from typing import Callable, Dict, Tuple +from typing import Callable, Dict from ....libs.api.params import MediaSearchParams, UserMediaListSearchParams from ....libs.api.types import ( - MediaSearchResult, MediaSort, MediaStatus, UserMediaListStatus, ) from ..session import Context, session -from ..state import InternalDirective, MediaApiState, State +from ..state import InternalDirective, MediaApiState, MenuName, State logger = logging.getLogger(__name__) -MenuAction = Callable[ - [], - Tuple[ - str, - MediaSearchResult | None, - MediaSearchParams | None, - UserMediaListSearchParams | None, - ], -] +MenuAction = Callable[[], State | InternalDirective] @session.menu def main(ctx: Context, state: State) -> State | InternalDirective: - """ - The main entry point menu for the interactive session. - Displays top-level categories for the user to browse and select. - """ icons = ctx.config.general.icons feedback = ctx.services.feedback feedback.clear_console() - # TODO: Make them just return the modified state or control flow options: Dict[str, MenuAction] = { - # --- Search-based Actions --- f"{'🔥 ' if icons else ''}Trending": _create_media_list_action( - ctx, MediaSort.TRENDING_DESC + ctx, state, MediaSort.TRENDING_DESC ), - f"{'✨ ' if icons else ''}Popular": _create_media_list_action( - ctx, MediaSort.POPULARITY_DESC - ), - f"{'💖 ' if icons else ''}Favourites": _create_media_list_action( - ctx, MediaSort.FAVOURITES_DESC - ), - f"{'💯 ' if icons else ''}Top Scored": _create_media_list_action( - ctx, MediaSort.SCORE_DESC - ), - f"{'🎬 ' if icons else ''}Upcoming": _create_media_list_action( - ctx, MediaSort.POPULARITY_DESC, MediaStatus.NOT_YET_RELEASED - ), - f"{'🔔 ' if icons else ''}Recently Updated": _create_media_list_action( - ctx, MediaSort.UPDATED_AT_DESC - ), - # --- special case media list -- - f"{'🎲 ' if icons else ''}Random": _create_random_media_list(ctx), - f"{'🔎 ' if icons else ''}Search": _create_search_media_list(ctx), - # --- Authenticated User List Actions --- + f"{'🎞️ ' if icons else ''}Recent": _create_recent_media_action(ctx, state), f"{'📺 ' if icons else ''}Watching": _create_user_list_action( - ctx, UserMediaListStatus.WATCHING - ), - f"{'📑 ' if icons else ''}Planned": _create_user_list_action( - ctx, UserMediaListStatus.PLANNING - ), - f"{'✅ ' if icons else ''}Completed": _create_user_list_action( - ctx, UserMediaListStatus.COMPLETED - ), - f"{'⏸️ ' if icons else ''}Paused": _create_user_list_action( - ctx, UserMediaListStatus.PAUSED - ), - f"{'🚮 ' if icons else ''}Dropped": _create_user_list_action( - ctx, UserMediaListStatus.DROPPED + ctx, state, UserMediaListStatus.WATCHING ), f"{'🔁 ' if icons else ''}Rewatching": _create_user_list_action( - ctx, UserMediaListStatus.REPEATING + ctx, state, UserMediaListStatus.REPEATING ), - f"{'🔁 ' if icons else ''}Recent": lambda: ( - "RESULTS", - ctx.services.media_registry.get_recently_watched( - ctx.config.anilist.per_page - ), - None, - None, + f"{'⏸️ ' if icons else ''}Paused": _create_user_list_action( + ctx, state, UserMediaListStatus.PAUSED ), - f"{'📝 ' if icons else ''}Edit Config": lambda: ( - "CONFIG_EDIT", - None, - None, - None, + f"{'📑 ' if icons else ''}Planned": _create_user_list_action( + ctx, state, UserMediaListStatus.PLANNING ), - f"{'❌ ' if icons else ''}Exit": lambda: ("EXIT", None, None, None), + f"{'🔎 ' if icons else ''}Search": _create_search_media_list(ctx, state), + f"{'🔔 ' if icons else ''}Recently Updated": _create_media_list_action( + ctx, state, MediaSort.UPDATED_AT_DESC + ), + f"{'✨ ' if icons else ''}Popular": _create_media_list_action( + ctx, state, MediaSort.POPULARITY_DESC + ), + f"{'💯 ' if icons else ''}Top Scored": _create_media_list_action( + ctx, state, MediaSort.SCORE_DESC + ), + f"{'💖 ' if icons else ''}Favourites": _create_media_list_action( + ctx, state, MediaSort.FAVOURITES_DESC + ), + f"{'🎲 ' if icons else ''}Random": _create_random_media_list(ctx, state), + f"{'🎬 ' if icons else ''}Upcoming": _create_media_list_action( + ctx, state, MediaSort.POPULARITY_DESC, MediaStatus.NOT_YET_RELEASED + ), + f"{'✅ ' if icons else ''}Completed": _create_user_list_action( + ctx, state, UserMediaListStatus.COMPLETED + ), + f"{'🚮 ' if icons else ''}Dropped": _create_user_list_action( + ctx, state, UserMediaListStatus.DROPPED + ), + f"{'📝 ' if icons else ''}Edit Config": lambda: InternalDirective.CONFIG_EDIT, + f"{'❌ ' if icons else ''}Exit": lambda: InternalDirective.EXIT, } - choice_str = ctx.selector.choose( + choice = ctx.selector.choose( prompt="Select Category", choices=list(options.keys()), ) + if not choice: + return InternalDirective.MAIN - if not choice_str: - return InternalDirective.EXIT + selected_action = options[choice] - # --- Action Handling --- - selected_action = options[choice_str] - - next_menu_name, result_data, api_params, user_list_params = selected_action() - - if next_menu_name == "EXIT": - return InternalDirective.EXIT - if next_menu_name == "CONFIG_EDIT": - return InternalDirective.CONFIG_EDIT - if next_menu_name == "SESSION_MANAGEMENT": - return State(menu_name="SESSION_MANAGEMENT") - if next_menu_name == "AUTH": - return State(menu_name="AUTH") - if next_menu_name == "ANILIST_LISTS": - return State(menu_name="ANILIST_LISTS") - if next_menu_name == "WATCH_HISTORY": - return State(menu_name="WATCH_HISTORY") - if next_menu_name == "CONTINUE": - return InternalDirective.CONTINUE - - if not result_data: - feedback.error( - f"Failed to fetch data for '{choice_str.strip()}'", - "Please check your internet connection and try again.", - ) - return InternalDirective.CONTINUE - - # On success, transition to the RESULTS menu state. - return State( - menu_name="RESULTS", - media_api=MediaApiState( - search_results=result_data, - original_api_params=api_params, - original_user_list_params=user_list_params, - ), - ) + next_step = selected_action() + return next_step def _create_media_list_action( - ctx: Context, sort: MediaSort, status: MediaStatus | None = None + ctx: Context, state: State, sort: MediaSort, status: MediaStatus | None = None ) -> MenuAction: - """A factory to create menu actions for fetching media lists""" - def action(): - # Create the search parameters + feedback = ctx.services.feedback search_params = MediaSearchParams(sort=sort, status=status) - result = ctx.media_api.search_media(search_params) + loading_message = f"Fetching media list" + result = None + with feedback.progress(loading_message): + result = ctx.media_api.search_media(search_params) - return ("RESULTS", result, search_params, None) + if result: + return State( + menu_name=MenuName.RESULTS, + media_api=MediaApiState( + search_result={ + media_item.id: media_item for media_item in result.media + }, + search_params=search_params, + page_info=result.page_info, + ), + ) + else: + return InternalDirective.MAIN return action -def _create_random_media_list(ctx: Context) -> MenuAction: +def _create_random_media_list(ctx: Context, state: State) -> MenuAction: def action(): + feedback = ctx.services.feedback search_params = MediaSearchParams(id_in=random.sample(range(1, 15000), k=50)) - result = ctx.media_api.search_media(search_params) + loading_message = f"Fetching media list" + result = None + with feedback.progress(loading_message): + result = ctx.media_api.search_media(search_params) - return ("RESULTS", result, search_params, None) + if result: + return State( + menu_name=MenuName.RESULTS, + media_api=MediaApiState( + search_result={ + media_item.id: media_item for media_item in result.media + }, + search_params=search_params, + page_info=result.page_info, + ), + ) + else: + return InternalDirective.MAIN return action -def _create_search_media_list(ctx: Context) -> MenuAction: +def _create_search_media_list(ctx: Context, state: State) -> MenuAction: def action(): + feedback = ctx.services.feedback + query = ctx.selector.ask("Search for Anime") if not query: - return "CONTINUE", None, None, None + return InternalDirective.MAIN search_params = MediaSearchParams(query=query) - result = ctx.media_api.search_media(search_params) - return ("RESULTS", result, search_params, None) + loading_message = f"Fetching media list" + result = None + with feedback.progress(loading_message): + result = ctx.media_api.search_media(search_params) + + if result: + return State( + menu_name=MenuName.RESULTS, + media_api=MediaApiState( + search_result={ + media_item.id: media_item for media_item in result.media + }, + search_params=search_params, + page_info=result.page_info, + ), + ) + else: + return InternalDirective.MAIN return action -def _create_user_list_action(ctx: Context, status: UserMediaListStatus) -> MenuAction: +def _create_user_list_action( + ctx: Context, state: State, status: UserMediaListStatus +) -> MenuAction: """A factory to create menu actions for fetching user lists, handling authentication.""" def action(): - # Check authentication + feedback = ctx.services.feedback if not ctx.media_api.is_authenticated(): - logger.warning("Not authenticated") - return "CONTINUE", None, None, None + feedback.error("You haven't logged in") + return InternalDirective.MAIN - user_list_params = UserMediaListSearchParams(status=status) + search_params = UserMediaListSearchParams(status=status) - result = ctx.media_api.search_media_list(user_list_params) + loading_message = f"Fetching media list" + result = None + with feedback.progress(loading_message): + result = ctx.media_api.search_media_list(search_params) - return ("RESULTS", result, None, user_list_params) + if result: + return State( + menu_name=MenuName.RESULTS, + media_api=MediaApiState( + search_result={ + media_item.id: media_item for media_item in result.media + }, + search_params=search_params, + page_info=result.page_info, + ), + ) + else: + return InternalDirective.MAIN + + return action + + +def _create_recent_media_action(ctx: Context, state: State) -> MenuAction: + def action(): + result = ctx.services.media_registry.get_recently_watched() + if result: + return State( + menu_name=MenuName.RESULTS, + media_api=MediaApiState( + search_result={ + media_item.id: media_item for media_item in result.media + }, + page_info=result.page_info, + ), + ) + else: + return InternalDirective.MAIN return action