mal/
app.rs

1#![allow(clippy::new_without_default)]
2#![allow(clippy::large_enum_variant)]
3use crate::api::{self, model::*};
4use crate::config::app_config::AppConfig;
5use crate::network::IoEvent;
6use chrono::Datelike;
7use image::{DynamicImage, ImageError};
8use ratatui::layout::Rect;
9use ratatui::style::Style;
10use ratatui::widgets::{Block, Borders};
11use ratatui::Frame;
12use ratatui_image::picker::Picker;
13use ratatui_image::protocol::StatefulProtocol;
14use std::collections::{HashMap, HashSet};
15use std::fmt::Debug;
16use std::sync::mpsc::Sender;
17use tui_logger::{TuiLoggerWidget, TuiWidgetState};
18
19use strum_macros::IntoStaticStr;
20use tracing::warn;
21use tui_scrollview::ScrollViewState;
22const DEFAULT_ROUTE: Route = Route {
23    data: None,
24    block: ActiveDisplayBlock::Empty, //afterTest: change to empty
25    title: String::new(),
26    image: None,
27};
28
29pub const DISPLAY_RAWS_NUMBER: usize = 5;
30
31pub const SEASONS: [&str; 4] = ["Winter", "Spring", "Summer", "Fall"];
32
33pub const DISPLAY_COLUMN_NUMBER: usize = 3;
34
35pub const ANIME_OPTIONS: [&str; 3] = ["Seasonal", "Ranking", "Suggested"];
36
37pub const USER_OPTIONS: [&str; 3] = ["Stats", "AnimeList", "MangaList"];
38
39pub const GENERAL_OPTIONS: [&str; 3] = ["Help", "About", "Quit"];
40
41pub const USER_WATCH_STATUS: [&str; 5] = [
42    "Watching",
43    "Completed",
44    "On Hold",
45    "Dropped",
46    "Plan to Watch",
47];
48pub const USER_READ_STATUS: [&str; 5] =
49    ["Reading", "Completed", "On Hold", "Dropped", "Plan To Read"];
50
51pub const ANIME_OPTIONS_RANGE: std::ops::Range<usize> = 0..3;
52
53pub const USER_OPTIONS_RANGE: std::ops::Range<usize> = 3..6;
54
55pub const GENERAL_OPTIONS_RANGE: std::ops::Range<usize> = 6..9;
56
57pub const RATING_OPTIONS: [&str; 11] = [
58    "None",
59    "(1) Appalling",
60    "(2) Horrible",
61    "(3) Very Bad",
62    "(4) Bad",
63    "(5) Average",
64    "(6) Fi\ne",
65    "(7) Good",
66    "(8) Very Good",
67    "(9) Great",
68    "(10) Masterpiece",
69];
70
71pub const ANIME_RANKING_TYPES: [&str; 9] = [
72    "All",
73    "Airing",
74    "Upcoming",
75    "Movie",
76    "Popularity",
77    "Special",
78    "TV",
79    "OVA",
80    "Favorite",
81];
82
83pub const MANGA_RANKING_TYPES: [&str; 9] = [
84    "All",
85    "Manga",
86    "Manhwa",
87    "Popularity",
88    "Novels",
89    "Oneshots",
90    "Doujin",
91    "Manhua",
92    "Favorite",
93];
94
95#[derive(Clone, Copy, PartialEq, Debug)]
96pub enum ActiveBlock {
97    Input,
98    DisplayBlock,
99    Anime,
100    Option,
101    User,
102    TopThree,
103    Error,
104}
105
106#[derive(Clone, Copy, PartialEq, Debug)]
107pub enum ActiveDisplayBlock {
108    SearchResultBlock,
109    Help,
110    UserInfo,
111    UserAnimeList,
112    UserMangaList,
113    Suggestions,
114    Seasonal,
115    AnimeRanking,
116    MangaRanking,
117    Loading,
118    Error,
119    Empty,
120    AnimeDetails,
121    MangaDetails,
122}
123#[derive(Clone, Copy, PartialEq, Debug)]
124pub enum SelectedSearchTab {
125    Anime,
126    Manga,
127}
128
129#[derive(Debug, Clone)]
130pub struct SearchResult {
131    pub anime: Option<Page<Anime>>,
132    pub manga: Option<Page<Manga>>,
133    pub selected_tab: SelectedSearchTab,
134    pub selected_display_card_index: Option<usize>,
135    pub max_index: u16,
136    pub max_page: u16,
137}
138
139#[derive(Clone)]
140pub struct ScrollablePages<T> {
141    index: usize,
142    pages: Vec<T>,
143}
144
145impl<T> ScrollablePages<T> {
146    pub fn new() -> Self {
147        Self {
148            index: 0,
149            pages: vec![],
150        }
151    }
152
153    pub fn get_results(&self, at_index: Option<usize>) -> Option<&T> {
154        self.pages.get(at_index.unwrap_or(self.index))
155    }
156
157    pub fn get_mut_results(&mut self, at_index: Option<usize>) -> Option<&mut T> {
158        self.pages.get_mut(at_index.unwrap_or(self.index))
159    }
160
161    pub fn add_pages(&mut self, new_pages: T) {
162        self.pages.push(new_pages);
163        self.index = self.pages.len() - 1;
164    }
165}
166
167pub struct Library {
168    pub selected_index: usize,
169    pub saved_anime: ScrollablePages<Page<Anime>>,
170    pub saved_manga: ScrollablePages<Page<Manga>>,
171}
172
173#[derive(Debug)]
174pub struct Navigator {
175    pub history: Vec<u16>,
176    pub index: usize,
177    pub data: HashMap<u16, Route>,
178    pub last_id: u16,
179}
180
181impl Navigator {
182    // explain every thing about the navigation system:
183    /*
184    navigator has the home route at initialization
185    it has a history of routes, where the first route is always the home route
186    each page has an id and it is mapped to its data in the data hashmap
187    */
188    pub fn new() -> Self {
189        let mut data = HashMap::new();
190        data.insert(0, DEFAULT_ROUTE);
191        Self {
192            history: vec![0],
193            index: 0,
194            data,
195            last_id: 0,
196        }
197    }
198
199    pub fn add_existing_route(&mut self, id: u16) {
200        self.history.push(id);
201        self.index = self.history.len() - 1;
202    }
203
204    pub fn add_route(&mut self, r: Route) {
205        self.last_id += 1;
206        self.data.insert(self.last_id, r);
207        self.history.push(self.last_id);
208        self.index = self.history.len() - 1;
209    }
210    pub fn validate_state(&self) -> bool {
211        // Check if index is within bounds
212        if self.index >= self.history.len() {
213            warn!(
214                "Navigation state invalid: index {} >= history length {}",
215                self.index,
216                self.history.len()
217            );
218            return false;
219        }
220
221        // // Check if current route ID exists in data
222        // let current_id = self.history[self.index];
223        // if !self.data.contains_key(&current_id) {
224        //     println!("Navigation state invalid: current route ID {} not in data map",
225        //              current_id);
226        //     return false;
227        // }
228
229        // Check if all history route IDs exist in data
230        for &route_id in &self.history {
231            if !self.data.contains_key(&route_id) {
232                warn!(
233                    "Navigation state invalid: history route ID {} not in data map",
234                    route_id
235                );
236                return false;
237            }
238        }
239
240        true
241    }
242    pub fn remove_old_history(&mut self) {
243        // when the history length exceeds the limit, we remove the oldest page which is 1 (0 is the home page)
244
245        self.history.remove(1);
246        self.clear_unused_data();
247    }
248
249    /// Removes route data for routes that are no longer in the navigation history.
250    /// This prevents memory leaks by cleaning up orphaned route data after history modifications.
251    /// Should be called after operations that remove entries from the history vector.
252    pub fn clear_unused_data(&mut self) {
253        let active_routes: HashSet<u16> = self.history.iter().copied().collect();
254        self.data.retain(|k, _| active_routes.contains(k));
255    }
256
257    pub fn get_current_title(&self) -> &String {
258        let id = self.history[self.index];
259        &self.data[&id].title
260    }
261
262    pub fn get_current_block(&self) -> ActiveDisplayBlock {
263        let id = self.history[self.index];
264        self.data[&id].block
265    }
266}
267
268pub struct App {
269    pub io_tx: Option<Sender<IoEvent>>,
270    pub app_config: AppConfig,
271    pub is_loading: bool,
272    pub api_error: String,
273    pub search_results: SearchResult,
274    pub size: Rect,
275    pub input: Vec<char>,
276    pub input_cursor_position: u16,
277    pub input_idx: usize,
278    pub library: Library,
279    pub help_menu_offset: u32,
280    pub help_menu_page: u32,
281    pub help_menu_max_lines: u32,
282    pub help_docs_size: u32,
283    // logger:
284    pub logger_state: TuiWidgetState,
285    // exit:
286    pub exit_flag: bool,
287    pub exit_confirmation_popup: bool,
288    // image:
289    pub picker: Option<Picker>,
290    pub media_image: Option<(String, u32, u32)>,
291    pub image_state: Option<StatefulProtocol>,
292    // state:
293    pub active_block: ActiveBlock,
294    pub active_display_block: ActiveDisplayBlock,
295    pub navigator: Navigator,
296    pub display_block_title: String,
297    pub popup: bool,
298    pub anime_details_synopsys_scroll_view_state: ScrollViewState,
299    pub anime_details_info_scroll_view_state: ScrollViewState,
300    pub manga_details_info_scroll_view_state: ScrollViewState,
301    pub manga_details_synopsys_scroll_view_state: ScrollViewState,
302    // top three bar:
303    pub top_three_anime: TopThreeAnime,
304    pub top_three_manga: TopThreeManga,
305    pub active_top_three: TopThreeBlock,
306    pub active_top_three_anime: Option<AnimeRankingType>,
307    pub active_top_three_manga: Option<MangaRankingType>,
308    pub selected_top_three: u32,
309    pub available_anime_ranking_types: Vec<AnimeRankingType>,
310    pub available_manga_ranking_types: Vec<MangaRankingType>,
311    pub active_anime_rank_index: u32,
312    pub active_manga_rank_index: u32,
313    // detail
314    pub anime_details: Option<Anime>,
315    pub manga_details: Option<Manga>,
316    pub active_detail_popup: DetailPopup,
317    pub active_anime_detail_block: ActiveAnimeDetailBlock,
318    pub active_manga_detail_block: ActiveMangaDetailBlock,
319    // detail popup
320    pub popup_post_req_success: bool,
321    pub result_popup: bool,
322    pub popup_is_loading: bool,
323    pub popup_post_req_success_message: Option<String>,
324    pub selected_popup_status: u8,
325    pub selected_popup_rate: u8,
326    pub temp_popup_num: u16,
327    // seasonal
328    pub anime_season: Seasonal,
329    //ranking
330    pub anime_ranking_data: Option<Ranking<RankingAnimePair>>,
331    pub anime_ranking_type: AnimeRankingType,
332    pub manga_ranking_data: Option<Ranking<RankingMangaPair>>,
333    pub manga_ranking_type: MangaRankingType,
334    pub anime_ranking_type_index: u8,
335    pub manga_ranking_type_index: u8,
336    //profile:
337    pub user_profile: Option<UserInfo>,
338    // use UserWatchStatus to determine the current tab
339    pub anime_list_status: Option<UserWatchStatus>,
340    // use UserReadStatus to determine the current tab
341    pub manga_list_status: Option<UserReadStatus>,
342    // to track pagination (with local data)
343    pub start_card_list_index: u16,
344}
345#[derive(Debug, Clone)]
346pub enum DetailPopup {
347    AddToList,
348    Rate,
349    Episodes,
350    Chapters,
351    Volumes,
352}
353
354#[derive(Debug, Clone, PartialEq)]
355pub enum ActiveAnimeDetailBlock {
356    Synopsis,
357    SideInfo,
358    AddToList,
359    Rate,
360    Episodes,
361}
362
363#[derive(Debug, Clone, PartialEq)]
364pub enum ActiveMangaDetailBlock {
365    Synopsis,
366    SideInfo,
367    AddToList,
368    Rate,
369    Chapters,
370    Volumes,
371}
372
373pub struct Seasonal {
374    pub anime_season: AnimeSeason,
375    pub popup_season_highlight: bool,
376    pub anime_sort: SortStyle,
377    pub selected_season: u8,
378    pub selected_year: u16,
379}
380
381#[derive(Debug, Clone, IntoStaticStr)]
382pub enum TopThreeBlock {
383    Anime(AnimeRankingType),
384    Manga(MangaRankingType),
385    Loading(RankingType),
386    Error(RankingType),
387}
388
389#[derive(Debug, Clone, Default)]
390pub struct TopThreeManga {
391    pub all: Option<[Manga; 3]>,
392    pub manga: Option<[Manga; 3]>,
393    pub novels: Option<[Manga; 3]>,
394    pub oneshots: Option<[Manga; 3]>,
395    pub doujin: Option<[Manga; 3]>,
396    pub manhwa: Option<[Manga; 3]>,
397    pub manhua: Option<[Manga; 3]>,
398    pub popular: Option<[Manga; 3]>,
399    pub favourite: Option<[Manga; 3]>,
400}
401
402#[derive(Debug, Clone, Default)]
403pub struct TopThreeAnime {
404    pub airing: Option<[Anime; 3]>,
405    pub upcoming: Option<[Anime; 3]>,
406    pub popular: Option<[Anime; 3]>,
407    pub all: Option<[Anime; 3]>,
408    pub tv: Option<[Anime; 3]>,
409    pub ova: Option<[Anime; 3]>,
410    pub movie: Option<[Anime; 3]>,
411    pub special: Option<[Anime; 3]>,
412    pub favourite: Option<[Anime; 3]>,
413}
414
415#[allow(clippy::large_enum_variant)]
416#[derive(Debug, Clone)]
417pub enum Data {
418    SearchResult(SearchResult),
419    Suggestions(SearchResult),
420    UserInfo(UserInfo),
421    Anime(Anime),
422    Manga(Manga),
423    UserAnimeList(UserAnimeList),
424    UserMangaList(UserMangaList),
425    AnimeRanking(Ranking<RankingAnimePair>),
426    MangaRanking(Ranking<RankingMangaPair>),
427}
428
429#[derive(Debug, Clone)]
430pub struct UserAnimeList {
431    pub anime_list: Page<Anime>,
432    pub status: Option<UserWatchStatus>,
433}
434#[derive(Debug, Clone)]
435pub struct UserMangaList {
436    pub manga_list: Page<Manga>,
437    pub status: Option<UserReadStatus>,
438}
439
440#[derive(Debug, Clone)]
441pub struct Route {
442    pub data: Option<Data>,
443    pub block: ActiveDisplayBlock,
444    pub title: String,
445    pub image: Option<(String, u32, u32)>,
446}
447
448impl App {
449    pub fn new(io_tx: Sender<IoEvent>, app_config: AppConfig) -> Self {
450        // let can_render =
451
452        let year = chrono::Utc::now().year();
453        let season = get_season();
454        let selected_season = get_selected_season(&season);
455        let picker_res = Picker::from_query_stdio();
456        let mut picker: Option<Picker> = None;
457        if picker_res.is_ok() {
458            picker = Some(picker_res.unwrap());
459        }
460        Self {
461            io_tx: Some(io_tx),
462            anime_season: Seasonal {
463                anime_season: AnimeSeason {
464                    year: year as u64,
465                    season,
466                },
467                anime_sort: SortStyle::ListScore,
468                popup_season_highlight: true,
469                selected_season,
470                selected_year: year as u16,
471            },
472            // logger:
473            logger_state: TuiWidgetState::default().set_default_display_level(app_config.log_level),
474
475            available_anime_ranking_types: app_config.top_three_anime_types.clone(),
476            active_top_three: TopThreeBlock::Anime(app_config.top_three_anime_types[0].clone()),
477            available_manga_ranking_types: app_config.top_three_manga_types.clone(),
478            app_config,
479            is_loading: false,
480            api_error: String::new(),
481            search_results: SearchResult {
482                anime: None,
483                manga: None,
484                selected_display_card_index: Some(0),
485                selected_tab: SelectedSearchTab::Anime,
486                max_index: 15,
487                max_page: 0,
488            },
489            size: Rect::default(),
490            input: vec![],
491            input_cursor_position: 0,
492            input_idx: 0,
493            library: Library {
494                saved_anime: ScrollablePages::new(),
495                saved_manga: ScrollablePages::new(),
496                selected_index: 9, // out of range to show nothing
497            },
498            help_menu_offset: 0,
499            help_menu_page: 0,
500            help_menu_max_lines: 0,
501            help_docs_size: 0,
502            active_block: ActiveBlock::DisplayBlock,
503            active_display_block: DEFAULT_ROUTE.block,
504            navigator: Navigator::new(),
505            // top three
506            top_three_anime: TopThreeAnime::default(),
507            top_three_manga: TopThreeManga::default(),
508            selected_top_three: 0, // out of index to select nothing
509            active_top_three_anime: None,
510            active_top_three_manga: None,
511            active_anime_rank_index: 0,
512            active_manga_rank_index: 0,
513            // ranking page
514            anime_ranking_data: None,
515            anime_ranking_type: AnimeRankingType::All,
516            anime_ranking_type_index: 0,
517            manga_ranking_data: None,
518            manga_ranking_type: MangaRankingType::All,
519            manga_ranking_type_index: 0,
520            // anime list
521            anime_list_status: None,
522            // manga list
523            manga_list_status: None,
524            // detail
525            active_detail_popup: DetailPopup::AddToList,
526            active_anime_detail_block: ActiveAnimeDetailBlock::Synopsis,
527            active_manga_detail_block: ActiveMangaDetailBlock::Synopsis,
528            anime_details: None,
529            manga_details: None,
530            user_profile: None,
531            display_block_title: String::new(),
532            // detail popup
533            selected_popup_status: 0,
534            selected_popup_rate: 0,
535            temp_popup_num: 0,
536            popup_post_req_success: false,
537            popup_post_req_success_message: None,
538            popup_is_loading: false,
539            result_popup: false,
540            popup: false,
541            // image:
542            media_image: None,
543            picker,
544            image_state: None,
545            anime_details_synopsys_scroll_view_state: ScrollViewState::default(),
546            anime_details_info_scroll_view_state: ScrollViewState::default(),
547            manga_details_info_scroll_view_state: ScrollViewState::default(),
548            manga_details_synopsys_scroll_view_state: ScrollViewState::default(),
549            start_card_list_index: 0,
550            // exit:
551            exit_flag: false,
552            exit_confirmation_popup: false,
553        }
554    }
555
556    pub fn render_logs(&mut self, f: &mut Frame, area: ratatui::layout::Rect) {
557        let logs = TuiLoggerWidget::default()
558            .block(Block::default().title("Logs").borders(Borders::ALL))
559            .style(Style::default().fg(self.app_config.theme.text))
560            .state(&self.logger_state);
561        f.render_widget(logs, area);
562    }
563
564    pub fn write_error(&mut self, e: api::Error) {
565        match e {
566            api::Error::NoAuth => {
567                self.api_error = "Auth Error, Please reload the App".to_string();
568            }
569            api::Error::TimedOut => {
570                self.api_error = "Conntection Timed Out, Please try again".to_string();
571            }
572            api::Error::Unknown => {
573                self.api_error = "Check you internet connection".to_string();
574            }
575            api::Error::NoBody => {
576                self.api_error = "there is No Body".to_string();
577            }
578            api::Error::ParseError(e) => {
579                self.api_error = format!("Parse Error: {}", e);
580            }
581            api::Error::QuerySerializeError(e) => {
582                self.api_error = format!("Query Serialize Error: {}", e);
583            }
584            api::Error::HttpError(e) => {
585                self.api_error = format!("Http Error: {}", e);
586            }
587        }
588    }
589
590    pub fn get_top_three(&mut self) {
591        let _ = &self.dispatch(IoEvent::GetTopThree(self.active_top_three.clone()));
592    }
593
594    pub fn dispatch(&mut self, event: IoEvent) {
595        self.is_loading = true;
596        if let Some(io_tx) = &self.io_tx {
597            if let Err(e) = io_tx.send(event) {
598                self.is_loading = false;
599                warn!("Error from dispatch {}", e);
600            }
601        };
602    }
603
604    pub fn clear_route_before_push(&mut self) {
605        // here we take the current index (position) and delete everything after it in the history
606        let index = self.navigator.index;
607
608        if index < self.navigator.history.len() - 1 {
609            for _ in index + 1..self.navigator.history.len() {
610                self.navigator.history.pop();
611            }
612        }
613        self.navigator.clear_unused_data();
614    }
615
616    fn push_existing_route(&mut self, id: u16) {
617        //
618        if !self.navigator.data.contains_key(&id) {
619            warn!("Route ID {} does not exist in data map, cannot push", id);
620            self.navigator.index = 0; // reset index to home
621            return;
622        }
623        self.clear_route_before_push();
624        self.navigator.add_existing_route(id);
625    }
626
627    pub fn push_navigation_stack(&mut self, r: Route) {
628        self.clear_route_before_push();
629        self.navigator.add_route(r);
630        self.remove_old_history();
631    }
632
633    fn remove_old_history(&mut self) {
634        // when the history length exceeds the limit, we remove the oldest page wich is 1 (0 is the home page)
635        if self.navigator.history.len() - 1 > self.app_config.navigation_stack_limit as usize {
636            self.navigator.remove_old_history();
637        }
638    }
639
640    pub fn get_current_route(&self) -> Option<&Route> {
641        let index = self.navigator.index;
642
643        // Ensure the index is within bounds
644        if index >= self.navigator.history.len() {
645            warn!("Error: Navigation index {} is out of bounds", index);
646            return None;
647        }
648
649        let id = self.navigator.history[index];
650
651        // Ensure the route ID exists in the data map
652        match self.navigator.data.get(&id) {
653            Some(route) => Some(route),
654            None => {
655                warn!("Error: Route ID {} not found in data map", id);
656                None
657            }
658        }
659    }
660
661    pub fn calculate_help_menu_offset(&mut self) {
662        let old_offset = self.help_menu_offset;
663        if self.help_menu_max_lines < self.help_docs_size {
664            self.help_menu_offset = self.help_menu_page * self.help_menu_max_lines;
665        }
666        if self.help_menu_offset > self.help_docs_size {
667            self.help_menu_offset = old_offset;
668            self.help_menu_page -= 1;
669        }
670    }
671
672    pub fn load_previous_route(&mut self) {
673        if self.popup {
674            // reset everything
675            self.popup = false;
676            self.result_popup = false;
677            self.popup_post_req_success = false;
678            self.popup_post_req_success_message = None;
679            return;
680        }
681
682        if self.navigator.index == 1 {
683            self.active_display_block = ActiveDisplayBlock::Empty;
684            self.display_block_title = "Home".to_string();
685            self.navigator.index = 0;
686            return;
687        }
688
689        if self.active_display_block == ActiveDisplayBlock::Loading {
690            return;
691        }
692
693        if self.active_display_block == ActiveDisplayBlock::Error
694            || self.active_display_block == ActiveDisplayBlock::Help
695        {
696            self.active_display_block = self.navigator.get_current_block();
697            return;
698        }
699        if self.navigator.index == 0 {
700            return;
701        }
702        let i = self.navigator.index.saturating_sub(1);
703        self.load_state_data(i);
704    }
705
706    pub fn load_next_route(&mut self) {
707        if self.popup {
708            return;
709        }
710        if self.navigator.index >= self.navigator.history.len() {
711            // if we exceeded the history length, we reset the index to the last route
712            warn!("Navigator index exceeded history length, resetting to last route");
713            self.navigator.index = self.navigator.history.len().saturating_sub(2);
714        }
715
716        if self.navigator.index == self.navigator.history.len() - 1 {
717            // if we are at the last route, we do nothing
718            return;
719        }
720
721        self.load_state_data(self.navigator.index + 1);
722    }
723
724    pub fn load_route(&mut self, id: u16) {
725        self.push_existing_route(id);
726        self.load_state_data(self.navigator.history.len() - 1);
727    }
728
729    fn load_state_data(&mut self, i: usize) {
730        warn!("{:?}", &self.navigator.history);
731        if !self.navigator.validate_state() {
732            warn!("Invalid navigation state");
733            self.navigator.index = 0; // reset index to home
734            return;
735        }
736        if i >= self.navigator.history.len() {
737            return;
738        }
739        let route_id = self.navigator.history[i];
740        if !self.navigator.data.contains_key(&route_id) {
741            warn!(
742                "Error: Route ID {} not found in data map when loading state",
743                route_id
744            );
745            self.navigator.index = 0; // reset index to home
746            self.navigator.history.remove(i);
747            return;
748        }
749        self.navigator.index = i;
750        let route = match self.get_current_route() {
751            Some(route) => route.clone(),
752            None => return,
753        };
754
755        let data = route.data.clone();
756        match data {
757            Some(data) => {
758                match data {
759                    Data::SearchResult(d) => {
760                        self.search_results.anime = d.anime.clone();
761                        self.search_results.manga = d.manga.clone();
762                    }
763
764                    Data::Suggestions(d) => {
765                        self.search_results = d.clone();
766                    }
767
768                    Data::Anime(d) => {
769                        // self.set_image_from_route(route.as_ref().unwrap(), Some(d.clone()));
770                        self.anime_details = Some(d.clone());
771
772                        if let Some(image) = &route.image {
773                            self.media_image = Some(image.clone());
774                            self.image_state = Some(
775                                self.picker
776                                    .as_ref()
777                                    .unwrap()
778                                    .new_resize_protocol(self.get_picture_from_cache().unwrap()),
779                            );
780                        }
781                    }
782
783                    Data::Manga(d) => {
784                        self.manga_details = Some(d.clone());
785                        if let Some(image) = &route.image {
786                            self.media_image = Some(image.clone());
787                            self.image_state = Some(
788                                self.picker
789                                    .as_ref()
790                                    .unwrap()
791                                    .new_resize_protocol(self.get_picture_from_cache().unwrap()),
792                            );
793                        }
794                    }
795
796                    Data::AnimeRanking(d) => {
797                        self.anime_ranking_data = Some(d.clone());
798                    }
799
800                    Data::MangaRanking(d) => {
801                        self.manga_ranking_data = Some(d.clone());
802                    }
803
804                    Data::UserInfo(d) => self.user_profile = Some(d.clone()),
805
806                    Data::UserAnimeList(d) => {
807                        self.anime_list_status = d.status.clone();
808                        self.search_results.anime = Some(d.anime_list.clone());
809                    }
810
811                    Data::UserMangaList(d) => {
812                        self.manga_list_status = d.status.clone();
813                        self.search_results.manga = Some(d.manga_list.clone());
814                    }
815                }
816
817                self.active_display_block = self.navigator.get_current_block();
818                self.display_block_title = self.navigator.get_current_title().clone();
819                self.active_block = ActiveBlock::DisplayBlock;
820            }
821
822            None => {
823                self.active_display_block = ActiveDisplayBlock::Empty;
824                self.display_block_title = "No data".to_string();
825            }
826        }
827    }
828
829    pub fn next_anime_list_status(&self) -> Option<UserWatchStatus> {
830        match &self.anime_list_status {
831            Some(s) => match s {
832                UserWatchStatus::Watching => Some(UserWatchStatus::Completed),
833                UserWatchStatus::Completed => Some(UserWatchStatus::OnHold),
834                UserWatchStatus::OnHold => Some(UserWatchStatus::Dropped),
835                UserWatchStatus::Dropped => Some(UserWatchStatus::PlanToWatch),
836                UserWatchStatus::PlanToWatch => None,
837                UserWatchStatus::Other(_) => None,
838            },
839            None => Some(UserWatchStatus::Watching),
840        }
841    }
842
843    pub fn previous_anime_list_status(&self) -> Option<UserWatchStatus> {
844        match &self.anime_list_status {
845            Some(s) => match s {
846                UserWatchStatus::Watching => None,
847                UserWatchStatus::Completed => Some(UserWatchStatus::Watching),
848                UserWatchStatus::OnHold => Some(UserWatchStatus::Completed),
849                UserWatchStatus::Dropped => Some(UserWatchStatus::OnHold),
850                UserWatchStatus::PlanToWatch => Some(UserWatchStatus::Dropped),
851                UserWatchStatus::Other(_) => Some(UserWatchStatus::PlanToWatch),
852            },
853            None => Some(UserWatchStatus::Watching),
854        }
855    }
856
857    pub fn get_picture_from_cache(&self) -> Result<DynamicImage, ImageError> {
858        // all images are stored in $HOME?/.cache/mal-cli/images/
859        let file_name = self.media_image.as_ref().unwrap().0.clone();
860        let file_path = self.app_config.paths.picture_cache_dir_path.join(file_name);
861        let image = image::ImageReader::open(file_path)?.decode()?;
862        Ok(image)
863    }
864    pub fn reset_result_index(&mut self) {
865        // reset the selected index in the search results
866        self.search_results.selected_display_card_index = Some(0);
867        self.start_card_list_index = 0;
868    }
869}
870
871fn get_season() -> Season {
872    let month = chrono::Utc::now().month();
873    match month {
874        3..=5 => Season::Spring,
875        6..=8 => Season::Summer,
876        9..=11 => Season::Fall,
877        _ => Season::Winter,
878    }
879}
880
881fn get_selected_season(season: &Season) -> u8 {
882    match *season {
883        Season::Winter => 0,
884        Season::Spring => 1,
885        Season::Summer => 2,
886        Season::Fall => 3,
887        Season::Other(_) => panic!("no season selected"),
888    }
889}
890
891#[cfg(test)]
892pub mod test {
893    use super::*;
894    use crate::config::app_config::AppConfig;
895    pub fn get_app() -> App {
896        let config = AppConfig::load();
897        let (sync_io_tx, _) = std::sync::mpsc::channel::<IoEvent>();
898
899        let mut app = App::new(sync_io_tx, config.unwrap());
900        let route = Route {
901            data: None,
902            block: ActiveDisplayBlock::Empty,
903            title: "Home".to_string(),
904            image: None,
905        };
906        app.push_navigation_stack(route.clone());
907        app.push_navigation_stack(route.clone());
908        app.push_navigation_stack(route.clone());
909        app.push_navigation_stack(route);
910        app
911    }
912    #[test]
913    fn test_navigation_push() {
914        let app = get_app();
915
916        assert_eq!(app.navigator.history.len(), 5);
917        assert_eq!(app.navigator.index, 4);
918    }
919
920    #[test]
921    fn test_backward_navigation() {
922        let mut app = get_app();
923        assert_eq!(app.navigator.index, 4);
924        app.load_previous_route();
925        assert_eq!(app.navigator.index, 3);
926        app.load_previous_route();
927        assert_eq!(app.navigator.index, 2);
928        app.load_previous_route();
929        assert_eq!(app.navigator.index, 1);
930        app.load_previous_route();
931        assert_eq!(app.navigator.index, 0);
932    }
933    #[test]
934    fn test_forward_navigation() {
935        let mut app = get_app();
936        app.navigator.index = 0;
937        app.load_next_route();
938        assert_eq!(app.navigator.index, 1);
939        app.load_next_route();
940        assert_eq!(app.navigator.index, 2);
941        app.load_next_route();
942        assert_eq!(app.navigator.index, 3);
943        app.load_next_route();
944        assert_eq!(app.navigator.index, 4);
945    }
946}