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, 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 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 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 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 self.history.remove(1);
246 self.clear_unused_data();
247 }
248
249 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 pub logger_state: TuiWidgetState,
285 pub exit_flag: bool,
287 pub exit_confirmation_popup: bool,
288 pub picker: Option<Picker>,
290 pub media_image: Option<(String, u32, u32)>,
291 pub image_state: Option<StatefulProtocol>,
292 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 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 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 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 pub anime_season: Seasonal,
329 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 pub user_profile: Option<UserInfo>,
338 pub anime_list_status: Option<UserWatchStatus>,
340 pub manga_list_status: Option<UserReadStatus>,
342 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 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_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, },
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_anime: TopThreeAnime::default(),
507 top_three_manga: TopThreeManga::default(),
508 selected_top_three: 0, active_top_three_anime: None,
510 active_top_three_manga: None,
511 active_anime_rank_index: 0,
512 active_manga_rank_index: 0,
513 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_status: None,
522 manga_list_status: None,
524 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 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 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_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 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 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; 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 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 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 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 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 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 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; 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; 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.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 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 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}