Add info boxes contents
This commit is contained in:
@@ -11,3 +11,5 @@ color-eyre.workspace = true
|
|||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
directories.workspace = true
|
directories.workspace = true
|
||||||
|
|
||||||
|
models = { path = "../models" }
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
pub mod types;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::{Arc, Mutex};
|
||||||
|
use color_eyre::eyre::eyre;
|
||||||
use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options};
|
use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options};
|
||||||
use serde::{Serialize};
|
use serde::{Serialize};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use crate::types::{RocksColumn, RocksReference, RocksReferences};
|
|
||||||
use color_eyre::Result;
|
use color_eyre::Result;
|
||||||
use directories::BaseDirs;
|
use directories::BaseDirs;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use models::db::{RocksColumn, RocksReference, RocksReferences};
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "sus_manager";
|
const APP_DIR_NAME: &str = "sus_manager";
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
@@ -37,39 +36,36 @@ pub struct RocksDBFactory {
|
|||||||
cfs: Vec<String>,
|
cfs: Vec<String>,
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
db_opts: Options,
|
db_opts: Options,
|
||||||
cf_opts: Options,
|
cf_opts: Options
|
||||||
context: Option<RocksDB>
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RocksDBFactory {
|
impl RocksDBFactory {
|
||||||
pub fn new(path: PathBuf, db_opts: Options, cf_opts: Options) -> Result<Self> {
|
pub fn new(path: PathBuf, db_opts: Options, cf_opts: Options) -> Result<Self> {
|
||||||
let instance = Self {
|
let mut instance = Self {
|
||||||
cfs: vec![],
|
cfs: vec![],
|
||||||
path,
|
path,
|
||||||
db_opts,
|
db_opts,
|
||||||
cf_opts,
|
cf_opts
|
||||||
context: None
|
|
||||||
};
|
};
|
||||||
|
instance.register::<models::dlsite::DLSiteManiax>();
|
||||||
|
instance.register::<models::dlsite::DLSiteCategory>();
|
||||||
|
instance.register::<models::dlsite::DLSiteGenre>();
|
||||||
if !instance.path.exists() {
|
if !instance.path.exists() {
|
||||||
std::fs::create_dir_all(instance.path.as_path())?;
|
std::fs::create_dir_all(instance.path.as_path())?;
|
||||||
}
|
}
|
||||||
Ok(instance)
|
Ok(instance)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn register<T>(&mut self) where T: RocksColumn {
|
fn register<T>(&mut self) where T: RocksColumn {
|
||||||
self.cfs.push(T::get_column_name());
|
self.cfs.push(T::get_column_name());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_current_context(&mut self) -> Result<RocksDB> {
|
pub fn get_current_context(&mut self) -> Result<RocksDB> {
|
||||||
if let Some(context) = &self.context {
|
|
||||||
return Ok(context.clone());
|
|
||||||
}
|
|
||||||
let cfs = self.cfs
|
let cfs = self.cfs
|
||||||
.iter()
|
.iter()
|
||||||
.map(|cf| ColumnFamilyDescriptor::new(cf, self.cf_opts.clone()))
|
.map(|cf| ColumnFamilyDescriptor::new(cf, self.cf_opts.clone()))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let context = RocksDB::new(cfs, self.path.clone(), self.db_opts.clone())?;
|
let context = RocksDB::new(cfs, self.path.clone(), self.db_opts.clone())?;
|
||||||
self.context = Some(context.clone());
|
|
||||||
Ok(context)
|
Ok(context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,11 +76,17 @@ impl Default for RocksDBFactory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct RocksDB {
|
pub struct RocksDB {
|
||||||
db: Arc<OptimisticTransactionDB>,
|
db: Arc<OptimisticTransactionDB>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for RocksDB {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
std::mem::drop(self.db.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl RocksDB {
|
impl RocksDB {
|
||||||
pub fn new(cfs: Vec<ColumnFamilyDescriptor>, path: PathBuf, db_opts: Options) -> Result<Self> {
|
pub fn new(cfs: Vec<ColumnFamilyDescriptor>, path: PathBuf, db_opts: Options) -> Result<Self> {
|
||||||
let db = OptimisticTransactionDB::open_cf_descriptors(
|
let db = OptimisticTransactionDB::open_cf_descriptors(
|
||||||
|
|||||||
@@ -11,6 +11,5 @@ lazy_static.workspace = true
|
|||||||
ratatui.workspace = true
|
ratatui.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
dashmap.workspace = true
|
dashmap.workspace = true
|
||||||
db = { path = "../db" }
|
|
||||||
language-tags = { version = "0.3.2", features = ["serde"] }
|
language-tags = { version = "0.3.2", features = ["serde"] }
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use color_eyre::Report;
|
use color_eyre::Report;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use db::types::{RocksColumn, RocksReferences};
|
|
||||||
use crate::config::ApplicationConfig;
|
use crate::config::ApplicationConfig;
|
||||||
|
use crate::db::{RocksColumn, RocksReferences};
|
||||||
use crate::dlsite::genre::DLSiteGenre;
|
use crate::dlsite::genre::DLSiteGenre;
|
||||||
use crate::dlsite::translation::DLSiteTranslation;
|
use crate::dlsite::translation::DLSiteTranslation;
|
||||||
|
|
||||||
@@ -10,6 +10,7 @@ pub struct DLSiteCategory {
|
|||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub genre_ids: Vec<u16>,
|
pub genre_ids: Vec<u16>,
|
||||||
|
//TODO: have to become multilingual
|
||||||
pub name: DLSiteTranslation
|
pub name: DLSiteTranslation
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use db::types::RocksColumn;
|
use crate::db::RocksColumn;
|
||||||
use super::translation::DLSiteTranslation;
|
use crate::dlsite::DLSiteTranslations;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct DLSiteGenre {
|
pub struct DLSiteGenre {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub id: u16,
|
pub id: u16,
|
||||||
pub name: Vec<DLSiteTranslation>
|
pub name: DLSiteTranslations
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RocksColumn for DLSiteGenre {
|
impl RocksColumn for DLSiteGenre {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use ratatui::text::Text;
|
use ratatui::text::Text;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use db::types::{RocksColumn, RocksReferences};
|
use crate::db::{RocksColumn, RocksReferences};
|
||||||
use super::genre::DLSiteGenre;
|
use super::genre::DLSiteGenre;
|
||||||
use super::translation::DLSiteTranslation;
|
use super::translation::{DLSiteTranslation, DLSiteTranslations};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct DLSiteManiax {
|
pub struct DLSiteManiax {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub rj_num: String,
|
pub rj_num: String,
|
||||||
pub genre_ids: Vec<u16>,
|
pub genre_ids: Vec<u16>,
|
||||||
pub name: Vec<DLSiteTranslation>,
|
pub name: DLSiteTranslations,
|
||||||
pub sells_count: u32,
|
pub sells_count: u32,
|
||||||
pub folder_path: PathBuf,
|
pub folder_path: PathBuf,
|
||||||
pub version: Option<String>
|
pub version: Option<String>
|
||||||
@@ -22,7 +22,7 @@ impl From<super::crawler::DLSiteManiax> for DLSiteManiax {
|
|||||||
Self {
|
Self {
|
||||||
rj_num: value.rj_num,
|
rj_num: value.rj_num,
|
||||||
genre_ids: value.genre_ids,
|
genre_ids: value.genre_ids,
|
||||||
name: vec![title],
|
name: DLSiteTranslations(vec![title]),
|
||||||
sells_count: value.sells_count,
|
sells_count: value.sells_count,
|
||||||
folder_path: value.folder_path,
|
folder_path: value.folder_path,
|
||||||
version: None
|
version: None
|
||||||
@@ -56,4 +56,10 @@ impl Into<Text<'_>> for &DLSiteManiax {
|
|||||||
fn into(self) -> Text<'static> {
|
fn into(self) -> Text<'static> {
|
||||||
Text::from(self.rj_num.to_string())
|
Text::from(self.rj_num.to_string())
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Into<Text<'_>> for DLSiteManiax {
|
||||||
|
fn into(self) -> Text<'static> {
|
||||||
|
Text::from(self.rj_num.to_string())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -8,7 +8,7 @@ mod genre;
|
|||||||
mod maniax;
|
mod maniax;
|
||||||
pub mod crawler;
|
pub mod crawler;
|
||||||
|
|
||||||
pub use translation::{EN_LOCALE, JP_LOCALE, DLSiteTranslation};
|
pub use translation::*;
|
||||||
pub use category::DLSiteCategory;
|
pub use category::DLSiteCategory;
|
||||||
pub use genre::DLSiteGenre;
|
pub use genre::DLSiteGenre;
|
||||||
pub use maniax::DLSiteManiax;
|
pub use maniax::DLSiteManiax;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use language_tags::LanguageTag;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
use crate::config::ApplicationConfig;
|
use crate::config::ApplicationConfig;
|
||||||
use super::matches_primary_language;
|
use super::{matches_primary_language, PrimaryLanguage};
|
||||||
|
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
pub static ref EN_LOCALE: LanguageTag = LanguageTag::parse("en").unwrap();
|
pub static ref EN_LOCALE: LanguageTag = LanguageTag::parse("en").unwrap();
|
||||||
@@ -17,6 +17,30 @@ pub enum DLSiteTranslation {
|
|||||||
EN(String), JP(String)
|
EN(String), JP(String)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||||
|
pub struct DLSiteTranslations(pub Vec<DLSiteTranslation>);
|
||||||
|
|
||||||
|
impl DLSiteTranslations {
|
||||||
|
pub fn get_translation(&self, language: LanguageTag) -> color_eyre::Result<String> {
|
||||||
|
let Self(translations) = self;
|
||||||
|
let primary_language = PrimaryLanguage::try_from(&language)?;
|
||||||
|
let translation = match primary_language {
|
||||||
|
PrimaryLanguage::EN => translations.iter().find(|v| matches!(v, DLSiteTranslation::EN(_))),
|
||||||
|
PrimaryLanguage::JP => translations.iter().find(|v| matches!(v, DLSiteTranslation::JP(_))),
|
||||||
|
};
|
||||||
|
match translation {
|
||||||
|
Some(translation) => Ok(translation.to_string()),
|
||||||
|
None => Err(eyre!("No translation found for {:?}", language))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DLSiteTranslation {
|
||||||
|
pub fn to_string(&self) -> String {
|
||||||
|
self.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl TryFrom<&str> for DLSiteTranslation {
|
impl TryFrom<&str> for DLSiteTranslation {
|
||||||
type Error = Report;
|
type Error = Report;
|
||||||
fn try_from(value: &str) -> color_eyre::Result<Self> {
|
fn try_from(value: &str) -> color_eyre::Result<Self> {
|
||||||
@@ -44,13 +68,12 @@ impl TryFrom<String> for DLSiteTranslation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryInto<String> for DLSiteTranslation {
|
impl Into<String> for &DLSiteTranslation {
|
||||||
type Error = Report;
|
|
||||||
|
|
||||||
fn try_into(self) -> Result<String, Self::Error> {
|
fn into(self) -> String {
|
||||||
match self {
|
match self {
|
||||||
DLSiteTranslation::EN(val) => Ok(val),
|
DLSiteTranslation::EN(val) => val.to_string(),
|
||||||
DLSiteTranslation::JP(val) => Ok(val),
|
DLSiteTranslation::JP(val) => val.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ use lazy_static::lazy_static;
|
|||||||
|
|
||||||
pub mod dlsite;
|
pub mod dlsite;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
|
pub mod db;
|
||||||
|
|
||||||
const APP_DIR_NAME: &str = "sus_manager";
|
const APP_DIR_NAME: &str = "sus_manager";
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use crossterm::event::{Event};
|
|||||||
use ratatui::{DefaultTerminal, Frame};
|
use ratatui::{DefaultTerminal, Frame};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use color_eyre::eyre::eyre;
|
use color_eyre::eyre::eyre;
|
||||||
use db::RocksDBFactory;
|
use db::{RocksDBFactory};
|
||||||
use models::config::ApplicationConfig;
|
use models::config::ApplicationConfig;
|
||||||
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax};
|
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax};
|
||||||
|
|
||||||
@@ -23,10 +23,7 @@ pub struct AppState {
|
|||||||
impl App {
|
impl App {
|
||||||
pub async fn create() -> Result<Self> {
|
pub async fn create() -> Result<Self> {
|
||||||
let config = ApplicationConfig::get_config()?;
|
let config = ApplicationConfig::get_config()?;
|
||||||
let mut db_factory = RocksDBFactory::default();
|
let db_factory = RocksDBFactory::default();
|
||||||
db_factory.register::<DLSiteManiax>();
|
|
||||||
db_factory.register::<DLSiteGenre>();
|
|
||||||
db_factory.register::<DLSiteCategory>();
|
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
view: Some(AppView::Main(MainView::new(db_factory.clone())?)),
|
view: Some(AppView::Main(MainView::new(db_factory.clone())?)),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ use indicatif::{ProgressBar, ProgressStyle};
|
|||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use crawler::DLSiteCrawler;
|
use crawler::DLSiteCrawler;
|
||||||
use db::RocksDBFactory;
|
use db::{RocksDBFactory};
|
||||||
use models::config::ApplicationConfig;
|
use models::config::ApplicationConfig;
|
||||||
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation};
|
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation, DLSiteTranslations};
|
||||||
use crate::helpers;
|
use crate::helpers;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
@@ -47,10 +47,7 @@ impl DLSiteSyncCommand {
|
|||||||
pub async fn handle(&self) -> Result<()> {
|
pub async fn handle(&self) -> Result<()> {
|
||||||
let now = Instant::now();
|
let now = Instant::now();
|
||||||
let app_conf = ApplicationConfig::get_config()?;
|
let app_conf = ApplicationConfig::get_config()?;
|
||||||
let mut db_factory = RocksDBFactory::default();
|
let db_factory = RocksDBFactory::default();
|
||||||
db_factory.register::<DLSiteManiax>();
|
|
||||||
db_factory.register::<DLSiteGenre>();
|
|
||||||
db_factory.register::<DLSiteCategory>();
|
|
||||||
let crawler = DLSiteCrawler::new()?;
|
let crawler = DLSiteCrawler::new()?;
|
||||||
if self.do_sync_genre {
|
if self.do_sync_genre {
|
||||||
let genre_now = Instant::now();
|
let genre_now = Instant::now();
|
||||||
@@ -95,17 +92,20 @@ impl DLSiteSyncCommand {
|
|||||||
existing_genres.iter().find(|v| v.id == id);
|
existing_genres.iter().find(|v| v.id == id);
|
||||||
if let Some(existing_genre) = existing_genre {
|
if let Some(existing_genre) = existing_genre {
|
||||||
let name = DLSiteTranslation::try_from(genre.name)?;
|
let name = DLSiteTranslation::try_from(genre.name)?;
|
||||||
if existing_genre.name.contains(&name) {
|
let DLSiteTranslations(existing_translations) = existing_genre.name.clone();
|
||||||
|
if existing_translations.contains(&name) {
|
||||||
modified_genres.push(existing_genre.clone());
|
modified_genres.push(existing_genre.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut modified_genre = existing_genre.clone();
|
let mut modified_genre = existing_genre.clone();
|
||||||
modified_genre.name.push(name);
|
let DLSiteTranslations(mut modified_translations) = modified_genre.name.clone();
|
||||||
|
modified_translations.push(name);
|
||||||
|
modified_genre.name = DLSiteTranslations(modified_translations);
|
||||||
modified_genres.push(modified_genre);
|
modified_genres.push(modified_genre);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
modified_genres.push(DLSiteGenre {
|
modified_genres.push(DLSiteGenre {
|
||||||
id, name: vec![DLSiteTranslation::try_from(genre.name)?]
|
id, name: DLSiteTranslations(vec![DLSiteTranslation::try_from(genre.name)?])
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,12 +135,15 @@ impl DLSiteSyncCommand {
|
|||||||
.find(|v| v.rj_num == maniax.rj_num);
|
.find(|v| v.rj_num == maniax.rj_num);
|
||||||
if let Some(existing_maniax) = existing_maniax {
|
if let Some(existing_maniax) = existing_maniax {
|
||||||
let name = DLSiteTranslation::try_from(maniax.title)?;
|
let name = DLSiteTranslation::try_from(maniax.title)?;
|
||||||
if existing_maniax.name.contains(&name) {
|
let DLSiteTranslations(existing_translations) = existing_maniax.name.clone();
|
||||||
|
if existing_translations.contains(&name) {
|
||||||
modified_maniaxes.push(existing_maniax.clone());
|
modified_maniaxes.push(existing_maniax.clone());
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let mut modified_maniax = existing_maniax.clone();
|
let mut modified_maniax = existing_maniax.clone();
|
||||||
modified_maniax.name.push(name);
|
let DLSiteTranslations(mut modified_translations) = modified_maniax.name.clone();
|
||||||
|
modified_translations.push(name);
|
||||||
|
modified_maniax.name = DLSiteTranslations(modified_translations);
|
||||||
modified_maniaxes.push(modified_maniax);
|
modified_maniaxes.push(modified_maniax);
|
||||||
} else {
|
} else {
|
||||||
let mut value: DLSiteManiax = maniax.into();
|
let mut value: DLSiteManiax = maniax.into();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ mod app;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod event;
|
mod event;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
mod models;
|
|
||||||
mod widgets;
|
mod widgets;
|
||||||
|
|
||||||
pub use cli::Cli;
|
pub use cli::Cli;
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
use color_eyre::Result;
|
|
||||||
use ratatui::widgets::ListState;
|
|
||||||
use serde::de::DeserializeOwned;
|
|
||||||
use db::types::RocksColumn;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub(crate) struct GameList<T> {
|
|
||||||
pub games: Vec<T>,
|
|
||||||
pub state: ListState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> Default for GameList<T> {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self {
|
|
||||||
games: Vec::new(),
|
|
||||||
state: ListState::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> GameList<T>
|
|
||||||
where T: DeserializeOwned + RocksColumn
|
|
||||||
{
|
|
||||||
pub fn new(games: Vec<T>) -> Result<Self> {
|
|
||||||
let mut state = ListState::default();
|
|
||||||
state.select_first();
|
|
||||||
let game_list = GameList {
|
|
||||||
games,
|
|
||||||
state
|
|
||||||
};
|
|
||||||
Ok(game_list)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
mod game_list;
|
|
||||||
pub use game_list::*;
|
|
||||||
82
ui/src/widgets/components/game_info_box.rs
Executable file
82
ui/src/widgets/components/game_info_box.rs
Executable file
@@ -0,0 +1,82 @@
|
|||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::prelude::Line;
|
||||||
|
use ratatui::style::Style;
|
||||||
|
use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget, Widget};
|
||||||
|
use db::RocksDBFactory;
|
||||||
|
use models::config::ApplicationConfig;
|
||||||
|
use models::db::RocksReferences;
|
||||||
|
use models::dlsite::{DLSiteGenre, DLSiteManiax, DLSiteTranslation};
|
||||||
|
use crate::widgets::components::Component;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GameInfoBox {
|
||||||
|
pub state: GameInfoBoxState
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GameInfoBoxState {
|
||||||
|
game_title: String,
|
||||||
|
genres: Vec<String>,
|
||||||
|
active: bool
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Component for GameInfoBoxState {
|
||||||
|
fn set_active(&mut self, active: bool) {
|
||||||
|
self.active = active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<DLSiteManiax> for GameInfoBoxState {
|
||||||
|
type Error = color_eyre::Report;
|
||||||
|
|
||||||
|
fn try_from(value: DLSiteManiax) -> Result<Self, Self::Error> {
|
||||||
|
let db = RocksDBFactory::default().get_current_context()?;
|
||||||
|
let locale = ApplicationConfig::get_config()?.basic_config.locale;
|
||||||
|
let game_title = value.name.get_translation(locale.clone())?;
|
||||||
|
let game_genres =
|
||||||
|
db.get_reference_values::<DLSiteGenre, DLSiteManiax>(&value.get_reference_ids())?
|
||||||
|
.iter()
|
||||||
|
.map(|v| v.name.get_translation(locale.clone()))
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
Ok(Self { game_title, genres: game_genres, active: false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameInfoBox {
|
||||||
|
pub fn new(maniax: DLSiteManiax) -> color_eyre::Result<Self> {
|
||||||
|
Ok(
|
||||||
|
Self {
|
||||||
|
state: GameInfoBoxState::try_from(maniax)?
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_info(&mut self, maniax: &DLSiteManiax) -> color_eyre::Result<()> {
|
||||||
|
self.state = GameInfoBoxState::try_from(maniax.clone())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StatefulWidget for GameInfoBox {
|
||||||
|
type State = GameInfoBoxState;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let block = Block::new()
|
||||||
|
.title(Line::raw("Info"))
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.style(Style::default());
|
||||||
|
let title_text = Line::styled(
|
||||||
|
state.game_title.clone(),
|
||||||
|
Style::default().fg(ratatui::style::Color::Yellow)
|
||||||
|
);
|
||||||
|
let text = Paragraph::new(
|
||||||
|
vec![
|
||||||
|
title_text,
|
||||||
|
Line::raw(state.genres.join(", "))
|
||||||
|
]
|
||||||
|
).block(block.clone());
|
||||||
|
text.render(area, buf);;
|
||||||
|
}
|
||||||
|
}
|
||||||
108
ui/src/widgets/components/game_list.rs
Executable file
108
ui/src/widgets/components/game_list.rs
Executable file
@@ -0,0 +1,108 @@
|
|||||||
|
use std::marker::PhantomData;
|
||||||
|
use color_eyre::Result;
|
||||||
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
|
use ratatui::buffer::Buffer;
|
||||||
|
use ratatui::layout::Rect;
|
||||||
|
use ratatui::style::palette::tailwind::SLATE;
|
||||||
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
|
use ratatui::text::{Line};
|
||||||
|
use ratatui::widgets::{Block, Borders, HighlightSpacing, List, ListItem, ListState, StatefulWidget};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use models::db::RocksColumn;
|
||||||
|
use crate::widgets::components::Component;
|
||||||
|
|
||||||
|
const SELECTED_STYLE: Style = Style::new()
|
||||||
|
.bg(SLATE.c800)
|
||||||
|
.add_modifier(Modifier::BOLD);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GameList<'a, T> where T: Into<ListItem<'a>> + DeserializeOwned + RocksColumn + Clone {
|
||||||
|
pub state: GameListState<'a, T>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct GameListState<'a, T> where T: Into<ListItem<'a>> + DeserializeOwned + RocksColumn + Clone {
|
||||||
|
games: Vec<T>,
|
||||||
|
list_state: ListState,
|
||||||
|
list_page_size: usize,
|
||||||
|
active: bool,
|
||||||
|
_phantom: PhantomData<&'a ()>
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Component for GameList<'a, T> where T: Into<ListItem<'a>> + DeserializeOwned + RocksColumn + Clone {
|
||||||
|
fn set_active(&mut self, active: bool) {
|
||||||
|
self.state.active = active;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> Default for GameList<'a, T> where T: Into<ListItem<'a>> + DeserializeOwned + RocksColumn + Clone {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new(vec![])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> StatefulWidget for GameList<'a, T>
|
||||||
|
where
|
||||||
|
T: Into<ListItem<'a>> + DeserializeOwned + RocksColumn + Clone
|
||||||
|
{
|
||||||
|
type State = GameListState<'a, T>;
|
||||||
|
|
||||||
|
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
|
||||||
|
let list_block = Block::new()
|
||||||
|
.title(Line::raw("Games"))
|
||||||
|
.borders(Borders::ALL);
|
||||||
|
let list_block =
|
||||||
|
if state.active { list_block.style(Style::default().fg(Color::Yellow)) }
|
||||||
|
else { list_block.style(Style::default()) };
|
||||||
|
let game_list = List::new(state.games.clone())
|
||||||
|
.block(list_block)
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.highlight_style(SELECTED_STYLE)
|
||||||
|
.highlight_symbol(">")
|
||||||
|
.highlight_spacing(HighlightSpacing::WhenSelected);
|
||||||
|
state.list_page_size = (area.height - 2) as usize;
|
||||||
|
game_list.render(area, buf, &mut state.list_state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> GameList<'a, T>
|
||||||
|
where
|
||||||
|
T: Into<ListItem<'a>> + DeserializeOwned + RocksColumn + Clone
|
||||||
|
{
|
||||||
|
pub fn new(games: Vec<T>) -> Self {
|
||||||
|
let mut state = ListState::default();
|
||||||
|
state.select_first();
|
||||||
|
let game_list = GameList {
|
||||||
|
state: GameListState::<T> {
|
||||||
|
games,
|
||||||
|
list_state: state,
|
||||||
|
_phantom: PhantomData,
|
||||||
|
list_page_size: 0,
|
||||||
|
active: false
|
||||||
|
},
|
||||||
|
};
|
||||||
|
game_list
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle_game_list_key(&mut self, event: &KeyEvent) -> Result<()> {
|
||||||
|
let mut game_list_state = self.state.list_state.clone();
|
||||||
|
match event.code {
|
||||||
|
KeyCode::Down => game_list_state.select_next(),
|
||||||
|
KeyCode::Up => game_list_state.select_previous(),
|
||||||
|
KeyCode::PageUp =>game_list_state.scroll_up_by(self.state.list_page_size as u16),
|
||||||
|
KeyCode::PageDown => game_list_state.scroll_down_by(self.state.list_page_size as u16),
|
||||||
|
KeyCode::Home => game_list_state.select_first(),
|
||||||
|
KeyCode::End => game_list_state.select_last(),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
self.state.list_state = game_list_state;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_selected_value(&self) -> Option<&T> {
|
||||||
|
let Some(index) = self.state.list_state.selected() else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
self.state.games.get(index.clamp(0, self.state.games.len() - 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,11 @@
|
|||||||
mod textarea;
|
mod textarea;
|
||||||
|
mod game_list;
|
||||||
|
mod game_info_box;
|
||||||
|
|
||||||
pub use textarea::*;
|
pub use textarea::*;
|
||||||
|
pub use game_list::*;
|
||||||
|
pub use game_info_box::*;
|
||||||
|
|
||||||
|
pub trait Component {
|
||||||
|
fn set_active(&mut self, active: bool);
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,8 +15,8 @@ impl AddFolderPopup {
|
|||||||
let mut textarea = TextArea::new(
|
let mut textarea = TextArea::new(
|
||||||
"Folder Path",
|
"Folder Path",
|
||||||
"",
|
"",
|
||||||
|x| {
|
|v| {
|
||||||
let path = Path::new(x);
|
let path = Path::new(v);
|
||||||
path.exists() && path.is_dir()
|
path.exists() && path.is_dir()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
use crate::widgets::popups::folder::AddFolderPopup;
|
use crate::widgets::popups::folder::AddFolderPopup;
|
||||||
use crossterm::event::KeyCode::Char;
|
use crossterm::event::KeyCode::Char;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
|
use crossterm::event::{Event, KeyCode, KeyEventKind};
|
||||||
use rat_cursor::HasScreenCursor;
|
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::{Color, Line, Span, Style, Text, Widget};
|
use ratatui::prelude::{Color, Line, Span, Style, Text, Widget};
|
||||||
use ratatui::style::Modifier;
|
use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget};
|
||||||
use ratatui::style::palette::tailwind::SLATE;
|
use db::{RocksDBFactory};
|
||||||
use ratatui::widgets::{Block, Borders, HighlightSpacing, List, Paragraph, StatefulWidget};
|
|
||||||
use db::RocksDBFactory;
|
|
||||||
use models::config::ApplicationConfig;
|
use models::config::ApplicationConfig;
|
||||||
use models::dlsite::DLSiteManiax;
|
use models::dlsite::{DLSiteManiax};
|
||||||
use crate::models::GameList;
|
use crate::widgets::components::{Component, GameInfoBox, GameList};
|
||||||
use crate::widgets::popups::AppPopup;
|
use crate::widgets::popups::AppPopup;
|
||||||
use crate::widgets::views::View;
|
use crate::widgets::views::View;
|
||||||
|
|
||||||
@@ -24,8 +22,8 @@ pub struct MainView {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MainViewState {
|
pub struct MainViewState {
|
||||||
status: Status,
|
status: Status,
|
||||||
dl_game_list: GameList<DLSiteManiax>,
|
dl_game_list: GameList<'static, DLSiteManiax>,
|
||||||
list_page_size: usize,
|
game_info_box: GameInfoBox
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -37,8 +35,11 @@ enum Status {
|
|||||||
|
|
||||||
impl MainView {
|
impl MainView {
|
||||||
pub fn new(mut db_factory: RocksDBFactory) -> color_eyre::Result<Self> {
|
pub fn new(mut db_factory: RocksDBFactory) -> color_eyre::Result<Self> {
|
||||||
let db = db_factory.get_current_context()?;
|
let mut games = {
|
||||||
let mut games = db.get_all_values::<DLSiteManiax>()?;
|
let db = db_factory.get_current_context()?;
|
||||||
|
let values = db.get_all_values::<DLSiteManiax>()?;
|
||||||
|
values
|
||||||
|
};
|
||||||
games.sort_by(|a, b| {
|
games.sort_by(|a, b| {
|
||||||
let left = a.rj_num
|
let left = a.rj_num
|
||||||
.chars().skip(2)
|
.chars().skip(2)
|
||||||
@@ -52,12 +53,14 @@ impl MainView {
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
left.cmp(&right)
|
left.cmp(&right)
|
||||||
});
|
});
|
||||||
let dl_game_list = GameList::new(games)?;
|
let first_game = games[0].clone();
|
||||||
|
let mut dl_game_list = GameList::new(games);
|
||||||
|
dl_game_list.set_active(true);
|
||||||
let view = Self {
|
let view = Self {
|
||||||
state: MainViewState {
|
state: MainViewState {
|
||||||
status: Status::Running,
|
status: Status::Running,
|
||||||
list_page_size: 0,
|
|
||||||
dl_game_list,
|
dl_game_list,
|
||||||
|
game_info_box: GameInfoBox::new(first_game)?
|
||||||
},
|
},
|
||||||
db_factory
|
db_factory
|
||||||
};
|
};
|
||||||
@@ -99,29 +102,6 @@ impl MainViewState {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn handle_game_list_key(&mut self, event: &KeyEvent) -> color_eyre::Result<()> {
|
|
||||||
let game_list_state = &mut self.dl_game_list.state;
|
|
||||||
let game_list_len = self.dl_game_list.games.len();
|
|
||||||
let selected_value = game_list_state.selected().unwrap_or(0);
|
|
||||||
match event.code {
|
|
||||||
KeyCode::Down => game_list_state.select_next(),
|
|
||||||
KeyCode::Up => game_list_state.select_previous(),
|
|
||||||
KeyCode::PageUp => {
|
|
||||||
let selected_index =
|
|
||||||
if selected_value < self.list_page_size { 0 }
|
|
||||||
else { selected_value - self.list_page_size };
|
|
||||||
game_list_state.select(Some(selected_index))
|
|
||||||
},
|
|
||||||
KeyCode::PageDown => {
|
|
||||||
game_list_state.select(Some((selected_value + self.list_page_size).clamp(0, game_list_len)))
|
|
||||||
},
|
|
||||||
KeyCode::Home => game_list_state.select_first(),
|
|
||||||
KeyCode::End => game_list_state.select_last(),
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl View for MainView {
|
impl View for MainView {
|
||||||
@@ -143,7 +123,13 @@ impl View for MainView {
|
|||||||
Char('a') => state.folder_popup(),
|
Char('a') => state.folder_popup(),
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
state.handle_game_list_key(key_event)?;
|
state.dl_game_list.handle_game_list_key(key_event)?;
|
||||||
|
match state.dl_game_list.get_selected_value() {
|
||||||
|
Some(value) => {
|
||||||
|
state.game_info_box.set_info(value)?;
|
||||||
|
}
|
||||||
|
None => println!("No game selected")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -169,7 +155,6 @@ impl StatefulWidget for MainView {
|
|||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
|
|
||||||
state.list_page_size = chunks[1].height as usize;
|
|
||||||
Self::render_header(chunks[0], buf);
|
Self::render_header(chunks[0], buf);
|
||||||
Self::render_game_info(chunks[1], buf, state);
|
Self::render_game_info(chunks[1], buf, state);
|
||||||
Self::render_footer(state, chunks[2], buf);
|
Self::render_footer(state, chunks[2], buf);
|
||||||
@@ -199,9 +184,6 @@ impl HasScreenCursor for MainView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MainView {
|
impl MainView {
|
||||||
const SELECTED_STYLE: Style = Style::new()
|
|
||||||
.bg(SLATE.c800)
|
|
||||||
.add_modifier(Modifier::BOLD);
|
|
||||||
fn render_game_info(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
fn render_game_info(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Horizontal)
|
.direction(Direction::Horizontal)
|
||||||
@@ -210,29 +192,8 @@ impl MainView {
|
|||||||
Constraint::Fill(0),
|
Constraint::Fill(0),
|
||||||
])
|
])
|
||||||
.split(area);
|
.split(area);
|
||||||
Self::render_game_list(chunks[0], buf, state);
|
state.dl_game_list.clone().render(chunks[0], buf, &mut state.dl_game_list.state);
|
||||||
Self::render_game_box(chunks[1], buf, state);
|
state.game_info_box.clone().render(chunks[1], buf, &mut state.game_info_box.state);
|
||||||
}
|
|
||||||
|
|
||||||
fn render_game_list(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
|
||||||
let list_block = Block::new()
|
|
||||||
.title(Line::raw("Games"))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default());
|
|
||||||
let game_list = List::new(&state.dl_game_list.games)
|
|
||||||
.block(list_block)
|
|
||||||
.highlight_style(Self::SELECTED_STYLE)
|
|
||||||
.highlight_symbol(">")
|
|
||||||
.highlight_spacing(HighlightSpacing::WhenSelected);
|
|
||||||
StatefulWidget::render(game_list, area, buf, &mut state.dl_game_list.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_game_box(area: Rect, buf: &mut Buffer, state: &mut MainViewState) {
|
|
||||||
let block = Block::new()
|
|
||||||
.title(Line::raw("Info"))
|
|
||||||
.borders(Borders::ALL)
|
|
||||||
.style(Style::default());
|
|
||||||
block.render(area, buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_header(area: Rect, buf: &mut Buffer) {
|
fn render_header(area: Rect, buf: &mut Buffer) {
|
||||||
|
|||||||
Reference in New Issue
Block a user