use crate::config::{Keys, Settings};
use crate::ui::{Id, LIMsg, Model, Msg, TEMsg, YSMsg};
use crate::utils::get_pin_yin;
use anyhow::{bail, Context, Result};
use std::fs::{remove_dir_all, remove_file, rename};
use std::path::{Path, PathBuf};
use tui_realm_treeview::{Node, Tree, TreeView, TREE_CMD_CLOSE, TREE_CMD_OPEN, TREE_INITIAL_NODE};
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::event::{Key, KeyEvent, KeyModifiers, NoUserEvent};
use tuirealm::props::{Alignment, BorderType, Borders, TableBuilder, TextSpan};
use tuirealm::tui::style::Color;
use tuirealm::{AttrValue, Attribute, Component, Event, MockComponent, State, StateValue};
#[derive(MockComponent)]
pub struct MusicLibrary {
component: TreeView,
keys: Keys,
pub init: bool,
}
impl MusicLibrary {
pub fn new(tree: &Tree, initial_node: Option<String>, config: &Settings) -> Self {
let initial_node = match initial_node {
Some(id) if tree.root().query(&id).is_some() => id,
_ => tree.root().id().to_string(),
};
Self {
component: TreeView::default()
.background(
config
.style_color_symbol
.library_background()
.unwrap_or(Color::Reset),
)
.foreground(
config
.style_color_symbol
.library_foreground()
.unwrap_or(Color::Magenta),
)
.borders(
Borders::default()
.color(
config
.style_color_symbol
.library_border()
.unwrap_or(Color::Magenta),
)
.modifiers(BorderType::Rounded),
)
.indent_size(2)
.scroll_step(6)
.title(" Library ", Alignment::Left)
.highlighted_color(
config
.style_color_symbol
.library_highlight()
.unwrap_or(Color::Yellow),
)
.highlight_symbol(&config.style_color_symbol.library_highlight_symbol)
.preserve_state(true)
.with_tree(tree.clone())
.initial_node(initial_node),
keys: config.keys.clone(),
init: true,
}
}
fn handle_left_key(&mut self) -> CmdResult {
if let State::One(StateValue::String(node_id)) = self.state() {
if let Some(node) = self.component.tree().root().query(&node_id) {
if node.is_leaf() {
self.perform(Cmd::GoTo(Position::Begin));
self.perform(Cmd::Move(Direction::Up));
} else {
if self.component.tree_state().is_closed(node) {
self.perform(Cmd::GoTo(Position::Begin));
self.perform(Cmd::Move(Direction::Up));
return CmdResult::None;
}
self.perform(Cmd::Custom(TREE_CMD_CLOSE));
}
}
}
CmdResult::None
}
}
impl Component<Msg, NoUserEvent> for MusicLibrary {
#[allow(clippy::too_many_lines)]
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
if self.init {
let root = self.component.tree().root();
if self.component.tree_state().is_closed(root) {
self.perform(Cmd::Custom(TREE_CMD_OPEN));
self.init = false;
}
}
let result = match ev {
Event::Keyboard(keyevent) if keyevent == self.keys.global_left.key_event() => {
self.handle_left_key()
}
Event::Keyboard(KeyEvent {
code: Key::Left,
modifiers: KeyModifiers::NONE,
}) => self.handle_left_key(),
Event::Keyboard(KeyEvent {
code: Key::Right,
modifiers: KeyModifiers::NONE,
}) => {
let current_node = self.component.tree_state().selected().unwrap();
let p: &Path = Path::new(current_node);
if p.is_dir() {
self.perform(Cmd::Custom(TREE_CMD_OPEN))
} else {
return Some(Msg::Playlist(crate::ui::PLMsg::Add(
current_node.to_string(),
)));
}
}
Event::Keyboard(keyevent) if keyevent == self.keys.global_right.key_event() => {
let current_node = self.component.tree_state().selected().unwrap();
let p: &Path = Path::new(current_node);
if p.is_dir() {
self.perform(Cmd::Custom(TREE_CMD_OPEN))
} else {
return Some(Msg::Playlist(crate::ui::PLMsg::Add(
current_node.to_string(),
)));
}
}
Event::Keyboard(keyevent) if keyevent == self.keys.global_down.key_event() => {
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(keyevent) if keyevent == self.keys.global_up.key_event() => {
self.perform(Cmd::Move(Direction::Up))
}
Event::Keyboard(KeyEvent {
code: Key::Down,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Move(Direction::Down)),
Event::Keyboard(KeyEvent {
code: Key::Up,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Move(Direction::Up)),
Event::Keyboard(keyevent) if keyevent == self.keys.library_load_dir.key_event() => {
let current_node = self.component.tree_state().selected().unwrap();
let p: &Path = Path::new(current_node);
if p.is_dir() {
return Some(Msg::Playlist(crate::ui::PLMsg::Add(
current_node.to_string(),
)));
}
CmdResult::None
}
Event::Keyboard(KeyEvent {
code: Key::PageDown,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Scroll(Direction::Down)),
Event::Keyboard(KeyEvent {
code: Key::PageUp,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Scroll(Direction::Up)),
Event::Keyboard(keyevent) if keyevent == self.keys.global_goto_top.key_event() => {
self.perform(Cmd::GoTo(Position::Begin))
}
Event::Keyboard(keyevent) if keyevent == self.keys.global_goto_bottom.key_event() => {
self.perform(Cmd::GoTo(Position::End))
}
Event::Keyboard(KeyEvent {
code: Key::Enter,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Submit),
Event::Keyboard(KeyEvent {
code: Key::Backspace,
modifiers: KeyModifiers::NONE,
}) => return Some(Msg::Library(LIMsg::TreeGoToUpperDir)),
Event::Keyboard(
KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: Key::BackTab,
modifiers: KeyModifiers::SHIFT,
},
) => return Some(Msg::Library(LIMsg::TreeBlur)),
Event::Keyboard(keyevent) if keyevent == self.keys.library_delete.key_event() => {
return Some(Msg::DeleteConfirmShow)
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_yank.key_event() => {
return Some(Msg::Library(LIMsg::Yank))
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_paste.key_event() => {
return Some(Msg::Library(LIMsg::Paste))
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_switch_root.key_event() => {
return Some(Msg::Library(LIMsg::SwitchRoot))
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_add_root.key_event() => {
return Some(Msg::Library(LIMsg::AddRoot))
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_remove_root.key_event() => {
return Some(Msg::Library(LIMsg::RemoveRoot))
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_search.key_event() => {
return Some(Msg::GeneralSearch(crate::ui::GSMsg::PopupShowLibrary))
}
Event::Keyboard(keyevent)
if keyevent == self.keys.library_search_youtube.key_event() =>
{
return Some(Msg::YoutubeSearch(YSMsg::InputPopupShow))
}
Event::Keyboard(keyevent)
if keyevent == self.keys.library_tag_editor_open.key_event() =>
{
let current_node = self.component.tree_state().selected().unwrap();
return Some(Msg::TagEditor(TEMsg::TagEditorRun(
current_node.to_string(),
)));
}
_ => CmdResult::None,
};
match result {
CmdResult::Submit(State::One(StateValue::String(node))) => {
Some(Msg::Library(LIMsg::TreeExtendDir(node)))
}
_ => Some(Msg::None),
}
}
}
impl Model {
pub fn library_scan_dir(&mut self, p: &Path) {
self.path = p.to_path_buf();
self.tree = Tree::new(Self::library_dir_tree(p, self.config.max_depth_cli));
}
pub fn library_upper_dir(&self) -> Option<PathBuf> {
self.path.parent().map(std::path::Path::to_path_buf)
}
pub fn library_dir_tree(p: &Path, depth: usize) -> Node {
let name: String = match p.file_name() {
None => "/".to_string(),
Some(n) => n.to_string_lossy().into_owned(),
};
let mut node: Node = Node::new(p.to_string_lossy().into_owned(), name);
if depth > 0 && p.is_dir() {
if let Ok(paths) = std::fs::read_dir(p) {
let mut paths: Vec<_> = paths
.filter_map(std::result::Result::ok)
.filter(|p| !p.file_name().to_string_lossy().to_string().starts_with('.'))
.collect();
paths.sort_by_cached_key(|k| get_pin_yin(&k.file_name().to_string_lossy()));
for p in paths {
node.add_child(Self::library_dir_tree(p.path().as_path(), depth - 1));
}
}
}
node
}
pub fn library_dir_children(p: &Path) -> Vec<String> {
let mut children: Vec<String> = vec![];
if p.is_dir() {
if let Ok(paths) = std::fs::read_dir(p) {
let mut paths: Vec<_> = paths.filter_map(std::result::Result::ok).collect();
paths.sort_by_cached_key(|k| get_pin_yin(&k.file_name().to_string_lossy()));
for p in paths {
if !p.path().is_dir() {
children.push(String::from(p.path().to_string_lossy()));
}
}
}
}
children
}
pub fn library_reload_with_node_focus(&mut self, node: Option<&str>) {
self.db.sync_database(self.path.as_path());
self.database_reload();
self.library_reload_tree();
if let Some(n) = node {
assert!(self
.app
.attr(
&Id::Library,
Attribute::Custom(TREE_INITIAL_NODE),
AttrValue::String(n.to_string()),
)
.is_ok());
}
}
pub fn library_reload_tree(&mut self) {
self.tree = Tree::new(Self::library_dir_tree(
self.path.as_ref(),
self.config.max_depth_cli,
));
let current_node = match self.app.state(&Id::Library).ok().unwrap() {
State::One(StateValue::String(id)) => Some(id),
_ => None,
};
let mut focus = false;
if let Ok(f) = self.app.query(&Id::Library, Attribute::Focus) {
if Some(AttrValue::Flag(true)) == f {
focus = true;
}
}
if focus {
self.app.umount(&Id::Library).ok();
assert!(self
.app
.mount(
Id::Library,
Box::new(MusicLibrary::new(
&self.tree.clone(),
current_node,
&self.config,
),),
Vec::new()
)
.is_ok());
self.app.active(&Id::Library).ok();
return;
}
assert!(self
.app
.remount(
Id::Library,
Box::new(MusicLibrary::new(
&self.tree.clone(),
current_node,
&self.config,
),),
Vec::new()
)
.is_ok());
}
pub fn library_stepinto(&mut self, node_id: &str) {
self.library_scan_dir(PathBuf::from(node_id).as_path());
self.library_reload_tree();
}
pub fn library_stepout(&mut self) {
if let Some(p) = self.library_upper_dir() {
self.library_scan_dir(p.as_path());
self.library_reload_tree();
}
}
pub fn library_before_delete(&mut self) {
if let Ok(State::One(StateValue::String(node_id))) = self.app.state(&Id::Library) {
let p: &Path = Path::new(node_id.as_str());
if p.is_file() {
self.mount_confirm_radio();
} else {
self.mount_confirm_input();
}
}
}
pub fn library_delete_song(&mut self) -> Result<()> {
if let Ok(State::One(StateValue::String(node_id))) = self.app.state(&Id::Library) {
if let Some(mut route) = self.tree.root().route_by_node(&node_id) {
let p: &Path = Path::new(node_id.as_str());
if p.is_file() {
remove_file(p)?;
} else {
p.canonicalize()?;
remove_dir_all(p)?;
}
self.library_reload_tree();
let tree = self.tree.clone();
if let Some(new_node) = tree.root().node_by_route(&route) {
self.library_reload_with_node_focus(Some(new_node.id()));
} else {
if let Some(last) = route.last_mut() {
if last > &mut 0 {
*last -= 1;
}
}
if let Some(new_node) = tree.root().node_by_route(&route) {
self.library_reload_with_node_focus(Some(new_node.id()));
} else {
route.truncate(route.len() - 1);
if let Some(new_node) = tree.root().node_by_route(&route) {
self.library_reload_with_node_focus(Some(new_node.id()));
}
}
}
}
self.playlist_update_library_delete();
}
Ok(())
}
pub fn library_yank(&mut self) {
if let Ok(State::One(StateValue::String(node_id))) = self.app.state(&Id::Library) {
self.yanked_node_id = Some(node_id);
}
}
pub fn library_paste(&mut self) -> Result<()> {
if let Ok(State::One(StateValue::String(new_id))) = self.app.state(&Id::Library) {
let old_id = self.yanked_node_id.as_ref().context("no id yanked")?;
let p: &Path = Path::new(new_id.as_str());
let pold: &Path = Path::new(old_id.as_str());
let p_parent = p.parent().context("no parent folder found")?;
let pold_filename = pold.file_name().context("no file name found")?;
let new_node_id = if p.is_dir() {
p.join(pold_filename)
} else {
p_parent.join(pold_filename)
};
rename(pold, new_node_id.as_path())?;
self.library_reload_with_node_focus(new_node_id.to_str());
}
self.yanked_node_id = None;
self.playlist_update_library_delete();
Ok(())
}
pub fn library_update_search(&mut self, input: &str) {
let mut table: TableBuilder = TableBuilder::default();
let root = self.tree.root();
let p: &Path = Path::new(root.id());
let all_items = walkdir::WalkDir::new(p).follow_links(true);
let mut idx = 0;
let search = format!("*{}*", input.to_lowercase());
for record in all_items.into_iter().filter_map(std::result::Result::ok) {
let file_name = record.path();
if wildmatch::WildMatch::new(&search)
.matches(&file_name.to_string_lossy().to_lowercase())
{
if idx > 0 {
table.add_row();
}
idx += 1;
table
.add_col(TextSpan::new(idx.to_string()))
.add_col(TextSpan::new(file_name.to_string_lossy()));
}
}
let table = table.build();
self.general_search_update_show(table);
}
pub fn library_switch_root(&mut self) {
let mut vec = Vec::new();
for dir in &self.config.music_dir {
let absolute_dir = shellexpand::tilde(dir).to_string();
vec.push(absolute_dir);
}
if let Some(dir) = &self.config.music_dir_from_cli {
let absolute_dir = shellexpand::tilde(&dir).to_string();
vec.push(absolute_dir);
}
if vec.is_empty() {
return;
}
let mut index = 0;
let current_path_str = self.path.to_string_lossy().to_string();
for (idx, dir) in vec.iter().enumerate() {
if current_path_str == *dir {
index = idx + 1;
break;
}
}
if index > vec.len() - 1 {
index = 0;
}
if let Some(dir) = vec.get(index) {
let pathbuf = PathBuf::from(dir);
self.library_scan_dir(pathbuf.as_path());
self.library_reload_with_node_focus(None);
}
}
pub fn library_add_root(&mut self) {
let current_path_string = self.path.to_string_lossy().to_string();
for dir in &self.config.music_dir {
let absolute_dir = shellexpand::tilde(dir).to_string();
if absolute_dir == current_path_string {
return;
}
}
self.config.music_dir.push(current_path_string);
}
pub fn library_remove_root(&mut self) -> Result<()> {
let current_path_string = self.path.to_string_lossy().to_string();
let mut vec = Vec::new();
for dir in &self.config.music_dir {
let absolute_dir = shellexpand::tilde(dir).to_string();
if absolute_dir == current_path_string {
continue;
}
vec.push(dir.clone());
}
if vec.is_empty() {
bail!("At least 1 root music directory should be kept");
}
self.config.music_dir = vec;
self.library_switch_root();
Ok(())
}
}