From 952f00261bbd9c8d6cc5fc2cac4d1b7c859d7eba Mon Sep 17 00:00:00 2001 From: fromost Date: Tue, 11 Nov 2025 02:15:43 +0800 Subject: [PATCH] Add basic list navigation and display --- src/app.rs | 4 +- src/cli/sync.rs | 2 +- src/config/mod.rs | 6 +-- src/helpers/db.rs | 8 +++- src/models/game.rs | 41 ++++++++++++++++++++ src/widgets/views/main_view.rs | 70 +++++++++++++++++++++++++++++----- 6 files changed, 113 insertions(+), 18 deletions(-) diff --git a/src/app.rs b/src/app.rs index 4d1defb..de5e87a 100755 --- a/src/app.rs +++ b/src/app.rs @@ -20,7 +20,7 @@ impl App { pub async fn create() -> Result { let config = ApplicationConfig::get_config()?; let state = AppState { - view: Some(AppView::Main(MainView::new())), + view: Some(AppView::Main(MainView::new()?)), }; let app = Self { events: EventHandler::new(Duration::from_millis(config.basic_config.tick_rate)), @@ -69,7 +69,7 @@ impl App { match current_view { AppView::Main(main_view) => { frame.render_stateful_widget( - MainView::new(), + MainView::new().unwrap(), frame.area(), &mut main_view.state, ); diff --git a/src/cli/sync.rs b/src/cli/sync.rs index f37e4ab..4be3c2a 100755 --- a/src/cli/sync.rs +++ b/src/cli/sync.rs @@ -48,7 +48,7 @@ impl DLSiteSyncCommand { pub async fn handle(&self) -> Result<()> { let now = Instant::now(); let app_conf = ApplicationConfig::get_config()?; - let mut db = RocksDB::new(DB_OPTIONS.clone(), DB_CF_OPTIONS.clone())?; + let mut db = RocksDB::default(); let crawler = DLSiteCrawler::new()?; if self.do_sync_genre { let genre_now = Instant::now(); diff --git a/src/config/mod.rs b/src/config/mod.rs index 4484563..7b623ca 100755 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,17 +4,13 @@ use color_eyre::Result; use std::path::PathBuf; use language_tags::LanguageTag; use ratatui::widgets::ListState; +use serde::Deserialize; use serde_json; pub mod types; const CONFIG_KEY: &str = "app_conf"; -pub(crate) struct GameList { - games: Vec, - state: ListState, -} - impl ApplicationConfig { pub fn get_config() -> Result { if CACHE_MAP.contains_key(CONFIG_KEY) && diff --git a/src/helpers/db.rs b/src/helpers/db.rs index d96ccc9..26df820 100755 --- a/src/helpers/db.rs +++ b/src/helpers/db.rs @@ -1,4 +1,4 @@ -use crate::constants::{APP_DB_DATA_DIR, DB_COLUMNS}; +use crate::constants::{APP_DB_DATA_DIR, DB_CF_OPTIONS, DB_COLUMNS, DB_OPTIONS}; use rocksdb::{ColumnFamilyDescriptor, IteratorMode, OptimisticTransactionDB, Options}; use serde::{Serialize}; use serde::de::DeserializeOwned; @@ -9,6 +9,12 @@ pub struct RocksDB { db: OptimisticTransactionDB, } +impl Default for RocksDB { + fn default() -> Self { + RocksDB::new(DB_OPTIONS.clone(), DB_CF_OPTIONS.clone()).unwrap() + } +} + impl RocksDB { pub fn new(db_opts: Options, cf_opts: Options) -> Result { let cfs = DB_COLUMNS.iter() diff --git a/src/models/game.rs b/src/models/game.rs index 9ecdd4a..f3844ae 100755 --- a/src/models/game.rs +++ b/src/models/game.rs @@ -1,12 +1,47 @@ +use color_eyre::Result; use std::path::PathBuf; use color_eyre::{eyre, Report}; +use ratatui::prelude::Text; +use ratatui::widgets::ListState; use serde::{Deserialize, Serialize}; +use serde::de::DeserializeOwned; use crate::config::types::ApplicationConfig; use crate::constants::{EN_LOCALE, JP_LOCALE}; use crate::crawler::DLSiteGenreCategory; +use crate::helpers::db::RocksDB; use crate::helpers::matches_primary_language; use crate::models::{RocksColumn, RocksReferences}; +#[derive(Debug)] +pub(crate) struct GameList { + pub games: Vec, + pub state: ListState, +} + +impl Default for GameList { + fn default() -> Self { + Self { + games: Vec::new(), + state: ListState::default(), + } + } +} + +impl GameList + where T: DeserializeOwned + RocksColumn +{ + pub fn new() -> Result { + let db = RocksDB::default(); + let mut state = ListState::default(); + state.select_first(); + let game_list = GameList { + games: db.get_all_values::()?, + state + }; + Ok(game_list) + } +} + //region Maniax #[derive(Clone, Debug, Serialize, Deserialize)] pub(crate) struct DLSiteManiax { @@ -54,6 +89,12 @@ impl RocksReferences for DLSiteManiax { self.genre_ids.clone() } } + +impl Into> for &DLSiteManiax { + fn into(self) -> Text<'static> { + Text::from(self.rj_num.to_string()) + } +} //endregion //region Genre diff --git a/src/widgets/views/main_view.rs b/src/widgets/views/main_view.rs index d8bc3d0..9d1f500 100755 --- a/src/widgets/views/main_view.rs +++ b/src/widgets/views/main_view.rs @@ -1,12 +1,15 @@ use crate::config::types::ApplicationConfig; use crate::widgets::popups::folder::AddFolderPopup; use crossterm::event::KeyCode::Char; -use crossterm::event::{Event, KeyCode, KeyEventKind}; +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 ratatui::style::Modifier; +use ratatui::style::palette::tailwind::SLATE; +use ratatui::widgets::{Block, Borders, HighlightSpacing, List, Paragraph, StatefulWidget}; +use crate::models::{DLSiteManiax, GameList}; use crate::widgets::popups::AppPopup; use crate::widgets::views::View; @@ -17,6 +20,8 @@ pub struct MainView { pub struct MainViewState { popup: Option, status: Status, + dl_game_list: GameList, + list_page_size: usize, } #[derive(Debug, Clone, Copy)] @@ -27,13 +32,17 @@ enum Status { } impl MainView { - pub fn new() -> Self { - Self { + pub fn new() -> color_eyre::Result { + let dl_game_list = GameList::new()?; + let view = Self { state: MainViewState { popup: None, status: Status::Running, + list_page_size: 0, + dl_game_list } - } + }; + Ok(view) } } @@ -72,6 +81,29 @@ 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 { @@ -94,6 +126,7 @@ impl View for MainView { Char('a') => state.folder_popup(), _ => {} } + state.handle_game_list_key(key_event)?; } } Ok(()) @@ -119,8 +152,9 @@ impl StatefulWidget for MainView { ]) .split(area); + state.list_page_size = chunks[1].height as usize; Self::render_header(chunks[0], buf); - Self::render_game_list(chunks[1], buf); + Self::render_game_info(chunks[1], buf, state); Self::render_footer(state, chunks[2], buf); let Some(popup) = state.popup.as_mut() else { @@ -148,12 +182,30 @@ impl HasScreenCursor for MainView { } impl MainView { - fn render_game_list(area: Rect, buf: &mut Buffer) { - let game_list = Block::new() + 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) + .constraints([ + Constraint::Length(13), + ]) + .split(area); + Self::render_game_list(chunks[0], 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()); - game_list.render(area, buf); + 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_header(area: Rect, buf: &mut Buffer) {