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