use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use async_trait::async_trait;
use parse_book_source::{
AuthDecision, BookSource, BrowserFetcher, BrowserOptions, BrowserUi, Engine, FetchMode,
};
use ratatui_kit::Atom;
use tokio::time::sleep;
#[derive(Clone)]
pub enum BrowserPrompt {
Authorize { source_name: String },
Click {
#[allow(dead_code)]
url: String,
cancel: Arc<AtomicBool>,
},
Notice { message: String },
}
static DECISION: Mutex<Option<AuthDecision>> = Mutex::new(None);
pub fn record_decision(decision: AuthDecision) {
if let Ok(mut d) = DECISION.lock() {
*d = Some(decision);
}
}
fn cached_decision() -> Option<AuthDecision> {
DECISION.lock().ok().and_then(|d| *d)
}
fn flag_path() -> Option<std::path::PathBuf> {
crate::utils::novel_catch_dir()
.ok()
.map(|d| d.join("browser_assist.on"))
}
pub fn always_allowed() -> bool {
flag_path().map(|p| p.exists()).unwrap_or(false)
}
pub fn set_always_allowed(on: bool) -> std::io::Result<()> {
let Some(p) = flag_path() else {
return Ok(());
};
if on {
if let Some(dir) = p.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(&p, b"on")?;
} else if p.exists() {
std::fs::remove_file(&p)?;
}
Ok(())
}
pub static BROWSER_PROMPT: Atom<Option<BrowserPrompt>> = Atom::new(|| None);
fn browser_ui() -> Option<Arc<dyn BrowserUi>> {
Some(Arc::new(TuiBrowserUi))
}
struct TuiBrowserUi;
#[async_trait]
impl BrowserUi for TuiBrowserUi {
async fn authorize(&self, source_name: &str) -> AuthDecision {
loop {
if always_allowed() {
return AuthDecision::Always;
}
if let Some(d) = cached_decision() {
return d;
}
{
let st = BROWSER_PROMPT.state();
let mut guard = st.write();
if guard.is_none() {
*guard = Some(BrowserPrompt::Authorize {
source_name: source_name.to_string(),
});
}
}
sleep(Duration::from_millis(150)).await;
}
}
fn prompt_click(&self, url: &str, cancel: Arc<AtomicBool>) {
BROWSER_PROMPT.set(Some(BrowserPrompt::Click {
url: url.to_string(),
cancel,
}));
}
fn notice(&self, message: &str) {
BROWSER_PROMPT.set(Some(BrowserPrompt::Notice {
message: message.to_string(),
}));
}
fn done(&self) {
if !matches!(
BROWSER_PROMPT.state().read().as_ref(),
Some(BrowserPrompt::Notice { .. })
) {
BROWSER_PROMPT.set(None);
}
}
}
pub fn build_engine(source: BookSource) -> parse_book_source::Result<Engine> {
let state = crate::cache::load_source_state(&source.url);
let engine = if matches!(source.http.fetcher, FetchMode::Reqwest) {
Engine::new(source)?
} else {
let mut opts = BrowserOptions::default();
if let Ok(dir) = crate::utils::novel_catch_dir() {
opts.profile_dir = dir.join("browser-profile");
}
opts.total_timeout = Duration::from_secs(90);
opts.ui = browser_ui();
match BrowserFetcher::detect(opts) {
Some(browser) => Engine::with_browser_assist(source, Some(browser))?,
None => Engine::new(source)?,
}
};
Ok(engine
.with_login_header(state.login_header)
.with_cookies(&state.cookies))
}