alot of edge cases removed, ui improvments

This commit is contained in:
krolxon 2024-04-26 14:32:58 +05:30
parent 59e8e8cbe6
commit 04e5d2ad28
13 changed files with 320 additions and 112 deletions

7
Cargo.lock generated
View File

@ -225,6 +225,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "indoc"
version = "2.0.5"
@ -386,6 +392,7 @@ version = "0.1.0"
dependencies = [
"clap",
"crossterm",
"humantime",
"mpd",
"ratatui",
"rust-fuzzy-search",

View File

@ -13,3 +13,4 @@ simple-dmenu = "0.1.0"
ratatui = "0.26.2"
crossterm = "0.27.0"
rust-fuzzy-search = "0.1.1"
humantime = "2.1.0"

View File

@ -1,7 +1,7 @@
A MPD client in Rust
### Keys
- `q` OR 'Ctr+C' to quit
- `q` OR `Ctr+C` to quit
- `p` to toggle pause
- `+` to increase volume
- `-` to decrease volume
@ -16,9 +16,18 @@ A MPD client in Rust
- `3` to go to playlists view
- `Enter` to add song/playlist to current playlist
- `a` to append the song to current playing queue
- `f` to go forward
- `b` to go backwords
- `f` to go forwards
- `b` to go backwards
- `>` to play next song from queue
- `<` to play previous song from queue
- `U` to update the MPD database
- `r` to toggle repeat
- `z` to toggle random
### TODO
- [x] fix performance issues
- [ ] improvements on queue control
- [ ] add to playlists, playlists view
- [ ] search for songs
- [ ] metadata based tree view
- [ ] Humantime format

View File

@ -14,7 +14,6 @@ pub struct App {
/// check if app is running
pub running: bool,
pub conn: Connection,
pub song_list: ContentList<String>,
pub queue_list: ContentList<String>,
pub pl_list: ContentList<String>,
pub selected_tab: SelectedTab,
@ -28,16 +27,6 @@ pub enum SelectedTab {
Playlists,
}
impl SelectedTab {
fn as_usize(&self) {
match self {
SelectedTab::Queue => 0,
SelectedTab::Playlists => 1,
SelectedTab::DirectoryBrowser => 2,
};
}
}
impl App {
pub fn builder(addrs: &str) -> AppResult<Self> {
let mut conn = Connection::new(addrs).unwrap();
@ -47,14 +36,10 @@ impl App {
Self::get_queue(&mut conn, &mut queue_list.list);
let mut song_list = ContentList::new();
song_list.list = conn.songs_filenames.clone();
let browser = FileBrowser::new();
Ok(Self {
running: true,
conn,
song_list,
queue_list,
pl_list,
selected_tab: SelectedTab::DirectoryBrowser,
@ -63,11 +48,9 @@ impl App {
}
pub fn tick(&mut self) {
self.conn.update_state();
self.conn.update_progress();
self.conn.update_volume();
self.conn.update_status();
self.update_queue();
self.browser.update_directory(&mut self.conn).unwrap();
// self.browser.update_directory(&mut self.conn).unwrap();
}
pub fn quit(&mut self) {

View File

@ -1,3 +1,5 @@
use mpd::Query;
use crate::{app::AppResult, connection::Connection};
#[derive(Debug)]
@ -59,32 +61,45 @@ impl FileBrowser {
if t == "directory" {
if path != "." {
self.prev_path = self.path.clone();
self.path = path.to_string();
self.path = self.prev_path.clone() + "/" + path;
self.update_directory(conn)?;
self.prev_selected = self.selected;
self.selected = 0;
// println!("self.path: {}", self.path);
// println!("self.prev_pat: {}", self.prev_path);
}
} else {
let list = conn
.songs_filenames
.iter()
.map(|f| f.as_str())
.collect::<Vec<&str>>();
let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list)
.get(0)
.unwrap()
.clone();
// let list = conn
// .songs_filenames
// .iter()
// .map(|f| f.as_str())
// .collect::<Vec<&str>>();
// let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&path, &list)
// .get(0)
// .unwrap()
// .clone();
let song = conn.get_song_with_only_filename(filename);
conn.push(&song)?;
for filename in conn.songs_filenames.clone().iter() {
if filename.contains(path) {
let song = conn.get_song_with_only_filename(filename);
conn.push(&song)?;
}
}
}
Ok(())
}
pub fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> {
self.path = self.prev_path.clone();
if self.prev_path != "." {
let r = self.path.rfind("/").unwrap();
self.path = self.path.as_str()[..r].to_string();
self.update_directory(conn)?;
} else {
self.path = self.prev_path.clone();
self.update_directory(conn)?;
}
self.selected = self.prev_selected;
self.update_directory(conn)?;
Ok(())
}
}

View File

@ -6,10 +6,10 @@ use clap::{Parser, Subcommand};
pub struct Args {
/// No TUI
#[arg(short= 'n', default_value="false")]
pub no_tui: bool,
pub tui: bool,
#[command(subcommand)]
pub command: Command,
pub command: Option<Command>,
}
#[derive(Debug, Subcommand)]

View File

@ -15,6 +15,8 @@ pub struct Connection {
pub elapsed: Duration,
pub total_duration: Duration,
pub volume: u8,
pub repeat: bool,
pub random: bool,
}
impl Connection {
@ -27,8 +29,11 @@ impl Connection {
.into_iter()
.map(|x| x.file)
.collect();
let (elapsed, total) = conn.status().unwrap().time.unwrap_or_default();
let volume: u8 = conn.status().unwrap_or_default().volume as u8;
let status = conn.status().unwrap();
let (elapsed, total) = status.time.unwrap_or_default();
let volume: u8 = status.volume as u8;
let repeat = status.repeat;
let random = status.random;
Ok(Self {
conn,
@ -37,6 +42,8 @@ impl Connection {
elapsed,
total_duration: total,
volume,
repeat,
random,
})
}
@ -64,25 +71,33 @@ impl Connection {
Ok(())
}
pub fn update_state(&mut self) -> String {
match self.conn.status().unwrap().state {
/// Update status
pub fn update_status(&mut self) {
let status = self.conn.status().unwrap();
// Playback State
match status.state {
State::Stop => self.state = "Stopped".to_string(),
State::Play => self.state = "Playing".to_string(),
State::Pause => self.state = "Paused".to_string(),
}
self.state.clone()
}
pub fn update_progress(&mut self) {
let (elapsed, total) = self.conn.status().unwrap().time.unwrap_or_default();
// Progress
let (elapsed, total) = status.time.unwrap_or_default();
self.elapsed = elapsed;
self.total_duration = total;
// Volume
self.volume = status.volume as u8;
// Repeat mode
self.repeat = status.repeat;
// Random mode
self.random = status.random;
}
pub fn update_volume(&mut self) {
self.volume = self.conn.status().unwrap_or_default().volume as u8;
}
/// Get progress ratio of current playing song
pub fn get_progress_ratio(&self) -> f64 {
let total = self.total_duration.as_secs_f64();
if total == 0.0 {
@ -143,11 +158,6 @@ impl Connection {
}
}
/// get current playing song
pub fn get_current_song(&mut self) -> Option<String> {
self.conn.currentsong().unwrap().unwrap_or_default().title
}
/// Print status to stdout
pub fn status(&mut self) {
let current_song = self.conn.currentsong();
@ -170,7 +180,7 @@ impl Connection {
let song = self.conn.currentsong()?.unwrap_or_default();
if let Some(s) = song.title {
if let Some(a) = song.artist {
return Ok(Some(format!("\"{}\" By {}", a, s)));
return Ok(Some(format!("\"{}\" By {}", s, a)));
} else {
return Ok(Some(s));
}
@ -190,7 +200,26 @@ impl Connection {
self.conn.toggle_pause().unwrap();
}
/// Toggle Repeat mode
pub fn toggle_repeat(&mut self) {
if self.conn.status().unwrap().repeat {
self.conn.repeat(false).unwrap();
} else {
self.conn.repeat(true).unwrap();
}
}
/// Toggle random mode
pub fn toggle_random(&mut self) {
if self.conn.status().unwrap().random {
self.conn.random(false).unwrap();
} else {
self.conn.random(true).unwrap();
}
}
// Volume controls
/// Increase Volume
pub fn inc_volume(&mut self, v: i8) {
let cur = self.conn.status().unwrap().volume;
if cur + v <= 100 {
@ -198,6 +227,7 @@ impl Connection {
}
}
/// Decrease volume
pub fn dec_volume(&mut self, v: i8) {
let cur = self.conn.status().unwrap().volume;
if cur - v >= 0 {

View File

@ -13,6 +13,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
app.quit();
} else {
app.conn.conn.clear()?;
app.conn.update_status();
}
}
@ -47,6 +48,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
.push_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
}
}
app.conn.update_status();
}
KeyCode::Char('h') => match app.selected_tab {
@ -68,6 +70,18 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
app.conn.pause();
}
// Toggle rpeat
KeyCode::Char('r') => {
app.conn.toggle_repeat();
app.conn.update_status();
}
// Toggle random
KeyCode::Char('z') => {
app.conn.toggle_random();
app.conn.update_status();
}
// Dmenu prompt
KeyCode::Char('D') => {
app.conn.play_dmenu()?;
@ -75,9 +89,23 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
// add to queue
KeyCode::Char('a') => {
let song = app.conn.get_song_with_only_filename(
app.conn.songs_filenames.get(app.song_list.index).unwrap(),
);
// let song = app.conn.get_song_with_only_filename(
// app.conn.songs_filenames.get(app.song_list.index).unwrap(),
// );
let list = app
.conn
.songs_filenames
.iter()
.map(|f| f.as_str())
.collect::<Vec<&str>>();
let (filename, _) = rust_fuzzy_search::fuzzy_search_sorted(&app.browser.path, &list)
.get(0)
.unwrap()
.clone();
let song = app.conn.get_song_with_only_filename(filename);
app.conn.conn.push(&song)?;
}
@ -127,16 +155,22 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
// Volume controls
KeyCode::Char('=') => {
app.conn.inc_volume(2);
app.conn.update_volume();
app.conn.update_status();
}
KeyCode::Char('-') => {
app.conn.dec_volume(2);
app.conn.update_volume();
app.conn.update_status();
}
// Delete highlighted song from the queue
KeyCode::Char('d') => {
if app.queue_list.index >= app.queue_list.list.len() - 1 {
if app.queue_list.index != 0 {
app.queue_list.index -= 1;
}
}
app.conn.conn.delete(app.queue_list.index as u32)?;
app.update_queue();
}

View File

@ -13,6 +13,9 @@ pub mod tui;
/// Content list
pub mod list;
/// Song
pub mod song;
/// File Browser
pub mod browser;

View File

@ -18,18 +18,24 @@ impl<T> ContentList<T> {
// self.index += 1;
// }
if self.index == self.list.len() - 1 {
self.index = 0;
} else {
self.index += 1;
let len = self.list.len();
if len != 0 {
if self.index == self.list.len() - 1 {
self.index = 0;
} else {
self.index += 1;
}
}
}
/// Go to previous item in list
pub fn prev(&mut self) {
if self.index == 0 {
self.index = self.list.len() - 1;
} else {
let len = self.list.len();
if len != 0 {
self.index = len - 1;
}
} else {
self.index -= 1;
}
}

View File

@ -9,6 +9,7 @@ use rmptui::connection::Connection;
use rmptui::event::Event;
use rmptui::event::EventHandler;
use rmptui::handler;
use rmptui::song::RSong;
use rmptui::tui;
use std::io;
@ -23,41 +24,52 @@ pub type Result<T> = core::result::Result<T, Error>;
pub type Error = Box<dyn std::error::Error>;
fn main() -> AppResult<()> {
// let args = Args::parse();
// if args.no_tui {
// handle_tui()?;
// } else {
// match args.command {
// Command::Volume { vol } => {
// conn.set_volume(vol);
// }
// Command::Dmenu => conn.play_dmenu().unwrap(),
// Command::Fzf => conn.play_fzf().unwrap(),
// Command::Status => conn.status(),
// Command::Pause => conn.pause(),
// Command::Toggle => conn.toggle_pause(),
// };
// }
let backend = CrosstermBackend::new(io::stderr());
let terminal = Terminal::new(backend)?;
let args = Args::parse();
let mut app = App::builder("127.0.0.1:6600")?;
let events = EventHandler::new(250);
if !args.tui {
handle_tui(&mut app)?;
} else {
match args.command {
Some(Command::Dmenu) => app.conn.play_dmenu()?,
Some(Command::Fzf) => app.conn.play_fzf().unwrap(),
Some(Command::Status) => app.conn.status(),
Some(Command::Pause) => app.conn.pause(),
Some(Command::Toggle) => app.conn.toggle_pause(),
_ => {
let mut vec: Vec<RSong> = Vec::new();
for filename in app.conn.songs_filenames {
let song = RSong::new(&mut app.conn.conn, filename);
vec.push(song);
}
println!("{:#?}", vec);
}
}
}
Ok(())
}
pub fn handle_tui(app: &mut App) -> AppResult<()> {
let backend = CrosstermBackend::new(io::stderr());
let terminal = Terminal::new(backend)?;
let events = EventHandler::new(1000);
let mut tui = tui::Tui::new(terminal, events);
tui.init()?;
// update the directory
app.browser.update_directory(&mut app.conn).unwrap();
while app.running {
tui.draw(&mut app)?;
tui.draw(app)?;
match tui.events.next()? {
Event::Tick => app.tick(),
Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?,
Event::Key(key_event) => handler::handle_key_events(key_event, app)?,
Event::Mouse(_) => {}
Event::Resize(_, _) => {}
}
}
Ok(())
}

67
src/song.rs Executable file
View File

@ -0,0 +1,67 @@
use mpd::{Client, Song};
#[derive(Debug)]
#[derive(Clone )]
pub struct RSong {
pub file: String,
pub artist: Option<String>,
pub title: Option<String>,
pub duration: Option<u32>,
pub last_mod: Option<String>,
pub name: Option<String>,
pub place: Option<String>,
pub range: Option<String>,
pub tags: Vec<(String, String)>,
}
impl RSong {
pub fn new(c: &mut Client, filename: String) -> Self {
let mut s = RSong {
file: filename.clone(),
artist: None,
title: None,
duration: None,
last_mod: None,
name: None,
place: None,
range: None,
tags: vec![],
};
// Dummy song
let song = Song {
file: filename.clone(),
artist: None,
title: None,
duration: None,
last_mod: None,
name: None,
place: None,
range: None,
tags: vec![("".to_string(), "".to_string())],
};
for (k, v) in (c.readcomments(song).unwrap()).flatten() {
if k.to_lowercase().contains("artist") {
s.artist = Some(v);
} else if k.to_lowercase().contains("title") {
s.title = Some(v);
} else if k.to_lowercase().contains("duration") {
s.duration = Some(v.parse::<u32>().unwrap());
} else if k.to_lowercase().contains("lastmod") {
s.last_mod = Some(v);
} else if k.to_lowercase().contains("name") {
s.name = Some(v);
} else if k.to_lowercase().contains("place") {
s.place = Some(v);
} else if k.to_lowercase().contains("range") {
s.range = Some(v);
} else {
s.tags.push((k, v));
}
}
s
}
}

View File

@ -14,7 +14,7 @@ pub fn render(app: &mut App, frame: &mut Frame) {
// Layout
let layout = Layout::default()
.direction(Direction::Vertical)
.constraints(vec![Constraint::Percentage(93), Constraint::Percentage(7)])
.constraints(vec![Constraint::Percentage(93), Constraint::Min(3)])
.split(frame.size());
match app.selected_tab {
@ -52,7 +52,12 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
)
.borders(Borders::ALL),
)
.highlight_style(Style::new().add_modifier(Modifier::REVERSED))
.highlight_style(
Style::new()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::REVERSED),
)
.highlight_symbol(">>")
.repeat_highlight_symbol(true)
.scroll_padding(20);
@ -72,7 +77,12 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
);
let list = List::new(app.queue_list.list.clone())
.block(title.borders(Borders::ALL))
.highlight_style(Style::new().add_modifier(Modifier::REVERSED))
.highlight_style(
Style::new()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::REVERSED),
)
.highlight_symbol(">>")
.repeat_highlight_symbol(true);
@ -93,7 +103,12 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) {
let list = List::new(app.pl_list.list.clone())
.block(title.borders(Borders::ALL))
.highlight_style(Style::new().add_modifier(Modifier::REVERSED))
.highlight_style(
Style::new()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::REVERSED),
)
.highlight_symbol(">>")
.repeat_highlight_symbol(true);
@ -103,39 +118,65 @@ fn draw_playlists(frame: &mut Frame, app: &mut App, size: Rect) {
/// Draws Progress Bar
fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
// Get the current playing song
let song = app
.conn
.now_playing()
.unwrap()
.unwrap_or_else(|| "No Title Found".to_string());
let state = &app.conn.state;
// let (elapsed, total) = app.conn.conn.status().unwrap().time.unwrap_or_default();
// Get the current playing state
let mut state: String = String::new();
if !app.queue_list.list.is_empty() {
state = app.conn.state.clone();
state.push(':');
}
// Get the current modes
let mut modes_bottom: String = String::new();
// we do this to check if at least one mode is enabled so we can push "[]"
if app.conn.repeat | app.conn.random {
modes_bottom.push('r');
}
if !modes_bottom.is_empty() {
modes_bottom.clear();
modes_bottom.push('[');
if app.conn.repeat {
modes_bottom.push('r');
}
if app.conn.random {
modes_bottom.push('z');
}
modes_bottom.push(']');
};
// get the duration
let duration = if app.conn.total_duration.as_secs() != 0 {
format!(
"[{}/{}]",
humantime::format_duration(app.conn.elapsed),
humantime::format_duration(app.conn.total_duration)
)
} else {
"".to_string()
};
// Define the title
let title = Block::default()
.title(Title::from(format!("{}: ", state).red().bold()))
.title(Title::from(format!("{}", state).red().bold()))
.title(Title::from(song.green().bold()))
.title(
Title::from(
format!(
"{}/{}",
app.conn.elapsed.as_secs(),
app.conn.total_duration.as_secs()
)
.cyan()
.bold(),
)
.alignment(Alignment::Right),
)
.title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
.title(Title::from(format!("{}", modes_bottom)).position(block::Position::Bottom))
.borders(Borders::ALL);
// .title(Title::from(app.conn.conn.status().unwrap_or_default().volume.to_string().yellow())).title_alignment(Alignment::Right);
let progress_bar = LineGauge::default()
.block(title.borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::LightBlue)
.bg(Color::Gray)
.fg(Color::Blue)
.bg(Color::Black)
.add_modifier(Modifier::BOLD),
)
.line_set(symbols::line::THICK)