mirror of
https://github.com/talwat/lowfi
synced 2025-01-14 12:21:28 +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,
|
} => scrape::scrape(extension, include_full).await,
|
||||||
}
|
}
|
||||||
} else {
|
} 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::Player;
|
||||||
use crate::player::{ui, Messages};
|
use crate::player::{ui, Messages};
|
||||||
|
use crate::Args;
|
||||||
|
|
||||||
/// Initializes the audio server, and then safely stops
|
/// Initializes the audio server, and then safely stops
|
||||||
/// it when the frontend quits.
|
/// 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 (tx, rx) = mpsc::channel(8);
|
||||||
|
|
||||||
let player = Arc::new(Player::new().await?);
|
let player = Arc::new(Player::new().await?);
|
||||||
let audio = task::spawn(Player::play(Arc::clone(&player), tx.clone(), rx));
|
let audio = task::spawn(Player::play(Arc::clone(&player), tx.clone(), rx));
|
||||||
tx.send(Messages::Init).await?;
|
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();
|
audio.abort();
|
||||||
player.sink.stop();
|
player.sink.stop();
|
||||||
|
@ -218,7 +218,9 @@ impl Player {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Messages::ChangeVolume(change) => {
|
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,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::Args;
|
||||||
|
|
||||||
use super::Player;
|
use super::Player;
|
||||||
use crossterm::{
|
use crossterm::{
|
||||||
cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show},
|
cursor::{Hide, MoveTo, MoveToColumn, MoveUp, Show},
|
||||||
@ -16,6 +18,7 @@ use crossterm::{
|
|||||||
style::{Print, Stylize},
|
style::{Print, Stylize},
|
||||||
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
terminal::{self, Clear, ClearType, EnterAlternateScreen, LeaveAlternateScreen},
|
||||||
};
|
};
|
||||||
|
use lazy_static::lazy_static;
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc::Sender,
|
sync::mpsc::Sender,
|
||||||
task::{self},
|
task::{self},
|
||||||
@ -29,53 +32,51 @@ mod components;
|
|||||||
/// The total width of the UI.
|
/// The total width of the UI.
|
||||||
const WIDTH: usize = 27;
|
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.
|
/// Self explanitory.
|
||||||
const FPS: usize = 12;
|
const FPS: usize = 12;
|
||||||
|
|
||||||
/// How long the audio bar will be visible for when audio is adjusted.
|
/// How long the audio bar will be visible for when audio is adjusted.
|
||||||
/// This is in frames.
|
/// This is in frames.
|
||||||
const AUDIO_BAR_DURATION: usize = 9;
|
const AUDIO_BAR_DURATION: usize = 10;
|
||||||
|
|
||||||
/// How long to wait in between frames.
|
/// How long to wait in between frames.
|
||||||
/// This is fairly arbitrary, but an ideal value should be enough to feel
|
/// This is fairly arbitrary, but an ideal value should be enough to feel
|
||||||
/// snappy but not require too many resources.
|
/// snappy but not require too many resources.
|
||||||
const FRAME_DELTA: f32 = 1.0 / FPS as f32;
|
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.
|
/// The code for the interface itself.
|
||||||
///
|
///
|
||||||
/// `volume_timer` is a bit strange, but it tracks how long the `volume` bar
|
/// `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.
|
/// 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 {
|
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 {
|
let middle = match timer {
|
||||||
0 => components::progress_bar(&player),
|
0 => components::progress_bar(&player, WIDTH - 16),
|
||||||
_ => components::audio_bar(&player),
|
_ => components::audio_bar(volume, &percentage, WIDTH - 17),
|
||||||
};
|
};
|
||||||
|
|
||||||
if timer > 0 && timer <= AUDIO_BAR_DURATION {
|
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 {
|
} else if timer > AUDIO_BAR_DURATION {
|
||||||
volume_timer.store(0, Ordering::Relaxed);
|
VOLUME_TIMER.store(0, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
let controls = [
|
let controls = components::controls(WIDTH);
|
||||||
format!("{}kip", "[s]".bold()),
|
|
||||||
format!("{}ause", "[p]".bold()),
|
|
||||||
format!("{}uit", "[q]".bold()),
|
|
||||||
];
|
|
||||||
|
|
||||||
// Formats the menu properly
|
// Formats the menu properly
|
||||||
let menu = [action, middle, controls.join(" ")]
|
let menu = [action, middle, controls].map(|x| format!("│ {} │\r\n", x.reset()).to_string());
|
||||||
.map(|x| format!("│ {} │\r\n", x.reset()).to_string());
|
|
||||||
|
|
||||||
crossterm::execute!(
|
crossterm::execute!(
|
||||||
stdout(),
|
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
|
/// `alternate` controls whether to use [EnterAlternateScreen] in order to hide
|
||||||
/// previous terminal history.
|
/// previous terminal history.
|
||||||
pub async fn start(
|
pub async fn start(queue: Arc<Player>, sender: Sender<Messages>, args: Args) -> eyre::Result<()> {
|
||||||
queue: Arc<Player>,
|
|
||||||
sender: Sender<Messages>,
|
|
||||||
alternate: bool,
|
|
||||||
) -> eyre::Result<()> {
|
|
||||||
crossterm::execute!(stdout(), Hide)?;
|
crossterm::execute!(stdout(), Hide)?;
|
||||||
|
|
||||||
terminal::enable_raw_mode()?;
|
terminal::enable_raw_mode()?;
|
||||||
|
|
||||||
if alternate {
|
if args.alternate {
|
||||||
crossterm::execute!(stdout(), EnterAlternateScreen, MoveTo(0, 0))?;
|
crossterm::execute!(stdout(), EnterAlternateScreen, MoveTo(0, 0))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let volume_timer = Arc::new(AtomicUsize::new(0));
|
task::spawn(interface(Arc::clone(&queue)));
|
||||||
|
|
||||||
task::spawn(interface(Arc::clone(&queue), volume_timer.clone()));
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let event::Event::Key(event) = event::read()? else {
|
let event::Event::Key(event) = event::read()? else {
|
||||||
@ -145,16 +140,16 @@ pub async fn start(
|
|||||||
_ => continue,
|
_ => 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.
|
// so that the ui thread will know that it should show the audio bar.
|
||||||
if let Messages::ChangeVolume(_) = messages {
|
if let Messages::ChangeVolume(_) = messages {
|
||||||
volume_timer.store(1, Ordering::Relaxed);
|
VOLUME_TIMER.store(1, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
||||||
sender.send(messages).await?;
|
sender.send(messages).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if alternate {
|
if args.alternate {
|
||||||
crossterm::execute!(stdout(), LeaveAlternateScreen)?;
|
crossterm::execute!(stdout(), LeaveAlternateScreen)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,15 +2,7 @@ use std::{sync::Arc, time::Duration};
|
|||||||
|
|
||||||
use crossterm::style::Stylize;
|
use crossterm::style::Stylize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{player::Player, tracks::TrackInfo};
|
||||||
player::{
|
|
||||||
ui::{AUDIO_WIDTH, PROGRESS_WIDTH},
|
|
||||||
Player,
|
|
||||||
},
|
|
||||||
tracks::TrackInfo,
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::WIDTH;
|
|
||||||
|
|
||||||
/// Small helper function to format durations.
|
/// Small helper function to format durations.
|
||||||
pub fn format_duration(duration: &Duration) -> String {
|
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.
|
/// 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 mut duration = Duration::new(0, 0);
|
||||||
let elapsed = player.sink.get_pos();
|
let elapsed = player.sink.get_pos();
|
||||||
|
|
||||||
@ -31,30 +23,27 @@ pub fn progress_bar(player: &Arc<Player>) -> String {
|
|||||||
duration = x;
|
duration = x;
|
||||||
|
|
||||||
let elapsed = elapsed.as_secs() as f32 / duration.as_secs() as f32;
|
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!(
|
format!(
|
||||||
" [{}{}] {}/{} ",
|
" [{}{}] {}/{} ",
|
||||||
"/".repeat(filled),
|
"/".repeat(filled),
|
||||||
" ".repeat(PROGRESS_WIDTH.saturating_sub(filled)),
|
" ".repeat(width.saturating_sub(filled)),
|
||||||
format_duration(&elapsed),
|
format_duration(&elapsed),
|
||||||
format_duration(&duration),
|
format_duration(&duration),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the audio bar, as well as all the padding needed.
|
/// Creates the audio bar, as well as all the padding needed.
|
||||||
pub fn audio_bar(player: &Arc<Player>) -> String {
|
pub fn audio_bar(volume: f32, percentage: &str, width: usize) -> String {
|
||||||
let volume = player.sink.volume();
|
let audio = (volume * width as f32).round() as usize;
|
||||||
|
|
||||||
let audio = (player.sink.volume() * AUDIO_WIDTH as f32).round() as usize;
|
|
||||||
let percentage = format!("{}%", (volume * 100.0).round().abs());
|
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
" volume: [{}{}] {}{} ",
|
" volume: [{}{}] {}{} ",
|
||||||
"/".repeat(audio),
|
"/".repeat(audio),
|
||||||
" ".repeat(AUDIO_WIDTH.saturating_sub(audio)),
|
" ".repeat(width.saturating_sub(audio)),
|
||||||
" ".repeat(4usize.saturating_sub(percentage.len())),
|
" ".repeat(4usize.saturating_sub(percentage.len())),
|
||||||
percentage,
|
percentage,
|
||||||
)
|
)
|
||||||
@ -91,7 +80,7 @@ impl ActionBar {
|
|||||||
|
|
||||||
/// Creates the top/action bar, which has the name of the track and it's status.
|
/// Creates the top/action bar, which has the name of the track and it's status.
|
||||||
/// This also creates all the needed padding.
|
/// 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
|
let (main, len) = player
|
||||||
.current
|
.current
|
||||||
.load()
|
.load()
|
||||||
@ -106,9 +95,19 @@ pub fn action(player: &Arc<Player>) -> String {
|
|||||||
})
|
})
|
||||||
.format();
|
.format();
|
||||||
|
|
||||||
if len > WIDTH {
|
if len > width {
|
||||||
format!("{}...", &main[..=WIDTH])
|
format!("{}...", &main[..=width])
|
||||||
} else {
|
} 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