diff --git a/src/app.rs b/src/app.rs index 0938bed..8a89d79 100755 --- a/src/app.rs +++ b/src/app.rs @@ -1,19 +1,18 @@ use crate::config::types::ApplicationConfig; use crate::event::{AppEvent, EventHandler}; use crate::widgets::views::{AppView, MainView}; -use crate::widgets::views::View; use color_eyre::Result; use crossterm::event::{Event}; -use rat_cursor::HasScreenCursor; use ratatui::{DefaultTerminal, Frame}; use std::time::Duration; +use color_eyre::eyre::eyre; pub(crate) struct App { events: EventHandler, state: AppState, } -struct AppState { +pub struct AppState { view: Option, } @@ -21,7 +20,7 @@ impl App { pub async fn create() -> Result { let config = ApplicationConfig::get_config()?; let state = AppState { - view: Some(AppView::MainView(MainView::new())), + view: Some(AppView::Main(MainView::new())), }; let app = Self { events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)), @@ -32,15 +31,16 @@ impl App { pub async fn run(mut self, terminal: &mut DefaultTerminal) -> Result<()> { loop { - terminal.draw(|frame| self.draw(frame))?; let event = self.events.next().await?; self.update(event)?; - if let Some(view) = self.state.view.as_mut() - && let AppView::MainView(main_view) = view - && !main_view.is_running() - { - break Ok(()); - } + let Some(current_view) = self.state.view.as_mut() else { + continue; + }; + let Some(view) = current_view.get_view() else { + continue; + }; + if !view.is_running() { break Ok(()) } + terminal.draw(|frame| self.draw(frame))?; } } @@ -48,34 +48,37 @@ impl App { if let AppEvent::Raw(cross_event) = event { self.handle_event(&cross_event)?; } - Ok(()) } fn handle_event(&mut self, key: &Event) -> Result<()> { - if let Some(current_view) = self.state.view.as_mut() { - match current_view { - AppView::MainView(main_view) => { - main_view.handle_input(key)? - } - } - } + let Some(current_view) = self.state.view.as_mut() else { + return Err(eyre!("there is no view")); + }; + let Some(view) = current_view.get_view() else { + return Err(eyre!("there is no view")); + }; + view.handle_input(key)?; Ok(()) } fn draw(&mut self, frame: &mut Frame) { - if let Some(view) = self.state.view.as_mut() { - match view { - AppView::MainView(main_view) => { - frame.render_stateful_widget( - MainView::new(), - frame.area(), - &mut main_view.state, - ); - if let Some(pos) = main_view.screen_cursor() { - frame.set_cursor_position(pos); - } - } + let Some(current_view) = self.state.view.as_mut() else { + return; + }; + let Some(view) = current_view.get_view() else { + return; + }; + if let Some(pos) = view.screen_cursor() { + frame.set_cursor_position(pos); + } + match current_view { + AppView::Main(main_view) => { + frame.render_stateful_widget( + MainView::new(), + frame.area(), + &mut main_view.state, + ); } } } diff --git a/src/config/mod.rs b/src/config/mod.rs index 5f9d51b..4484563 100755 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,5 +1,5 @@ use crate::config::types::{ApplicationConfig, BasicConfig, PathConfig}; -use crate::constants::{APP_CONIFG_FILE_PATH, APP_DB_DATA_DIR, CACHE_MAP}; +use crate::constants::{APP_CONIFG_FILE_PATH, CACHE_MAP}; use color_eyre::Result; use std::path::PathBuf; use language_tags::LanguageTag; diff --git a/src/crawler/mod.rs b/src/crawler/mod.rs index 0578470..577ec3a 100755 --- a/src/crawler/mod.rs +++ b/src/crawler/mod.rs @@ -1,6 +1,6 @@ pub mod dlsite; - pub use dlsite::*; + use color_eyre::eyre::eyre; use crate::constants::APP_CACHE_PATH; use color_eyre::Result; diff --git a/src/widgets/components/textarea.rs b/src/widgets/components/textarea.rs index 2a8bf63..924c149 100755 --- a/src/widgets/components/textarea.rs +++ b/src/widgets/components/textarea.rs @@ -43,34 +43,37 @@ impl StatefulWidget for TextArea { { let input_value = state.input.value().to_string(); let title = self.title.clone(); - if matches!(self.style, TextAreaStyle::Block) { - let block = Block::default().borders(Borders::ALL).title(title); - let paragraph = Paragraph::new(Text::from(input_value)).block(block); - state.input_area = Some(area); - paragraph.render(area, buf); - } else if matches!(self.style, TextAreaStyle::SingleLine) { - let chunks = Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Max((self.title.len() + 1) as u16), Constraint::Fill(0)]) - .split(area); - let label_text = Text::from(self.title); - let label = Paragraph::new(label_text); - - let input_text = Span::from(input_value.clone()).fg(Color::White); - let input_line = Line::from(input_text); - let paragraph = Paragraph::new(input_line) - .bg(if state.is_valid { Color::Green } else { Color::Red }) - .scroll((0, state.scroll_offset)); - - if state.input_area.is_none() { - state.input_area = Some(chunks[1]); - } - else if let Some(area) = self.state.input_area && area != chunks[1] { - state.input_area = Some(chunks[1]); + match self.style { + TextAreaStyle::Block => { + let block = Block::default().borders(Borders::ALL).title(title); + let paragraph = Paragraph::new(Text::from(input_value)).block(block); + state.input_area = Some(area); + paragraph.render(area, buf); } + TextAreaStyle::SingleLine => { + let chunks = Layout::default() + .direction(Direction::Horizontal) + .constraints([Constraint::Max((self.title.len() + 1) as u16), Constraint::Fill(0)]) + .split(area); + let label_text = Text::from(self.title); + let label = Paragraph::new(label_text); - label.render(chunks[0], buf); - paragraph.render(chunks[1], buf); + let input_text = Span::from(input_value.clone()).fg(Color::White); + let input_line = Line::from(input_text); + let paragraph = Paragraph::new(input_line) + .bg(if state.is_valid { Color::Green } else { Color::Red }) + .scroll((0, state.scroll_offset)); + + if state.input_area.is_none() { + state.input_area = Some(chunks[1]); + } + else if let Some(area) = self.state.input_area && area != chunks[1] { + state.input_area = Some(chunks[1]); + } + + label.render(chunks[0], buf); + paragraph.render(chunks[1], buf); + } } } } @@ -91,9 +94,7 @@ impl TextArea { pub fn new(title: &str, placeholder_text: &str, validate_fn: fn(&str) -> bool, - ) - -> Self - { + ) -> Self { let func = Arc::new(validate_fn); Self { title: title.to_string(), @@ -129,14 +130,15 @@ impl TextArea { { let scroll_offset = self.state.scroll_offset; let cursor_pos = self.state.input.cursor() as u16; - if scroll_offset> cursor_pos { + if scroll_offset > cursor_pos { self.state.scroll_offset = cursor_pos; } else if cursor_pos >= area.width + scroll_offset { self.state.scroll_offset = cursor_pos - area.width; } 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 - let _ = self.state.input.handle_event(&Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty()))); + let key_event = Event::Key(KeyEvent::new(KeyCode::Left, KeyModifiers::empty())); + let _ = self.state.input.handle_event(&key_event); } } @@ -151,6 +153,7 @@ impl TextArea { } pub fn reset_value(&mut self) -> Result<()> { + self.state.is_valid = false; Ok(self.state.input.reset()) } } diff --git a/src/widgets/popups/mod.rs b/src/widgets/popups/mod.rs index a370e97..470d1ba 100755 --- a/src/widgets/popups/mod.rs +++ b/src/widgets/popups/mod.rs @@ -1,5 +1,7 @@ +use ratatui::widgets::StatefulWidget; + pub mod folder; pub enum AppPopup { AddFolder(folder::AddFolderPopup) -} +} \ No newline at end of file diff --git a/src/widgets/views/main_view.rs b/src/widgets/views/main_view.rs index 3f1f618..f9b2308 100755 --- a/src/widgets/views/main_view.rs +++ b/src/widgets/views/main_view.rs @@ -1,6 +1,6 @@ use crate::config::types::ApplicationConfig; use crate::widgets::popups::folder::AddFolderPopup; -use crate::widgets::views::View; +use crate::widgets::views::{StatefulView, View}; use crossterm::event::KeyCode::Char; use crossterm::event::{Event, KeyCode, KeyEventKind}; use rat_cursor::HasScreenCursor; @@ -11,7 +11,7 @@ use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget}; use crate::widgets::popups::AppPopup; pub struct MainView { - pub state: MainViewState, + pub state: MainViewState } pub struct MainViewState { @@ -35,53 +35,63 @@ impl MainView { } } } +} +impl MainViewState { fn quit(&mut self) -> color_eyre::Result<()> { - if self.state.popup.is_none() { - self.state.status = Status::Exiting; + if self.popup.is_none() { + self.status = Status::Exiting; ApplicationConfig::get_config()?.save()?; } Ok(()) } fn folder_popup(&mut self) { - self.state.popup = Some(AppPopup::AddFolder(AddFolderPopup::new())); - self.state.status = Status::Popup; + self.popup = Some(AppPopup::AddFolder(AddFolderPopup::new())); + self.status = Status::Popup; } -} -impl View for MainView { - fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> { - if let Some(current_popup) = self.state.popup.as_mut() { - match current_popup { - AppPopup::AddFolder(folder_popup) => { - folder_popup.textarea.handle_input(event)?; - if let Event::Key(key) = event && - key.code.is_enter() && - let Some(value) = folder_popup.get_folder_value() - { - let mut config = ApplicationConfig::get_config()?; - config.path_config.dlsite_paths.push(value); + fn handle_popup(&mut self, event: &Event) -> color_eyre::Result<()> { + let Some(current_popup) = self.popup.as_mut() else { + return Ok(()); + }; + match current_popup { + AppPopup::AddFolder(folder_popup) => { + folder_popup.textarea.handle_input(event)?; + if let Event::Key(key) = event && + key.code.is_enter() && + let Some(value) = folder_popup.get_folder_value() + { + let mut config = ApplicationConfig::get_config()?; + config.path_config.dlsite_paths.push(value); - folder_popup.textarea.reset_value()?; - config.save()?; - } + folder_popup.textarea.reset_value()?; + config.save()?; } } } + Ok(()) + } +} + +impl StatefulView for MainView { + type State = MainViewState; + fn handle_input(state: &mut Self::State, event: &Event) -> color_eyre::Result<()> { + state.handle_popup(event)?; + if let Event::Key(key_event) = event { - if matches!(self.state.status, Status::Popup) && + if matches!(state.status, Status::Popup) && matches!(key_event.code, KeyCode::Esc) { - self.state.status = Status::Running; - self.state.popup = None; + state.status = Status::Running; + state.popup = None; } - if !matches!(self.state.status, Status::Popup) && + if !matches!(state.status, Status::Popup) && matches!(key_event.kind, KeyEventKind::Press) { match key_event.code { - Char('q') => self.quit()?, - Char('a') => self.folder_popup(), + Char('q') => state.quit()?, + Char('a') => state.folder_popup(), _ => {} } } @@ -89,8 +99,8 @@ impl View for MainView { Ok(()) } - fn is_running(&self) -> bool { - !matches!(self.state.status, Status::Exiting) + fn is_running(state: &Self::State) -> bool { + !matches!(state.status, Status::Exiting) } } diff --git a/src/widgets/views/mod.rs b/src/widgets/views/mod.rs index 6b3c8a2..4c10442 100755 --- a/src/widgets/views/mod.rs +++ b/src/widgets/views/mod.rs @@ -1,13 +1,30 @@ mod main_view; use crossterm::event::{Event}; +use rat_cursor::HasScreenCursor; pub use main_view::MainView; -pub trait View { +pub trait View: HasScreenCursor { fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()>; fn is_running(&self) -> bool; } +pub trait StatefulView: HasScreenCursor { + type State; + fn handle_input(state: &mut Self::State, event: &Event) -> color_eyre::Result<()>; + fn is_running(state: &Self::State) -> bool; +} + pub enum AppView { - MainView(MainView), + Main(MainView), +} + +impl AppView { + pub fn get_view(&mut self) -> Option<&mut dyn View> { + match self { + _ => None + } + } + + //TODO: Implement Stateful View } \ No newline at end of file