Add basic list navigation and display

This commit is contained in:
2025-11-11 02:15:43 +08:00
parent e7e6f0695f
commit 952f00261b
6 changed files with 113 additions and 18 deletions

View File

@@ -20,7 +20,7 @@ impl App {
pub async fn create() -> Result<Self> {
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,
);

View File

@@ -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();

View File

@@ -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<T> {
games: Vec<T>,
state: ListState,
}
impl ApplicationConfig {
pub fn get_config() -> Result<Self> {
if CACHE_MAP.contains_key(CONFIG_KEY) &&

View File

@@ -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<Self> {
let cfs = DB_COLUMNS.iter()

View File

@@ -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<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() -> Result<Self> {
let db = RocksDB::default();
let mut state = ListState::default();
state.select_first();
let game_list = GameList {
games: db.get_all_values::<T>()?,
state
};
Ok(game_list)
}
}
//region Maniax
#[derive(Clone, Debug, Serialize, Deserialize)]
pub(crate) struct DLSiteManiax {
@@ -54,6 +89,12 @@ impl RocksReferences<DLSiteGenre> for DLSiteManiax {
self.genre_ids.clone()
}
}
impl Into<Text<'_>> for &DLSiteManiax {
fn into(self) -> Text<'static> {
Text::from(self.rj_num.to_string())
}
}
//endregion
//region Genre

View File

@@ -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<AppPopup>,
status: Status,
dl_game_list: GameList<DLSiteManiax>,
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<Self> {
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) {