use crossterm::event::{Event, KeyCode, KeyEventKind};
use ratatui::{
layout::{Alignment, Constraint},
text::Line,
widgets::{Block, Scrollbar},
};
use ratatui_kit::prelude::*;
use tui_tree_widget::{TreeItem, TreeState};
use crate::{components::search_input::SearchInput, hooks::UseThemeConfig, novel::VolumeMarker};
#[derive(Default, Clone)]
pub struct ChapterName(pub String, pub usize);
impl From<(String, usize)> for ChapterName {
fn from(value: (String, usize)) -> Self {
Self(value.0, value.1)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum TocId {
Volume(usize),
Chapter(usize),
}
#[derive(Default, Props)]
pub struct SelectChapterProps {
pub is_editing: bool,
pub chapters: Vec<ChapterName>,
pub volumes: Vec<VolumeMarker>,
pub on_select: Handler<'static, usize>,
pub default_value: Option<usize>,
}
fn locate_volume(idx: usize, volumes: &[VolumeMarker]) -> Option<usize> {
volumes
.iter()
.enumerate()
.rev()
.find(|(_, v)| v.first_chapter_index <= idx)
.map(|(i, _)| i)
}
#[component]
pub fn SelectChapter(
props: &mut SelectChapterProps,
mut hooks: Hooks,
) -> impl Into<AnyElement<'static>> {
let mut filter_text = hooks.use_state(String::default);
let theme = hooks.use_theme_config();
let is_inputting = *hooks.use_context::<State<bool>>();
let state = {
let dv = props.default_value;
let volumes = props.volumes.clone();
hooks.use_state(move || {
let mut st = TreeState::<TocId>::default();
if let Some(idx) = dv {
if let Some(vi) = locate_volume(idx, &volumes) {
st.open(vec![TocId::Volume(vi)]);
st.select(vec![TocId::Volume(vi), TocId::Chapter(idx)]);
} else {
st.select(vec![TocId::Chapter(idx)]);
}
}
st
})
};
let is_editing = props.is_editing;
let is_empty = props.chapters.is_empty();
let items = hooks.use_memo(
|| build_items(&props.chapters, &props.volumes, &filter_text.read(), &theme),
(
filter_text.read().clone(),
props.chapters.len(),
props.volumes.len(),
),
);
let mut on_select = props.on_select.take();
hooks.use_events(move |event| {
if let Event::Key(key) = event
&& key.kind == KeyEventKind::Press
&& is_editing
&& !is_inputting.get()
{
match key.code {
KeyCode::Char('h') | KeyCode::Left => {
state.write().key_left();
}
KeyCode::Char('j') | KeyCode::Down => {
state.write().key_down();
}
KeyCode::Char('k') | KeyCode::Up => {
state.write().key_up();
}
KeyCode::Char('l') | KeyCode::Right | KeyCode::Enter => {
let selected = state.read().selected().last().cloned();
match selected {
Some(TocId::Chapter(idx)) => on_select(idx),
Some(TocId::Volume(_)) => {
state.write().toggle_selected();
}
None => {}
}
}
_ => {}
}
}
});
let border = Block::bordered()
.border_style(theme.basic.border)
.title_top(
Line::from("目录")
.style(theme.basic.border_title)
.centered(),
);
element!(View {
SearchInput(
placeholder: "按s搜索章节,以$开头输入数字表示索引",
on_submit: move |text| {
filter_text.set(text);
true
},
clear_on_escape: true,
on_clear: move |_| {
filter_text.set(String::default());
},
validate: |input: String| {
if let Some(stripped) = input.strip_prefix('$') {
if stripped.parse::<usize>().is_ok() {
(true, "".to_owned())
} else {
(false, "请输入正确的数字".to_owned())
}
} else {
(true, "".to_owned())
}
},
is_editing: is_editing,
)
#(if is_empty {
element!(Border(
top_title: Some(Line::from("目录").style(theme.basic.border_title).centered()),
border_style: theme.basic.border,
){
Center(height: Constraint::Length(5), width: Constraint::Percentage(50)){
Text(
text: if filter_text.read().is_empty() { "暂无章节".to_owned() } else { "无匹配章节".to_owned() },
alignment: Alignment::Center,
style: theme.colors.warning_color,
wrap: true,
)
}
}).into_any()
} else {
element!(TreeSelect<TocId>(
style: theme.basic.text,
highlight_style: theme.selected,
state: state,
items: items.clone(),
scrollbar: Scrollbar::default(),
block: border,
)).into_any()
})
})
}
fn build_items(
chapters: &[ChapterName],
volumes: &[VolumeMarker],
filter: &str,
theme: &crate::ThemeConfig,
) -> Vec<TreeItem<'static, TocId>> {
let leaf = |c: &ChapterName| TreeItem::new_leaf(TocId::Chapter(c.1), c.0.clone());
if !filter.is_empty() {
return chapters
.iter()
.filter(|c| {
if let Some(idx) = filter.strip_prefix('$') {
idx.parse::<usize>().map(|n| n == c.1).unwrap_or(false)
} else {
c.0.contains(filter)
}
})
.map(leaf)
.collect();
}
if volumes.is_empty() {
return chapters.iter().map(leaf).collect();
}
let len = chapters.len();
let mut items = Vec::new();
let first_start = volumes[0].first_chapter_index.min(len);
items.extend(chapters[..first_start].iter().map(leaf));
for (vi, v) in volumes.iter().enumerate() {
let start = v.first_chapter_index.min(len);
let end = volumes
.get(vi + 1)
.map(|nv| nv.first_chapter_index.min(len))
.unwrap_or(len);
let children: Vec<_> = chapters[start..end].iter().map(leaf).collect();
let title = Line::from(v.title.clone()).style(theme.basic.border_title);
if let Ok(node) = TreeItem::new(TocId::Volume(vi), title, children) {
items.push(node);
}
}
items
}