Reformat code
This commit is contained in:
53
src/app.rs
53
src/app.rs
@@ -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::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::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 {
|
||||
events: EventHandler,
|
||||
@@ -25,17 +25,21 @@ struct AppState {
|
||||
|
||||
impl App {
|
||||
pub async fn create() -> Self {
|
||||
let app_conf =
|
||||
if APP_CONIFG_FILE_PATH.exists() { ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH).unwrap() }
|
||||
else { ApplicationConfig::new() };
|
||||
let app_conf = if APP_CONIFG_FILE_PATH.exists() {
|
||||
ApplicationConfig::from_file(&APP_CONIFG_FILE_PATH).unwrap()
|
||||
} else {
|
||||
ApplicationConfig::new()
|
||||
};
|
||||
Self::initialize_folders();
|
||||
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 {
|
||||
events: EventHandler::new(Duration::from_millis(app_conf.basic_config.tick_rate)),
|
||||
db_connection: db_conn,
|
||||
app_config: app_conf,
|
||||
state
|
||||
state,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,10 +63,11 @@ impl App {
|
||||
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 Some(main_view) = view.downcast_ref::<MainView>() &&
|
||||
!main_view.is_running() {
|
||||
break Ok(())
|
||||
if let Some(view) = self.state.view.as_mut()
|
||||
&& let Some(main_view) = view.downcast_ref::<MainView>()
|
||||
&& !main_view.is_running()
|
||||
{
|
||||
break Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,7 +84,7 @@ impl App {
|
||||
}
|
||||
|
||||
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>() {
|
||||
main_view.handle_key_input(key)?;
|
||||
}
|
||||
@@ -99,11 +104,15 @@ impl App {
|
||||
fn draw(&mut self, frame: &mut Frame) {
|
||||
if let Some(view) = self.state.view.as_mut() {
|
||||
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() {
|
||||
frame.set_cursor_position(pos);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
53
src/cli.rs
53
src/cli.rs
@@ -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::config::types::ApplicationConfig;
|
||||
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)]
|
||||
struct FolderAddCommand {
|
||||
path: String
|
||||
path: String,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
enum FolderSubCommand {
|
||||
Add(FolderAddCommand)
|
||||
Add(FolderAddCommand),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
struct FolderCommand {
|
||||
#[command(subcommand)]
|
||||
subcommand: FolderSubCommand
|
||||
subcommand: FolderSubCommand,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
@@ -35,20 +31,18 @@ enum CliSubCommand {
|
||||
#[command(version, about)]
|
||||
pub(crate) struct Cli {
|
||||
#[command(subcommand)]
|
||||
subcommand: Option<CliSubCommand>
|
||||
subcommand: Option<CliSubCommand>,
|
||||
}
|
||||
|
||||
impl Subcommand for Cli {
|
||||
fn augment_subcommands(cmd: Command) -> Command {
|
||||
cmd.subcommand(FolderCommand::augment_args(
|
||||
Command::new("folder"))
|
||||
).subcommand_required(true)
|
||||
cmd.subcommand(FolderCommand::augment_args(Command::new("folder")))
|
||||
.subcommand_required(true)
|
||||
}
|
||||
|
||||
fn augment_subcommands_for_update(cmd: Command) -> Command {
|
||||
cmd.subcommand(FolderCommand::augment_args
|
||||
(Command::new("folder"))
|
||||
).subcommand_required(true)
|
||||
cmd.subcommand(FolderCommand::augment_args(Command::new("folder")))
|
||||
.subcommand_required(true)
|
||||
}
|
||||
|
||||
fn has_subcommand(name: &str) -> bool {
|
||||
@@ -58,15 +52,13 @@ impl Subcommand for Cli {
|
||||
|
||||
impl Subcommand for FolderCommand {
|
||||
fn augment_subcommands(cmd: Command) -> Command {
|
||||
cmd.subcommand(FolderAddCommand::augment_args(
|
||||
Command::new("add"))
|
||||
).subcommand_required(true)
|
||||
cmd.subcommand(FolderAddCommand::augment_args(Command::new("add")))
|
||||
.subcommand_required(true)
|
||||
}
|
||||
|
||||
fn augment_subcommands_for_update(cmd: Command) -> Command {
|
||||
cmd.subcommand(FolderAddCommand::augment_args(
|
||||
Command::new("add"))
|
||||
).subcommand_required(true)
|
||||
cmd.subcommand(FolderAddCommand::augment_args(Command::new("add")))
|
||||
.subcommand_required(true)
|
||||
}
|
||||
|
||||
fn has_subcommand(name: &str) -> bool {
|
||||
@@ -77,10 +69,10 @@ impl Subcommand for FolderCommand {
|
||||
impl Cli {
|
||||
pub async fn run(&self) -> Result<()> {
|
||||
if self.subcommand.is_none() {
|
||||
return self.start_tui().await
|
||||
return self.start_tui().await;
|
||||
}
|
||||
if let Some(sub_command) = &self.subcommand {
|
||||
return sub_command.handle().await
|
||||
return sub_command.handle().await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -101,7 +93,7 @@ impl Cli {
|
||||
impl CliSubCommand {
|
||||
pub async fn handle(&self) -> Result<()> {
|
||||
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 path = PathBuf::from(&self.path);
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::path::{PathBuf};
|
||||
use ini::Ini;
|
||||
use color_eyre::Result;
|
||||
use crate::config::types::{ApplicationConfig, BasicConfig, PathConfig};
|
||||
use crate::constants::{APP_CONIFG_FILE_PATH, APP_DATA_DIR};
|
||||
use color_eyre::Result;
|
||||
use ini::Ini;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub mod types;
|
||||
|
||||
@@ -16,26 +16,37 @@ impl ApplicationConfig {
|
||||
};
|
||||
let path_conf_section = conf.section(Some("Path")).unwrap();
|
||||
let path_conf = PathConfig {
|
||||
dlsite_paths: path_conf_section.get("DLSite").unwrap()
|
||||
.split(":").map(|s| s.to_string()).collect(),
|
||||
dlsite_paths: path_conf_section
|
||||
.get("DLSite")
|
||||
.unwrap()
|
||||
.split(":")
|
||||
.map(|s| s.to_string())
|
||||
.collect(),
|
||||
};
|
||||
Ok(Self {
|
||||
basic_config: basic_conf,
|
||||
path_config: path_conf
|
||||
path_config: path_conf,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
let conf = Self {
|
||||
basic_config: BasicConfig {
|
||||
db_path: APP_DATA_DIR.clone().join("games.db").to_str().unwrap().to_string(),
|
||||
tick_rate: 250
|
||||
db_path: APP_DATA_DIR
|
||||
.clone()
|
||||
.join("games.db")
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_string(),
|
||||
tick_rate: 250,
|
||||
},
|
||||
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
|
||||
}
|
||||
|
||||
@@ -44,10 +55,16 @@ impl ApplicationConfig {
|
||||
conf.with_section(Some("Basic"))
|
||||
.set("DBPath", self.basic_config.db_path)
|
||||
.set("TickRate", self.basic_config.tick_rate.to_string());
|
||||
conf.with_section(Some("Path"))
|
||||
.set("DLSite", self.path_config.dlsite_paths.into_iter()
|
||||
.filter(|x| !x.is_empty()).collect::<Vec<String>>().join(":"));
|
||||
conf.with_section(Some("Path")).set(
|
||||
"DLSite",
|
||||
self.path_config
|
||||
.dlsite_paths
|
||||
.into_iter()
|
||||
.filter(|x| !x.is_empty())
|
||||
.collect::<Vec<String>>()
|
||||
.join(":"),
|
||||
);
|
||||
conf.write_to_file(path)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ApplicationConfig {
|
||||
pub(crate) basic_config: BasicConfig,
|
||||
pub(crate) path_config: PathConfig
|
||||
pub(crate) path_config: PathConfig,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct BasicConfig {
|
||||
pub(crate) db_path: String,
|
||||
pub(crate) tick_rate: u64
|
||||
pub(crate) tick_rate: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct PathConfig {
|
||||
pub(crate) dlsite_paths: Vec<String>
|
||||
}
|
||||
pub(crate) dlsite_paths: Vec<String>,
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
use std::path::PathBuf;
|
||||
use directories::BaseDirs;
|
||||
use lazy_static::lazy_static;
|
||||
use std::path::PathBuf;
|
||||
|
||||
const APP_DIR_NAME: &str = "sus_manager";
|
||||
lazy_static!(
|
||||
lazy_static! {
|
||||
static ref BASE_DIRS: BaseDirs = BaseDirs::new().unwrap();
|
||||
pub static ref APP_CONFIG_DIR: PathBuf = 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()
|
||||
.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_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone()
|
||||
.join("config.ini");
|
||||
);
|
||||
pub static ref APP_CONFIG_DIR: PathBuf =
|
||||
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().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_CONIFG_FILE_PATH: PathBuf = APP_CONFIG_DIR.clone().join("config.ini");
|
||||
}
|
||||
@@ -5,6 +5,4 @@ pub(crate) struct DLSiteCrawler {
|
||||
crawler: Crawler,
|
||||
}
|
||||
|
||||
impl DLSiteCrawler {
|
||||
|
||||
}
|
||||
impl DLSiteCrawler {}
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
mod dlsite;
|
||||
|
||||
use reqwest::{Client, Url};
|
||||
use robotstxt::{DefaultMatcher};
|
||||
use crate::constants::APP_CACHE_PATH;
|
||||
use color_eyre::Result;
|
||||
use reqwest::{Client, Url};
|
||||
use robotstxt::DefaultMatcher;
|
||||
use scraper::Html;
|
||||
use crate::constants::{APP_CACHE_PATH};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct Crawler {
|
||||
id: String,
|
||||
base_url: Url,
|
||||
client: Client,
|
||||
robots_txt: String
|
||||
robots_txt: String,
|
||||
}
|
||||
|
||||
impl Crawler {
|
||||
@@ -23,7 +23,11 @@ impl Crawler {
|
||||
base_url,
|
||||
};
|
||||
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 {
|
||||
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> {
|
||||
let local_robots_path = APP_CACHE_PATH.clone()
|
||||
.join(id).join("robots.txt");
|
||||
let local_robots_path = APP_CACHE_PATH.clone().join(id).join("robots.txt");
|
||||
if !local_robots_path.exists() {
|
||||
let mut robots_url = base_url.clone();
|
||||
robots_url.set_path("/robots.txt");
|
||||
let response = reqwest::get(robots_url).await
|
||||
.expect(format!("Failed to get robots.txt in `{}/robots.txt`", base_url.as_str()).as_str());
|
||||
let response = reqwest::get(robots_url).await.expect(
|
||||
format!(
|
||||
"Failed to get robots.txt in `{}/robots.txt`",
|
||||
base_url.as_str()
|
||||
)
|
||||
.as_str(),
|
||||
);
|
||||
let content = response.text().await?;
|
||||
tokio::fs::create_dir_all(local_robots_path.parent().unwrap()).await?;
|
||||
tokio::fs::write(&local_robots_path, &content).await?;
|
||||
Ok(content)
|
||||
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
Ok(tokio::fs::read_to_string(&local_robots_path).await?)
|
||||
}
|
||||
}
|
||||
@@ -55,4 +61,4 @@ impl Crawler {
|
||||
let html_text = &self.client.get(url).send().await?.text().await?;
|
||||
Ok(Html::parse_document(html_text))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
12
src/event.rs
12
src/event.rs
@@ -1,8 +1,8 @@
|
||||
use futures::FutureExt;
|
||||
use color_eyre::eyre::{Result, eyre};
|
||||
use std::time::Duration;
|
||||
use color_eyre::eyre::{eyre, Result};
|
||||
use crossterm::event::EventStream;
|
||||
use futures::FutureExt;
|
||||
use futures::StreamExt;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
@@ -16,7 +16,7 @@ pub(crate) enum AppEvent {
|
||||
pub(crate) struct EventHandler {
|
||||
_tx: UnboundedSender<AppEvent>,
|
||||
rx: tokio::sync::mpsc::UnboundedReceiver<AppEvent>,
|
||||
pub task: JoinHandle<()>
|
||||
pub task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
@@ -45,9 +45,7 @@ impl EventHandler {
|
||||
}
|
||||
}
|
||||
});
|
||||
Self {
|
||||
_tx, rx, task
|
||||
}
|
||||
Self { _tx, rx, task }
|
||||
}
|
||||
|
||||
pub(crate) async fn next(&mut self) -> Result<AppEvent> {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
|
||||
14
src/main.rs
14
src/main.rs
@@ -1,18 +1,18 @@
|
||||
mod app;
|
||||
mod cli;
|
||||
mod config;
|
||||
mod constants;
|
||||
mod crawler;
|
||||
mod event;
|
||||
mod helpers;
|
||||
mod schema;
|
||||
mod types;
|
||||
mod config;
|
||||
mod helpers;
|
||||
mod crawler;
|
||||
mod constants;
|
||||
mod cli;
|
||||
mod widgets;
|
||||
|
||||
use clap::{Parser};
|
||||
use crate::cli::Cli;
|
||||
use clap::Parser;
|
||||
use color_eyre::Result;
|
||||
use tokio;
|
||||
use crate::cli::Cli;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
|
||||
@@ -10,5 +10,5 @@ pub(crate) struct GameList<T> {
|
||||
#[diesel(table_name = crate::schema::dl_games)]
|
||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||
pub(crate) struct DLSiteGame {
|
||||
serial_number: String
|
||||
serial_number: String,
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod game;
|
||||
pub mod game;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
mod textarea;
|
||||
pub use textarea::*;
|
||||
pub use textarea::*;
|
||||
|
||||
@@ -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::layout::{Constraint, Direction, Layout, Rect};
|
||||
use ratatui::prelude::StatefulWidget;
|
||||
use ratatui::style::{Color, Stylize};
|
||||
use ratatui::text::Text;
|
||||
use ratatui::widgets::{Block, Borders, Paragraph, Widget};
|
||||
use tui_input::backend::crossterm::{EventHandler};
|
||||
use tui_input::{Input};
|
||||
use color_eyre::Result;
|
||||
use rat_cursor::HasScreenCursor;
|
||||
use ratatui::style::{Color, Stylize};
|
||||
use tui_input::backend::crossterm::EventHandler;
|
||||
use tui_input::Input;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct TextArea {
|
||||
input: Input,
|
||||
title: String,
|
||||
style: TextAreaStyle,
|
||||
area: Option<Rect>,
|
||||
input_area: Option<Rect>,
|
||||
pub active: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum TextAreaStyle {
|
||||
Block, SingleLine
|
||||
Block,
|
||||
SingleLine,
|
||||
}
|
||||
|
||||
impl StatefulWidget for TextArea {
|
||||
type State = TextArea;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||
where
|
||||
Self: Sized
|
||||
Self: Sized,
|
||||
{
|
||||
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);
|
||||
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) {
|
||||
} else if matches!(self.style, TextAreaStyle::SingleLine) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints([
|
||||
Constraint::Percentage(10),
|
||||
Constraint::Fill(0),
|
||||
])
|
||||
.constraints([Constraint::Percentage(10), Constraint::Fill(0)])
|
||||
.split(area);
|
||||
let text = Text::from(self.title);
|
||||
let line = Paragraph::new(text);
|
||||
let input_text = Text::from(input_value)
|
||||
.fg(Color::White);
|
||||
let paragraph = Paragraph::new(input_text)
|
||||
.bg(Color::Green);
|
||||
state.area = Some(chunks[1]);
|
||||
let input_text = Text::from(input_value).fg(Color::White);
|
||||
let paragraph = Paragraph::new(input_text).bg(Color::Green);
|
||||
state.input_area = Some(chunks[1]);
|
||||
line.render(chunks[0], buf);
|
||||
paragraph.render(chunks[1], buf);
|
||||
}
|
||||
@@ -63,10 +56,10 @@ impl StatefulWidget for TextArea {
|
||||
|
||||
impl HasScreenCursor for TextArea {
|
||||
fn screen_cursor(&self) -> Option<(u16, u16)> {
|
||||
if self.area.is_none() {
|
||||
if self.input_area.is_none() {
|
||||
return None;
|
||||
}
|
||||
let area = self.area.unwrap();
|
||||
let area = self.input_area.unwrap();
|
||||
let width = area.width.max(3) - 3;
|
||||
let scroll = self.input.visual_scroll(width as usize);
|
||||
let x = (self.input.visual_cursor().max(scroll) - scroll) as u16;
|
||||
@@ -81,12 +74,12 @@ impl TextArea {
|
||||
title: title.to_string(),
|
||||
active: false,
|
||||
style: style.unwrap_or(TextAreaStyle::SingleLine),
|
||||
area: None,
|
||||
input_area: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_input(&mut self, event: &Event) -> Result<()> {
|
||||
let state_result = self.input.handle_event(event);
|
||||
let _ = self.input.handle_event(event);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
pub mod components;
|
||||
pub mod popups;
|
||||
pub mod views;
|
||||
pub mod views;
|
||||
|
||||
@@ -1,21 +1,19 @@
|
||||
use crate::widgets::components::TextArea;
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::{Constraint, Direction, Layout, Margin, Rect};
|
||||
use ratatui::prelude::{StatefulWidget, Widget};
|
||||
use ratatui::widgets::{Block, Borders};
|
||||
use crate::widgets::components::TextArea;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AddFolderPopup {
|
||||
pub textarea: TextArea
|
||||
pub textarea: TextArea,
|
||||
}
|
||||
|
||||
impl AddFolderPopup {
|
||||
pub fn new() -> Self {
|
||||
let mut textarea = TextArea::new("Folder Path", "", None);
|
||||
textarea.active = true;
|
||||
Self {
|
||||
textarea
|
||||
}
|
||||
Self { textarea }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +21,7 @@ impl StatefulWidget for AddFolderPopup {
|
||||
type State = AddFolderPopup;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||
where
|
||||
Self: Sized
|
||||
Self: Sized,
|
||||
{
|
||||
let popup_area = Rect {
|
||||
x: area.width / 4,
|
||||
@@ -37,10 +35,8 @@ impl StatefulWidget for AddFolderPopup {
|
||||
block.render(popup_area, buf);
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Length(1)
|
||||
])
|
||||
.constraints(vec![Constraint::Length(1)])
|
||||
.split(popup_area.inner(Margin::new(1, 1)));
|
||||
self.textarea.render(chunks[0], buf, &mut state.textarea);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
pub mod folder;
|
||||
pub mod folder;
|
||||
|
||||
@@ -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::constants::APP_CONIFG_FILE_PATH;
|
||||
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 {
|
||||
app_config: ApplicationConfig,
|
||||
@@ -27,7 +26,7 @@ pub struct MainViewState {
|
||||
enum Status {
|
||||
Running,
|
||||
Exiting,
|
||||
Popup
|
||||
Popup,
|
||||
}
|
||||
|
||||
impl MainView {
|
||||
@@ -35,9 +34,9 @@ impl MainView {
|
||||
Self {
|
||||
state: MainViewState {
|
||||
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 {
|
||||
fn handle_input(&mut self, event: &Event) -> color_eyre::Result<()> {
|
||||
if let Some(any) = self.state.popup.as_mut() &&
|
||||
let Some(popup) = any.downcast_mut::<AddFolderPopup>(){
|
||||
if let Some(any) = self.state.popup.as_mut()
|
||||
&& let Some(popup) = any.downcast_mut::<AddFolderPopup>()
|
||||
{
|
||||
popup.textarea.handle_input(event)?;
|
||||
}
|
||||
Ok(())
|
||||
@@ -90,7 +90,7 @@ impl StatefulWidget for MainView {
|
||||
type State = MainViewState;
|
||||
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State)
|
||||
where
|
||||
Self: Sized
|
||||
Self: Sized,
|
||||
{
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
@@ -101,12 +101,13 @@ impl StatefulWidget for MainView {
|
||||
])
|
||||
.split(area);
|
||||
|
||||
Self::render_header(state,chunks[0], buf);
|
||||
Self::render_game_list(state,chunks[1], buf);
|
||||
Self::render_footer(state,chunks[2], buf);
|
||||
Self::render_header(chunks[0], buf);
|
||||
Self::render_game_list(chunks[1], buf);
|
||||
Self::render_footer(state, chunks[2], buf);
|
||||
|
||||
if let Some(boxed) = state.popup.as_mut() &&
|
||||
let Some(popup) = boxed.downcast_mut::<AddFolderPopup>() {
|
||||
if let Some(boxed) = state.popup.as_mut()
|
||||
&& let Some(popup) = boxed.downcast_mut::<AddFolderPopup>()
|
||||
{
|
||||
popup.clone().render(area, buf, popup);
|
||||
}
|
||||
}
|
||||
@@ -114,16 +115,17 @@ impl StatefulWidget for MainView {
|
||||
|
||||
impl HasScreenCursor for MainView {
|
||||
fn screen_cursor(&self) -> Option<(u16, u16)> {
|
||||
if let Some(popup) = &self.state.popup &&
|
||||
let Some(add_folder) = popup.downcast_ref::<AddFolderPopup>() {
|
||||
return add_folder.textarea.screen_cursor()
|
||||
if let Some(popup) = &self.state.popup
|
||||
&& let Some(add_folder) = popup.downcast_ref::<AddFolderPopup>()
|
||||
{
|
||||
return add_folder.textarea.screen_cursor();
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
.title(Line::raw("Games"))
|
||||
.borders(Borders::ALL)
|
||||
@@ -131,20 +133,19 @@ impl MainView {
|
||||
game_list.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_header(state: &mut MainViewState, area: Rect, buf: &mut Buffer) {
|
||||
let title = Paragraph::new(
|
||||
Text::styled(
|
||||
"SuS Manager",
|
||||
Style::default().fg(Color::Green),
|
||||
)
|
||||
);
|
||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||
let title = Paragraph::new(Text::styled(
|
||||
"SuS Manager",
|
||||
Style::default().fg(Color::Green),
|
||||
));
|
||||
title.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_footer(state: &mut MainViewState, area: Rect, buf: &mut Buffer) {
|
||||
let mut navigation_text = vec![
|
||||
Span::styled("(q) quit / (a) add folders", Style::default().fg(Color::Green)),
|
||||
];
|
||||
let mut navigation_text = vec![Span::styled(
|
||||
"(q) quit / (a) add folders",
|
||||
Style::default().fg(Color::Green),
|
||||
)];
|
||||
if matches!(state.status, Status::Popup) {
|
||||
navigation_text[0] = Span::styled("(Esc) close", Style::default().fg(Color::Green));
|
||||
}
|
||||
@@ -152,4 +153,4 @@ impl MainView {
|
||||
let footer = Paragraph::new(line);
|
||||
footer.render(area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ mod main_view;
|
||||
|
||||
use crossterm::event::{Event, KeyEvent};
|
||||
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_key_input(&mut self, key: &KeyEvent) -> color_eyre::Result<()>;
|
||||
fn is_running(&self) -> bool;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user