Reformat code
This commit is contained in:
49
src/app.rs
49
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::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(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
51
src/cli.rs
51
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::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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>,
|
||||||
}
|
}
|
||||||
@@ -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");
|
|
||||||
);
|
|
||||||
@@ -5,6 +5,4 @@ pub(crate) struct DLSiteCrawler {
|
|||||||
crawler: Crawler,
|
crawler: Crawler,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DLSiteCrawler {
|
impl DLSiteCrawler {}
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
12
src/event.rs
12
src/event.rs
@@ -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> {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
|
|||||||
14
src/main.rs
14
src/main.rs
@@ -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<()> {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user