Reformat code

This commit is contained in:
2025-10-15 21:57:33 +08:00
parent a776e55187
commit d033d8b93f
19 changed files with 206 additions and 194 deletions

View File

@@ -1,16 +1,16 @@
use std::any::{Any};
use crate::event::{AppEvent, EventHandler};
use ratatui::{DefaultTerminal, Frame};
use std::time::Duration;
use color_eyre::Result;
use crossterm::event::{Event, KeyEvent};
use crossterm::event::Event as CrosstermEvent;
use diesel::{Connection, SqliteConnection};
use rat_cursor::HasScreenCursor;
use crate::config::types::ApplicationConfig; use crate::config::types::ApplicationConfig;
use crate::constants::{APP_CONFIG_DIR, APP_CONIFG_FILE_PATH, APP_DATA_DIR}; use crate::constants::{APP_CONFIG_DIR, APP_CONIFG_FILE_PATH, APP_DATA_DIR};
use crate::widgets::views::{View}; use crate::event::{AppEvent, EventHandler};
use crate::widgets::views::MainView; use crate::widgets::views::MainView;
use crate::widgets::views::View;
use color_eyre::Result;
use crossterm::event::Event as CrosstermEvent;
use crossterm::event::{Event, KeyEvent};
use diesel::{Connection, SqliteConnection};
use rat_cursor::HasScreenCursor;
use ratatui::{DefaultTerminal, Frame};
use std::any::Any;
use std::time::Duration;
pub(crate) struct App { pub(crate) struct App {
events: EventHandler, events: EventHandler,
@@ -25,17 +25,21 @@ struct AppState {
impl App { impl App {
pub async fn create() -> Self { pub async fn create() -> Self {
let app_conf = let app_conf = if APP_CONIFG_FILE_PATH.exists() {
if APP_CONIFG_FILE_PATH.exists() { ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH).unwrap() } ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH).unwrap()
else { ApplicationConfig::new() }; } else {
ApplicationConfig::new()
};
Self::initialize_folders(); Self::initialize_folders();
let db_conn = Self::establish_db_connection(app_conf.clone()); let db_conn = Self::establish_db_connection(app_conf.clone());
let state = AppState { view: Some(Box::new(MainView::new(&app_conf))) }; let state = AppState {
view: Some(Box::new(MainView::new(&app_conf))),
};
Self { Self {
events: EventHandler::new(Duration::from_millis(app_conf.basic_config.tick_rate)), events: EventHandler::new(Duration::from_millis(app_conf.basic_config.tick_rate)),
db_connection: db_conn, db_connection: db_conn,
app_config: app_conf, app_config: app_conf,
state state,
} }
} }
@@ -59,10 +63,11 @@ impl App {
terminal.draw(|frame| self.draw(frame))?; terminal.draw(|frame| self.draw(frame))?;
let event = self.events.next().await?; let event = self.events.next().await?;
self.update(event)?; self.update(event)?;
if let Some(view) = self.state.view.as_mut() && if let Some(view) = self.state.view.as_mut()
let Some(main_view) = view.downcast_ref::<MainView>() && && let Some(main_view) = view.downcast_ref::<MainView>()
!main_view.is_running() { && !main_view.is_running()
break Ok(()) {
break Ok(());
} }
} }
} }
@@ -79,7 +84,7 @@ impl App {
} }
fn handle_key_event(&mut self, key: &KeyEvent) -> Result<()> { fn handle_key_event(&mut self, key: &KeyEvent) -> Result<()> {
if let Some(any) = self.state.view .as_mut() { if let Some(any) = self.state.view.as_mut() {
if let Some(main_view) = any.downcast_mut::<MainView>() { if let Some(main_view) = any.downcast_mut::<MainView>() {
main_view.handle_key_input(key)?; main_view.handle_key_input(key)?;
} }
@@ -99,7 +104,11 @@ impl App {
fn draw(&mut self, frame: &mut Frame) { fn draw(&mut self, frame: &mut Frame) {
if let Some(view) = self.state.view.as_mut() { if let Some(view) = self.state.view.as_mut() {
if let Some(main_view) = view.downcast_mut::<MainView>() { if let Some(main_view) = view.downcast_mut::<MainView>() {
frame.render_stateful_widget(MainView::new(&self.app_config), frame.area(), &mut main_view.state); frame.render_stateful_widget(
MainView::new(&self.app_config),
frame.area(),
&mut main_view.state,
);
if let Some(pos) = main_view.screen_cursor() { if let Some(pos) = main_view.screen_cursor() {
frame.set_cursor_position(pos); frame.set_cursor_position(pos);
} }

View File

@@ -1,29 +1,25 @@
use std::io::stdout;
use std::path::PathBuf;
use clap::{command, Args, Command, Parser, Subcommand};
use color_eyre::Result;
use crossterm::cursor::{Hide, Show};
use crossterm::execute;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::crossterm;
use crate::app; use crate::app;
use crate::config::types::ApplicationConfig; use crate::config::types::ApplicationConfig;
use crate::constants::APP_CONIFG_FILE_PATH; use crate::constants::APP_CONIFG_FILE_PATH;
use clap::{command, Args, Command, Parser, Subcommand};
use color_eyre::Result;
use ratatui::crossterm;
use std::path::PathBuf;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FolderAddCommand { struct FolderAddCommand {
path: String path: String,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
enum FolderSubCommand { enum FolderSubCommand {
Add(FolderAddCommand) Add(FolderAddCommand),
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FolderCommand { struct FolderCommand {
#[command(subcommand)] #[command(subcommand)]
subcommand: FolderSubCommand subcommand: FolderSubCommand,
} }
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
@@ -35,20 +31,18 @@ enum CliSubCommand {
#[command(version, about)] #[command(version, about)]
pub(crate) struct Cli { pub(crate) struct Cli {
#[command(subcommand)] #[command(subcommand)]
subcommand: Option<CliSubCommand> subcommand: Option<CliSubCommand>,
} }
impl Subcommand for Cli { impl Subcommand for Cli {
fn augment_subcommands(cmd: Command) -> Command { fn augment_subcommands(cmd: Command) -> Command {
cmd.subcommand(FolderCommand::augment_args( cmd.subcommand(FolderCommand::augment_args(Command::new("folder")))
Command::new("folder")) .subcommand_required(true)
).subcommand_required(true)
} }
fn augment_subcommands_for_update(cmd: Command) -> Command { fn augment_subcommands_for_update(cmd: Command) -> Command {
cmd.subcommand(FolderCommand::augment_args cmd.subcommand(FolderCommand::augment_args(Command::new("folder")))
(Command::new("folder")) .subcommand_required(true)
).subcommand_required(true)
} }
fn has_subcommand(name: &str) -> bool { fn has_subcommand(name: &str) -> bool {
@@ -58,15 +52,13 @@ impl Subcommand for Cli {
impl Subcommand for FolderCommand { impl Subcommand for FolderCommand {
fn augment_subcommands(cmd: Command) -> Command { fn augment_subcommands(cmd: Command) -> Command {
cmd.subcommand(FolderAddCommand::augment_args( cmd.subcommand(FolderAddCommand::augment_args(Command::new("add")))
Command::new("add")) .subcommand_required(true)
).subcommand_required(true)
} }
fn augment_subcommands_for_update(cmd: Command) -> Command { fn augment_subcommands_for_update(cmd: Command) -> Command {
cmd.subcommand(FolderAddCommand::augment_args( cmd.subcommand(FolderAddCommand::augment_args(Command::new("add")))
Command::new("add")) .subcommand_required(true)
).subcommand_required(true)
} }
fn has_subcommand(name: &str) -> bool { fn has_subcommand(name: &str) -> bool {
@@ -77,10 +69,10 @@ impl Subcommand for FolderCommand {
impl Cli { impl Cli {
pub async fn run(&self) -> Result<()> { pub async fn run(&self) -> Result<()> {
if self.subcommand.is_none() { if self.subcommand.is_none() {
return self.start_tui().await return self.start_tui().await;
} }
if let Some(sub_command) = &self.subcommand { if let Some(sub_command) = &self.subcommand {
return sub_command.handle().await return sub_command.handle().await;
} }
Ok(()) Ok(())
} }
@@ -101,7 +93,7 @@ impl Cli {
impl CliSubCommand { impl CliSubCommand {
pub async fn handle(&self) -> Result<()> { pub async fn handle(&self) -> Result<()> {
match self { match self {
CliSubCommand::Folder(cmd) => cmd.subcommand.handle().await CliSubCommand::Folder(cmd) => cmd.subcommand.handle().await,
} }
} }
} }
@@ -119,7 +111,10 @@ impl FolderAddCommand {
let mut config = ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH.to_path_buf())?; let mut config = ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH.to_path_buf())?;
let path = PathBuf::from(&self.path); let path = PathBuf::from(&self.path);
let abs_path = path.canonicalize()?; let abs_path = path.canonicalize()?;
config.path_config.dlsite_paths.push(abs_path.to_str().unwrap().to_string()); config
.path_config
.dlsite_paths
.push(abs_path.to_str().unwrap().to_string());
config.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf()) config.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())
} }
} }

View File

@@ -1,8 +1,8 @@
use std::path::{PathBuf};
use ini::Ini;
use color_eyre::Result;
use crate::config::types::{ApplicationConfig, BasicConfig, PathConfig}; use crate::config::types::{ApplicationConfig, BasicConfig, PathConfig};
use crate::constants::{APP_CONIFG_FILE_PATH, APP_DATA_DIR}; use crate::constants::{APP_CONIFG_FILE_PATH, APP_DATA_DIR};
use color_eyre::Result;
use ini::Ini;
use std::path::PathBuf;
pub mod types; pub mod types;
@@ -16,26 +16,37 @@ impl ApplicationConfig {
}; };
let path_conf_section = conf.section(Some("Path")).unwrap(); let path_conf_section = conf.section(Some("Path")).unwrap();
let path_conf = PathConfig { let path_conf = PathConfig {
dlsite_paths: path_conf_section.get("DLSite").unwrap() dlsite_paths: path_conf_section
.split(":").map(|s| s.to_string()).collect(), .get("DLSite")
.unwrap()
.split(":")
.map(|s| s.to_string())
.collect(),
}; };
Ok(Self { Ok(Self {
basic_config: basic_conf, basic_config: basic_conf,
path_config: path_conf path_config: path_conf,
}) })
} }
pub fn new() -> Self { pub fn new() -> Self {
let conf = Self { let conf = Self {
basic_config: BasicConfig { basic_config: BasicConfig {
db_path: APP_DATA_DIR.clone().join("games.db").to_str().unwrap().to_string(), db_path: APP_DATA_DIR
tick_rate: 250 .clone()
.join("games.db")
.to_str()
.unwrap()
.to_string(),
tick_rate: 250,
}, },
path_config: PathConfig { path_config: PathConfig {
dlsite_paths: vec![] dlsite_paths: vec![],
} },
}; };
conf.clone().write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf()).unwrap(); conf.clone()
.write_to_file(&APP_CONIFG_FILE_PATH.to_path_buf())
.unwrap();
conf conf
} }
@@ -44,9 +55,15 @@ impl ApplicationConfig {
conf.with_section(Some("Basic")) conf.with_section(Some("Basic"))
.set("DBPath", self.basic_config.db_path) .set("DBPath", self.basic_config.db_path)
.set("TickRate", self.basic_config.tick_rate.to_string()); .set("TickRate", self.basic_config.tick_rate.to_string());
conf.with_section(Some("Path")) conf.with_section(Some("Path")).set(
.set("DLSite", self.path_config.dlsite_paths.into_iter() "DLSite",
.filter(|x| !x.is_empty()).collect::<Vec<String>>().join(":")); self.path_config
.dlsite_paths
.into_iter()
.filter(|x| !x.is_empty())
.collect::<Vec<String>>()
.join(":"),
);
conf.write_to_file(path)?; conf.write_to_file(path)?;
Ok(()) Ok(())
} }

View File

@@ -1,16 +1,16 @@
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ApplicationConfig { pub(crate) struct ApplicationConfig {
pub(crate) basic_config: BasicConfig, pub(crate) basic_config: BasicConfig,
pub(crate) path_config: PathConfig pub(crate) path_config: PathConfig,
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct BasicConfig { pub(crate) struct BasicConfig {
pub(crate) db_path: String, pub(crate) db_path: String,
pub(crate) tick_rate: u64 pub(crate) tick_rate: u64,
} }
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct PathConfig { pub(crate) struct PathConfig {
pub(crate) dlsite_paths: Vec<String> pub(crate) dlsite_paths: Vec<String>,
} }

View File

@@ -1,16 +1,13 @@
use std::path::PathBuf;
use directories::BaseDirs; use directories::BaseDirs;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use std::path::PathBuf;
const APP_DIR_NAME: &str = "sus_manager"; const APP_DIR_NAME: &str = "sus_manager";
lazy_static!( lazy_static! {
static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap(); static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap();
pub static ref APP_CONFIG_DIR: PathBuf = BASE_DIRS.config_dir().to_path_buf() pub static ref APP_CONFIG_DIR: PathBuf =
.join(APP_DIR_NAME); BASE_DIRS.config_dir().to_path_buf().join(APP_DIR_NAME);
pub static ref APP_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf() pub static ref APP_DATA_DIR: PathBuf = BASE_DIRS.data_dir().to_path_buf().join(APP_DIR_NAME);
.join(APP_DIR_NAME); pub static ref APP_CACHE_PATH: PathBuf = BASE_DIRS.cache_dir().to_path_buf().join(APP_DIR_NAME);
pub static ref APP_CACHE_PATH: PathBuf = BASE_DIRS.cache_dir().to_path_buf() pub static ref APP_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone().join("config.ini");
.join(APP_DIR_NAME); }
pub static ref APP_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone()
.join("config.ini");
);

View File

@@ -5,6 +5,4 @@ pub(crate) struct DLSiteCrawler {
crawler: Crawler, crawler: Crawler,
} }
impl DLSiteCrawler { impl DLSiteCrawler {}
}

View File

@@ -1,17 +1,17 @@
mod dlsite; mod dlsite;
use reqwest::{Client, Url}; use crate::constants::APP_CACHE_PATH;
use robotstxt::{DefaultMatcher};
use color_eyre::Result; use color_eyre::Result;
use reqwest::{Client, Url};
use robotstxt::DefaultMatcher;
use scraper::Html; use scraper::Html;
use crate::constants::{APP_CACHE_PATH};
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct Crawler { pub(crate) struct Crawler {
id: String, id: String,
base_url: Url, base_url: Url,
client: Client, client: Client,
robots_txt: String robots_txt: String,
} }
impl Crawler { impl Crawler {
@@ -23,7 +23,11 @@ impl Crawler {
base_url, base_url,
}; };
let mut matcher = DefaultMatcher::default(); let mut matcher = DefaultMatcher::default();
let is_access_allowed = matcher.one_agent_allowed_by_robots(&crawler.robots_txt, "reqwest", crawler.base_url.as_str()); let is_access_allowed = matcher.one_agent_allowed_by_robots(
&crawler.robots_txt,
"reqwest",
crawler.base_url.as_str(),
);
if !is_access_allowed { if !is_access_allowed {
panic!("Crawler cannot access site {}", crawler.base_url.as_str()); panic!("Crawler cannot access site {}", crawler.base_url.as_str());
} }
@@ -31,20 +35,22 @@ impl Crawler {
} }
async fn get_robots_txt(id: &str, base_url: &Url) -> Result<String> { async fn get_robots_txt(id: &str, base_url: &Url) -> Result<String> {
let local_robots_path = APP_CACHE_PATH.clone() let local_robots_path = APP_CACHE_PATH.clone().join(id).join("robots.txt");
.join(id).join("robots.txt");
if !local_robots_path.exists() { if !local_robots_path.exists() {
let mut robots_url = base_url.clone(); let mut robots_url = base_url.clone();
robots_url.set_path("/robots.txt"); robots_url.set_path("/robots.txt");
let response = reqwest::get(robots_url).await let response = reqwest::get(robots_url).await.expect(
.expect(format!("Failed to get robots.txt in `{}/robots.txt`", base_url.as_str()).as_str()); format!(
"Failed to get robots.txt in `{}/robots.txt`",
base_url.as_str()
)
.as_str(),
);
let content = response.text().await?; let content = response.text().await?;
tokio::fs::create_dir_all(local_robots_path.parent().unwrap()).await?; tokio::fs::create_dir_all(local_robots_path.parent().unwrap()).await?;
tokio::fs::write(&local_robots_path, &content).await?; tokio::fs::write(&local_robots_path, &content).await?;
Ok(content) Ok(content)
} else {
}
else {
Ok(tokio::fs::read_to_string(&local_robots_path).await?) Ok(tokio::fs::read_to_string(&local_robots_path).await?)
} }
} }

View File

@@ -1,8 +1,8 @@
use futures::FutureExt; use color_eyre::eyre::{eyre, Result};
use color_eyre::eyre::{Result, eyre};
use std::time::Duration;
use crossterm::event::EventStream; use crossterm::event::EventStream;
use futures::FutureExt;
use futures::StreamExt; use futures::StreamExt;
use std::time::Duration;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tokio::task::JoinHandle; use tokio::task::JoinHandle;
@@ -16,7 +16,7 @@ pub(crate) enum AppEvent {
pub(crate) struct EventHandler { pub(crate) struct EventHandler {
_tx: UnboundedSender<AppEvent>, _tx: UnboundedSender<AppEvent>,
rx: tokio::sync::mpsc::UnboundedReceiver<AppEvent>, rx: tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
pub task: JoinHandle<()> pub task: JoinHandle<()>,
} }
impl EventHandler { impl EventHandler {
@@ -45,9 +45,7 @@ impl EventHandler {
} }
} }
}); });
Self { Self { _tx, rx, task }
_tx, rx, task
}
} }
pub(crate) async fn next(&mut self) -> Result<AppEvent> { pub(crate) async fn next(&mut self) -> Result<AppEvent> {

View File

@@ -0,0 +1 @@

View File

@@ -1,18 +1,18 @@
mod app; mod app;
mod cli;
mod config;
mod constants;
mod crawler;
mod event; mod event;
mod helpers;
mod schema; mod schema;
mod types; mod types;
mod config;
mod helpers;
mod crawler;
mod constants;
mod cli;
mod widgets; mod widgets;
use clap::{Parser}; use crate::cli::Cli;
use clap::Parser;
use color_eyre::Result; use color_eyre::Result;
use tokio; use tokio;
use crate::cli::Cli;
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {

View File

@@ -10,5 +10,5 @@ pub(crate) struct GameList<T> {
#[diesel(table_name = crate::schema::dl_games)] #[diesel(table_name = crate::schema::dl_games)]
#[diesel(check_for_backend(diesel::sqlite::Sqlite))] #[diesel(check_for_backend(diesel::sqlite::Sqlite))]
pub(crate) struct DLSiteGame { pub(crate) struct DLSiteGame {
serial_number: String serial_number: String,
} }

View File

@@ -1,60 +1,53 @@
use crossterm::event::{Event}; use color_eyre::Result;
use crossterm::event::Event;
use rat_cursor::HasScreenCursor;
use ratatui::buffer::Buffer; use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect}; use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::StatefulWidget; use ratatui::prelude::StatefulWidget;
use ratatui::style::{Color, Stylize};
use ratatui::text::Text; use ratatui::text::Text;
use ratatui::widgets::{Block, Borders, Paragraph, Widget}; use ratatui::widgets::{Block, Borders, Paragraph, Widget};
use tui_input::backend::crossterm::{EventHandler}; use tui_input::backend::crossterm::EventHandler;
use tui_input::{Input}; use tui_input::Input;
use color_eyre::Result;
use rat_cursor::HasScreenCursor;
use ratatui::style::{Color, Stylize};
#[derive(Clone)] #[derive(Clone)]
pub struct TextArea { pub struct TextArea {
input: Input, input: Input,
title: String, title: String,
style: TextAreaStyle, style: TextAreaStyle,
area: Option<Rect>, input_area: Option<Rect>,
pub active: bool, pub active: bool,
} }
#[derive(Clone)] #[derive(Clone)]
pub enum TextAreaStyle { pub enum TextAreaStyle {
Block, SingleLine Block,
SingleLine,
} }
impl StatefulWidget for TextArea { impl StatefulWidget for TextArea {
type State = TextArea; type State = TextArea;
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,
{ {
let input_value = state.input.value().to_string(); let input_value = state.input.value().to_string();
let title = self.title.clone(); let title = self.title.clone();
if matches!(self.style, TextAreaStyle::Block) { if matches!(self.style, TextAreaStyle::Block) {
let block = Block::default() let block = Block::default().borders(Borders::ALL).title(title);
.borders(Borders::ALL) let paragraph = Paragraph::new(Text::from(input_value)).block(block);
.title(title); state.input_area = Some(area);
let paragraph = Paragraph::new(Text::from(input_value))
.block(block);
paragraph.render(area, buf); paragraph.render(area, buf);
} } else if matches!(self.style, TextAreaStyle::SingleLine) {
else if matches!(self.style, TextAreaStyle::SingleLine) {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Horizontal) .direction(Direction::Horizontal)
.constraints([ .constraints([Constraint::Percentage(10), Constraint::Fill(0)])
Constraint::Percentage(10),
Constraint::Fill(0),
])
.split(area); .split(area);
let text = Text::from(self.title); let text = Text::from(self.title);
let line = Paragraph::new(text); let line = Paragraph::new(text);
let input_text = Text::from(input_value) let input_text = Text::from(input_value).fg(Color::White);
.fg(Color::White); let paragraph = Paragraph::new(input_text).bg(Color::Green);
let paragraph = Paragraph::new(input_text) state.input_area = Some(chunks[1]);
.bg(Color::Green);
state.area = Some(chunks[1]);
line.render(chunks[0], buf); line.render(chunks[0], buf);
paragraph.render(chunks[1], buf); paragraph.render(chunks[1], buf);
} }
@@ -63,10 +56,10 @@ 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.area.is_none() { if self.input_area.is_none() {
return None; return None;
} }
let area = self.area.unwrap(); let area = self.input_area.unwrap();
let width = area.width.max(3) - 3; let width = area.width.max(3) - 3;
let scroll = self.input.visual_scroll(width as usize); let scroll = self.input.visual_scroll(width as usize);
let x = (self.input.visual_cursor().max(scroll) - scroll) as u16; let x = (self.input.visual_cursor().max(scroll) - scroll) as u16;
@@ -81,12 +74,12 @@ impl TextArea {
title: title.to_string(), title: title.to_string(),
active: false, active: false,
style: style.unwrap_or(TextAreaStyle::SingleLine), style: style.unwrap_or(TextAreaStyle::SingleLine),
area: None, input_area: None,
} }
} }
pub fn handle_input(&mut self, event: &Event) -> Result<()> { pub fn handle_input(&mut self, event: &Event) -> Result<()> {
let state_result = self.input.handle_event(event); let _ = self.input.handle_event(event);
Ok(()) Ok(())
} }
} }

View File

@@ -1,21 +1,19 @@
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};
use ratatui::prelude::{StatefulWidget, Widget}; use ratatui::prelude::{StatefulWidget, Widget};
use ratatui::widgets::{Block, Borders}; use ratatui::widgets::{Block, Borders};
use crate::widgets::components::TextArea;
#[derive(Clone)] #[derive(Clone)]
pub struct AddFolderPopup { pub struct AddFolderPopup {
pub textarea: TextArea pub textarea: TextArea,
} }
impl AddFolderPopup { impl AddFolderPopup {
pub fn new() -> Self { pub fn new() -> Self {
let mut textarea = TextArea::new("Folder Path", "", None); let mut textarea = TextArea::new("Folder Path", "", None);
textarea.active = true; textarea.active = true;
Self { Self { textarea }
textarea
}
} }
} }
@@ -23,7 +21,7 @@ impl StatefulWidget for AddFolderPopup {
type State = AddFolderPopup; type State = AddFolderPopup;
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,
{ {
let popup_area = Rect { let popup_area = Rect {
x: area.width / 4, x: area.width / 4,
@@ -37,9 +35,7 @@ impl StatefulWidget for AddFolderPopup {
block.render(popup_area, buf); block.render(popup_area, buf);
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints(vec![ .constraints(vec![Constraint::Length(1)])
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);
} }

View File

@@ -1,16 +1,15 @@
use std::any::Any;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use crossterm::event::KeyCode::Char;
use rat_cursor::HasScreenCursor;
use ratatui::buffer::Buffer;
use ratatui::Frame;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Color, Line, Span, Style, Text, Widget};
use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget};
use crate::config::types::ApplicationConfig; use crate::config::types::ApplicationConfig;
use crate::constants::APP_CONIFG_FILE_PATH; 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::{Event, KeyCode, KeyEvent, KeyEventKind};
use rat_cursor::HasScreenCursor;
use ratatui::buffer::Buffer;
use ratatui::layout::{Constraint, Direction, Layout, Rect};
use ratatui::prelude::{Color, Line, Span, Style, Text, Widget};
use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget};
use std::any::Any;
pub struct MainView { pub struct MainView {
app_config: ApplicationConfig, app_config: ApplicationConfig,
@@ -27,7 +26,7 @@ pub struct MainViewState {
enum Status { enum Status {
Running, Running,
Exiting, Exiting,
Popup Popup,
} }
impl MainView { impl MainView {
@@ -35,9 +34,9 @@ impl MainView {
Self { Self {
state: MainViewState { state: MainViewState {
popup: None, popup: None,
status: Status::Running status: Status::Running,
}, },
app_config: app_conf.clone() app_config: app_conf.clone(),
} }
} }
@@ -59,8 +58,9 @@ impl MainView {
impl View for MainView { impl View for MainView {
fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> { fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> {
if let Some(any) = self.state.popup.as_mut() && if let Some(any) = self.state.popup.as_mut()
let Some(popup) = any.downcast_mut::<AddFolderPopup>(){ && let Some(popup) = any.downcast_mut::<AddFolderPopup>()
{
popup.textarea.handle_input(event)?; popup.textarea.handle_input(event)?;
} }
Ok(()) Ok(())
@@ -90,7 +90,7 @@ impl StatefulWidget for MainView {
type State = MainViewState; type State = MainViewState;
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,
{ {
let chunks = Layout::default() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
@@ -101,12 +101,13 @@ impl StatefulWidget for MainView {
]) ])
.split(area); .split(area);
Self::render_header(state,chunks[0], buf); Self::render_header(chunks[0], buf);
Self::render_game_list(state,chunks[1], buf); Self::render_game_list(chunks[1], buf);
Self::render_footer(state,chunks[2], buf); Self::render_footer(state, chunks[2], buf);
if let Some(boxed) = state.popup.as_mut() && if let Some(boxed) = state.popup.as_mut()
let Some(popup) = boxed.downcast_mut::<AddFolderPopup>() { && let Some(popup) = boxed.downcast_mut::<AddFolderPopup>()
{
popup.clone().render(area, buf, popup); popup.clone().render(area, buf, popup);
} }
} }
@@ -114,16 +115,17 @@ impl StatefulWidget for MainView {
impl HasScreenCursor for MainView { impl HasScreenCursor for MainView {
fn screen_cursor(&self) -> Option<(u16, u16)> { fn screen_cursor(&self) -> Option<(u16, u16)> {
if let Some(popup) = &self.state.popup && if let Some(popup) = &self.state.popup
let Some(add_folder) = popup.downcast_ref::<AddFolderPopup>() { && let Some(add_folder) = popup.downcast_ref::<AddFolderPopup>()
return add_folder.textarea.screen_cursor() {
return add_folder.textarea.screen_cursor();
} }
None None
} }
} }
impl MainView { impl MainView {
fn render_game_list(state: &mut MainViewState, area: Rect, buf: &mut Buffer) { fn render_game_list(area: Rect, buf: &mut Buffer) {
let game_list = Block::new() let game_list = Block::new()
.title(Line::raw("Games")) .title(Line::raw("Games"))
.borders(Borders::ALL) .borders(Borders::ALL)
@@ -131,20 +133,19 @@ impl MainView {
game_list.render(area, buf); game_list.render(area, buf);
} }
fn render_header(state: &mut MainViewState, area: Rect, buf: &mut Buffer) { fn render_header(area: Rect, buf: &mut Buffer) {
let title = Paragraph::new( let title = Paragraph::new(Text::styled(
Text::styled(
"SuS Manager", "SuS Manager",
Style::default().fg(Color::Green), Style::default().fg(Color::Green),
) ));
);
title.render(area, buf); title.render(area, buf);
} }
fn render_footer(state: &mut MainViewState, area: Rect, buf: &mut Buffer) { fn render_footer(state: &mut MainViewState, area: Rect, buf: &mut Buffer) {
let mut navigation_text = vec![ let mut navigation_text = vec![Span::styled(
Span::styled("(q) quit / (a) add folders", Style::default().fg(Color::Green)), "(q) quit / (a) add folders",
]; Style::default().fg(Color::Green),
)];
if matches!(state.status, Status::Popup) { if matches!(state.status, Status::Popup) {
navigation_text[0] = Span::styled("(Esc) close", Style::default().fg(Color::Green)); navigation_text[0] = Span::styled("(Esc) close", Style::default().fg(Color::Green));
} }

View File

@@ -2,8 +2,9 @@ mod main_view;
use crossterm::event::{Event, KeyEvent}; use crossterm::event::{Event, KeyEvent};
pub use main_view::MainView; pub use main_view::MainView;
use std::any::Any;
pub trait View { pub trait View: Any + 'static {
fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()>; fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()>;
fn handle_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()>; fn handle_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()>;
fn is_running(&self) -> bool; fn is_running(&self) -> bool;