Add validation to textarea
This commit is contained in:
@@ -115,6 +115,7 @@ impl FolderAddCommand {
|
|||||||
.path_config
|
.path_config
|
||||||
.dlsite_paths
|
.dlsite_paths
|
||||||
.push(abs_path.to_str().unwrap().to_string());
|
.push(abs_path.to_str().unwrap().to_string());
|
||||||
config.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())
|
config.save()?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ impl ApplicationConfig {
|
|||||||
conf
|
conf
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_to_file(self, path: &PathBuf) -> Result<()> {
|
fn write_to_file(self, path: &PathBuf) -> Result<()> {
|
||||||
let mut conf = Ini::new();
|
let mut conf = Ini::new();
|
||||||
conf.with_section(Some("Basic"))
|
conf.with_section(Some("Basic"))
|
||||||
.set("DBPath", self.basic_config.db_path)
|
.set("DBPath", self.basic_config.db_path)
|
||||||
@@ -67,4 +67,8 @@ impl ApplicationConfig {
|
|||||||
conf.write_to_file(path)?;
|
conf.write_to_file(path)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn save(&self) -> Result<()> {
|
||||||
|
self.clone().write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
|
||||||
use rat_cursor::HasScreenCursor;
|
use rat_cursor::HasScreenCursor;
|
||||||
@@ -12,13 +13,20 @@ use tui_input::Input;
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct TextArea {
|
pub struct TextArea {
|
||||||
input: Input,
|
|
||||||
title: String,
|
title: String,
|
||||||
style: TextAreaStyle,
|
style: TextAreaStyle,
|
||||||
|
auto_scroll: bool,
|
||||||
|
validate_fn: Arc<dyn Fn(&str) -> bool>,
|
||||||
|
pub state: TextAreaState,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct TextAreaState {
|
||||||
|
pub is_active: bool,
|
||||||
input_area: Option<Rect>,
|
input_area: Option<Rect>,
|
||||||
scroll_offset: u16,
|
scroll_offset: u16,
|
||||||
auto_scroll: bool,
|
input: Input,
|
||||||
pub active: bool,
|
is_valid: bool
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -28,7 +36,7 @@ pub enum TextAreaStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl StatefulWidget for TextArea {
|
impl StatefulWidget for TextArea {
|
||||||
type State = TextArea;
|
type State = TextAreaState;
|
||||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||||
where
|
where
|
||||||
Self: Sized,
|
Self: Sized,
|
||||||
@@ -48,20 +56,16 @@ impl StatefulWidget for TextArea {
|
|||||||
let label_text = Text::from(self.title);
|
let label_text = Text::from(self.title);
|
||||||
let label = Paragraph::new(label_text);
|
let label = Paragraph::new(label_text);
|
||||||
|
|
||||||
// let scroll_offset = if self.input.cursor() > chunks[1].width as usize {
|
|
||||||
// self.input.cursor() - chunks[1].width as usize
|
|
||||||
// } else { 0 };
|
|
||||||
|
|
||||||
let input_text = Span::from(input_value.clone()).fg(Color::White);
|
let input_text = Span::from(input_value.clone()).fg(Color::White);
|
||||||
let input_line = Line::from(input_text);
|
let input_line = Line::from(input_text);
|
||||||
let paragraph = Paragraph::new(input_line)
|
let paragraph = Paragraph::new(input_line)
|
||||||
.bg(Color::Green)
|
.bg(if state.is_valid { Color::Green } else { Color::Red })
|
||||||
.scroll((0, self.scroll_offset));
|
.scroll((0, state.scroll_offset));
|
||||||
|
|
||||||
if self.input_area.is_none() {
|
if state.input_area.is_none() {
|
||||||
state.input_area = Some(chunks[1]);
|
state.input_area = Some(chunks[1]);
|
||||||
}
|
}
|
||||||
else if let Some(area) = self.input_area && area != chunks[1] {
|
else if let Some(area) = self.state.input_area && area != chunks[1] {
|
||||||
state.input_area = Some(chunks[1]);
|
state.input_area = Some(chunks[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,48 +77,72 @@ impl StatefulWidget for TextArea {
|
|||||||
|
|
||||||
impl HasScreenCursor for TextArea {
|
impl HasScreenCursor for TextArea {
|
||||||
fn screen_cursor(&self) -> Option<(u16, u16)> {
|
fn screen_cursor(&self) -> Option<(u16, u16)> {
|
||||||
if self.input_area.is_none() {
|
if self.state.input_area.is_none() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let area = self.input_area.unwrap();
|
let area = self.state.input_area.unwrap();
|
||||||
let scroll = self.input.visual_scroll(1);
|
let scroll = self.state.input.visual_scroll(1);
|
||||||
let x = self.input.visual_cursor().max(scroll) as u16 - self.scroll_offset;
|
let x = self.state.input.visual_cursor().max(scroll) as u16 - self.state.scroll_offset;
|
||||||
Some((area.x + x, area.y))
|
Some((area.x + x, area.y))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextArea {
|
impl TextArea {
|
||||||
pub fn new(title: &str, placeholder_text: &str, style: Option<TextAreaStyle>, auto_scroll: Option<bool>) -> Self {
|
pub fn new(title: &str,
|
||||||
|
placeholder_text: &str,
|
||||||
|
validate_fn: fn(&str) -> bool,
|
||||||
|
style: Option<TextAreaStyle>,
|
||||||
|
auto_scroll: Option<bool>,
|
||||||
|
)
|
||||||
|
-> Self
|
||||||
|
{
|
||||||
|
let func = Arc::new(validate_fn);
|
||||||
Self {
|
Self {
|
||||||
input: Input::new(placeholder_text.to_string()),
|
|
||||||
title: title.to_string(),
|
title: title.to_string(),
|
||||||
active: false,
|
|
||||||
style: style.unwrap_or(TextAreaStyle::SingleLine),
|
style: style.unwrap_or(TextAreaStyle::SingleLine),
|
||||||
input_area: None,
|
|
||||||
auto_scroll: auto_scroll.unwrap_or(true),
|
auto_scroll: auto_scroll.unwrap_or(true),
|
||||||
scroll_offset: 0
|
validate_fn: func,
|
||||||
|
state: TextAreaState {
|
||||||
|
input: Input::new(placeholder_text.to_string()),
|
||||||
|
is_active: false,
|
||||||
|
input_area: None,
|
||||||
|
scroll_offset: 0,
|
||||||
|
is_valid: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input(&mut self, event: &Event) -> Result<()> {
|
pub fn handle_input(&mut self, event: &Event) -> Result<()> {
|
||||||
let _ = self.input.handle_event(event);
|
let _ = self.state.input.handle_event(event);
|
||||||
|
self.state.is_valid = (self.validate_fn)(self.state.input.value());
|
||||||
if let Event::Key(key) = event &&
|
if let Event::Key(key) = event &&
|
||||||
!matches!(key.kind, KeyEventKind::Release) &&
|
!matches!(key.kind, KeyEventKind::Release) &&
|
||||||
let Some(area) = self.input_area {
|
let Some(area) = self.state.input_area
|
||||||
|
{
|
||||||
let cursor_pos = self.input.cursor() as u16;
|
let scroll_offset = self.state.scroll_offset;
|
||||||
if self.scroll_offset > cursor_pos {
|
let cursor_pos = self.state.input.cursor() as u16;
|
||||||
self.scroll_offset = cursor_pos;
|
if scroll_offset> cursor_pos {
|
||||||
} else if cursor_pos >= area.width + self.scroll_offset {
|
self.state.scroll_offset = cursor_pos;
|
||||||
self.scroll_offset = cursor_pos - area.width;
|
} else if cursor_pos >= area.width + scroll_offset {
|
||||||
} else if self.auto_scroll && self.scroll_offset > 0 && (key.code.is_delete() || key.code.is_backspace()) {
|
self.state.scroll_offset = cursor_pos - area.width;
|
||||||
self.scroll_offset -= 1;
|
} else if self.auto_scroll && scroll_offset > 0 && key.code.is_delete() {
|
||||||
|
self.state.scroll_offset -= 1;
|
||||||
// HACK: with_cursor function requires to be owned so use handle event
|
// HACK: with_cursor function requires to be owned so use handle event
|
||||||
let _ = self.input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty())));
|
let _ = self.state.input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty())));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_value(&self) -> Option<String> {
|
||||||
|
if self.state.is_valid {
|
||||||
|
return Some(self.state.input.value().to_string());
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn reset_value(&mut self) -> Result<()> {
|
||||||
|
Ok(self.state.input.reset())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::path::Path;
|
||||||
use crate::widgets::components::TextArea;
|
use crate::widgets::components::TextArea;
|
||||||
use ratatui::buffer::Buffer;
|
use ratatui::buffer::Buffer;
|
||||||
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
|
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
|
||||||
@@ -11,10 +12,30 @@ pub struct AddFolderPopup {
|
|||||||
|
|
||||||
impl AddFolderPopup {
|
impl AddFolderPopup {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut textarea = TextArea::new("Folder Path", "", None, None);
|
let mut textarea = TextArea::new(
|
||||||
textarea.active = true;
|
"Folder Path",
|
||||||
|
"",
|
||||||
|
|x| {
|
||||||
|
let path = Path::new(x);
|
||||||
|
path.exists() && path.is_dir()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
None
|
||||||
|
);
|
||||||
|
textarea.state.is_active = true;
|
||||||
Self { textarea }
|
Self { textarea }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_folder_value(&mut self) -> Option<String> {
|
||||||
|
let value = self.textarea.get_value();
|
||||||
|
if value.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if let Some(path) = value && !path.is_empty() {
|
||||||
|
return Some(path);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StatefulWidget for AddFolderPopup {
|
impl StatefulWidget for AddFolderPopup {
|
||||||
@@ -37,6 +58,6 @@ impl StatefulWidget for AddFolderPopup {
|
|||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints(vec![Constraint::Length(1)])
|
.constraints(vec![Constraint::Length(1)])
|
||||||
.split(popup_area.inner(Margin::new(1, 1)));
|
.split(popup_area.inner(Margin::new(1, 1)));
|
||||||
self.textarea.render(chunks[0], buf, &mut state.textarea);
|
self.textarea.render(chunks[0], buf, &mut state.textarea.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use crate::config::types::ApplicationConfig;
|
use crate::config::types::ApplicationConfig;
|
||||||
use crate::constants::APP_CONIFG_FILE_PATH;
|
|
||||||
use crate::widgets::popups::folder::AddFolderPopup;
|
use crate::widgets::popups::folder::AddFolderPopup;
|
||||||
use crate::widgets::views::View;
|
use crate::widgets::views::View;
|
||||||
use crossterm::event::KeyCode::Char;
|
use crossterm::event::KeyCode::Char;
|
||||||
@@ -43,9 +42,7 @@ impl MainView {
|
|||||||
fn quit(&mut self) -> color_eyre::Result<()> {
|
fn quit(&mut self) -> color_eyre::Result<()> {
|
||||||
if self.state.popup.is_none() {
|
if self.state.popup.is_none() {
|
||||||
self.state.status = Status::Exiting;
|
self.state.status = Status::Exiting;
|
||||||
self.app_config
|
self.app_config.save()?;
|
||||||
.clone()
|
|
||||||
.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())?;
|
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -71,6 +68,15 @@ impl View for MainView {
|
|||||||
self.state.status = Status::Running;
|
self.state.status = Status::Running;
|
||||||
self.state.popup = None;
|
self.state.popup = None;
|
||||||
}
|
}
|
||||||
|
if let Some(any) = self.state.popup.as_mut() &&
|
||||||
|
let Some(popup) = any.downcast_mut::<AddFolderPopup>() &&
|
||||||
|
let Some(value) = popup.get_folder_value() &&
|
||||||
|
key.code.is_enter()
|
||||||
|
{
|
||||||
|
self.app_config.path_config.dlsite_paths.push(value);
|
||||||
|
popup.textarea.reset_value()?;
|
||||||
|
self.app_config.save()?;
|
||||||
|
}
|
||||||
if !matches!(self.state.status, Status::Popup) && matches!(key.kind, KeyEventKind::Press) {
|
if !matches!(self.state.status, Status::Popup) && matches!(key.kind, KeyEventKind::Press) {
|
||||||
match key.code {
|
match key.code {
|
||||||
Char('q') => self.quit()?,
|
Char('q') => self.quit()?,
|
||||||
|
|||||||
Reference in New Issue
Block a user