diff --git a/ComicRead-AdGuard.user.js b/ComicRead-AdGuard.user.js index 13738161..03225aee 100644 --- a/ComicRead-AdGuard.user.js +++ b/ComicRead-AdGuard.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name ComicRead // @namespace ComicRead -// @version 9.6.1 +// @version 9.6.2 // @description 为漫画站增加双页阅读、翻译等优化体验的增强功能。百合会(记录阅读历史、自动签到等)、百合会新站、动漫之家(解锁隐藏漫画)、E-Hentai(关联 nhentai、快捷收藏、标签染色、识别广告页等)、nhentai(彻底屏蔽漫画、无限滚动)、Yurifans(自动签到)、拷贝漫画(copymanga)(显示最后阅读记录)、PonpomuYuri、明日方舟泰拉记事社、禁漫天堂、漫画柜(manhuagui)、漫画DB(manhuadb)、动漫屋(dm5)、绅士漫画(wnacg)、mangabz、komiic、无限动漫、新新漫画、hitomi、koharu、kemono、nekohouse、welovemanga // @description:en Add enhanced features to the comic site for optimized experience, including dual-page reading and translation. E-Hentai (Associate nhentai, Quick favorite, Colorize tags, Floating tag list, etc.) | nhentai (Totally block comics, Auto page turning) | hitomi | Anchira | kemono | nekohouse | welovemanga. // @description:ru Добавляет расширенные функции для удобства на сайт, такие как двухстраничный режим и перевод. @@ -120,12 +120,6 @@ // @downloadURL https://github.com/hymbz/ComicReadScript/raw/master/ComicRead-AdGuard.user.js // ==/UserScript== -/** - * 虽然在打包的时候已经尽可能保持代码格式不变了,但因为脚本代码比较多的缘故 - * 所以真对脚本代码感兴趣的话,推荐还是直接上 github 仓库来看 - * - * 对站点逻辑感兴趣的,结合 `src\index.ts` 看 `src\site` 下的对应文件即可 - */ const gmApi = { GM, @@ -162,119 +156,17 @@ const evalCode = code => { * @param name \@resource 引用的资源名 */ const selfImportSync = name => { - const code = name === 'main' ?` + let code; + + // 为了方便打包、减少在无关站点上的运行损耗、顺带隔离下作用域 + // 除站点逻辑外的代码会作为字符串存着,要用时再像外部模块一样导入 + switch (name) { + case 'helper': + code =` const solidJs = require('solid-js'); const web = require('solid-js/web'); -const store$2 = require('solid-js/store'); -const fflate = require('fflate'); -const main = require('main'); -const QrScanner = require('qr-scanner'); - -// src/index.ts -var triggerOptions = !web.isServer && solidJs.DEV ? { equals: false, name: "trigger" } : { equals: false }; -var triggerCacheOptions = !web.isServer && solidJs.DEV ? { equals: false, internal: true } : triggerOptions; -var TriggerCache = class { - #map; - constructor(mapConstructor = Map) { - this.#map = new mapConstructor(); - } - dirty(key) { - if (web.isServer) - return; - this.#map.get(key)?.$$(); - } - track(key) { - if (!solidJs.getListener()) - return; - let trigger = this.#map.get(key); - if (!trigger) { - const [$, $$] = solidJs.createSignal(void 0, triggerCacheOptions); - this.#map.set(key, trigger = { $, $$, n: 1 }); - } else - trigger.n++; - solidJs.onCleanup(() => { - if (trigger.n-- === 1) - queueMicrotask(() => trigger.n === 0 && this.#map.delete(key)); - }); - trigger.$(); - } -}; - -// src/index.ts -var $KEYS = Symbol("track-keys"); -var ReactiveSet = class extends Set { - #triggers = new TriggerCache(); - constructor(values) { - super(); - if (values) - for (const v of values) - super.add(v); - } - // reads - get size() { - this.#triggers.track($KEYS); - return super.size; - } - has(v) { - this.#triggers.track(v); - return super.has(v); - } - *keys() { - for (const key of super.keys()) { - this.#triggers.track(key); - yield key; - } - this.#triggers.track($KEYS); - } - values() { - return this.keys(); - } - *entries() { - for (const key of super.keys()) { - this.#triggers.track(key); - yield [key, key]; - } - this.#triggers.track($KEYS); - } - [Symbol.iterator]() { - return this.values(); - } - forEach(callbackfn) { - this.#triggers.track($KEYS); - super.forEach(callbackfn); - } - // writes - add(v) { - if (!super.has(v)) { - super.add(v); - solidJs.batch(() => { - this.#triggers.dirty(v); - this.#triggers.dirty($KEYS); - }); - } - return this; - } - delete(v) { - const r = super.delete(v); - if (r) { - solidJs.batch(() => { - this.#triggers.dirty(v); - this.#triggers.dirty($KEYS); - }); - } - return r; - } - clear() { - if (super.size) { - solidJs.batch(() => { - for (const v of super.keys()) - this.#triggers.dirty(v); - super.clear(); - this.#triggers.dirty($KEYS); - }); - } - } -}; +const helper = require('helper'); +const store = require('solid-js/store'); // src/index.ts var debounce$1 = (callback, wait) => { @@ -614,7 +506,7 @@ async function wait(fn, timeout = Number.POSITIVE_INFINITY) { let res = await fn(); let _timeout = timeout; while (_timeout > 0 && !res) { - await sleep(10); + await sleep(100); _timeout -= 10; res = await fn(); } @@ -628,16 +520,19 @@ const waitDom = selector => wait(() => querySelector(selector)); const waitImgLoad = (target, timeout) => new Promise((resolve, reject) => { const img = typeof target === 'string' ? new Image() : target; const id = timeout ? window.setTimeout(() => reject(new Error('timeout')), timeout) : undefined; - img.addEventListener('load', () => { + const handleError = e => { + window.clearTimeout(id); + reject(new Error(e.message)); + }; + const handleLoad = () => { window.clearTimeout(id); + img.removeEventListener('error', handleError); resolve(img); - }, { + }; + img.addEventListener('load', handleLoad, { once: true }); - img.addEventListener('error', e => { - window.clearTimeout(id); - reject(new Error(e.message)); - }, { + img.addEventListener('error', handleError, { once: true }); if (typeof target === 'string') img.src = target; @@ -789,1510 +684,1160 @@ const hijackFn = (fnName, fn) => { unsafeWindow[fnName] = (...args) => fn(rawFn, args); }; -/* eslint-disable no-console */ +/** 会自动设置 equals 的 createSignal */ +const createEqualsSignal = (init, options) => solidJs.createSignal(init, { + equals: isEqual, + ...options +}); -const prefix = ['%cComicRead', 'background-color: #607d8b; color: white; padding: 2px 4px; border-radius: 4px;']; -const log = (...args) => console.log(...prefix, ...args); -log.warn = (...args) => console.warn(...prefix, ...args); -log.error = (...args) => { - console.error(...prefix, ...args); - if (args[0] instanceof Error) throw args[0]; +/** 会自动设置 equals 和 createRoot 的 createMemo */ +const createRootMemo = (fn, init, options) => { + const _init = init ?? fn(undefined); + // 自动为对象类型设置 equals + const _options = options?.equals === undefined && typeof init === 'object' ? { + ...options, + equals: isEqual + } : options; + return solidJs.getOwner() ? solidJs.createMemo(fn, _init, _options) : solidJs.createRoot(() => solidJs.createMemo(fn, _init, _options)); }; -const zh = { - alert: { - comic_load_error: "漫画加载出错", - download_failed: "下载失败", - fetch_comic_img_failed: "获取漫画图片失败", - img_load_failed: "图片加载失败", - no_img_download: "没有能下载的图片", - repeat_load: "加载图片中,请稍候", - server_connect_failed: "无法连接到服务器" - }, - button: { - close_current_page_translation: "关闭当前页的翻译", - download: "下载", - download_completed: "下载完成", - downloading: "下载中", - exit: "退出", - grid_mode: "网格模式", - packaging: "打包中", - page_fill: "页面填充", - page_mode_double: "双页模式", - page_mode_single: "单页模式", - scroll_mode: "卷轴模式", - setting: "设置", - translate_current_page: "翻译当前页", - zoom_in: "放大", - zoom_out: "缩小" - }, - description: "为漫画站增加双页阅读、翻译等优化体验的增强功能。", - end_page: { - next_button: "下一话", - prev_button: "上一话", - tip: { - end_jump: "已到结尾,继续向下翻页将跳至下一话", - exit: "已到结尾,继续翻页将退出", - start_jump: "已到开头,继续向上翻页将跳至上一话" - } - }, - hotkeys: { - enter_read_mode: "进入阅读模式", - exit: "退出", - float_tag_list: "悬浮标签列表", - jump_to_end: "跳至尾页", - jump_to_home: "跳至首页", - page_down: "向下翻页", - page_up: "向上翻页", - scroll_down: "向下滚动", - scroll_left: "向左滚动", - scroll_right: "向右滚动", - scroll_up: "向上滚动", - switch_auto_enlarge: "切换图片自动放大选项", - switch_dir: "切换阅读方向", - switch_grid_mode: "切换网格模式", - switch_page_fill: "切换页面填充", - switch_scroll_mode: "切换卷轴模式", - switch_single_double_page_mode: "切换单双页模式", - translate_current_page: "翻译当前页" - }, - img_status: { - error: "加载出错", - loading: "正在加载", - wait: "等待加载" - }, - other: { - auto_enter_read_mode: "自动进入阅读模式", - "default": "默认", - disable: "禁用", - enter_comic_read_mode: "进入漫画阅读模式", - fab_hidden: "隐藏悬浮按钮", - fab_show: "显示悬浮按钮", - fill_page: "填充页", - img_loading: "图片加载中", - loading_img: "加载图片中", - read_mode: "阅读模式" - }, - pwa: { - alert: { - img_data_error: "图片数据错误", - img_not_found: "找不到图片", - img_not_found_files: "请选择图片文件或含有图片文件的压缩包", - img_not_found_folder: "文件夹下没有图片文件或含有图片文件的压缩包", - not_valid_url: "不是有效的 URL", - repeat_load: "正在加载其他文件中……", - unzip_error: "解压出错", - unzip_password_error: "解压密码错误", - userscript_not_installed: "未安装 ComicRead 脚本" - }, - button: { - enter_url: "输入 URL", - install: "安装", - no_more_prompt: "不再提示", - resume_read: "恢复阅读", - select_files: "选择文件", - select_folder: "选择文件夹" - }, - install_md: "### 每次都要打开这个网页很麻烦?\\n如果你希望\\n1. 能有独立的窗口,像是在使用本地软件一样\\n1. 加入本地压缩文件的打开方式之中,方便直接打开\\n1. 离线使用~~(主要是担心国内网络抽风无法访问这个网页~~\\n### 欢迎将本页面作为 PWA 应用安装到电脑上😃👍", - message: { - enter_password: "请输入密码", - unzipping: "解压缩中" - }, - tip_enter_url: "请输入压缩包 URL", - tip_md: "# ComicRead PWA\\n使用 [ComicRead](https://github.com/hymbz/ComicReadScript) 的阅读模式阅读**本地**漫画\\n---\\n### 将图片文件、文件夹、压缩包直接拖入即可开始阅读\\n*也可以选择**直接粘贴**或**输入**压缩包 URL 下载阅读*" - }, - setting: { - hotkeys: { - add: "添加新快捷键", - restore: "恢复默认快捷键" - }, - language: "语言", - option: { - abreast_duplicate: "每列重复比例", - abreast_mode: "并排卷轴模式", - always_load_all_img: "始终加载所有图片", - auto_switch_page_mode: "自动切换单双页模式", - background_color: "背景颜色", - click_page_turn_area: "点击区域", - click_page_turn_enabled: "点击翻页", - click_page_turn_swap_area: "左右点击区域交换", - click_page_turn_vertical: "上下翻页", - dark_mode: "夜间模式", - dir_ltr: "从左到右(美漫)", - dir_rtl: "从右到左(日漫)", - disable_auto_enlarge: "禁止图片自动放大", - first_page_fill: "默认启用首页填充", - fit_to_width: "图片适合宽度", - jump_to_next_chapter: "翻页至上/下一话", - paragraph_dir: "阅读方向", - paragraph_display: "显示", - paragraph_hotkeys: "快捷键", - paragraph_operation: "操作", - paragraph_other: "其他", - paragraph_scrollbar: "滚动条", - paragraph_translation: "翻译", - preload_page_num: "预加载页数", - scroll_mode_img_scale: "卷轴图片缩放", - scroll_mode_img_spacing: "卷轴图片间距", - scrollbar_auto_hidden: "自动隐藏", - scrollbar_easy_scroll: "快捷滚动", - scrollbar_position: "位置", - scrollbar_position_auto: "自动", - scrollbar_position_bottom: "底部", - scrollbar_position_hidden: "隐藏", - scrollbar_position_right: "右侧", - scrollbar_position_top: "顶部", - scrollbar_show_img_status: "显示图片加载状态", - show_clickable_area: "显示点击区域", - show_comments: "在结束页显示评论", - swap_page_turn_key: "左右翻页键交换", - zoom: "图片缩放" - }, - translation: { - cotrans_tip: "

将使用 Cotrans 提供的接口翻译图片,该服务器由其维护者用爱发电自费维护

\\n

多人同时使用时需要排队等待,等待队列达到上限后再上传新图片会报错,需要过段时间再试

\\n

所以还请 注意用量

\\n

更推荐使用自己本地部署的项目,既不占用服务器资源也不需要排队

", - options: { - detection_resolution: "文本扫描清晰度", - direction: "渲染字体方向", - direction_auto: "原文一致", - direction_horizontal: "仅限水平", - direction_vertical: "仅限垂直", - forceRetry: "忽略缓存强制重试", - localUrl: "自定义服务器 URL", - onlyDownloadTranslated: "只下载完成翻译的图片", - target_language: "目标语言", - text_detector: "文本扫描器", - translator: "翻译服务" - }, - server: "翻译服务器", - server_selfhosted: "本地部署", - translate_after_current: "翻译当前页至结尾", - translate_all_img: "翻译全部图片" - } - }, - site: { - add_feature: { - associate_nhentai: "关联nhentai", - auto_page_turn: "无限滚动", - block_totally: "彻底屏蔽漫画", - colorize_tag: "标签染色", - detect_ad: "识别广告页", - float_tag_list: "悬浮标签列表", - hotkeys: "快捷键", - load_original_image: "加载原图", - open_link_new_page: "在新页面中打开链接", - quick_favorite: "快捷收藏", - quick_rating: "快捷评分", - quick_tag_define: "快捷查看标签定义", - remember_current_site: "记住当前站点" - }, - changed_load_failed: "网站发生变化,无法加载漫画", - ehentai: { - change_favorite_failed: "收藏夹修改失败", - change_favorite_success: "收藏夹修改成功", - change_rating_failed: "评分修改失败", - change_rating_success: "评分修改成功", - fetch_favorite_failed: "获取收藏夹信息失败", - fetch_img_page_source_failed: "获取图片页源码失败", - fetch_img_page_url_failed: "从详情页获取图片页地址失败", - fetch_img_url_failed: "从图片页获取图片地址失败", - html_changed_nhentai_failed: "页面结构发生改变,关联 nhentai 漫画功能无法正常生效", - ip_banned: "IP地址被禁", - nhentai_error: "nhentai 匹配出错", - nhentai_failed: "匹配失败,请在确认登录 {{nhentai}} 后刷新" - }, - need_captcha: "需要人机验证", - nhentai: { - fetch_next_page_failed: "获取下一页漫画数据失败", - tag_blacklist_fetch_failed: "标签黑名单获取失败" - }, - settings_tip: "设置", - show_settings_menu: "显示设置菜单", - simple: { - auto_read_mode_message: "已默认开启「自动进入阅读模式」", - no_img: "未找到合适的漫画图片,\\n如有需要可点此关闭简易阅读模式", - simple_read_mode: "使用简易阅读模式" - } - }, - touch_area: { - menu: "菜单", - next: "下页", - prev: "上页", - type: { - edge: "边缘", - l: "L", - left_right: "左右", - up_down: "上下" - } - }, - translation: { - status: { - "default": "未知状态", - detection: "正在检测文本", - downscaling: "正在缩小图片", - error: "翻译出错", - "error-lang": "你选择的翻译服务不支持你选择的语言", - "error-translating": "翻译服务没有返回任何文本", - "error-with-id": "翻译出错", - finished: "正在整理结果", - inpainting: "正在修补图片", - "mask-generation": "正在生成文本掩码", - ocr: "正在识别文本", - pending: "正在等待", - "pending-pos": "正在等待", - rendering: "正在渲染", - saved: "保存结果", - textline_merge: "正在整合文本", - translating: "正在翻译文本", - upscaling: "正在放大图片" - }, - tip: { - check_img_status_failed: "检查图片状态失败", - download_img_failed: "下载图片失败", - error: "翻译出错", - get_translator_list_error: "获取可用翻译服务列表时出错", - id_not_returned: "未返回 id", - img_downloading: "正在下载图片", - img_not_fully_loaded: "图片未加载完毕", - pending: "正在等待,列队还有 {{pos}} 张图片", - resize_img_failed: "缩放图片失败", - translation_completed: "翻译完成", - upload_error: "图片上传出错", - upload_return_error: "服务器翻译出错", - wait_translation: "等待翻译" - }, - translator: { - baidu: "百度", - deepl: "DeepL", - google: "谷歌", - "gpt3.5": "GPT-3.5", - none: "删除文本", - offline: "离线模型", - original: "原文", - youdao: "有道" - } - } -}; - -const en = { - alert: { - comic_load_error: "Comic loading error", - download_failed: "Download failed", - fetch_comic_img_failed: "Failed to fetch comic images", - img_load_failed: "Image loading failed", - no_img_download: "No images available for download", - repeat_load: "Loading image, please wait", - server_connect_failed: "Unable to connect to the server" - }, - button: { - close_current_page_translation: "Close translation of the current page", - download: "Download", - download_completed: "Download completed", - downloading: "Downloading", - exit: "Exit", - grid_mode: "Grid mode", - packaging: "Packaging", - page_fill: "Page fill", - page_mode_double: "Double page mode", - page_mode_single: "Single page mode", - scroll_mode: "Scroll mode", - setting: "Settings", - translate_current_page: "Translate current page", - zoom_in: "Zoom in", - zoom_out: "Zoom out" - }, - description: "Add enhanced features to the comic site for optimized experience, including dual-page reading and translation.", - end_page: { - next_button: "Next chapter", - prev_button: "Prev chapter", - tip: { - end_jump: "Reached the last page, scrolling down will jump to the next chapter", - exit: "Reached the last page, scrolling down will exit", - start_jump: "Reached the first page, scrolling up will jump to the previous chapter" - } - }, - hotkeys: { - enter_read_mode: "Enter reading mode", - exit: "Exit", - float_tag_list: "Floating tag list", - jump_to_end: "Jump to the last page", - jump_to_home: "Jump to the first page", - page_down: "Turn the page to the down", - page_up: "Turn the page to the up", - scroll_down: "Scroll down", - scroll_left: "Scroll left", - scroll_right: "Scroll right", - scroll_up: "Scroll up", - switch_auto_enlarge: "Switch auto image enlarge option", - switch_dir: "Switch reading direction", - switch_grid_mode: "Switch grid mode", - switch_page_fill: "Switch page fill", - switch_scroll_mode: "Switch scroll mode", - switch_single_double_page_mode: "Switch single/double page mode", - translate_current_page: "Translate current page" - }, - img_status: { - error: "Load Error", - loading: "Loading", - wait: "Waiting for load" - }, - other: { - auto_enter_read_mode: "Auto enter reading mode", - "default": "Default", - disable: "Disable", - enter_comic_read_mode: "Enter comic reading mode", - fab_hidden: "Hide floating button", - fab_show: "Show floating button", - fill_page: "Fill Page", - img_loading: "Image loading", - loading_img: "Loading image", - read_mode: "Reading mode" - }, - pwa: { - alert: { - img_data_error: "Image data error", - img_not_found: "Image not found", - img_not_found_files: "Please select an image file or a compressed file containing image files", - img_not_found_folder: "No image files or compressed files containing image files in the folder", - not_valid_url: "Not a valid URL", - repeat_load: "Loading other files…", - unzip_error: "Decompression error", - unzip_password_error: "Decompression password error", - userscript_not_installed: "ComicRead userscript not installed" - }, - button: { - enter_url: "Enter URL", - install: "Install", - no_more_prompt: "Do not prompt again", - resume_read: "Restore reading", - select_files: "Select File", - select_folder: "Select folder" - }, - install_md: "### Tired of opening this webpage every time?\\nIf you wish to:\\n1. Have an independent window, as if using local software\\n1. Add to the local compressed file opening method for easy direct opening\\n1. Use offline\\n### Welcome to install this page as a PWA app on your computer😃👍", - message: { - enter_password: "Please enter your password", - unzipping: "Unzipping" - }, - tip_enter_url: "Please enter the URL of the compressed file", - tip_md: "# ComicRead PWA\\nRead **local** comics using [ComicRead](https://github.com/hymbz/ComicReadScript) reading mode.\\n---\\n### Drag and drop image files, folders, or compressed files directly to start reading\\n*You can also choose to **paste directly** or **enter** the URL of the compressed file for downloading and reading*" - }, - setting: { - hotkeys: { - add: "Add new hotkeys", - restore: "Restore default hotkeys" - }, - language: "Language", - option: { - abreast_duplicate: "Column duplicates ratio", - abreast_mode: "Abreast scroll mode", - always_load_all_img: "Always load all images", - auto_switch_page_mode: "Auto switch single/double page mode", - background_color: "Background Color", - click_page_turn_area: "Touch area", - click_page_turn_enabled: "Click to turn page", - click_page_turn_swap_area: "Swap LR clickable areas", - click_page_turn_vertical: "Vertically arranged clickable areas", - dark_mode: "Dark mode", - dir_ltr: "LTR (American comics)", - dir_rtl: "RTL (Japanese manga)", - disable_auto_enlarge: "Disable automatic image enlarge", - first_page_fill: "Enable first page fill by default", - fit_to_width: "Fit to width", - jump_to_next_chapter: "Turn to the next/previous chapter", - paragraph_dir: "Reading direction", - paragraph_display: "Display", - paragraph_hotkeys: "Hotkeys", - paragraph_operation: "Operation", - paragraph_other: "Other", - paragraph_scrollbar: "Scrollbar", - paragraph_translation: "Translation", - preload_page_num: "Preload page number", - scroll_mode_img_scale: "Scroll mode image zoom ratio", - scroll_mode_img_spacing: "Scroll mode image spacing", - scrollbar_auto_hidden: "Auto hide", - scrollbar_easy_scroll: "Easy scroll", - scrollbar_position: "position", - scrollbar_position_auto: "Auto", - scrollbar_position_bottom: "Bottom", - scrollbar_position_hidden: "Hidden", - scrollbar_position_right: "Right", - scrollbar_position_top: "Top", - scrollbar_show_img_status: "Show image loading status", - show_clickable_area: "Show clickable areas", - show_comments: "Show comments on the end page", - swap_page_turn_key: "Swap LR page-turning keys", - zoom: "Image zoom ratio" - }, - translation: { - cotrans_tip: "

Using the interface provided by Cotrans to translate images, which is maintained by its maintainer at their own expense.

\\n

When multiple people use it at the same time, they need to queue and wait. If the waiting queue reaches its limit, uploading new images will result in an error. Please try again after a while.

\\n

So please mind the frequency of use.

\\n

It is highly recommended to use your own locally deployed project, as it does not consume server resources and does not require queuing.

