From 39468ce5ba037cb40609f4d32c5f7b4ed0255c72 Mon Sep 17 00:00:00 2001 From: Jika Date: Mon, 31 Mar 2025 23:58:06 +0200 Subject: [PATCH] First progress bar version --- src/app.rs | 19 +++++++--- src/player.rs | 19 +++++++++- src/widgets/mod.rs | 2 + src/widgets/progress.rs | 83 +++++++++++++++++++++++++++++++++++++++++ src/widgets/queue.rs | 25 +++++++++++-- 5 files changed, 137 insertions(+), 11 deletions(-) create mode 100644 src/widgets/progress.rs diff --git a/src/app.rs b/src/app.rs index 722c0ca..0417fdd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -3,7 +3,7 @@ use crossterm::event::{Event as CrosstermEvent, KeyCode, KeyEvent}; use ratatui::{ Terminal, buffer::Buffer, - layout::{Constraint, Flex, Layout, Rect}, + layout::{Constraint, Direction, Flex, Layout, Rect}, prelude::CrosstermBackend, widgets::{Block, Clear, Paragraph, StatefulWidget, TableState, Widget, Wrap}, }; @@ -18,7 +18,7 @@ use crate::{ event::{Event, Events}, player::Player, subsonic_helper, - widgets::Queue, + widgets::{Progress, Queue}, }; #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] @@ -161,7 +161,7 @@ impl App { Action::TryPlaySong(id) => self.player.try_play(id, self.client.clone())?, Action::AddToSink(id) => { let res = self.player.add_to_sink(id); - if res.is_err(){ + if res.is_err() { self.tx.send(Action::TrackEnded)?; } res? @@ -229,18 +229,27 @@ impl App { impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![Constraint::Min(0), Constraint::Percentage(15)]) + .split(area); + match self.mode { Mode::Help => {} Mode::Popup => {} Mode::Quit => {} - Mode::Queue => StatefulWidget::render( + Mode::Queue => + StatefulWidget::render( &Queue::new(self.player.songs_list.clone(), self.player.current_playing), - area, + layout[0], buf, &mut self.queue_state, ), }; + + Widget::render(&Progress::new(&self.player), layout[1],buf); + if let Some((title, message)) = &mut self.popup { let block = Block::bordered().title(title.as_str()); let area = popup_area(area, 60, 20); diff --git a/src/player.rs b/src/player.rs index 4e3c20f..5f45fd8 100644 --- a/src/player.rs +++ b/src/player.rs @@ -3,7 +3,6 @@ use cpal::traits::HostTrait; use futures::StreamExt; use reqwest::Client; use rodio::{OutputStream, Sink, Source}; -use tracing::debug; use std::fs::{self, File}; use std::io::BufReader; use std::sync::{Arc, Mutex}; @@ -11,6 +10,7 @@ use std::time::Duration; use subsonic_types::request; use subsonic_types::response::Child; use tokio::sync::mpsc::UnboundedSender; +use tracing::debug; use crate::action::Action; use crate::config; @@ -95,7 +95,7 @@ impl Player { let file = std::fs::File::open(&path)?; let decoder = rodio::Decoder::new(BufReader::new(file))?; - // This help having a constant volume + // This help having a constant volume let agc_source = decoder.automatic_gain_control(1.0, 4.0, 0.005, 5.0); self.sink.append(agc_source); @@ -140,6 +140,21 @@ impl Player { pub fn skip_next(&self) { self.sink.skip_one(); } + + pub fn current_pos_duration(&self) -> Duration { + self.sink.get_pos() + } + pub fn ratio_played(&self) -> f64 { + if let Some(song) = self.songs_list.lock().unwrap().get(self.current_playing) { + self.sink.get_pos().as_secs() as f64 + / song + .duration + .map(|d| d.to_duration().as_secs()) + .unwrap_or(0) as f64 + } else { + 0. + } + } } async fn async_audio_dl(id: String, client: Client) -> Result<()> { diff --git a/src/widgets/mod.rs b/src/widgets/mod.rs index 58cf05a..e6dbdb8 100644 --- a/src/widgets/mod.rs +++ b/src/widgets/mod.rs @@ -1,3 +1,5 @@ pub mod queue; +pub mod progress; pub use queue::*; +pub use progress::*; diff --git a/src/widgets/progress.rs b/src/widgets/progress.rs new file mode 100644 index 0000000..a641a2f --- /dev/null +++ b/src/widgets/progress.rs @@ -0,0 +1,83 @@ +use ratatui::{ + layout::{Constraint, Direction, Layout}, + style::{Style, Stylize}, + text::Text, + widgets::{Block, BorderType, Borders, Gauge, Widget}, +}; + +use crate::player::Player; + +pub struct Progress<'a> { + player: &'a Player, +} + +impl<'a> Progress<'a> { + pub fn new(player: &'a Player) -> Self { + Self { player } + } +} + +impl Widget for &Progress<'_> { + fn render(self, area: ratatui::prelude::Rect, buf: &mut ratatui::prelude::Buffer) { + let ratio = self.player.ratio_played(); + let pos = self.player.current_pos_duration().as_secs(); + if let Some(song) = self + .player + .songs_list + .lock() + .unwrap() + .get(self.player.current_playing) + { + Block::default() + .title(format!( + "{} - {}", + song.title.clone(), + song.artist.clone().unwrap_or_default() + )) + .borders(Borders::all()) + .border_type(BorderType::Rounded) + .render(area, buf); + let layout = Layout::default() + .direction(Direction::Vertical) + .constraints(vec![ + Constraint::Min(0), + Constraint::Length(1), + Constraint::Min(0), + ]) + .split(area); + let inner_layout = Layout::default() + .direction(Direction::Horizontal) + .constraints(vec![ + Constraint::Max(25), + Constraint::Min(0), + Constraint::Max(25), + ]) + .split(layout[1]); + + Text::from(format!( + "{:0>2}:{:0>2}:{:0>2} ", + pos / (60 * 60), + pos / 60, + pos % 60 + )) + .right_aligned() + .render(inner_layout[0], buf); + + let pos = song.duration.map(|d| d.to_duration().as_secs() ).unwrap_or_default(); + Text::from(format!( + " {:0>2}:{:0>2}:{:0>2}", + pos / (60 * 60), + pos / 60, + pos % 60 + )) + .left_aligned() + .render(inner_layout[2], buf); + + Gauge::default() + .label("") + .gauge_style(Style::new().cyan()) + .ratio(ratio) + .render(inner_layout[1], buf); + } + } +} diff --git a/src/widgets/queue.rs b/src/widgets/queue.rs index 1199e9a..7aba836 100644 --- a/src/widgets/queue.rs +++ b/src/widgets/queue.rs @@ -27,9 +27,8 @@ impl Queue { } } -impl StatefulWidget for &Queue { - type State = TableState; - fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { +impl Queue { + fn render_table(&self, area: Rect, buf: &mut Buffer, state: Option<&mut TableState>) { if !self.songs_list.lock().unwrap().is_empty() { let header_cells = ["#", "Title", "Artist", "Duration"].map(|h| h.bold()); let header = Row::new(header_cells).height(2); @@ -80,13 +79,31 @@ impl StatefulWidget for &Queue { .highlight_symbol(">>") .block(block); - StatefulWidget::render(table, area, buf, state); + if let Some(state) = state { + StatefulWidget::render(table, area, buf, state); + } else { + Widget::render(table, area, buf); + } + } else { Text::render(Text::from("Empty queue").centered(), area, buf); } } } +impl Widget for &Queue { + fn render(self, area: Rect, buf: &mut Buffer) { + self.render_table(area, buf, None); + } +} + +impl StatefulWidget for &Queue { + type State = TableState; + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + self.render_table(area, buf, Some(state)); + } +} + fn row_from_song(index: usize, song: &Child) -> Row { let duration = song.duration.unwrap().to_duration().as_secs(); Row::new([