From b85d8c0be238e529096d5ad135d4a8a0c733a38f Mon Sep 17 00:00:00 2001 From: Tal <83217276+talwat@users.noreply.github.com> Date: Thu, 3 Oct 2024 18:05:22 +0200 Subject: [PATCH] feat: improve UI flexibility if resizing is implemented --- src/main.rs | 2 +- src/play.rs | 5 +-- src/player.rs | 4 ++- src/player/ui.rs | 61 +++++++++++++++++-------------------- src/player/ui/components.rs | 43 +++++++++++++------------- 5 files changed, 56 insertions(+), 59 deletions(-) diff --git a/src/main.rs b/src/main.rs index e8b2000..3bb4fef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,6 +46,6 @@ async fn main() -> eyre::Result<()> { } => scrape::scrape(extension, include_full).await, } } else { - play::play(cli.alternate).await + play::play(cli).await } } diff --git a/src/play.rs b/src/play.rs index df1484e..b47842b 100644 --- a/src/play.rs +++ b/src/play.rs @@ -6,17 +6,18 @@ use tokio::{sync::mpsc, task}; use crate::player::Player; use crate::player::{ui, Messages}; +use crate::Args; /// Initializes the audio server, and then safely stops /// it when the frontend quits. -pub async fn play(alternate: bool) -> eyre::Result<()> { +pub async fn play(args: Args) -> eyre::Result<()> { let (tx, rx) = mpsc::channel(8); let player = Arc::new(Player::new().await?); let audio = task::spawn(Player::play(Arc::clone(&player), tx.clone(), rx)); tx.send(Messages::Init).await?; - ui::start(Arc::clone(&player), tx.clone(), alternate).await?; + ui::start(Arc::clone(&player), tx.clone(), args).await?; audio.abort(); player.sink.stop(); diff --git a/src/player.rs b/src/player.rs index dc0a005..6632f9c 100644 --- a/src/player.rs +++ b/src/player.rs @@ -218,7 +218,9 @@ impl Player { } } Messages::ChangeVolume(change) => { - player.sink.set_volume((player.sink.volume() + change).clamp(0.0, 1.0)); + player + .sink + .set_volume((player.sink.volume() + change).clamp(0.0, 1.0)); } } } diff --git a/src/player/ui.rs b/src/player/ui.rs index 23edeb7..6ac8dd1 100644 --- a/src/player/ui.rs +++ b/src/player/ui.rs @@ -9,6 +9,8 @@ use std::{ time::Duration, }; +use crate::Args; + use super::Player; use crossterm::{ cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show}, @@ -16,6 +18,7 @@ use crossterm::{ style::{Print, Stylize}, terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen}, }; +use lazy_static::lazy_static; use tokio::{ sync::mpsc::Sender, task::{self}, @@ -29,53 +32,51 @@ mod components; /// The total width of the UI. const WIDTH: usize = 27; -/// The width of the progress bar, not including the borders (`[` and `]`) or padding. -const PROGRESS_WIDTH: usize = WIDTH - 16; - -/// The width of the audio bar, again not including borders or padding. -const AUDIO_WIDTH: usize = WIDTH - 17; - /// Self explanitory. const FPS: usize = 12; /// How long the audio bar will be visible for when audio is adjusted. /// This is in frames. -const AUDIO_BAR_DURATION: usize = 9; +const AUDIO_BAR_DURATION: usize = 10; /// How long to wait in between frames. /// This is fairly arbitrary, but an ideal value should be enough to feel /// snappy but not require too many resources. const FRAME_DELTA: f32 = 1.0 / FPS as f32; +lazy_static! { + /// The volume timer, which controls how long the volume display should + /// show up and when it should disappear. + static ref VOLUME_TIMER: AtomicUsize = AtomicUsize::new(0); +} + /// The code for the interface itself. /// /// `volume_timer` is a bit strange, but it tracks how long the `volume` bar /// has been displayed for, so that it's only displayed for a certain amount of frames. -async fn interface(player: Arc, volume_timer: Arc) -> eyre::Result<()> { +async fn interface(player: Arc) -> eyre::Result<()> { loop { - let action = components::action(&player); + let action = components::action(&player, WIDTH); + + let timer = VOLUME_TIMER.load(Ordering::Relaxed); + let volume = player.sink.volume(); + let percentage = format!("{}%", (volume * 100.0).round().abs()); - let timer = volume_timer.load(Ordering::Relaxed); let middle = match timer { - 0 => components::progress_bar(&player), - _ => components::audio_bar(&player), + 0 => components::progress_bar(&player, WIDTH - 16), + _ => components::audio_bar(volume, &percentage, WIDTH - 17), }; if timer > 0 && timer <= AUDIO_BAR_DURATION { - volume_timer.fetch_add(1, Ordering::Relaxed); + VOLUME_TIMER.fetch_add(1, Ordering::Relaxed); } else if timer > AUDIO_BAR_DURATION { - volume_timer.store(0, Ordering::Relaxed); + VOLUME_TIMER.store(0, Ordering::Relaxed); } - let controls = [ - format!("{}kip", "[s]".bold()), - format!("{}ause", "[p]".bold()), - format!("{}uit", "[q]".bold()), - ]; + let controls = components::controls(WIDTH); // Formats the menu properly - let menu = [action, middle, controls.join(" ")] - .map(|x| format!("│ {} │\r\n", x.reset()).to_string()); + let menu = [action, middle, controls].map(|x| format!("│ {} │\r\n", x.reset()).to_string()); crossterm::execute!( stdout(), @@ -96,22 +97,16 @@ async fn interface(player: Arc, volume_timer: Arc) -> eyre: /// /// `alternate` controls whether to use [EnterAlternateScreen] in order to hide /// previous terminal history. -pub async fn start( - queue: Arc, - sender: Sender, - alternate: bool, -) -> eyre::Result<()> { +pub async fn start(queue: Arc, sender: Sender, args: Args) -> eyre::Result<()> { crossterm::execute!(stdout(), Hide)?; terminal::enable_raw_mode()?; - if alternate { + if args.alternate { crossterm::execute!(stdout(), EnterAlternateScreen, MoveTo(0, 0))?; } - let volume_timer = Arc::new(AtomicUsize::new(0)); - - task::spawn(interface(Arc::clone(&queue), volume_timer.clone())); + task::spawn(interface(Arc::clone(&queue))); loop { let event::Event::Key(event) = event::read()? else { @@ -145,16 +140,16 @@ pub async fn start( _ => continue, }; - // If it's modifying the volume, then we'll set the `volume_timer` to 1 + // If it's modifying the volume, then we'll set the `VOLUME_TIMER` to 1 // so that the ui thread will know that it should show the audio bar. if let Messages::ChangeVolume(_) = messages { - volume_timer.store(1, Ordering::Relaxed); + VOLUME_TIMER.store(1, Ordering::Relaxed); } sender.send(messages).await?; } - if alternate { + if args.alternate { crossterm::execute!(stdout(), LeaveAlternateScreen)?; } diff --git a/src/player/ui/components.rs b/src/player/ui/components.rs index cb9fb66..f7ce03c 100644 --- a/src/player/ui/components.rs +++ b/src/player/ui/components.rs @@ -2,15 +2,7 @@ use std::{sync::Arc, time::Duration}; use crossterm::style::Stylize; -use crate::{ - player::{ - ui::{AUDIO_WIDTH, PROGRESS_WIDTH}, - Player, - }, - tracks::TrackInfo, -}; - -use super::WIDTH; +use crate::{player::Player, tracks::TrackInfo}; /// Small helper function to format durations. pub fn format_duration(duration: &Duration) -> String { @@ -21,7 +13,7 @@ pub fn format_duration(duration: &Duration) -> String { } /// Creates the progress bar, as well as all the padding needed. -pub fn progress_bar(player: &Arc) -> String { +pub fn progress_bar(player: &Arc, width: usize) -> String { let mut duration = Duration::new(0, 0); let elapsed = player.sink.get_pos(); @@ -31,30 +23,27 @@ pub fn progress_bar(player: &Arc) -> String { duration = x; let elapsed = elapsed.as_secs() as f32 / duration.as_secs() as f32; - filled = (elapsed * PROGRESS_WIDTH as f32).round() as usize; + filled = (elapsed * width as f32).round() as usize; } }; format!( " [{}{}] {}/{} ", "/".repeat(filled), - " ".repeat(PROGRESS_WIDTH.saturating_sub(filled)), + " ".repeat(width.saturating_sub(filled)), format_duration(&elapsed), format_duration(&duration), ) } /// Creates the audio bar, as well as all the padding needed. -pub fn audio_bar(player: &Arc) -> String { - let volume = player.sink.volume(); - - let audio = (player.sink.volume() * AUDIO_WIDTH as f32).round() as usize; - let percentage = format!("{}%", (volume * 100.0).round().abs()); +pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String { + let audio = (volume * width as f32).round() as usize; format!( " volume: [{}{}] {}{} ", "/".repeat(audio), - " ".repeat(AUDIO_WIDTH.saturating_sub(audio)), + " ".repeat(width.saturating_sub(audio)), " ".repeat(4usize.saturating_sub(percentage.len())), percentage, ) @@ -91,7 +80,7 @@ impl ActionBar { /// Creates the top/action bar, which has the name of the track and it's status. /// This also creates all the needed padding. -pub fn action(player: &Arc) -> String { +pub fn action(player: &Arc, width: usize) -> String { let (main, len) = player .current .load() @@ -106,9 +95,19 @@ pub fn action(player: &Arc) -> String { }) .format(); - if len > WIDTH { - format!("{}...", &main[..=WIDTH]) + if len > width { + format!("{}...", &main[..=width]) } else { - format!("{}{}", main, " ".repeat(WIDTH - len)) + format!("{}{}", main, " ".repeat(width - len)) } } + +pub fn controls(width: usize) -> String { + let controls = [["[s]", "kip"], ["[p]", "ause"], ["[q]", "uit"]]; + + let len: usize = controls.concat().iter().map(|x| x.len()).sum(); + + let controls = controls.map(|x| format!("{}{}", x[0].bold(), x[1])); + + controls.join(&" ".repeat((width - len) / (controls.len() - 1))) +}