use crate::browser_assist::BrowserPrompt;
use crate::theme::AppChromeTheme;
use crossterm::event::{Event, KeyCode, KeyEventKind};
use parse_book_source::AuthDecision;
use ratatui::{
layout::{Alignment, Constraint, Margin},
style::Style,
text::Line,
};
use ratatui_kit::{
AnyElement, AtomState, EventPriority, EventResult, EventScope, Hooks, UseAtom, UseEventHandler,
UseInputLayer, UseTheme, component, element,
prelude::{Border, Modal, Text, View},
};
use std::sync::atomic::Ordering;
#[component]
pub fn BrowserPromptModal(mut hooks: Hooks) -> impl Into<AnyElement<'static>> {
let theme = hooks.use_component_theme::<AppChromeTheme>();
let state = hooks.use_atom(&crate::browser_assist::BROWSER_PROMPT);
let open = state.read().is_some();
let layer = hooks.use_input_layer(open, true);
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;
}
let variant = match state.read().as_ref() {
Some(BrowserPrompt::Authorize { .. }) => 1u8,
Some(BrowserPrompt::Click { .. }) => 2u8,
Some(BrowserPrompt::Notice { .. }) => 3u8,
None => return EventResult::Ignored,
};
match (variant, key.code) {
(1, KeyCode::Char('y') | KeyCode::Char('Y')) => {
respond(state, AuthDecision::Once, false);
EventResult::Consumed
}
(1, KeyCode::Char('a') | KeyCode::Char('A')) => {
respond(state, AuthDecision::Always, true);
EventResult::Consumed
}
(1, KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc) => {
respond(state, AuthDecision::Deny, false);
EventResult::Consumed
}
(2, KeyCode::Esc) => {
cancel_click(state);
EventResult::Consumed
}
(3, KeyCode::Enter | KeyCode::Esc) => {
*state.write() = None;
EventResult::Consumed
}
_ => EventResult::Ignored,
}
},
);
let (title, content) = match state.read().as_ref() {
Some(BrowserPrompt::Authorize { source_name, .. }) => (
"需要浏览器验证".to_string(),
format!(
"书源「{source_name}」遇到人机验证。\n\n是否打开系统浏览器协助通过?\n\n[Y] 本次允许 [A] 总是允许 [N] 拒绝"
),
),
Some(BrowserPrompt::Click { .. }) => (
"请在浏览器中确认".to_string(),
"请在弹出的浏览器窗口里点一下「确认您是真人」。\n完成后会自动继续。\n\n[Esc] 取消"
.to_string(),
),
Some(BrowserPrompt::Notice { message }) => (
"浏览器会话提醒".to_string(),
format!("{message}\n\n[Enter/Esc] 关闭"),
),
None => (String::new(), String::new()),
};
element!(Modal(
open: open,
layer: Some(layer),
width: Constraint::Percentage(60),
height: Constraint::Length(11),
style: Style::default().dim()
){
Border(
border_style: theme.title,
top_title: Some(Line::from(title).style(theme.title).centered())
){
View(margin: Margin::new(2, 2)){
Text(
text: content,
style: theme.text,
alignment: Alignment::Center,
)
}
}
})
}
fn respond(state: AtomState<Option<BrowserPrompt>>, decision: AuthDecision, persist_always: bool) {
if persist_always {
let _ = crate::browser_assist::set_always_allowed(true);
} else {
crate::browser_assist::record_decision(decision);
}
*state.write() = None;
}
fn cancel_click(state: AtomState<Option<BrowserPrompt>>) {
if let Some(BrowserPrompt::Click { cancel, .. }) = state.read().clone() {
cancel.store(true, Ordering::Relaxed);
}
*state.write() = None;
}