Add info boxes contents

This commit is contained in:
2025-12-15 02:12:01 +08:00
parent 979afc27e3
commit e76f12527f
20 changed files with 307 additions and 149 deletions

View File

@@ -11,3 +11,5 @@ color-eyre.workspace = true
serde.workspace = true
serde_json.workspace = true
directories.workspace = true
models = { path = "../models" }

View File

@@ -1,14 +1,13 @@
pub mod types;
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 serde::{Serialize};
use serde::de::DeserializeOwned;
use crate::types::{RocksColumn, RocksReference, RocksReferences};
use color_eyre::Result;
use directories::BaseDirs;
use lazy_static::lazy_static;
use models::db::{RocksColumn, RocksReference, RocksReferences};
const APP_DIR_NAME: &str = "sus_manager";
lazy_static! {
@@ -37,39 +36,36 @@ pub struct RocksDBFactory {
cfs: Vec<String>,
path: PathBuf,
db_opts: Options,
cf_opts: Options,
context: Option<RocksDB>
cf_opts: Options
}
impl RocksDBFactory {
pub fn new(path: PathBuf, db_opts: Options, cf_opts: Options) -> Result<Self> {
let instance = Self {
let mut instance = Self {
cfs: vec![],
path,
db_opts,
cf_opts,
context: None
cf_opts
};
instance.register::<models::dlsite::DLSiteManiax>();
instance.register::<models::dlsite::DLSiteCategory>();
instance.register::<models::dlsite::DLSiteGenre>();
if !instance.path.exists() {
std::fs::create_dir_all(instance.path.as_path())?;
}
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());
}
pub fn get_current_context(&mut self) -> Result<RocksDB> {
if let Some(context) = &self.context {
return Ok(context.clone());
}
let cfs = self.cfs
.iter()
.map(|cf| ColumnFamilyDescriptor::new(cf, self.cf_opts.clone()))
.collect::<Vec<_>>();
let context = RocksDB::new(cfs, self.path.clone(), self.db_opts.clone())?;
self.context = Some(context.clone());
Ok(context)
}
}
@@ -80,11 +76,17 @@ impl Default for RocksDBFactory {
}
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct RocksDB {
db: Arc<OptimisticTransactionDB>,
}
impl Drop for RocksDB {
fn drop(&mut self) {
std::mem::drop(self.db.clone());
}
}
impl RocksDB {
pub fn new(cfs: Vec<ColumnFamilyDescriptor>, path: PathBuf, db_opts: Options) -> Result<Self> {
let db = OptimisticTransactionDB::open_cf_descriptors(

View File

@@ -11,6 +11,5 @@ lazy_static.workspace = true
ratatui.workspace = true
serde_json.workspace = true
dashmap.workspace = true
db = { path = "../db" }
language-tags = { version = "0.3.2", features = ["serde"] }
sys-locale = "0.3.2"

View File

@@ -1,7 +1,7 @@
use color_eyre::Report;
use serde::{Deserialize, Serialize};
use db::types::{RocksColumn, RocksReferences};
use crate::config::ApplicationConfig;
use crate::db::{RocksColumn, RocksReferences};
use crate::dlsite::genre::DLSiteGenre;
use crate::dlsite::translation::DLSiteTranslation;
@@ -10,6 +10,7 @@ pub struct DLSiteCategory {
#[serde(skip)]
pub id: String,
pub genre_ids: Vec<u16>,
//TODO: have to become multilingual
pub name: DLSiteTranslation
}

View File

@@ -1,12 +1,12 @@
use serde::{Deserialize, Serialize};
use db::types::RocksColumn;
use super::translation::DLSiteTranslation;
use crate::db::RocksColumn;
use crate::dlsite::DLSiteTranslations;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DLSiteGenre {
#[serde(skip)]
pub id: u16,
pub name: Vec<DLSiteTranslation>
pub name: DLSiteTranslations
}
impl RocksColumn for DLSiteGenre {

View File

@@ -1,16 +1,16 @@
use std::path::PathBuf;
use ratatui::text::Text;
use serde::{Deserialize, Serialize};
use db::types::{RocksColumn, RocksReferences};
use crate::db::{RocksColumn, RocksReferences};
use super::genre::DLSiteGenre;
use super::translation::DLSiteTranslation;
use super::translation::{DLSiteTranslation, DLSiteTranslations};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct DLSiteManiax {
#[serde(skip)]
pub rj_num: String,
pub genre_ids: Vec<u16>,
pub name: Vec<DLSiteTranslation>,
pub name: DLSiteTranslations,
pub sells_count: u32,
pub folder_path: PathBuf,
pub version: Option<String>
@@ -22,7 +22,7 @@ impl From<super::crawler::DLSiteManiax> for DLSiteManiax {
Self {
rj_num: value.rj_num,
genre_ids: value.genre_ids,
name: vec![title],
name: DLSiteTranslations(vec![title]),
sells_count: value.sells_count,
folder_path: value.folder_path,
version: None
@@ -56,4 +56,10 @@ impl Into<Text<'_>> for &DLSiteManiax {
fn into(self) -> Text<'static> {
Text::from(self.rj_num.to_string())
}
}
impl Into<Text<'_>> for DLSiteManiax {
fn into(self) -> Text<'static> {
Text::from(self.rj_num.to_string())
}
}

View File

@@ -8,7 +8,7 @@ mod genre;
mod maniax;
pub mod crawler;
pub use translation::{EN_LOCALE, JP_LOCALE, DLSiteTranslation};
pub use translation::*;
pub use category::DLSiteCategory;
pub use genre::DLSiteGenre;
pub use maniax::DLSiteManiax;

View File

@@ -4,7 +4,7 @@ use language_tags::LanguageTag;
use serde::{Deserialize, Serialize};
use lazy_static::lazy_static;
use crate::config::ApplicationConfig;
use super::matches_primary_language;
use super::{matches_primary_language, PrimaryLanguage};
lazy_static! {
pub static ref EN_LOCALE: LanguageTag = LanguageTag::parse("en").unwrap();
@@ -17,6 +17,30 @@ pub enum DLSiteTranslation {
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 {
type Error = Report;
fn try_from(value: &str) -> color_eyre::Result<Self> {
@@ -44,13 +68,12 @@ impl TryFrom<String> for DLSiteTranslation {
}
}
impl TryInto<String> for DLSiteTranslation {
type Error = Report;
impl Into<String> for &DLSiteTranslation {
fn try_into(self) -> Result<String, Self::Error> {
fn into(self) -> String {
match self {
DLSiteTranslation::EN(val) => Ok(val),
DLSiteTranslation::JP(val) => Ok(val),
DLSiteTranslation::EN(val) => val.to_string(),
DLSiteTranslation::JP(val) => val.to_string(),
}
}
}

View File

@@ -7,6 +7,7 @@ use lazy_static::lazy_static;
pub mod dlsite;
pub mod config;
pub mod db;
const APP_DIR_NAME: &str = "sus_manager";
lazy_static! {

View File

@@ -5,7 +5,7 @@ use crossterm::event::{Event};
use ratatui::{DefaultTerminal, Frame};
use std::time::Duration;
use color_eyre::eyre::eyre;
use db::RocksDBFactory;
use db::{RocksDBFactory};
use models::config::ApplicationConfig;
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax};
@@ -23,10 +23,7 @@ pub struct AppState {
impl App {
pub async fn create() -> Result<Self> {
let config = ApplicationConfig::get_config()?;
let mut db_factory = RocksDBFactory::default();
db_factory.register::<DLSiteManiax>();
db_factory.register::<DLSiteGenre>();
db_factory.register::<DLSiteCategory>();
let db_factory = RocksDBFactory::default();
let state = AppState {
view: Some(AppView::Main(MainView::new(db_factory.clone())?)),
};

View File

@@ -8,9 +8,9 @@ use indicatif::{ProgressBar, ProgressStyle};
use itertools::Itertools;
use tokio::time::Instant;
use crawler::DLSiteCrawler;
use db::RocksDBFactory;
use db::{RocksDBFactory};
use models::config::ApplicationConfig;
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation};
use models::dlsite::{DLSiteCategory, DLSiteGenre, DLSiteManiax, DLSiteTranslation, DLSiteTranslations};
use crate::helpers;
#[derive(Parser, Debug)]
@@ -47,10 +47,7 @@ impl DLSiteSyncCommand {
pub async fn handle(&self) -> Result<()> {
let now = Instant::now();
let app_conf = ApplicationConfig::get_config()?;
let mut db_factory = RocksDBFactory::default();
db_factory.register::<DLSiteManiax>();
db_factory.register::<DLSiteGenre>();
db_factory.register::<DLSiteCategory>();
let db_factory = RocksDBFactory::default();
let crawler = DLSiteCrawler::new()?;
if self.do_sync_genre {
let genre_now = Instant::now();
@@ -95,17 +92,20 @@ impl DLSiteSyncCommand {
existing_genres.iter().find(|v| v.id == id);
if let Some(existing_genre) = existing_genre {
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());
continue;
}
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);
}
else {
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);
if let Some(existing_maniax) = existing_maniax {
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());
continue;
}
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);
} else {
let mut value: DLSiteManiax = maniax.into();

View File

@@ -2,7 +2,6 @@ mod app;
mod cli;
mod event;
mod helpers;
mod models;
mod widgets;
pub use cli::Cli;

View File

@@ -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)
}
}

View File

@@ -1,2 +0,0 @@
mod game_list;
pub use game_list::*;

View 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);;
}
}

View 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))
}
}

