mirror of
				https://github.com/talwat/lowfi
				synced 2025-10-30 18:58:45 +00:00 
			
		
		
		
	feat: improve UI flexibility if resizing is implemented
This commit is contained in:
		
							parent
							
								
									9741d4b0d5
								
							
						
					
					
						commit
						b85d8c0be2
					
				| @ -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 | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
| @ -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)); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|  | ||||
| @ -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<Player>, volume_timer: Arc<AtomicUsize>) -> eyre::Result<()> { | ||||
| async fn interface(player: Arc<Player>) -> 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<Player>, volume_timer: Arc<AtomicUsize>) -> eyre: | ||||
| ///
 | ||||
| /// `alternate` controls whether to use [EnterAlternateScreen] in order to hide
 | ||||
| /// previous terminal history.
 | ||||
| pub async fn start( | ||||
|     queue: Arc<Player>, | ||||
|     sender: Sender<Messages>, | ||||
|     alternate: bool, | ||||
| ) -> eyre::Result<()> { | ||||
| pub async fn start(queue: Arc<Player>, sender: Sender<Messages>, 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)?; | ||||
|     } | ||||
| 
 | ||||
|  | ||||
| @ -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<Player>) -> String { | ||||
| pub fn progress_bar(player: &Arc<Player>, 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<Player>) -> 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<Player>) -> 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<Player>) -> String { | ||||
| pub fn action(player: &Arc<Player>, width: usize) -> String { | ||||
|     let (main, len) = player | ||||
|         .current | ||||
|         .load() | ||||
| @ -106,9 +95,19 @@ pub fn action(player: &Arc<Player>) -> 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))) | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user