From ebf1be2cb9548ad7765990192c976bfd9f371b59 Mon Sep 17 00:00:00 2001 From: nicknase27 Date: Mon, 27 Jul 2026 18:51:11 +0200 Subject: [PATCH] add help menu and remove unnecessary function --- src/app.rs | 92 ++++++++++++++++++++++++++++-------------------------- src/ui.rs | 85 +++++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 122 insertions(+), 55 deletions(-) diff --git a/src/app.rs b/src/app.rs index fe4f723..9e4183c 100644 --- a/src/app.rs +++ b/src/app.rs @@ -42,6 +42,7 @@ pub struct App { pub song_state: ListState, pub queue_state: ListState, pub focus: Focus, + pub control_popup: bool, pub current_song: Option, pub paused_at: i64, @@ -82,6 +83,7 @@ impl App { song_state: ListState::default().with_selected(Some(0)), queue_state: ListState::default().with_selected(Some(0)), focus: Focus::default(), + control_popup: false, events: EventHandler::new(), current_song: None, paused_at: 1, @@ -131,7 +133,13 @@ impl App { /// Handles the key events and updates the state of [`App`]. pub fn handle_key_events(&mut self, key_event: KeyEvent) -> color_eyre::Result<()> { match key_event.code { - KeyCode::Esc | KeyCode::Char('q') => self.events.send(AppEvent::Quit), + KeyCode::Esc | KeyCode::Char('q') => { + if self.control_popup == false { + self.events.send(AppEvent::Quit) + } else { + self.control_popup = !self.control_popup; + } + } KeyCode::Char('c' | 'C') if key_event.modifiers == KeyModifiers::CONTROL => { self.events.send(AppEvent::Quit) } @@ -141,10 +149,16 @@ impl App { self.library.save(); } + KeyCode::Char('?') => { + self.control_popup = !self.control_popup; + } + KeyCode::Char('l') | KeyCode::Right | KeyCode::Tab => { self.events.send(AppEvent::MoveRight) } - KeyCode::Char('h') | KeyCode::Left => self.events.send(AppEvent::MoveLeft), + KeyCode::Char('h') | KeyCode::Left | KeyCode::BackTab => { + self.events.send(AppEvent::MoveLeft) + } KeyCode::Char('j') | KeyCode::Down => match self.focus { Focus::Artists => self.artist_state.select_next(), @@ -212,49 +226,6 @@ impl App { song_indexes.to_owned() } - // pub fn play_pause(&mut self) { - // if self.current_song().is_some() { - // let song = &self.library.songs[self.current_song.unwrap()]; - // let duration = song.duration.unwrap_or(1.0) as i64; - // let artist = &song.artist; - // if let Some(client) = self.discord_client.as_mut() { - // if !self.player.is_paused() { - // self.player.pause(); - // self.paused_at = self.player.get_pos().as_secs() as i64; - // - // let asset = Assets::new(); - // let activity = activity::Activity::new() - // .name("NCMPRS") - // .details(song.title.clone()) - // .state("Paused") - // .activity_type(activity::ActivityType::Listening) - // .assets(asset.large_image("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/navidrome.png")); - // let _ = client.set_activity(activity); - // } else if self.player.is_paused() { - // self.player.play(); - // let now = SystemTime::now() - // .duration_since(UNIX_EPOCH) - // .unwrap() - // .as_secs() as i64; - // let timestamps = Timestamps::new() - // .start(now - self.paused_at) - // .end((now - self.paused_at) + duration); - // let asset = Assets::new(); - // - // let payload = activity::Activity::new() - // .name("NCMPRS") - // .details(song.title.clone()) - // .state(artist) - // .activity_type(activity::ActivityType::Listening) - // .timestamps(timestamps) - // .assets(asset.large_image("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/navidrome.png")); - // - // let _ = client.set_activity(payload); - // } - // } - // } - // } - pub fn play_pause(&mut self) { let song_idx = match self.current_song { Some(idx) => idx, @@ -413,6 +384,37 @@ impl App { let _ = client.set_activity(activity); } + pub fn get_controls(&mut self) -> Vec> { + let rows = [ + ratatui::widgets::Row::new(["", "General"]), + ratatui::widgets::Row::new(["", "Q / Esc / Ctrl+C", "Quit"]), + ratatui::widgets::Row::new(["", "R", "Rescan music library"]), + ratatui::widgets::Row::new(["", "?", "Show the help menu"]), + ratatui::widgets::Row::new(["", ""]), + ratatui::widgets::Row::new(["", "Navigation"]), + ratatui::widgets::Row::new(["", "H / Shift+Tab", "Left"]), + ratatui::widgets::Row::new(["", "J", "Down"]), + ratatui::widgets::Row::new(["", "K", "Up"]), + ratatui::widgets::Row::new(["", "L / Tab", "Right"]), + ratatui::widgets::Row::new(["", "g", "Jump to first item"]), + ratatui::widgets::Row::new(["", "G", "Jump to last item"]), + ratatui::widgets::Row::new(["", ""]), + ratatui::widgets::Row::new(["", "Playback"]), + ratatui::widgets::Row::new(["", "Enter", "Play selected song or album"]), + ratatui::widgets::Row::new(["", "Space", "Play / Pause"]), + ratatui::widgets::Row::new(["", "X", "Stop Playback"]), + ratatui::widgets::Row::new(["", ">", "Skip current track"]), + ratatui::widgets::Row::new(["", ""]), + ratatui::widgets::Row::new(["", "Queue"]), + ratatui::widgets::Row::new(["", "Delete", "Remove selected song from queue"]), + ratatui::widgets::Row::new(["", ""]), + ratatui::widgets::Row::new(["", "Volume"]), + ratatui::widgets::Row::new(["", "+ / =", "Increase volume"]), + ratatui::widgets::Row::new(["", "- / _", "Decrease volume"]), + ]; + return rows.to_vec(); + } + /// Handles the tick event of the terminal. /// /// The tick event is where you can update the state of your application with any logic that diff --git a/src/ui.rs b/src/ui.rs index 2c84dba..8ab2289 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,16 @@ use ratatui::{ - buffer::Buffer, layout::{ + buffer::Buffer, + layout::{ Alignment, Constraint, Direction::{self}, Layout, Rect, - }, style::{Color, Modifier, Style}, symbols, widgets::{ - Block, BorderType::{self, Rounded}, LineGauge, List, ListDirection, ListItem, Paragraph, StatefulWidget, Widget, + }, + style::{Color, Modifier, Style, Stylize}, + symbols, + text::Line, + widgets::{ + Block, BorderType::Rounded, Clear, LineGauge, List, ListDirection, ListItem, Paragraph, + Row, StatefulWidget, Table, Widget, }, }; @@ -42,7 +48,12 @@ impl Widget for &mut App { ]) .split(outer_layout[1]); - let now_playing_block = Block::bordered().title(format!("Now Playing | Vol: {:.0}", self.player.volume() * 100.0)).border_type(Rounded); + let now_playing_block = Block::bordered() + .title(format!( + "Now Playing | Vol: {:.0}", + self.player.volume() * 100.0 + )) + .border_type(Rounded); let inner = now_playing_block.inner(center_area[1]); Widget::render(now_playing_block, center_area[1], buf); @@ -89,6 +100,7 @@ impl Widget for &mut App { Color::Gray }; + // Artists let artists: Vec = self .library .artists @@ -100,7 +112,8 @@ impl Widget for &mut App { .block( Block::bordered() .title("Artists") - .border_style(Style::new().fg(artist_border_color)).border_type(Rounded), + .border_style(Style::new().fg(artist_border_color)) + .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") @@ -110,6 +123,13 @@ impl Widget for &mut App { StatefulWidget::render(artist_list, left_area[0], buf, &mut self.artist_state); + // Info + let instructions = Line::from(vec![ + " Help ".gray().bold(), + " ".blue().bold(), + ]); + + // Albums let albums: Vec = self .library .albums_for_artist( @@ -123,7 +143,9 @@ impl Widget for &mut App { .block( Block::bordered() .title("Albums") - .border_style(Style::new().fg(album_border_color)).border_type(Rounded), + .title_bottom(instructions.right_aligned()) + .border_style(Style::new().fg(album_border_color)) + .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") @@ -133,18 +155,26 @@ impl Widget for &mut App { StatefulWidget::render(album_list, center_area[0], buf, &mut self.album_state); + // Songs let song_indexes: Vec = self.current_selected_song(); let song_items: Vec = song_indexes .iter() - .map(|&index| ListItem::new(format!("{} ({})", self.library.songs[index].title.clone(), format_time(self.library.songs[index].duration.unwrap())))) + .map(|&index| { + ListItem::new(format!( + "{} ({})", + self.library.songs[index].title.clone(), + format_time(self.library.songs[index].duration.unwrap()) + )) + }) .collect(); let song_list = List::new(song_items) .block( Block::bordered() .title("Songs") - .border_style(Style::new().fg(song_border_color)).border_type(Rounded), + .border_style(Style::new().fg(song_border_color)) + .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") @@ -154,14 +184,26 @@ impl Widget for &mut App { StatefulWidget::render(song_list, right_area[0], buf, &mut self.song_state); + // Queue let queue_items: Vec = self .queue .iter() - .map(|&index| ListItem::new(format!("{} ({}))", self.library.songs[index].title.as_str(), format_time(self.library.songs[index].duration.unwrap())))) + .map(|&index| { + ListItem::new(format!( + "{} ({}))", + self.library.songs[index].title.as_str(), + format_time(self.library.songs[index].duration.unwrap()) + )) + }) .collect(); let queue_list = List::new(queue_items) - .block(Block::bordered().title("Queue").border_style(Style::new().fg(queue_border_color)).border_type(Rounded)) + .block( + Block::bordered() + .title("Queue") + .border_style(Style::new().fg(queue_border_color)) + .border_type(Rounded), + ) .style(Style::new().white()) .highlight_symbol(">>") .repeat_highlight_symbol(true) @@ -170,6 +212,29 @@ impl Widget for &mut App { StatefulWidget::render(queue_list, right_area[1], buf, &mut self.queue_state); + // Help Menu + if self.control_popup { + let popup_block = Block::bordered().title("Controls").border_type(Rounded); + Widget::render(Clear, center_area[0], buf); + + let header = Row::new(["", "Key", "Function"]) + .style(Style::new().bold()) + .bottom_margin(1); + + let rows = self.get_controls(); + + let widths = [Constraint::Percentage(10), Constraint::Percentage(45), Constraint::Percentage(45)]; + + let help_menu = Table::new(rows, widths) + .header(header) + .column_spacing(1) + .style(Color::White) + .block(popup_block); + + Widget::render(help_menu, center_area[0], buf); + } + + // Play area if let Some(song) = self.current_song() { let duration = song.duration.unwrap_or(1.0); Paragraph::new(song.title.as_str())