use crossterm::event::{Event, KeyCode, KeyEventKind};
use parse_book_source::{BookSource, LoginSignal, source::RowUiType};
use ratatui::{
layout::{Constraint, Margin},
text::{Line, Span},
widgets::Paragraph,
};
use ratatui_kit::prelude::*;
use std::sync::atomic::Ordering;
use tui_input::{Input, backend::crossterm::EventHandler};
use crate::{
components::{Loading, WarningModal},
errors::Errors,
hooks::UseInitState,
login::{browser_login, is_script_login, login_info_json, script_login},
theme::AppChromeTheme,
};
#[component]
pub fn BookSourceLogin(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let source = (*hooks.use_route_state::<BookSource>()).clone();
let theme = hooks.use_component_theme::<AppChromeTheme>();
let mut navigate = hooks.use_navigate();
let layer = hooks.use_input_layer(true, true);
let ui = source.login_ui.clone();
let n = ui.len();
let script = is_script_login(&source);
let misconfigured = !script && source.login_url.trim().is_empty() && n > 0;
let fields = hooks.use_state(|| ui.iter().map(|_| Input::default()).collect::<Vec<Input>>());
let mut active = hooks.use_state(|| 0usize);
let mut submit = hooks.use_state(|| 0u32);
let signal = hooks.use_state(LoginSignal::default);
let mut browsering = hooks.use_state(|| false);
let mut in_flight = hooks.use_state(|| false);
let (_done, loading, error) = hooks.use_effect_state(
{
let source = source.clone();
async move {
if submit.get() == 0 {
return Ok::<(), Errors>(());
}
let result = async {
if script {
let info = if n > 0 {
let pairs: Vec<(String, String)> = source
.login_ui
.iter()
.zip(fields.read().iter())
.map(|(row, inp)| (row.name.clone(), inp.value().to_string()))
.collect();
Some(login_info_json(&pairs))
} else {
None
};
script_login(source.clone(), info).await?;
} else {
let sig = signal.read().clone();
browser_login(source.clone(), sig).await?;
}
Ok::<(), Errors>(())
}
.await;
in_flight.set(false);
result
}
},
submit.get(),
);
hooks.use_effect(
{
let source = source.clone();
move || {
if submit.get() > 0 && !loading.get() && error.read().is_none() {
navigate.push_with_state("/select-books", source.clone());
}
}
},
loading.get(),
);
hooks.use_event_handler(
EventScope::Layer(layer),
EventPriority::Normal,
move |event| {
let Event::Key(key) = event else {
return EventResult::Ignored;
};
if key.kind != KeyEventKind::Press {
return EventResult::Ignored;
}
if in_flight.get() {
if browsering.get() {
match key.code {
KeyCode::Enter if loading.get() => {
signal.read().done.store(true, Ordering::Relaxed)
}
KeyCode::Esc if loading.get() => {
signal.read().cancel.store(true, Ordering::Relaxed)
}
_ => {}
}
} else if key.code == KeyCode::Esc {
navigate.push("/book-source");
}
return EventResult::Consumed;
}
match key.code {
KeyCode::Esc => {
navigate.push("/book-source");
EventResult::Consumed
}
KeyCode::Down | KeyCode::Tab if n > 0 => {
active.set((active.get() + 1) % n);
EventResult::Consumed
}
KeyCode::Up | KeyCode::BackTab if n > 0 => {
active.set((active.get() + n - 1) % n);
EventResult::Consumed
}
KeyCode::Enter => {
if misconfigured {
return EventResult::Consumed;
}
error.write().take();
if !script {
browsering.set(true);
}
in_flight.set(true);
submit.set(submit.get() + 1);
EventResult::Consumed
}
_ if n > 0 => {
let i = active.get().min(n - 1);
fields.write()[i].handle_event(&event);
EventResult::Consumed
}
_ => EventResult::Ignored,
}
},
);
let active_idx = active.get();
let mut lines: Vec<Line> = Vec::new();
for (i, row) in ui.iter().enumerate() {
let raw = fields.read()[i].value().to_string();
let shown = if row.ui_type == RowUiType::Password {
"•".repeat(raw.chars().count())
} else {
raw
};
let cursor = if i == active_idx { "▏" } else { "" };
let label_style = if i == active_idx {
theme.meta_label.bold()
} else {
theme.meta_label
};
lines.push(Line::from(vec![
Span::from(format!("{}: ", row.name)).style(label_style),
Span::from(format!("{shown}{cursor}")).style(theme.text),
]));
lines.push(Line::from(""));
}
if n == 0 {
let tip = if script {
"此书源为脚本登录(无需填写表单)。按 Enter 开始登录。"
} else {
"此书源为浏览器登录。按 Enter 打开系统浏览器,在真实页面登录后回到此处按 Enter 完成。"
};
lines.push(Line::from(tip).style(theme.text));
}
if misconfigured {
lines.push(
Line::from("书源配置错误:仅配置 loginUi 而无登录脚本/loginUrl,无法执行登录。")
.style(theme.text.bold()),
);
}
let hint = if misconfigured {
"该书源登录配置不完整(联系书源作者修正)· Esc 返回"
} else if in_flight.get() && !browsering.get() {
"脚本登录中... Esc 放弃等待并返回"
} else if browsering.get() && loading.get() {
"浏览器登录中:在弹出的浏览器里登录,完成后按 Enter,取消按 Esc"
} else if script {
"↑/↓/Tab 切换字段 · 输入凭据 · Enter 登录 · Esc 返回"
} else {
"Enter 打开浏览器登录 · Esc 返回"
};
let body = Paragraph::new(lines);
element!(Border(
top_title: Line::from(format!("书源登录 · {}", source.name)).centered().style(theme.title),
border_style: theme.border,
){
View(margin: Margin::new(2, 1), flex_direction: ratatui::layout::Direction::Vertical){
View(height: Constraint::Fill(1)){
Text(text: body, style: theme.text)
}
View(height: Constraint::Length(1)){
Text(text: Paragraph::new(Line::from(hint).style(theme.meta_label)))
}
}
{if loading.get() {
element!(Loading(tip: if browsering.get() {"等待浏览器登录..."} else {"登录中..."})).into_any()
} else {
element!(Fragment).into_any()
}}
WarningModal(
tip: format!("登录失败:{}", error.read().as_ref().map(|e| e.to_string()).unwrap_or_default()),
is_error: error.read().is_some(),
open: error.read().is_some(),
on_close: move |_| {
error.write().take();
},
)
})
}