main
  1use crate::app::{App, CurrentTrack};
  2use ratatui::{
  3    layout::{Constraint, Direction, Layout, Rect},
  4    style::{Color, Modifier, Style},
  5    text::{Line, Span},
  6    widgets::{Block, Borders, Gauge, Paragraph, Wrap},
  7    Frame,
  8};
  9
 10pub fn render_now_playing_enhanced(f: &mut Frame, app: &App, area: Rect) {
 11    if let Some(track) = &app.current_track {
 12        // Create a podcast-focused layout
 13        let main_chunks = Layout::default()
 14            .direction(Direction::Vertical)
 15            .constraints([
 16                Constraint::Length(3), // Title bar
 17                Constraint::Length(4), // Episode info header
 18                Constraint::Min(8),    // Episode details/description
 19                Constraint::Length(4), // Progress and controls
 20            ])
 21            .split(area);
 22
 23        // Title bar
 24        let title_bar = Paragraph::new("♪ GHETTO-BLASTER ♪")
 25            .style(
 26                Style::default()
 27                    .fg(Color::Green)
 28                    .add_modifier(Modifier::BOLD),
 29            )
 30            .block(Block::default().borders(Borders::ALL))
 31            .alignment(ratatui::layout::Alignment::Center);
 32        f.render_widget(title_bar, main_chunks[0]);
 33
 34        // Episode info header
 35        render_episode_header(f, app, track, main_chunks[1]);
 36
 37        // Episode details area
 38        render_episode_details(f, app, track, main_chunks[2]);
 39
 40        // Progress and controls
 41        render_progress_and_controls(f, app, main_chunks[3]);
 42    }
 43}
 44
 45fn render_episode_header(f: &mut Frame, app: &App, track: &CurrentTrack, area: Rect) {
 46    let header_chunks = Layout::default()
 47        .direction(Direction::Horizontal)
 48        .constraints([Constraint::Percentage(70), Constraint::Percentage(30)])
 49        .split(area);
 50
 51    // Episode and podcast info
 52    let episode_info = format!(
 53        "Episode: {}\nPodcast: {}",
 54        truncate_string(&track.title, 50),
 55        track.podcast_name
 56    );
 57    let info_widget = Paragraph::new(episode_info)
 58        .style(Style::default().fg(Color::White))
 59        .block(Block::default().title("Now Playing").borders(Borders::ALL));
 60    f.render_widget(info_widget, header_chunks[0]);
 61
 62    // Status and cache info
 63    let status_info = get_episode_status_info(app);
 64    let status_widget = Paragraph::new(status_info)
 65        .style(Style::default().fg(Color::Cyan))
 66        .block(Block::default().title("Status").borders(Borders::ALL));
 67    f.render_widget(status_widget, header_chunks[1]);
 68}
 69
 70fn render_episode_details(f: &mut Frame, app: &App, _track: &CurrentTrack, area: Rect) {
 71    let detail_chunks = Layout::default()
 72        .direction(Direction::Horizontal)
 73        .constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
 74        .split(area);
 75
 76    // Episode metadata
 77    render_episode_metadata(f, app, detail_chunks[0]);
 78
 79    // Episode description/show notes
 80    render_episode_description(f, app, detail_chunks[1]);
 81}
 82
 83fn render_episode_metadata(f: &mut Frame, app: &App, area: Rect) {
 84    let mut metadata_lines = vec![
 85        Line::from(Span::styled(
 86            "Episode Metadata",
 87            Style::default()
 88                .fg(Color::Yellow)
 89                .add_modifier(Modifier::BOLD),
 90        )),
 91        Line::from(""),
 92    ];
 93
 94    // Get current episode data
 95    if let Some(feed) = app.feeds.get(app.selected_feed) {
 96        if let Some(episode) = feed.episodes.get(app.selected_episode) {
 97            // Published date
 98            let pub_date = episode.published_at.format("%B %d, %Y at %I:%M %p UTC");
 99            metadata_lines.push(Line::from(vec![
100                Span::styled("Published: ", Style::default().fg(Color::Green)),
101                Span::raw(pub_date.to_string()),
102            ]));
103
104            // Duration
105            if let Some(duration) = &episode._duration {
106                metadata_lines.push(Line::from(vec![
107                    Span::styled("Duration: ", Style::default().fg(Color::Green)),
108                    Span::raw(duration.clone()),
109                ]));
110            }
111
112            // Cache status
113            let cache_status = if episode.is_cached {
114                ("Cached Locally", Color::Green)
115            } else {
116                ("Streaming", Color::Yellow)
117            };
118            metadata_lines.push(Line::from(vec![
119                Span::styled("Source: ", Style::default().fg(Color::Green)),
120                Span::styled(cache_status.0, Style::default().fg(cache_status.1)),
121            ]));
122
123            // File info if cached
124            if episode.is_cached {
125                if let Some(local_path) = &episode.local_path {
126                    let filename = std::path::Path::new(local_path)
127                        .file_name()
128                        .unwrap_or_default()
129                        .to_string_lossy();
130                    metadata_lines.push(Line::from(vec![
131                        Span::styled("File: ", Style::default().fg(Color::Green)),
132                        Span::raw(filename.to_string()),
133                    ]));
134                }
135            }
136
137            metadata_lines.push(Line::from(""));
138
139            // Playback position
140            let position_text = app.player.get_position_formatted();
141            let duration_text = app.player.get_duration_formatted();
142            metadata_lines.push(Line::from(vec![
143                Span::styled("Position: ", Style::default().fg(Color::Blue)),
144                Span::raw(format!("{} / {}", position_text, duration_text)),
145            ]));
146        }
147    }
148
149    let metadata_widget = Paragraph::new(metadata_lines)
150        .block(Block::default().title("Details").borders(Borders::ALL))
151        .wrap(Wrap { trim: true });
152    f.render_widget(metadata_widget, area);
153}
154
155fn render_episode_description(f: &mut Frame, app: &App, area: Rect) {
156    let mut description_text = "No description available.".to_string();
157
158    // Get episode description
159    if let Some(feed) = app.feeds.get(app.selected_feed) {
160        if let Some(episode) = feed.episodes.get(app.selected_episode) {
161            description_text = episode.description.clone();
162
163            // Clean up HTML tags if present
164            description_text = clean_html_tags(&description_text);
165
166            // Limit length for display
167            if description_text.len() > 800 {
168                description_text.truncate(800);
169                description_text.push_str("...");
170            }
171        }
172    }
173
174    let description_widget = Paragraph::new(description_text)
175        .style(Style::default().fg(Color::White))
176        .block(
177            Block::default()
178                .title("Episode Description")
179                .borders(Borders::ALL),
180        )
181        .wrap(Wrap { trim: true });
182    f.render_widget(description_widget, area);
183}
184
185fn render_progress_and_controls(f: &mut Frame, app: &App, area: Rect) {
186    let progress_chunks = Layout::default()
187        .direction(Direction::Horizontal)
188        .constraints([
189            Constraint::Percentage(30),
190            Constraint::Percentage(40),
191            Constraint::Percentage(30),
192        ])
193        .split(area);
194
195    // Volume gauge
196    let volume_gauge = Gauge::default()
197        .block(Block::default().title("Volume").borders(Borders::ALL))
198        .gauge_style(Style::default().fg(Color::Cyan))
199        .percent(app.volume as u16)
200        .label(format!("{}%", app.volume));
201    f.render_widget(volume_gauge, progress_chunks[0]);
202
203    // Progress bar with visual indicator
204    let position = app.player.get_position();
205    let progress_text = format!(
206        "{} / {}",
207        app.player.get_position_formatted(),
208        app.player.get_duration_formatted()
209    );
210
211    // Create a simple text-based progress bar if we have duration
212    let progress_display = if let Some(duration) = app.player.get_duration() {
213        let progress_percentage = ((position / duration) * 100.0) as u16;
214        let progress_gauge = Gauge::default()
215            .block(Block::default().title("Progress").borders(Borders::ALL))
216            .gauge_style(Style::default().fg(Color::Yellow))
217            .percent(progress_percentage.min(100))
218            .label(progress_text);
219        f.render_widget(progress_gauge, progress_chunks[1]);
220        return;
221    } else {
222        Paragraph::new(progress_text)
223            .style(Style::default().fg(Color::Yellow))
224            .block(Block::default().title("Progress").borders(Borders::ALL))
225            .alignment(ratatui::layout::Alignment::Center)
226    };
227
228    f.render_widget(progress_display, progress_chunks[1]);
229
230    // Controls help with seeking info
231    let controls_text = if app.player.is_paused() {
232        "⏸ PAUSED | SPACE:play | s:stop | ±:vol | j/l:±15s | J/L:±1m | m:music | ESC:back | q:quit"
233    } else {
234        "▶ PLAYING | SPACE:pause | s:stop | ±:vol | j/l:±15s | J/L:±1m | m:music | ESC:back | q:quit"
235    };
236    let controls_widget = Paragraph::new(controls_text)
237        .style(Style::default().fg(Color::DarkGray))
238        .block(Block::default().title("Controls").borders(Borders::ALL))
239        .wrap(Wrap { trim: true });
240    f.render_widget(controls_widget, progress_chunks[2]);
241}
242
243fn get_episode_status_info(app: &App) -> String {
244    let playback_status = if app.player.is_paused() {
245        "⏸ Paused"
246    } else {
247        "▶ Playing"
248    };
249
250    let cache_status = if let Some(feed) = app.feeds.get(app.selected_feed) {
251        if let Some(episode) = feed.episodes.get(app.selected_episode) {
252            if episode.is_cached {
253                "💾 Local"
254            } else {
255                "🌐 Stream"
256            }
257        } else {
258            "❓ Unknown"
259        }
260    } else {
261        "❓ Unknown"
262    };
263
264    format!("{}\n{}", playback_status, cache_status)
265}
266
267fn truncate_string(s: &str, max_len: usize) -> String {
268    if s.len() <= max_len {
269        s.to_string()
270    } else {
271        format!("{}...", &s[..max_len.saturating_sub(3)])
272    }
273}
274
275fn clean_html_tags(html: &str) -> String {
276    // Simple HTML tag removal (basic implementation)
277    let mut result = String::new();
278    let mut in_tag = false;
279
280    for ch in html.chars() {
281        match ch {
282            '<' => in_tag = true,
283            '>' => in_tag = false,
284            _ if !in_tag => result.push(ch),
285            _ => {}
286        }
287    }
288
289    // Clean up extra whitespace
290    result.split_whitespace().collect::<Vec<&str>>().join(" ")
291}