", - options: { - detection_resolution: "Text detection resolution", - direction: "Render text orientation", - direction_auto: "Follow source", - direction_horizontal: "Horizontal only", - direction_vertical: "Vertical only", - forceRetry: "Force retry (ignore cache)", - localUrl: "customize server URL", - onlyDownloadTranslated: "Download only the translated images", - target_language: "Target language", - text_detector: "Text detector", - translator: "Translator" - }, - server: "Translation server", - server_selfhosted: "Selfhosted", - translate_after_current: "Translate the current page to the end", - translate_all_img: "Translate all images" - } - }, - site: { - add_feature: { - associate_nhentai: "Associate nhentai", - auto_page_turn: "Infinite scroll", - block_totally: "Totally block comics", - colorize_tag: "Colorize tags", - detect_ad: "Detect advertise page", - float_tag_list: "Floating tag list", - hotkeys: "Hotkeys", - load_original_image: "Load original image", - open_link_new_page: "Open links in a new page", - quick_favorite: "Quick favorite", - quick_rating: "Quick rating", - quick_tag_define: "Quick view tag define", - remember_current_site: "Remember the current site" - }, - changed_load_failed: "The website has undergone changes, unable to load comics", - ehentai: { - change_favorite_failed: "Failed to change the favorite", - change_favorite_success: "Successfully changed the favorite", - change_rating_failed: "Failed to change the rating", - change_rating_success: "Successfully changed the rating", - fetch_favorite_failed: "Failed to get favorite info", - fetch_img_page_source_failed: "Failed to get the source code of the image page", - fetch_img_page_url_failed: "Failed to get the image page address from the detail page", - fetch_img_url_failed: "Failed to get the image address from the image page", - html_changed_nhentai_failed: "The web page structure has changed, the function to associate nhentai comics is not working properly", - ip_banned: "IP address is banned", - nhentai_error: "Error in nhentai matching", - nhentai_failed: "Matching failed, please refresh after confirming login to {{nhentai}}" - }, - need_captcha: "Need CAPTCHA verification", - nhentai: { - fetch_next_page_failed: "Failed to get next page of comic data", - tag_blacklist_fetch_failed: "Failed to fetch tag blacklist" - }, - settings_tip: "Settings", - show_settings_menu: "Show settings menu", - simple: { - auto_read_mode_message: "\\"Auto enter reading mode\\" is enabled by default", - no_img: "No suitable comic images were found.\\nIf necessary, you can click here to close the simple reading mode.", - simple_read_mode: "Enter simple reading mode" - } - }, - touch_area: { - menu: "Menu", - next: "Next Page", - prev: "Prev Page", - type: { - edge: "Edge", - l: "L", - left_right: "Left Right", - up_down: "Up Down" - } - }, - translation: { - status: { - "default": "Unknown status", - detection: "Detecting text", - downscaling: "Downscaling", - error: "Error during translation", - "error-lang": "The target language is not supported by the chosen translator", - "error-translating": "Did not get any text back from the text translation service", - "error-with-id": "Error during translation", - finished: "Finishing", - inpainting: "Inpainting", - "mask-generation": "Generating mask", - ocr: "Scanning text", - pending: "Pending", - "pending-pos": "Pending", - rendering: "Rendering", - saved: "Saved", - textline_merge: "Merging text lines", - translating: "Translating", - upscaling: "Upscaling" - }, - tip: { - check_img_status_failed: "Failed to check image status", - download_img_failed: "Failed to download image", - error: "Translation error", - get_translator_list_error: "Error occurred while getting the list of available translation services", - id_not_returned: "No id returned", - img_downloading: "Downloading images", - img_not_fully_loaded: "Image has not finished loading", - pending: "Pending, {{pos}} in queue", - resize_img_failed: "Failed to resize image", - translation_completed: "Translation completed", - upload_error: "Image upload error", - upload_return_error: "Error during server translation", - wait_translation: "Waiting for translation" - }, - translator: { - baidu: "baidu", - deepl: "DeepL", - google: "Google", - "gpt3.5": "GPT-3.5", - none: "Remove texts", - offline: "offline translator", - original: "Original", - youdao: "youdao" - } - } -}; - -const ru = { - alert: { - comic_load_error: "Ошибка загрузки комикса", - download_failed: "Ошибка загрузки", - fetch_comic_img_failed: "Не удалось загрузить изображения", - img_load_failed: "Не удалось загрузить изображение", - no_img_download: "Нет доступных картинок для загрузки", - repeat_load: "Загрузка изображения, пожалуйста подождите", - server_connect_failed: "Не удалось подключиться к серверу" - }, - button: { - close_current_page_translation: "Скрыть перевод текущей страницы", - download: "Скачать", - download_completed: "Загрузка завершена", - downloading: "Скачивание", - exit: "Выход", - grid_mode: "Режим сетки", - packaging: "Упаковка", - page_fill: "Заполнить страницу", - page_mode_double: "Двухчастичный режим", - page_mode_single: "Одностраничный режим", - scroll_mode: "Режим прокрутки", - setting: "Настройки", - translate_current_page: "Перевести текущую страницу", - zoom_in: "Приблизить", - zoom_out: "Уменьшить" - }, - description: "Добавляет расширенные функции для удобства на сайт, такие как двухстраничный режим и перевод.", - end_page: { - next_button: "Следующая глава", - prev_button: "Предыдущая глава", - tip: { - end_jump: "Последняя страница, следующая глава ниже", - exit: "Последняя страница, ниже комикс будет закрыт", - start_jump: "Первая страница, выше будет загружена предыдущая глава" - } - }, - hotkeys: { - enter_read_mode: "Режим чтения", - exit: "Выход", - float_tag_list: "Плавающий список тегов", - jump_to_end: "Перейти к последней странице", - jump_to_home: "Перейти к первой странице", - page_down: "Перелистнуть страницу вниз", - page_up: "Перелистнуть страницу вверх", - scroll_down: "Прокрутить вниз", - scroll_left: "Прокрутить влево", - scroll_right: "Прокрутите вправо", - scroll_up: "Прокрутите вверх", - switch_auto_enlarge: "Автоматическое приближение", - switch_dir: "Направление чтения", - switch_grid_mode: "Режим сетки", - switch_page_fill: "Заполнение страницы", - switch_scroll_mode: "Режим прокрутки", - switch_single_double_page_mode: "Одностраничный/Двухстраничный режим", - translate_current_page: "Перевести текущую страницу" - }, - img_status: { - error: "Ошибка загрузки", - loading: "Загрузка", - wait: "Ожидание загрузки" - }, - other: { - auto_enter_read_mode: "Автоматически включать режим чтения", - "default": "Дефолт", - disable: "Отключить", - enter_comic_read_mode: "Режим чтения комиксов", - fab_hidden: "Скрыть плавающую кнопку", - fab_show: "Показать плавающую кнопку", - fill_page: "Заполнить страницу", - img_loading: "Изображение загружается", - loading_img: "Загрузка изображения", - read_mode: "Режим чтения" - }, - pwa: { - alert: { - img_data_error: "Ошибка данных изображения", - img_not_found: "Изображение не найдено", - img_not_found_files: "Пожалуйста выберите файл или архив с изображениями", - img_not_found_folder: "В папке не найдены изображения или архивы с изображениями", - not_valid_url: "Невалидный URL", - repeat_load: "Загрузка других файлов…", - unzip_error: "Ошибка распаковки", - unzip_password_error: "Неверный пароль от архива", - userscript_not_installed: "ComicRead не установлен" - }, - button: { - enter_url: "Ввести URL", - install: "Установить", - no_more_prompt: "Больше не показывать", - resume_read: "Продолжить чтение", - select_files: "Выбрать файл", - select_folder: "Выбрать папку" - }, - install_md: "### Устали открывать эту страницу каждый раз?\\nЕсли вы хотите:\\n1. Иметь отдельное окно, как если бы вы использовали обычное программное обеспечение\\n1. Открывать архивы напрямую\\n1. Пользоваться оффлайн\\n### Установите эту страницу в качестве [PWA](https://ru.wikipedia.org/wiki/%D0%9F%D1%80%D0%BE%D0%B3%D1%80%D0%B5%D1%81%D1%81%D0%B8%D0%B2%D0%BD%D0%BE%D0%B5_%D0%B2%D0%B5%D0%B1-%D0%BF%D1%80%D0%B8%D0%BB%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D0%B5) на свой компьютер 🐺☝️", - message: { - enter_password: "Пожалуйста введите пароль", - unzipping: "Распаковка" - }, - tip_enter_url: "Введите URL архива", - tip_md: "# ComicRead PWA\\nИспользуйте [ComicRead](https://github.com/hymbz/ComicReadScript) для чтения комиксов **локально**.\\n---\\n### Перетащите изображения, папки или архивы чтобы начать читать\\n*Вы так же можете **открыть** или **вставить** URL архива на напрямую*" - }, - setting: { - hotkeys: { - add: "Добавить горячие клавиши", - restore: "Восстановить горячие клавиши по умолчанию" - }, - language: "Язык", - option: { - abreast_duplicate: "Коэффициент дублирования столбцов", - abreast_mode: "Режим прокрутки в ряд", - always_load_all_img: "Всегда загружать все изображения", - auto_switch_page_mode: "Автоматическое переключение режима одиночной/двойной страницы", - background_color: "Цвет фона", - click_page_turn_area: "Область нажатия", - click_page_turn_enabled: "Перелистывать по клику", - click_page_turn_swap_area: "Поменять местами правую и левую области переключения страниц", - click_page_turn_vertical: "Вертикальная область переключения страниц", - dark_mode: "Ночная тема", - dir_ltr: "Чтение слева направо (Американские комиксы)", - dir_rtl: "Чтение справа налево (Японская манга)", - disable_auto_enlarge: "Отключить автоматическое масштабирование изображений", - first_page_fill: "Включить заполнение первой страницы по умолчанию", - fit_to_width: "По ширине", - jump_to_next_chapter: "Перелистнуть главу", - paragraph_dir: "Направление чтения", - paragraph_display: "Отображение", - paragraph_hotkeys: "Горячие клавиши", - paragraph_operation: "Управление", - paragraph_other: "Другое", - paragraph_scrollbar: "Полоса прокрутки", - paragraph_translation: "Перевод", - preload_page_num: "Предзагружать страниц", - scroll_mode_img_scale: "Коэффициент масштабирования изображения в режиме скроллинга", - scroll_mode_img_spacing: "Расстояние между страницами в режиме скроллинга", - scrollbar_auto_hidden: "Автоматически скрывать", - scrollbar_easy_scroll: "Лёгкая прокрутка", - scrollbar_position: "Позиция", - scrollbar_position_auto: "Авто", - scrollbar_position_bottom: "Снизу", - scrollbar_position_hidden: "Спрятано", - scrollbar_position_right: "Справа", - scrollbar_position_top: "Сверху", - scrollbar_show_img_status: "Показывать статус загрузки изображения", - show_clickable_area: "Показывать кликабельные области", - show_comments: "Показывать комментарии на последней странице", - swap_page_turn_key: "Поменять местами клавиши переключения страниц", - zoom: "Коэффициент масштабирования изображения" - }, - translation: { - cotrans_tip: "

Использует для перевода Cotrans API, работающий исключительно за счёт своего создателя.

\\n

Запросы обрабатываются по одному в порядке синхронной очереди. Когда очередь превышает лимит новые запросы будут приводить к ошибке. Если такое случилось попробуйте позже.

\\n

Так что пожалуйста учитывайте загруженность при выборе

\\n

Настоятельно рекомендовано использовать проект развёрнутый локально т.к. это не потребляет серверные ресурсы и вы не ограничены очередью.

