use crate::config::{Keys, Settings};
use crate::sqlite::SearchCriteria;
use crate::ui::{DBMsg, Id, Model, Msg};
use crate::utils::{is_playlist, playlist_get_vec};
use std::path::Path;
use tui_realm_stdlib::List;
use tuirealm::command::{Cmd, CmdResult, Direction, Position};
use tuirealm::props::{Alignment, BorderType, TableBuilder, TextSpan};
use tuirealm::props::{Borders, Color};
use tuirealm::{
event::{Key, KeyEvent, KeyModifiers, NoUserEvent},
AttrValue, Attribute, Component, Event, MockComponent, State, StateValue,
};
#[derive(MockComponent)]
pub struct DBListCriteria {
component: List,
on_key_tab: Msg,
on_key_backtab: Msg,
keys: Keys,
}
impl DBListCriteria {
pub fn new(config: &Settings, on_key_tab: Msg, on_key_backtab: Msg) -> Self {
Self {
component: List::default()
.borders(
Borders::default().modifiers(BorderType::Rounded).color(
config
.style_color_symbol
.library_border()
.unwrap_or(Color::Blue),
),
)
.background(
config
.style_color_symbol
.library_background()
.unwrap_or(Color::Reset),
)
.foreground(
config
.style_color_symbol
.library_foreground()
.unwrap_or(Color::Yellow),
)
.title(" DataBase ", Alignment::Left)
.scroll(true)
.highlighted_color(
config
.style_color_symbol
.library_highlight()
.unwrap_or(Color::LightBlue),
)
.highlighted_str(&config.style_color_symbol.library_highlight_symbol)
.rewind(false)
.step(4)
.scroll(true)
.rows(
TableBuilder::default()
.add_col(TextSpan::from("Artist"))
.add_row()
.add_col(TextSpan::from("Album"))
.add_row()
.add_col(TextSpan::from("Genre"))
.add_row()
.add_col(TextSpan::from("Directory"))
.add_row()
.add_col(TextSpan::from("Playlists"))
.build(),
),
on_key_tab,
on_key_backtab,
keys: config.keys.clone(),
}
}
}
impl Component<Msg, NoUserEvent> for DBListCriteria {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let _cmd_result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Down,
modifiers: KeyModifiers::NONE,
}) => {
if let Some(AttrValue::Table(t)) = self.query(Attribute::Content) {
if let State::One(StateValue::Usize(index)) = self.state() {
if index >= t.len() - 1 {
return Some(self.on_key_tab.clone());
}
}
}
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(KeyEvent {
code: Key::Up,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Move(Direction::Up)),
Event::Keyboard(key) if key == self.keys.global_down.key_event() => {
if let Some(AttrValue::Table(t)) = self.query(Attribute::Content) {
if let State::One(StateValue::Usize(index)) = self.state() {
if index >= t.len() - 1 {
return Some(self.on_key_tab.clone());
}
}
}
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(key) if key == self.keys.global_up.key_event() => {
self.perform(Cmd::Move(Direction::Up))
}
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(key) if key == self.keys.global_goto_top.key_event() => {
self.perform(Cmd::GoTo(Position::Begin))
}
Event::Keyboard(key) if key == self.keys.global_goto_bottom.key_event() => {
self.perform(Cmd::GoTo(Position::End))
}
Event::Keyboard(KeyEvent {
code: Key::Home,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::Begin)),
Event::Keyboard(KeyEvent {
code: Key::Enter,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::DataBase(DBMsg::SearchResult(index)));
}
CmdResult::None
}
Event::Keyboard(KeyEvent {
code: Key::End,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::End)),
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => return Some(self.on_key_tab.clone()),
Event::Keyboard(KeyEvent {
code: Key::BackTab,
modifiers: KeyModifiers::SHIFT,
}) => return Some(self.on_key_backtab.clone()),
Event::Keyboard(keyevent) if keyevent == self.keys.library_search.key_event() => {
return Some(Msg::GeneralSearch(crate::ui::GSMsg::PopupShowDatabase))
}
_ => CmdResult::None,
};
Some(Msg::None)
}
}
#[derive(MockComponent)]
pub struct DBListSearchResult {
component: List,
on_key_tab: Msg,
on_key_backtab: Msg,
keys: Keys,
}
impl DBListSearchResult {
pub fn new(config: &Settings, on_key_tab: Msg, on_key_backtab: Msg) -> Self {
Self {
component: List::default()
.borders(
Borders::default().modifiers(BorderType::Rounded).color(
config
.style_color_symbol
.library_border()
.unwrap_or(Color::Blue),
),
)
.background(
config
.style_color_symbol
.library_background()
.unwrap_or(Color::Reset),
)
.foreground(
config
.style_color_symbol
.library_foreground()
.unwrap_or(Color::Yellow),
)
.title(" Result ", Alignment::Left)
.scroll(true)
.highlighted_color(
config
.style_color_symbol
.library_highlight()
.unwrap_or(Color::LightBlue),
)
.highlighted_str(&config.style_color_symbol.library_highlight_symbol)
.rewind(false)
.step(4)
.scroll(true)
.rows(
TableBuilder::default()
.add_col(TextSpan::from("Empty"))
.build(),
),
on_key_tab,
on_key_backtab,
keys: config.keys.clone(),
}
}
}
impl Component<Msg, NoUserEvent> for DBListSearchResult {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let _cmd_result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Down,
modifiers: KeyModifiers::NONE,
}) => {
if let Some(AttrValue::Table(t)) = self.query(Attribute::Content) {
if let State::One(StateValue::Usize(index)) = self.state() {
if index >= t.len() - 1 {
return Some(self.on_key_tab.clone());
}
}
}
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(KeyEvent {
code: Key::Up,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
if index == 0 {
return Some(self.on_key_backtab.clone());
}
}
self.perform(Cmd::Move(Direction::Up))
}
Event::Keyboard(key) if key == self.keys.global_down.key_event() => {
if let Some(AttrValue::Table(t)) = self.query(Attribute::Content) {
if let State::One(StateValue::Usize(index)) = self.state() {
if index >= t.len() - 1 {
return Some(self.on_key_tab.clone());
}
}
}
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(key) if key == self.keys.global_up.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
if index == 0 {
return Some(self.on_key_backtab.clone());
}
}
self.perform(Cmd::Move(Direction::Up))
}
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(key) if key == self.keys.global_goto_top.key_event() => {
self.perform(Cmd::GoTo(Position::Begin))
}
Event::Keyboard(key) if key == self.keys.global_goto_bottom.key_event() => {
self.perform(Cmd::GoTo(Position::End))
}
Event::Keyboard(KeyEvent {
code: Key::Home,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::Begin)),
Event::Keyboard(KeyEvent {
code: Key::End,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::End)),
Event::Keyboard(KeyEvent {
code: Key::Enter,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::DataBase(DBMsg::SearchTrack(index)));
}
CmdResult::None
}
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => return Some(self.on_key_tab.clone()),
Event::Keyboard(KeyEvent {
code: Key::BackTab,
modifiers: KeyModifiers::SHIFT,
}) => return Some(self.on_key_backtab.clone()),
Event::Keyboard(keyevent) if keyevent == self.keys.library_search.key_event() => {
return Some(Msg::GeneralSearch(crate::ui::GSMsg::PopupShowDatabase))
}
_ => CmdResult::None,
};
Some(Msg::None)
}
}
#[derive(MockComponent)]
pub struct DBListSearchTracks {
component: List,
on_key_tab: Msg,
on_key_backtab: Msg,
keys: Keys,
}
impl DBListSearchTracks {
pub fn new(config: &Settings, on_key_tab: Msg, on_key_backtab: Msg) -> Self {
Self {
component: List::default()
.borders(
Borders::default().modifiers(BorderType::Rounded).color(
config
.style_color_symbol
.library_border()
.unwrap_or(Color::Blue),
),
)
.background(
config
.style_color_symbol
.library_background()
.unwrap_or(Color::Reset),
)
.foreground(
config
.style_color_symbol
.library_foreground()
.unwrap_or(Color::Yellow),
)
.title(" Tracks ", Alignment::Left)
.scroll(true)
.highlighted_color(
config
.style_color_symbol
.library_highlight()
.unwrap_or(Color::LightBlue),
)
.highlighted_str(&config.style_color_symbol.library_highlight_symbol)
.rewind(false)
.step(4)
.scroll(true)
.rows(
TableBuilder::default()
.add_col(TextSpan::from("Empty"))
.build(),
),
on_key_tab,
on_key_backtab,
keys: config.keys.clone(),
}
}
}
impl Component<Msg, NoUserEvent> for DBListSearchTracks {
fn on(&mut self, ev: Event<NoUserEvent>) -> Option<Msg> {
let _cmd_result = match ev {
Event::Keyboard(KeyEvent {
code: Key::Down,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::Move(Direction::Down)),
Event::Keyboard(KeyEvent {
code: Key::Up,
modifiers: KeyModifiers::NONE,
}) => {
if let State::One(StateValue::Usize(index)) = self.state() {
if index == 0 {
return Some(self.on_key_backtab.clone());
}
}
self.perform(Cmd::Move(Direction::Up))
}
Event::Keyboard(key) if key == self.keys.global_down.key_event() => {
self.perform(Cmd::Move(Direction::Down))
}
Event::Keyboard(key) if key == self.keys.global_up.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
if index == 0 {
return Some(self.on_key_backtab.clone());
}
}
self.perform(Cmd::Move(Direction::Up))
}
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(key) if key == self.keys.global_goto_top.key_event() => {
self.perform(Cmd::GoTo(Position::Begin))
}
Event::Keyboard(key) if key == self.keys.global_goto_bottom.key_event() => {
self.perform(Cmd::GoTo(Position::End))
}
Event::Keyboard(KeyEvent {
code: Key::Home,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::Begin)),
Event::Keyboard(KeyEvent {
code: Key::End,
modifiers: KeyModifiers::NONE,
}) => self.perform(Cmd::GoTo(Position::End)),
Event::Keyboard(KeyEvent {
code: Key::Tab,
modifiers: KeyModifiers::NONE,
}) => return Some(self.on_key_tab.clone()),
Event::Keyboard(KeyEvent {
code: Key::BackTab,
modifiers: KeyModifiers::SHIFT,
}) => return Some(self.on_key_backtab.clone()),
Event::Keyboard(keyevent) if keyevent == self.keys.global_right.key_event() => {
if let State::One(StateValue::Usize(index)) = self.state() {
return Some(Msg::DataBase(DBMsg::AddPlaylist(index)));
}
CmdResult::None
}
Event::Keyboard(keyevent) if keyevent == self.keys.database_add_all.key_event() => {
return Some(Msg::DataBase(DBMsg::AddAllToPlaylist))
}
Event::Keyboard(keyevent) if keyevent == self.keys.library_search.key_event() => {
return Some(Msg::GeneralSearch(crate::ui::GSMsg::PopupShowDatabase))
}
_ => CmdResult::None,
};
Some(Msg::None)
}
}
impl Model {
pub fn database_sync_tracks(&mut self) {
let mut table: TableBuilder = TableBuilder::default();
for (idx, record) in self.db_search_tracks.iter().enumerate() {
if idx > 0 {
table.add_row();
}
table
.add_col(TextSpan::from(format!("{}", idx + 1)))
.add_col(TextSpan::from(" "))
.add_col(TextSpan::from(record.name.to_string()));
}
if self.db_search_results.is_empty() {
table.add_col(TextSpan::from("empty results"));
}
let table = table.build();
self.app
.attr(
&Id::DBListSearchTracks,
tuirealm::Attribute::Content,
tuirealm::AttrValue::Table(table),
)
.ok();
}
pub fn database_sync_results(&mut self) {
let mut table: TableBuilder = TableBuilder::default();
let mut index = 0;
for (idx, record) in self.db_search_results.iter().enumerate() {
let mut display_name = String::new();
match self.db_criteria {
SearchCriteria::Playlist => {
let path = Path::new(record);
let path_string = path.to_string_lossy().to_string();
let mut vec = path_string.split('/');
if let Some(v1) = vec.next_back() {
if let Some(v2) = vec.next_back() {
display_name = format!("{v2}/{v1}");
}
}
}
SearchCriteria::Directory => {
let path = Path::new(record);
let path_string = path.to_string_lossy().to_string();
let mut vec = path_string.split('/');
if let Some(v1) = vec.next_back() {
if let Some(v2) = vec.next_back() {
display_name = format!("{v2}/{v1}/");
}
}
}
_ => {
display_name = record.clone();
}
};
if !display_name.is_empty() {
if idx > 0 {
table.add_row();
}
index += 1;
table
.add_col(TextSpan::from(format!("{index}")))
.add_col(TextSpan::from(" "))
.add_col(TextSpan::from(display_name));
}
}
if self.db_search_results.is_empty() {
table.add_col(TextSpan::from("empty results"));
}
let table = table.build();
self.app
.attr(
&Id::DBListSearchResult,
tuirealm::Attribute::Content,
tuirealm::AttrValue::Table(table),
)
.ok();
}
pub fn database_update_search_results(&mut self) {
match self.db_criteria {
SearchCriteria::Playlist => {
self.db_search_results = self.database_get_playlist();
}
_ => self.db_search_results = self.db.get_criterias(&self.db_criteria),
}
self.database_sync_results();
self.app.active(&Id::DBListSearchResult).ok();
}
fn database_get_playlist(&self) -> Vec<String> {
let mut vec = Vec::new();
let root = self.tree.root();
let p: &Path = Path::new(root.id());
let all_items = walkdir::WalkDir::new(p).follow_links(true);
for record in all_items
.into_iter()
.filter_map(std::result::Result::ok)
.filter(|p| is_playlist(&p.path().to_string_lossy()))
{
let full_path_name = record.path().to_string_lossy().to_string();
vec.push(full_path_name);
}
vec
}
pub fn database_update_search_tracks(&mut self, index: usize) {
match self.db_criteria {
SearchCriteria::Playlist => {
if let Some(result) = self.db_search_results.get(index) {
if let Ok(vec) = playlist_get_vec(result) {
let mut vec_db = Vec::new();
for item in vec {
if let Ok(i) = self.db.get_record_by_path(&item) {
vec_db.push(i);
}
}
self.db_search_tracks = vec_db;
}
}
}
_ => {
if let Ok(vec) = self
.db
.get_record_by_criteria(&self.db_search_results[index], &self.db_criteria)
{
self.db_search_tracks = vec;
};
}
}
self.database_sync_tracks();
self.app.active(&Id::DBListSearchTracks).ok();
}
#[allow(unused)]
pub fn database_reload(&mut self) {
assert!(self
.app
.remount(
Id::DBListCriteria,
Box::new(DBListCriteria::new(
&self.config,
Msg::DataBase(DBMsg::CriteriaBlurDown),
Msg::DataBase(DBMsg::CriteriaBlurUp)
)),
Vec::new()
)
.is_ok());
assert!(self
.app
.remount(
Id::DBListSearchResult,
Box::new(DBListSearchResult::new(
&self.config,
Msg::DataBase(DBMsg::SearchResultBlurDown),
Msg::DataBase(DBMsg::SearchResultBlurUp)
)),
Vec::new()
)
.is_ok());
assert!(self
.app
.remount(
Id::DBListSearchTracks,
Box::new(DBListSearchTracks::new(
&self.config,
Msg::DataBase(DBMsg::SearchTracksBlurDown),
Msg::DataBase(DBMsg::SearchTracksBlurUp)
)),
Vec::new()
)
.is_ok());
self.db_search_results = vec![];
self.db_search_tracks = vec![];
self.database_sync_tracks();
self.database_sync_results();
}
pub fn database_update_search(&mut self, input: &str) {
let mut table: TableBuilder = TableBuilder::default();
let mut idx = 0;
let search = format!("*{}*", input.to_lowercase());
let mut db_tracks = vec![];
if let Ok(tracks) = self.db.get_all_records() {
db_tracks = tracks.clone();
for record in tracks {
if wildmatch::WildMatch::new(&search).matches(&record.artist.to_lowercase())
| wildmatch::WildMatch::new(&search).matches(&record.title.to_lowercase())
{
if idx > 0 {
table.add_row();
}
let duration = crate::track::Track::duration_formatted_short(&record.duration);
let duration_string = format!("[{duration:^6.6}]");
table
.add_col(TextSpan::new(duration_string.as_str()))
.add_col(
TextSpan::new(record.artist)
.fg(tuirealm::tui::style::Color::LightYellow),
)
.add_col(TextSpan::new(record.title).bold())
.add_col(TextSpan::new(record.file));
idx += 1;
}
}
}
if db_tracks.is_empty() {
table.add_col(TextSpan::from("0"));
table.add_col(TextSpan::from("empty tracks from db"));
table.add_col(TextSpan::from(""));
}
let table = table.build();
self.general_search_update_show(table);
}
}