|
|
|
|
@ -24,6 +24,7 @@ const SETTLE_GRACE_MS: u64 = 1200;
|
|
|
|
|
/// Global state shared across commands (just the shared HTTP client for image proxying)
|
|
|
|
|
struct AppState {
|
|
|
|
|
client: reqwest::Client,
|
|
|
|
|
latest_cookies: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static STATE: std::sync::OnceLock<Arc<Mutex<AppState>>> = std::sync::OnceLock::new();
|
|
|
|
|
@ -50,7 +51,7 @@ fn get_state() -> Arc<Mutex<AppState>> {
|
|
|
|
|
.build()
|
|
|
|
|
.expect("Failed to build HTTP client");
|
|
|
|
|
|
|
|
|
|
Arc::new(Mutex::new(AppState { client }))
|
|
|
|
|
Arc::new(Mutex::new(AppState { client, latest_cookies: None }))
|
|
|
|
|
}).clone()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@ -82,6 +83,7 @@ struct HarvestState {
|
|
|
|
|
prev: Option<String>,
|
|
|
|
|
next: Option<String>,
|
|
|
|
|
href: Option<String>,
|
|
|
|
|
cookies: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Injected at document start: watches the DOM for the chapter's images to
|
|
|
|
|
@ -94,6 +96,28 @@ const INIT_SCRIPT: &str = r#"
|
|
|
|
|
window.__manaInstalled = true;
|
|
|
|
|
window.__mana = { ready: false };
|
|
|
|
|
|
|
|
|
|
// === Anti-bot bypass: spoof viewport/screen dimensions ===
|
|
|
|
|
// The site's ad-guard WASM reads screen/window dimensions for fingerprinting.
|
|
|
|
|
// Override them so a small window appears as a full-size browser.
|
|
|
|
|
try {
|
|
|
|
|
var FAKE_W = 1920, FAKE_H = 1080;
|
|
|
|
|
Object.defineProperty(window, 'innerWidth', { get: function(){ return FAKE_W; }, configurable: true });
|
|
|
|
|
Object.defineProperty(window, 'innerHeight', { get: function(){ return FAKE_H; }, configurable: true });
|
|
|
|
|
Object.defineProperty(window, 'outerWidth', { get: function(){ return FAKE_W; }, configurable: true });
|
|
|
|
|
Object.defineProperty(window, 'outerHeight', { get: function(){ return FAKE_H; }, configurable: true });
|
|
|
|
|
Object.defineProperty(document.documentElement, 'clientWidth', { get: function(){ return FAKE_W; }, configurable: true });
|
|
|
|
|
Object.defineProperty(document.documentElement, 'clientHeight', { get: function(){ return FAKE_H; }, configurable: true });
|
|
|
|
|
if (window.screen) {
|
|
|
|
|
Object.defineProperty(screen, 'width', { get: function(){ return FAKE_W; }, configurable: true });
|
|
|
|
|
Object.defineProperty(screen, 'height', { get: function(){ return FAKE_H; }, configurable: true });
|
|
|
|
|
Object.defineProperty(screen, 'availWidth', { get: function(){ return FAKE_W; }, configurable: true });
|
|
|
|
|
Object.defineProperty(screen, 'availHeight',{ get: function(){ return FAKE_H; }, configurable: true });
|
|
|
|
|
}
|
|
|
|
|
// Spoof Page Visibility API — always report visible
|
|
|
|
|
Object.defineProperty(document, 'hidden', { get: function(){ return false; }, configurable: true });
|
|
|
|
|
Object.defineProperty(document, 'visibilityState', { get: function(){ return 'visible'; }, configurable: true });
|
|
|
|
|
} catch(e) {}
|
|
|
|
|
|
|
|
|
|
var AD_HINTS = /ad|banner|gnb|lnb|footer|header|sns|menu|logo|popup|share|comment/i;
|
|
|
|
|
// Filename/path hints for non-page images (ads, banners, UI chrome,
|
|
|
|
|
// lazy-load placeholders) — checked against the resolved image URL itself,
|
|
|
|
|
@ -283,7 +307,7 @@ const INIT_SCRIPT: &str = r#"
|
|
|
|
|
|
|
|
|
|
/// Reads back the collector's current snapshot. Always resolves to an object
|
|
|
|
|
/// (never throws) so `eval_with_callback` has something to serialize.
|
|
|
|
|
const COLLECTOR_JS: &str = r#"(function(){ try { return window.__mana || {ready:false}; } catch(e) { return {ready:false}; } })()"#;
|
|
|
|
|
const COLLECTOR_JS: &str = r#"(function(){ try { var m = window.__mana || {ready:false}; m.cookies = document.cookie; return m; } catch(e) { return {ready:false}; } })()"#;
|
|
|
|
|
|
|
|
|
|
async fn get_or_create_harvester(
|
|
|
|
|
app: &AppHandle,
|
|
|
|
|
@ -302,14 +326,26 @@ async fn get_or_create_harvester(
|
|
|
|
|
|
|
|
|
|
let win = WebviewWindowBuilder::new(app, HARVESTER_LABEL, WebviewUrl::External(url.clone()))
|
|
|
|
|
.title("Mana Viewer — site session")
|
|
|
|
|
.inner_size(420.0, 620.0)
|
|
|
|
|
.position(24.0, 24.0)
|
|
|
|
|
.inner_size(200.0, 200.0)
|
|
|
|
|
.position(1600.0, 900.0)
|
|
|
|
|
.decorations(false)
|
|
|
|
|
.focused(false)
|
|
|
|
|
.visible(true)
|
|
|
|
|
.initialization_script(INIT_SCRIPT)
|
|
|
|
|
.build()
|
|
|
|
|
.map_err(|e| e.to_string())?;
|
|
|
|
|
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
{
|
|
|
|
|
use objc::{msg_send, sel, sel_impl};
|
|
|
|
|
if let Ok(ns_window) = win.ns_window() {
|
|
|
|
|
let ns_window = ns_window as *mut objc::runtime::Object;
|
|
|
|
|
unsafe {
|
|
|
|
|
let _: () = msg_send![ns_window, setAlphaValue: 0.0f64];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Debug builds only: open the Web Inspector for the harvester so its
|
|
|
|
|
// console/DOM can be inspected directly — useful while tuning the
|
|
|
|
|
// collector heuristics.
|
|
|
|
|
@ -401,6 +437,7 @@ async fn poll_harvest(app: AppHandle, req_id: u64, url: String) {
|
|
|
|
|
next_chapter: state.next.clone(),
|
|
|
|
|
title: state.title.clone(),
|
|
|
|
|
series_url: state.href.as_deref().and_then(extract_series_url),
|
|
|
|
|
cookies: state.cookies.clone(),
|
|
|
|
|
};
|
|
|
|
|
let _ = app.emit(
|
|
|
|
|
"chapter-update",
|
|
|
|
|
@ -419,19 +456,42 @@ async fn poll_harvest(app: AppHandle, req_id: u64, url: String) {
|
|
|
|
|
next_chapter: state.next,
|
|
|
|
|
title: state.title,
|
|
|
|
|
series_url: state.href.as_deref().and_then(extract_series_url),
|
|
|
|
|
cookies: state.cookies,
|
|
|
|
|
};
|
|
|
|
|
eprintln!("[harvest#{}] ready with {} images", req_id, chapter.images.len());
|
|
|
|
|
|
|
|
|
|
// Save harvested cookies so fetch_image can use them
|
|
|
|
|
if let Some(cookies) = &chapter.cookies {
|
|
|
|
|
let state = get_state();
|
|
|
|
|
let mut state_lock = state.lock().await;
|
|
|
|
|
state_lock.latest_cookies = Some(cookies.clone());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let _ = app.emit(
|
|
|
|
|
"chapter-ready",
|
|
|
|
|
serde_json::json!({ "reqId": req_id, "data": chapter, "href": state.href }),
|
|
|
|
|
);
|
|
|
|
|
let _ = win.hide();
|
|
|
|
|
// Don't hide — the site detects hide/show cycles and blocks
|
|
|
|
|
// image loading. The window stays small (200x200) in the corner.
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !attention_sent && start.elapsed() >= Duration::from_secs(STALL_BEFORE_ATTENTION_SECS) {
|
|
|
|
|
eprintln!("[harvest#{}] stalled, showing harvester window", req_id);
|
|
|
|
|
// Resize to usable dimensions so the user can solve the captcha
|
|
|
|
|
let _ = win.set_size(tauri::Size::Logical(tauri::LogicalSize { width: 420.0, height: 620.0 }));
|
|
|
|
|
let _ = win.set_position(tauri::Position::Logical(tauri::LogicalPosition { x: 24.0, y: 24.0 }));
|
|
|
|
|
#[cfg(target_os = "macos")]
|
|
|
|
|
{
|
|
|
|
|
use objc::{msg_send, sel, sel_impl};
|
|
|
|
|
if let Ok(ns_window) = win.ns_window() {
|
|
|
|
|
let ns_window = ns_window as *mut objc::runtime::Object;
|
|
|
|
|
unsafe {
|
|
|
|
|
let _: () = msg_send![ns_window, setAlphaValue: 1.0f64];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let _ = win.show();
|
|
|
|
|
let _ = win.set_focus();
|
|
|
|
|
let _ = app.emit(
|
|
|
|
|
@ -464,11 +524,16 @@ pub async fn fetch_image(url: String, referer: String) -> Result<String, String>
|
|
|
|
|
let state = get_state();
|
|
|
|
|
let state_lock = state.lock().await;
|
|
|
|
|
|
|
|
|
|
let response = state_lock.client
|
|
|
|
|
let mut req = state_lock.client
|
|
|
|
|
.get(&url)
|
|
|
|
|
.header(REFERER, &referer)
|
|
|
|
|
.header("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8")
|
|
|
|
|
.send()
|
|
|
|
|
.header("Accept", "image/avif,image/webp,image/apng,image/*,*/*;q=0.8");
|
|
|
|
|
|
|
|
|
|
if let Some(cookies) = &state_lock.latest_cookies {
|
|
|
|
|
req = req.header("Cookie", cookies);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let response = req.send()
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|e| format!("Image fetch error: {}", e))?;
|
|
|
|
|
|
|
|
|
|
|