mirror of
https://github.com/talwat/lowfi
synced 2025-01-13 20:01:27 +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