Compare commits
No commits in common. '3b8e50ac6f397b7bbe36587ef8d5a8bfee00ea37' and 'ac362351bf3585e88b5c4193f95a6d0825f4a1b2' have entirely different histories.
3b8e50ac6f
...
ac362351bf
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,490 +0,0 @@
|
|||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
export interface ParsedTitle {
|
|
||||||
title: string;
|
|
||||||
episode: string | null;
|
|
||||||
fullCleanTitle: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseTitleAndEpisode(rawTitle: string | null): ParsedTitle {
|
|
||||||
if (!rawTitle) return { title: '', episode: null, fullCleanTitle: '' };
|
|
||||||
|
|
||||||
// Remove site name suffixes like "- 마나토키 330", "- 뉴토끼", "- newtoki"
|
|
||||||
let clean = rawTitle
|
|
||||||
.replace(/\s*[-|_]\s*(마나토키|뉴토끼|newtoki|manatoki|웹툰|만화).*$/i, '')
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
// 1. Match explicit episode format like "150화", "150.5화", "EP.15"
|
|
||||||
const epMatch = clean.match(/(\d+(?:\.\d+)?\s*화|ep\.?\s*\d+)/i);
|
|
||||||
if (epMatch) {
|
|
||||||
let episode = epMatch[1].replace(/\s+/g, '');
|
|
||||||
if (!episode.endsWith('화') && !episode.toLowerCase().startsWith('ep')) {
|
|
||||||
episode += '화';
|
|
||||||
}
|
|
||||||
// Remove the matched episode part from the title
|
|
||||||
let title = clean.replace(epMatch[0], '').trim();
|
|
||||||
// Clean up trailing dashes/colons/dots left over
|
|
||||||
title = title.replace(/[-|_:]\s*$/, '').trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: title || clean,
|
|
||||||
episode,
|
|
||||||
fullCleanTitle: clean,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Match trailing numbers like "제목 150"
|
|
||||||
const numMatch = clean.match(/(\d+(?:\.\d+)?)\s*$/);
|
|
||||||
if (numMatch) {
|
|
||||||
const episode = `${numMatch[1]}화`;
|
|
||||||
let title = clean.slice(0, numMatch.index).trim();
|
|
||||||
title = title.replace(/[-|_:]\s*$/, '').trim();
|
|
||||||
|
|
||||||
return {
|
|
||||||
title: title || clean,
|
|
||||||
episode,
|
|
||||||
fullCleanTitle: clean,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return { title: clean, episode: null, fullCleanTitle: clean };
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue