use crate::{
History,
components::{KeyShortcutInfo, Loading, ShortcutInfoModal, WarningModal},
errors::Errors,
hooks::{DebounceOptions, UseDebounceEffect, UseInitState},
keymap::{ReaderAction, display_keys},
novel::{Novel, VolumeMarker},
};
use futures::FutureExt;
use ratatui::layout::Direction;
use ratatui_kit::prelude::*;
use ratatui_kit_keymap::UseKeymapHandler;
mod select_chapter;
pub use select_chapter::*;
mod read_content;
pub use read_content::*;
use std::sync::Arc;
use tokio::sync::Notify;
use tokio::time::{Duration, sleep};
mod settings;
pub use settings::*;
mod tts;
pub use tts::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Panel {
None,
Tts,
ReaderSettings,
}
#[component]
pub fn ReadNovel<T>(mut hooks: Hooks) -> impl Into<AnyElement<'static>>
where
T: Novel + Send + Sync + Unpin + 'static,
{
let route_state = hooks.use_route_state::<T::Args>();
let history = *hooks.use_context::<State<Option<History>>>();
let mut chapters = hooks.use_state(std::vec::Vec::new);
let mut volumes = hooks.use_state(Vec::<VolumeMarker>::new);
let mut current_chapter = hooks.use_state(|| 0usize);
let mut content = hooks.use_state(String::default);
let mut is_read_mode = hooks.use_state(|| false);
let mut panel = hooks.use_state(|| Panel::None);
let (width, height) = hooks.use_terminal_size();
let mut content_loading = hooks.use_state(|| false);
let mut info_modal_open = hooks.use_state(|| false);
let mut scroll_target = hooks.use_state(ScrollTarget::default);
let (novel, loading, error) = hooks.use_init_state(async move {
let args = route_state.as_ref().clone();
tokio::spawn(async move {
let mut res = T::init(args).await?;
if res.get_chapters().is_none() {
let (chapter_list, volume_list) = res.request_toc().await?;
res.set_chapters(&chapter_list);
res.set_volumes(volume_list);
}
chapters.set(
res.get_chapters_names()?
.into_iter()
.map(ChapterName::from)
.collect(),
);
volumes.set(res.get_volumes().to_vec());
current_chapter.set(res.current_chapter);
content_loading.set(true);
content.set(res.get_content().await?);
content_loading.set(false);
scroll_target.set(ScrollTarget::from_ratio(res.line_percent));
Ok::<T, Errors>(res)
})
.await?
});
hooks.use_on_drop({
let mut novel = novel.read().clone();
let mut history = history.read().clone();
move || {
if let Some(novel) = novel.as_mut() {
novel.line_percent = scroll_target.get().as_ratio();
novel.current_chapter = current_chapter.get();
if let Some(history) = history.as_mut() {
let history_item = novel.to_history_item().expect("to_history_item failed");
history.add(&novel.get_id(), history_item);
history.save().expect("save history failed");
}
}
}
});
hooks.use_async_effect(
async move {
let notify = Arc::new(Notify::new());
let notify_clone = notify.clone();
let show_loading_handle = tokio::spawn(async move {
sleep(Duration::from_millis(200)).await;
if notify_clone.notified().now_or_never().is_none() {
content_loading.set(true);
}
});
let novel = novel.read().clone();
let content_result = novel.map(|n| tokio::spawn(async move { n.get_content().await }));
if let Some(fut) = content_result {
match fut.await {
Ok(c) => match c {
Ok(c) => {
content.set(c);
}
Err(e) => {
error.write().replace(e);
}
},
Err(e) => {
error.write().replace(e.into());
}
}
}
notify.notify_one();
content_loading.set(false);
let _ = show_loading_handle.await;
},
current_chapter.get(),
);
let reader_display = hooks.use_atom(&crate::state::READER_DISPLAY);
let saved_display = hooks.use_state(|| crate::state::READER_DISPLAY.get());
hooks.use_debounce_effect(
move || {
let current = *reader_display.read();
if current == saved_display.get() {
return;
}
let mut saved_display = saved_display;
tokio::task::spawn_blocking(move || {
if current.save().is_ok() {
saved_display.set(current);
}
});
},
*reader_display.read(),
DebounceOptions::default(),
);
hooks.use_on_drop(move || {
let current = *reader_display.read();
if current != saved_display.get() {
let _ = current.save();
}
});
let reader_keymap = hooks.use_atom(&crate::state::KEYMAP).read().reader.clone();
hooks.use_keymap_handler(
EventScope::Current,
EventPriority::Normal,
reader_keymap.clone(),
move |action, _key| match action {
ReaderAction::ToggleReadMode => {
is_read_mode.set(!is_read_mode.get());
EventResult::Consumed
}
ReaderAction::ToggleInfo => {
info_modal_open.set(!info_modal_open.get());
EventResult::Consumed
}
ReaderAction::ToggleTts | ReaderAction::ToggleReaderSettings
if !info_modal_open.get() =>
{
let target = if action == ReaderAction::ToggleTts {
Panel::Tts
} else {
Panel::ReaderSettings
};
if is_read_mode.get() {
panel.set(if panel.get() == target {
Panel::None
} else {
target
});
} else {
is_read_mode.set(true);
panel.set(target);
}
EventResult::Consumed
}
_ => EventResult::Ignored,
},
);
if loading.get() {
return element!(Loading(tip:"加载小说中...")).into_any();
}
let chapter_name = novel
.read()
.as_ref()
.and_then(|n| n.get_current_chapter_name().ok())
.unwrap_or_default();
let chapter_percent = novel
.read()
.as_ref()
.and_then(|n| n.chapter_percent().ok())
.unwrap_or_default();
let has_prev = current_chapter.get() > 0;
let has_next = current_chapter.get() + 1 < chapters.read().len();
element!(Fragment {
{ if is_read_mode.get() {
element!(View{
ReadContent(
is_scroll: panel.get() == Panel::None && !info_modal_open.get(),
width: width,
height: height,
content: content.read().clone(),
chapter_name: chapter_name,
chapter_percent: chapter_percent,
is_loading: content_loading.get(),
has_prev: has_prev,
has_next: has_next,
on_next: move |_| {
let new_chapter=current_chapter.get() + 1;
if let Some(novel) = novel.write().as_mut() {
if new_chapter >= chapters.read().len() {
return;
}
if let Err(e) = novel.set_chapter(new_chapter) {
error.write().replace(e);
return;
}
current_chapter.set(new_chapter);
scroll_target.set(ScrollTarget::Ratio(0.0));
}
},
on_prev: move |is_scroll_top| {
if current_chapter.get() == 0 {
return;
}
let new_chapter = current_chapter.get().saturating_sub(1);
if let Some(novel) = novel.write().as_mut() {
if let Err(e) = novel.set_chapter(new_chapter) {
error.write().replace(e);
return;
}
current_chapter.set(new_chapter);
scroll_target.set(if is_scroll_top {
ScrollTarget::ChapterEnd
} else {
ScrollTarget::Ratio(0.0)
});
}
},
scroll_target: scroll_target,
)
TTSManager(
open: panel.get() == Panel::Tts,
is_editing: panel.get() == Panel::Tts && !info_modal_open.get(),
)
ReaderSettingsModal(
open: panel.get() == Panel::ReaderSettings,
is_editing: panel.get() == Panel::ReaderSettings && !info_modal_open.get(),
)
ShortcutInfoModal(
key_shortcut_info: if !info_modal_open.get() {
KeyShortcutInfo::default()
} else {
let dk = |label: &str, action| (label.to_string(), display_keys(&reader_keymap, action));
let sk = |label: &str, keys: &str| (label.to_string(), keys.to_string());
if panel.get() == Panel::Tts {
KeyShortcutInfo(vec![
dk("切换章节选择模式", ReaderAction::ToggleReadMode),
dk("关闭TTS设置模式", ReaderAction::ToggleTts),
sk("上一项", "↑ / K"),
sk("下一项", "↓ / J"),
sk("确认/开始下载", "Enter"),
sk("取消下载", "Esc"),
sk("减小速度/音量", "← / H"),
sk("增大速度/音量", "→ / L"),
sk("切换自动播放", "← / →"),
])
} else if panel.get() == Panel::ReaderSettings {
KeyShortcutInfo(vec![
dk("切换章节选择模式", ReaderAction::ToggleReadMode),
dk("关闭阅读设置", ReaderAction::ToggleReaderSettings),
sk("上一项", "↑ / K"),
sk("下一项", "↓ / J"),
sk("调小/关闭", "← / H"),
sk("调大/开启", "→ / L"),
])
} else {
KeyShortcutInfo(vec![
dk("切换章节选择模式", ReaderAction::ToggleReadMode),
dk("隐藏/显示标题", ReaderAction::ToggleTitle),
dk("打开阅读设置", ReaderAction::ToggleReaderSettings),
dk("打开TTS设置模式", ReaderAction::ToggleTts),
dk("播放/暂停", ReaderAction::TogglePlay),
dk("增大音量", ReaderAction::VolumeUp),
dk("减小音量", ReaderAction::VolumeDown),
dk("向上滚动(章首连按翻上一章)", ReaderAction::ScrollUp),
dk("向下滚动(章末连按翻下一章)", ReaderAction::ScrollDown),
dk("上一章(章首)", ReaderAction::PrevChapter),
dk("下一章", ReaderAction::NextChapter),
dk("上一页", ReaderAction::PageUp),
dk("下一页", ReaderAction::PageDown),
dk("跳到开头", ReaderAction::GoTop),
dk("跳到结尾", ReaderAction::GoBottom),
])
}
},
open: info_modal_open.get(),
)
})
}else{
element!(View(flex_direction:Direction::Horizontal){
SelectChapter(
is_editing: !info_modal_open.get(),
chapters: chapters.read().clone(),
volumes: volumes.read().clone(),
default_value: current_chapter.get(),
on_select: move |index| {
if let Some(novel) = novel.write().as_mut() {
if let Err(e)=novel.set_chapter(index){
error.write().replace(e);
return;
}
current_chapter.set(index);
scroll_target.set(ScrollTarget::Ratio(0.0));
is_read_mode.set(true);
};
},
)
ReadContent(
content: content.read().clone(),
chapter_name: chapter_name,
chapter_percent: chapter_percent,
width: width / 2,
height: height,
is_loading: content_loading.get(),
scroll_target: scroll_target,
)
ShortcutInfoModal(
key_shortcut_info: if !info_modal_open.get() {
KeyShortcutInfo::default()
} else {
KeyShortcutInfo(vec![
("切换阅读模式".to_string(), display_keys(&reader_keymap, ReaderAction::ToggleReadMode)),
("选择上一章".to_string(), "↑ / K".to_string()),
("选择下一章".to_string(), "↓ / J".to_string()),
("确认选择章节".to_string(), "Enter".to_string()),
("搜索章节".to_string(), "S".to_string()),
])
},
open: info_modal_open.get(),
)
})
} }
WarningModal(
tip: format!("加载失败:{:?}", error.read().as_ref()),
is_error: error.read().is_some(),
open: error.read().is_some(),
)
})
.into_any()
}