334 lines
10 KiB
Rust
334 lines
10 KiB
Rust
use crate::modules::source::{FileDatabase, Word, WordSource};
|
|
use crate::modules::ui::app::UiState::FILTERLANG;
|
|
use crate::modules::ui::handler::handle_user_input;
|
|
use crossterm::event::KeyEvent;
|
|
use ratatui::{
|
|
DefaultTerminal, Frame,
|
|
layout::{Constraint, Direction, Flex, Layout},
|
|
style::{Color, Style},
|
|
text::Text,
|
|
widgets::{Block, Borders, List, ListDirection, ListItem, ListState, Padding, Paragraph, Wrap},
|
|
};
|
|
use std::io;
|
|
|
|
#[derive(Copy, Clone, PartialEq)]
|
|
pub enum UiState {
|
|
LIST,
|
|
FILTERWORD,
|
|
FILTERLANG,
|
|
EDIT,
|
|
ADD,
|
|
DELETE,
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq)]
|
|
pub enum AddState {
|
|
KEY,
|
|
DESCRIPTION,
|
|
LANG,
|
|
}
|
|
|
|
pub struct Preferences {
|
|
help: bool,
|
|
}
|
|
|
|
pub struct App {
|
|
pub state: UiState,
|
|
pub file_database: FileDatabase,
|
|
pub list_state: ListState,
|
|
pub add_state: AddState,
|
|
pub description_scroll: u16,
|
|
pub word_list: Vec<Word>,
|
|
pub word_to_add: Option<Word>,
|
|
pub word_to_edit: Option<Word>,
|
|
pub word_filter: Option<String>,
|
|
pub lang_filter: Option<String>,
|
|
pub last_key: Option<KeyEvent>,
|
|
pub exit: bool,
|
|
}
|
|
|
|
impl App {
|
|
pub fn new(db: FileDatabase) -> Self {
|
|
let wl = db.get_all_words();
|
|
|
|
Self {
|
|
state: UiState::LIST,
|
|
file_database: db,
|
|
list_state: ListState::default().with_selected(Some(0)),
|
|
add_state: AddState::KEY,
|
|
description_scroll: 0,
|
|
word_list: wl,
|
|
word_to_add: None,
|
|
word_to_edit: None,
|
|
word_filter: None,
|
|
lang_filter: None,
|
|
last_key: None,
|
|
exit: false,
|
|
}
|
|
}
|
|
|
|
pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> {
|
|
while !self.exit {
|
|
terminal.draw(|frame| self.draw(frame))?;
|
|
let event_result = self.handle_events();
|
|
if let Err(_) = event_result {
|
|
break;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
fn draw(&mut self, frame: &mut Frame) {
|
|
let main_layout = Layout::default()
|
|
.direction(Direction::Vertical)
|
|
.spacing(0)
|
|
.margin(1)
|
|
.constraints(vec![
|
|
Constraint::Length(3),
|
|
Constraint::Fill(1),
|
|
Constraint::Length(1),
|
|
])
|
|
.split(frame.area());
|
|
|
|
/*
|
|
* Filters block
|
|
* */
|
|
let filters_layout = Layout::default()
|
|
.direction(Direction::Horizontal)
|
|
.spacing(1)
|
|
.constraints(vec![Constraint::Fill(1), Constraint::Length(8)])
|
|
.split(main_layout[0]);
|
|
|
|
let word_filter: String = {
|
|
if let Some(l) = &self.word_filter {
|
|
l.to_string()
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
};
|
|
frame.render_widget(
|
|
Paragraph::new(word_filter.clone()).block(
|
|
Block::new()
|
|
.title(self.format_ui_block_title("Word", UiState::FILTERWORD))
|
|
.borders(Borders::ALL),
|
|
),
|
|
filters_layout[0],
|
|
);
|
|
|
|
let lang_filter: String = {
|
|
if let Some(l) = &self.lang_filter {
|
|
l.to_string()
|
|
} else {
|
|
"".to_string()
|
|
}
|
|
};
|
|
frame.render_widget(
|
|
Paragraph::new(lang_filter).block(
|
|
Block::new()
|
|
.title(self.format_ui_block_title("Lang", UiState::FILTERLANG))
|
|
.borders(Borders::ALL),
|
|
),
|
|
filters_layout[1],
|
|
);
|
|
|
|
/*
|
|
* Word list / add / edit block
|
|
* */
|
|
let scroll_position: usize = {
|
|
if let Some(i) = self.list_state.selected() {
|
|
i
|
|
} else {
|
|
0
|
|
}
|
|
};
|
|
// PERF: do not clone
|
|
let selected_word = self.word_list.clone().into_iter().nth(scroll_position);
|
|
if self.state == UiState::ADD || self.state == UiState::EDIT {
|
|
let add_words_heading_layout =
|
|
Layout::horizontal(vec![Constraint::Fill(1), Constraint::Length(8)])
|
|
.spacing(1)
|
|
.split(main_layout[0]);
|
|
|
|
// PERF: maybe not clone
|
|
let word_to_add = {
|
|
if self.state == UiState::EDIT
|
|
&& let Some(w) = self.word_to_edit.clone()
|
|
{
|
|
w
|
|
} else {
|
|
self.word_to_add.clone().unwrap_or(Word {
|
|
key: "".to_string(),
|
|
description: "".to_string(),
|
|
lang: "".to_string(),
|
|
})
|
|
}
|
|
};
|
|
frame.render_widget(
|
|
Paragraph::new(word_to_add.key).block(
|
|
Block::new()
|
|
.title(self.format_add_block_title("Word", AddState::KEY))
|
|
.borders(Borders::ALL),
|
|
),
|
|
add_words_heading_layout[0],
|
|
);
|
|
frame.render_widget(
|
|
Paragraph::new(word_to_add.lang).block(
|
|
Block::new()
|
|
.title(self.format_add_block_title("Lang", AddState::LANG))
|
|
.borders(Borders::ALL),
|
|
),
|
|
add_words_heading_layout[1],
|
|
);
|
|
frame.render_widget(
|
|
Paragraph::new(word_to_add.description).block(
|
|
Block::new()
|
|
.title(self.format_add_block_title("Description", AddState::DESCRIPTION))
|
|
.borders(Borders::ALL),
|
|
),
|
|
main_layout[1],
|
|
);
|
|
} else {
|
|
let highligh_symbol: &str = {
|
|
if self.state == UiState::LIST {
|
|
"> "
|
|
} else if self.state == UiState::DELETE {
|
|
"x "
|
|
} else {
|
|
" "
|
|
}
|
|
};
|
|
|
|
let highligh_color: Color = {
|
|
if self.state == UiState::LIST {
|
|
Color::Yellow
|
|
} else if self.state == UiState::DELETE {
|
|
Color::LightRed
|
|
} else {
|
|
Color::default()
|
|
}
|
|
};
|
|
|
|
let words_layout =
|
|
Layout::horizontal(vec![Constraint::Percentage(30), Constraint::Percentage(70)])
|
|
.split(main_layout[1]);
|
|
let word_key_list_items: Vec<ListItem> = self
|
|
.word_list
|
|
.clone()
|
|
.iter()
|
|
.map(|w| ListItem::new(w.key.clone()))
|
|
.collect();
|
|
|
|
let word_key_list = List::new(word_key_list_items)
|
|
.direction(ListDirection::TopToBottom)
|
|
.block(
|
|
Block::default()
|
|
.title(self.format_ui_block_title("Entries", UiState::LIST))
|
|
.borders(Borders::ALL)
|
|
.padding(Padding::horizontal(1)),
|
|
)
|
|
.highlight_style(Style::default().fg(highligh_color))
|
|
.highlight_symbol(highligh_symbol)
|
|
.style(Style::default().fg(Color::DarkGray));
|
|
frame.render_stateful_widget(word_key_list, words_layout[0], &mut self.list_state);
|
|
|
|
if let Some(w) = selected_word.clone() {
|
|
frame.render_widget(
|
|
Paragraph::new(w.description)
|
|
.wrap(Wrap { trim: true })
|
|
.scroll((self.description_scroll, 0))
|
|
.block(Block::new().borders(Borders::ALL)),
|
|
words_layout[1],
|
|
);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Help block
|
|
* */
|
|
let help_layout = Layout::horizontal(vec![Constraint::Fill(1)])
|
|
.flex(Flex::Center)
|
|
.split(main_layout[2]);
|
|
|
|
let help_text = {
|
|
if self.state == UiState::DELETE
|
|
&& let Some(s_w) = selected_word.clone()
|
|
{
|
|
Text::raw(format!("Sure you want to delete '{0}'? (y)es", s_w.key))
|
|
.style(Style::new().fg(Color::LightRed))
|
|
} else {
|
|
Text::raw("(f)ilter | (l)ang | (d)elete | (e)dit | (a)dd | (q)uit ")
|
|
.style(Style::new().fg(Color::DarkGray))
|
|
}
|
|
};
|
|
|
|
let help_area = help_layout[0].centered(
|
|
Constraint::Length(help_text.width() as u16),
|
|
Constraint::Length(1),
|
|
);
|
|
|
|
frame.render_widget(help_text, help_area);
|
|
|
|
/*
|
|
* State block
|
|
* */
|
|
// let state_layout = Layout::horizontal(vec![Constraint::Fill(1)])
|
|
// .flex(Flex::Center)
|
|
// .split(main_layout[2]);
|
|
// frame.render_widget(help_text, help_area);
|
|
}
|
|
|
|
pub fn update_word_list(&mut self) {
|
|
let word_list = {
|
|
if let Some(w_filter) = &self.word_filter
|
|
&& let Some(l_filter) = &self.lang_filter
|
|
{
|
|
self.file_database
|
|
.find_by_word_and_lang(&w_filter, &l_filter)
|
|
} else if let Some(w_filter) = &self.word_filter {
|
|
self.file_database.find_by_word(&w_filter)
|
|
} else if let Some(l_filter) = &self.lang_filter {
|
|
self.file_database.find_by_lang(&l_filter)
|
|
} else {
|
|
self.file_database.get_all_words()
|
|
}
|
|
};
|
|
self.description_scroll = 0;
|
|
self.word_list = word_list;
|
|
}
|
|
|
|
fn format_ui_block_title(&self, input: &str, matching_state: UiState) -> String {
|
|
if matching_state == self.state {
|
|
let mut result = input.to_string();
|
|
result.push_str(" ● ");
|
|
result
|
|
} else {
|
|
input.to_string()
|
|
}
|
|
}
|
|
|
|
fn format_add_block_title(&self, input: &str, matching_state: AddState) -> String {
|
|
if matching_state == self.add_state {
|
|
let mut result = input.to_string();
|
|
result.push_str(" ● ");
|
|
result
|
|
} else {
|
|
input.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn get_selected_word(&self) -> Option<Word> {
|
|
let scroll_position: usize = {
|
|
if let Some(i) = self.list_state.selected() {
|
|
i
|
|
} else {
|
|
0
|
|
}
|
|
};
|
|
self.word_list.clone().into_iter().nth(scroll_position)
|
|
}
|
|
|
|
fn handle_events(&mut self) -> io::Result<()> {
|
|
return handle_user_input(self);
|
|
}
|
|
}
|