Compare commits
60 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
5e61e0fad5 | |
|
|
9941e1e42b | |
|
|
df10c2cf63 | |
|
|
f2b9051489 | |
|
|
5795d00831 | |
|
|
e1dc925ecc | |
|
|
3862d5d324 | |
|
|
65f76181d3 | |
|
|
8a8176e97b | |
|
|
a63845bae8 | |
|
|
a6fa8add79 | |
|
|
3e3ae64c72 | |
|
|
8f41c6e1d0 | |
|
|
faaddc2bd7 | |
|
|
53522c936d | |
|
|
a2f44eb6e6 | |
|
|
15be9357da | |
|
|
7ae0a2cc19 | |
|
|
dc3f561de3 | |
|
|
311cbc2631 | |
|
|
def91deabe | |
|
|
7b14b68164 | |
|
|
6ae0aca868 | |
|
|
b31e16554c | |
|
|
48fd5a7508 | |
|
|
e3ba0169ea | |
|
|
230c75790c | |
|
|
380a4193c9 | |
|
|
c3dc9931d5 | |
|
|
6a9e3d9801 | |
|
|
6cc0b4eb9f | |
|
|
2735afa943 | |
|
|
1da0b85fa9 | |
|
|
c51d9cc12e | |
|
|
5d4e428f97 | |
|
|
0b184ec714 | |
|
|
d48e57a3c7 | |
|
|
bcec798632 | |
|
|
35db5fb07d | |
|
|
1d70e28c4e | |
|
|
ce06f50e56 | |
|
|
f6a375fc46 | |
|
|
169ea38138 | |
|
|
a08c3a6e78 | |
|
|
074021c5aa | |
|
|
420389664b | |
|
|
bba509fabf | |
|
|
bc7ab75f0c | |
|
|
2025f348f7 | |
|
|
be4505e5e5 | |
|
|
ddf7423229 | |
|
|
4015a7e2a4 | |
|
|
aca208e0d8 | |
|
|
9bdc76b6a9 | |
|
|
f2783c00d2 | |
|
|
bb5bcea3ff | |
|
|
1e02da68cc | |
|
|
51a3094d87 | |
|
|
98cfe4eb26 | |
|
|
8c4d19f849 |
|
|
@ -0,0 +1,4 @@
|
|||
## rmptui-v0.1.6
|
||||
- Add songs to new playlist feature
|
||||
- Code refactoring
|
||||
- fix #9
|
||||
|
|
@ -282,7 +282,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "rmptui"
|
||||
version = "0.1.0"
|
||||
version = "0.1.6"
|
||||
dependencies = [
|
||||
"crossterm",
|
||||
"mpd",
|
||||
|
|
|
|||
13
Cargo.toml
13
Cargo.toml
|
|
@ -1,13 +1,22 @@
|
|||
[package]
|
||||
name = "rmptui"
|
||||
version = "0.1.0"
|
||||
authors = ["krolyxon"]
|
||||
description = """
|
||||
a fast and minimal tui mpd client
|
||||
"""
|
||||
readme = "README.md"
|
||||
version = "0.1.6"
|
||||
edition = "2021"
|
||||
repository = "https://github.com/krolyxon/rmptui"
|
||||
keywords = ["rmptui", "mpd", "music", "cli", "tui", "client"]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
mpd = "0.1.0"
|
||||
simple-dmenu = "0.1.0"
|
||||
ratatui = "0.26.2"
|
||||
ratatui = { version = "0.26.2", default-features = false, features = [
|
||||
'crossterm',
|
||||
] }
|
||||
crossterm = "0.27.0"
|
||||
rust-fuzzy-search = "0.1.1"
|
||||
|
|
|
|||
|
|
@ -1,38 +1,50 @@
|
|||
## rmptui (Rust Music Player TUI(💀))
|
||||
A MPD client in Rust
|
||||
## rmptui - A MPD client in Rust
|
||||

|
||||

|
||||
[](https://github.com/krolyxon/rmptui/releases)
|
||||
|
||||
rmptui is a minimal tui mpd client made with rust.
|
||||
|
||||
## rmptui in action
|
||||

|
||||
|
||||
### Keys
|
||||
- `q` OR `Ctr+C` to quit
|
||||
- `p` to toggle pause
|
||||
- `+` to increase volume
|
||||
- `-` to decrease volume
|
||||
- `D` to get dmenu prompt
|
||||
- `j` OR `Down` to scroll down
|
||||
- `k` OR `Up` to scroll up
|
||||
- `J` to swap highlighted song with next one
|
||||
- `K` to swap highlighted song with previous one
|
||||
- `l` OR `Right` add song to playlist or go inside the directory
|
||||
- `h` OR `Left` to go back to previous directory
|
||||
- `Tab` to cycle through tabs
|
||||
- `1` to go to queue
|
||||
- `2` to go to directory browser
|
||||
- `3` to go to playlists view
|
||||
- `Enter` OR `l` OR `Right` to add song/playlist to current playlist
|
||||
- `a` to append the song to current playing queue
|
||||
- `Space`/`BackSpace` to delete the highlighted song from queue
|
||||
- `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
|
||||
- `/` to search
|
||||
- `g` to go to top of list
|
||||
- `G` to go to bottom of list
|
||||
| Key | Action |
|
||||
| --- | --- |
|
||||
| `q`/`Ctr+C` | Quit |
|
||||
| `p` | Toggle pause |
|
||||
| `+`/`=` | Increase volume |
|
||||
| `-` | Decrease volume |
|
||||
| `m` | Toggle Mute |
|
||||
| `D` | Get dmenu prompt |
|
||||
| `j`/`Down` | Scroll down |
|
||||
| `k`/`Up` | Scroll up |
|
||||
| `J` | Swap highlighted song with next one |
|
||||
| `K` | Swap highlighted song with previous one |
|
||||
| `l`/`Right` | Add song to playlist or go inside the directory |
|
||||
| `h`/`Left` | Go back to previous directory |
|
||||
| `Tab` | Cycle through tabs |
|
||||
| `1` | Go to queue |
|
||||
| `2` | Go to directory browser |
|
||||
| `3` | Go to playlists view |
|
||||
| `Enter`/`l`/`Right` | Add song/playlist to current playlist |
|
||||
| `a` | Append the song to current playing queue |
|
||||
| `Space` | Delete the highlighted song from queue |
|
||||
| `f` | Go forwards |
|
||||
| `b` | Go backwards |
|
||||
| `>` | Play next song from queue |
|
||||
| `<` | Play previous song from queue |
|
||||
| `U` | Update the MPD database |
|
||||
| `r` | Toggle repeat |
|
||||
| `z` | Toggle random |
|
||||
| `/` | Search |
|
||||
| `R` | Rename Playlist |
|
||||
| `g` | Go to top of list |
|
||||
| `G` | Go to bottom of list |
|
||||
|
||||
### Prerequisites
|
||||
- [MPD](https://wiki.archlinux.org/title/Music_Player_Daemon) installed and configured.
|
||||
- [dmenu](https://tools.suckless.org/dmenu/) (optional)
|
||||
|
||||
### TODO
|
||||
- [x] fix performance issues
|
||||
|
|
@ -43,4 +55,5 @@ A MPD client in Rust
|
|||
- [x] metadata based tree view
|
||||
- [x] view playlist
|
||||
- [x] change playlist name
|
||||
- [x] add to new playlist
|
||||
- [ ] add lyrics fetcher
|
||||
|
|
|
|||
BIN
assets/ss.png
BIN
assets/ss.png
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 1.9 MiB |
211
src/app.rs
211
src/app.rs
|
|
@ -1,10 +1,12 @@
|
|||
use std::time::Duration;
|
||||
use std::{path::Path, time::Duration};
|
||||
|
||||
use crate::browser::FileBrowser;
|
||||
use crate::connection::Connection;
|
||||
use crate::list::ContentList;
|
||||
use crate::ui::InputMode;
|
||||
use crate::utils::FileExtension;
|
||||
use mpd::{Client, Song};
|
||||
use ratatui::widgets::{ListState, TableState};
|
||||
|
||||
// Application result type
|
||||
pub type AppResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||
|
|
@ -23,13 +25,26 @@ pub struct App {
|
|||
pub inputmode: InputMode, // Defines input mode, Normal or Search
|
||||
pub search_input: String, // Stores the userinput to be searched
|
||||
pub search_cursor_pos: usize, // Stores the cursor position for searching
|
||||
|
||||
pub pl_newname_input: String, // Stores the new name of the playlist
|
||||
pub pl_cursor_pos: usize, // Stores the cursor position for renaming playlist
|
||||
|
||||
pub pl_new_pl_input: String, // Stores the name of new playlist to be created
|
||||
pub pl_new_pl_cursor_pos: usize, // Stores the cursor position of new playlist to be created
|
||||
pub pl_new_pl_songs_buffer: Vec<Song>, // Buffer for songs that need to be added to the newly created playlist
|
||||
|
||||
// playlist variables
|
||||
// used to show playlist popup
|
||||
pub playlist_popup: bool,
|
||||
pub append_list: ContentList<String>,
|
||||
|
||||
// Determines if the database should be updated or not
|
||||
pub should_update_song_list: bool,
|
||||
|
||||
// States
|
||||
pub queue_state: TableState,
|
||||
pub browser_state: TableState,
|
||||
pub playlists_state: ListState,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone)]
|
||||
|
|
@ -41,16 +56,22 @@ pub enum SelectedTab {
|
|||
|
||||
impl App {
|
||||
pub fn builder(addrs: &str) -> AppResult<Self> {
|
||||
let mut conn = Connection::new(addrs).unwrap();
|
||||
let mut conn = Connection::builder(addrs)?;
|
||||
let mut queue_list = ContentList::new();
|
||||
let mut pl_list = ContentList::new();
|
||||
|
||||
pl_list.list = Self::get_playlist(&mut conn.conn)?;
|
||||
pl_list.list.sort();
|
||||
|
||||
let append_list = Self::get_append_list(&mut conn.conn)?;
|
||||
Self::get_queue(&mut conn, &mut queue_list.list);
|
||||
|
||||
let browser = FileBrowser::new();
|
||||
|
||||
let queue_state = TableState::new();
|
||||
let browser_state = TableState::new();
|
||||
let playlists_state = ListState::default();
|
||||
|
||||
Ok(Self {
|
||||
running: true,
|
||||
conn,
|
||||
|
|
@ -64,13 +85,38 @@ impl App {
|
|||
search_cursor_pos: 0,
|
||||
pl_cursor_pos: 0,
|
||||
playlist_popup: false,
|
||||
pl_new_pl_input: String::new(),
|
||||
pl_new_pl_cursor_pos: 0,
|
||||
pl_new_pl_songs_buffer: Vec::new(),
|
||||
append_list,
|
||||
should_update_song_list: false,
|
||||
queue_state,
|
||||
browser_state,
|
||||
playlists_state,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
pub fn tick(&mut self) -> AppResult<()> {
|
||||
self.conn.update_status();
|
||||
self.update_queue();
|
||||
|
||||
// Deals with database update
|
||||
if self.should_update_song_list && self.conn.status.updating_db.is_none() {
|
||||
// Update the songs list
|
||||
self.conn.songs_filenames = self
|
||||
.conn
|
||||
.conn
|
||||
.listall()?
|
||||
.into_iter()
|
||||
.map(|x| x.file)
|
||||
.collect();
|
||||
|
||||
self.browser.update_directory(&mut self.conn)?;
|
||||
|
||||
self.should_update_song_list = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn quit(&mut self) {
|
||||
|
|
@ -78,17 +124,6 @@ impl App {
|
|||
}
|
||||
|
||||
pub fn get_queue(conn: &mut Connection, vec: &mut Vec<Song>) {
|
||||
// conn.conn.queue().unwrap().into_iter().for_each(|x| {
|
||||
// if let Some(title) = x.title {
|
||||
// if let Some(artist) = x.artist {
|
||||
// vec.push(format!("{} - {}", artist, title));
|
||||
// } else {
|
||||
// vec.push(title)
|
||||
// }
|
||||
// } else {
|
||||
// vec.push(x.file)
|
||||
// }
|
||||
// });
|
||||
conn.conn.queue().unwrap().into_iter().for_each(|x| {
|
||||
vec.push(x);
|
||||
});
|
||||
|
|
@ -108,6 +143,7 @@ impl App {
|
|||
pub fn get_append_list(conn: &mut Client) -> AppResult<ContentList<String>> {
|
||||
let mut list = ContentList::new();
|
||||
list.list.push("Current Playlist".to_string());
|
||||
list.list.push("New Playlist".to_string());
|
||||
for item in Self::get_playlist(conn)? {
|
||||
list.list.push(item.to_string());
|
||||
}
|
||||
|
|
@ -119,23 +155,50 @@ impl App {
|
|||
pub fn handle_add_or_remove_from_current_playlist(&mut self) -> AppResult<()> {
|
||||
match self.selected_tab {
|
||||
SelectedTab::DirectoryBrowser => {
|
||||
let (_, file) = self.browser.filetree.get(self.browser.selected).unwrap();
|
||||
|
||||
let (content_type, content) =
|
||||
self.browser.filetree.get(self.browser.selected).unwrap();
|
||||
if content_type == "directory" {
|
||||
let file = format!("{}/{}", self.browser.path, content);
|
||||
let songs = self.conn.conn.listfiles(&file).unwrap_or_default();
|
||||
for (t, f) in songs.iter() {
|
||||
if t == "file"
|
||||
&& Path::new(&f).has_extension(&[
|
||||
"mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape", "wma",
|
||||
"mpc", "aiff", "dff", "mp2", "mka",
|
||||
])
|
||||
{
|
||||
let path = file.clone() + "/" + f;
|
||||
let full_path = path.strip_prefix("./").unwrap_or("");
|
||||
let song = self.conn.get_song_with_only_filename(full_path);
|
||||
self.conn.conn.push(&song)?;
|
||||
}
|
||||
}
|
||||
} else if content_type == "file" {
|
||||
let mut status = false;
|
||||
for (i, song) in self.queue_list.list.clone().iter().enumerate() {
|
||||
if song.file.contains(file) {
|
||||
let song_path = song.file.split('/').last().unwrap_or_default();
|
||||
if song_path.eq(content) {
|
||||
self.conn.conn.delete(i as u32).unwrap();
|
||||
status = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !status {
|
||||
if let Some(full_path) = &self.conn.get_full_path(file) {
|
||||
let song = self.conn.get_song_with_only_filename(full_path);
|
||||
let mut filename = format!("{}/{}", self.browser.path, content);
|
||||
|
||||
// Remove "./" from the beginning of filename
|
||||
filename.remove(0);
|
||||
filename.remove(0);
|
||||
|
||||
let song = self.conn.get_song_with_only_filename(&filename);
|
||||
self.conn.conn.push(&song)?;
|
||||
|
||||
// updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated
|
||||
self.update_queue();
|
||||
}
|
||||
}
|
||||
|
||||
// Highlight next row if possible
|
||||
if self.browser.selected != self.browser.filetree.len() - 1 {
|
||||
self.browser.selected += 1;
|
||||
}
|
||||
|
|
@ -154,7 +217,7 @@ impl App {
|
|||
.to_string();
|
||||
|
||||
for (i, song) in self.queue_list.list.clone().iter().enumerate() {
|
||||
if song.file.contains(&file) {
|
||||
if song.file.eq(&file) {
|
||||
self.conn.conn.delete(i as u32).unwrap();
|
||||
if self.queue_list.index == self.queue_list.list.len() - 1
|
||||
&& self.queue_list.index != 0
|
||||
|
|
@ -169,6 +232,7 @@ impl App {
|
|||
}
|
||||
|
||||
self.update_queue();
|
||||
self.conn.update_status();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
@ -187,37 +251,29 @@ impl App {
|
|||
let (t, path) = browser.filetree.get(browser.selected).unwrap();
|
||||
if t == "directory" {
|
||||
if path != "." {
|
||||
browser.prev_path = browser.path.clone();
|
||||
browser.prev_path.clone_from(&browser.path);
|
||||
browser.path = browser.prev_path.clone() + "/" + path;
|
||||
browser.update_directory(&mut self.conn)?;
|
||||
browser.prev_selected = browser.selected;
|
||||
browser.selected = 0;
|
||||
}
|
||||
} 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 index = self
|
||||
.queue_list
|
||||
.list
|
||||
.iter()
|
||||
.position(|x| x.file.contains(path));
|
||||
let index = self.queue_list.list.iter().position(|x| {
|
||||
let file = x.file.split('/').last().unwrap_or_default();
|
||||
file.eq(path)
|
||||
});
|
||||
|
||||
if index.is_some() {
|
||||
self.conn.conn.switch(index.unwrap() as u32)?;
|
||||
} else {
|
||||
for filename in self.conn.songs_filenames.clone().iter() {
|
||||
if filename.contains(path) {
|
||||
let song = self.conn.get_song_with_only_filename(filename);
|
||||
let mut filename = format!("{}/{}", browser.path, path);
|
||||
|
||||
// Remove "./" from the beginning of filename
|
||||
filename.remove(0);
|
||||
filename.remove(0);
|
||||
|
||||
let song = self.conn.get_song_with_only_filename(&filename);
|
||||
self.conn.push(&song)?;
|
||||
}
|
||||
}
|
||||
|
||||
// updating queue, to avoid multiple pushes of the same songs if we enter multiple times before the queue gets updated
|
||||
self.update_queue();
|
||||
|
|
@ -237,6 +293,10 @@ impl App {
|
|||
let cursor_moved_left = self.search_cursor_pos.saturating_sub(1);
|
||||
self.search_cursor_pos = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
InputMode::NewPlaylist => {
|
||||
let cursor_moved_left = self.pl_new_pl_cursor_pos.saturating_sub(1);
|
||||
self.pl_new_pl_cursor_pos = self.clamp_cursor(cursor_moved_left);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -251,6 +311,12 @@ impl App {
|
|||
let cursor_moved_right = self.search_cursor_pos.saturating_add(1);
|
||||
self.search_cursor_pos = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
InputMode::NewPlaylist => {
|
||||
let cursor_moved_right = self.pl_new_pl_cursor_pos.saturating_add(1);
|
||||
self.pl_new_pl_cursor_pos = self.clamp_cursor(cursor_moved_right);
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -259,19 +325,24 @@ impl App {
|
|||
match self.inputmode {
|
||||
InputMode::PlaylistRename => {
|
||||
self.pl_newname_input.insert(self.pl_cursor_pos, new_char);
|
||||
self.move_cursor_right();
|
||||
}
|
||||
InputMode::NewPlaylist => {
|
||||
self.pl_new_pl_input
|
||||
.insert(self.pl_new_pl_cursor_pos, new_char);
|
||||
}
|
||||
InputMode::Editing => {
|
||||
self.search_input.insert(self.search_cursor_pos, new_char);
|
||||
self.move_cursor_right();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
self.move_cursor_right();
|
||||
}
|
||||
|
||||
pub fn delete_char(&mut self) {
|
||||
let is_not_cursor_leftmost = match self.inputmode {
|
||||
InputMode::PlaylistRename => self.pl_cursor_pos != 0,
|
||||
InputMode::NewPlaylist => self.pl_new_pl_cursor_pos != 0,
|
||||
InputMode::Editing => self.search_cursor_pos != 0,
|
||||
_ => false,
|
||||
};
|
||||
|
|
@ -284,31 +355,35 @@ impl App {
|
|||
let current_index = match self.inputmode {
|
||||
InputMode::Editing => self.search_cursor_pos,
|
||||
InputMode::PlaylistRename => self.pl_cursor_pos,
|
||||
InputMode::NewPlaylist => self.pl_new_pl_cursor_pos,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
let from_left_to_current_index = current_index - 1;
|
||||
|
||||
if self.inputmode == InputMode::PlaylistRename {
|
||||
// Getting all characters before the selected character.
|
||||
let before_char_to_delete = self
|
||||
.pl_newname_input
|
||||
.chars()
|
||||
.take(from_left_to_current_index);
|
||||
// Getting all characters after selected character.
|
||||
let after_char_to_delete = self.pl_newname_input.chars().skip(current_index);
|
||||
// Put all characters together except the selected one.
|
||||
// By leaving the selected one out, it is forgotten and therefore deleted.
|
||||
|
||||
self.pl_newname_input = before_char_to_delete.chain(after_char_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
} else if self.inputmode == InputMode::NewPlaylist {
|
||||
let before_char_to_delete = self
|
||||
.pl_new_pl_input
|
||||
.chars()
|
||||
.take(from_left_to_current_index);
|
||||
let after_char_to_delete = self.pl_new_pl_input.chars().skip(current_index);
|
||||
|
||||
self.pl_new_pl_input = before_char_to_delete.chain(after_char_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
} else if self.inputmode == InputMode::Editing {
|
||||
// Getting all characters before the selected character.
|
||||
let before_char_to_delete =
|
||||
self.search_input.chars().take(from_left_to_current_index);
|
||||
// Getting all characters after selected character.
|
||||
let after_char_to_delete = self.search_input.chars().skip(current_index);
|
||||
// Put all characters together except the selected one.
|
||||
// By leaving the selected one out, it is forgotten and therefore deleted.
|
||||
|
||||
self.search_input = before_char_to_delete.chain(after_char_to_delete).collect();
|
||||
self.move_cursor_left();
|
||||
}
|
||||
|
|
@ -318,6 +393,7 @@ impl App {
|
|||
pub fn clamp_cursor(&self, new_cursor_pos: usize) -> usize {
|
||||
match self.inputmode {
|
||||
InputMode::PlaylistRename => new_cursor_pos.clamp(0, self.pl_newname_input.len()),
|
||||
InputMode::NewPlaylist => new_cursor_pos.clamp(0, self.pl_new_pl_input.len()),
|
||||
InputMode::Editing => new_cursor_pos.clamp(0, self.search_input.len()),
|
||||
_ => 0,
|
||||
}
|
||||
|
|
@ -331,6 +407,9 @@ impl App {
|
|||
InputMode::PlaylistRename => {
|
||||
self.pl_cursor_pos = 0;
|
||||
}
|
||||
InputMode::NewPlaylist => {
|
||||
self.pl_new_pl_cursor_pos = 0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -348,10 +427,36 @@ impl App {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn change_playlist_name(&mut self) -> AppResult<()> {
|
||||
if self.selected_tab == SelectedTab::Playlists {
|
||||
self.inputmode = InputMode::PlaylistRename;
|
||||
// Mouse event handlers
|
||||
pub fn handle_scroll_up(&mut self) {
|
||||
match self.selected_tab {
|
||||
SelectedTab::Queue => {
|
||||
self.queue_list.prev();
|
||||
}
|
||||
SelectedTab::DirectoryBrowser => {
|
||||
self.browser.prev();
|
||||
}
|
||||
SelectedTab::Playlists => {
|
||||
self.pl_list.prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_scroll_down(&mut self) {
|
||||
match self.selected_tab {
|
||||
SelectedTab::Queue => {
|
||||
self.queue_list.next();
|
||||
}
|
||||
SelectedTab::DirectoryBrowser => {
|
||||
self.browser.next();
|
||||
}
|
||||
SelectedTab::Playlists => {
|
||||
self.pl_list.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_mouse_left_click(&mut self, _x: u16, _y: u16) -> AppResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
use mpd::Song;
|
||||
|
||||
use crate::{app::AppResult, connection::Connection};
|
||||
use crate::{app::AppResult, connection::Connection, utils::FileExtension};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// struct for working with directory browser tab in rmptui
|
||||
|
|
@ -16,23 +15,6 @@ pub struct FileBrowser {
|
|||
pub songs: Vec<Song>,
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type
|
||||
pub trait FileExtension {
|
||||
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool;
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> FileExtension for P {
|
||||
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool {
|
||||
if let Some(extension) = self.as_ref().extension().and_then(OsStr::to_str) {
|
||||
return extensions
|
||||
.iter()
|
||||
.any(|x| x.as_ref().eq_ignore_ascii_case(extension));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl FileBrowser {
|
||||
pub fn new() -> FileBrowser {
|
||||
FileBrowser {
|
||||
|
|
@ -50,7 +32,7 @@ impl FileBrowser {
|
|||
let mut file_vec: Vec<(String, String)> = vec![];
|
||||
let mut dir_vec: Vec<(String, String)> = vec![];
|
||||
for (t, f) in conn.conn.listfiles(self.path.as_str())?.into_iter() {
|
||||
if t == "directory" {
|
||||
if t == "directory" && !f.starts_with('.') {
|
||||
dir_vec.push((t, f));
|
||||
} else if t == "file"
|
||||
&& Path::new(&f).has_extension(&[
|
||||
|
|
@ -62,19 +44,37 @@ impl FileBrowser {
|
|||
}
|
||||
}
|
||||
|
||||
// dir_vec.sort_by(|a, b| a.1.cmp(&b.1));
|
||||
dir_vec.sort_by(|a, b| {
|
||||
let num_a = a.1.parse::<u32>().unwrap_or(u32::MAX);
|
||||
let num_b = b.1.parse::<u32>().unwrap_or(u32::MAX);
|
||||
num_a
|
||||
.cmp(&num_b)
|
||||
.then_with(|| a.1.to_lowercase().cmp(&b.1.to_lowercase()))
|
||||
});
|
||||
|
||||
file_vec.sort_by(|a, b| {
|
||||
let num_a = a.1.parse::<u32>().unwrap_or(u32::MAX);
|
||||
let num_b = b.1.parse::<u32>().unwrap_or(u32::MAX);
|
||||
num_a
|
||||
.cmp(&num_b)
|
||||
.then_with(|| a.1.to_lowercase().cmp(&b.1.to_lowercase()))
|
||||
});
|
||||
|
||||
// Add metadata
|
||||
dir_vec.extend(file_vec);
|
||||
self.filetree = dir_vec;
|
||||
|
||||
// Add metadata
|
||||
self.songs.clear();
|
||||
for (t, song) in self.filetree.iter() {
|
||||
if t == "file" {
|
||||
let v = conn
|
||||
.conn
|
||||
.lsinfo(Song {
|
||||
file: conn
|
||||
.get_full_path(song)
|
||||
.unwrap_or_else(|| "Not a song".to_string()),
|
||||
file: (self.path.clone() + "/" + song)
|
||||
.strip_prefix("./")
|
||||
.unwrap_or("")
|
||||
.to_string(),
|
||||
..Default::default()
|
||||
})
|
||||
.unwrap_or_else(|_| {
|
||||
|
|
@ -103,12 +103,6 @@ impl FileBrowser {
|
|||
if self.selected < self.filetree.len() - 1 {
|
||||
self.selected += 1;
|
||||
}
|
||||
|
||||
// if self.selected == self.filetree.len() - 1 {
|
||||
// self.selected = 0;
|
||||
// } else {
|
||||
// self.selected += 1;
|
||||
// }
|
||||
}
|
||||
|
||||
/// Go to previous item in filetree
|
||||
|
|
@ -116,21 +110,18 @@ impl FileBrowser {
|
|||
if self.selected != 0 {
|
||||
self.selected -= 1;
|
||||
}
|
||||
// if self.selected == 0 {
|
||||
// self.selected = self.filetree.len() - 1;
|
||||
// } else {
|
||||
// self.selected -= 1;
|
||||
// }
|
||||
}
|
||||
|
||||
/// handles going back event
|
||||
pub fn handle_go_back(&mut self, conn: &mut Connection) -> AppResult<()> {
|
||||
if self.prev_path != "." {
|
||||
let r = self.path.rfind('/').unwrap();
|
||||
let r = self.path.rfind('/');
|
||||
if let Some(r) = r {
|
||||
self.path = self.path.as_str()[..r].to_string();
|
||||
self.update_directory(conn)?;
|
||||
}
|
||||
} else {
|
||||
self.path = self.prev_path.clone();
|
||||
self.path.clone_from(&self.prev_path);
|
||||
self.update_directory(conn)?;
|
||||
}
|
||||
|
||||
|
|
@ -139,8 +130,6 @@ impl FileBrowser {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
impl Default for FileBrowser {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
|
|
|
|||
|
|
@ -1,11 +1,16 @@
|
|||
use crate::app::AppResult;
|
||||
use crate::utils::is_installed;
|
||||
use mpd::song::Song;
|
||||
use mpd::{Client, State};
|
||||
use simple_dmenu::dmenu;
|
||||
use std::process::Command;
|
||||
use std::time::Duration;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
pub type Error = Box<dyn std::error::Error>;
|
||||
/// Defines the current status of volume (Muted or UnMuted)
|
||||
#[derive(Debug)]
|
||||
pub enum VolumeStatus {
|
||||
Muted(i8),
|
||||
Unmuted,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// struct storing the mpd Client related stuff
|
||||
|
|
@ -15,58 +20,54 @@ pub struct Connection {
|
|||
pub state: String,
|
||||
pub elapsed: Duration,
|
||||
pub total_duration: Duration,
|
||||
pub volume: u8,
|
||||
pub repeat: bool,
|
||||
pub random: bool,
|
||||
pub current_song: Song,
|
||||
pub stats: mpd::Stats,
|
||||
pub status: mpd::Status,
|
||||
pub volume_status: VolumeStatus,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
/// Create a new connection
|
||||
pub fn new(addrs: &str) -> Result<Self> {
|
||||
let mut conn = Client::connect(addrs).unwrap();
|
||||
pub fn builder(addrs: &str) -> AppResult<Self> {
|
||||
let mut conn = Client::connect(addrs).unwrap_or_else(|_| {
|
||||
eprintln!("Error connecting to mpd server, Make sure mpd is running");
|
||||
std::process::exit(1);
|
||||
});
|
||||
|
||||
let empty_song = Song {
|
||||
file: "No Song playing or in Queue".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let songs_filenames: Vec<String> = conn
|
||||
.listall()
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|x| x.file)
|
||||
.collect();
|
||||
let songs_filenames: Vec<String> = conn.listall()?.into_iter().map(|x| x.file).collect();
|
||||
|
||||
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;
|
||||
let stats = conn.stats().unwrap_or_default();
|
||||
|
||||
let current_song = conn
|
||||
.currentsong()
|
||||
.unwrap_or_else(|_| Some(empty_song.clone()))
|
||||
.unwrap_or(empty_song);
|
||||
|
||||
let volume_status = VolumeStatus::Unmuted;
|
||||
|
||||
Ok(Self {
|
||||
conn,
|
||||
songs_filenames,
|
||||
state: "Stopped".to_string(),
|
||||
elapsed,
|
||||
total_duration: total,
|
||||
volume,
|
||||
repeat,
|
||||
random,
|
||||
current_song,
|
||||
stats,
|
||||
status,
|
||||
volume_status,
|
||||
})
|
||||
}
|
||||
|
||||
/// Dmenu prompt for selecting songs
|
||||
pub fn play_dmenu(&mut self) -> Result<()> {
|
||||
is_installed("dmenu")?;
|
||||
pub fn play_dmenu(&mut self) -> AppResult<()> {
|
||||
if is_installed("dmenu") {
|
||||
let ss: Vec<&str> = self.songs_filenames.iter().map(|x| x.as_str()).collect();
|
||||
let op = dmenu!(iter &ss; args "-p", "Choose a song: ", "-l", "30");
|
||||
let index = ss.iter().position(|s| s == &op);
|
||||
|
|
@ -74,41 +75,39 @@ impl Connection {
|
|||
let song = self.get_song_with_only_filename(ss.get(i).unwrap());
|
||||
self.push(&song)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Update status
|
||||
pub fn update_status(&mut self) {
|
||||
let status = self.conn.status().unwrap();
|
||||
let stats = self.conn.stats().unwrap_or_default();
|
||||
|
||||
let empty_song = self.get_song_with_only_filename("No Song playing or in Queue");
|
||||
|
||||
let current_song = self
|
||||
.conn
|
||||
.currentsong()
|
||||
.unwrap_or_else(|_| Some(empty_song.clone()))
|
||||
.unwrap_or(empty_song);
|
||||
let stats = self.conn.stats().unwrap_or_default();
|
||||
|
||||
// Status
|
||||
self.status = status.clone();
|
||||
|
||||
// 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 = match status.state {
|
||||
State::Stop => "Stopped".to_string(),
|
||||
State::Play => "Playing".to_string(),
|
||||
State::Pause => "Paused".to_string(),
|
||||
};
|
||||
|
||||
// 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;
|
||||
|
||||
// Current song
|
||||
self.current_song = current_song;
|
||||
|
||||
|
|
@ -132,7 +131,7 @@ impl Connection {
|
|||
}
|
||||
|
||||
/// push the given song to queue
|
||||
pub fn push(&mut self, song: &Song) -> Result<()> {
|
||||
pub fn push(&mut self, song: &Song) -> AppResult<()> {
|
||||
if self.conn.queue().unwrap().is_empty() {
|
||||
self.conn.push(song).unwrap();
|
||||
self.conn.play().unwrap();
|
||||
|
|
@ -149,14 +148,14 @@ impl Connection {
|
|||
}
|
||||
|
||||
/// Push all songs of a playlist into queue
|
||||
pub fn load_playlist(&mut self, playlist: &str) -> Result<()> {
|
||||
pub fn load_playlist(&mut self, playlist: &str) -> AppResult<()> {
|
||||
self.conn.load(playlist, ..)?;
|
||||
self.conn.play()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add given song to playlist
|
||||
pub fn add_to_playlist(&mut self, playlist: &str, song: &Song) -> Result<()> {
|
||||
pub fn add_to_playlist(&mut self, playlist: &str, song: &Song) -> AppResult<()> {
|
||||
self.conn.pl_push(playlist, song)?;
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -170,17 +169,17 @@ impl Connection {
|
|||
}
|
||||
|
||||
/// Given a song name from a directory, it returns the full path of the song in the database
|
||||
pub fn get_full_path(&self, short_path: &str) -> Option<String> {
|
||||
pub fn get_full_path(&self, short_path: &str) -> Option<&str> {
|
||||
for (i, f) in self.songs_filenames.iter().enumerate() {
|
||||
if f.contains(short_path) {
|
||||
return Some(self.songs_filenames.get(i).unwrap().to_string());
|
||||
return Some(self.songs_filenames.get(i).unwrap());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Gives title of current playing song
|
||||
pub fn now_playing(&mut self) -> Result<Option<String>> {
|
||||
pub fn now_playing(&mut self) -> AppResult<Option<String>> {
|
||||
if let Some(s) = &self.current_song.title {
|
||||
if let Some(a) = &self.current_song.artist {
|
||||
Ok(Some(format!("\"{}\" By {}", s, a)))
|
||||
|
|
@ -205,50 +204,32 @@ impl Connection {
|
|||
|
||||
/// 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();
|
||||
}
|
||||
let mode = self.status.repeat;
|
||||
self.conn.repeat(!mode).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();
|
||||
}
|
||||
let mode = self.status.random;
|
||||
self.conn.random(!mode).unwrap();
|
||||
}
|
||||
|
||||
// Volume controls
|
||||
/// Increase Volume
|
||||
pub fn inc_volume(&mut self, v: i8) {
|
||||
let cur = self.conn.status().unwrap().volume;
|
||||
let cur = self.status.volume;
|
||||
if cur + v <= 100 {
|
||||
self.volume_status = VolumeStatus::Unmuted;
|
||||
self.conn.volume(cur + v).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Decrease volume
|
||||
pub fn dec_volume(&mut self, v: i8) {
|
||||
let cur = self.conn.status().unwrap().volume;
|
||||
let cur = self.status.volume;
|
||||
if cur - v >= 0 {
|
||||
self.volume_status = VolumeStatus::Unmuted;
|
||||
self.conn.volume(cur - v).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks if given program is installed in your system
|
||||
fn is_installed(ss: &str) -> Result<()> {
|
||||
let output = Command::new("which")
|
||||
.arg(ss)
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
if output.status.success() {
|
||||
Ok(())
|
||||
} else {
|
||||
let err = format!("{} not installed", ss);
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,82 +0,0 @@
|
|||
use crate::app::{App, AppResult, SelectedTab};
|
||||
use crate::browser::FileExtension;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
||||
match key_event.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
app.playlist_popup = false;
|
||||
}
|
||||
|
||||
KeyCode::Char('j') | KeyCode::Down => app.append_list.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.append_list.prev(),
|
||||
|
||||
KeyCode::Enter => {
|
||||
let pl_index = app.append_list.index;
|
||||
let pl_name = &app.append_list.list.get(pl_index).unwrap();
|
||||
|
||||
let s_index: usize;
|
||||
match app.selected_tab {
|
||||
SelectedTab::Queue => {
|
||||
s_index = app.queue_list.index;
|
||||
let short_path = &app.queue_list.list.get(s_index).unwrap().file;
|
||||
|
||||
if let Some(full_path) = app.conn.get_full_path(short_path) {
|
||||
let song = app.conn.get_song_with_only_filename(&full_path);
|
||||
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.conn.push(&song)?;
|
||||
app.update_queue();
|
||||
} else {
|
||||
app.conn.add_to_playlist(pl_name, &song)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectedTab::DirectoryBrowser => {
|
||||
let (t, f) = app.browser.filetree.get(app.browser.selected).unwrap();
|
||||
if t == "file" {
|
||||
let short_path = f;
|
||||
if let Some(full_path) = app.conn.get_full_path(short_path) {
|
||||
let song = app.conn.get_song_with_only_filename(&full_path);
|
||||
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.conn.push(&song)?;
|
||||
app.update_queue();
|
||||
} else {
|
||||
app.conn.add_to_playlist(pl_name, &song)?;
|
||||
}
|
||||
}
|
||||
} else if t == "directory" {
|
||||
for (t, f) in app.conn.conn.listfiles(f)?.iter() {
|
||||
// dir_vec.push((t, f));
|
||||
if t == "file"
|
||||
&& Path::new(&f).has_extension(&[
|
||||
"mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape",
|
||||
"wma", "mpc", "aiff", "dff", "mp2", "mka",
|
||||
])
|
||||
{
|
||||
let full_path = app.conn.get_full_path(f).unwrap_or_default();
|
||||
let song = app.conn.get_song_with_only_filename(&full_path);
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.conn.push(&song)?;
|
||||
} else {
|
||||
app.conn.add_to_playlist(pl_name, &song)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// hide the playlist popup
|
||||
app.playlist_popup = false;
|
||||
app.append_list.index = 0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
use crate::{
|
||||
app::{App, AppResult, SelectedTab},
|
||||
connection::VolumeStatus,
|
||||
ui::InputMode,
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers, MouseEvent, MouseEventKind};
|
||||
use std::time::Duration;
|
||||
|
||||
use super::{pl_append_keys, pl_rename_keys, search_keys};
|
||||
use super::{new_pl_keys, pl_append_keys, pl_rename_keys, search_keys};
|
||||
|
||||
pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
||||
// searching, playlist renaming, playlist appending
|
||||
|
|
@ -13,6 +14,8 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
search_keys::handle_search_keys(key_event, app)?;
|
||||
} else if app.inputmode == InputMode::PlaylistRename {
|
||||
pl_rename_keys::handle_pl_rename_keys(key_event, app)?;
|
||||
} else if app.inputmode == InputMode::NewPlaylist {
|
||||
new_pl_keys::handle_new_pl_keys(key_event, app)?;
|
||||
} else if app.playlist_popup {
|
||||
pl_append_keys::hande_pl_append_keys(key_event, app)?;
|
||||
} else {
|
||||
|
|
@ -33,10 +36,16 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
|
||||
// Playback controls
|
||||
// Toggle Pause
|
||||
KeyCode::Char('p') => app.conn.toggle_pause(),
|
||||
KeyCode::Char('p') => {
|
||||
app.conn.toggle_pause();
|
||||
app.conn.update_status();
|
||||
}
|
||||
|
||||
// Pause
|
||||
KeyCode::Char('s') => app.conn.pause(),
|
||||
KeyCode::Char('s') => {
|
||||
app.conn.pause();
|
||||
app.conn.update_status();
|
||||
}
|
||||
|
||||
// Toggle rpeat
|
||||
KeyCode::Char('r') => {
|
||||
|
|
@ -51,25 +60,36 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
}
|
||||
|
||||
// Dmenu prompt
|
||||
KeyCode::Char('D') => app.conn.play_dmenu()?,
|
||||
KeyCode::Char('D') => {
|
||||
app.conn.play_dmenu()?;
|
||||
app.conn.update_status();
|
||||
}
|
||||
|
||||
// add to queue
|
||||
KeyCode::Char('a') => app.playlist_popup = true,
|
||||
|
||||
// Fast forward
|
||||
KeyCode::Char('f') => {
|
||||
let place = app.conn.conn.status().unwrap().song.unwrap().pos;
|
||||
let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
|
||||
if !app.queue_list.list.is_empty() {
|
||||
let status = app.conn.conn.status().unwrap_or_default();
|
||||
let place = status.song.unwrap_or_default().pos;
|
||||
let (pos, _) = status.time.unwrap_or_default();
|
||||
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
|
||||
app.conn.conn.seek(place, pos)?;
|
||||
app.conn.update_status();
|
||||
}
|
||||
}
|
||||
|
||||
// backward
|
||||
KeyCode::Char('b') => {
|
||||
let place = app.conn.conn.status().unwrap().song.unwrap().pos;
|
||||
let (pos, _) = app.conn.conn.status().unwrap().time.unwrap();
|
||||
let pos = Duration::from_secs(pos.as_secs().wrapping_add(2));
|
||||
if !app.queue_list.list.is_empty() {
|
||||
let status = app.conn.conn.status().unwrap_or_default();
|
||||
let place = status.song.unwrap_or_default().pos;
|
||||
let (pos, _) = status.time.unwrap_or_default();
|
||||
let pos = Duration::from_secs(pos.as_secs().wrapping_sub(2));
|
||||
app.conn.conn.seek(place, pos)?;
|
||||
app.conn.update_status();
|
||||
}
|
||||
}
|
||||
|
||||
// Cycle through tabs
|
||||
|
|
@ -111,7 +131,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
}
|
||||
|
||||
// Volume controls
|
||||
KeyCode::Char('=') => {
|
||||
KeyCode::Char('=') | KeyCode::Char('+') => {
|
||||
app.conn.inc_volume(2);
|
||||
app.conn.update_status();
|
||||
}
|
||||
|
|
@ -121,10 +141,26 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
app.conn.update_status();
|
||||
}
|
||||
|
||||
// Toggle Mute
|
||||
KeyCode::Char('m') => {
|
||||
match app.conn.volume_status {
|
||||
VolumeStatus::Muted(v) => {
|
||||
app.conn.conn.volume(v)?;
|
||||
app.conn.volume_status = VolumeStatus::Unmuted;
|
||||
}
|
||||
VolumeStatus::Unmuted => {
|
||||
let current_volume = app.conn.status.volume;
|
||||
app.conn.conn.volume(0)?;
|
||||
app.conn.volume_status = VolumeStatus::Muted(current_volume);
|
||||
}
|
||||
}
|
||||
app.conn.update_status();
|
||||
}
|
||||
|
||||
// Update MPD database
|
||||
KeyCode::Char('U') => {
|
||||
app.conn.conn.rescan()?;
|
||||
app.browser.update_directory(&mut app.conn)?;
|
||||
app.should_update_song_list = true;
|
||||
}
|
||||
|
||||
// Search for songs
|
||||
|
|
@ -137,7 +173,7 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
}
|
||||
|
||||
// Add or Remove from Current Playlist
|
||||
KeyCode::Char(' ') | KeyCode::Backspace => {
|
||||
KeyCode::Char(' ') => {
|
||||
app.handle_add_or_remove_from_current_playlist()?;
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -261,16 +297,20 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
// go to bottom of list
|
||||
KeyCode::Char('G') => app.pl_list.index = app.pl_list.list.len() - 1,
|
||||
|
||||
// Change playlist name
|
||||
KeyCode::Char('e') => app.change_playlist_name()?,
|
||||
// Playlist Rename
|
||||
KeyCode::Char('R') => {
|
||||
app.inputmode = InputMode::PlaylistRename;
|
||||
}
|
||||
|
||||
// add to current playlist
|
||||
KeyCode::Enter | KeyCode::Char('l') | KeyCode::Right | KeyCode::Char(' ') => {
|
||||
// app.update_queue();
|
||||
if !app.pl_list.list.is_empty() {
|
||||
app.conn
|
||||
.load_playlist(app.pl_list.list.get(app.pl_list.index).unwrap())?;
|
||||
app.conn.update_status();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -278,3 +318,18 @@ pub fn handle_key_events(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
|||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_mouse_events(mouse_event: MouseEvent, app: &mut App) -> AppResult<()> {
|
||||
match mouse_event.kind {
|
||||
MouseEventKind::ScrollUp => app.handle_scroll_up(),
|
||||
MouseEventKind::ScrollDown => app.handle_scroll_down(),
|
||||
MouseEventKind::Down(button) => {
|
||||
let (x, y) = (mouse_event.column, mouse_event.row);
|
||||
if button == crossterm::event::MouseButton::Left {
|
||||
app.handle_mouse_left_click(x, y)?;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -3,3 +3,4 @@ pub mod handler;
|
|||
pub mod search_keys;
|
||||
pub mod pl_rename_keys;
|
||||
pub mod pl_append_keys;
|
||||
pub mod new_pl_keys;
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
use crate::{
|
||||
app::{App, AppResult},
|
||||
ui::InputMode,
|
||||
};
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
|
||||
pub fn handle_new_pl_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
||||
match key_event.code {
|
||||
KeyCode::Esc => {
|
||||
app.pl_new_pl_input.clear();
|
||||
app.reset_cursor();
|
||||
app.inputmode = InputMode::Normal;
|
||||
}
|
||||
KeyCode::Char(to_insert) => {
|
||||
app.enter_char(to_insert);
|
||||
}
|
||||
KeyCode::Enter => {
|
||||
let pl_name = &app.pl_new_pl_input;
|
||||
|
||||
for song in app.pl_new_pl_songs_buffer.iter() {
|
||||
app.conn.conn.pl_push(pl_name, song)?;
|
||||
}
|
||||
app.pl_new_pl_input.clear();
|
||||
|
||||
app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;
|
||||
app.append_list = App::get_append_list(&mut app.conn.conn)?;
|
||||
|
||||
app.reset_cursor();
|
||||
app.inputmode = InputMode::Normal;
|
||||
}
|
||||
|
||||
KeyCode::Backspace => {
|
||||
app.delete_char();
|
||||
}
|
||||
|
||||
KeyCode::Left => {
|
||||
app.move_cursor_left();
|
||||
}
|
||||
|
||||
KeyCode::Right => {
|
||||
app.move_cursor_right();
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -0,0 +1,115 @@
|
|||
use crate::app::{App, AppResult, SelectedTab};
|
||||
use crate::ui::InputMode;
|
||||
use crate::utils::FileExtension;
|
||||
use crossterm::event::{KeyCode, KeyEvent};
|
||||
use std::path::Path;
|
||||
|
||||
pub fn hande_pl_append_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()> {
|
||||
match key_event.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => {
|
||||
app.playlist_popup = false;
|
||||
}
|
||||
|
||||
KeyCode::Char('j') | KeyCode::Down => app.append_list.next(),
|
||||
KeyCode::Char('k') | KeyCode::Up => app.append_list.prev(),
|
||||
|
||||
KeyCode::Enter => {
|
||||
// name of highlighted playlist in append list
|
||||
let pl_name = &app.append_list.get_item_at_current_index();
|
||||
|
||||
match app.selected_tab {
|
||||
SelectedTab::Queue => {
|
||||
// Just exit out the menu if no item is selected in the Queue
|
||||
if app.queue_list.list.is_empty() {
|
||||
app.playlist_popup = false;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Ok(songs) = app.conn.conn.songs(app.queue_list.index as u32) {
|
||||
let option_song = songs.first();
|
||||
if let Some(song) = option_song {
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.conn.push(song)?;
|
||||
app.update_queue();
|
||||
} else if *pl_name == "New Playlist" {
|
||||
app.pl_new_pl_songs_buffer.clear();
|
||||
app.pl_new_pl_songs_buffer.push(song.clone());
|
||||
app.inputmode = InputMode::NewPlaylist;
|
||||
} else {
|
||||
app.conn.add_to_playlist(pl_name, song)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectedTab::DirectoryBrowser => {
|
||||
let (t, f) = app.browser.filetree.get(app.browser.selected).unwrap();
|
||||
if t == "file" {
|
||||
let short_path = f;
|
||||
if let Some(full_path) = app.conn.get_full_path(short_path) {
|
||||
let song = app.conn.get_song_with_only_filename(full_path);
|
||||
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.conn.push(&song)?;
|
||||
app.update_queue();
|
||||
} else if *pl_name == "New Playlist" {
|
||||
app.pl_new_pl_songs_buffer.clear();
|
||||
app.pl_new_pl_songs_buffer.push(song.clone());
|
||||
app.inputmode = InputMode::NewPlaylist;
|
||||
} else {
|
||||
app.conn.add_to_playlist(pl_name, &song)?;
|
||||
}
|
||||
}
|
||||
} else if t == "directory" {
|
||||
let file = format!("{}/{}", app.browser.path, f);
|
||||
app.pl_new_pl_songs_buffer.clear();
|
||||
for (t, f) in app.conn.conn.listfiles(&file)?.iter() {
|
||||
// dir_vec.push((t, f));
|
||||
if t == "file"
|
||||
&& Path::new(&f).has_extension(&[
|
||||
"mp3", "ogg", "flac", "m4a", "wav", "aac", "opus", "ape",
|
||||
"wma", "mpc", "aiff", "dff", "mp2", "mka",
|
||||
])
|
||||
{
|
||||
let full_path = app.conn.get_full_path(f).unwrap_or_default();
|
||||
let song = app.conn.get_song_with_only_filename(full_path);
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.conn.push(&song)?;
|
||||
} else if *pl_name == "New Playlist" {
|
||||
app.pl_new_pl_songs_buffer.push(song.clone());
|
||||
app.inputmode = InputMode::NewPlaylist;
|
||||
} else {
|
||||
app.conn.add_to_playlist(pl_name, &song)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SelectedTab::Playlists => {
|
||||
let playlist_name = app.pl_list.get_item_at_current_index();
|
||||
if *pl_name == "Current Playlist" {
|
||||
app.conn.load_playlist(playlist_name)?;
|
||||
app.update_queue();
|
||||
} else if *pl_name == "New Playlist" {
|
||||
app.inputmode = InputMode::NewPlaylist;
|
||||
} else {
|
||||
let songs = app.conn.conn.playlist(playlist_name)?;
|
||||
for song in songs {
|
||||
// We ignore the Err() since there could be songs in playlists, which do not exist in the db anymore.
|
||||
// So instead of panicking, we just ignore if the song does not exists
|
||||
app.conn.add_to_playlist(pl_name, &song).unwrap_or(());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// hide the playlist popup
|
||||
app.playlist_popup = false;
|
||||
app.append_list.index = 0;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -16,7 +16,7 @@ pub fn handle_pl_rename_keys(key_event: KeyEvent, app: &mut App) -> AppResult<()
|
|||
}
|
||||
KeyCode::Enter => {
|
||||
app.conn.conn.pl_rename(
|
||||
app.pl_list.list.get(app.pl_list.index).unwrap(),
|
||||
app.pl_list.get_item_at_current_index(),
|
||||
&app.pl_newname_input,
|
||||
)?;
|
||||
app.pl_list.list = App::get_playlist(&mut app.conn.conn)?;
|
||||
|
|
@ -14,7 +14,10 @@ pub mod list;
|
|||
pub mod browser;
|
||||
|
||||
/// Event Handler/ keymaps
|
||||
pub mod event;
|
||||
pub mod event_handler;
|
||||
|
||||
/// Application
|
||||
pub mod app;
|
||||
|
||||
/// Utilities
|
||||
pub mod utils;
|
||||
|
|
|
|||
|
|
@ -30,6 +30,11 @@ impl<T> ContentList<T> {
|
|||
pub fn reset_index(&mut self) {
|
||||
self.index = 0;
|
||||
}
|
||||
|
||||
/// Returns the self.list[self.index] item
|
||||
pub fn get_item_at_current_index(&mut self) -> &T {
|
||||
self.list.get(self.index).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for ContentList<T> {
|
||||
|
|
|
|||
22
src/main.rs
22
src/main.rs
|
|
@ -1,15 +1,12 @@
|
|||
use ratatui::prelude::*;
|
||||
use rmptui::app::App;
|
||||
use rmptui::app::AppResult;
|
||||
use rmptui::event::event::Event;
|
||||
use rmptui::event::event::EventHandler;
|
||||
use rmptui::event::handler;
|
||||
use rmptui::event_handler::event::Event;
|
||||
use rmptui::event_handler::event::EventHandler;
|
||||
use rmptui::event_handler::handler;
|
||||
use rmptui::tui;
|
||||
use std::env;
|
||||
use std::io;
|
||||
use ratatui::prelude::*;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
pub type Error = Box<dyn std::error::Error>;
|
||||
|
||||
fn main() -> AppResult<()> {
|
||||
// Connection
|
||||
|
|
@ -26,14 +23,19 @@ fn main() -> AppResult<()> {
|
|||
tui.init()?;
|
||||
|
||||
// initial directory read
|
||||
app.browser.update_directory(&mut app.conn).unwrap();
|
||||
app.browser.update_directory(&mut app.conn)?;
|
||||
|
||||
// initially set the queue's highlighted item to the current playing item
|
||||
if let Ok(item) = app.conn.conn.currentsong() {
|
||||
app.queue_list.index = item.unwrap_or_default().place.unwrap_or_default().pos as usize;
|
||||
}
|
||||
|
||||
while app.running {
|
||||
tui.draw(&mut app)?;
|
||||
match tui.events.next()? {
|
||||
Event::Tick => app.tick(),
|
||||
Event::Tick => app.tick()?,
|
||||
Event::Key(key_event) => handler::handle_key_events(key_event, &mut app)?,
|
||||
Event::Mouse(_) => {}
|
||||
Event::Mouse(mouse_event) => handler::handle_mouse_events(mouse_event, &mut app)?,
|
||||
Event::Resize(_, _) => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ use crossterm::terminal::{self, *};
|
|||
use std::panic;
|
||||
|
||||
use crate::app::{App, AppResult};
|
||||
use crate::event::event::EventHandler;
|
||||
use crate::event_handler::event::EventHandler;
|
||||
|
||||
pub type CrosstermTerminal = ratatui::Terminal<ratatui::backend::CrosstermBackend<std::io::Stderr>>;
|
||||
|
||||
|
|
|
|||
119
src/ui.rs
119
src/ui.rs
|
|
@ -1,6 +1,9 @@
|
|||
use std::time::Duration;
|
||||
|
||||
use crate::app::{App, SelectedTab};
|
||||
use crate::{
|
||||
app::{App, SelectedTab},
|
||||
connection::VolumeStatus,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{block::Title, *},
|
||||
|
|
@ -11,6 +14,7 @@ pub enum InputMode {
|
|||
Editing,
|
||||
Normal,
|
||||
PlaylistRename,
|
||||
NewPlaylist,
|
||||
}
|
||||
|
||||
/// Renders the user interface widgets
|
||||
|
|
@ -37,6 +41,9 @@ pub fn render(app: &mut App, frame: &mut Frame) {
|
|||
InputMode::PlaylistRename => {
|
||||
draw_rename_playlist(frame, app, layout[1]);
|
||||
}
|
||||
InputMode::NewPlaylist => {
|
||||
draw_new_playlist(frame, app, layout[1]);
|
||||
}
|
||||
}
|
||||
|
||||
if app.playlist_popup {
|
||||
|
|
@ -76,7 +83,8 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
|
||||
let mut status: bool = false;
|
||||
for sn in app.queue_list.list.iter() {
|
||||
if sn.file.contains(s) {
|
||||
let file = sn.file.split('/').last().unwrap_or_default();
|
||||
if file.eq(s) {
|
||||
status = true;
|
||||
}
|
||||
}
|
||||
|
|
@ -90,7 +98,7 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
]);
|
||||
|
||||
if status {
|
||||
row.magenta().bold()
|
||||
row.bold()
|
||||
} else {
|
||||
row
|
||||
}
|
||||
|
|
@ -100,7 +108,6 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
}
|
||||
});
|
||||
|
||||
let mut state = TableState::new();
|
||||
let header = ["Artist", "Track", "Title", "Album", "Time"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
|
|
@ -121,13 +128,18 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
Block::default()
|
||||
.title(format!("Song Browser: {}", app.browser.path.clone()).bold())
|
||||
.title(
|
||||
Title::from(format!("Total Songs: {}", total_songs).bold().green())
|
||||
Title::from(format!("Total Songs: {}", total_songs).green())
|
||||
.alignment(Alignment::Center),
|
||||
)
|
||||
.title(
|
||||
Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
|
||||
.alignment(Alignment::Right),
|
||||
)
|
||||
.title(match app.conn.volume_status {
|
||||
VolumeStatus::Unmuted => {
|
||||
Title::from(format!("Volume: {}%", app.conn.status.volume).green())
|
||||
.alignment(Alignment::Right)
|
||||
}
|
||||
VolumeStatus::Muted(_v) => {
|
||||
Title::from("Muted".red()).alignment(Alignment::Right)
|
||||
}
|
||||
})
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.highlight_style(
|
||||
|
|
@ -139,8 +151,8 @@ fn draw_directory_browser(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
.header(header)
|
||||
.flex(layout::Flex::Legacy);
|
||||
|
||||
state.select(Some(app.browser.selected));
|
||||
frame.render_stateful_widget(table, size, &mut state);
|
||||
app.browser_state.select(Some(app.browser.selected));
|
||||
frame.render_stateful_widget(table, size, &mut app.browser_state);
|
||||
}
|
||||
|
||||
/// draws playing queue
|
||||
|
|
@ -168,17 +180,25 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
let time = App::format_time(song.clone().duration.unwrap_or_else(|| Duration::new(0, 0)));
|
||||
|
||||
let row = Row::new(vec![
|
||||
Cell::from(artist),
|
||||
Cell::from(track.green()),
|
||||
Cell::from(title),
|
||||
Cell::from(album.cyan()),
|
||||
Cell::from(artist.clone()),
|
||||
Cell::from(track.clone().green()),
|
||||
Cell::from(title.clone()),
|
||||
Cell::from(album.clone().cyan()),
|
||||
Cell::from(time.to_string().magenta()),
|
||||
]);
|
||||
|
||||
let place = app.conn.current_song.place;
|
||||
if let Some(pos) = place {
|
||||
if i == pos.pos as usize {
|
||||
row.magenta().bold()
|
||||
let row = Row::new(vec![
|
||||
Cell::from(format!("> {}", artist)),
|
||||
Cell::from(format!(" {}", track).green()),
|
||||
Cell::from(format!(" {}", title)),
|
||||
Cell::from(album.cyan()),
|
||||
Cell::from(time.to_string().magenta()),
|
||||
]);
|
||||
|
||||
row
|
||||
} else {
|
||||
row
|
||||
}
|
||||
|
|
@ -187,7 +207,6 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
}
|
||||
});
|
||||
|
||||
let mut state = TableState::new();
|
||||
let header = ["Artist", "Track", "Title", "Album", "Time"]
|
||||
.into_iter()
|
||||
.map(Cell::from)
|
||||
|
|
@ -210,10 +229,15 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
.title(Title::from(
|
||||
format!("({} items)", app.queue_list.list.len()).bold(),
|
||||
))
|
||||
.title(
|
||||
Title::from(format!("Volume: {}%", app.conn.volume).bold().green())
|
||||
.alignment(Alignment::Right),
|
||||
)
|
||||
.title(match app.conn.volume_status {
|
||||
VolumeStatus::Unmuted => {
|
||||
Title::from(format!("Volume: {}%", app.conn.status.volume).green())
|
||||
.alignment(Alignment::Right)
|
||||
}
|
||||
VolumeStatus::Muted(_v) => {
|
||||
Title::from("Muted".red()).alignment(Alignment::Right)
|
||||
}
|
||||
})
|
||||
.borders(Borders::ALL),
|
||||
)
|
||||
.highlight_style(
|
||||
|
|
@ -225,8 +249,8 @@ fn draw_queue(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
.header(header)
|
||||
.flex(layout::Flex::Legacy);
|
||||
|
||||
state.select(Some(app.queue_list.index));
|
||||
frame.render_stateful_widget(table, size, &mut state);
|
||||
app.queue_state.select(Some(app.queue_list.index));
|
||||
frame.render_stateful_widget(table, size, &mut app.queue_state);
|
||||
}
|
||||
|
||||
// Draw search bar
|
||||
|
|
@ -264,24 +288,24 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
// Get the current playing state
|
||||
let mut state: String = String::new();
|
||||
if !app.queue_list.list.is_empty() {
|
||||
state = app.conn.state.clone();
|
||||
state.clone_from(&app.conn.state);
|
||||
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 {
|
||||
if app.conn.status.repeat | app.conn.status.random {
|
||||
modes_bottom.push('r');
|
||||
}
|
||||
|
||||
if !modes_bottom.is_empty() {
|
||||
modes_bottom.clear();
|
||||
modes_bottom.push('[');
|
||||
if app.conn.repeat {
|
||||
if app.conn.status.repeat {
|
||||
modes_bottom.push('r');
|
||||
}
|
||||
if app.conn.random {
|
||||
if app.conn.status.random {
|
||||
modes_bottom.push('z');
|
||||
}
|
||||
modes_bottom.push(']');
|
||||
|
|
@ -301,7 +325,7 @@ fn draw_progress_bar(frame: &mut Frame, app: &mut App, size: Rect) {
|
|||
// Define the title
|
||||
let title = Block::default()
|
||||
.title(Title::from(state.red().bold()))
|
||||
.title(Title::from(song.green().bold()))
|
||||
.title(Title::from(song.green()))
|
||||
.title(Title::from(duration.cyan().bold()).alignment(Alignment::Right))
|
||||
.title(Title::from(modes_bottom).position(block::Position::Bottom))
|
||||
.borders(Borders::ALL);
|
||||
|
|
@ -327,7 +351,6 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||
.split(area);
|
||||
|
||||
// Draw list of playlists
|
||||
let mut state = ListState::default();
|
||||
let title = Block::default().title(Title::from("Playlist".green().bold()));
|
||||
let list = List::new(app.pl_list.list.clone())
|
||||
.block(title.borders(Borders::ALL))
|
||||
|
|
@ -339,11 +362,23 @@ fn draw_playlist_viewer(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||
.add_modifier(Modifier::REVERSED),
|
||||
)
|
||||
.repeat_highlight_symbol(true);
|
||||
state.select(Some(app.pl_list.index));
|
||||
frame.render_stateful_widget(list, layouts[0], &mut state);
|
||||
app.playlists_state.select(Some(app.pl_list.index));
|
||||
frame.render_stateful_widget(list, layouts[0], &mut app.playlists_state);
|
||||
|
||||
// Playlist viewer
|
||||
let pl_name = app.pl_list.list.get(app.pl_list.index).unwrap();
|
||||
|
||||
// Handle if there are no playlists in the mpd database
|
||||
if app.pl_list.list.is_empty() {
|
||||
let title = Block::default()
|
||||
.title(Title::from("No Playlists Found".red().bold()))
|
||||
.title_alignment(Alignment::Center)
|
||||
.borders(Borders::ALL);
|
||||
frame.render_widget(Clear, area); //this clears out the background
|
||||
frame.render_widget(title, area);
|
||||
return;
|
||||
}
|
||||
|
||||
let pl_name = app.pl_list.get_item_at_current_index();
|
||||
let songs = app.conn.conn.playlist(pl_name).unwrap();
|
||||
let rows = songs.iter().map(|song| {
|
||||
let title = song.clone().title.unwrap_or_default().cyan();
|
||||
|
|
@ -422,6 +457,26 @@ fn draw_rename_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
|
|||
frame.render_widget(input, area);
|
||||
}
|
||||
|
||||
fn draw_new_playlist(frame: &mut Frame, app: &mut App, area: Rect) {
|
||||
#[allow(clippy::cast_possible_truncation)]
|
||||
frame.set_cursor(
|
||||
// Draw the cursor at the current position in the input field.
|
||||
// This position is can be controlled via the left and right arrow key
|
||||
area.x + app.pl_new_pl_cursor_pos as u16 + 2,
|
||||
// Move one line down, from the border to the input line
|
||||
area.y + 1,
|
||||
);
|
||||
|
||||
let input = Paragraph::new("/".to_string() + &app.pl_new_pl_input)
|
||||
.style(Style::default())
|
||||
.block(
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title("Enter New Playlist's Name: ".bold().green()),
|
||||
);
|
||||
frame.render_widget(input, area);
|
||||
}
|
||||
|
||||
fn centered_rect(percent_x: u16, percent_y: u16, r: Rect) -> Rect {
|
||||
let popup_layout = Layout::vertical([
|
||||
Constraint::Percentage((100 - percent_y) / 2),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
use std::process::Command;
|
||||
use std::ffi::OsStr;
|
||||
use std::path::Path;
|
||||
|
||||
/// Checks if given program is installed in your system
|
||||
pub fn is_installed(ss: &str) -> bool {
|
||||
let output = Command::new("which")
|
||||
.arg(ss)
|
||||
.output()
|
||||
.expect("Failed to execute command");
|
||||
|
||||
output.status.success()
|
||||
}
|
||||
|
||||
/// Checks if a file has a given extension
|
||||
// https://stackoverflow.com/questions/72392835/check-if-a-file-is-of-a-given-type
|
||||
pub trait FileExtension {
|
||||
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool;
|
||||
}
|
||||
|
||||
impl<P: AsRef<Path>> FileExtension for P {
|
||||
fn has_extension<S: AsRef<str>>(&self, extensions: &[S]) -> bool {
|
||||
if let Some(extension) = self.as_ref().extension().and_then(OsStr::to_str) {
|
||||
return extensions
|
||||
.iter()
|
||||
.any(|x| x.as_ref().eq_ignore_ascii_case(extension));
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue