feat: implement site profile template parsing system with JSON editor UI
parent
0f98717c8f
commit
3b8e50ac6f
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"id": "_default",
|
||||||
|
"name": "기본 (범용)",
|
||||||
|
"version": 1,
|
||||||
|
"domains": ["*"],
|
||||||
|
"selectors": {
|
||||||
|
"images": {
|
||||||
|
"containerSelector": null,
|
||||||
|
"imgSelector": "img",
|
||||||
|
"srcAttributes": ["data-src", "data-original", "data-lazy-src", "data-lazy", "currentSrc", "src"],
|
||||||
|
"pageOrderAttribute": null,
|
||||||
|
"minWidth": 150,
|
||||||
|
"excludeLandscape": true
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"prevSelectors": [".theme-viewer-prev", ".btn-prev", ".prev", "a.prev"],
|
||||||
|
"nextSelectors": [".theme-viewer-next", ".btn-next", ".next", "a.next"],
|
||||||
|
"prevText": "이전화",
|
||||||
|
"nextText": "다음화"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"selectors": [".toon-title", "h1.title", "h1"],
|
||||||
|
"fallbackToDocumentTitle": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"adClassPattern": "ad|banner|gnb|lnb|footer|header|sns|menu|logo|popup|share|comment",
|
||||||
|
"adFilenamePattern": "(^|[\\/_.\\-])(ad|ads|banner|event|popup|logo|gnb|lnb|loading|spinner|placeholder|blank|noimage|no_image|notice|thumb|sns|share|button|btn|icon|dummy)([\\/_.\\-]|$)"
|
||||||
|
},
|
||||||
|
"antiBot": {
|
||||||
|
"spoofViewport": true,
|
||||||
|
"spoofVisibility": true,
|
||||||
|
"fakeWidth": 1920,
|
||||||
|
"fakeHeight": 1080
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"needsCookies": true,
|
||||||
|
"refererPolicy": "origin"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"id": "newtoki",
|
||||||
|
"name": "뉴토끼",
|
||||||
|
"version": 1,
|
||||||
|
"domains": ["newtoki*.org", "newtoki*.com", "manatoki*.net", "manatoki*.org"],
|
||||||
|
"selectors": {
|
||||||
|
"images": {
|
||||||
|
"containerSelector": null,
|
||||||
|
"imgSelector": "img",
|
||||||
|
"srcAttributes": ["data-theme-page", "data-src", "data-original", "data-lazy-src", "data-lazy", "currentSrc", "src"],
|
||||||
|
"pageOrderAttribute": "data-theme-page",
|
||||||
|
"minWidth": 150,
|
||||||
|
"excludeLandscape": true
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"prevSelectors": [".theme-viewer-prev", ".btn-prev", ".prev"],
|
||||||
|
"nextSelectors": [".theme-viewer-next", ".btn-next", ".next"],
|
||||||
|
"prevText": "이전화",
|
||||||
|
"nextText": "다음화"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"selectors": [".toon-title", "h1.title"],
|
||||||
|
"fallbackToDocumentTitle": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"adClassPattern": "ad|banner|gnb|lnb|footer|header|sns|menu|logo|popup|share|comment",
|
||||||
|
"adFilenamePattern": "(^|[\\/_.\\-])(ad|ads|banner|event|popup|logo|gnb|lnb|loading|spinner|placeholder|blank|noimage|no_image|notice|thumb|sns|share|button|btn|icon|dummy)([\\/_.\\-]|$)"
|
||||||
|
},
|
||||||
|
"antiBot": {
|
||||||
|
"spoofViewport": true,
|
||||||
|
"spoofVisibility": true,
|
||||||
|
"fakeWidth": 1920,
|
||||||
|
"fakeHeight": 1080
|
||||||
|
},
|
||||||
|
"http": {
|
||||||
|
"needsCookies": true,
|
||||||
|
"refererPolicy": "origin"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,490 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use tauri::{AppHandle, Manager};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ImageSelectorConfig {
|
||||||
|
pub container_selector: Option<String>,
|
||||||
|
pub img_selector: String,
|
||||||
|
pub src_attributes: Vec<String>,
|
||||||
|
pub page_order_attribute: Option<String>,
|
||||||
|
pub min_width: u32,
|
||||||
|
pub exclude_landscape: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct NavigationSelectorConfig {
|
||||||
|
pub prev_selectors: Vec<String>,
|
||||||
|
pub next_selectors: Vec<String>,
|
||||||
|
pub prev_text: Option<String>,
|
||||||
|
pub next_text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct TitleSelectorConfig {
|
||||||
|
pub selectors: Vec<String>,
|
||||||
|
pub fallback_to_document_title: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SelectorsConfig {
|
||||||
|
pub images: ImageSelectorConfig,
|
||||||
|
pub navigation: NavigationSelectorConfig,
|
||||||
|
pub title: TitleSelectorConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct FiltersConfig {
|
||||||
|
pub ad_class_pattern: String,
|
||||||
|
pub ad_filename_pattern: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AntiBotConfig {
|
||||||
|
pub spoof_viewport: bool,
|
||||||
|
pub spoof_visibility: bool,
|
||||||
|
pub fake_width: u32,
|
||||||
|
pub fake_height: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct HttpConfig {
|
||||||
|
pub needs_cookies: bool,
|
||||||
|
pub referer_policy: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct SiteProfile {
|
||||||
|
pub id: String,
|
||||||
|
pub name: String,
|
||||||
|
pub version: u32,
|
||||||
|
pub domains: Vec<String>,
|
||||||
|
pub selectors: SelectorsConfig,
|
||||||
|
pub filters: FiltersConfig,
|
||||||
|
pub anti_bot: AntiBotConfig,
|
||||||
|
pub http: HttpConfig,
|
||||||
|
#[serde(default)]
|
||||||
|
pub is_custom: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ProfileManager {
|
||||||
|
builtin_profiles: Vec<SiteProfile>,
|
||||||
|
custom_profiles: Vec<SiteProfile>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn glob_match(pattern: &str, target: &str) -> bool {
|
||||||
|
if pattern == "*" {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
let parts: Vec<&str> = pattern.split('*').collect();
|
||||||
|
if parts.len() == 1 {
|
||||||
|
return pattern.eq_ignore_ascii_case(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut remaining = target;
|
||||||
|
for (i, part) in parts.iter().enumerate() {
|
||||||
|
if part.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
if !remaining.to_lowercase().starts_with(&part.to_lowercase()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
remaining = &remaining[part.len()..];
|
||||||
|
} else if i == parts.len() - 1 {
|
||||||
|
return remaining.to_lowercase().ends_with(&part.to_lowercase());
|
||||||
|
} else {
|
||||||
|
if let Some(pos) = remaining.to_lowercase().find(&part.to_lowercase()) {
|
||||||
|
remaining = &remaining[pos + part.len()..];
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ProfileManager {
|
||||||
|
pub fn new(app: Option<&AppHandle>) -> Self {
|
||||||
|
let default_json = include_str!("../profiles/_default.json");
|
||||||
|
let newtoki_json = include_str!("../profiles/newtoki.json");
|
||||||
|
|
||||||
|
let default_prof: SiteProfile = serde_json::from_str(default_json).expect("Invalid _default.json");
|
||||||
|
let newtoki_prof: SiteProfile = serde_json::from_str(newtoki_json).expect("Invalid newtoki.json");
|
||||||
|
|
||||||
|
let builtin_profiles = vec![newtoki_prof, default_prof];
|
||||||
|
let mut manager = Self {
|
||||||
|
builtin_profiles,
|
||||||
|
custom_profiles: Vec::new(),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(app_handle) = app {
|
||||||
|
manager.load_custom_profiles(app_handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
manager
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_user_profiles_dir(app: &AppHandle) -> Option<PathBuf> {
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."));
|
||||||
|
let repo_dir = if cwd.join("src-tauri").exists() {
|
||||||
|
cwd.join("src-tauri").join("profiles")
|
||||||
|
} else if cwd.ends_with("src-tauri") {
|
||||||
|
cwd.join("profiles")
|
||||||
|
} else {
|
||||||
|
cwd.join("src-tauri").join("profiles")
|
||||||
|
};
|
||||||
|
if repo_dir.exists() || fs::create_dir_all(&repo_dir).is_ok() {
|
||||||
|
Some(repo_dir)
|
||||||
|
} else {
|
||||||
|
app.path().app_data_dir().ok().map(|dir| dir.join("profiles"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_custom_profiles(&mut self, app: &AppHandle) {
|
||||||
|
self.custom_profiles.clear();
|
||||||
|
if let Some(dir) = Self::get_user_profiles_dir(app) {
|
||||||
|
if dir.exists() {
|
||||||
|
if let Ok(entries) = fs::read_dir(dir) {
|
||||||
|
for entry in entries.flatten() {
|
||||||
|
let path = entry.path();
|
||||||
|
if path.extension().and_then(|s| s.to_str()) == Some("json") {
|
||||||
|
if let Ok(content) = fs::read_to_string(&path) {
|
||||||
|
if let Ok(mut prof) = serde_json::from_str::<SiteProfile>(&content) {
|
||||||
|
prof.is_custom = true;
|
||||||
|
self.custom_profiles.push(prof);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list_profiles(&self) -> Vec<SiteProfile> {
|
||||||
|
let mut list = self.custom_profiles.clone();
|
||||||
|
for b in &self.builtin_profiles {
|
||||||
|
if !list.iter().any(|p| p.id == b.id) {
|
||||||
|
list.push(b.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn find_profile_for_url(&self, url_str: &str) -> SiteProfile {
|
||||||
|
let host = Url::parse(url_str)
|
||||||
|
.ok()
|
||||||
|
.and_then(|u| u.host_str().map(|h| h.to_string()))
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// 1. Check custom profiles first
|
||||||
|
for prof in &self.custom_profiles {
|
||||||
|
for pattern in &prof.domains {
|
||||||
|
if glob_match(pattern, &host) {
|
||||||
|
return prof.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check built-in profiles (excluding default fallback)
|
||||||
|
for prof in &self.builtin_profiles {
|
||||||
|
if prof.id == "_default" {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for pattern in &prof.domains {
|
||||||
|
if glob_match(pattern, &host) {
|
||||||
|
return prof.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fallback to _default
|
||||||
|
self.builtin_profiles
|
||||||
|
.iter()
|
||||||
|
.find(|p| p.id == "_default")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| self.builtin_profiles[0].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save_custom_profile(&mut self, app: &AppHandle, mut profile: SiteProfile) -> Result<(), String> {
|
||||||
|
profile.is_custom = true;
|
||||||
|
let dir = Self::get_user_profiles_dir(app).ok_or("Failed to get app_data_dir")?;
|
||||||
|
fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
let file_path = dir.join(format!("{}.json", profile.id));
|
||||||
|
let content = serde_json::to_string_pretty(&profile).map_err(|e| e.to_string())?;
|
||||||
|
fs::write(file_path, content).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
self.load_custom_profiles(app);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn delete_custom_profile(&mut self, app: &AppHandle, id: &str) -> Result<(), String> {
|
||||||
|
let dir = Self::get_user_profiles_dir(app).ok_or("Failed to get app_data_dir")?;
|
||||||
|
let file_path = dir.join(format!("{}.json", id));
|
||||||
|
if file_path.exists() {
|
||||||
|
fs::remove_file(file_path).map_err(|e| e.to_string())?;
|
||||||
|
}
|
||||||
|
self.load_custom_profiles(app);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_init_script(profile: &SiteProfile) -> String {
|
||||||
|
let src_attrs_json = serde_json::to_string(&profile.selectors.images.src_attributes).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let prev_selectors_json = serde_json::to_string(&profile.selectors.navigation.prev_selectors).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let next_selectors_json = serde_json::to_string(&profile.selectors.navigation.next_selectors).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
let title_selectors_json = serde_json::to_string(&profile.selectors.title.selectors).unwrap_or_else(|_| "[]".to_string());
|
||||||
|
|
||||||
|
let page_order_attr_js = match &profile.selectors.images.page_order_attribute {
|
||||||
|
Some(attr) => format!("'{}'", attr),
|
||||||
|
None => "null".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let prev_text_js = match &profile.selectors.navigation.prev_text {
|
||||||
|
Some(txt) => format!("'{}'", txt),
|
||||||
|
None => "null".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let next_text_js = match &profile.selectors.navigation.next_text {
|
||||||
|
Some(txt) => format!("'{}'", txt),
|
||||||
|
None => "null".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let spoof_viewport_js = if profile.anti_bot.spoof_viewport {
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
try {{
|
||||||
|
var FAKE_W = {}, FAKE_H = {};
|
||||||
|
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 }});
|
||||||
|
}}
|
||||||
|
}} catch(e) {{}}
|
||||||
|
"#,
|
||||||
|
profile.anti_bot.fake_width, profile.anti_bot.fake_height
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let spoof_visibility_js = if profile.anti_bot.spoof_visibility {
|
||||||
|
r#"
|
||||||
|
try {
|
||||||
|
Object.defineProperty(document, 'hidden', { get: function(){ return false; }, configurable: true });
|
||||||
|
Object.defineProperty(document, 'visibilityState', { get: function(){ return 'visible'; }, configurable: true });
|
||||||
|
} catch(e) {}
|
||||||
|
"#
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
};
|
||||||
|
|
||||||
|
format!(
|
||||||
|
r#"(function() {{
|
||||||
|
if (window.__manaInstalled) return;
|
||||||
|
window.__manaInstalled = true;
|
||||||
|
window.__mana = {{ ready: false }};
|
||||||
|
|
||||||
|
// Anti-bot spoofing
|
||||||
|
{spoof_viewport_js}
|
||||||
|
{spoof_visibility_js}
|
||||||
|
|
||||||
|
var FILENAME_AD_HINTS = new RegExp({ad_filename_pattern:?}, 'i');
|
||||||
|
|
||||||
|
function isLikelyPageImage(src) {{
|
||||||
|
try {{
|
||||||
|
var u = new URL(src, location.href);
|
||||||
|
if (FILENAME_AD_HINTS.test(u.pathname)) return false;
|
||||||
|
}} catch (e) {{}}
|
||||||
|
return true;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function isValidLink(href) {{
|
||||||
|
return !!href && href !== '#' && href.indexOf('javascript:') !== 0 && href.indexOf('mailto:') !== 0;
|
||||||
|
}}
|
||||||
|
|
||||||
|
function resolveUrl(href) {{
|
||||||
|
try {{ return new URL(href, location.href).href; }} catch (e) {{ return href; }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
function findNav() {{
|
||||||
|
var prev = null, next = null;
|
||||||
|
var prevSels = {prev_selectors_json};
|
||||||
|
var nextSels = {next_selectors_json};
|
||||||
|
var prevTxt = {prev_text_js};
|
||||||
|
var nextTxt = {next_text_js};
|
||||||
|
|
||||||
|
for (var i = 0; i < Math.max(prevSels.length, nextSels.length); i++) {{
|
||||||
|
var p = prevSels[i] ? document.querySelector(prevSels[i]) : null;
|
||||||
|
var n = nextSels[i] ? document.querySelector(nextSels[i]) : null;
|
||||||
|
var pHref = p && p.getAttribute('href');
|
||||||
|
var nHref = n && n.getAttribute('href');
|
||||||
|
if (!prev && pHref && isValidLink(pHref)) prev = resolveUrl(pHref);
|
||||||
|
if (!next && nHref && isValidLink(nHref)) next = resolveUrl(nHref);
|
||||||
|
if (prev && next) break;
|
||||||
|
}}
|
||||||
|
|
||||||
|
if (!prev || !next) {{
|
||||||
|
var links = document.querySelectorAll('a');
|
||||||
|
for (var j = 0; j < links.length; j++) {{
|
||||||
|
var a = links[j];
|
||||||
|
var href = a.getAttribute('href');
|
||||||
|
if (!href || !isValidLink(href)) continue;
|
||||||
|
var text = (a.textContent || '').trim();
|
||||||
|
if (!prev && prevTxt && text.indexOf(prevTxt) !== -1) prev = resolveUrl(href);
|
||||||
|
if (!next && nextTxt && text.indexOf(nextTxt) !== -1) next = resolveUrl(href);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
return {{ prev: prev, next: next }};
|
||||||
|
}}
|
||||||
|
|
||||||
|
function findTitle() {{
|
||||||
|
var sels = {title_selectors_json};
|
||||||
|
for (var i = 0; i < sels.length; i++) {{
|
||||||
|
var el = document.querySelector(sels[i]);
|
||||||
|
if (el && el.textContent && el.textContent.trim()) return el.textContent.trim();
|
||||||
|
}}
|
||||||
|
return {fallback_doc_title} ? (document.title || null) : null;
|
||||||
|
}}
|
||||||
|
|
||||||
|
window.__mana_found_pages = window.__mana_found_pages || {{}};
|
||||||
|
window.__mana_found_list = window.__mana_found_list || [];
|
||||||
|
|
||||||
|
function collectImages() {{
|
||||||
|
var imgs = document.querySelectorAll('{img_selector}');
|
||||||
|
var srcAttrs = {src_attrs_json};
|
||||||
|
var pageOrderAttr = {page_order_attr_js};
|
||||||
|
var minW = {min_width};
|
||||||
|
var excludeLandscape = {exclude_landscape};
|
||||||
|
|
||||||
|
for (var i = 0; i < imgs.length; i++) {{
|
||||||
|
var img = imgs[i];
|
||||||
|
var src = '';
|
||||||
|
|
||||||
|
for (var a = 0; a < srcAttrs.length; a++) {{
|
||||||
|
var val = img.getAttribute(srcAttrs[a]);
|
||||||
|
if (val && val.indexOf('data:') !== 0) {{
|
||||||
|
src = val;
|
||||||
|
break;
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
if (!src) src = img.currentSrc || img.src || '';
|
||||||
|
if (!src || src.indexOf('data:') === 0) continue;
|
||||||
|
|
||||||
|
var pageNum = pageOrderAttr ? img.getAttribute(pageOrderAttr) : null;
|
||||||
|
if (pageNum) {{
|
||||||
|
var p = parseInt(pageNum, 10);
|
||||||
|
if (!window.__mana_found_pages[p]) {{
|
||||||
|
window.__mana_found_pages[p] = src;
|
||||||
|
}} else if (window.__mana_found_pages[p].indexOf('blank') !== -1 && src.indexOf('blank') === -1) {{
|
||||||
|
window.__mana_found_pages[p] = src;
|
||||||
|
}}
|
||||||
|
}} else {{
|
||||||
|
if (!isLikelyPageImage(src)) continue;
|
||||||
|
var w = img.naturalWidth || parseInt(img.getAttribute('width') || '', 10) || 0;
|
||||||
|
var h = img.naturalHeight || parseInt(img.getAttribute('height') || '', 10) || 0;
|
||||||
|
if (excludeLandscape && w > 0 && h > 0 && w >= h) continue;
|
||||||
|
if (minW > 0 && w > 0 && w < minW) continue;
|
||||||
|
|
||||||
|
if (window.__mana_found_list.indexOf(src) === -1) {{
|
||||||
|
window.__mana_found_list.push(src);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
|
||||||
|
var maxPage = 0;
|
||||||
|
for (var k in window.__mana_found_pages) {{
|
||||||
|
if (parseInt(k, 10) > maxPage) maxPage = parseInt(k, 10);
|
||||||
|
}}
|
||||||
|
var out = [];
|
||||||
|
for (var j = 1; j <= maxPage; j++) {{
|
||||||
|
if (window.__mana_found_pages[j]) {{
|
||||||
|
out.push(window.__mana_found_pages[j]);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
for (var m = 0; m < window.__mana_found_list.length; m++) {{
|
||||||
|
var s = window.__mana_found_list[m];
|
||||||
|
if (out.indexOf(s) === -1) {{
|
||||||
|
out.push(s);
|
||||||
|
}}
|
||||||
|
}}
|
||||||
|
return out;
|
||||||
|
}}
|
||||||
|
|
||||||
|
var lastCount = -1;
|
||||||
|
var stableTicks = 0;
|
||||||
|
var debounceTimer = null;
|
||||||
|
|
||||||
|
function evaluate() {{
|
||||||
|
var images = collectImages();
|
||||||
|
|
||||||
|
if (images.length > 0 && images.length === lastCount) {{
|
||||||
|
stableTicks++;
|
||||||
|
}} else {{
|
||||||
|
stableTicks = 0;
|
||||||
|
}}
|
||||||
|
lastCount = images.length;
|
||||||
|
|
||||||
|
var nav = findNav();
|
||||||
|
var isReady = images.length > 0 && stableTicks >= 2;
|
||||||
|
|
||||||
|
window.__mana = {{
|
||||||
|
ready: isReady,
|
||||||
|
images: images,
|
||||||
|
title: findTitle(),
|
||||||
|
prev: nav.prev,
|
||||||
|
next: nav.next,
|
||||||
|
href: location.href
|
||||||
|
}};
|
||||||
|
}}
|
||||||
|
|
||||||
|
function scheduleEvaluate() {{
|
||||||
|
if (debounceTimer) clearTimeout(debounceTimer);
|
||||||
|
debounceTimer = setTimeout(evaluate, 200);
|
||||||
|
}}
|
||||||
|
|
||||||
|
var observer = new MutationObserver(scheduleEvaluate);
|
||||||
|
observer.observe(document.documentElement, {{
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
attributes: true
|
||||||
|
}});
|
||||||
|
|
||||||
|
setInterval(evaluate, 200);
|
||||||
|
scheduleEvaluate();
|
||||||
|
}})();"#,
|
||||||
|
spoof_viewport_js = spoof_viewport_js,
|
||||||
|
spoof_visibility_js = spoof_visibility_js,
|
||||||
|
ad_filename_pattern = profile.filters.ad_filename_pattern,
|
||||||
|
prev_selectors_json = prev_selectors_json,
|
||||||
|
next_selectors_json = next_selectors_json,
|
||||||
|
prev_text_js = prev_text_js,
|
||||||
|
next_text_js = next_text_js,
|
||||||
|
title_selectors_json = title_selectors_json,
|
||||||
|
fallback_doc_title = profile.selectors.title.fallback_to_document_title,
|
||||||
|
img_selector = profile.selectors.images.img_selector,
|
||||||
|
src_attrs_json = src_attrs_json,
|
||||||
|
page_order_attr_js = page_order_attr_js,
|
||||||
|
min_width = profile.selectors.images.min_width,
|
||||||
|
exclude_landscape = profile.selectors.images.exclude_landscape
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue