diff options
| -rw-r--r-- | src/app.rs | 122 | ||||
| -rw-r--r-- | src/main.rs | 82 | ||||
| -rw-r--r-- | src/podcast_ui.rs | 4 |
3 files changed, 205 insertions, 3 deletions
@@ -8,6 +8,7 @@ pub enum CurrentScreen { FeedList, EpisodeList, NowPlaying, + MusicDiscovery, } pub struct App { @@ -19,6 +20,8 @@ pub struct App { pub player: Player, pub current_track: Option<CurrentTrack>, pub volume: u8, + pub selected_genre: usize, + pub cjsw_shows: Vec<CjswShow>, feed_receiver: mpsc::Receiver<(usize, Feed)>, } @@ -30,6 +33,15 @@ pub struct CurrentTrack { pub _position: f64, // seconds } +#[derive(Debug, Clone)] +pub struct CjswShow { + pub name: String, + pub genre: String, + pub time_slot: String, + pub description: String, + pub day: String, +} + impl App { pub fn new() -> Result<Self> { let config = Config::load()?; @@ -64,6 +76,8 @@ impl App { player: Player::new()?, current_track: None, volume: 70, // Default volume 70% + selected_genre: 0, + cjsw_shows: Self::load_cjsw_shows(), feed_receiver: receiver, }) } @@ -212,4 +226,112 @@ impl App { } } } + + // Music discovery navigation + pub fn go_to_music_discovery(&mut self) { + self.current_screen = CurrentScreen::MusicDiscovery; + self.selected_genre = 0; + } + + pub fn next_genre(&mut self) { + if !self.cjsw_shows.is_empty() { + let unique_genres = self.get_unique_genres(); + if !unique_genres.is_empty() { + self.selected_genre = (self.selected_genre + 1) % unique_genres.len(); + } + } + } + + pub fn previous_genre(&mut self) { + if !self.cjsw_shows.is_empty() { + let unique_genres = self.get_unique_genres(); + if !unique_genres.is_empty() { + self.selected_genre = if self.selected_genre == 0 { + unique_genres.len() - 1 + } else { + self.selected_genre - 1 + }; + } + } + } + + pub fn get_unique_genres(&self) -> Vec<String> { + let mut genres: Vec<String> = self.cjsw_shows + .iter() + .map(|show| show.genre.clone()) + .collect::<std::collections::HashSet<_>>() + .into_iter() + .collect(); + genres.sort(); + genres + } + + pub fn get_shows_by_genre(&self, genre: &str) -> Vec<&CjswShow> { + self.cjsw_shows + .iter() + .filter(|show| show.genre == genre) + .collect() + } + + // Load CJSW shows data + fn load_cjsw_shows() -> Vec<CjswShow> { + vec![ + CjswShow { + name: "Black Milk".to_string(), + genre: "Electronic/Experimental".to_string(), + time_slot: "Monday 1:00-3:00 AM".to_string(), + description: "Electronic and experimental music exploration".to_string(), + day: "Monday".to_string(), + }, + CjswShow { + name: "Soular Power".to_string(), + genre: "R&B/Soul".to_string(), + time_slot: "Tuesday 7:00-9:00 PM".to_string(), + description: "Classic and contemporary R&B and soul".to_string(), + day: "Tuesday".to_string(), + }, + CjswShow { + name: "Fade to Bass".to_string(), + genre: "Electronic".to_string(), + time_slot: "Friday 11:00 PM-1:00 AM".to_string(), + description: "House and techno music journey".to_string(), + day: "Friday".to_string(), + }, + CjswShow { + name: "Noise".to_string(), + genre: "Experimental".to_string(), + time_slot: "Wednesday 2:00-4:00 AM".to_string(), + description: "30+ years of avant-garde experimental music".to_string(), + day: "Wednesday".to_string(), + }, + CjswShow { + name: "Local Singles".to_string(), + genre: "Local/Indie".to_string(), + time_slot: "Thursday 6:00-8:00 PM".to_string(), + description: "Featuring Calgary and Alberta local artists".to_string(), + day: "Thursday".to_string(), + }, + CjswShow { + name: "CantoStars".to_string(), + genre: "Multicultural".to_string(), + time_slot: "Sunday 10:00 AM-12:00 PM".to_string(), + description: "Cantonese music and cultural programming".to_string(), + day: "Sunday".to_string(), + }, + CjswShow { + name: "Sonic Cycle".to_string(), + genre: "Indie Pop/Rock".to_string(), + time_slot: "Saturday 3:00-5:00 PM".to_string(), + description: "Genre-blending indie music journey".to_string(), + day: "Saturday".to_string(), + }, + CjswShow { + name: "Jazz Spectrum".to_string(), + genre: "Jazz".to_string(), + time_slot: "Monday 8:00-10:00 PM".to_string(), + description: "Classic to contemporary jazz exploration".to_string(), + day: "Monday".to_string(), + }, + ] + } } diff --git a/src/main.rs b/src/main.rs index 3ad8c05..d6ff5cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,7 +6,7 @@ use crossterm::{ }; use ratatui::{ backend::{Backend, CrosstermBackend}, - layout::{Constraint, Direction, Layout}, + layout::{Constraint, Direction, Layout, Rect}, style::{Color, Modifier, Style}, text::{Line, Span}, widgets::{Block, Borders, List, ListItem, ListState, Paragraph}, @@ -119,6 +119,7 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> KeyCode::Char('j') | KeyCode::Down => app.next_episode(), KeyCode::Char('k') | KeyCode::Up => app.previous_episode(), KeyCode::Enter | KeyCode::Char(' ') => app.play_episode()?, + KeyCode::Char('m') => app.go_to_music_discovery(), _ => {} }, CurrentScreen::NowPlaying => match key.code { @@ -134,6 +135,14 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> // 1-minute seeking KeyCode::Char('L') => app.skip_forward_long()?, KeyCode::Char('J') => app.skip_backward_long()?, + KeyCode::Char('m') => app.go_to_music_discovery(), + _ => {} + }, + CurrentScreen::MusicDiscovery => match key.code { + KeyCode::Char('q') => return Ok(()), + KeyCode::Char('h') | KeyCode::Left | KeyCode::Esc => app.back_to_episodes(), + KeyCode::Char('j') | KeyCode::Down => app.next_genre(), + KeyCode::Char('k') | KeyCode::Up => app.previous_genre(), _ => {} }, } @@ -142,6 +151,74 @@ fn run_app<B: Backend>(terminal: &mut Terminal<B>, app: &mut App) -> Result<()> } } +fn render_music_discovery(f: &mut Frame, app: &App, area: Rect) { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([ + Constraint::Length(3), // Title + Constraint::Length(4), // Genre selector + Constraint::Min(8), // Shows list + Constraint::Length(3), // Help + ]) + .split(area); + + // Title + let title = Paragraph::new("🎵 CJSW Music Discovery") + .style(Style::default().fg(Color::Green).add_modifier(Modifier::BOLD)) + .block(Block::default().borders(Borders::ALL)) + .alignment(ratatui::layout::Alignment::Center); + f.render_widget(title, chunks[0]); + + // Genre selector + let genres = app.get_unique_genres(); + let selected_genre = if genres.is_empty() { + "No genres available".to_string() + } else { + genres.get(app.selected_genre).unwrap_or(&"Unknown".to_string()).clone() + }; + + let genre_info = format!("Genre: {} ({}/{})", selected_genre, app.selected_genre + 1, genres.len()); + let genre_widget = Paragraph::new(genre_info) + .style(Style::default().fg(Color::Yellow)) + .block(Block::default().title("Browse Genres").borders(Borders::ALL)); + f.render_widget(genre_widget, chunks[1]); + + // Shows list + if !genres.is_empty() { + let shows = app.get_shows_by_genre(&selected_genre); + let show_items: Vec<ListItem> = shows + .iter() + .map(|show| { + ListItem::new(Line::from(vec![ + Span::styled(&show.name, Style::default().fg(Color::White).add_modifier(Modifier::BOLD)), + Span::raw(" - "), + Span::styled(&show.time_slot, Style::default().fg(Color::Cyan)), + Span::raw(" - "), + Span::styled(&show.day, Style::default().fg(Color::Green)), + ])) + }) + .collect(); + + let shows_list = List::new(show_items) + .block(Block::default().title(format!("{} Shows", selected_genre)).borders(Borders::ALL)) + .style(Style::default().fg(Color::White)); + f.render_widget(shows_list, chunks[2]); + } else { + let no_shows = Paragraph::new("No shows available") + .style(Style::default().fg(Color::DarkGray)) + .block(Block::default().title("Shows").borders(Borders::ALL)) + .alignment(ratatui::layout::Alignment::Center); + f.render_widget(no_shows, chunks[2]); + } + + // Help + let help_text = "Navigation: j/k or ↑/↓ - browse genres | ESC/h - back | q - quit"; + let help = Paragraph::new(help_text) + .style(Style::default().fg(Color::DarkGray)) + .block(Block::default().title("Help").borders(Borders::ALL)); + f.render_widget(help, chunks[3]); +} + fn ui(f: &mut Frame, app: &App) { let chunks = Layout::default() .direction(Direction::Horizontal) @@ -274,5 +351,8 @@ Navigation: CurrentScreen::NowPlaying => { podcast_ui::render_now_playing_enhanced(f, app, chunks[1]); } + CurrentScreen::MusicDiscovery => { + render_music_discovery(f, app, chunks[1]); + } } } diff --git a/src/podcast_ui.rs b/src/podcast_ui.rs index 67c4ffd..e39ccbe 100644 --- a/src/podcast_ui.rs +++ b/src/podcast_ui.rs @@ -229,9 +229,9 @@ fn render_progress_and_controls(f: &mut Frame, app: &App, area: Rect) { // Controls help with seeking info let controls_text = if app.player.is_paused() { - "⏸ PAUSED | SPACE:play | s:stop | ±:vol | j/l:±15s | J/L:±1m | ESC:back | q:quit" + "⏸ PAUSED | SPACE:play | s:stop | ±:vol | j/l:±15s | J/L:±1m | m:music | ESC:back | q:quit" } else { - "▶ PLAYING | SPACE:pause | s:stop | ±:vol | j/l:±15s | J/L:±1m | ESC:back | q:quit" + "▶ PLAYING | SPACE:pause | s:stop | ±:vol | j/l:±15s | J/L:±1m | m:music | ESC:back | q:quit" }; let controls_widget = Paragraph::new(controls_text) .style(Style::default().fg(Color::DarkGray)) |
