mal/
network.rs

1use crate::{
2    api::{
3        self, model::*, GetAnimeDetailQuery, GetAnimeRankingQuery, GetMangaDetailQuery,
4        GetMangaRankingQuery, GetSeasonalAnimeQuery, GetSuggestedAnimeQuery,
5        GetUserInformationQuery, UpdateUserAnimeListStatusQuery, UpdateUserMangaStatus,
6    },
7    app::{
8        ActiveBlock, ActiveDisplayBlock, App, Data, Route, SelectedSearchTab, TopThreeBlock,
9        UserAnimeList, UserMangaList,
10    },
11    auth::OAuth,
12};
13use bytes::Bytes;
14use std::path::{Path, PathBuf};
15use std::sync::Arc;
16use tokio::sync::Mutex;
17use tracing::warn;
18
19#[derive(Debug)]
20pub enum IoEvent {
21    GetSearchResults(String),
22    GetAnimeSearchResults(String),
23    GetMangaSearchResults(String),
24    GetAnime(u64),
25    GetManga(u64),
26    GetAnimeRanking(AnimeRankingType),
27    GetMangaRanking(MangaRankingType),
28    GetSeasonalAnime,
29    GetSuggestedAnime,
30    UpdateAnimeListStatus(u64, UpdateUserAnimeListStatusQuery),
31    DeleteAnimeListStatus(String),
32    GetAnimeList(Option<UserWatchStatus>),
33    GetMangaList(Option<UserReadStatus>),
34    UpdateMangaListStatus(u64, UpdateUserMangaStatus),
35    DeleteMangaListStatus(String),
36    GetUserInfo,
37    GetTopThree(TopThreeBlock),
38}
39
40#[derive(Clone)]
41pub struct Network<'a> {
42    oauth: OAuth,
43    large_search_limit: u64,
44    // small_search_limit: u64,
45    app: &'a Arc<Mutex<App>>,
46}
47
48impl<'a> Network<'a> {
49    pub fn new(oauth: OAuth, app: &'a Arc<Mutex<App>>, search_limit: u64) -> Self {
50        Self {
51            oauth,
52            large_search_limit: search_limit,
53            // small_search_limit: 3,
54            app,
55        }
56    }
57
58    pub async fn handle_network_event(&mut self, io_event: IoEvent) {
59        match io_event {
60            IoEvent::GetSearchResults(q) => self.get_search_results(q).await,
61
62            IoEvent::GetSeasonalAnime => self.get_seasonal().await,
63
64            IoEvent::GetAnimeRanking(r) => self.get_anime_ranking(r).await,
65
66            IoEvent::GetMangaRanking(r) => self.get_manga_ranking(r).await,
67
68            IoEvent::GetSuggestedAnime => self.get_suggested().await,
69
70            IoEvent::GetAnimeList(s) => self.get_user_anime_list(s).await,
71
72            IoEvent::GetMangaList(s) => self.get_user_manga_list(s).await,
73
74            IoEvent::GetAnime(id) => self.get_anime_details(id).await,
75
76            IoEvent::GetManga(id) => self.get_manga_details(id).await,
77
78            // IoEvent::GetAnimeSearchResults(String) => {}
79            // IoEvent::GetMangaSearchResults(String) => {}
80            // IoEvent::GetSuggestedAnime(String) => {}
81            // IoEvent::UpdateAnimeListStatus(String) => {}
82            // IoEvent::DeleteAnimeListStatus(String) => {}
83            // IoEvent::GetMangaRanking(String) => {}
84            // IoEvent::UpdateMangaListStatus(String) => {}
85            // IoEvent::DeleteMangaListStatus(String) => {}
86            IoEvent::GetUserInfo => self.get_user_info().await,
87            IoEvent::GetTopThree(r) => self.get_top_three(r).await,
88
89            IoEvent::UpdateAnimeListStatus(anime_id, query) => {
90                self.update_anime_list_status(anime_id, query).await
91            }
92            IoEvent::UpdateMangaListStatus(manga_id, query) => {
93                self.update_manga_list_status(manga_id, query).await
94            }
95            _ => (),
96        }
97
98        let mut app = self.app.lock().await;
99        app.is_loading = false
100    }
101
102    async fn get_anime_details(&mut self, id: u64) {
103        self.oauth.refresh().unwrap();
104        let mut app = self.app.lock().await;
105
106        let query = GetAnimeDetailQuery {
107            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
108            nsfw: app.app_config.nsfw,
109        };
110
111        match api::get_anime_details(id, &query, &self.oauth).await {
112            Ok(result) => {
113                app.anime_details = Some(result.clone());
114            }
115            Err(e) => {
116                app.write_error(e);
117                app.active_display_block = ActiveDisplayBlock::Error;
118                return;
119            }
120        }
121
122        let mut image = None;
123        if app.picker.is_some() {
124            image = get_picture(
125                app.app_config.paths.picture_cache_dir_path.clone(),
126                app.anime_details.as_ref().unwrap().id,
127                &app.anime_details.as_ref().unwrap().main_picture,
128                app.app_config.max_cached_images,
129            )
130            .await;
131            app.media_image = image.clone();
132            app.image_state = Some(
133                app.picker
134                    .as_ref()
135                    .unwrap()
136                    .new_resize_protocol(app.get_picture_from_cache().unwrap()),
137            );
138        }
139
140        let route = Route {
141            data: Some(Data::Anime(app.anime_details.as_ref().unwrap().clone())),
142            block: ActiveDisplayBlock::AnimeDetails,
143            title: app.anime_details.as_ref().unwrap().title.clone(),
144            image,
145        };
146        app.push_navigation_stack(route);
147        app.active_block = ActiveBlock::DisplayBlock;
148        app.active_display_block = ActiveDisplayBlock::AnimeDetails;
149        app.display_block_title = app.anime_details.as_ref().unwrap().title.clone();
150    }
151
152    async fn get_manga_details(&mut self, id: u64) {
153        self.oauth.refresh().unwrap();
154        let mut app = self.app.lock().await;
155
156        let query = GetMangaDetailQuery {
157            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
158            nsfw: app.app_config.nsfw,
159        };
160
161        match api::get_manga_details(id, &query, &self.oauth).await {
162            Ok(result) => {
163                app.manga_details = Some(result.clone());
164            }
165            Err(e) => {
166                app.write_error(e);
167                app.active_display_block = ActiveDisplayBlock::Error;
168                return;
169            }
170        }
171
172        let mut image = None;
173        if app.picker.is_some() {
174            image = get_picture(
175                app.app_config.paths.picture_cache_dir_path.clone(),
176                app.manga_details.as_ref().unwrap().id,
177                &app.manga_details.as_ref().unwrap().main_picture,
178                app.app_config.max_cached_images,
179            )
180            .await;
181            app.media_image = image.clone();
182            app.image_state = Some(
183                app.picker
184                    .as_ref()
185                    .unwrap()
186                    .new_resize_protocol(app.get_picture_from_cache().unwrap()),
187            );
188        }
189        let route = Route {
190            data: Some(Data::Manga(app.manga_details.as_ref().unwrap().clone())),
191            block: ActiveDisplayBlock::MangaDetails,
192            title: app.manga_details.as_ref().unwrap().title.clone(),
193            image,
194        };
195        app.push_navigation_stack(route);
196        app.active_block = ActiveBlock::DisplayBlock;
197        app.active_display_block = ActiveDisplayBlock::MangaDetails;
198        app.display_block_title = app.manga_details.as_ref().unwrap().title.clone();
199    }
200
201    async fn get_anime_ranking(&mut self, ranking_type: AnimeRankingType) {
202        self.oauth.refresh().unwrap();
203        let mut app = self.app.lock().await;
204        let query = GetAnimeRankingQuery {
205            ranking_type: ranking_type.clone(),
206            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
207            limit: self.large_search_limit,
208            nsfw: app.app_config.nsfw,
209            offset: 0,
210        };
211        let title = format!("Top Anime by {}", ranking_type);
212        match api::get_anime_ranking(&query, &self.oauth).await {
213            Ok(result) => {
214                app.anime_ranking_data = Some(result.clone());
215            }
216            Err(e) => {
217                app.write_error(e);
218                app.active_display_block = ActiveDisplayBlock::Error;
219                return;
220            }
221        }
222
223        let route = Route {
224            data: Some(Data::AnimeRanking(
225                app.anime_ranking_data.as_ref().unwrap().clone(),
226            )),
227            block: ActiveDisplayBlock::AnimeRanking,
228            title: title.clone(),
229            image: None,
230        };
231        app.push_navigation_stack(route);
232        app.active_block = ActiveBlock::DisplayBlock;
233        app.active_display_block = ActiveDisplayBlock::AnimeRanking;
234        app.display_block_title = title;
235    }
236
237    async fn get_manga_ranking(&mut self, ranking_type: MangaRankingType) {
238        self.oauth.refresh().unwrap();
239        let mut app = self.app.lock().await;
240        let query = GetMangaRankingQuery {
241            ranking_type: ranking_type.clone(),
242            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
243            limit: self.large_search_limit,
244            nsfw: app.app_config.nsfw,
245            offset: 0,
246        };
247        // better title:
248        let mut rank = ranking_type.to_string();
249        if rank == *"bypopularity" {
250            rank = "Popular Manga".to_string();
251        }
252        let title = format!("Top {}", rank);
253        match api::get_manga_ranking(&query, &self.oauth).await {
254            Ok(result) => {
255                app.manga_ranking_data = Some(result.clone());
256            }
257            Err(e) => {
258                app.write_error(e);
259                app.active_display_block = ActiveDisplayBlock::Error;
260                return;
261            }
262        }
263
264        let route = Route {
265            data: Some(Data::MangaRanking(
266                app.manga_ranking_data.as_ref().unwrap().clone(),
267            )),
268            block: ActiveDisplayBlock::MangaRanking,
269            title: title.clone(),
270            image: None,
271        };
272        app.push_navigation_stack(route);
273
274        app.active_block = ActiveBlock::DisplayBlock;
275        app.active_display_block = ActiveDisplayBlock::MangaRanking;
276        app.display_block_title = title;
277    }
278
279    async fn get_top_three(&mut self, ranking_type: TopThreeBlock) {
280        match ranking_type {
281            TopThreeBlock::Anime(r) => self.get_anime_top_three(r).await,
282            TopThreeBlock::Manga(r) => self.get_manga_top_three(r).await,
283            _ => (),
284        }
285    }
286
287    async fn get_anime_top_three(&mut self, rank_type: AnimeRankingType) {
288        self.oauth.refresh().unwrap();
289        let mut app = self.app.lock().await;
290        let query = GetAnimeRankingQuery {
291            ranking_type: rank_type.clone(),
292            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
293            limit: 3,
294            nsfw: app.app_config.nsfw,
295            offset: 0,
296        };
297        match api::get_anime_ranking(&query, &self.oauth).await {
298            Ok(result) => match &rank_type {
299                AnimeRankingType::Airing => {
300                    app.top_three_anime.airing = Some([
301                        result.data[0].node.clone(),
302                        result.data[1].node.clone(),
303                        result.data[2].node.clone(),
304                    ]);
305                }
306                AnimeRankingType::All => {
307                    app.top_three_anime.all = Some([
308                        result.data[0].node.clone(),
309                        result.data[1].node.clone(),
310                        result.data[2].node.clone(),
311                    ])
312                }
313                AnimeRankingType::Upcoming => {
314                    app.top_three_anime.upcoming = Some([
315                        result.data[0].node.clone(),
316                        result.data[1].node.clone(),
317                        result.data[2].node.clone(),
318                    ]);
319                }
320                AnimeRankingType::ByPopularity => {
321                    app.top_three_anime.popular = Some([
322                        result.data[0].node.clone(),
323                        result.data[1].node.clone(),
324                        result.data[2].node.clone(),
325                    ]);
326                }
327                AnimeRankingType::Favorite => {
328                    app.top_three_anime.favourite = Some([
329                        result.data[0].node.clone(),
330                        result.data[1].node.clone(),
331                        result.data[2].node.clone(),
332                    ]);
333                }
334                AnimeRankingType::Movie => {
335                    app.top_three_anime.movie = Some([
336                        result.data[0].node.clone(),
337                        result.data[1].node.clone(),
338                        result.data[2].node.clone(),
339                    ]);
340                }
341                AnimeRankingType::OVA => {
342                    app.top_three_anime.ova = Some([
343                        result.data[0].node.clone(),
344                        result.data[1].node.clone(),
345                        result.data[2].node.clone(),
346                    ]);
347                }
348                AnimeRankingType::TV => {
349                    app.top_three_anime.tv = Some([
350                        result.data[0].node.clone(),
351                        result.data[1].node.clone(),
352                        result.data[2].node.clone(),
353                    ]);
354                }
355                AnimeRankingType::Special => {
356                    app.top_three_anime.special = Some([
357                        result.data[0].node.clone(),
358                        result.data[1].node.clone(),
359                        result.data[2].node.clone(),
360                    ]);
361                }
362                AnimeRankingType::Other(_s) => {}
363            },
364            Err(e) => {
365                app.write_error(e);
366                app.active_top_three = TopThreeBlock::Error(RankingType::AnimeRankingType(
367                    app.active_top_three_anime
368                        .as_ref()
369                        .unwrap_or(&app.available_anime_ranking_types[0])
370                        .clone(),
371                ));
372                return;
373            }
374        }
375        app.active_top_three = TopThreeBlock::Anime(
376            app.active_top_three_anime
377                .as_ref()
378                .unwrap_or(&app.available_anime_ranking_types[0])
379                .clone(),
380        );
381    }
382
383    async fn get_manga_top_three(&mut self, rank_type: MangaRankingType) {
384        self.oauth.refresh().unwrap();
385        let mut app = self.app.lock().await;
386        let query = GetMangaRankingQuery {
387            ranking_type: rank_type.clone(),
388            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
389            limit: 3,
390            nsfw: app.app_config.nsfw,
391            offset: 0,
392        };
393
394        match api::get_manga_ranking(&query, &self.oauth).await {
395            Ok(results) => match &rank_type {
396                MangaRankingType::All => {
397                    app.top_three_manga.all = Some([
398                        results.data[0].node.clone(),
399                        results.data[1].node.clone(),
400                        results.data[2].node.clone(),
401                    ]);
402                }
403                MangaRankingType::Manga => {
404                    app.top_three_manga.manga = Some([
405                        results.data[0].node.clone(),
406                        results.data[1].node.clone(),
407                        results.data[2].node.clone(),
408                    ]);
409                }
410                MangaRankingType::Novels => {
411                    app.top_three_manga.novels = Some([
412                        results.data[0].node.clone(),
413                        results.data[1].node.clone(),
414                        results.data[2].node.clone(),
415                    ]);
416                }
417                MangaRankingType::OneShots => {
418                    app.top_three_manga.oneshots = Some([
419                        results.data[0].node.clone(),
420                        results.data[1].node.clone(),
421                        results.data[2].node.clone(),
422                    ]);
423                }
424                MangaRankingType::Favorite => {
425                    app.top_three_manga.favourite = Some([
426                        results.data[0].node.clone(),
427                        results.data[1].node.clone(),
428                        results.data[2].node.clone(),
429                    ]);
430                }
431                MangaRankingType::Doujinshi => {
432                    app.top_three_manga.doujin = Some([
433                        results.data[0].node.clone(),
434                        results.data[1].node.clone(),
435                        results.data[2].node.clone(),
436                    ]);
437                }
438                MangaRankingType::Manhwa => {
439                    app.top_three_manga.manhwa = Some([
440                        results.data[0].node.clone(),
441                        results.data[1].node.clone(),
442                        results.data[2].node.clone(),
443                    ]);
444                }
445                MangaRankingType::Manhua => {
446                    app.top_three_manga.manhua = Some([
447                        results.data[0].node.clone(),
448                        results.data[1].node.clone(),
449                        results.data[2].node.clone(),
450                    ]);
451                }
452                MangaRankingType::ByPopularity => {
453                    app.top_three_manga.popular = Some([
454                        results.data[0].node.clone(),
455                        results.data[1].node.clone(),
456                        results.data[2].node.clone(),
457                    ]);
458                }
459                MangaRankingType::Other(_) => {}
460            },
461
462            Err(e) => {
463                app.write_error(e);
464                app.active_top_three = TopThreeBlock::Error(RankingType::MangaRankingType(
465                    app.active_top_three_manga
466                        .as_ref()
467                        .unwrap_or(&app.available_manga_ranking_types[0])
468                        .clone(),
469                ));
470                return;
471            }
472        }
473        app.active_top_three = TopThreeBlock::Manga(
474            app.active_top_three_manga
475                .as_ref()
476                .unwrap_or(&app.available_manga_ranking_types[0])
477                .clone(),
478        );
479    }
480
481    async fn get_user_info(&mut self) {
482        self.oauth.refresh().unwrap();
483        let mut app = self.app.lock().await;
484        let query = GetUserInformationQuery {
485            fields: Some(ALL_USER_FIELDS.to_string()),
486        };
487        //? we can only use @me for the user in the current api version
488        match api::get_my_user_information("@me".to_string(), &query, &self.oauth).await {
489            Ok(result) => {
490                app.user_profile = Some(result.clone());
491            }
492            Err(e) => {
493                app.write_error(e);
494                app.active_display_block = ActiveDisplayBlock::Error;
495                return;
496            }
497        }
498        let route = Route {
499            data: Some(Data::UserInfo(app.user_profile.as_ref().unwrap().clone())),
500            block: ActiveDisplayBlock::UserInfo,
501            title: "Profile".to_string(),
502            image: None,
503        };
504        app.push_navigation_stack(route);
505        app.active_block = ActiveBlock::DisplayBlock;
506        app.active_display_block = ActiveDisplayBlock::UserInfo;
507        app.display_block_title = "Profile".to_string();
508    }
509
510    async fn get_suggested(&mut self) {
511        self.oauth.refresh().unwrap();
512        let mut app = self.app.lock().await;
513        let query = GetSuggestedAnimeQuery {
514            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
515            limit: self.large_search_limit,
516            nsfw: app.app_config.nsfw,
517            offset: 0,
518        };
519        match api::get_suggested_anime(&query, &self.oauth).await {
520            Ok(result) => {
521                app.search_results.anime = Some(result.clone());
522            }
523            Err(e) => {
524                app.write_error(e);
525                app.active_display_block = ActiveDisplayBlock::Error;
526                return;
527            }
528        }
529
530        let route = Route {
531            data: Some(Data::Suggestions(app.search_results.clone())),
532            block: ActiveDisplayBlock::Suggestions,
533            title: "Suggested Anime".to_string(),
534            image: None,
535        };
536        app.push_navigation_stack(route);
537        app.active_block = ActiveBlock::DisplayBlock;
538        app.active_display_block = ActiveDisplayBlock::Suggestions;
539        app.display_block_title = "Suggested Anime".to_string();
540    }
541
542    async fn get_seasonal(&mut self) {
543        self.oauth.refresh().unwrap();
544        let mut app = self.app.lock().await;
545        let query = GetSeasonalAnimeQuery {
546            sort: Some(app.anime_season.anime_sort.clone()),
547            offset: 0,
548            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
549            limit: self.large_search_limit,
550            nsfw: app.app_config.nsfw,
551        };
552        match api::get_seasonal_anime(&app.anime_season.anime_season, &query, &self.oauth).await {
553            Ok(result) => app.search_results.anime = Some(result),
554            Err(e) => {
555                app.write_error(e);
556                app.active_display_block = ActiveDisplayBlock::Error;
557                return;
558            }
559        }
560
561        let title = format!(
562            "Seasonal Anime: {} {}",
563            app.anime_season.anime_season.season, app.anime_season.anime_season.year,
564        );
565
566        let route = Route {
567            data: Some(Data::SearchResult(app.search_results.clone())),
568            block: ActiveDisplayBlock::Seasonal,
569            title: title.clone(),
570            image: None,
571        };
572        app.push_navigation_stack(route);
573        app.active_block = ActiveBlock::DisplayBlock;
574        app.active_display_block = ActiveDisplayBlock::Seasonal;
575        app.display_block_title = title;
576    }
577
578    async fn get_user_anime_list(&mut self, status: Option<UserWatchStatus>) {
579        self.oauth.refresh().unwrap();
580        let mut app = self.app.lock().await;
581        let query = api::GetUserAnimeListQuery {
582            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
583            status: status.clone(),
584            sort: Some(SortStyle::ListScore),
585            limit: self.large_search_limit,
586            offset: 0,
587            nsfw: app.app_config.nsfw,
588        };
589        match api::get_user_anime_list("@me".to_string(), &query, &self.oauth).await {
590            Ok(result) => {
591                app.search_results.anime = Some(result.clone());
592            }
593            Err(e) => {
594                app.write_error(e);
595                app.active_display_block = ActiveDisplayBlock::Error;
596                return;
597            }
598        }
599
600        let data = UserAnimeList {
601            anime_list: app.search_results.anime.as_ref().unwrap().clone(),
602            status: status.clone(),
603        };
604        let route = Route {
605            block: ActiveDisplayBlock::UserAnimeList,
606            data: Some(Data::UserAnimeList(data)),
607            title: format!("My Anime List: {}", get_status_string(status)),
608            image: None,
609        };
610        app.active_block = ActiveBlock::DisplayBlock;
611        app.active_display_block = ActiveDisplayBlock::UserAnimeList;
612        app.display_block_title = route.title.clone();
613        app.push_navigation_stack(route);
614    }
615
616    async fn get_user_manga_list(&mut self, status: Option<UserReadStatus>) {
617        self.oauth.refresh().unwrap();
618        let mut app = self.app.lock().await;
619        let query = api::GetUserMangaListQuery {
620            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
621            status: status.clone(),
622            sort: Some(SortStyle::ListScore),
623            limit: self.large_search_limit,
624            offset: 0,
625            nsfw: app.app_config.nsfw,
626        };
627        match api::get_user_manga_list("@me".to_string(), &query, &self.oauth).await {
628            Ok(result) => {
629                app.search_results.manga = Some(result.clone());
630            }
631            Err(e) => {
632                app.write_error(e);
633                app.active_display_block = ActiveDisplayBlock::Error;
634                return;
635            }
636        }
637
638        let data = UserMangaList {
639            manga_list: app.search_results.manga.as_ref().unwrap().clone(),
640            status: status.clone(),
641        };
642        let route = Route {
643            block: ActiveDisplayBlock::UserMangaList,
644            data: Some(Data::UserMangaList(data)),
645            title: format!("My Manga List: {}", get_manga_status_string(status)),
646            image: None,
647        };
648
649        app.active_block = ActiveBlock::DisplayBlock;
650        app.active_display_block = ActiveDisplayBlock::UserMangaList;
651        app.display_block_title = route.title.clone();
652        app.push_navigation_stack(route);
653    }
654
655    async fn get_search_results(&mut self, q: String) {
656        self.oauth.refresh().unwrap();
657        let mut app = self.app.lock().await;
658
659        let anime_query = api::GetAnimeListQuery {
660            q: q.clone(),
661            limit: self.large_search_limit,
662            offset: 0,
663            nsfw: app.app_config.nsfw,
664            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
665        };
666
667        let manga_query = api::GetMangaListQuery {
668            q: q.clone(),
669            limit: self.large_search_limit,
670            offset: 0,
671            nsfw: app.app_config.nsfw,
672            fields: Some(ALL_ANIME_AND_MANGA_FIELDS.to_string()),
673        };
674
675        match api::get_anime_list(&anime_query, &self.oauth).await {
676            Ok(results) => {
677                app.search_results.anime = Some(results);
678            }
679            Err(e) => {
680                app.write_error(e);
681                app.active_display_block = ActiveDisplayBlock::Error;
682                return;
683            }
684        };
685
686        match api::get_manga_list(&manga_query, &self.oauth).await {
687            Ok(results) => {
688                app.search_results.manga = Some(results);
689            }
690            Err(e) => {
691                app.write_error(e);
692                app.active_display_block = ActiveDisplayBlock::Error;
693                return;
694            }
695        };
696
697        let route = Route {
698            data: Some(Data::SearchResult(app.search_results.clone())),
699            block: ActiveDisplayBlock::SearchResultBlock,
700            title: format!("Search Results: {}", q.clone()).to_string(),
701            image: None,
702        };
703        app.push_navigation_stack(route);
704
705        app.search_results.selected_tab = SelectedSearchTab::Anime;
706        app.active_display_block = ActiveDisplayBlock::SearchResultBlock;
707        app.display_block_title = format!("Search Results: {}", q).to_string()
708    }
709
710    async fn update_anime_list_status(
711        &mut self,
712        anime_id: u64,
713        query: UpdateUserAnimeListStatusQuery,
714    ) {
715        self.oauth.refresh().unwrap();
716        let mut app = self.app.lock().await;
717
718        match api::update_anime_list_status(anime_id, &query, &self.oauth).await {
719            Ok(result) => {
720                app.anime_details.as_mut().unwrap().my_list_status = Some(result);
721                app.popup_post_req_success_message = Some("updated Successfully".to_string());
722                app.popup_post_req_success = true;
723            }
724            Err(e) => {
725                app.write_error(e);
726                app.popup_post_req_success = false;
727            }
728        }
729        app.popup_is_loading = false;
730    }
731
732    async fn update_manga_list_status(&mut self, manga_id: u64, query: UpdateUserMangaStatus) {
733        self.oauth.refresh().unwrap();
734        let mut app = self.app.lock().await;
735        match api::update_manga_list_status(manga_id, &query, &self.oauth).await {
736            Ok(result) => {
737                //
738                app.manga_details.as_mut().unwrap().my_list_status = Some(result);
739                app.popup_post_req_success = true;
740                app.popup_post_req_success_message = Some("updated Successfully".to_string());
741            }
742            Err(e) => {
743                app.write_error(e);
744                app.popup_post_req_success = false;
745            }
746        }
747        app.popup_is_loading = false;
748    }
749}
750
751fn get_status_string(status: Option<UserWatchStatus>) -> String {
752    match status {
753        Some(s) => match s {
754            UserWatchStatus::Completed => "completed".to_string(),
755            UserWatchStatus::Watching => "watching".to_string(),
756            UserWatchStatus::OnHold => "on_hold".to_string(),
757            UserWatchStatus::Dropped => "dropped".to_string(),
758            UserWatchStatus::PlanToWatch => "plan_to_watch".to_string(),
759            UserWatchStatus::Other(_) => "All".to_string(),
760        },
761        None => "All".to_string(),
762    }
763}
764
765fn get_manga_status_string(status: Option<UserReadStatus>) -> String {
766    match status {
767        Some(s) => match s {
768            UserReadStatus::Completed => "completed".to_string(),
769            UserReadStatus::Reading => "reading".to_string(),
770            UserReadStatus::OnHold => "on_hold".to_string(),
771            UserReadStatus::Dropped => "dropped".to_string(),
772            UserReadStatus::PlanToRead => "plan_to_read".to_string(),
773            UserReadStatus::Other(_) => "All".to_string(),
774        },
775        None => "All".to_string(),
776    }
777}
778
779async fn fetch_image(url: &str) -> Result<Bytes, Box<dyn std::error::Error>> {
780    let response = reqwest::get(url).await?;
781    let bytes = response.bytes().await?;
782    Ok(bytes)
783}
784
785async fn get_picture(
786    image_dir_path: PathBuf,
787    id: u64,
788    pictures: &Option<Picture>,
789    max_limit: u16,
790) -> Option<(String, u32, u32)> {
791    // check if the image is already in the cache, if not we fetch it and save it
792    // look for it in the cache first:
793    let file_path = image_dir_path.join(format!("{}.png", id));
794    if file_path.exists() {
795        let image = image::open(&file_path).ok()?;
796        return Some((
797            file_path.to_string_lossy().to_string(),
798            image.width(),
799            image.height(),
800        ));
801    }
802
803    // fetch the image and save it
804    // after fetch we need to look into the total count of files and if it exceeds the limit we delete the last one used
805    //? make the image number limit a conf var
806    //? we ll implement a FIFO mechanisme
807    // so the flow is:
808    // when an image is fetchead we enter its name  in the array (just use array for poping and pushing)
809
810    // checking the json file
811    let cache_index_file = image_dir_path.join("cache_index.json");
812    if !cache_index_file.exists() {
813        // create an empty  file with emtpy array
814        let cache_index = Vec::<String>::new();
815        // Read all existing images in the folder and create an array
816        let mut existing_images = Vec::<String>::new();
817        if let Ok(entries) = std::fs::read_dir(&image_dir_path) {
818            for entry in entries.flatten() {
819                if let Some(file_name) = entry.file_name().to_str() {
820                    if file_name != "cache_index.json" {
821                        existing_images.push(file_name.to_string());
822                    }
823                }
824            }
825        }
826        let cache_index_json =
827            serde_json::to_string(&cache_index).unwrap_or_else(|_| "[]".to_string());
828        std::fs::write(&cache_index_file, cache_index_json).ok();
829    }
830
831    if let Some(p) = pictures {
832        let urls = vec![&p.large, &p.medium];
833        // loop all image urls and return the first fetched one
834        for url in urls.into_iter().flatten() {
835            // save the image in the .cache/mal-tui/media-images folder
836            let image = fetch_image(url).await;
837            match image {
838                Ok(bytes) => {
839                    let file_name = format!("{}.png", id);
840                    let file_path = image_dir_path.join(file_name.clone());
841                    let image = image::load_from_memory(&bytes).ok();
842                    if let Some(image) = image {
843                        image
844                            .save_with_format(&file_path, image::ImageFormat::Png)
845                            .ok();
846                        // after saving the image we need to update the index file
847                        // first push to array then check if it reached the max size if yes then we remove the first element(image then array element)
848                        let res = update_image_cache(
849                            &cache_index_file,
850                            &image_dir_path,
851                            &file_name,
852                            max_limit,
853                        )
854                        .await;
855                        if let Err(e) = res {
856                            warn!("error updating the cache index file: {}", e)
857                        }
858
859                        return Some((
860                            file_path.to_string_lossy().to_string(),
861                            image.width(),
862                            image.height(),
863                        ));
864                    }
865                }
866                Err(e) => {
867                    warn!("Error fetching image: {}", e);
868                }
869            }
870        }
871    }
872    None
873}
874
875async fn update_image_cache(
876    cache_index_file: &PathBuf,
877    image_dir_path: &Path,
878    file_name: &str,
879    max_limit: u16,
880) -> Result<(), Box<dyn std::error::Error>> {
881    // read the current cache index
882    let cache_content = std::fs::read_to_string(cache_index_file)?;
883    let mut cache_index: Vec<String> = serde_json::from_str(&cache_content).unwrap_or_default();
884
885    // add the new image name to the array
886    cache_index.push(file_name.to_string());
887
888    // Check if we've reached the capacity limit
889    if cache_index.len() > max_limit as usize {
890        // Remove the first element and delete the corresponding image
891        let oldest_image = cache_index.remove(0);
892        let oldest_image_path = image_dir_path.join(&oldest_image);
893        if oldest_image_path.exists() {
894            std::fs::remove_file(oldest_image_path).ok();
895        }
896    }
897
898    // Write the updated cache index back to the file
899    let updated_cache_json = serde_json::to_string(&cache_index)?;
900    std::fs::write(cache_index_file, updated_cache_json)?;
901
902    Ok(())
903}