", - options: { - detection_resolution: "Разрешение распознавания текста", - direction: "Ориетнация текста", - direction_auto: "Следование оригиналу", - direction_horizontal: "Только горизонтально", - direction_vertical: "Только вертикально", - forceRetry: "Принудительный повтор(Игнорировать кэш)", - localUrl: "Настроить URL сервера", - onlyDownloadTranslated: "Скачать только переведённые изображения", - target_language: "Целевой язык", - text_detector: "Детектор текста", - translator: "Переводчик" - }, - server: "Сервер", - server_selfhosted: "Свой", - translate_after_current: "Переводить страницу до конца", - translate_all_img: "Перевести все изображения" - } - }, - site: { - add_feature: { - associate_nhentai: "Ассоциация с nhentai", - auto_page_turn: "Автопереворот страниц", - block_totally: "Глобально заблокировать комиксы", - colorize_tag: "Раскрасить теги", - detect_ad: "Detect advertise page", - float_tag_list: "Плавающий список тегов", - hotkeys: "Горячие клавиши", - load_original_image: "Загружать оригинальное изображение", - open_link_new_page: "Открывать ссылки в новой вкладке", - quick_favorite: "Быстрый фаворит", - quick_rating: "Быстрый рейтинг", - quick_tag_define: "Определение тега быстрого просмотра", - remember_current_site: "Запомнить текущий сайт" - }, - changed_load_failed: "Страница изменилась, невозможно загрузить комикс", - ehentai: { - change_favorite_failed: "Не удалось изменить избранное", - change_favorite_success: "Избранное успешно изменено", - change_rating_failed: "Не удалось изменить оценку", - change_rating_success: "Успешно изменен рейтинг", - fetch_favorite_failed: "Не удалось получить информацию о избранном", - fetch_img_page_source_failed: "Не удалось получить исходный код страницы с изображениями", - fetch_img_page_url_failed: "Не удалось получить адрес страницы изображений из деталей", - fetch_img_url_failed: "Не удалось получить адрес изображения", - html_changed_nhentai_failed: "Структура страницы изменилась, функция nhentai manga работает некорректно", - ip_banned: "IP адрес забанен", - nhentai_error: "Ошибка сопоставления с nhentai", - nhentai_failed: "Ошибка сопостовления. Пожалуйста перезагрузите страницу после входа на {{nhentai}}" - }, - need_captcha: "CAPTCHA", - nhentai: { - fetch_next_page_failed: "Не удалось получить следующую страницу", - tag_blacklist_fetch_failed: "Не удалось получить заблокированные теги" - }, - settings_tip: "Настройки", - show_settings_menu: "Показать меню настроек", - simple: { - auto_read_mode_message: "\\"Автоматически включать режим чтения\\" по умолчанию", - no_img: "Не найдено подходящих изображений. Можно нажать тут что бы выключить режим простого чтения.", - simple_read_mode: "Включить простой режим чтения" - } - }, - touch_area: { - menu: "Меню", - next: "Следующая страница", - prev: "Предыдущая страница", - type: { - edge: "Грань", - l: "L", - left_right: "Лево Право", - up_down: "Верх Низ" - } - }, - translation: { - status: { - "default": "Неизвестный статус", - detection: "Распознавание текста", - downscaling: "Уменьшение масштаба", - error: "Ошибка перевода", - "error-lang": "Целевой язык не поддерживается выбранным переводчиком", - "error-translating": "Ошибка перевода(пустой ответ)", - "error-with-id": "Ошибка во время перевода", - finished: "Завершение", - inpainting: "Наложение", - "mask-generation": "Генерация маски", - ocr: "Распознавание текста", - pending: "Ожидание", - "pending-pos": "Ожидание", - rendering: "Отрисовка", - saved: "Сохранено", - textline_merge: "Обьединение текста", - translating: "Переводится", - upscaling: "Увеличение изображения" - }, - tip: { - check_img_status_failed: "Не удалось проверить статус изображения", - download_img_failed: "Не удалось скачать изображение", - error: "Ошибка перевода", - get_translator_list_error: "Произошла ошибка во время получения списка доступных переводчиков", - id_not_returned: "ID не вернули(", - img_downloading: "Скачивание изображений", - img_not_fully_loaded: "Изображение всё ещё загружается", - pending: "Ожидение, позиция в очереди {{pos}}", - resize_img_failed: "Не удалось изменить размер изображения", - translation_completed: "Перевод завершён", - upload_error: "Ошибка загрузки изображения", - upload_return_error: "Ошибка перевода на сервере", - wait_translation: "Ожидание перевода" - }, - translator: { - baidu: "baidu", - deepl: "DeepL", - google: "Google", - "gpt3.5": "GPT-3.5", - none: "Убрать текст", - offline: "Оффлайн переводчик", - original: "Оригинал", - youdao: "youdao" - } - } -}; - -const langList = ['zh', 'en', 'ru']; -/** 判断传入的字符串是否是支持的语言类型代码 */ -const isLanguages = lang => Boolean(lang) && langList.includes(lang); - -/** 返回浏览器偏好语言 */ -const getBrowserLang = () => { - let newLang; - for (let i = 0; i < navigator.languages.length; i++) { - const language = navigator.languages[i]; - const matchLang = langList.find(l => l === language || l === language.split('-')[0]); - if (matchLang) { - newLang = matchLang; - break; - } - } - return newLang; -}; -const getSaveLang = async () => typeof GM === 'undefined' ? localStorage.getItem('Languages') : GM.getValue('Languages'); -const setSaveLang = async val => typeof GM === 'undefined' ? localStorage.setItem('Languages', val) : GM.setValue('Languages', val); -const getInitLang = async () => { - const saveLang = await getSaveLang(); - if (isLanguages(saveLang)) return saveLang; - const lang = getBrowserLang() ?? 'zh'; - setSaveLang(lang); - return lang; +/** 节流的 createMemo */ +const createThrottleMemo = (fn, wait = 100, init = fn(undefined), options = undefined) => { + const scheduled = createScheduled(_fn => throttle(_fn, wait)); + return createRootMemo(prev => scheduled() ? fn(prev) : prev, init, options); }; - -const [lang, setLang] = solidJs.createSignal('zh'); -const setInitLang = async () => setLang(await getInitLang()); -const t = solidJs.createRoot(() => { - solidJs.createEffect(solidJs.on(lang, async () => setSaveLang(lang()), { - defer: true +const createMemoMap = fnMap => { + const memoMap = Object.fromEntries(Object.entries(fnMap).map(([key, fn]) => { + // 如果函数已经是 createMemo 创建的,就直接使用 + if (fn.name === 'bound readSignal') return [key, fn]; + return [key, createRootMemo(fn, undefined)]; })); - const locales = solidJs.createMemo(() => { - switch (lang()) { - case 'en': - return en; - case 'ru': - return ru; - default: - return zh; - } + const map = createRootMemo(() => { + const obj = {}; + for (const key of Object.keys(memoMap)) Reflect.set(obj, key, memoMap[key]()); + return obj; }); - return (keys, variables) => { - let text = byPath(locales(), keys) ?? ''; - if (variables) for (const [k, v] of Object.entries(variables)) text = text.replaceAll(\`{{\${k}}}\`, \`\${String(v)}\`); - return text; - }; -}); - -var css$3 = ".root{align-items:flex-end;bottom:0;flex-direction:column;font-size:16px;pointer-events:none;position:fixed;right:0;z-index:2147483647}.item,.root{display:flex}.item{align-items:center;animation:bounceInRight .5s 1;background:#fff;border-radius:4px;box-shadow:0 1px 10px 0 #0000001a,0 2px 15px 0 #0000000d;color:#000;cursor:pointer;margin:1em;max-width:min(30em,100vw);overflow:hidden;padding:.8em 1em;pointer-events:auto;position:relative;width:fit-content}.item>svg{color:var(--theme);margin-right:.5em;width:1.5em}.item[data-exit]{animation:bounceOutRight .5s 1}.schedule{background-color:var(--theme);bottom:0;height:.2em;left:0;position:absolute;transform-origin:left;width:100%}.item[data-schedule] .schedule{transition:transform .1s}.item:not([data-schedule]) .schedule{animation:schedule linear 1 forwards}:is(.item:hover,.item[data-schedule],.root[data-paused]) .schedule{animation-play-state:paused}.msg{line-height:1.4em;text-align:start;white-space:break-spaces;width:fit-content;word-break:break-word}.msg h2{margin:0}.msg h3{margin:.7em 0}.msg ul{margin:0;text-align:left}.msg button{background-color:#eee;border:none;border-radius:.4em;cursor:pointer;font-size:inherit;margin:0 .5em;outline:none;padding:.2em .6em}:is(.msg button):hover{background:#e0e0e0}p{margin:0}@keyframes schedule{0%{transform:scaleX(1)}to{transform:scaleX(0)}}@keyframes bounceInRight{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(3000px,0,0) scaleX(3)}60%{opacity:1;transform:translate3d(-25px,0,0) scaleX(1)}75%{transform:translate3d(10px,0,0) scaleX(.98)}90%{transform:translate3d(-5px,0,0) scaleX(.995)}to{transform:translateZ(0)}}@keyframes bounceOutRight{20%{opacity:1;transform:translate3d(-20px,0,0) scaleX(.9)}to{opacity:0;transform:translate3d(2000px,0,0) scaleX(2)}}"; -var modules_c21c94f2$3 = {"root":"root","item":"item","bounceInRight":"bounceInRight","bounceOutRight":"bounceOutRight","schedule":"schedule","msg":"msg"}; - -const [_state$1, _setState$1] = store$2.createStore({ - list: [], - map: {} -}); -const setState$1 = fn => _setState$1(store$2.produce(fn)); -const store$1 = _state$1; -const creatId = () => { - let id = \`\${Date.now()}\`; - while (Reflect.has(store$1.map, id)) id += '_'; - return id; + return map; }; - -var _tmpl$$P = /*#__PURE__*/web.template(\`\`); -const MdCheckCircle = ((props = {}) => (() => { - var _el$ = _tmpl$$P(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var _tmpl$$O = /*#__PURE__*/web.template(\`\`); -const MdWarning = ((props = {}) => (() => { - var _el$ = _tmpl$$O(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var _tmpl$$N = /*#__PURE__*/web.template(\`\`); -const MdError = ((props = {}) => (() => { - var _el$ = _tmpl$$N(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var _tmpl$$M = /*#__PURE__*/web.template(\`\`); -const MdInfo = ((props = {}) => (() => { - var _el$ = _tmpl$$M(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -const toast$2 = (msg, options) => { - if (!msg) return; - const id = options?.id ?? (typeof msg === 'string' ? msg : creatId()); - setState$1(state => { - if (Reflect.has(state.map, id)) { - Object.assign(state.map[id], { - msg, - ...options, - update: true - }); - return; - } - state.map[id] = { - id, - type: 'info', - duration: 3000, - msg, - ...options - }; - state.list.push(id); +const createRootEffect = (fn, val, options) => solidJs.getOwner() ? solidJs.createEffect(fn, val, options) : solidJs.createRoot(() => solidJs.createEffect(fn, val, options)); +const createEffectOn = (deps, fn, options) => createRootEffect(solidJs.on(deps, fn, options)); +const onAutoMount = fn => { + const owner = solidJs.getOwner(); + if (!owner) return fn(owner); + solidJs.onMount(() => { + const cleanFn = fn(owner); + if (cleanFn) solidJs.onCleanup(cleanFn); }); - - /** 弹窗后记录一下 */ - let fn = log; - switch (options?.type) { - case 'warn': - fn = log.warn; - break; - case 'error': - fn = log.error; - break; - } - fn('Toast:', msg); - if (options?.throw && typeof msg === 'string') throw new Error(msg); -}; -toast$2.dismiss = id => { - if (!Reflect.has(store$1.map, id)) return; - _setState$1('map', id, 'exit', true); -}; -toast$2.set = (id, options) => { - if (!Reflect.has(store$1.map, id)) return; - setState$1(state => Object.assign(state.map[id], options)); -}; -toast$2.success = (msg, options) => toast$2(msg, { - ...options, - exit: undefined, - type: 'success' -}); -toast$2.warn = (msg, options) => toast$2(msg, { - ...options, - exit: undefined, - type: 'warn' -}); -toast$2.error = (msg, options) => toast$2(msg, { - ...options, - exit: undefined, - type: 'error' -}); - -var _tmpl$$L = /*#__PURE__*/web.template(\`
\`), - _tmpl$2$c = /*#__PURE__*/web.template(\`
\`); -const iconMap = { - info: MdInfo, - success: MdCheckCircle, - warn: MdWarning, - error: MdError -}; -const colorMap = { - info: '#3a97d7', - success: '#23bb35', - warn: '#f0c53e', - error: '#e45042', - custom: '#1f2936' }; -/** 删除 toast */ -const dismissToast = id => setState$1(state => { - state.map[id].onDismiss?.({ - ...state.map[id] - }); - const i = state.list.indexOf(id); - if (i !== -1) state.list.splice(i, 1); - Reflect.deleteProperty(state.map, id); +const promisifyRequest = request => new Promise((resolve, reject) => { + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); }); - -/** 重置 toast 的 update 属性 */ -const resetToastUpdate = id => _setState$1('map', id, 'update', undefined); -const ToastItem = props => { - /** 是否要显示进度 */ - const showSchedule = solidJs.createMemo(() => props.duration === Number.POSITIVE_INFINITY && props.schedule ? true : undefined); - const dismiss = e => { - e.stopPropagation(); - if (showSchedule() && 'animationName' in e) return; - toast$2.dismiss(props.id); +const openDb = (version, initSchema) => new Promise((resolve, reject) => { + const request = indexedDB.open('ComicReadScript', version); + request.onupgradeneeded = () => initSchema(request.result); + request.onsuccess = () => resolve(request.result); + request.onerror = error => { + console.error('数据库打开失败', error); + reject(new Error('数据库打开失败')); }; - - // 在退出动画结束后才真的删除 - const handleAnimationEnd = () => { - if (!props.exit) return; - dismissToast(props.id); +}); +const useCache = async (initSchema, version = 1) => { + const db = await openDb(version, initSchema); + return { + set: (storeName, value) => promisifyRequest(db.transaction(storeName, 'readwrite').objectStore(storeName).put(value)), + get: async (storeName, query) => promisifyRequest(db.transaction(storeName, 'readonly').objectStore(storeName).get(query)), + del: (storeName, query) => promisifyRequest(db.transaction(storeName, 'readwrite').objectStore(storeName).delete(query)) }; - let scheduleRef; - solidJs.createEffect(() => { - if (!props.update) return; - resetToastUpdate(props.id); - if (!scheduleRef) return; - for (const animation of scheduleRef.getAnimations()) { - animation.cancel(); - animation.play(); - } - }); - const handleClick = e => { - props.onClick?.(); - dismiss(e); +}; + +const createPointerState = (e, type = 'down') => { + const xy = [e.clientX, e.clientY]; + return { + id: e.pointerId, + type, + xy, + initial: xy, + last: xy, + startTime: performance.now(), + target: e.target }; - return (() => { - var _el$ = _tmpl$2$c(), - _el$2 = _el$.firstChild; - _el$.addEventListener("animationend", handleAnimationEnd); - _el$.addEventListener("click", handleClick); - web.insert(_el$, web.createComponent(web.Dynamic, { - get component() { - return iconMap[props.type]; - } - }), _el$2); - web.insert(_el$2, (() => { - var _c$ = web.memo(() => typeof props.msg === 'string'); - return () => _c$() ? props.msg : web.createComponent(props.msg, {}); - })()); - web.insert(_el$, web.createComponent(solidJs.Show, { - get when() { - return props.duration !== Number.POSITIVE_INFINITY || props.schedule !== undefined; - }, - get children() { - var _el$3 = _tmpl$$L(); - _el$3.addEventListener("animationend", dismiss); - var _ref$ = scheduleRef; - typeof _ref$ === "function" ? web.use(_ref$, _el$3) : scheduleRef = _el$3; - web.effect(_p$ => { - var _v$ = modules_c21c94f2$3.schedule, - _v$2 = \`\${props.duration}ms\`, - _v$3 = showSchedule() ? \`scaleX(\${props.schedule})\` : undefined; - _v$ !== _p$.e && web.className(_el$3, _p$.e = _v$); - _v$2 !== _p$.t && ((_p$.t = _v$2) != null ? _el$3.style.setProperty("animation-duration", _v$2) : _el$3.style.removeProperty("animation-duration")); - _v$3 !== _p$.a && ((_p$.a = _v$3) != null ? _el$3.style.setProperty("transform", _v$3) : _el$3.style.removeProperty("transform")); - return _p$; - }, { - e: undefined, - t: undefined, - a: undefined - }); - return _el$3; - } - }), null); - web.effect(_p$ => { - var _v$4 = modules_c21c94f2$3.item, - _v$5 = colorMap[props.type], - _v$6 = showSchedule(), - _v$7 = props.exit, - _v$8 = modules_c21c94f2$3.msg; - _v$4 !== _p$.e && web.className(_el$, _p$.e = _v$4); - _v$5 !== _p$.t && ((_p$.t = _v$5) != null ? _el$.style.setProperty("--theme", _v$5) : _el$.style.removeProperty("--theme")); - _v$6 !== _p$.a && web.setAttribute(_el$, "data-schedule", _p$.a = _v$6); - _v$7 !== _p$.o && web.setAttribute(_el$, "data-exit", _p$.o = _v$7); - _v$8 !== _p$.i && web.className(_el$2, _p$.i = _v$8); - return _p$; +}; +const useDrag = ({ + ref, + handleDrag, + easyMode, + handleClick, + skip, + touches = new Map() +}) => { + helper.onAutoMount(() => { + const controller = new AbortController(); + const options = { + capture: false, + passive: true, + signal: controller.signal + }; + const handleDown = e => { + if (skip?.(e)) return; + e.stopPropagation(); + if (!easyMode?.() && e.buttons !== 1) return; + ref.setPointerCapture(e.pointerId); + const state = createPointerState(e); + touches.set(e.pointerId, state); + handleDrag(state, e); + }; + const handleMove = e => { + e.preventDefault(); + if (!easyMode?.() && e.buttons !== 1) return; + const state = touches.get(e.pointerId); + if (!state) return; + state.type = 'move'; + state.xy = [e.clientX, e.clientY]; + handleDrag(state, e); + state.last = state.xy; + }; + const handleUp = e => { + e.stopPropagation(); + ref.releasePointerCapture(e.pointerId); + const state = touches.get(e.pointerId); + if (!state) return; + touches.delete(e.pointerId); + state.type = 'up'; + state.xy = [e.clientX, e.clientY]; + + // 判断单击 + if (handleClick && touches.size === 0 && approx(state.xy[0] - state.initial[0], 0, 5) && approx(state.xy[1] - state.initial[1], 0, 5) && performance.now() - state.startTime < 200) handleClick(e, state.target); + handleDrag(state, e); + }; + ref.addEventListener('pointerdown', handleDown, options); + ref.addEventListener('pointermove', handleMove, { + ...options, + passive: false + }); + ref.addEventListener('pointerup', handleUp, options); + ref.addEventListener('pointercancel', e => { + e.stopPropagation(); + const state = touches.get(e.pointerId); + if (!state) return; + state.type = 'cancel'; + handleDrag(state, e); + touches.clear(); }, { - e: undefined, - t: undefined, - a: undefined, - o: undefined, - i: undefined + capture: false, + passive: true, + signal: controller.signal }); - return _el$; - })(); + if (easyMode) { + ref.addEventListener('pointerover', handleDown, options); + ref.addEventListener('pointerout', handleUp, options); + } + return () => controller.abort(); + }); }; -var _tmpl$$K = /*#__PURE__*/web.template(\`
\`); -const [ref, setRef] = solidJs.createSignal(); -const Toaster = () => { - const [visible, setVisible] = solidJs.createSignal(document.visibilityState === 'visible'); - solidJs.onMount(() => { - const handleVisibilityChange = () => { - setVisible(document.visibilityState === 'visible'); +const useStore = initState => { + const [_state, _setState] = store.createStore(initState); + return { + _state, + _setState, + setState: fn => _setState(store.produce(fn)), + store: _state + }; +}; + +const useStyleSheet = e => { + const styleSheet = new CSSStyleSheet(); + helper.onAutoMount(() => { + const root = e?.getRootNode() ?? document; + root.adoptedStyleSheets = [...root.adoptedStyleSheets, styleSheet]; + return () => { + const index = root.adoptedStyleSheets.indexOf(styleSheet); + if (index !== -1) root.adoptedStyleSheets.splice(index, 1); }; - document.addEventListener('visibilitychange', handleVisibilityChange); - solidJs.onCleanup(() => document.removeEventListener('visibilitychange', handleVisibilityChange)); }); - return (() => { - var _el$ = _tmpl$$K(); - web.use(setRef, _el$); - web.insert(_el$, web.createComponent(solidJs.For, { - get each() { - return store$1.list; + return styleSheet; +}; +const useStyle = (css, e) => { + const styleSheet = useStyleSheet(e); + if (typeof css === 'string') styleSheet.replaceSync(css);else helper.createEffectOn(css, style => styleSheet.replaceSync(style)); +}; +/** 用 CSSStyleSheet 实现和修改 style 一样的效果 */ +const useStyleMemo = (selector, styleMapArg, e) => { + const styleSheet = useStyleSheet(e); + styleSheet.insertRule(\`\${selector} { }\`); + const { + style + } = styleSheet.cssRules[0]; + // 等火狐实现了 CSS Typed OM 后改用 styleMap 性能会更好,也能使用 CSS Typed OM 的 单位 + + const setStyle = (key, val) => { + if (val === undefined || val === '') return style.removeProperty(key); + style.setProperty(key, typeof val === 'string' ? val : \`\${val}\`); + }; + const styleMapList = Array.isArray(styleMapArg) ? styleMapArg : [styleMapArg]; + for (const styleMap of styleMapList) { + if (typeof styleMap === 'object') { + for (const [key, val] of Object.entries(styleMap)) { + const styleText = helper.createRootMemo(val); + helper.createEffectOn(styleText, newVal => setStyle(key, newVal)); + } + } else { + const styleMemoMap = helper.createRootMemo(styleMap); + helper.createEffectOn(styleMemoMap, map => { + for (const [key, val] of Object.entries(map)) setStyle(key, val); + }); + } + } +}; + +const zh = { + alert: { + comic_load_error: "漫画加载出错", + download_failed: "下载失败", + fetch_comic_img_failed: "获取漫画图片失败", + img_load_failed: "图片加载失败", + no_img_download: "没有能下载的图片", + repeat_load: "加载图片中,请稍候", + server_connect_failed: "无法连接到服务器" + }, + button: { + close_current_page_translation: "关闭当前页的翻译", + download: "下载", + download_completed: "下载完成", + downloading: "下载中", + exit: "退出", + grid_mode: "网格模式", + packaging: "打包中", + page_fill: "页面填充", + page_mode_double: "双页模式", + page_mode_single: "单页模式", + scroll_mode: "卷轴模式", + setting: "设置", + translate_current_page: "翻译当前页", + zoom_in: "放大", + zoom_out: "缩小" + }, + description: "为漫画站增加双页阅读、翻译等优化体验的增强功能。", + end_page: { + next_button: "下一话", + prev_button: "上一话", + tip: { + end_jump: "已到结尾,继续向下翻页将跳至下一话", + exit: "已到结尾,继续翻页将退出", + start_jump: "已到开头,继续向上翻页将跳至上一话" + } + }, + hotkeys: { + enter_read_mode: "进入阅读模式", + exit: "退出", + float_tag_list: "悬浮标签列表", + jump_to_end: "跳至尾页", + jump_to_home: "跳至首页", + page_down: "向下翻页", + page_up: "向上翻页", + scroll_down: "向下滚动", + scroll_left: "向左滚动", + scroll_right: "向右滚动", + scroll_up: "向上滚动", + switch_auto_enlarge: "切换图片自动放大选项", + switch_dir: "切换阅读方向", + switch_grid_mode: "切换网格模式", + switch_page_fill: "切换页面填充", + switch_scroll_mode: "切换卷轴模式", + switch_single_double_page_mode: "切换单双页模式", + translate_current_page: "翻译当前页" + }, + img_status: { + error: "加载出错", + loading: "正在加载", + wait: "等待加载" + }, + other: { + auto_enter_read_mode: "自动进入阅读模式", + "default": "默认", + disable: "禁用", + enter_comic_read_mode: "进入漫画阅读模式", + fab_hidden: "隐藏悬浮按钮", + fab_show: "显示悬浮按钮", + fill_page: "填充页", + img_loading: "图片加载中", + loading_img: "加载图片中", + read_mode: "阅读模式" + }, + pwa: { + alert: { + img_data_error: "图片数据错误", + img_not_found: "找不到图片", + img_not_found_files: "请选择图片文件或含有图片文件的压缩包", + img_not_found_folder: "文件夹下没有图片文件或含有图片文件的压缩包", + not_valid_url: "不是有效的 URL", + repeat_load: "正在加载其他文件中……", + unzip_error: "解压出错", + unzip_password_error: "解压密码错误", + userscript_not_installed: "未安装 ComicRead 脚本" + }, + button: { + enter_url: "输入 URL", + install: "安装", + no_more_prompt: "不再提示", + resume_read: "恢复阅读", + select_files: "选择文件", + select_folder: "选择文件夹" + }, + install_md: "### 每次都要打开这个网页很麻烦?\\n如果你希望\\n1. 能有独立的窗口,像是在使用本地软件一样\\n1. 加入本地压缩文件的打开方式之中,方便直接打开\\n1. 离线使用~~(主要是担心国内网络抽风无法访问这个网页~~\\n### 欢迎将本页面作为 PWA 应用安装到电脑上😃👍", + message: { + enter_password: "请输入密码", + unzipping: "解压缩中" + }, + tip_enter_url: "请输入压缩包 URL", + tip_md: "# ComicRead PWA\\n使用 [ComicRead](https://github.com/hymbz/ComicReadScript) 的阅读模式阅读**本地**漫画\\n---\\n### 将图片文件、文件夹、压缩包直接拖入即可开始阅读\\n*也可以选择**直接粘贴**或**输入**压缩包 URL 下载阅读*" + }, + setting: { + hotkeys: { + add: "添加新快捷键", + restore: "恢复默认快捷键" + }, + language: "语言", + option: { + abreast_duplicate: "每列重复比例", + abreast_mode: "并排卷轴模式", + always_load_all_img: "始终加载所有图片", + auto_switch_page_mode: "自动切换单双页模式", + background_color: "背景颜色", + click_page_turn_area: "点击区域", + click_page_turn_enabled: "点击翻页", + click_page_turn_swap_area: "左右点击区域交换", + click_page_turn_vertical: "上下翻页", + dark_mode: "夜间模式", + dir_ltr: "从左到右(美漫)", + dir_rtl: "从右到左(日漫)", + disable_auto_enlarge: "禁止图片自动放大", + first_page_fill: "默认启用首页填充", + fit_to_width: "图片适合宽度", + jump_to_next_chapter: "翻页至上/下一话", + paragraph_dir: "阅读方向", + paragraph_display: "显示", + paragraph_hotkeys: "快捷键", + paragraph_operation: "操作", + paragraph_other: "其他", + paragraph_scrollbar: "滚动条", + paragraph_translation: "翻译", + preload_page_num: "预加载页数", + scroll_mode_img_scale: "卷轴图片缩放", + scroll_mode_img_spacing: "卷轴图片间距", + scrollbar_auto_hidden: "自动隐藏", + scrollbar_easy_scroll: "快捷滚动", + scrollbar_position: "位置", + scrollbar_position_auto: "自动", + scrollbar_position_bottom: "底部", + scrollbar_position_hidden: "隐藏", + scrollbar_position_right: "右侧", + scrollbar_position_top: "顶部", + scrollbar_show_img_status: "显示图片加载状态", + show_clickable_area: "显示点击区域", + show_comments: "在结束页显示评论", + swap_page_turn_key: "左右翻页键交换", + zoom: "图片缩放" + }, + translation: { + cotrans_tip: "

将使用 Cotrans 提供的接口翻译图片,该服务器由其维护者用爱发电自费维护

\\n

多人同时使用时需要排队等待,等待队列达到上限后再上传新图片会报错,需要过段时间再试

\\n

所以还请 注意用量

\\n

更推荐使用自己本地部署的项目,既不占用服务器资源也不需要排队

", + options: { + detection_resolution: "文本扫描清晰度", + direction: "渲染字体方向", + direction_auto: "原文一致", + direction_horizontal: "仅限水平", + direction_vertical: "仅限垂直", + forceRetry: "忽略缓存强制重试", + localUrl: "自定义服务器 URL", + onlyDownloadTranslated: "只下载完成翻译的图片", + target_language: "目标语言", + text_detector: "文本扫描器", + translator: "翻译服务" }, - children: id => web.createComponent(ToastItem, web.mergeProps(() => store$1.map[id])) - })); - web.effect(_p$ => { - var _v$ = modules_c21c94f2$3.root, - _v$2 = visible() ? undefined : ''; - _v$ !== _p$.e && web.className(_el$, _p$.e = _v$); - _v$2 !== _p$.t && web.setAttribute(_el$, "data-paused", _p$.t = _v$2); - return _p$; - }, { - e: undefined, - t: undefined - }); - return _el$; - })(); -}; - -const ToastStyle = new CSSStyleSheet(); -ToastStyle.replaceSync(css$3); - -const getDom = id => { - let dom = document.getElementById(id); - if (dom) { - dom.innerHTML = ''; - return dom; + server: "翻译服务器", + server_selfhosted: "本地部署", + translate_after_current: "翻译当前页至结尾", + translate_all_img: "翻译全部图片" + } + }, + site: { + add_feature: { + associate_nhentai: "关联nhentai", + auto_page_turn: "无限滚动", + block_totally: "彻底屏蔽漫画", + colorize_tag: "标签染色", + detect_ad: "识别广告页", + float_tag_list: "悬浮标签列表", + hotkeys: "快捷键", + load_original_image: "加载原图", + open_link_new_page: "在新页面中打开链接", + quick_favorite: "快捷收藏", + quick_rating: "快捷评分", + quick_tag_define: "快捷查看标签定义", + remember_current_site: "记住当前站点" + }, + changed_load_failed: "网站发生变化,无法加载漫画", + ehentai: { + change_favorite_failed: "收藏夹修改失败", + change_favorite_success: "收藏夹修改成功", + change_rating_failed: "评分修改失败", + change_rating_success: "评分修改成功", + fetch_favorite_failed: "获取收藏夹信息失败", + fetch_img_page_source_failed: "获取图片页源码失败", + fetch_img_page_url_failed: "从详情页获取图片页地址失败", + fetch_img_url_failed: "从图片页获取图片地址失败", + html_changed_nhentai_failed: "页面结构发生改变,关联 nhentai 漫画功能无法正常生效", + ip_banned: "IP地址被禁", + nhentai_error: "nhentai 匹配出错", + nhentai_failed: "匹配失败,请在确认登录 {{nhentai}} 后刷新" + }, + need_captcha: "需要人机验证", + nhentai: { + fetch_next_page_failed: "获取下一页漫画数据失败", + tag_blacklist_fetch_failed: "标签黑名单获取失败" + }, + settings_tip: "设置", + show_settings_menu: "显示设置菜单", + simple: { + auto_read_mode_message: "已默认开启「自动进入阅读模式」", + no_img: "未找到合适的漫画图片,\\n如有需要可点此关闭简易阅读模式", + simple_read_mode: "使用简易阅读模式" + } + }, + touch_area: { + menu: "菜单", + next: "下页", + prev: "上页", + type: { + edge: "边缘", + l: "L", + left_right: "左右", + up_down: "上下" + } + }, + translation: { + status: { + "default": "未知状态", + detection: "正在检测文本", + downscaling: "正在缩小图片", + error: "翻译出错", + "error-lang": "你选择的翻译服务不支持你选择的语言", + "error-translating": "翻译服务没有返回任何文本", + "error-with-id": "翻译出错", + finished: "正在整理结果", + inpainting: "正在修补图片", + "mask-generation": "正在生成文本掩码", + ocr: "正在识别文本", + pending: "正在等待", + "pending-pos": "正在等待", + rendering: "正在渲染", + saved: "保存结果", + textline_merge: "正在整合文本", + translating: "正在翻译文本", + upscaling: "正在放大图片" + }, + tip: { + check_img_status_failed: "检查图片状态失败", + download_img_failed: "下载图片失败", + error: "翻译出错", + get_translator_list_error: "获取可用翻译服务列表时出错", + id_not_returned: "未返回 id", + img_downloading: "正在下载图片", + img_not_fully_loaded: "图片未加载完毕", + pending: "正在等待,列队还有 {{pos}} 张图片", + resize_img_failed: "缩放图片失败", + translation_completed: "翻译完成", + upload_error: "图片上传出错", + upload_return_error: "服务器翻译出错", + wait_translation: "等待翻译" + }, + translator: { + baidu: "百度", + deepl: "DeepL", + google: "谷歌", + "gpt3.5": "GPT-3.5", + none: "删除文本", + offline: "离线模型", + original: "原文", + youdao: "有道" + } } - dom = document.createElement('div'); - dom.id = id; - document.body.append(dom); - return dom; -}; - -/** 挂载 solid-js 组件 */ -const mountComponents = (id, fc, styleSheets) => { - const dom = getDom(id); - dom.style.setProperty('display', 'unset', 'important'); - const shadowDom = dom.attachShadow({ - mode: 'closed' - }); - if (styleSheets) shadowDom.adoptedStyleSheets = styleSheets; - web.render(fc, shadowDom); - return dom; }; -let dom$2; -const init = () => { - if (dom$2 || ref()) return; - - // 提前挂载漫画节点,防止 toast 没法显示在漫画上层 - if (!document.getElementById('comicRead')) { - const _dom = document.createElement('div'); - _dom.id = 'comicRead'; - document.body.append(_dom); - } - dom$2 = mountComponents('toast', () => web.createComponent(Toaster, {}), [ToastStyle]); - dom$2.style.setProperty('z-index', '2147483647', 'important'); -}; -const toast$1 = new Proxy(toast$2, { - get(target, propKey) { - init(); - return target[propKey]; +const en = { + alert: { + comic_load_error: "Comic loading error", + download_failed: "Download failed", + fetch_comic_img_failed: "Failed to fetch comic images", + img_load_failed: "Image loading failed", + no_img_download: "No images available for download", + repeat_load: "Loading image, please wait", + server_connect_failed: "Unable to connect to the server" }, - apply(target, propKey, args) { - init(); - const fn = propKey in target ? target[propKey] : target; - return fn(...args); - } -}); - -// 将 xmlHttpRequest 包装为 Promise -const xmlHttpRequest = details => new Promise((resolve, reject) => { - GM_xmlhttpRequest({ - ...details, - onload: resolve, - onerror: reject, - ontimeout: reject - }); -}); -/** 发起请求 */ -const request$1 = async (url, details, retryNum = 0, errorNum = 0) => { - const headers = { - Referer: window.location.href - }; - const errorText = \`\${details?.errorText ?? t('alert.comic_load_error')}\\nurl: \${url}\`; - try { - // 虽然 GM_xmlhttpRequest 有 fetch 选项,但在 stay 上不太稳定 - // 为了支持 ios 端只能自己实现一下了 - if (details?.fetch ?? (url.startsWith('/') || url.startsWith(window.location.origin))) { - const res = await fetch(url, { - method: 'GET', - headers, - ...details, - // eslint-disable-next-line unicorn/no-invalid-fetch-options - body: details?.data, - signal: AbortSignal.timeout?.(details?.timeout ?? 1000 * 10) - }); - if (!details?.noCheckCode && res.status !== 200) { - log.error(errorText, res); - throw new Error(errorText); - } - let response = null; - switch (details?.responseType) { - case 'arraybuffer': - response = await res.arrayBuffer(); - break; - case 'blob': - response = await res.blob(); - break; - case 'json': - response = await res.json(); - break; - } - return { - status: res.status, - statusText: res.statusText, - response, - responseText: response ? '' : await res.text() - }; - } - const res = await xmlHttpRequest({ - method: 'GET', - url, - headers, - timeout: 1000 * 10, - ...details - }); - if (!details?.noCheckCode && res.status !== 200) { - log.error(errorText, res); - throw new Error(errorText); + button: { + close_current_page_translation: "Close translation of the current page", + download: "Download", + download_completed: "Download completed", + downloading: "Downloading", + exit: "Exit", + grid_mode: "Grid mode", + packaging: "Packaging", + page_fill: "Page fill", + page_mode_double: "Double page mode", + page_mode_single: "Single page mode", + scroll_mode: "Scroll mode", + setting: "Settings", + translate_current_page: "Translate current page", + zoom_in: "Zoom in", + zoom_out: "Zoom out" + }, + description: "Add enhanced features to the comic site for optimized experience, including dual-page reading and translation.", + end_page: { + next_button: "Next chapter", + prev_button: "Prev chapter", + tip: { + end_jump: "Reached the last page, scrolling down will jump to the next chapter", + exit: "Reached the last page, scrolling down will exit", + start_jump: "Reached the first page, scrolling up will jump to the previous chapter" } - return res; - } catch (error) { - if (errorNum >= retryNum) { - (details?.noTip ? console.error : toast$1.error)(errorText); - throw new Error(errorText); + }, + hotkeys: { + enter_read_mode: "Enter reading mode", + exit: "Exit", + float_tag_list: "Floating tag list", + jump_to_end: "Jump to the last page", + jump_to_home: "Jump to the first page", + page_down: "Turn the page to the down", + page_up: "Turn the page to the up", + scroll_down: "Scroll down", + scroll_left: "Scroll left", + scroll_right: "Scroll right", + scroll_up: "Scroll up", + switch_auto_enlarge: "Switch auto image enlarge option", + switch_dir: "Switch reading direction", + switch_grid_mode: "Switch grid mode", + switch_page_fill: "Switch page fill", + switch_scroll_mode: "Switch scroll mode", + switch_single_double_page_mode: "Switch single/double page mode", + translate_current_page: "Translate current page" + }, + img_status: { + error: "Load Error", + loading: "Loading", + wait: "Waiting for load" + }, + other: { + auto_enter_read_mode: "Auto enter reading mode", + "default": "Default", + disable: "Disable", + enter_comic_read_mode: "Enter comic reading mode", + fab_hidden: "Hide floating button", + fab_show: "Show floating button", + fill_page: "Fill Page", + img_loading: "Image loading", + loading_img: "Loading image", + read_mode: "Reading mode" + }, + pwa: { + alert: { + img_data_error: "Image data error", + img_not_found: "Image not found", + img_not_found_files: "Please select an image file or a compressed file containing image files", + img_not_found_folder: "No image files or compressed files containing image files in the folder", + not_valid_url: "Not a valid URL", + repeat_load: "Loading other files…", + unzip_error: "Decompression error", + unzip_password_error: "Decompression password error", + userscript_not_installed: "ComicRead userscript not installed" + }, + button: { + enter_url: "Enter URL", + install: "Install", + no_more_prompt: "Do not prompt again", + resume_read: "Restore reading", + select_files: "Select File", + select_folder: "Select folder" + }, + install_md: "### Tired of opening this webpage every time?\\nIf you wish to:\\n1. Have an independent window, as if using local software\\n1. Add to the local compressed file opening method for easy direct opening\\n1. Use offline\\n### Welcome to install this page as a PWA app on your computer😃👍", + message: { + enter_password: "Please enter your password", + unzipping: "Unzipping" + }, + tip_enter_url: "Please enter the URL of the compressed file", + tip_md: "# ComicRead PWA\\nRead **local** comics using [ComicRead](https://github.com/hymbz/ComicReadScript) reading mode.\\n---\\n### Drag and drop image files, folders, or compressed files directly to start reading\\n*You can also choose to **paste directly** or **enter** the URL of the compressed file for downloading and reading*" + }, + setting: { + hotkeys: { + add: "Add new hotkeys", + restore: "Restore default hotkeys" + }, + language: "Language", + option: { + abreast_duplicate: "Column duplicates ratio", + abreast_mode: "Abreast scroll mode", + always_load_all_img: "Always load all images", + auto_switch_page_mode: "Auto switch single/double page mode", + background_color: "Background Color", + click_page_turn_area: "Touch area", + click_page_turn_enabled: "Click to turn page", + click_page_turn_swap_area: "Swap LR clickable areas", + click_page_turn_vertical: "Vertically arranged clickable areas", + dark_mode: "Dark mode", + dir_ltr: "LTR (American comics)", + dir_rtl: "RTL (Japanese manga)", + disable_auto_enlarge: "Disable automatic image enlarge", + first_page_fill: "Enable first page fill by default", + fit_to_width: "Fit to width", + jump_to_next_chapter: "Turn to the next/previous chapter", + paragraph_dir: "Reading direction", + paragraph_display: "Display", + paragraph_hotkeys: "Hotkeys", + paragraph_operation: "Operation", + paragraph_other: "Other", + paragraph_scrollbar: "Scrollbar", + paragraph_translation: "Translation", + preload_page_num: "Preload page number", + scroll_mode_img_scale: "Scroll mode image zoom ratio", + scroll_mode_img_spacing: "Scroll mode image spacing", + scrollbar_auto_hidden: "Auto hide", + scrollbar_easy_scroll: "Easy scroll", + scrollbar_position: "position", + scrollbar_position_auto: "Auto", + scrollbar_position_bottom: "Bottom", + scrollbar_position_hidden: "Hidden", + scrollbar_position_right: "Right", + scrollbar_position_top: "Top", + scrollbar_show_img_status: "Show image loading status", + show_clickable_area: "Show clickable areas", + show_comments: "Show comments on the end page", + swap_page_turn_key: "Swap LR page-turning keys", + zoom: "Image zoom ratio" + }, + translation: { + cotrans_tip: "

Using the interface provided by Cotrans to translate images, which is maintained by its maintainer at their own expense.

\\n

When multiple people use it at the same time, they need to queue and wait. If the waiting queue reaches its limit, uploading new images will result in an error. Please try again after a while.

\\n

So please mind the frequency of use.

\\n

It is highly recommended to use your own locally deployed project, as it does not consume server resources and does not require queuing.

", + options: { + detection_resolution: "Text detection resolution", + direction: "Render text orientation", + direction_auto: "Follow source", + direction_horizontal: "Horizontal only", + direction_vertical: "Vertical only", + forceRetry: "Force retry (ignore cache)", + localUrl: "customize server URL", + onlyDownloadTranslated: "Download only the translated images", + target_language: "Target language", + text_detector: "Text detector", + translator: "Translator" + }, + server: "Translation server", + server_selfhosted: "Selfhosted", + translate_after_current: "Translate the current page to the end", + translate_all_img: "Translate all images" } - log.error(errorText, error); - await sleep(1000); - return request$1(url, details, retryNum, errorNum + 1); - } -}; - -/** 轮流向多个 api 发起请求 */ -const eachApi = async (url, baseUrlList, details) => { - for (const baseUrl of baseUrlList) { - try { - return await request$1(\`\${baseUrl}\${url}\`, { - ...details, - noTip: true - }); - } catch {} - } - const errorText = details?.errorText ?? t('alert.comic_load_error'); - if (!details?.noTip) toast$1.error(errorText); - log.error('所有 api 请求均失败', url, baseUrlList, details); - throw new Error(errorText); -}; - -var _tmpl$$J = /*#__PURE__*/web.template(\`\`); -const MdAutoFixHigh = ((props = {}) => (() => { - var _el$ = _tmpl$$J(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var _tmpl$$I = /*#__PURE__*/web.template(\`\`); -const MdAutoFixOff = ((props = {}) => (() => { - var _el$ = _tmpl$$I(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var _tmpl$$H = /*#__PURE__*/web.template(\`\`); -const MdAutoFlashOn = ((props = {}) => (() => { - var _el$ = _tmpl$$H(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var _tmpl$$G = /*#__PURE__*/web.template(\`\`); -const MdAutoFlashOff = ((props = {}) => (() => { - var _el$ = _tmpl$$G(); - web.spread(_el$, props, true, true); - return _el$; -})()); - -var css$2 = ".iconButtonItem{position:relative}.iconButton,.iconButtonItem{align-items:center;display:flex}.iconButton{background-color:initial;border-radius:9999px;border-style:none;color:var(--text,#fff);cursor:pointer;font-size:1.5em;height:1.5em;justify-content:center;margin:.1em;outline:none;padding:0;width:1.5em}.iconButton:focus,.iconButton:hover{background-color:var(--hover-bg-color,#fff3)}.iconButton.enabled{background-color:var(--text,#fff);color:var(--text-bg,#121212)}.iconButton.enabled:focus,.iconButton.enabled:hover{background-color:var(--hover-bg-color-enable,#fffa)}.iconButton>svg{width:1em}.iconButtonPopper{align-items:center;background-color:#303030;border-radius:.3em;color:#fff;display:flex;font-size:.8em;opacity:0;padding:.4em .5em;pointer-events:none;position:absolute;top:50%;transform:translateY(-50%);-webkit-user-select:none;user-select:none;white-space:nowrap}.iconButtonPopper[data-placement=right]{left:calc(100% + 1.5em)}.iconButtonPopper[data-placement=right]:before{border-right-color:var(--switch-bg,#6e6e6e);border-right-width:.5em;right:calc(100% + .5em)}.iconButtonPopper[data-placement=left]{right:calc(100% + 1.5em)}.iconButtonPopper[data-placement=left]:before{border-left-color:var(--switch-bg,#6e6e6e);border-left-width:.5em;left:calc(100% + .5em)}.iconButtonPopper:before{background-color:initial;border:.4em solid #0000;content:\\"\\";pointer-events:none;position:absolute;transition:opacity .15s}.iconButtonItem:is(:hover,:focus,[data-show=true]) .iconButtonPopper{opacity:1}.hidden{display:none}"; -var modules_c21c94f2$2 = {"iconButtonItem":"iconButtonItem","iconButton":"iconButton","enabled":"enabled","iconButtonPopper":"iconButtonPopper","hidden":"hidden"}; - -var _tmpl$$F = /*#__PURE__*/web.template(\`

`); - main.querySelector('button').addEventListener('click', async () => { - const comicName = main.querySelector('input')?.value; + helper.querySelector('button').addEventListener('click', async () => { + const comicName = helper.querySelector('input')?.value; if (!comicName) return; const res = await main.request(`https://s.acg.dmzj.com/comicsum/search.php?s=${comicName}`, { errorText: '搜索漫画时出错' }); const comicList = JSON.parse(res.responseText.slice(20, -1)); - main.querySelector('#list').innerHTML = comicList.map(({ + helper.querySelector('#list').innerHTML = comicList.map(({ id, comic_name, comic_author, @@ -9065,13 +8771,13 @@ const getViewpoint = async (comicId, chapterId) => { } } = dmzjDecrypt(res.responseText); document.title = title; - main.insertNode(document.body, `

${title}

`); + helper.insertNode(document.body, `

${title}

`); for (const chapter of Object.values(chapters)) { // 手动构建添加章节 dom let temp = `

${chapter.title}

`; let i = chapter.data.length; while (i--) temp += `
${chapter.data[i].chapter_title}`; - main.insertNode(document.body, temp); + helper.insertNode(document.body, temp); } document.body.childNodes[0].remove(); GM_addStyle(` @@ -9107,10 +8813,10 @@ const getViewpoint = async (comicId, chapterId) => { GM_addStyle('.subHeader{display:none !important}'); await main.universalInit({ name: 'dmzj', - getImgList: () => main.querySelectorAll('#commicBox img').map(e => e.dataset.original).filter(Boolean), - getCommentList: () => getViewpoint(unsafeWindow.subId, unsafeWindow.chapterId), - onNext: main.querySelectorClick('#loadNextChapter'), - onPrev: main.querySelectorClick('#loadPrevChapter') + getImgList: () => helper.querySelectorAll('#commicBox img').map(e => e.dataset.original).filter(Boolean), + getCommentList: () => dmzjApi.getViewpoint(unsafeWindow.subId, unsafeWindow.chapterId), + onNext: helper.querySelectorClick('#loadNextChapter'), + onPrev: helper.querySelectorClick('#loadPrevChapter') }); return; } @@ -9122,7 +8828,7 @@ const getViewpoint = async (comicId, chapterId) => { let chapterId; try { [, comicId, chapterId] = /(\d+)\/(\d+)/.exec(window.location.pathname); - data = await getChapterInfo(comicId, chapterId); + data = await dmzjApi.getChapterInfo(comicId, chapterId); } catch (error) { main.toast.error('获取漫画数据失败', { duration: Number.POSITIVE_INFINITY @@ -9160,28 +8866,20 @@ const getViewpoint = async (comicId, chapterId) => { tipDom.innerHTML = `无法获得漫画数据,请通过
GithubGreasy Fork 进行反馈`; return []; }); - setManga('commentList', await getViewpoint(comicId, chapterId)); + setManga('commentList', await dmzjApi.getViewpoint(comicId, chapterId)); break; } } -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } case 'www.idmzj.com': case 'www.dmzj.com': { -require('solid-js/store'); +const dmzjApi = require('dmzjApi'); const main = require('main'); - -/** 根据漫画 id 和章节 id 获取章节数据 */ -const getChapterInfo = async (comicId, chapterId) => { - const res = await main.request(`https://m.dmzj.com/chapinfo/${comicId}/${chapterId}.html`, { - responseType: 'json', - errorText: '获取章节数据失败' - }); - return res.response; -}; +const helper = require('helper'); const turnPage = chapterId => { if (!chapterId) return undefined; @@ -9190,9 +8888,9 @@ const turnPage = chapterId => { }; }; (async () => { - await main.waitDom('.head_wz'); + await helper.waitDom('.head_wz'); // 只在漫画页内运行 - const comicId = main.querySelector('.head_wz [id]')?.id; + const comicId = helper.querySelector('.head_wz [id]')?.id; const chapterId = /(?<=\/)\d+(?=\.html)/.exec(window.location.pathname)?.[0]; if (!comicId || !chapterId) return; const { @@ -9201,38 +8899,190 @@ const turnPage = chapterId => { } = await main.useInit('dmzj'); try { const { - next_chap_id, - prev_chap_id, - page_url - } = await getChapterInfo(comicId, chapterId); - init(() => page_url); - setManga({ - onNext: turnPage(next_chap_id), - onPrev: turnPage(prev_chap_id) + next_chap_id, + prev_chap_id, + page_url + } = await dmzjApi.getChapterInfo(comicId, chapterId); + init(() => page_url); + setManga({ + onNext: turnPage(next_chap_id), + onPrev: turnPage(prev_chap_id) + }); + } catch { + main.toast.error('获取漫画数据失败', { + duration: Number.POSITIVE_INFINITY + }); + } +})().catch(error => helper.log.error(error)); + + break; + } + + // #E-Hentai(关联 nhentai、快捷收藏、标签染色、识别广告页等) + case 'exhentai.org': + case 'e-hentai.org': + { +const Manga = require('components/Manga'); +const main = require('main'); +const helper = require('helper'); +const QrScanner = require('qr-scanner'); +const web = require('solid-js/web'); +const solidJs = require('solid-js'); +const store = require('solid-js/store'); + +const getAdPage = async (list, isAdPage, adList = new Set()) => { + let i = list.length - 1; + let normalNum = 0; + // 只检查最后十张 + for (; i >= list.length - 10; i--) { + // 开头肯定不会是广告 + if (i <= 2) break; + if (adList.has(i)) continue; + const item = list[i]; + if (!item) break; + if (await isAdPage(item)) adList.add(i); + // 找到连续两张正常漫画页后中断 + else if (normalNum) break;else normalNum += 1; + } + let adNum = 0; + for (i = Math.min(...adList); i < list.length; i++) { + if (adList.has(i)) { + adNum += 1; + continue; + } + + // 连续两张广告后面的肯定也都是广告 + if (adNum >= 2) adList.add(i); + // 夹在两张广告中间的肯定也是广告 + else if (adList.has(i - 1) && adList.has(i + 1)) adList.add(i);else adNum = 0; + } + return adList; +}; + +/** 判断像素点是否是灰阶 */ +const isGrayscalePixel = (r, g, b) => r === g && r === b; + +/** 判断一张图是否是彩图 */ +const isColorImg = imgCanvas => { + // 缩小尺寸放弃细节,避免被黑白图上的小段彩色文字干扰 + const canvas = new OffscreenCanvas(3, 3); + const ctx = canvas.getContext('2d', { + alpha: false + }); + ctx.drawImage(imgCanvas, 0, 0, canvas.width, canvas.height); + const { + data + } = ctx.getImageData(0, 0, canvas.width, canvas.height); + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + if (!isGrayscalePixel(r, g, b)) return true; + } + return false; +}; +const imgToCanvas = async img => { + if (typeof img !== 'string') { + await helper.wait(() => img.naturalHeight && img.naturalWidth, 1000 * 10); + try { + const canvas = new OffscreenCanvas(img.width, img.height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + // 没被 CORS 污染就直接使用这个 canvas + if (ctx.getImageData(0, 0, 1, 1)) return canvas; + } catch {} + } + const url = typeof img === 'string' ? img : img.src; + const res = await main.request(url, { + responseType: 'blob' + }); + const image = await helper.waitImgLoad(URL.createObjectURL(res.response)); + const canvas = new OffscreenCanvas(image.width, image.height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + return canvas; +}; + +/** 二维码白名单 */ +const qrCodeWhiteList = [ +// fanbox +/^https:\/\/[^.]+\.fanbox\.cc/, +// twitter +/^https:\/\/twitter\.com/, /^https:\/\/x\.com/, +// fantia +/^https:\/\/fantia\.jp/, +// 棉花糖 +/^https:\/\/marshmallow-qa\.com/]; + +/** 判断是否含有二维码 */ +const hasQrCode = async (imgCanvas, scanRegion, qrEngine, canvas) => { + try { + const { + data + } = await QrScanner.scanImage(imgCanvas, { + qrEngine, + canvas: canvas, + scanRegion, + alsoTryWithoutScanRegion: true }); + if (!data) return false; + helper.log(`检测到二维码: ${data}`); + return qrCodeWhiteList.every(reg => !reg.test(data)); } catch { - main.toast.error('获取漫画数据失败', { - duration: Number.POSITIVE_INFINITY - }); + return false; } -})().catch(error => main.log.error(error)); -; - break; - } +}; +const isAdImg = async (imgCanvas, qrEngine, canvas) => { + // 黑白图肯定不是广告 + if (!isColorImg(imgCanvas)) return false; + const width = imgCanvas.width / 2; + const height = imgCanvas.height / 2; - // #E-Hentai(关联 nhentai、快捷收藏、标签染色、识别广告页等) - case 'exhentai.org': - case 'e-hentai.org': - { -const main = require('main'); -const web = require('solid-js/web'); -const solidJs = require('solid-js'); -const store = require('solid-js/store'); + // 分区块扫描图片 + const scanRegionList = [undefined, + // 右下 + { + x: width, + y: height, + width, + height + }, + // 左下 + { + x: 0, + y: height, + width, + height + }, + // 右上 + { + x: width, + y: 0, + width, + height + }, + // 左上 + { + x: 0, + y: 0, + width, + height + }]; + for (const scanRegion of scanRegionList) if (await hasQrCode(imgCanvas, scanRegion, qrEngine, canvas)) return true; + return false; +}; +const byContent = (qrEngine, canvas) => async img => isAdImg(await imgToCanvas(img), qrEngine, canvas); + +/** 通过图片内容判断是否是广告 */ +const getAdPageByContent = async (imgList, adList = new Set()) => { + const qrEngine = await QrScanner.createQrEngine(); + const canvas = new OffscreenCanvas(1, 1); + return getAdPage(imgList, byContent(qrEngine, canvas), adList); +}; + +/** 通过文件名判断是否是广告 */ +const getAdPageByFileName = async (fileNameList, adList = new Set()) => getAdPage(fileNameList, fileName => /^[zZ]+/.test(fileName), adList); -var _tmpl$$3 = /*#__PURE__*/web.template(`
`), - _tmpl$2$2 = /*#__PURE__*/web.template(`
`), - _tmpl$3$1 = /*#__PURE__*/web.template(``), - _tmpl$4$1 = /*#__PURE__*/web.template(`

loading...`); let hasStyle = false; const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { if (!hasStyle) { @@ -9295,14 +9145,14 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { const updateFavorite = async () => { try { const res = await main.request(apiUrl, { - errorText: main.t('site.ehentai.fetch_favorite_failed') + errorText: helper.t('site.ehentai.fetch_favorite_failed') }); - const dom = main.domParse(res.responseText); + const dom = helper.domParse(res.responseText); const list = [...dom.querySelectorAll('.nosel > div')]; if (list.length === 10) list[0].querySelector('input').checked = false; setFavorites(list); } catch { - main.toast.error(main.t('site.ehentai.fetch_favorite_failed')); + main.toast.error(helper.t('site.ehentai.fetch_favorite_failed')); setFavorites([]); } }; @@ -9323,9 +9173,9 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { const res = await main.request(apiUrl, { method: 'POST', data: formData, - errorText: main.t('site.ehentai.change_favorite_failed') + errorText: helper.t('site.ehentai.change_favorite_failed') }); - main.toast.success(main.t('site.ehentai.change_favorite_success')); + main.toast.success(helper.t('site.ehentai.change_favorite_success')); // 修改收藏按钮样式的 js 代码 const updateCode = /\nif\(window.opener.document.+\n/.exec(res.responseText)?.[0]?.replaceAll('window.opener.document', 'window.document'); @@ -9334,7 +9184,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { await updateFavorite(); }; return (() => { - var _el$ = _tmpl$2$2(), + var _el$ = web.template(`
`)(), _el$2 = _el$.firstChild; _el$.$$click = handleClick; _el$2.checked = checked; @@ -9343,7 +9193,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { return index() <= 9; }, get children() { - var _el$3 = _tmpl$$3(); + var _el$3 = web.template(`
`)(); web.effect(_$p => (_$p = `0px -${2 + 19 * index()}px`) != null ? _el$3.style.setProperty("background-position", _$p) : _el$3.style.removeProperty("background-position")); return _el$3; } @@ -9363,7 +9213,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { return show(); }, get children() { - var _el$4 = _tmpl$3$1(); + var _el$4 = web.template(``)(); background != null ? _el$4.style.setProperty("background", background) : _el$4.style.removeProperty("background"); web.insert(_el$4, web.createComponent(solidJs.For, { get each() { @@ -9371,7 +9221,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { }, children: FavoriteItem, get fallback() { - return _tmpl$4$1(); + return web.template(`

loading...`)(); } })); web.effect(_p$ => { @@ -9406,8 +9256,8 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { /** 快捷收藏的界面 */ const quickFavorite = pageType => { if (pageType === 'gallery') { - const button = main.querySelector('#gdf'); - const root = main.querySelector('#gd3'); + const button = helper.querySelector('#gdf'); + const root = helper.querySelector('#gd3'); addQuickFavorite(button, root, `${unsafeWindow.popbase}addfav`, [0, button.firstElementChild.offsetTop]); return; } @@ -9416,7 +9266,7 @@ const quickFavorite = pageType => { switch (pageType) { case 't': { - for (const item of main.querySelectorAll('.gl1t')) { + for (const item of helper.querySelectorAll('.gl1t')) { const button = item.querySelector('[id][onclick]'); const top = item.firstElementChild.getBoundingClientRect().bottom - item.getBoundingClientRect().top; const bottom = item.lastElementChild.getBoundingClientRect().top - item.getBoundingClientRect().top; @@ -9426,7 +9276,7 @@ const quickFavorite = pageType => { } case 'e': { - for (const item of main.querySelectorAll('.gl1e')) { + for (const item of helper.querySelectorAll('.gl1e')) { const button = item.nextElementSibling.querySelector('[id][onclick]'); addQuickFavorite(button, item, /http.+?(?=')/.exec(button.getAttribute('onclick'))[0], [0, Number.parseInt(getComputedStyle(item).height, 10)]); } @@ -9439,9 +9289,9 @@ web.delegateEvents(["click"]); /** 关联 nhentai */ const associateNhentai = async (init, dynamicUpdate) => { const titleDom = document.getElementById('gn'); - const taglistDom = main.querySelector('#taglist tbody'); + const taglistDom = helper.querySelector('#taglist tbody'); if (!titleDom || !taglistDom) { - if ((document.getElementById('taglist')?.children.length ?? 1) > 0) main.toast.error(main.t('site.ehentai.html_changed_nhentai_failed')); + if ((document.getElementById('taglist')?.children.length ?? 1) > 0) main.toast.error(helper.t('site.ehentai.html_changed_nhentai_failed')); return; } const title = encodeURI(titleDom.textContent); @@ -9450,7 +9300,7 @@ const associateNhentai = async (init, dynamicUpdate) => { try { const res = await main.request(`https://nhentai.net/api/galleries/search?query=${title}`, { responseType: 'json', - errorText: main.t('site.ehentai.nhentai_error'), + errorText: helper.t('site.ehentai.nhentai_error'), noTip: true }); nHentaiComicInfo = res.response; @@ -9458,7 +9308,7 @@ const associateNhentai = async (init, dynamicUpdate) => { newTagLine.innerHTML = ` nhentai: - ${main.t('site.ehentai.nhentai_failed', { + ${helper.t('site.ehentai.nhentai_failed', { nhentai: `nhentai` })} `; @@ -9511,7 +9361,7 @@ const associateNhentai = async (init, dynamicUpdate) => { }; const showNhentaiComic = init(dynamicUpdate(async setImg => { nhentaiComicReadButton.innerHTML = ` loading - 0/${num_pages}`; - nhentaiImgList[selected_tagname] = await main.plimit(images.pages.map((page, i) => async () => { + nhentaiImgList[selected_tagname] = await helper.plimit(images.pages.map((page, i) => async () => { const imgRes = await main.request(`https://i.nhentai.net/galleries/${media_id}/${i + 1}.${fileType[page.t]}`, { headers: { Referer: `https://nhentai.net/g/${media_id}` @@ -9538,16 +9388,16 @@ const associateNhentai = async (init, dynamicUpdate) => { /** 快捷键翻页 */ const hotkeysPageTurn = pageType => { if (pageType === 'gallery') { - main.linstenKeydown(e => { + helper.linstenKeydown(e => { switch (e.key) { case 'ArrowRight': case 'd': e.preventDefault(); - return main.querySelector('.ptt td:last-child:not(.ptdd)')?.click(); + return helper.querySelector('.ptt td:last-child:not(.ptdd)')?.click(); case 'ArrowLeft': case 'a': e.preventDefault(); - return main.querySelector('.ptt td:first-child:not(.ptdd)')?.click(); + return helper.querySelector('.ptt td:first-child:not(.ptdd)')?.click(); case 'Escape': if (unsafeWindow.selected_tagname) { unsafeWindow.toggle_tagmenu(); @@ -9567,16 +9417,16 @@ const hotkeysPageTurn = pageType => { } }); } else { - main.linstenKeydown(e => { + helper.linstenKeydown(e => { switch (e.key) { case 'ArrowRight': case 'd': e.preventDefault(); - return main.querySelector('#unext')?.click(); + return helper.querySelector('#unext')?.click(); case 'ArrowLeft': case 'a': e.preventDefault(); - return main.querySelector('#uprev')?.click(); + return helper.querySelector('#uprev')?.click(); } }); } @@ -9599,7 +9449,7 @@ const getTagSetHtml = async tagset => { const res = await main.request(url, { fetch: true }); - return main.domParse(res.responseText); + return helper.domParse(res.responseText); }; /** 获取最新的标签颜色数据 */ @@ -9704,8 +9554,6 @@ const colorizeTag = async pageType => { } }; -var _tmpl$$2 = /*#__PURE__*/web.template(``), - _tmpl$2$1 = /*#__PURE__*/web.template(``); /** 快捷评分 */ const quickRating = pageType => { let list; @@ -9714,15 +9562,15 @@ const quickRating = pageType => { case 'mytags': return; case 'e': - list = main.querySelectorAll('#favform > table > tbody > tr'); + list = helper.querySelectorAll('#favform > table > tbody > tr'); break; case 'm': case 'p': case 'l': - list = main.querySelectorAll('#favform > table > tbody > tr').slice(1); + list = helper.querySelectorAll('#favform > table > tbody > tr').slice(1); break; case 't': - list = main.querySelectorAll('.gl1t'); + list = helper.querySelectorAll('.gl1t'); break; } GM_addStyle(` @@ -9739,11 +9587,11 @@ const quickRating = pageType => { const editRating = async (url, num) => { try { const dataRes = await main.request(url, { - errorText: main.t('site.ehentai.change_rating_failed'), + errorText: helper.t('site.ehentai.change_rating_failed'), noTip: true }); const reRes = /api_url = "(.+?)".+?gid = (\d+).+?token = "(.+?)".+?apiuid = (\d+).+?apikey = "(.+?)"/s.exec(dataRes.responseText); - if (!reRes) throw new Error(main.t('site.ehentai.change_rating_failed')); + if (!reRes) throw new Error(helper.t('site.ehentai.change_rating_failed')); const [, api_url, gid, token, apiuid, apikey] = reRes; const res = await main.request(api_url, { method: 'POST', @@ -9759,11 +9607,11 @@ const quickRating = pageType => { fetch: true, noTip: true }); - main.toast.success(`${main.t('site.ehentai.change_rating_success')}: ${res.response.rating_usr}`); + main.toast.success(`${helper.t('site.ehentai.change_rating_success')}: ${res.response.rating_usr}`); return res.response; } catch { - main.toast.error(main.t('site.ehentai.change_rating_failed')); - throw new Error(main.t('site.ehentai.change_rating_failed')); + main.toast.error(helper.t('site.ehentai.change_rating_failed')); + throw new Error(helper.t('site.ehentai.change_rating_failed')); } }; @@ -9778,7 +9626,7 @@ const quickRating = pageType => { const renderQuickRating = (item, ir, index) => { let basePosition = ir.style.backgroundPosition; web.render(() => (() => { - var _el$ = _tmpl$$2(), + var _el$ = web.template(``)(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling; _el$.$$mouseout = () => { @@ -9790,7 +9638,7 @@ const quickRating = pageType => { web.insert(_el$3, web.createComponent(solidJs.For, { each: coordsList, children: (coords, i) => (() => { - var _el$4 = _tmpl$2$1(); + var _el$4 = web.template(``)(); _el$4.$$click = async () => { const res = await editRating(item.querySelector('a').href, i() + 1); ir.className = res.rating_cls; @@ -9816,17 +9664,12 @@ const quickRating = pageType => { }; web.delegateEvents(["mouseout", "mouseover", "click"]); -var _tmpl$$1 = /*#__PURE__*/web.template(``); const MDLaunch = ((props = {}) => (() => { - var _el$ = _tmpl$$1(); + var _el$ = web.template(``)(); web.spread(_el$, props, true, true); return _el$; })()); -var _tmpl$ = /*#__PURE__*/web.template(`

`), - _tmpl$2 = /*#__PURE__*/web.template(`

`), - _tmpl$3 = /*#__PURE__*/web.template(``), - _tmpl$4 = /*#__PURE__*/web.template(`

loading...`); /** 快捷查看标签定义 */ const quickTagDefine = pageType => { if (pageType !== 'gallery') return; @@ -9839,13 +9682,13 @@ const quickTagDefine = pageType => { }); if (res.status !== 200) { tagContent[tag] = (() => { - var _el$ = _tmpl$(); + var _el$ = web.template(`

`)(); web.insert(_el$, () => `${res.status} - ${res.statusText}`); return _el$; })(); return; } - const html = main.domParse(res.responseText); + const html = helper.domParse(res.responseText); const content = html.querySelector('#mw-content-text'); // 将相对链接转换成正确的链接 @@ -9859,7 +9702,7 @@ const quickTagDefine = pageType => { // 删掉附加图 for (const dom of content.querySelectorAll('.thumb')) dom.remove(); tagContent[tag] = [(() => { - var _el$2 = _tmpl$2(), + var _el$2 = web.template(`

`)(), _el$3 = _el$2.firstChild; web.setAttribute(_el$3, "href", url); web.insert(_el$3, tag, null); @@ -9916,7 +9759,7 @@ const quickTagDefine = pageType => { } `); const [show, setShow] = solidJs.createSignal(false); - const root = main.querySelector('#taglist'); + const root = helper.querySelector('#taglist'); let background = 'rgba(0, 0, 0, 0)'; let dom = root; while (background === 'rgba(0, 0, 0, 0)') { @@ -9928,9 +9771,9 @@ const quickTagDefine = pageType => { return show(); }, get children() { - var _el$4 = _tmpl$3(); + var _el$4 = web.template(``)(); background != null ? _el$4.style.setProperty("background", background) : _el$4.style.removeProperty("background"); - web.insert(_el$4, () => tagContent[unsafeWindow.selected_tagname] ?? _tmpl$4()); + web.insert(_el$4, () => tagContent[unsafeWindow.selected_tagname] ?? web.template(`

loading...`)()); return _el$4; } }), root); @@ -9947,7 +9790,7 @@ const quickTagDefine = pageType => { }; // Esc 关闭 - main.linstenKeydown(e => { + helper.linstenKeydown(e => { if (e.key !== 'Escape' || !show()) return; setShow(false); e.stopImmediatePropagation(); @@ -9971,7 +9814,7 @@ const getDomPosition = dom => { }; const floatTagList = (pageType, mangaProps) => { if (pageType !== 'gallery') return; - const gd4 = main.querySelector('#gd4'); + const gd4 = helper.querySelector('#gd4'); const gd4Style = getComputedStyle(gd4); /** 背景颜色 */ @@ -9983,7 +9826,7 @@ const floatTagList = (pageType, mangaProps) => { } const { borderColor - } = getComputedStyle(main.querySelector('#gdt')); + } = getComputedStyle(helper.querySelector('#gdt')); /** 边框样式 */ const border = `1px solid ${borderColor}`; GM_addStyle(` @@ -10041,7 +9884,7 @@ const floatTagList = (pageType, mangaProps) => { setState, _setState, _state - } = main.useStore({ + } = helper.useStore({ open: false, top: 0, left: 0, @@ -10056,11 +9899,11 @@ const floatTagList = (pageType, mangaProps) => { } }); const setPos = (state, top, left) => { - state.top = main.clamp(0, top, state.bound.height); - state.left = main.clamp(0, left, state.bound.width); + state.top = helper.clamp(0, top, state.bound.height); + state.left = helper.clamp(0, left, state.bound.width); }; const setOpacity = opacity => { - _setState('opacity', main.clamp(0.5, opacity, 1)); + _setState('opacity', helper.clamp(0.5, opacity, 1)); }; setOpacity(Number(localStorage.getItem('floatTagListOpacity')) || 1); @@ -10075,13 +9918,13 @@ const floatTagList = (pageType, mangaProps) => { setState(state => { state.bound.width = window.innerWidth - gd4.clientWidth; state.bound.height = window.innerHeight - gd4.clientHeight; - state.top = main.clamp(0, state.top, state.bound.height); - state.left = main.clamp(0, state.left, state.bound.width); + state.top = helper.clamp(0, state.top, state.bound.height); + state.left = helper.clamp(0, state.left, state.bound.width); }); }; window.addEventListener('resize', hadnleResize); hadnleResize(); - main.useStyleMemo('#comicread-tag-box', { + helper.useStyleMemo('#comicread-tag-box', { display: () => store.open ? undefined : 'none', top: () => `${store.top}px`, left: () => `${store.left}px`, @@ -10113,7 +9956,7 @@ const floatTagList = (pageType, mangaProps) => { top: 0, left: 0 }; - main.useDrag({ + helper.useDrag({ ref: gd4, handleDrag({ type, @@ -10138,7 +9981,7 @@ const floatTagList = (pageType, mangaProps) => { // 窗口移到原位附近时自动收回 if (mangaProps.show) return; const rect = placeholder.getBoundingClientRect(); - if (main.approx(state.top, rect.top, 50) && main.approx(state.left, rect.left, 50)) state.open = false; + if (helper.approx(state.top, rect.top, 50) && helper.approx(state.left, rect.left, 50)) state.open = false; }); break; case 'move': @@ -10156,12 +9999,12 @@ const floatTagList = (pageType, mangaProps) => { let ehsParent; const handleEhs = () => { if (ehs) return; - ehs = main.querySelector('#ehs-introduce-box'); + ehs = helper.querySelector('#ehs-introduce-box'); if (!ehs) return; ehsParent = ehs.parentElement; // 让 ehs 的自动补全列表能显示在顶部 - const autoComplete = main.querySelector('.eh-syringe-lite-auto-complete-list'); + const autoComplete = helper.querySelector('.eh-syringe-lite-auto-complete-list'); if (autoComplete) { autoComplete.classList.add('comicread-ignore'); autoComplete.style.zIndex = '2147483647'; @@ -10169,13 +10012,13 @@ const floatTagList = (pageType, mangaProps) => { } // 只在当前有标签被选中时显示 ehs 的标签介绍 - main.hijackFn('toggle_tagmenu', (rawFn, args) => { + helper.hijackFn('toggle_tagmenu', (rawFn, args) => { const res = rawFn(...args); - if (!unsafeWindow.selected_tagname) main.querySelector('#ehs-introduce-box .ehs-close')?.click(); + if (!unsafeWindow.selected_tagname) helper.querySelector('#ehs-introduce-box .ehs-close')?.click(); return res; }); }; - main.createEffectOn(() => store.open, open => { + helper.createEffectOn(() => store.open, open => { handleEhs(); if (open) { const { @@ -10193,22 +10036,22 @@ const floatTagList = (pageType, mangaProps) => { gd4.style.width = ''; placeholder.after(gd4); if (ehs) ehsParent.append(ehs); - main.focus(); + Manga.focus(); } }, { defer: true }); - main.setDefaultHotkeys(hotkeys => ({ + Manga.setDefaultHotkeys(hotkeys => ({ ...hotkeys, float_tag_list: ['q'] })); - main.linstenKeydown(e => { + helper.linstenKeydown(e => { if (e.key === 'Escape' && store.open) { _setState('open', false); return e.stopImmediatePropagation(); } - const code = main.getKeyboardCode(e); - if (main.hotkeysMap()[code] !== 'float_tag_list') return; + const code = helper.getKeyboardCode(e); + if (Manga.hotkeysMap()[code] !== 'float_tag_list') return; e.stopPropagation(); e.preventDefault(); setState(state => { @@ -10219,47 +10062,43 @@ const floatTagList = (pageType, mangaProps) => { }); // 在悬浮状态下打完标签后移开焦点,以便能快速用快捷键关掉悬浮界面 - main.hijackFn('tag_from_field', (rawFn, args) => { + helper.hijackFn('tag_from_field', (rawFn, args) => { if (store.open) document.activeElement.blur(); return rawFn(...args); }); - const newTagInput = main.querySelector('#newtagfield'); + const newTagInput = helper.querySelector('#newtagfield'); // 悬浮状态下鼠标划过自动聚焦输入框 newTagInput.addEventListener('pointerenter', () => store.open && newTagInput.focus()); /** 根据标签链接获取对应的标签名 */ const getDropTag = tagUrl => { - const tagDom = main.querySelector(`a[href=${CSS.escape(tagUrl)}]`); + const tagDom = helper.querySelector(`a[href=${CSS.escape(tagUrl)}]`); if (!tagDom) return; // 有 ehs 的情况下 title 会是标签的简写 return tagDom.title || tagDom.id.slice(3).replaceAll('_', ' '); }; // 让标签可以直接拖进输入框,方便一次性点赞多个标签 - newTagInput.addEventListener('drop', e => { - e.dataTransfer?.items[0].getAsString(url => { - const tag = getDropTag(url); - if (!tag) return; - newTagInput.value = newTagInput.value.replace(url, `${tag}, `); - }); - }); + const handleDrop = e => { + const text = e.dataTransfer.getData('text'); + const tag = getDropTag(text); + if (!tag) return; + e.preventDefault(); + if (!newTagInput.value.includes(tag)) newTagInput.value += `${tag}, `; + }; + newTagInput.addEventListener('drop', handleDrop); // 增大拖拽标签的放置范围,不用非得拖进框 - const taglist = main.querySelector('#taglist'); + const taglist = helper.querySelector('#taglist'); taglist.addEventListener('dragover', e => e.preventDefault()); - taglist.addEventListener('drop', e => { - e.dataTransfer?.items[0].getAsString(url => { - const tag = getDropTag(url); - if (!tag || newTagInput.value.includes(tag)) return; - newTagInput.value += `${tag}, `; - }); - }); + taglist.addEventListener('dragenter', e => e.preventDefault()); + taglist.addEventListener('drop', handleDrop); }; (async () => { let pageType; - if (Reflect.has(unsafeWindow, 'display_comment_field')) pageType = 'gallery';else if (location.pathname === '/mytags') pageType = 'mytags';else pageType = main.querySelector('#ujumpbox ~ div > select')?.value; + if (Reflect.has(unsafeWindow, 'display_comment_field')) pageType = 'gallery';else if (location.pathname === '/mytags') pageType = 'mytags';else pageType = helper.querySelector('#ujumpbox ~ div > select')?.value; if (!pageType) return; const { options, @@ -10289,12 +10128,12 @@ const floatTagList = (pageType, mangaProps) => { autoShow: false }); if (Reflect.has(unsafeWindow, 'mpvkey')) { - const imgEleList = main.querySelectorAll('.mi0[id]'); - init(dynamicUpdate(setImg => main.plimit(imgEleList.map((ele, i) => async () => { + const imgEleList = helper.querySelectorAll('.mi0[id]'); + init(dynamicUpdate(setImg => helper.plimit(imgEleList.map((ele, i) => async () => { const getUrl = () => ele.querySelector('img')?.src; if (!getUrl()) unsafeWindow.load_image(i + 1); unsafeWindow.next_possible_request = 0; - const imgUrl = await main.wait(getUrl); + const imgUrl = await helper.wait(getUrl); setImg(i, imgUrl); }), undefined, 4), imgEleList.length)); return; @@ -10309,9 +10148,9 @@ const floatTagList = (pageType, mangaProps) => { // 快捷收藏。必须处于登录状态 if (unsafeWindow.apiuid !== -1 && options.quick_favorite) quickFavorite(pageType); // 快捷评分 - if (options.quick_rating) main.requestIdleCallback(() => quickRating(pageType), 1000); + if (options.quick_rating) helper.requestIdleCallback(() => quickRating(pageType), 1000); // 快捷查看标签定义 - if (options.quick_tag_define) main.requestIdleCallback(() => quickTagDefine(pageType), 1000); + if (options.quick_tag_define) helper.requestIdleCallback(() => quickTagDefine(pageType), 1000); // 不是漫画页的话 if (pageType !== 'gallery') return; @@ -10319,19 +10158,19 @@ const floatTagList = (pageType, mangaProps) => { // 表站开启了 Multi-Page Viewer 的话会将点击按钮挤出去,得缩一下位置 if (sidebarDom.children[6]) sidebarDom.children[6].style.padding = '0'; // 虽然有 Fab 了不需要这个按钮,但都点习惯了没有还挺别扭的( - main.insertNode(sidebarDom, '

Load comic

'); + helper.insertNode(sidebarDom, '

Load comic

'); const comicReadModeDom = document.getElementById('comicReadMode'); /** 从图片页获取图片地址 */ const getImgFromImgPage = async url => { const res = await main.request(url, { fetch: true, - errorText: main.t('site.ehentai.fetch_img_page_source_failed') + errorText: helper.t('site.ehentai.fetch_img_page_source_failed') }, 10); try { return /id="img" src="(.+?)"/.exec(res.responseText)[1]; } catch { - throw new Error(main.t('site.ehentai.fetch_img_url_failed')); + throw new Error(helper.t('site.ehentai.fetch_img_url_failed')); } }; @@ -10339,23 +10178,23 @@ const floatTagList = (pageType, mangaProps) => { const getImgFromDetailsPage = async (pageNum = 0) => { const res = await main.request(`${window.location.pathname}${pageNum ? `?p=${pageNum}` : ''}`, { fetch: true, - errorText: main.t('site.ehentai.fetch_img_page_url_failed') + errorText: helper.t('site.ehentai.fetch_img_page_url_failed') }); // 从详情页获取图片页的地址 const reRes = res.responseText.matchAll(/.+?title=".+?: [url, fileName]); }; const getImgNum = async () => { - let numText = main.querySelector('.gtb .gpc')?.textContent?.replaceAll(',', '').match(/\d+/g)?.at(-1); + let numText = helper.querySelector('.gtb .gpc')?.textContent?.replaceAll(',', '').match(/\d+/g)?.at(-1); if (numText) return Number(numText); const res = await main.request(window.location.href); numText = /(?<=)\d+(?= pages<\/td>)/.exec(res.responseText)?.[0]; if (numText) return Number(numText); - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); return 0; }; const totalImgNum = await getImgNum(); @@ -10368,7 +10207,7 @@ const floatTagList = (pageType, mangaProps) => { setManga('adList', new main.ReactiveSet()); /** 缩略图元素列表 */ const thumbnailEleList = []; - for (const e of main.querySelectorAll('#gdt img')) { + for (const e of helper.querySelectorAll('#gdt img')) { const index = Number(e.alt) - 1; if (Number.isNaN(index)) return; thumbnailEleList[index] = e; @@ -10376,14 +10215,14 @@ const floatTagList = (pageType, mangaProps) => { [, ehImgFileNameList[index]] = e.title.split(/:|: /); } // 先根据文件名判断一次 - await main.getAdPageByFileName(ehImgFileNameList, mangaProps.adList); + await getAdPageByFileName(ehImgFileNameList, mangaProps.adList); // 不行的话再用缩略图识别 - if (mangaProps.adList.size === 0) await main.getAdPageByContent(thumbnailEleList, mangaProps.adList); + if (mangaProps.adList.size === 0) await getAdPageByContent(thumbnailEleList, mangaProps.adList); // 模糊广告页的缩略图 const stylesheet = new CSSStyleSheet(); document.adoptedStyleSheets.push(stylesheet); - main.createEffectOn(() => [...(mangaProps.adList ?? [])], adList => { + helper.createEffectOn(() => [...(mangaProps.adList ?? [])], adList => { if (adList.length === 0) return; const styleList = adList.map(i => { const alt = `${i + 1}`.padStart(placeValueNum, '0'); @@ -10400,11 +10239,11 @@ const floatTagList = (pageType, mangaProps) => { loadImgList } = init(dynamicUpdate(async setImg => { comicReadModeDom.innerHTML = ` loading`; - const totalPageNum = Number(main.querySelector('.ptt td:nth-last-child(2)').textContent); + const totalPageNum = Number(helper.querySelector('.ptt td:nth-last-child(2)').textContent); for (let pageNum = 0; pageNum < totalPageNum; pageNum++) { const startIndex = ehImgList.length; const imgPageUrlList = await getImgFromDetailsPage(pageNum); - await main.plimit(imgPageUrlList.map(([imgPageUrl, fileName], i) => async () => { + await helper.plimit(imgPageUrlList.map(([imgPageUrl, fileName], i) => async () => { const imgUrl = await getImgFromImgPage(imgPageUrl); const index = startIndex + i; ehImgList[index] = imgUrl; @@ -10415,14 +10254,14 @@ const floatTagList = (pageType, mangaProps) => { const doneNum = startIndex + _doneNum; setFab({ progress: doneNum / totalImgNum, - tip: `${main.t('other.loading_img')} - ${doneNum}/${totalImgNum}` + tip: `${helper.t('other.loading_img')} - ${doneNum}/${totalImgNum}` }); comicReadModeDom.innerHTML = ` loading - ${doneNum}/${totalImgNum}`; if (doneNum === totalImgNum) { comicReadModeDom.innerHTML = ` Read`; if (enableDetectAd) { - await main.getAdPageByFileName(ehImgFileNameList, mangaProps.adList); - await main.getAdPageByContent(ehImgList, mangaProps.adList); + await getAdPageByFileName(ehImgFileNameList, mangaProps.adList); + await getAdPageByContent(ehImgList, mangaProps.adList); } } }); @@ -10432,10 +10271,10 @@ const floatTagList = (pageType, mangaProps) => { /** 获取新的图片页地址 */ const getNewImgPageUrl = async url => { const res = await main.request(url, { - errorText: main.t('site.ehentai.fetch_img_page_source_failed') + errorText: helper.t('site.ehentai.fetch_img_page_source_failed') }); const nl = /nl\('(.+?)'\)/.exec(res.responseText)?.[1]; - if (!nl) throw new Error(main.t('site.ehentai.fetch_img_url_failed')); + if (!nl) throw new Error(helper.t('site.ehentai.fetch_img_url_failed')); const newUrl = new URL(url); newUrl.searchParams.set('nl', nl); return newUrl.href; @@ -10445,9 +10284,9 @@ const floatTagList = (pageType, mangaProps) => { const reloadImg = async i => { const pageUrl = await getNewImgPageUrl(ehImgPageList[i]); let imgUrl = ''; - while (!imgUrl || !(await main.testImgUrl(imgUrl))) { + while (!imgUrl || !(await helper.testImgUrl(imgUrl))) { imgUrl = await getImgFromImgPage(pageUrl); - main.log(`刷新图片 ${i}\n${ehImgList[i]} ->\n${imgUrl}`); + helper.log(`刷新图片 ${i}\n${ehImgList[i]} ->\n${imgUrl}`); } ehImgList[i] = imgUrl; ehImgPageList[i] = pageUrl; @@ -10455,10 +10294,10 @@ const floatTagList = (pageType, mangaProps) => { }; /** 判断当前显示的是否是 eh 源 */ - const isShowEh = () => main.store.imgList[0]?.src === ehImgList[0]; + const isShowEh = () => Manga.store.imgList[0]?.src === ehImgList[0]; /** 刷新所有错误图片 */ - const reloadErrorImg = main.singleThreaded(() => main.plimit(main.store.imgList.map(({ + const reloadErrorImg = helper.singleThreaded(() => helper.plimit(Manga.store.imgList.map(({ loadType }, i) => () => { if (loadType !== 'error' || !isShowEh()) return; @@ -10466,14 +10305,14 @@ const floatTagList = (pageType, mangaProps) => { }))); setManga({ onExit(isEnd) { - if (isEnd) main.scrollIntoView('#cdiv'); + if (isEnd) helper.scrollIntoView('#cdiv'); setManga('show', false); }, // 在图片加载出错时刷新图片 async onLoading(imgList, img) { onLoading(imgList, img); if (!img) return; - if (img.loadType !== 'error' || (await main.testImgUrl(img.src))) return; + if (img.loadType !== 'error' || (await helper.testImgUrl(img.src))) return; return reloadErrorImg(); } }); @@ -10482,8 +10321,8 @@ const floatTagList = (pageType, mangaProps) => { // 关联 nhentai if (options.associate_nhentai) associateNhentai(init, dynamicUpdate); -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } @@ -10491,6 +10330,7 @@ const floatTagList = (pageType, mangaProps) => { case 'nhentai.net': { const main = require('main'); +const helper = require('helper'); /** 用于转换获得图片文件扩展名 */ const fileType = { @@ -10517,13 +10357,13 @@ const fileType = { if (Reflect.has(unsafeWindow, 'gallery')) { setManga({ onExit(isEnd) { - if (isEnd) main.scrollIntoView('#comment-container'); + if (isEnd) helper.scrollIntoView('#comment-container'); setManga('show', false); } }); // 虽然有 Fab 了不需要这个按钮,但我自己都点习惯了没有还挺别扭的( - main.insertNode(document.getElementById('download').parentNode, ' Read'); + helper.insertNode(document.getElementById('download').parentNode, ' Read'); const comicReadModeDom = document.getElementById('comicReadMode'); const { showComic @@ -10538,9 +10378,9 @@ const fileType = { // 在漫画浏览页 if (document.getElementsByClassName('gallery').length > 0) { - if (options.open_link_new_page) for (const e of main.querySelectorAll('a:not([href^="javascript:"])')) e.setAttribute('target', '_blank'); + if (options.open_link_new_page) for (const e of helper.querySelectorAll('a:not([href^="javascript:"])')) e.setAttribute('target', '_blank'); const blacklist = (unsafeWindow?._n_app ?? unsafeWindow?.n)?.options?.blacklisted_tags; - if (blacklist === undefined) main.toast.error(main.t('site.nhentai.tag_blacklist_fetch_failed')); + if (blacklist === undefined) main.toast.error(helper.t('site.nhentai.tag_blacklist_fetch_failed')); // blacklist === null 时是未登录 if (options.block_totally && blacklist?.length) GM_addStyle('.blacklisted.gallery { display: none; }'); @@ -10551,19 +10391,19 @@ const fileType = { hr:not(:last-child) { display: none; } @keyframes load { 0% { width: 100%; } 100% { width: 0; } } `); - let pageNum = Number(main.querySelector('.page.current')?.innerHTML ?? ''); + let pageNum = Number(helper.querySelector('.page.current')?.innerHTML ?? ''); if (Number.isNaN(pageNum)) return; const contentDom = document.getElementById('content'); let apiUrl = ''; - if (window.location.pathname === '/') apiUrl = '/api/galleries/all?';else if (main.querySelector('a.tag')) apiUrl = `/api/galleries/tagged?tag_id=${main.querySelector('a.tag')?.classList[1].split('-')[1]}&`;else if (window.location.pathname.includes('search')) apiUrl = `/api/galleries/search?query=${new URLSearchParams(window.location.search).get('q')}&`; + if (window.location.pathname === '/') apiUrl = '/api/galleries/all?';else if (helper.querySelector('a.tag')) apiUrl = `/api/galleries/tagged?tag_id=${helper.querySelector('a.tag')?.classList[1].split('-')[1]}&`;else if (window.location.pathname.includes('search')) apiUrl = `/api/galleries/search?query=${new URLSearchParams(window.location.search).get('q')}&`; let observer; // eslint-disable-line no-autofix/prefer-const - const loadNewComic = main.singleThreaded(async () => { + const loadNewComic = helper.singleThreaded(async () => { pageNum += 1; const res = await main.request(`${apiUrl}page=${pageNum}${window.location.pathname.includes('popular') ? '&sort=popular ' : ''}`, { fetch: true, responseType: 'json', - errorText: main.t('site.nhentai.fetch_next_page_failed') + errorText: helper.t('site.nhentai.fetch_next_page_failed') }); const { result, @@ -10582,7 +10422,7 @@ const fileType = { for (let i = pageNum - 5; i <= pageNum + 5; i += 1) { if (i > 0 && i <= num_pages) pageNumDom.push(`${i}`); } - main.insertNode(contentDom, `

${pageNum}

+ helper.insertNode(contentDom, `

${pageNum}

${comicDomHtml}
@@ -10615,11 +10455,11 @@ const fileType = { }, false); observer = new IntersectionObserver(entries => entries[0].isIntersecting && loadNewComic()); observer.observe(contentDom.lastElementChild); - if (main.querySelector('section.pagination')) contentDom.append(document.createElement('hr')); + if (helper.querySelector('section.pagination')) contentDom.append(document.createElement('hr')); } } -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } @@ -10627,6 +10467,7 @@ const fileType = { case 'yuri.website': { const main = require('main'); +const helper = require('helper'); (async () => { const { @@ -10665,18 +10506,18 @@ const main = require('main'); })(); // 跳过漫画区外的页面 - if (!main.querySelector('a.post-list-cat-item[title="在线区-漫画"]')) return; + if (!helper.querySelector('a.post-list-cat-item[title="在线区-漫画"]')) return; // 需要购买的漫画 - if (main.querySelector('.content-hidden')) { - const imgBody = main.querySelector('.content-hidden'); + if (helper.querySelector('.content-hidden')) { + const imgBody = helper.querySelector('.content-hidden'); const imgList = imgBody.getElementsByTagName('img'); - if (await main.wait(() => imgList.length, 1000)) init(() => [...imgList].map(e => e.src)); + if (await helper.wait(() => imgList.length, 1000)) init(() => [...imgList].map(e => e.src)); return; } // 有折叠内容的漫画 - if (main.querySelector('.xControl')) { + if (helper.querySelector('.xControl')) { needAutoShow.val = false; const { loadImgList @@ -10690,7 +10531,7 @@ const main = require('main'); onNext: i === imgListMap.length - 1 ? undefined : () => loadChapterImg(i + 1) }); }; - for (const [i, a] of main.querySelectorAll('.xControl > a').entries()) { + for (const [i, a] of helper.querySelectorAll('.xControl > a').entries()) { const imgRoot = a.parentElement.nextElementSibling; imgListMap.push(imgRoot.getElementsByTagName('img')); a.addEventListener('click', () => { @@ -10702,10 +10543,10 @@ const main = require('main'); } // 没有折叠的单篇漫画 - await main.wait(() => main.querySelectorAll('.entry-content img').length); - return init(() => main.querySelectorAll('.entry-content img').map(e => e.src)); + await helper.wait(() => helper.querySelectorAll('.entry-content img').length); + return init(() => helper.querySelectorAll('.entry-content img').map(e => e.src)); })(); -; + break; } @@ -10726,6 +10567,7 @@ const main = require('main'); case 'www.copymanga.com': { const main = require('main'); +const helper = require('helper'); (() => { const headers = { @@ -10758,8 +10600,8 @@ const main = require('main'); options = { name: 'copymanga', getImgList, - onNext: main.querySelectorClick('.comicContent-next a:not(.prev-null)'), - onPrev: main.querySelectorClick('.comicContent-prev:not(.index,.list) a:not(.prev-null)'), + onNext: helper.querySelectorClick('.comicContent-next a:not(.prev-null)'), + onPrev: helper.querySelectorClick('.comicContent-prev:not(.index,.list) a:not(.prev-null)'), async getCommentList() { const chapter_id = window.location.pathname.split('/').at(-1); const res = await main.request(`/api/v3/roasts?chapter_id=${chapter_id}&limit=100&offset=0&_update=true`, { @@ -10785,7 +10627,7 @@ const main = require('main'); // 因为拷贝漫画的目录是动态加载的,所以要等目录加载出来再往上添加 if (!a) (async () => { a = document.createElement('a'); - const tableRight = await main.wait(() => main.querySelector('.table-default-right')); + const tableRight = await helper.wait(() => helper.querySelector('.table-default-right')); a.target = '_blank'; tableRight.insertBefore(a, tableRight.firstElementChild); const span = document.createElement('span'); @@ -10820,7 +10662,7 @@ const main = require('main'); document.addEventListener('visibilitychange', updateLastChapter); } })(); -; + break; } @@ -10829,12 +10671,12 @@ const main = require('main'); { options = { name: 'terraHistoricus', - wait: () => Boolean(main.querySelector('.comic-page-container img')), - getImgList: () => main.querySelectorAll('.comic-page-container img').map(e => e.dataset.srcset), + wait: () => Boolean(helper.querySelector('.comic-page-container img')), + getImgList: () => helper.querySelectorAll('.comic-page-container img').map(e => e.dataset.srcset), SPA: { isMangaPage: () => window.location.href.includes('/comic/'), - getOnPrev: () => main.querySelectorClick('.prev-btn:not(.invisible) a'), - getOnNext: () => main.querySelectorClick('.next-btn:not(.invisible) a') + getOnPrev: () => helper.querySelectorClick('.prev-btn:not(.invisible) a'), + getOnNext: () => helper.querySelectorClick('.next-btn:not(.invisible) a') } }; break; @@ -10850,14 +10692,14 @@ const main = require('main'); }; options = { name: 'terraHistoricus', - wait: () => Boolean(main.querySelector('.HG_COMIC_READER_main')), + wait: () => Boolean(helper.querySelector('.HG_COMIC_READER_main')), async getImgList({ setFab }) { const res = await main.request(apiUrl()); const pageList = JSON.parse(res.responseText).data.pageInfos; if (pageList.length === 0 && window.location.pathname.includes('episode')) throw new Error('获取图片列表时出错'); - return main.plimit(main.createSequence(pageList.length).map(getImgUrl), (doneNum, totalNum) => { + return helper.plimit(helper.createSequence(pageList.length).map(getImgUrl), (doneNum, totalNum) => { setFab({ progress: doneNum / totalNum, tip: `加载图片中 - ${doneNum}/${totalNum}` @@ -10866,8 +10708,8 @@ const main = require('main'); }, SPA: { isMangaPage: () => window.location.href.includes('episode'), - getOnPrev: () => main.querySelectorClick('footer .HG_COMIC_READER_prev a'), - getOnNext: () => main.querySelectorClick('footer .HG_COMIC_READER_prev+.HG_COMIC_READER_buttonEp a') + getOnPrev: () => helper.querySelectorClick('footer .HG_COMIC_READER_prev a'), + getOnNext: () => helper.querySelectorClick('footer .HG_COMIC_READER_prev+.HG_COMIC_READER_buttonEp a') } }; break; @@ -10880,6 +10722,7 @@ const main = require('main'); case '18comic.vip': { const main = require('main'); +const helper = require('helper'); // 已知问题:某些漫画始终会有几页在下载原图时出错 // 并且这类漫画下即使关掉脚本,也还是会有几页就是加载不出来 @@ -10904,13 +10747,13 @@ const main = require('main'); }); return; } - await main.sleep(100); + await helper.sleep(100); } setManga({ - onPrev: main.querySelectorClick(() => main.querySelector('.menu-bolock-ul .fa-angle-double-left')?.parentElement), - onNext: main.querySelectorClick(() => main.querySelector('.menu-bolock-ul .fa-angle-double-right')?.parentElement) + onPrev: helper.querySelectorClick(() => helper.querySelector('.menu-bolock-ul .fa-angle-double-left')?.parentElement), + onNext: helper.querySelectorClick(() => helper.querySelector('.menu-bolock-ul .fa-angle-double-right')?.parentElement) }); - const imgEleList = main.querySelectorAll('.scramble-page:not(.thewayhome) > img'); + const imgEleList = helper.querySelectorAll('.scramble-page:not(.thewayhome) > img'); // 判断当前漫画是否有被分割,没有就直接获取图片链接加载 // 判断条件来自页面上的 scramble_image 函数 @@ -10944,7 +10787,7 @@ const main = require('main'); } imgEle.src = URL.createObjectURL(res.response); try { - await main.waitImgLoad(imgEle, 1000 * 10); + await helper.waitImgLoad(imgEle, 1000 * 10); } catch { URL.revokeObjectURL(imgEle.src); imgEle.src = originalUrl; @@ -10955,7 +10798,7 @@ const main = require('main'); // 原有的 canvas 可能已被污染,直接删掉 if (imgEle.nextElementSibling?.tagName === 'CANVAS') imgEle.nextElementSibling.remove(); unsafeWindow.onImageLoaded(imgEle); - const blob = await main.canvasToBlob(imgEle.nextElementSibling, 'image/webp', 1); + const blob = await helper.canvasToBlob(imgEle.nextElementSibling, 'image/webp', 1); URL.revokeObjectURL(imgEle.src); if (!blob) throw new Error('转换图片时出错'); return `${URL.createObjectURL(blob)}#.webp`; @@ -10967,18 +10810,18 @@ const main = require('main'); }; // 先等懒加载触发完毕 - await main.wait(() => { - const loadedNum = main.querySelectorAll('.lazy-loaded').length; - return loadedNum > 0 && main.querySelectorAll('canvas').length - loadedNum <= 1; + await helper.wait(() => { + const loadedNum = helper.querySelectorAll('.lazy-loaded').length; + return loadedNum > 0 && helper.querySelectorAll('canvas').length - loadedNum <= 1; }); - init(dynamicUpdate(setImg => main.plimit(imgEleList.map((img, i) => async () => setImg(i, await getImgUrl(img))), (doneNum, totalNum) => { + init(dynamicUpdate(setImg => helper.plimit(imgEleList.map((img, i) => async () => setImg(i, await getImgUrl(img))), (doneNum, totalNum) => { setFab({ progress: doneNum / totalNum, tip: `加载图片中 - ${doneNum}/${totalNum}` }); }), imgEleList.length)); -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } @@ -10991,14 +10834,14 @@ const main = require('main'); if (!/\/comic\/\d+\/\d+\.html/.test(window.location.pathname)) break; let comicInfo; try { - const dataScript = main.querySelectorAll('body > script:not([src])').find(script => script.innerHTML.startsWith('window[')); - if (!dataScript) throw new Error(main.t('site.changed_load_failed')); + const dataScript = helper.querySelectorAll('body > script:not([src])').find(script => script.innerHTML.startsWith('window[')); + if (!dataScript) throw new Error(helper.t('site.changed_load_failed')); comicInfo = JSON.parse( // 只能通过 eval 获得数据 // eslint-disable-next-line no-eval eval(dataScript.innerHTML.slice(26)).match(/(?<=.*?\(){.+}/)[0]); } catch { - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); break; } @@ -11017,10 +10860,10 @@ const main = require('main'); if (comicInfo.images) { const { origin - } = new URL(main.querySelector('#manga img').src); + } = new URL(helper.querySelector('#manga img').src); return comicInfo.images.map(url => `${origin}${url}?${sl}`); } - main.toast.error(main.t('site.changed_load_failed'), { + main.toast.error(helper.t('site.changed_load_failed'), { throw: true }); return []; @@ -11056,7 +10899,7 @@ const main = require('main'); if (!Reflect.has(unsafeWindow, 'DM5_CID')) break; const imgNum = unsafeWindow.DM5_IMAGE_COUNT ?? unsafeWindow.imgsLen; if (!(Number.isSafeInteger(imgNum) && imgNum > 0)) { - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); break; } const getPageImg = async i => { @@ -11078,14 +10921,14 @@ const main = require('main'); // eslint-disable-next-line no-eval return eval(res); }; - const handlePrevNext = (pcSelector, mobileText) => main.querySelectorClick(() => main.querySelector(pcSelector) ?? main.querySelectorAll('.view-bottom-bar a').find(e => e.textContent?.includes(mobileText))); + const handlePrevNext = (pcSelector, mobileText) => helper.querySelectorClick(() => helper.querySelector(pcSelector) ?? helper.querySelectorAll('.view-bottom-bar a').find(e => e.textContent?.includes(mobileText))); options = { name: 'dm5', getImgList({ dynamicUpdate }) { // manhuaren 和 1kkk 的移动端上会直接用一个变量存储所有图片的链接 - if (Array.isArray(unsafeWindow.newImgs) && unsafeWindow.newImgs.every(main.isUrl)) return unsafeWindow.newImgs; + if (Array.isArray(unsafeWindow.newImgs) && unsafeWindow.newImgs.every(helper.isUrl)) return unsafeWindow.newImgs; return dynamicUpdate(async setImg => { const imgList = new Set(); while (imgList.size < imgNum) { @@ -11100,7 +10943,7 @@ const main = require('main'); }, onPrev: handlePrevNext('.logo_1', '上一章'), onNext: handlePrevNext('.logo_2', '下一章'), - onExit: isEnd => isEnd && main.scrollIntoView('.postlist') + onExit: isEnd => isEnd && helper.scrollIntoView('.postlist') }; break; } @@ -11112,7 +10955,7 @@ const main = require('main'); case 'wnacg.com': { // 突出显示下拉阅读的按钮 - const buttonDom = main.querySelector('#bodywrap a.btn'); + const buttonDom = helper.querySelector('#bodywrap a.btn'); if (buttonDom) { buttonDom.style.setProperty('background-color', '#607d8b'); buttonDom.style.setProperty('background-image', 'none'); @@ -11136,7 +10979,7 @@ const main = require('main'); if (!Reflect.has(unsafeWindow, 'MANGABZ_CID')) break; const imgNum = unsafeWindow.MANGABZ_IMAGE_COUNT ?? unsafeWindow.imgsLen; if (!(Number.isSafeInteger(imgNum) && imgNum > 0)) { - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); break; } const getPageImg = async i => { @@ -11156,7 +10999,7 @@ const main = require('main'); // eslint-disable-next-line no-eval return eval(res); }; - const handlePrevNext = (pcSelector, mobileText) => main.querySelectorClick(() => main.querySelector(pcSelector) ?? main.querySelectorAll('.bottom-bar-tool a').find(e => e.textContent?.includes(mobileText))); + const handlePrevNext = (pcSelector, mobileText) => helper.querySelectorClick(() => helper.querySelector(pcSelector) ?? helper.querySelectorAll('.bottom-bar-tool a').find(e => e.textContent?.includes(mobileText))); options = { name: 'mangabz', getImgList: ({ @@ -11193,7 +11036,7 @@ const main = require('main'); }`; const getImgList = async () => { const chapterId = /chapter\/(\d+)/.exec(window.location.pathname)?.[1]; - if (!chapterId) throw new Error(main.t('site.changed_load_failed')); + if (!chapterId) throw new Error(helper.t('site.changed_load_failed')); const res = await main.request('/api/query', { method: 'POST', responseType: 'json', @@ -11213,8 +11056,8 @@ const main = require('main'); }) => `https://komiic.com/api/image/${kid}`); }; const handlePrevNext = text => async () => { - await main.waitDom('.v-bottom-navigation__content'); - return main.querySelectorClick('.v-bottom-navigation__content > button:not([disabled])', text); + await helper.waitDom('.v-bottom-navigation__content'); + return helper.querySelectorClick('.v-bottom-navigation__content > button:not([disabled])', text); }; options = { name: 'komiic', @@ -11269,8 +11112,8 @@ const main = require('main'); options = { name: '8comic', getImgList, - onNext: main.querySelectorClick('#nextvol'), - onPrev: main.querySelectorClick('#prevvol') + onNext: helper.querySelectorClick('#nextvol'), + onPrev: helper.querySelectorClick('#prevvol') }; break; } @@ -11290,8 +11133,8 @@ const main = require('main'); const baseUrl = unsafeWindow.img_qianz ?? unsafeWindow.ImgSvrList; return unsafeWindow.msg.split('|').map(path => `${baseUrl}${path}`); }, - onNext: main.querySelectorClick('#pnpage > a', '下一'), - onPrev: main.querySelectorClick('#pnpage > a', '上一') + onNext: helper.querySelectorClick('#pnpage > a', '下一'), + onPrev: helper.querySelectorClick('#pnpage > a', '上一') }; break; } @@ -11354,7 +11197,7 @@ const main = require('main'); if (!isMangaPage) break; const startTime = performance.now(); setImg(i, await downloadImg(`${base}${path}`)); - await main.sleep(500 - (performance.now() - startTime)); + await helper.sleep(500 - (performance.now() - startTime)); } }, totalPageNum)(); }, @@ -11370,6 +11213,7 @@ const main = require('main'); case 'kemono.party': { const main = require('main'); +const helper = require('helper'); (async () => { const { @@ -11384,15 +11228,15 @@ const main = require('main'); /** 加载原图 */ load_original_image: true }); - const getImglist = () => options.load_original_image ? main.querySelectorAll('.post__thumbnail a').map(e => e.href) : main.querySelectorAll('.post__thumbnail img').map(e => e.src); + const getImglist = () => options.load_original_image ? helper.querySelectorAll('.post__thumbnail a').map(e => e.href) : helper.querySelectorAll('.post__thumbnail img').map(e => e.src); init(getImglist); // 在切换时重新获取图片 - main.createEffectOn(() => options.load_original_image, () => setManga('imgList', getImglist())); + helper.createEffectOn(() => options.load_original_image, () => setManga('imgList', getImglist())); // 加上跳转至 pwa 的链接 const zipExtension = new Set(['zip', 'rar', '7z', 'cbz', 'cbr', 'cb7']); - for (const e of main.querySelectorAll('.post__attachment a')) { + for (const e of helper.querySelectorAll('.post__attachment a')) { if (!zipExtension.has(e.href.split('.').pop())) continue; const a = document.createElement('a'); a.href = `https://comic-read.pages.dev/?url=${encodeURIComponent(e.href)}`; @@ -11402,7 +11246,7 @@ const main = require('main'); e.parentNode.insertBefore(a, e.nextElementSibling); } })(); -; + break; } @@ -11411,7 +11255,7 @@ const main = require('main'); { options = { name: 'nekohouse', - getImgList: () => main.querySelectorAll('.fileThumb').map(e => e.getAttribute('href')), + getImgList: () => helper.querySelectorAll('.fileThumb').map(e => e.getAttribute('href')), initOptions: { autoShow: false, defaultOption: { @@ -11427,18 +11271,18 @@ const main = require('main'); case 'weloma.art': case 'welovemanga.one': { - if (!main.querySelector('#listImgs, .chapter-content')) break; + if (!helper.querySelector('#listImgs, .chapter-content')) break; const getImgList = async () => { - const imgList = main.querySelectorAll('img.chapter-img:not(.ls-is-cached)').map(e => (e.dataset.src ?? e.dataset.srcset ?? e.dataset.original ?? e.src).trim()); + const imgList = helper.querySelectorAll('img.chapter-img:not(.ls-is-cached)').map(e => (e.dataset.src ?? e.dataset.srcset ?? e.dataset.original ?? e.src).trim()); if (imgList.length > 0 && imgList.every(url => !/loading.*\.gif/.test(url))) return imgList; - await main.sleep(500); + await helper.sleep(500); return getImgList(); }; options = { name: 'welovemanga', getImgList, - onNext: main.querySelectorClick('.rd_top-right.next:not(.disabled)'), - onPrev: main.querySelectorClick('.rd_top-left.prev:not(.disabled)') + onNext: helper.querySelectorClick('.rd_top-right.next:not(.disabled)'), + onPrev: helper.querySelectorClick('.rd_top-left.prev:not(.disabled)') }; break; } @@ -11453,35 +11297,11 @@ const main = require('main'); } default: { +const web = require('solid-js/web'); +const helper = require('helper'); +const Manga = require('components/Manga'); const main = require('main'); -const langList = ['zh', 'en', 'ru']; -/** 判断传入的字符串是否是支持的语言类型代码 */ -const isLanguages = lang => Boolean(lang) && langList.includes(lang); - -/** 返回浏览器偏好语言 */ -const getBrowserLang = () => { - let newLang; - for (let i = 0; i < navigator.languages.length; i++) { - const language = navigator.languages[i]; - const matchLang = langList.find(l => l === language || l === language.split('-')[0]); - if (matchLang) { - newLang = matchLang; - break; - } - } - return newLang; -}; -const getSaveLang = async () => typeof GM === 'undefined' ? localStorage.getItem('Languages') : GM.getValue('Languages'); -const setSaveLang = async val => typeof GM === 'undefined' ? localStorage.setItem('Languages', val) : GM.setValue('Languages', val); -const getInitLang = async () => { - const saveLang = await getSaveLang(); - if (isLanguages(saveLang)) return saveLang; - const lang = getBrowserLang() ?? 'zh'; - setSaveLang(lang); - return lang; -}; - const getTagText = ele => { let text = ele.nodeName; if (ele.id && !/\d/.test(ele.id)) text += `#${ele.id}`; @@ -11514,6 +11334,144 @@ const isEleSelector = (ele, selector) => { // 目录页和漫画页的图片层级相同 // https://www.biliplus.com/manga/ // 图片路径上有 id 元素并且 id 含有漫画 id,不同话数 id 也不同 +const createImgData = (oldSrc = '') => ({ + triggedNum: 0, + observerTimeout: 0, + oldSrc +}); + +/** 用于判断是否是图片 url 的正则 */ +const isImgUrlRe = /^(((https?|ftp|file):)?\/)?\/[-\w+&@#/%?=~|!:,.;]+[-\w+&@#%=~|]$/; + +/** 检查元素属性,将格式为图片 url 的属性值作为 src */ +const tryCorrectUrl = e => { + e.getAttributeNames().some(key => { + // 跳过白名单 + switch (key) { + case 'src': + case 'alt': + case 'class': + case 'style': + case 'id': + case 'title': + case 'onload': + case 'onerror': + return false; + } + const val = e.getAttribute(key).trim(); + if (!isImgUrlRe.test(val)) return false; + e.setAttribute('src', val); + return true; + }); +}; + +/** + * + * 通过滚动到指定图片元素位置并停留一会来触发图片的懒加载,返回图片 src 是否发生变化 + * + * 会在触发后重新滚回原位,当 time 为 0 时,因为滚动速度很快所以是无感的 + */ +const triggerEleLazyLoad = async (e, time, isLazyLoaded) => { + const nowScroll = window.scrollY; + e.scrollIntoView({ + behavior: 'instant' + }); + e.dispatchEvent(new Event('scroll', { + bubbles: true + })); + try { + if (isLazyLoaded && time) return await helper.wait(isLazyLoaded, time); + } finally { + window.scroll({ + top: nowScroll, + behavior: 'instant' + }); + } +}; + +/** 判断一个元素是否已经触发完懒加载 */ +const isLazyLoaded = (e, oldSrc) => { + if (!e.src) return false; + if (!e.offsetParent) return false; + // 有些网站会使用 svg 占位 + if (e.src.startsWith('data:image/svg')) return false; + if (oldSrc !== undefined && e.src !== oldSrc) return true; + if (e.naturalWidth > 500 || e.naturalHeight > 500) return true; + return false; +}; +const imgMap = new WeakMap(); +// eslint-disable-next-line no-autofix/prefer-const +let imgShowObserver; +const getImg = e => imgMap.get(e) ?? createImgData(); +const MAX_TRIGGED_NUM = 5; + +/** 判断图片元素是否需要触发懒加载 */ +const needTrigged = e => !isLazyLoaded(e, imgMap.get(e)?.oldSrc) && (imgMap.get(e)?.triggedNum ?? 0) < MAX_TRIGGED_NUM; + +/** 图片懒加载触发完后调用 */ +const handleTrigged = e => { + const img = getImg(e); + img.observerTimeout = 0; + img.triggedNum += 1; + if (isLazyLoaded(e, img.oldSrc) && img.triggedNum < MAX_TRIGGED_NUM) img.triggedNum = MAX_TRIGGED_NUM; + imgMap.set(e, img); + if (!needTrigged(e)) imgShowObserver.unobserve(e); +}; + +/** 监视图片是否被显示的 Observer */ +imgShowObserver = new IntersectionObserver(entries => { + for (const img of entries) { + const ele = img.target; + if (img.isIntersecting) { + imgMap.set(ele, { + ...getImg(ele), + observerTimeout: window.setTimeout(handleTrigged, 290, ele) + }); + } + const timeoutID = imgMap.get(ele)?.observerTimeout; + if (timeoutID) window.clearTimeout(timeoutID); + } +}); +const turnPageScheduled = helper.createScheduled(fn => helper.throttle(fn, 1000)); +/** 触发翻页 */ +const triggerTurnPage = async (waitTime = 0) => { + if (!turnPageScheduled()) return; + const nowScroll = window.scrollY; + // 滚到底部再滚回来,触发可能存在的自动翻页脚本 + window.scroll({ + top: document.body.scrollHeight, + behavior: 'instant' + }); + document.body.dispatchEvent(new Event('scroll', { + bubbles: true + })); + if (waitTime) await helper.sleep(waitTime); + window.scroll({ + top: nowScroll, + behavior: 'instant' + }); +}; +const waitTime = 300; + +/** 触发页面上所有图片元素的懒加载 */ +const triggerLazyLoad = helper.singleThreaded(async (state, getAllImg, runCondition) => { + // 过滤掉已经被触发过懒加载的图片 + const targetImgList = getAllImg().filter(needTrigged).sort((a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y); + for (const e of targetImgList) { + imgShowObserver.observe(e); + if (!imgMap.has(e)) imgMap.set(e, createImgData(e.src)); + } + for (const e of targetImgList) { + await helper.wait(runCondition); + await triggerTurnPage(waitTime); + if (!needTrigged(e)) continue; + tryCorrectUrl(e); + if (await triggerEleLazyLoad(e, waitTime, () => isLazyLoaded(e, imgMap.get(e)?.oldSrc))) handleTrigged(e); + } + await triggerTurnPage(); + if (targetImgList.length > 0) state.continueRun = true; +}); + // 测试案例 // https://www.177picyy.com/html/2023/03/5505307.html @@ -11542,23 +11500,32 @@ const isEleSelector = (ele, selector) => { await GM.deleteValue(window.location.hostname); return true; } - if (!isStored) main.toast(main.autoReadModeMessage(setOptions), { + if (!isStored) main.toast(() => (() => { + var _el$ = web.template(`

`); - main.querySelector('button').addEventListener('click', async () => { - const comicName = main.querySelector('input')?.value; + helper.querySelector('button').addEventListener('click', async () => { + const comicName = helper.querySelector('input')?.value; if (!comicName) return; const res = await main.request(`https://s.acg.dmzj.com/comicsum/search.php?s=${comicName}`, { errorText: '搜索漫画时出错' }); const comicList = JSON.parse(res.responseText.slice(20, -1)); - main.querySelector('#list').innerHTML = comicList.map(({ + helper.querySelector('#list').innerHTML = comicList.map(({ id, comic_name, comic_author, @@ -8999,13 +8705,13 @@ const getViewpoint = async (comicId, chapterId) => { } } = dmzjDecrypt(res.responseText); document.title = title; - main.insertNode(document.body, `

${title}

`); + helper.insertNode(document.body, `

${title}

`); for (const chapter of Object.values(chapters)) { // 手动构建添加章节 dom let temp = `

${chapter.title}

`; let i = chapter.data.length; while (i--) temp += `
${chapter.data[i].chapter_title}`; - main.insertNode(document.body, temp); + helper.insertNode(document.body, temp); } document.body.childNodes[0].remove(); GM_addStyle(` @@ -9041,10 +8747,10 @@ const getViewpoint = async (comicId, chapterId) => { GM_addStyle('.subHeader{display:none !important}'); await main.universalInit({ name: 'dmzj', - getImgList: () => main.querySelectorAll('#commicBox img').map(e => e.dataset.original).filter(Boolean), - getCommentList: () => getViewpoint(unsafeWindow.subId, unsafeWindow.chapterId), - onNext: main.querySelectorClick('#loadNextChapter'), - onPrev: main.querySelectorClick('#loadPrevChapter') + getImgList: () => helper.querySelectorAll('#commicBox img').map(e => e.dataset.original).filter(Boolean), + getCommentList: () => dmzjApi.getViewpoint(unsafeWindow.subId, unsafeWindow.chapterId), + onNext: helper.querySelectorClick('#loadNextChapter'), + onPrev: helper.querySelectorClick('#loadPrevChapter') }); return; } @@ -9056,7 +8762,7 @@ const getViewpoint = async (comicId, chapterId) => { let chapterId; try { [, comicId, chapterId] = /(\d+)\/(\d+)/.exec(window.location.pathname); - data = await getChapterInfo(comicId, chapterId); + data = await dmzjApi.getChapterInfo(comicId, chapterId); } catch (error) { main.toast.error('获取漫画数据失败', { duration: Number.POSITIVE_INFINITY @@ -9094,28 +8800,20 @@ const getViewpoint = async (comicId, chapterId) => { tipDom.innerHTML = `无法获得漫画数据,请通过
GithubGreasy Fork 进行反馈`; return []; }); - setManga('commentList', await getViewpoint(comicId, chapterId)); + setManga('commentList', await dmzjApi.getViewpoint(comicId, chapterId)); break; } } -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } case 'www.idmzj.com': case 'www.dmzj.com': { -require('solid-js/store'); +const dmzjApi = require('dmzjApi'); const main = require('main'); - -/** 根据漫画 id 和章节 id 获取章节数据 */ -const getChapterInfo = async (comicId, chapterId) => { - const res = await main.request(`https://m.dmzj.com/chapinfo/${comicId}/${chapterId}.html`, { - responseType: 'json', - errorText: '获取章节数据失败' - }); - return res.response; -}; +const helper = require('helper'); const turnPage = chapterId => { if (!chapterId) return undefined; @@ -9124,9 +8822,9 @@ const turnPage = chapterId => { }; }; (async () => { - await main.waitDom('.head_wz'); + await helper.waitDom('.head_wz'); // 只在漫画页内运行 - const comicId = main.querySelector('.head_wz [id]')?.id; + const comicId = helper.querySelector('.head_wz [id]')?.id; const chapterId = /(?<=\/)\d+(?=\.html)/.exec(window.location.pathname)?.[0]; if (!comicId || !chapterId) return; const { @@ -9135,38 +8833,190 @@ const turnPage = chapterId => { } = await main.useInit('dmzj'); try { const { - next_chap_id, - prev_chap_id, - page_url - } = await getChapterInfo(comicId, chapterId); - init(() => page_url); - setManga({ - onNext: turnPage(next_chap_id), - onPrev: turnPage(prev_chap_id) + next_chap_id, + prev_chap_id, + page_url + } = await dmzjApi.getChapterInfo(comicId, chapterId); + init(() => page_url); + setManga({ + onNext: turnPage(next_chap_id), + onPrev: turnPage(prev_chap_id) + }); + } catch { + main.toast.error('获取漫画数据失败', { + duration: Number.POSITIVE_INFINITY + }); + } +})().catch(error => helper.log.error(error)); + + break; + } + + // #E-Hentai(关联 nhentai、快捷收藏、标签染色、识别广告页等) + case 'exhentai.org': + case 'e-hentai.org': + { +const Manga = require('components/Manga'); +const main = require('main'); +const helper = require('helper'); +const QrScanner = require('qr-scanner'); +const web = require('solid-js/web'); +const solidJs = require('solid-js'); +const store = require('solid-js/store'); + +const getAdPage = async (list, isAdPage, adList = new Set()) => { + let i = list.length - 1; + let normalNum = 0; + // 只检查最后十张 + for (; i >= list.length - 10; i--) { + // 开头肯定不会是广告 + if (i <= 2) break; + if (adList.has(i)) continue; + const item = list[i]; + if (!item) break; + if (await isAdPage(item)) adList.add(i); + // 找到连续两张正常漫画页后中断 + else if (normalNum) break;else normalNum += 1; + } + let adNum = 0; + for (i = Math.min(...adList); i < list.length; i++) { + if (adList.has(i)) { + adNum += 1; + continue; + } + + // 连续两张广告后面的肯定也都是广告 + if (adNum >= 2) adList.add(i); + // 夹在两张广告中间的肯定也是广告 + else if (adList.has(i - 1) && adList.has(i + 1)) adList.add(i);else adNum = 0; + } + return adList; +}; + +/** 判断像素点是否是灰阶 */ +const isGrayscalePixel = (r, g, b) => r === g && r === b; + +/** 判断一张图是否是彩图 */ +const isColorImg = imgCanvas => { + // 缩小尺寸放弃细节,避免被黑白图上的小段彩色文字干扰 + const canvas = new OffscreenCanvas(3, 3); + const ctx = canvas.getContext('2d', { + alpha: false + }); + ctx.drawImage(imgCanvas, 0, 0, canvas.width, canvas.height); + const { + data + } = ctx.getImageData(0, 0, canvas.width, canvas.height); + for (let i = 0; i < data.length; i += 4) { + const r = data[i]; + const g = data[i + 1]; + const b = data[i + 2]; + if (!isGrayscalePixel(r, g, b)) return true; + } + return false; +}; +const imgToCanvas = async img => { + if (typeof img !== 'string') { + await helper.wait(() => img.naturalHeight && img.naturalWidth, 1000 * 10); + try { + const canvas = new OffscreenCanvas(img.width, img.height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + // 没被 CORS 污染就直接使用这个 canvas + if (ctx.getImageData(0, 0, 1, 1)) return canvas; + } catch {} + } + const url = typeof img === 'string' ? img : img.src; + const res = await main.request(url, { + responseType: 'blob' + }); + const image = await helper.waitImgLoad(URL.createObjectURL(res.response)); + const canvas = new OffscreenCanvas(image.width, image.height); + const ctx = canvas.getContext('2d'); + ctx.drawImage(image, 0, 0); + return canvas; +}; + +/** 二维码白名单 */ +const qrCodeWhiteList = [ +// fanbox +/^https:\/\/[^.]+\.fanbox\.cc/, +// twitter +/^https:\/\/twitter\.com/, /^https:\/\/x\.com/, +// fantia +/^https:\/\/fantia\.jp/, +// 棉花糖 +/^https:\/\/marshmallow-qa\.com/]; + +/** 判断是否含有二维码 */ +const hasQrCode = async (imgCanvas, scanRegion, qrEngine, canvas) => { + try { + const { + data + } = await QrScanner.scanImage(imgCanvas, { + qrEngine, + canvas: canvas, + scanRegion, + alsoTryWithoutScanRegion: true }); + if (!data) return false; + helper.log(`检测到二维码: ${data}`); + return qrCodeWhiteList.every(reg => !reg.test(data)); } catch { - main.toast.error('获取漫画数据失败', { - duration: Number.POSITIVE_INFINITY - }); + return false; } -})().catch(error => main.log.error(error)); -; - break; - } +}; +const isAdImg = async (imgCanvas, qrEngine, canvas) => { + // 黑白图肯定不是广告 + if (!isColorImg(imgCanvas)) return false; + const width = imgCanvas.width / 2; + const height = imgCanvas.height / 2; - // #E-Hentai(关联 nhentai、快捷收藏、标签染色、识别广告页等) - case 'exhentai.org': - case 'e-hentai.org': - { -const main = require('main'); -const web = require('solid-js/web'); -const solidJs = require('solid-js'); -const store = require('solid-js/store'); + // 分区块扫描图片 + const scanRegionList = [undefined, + // 右下 + { + x: width, + y: height, + width, + height + }, + // 左下 + { + x: 0, + y: height, + width, + height + }, + // 右上 + { + x: width, + y: 0, + width, + height + }, + // 左上 + { + x: 0, + y: 0, + width, + height + }]; + for (const scanRegion of scanRegionList) if (await hasQrCode(imgCanvas, scanRegion, qrEngine, canvas)) return true; + return false; +}; +const byContent = (qrEngine, canvas) => async img => isAdImg(await imgToCanvas(img), qrEngine, canvas); + +/** 通过图片内容判断是否是广告 */ +const getAdPageByContent = async (imgList, adList = new Set()) => { + const qrEngine = await QrScanner.createQrEngine(); + const canvas = new OffscreenCanvas(1, 1); + return getAdPage(imgList, byContent(qrEngine, canvas), adList); +}; + +/** 通过文件名判断是否是广告 */ +const getAdPageByFileName = async (fileNameList, adList = new Set()) => getAdPage(fileNameList, fileName => /^[zZ]+/.test(fileName), adList); -var _tmpl$$3 = /*#__PURE__*/web.template(`
`), - _tmpl$2$2 = /*#__PURE__*/web.template(`
`), - _tmpl$3$1 = /*#__PURE__*/web.template(``), - _tmpl$4$1 = /*#__PURE__*/web.template(`

loading...`); let hasStyle = false; const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { if (!hasStyle) { @@ -9229,14 +9079,14 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { const updateFavorite = async () => { try { const res = await main.request(apiUrl, { - errorText: main.t('site.ehentai.fetch_favorite_failed') + errorText: helper.t('site.ehentai.fetch_favorite_failed') }); - const dom = main.domParse(res.responseText); + const dom = helper.domParse(res.responseText); const list = [...dom.querySelectorAll('.nosel > div')]; if (list.length === 10) list[0].querySelector('input').checked = false; setFavorites(list); } catch { - main.toast.error(main.t('site.ehentai.fetch_favorite_failed')); + main.toast.error(helper.t('site.ehentai.fetch_favorite_failed')); setFavorites([]); } }; @@ -9257,9 +9107,9 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { const res = await main.request(apiUrl, { method: 'POST', data: formData, - errorText: main.t('site.ehentai.change_favorite_failed') + errorText: helper.t('site.ehentai.change_favorite_failed') }); - main.toast.success(main.t('site.ehentai.change_favorite_success')); + main.toast.success(helper.t('site.ehentai.change_favorite_success')); // 修改收藏按钮样式的 js 代码 const updateCode = /\nif\(window.opener.document.+\n/.exec(res.responseText)?.[0]?.replaceAll('window.opener.document', 'window.document'); @@ -9268,7 +9118,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { await updateFavorite(); }; return (() => { - var _el$ = _tmpl$2$2(), + var _el$ = web.template(`
`)(), _el$2 = _el$.firstChild; _el$.$$click = handleClick; _el$2.checked = checked; @@ -9277,7 +9127,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { return index() <= 9; }, get children() { - var _el$3 = _tmpl$$3(); + var _el$3 = web.template(`
`)(); web.effect(_$p => (_$p = `0px -${2 + 19 * index()}px`) != null ? _el$3.style.setProperty("background-position", _$p) : _el$3.style.removeProperty("background-position")); return _el$3; } @@ -9297,7 +9147,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { return show(); }, get children() { - var _el$4 = _tmpl$3$1(); + var _el$4 = web.template(``)(); background != null ? _el$4.style.setProperty("background", background) : _el$4.style.removeProperty("background"); web.insert(_el$4, web.createComponent(solidJs.For, { get each() { @@ -9305,7 +9155,7 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { }, children: FavoriteItem, get fallback() { - return _tmpl$4$1(); + return web.template(`

loading...`)(); } })); web.effect(_p$ => { @@ -9340,8 +9190,8 @@ const addQuickFavorite = (favoriteButton, root, apiUrl, position) => { /** 快捷收藏的界面 */ const quickFavorite = pageType => { if (pageType === 'gallery') { - const button = main.querySelector('#gdf'); - const root = main.querySelector('#gd3'); + const button = helper.querySelector('#gdf'); + const root = helper.querySelector('#gd3'); addQuickFavorite(button, root, `${unsafeWindow.popbase}addfav`, [0, button.firstElementChild.offsetTop]); return; } @@ -9350,7 +9200,7 @@ const quickFavorite = pageType => { switch (pageType) { case 't': { - for (const item of main.querySelectorAll('.gl1t')) { + for (const item of helper.querySelectorAll('.gl1t')) { const button = item.querySelector('[id][onclick]'); const top = item.firstElementChild.getBoundingClientRect().bottom - item.getBoundingClientRect().top; const bottom = item.lastElementChild.getBoundingClientRect().top - item.getBoundingClientRect().top; @@ -9360,7 +9210,7 @@ const quickFavorite = pageType => { } case 'e': { - for (const item of main.querySelectorAll('.gl1e')) { + for (const item of helper.querySelectorAll('.gl1e')) { const button = item.nextElementSibling.querySelector('[id][onclick]'); addQuickFavorite(button, item, /http.+?(?=')/.exec(button.getAttribute('onclick'))[0], [0, Number.parseInt(getComputedStyle(item).height, 10)]); } @@ -9373,9 +9223,9 @@ web.delegateEvents(["click"]); /** 关联 nhentai */ const associateNhentai = async (init, dynamicUpdate) => { const titleDom = document.getElementById('gn'); - const taglistDom = main.querySelector('#taglist tbody'); + const taglistDom = helper.querySelector('#taglist tbody'); if (!titleDom || !taglistDom) { - if ((document.getElementById('taglist')?.children.length ?? 1) > 0) main.toast.error(main.t('site.ehentai.html_changed_nhentai_failed')); + if ((document.getElementById('taglist')?.children.length ?? 1) > 0) main.toast.error(helper.t('site.ehentai.html_changed_nhentai_failed')); return; } const title = encodeURI(titleDom.textContent); @@ -9384,7 +9234,7 @@ const associateNhentai = async (init, dynamicUpdate) => { try { const res = await main.request(`https://nhentai.net/api/galleries/search?query=${title}`, { responseType: 'json', - errorText: main.t('site.ehentai.nhentai_error'), + errorText: helper.t('site.ehentai.nhentai_error'), noTip: true }); nHentaiComicInfo = res.response; @@ -9392,7 +9242,7 @@ const associateNhentai = async (init, dynamicUpdate) => { newTagLine.innerHTML = ` nhentai: - ${main.t('site.ehentai.nhentai_failed', { + ${helper.t('site.ehentai.nhentai_failed', { nhentai: `nhentai` })} `; @@ -9445,7 +9295,7 @@ const associateNhentai = async (init, dynamicUpdate) => { }; const showNhentaiComic = init(dynamicUpdate(async setImg => { nhentaiComicReadButton.innerHTML = ` loading - 0/${num_pages}`; - nhentaiImgList[selected_tagname] = await main.plimit(images.pages.map((page, i) => async () => { + nhentaiImgList[selected_tagname] = await helper.plimit(images.pages.map((page, i) => async () => { const imgRes = await main.request(`https://i.nhentai.net/galleries/${media_id}/${i + 1}.${fileType[page.t]}`, { headers: { Referer: `https://nhentai.net/g/${media_id}` @@ -9472,16 +9322,16 @@ const associateNhentai = async (init, dynamicUpdate) => { /** 快捷键翻页 */ const hotkeysPageTurn = pageType => { if (pageType === 'gallery') { - main.linstenKeydown(e => { + helper.linstenKeydown(e => { switch (e.key) { case 'ArrowRight': case 'd': e.preventDefault(); - return main.querySelector('.ptt td:last-child:not(.ptdd)')?.click(); + return helper.querySelector('.ptt td:last-child:not(.ptdd)')?.click(); case 'ArrowLeft': case 'a': e.preventDefault(); - return main.querySelector('.ptt td:first-child:not(.ptdd)')?.click(); + return helper.querySelector('.ptt td:first-child:not(.ptdd)')?.click(); case 'Escape': if (unsafeWindow.selected_tagname) { unsafeWindow.toggle_tagmenu(); @@ -9501,16 +9351,16 @@ const hotkeysPageTurn = pageType => { } }); } else { - main.linstenKeydown(e => { + helper.linstenKeydown(e => { switch (e.key) { case 'ArrowRight': case 'd': e.preventDefault(); - return main.querySelector('#unext')?.click(); + return helper.querySelector('#unext')?.click(); case 'ArrowLeft': case 'a': e.preventDefault(); - return main.querySelector('#uprev')?.click(); + return helper.querySelector('#uprev')?.click(); } }); } @@ -9533,7 +9383,7 @@ const getTagSetHtml = async tagset => { const res = await main.request(url, { fetch: true }); - return main.domParse(res.responseText); + return helper.domParse(res.responseText); }; /** 获取最新的标签颜色数据 */ @@ -9638,8 +9488,6 @@ const colorizeTag = async pageType => { } }; -var _tmpl$$2 = /*#__PURE__*/web.template(``), - _tmpl$2$1 = /*#__PURE__*/web.template(``); /** 快捷评分 */ const quickRating = pageType => { let list; @@ -9648,15 +9496,15 @@ const quickRating = pageType => { case 'mytags': return; case 'e': - list = main.querySelectorAll('#favform > table > tbody > tr'); + list = helper.querySelectorAll('#favform > table > tbody > tr'); break; case 'm': case 'p': case 'l': - list = main.querySelectorAll('#favform > table > tbody > tr').slice(1); + list = helper.querySelectorAll('#favform > table > tbody > tr').slice(1); break; case 't': - list = main.querySelectorAll('.gl1t'); + list = helper.querySelectorAll('.gl1t'); break; } GM_addStyle(` @@ -9673,11 +9521,11 @@ const quickRating = pageType => { const editRating = async (url, num) => { try { const dataRes = await main.request(url, { - errorText: main.t('site.ehentai.change_rating_failed'), + errorText: helper.t('site.ehentai.change_rating_failed'), noTip: true }); const reRes = /api_url = "(.+?)".+?gid = (\d+).+?token = "(.+?)".+?apiuid = (\d+).+?apikey = "(.+?)"/s.exec(dataRes.responseText); - if (!reRes) throw new Error(main.t('site.ehentai.change_rating_failed')); + if (!reRes) throw new Error(helper.t('site.ehentai.change_rating_failed')); const [, api_url, gid, token, apiuid, apikey] = reRes; const res = await main.request(api_url, { method: 'POST', @@ -9693,11 +9541,11 @@ const quickRating = pageType => { fetch: true, noTip: true }); - main.toast.success(`${main.t('site.ehentai.change_rating_success')}: ${res.response.rating_usr}`); + main.toast.success(`${helper.t('site.ehentai.change_rating_success')}: ${res.response.rating_usr}`); return res.response; } catch { - main.toast.error(main.t('site.ehentai.change_rating_failed')); - throw new Error(main.t('site.ehentai.change_rating_failed')); + main.toast.error(helper.t('site.ehentai.change_rating_failed')); + throw new Error(helper.t('site.ehentai.change_rating_failed')); } }; @@ -9712,7 +9560,7 @@ const quickRating = pageType => { const renderQuickRating = (item, ir, index) => { let basePosition = ir.style.backgroundPosition; web.render(() => (() => { - var _el$ = _tmpl$$2(), + var _el$ = web.template(``)(), _el$2 = _el$.firstChild, _el$3 = _el$2.nextSibling; _el$.$$mouseout = () => { @@ -9724,7 +9572,7 @@ const quickRating = pageType => { web.insert(_el$3, web.createComponent(solidJs.For, { each: coordsList, children: (coords, i) => (() => { - var _el$4 = _tmpl$2$1(); + var _el$4 = web.template(``)(); _el$4.$$click = async () => { const res = await editRating(item.querySelector('a').href, i() + 1); ir.className = res.rating_cls; @@ -9750,17 +9598,12 @@ const quickRating = pageType => { }; web.delegateEvents(["mouseout", "mouseover", "click"]); -var _tmpl$$1 = /*#__PURE__*/web.template(``); const MDLaunch = ((props = {}) => (() => { - var _el$ = _tmpl$$1(); + var _el$ = web.template(``)(); web.spread(_el$, props, true, true); return _el$; })()); -var _tmpl$ = /*#__PURE__*/web.template(`

`), - _tmpl$2 = /*#__PURE__*/web.template(`

`), - _tmpl$3 = /*#__PURE__*/web.template(``), - _tmpl$4 = /*#__PURE__*/web.template(`

loading...`); /** 快捷查看标签定义 */ const quickTagDefine = pageType => { if (pageType !== 'gallery') return; @@ -9773,13 +9616,13 @@ const quickTagDefine = pageType => { }); if (res.status !== 200) { tagContent[tag] = (() => { - var _el$ = _tmpl$(); + var _el$ = web.template(`

`)(); web.insert(_el$, () => `${res.status} - ${res.statusText}`); return _el$; })(); return; } - const html = main.domParse(res.responseText); + const html = helper.domParse(res.responseText); const content = html.querySelector('#mw-content-text'); // 将相对链接转换成正确的链接 @@ -9793,7 +9636,7 @@ const quickTagDefine = pageType => { // 删掉附加图 for (const dom of content.querySelectorAll('.thumb')) dom.remove(); tagContent[tag] = [(() => { - var _el$2 = _tmpl$2(), + var _el$2 = web.template(`

`)(), _el$3 = _el$2.firstChild; web.setAttribute(_el$3, "href", url); web.insert(_el$3, tag, null); @@ -9850,7 +9693,7 @@ const quickTagDefine = pageType => { } `); const [show, setShow] = solidJs.createSignal(false); - const root = main.querySelector('#taglist'); + const root = helper.querySelector('#taglist'); let background = 'rgba(0, 0, 0, 0)'; let dom = root; while (background === 'rgba(0, 0, 0, 0)') { @@ -9862,9 +9705,9 @@ const quickTagDefine = pageType => { return show(); }, get children() { - var _el$4 = _tmpl$3(); + var _el$4 = web.template(``)(); background != null ? _el$4.style.setProperty("background", background) : _el$4.style.removeProperty("background"); - web.insert(_el$4, () => tagContent[unsafeWindow.selected_tagname] ?? _tmpl$4()); + web.insert(_el$4, () => tagContent[unsafeWindow.selected_tagname] ?? web.template(`

loading...`)()); return _el$4; } }), root); @@ -9881,7 +9724,7 @@ const quickTagDefine = pageType => { }; // Esc 关闭 - main.linstenKeydown(e => { + helper.linstenKeydown(e => { if (e.key !== 'Escape' || !show()) return; setShow(false); e.stopImmediatePropagation(); @@ -9905,7 +9748,7 @@ const getDomPosition = dom => { }; const floatTagList = (pageType, mangaProps) => { if (pageType !== 'gallery') return; - const gd4 = main.querySelector('#gd4'); + const gd4 = helper.querySelector('#gd4'); const gd4Style = getComputedStyle(gd4); /** 背景颜色 */ @@ -9917,7 +9760,7 @@ const floatTagList = (pageType, mangaProps) => { } const { borderColor - } = getComputedStyle(main.querySelector('#gdt')); + } = getComputedStyle(helper.querySelector('#gdt')); /** 边框样式 */ const border = `1px solid ${borderColor}`; GM_addStyle(` @@ -9975,7 +9818,7 @@ const floatTagList = (pageType, mangaProps) => { setState, _setState, _state - } = main.useStore({ + } = helper.useStore({ open: false, top: 0, left: 0, @@ -9990,11 +9833,11 @@ const floatTagList = (pageType, mangaProps) => { } }); const setPos = (state, top, left) => { - state.top = main.clamp(0, top, state.bound.height); - state.left = main.clamp(0, left, state.bound.width); + state.top = helper.clamp(0, top, state.bound.height); + state.left = helper.clamp(0, left, state.bound.width); }; const setOpacity = opacity => { - _setState('opacity', main.clamp(0.5, opacity, 1)); + _setState('opacity', helper.clamp(0.5, opacity, 1)); }; setOpacity(Number(localStorage.getItem('floatTagListOpacity')) || 1); @@ -10009,13 +9852,13 @@ const floatTagList = (pageType, mangaProps) => { setState(state => { state.bound.width = window.innerWidth - gd4.clientWidth; state.bound.height = window.innerHeight - gd4.clientHeight; - state.top = main.clamp(0, state.top, state.bound.height); - state.left = main.clamp(0, state.left, state.bound.width); + state.top = helper.clamp(0, state.top, state.bound.height); + state.left = helper.clamp(0, state.left, state.bound.width); }); }; window.addEventListener('resize', hadnleResize); hadnleResize(); - main.useStyleMemo('#comicread-tag-box', { + helper.useStyleMemo('#comicread-tag-box', { display: () => store.open ? undefined : 'none', top: () => `${store.top}px`, left: () => `${store.left}px`, @@ -10047,7 +9890,7 @@ const floatTagList = (pageType, mangaProps) => { top: 0, left: 0 }; - main.useDrag({ + helper.useDrag({ ref: gd4, handleDrag({ type, @@ -10072,7 +9915,7 @@ const floatTagList = (pageType, mangaProps) => { // 窗口移到原位附近时自动收回 if (mangaProps.show) return; const rect = placeholder.getBoundingClientRect(); - if (main.approx(state.top, rect.top, 50) && main.approx(state.left, rect.left, 50)) state.open = false; + if (helper.approx(state.top, rect.top, 50) && helper.approx(state.left, rect.left, 50)) state.open = false; }); break; case 'move': @@ -10090,12 +9933,12 @@ const floatTagList = (pageType, mangaProps) => { let ehsParent; const handleEhs = () => { if (ehs) return; - ehs = main.querySelector('#ehs-introduce-box'); + ehs = helper.querySelector('#ehs-introduce-box'); if (!ehs) return; ehsParent = ehs.parentElement; // 让 ehs 的自动补全列表能显示在顶部 - const autoComplete = main.querySelector('.eh-syringe-lite-auto-complete-list'); + const autoComplete = helper.querySelector('.eh-syringe-lite-auto-complete-list'); if (autoComplete) { autoComplete.classList.add('comicread-ignore'); autoComplete.style.zIndex = '2147483647'; @@ -10103,13 +9946,13 @@ const floatTagList = (pageType, mangaProps) => { } // 只在当前有标签被选中时显示 ehs 的标签介绍 - main.hijackFn('toggle_tagmenu', (rawFn, args) => { + helper.hijackFn('toggle_tagmenu', (rawFn, args) => { const res = rawFn(...args); - if (!unsafeWindow.selected_tagname) main.querySelector('#ehs-introduce-box .ehs-close')?.click(); + if (!unsafeWindow.selected_tagname) helper.querySelector('#ehs-introduce-box .ehs-close')?.click(); return res; }); }; - main.createEffectOn(() => store.open, open => { + helper.createEffectOn(() => store.open, open => { handleEhs(); if (open) { const { @@ -10127,22 +9970,22 @@ const floatTagList = (pageType, mangaProps) => { gd4.style.width = ''; placeholder.after(gd4); if (ehs) ehsParent.append(ehs); - main.focus(); + Manga.focus(); } }, { defer: true }); - main.setDefaultHotkeys(hotkeys => ({ + Manga.setDefaultHotkeys(hotkeys => ({ ...hotkeys, float_tag_list: ['q'] })); - main.linstenKeydown(e => { + helper.linstenKeydown(e => { if (e.key === 'Escape' && store.open) { _setState('open', false); return e.stopImmediatePropagation(); } - const code = main.getKeyboardCode(e); - if (main.hotkeysMap()[code] !== 'float_tag_list') return; + const code = helper.getKeyboardCode(e); + if (Manga.hotkeysMap()[code] !== 'float_tag_list') return; e.stopPropagation(); e.preventDefault(); setState(state => { @@ -10153,47 +9996,43 @@ const floatTagList = (pageType, mangaProps) => { }); // 在悬浮状态下打完标签后移开焦点,以便能快速用快捷键关掉悬浮界面 - main.hijackFn('tag_from_field', (rawFn, args) => { + helper.hijackFn('tag_from_field', (rawFn, args) => { if (store.open) document.activeElement.blur(); return rawFn(...args); }); - const newTagInput = main.querySelector('#newtagfield'); + const newTagInput = helper.querySelector('#newtagfield'); // 悬浮状态下鼠标划过自动聚焦输入框 newTagInput.addEventListener('pointerenter', () => store.open && newTagInput.focus()); /** 根据标签链接获取对应的标签名 */ const getDropTag = tagUrl => { - const tagDom = main.querySelector(`a[href=${CSS.escape(tagUrl)}]`); + const tagDom = helper.querySelector(`a[href=${CSS.escape(tagUrl)}]`); if (!tagDom) return; // 有 ehs 的情况下 title 会是标签的简写 return tagDom.title || tagDom.id.slice(3).replaceAll('_', ' '); }; // 让标签可以直接拖进输入框,方便一次性点赞多个标签 - newTagInput.addEventListener('drop', e => { - e.dataTransfer?.items[0].getAsString(url => { - const tag = getDropTag(url); - if (!tag) return; - newTagInput.value = newTagInput.value.replace(url, `${tag}, `); - }); - }); + const handleDrop = e => { + const text = e.dataTransfer.getData('text'); + const tag = getDropTag(text); + if (!tag) return; + e.preventDefault(); + if (!newTagInput.value.includes(tag)) newTagInput.value += `${tag}, `; + }; + newTagInput.addEventListener('drop', handleDrop); // 增大拖拽标签的放置范围,不用非得拖进框 - const taglist = main.querySelector('#taglist'); + const taglist = helper.querySelector('#taglist'); taglist.addEventListener('dragover', e => e.preventDefault()); - taglist.addEventListener('drop', e => { - e.dataTransfer?.items[0].getAsString(url => { - const tag = getDropTag(url); - if (!tag || newTagInput.value.includes(tag)) return; - newTagInput.value += `${tag}, `; - }); - }); + taglist.addEventListener('dragenter', e => e.preventDefault()); + taglist.addEventListener('drop', handleDrop); }; (async () => { let pageType; - if (Reflect.has(unsafeWindow, 'display_comment_field')) pageType = 'gallery';else if (location.pathname === '/mytags') pageType = 'mytags';else pageType = main.querySelector('#ujumpbox ~ div > select')?.value; + if (Reflect.has(unsafeWindow, 'display_comment_field')) pageType = 'gallery';else if (location.pathname === '/mytags') pageType = 'mytags';else pageType = helper.querySelector('#ujumpbox ~ div > select')?.value; if (!pageType) return; const { options, @@ -10223,12 +10062,12 @@ const floatTagList = (pageType, mangaProps) => { autoShow: false }); if (Reflect.has(unsafeWindow, 'mpvkey')) { - const imgEleList = main.querySelectorAll('.mi0[id]'); - init(dynamicUpdate(setImg => main.plimit(imgEleList.map((ele, i) => async () => { + const imgEleList = helper.querySelectorAll('.mi0[id]'); + init(dynamicUpdate(setImg => helper.plimit(imgEleList.map((ele, i) => async () => { const getUrl = () => ele.querySelector('img')?.src; if (!getUrl()) unsafeWindow.load_image(i + 1); unsafeWindow.next_possible_request = 0; - const imgUrl = await main.wait(getUrl); + const imgUrl = await helper.wait(getUrl); setImg(i, imgUrl); }), undefined, 4), imgEleList.length)); return; @@ -10243,9 +10082,9 @@ const floatTagList = (pageType, mangaProps) => { // 快捷收藏。必须处于登录状态 if (unsafeWindow.apiuid !== -1 && options.quick_favorite) quickFavorite(pageType); // 快捷评分 - if (options.quick_rating) main.requestIdleCallback(() => quickRating(pageType), 1000); + if (options.quick_rating) helper.requestIdleCallback(() => quickRating(pageType), 1000); // 快捷查看标签定义 - if (options.quick_tag_define) main.requestIdleCallback(() => quickTagDefine(pageType), 1000); + if (options.quick_tag_define) helper.requestIdleCallback(() => quickTagDefine(pageType), 1000); // 不是漫画页的话 if (pageType !== 'gallery') return; @@ -10253,19 +10092,19 @@ const floatTagList = (pageType, mangaProps) => { // 表站开启了 Multi-Page Viewer 的话会将点击按钮挤出去,得缩一下位置 if (sidebarDom.children[6]) sidebarDom.children[6].style.padding = '0'; // 虽然有 Fab 了不需要这个按钮,但都点习惯了没有还挺别扭的( - main.insertNode(sidebarDom, '

Load comic

'); + helper.insertNode(sidebarDom, '

Load comic

'); const comicReadModeDom = document.getElementById('comicReadMode'); /** 从图片页获取图片地址 */ const getImgFromImgPage = async url => { const res = await main.request(url, { fetch: true, - errorText: main.t('site.ehentai.fetch_img_page_source_failed') + errorText: helper.t('site.ehentai.fetch_img_page_source_failed') }, 10); try { return /id="img" src="(.+?)"/.exec(res.responseText)[1]; } catch { - throw new Error(main.t('site.ehentai.fetch_img_url_failed')); + throw new Error(helper.t('site.ehentai.fetch_img_url_failed')); } }; @@ -10273,23 +10112,23 @@ const floatTagList = (pageType, mangaProps) => { const getImgFromDetailsPage = async (pageNum = 0) => { const res = await main.request(`${window.location.pathname}${pageNum ? `?p=${pageNum}` : ''}`, { fetch: true, - errorText: main.t('site.ehentai.fetch_img_page_url_failed') + errorText: helper.t('site.ehentai.fetch_img_page_url_failed') }); // 从详情页获取图片页的地址 const reRes = res.responseText.matchAll(/.+?title=".+?: [url, fileName]); }; const getImgNum = async () => { - let numText = main.querySelector('.gtb .gpc')?.textContent?.replaceAll(',', '').match(/\d+/g)?.at(-1); + let numText = helper.querySelector('.gtb .gpc')?.textContent?.replaceAll(',', '').match(/\d+/g)?.at(-1); if (numText) return Number(numText); const res = await main.request(window.location.href); numText = /(?<=)\d+(?= pages<\/td>)/.exec(res.responseText)?.[0]; if (numText) return Number(numText); - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); return 0; }; const totalImgNum = await getImgNum(); @@ -10302,7 +10141,7 @@ const floatTagList = (pageType, mangaProps) => { setManga('adList', new main.ReactiveSet()); /** 缩略图元素列表 */ const thumbnailEleList = []; - for (const e of main.querySelectorAll('#gdt img')) { + for (const e of helper.querySelectorAll('#gdt img')) { const index = Number(e.alt) - 1; if (Number.isNaN(index)) return; thumbnailEleList[index] = e; @@ -10310,14 +10149,14 @@ const floatTagList = (pageType, mangaProps) => { [, ehImgFileNameList[index]] = e.title.split(/:|: /); } // 先根据文件名判断一次 - await main.getAdPageByFileName(ehImgFileNameList, mangaProps.adList); + await getAdPageByFileName(ehImgFileNameList, mangaProps.adList); // 不行的话再用缩略图识别 - if (mangaProps.adList.size === 0) await main.getAdPageByContent(thumbnailEleList, mangaProps.adList); + if (mangaProps.adList.size === 0) await getAdPageByContent(thumbnailEleList, mangaProps.adList); // 模糊广告页的缩略图 const stylesheet = new CSSStyleSheet(); document.adoptedStyleSheets.push(stylesheet); - main.createEffectOn(() => [...(mangaProps.adList ?? [])], adList => { + helper.createEffectOn(() => [...(mangaProps.adList ?? [])], adList => { if (adList.length === 0) return; const styleList = adList.map(i => { const alt = `${i + 1}`.padStart(placeValueNum, '0'); @@ -10334,11 +10173,11 @@ const floatTagList = (pageType, mangaProps) => { loadImgList } = init(dynamicUpdate(async setImg => { comicReadModeDom.innerHTML = ` loading`; - const totalPageNum = Number(main.querySelector('.ptt td:nth-last-child(2)').textContent); + const totalPageNum = Number(helper.querySelector('.ptt td:nth-last-child(2)').textContent); for (let pageNum = 0; pageNum < totalPageNum; pageNum++) { const startIndex = ehImgList.length; const imgPageUrlList = await getImgFromDetailsPage(pageNum); - await main.plimit(imgPageUrlList.map(([imgPageUrl, fileName], i) => async () => { + await helper.plimit(imgPageUrlList.map(([imgPageUrl, fileName], i) => async () => { const imgUrl = await getImgFromImgPage(imgPageUrl); const index = startIndex + i; ehImgList[index] = imgUrl; @@ -10349,14 +10188,14 @@ const floatTagList = (pageType, mangaProps) => { const doneNum = startIndex + _doneNum; setFab({ progress: doneNum / totalImgNum, - tip: `${main.t('other.loading_img')} - ${doneNum}/${totalImgNum}` + tip: `${helper.t('other.loading_img')} - ${doneNum}/${totalImgNum}` }); comicReadModeDom.innerHTML = ` loading - ${doneNum}/${totalImgNum}`; if (doneNum === totalImgNum) { comicReadModeDom.innerHTML = ` Read`; if (enableDetectAd) { - await main.getAdPageByFileName(ehImgFileNameList, mangaProps.adList); - await main.getAdPageByContent(ehImgList, mangaProps.adList); + await getAdPageByFileName(ehImgFileNameList, mangaProps.adList); + await getAdPageByContent(ehImgList, mangaProps.adList); } } }); @@ -10366,10 +10205,10 @@ const floatTagList = (pageType, mangaProps) => { /** 获取新的图片页地址 */ const getNewImgPageUrl = async url => { const res = await main.request(url, { - errorText: main.t('site.ehentai.fetch_img_page_source_failed') + errorText: helper.t('site.ehentai.fetch_img_page_source_failed') }); const nl = /nl\('(.+?)'\)/.exec(res.responseText)?.[1]; - if (!nl) throw new Error(main.t('site.ehentai.fetch_img_url_failed')); + if (!nl) throw new Error(helper.t('site.ehentai.fetch_img_url_failed')); const newUrl = new URL(url); newUrl.searchParams.set('nl', nl); return newUrl.href; @@ -10379,9 +10218,9 @@ const floatTagList = (pageType, mangaProps) => { const reloadImg = async i => { const pageUrl = await getNewImgPageUrl(ehImgPageList[i]); let imgUrl = ''; - while (!imgUrl || !(await main.testImgUrl(imgUrl))) { + while (!imgUrl || !(await helper.testImgUrl(imgUrl))) { imgUrl = await getImgFromImgPage(pageUrl); - main.log(`刷新图片 ${i}\n${ehImgList[i]} ->\n${imgUrl}`); + helper.log(`刷新图片 ${i}\n${ehImgList[i]} ->\n${imgUrl}`); } ehImgList[i] = imgUrl; ehImgPageList[i] = pageUrl; @@ -10389,10 +10228,10 @@ const floatTagList = (pageType, mangaProps) => { }; /** 判断当前显示的是否是 eh 源 */ - const isShowEh = () => main.store.imgList[0]?.src === ehImgList[0]; + const isShowEh = () => Manga.store.imgList[0]?.src === ehImgList[0]; /** 刷新所有错误图片 */ - const reloadErrorImg = main.singleThreaded(() => main.plimit(main.store.imgList.map(({ + const reloadErrorImg = helper.singleThreaded(() => helper.plimit(Manga.store.imgList.map(({ loadType }, i) => () => { if (loadType !== 'error' || !isShowEh()) return; @@ -10400,14 +10239,14 @@ const floatTagList = (pageType, mangaProps) => { }))); setManga({ onExit(isEnd) { - if (isEnd) main.scrollIntoView('#cdiv'); + if (isEnd) helper.scrollIntoView('#cdiv'); setManga('show', false); }, // 在图片加载出错时刷新图片 async onLoading(imgList, img) { onLoading(imgList, img); if (!img) return; - if (img.loadType !== 'error' || (await main.testImgUrl(img.src))) return; + if (img.loadType !== 'error' || (await helper.testImgUrl(img.src))) return; return reloadErrorImg(); } }); @@ -10416,8 +10255,8 @@ const floatTagList = (pageType, mangaProps) => { // 关联 nhentai if (options.associate_nhentai) associateNhentai(init, dynamicUpdate); -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } @@ -10425,6 +10264,7 @@ const floatTagList = (pageType, mangaProps) => { case 'nhentai.net': { const main = require('main'); +const helper = require('helper'); /** 用于转换获得图片文件扩展名 */ const fileType = { @@ -10451,13 +10291,13 @@ const fileType = { if (Reflect.has(unsafeWindow, 'gallery')) { setManga({ onExit(isEnd) { - if (isEnd) main.scrollIntoView('#comment-container'); + if (isEnd) helper.scrollIntoView('#comment-container'); setManga('show', false); } }); // 虽然有 Fab 了不需要这个按钮,但我自己都点习惯了没有还挺别扭的( - main.insertNode(document.getElementById('download').parentNode, ' Read'); + helper.insertNode(document.getElementById('download').parentNode, ' Read'); const comicReadModeDom = document.getElementById('comicReadMode'); const { showComic @@ -10472,9 +10312,9 @@ const fileType = { // 在漫画浏览页 if (document.getElementsByClassName('gallery').length > 0) { - if (options.open_link_new_page) for (const e of main.querySelectorAll('a:not([href^="javascript:"])')) e.setAttribute('target', '_blank'); + if (options.open_link_new_page) for (const e of helper.querySelectorAll('a:not([href^="javascript:"])')) e.setAttribute('target', '_blank'); const blacklist = (unsafeWindow?._n_app ?? unsafeWindow?.n)?.options?.blacklisted_tags; - if (blacklist === undefined) main.toast.error(main.t('site.nhentai.tag_blacklist_fetch_failed')); + if (blacklist === undefined) main.toast.error(helper.t('site.nhentai.tag_blacklist_fetch_failed')); // blacklist === null 时是未登录 if (options.block_totally && blacklist?.length) GM_addStyle('.blacklisted.gallery { display: none; }'); @@ -10485,19 +10325,19 @@ const fileType = { hr:not(:last-child) { display: none; } @keyframes load { 0% { width: 100%; } 100% { width: 0; } } `); - let pageNum = Number(main.querySelector('.page.current')?.innerHTML ?? ''); + let pageNum = Number(helper.querySelector('.page.current')?.innerHTML ?? ''); if (Number.isNaN(pageNum)) return; const contentDom = document.getElementById('content'); let apiUrl = ''; - if (window.location.pathname === '/') apiUrl = '/api/galleries/all?';else if (main.querySelector('a.tag')) apiUrl = `/api/galleries/tagged?tag_id=${main.querySelector('a.tag')?.classList[1].split('-')[1]}&`;else if (window.location.pathname.includes('search')) apiUrl = `/api/galleries/search?query=${new URLSearchParams(window.location.search).get('q')}&`; + if (window.location.pathname === '/') apiUrl = '/api/galleries/all?';else if (helper.querySelector('a.tag')) apiUrl = `/api/galleries/tagged?tag_id=${helper.querySelector('a.tag')?.classList[1].split('-')[1]}&`;else if (window.location.pathname.includes('search')) apiUrl = `/api/galleries/search?query=${new URLSearchParams(window.location.search).get('q')}&`; let observer; // eslint-disable-line no-autofix/prefer-const - const loadNewComic = main.singleThreaded(async () => { + const loadNewComic = helper.singleThreaded(async () => { pageNum += 1; const res = await main.request(`${apiUrl}page=${pageNum}${window.location.pathname.includes('popular') ? '&sort=popular ' : ''}`, { fetch: true, responseType: 'json', - errorText: main.t('site.nhentai.fetch_next_page_failed') + errorText: helper.t('site.nhentai.fetch_next_page_failed') }); const { result, @@ -10516,7 +10356,7 @@ const fileType = { for (let i = pageNum - 5; i <= pageNum + 5; i += 1) { if (i > 0 && i <= num_pages) pageNumDom.push(`${i}`); } - main.insertNode(contentDom, `

${pageNum}

+ helper.insertNode(contentDom, `

${pageNum}

${comicDomHtml}
@@ -10549,11 +10389,11 @@ const fileType = { }, false); observer = new IntersectionObserver(entries => entries[0].isIntersecting && loadNewComic()); observer.observe(contentDom.lastElementChild); - if (main.querySelector('section.pagination')) contentDom.append(document.createElement('hr')); + if (helper.querySelector('section.pagination')) contentDom.append(document.createElement('hr')); } } -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } @@ -10561,6 +10401,7 @@ const fileType = { case 'yuri.website': { const main = require('main'); +const helper = require('helper'); (async () => { const { @@ -10599,18 +10440,18 @@ const main = require('main'); })(); // 跳过漫画区外的页面 - if (!main.querySelector('a.post-list-cat-item[title="在线区-漫画"]')) return; + if (!helper.querySelector('a.post-list-cat-item[title="在线区-漫画"]')) return; // 需要购买的漫画 - if (main.querySelector('.content-hidden')) { - const imgBody = main.querySelector('.content-hidden'); + if (helper.querySelector('.content-hidden')) { + const imgBody = helper.querySelector('.content-hidden'); const imgList = imgBody.getElementsByTagName('img'); - if (await main.wait(() => imgList.length, 1000)) init(() => [...imgList].map(e => e.src)); + if (await helper.wait(() => imgList.length, 1000)) init(() => [...imgList].map(e => e.src)); return; } // 有折叠内容的漫画 - if (main.querySelector('.xControl')) { + if (helper.querySelector('.xControl')) { needAutoShow.val = false; const { loadImgList @@ -10624,7 +10465,7 @@ const main = require('main'); onNext: i === imgListMap.length - 1 ? undefined : () => loadChapterImg(i + 1) }); }; - for (const [i, a] of main.querySelectorAll('.xControl > a').entries()) { + for (const [i, a] of helper.querySelectorAll('.xControl > a').entries()) { const imgRoot = a.parentElement.nextElementSibling; imgListMap.push(imgRoot.getElementsByTagName('img')); a.addEventListener('click', () => { @@ -10636,10 +10477,10 @@ const main = require('main'); } // 没有折叠的单篇漫画 - await main.wait(() => main.querySelectorAll('.entry-content img').length); - return init(() => main.querySelectorAll('.entry-content img').map(e => e.src)); + await helper.wait(() => helper.querySelectorAll('.entry-content img').length); + return init(() => helper.querySelectorAll('.entry-content img').map(e => e.src)); })(); -; + break; } @@ -10660,6 +10501,7 @@ const main = require('main'); case 'www.copymanga.com': { const main = require('main'); +const helper = require('helper'); (() => { const headers = { @@ -10692,8 +10534,8 @@ const main = require('main'); options = { name: 'copymanga', getImgList, - onNext: main.querySelectorClick('.comicContent-next a:not(.prev-null)'), - onPrev: main.querySelectorClick('.comicContent-prev:not(.index,.list) a:not(.prev-null)'), + onNext: helper.querySelectorClick('.comicContent-next a:not(.prev-null)'), + onPrev: helper.querySelectorClick('.comicContent-prev:not(.index,.list) a:not(.prev-null)'), async getCommentList() { const chapter_id = window.location.pathname.split('/').at(-1); const res = await main.request(`/api/v3/roasts?chapter_id=${chapter_id}&limit=100&offset=0&_update=true`, { @@ -10719,7 +10561,7 @@ const main = require('main'); // 因为拷贝漫画的目录是动态加载的,所以要等目录加载出来再往上添加 if (!a) (async () => { a = document.createElement('a'); - const tableRight = await main.wait(() => main.querySelector('.table-default-right')); + const tableRight = await helper.wait(() => helper.querySelector('.table-default-right')); a.target = '_blank'; tableRight.insertBefore(a, tableRight.firstElementChild); const span = document.createElement('span'); @@ -10754,7 +10596,7 @@ const main = require('main'); document.addEventListener('visibilitychange', updateLastChapter); } })(); -; + break; } @@ -10763,12 +10605,12 @@ const main = require('main'); { options = { name: 'terraHistoricus', - wait: () => Boolean(main.querySelector('.comic-page-container img')), - getImgList: () => main.querySelectorAll('.comic-page-container img').map(e => e.dataset.srcset), + wait: () => Boolean(helper.querySelector('.comic-page-container img')), + getImgList: () => helper.querySelectorAll('.comic-page-container img').map(e => e.dataset.srcset), SPA: { isMangaPage: () => window.location.href.includes('/comic/'), - getOnPrev: () => main.querySelectorClick('.prev-btn:not(.invisible) a'), - getOnNext: () => main.querySelectorClick('.next-btn:not(.invisible) a') + getOnPrev: () => helper.querySelectorClick('.prev-btn:not(.invisible) a'), + getOnNext: () => helper.querySelectorClick('.next-btn:not(.invisible) a') } }; break; @@ -10784,14 +10626,14 @@ const main = require('main'); }; options = { name: 'terraHistoricus', - wait: () => Boolean(main.querySelector('.HG_COMIC_READER_main')), + wait: () => Boolean(helper.querySelector('.HG_COMIC_READER_main')), async getImgList({ setFab }) { const res = await main.request(apiUrl()); const pageList = JSON.parse(res.responseText).data.pageInfos; if (pageList.length === 0 && window.location.pathname.includes('episode')) throw new Error('获取图片列表时出错'); - return main.plimit(main.createSequence(pageList.length).map(getImgUrl), (doneNum, totalNum) => { + return helper.plimit(helper.createSequence(pageList.length).map(getImgUrl), (doneNum, totalNum) => { setFab({ progress: doneNum / totalNum, tip: `加载图片中 - ${doneNum}/${totalNum}` @@ -10800,8 +10642,8 @@ const main = require('main'); }, SPA: { isMangaPage: () => window.location.href.includes('episode'), - getOnPrev: () => main.querySelectorClick('footer .HG_COMIC_READER_prev a'), - getOnNext: () => main.querySelectorClick('footer .HG_COMIC_READER_prev+.HG_COMIC_READER_buttonEp a') + getOnPrev: () => helper.querySelectorClick('footer .HG_COMIC_READER_prev a'), + getOnNext: () => helper.querySelectorClick('footer .HG_COMIC_READER_prev+.HG_COMIC_READER_buttonEp a') } }; break; @@ -10814,6 +10656,7 @@ const main = require('main'); case '18comic.vip': { const main = require('main'); +const helper = require('helper'); // 已知问题:某些漫画始终会有几页在下载原图时出错 // 并且这类漫画下即使关掉脚本,也还是会有几页就是加载不出来 @@ -10838,13 +10681,13 @@ const main = require('main'); }); return; } - await main.sleep(100); + await helper.sleep(100); } setManga({ - onPrev: main.querySelectorClick(() => main.querySelector('.menu-bolock-ul .fa-angle-double-left')?.parentElement), - onNext: main.querySelectorClick(() => main.querySelector('.menu-bolock-ul .fa-angle-double-right')?.parentElement) + onPrev: helper.querySelectorClick(() => helper.querySelector('.menu-bolock-ul .fa-angle-double-left')?.parentElement), + onNext: helper.querySelectorClick(() => helper.querySelector('.menu-bolock-ul .fa-angle-double-right')?.parentElement) }); - const imgEleList = main.querySelectorAll('.scramble-page:not(.thewayhome) > img'); + const imgEleList = helper.querySelectorAll('.scramble-page:not(.thewayhome) > img'); // 判断当前漫画是否有被分割,没有就直接获取图片链接加载 // 判断条件来自页面上的 scramble_image 函数 @@ -10878,7 +10721,7 @@ const main = require('main'); } imgEle.src = URL.createObjectURL(res.response); try { - await main.waitImgLoad(imgEle, 1000 * 10); + await helper.waitImgLoad(imgEle, 1000 * 10); } catch { URL.revokeObjectURL(imgEle.src); imgEle.src = originalUrl; @@ -10889,7 +10732,7 @@ const main = require('main'); // 原有的 canvas 可能已被污染,直接删掉 if (imgEle.nextElementSibling?.tagName === 'CANVAS') imgEle.nextElementSibling.remove(); unsafeWindow.onImageLoaded(imgEle); - const blob = await main.canvasToBlob(imgEle.nextElementSibling, 'image/webp', 1); + const blob = await helper.canvasToBlob(imgEle.nextElementSibling, 'image/webp', 1); URL.revokeObjectURL(imgEle.src); if (!blob) throw new Error('转换图片时出错'); return `${URL.createObjectURL(blob)}#.webp`; @@ -10901,18 +10744,18 @@ const main = require('main'); }; // 先等懒加载触发完毕 - await main.wait(() => { - const loadedNum = main.querySelectorAll('.lazy-loaded').length; - return loadedNum > 0 && main.querySelectorAll('canvas').length - loadedNum <= 1; + await helper.wait(() => { + const loadedNum = helper.querySelectorAll('.lazy-loaded').length; + return loadedNum > 0 && helper.querySelectorAll('canvas').length - loadedNum <= 1; }); - init(dynamicUpdate(setImg => main.plimit(imgEleList.map((img, i) => async () => setImg(i, await getImgUrl(img))), (doneNum, totalNum) => { + init(dynamicUpdate(setImg => helper.plimit(imgEleList.map((img, i) => async () => setImg(i, await getImgUrl(img))), (doneNum, totalNum) => { setFab({ progress: doneNum / totalNum, tip: `加载图片中 - ${doneNum}/${totalNum}` }); }), imgEleList.length)); -})().catch(error => main.log.error(error)); -; +})().catch(error => helper.log.error(error)); + break; } @@ -10925,14 +10768,14 @@ const main = require('main'); if (!/\/comic\/\d+\/\d+\.html/.test(window.location.pathname)) break; let comicInfo; try { - const dataScript = main.querySelectorAll('body > script:not([src])').find(script => script.innerHTML.startsWith('window[')); - if (!dataScript) throw new Error(main.t('site.changed_load_failed')); + const dataScript = helper.querySelectorAll('body > script:not([src])').find(script => script.innerHTML.startsWith('window[')); + if (!dataScript) throw new Error(helper.t('site.changed_load_failed')); comicInfo = JSON.parse( // 只能通过 eval 获得数据 // eslint-disable-next-line no-eval eval(dataScript.innerHTML.slice(26)).match(/(?<=.*?\(){.+}/)[0]); } catch { - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); break; } @@ -10951,10 +10794,10 @@ const main = require('main'); if (comicInfo.images) { const { origin - } = new URL(main.querySelector('#manga img').src); + } = new URL(helper.querySelector('#manga img').src); return comicInfo.images.map(url => `${origin}${url}?${sl}`); } - main.toast.error(main.t('site.changed_load_failed'), { + main.toast.error(helper.t('site.changed_load_failed'), { throw: true }); return []; @@ -10990,7 +10833,7 @@ const main = require('main'); if (!Reflect.has(unsafeWindow, 'DM5_CID')) break; const imgNum = unsafeWindow.DM5_IMAGE_COUNT ?? unsafeWindow.imgsLen; if (!(Number.isSafeInteger(imgNum) && imgNum > 0)) { - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); break; } const getPageImg = async i => { @@ -11012,14 +10855,14 @@ const main = require('main'); // eslint-disable-next-line no-eval return eval(res); }; - const handlePrevNext = (pcSelector, mobileText) => main.querySelectorClick(() => main.querySelector(pcSelector) ?? main.querySelectorAll('.view-bottom-bar a').find(e => e.textContent?.includes(mobileText))); + const handlePrevNext = (pcSelector, mobileText) => helper.querySelectorClick(() => helper.querySelector(pcSelector) ?? helper.querySelectorAll('.view-bottom-bar a').find(e => e.textContent?.includes(mobileText))); options = { name: 'dm5', getImgList({ dynamicUpdate }) { // manhuaren 和 1kkk 的移动端上会直接用一个变量存储所有图片的链接 - if (Array.isArray(unsafeWindow.newImgs) && unsafeWindow.newImgs.every(main.isUrl)) return unsafeWindow.newImgs; + if (Array.isArray(unsafeWindow.newImgs) && unsafeWindow.newImgs.every(helper.isUrl)) return unsafeWindow.newImgs; return dynamicUpdate(async setImg => { const imgList = new Set(); while (imgList.size < imgNum) { @@ -11034,7 +10877,7 @@ const main = require('main'); }, onPrev: handlePrevNext('.logo_1', '上一章'), onNext: handlePrevNext('.logo_2', '下一章'), - onExit: isEnd => isEnd && main.scrollIntoView('.postlist') + onExit: isEnd => isEnd && helper.scrollIntoView('.postlist') }; break; } @@ -11046,7 +10889,7 @@ const main = require('main'); case 'wnacg.com': { // 突出显示下拉阅读的按钮 - const buttonDom = main.querySelector('#bodywrap a.btn'); + const buttonDom = helper.querySelector('#bodywrap a.btn'); if (buttonDom) { buttonDom.style.setProperty('background-color', '#607d8b'); buttonDom.style.setProperty('background-image', 'none'); @@ -11070,7 +10913,7 @@ const main = require('main'); if (!Reflect.has(unsafeWindow, 'MANGABZ_CID')) break; const imgNum = unsafeWindow.MANGABZ_IMAGE_COUNT ?? unsafeWindow.imgsLen; if (!(Number.isSafeInteger(imgNum) && imgNum > 0)) { - main.toast.error(main.t('site.changed_load_failed')); + main.toast.error(helper.t('site.changed_load_failed')); break; } const getPageImg = async i => { @@ -11090,7 +10933,7 @@ const main = require('main'); // eslint-disable-next-line no-eval return eval(res); }; - const handlePrevNext = (pcSelector, mobileText) => main.querySelectorClick(() => main.querySelector(pcSelector) ?? main.querySelectorAll('.bottom-bar-tool a').find(e => e.textContent?.includes(mobileText))); + const handlePrevNext = (pcSelector, mobileText) => helper.querySelectorClick(() => helper.querySelector(pcSelector) ?? helper.querySelectorAll('.bottom-bar-tool a').find(e => e.textContent?.includes(mobileText))); options = { name: 'mangabz', getImgList: ({ @@ -11127,7 +10970,7 @@ const main = require('main'); }`; const getImgList = async () => { const chapterId = /chapter\/(\d+)/.exec(window.location.pathname)?.[1]; - if (!chapterId) throw new Error(main.t('site.changed_load_failed')); + if (!chapterId) throw new Error(helper.t('site.changed_load_failed')); const res = await main.request('/api/query', { method: 'POST', responseType: 'json', @@ -11147,8 +10990,8 @@ const main = require('main'); }) => `https://komiic.com/api/image/${kid}`); }; const handlePrevNext = text => async () => { - await main.waitDom('.v-bottom-navigation__content'); - return main.querySelectorClick('.v-bottom-navigation__content > button:not([disabled])', text); + await helper.waitDom('.v-bottom-navigation__content'); + return helper.querySelectorClick('.v-bottom-navigation__content > button:not([disabled])', text); }; options = { name: 'komiic', @@ -11203,8 +11046,8 @@ const main = require('main'); options = { name: '8comic', getImgList, - onNext: main.querySelectorClick('#nextvol'), - onPrev: main.querySelectorClick('#prevvol') + onNext: helper.querySelectorClick('#nextvol'), + onPrev: helper.querySelectorClick('#prevvol') }; break; } @@ -11224,8 +11067,8 @@ const main = require('main'); const baseUrl = unsafeWindow.img_qianz ?? unsafeWindow.ImgSvrList; return unsafeWindow.msg.split('|').map(path => `${baseUrl}${path}`); }, - onNext: main.querySelectorClick('#pnpage > a', '下一'), - onPrev: main.querySelectorClick('#pnpage > a', '上一') + onNext: helper.querySelectorClick('#pnpage > a', '下一'), + onPrev: helper.querySelectorClick('#pnpage > a', '上一') }; break; } @@ -11288,7 +11131,7 @@ const main = require('main'); if (!isMangaPage) break; const startTime = performance.now(); setImg(i, await downloadImg(`${base}${path}`)); - await main.sleep(500 - (performance.now() - startTime)); + await helper.sleep(500 - (performance.now() - startTime)); } }, totalPageNum)(); }, @@ -11304,6 +11147,7 @@ const main = require('main'); case 'kemono.party': { const main = require('main'); +const helper = require('helper'); (async () => { const { @@ -11318,15 +11162,15 @@ const main = require('main'); /** 加载原图 */ load_original_image: true }); - const getImglist = () => options.load_original_image ? main.querySelectorAll('.post__thumbnail a').map(e => e.href) : main.querySelectorAll('.post__thumbnail img').map(e => e.src); + const getImglist = () => options.load_original_image ? helper.querySelectorAll('.post__thumbnail a').map(e => e.href) : helper.querySelectorAll('.post__thumbnail img').map(e => e.src); init(getImglist); // 在切换时重新获取图片 - main.createEffectOn(() => options.load_original_image, () => setManga('imgList', getImglist())); + helper.createEffectOn(() => options.load_original_image, () => setManga('imgList', getImglist())); // 加上跳转至 pwa 的链接 const zipExtension = new Set(['zip', 'rar', '7z', 'cbz', 'cbr', 'cb7']); - for (const e of main.querySelectorAll('.post__attachment a')) { + for (const e of helper.querySelectorAll('.post__attachment a')) { if (!zipExtension.has(e.href.split('.').pop())) continue; const a = document.createElement('a'); a.href = `https://comic-read.pages.dev/?url=${encodeURIComponent(e.href)}`; @@ -11336,7 +11180,7 @@ const main = require('main'); e.parentNode.insertBefore(a, e.nextElementSibling); } })(); -; + break; } @@ -11345,7 +11189,7 @@ const main = require('main'); { options = { name: 'nekohouse', - getImgList: () => main.querySelectorAll('.fileThumb').map(e => e.getAttribute('href')), + getImgList: () => helper.querySelectorAll('.fileThumb').map(e => e.getAttribute('href')), initOptions: { autoShow: false, defaultOption: { @@ -11361,18 +11205,18 @@ const main = require('main'); case 'weloma.art': case 'welovemanga.one': { - if (!main.querySelector('#listImgs, .chapter-content')) break; + if (!helper.querySelector('#listImgs, .chapter-content')) break; const getImgList = async () => { - const imgList = main.querySelectorAll('img.chapter-img:not(.ls-is-cached)').map(e => (e.dataset.src ?? e.dataset.srcset ?? e.dataset.original ?? e.src).trim()); + const imgList = helper.querySelectorAll('img.chapter-img:not(.ls-is-cached)').map(e => (e.dataset.src ?? e.dataset.srcset ?? e.dataset.original ?? e.src).trim()); if (imgList.length > 0 && imgList.every(url => !/loading.*\.gif/.test(url))) return imgList; - await main.sleep(500); + await helper.sleep(500); return getImgList(); }; options = { name: 'welovemanga', getImgList, - onNext: main.querySelectorClick('.rd_top-right.next:not(.disabled)'), - onPrev: main.querySelectorClick('.rd_top-left.prev:not(.disabled)') + onNext: helper.querySelectorClick('.rd_top-right.next:not(.disabled)'), + onPrev: helper.querySelectorClick('.rd_top-left.prev:not(.disabled)') }; break; } @@ -11387,35 +11231,11 @@ const main = require('main'); } default: { +const web = require('solid-js/web'); +const helper = require('helper'); +const Manga = require('components/Manga'); const main = require('main'); -const langList = ['zh', 'en', 'ru']; -/** 判断传入的字符串是否是支持的语言类型代码 */ -const isLanguages = lang => Boolean(lang) && langList.includes(lang); - -/** 返回浏览器偏好语言 */ -const getBrowserLang = () => { - let newLang; - for (let i = 0; i < navigator.languages.length; i++) { - const language = navigator.languages[i]; - const matchLang = langList.find(l => l === language || l === language.split('-')[0]); - if (matchLang) { - newLang = matchLang; - break; - } - } - return newLang; -}; -const getSaveLang = async () => typeof GM === 'undefined' ? localStorage.getItem('Languages') : GM.getValue('Languages'); -const setSaveLang = async val => typeof GM === 'undefined' ? localStorage.setItem('Languages', val) : GM.setValue('Languages', val); -const getInitLang = async () => { - const saveLang = await getSaveLang(); - if (isLanguages(saveLang)) return saveLang; - const lang = getBrowserLang() ?? 'zh'; - setSaveLang(lang); - return lang; -}; - const getTagText = ele => { let text = ele.nodeName; if (ele.id && !/\d/.test(ele.id)) text += `#${ele.id}`; @@ -11448,6 +11268,144 @@ const isEleSelector = (ele, selector) => { // 目录页和漫画页的图片层级相同 // https://www.biliplus.com/manga/ // 图片路径上有 id 元素并且 id 含有漫画 id,不同话数 id 也不同 +const createImgData = (oldSrc = '') => ({ + triggedNum: 0, + observerTimeout: 0, + oldSrc +}); + +/** 用于判断是否是图片 url 的正则 */ +const isImgUrlRe = /^(((https?|ftp|file):)?\/)?\/[-\w+&@#/%?=~|!:,.;]+[-\w+&@#%=~|]$/; + +/** 检查元素属性,将格式为图片 url 的属性值作为 src */ +const tryCorrectUrl = e => { + e.getAttributeNames().some(key => { + // 跳过白名单 + switch (key) { + case 'src': + case 'alt': + case 'class': + case 'style': + case 'id': + case 'title': + case 'onload': + case 'onerror': + return false; + } + const val = e.getAttribute(key).trim(); + if (!isImgUrlRe.test(val)) return false; + e.setAttribute('src', val); + return true; + }); +}; + +/** + * + * 通过滚动到指定图片元素位置并停留一会来触发图片的懒加载,返回图片 src 是否发生变化 + * + * 会在触发后重新滚回原位,当 time 为 0 时,因为滚动速度很快所以是无感的 + */ +const triggerEleLazyLoad = async (e, time, isLazyLoaded) => { + const nowScroll = window.scrollY; + e.scrollIntoView({ + behavior: 'instant' + }); + e.dispatchEvent(new Event('scroll', { + bubbles: true + })); + try { + if (isLazyLoaded && time) return await helper.wait(isLazyLoaded, time); + } finally { + window.scroll({ + top: nowScroll, + behavior: 'instant' + }); + } +}; + +/** 判断一个元素是否已经触发完懒加载 */ +const isLazyLoaded = (e, oldSrc) => { + if (!e.src) return false; + if (!e.offsetParent) return false; + // 有些网站会使用 svg 占位 + if (e.src.startsWith('data:image/svg')) return false; + if (oldSrc !== undefined && e.src !== oldSrc) return true; + if (e.naturalWidth > 500 || e.naturalHeight > 500) return true; + return false; +}; +const imgMap = new WeakMap(); +// eslint-disable-next-line no-autofix/prefer-const +let imgShowObserver; +const getImg = e => imgMap.get(e) ?? createImgData(); +const MAX_TRIGGED_NUM = 5; + +/** 判断图片元素是否需要触发懒加载 */ +const needTrigged = e => !isLazyLoaded(e, imgMap.get(e)?.oldSrc) && (imgMap.get(e)?.triggedNum ?? 0) < MAX_TRIGGED_NUM; + +/** 图片懒加载触发完后调用 */ +const handleTrigged = e => { + const img = getImg(e); + img.observerTimeout = 0; + img.triggedNum += 1; + if (isLazyLoaded(e, img.oldSrc) && img.triggedNum < MAX_TRIGGED_NUM) img.triggedNum = MAX_TRIGGED_NUM; + imgMap.set(e, img); + if (!needTrigged(e)) imgShowObserver.unobserve(e); +}; + +/** 监视图片是否被显示的 Observer */ +imgShowObserver = new IntersectionObserver(entries => { + for (const img of entries) { + const ele = img.target; + if (img.isIntersecting) { + imgMap.set(ele, { + ...getImg(ele), + observerTimeout: window.setTimeout(handleTrigged, 290, ele) + }); + } + const timeoutID = imgMap.get(ele)?.observerTimeout; + if (timeoutID) window.clearTimeout(timeoutID); + } +}); +const turnPageScheduled = helper.createScheduled(fn => helper.throttle(fn, 1000)); +/** 触发翻页 */ +const triggerTurnPage = async (waitTime = 0) => { + if (!turnPageScheduled()) return; + const nowScroll = window.scrollY; + // 滚到底部再滚回来,触发可能存在的自动翻页脚本 + window.scroll({ + top: document.body.scrollHeight, + behavior: 'instant' + }); + document.body.dispatchEvent(new Event('scroll', { + bubbles: true + })); + if (waitTime) await helper.sleep(waitTime); + window.scroll({ + top: nowScroll, + behavior: 'instant' + }); +}; +const waitTime = 300; + +/** 触发页面上所有图片元素的懒加载 */ +const triggerLazyLoad = helper.singleThreaded(async (state, getAllImg, runCondition) => { + // 过滤掉已经被触发过懒加载的图片 + const targetImgList = getAllImg().filter(needTrigged).sort((a, b) => a.getBoundingClientRect().y - b.getBoundingClientRect().y); + for (const e of targetImgList) { + imgShowObserver.observe(e); + if (!imgMap.has(e)) imgMap.set(e, createImgData(e.src)); + } + for (const e of targetImgList) { + await helper.wait(runCondition); + await triggerTurnPage(waitTime); + if (!needTrigged(e)) continue; + tryCorrectUrl(e); + if (await triggerEleLazyLoad(e, waitTime, () => isLazyLoaded(e, imgMap.get(e)?.oldSrc))) handleTrigged(e); + } + await triggerTurnPage(); + if (targetImgList.length > 0) state.continueRun = true; +}); + // 测试案例 // https://www.177picyy.com/html/2023/03/5505307.html @@ -11476,23 +11434,32 @@ const isEleSelector = (ele, selector) => { await GM.deleteValue(window.location.hostname); return true; } - if (!isStored) main.toast(main.autoReadModeMessage(setOptions), { + if (!isStored) main.toast(() => (() => { + var _el$ = web.template(`