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

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