1 use super::user_config::UserConfig;
2 use crate::network::IoEvent;
3 use anyhow::anyhow;
4 use rspotify::{
5   model::{
6     album::{FullAlbum, SavedAlbum, SimplifiedAlbum},
7     artist::FullArtist,
8     audio::AudioAnalysis,
9     context::CurrentlyPlaybackContext,
10     device::DevicePayload,
11     page::{CursorBasedPage, Page},
12     playing::PlayHistory,
13     playlist::{PlaylistTrack, SimplifiedPlaylist},
14     show::{FullShow, Show, SimplifiedEpisode, SimplifiedShow},
15     track::{FullTrack, SavedTrack, SimplifiedTrack},
16     user::PrivateUser,
17     PlayingItem,
18   },
19   senum::Country,
20 };
21 use std::str::FromStr;
22 use std::sync::mpsc::Sender;
23 use std::{
24   cmp::{max, min},
25   collections::HashSet,
26   time::{Instant, SystemTime},
27 };
28 use tui::layout::Rect;
29 
30 use arboard::Clipboard;
31 
32 pub const LIBRARY_OPTIONS: [&str; 6] = [
33   "Made For You",
34   "Recently Played",
35   "Liked Songs",
36   "Albums",
37   "Artists",
38   "Podcasts",
39 ];
40 
41 const DEFAULT_ROUTE: Route = Route {
42   id: RouteId::Home,
43   active_block: ActiveBlock::Empty,
44   hovered_block: ActiveBlock::Library,
45 };
46 
47 #[derive(Clone)]
48 pub struct ScrollableResultPages<T> {
49   index: usize,
50   pub pages: Vec<T>,
51 }
52 
53 impl<T> ScrollableResultPages<T> {
new() -> ScrollableResultPages<T>54   pub fn new() -> ScrollableResultPages<T> {
55     ScrollableResultPages {
56       index: 0,
57       pages: vec![],
58     }
59   }
60 
get_results(&self, at_index: Option<usize>) -> Option<&T>61   pub fn get_results(&self, at_index: Option<usize>) -> Option<&T> {
62     self.pages.get(at_index.unwrap_or(self.index))
63   }
64 
get_mut_results(&mut self, at_index: Option<usize>) -> Option<&mut T>65   pub fn get_mut_results(&mut self, at_index: Option<usize>) -> Option<&mut T> {
66     self.pages.get_mut(at_index.unwrap_or(self.index))
67   }
68 
add_pages(&mut self, new_pages: T)69   pub fn add_pages(&mut self, new_pages: T) {
70     self.pages.push(new_pages);
71     // Whenever a new page is added, set the active index to the end of the vector
72     self.index = self.pages.len() - 1;
73   }
74 }
75 
76 #[derive(Default)]
77 pub struct SpotifyResultAndSelectedIndex<T> {
78   pub index: usize,
79   pub result: T,
80 }
81 
82 #[derive(Clone)]
83 pub struct Library {
84   pub selected_index: usize,
85   pub saved_tracks: ScrollableResultPages<Page<SavedTrack>>,
86   pub made_for_you_playlists: ScrollableResultPages<Page<SimplifiedPlaylist>>,
87   pub saved_albums: ScrollableResultPages<Page<SavedAlbum>>,
88   pub saved_shows: ScrollableResultPages<Page<Show>>,
89   pub saved_artists: ScrollableResultPages<CursorBasedPage<FullArtist>>,
90   pub show_episodes: ScrollableResultPages<Page<SimplifiedEpisode>>,
91 }
92 
93 #[derive(PartialEq, Debug)]
94 pub enum SearchResultBlock {
95   AlbumSearch,
96   SongSearch,
97   ArtistSearch,
98   PlaylistSearch,
99   ShowSearch,
100   Empty,
101 }
102 
103 #[derive(PartialEq, Debug, Clone)]
104 pub enum ArtistBlock {
105   TopTracks,
106   Albums,
107   RelatedArtists,
108   Empty,
109 }
110 
111 #[derive(Clone, Copy, PartialEq, Debug)]
112 pub enum DialogContext {
113   PlaylistWindow,
114   PlaylistSearch,
115 }
116 
117 #[derive(Clone, Copy, PartialEq, Debug)]
118 pub enum ActiveBlock {
119   Analysis,
120   PlayBar,
121   AlbumTracks,
122   AlbumList,
123   ArtistBlock,
124   Empty,
125   Error,
126   HelpMenu,
127   Home,
128   Input,
129   Library,
130   MyPlaylists,
131   Podcasts,
132   EpisodeTable,
133   RecentlyPlayed,
134   SearchResultBlock,
135   SelectDevice,
136   TrackTable,
137   MadeForYou,
138   Artists,
139   BasicView,
140   Dialog(DialogContext),
141 }
142 
143 #[derive(Clone, PartialEq, Debug)]
144 pub enum RouteId {
145   Analysis,
146   AlbumTracks,
147   AlbumList,
148   Artist,
149   BasicView,
150   Error,
151   Home,
152   RecentlyPlayed,
153   Search,
154   SelectedDevice,
155   TrackTable,
156   MadeForYou,
157   Artists,
158   Podcasts,
159   PodcastEpisodes,
160   Recommendations,
161 }
162 
163 #[derive(Debug)]
164 pub struct Route {
165   pub id: RouteId,
166   pub active_block: ActiveBlock,
167   pub hovered_block: ActiveBlock,
168 }
169 
170 // Is it possible to compose enums?
171 #[derive(PartialEq, Debug)]
172 pub enum TrackTableContext {
173   MyPlaylists,
174   AlbumSearch,
175   PlaylistSearch,
176   SavedTracks,
177   RecommendedTracks,
178   MadeForYou,
179 }
180 
181 // Is it possible to compose enums?
182 #[derive(Clone, PartialEq, Debug, Copy)]
183 pub enum AlbumTableContext {
184   Simplified,
185   Full,
186 }
187 
188 #[derive(Clone, PartialEq, Debug, Copy)]
189 pub enum EpisodeTableContext {
190   Simplified,
191   Full,
192 }
193 
194 #[derive(Clone, PartialEq, Debug)]
195 pub enum RecommendationsContext {
196   Artist,
197   Song,
198 }
199 
200 pub struct SearchResult {
201   pub albums: Option<Page<SimplifiedAlbum>>,
202   pub artists: Option<Page<FullArtist>>,
203   pub playlists: Option<Page<SimplifiedPlaylist>>,
204   pub tracks: Option<Page<FullTrack>>,
205   pub shows: Option<Page<SimplifiedShow>>,
206   pub selected_album_index: Option<usize>,
207   pub selected_artists_index: Option<usize>,
208   pub selected_playlists_index: Option<usize>,
209   pub selected_tracks_index: Option<usize>,
210   pub selected_shows_index: Option<usize>,
211   pub hovered_block: SearchResultBlock,
212   pub selected_block: SearchResultBlock,
213 }
214 
215 #[derive(Default)]
216 pub struct TrackTable {
217   pub tracks: Vec<FullTrack>,
218   pub selected_index: usize,
219   pub context: Option<TrackTableContext>,
220 }
221 
222 #[derive(Clone)]
223 pub struct SelectedShow {
224   pub show: SimplifiedShow,
225 }
226 
227 #[derive(Clone)]
228 pub struct SelectedFullShow {
229   pub show: FullShow,
230 }
231 
232 #[derive(Clone)]
233 pub struct SelectedAlbum {
234   pub album: SimplifiedAlbum,
235   pub tracks: Page<SimplifiedTrack>,
236   pub selected_index: usize,
237 }
238 
239 #[derive(Clone)]
240 pub struct SelectedFullAlbum {
241   pub album: FullAlbum,
242   pub selected_index: usize,
243 }
244 
245 #[derive(Clone)]
246 pub struct Artist {
247   pub artist_name: String,
248   pub albums: Page<SimplifiedAlbum>,
249   pub related_artists: Vec<FullArtist>,
250   pub top_tracks: Vec<FullTrack>,
251   pub selected_album_index: usize,
252   pub selected_related_artist_index: usize,
253   pub selected_top_track_index: usize,
254   pub artist_hovered_block: ArtistBlock,
255   pub artist_selected_block: ArtistBlock,
256 }
257 
258 pub struct App {
259   pub instant_since_last_current_playback_poll: Instant,
260   navigation_stack: Vec<Route>,
261   pub audio_analysis: Option<AudioAnalysis>,
262   pub home_scroll: u16,
263   pub user_config: UserConfig,
264   pub artists: Vec<FullArtist>,
265   pub artist: Option<Artist>,
266   pub album_table_context: AlbumTableContext,
267   pub saved_album_tracks_index: usize,
268   pub api_error: String,
269   pub current_playback_context: Option<CurrentlyPlaybackContext>,
270   pub devices: Option<DevicePayload>,
271   // Inputs:
272   // input is the string for input;
273   // input_idx is the index of the cursor in terms of character;
274   // input_cursor_position is the sum of the width of characters preceding the cursor.
275   // Reason for this complication is due to non-ASCII characters, they may
276   // take more than 1 bytes to store and more than 1 character width to display.
277   pub input: Vec<char>,
278   pub input_idx: usize,
279   pub input_cursor_position: u16,
280   pub liked_song_ids_set: HashSet<String>,
281   pub followed_artist_ids_set: HashSet<String>,
282   pub saved_album_ids_set: HashSet<String>,
283   pub saved_show_ids_set: HashSet<String>,
284   pub large_search_limit: u32,
285   pub library: Library,
286   pub playlist_offset: u32,
287   pub made_for_you_offset: u32,
288   pub playlist_tracks: Option<Page<PlaylistTrack>>,
289   pub made_for_you_tracks: Option<Page<PlaylistTrack>>,
290   pub playlists: Option<Page<SimplifiedPlaylist>>,
291   pub recently_played: SpotifyResultAndSelectedIndex<Option<CursorBasedPage<PlayHistory>>>,
292   pub recommended_tracks: Vec<FullTrack>,
293   pub recommendations_seed: String,
294   pub recommendations_context: Option<RecommendationsContext>,
295   pub search_results: SearchResult,
296   pub selected_album_simplified: Option<SelectedAlbum>,
297   pub selected_album_full: Option<SelectedFullAlbum>,
298   pub selected_device_index: Option<usize>,
299   pub selected_playlist_index: Option<usize>,
300   pub active_playlist_index: Option<usize>,
301   pub size: Rect,
302   pub small_search_limit: u32,
303   pub song_progress_ms: u128,
304   pub seek_ms: Option<u128>,
305   pub track_table: TrackTable,
306   pub episode_table_context: EpisodeTableContext,
307   pub selected_show_simplified: Option<SelectedShow>,
308   pub selected_show_full: Option<SelectedFullShow>,
309   pub user: Option<PrivateUser>,
310   pub album_list_index: usize,
311   pub made_for_you_index: usize,
312   pub artists_list_index: usize,
313   pub clipboard: Option<Clipboard>,
314   pub shows_list_index: usize,
315   pub episode_list_index: usize,
316   pub help_docs_size: u32,
317   pub help_menu_page: u32,
318   pub help_menu_max_lines: u32,
319   pub help_menu_offset: u32,
320   pub is_loading: bool,
321   io_tx: Option<Sender<IoEvent>>,
322   pub is_fetching_current_playback: bool,
323   pub spotify_token_expiry: SystemTime,
324   pub dialog: Option<String>,
325   pub confirm: bool,
326 }
327 
328 impl Default for App {
default() -> Self329   fn default() -> Self {
330     App {
331       audio_analysis: None,
332       album_table_context: AlbumTableContext::Full,
333       album_list_index: 0,
334       made_for_you_index: 0,
335       artists_list_index: 0,
336       shows_list_index: 0,
337       episode_list_index: 0,
338       artists: vec![],
339       artist: None,
340       user_config: UserConfig::new(),
341       saved_album_tracks_index: 0,
342       recently_played: Default::default(),
343       size: Rect::default(),
344       selected_album_simplified: None,
345       selected_album_full: None,
346       home_scroll: 0,
347       library: Library {
348         saved_tracks: ScrollableResultPages::new(),
349         made_for_you_playlists: ScrollableResultPages::new(),
350         saved_albums: ScrollableResultPages::new(),
351         saved_shows: ScrollableResultPages::new(),
352         saved_artists: ScrollableResultPages::new(),
353         show_episodes: ScrollableResultPages::new(),
354         selected_index: 0,
355       },
356       liked_song_ids_set: HashSet::new(),
357       followed_artist_ids_set: HashSet::new(),
358       saved_album_ids_set: HashSet::new(),
359       saved_show_ids_set: HashSet::new(),
360       navigation_stack: vec![DEFAULT_ROUTE],
361       large_search_limit: 20,
362       small_search_limit: 4,
363       api_error: String::new(),
364       current_playback_context: None,
365       devices: None,
366       input: vec![],
367       input_idx: 0,
368       input_cursor_position: 0,
369       playlist_offset: 0,
370       made_for_you_offset: 0,
371       playlist_tracks: None,
372       made_for_you_tracks: None,
373       playlists: None,
374       recommended_tracks: vec![],
375       recommendations_context: None,
376       recommendations_seed: "".to_string(),
377       search_results: SearchResult {
378         hovered_block: SearchResultBlock::SongSearch,
379         selected_block: SearchResultBlock::Empty,
380         albums: None,
381         artists: None,
382         playlists: None,
383         shows: None,
384         selected_album_index: None,
385         selected_artists_index: None,
386         selected_playlists_index: None,
387         selected_tracks_index: None,
388         selected_shows_index: None,
389         tracks: None,
390       },
391       song_progress_ms: 0,
392       seek_ms: None,
393       selected_device_index: None,
394       selected_playlist_index: None,
395       active_playlist_index: None,
396       track_table: Default::default(),
397       episode_table_context: EpisodeTableContext::Full,
398       selected_show_simplified: None,
399       selected_show_full: None,
400       user: None,
401       instant_since_last_current_playback_poll: Instant::now(),
402       clipboard: Clipboard::new().ok(),
403       help_docs_size: 0,
404       help_menu_page: 0,
405       help_menu_max_lines: 0,
406       help_menu_offset: 0,
407       is_loading: false,
408       io_tx: None,
409       is_fetching_current_playback: false,
410       spotify_token_expiry: SystemTime::now(),
411       dialog: None,
412       confirm: false,
413     }
414   }
415 }
416 
417 impl App {
new( io_tx: Sender<IoEvent>, user_config: UserConfig, spotify_token_expiry: SystemTime, ) -> App418   pub fn new(
419     io_tx: Sender<IoEvent>,
420     user_config: UserConfig,
421     spotify_token_expiry: SystemTime,
422   ) -> App {
423     App {
424       io_tx: Some(io_tx),
425       user_config,
426       spotify_token_expiry,
427       ..App::default()
428     }
429   }
430 
431   // Send a network event to the network thread
dispatch(&mut self, action: IoEvent)432   pub fn dispatch(&mut self, action: IoEvent) {
433     // `is_loading` will be set to false again after the async action has finished in network.rs
434     self.is_loading = true;
435     if let Some(io_tx) = &self.io_tx {
436       if let Err(e) = io_tx.send(action) {
437         self.is_loading = false;
438         println!("Error from dispatch {}", e);
439         // TODO: handle error
440       };
441     }
442   }
443 
apply_seek(&mut self, seek_ms: u32)444   fn apply_seek(&mut self, seek_ms: u32) {
445     if let Some(CurrentlyPlaybackContext {
446       item: Some(item), ..
447     }) = &self.current_playback_context
448     {
449       let duration_ms = match item {
450         PlayingItem::Track(track) => track.duration_ms,
451         PlayingItem::Episode(episode) => episode.duration_ms,
452       };
453 
454       let event = if seek_ms < duration_ms {
455         IoEvent::Seek(seek_ms)
456       } else {
457         IoEvent::NextTrack
458       };
459 
460       self.dispatch(event);
461     }
462   }
463 
poll_current_playback(&mut self)464   fn poll_current_playback(&mut self) {
465     // Poll every 5 seconds
466     let poll_interval_ms = 5_000;
467 
468     let elapsed = self
469       .instant_since_last_current_playback_poll
470       .elapsed()
471       .as_millis();
472 
473     if !self.is_fetching_current_playback && elapsed >= poll_interval_ms {
474       self.is_fetching_current_playback = true;
475       // Trigger the seek if the user has set a new position
476       match self.seek_ms {
477         Some(seek_ms) => self.apply_seek(seek_ms as u32),
478         None => self.dispatch(IoEvent::GetCurrentPlayback),
479       }
480     }
481   }
482 
update_on_tick(&mut self)483   pub fn update_on_tick(&mut self) {
484     self.poll_current_playback();
485     if let Some(CurrentlyPlaybackContext {
486       item: Some(item),
487       progress_ms: Some(progress_ms),
488       is_playing,
489       ..
490     }) = &self.current_playback_context
491     {
492       // Update progress even when the song is not playing,
493       // because seeking is possible while paused
494       let elapsed = if *is_playing {
495         self
496           .instant_since_last_current_playback_poll
497           .elapsed()
498           .as_millis()
499       } else {
500         0u128
501       } + u128::from(*progress_ms);
502 
503       let duration_ms = match item {
504         PlayingItem::Track(track) => track.duration_ms,
505         PlayingItem::Episode(episode) => episode.duration_ms,
506       };
507 
508       if elapsed < u128::from(duration_ms) {
509         self.song_progress_ms = elapsed;
510       } else {
511         self.song_progress_ms = duration_ms.into();
512       }
513     }
514   }
515 
seek_forwards(&mut self)516   pub fn seek_forwards(&mut self) {
517     if let Some(CurrentlyPlaybackContext {
518       item: Some(item), ..
519     }) = &self.current_playback_context
520     {
521       let duration_ms = match item {
522         PlayingItem::Track(track) => track.duration_ms,
523         PlayingItem::Episode(episode) => episode.duration_ms,
524       };
525 
526       let old_progress = match self.seek_ms {
527         Some(seek_ms) => seek_ms,
528         None => self.song_progress_ms,
529       };
530 
531       let new_progress = min(
532         old_progress as u32 + self.user_config.behavior.seek_milliseconds,
533         duration_ms,
534       );
535 
536       self.seek_ms = Some(new_progress as u128);
537     }
538   }
539 
seek_backwards(&mut self)540   pub fn seek_backwards(&mut self) {
541     let old_progress = match self.seek_ms {
542       Some(seek_ms) => seek_ms,
543       None => self.song_progress_ms,
544     };
545     let new_progress = if old_progress as u32 > self.user_config.behavior.seek_milliseconds {
546       old_progress as u32 - self.user_config.behavior.seek_milliseconds
547     } else {
548       0u32
549     };
550     self.seek_ms = Some(new_progress as u128);
551   }
552 
get_recommendations_for_seed( &mut self, seed_artists: Option<Vec<String>>, seed_tracks: Option<Vec<String>>, first_track: Option<FullTrack>, )553   pub fn get_recommendations_for_seed(
554     &mut self,
555     seed_artists: Option<Vec<String>>,
556     seed_tracks: Option<Vec<String>>,
557     first_track: Option<FullTrack>,
558   ) {
559     let user_country = self.get_user_country();
560     self.dispatch(IoEvent::GetRecommendationsForSeed(
561       seed_artists,
562       seed_tracks,
563       Box::new(first_track),
564       user_country,
565     ));
566   }
567 
get_recommendations_for_track_id(&mut self, id: String)568   pub fn get_recommendations_for_track_id(&mut self, id: String) {
569     let user_country = self.get_user_country();
570     self.dispatch(IoEvent::GetRecommendationsForTrackId(id, user_country));
571   }
572 
increase_volume(&mut self)573   pub fn increase_volume(&mut self) {
574     if let Some(context) = self.current_playback_context.clone() {
575       let current_volume = context.device.volume_percent as u8;
576       let next_volume = min(
577         current_volume + self.user_config.behavior.volume_increment,
578         100,
579       );
580 
581       if next_volume != current_volume {
582         self.dispatch(IoEvent::ChangeVolume(next_volume));
583       }
584     }
585   }
586 
decrease_volume(&mut self)587   pub fn decrease_volume(&mut self) {
588     if let Some(context) = self.current_playback_context.clone() {
589       let current_volume = context.device.volume_percent as i8;
590       let next_volume = max(
591         current_volume - self.user_config.behavior.volume_increment as i8,
592         0,
593       );
594 
595       if next_volume != current_volume {
596         self.dispatch(IoEvent::ChangeVolume(next_volume as u8));
597       }
598     }
599   }
600 
handle_error(&mut self, e: anyhow::Error)601   pub fn handle_error(&mut self, e: anyhow::Error) {
602     self.push_navigation_stack(RouteId::Error, ActiveBlock::Error);
603     self.api_error = e.to_string();
604   }
605 
toggle_playback(&mut self)606   pub fn toggle_playback(&mut self) {
607     if let Some(CurrentlyPlaybackContext {
608       is_playing: true, ..
609     }) = &self.current_playback_context
610     {
611       self.dispatch(IoEvent::PausePlayback);
612     } else {
613       // When no offset or uris are passed, spotify will resume current playback
614       self.dispatch(IoEvent::StartPlayback(None, None, None));
615     }
616   }
617 
previous_track(&mut self)618   pub fn previous_track(&mut self) {
619     if self.song_progress_ms >= 3_000 {
620       self.dispatch(IoEvent::Seek(0));
621     } else {
622       self.dispatch(IoEvent::PreviousTrack);
623     }
624   }
625 
626   // The navigation_stack actually only controls the large block to the right of `library` and
627   // `playlists`
push_navigation_stack(&mut self, next_route_id: RouteId, next_active_block: ActiveBlock)628   pub fn push_navigation_stack(&mut self, next_route_id: RouteId, next_active_block: ActiveBlock) {
629     if !self
630       .navigation_stack
631       .last()
632       .map(|last_route| last_route.id == next_route_id)
633       .unwrap_or(false)
634     {
635       self.navigation_stack.push(Route {
636         id: next_route_id,
637         active_block: next_active_block,
638         hovered_block: next_active_block,
639       });
640     }
641   }
642 
pop_navigation_stack(&mut self) -> Option<Route>643   pub fn pop_navigation_stack(&mut self) -> Option<Route> {
644     if self.navigation_stack.len() == 1 {
645       None
646     } else {
647       self.navigation_stack.pop()
648     }
649   }
650 
get_current_route(&self) -> &Route651   pub fn get_current_route(&self) -> &Route {
652     // if for some reason there is no route return the default
653     self.navigation_stack.last().unwrap_or(&DEFAULT_ROUTE)
654   }
655 
get_current_route_mut(&mut self) -> &mut Route656   fn get_current_route_mut(&mut self) -> &mut Route {
657     self.navigation_stack.last_mut().unwrap()
658   }
659 
set_current_route_state( &mut self, active_block: Option<ActiveBlock>, hovered_block: Option<ActiveBlock>, )660   pub fn set_current_route_state(
661     &mut self,
662     active_block: Option<ActiveBlock>,
663     hovered_block: Option<ActiveBlock>,
664   ) {
665     let mut current_route = self.get_current_route_mut();
666     if let Some(active_block) = active_block {
667       current_route.active_block = active_block;
668     }
669     if let Some(hovered_block) = hovered_block {
670       current_route.hovered_block = hovered_block;
671     }
672   }
673 
copy_song_url(&mut self)674   pub fn copy_song_url(&mut self) {
675     let clipboard = match &mut self.clipboard {
676       Some(ctx) => ctx,
677       None => return,
678     };
679 
680     if let Some(CurrentlyPlaybackContext {
681       item: Some(item), ..
682     }) = &self.current_playback_context
683     {
684       match item {
685         PlayingItem::Track(track) => {
686           if let Err(e) = clipboard.set_text(format!(
687             "https://open.spotify.com/track/{}",
688             track.id.to_owned().unwrap_or_default()
689           )) {
690             self.handle_error(anyhow!("failed to set clipboard content: {}", e));
691           }
692         }
693         PlayingItem::Episode(episode) => {
694           if let Err(e) = clipboard.set_text(format!(
695             "https://open.spotify.com/episode/{}",
696             episode.id.to_owned()
697           )) {
698             self.handle_error(anyhow!("failed to set clipboard content: {}", e));
699           }
700         }
701       }
702     }
703   }
704 
copy_album_url(&mut self)705   pub fn copy_album_url(&mut self) {
706     let clipboard = match &mut self.clipboard {
707       Some(ctx) => ctx,
708       None => return,
709     };
710 
711     if let Some(CurrentlyPlaybackContext {
712       item: Some(item), ..
713     }) = &self.current_playback_context
714     {
715       match item {
716         PlayingItem::Track(track) => {
717           if let Err(e) = clipboard.set_text(format!(
718             "https://open.spotify.com/album/{}",
719             track.album.id.to_owned().unwrap_or_default()
720           )) {
721             self.handle_error(anyhow!("failed to set clipboard content: {}", e));
722           }
723         }
724         PlayingItem::Episode(episode) => {
725           if let Err(e) = clipboard.set_text(format!(
726             "https://open.spotify.com/show/{}",
727             episode.show.id.to_owned()
728           )) {
729             self.handle_error(anyhow!("failed to set clipboard content: {}", e));
730           }
731         }
732       }
733     }
734   }
735 
set_saved_tracks_to_table(&mut self, saved_track_page: &Page<SavedTrack>)736   pub fn set_saved_tracks_to_table(&mut self, saved_track_page: &Page<SavedTrack>) {
737     self.dispatch(IoEvent::SetTracksToTable(
738       saved_track_page
739         .items
740         .clone()
741         .into_iter()
742         .map(|item| item.track)
743         .collect::<Vec<FullTrack>>(),
744     ));
745   }
746 
set_saved_artists_to_table(&mut self, saved_artists_page: &CursorBasedPage<FullArtist>)747   pub fn set_saved_artists_to_table(&mut self, saved_artists_page: &CursorBasedPage<FullArtist>) {
748     self.dispatch(IoEvent::SetArtistsToTable(
749       saved_artists_page
750         .items
751         .clone()
752         .into_iter()
753         .collect::<Vec<FullArtist>>(),
754     ))
755   }
756 
get_current_user_saved_artists_next(&mut self)757   pub fn get_current_user_saved_artists_next(&mut self) {
758     match self
759       .library
760       .saved_artists
761       .get_results(Some(self.library.saved_artists.index + 1))
762       .cloned()
763     {
764       Some(saved_artists) => {
765         self.set_saved_artists_to_table(&saved_artists);
766         self.library.saved_artists.index += 1
767       }
768       None => {
769         if let Some(saved_artists) = &self.library.saved_artists.clone().get_results(None) {
770           match saved_artists.items.last() {
771             Some(last_artist) => {
772               self.dispatch(IoEvent::GetFollowedArtists(Some(last_artist.id.clone())));
773             }
774             None => {
775               return;
776             }
777           }
778         }
779       }
780     }
781   }
782 
get_current_user_saved_artists_previous(&mut self)783   pub fn get_current_user_saved_artists_previous(&mut self) {
784     if self.library.saved_artists.index > 0 {
785       self.library.saved_artists.index -= 1;
786     }
787 
788     if let Some(saved_artists) = &self.library.saved_artists.get_results(None).cloned() {
789       self.set_saved_artists_to_table(saved_artists);
790     }
791   }
792 
get_current_user_saved_tracks_next(&mut self)793   pub fn get_current_user_saved_tracks_next(&mut self) {
794     // Before fetching the next tracks, check if we have already fetched them
795     match self
796       .library
797       .saved_tracks
798       .get_results(Some(self.library.saved_tracks.index + 1))
799       .cloned()
800     {
801       Some(saved_tracks) => {
802         self.set_saved_tracks_to_table(&saved_tracks);
803         self.library.saved_tracks.index += 1
804       }
805       None => {
806         if let Some(saved_tracks) = &self.library.saved_tracks.get_results(None) {
807           let offset = Some(saved_tracks.offset + saved_tracks.limit);
808           self.dispatch(IoEvent::GetCurrentSavedTracks(offset));
809         }
810       }
811     }
812   }
813 
get_current_user_saved_tracks_previous(&mut self)814   pub fn get_current_user_saved_tracks_previous(&mut self) {
815     if self.library.saved_tracks.index > 0 {
816       self.library.saved_tracks.index -= 1;
817     }
818 
819     if let Some(saved_tracks) = &self.library.saved_tracks.get_results(None).cloned() {
820       self.set_saved_tracks_to_table(saved_tracks);
821     }
822   }
823 
shuffle(&mut self)824   pub fn shuffle(&mut self) {
825     if let Some(context) = &self.current_playback_context.clone() {
826       self.dispatch(IoEvent::Shuffle(context.shuffle_state));
827     };
828   }
829 
get_current_user_saved_albums_next(&mut self)830   pub fn get_current_user_saved_albums_next(&mut self) {
831     match self
832       .library
833       .saved_albums
834       .get_results(Some(self.library.saved_albums.index + 1))
835       .cloned()
836     {
837       Some(_) => self.library.saved_albums.index += 1,
838       None => {
839         if let Some(saved_albums) = &self.library.saved_albums.get_results(None) {
840           let offset = Some(saved_albums.offset + saved_albums.limit);
841           self.dispatch(IoEvent::GetCurrentUserSavedAlbums(offset));
842         }
843       }
844     }
845   }
846 
get_current_user_saved_albums_previous(&mut self)847   pub fn get_current_user_saved_albums_previous(&mut self) {
848     if self.library.saved_albums.index > 0 {
849       self.library.saved_albums.index -= 1;
850     }
851   }
852 
current_user_saved_album_delete(&mut self, block: ActiveBlock)853   pub fn current_user_saved_album_delete(&mut self, block: ActiveBlock) {
854     match block {
855       ActiveBlock::SearchResultBlock => {
856         if let Some(albums) = &self.search_results.albums {
857           if let Some(selected_index) = self.search_results.selected_album_index {
858             let selected_album = &albums.items[selected_index];
859             if let Some(album_id) = selected_album.id.clone() {
860               self.dispatch(IoEvent::CurrentUserSavedAlbumDelete(album_id));
861             }
862           }
863         }
864       }
865       ActiveBlock::AlbumList => {
866         if let Some(albums) = self.library.saved_albums.get_results(None) {
867           if let Some(selected_album) = albums.items.get(self.album_list_index) {
868             let album_id = selected_album.album.id.clone();
869             self.dispatch(IoEvent::CurrentUserSavedAlbumDelete(album_id));
870           }
871         }
872       }
873       ActiveBlock::ArtistBlock => {
874         if let Some(artist) = &self.artist {
875           if let Some(selected_album) = artist.albums.items.get(artist.selected_album_index) {
876             if let Some(album_id) = selected_album.id.clone() {
877               self.dispatch(IoEvent::CurrentUserSavedAlbumDelete(album_id));
878             }
879           }
880         }
881       }
882       _ => (),
883     }
884   }
885 
current_user_saved_album_add(&mut self, block: ActiveBlock)886   pub fn current_user_saved_album_add(&mut self, block: ActiveBlock) {
887     match block {
888       ActiveBlock::SearchResultBlock => {
889         if let Some(albums) = &self.search_results.albums {
890           if let Some(selected_index) = self.search_results.selected_album_index {
891             let selected_album = &albums.items[selected_index];
892             if let Some(album_id) = selected_album.id.clone() {
893               self.dispatch(IoEvent::CurrentUserSavedAlbumAdd(album_id));
894             }
895           }
896         }
897       }
898       ActiveBlock::ArtistBlock => {
899         if let Some(artist) = &self.artist {
900           if let Some(selected_album) = artist.albums.items.get(artist.selected_album_index) {
901             if let Some(album_id) = selected_album.id.clone() {
902               self.dispatch(IoEvent::CurrentUserSavedAlbumAdd(album_id));
903             }
904           }
905         }
906       }
907       _ => (),
908     }
909   }
910 
get_current_user_saved_shows_next(&mut self)911   pub fn get_current_user_saved_shows_next(&mut self) {
912     match self
913       .library
914       .saved_shows
915       .get_results(Some(self.library.saved_shows.index + 1))
916       .cloned()
917     {
918       Some(_) => self.library.saved_shows.index += 1,
919       None => {
920         if let Some(saved_shows) = &self.library.saved_shows.get_results(None) {
921           let offset = Some(saved_shows.offset + saved_shows.limit);
922           self.dispatch(IoEvent::GetCurrentUserSavedShows(offset));
923         }
924       }
925     }
926   }
927 
get_current_user_saved_shows_previous(&mut self)928   pub fn get_current_user_saved_shows_previous(&mut self) {
929     if self.library.saved_shows.index > 0 {
930       self.library.saved_shows.index -= 1;
931     }
932   }
933 
get_episode_table_next(&mut self, show_id: String)934   pub fn get_episode_table_next(&mut self, show_id: String) {
935     match self
936       .library
937       .show_episodes
938       .get_results(Some(self.library.show_episodes.index + 1))
939       .cloned()
940     {
941       Some(_) => self.library.show_episodes.index += 1,
942       None => {
943         if let Some(show_episodes) = &self.library.show_episodes.get_results(None) {
944           let offset = Some(show_episodes.offset + show_episodes.limit);
945           self.dispatch(IoEvent::GetCurrentShowEpisodes(show_id, offset));
946         }
947       }
948     }
949   }
950 
get_episode_table_previous(&mut self)951   pub fn get_episode_table_previous(&mut self) {
952     if self.library.show_episodes.index > 0 {
953       self.library.show_episodes.index -= 1;
954     }
955   }
956 
user_unfollow_artists(&mut self, block: ActiveBlock)957   pub fn user_unfollow_artists(&mut self, block: ActiveBlock) {
958     match block {
959       ActiveBlock::SearchResultBlock => {
960         if let Some(artists) = &self.search_results.artists {
961           if let Some(selected_index) = self.search_results.selected_artists_index {
962             let selected_artist: &FullArtist = &artists.items[selected_index];
963             let artist_id = selected_artist.id.clone();
964             self.dispatch(IoEvent::UserUnfollowArtists(vec![artist_id]));
965           }
966         }
967       }
968       ActiveBlock::AlbumList => {
969         if let Some(artists) = self.library.saved_artists.get_results(None) {
970           if let Some(selected_artist) = artists.items.get(self.artists_list_index) {
971             let artist_id = selected_artist.id.clone();
972             self.dispatch(IoEvent::UserUnfollowArtists(vec![artist_id]));
973           }
974         }
975       }
976       ActiveBlock::ArtistBlock => {
977         if let Some(artist) = &self.artist {
978           let selected_artis = &artist.related_artists[artist.selected_related_artist_index];
979           let artist_id = selected_artis.id.clone();
980           self.dispatch(IoEvent::UserUnfollowArtists(vec![artist_id]));
981         }
982       }
983       _ => (),
984     };
985   }
986 
user_follow_artists(&mut self, block: ActiveBlock)987   pub fn user_follow_artists(&mut self, block: ActiveBlock) {
988     match block {
989       ActiveBlock::SearchResultBlock => {
990         if let Some(artists) = &self.search_results.artists {
991           if let Some(selected_index) = self.search_results.selected_artists_index {
992             let selected_artist: &FullArtist = &artists.items[selected_index];
993             let artist_id = selected_artist.id.clone();
994             self.dispatch(IoEvent::UserFollowArtists(vec![artist_id]));
995           }
996         }
997       }
998       ActiveBlock::ArtistBlock => {
999         if let Some(artist) = &self.artist {
1000           let selected_artis = &artist.related_artists[artist.selected_related_artist_index];
1001           let artist_id = selected_artis.id.clone();
1002           self.dispatch(IoEvent::UserFollowArtists(vec![artist_id]));
1003         }
1004       }
1005       _ => (),
1006     }
1007   }
1008 
user_follow_playlist(&mut self)1009   pub fn user_follow_playlist(&mut self) {
1010     if let SearchResult {
1011       playlists: Some(ref playlists),
1012       selected_playlists_index: Some(selected_index),
1013       ..
1014     } = self.search_results
1015     {
1016       let selected_playlist: &SimplifiedPlaylist = &playlists.items[selected_index];
1017       let selected_id = selected_playlist.id.clone();
1018       let selected_public = selected_playlist.public;
1019       let selected_owner_id = selected_playlist.owner.id.clone();
1020       self.dispatch(IoEvent::UserFollowPlaylist(
1021         selected_owner_id,
1022         selected_id,
1023         selected_public,
1024       ));
1025     }
1026   }
1027 
user_unfollow_playlist(&mut self)1028   pub fn user_unfollow_playlist(&mut self) {
1029     if let (Some(playlists), Some(selected_index), Some(user)) =
1030       (&self.playlists, self.selected_playlist_index, &self.user)
1031     {
1032       let selected_playlist = &playlists.items[selected_index];
1033       let selected_id = selected_playlist.id.clone();
1034       let user_id = user.id.clone();
1035       self.dispatch(IoEvent::UserUnfollowPlaylist(user_id, selected_id))
1036     }
1037   }
1038 
user_unfollow_playlist_search_result(&mut self)1039   pub fn user_unfollow_playlist_search_result(&mut self) {
1040     if let (Some(playlists), Some(selected_index), Some(user)) = (
1041       &self.search_results.playlists,
1042       self.search_results.selected_playlists_index,
1043       &self.user,
1044     ) {
1045       let selected_playlist = &playlists.items[selected_index];
1046       let selected_id = selected_playlist.id.clone();
1047       let user_id = user.id.clone();
1048       self.dispatch(IoEvent::UserUnfollowPlaylist(user_id, selected_id))
1049     }
1050   }
1051 
user_follow_show(&mut self, block: ActiveBlock)1052   pub fn user_follow_show(&mut self, block: ActiveBlock) {
1053     match block {
1054       ActiveBlock::SearchResultBlock => {
1055         if let Some(shows) = &self.search_results.shows {
1056           if let Some(selected_index) = self.search_results.selected_shows_index {
1057             if let Some(show_id) = shows.items.get(selected_index).map(|item| item.id.clone()) {
1058               self.dispatch(IoEvent::CurrentUserSavedShowAdd(show_id));
1059             }
1060           }
1061         }
1062       }
1063       ActiveBlock::EpisodeTable => match self.episode_table_context {
1064         EpisodeTableContext::Full => {
1065           if let Some(selected_episode) = self.selected_show_full.clone() {
1066             let show_id = selected_episode.show.id;
1067             self.dispatch(IoEvent::CurrentUserSavedShowAdd(show_id));
1068           }
1069         }
1070         EpisodeTableContext::Simplified => {
1071           if let Some(selected_episode) = self.selected_show_simplified.clone() {
1072             let show_id = selected_episode.show.id;
1073             self.dispatch(IoEvent::CurrentUserSavedShowAdd(show_id));
1074           }
1075         }
1076       },
1077       _ => (),
1078     }
1079   }
1080 
user_unfollow_show(&mut self, block: ActiveBlock)1081   pub fn user_unfollow_show(&mut self, block: ActiveBlock) {
1082     match block {
1083       ActiveBlock::Podcasts => {
1084         if let Some(shows) = self.library.saved_shows.get_results(None) {
1085           if let Some(selected_show) = shows.items.get(self.shows_list_index) {
1086             let show_id = selected_show.show.id.clone();
1087             self.dispatch(IoEvent::CurrentUserSavedShowDelete(show_id));
1088           }
1089         }
1090       }
1091       ActiveBlock::SearchResultBlock => {
1092         if let Some(shows) = &self.search_results.shows {
1093           if let Some(selected_index) = self.search_results.selected_shows_index {
1094             let show_id = shows.items[selected_index].id.to_owned();
1095             self.dispatch(IoEvent::CurrentUserSavedShowDelete(show_id));
1096           }
1097         }
1098       }
1099       ActiveBlock::EpisodeTable => match self.episode_table_context {
1100         EpisodeTableContext::Full => {
1101           if let Some(selected_episode) = self.selected_show_full.clone() {
1102             let show_id = selected_episode.show.id;
1103             self.dispatch(IoEvent::CurrentUserSavedShowDelete(show_id));
1104           }
1105         }
1106         EpisodeTableContext::Simplified => {
1107           if let Some(selected_episode) = self.selected_show_simplified.clone() {
1108             let show_id = selected_episode.show.id;
1109             self.dispatch(IoEvent::CurrentUserSavedShowDelete(show_id));
1110           }
1111         }
1112       },
1113       _ => (),
1114     }
1115   }
1116 
get_made_for_you(&mut self)1117   pub fn get_made_for_you(&mut self) {
1118     // TODO: replace searches when relevant endpoint is added
1119     const DISCOVER_WEEKLY: &str = "Discover Weekly";
1120     const RELEASE_RADAR: &str = "Release Radar";
1121     const ON_REPEAT: &str = "On Repeat";
1122     const REPEAT_REWIND: &str = "Repeat Rewind";
1123     const DAILY_DRIVE: &str = "Daily Drive";
1124 
1125     if self.library.made_for_you_playlists.pages.is_empty() {
1126       // We shouldn't be fetching all the results immediately - only load the data when the
1127       // user selects the playlist
1128       self.made_for_you_search_and_add(DISCOVER_WEEKLY);
1129       self.made_for_you_search_and_add(RELEASE_RADAR);
1130       self.made_for_you_search_and_add(ON_REPEAT);
1131       self.made_for_you_search_and_add(REPEAT_REWIND);
1132       self.made_for_you_search_and_add(DAILY_DRIVE);
1133     }
1134   }
1135 
made_for_you_search_and_add(&mut self, search_string: &str)1136   fn made_for_you_search_and_add(&mut self, search_string: &str) {
1137     let user_country = self.get_user_country();
1138     self.dispatch(IoEvent::MadeForYouSearchAndAdd(
1139       search_string.to_string(),
1140       user_country,
1141     ));
1142   }
1143 
get_audio_analysis(&mut self)1144   pub fn get_audio_analysis(&mut self) {
1145     if let Some(CurrentlyPlaybackContext {
1146       item: Some(item), ..
1147     }) = &self.current_playback_context
1148     {
1149       match item {
1150         PlayingItem::Track(track) => {
1151           if self.get_current_route().id != RouteId::Analysis {
1152             let uri = track.uri.clone();
1153             self.dispatch(IoEvent::GetAudioAnalysis(uri));
1154             self.push_navigation_stack(RouteId::Analysis, ActiveBlock::Analysis);
1155           }
1156         }
1157         PlayingItem::Episode(_episode) => {
1158           // No audio analysis available for podcast uris, so just default to the empty analysis
1159           // view to avoid a 400 error code
1160           self.push_navigation_stack(RouteId::Analysis, ActiveBlock::Analysis);
1161         }
1162       }
1163     }
1164   }
1165 
repeat(&mut self)1166   pub fn repeat(&mut self) {
1167     if let Some(context) = &self.current_playback_context.clone() {
1168       self.dispatch(IoEvent::Repeat(context.repeat_state));
1169     }
1170   }
1171 
get_artist(&mut self, artist_id: String, input_artist_name: String)1172   pub fn get_artist(&mut self, artist_id: String, input_artist_name: String) {
1173     let user_country = self.get_user_country();
1174     self.dispatch(IoEvent::GetArtist(
1175       artist_id,
1176       input_artist_name,
1177       user_country,
1178     ));
1179   }
1180 
get_user_country(&self) -> Option<Country>1181   pub fn get_user_country(&self) -> Option<Country> {
1182     self
1183       .user
1184       .to_owned()
1185       .and_then(|user| Country::from_str(&user.country.unwrap_or_else(|| "".to_string())).ok())
1186   }
1187 
calculate_help_menu_offset(&mut self)1188   pub fn calculate_help_menu_offset(&mut self) {
1189     let old_offset = self.help_menu_offset;
1190 
1191     if self.help_menu_max_lines < self.help_docs_size {
1192       self.help_menu_offset = self.help_menu_page * self.help_menu_max_lines;
1193     }
1194     if self.help_menu_offset > self.help_docs_size {
1195       self.help_menu_offset = old_offset;
1196       self.help_menu_page -= 1;
1197     }
1198   }
1199 }
1200