use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
use async_trait::async_trait;
use parse_book_source::{
AuthDecision, BookSource, BrowserFetcher, BrowserOptions, BrowserUi, Engine, FetchMode,
};
use ratatui_kit::prelude::State;
use tokio::time::sleep;
#[derive(Clone)]
pub enum BrowserPrompt {
Authorize { source_name: String },
Click {
#[allow(dead_code)]
url: String,
cancel: Arc<AtomicBool>,
},
}
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(())
}
static BROWSER_UI: OnceLock<Arc<dyn BrowserUi>> = OnceLock::new();
pub fn init_browser_ui(state: State<Option<BrowserPrompt>>) {
let _ = BROWSER_UI.set(Arc::new(TuiBrowserUi { state }));
}
fn browser_ui() -> Option<Arc<dyn BrowserUi>> {
BROWSER_UI.get().cloned()
}
struct TuiBrowserUi {
state: State<Option<BrowserPrompt>>,
}
#[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 mut st = self.state.write();
if st.is_none() {
*st = Some(BrowserPrompt::Authorize {
source_name: source_name.to_string(),
});
}
}
sleep(Duration::from_millis(150)).await;
}
}
fn prompt_click(&self, url: &str, cancel: Arc<AtomicBool>) {
*self.state.write() = Some(BrowserPrompt::Click {
url: url.to_string(),
cancel,
});
}
fn done(&self) {
*self.state.write() = None;
}
}
pub fn build_engine(source: BookSource) -> parse_book_source::Result<Engine> {
if matches!(source.http.fetcher, FetchMode::Reqwest) {
return Engine::new(source);
}
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),
}
}