View File

@@ -1,2 +1,11 @@
mod textarea;
mod game_list;
mod game_info_box;
pub use textarea::*;
pub use game_list::*;
pub use game_info_box::*;
pub trait Component {
fn set_active(&mut self, active: bool);
}

View File

@@ -15,8 +15,8 @@ impl AddFolderPopup {
let mut textarea = TextArea::new(
"Folder Path",
"",
|x| {
let path = Path::new(x);
|v| {
let path = Path::new(v);
path.exists() && path.is_dir()
}
);

View File

@@ -1,17 +1,15 @@
use crate::widgets::popups::folder::AddFolderPopup;
use crossterm::event::KeyCode::Char;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyEventKind};
use crossterm::event::{Event, KeyCode, 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::style::Modifier;
use ratatui::style::palette::tailwind::SLATE;
use ratatui::widgets::{Block, Borders, HighlightSpacing, List, Paragraph, StatefulWidget};
use db::RocksDBFactory;
use ratatui::widgets::{Block, Borders, Paragraph, StatefulWidget};
use db::{RocksDBFactory};
use models::config::ApplicationConfig;
use models::dlsite::DLSiteManiax;
use crate::models::GameList;
use models::dlsite::{DLSiteManiax};
use crate::widgets::components::{Component, GameInfoBox, GameList};
use crate::widgets::popups::AppPopup;
use crate::widgets::views::View;
@@ -24,8 +22,8 @@ pub struct MainView {
#[derive(Clone)]
pub struct MainViewState {
status: Status,
dl_game_list: GameList<DLSiteManiax>,
list_page_size: usize,
dl_game_list: GameList<'static, DLSiteManiax>,
game_info_box: GameInfoBox
}
#[derive(Clone)]
@@ -37,8 +35,11 @@ enum Status {
impl MainView {
pub fn new(mut db_factory: RocksDBFactory) -> color_eyre::Result<Self> {
let db = db_factory.get_current_context()?;
let mut games = db.get_all_values::<DLSiteManiax>()?;
let mut games = {
let db = db_factory.get_current_context()?;
let values = db.get_all_values::<DLSiteManiax>()?;
values
};
games.sort_by(|a, b| {
let left = a.rj_num
.chars().skip(2)
@@ -52,12 +53,14 @@ impl MainView {
.unwrap();
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 {
state: MainViewState {
status: Status::Running,
list_page_size: 0,
dl_game_list,
game_info_box: GameInfoBox::new(first_game)?
},
db_factory
};
@@ -99,29 +102,6 @@ impl MainViewState {
}
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 {
@@ -143,7 +123,13 @@ impl View for MainView {
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(())
@@ -169,7 +155,6 @@ impl StatefulWidget for MainView {
])
.split(area);
state.list_page_size = chunks[1].height as usize;
Self::render_header(chunks[0], buf);
Self::render_game_info(chunks[1], buf, state);
Self::render_footer(state, chunks[2], buf);
@@ -199,9 +184,6 @@ impl HasScreenCursor for 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) {
let chunks = Layout::default()
.direction(Direction::Horizontal)
@@ -210,29 +192,8 @@ impl MainView {
Constraint::Fill(0),
])
.split(area);
Self::render_game_list(chunks[0], buf, state);
Self::render_game_box(chunks[1], buf, 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);
state.dl_game_list.clone().render(chunks[0], buf, &mut state.dl_game_list.state);
state.game_info_box.clone().render(chunks[1], buf, &mut state.game_info_box.state);
}
fn render_header(area: Rect, buf: &mut Buffer) {