轻小说文库+

轻小说文库全方位体验改善,涵盖阅读、下载、书架、推荐、书评、账号、页面个性化等各种方面,你能想到的这里都有。没有?欢迎提出你的点子。

// ==UserScript==
// @name               轻小说文库+
// @namespace          https://gf.qytechs.cn/users/667968-pyudng
// @version            2.alpha.7.1
// @description        轻小说文库全方位体验改善,涵盖阅读、下载、书架、推荐、书评、账号、页面个性化等各种方面,你能想到的这里都有。没有?欢迎提出你的点子。
// @author             PY-DNG
// @license            GPL-3.0-or-later
// @homepageURL        https://gf.qytechs.cn/scripts/539514
// @supportURL         https://gf.qytechs.cn/scripts/539514/feedback
// @match              http*://*.wenku8.com/*
// @match              http*://*.wenku8.net/*
// @match              http*://*.wenku8.cc/*
// @require            data:application/javascript,window.setImmediate%20%3D%20window.setImmediate%20%7C%7C%20((f%2C%20...args)%20%3D%3E%20window.setTimeout(()%20%3D%3E%20f(args)%2C%200))%3B
// @require            https://update.gf.qytechs.cn/scripts/456034/1606773/Basic%20Functions%20%28For%20userscripts%29.js
// @require            https://update.gf.qytechs.cn/scripts/471280/1247074/URL%20Encoder.js
// @require            https://fastly.jsdelivr.net/npm/[email protected]/Sortable.min.js
// @require            https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js
// @require            https://fastly.jsdelivr.net/npm/[email protected]/ejs.min.js
// @require            https://fastly.jsdelivr.net/npm/[email protected]/dist/jepub.min.js
// @require            https://fastly.jsdelivr.net/npm/[email protected]/dist/confetti.browser.min.js
// @resource           vue-js      https://unpkg.com/[email protected]/dist/vue.global.prod.js
// @resource           quasar-icon https://fonts.font.im/css?family=Roboto:100,300,400,500,700,900|Material+Icons
// @resource           quasar-css  https://fastly.jsdelivr.net/npm/[email protected]/dist/quasar.prod.css
// @resource           quasar-js   https://fastly.jsdelivr.net/npm/[email protected]/dist/quasar.umd.prod.js
// @resource           vue-js-bak  https://fastly.jsdelivr.net/npm/[email protected]/dist/vue.global.min.js
// @resource           quasar-icon-bak https://google-fonts.mirrors.sjtug.sjtu.edu.cn/css?family=Roboto:100,300,400,500,700,900|Material+Icons
// @resource           quasar-css-bak  https://unpkg.com/[email protected]/dist/quasar.prod.css
// @resource           quasar-js-bak   https://unpkg.com/[email protected]/dist/quasar.umd.prod.js
// @connect            wenku8.com
// @connect            wenku8.net
// @connect            wenku8.cc
// @connect            777743.xyz
// @icon               https://www.wenku8.cc/favicon.ico
// @grant              GM_getResourceText
// @grant              GM_registerMenuCommand
// @grant              GM_setValue
// @grant              GM_getValue
// @grant              GM_addValueChangeListener
// @grant              GM_removeValueChangeListener
// @grant              GM_log
// @grant              GM_addElement
// @grant              GM_xmlhttpRequest
// @grant              GM_setClipboard
// ==/UserScript==

const { Component } = require('react');
const Settings = require('settings');

/* eslint-disable no-multi-spaces */
/* eslint-disable no-return-assign */

/* global LogLevel DoLog Err Assert $ $All $CrE $AEL $$CrE addStyle detectDom destroyEvent copyProp copyProps parseArgs escJsStr replaceText getUrlArgv dl_browser dl_GM AsyncManager queueTask FunctionLoader loadFuncs require isLoaded default_pool */
/* global $URL, Vue, Quasar, Sortable, confetti, JSZip, jEpub */

(function __MAIN__() {
    'use strict';

    const CONST = {
        // UI用文本常量
        TextAllLang: {
            DEFAULT: 'zh-CN',
            'zh-CN': {
                ExportDebugInfo: '导出调试信息',
                EnableScriptDebugging: '开启调试',
                DisableScriptDebugging: '关闭调试',
                Announcements: {
                    Running: `${GM_info.script.name} v${GM_info.script.version} 正在运行`
                },
                Unlocker: {
                    FetchingContent: `[${GM_info.script.name}] 正在获取章节内容...`,
                    ConstructingPage: `[${GM_info.script.name}] 正在构建页面...`,
                    FetchingDownloadInfo: `[${GM_info.script.name}] 正在获取下载信息...`,
                },
                SidePanel: {
                    PanelShowHide: '显示/隐藏',
                    GotoTop: '回到顶部',
                    GotoBottom: '跳至底部',
                    Refresh: '刷新页面'
                },
                Settings: {
                    SideButtonLabel: '设置',
                    DialogTitle: '设置',
                    NeedsReload: '修改后需要重新加载页面以生效',
                    OtherPageNeedsReload: '修改后其他页面需要重新加载以生效',
                    Component: {
                        SelectImage: '选择图片',
                        PleaseChoose: '请选择',
                    },
                    Tabs: {
                        ModuleSettings: '模块设置',
                        About: '关于',
                        AboutTab: '关于脚本',
                        FAQ: '常见问题',
                    },
                    About: {
                        Version: `版本: ${ GM_info.script.version }`,
                        Author: `作者: ${ GM_info.script.author }`,
                        Homepage: `主页: <a href="${ GM_info.script.homepageURL }">Greasyfork</a>`,
                        get TechnicalNote() { return `${ GM_info.script.name } 使用自行编写的模块加载系统驱动,由 ${ Object.keys(functions).length } 个模块共同组成;在UI方面,使用了<a href="cn.vuejs.org" target="_blank">Vue.js</a> 和 <a href="https://quasar.dev/" target="_blank">Quasar</a> 框架,并以 <a href="https://fonts.google.com/icons" target="_blank">Material Symbols & Icons</a> 作为图标库。`; },
                        FAQ: [{
                            Q: '为什么模块的设置时常消失?',
                            A: '模块只会在需要它的功能的页面运行,而在其他页面上由于模块不会运行,其设置项也不会在这些不运行的页面上存在。比如,「书评增强」模块的设置只会在书评页面出现。',
                        }],
                    },
                },
                Styling: {
                    Settings: {
                        Title: '页面主题',
                        Enabled: '启用主题功能',
                        EnabledCaption: '未启用时,使用原版文库界面',
                    },
                },
                Darkmode: {
                    Switch2Dark: '切换到深色模式',
                    Switch2Light: '切换到浅色模式',
                    FollowEnabledTip: '您已开启深色模式跟随系统,此时手动切换深色模式无作用',
                    FollowEnabledTipCaption: '您可到设置中关闭深色模式跟随系统,即可手动切换深色模式',
                    Settings: {
                        Label: '深色模式',
                        Enbaled: '启用深色模式',
                        EnabledCaption: '此项亦可在右下角侧边栏按钮中快速切换',
                        FollowSystem: '深色模式跟随系统',
                        FollowSystemCaption: '此项启用后优先级高于上面的手动开关',
                        SideButton: '侧边栏快捷切换按钮',
                        SideButtonCaption: '用于手动控制深色模式开关',
                    },
                },
                Review: {
                    FloorManager: {
                        UpdatingFloors: '正在更新楼层...',
                        FloorUpdated: '楼层已更新',
                        FloorUpdatedCaption: '发现 {Updated} 条新内容'
                    },
                    Cite: {
                        Cite: '引用'
                    },
                    UBBEditor: {
                        InsertImage: {
                            InputUrl: '请输入图片链接:',
                            Title: '插入图片',
                            Ok: '完成',
                            Cancel: '取消',
                            UrlFormatTip: '图片链接应该以 "http://" 或 "https://" 开头,以".jpg" 或 ".png" 等图片文件扩展名结尾',
                        },
                        InsertUrl: {
                            InputUrl: '请输入链接:',
                            Title: '插入链接',
                            Ok: '完成',
                            Cancel: '取消',
                            UrlFormatTip: '链接应该以 "http://" 或 "https://" 开头',
                        },
                    },
                    ReplyInPage: {
                        NoEmptyContent: '已成功发送空气',
                        NoEmptyContentCaption: '不可发送空白内容',
                        SendingReply: '正在发送评论...',
                        ReplySent: '已提交评论',
                        SentStatusDetails: '查看详情',
                        DetailsOk: '已阅',
                    },
                    Settings: {
                        Label: '书评吐嘈增强',
                        NoContent: '引用时仅引用楼号',
                        NoContentCaption: '[url=yidxxxxx]#1[/url]',
                        Pangu: '引用隔离',
                        PanguCaption: '保持引用部分和周围文字之间有且仅有一个空格',
                        Select: '引用后选中',
                        SelectCaption: '引用后,选中插入到输入框的、引用的文字部分',
                        FloorJump: '页面内跳转(楼层)',
                        FloorJumpCaption: '点击书评中到某一楼层的链接时,若链接楼层就在本页内,直接跳转至楼层,不再重新加载页面',
                        PageJump: '页面内跳转(页码)',
                        PageJumpCaption: '点击右下角切换评论页数时,直接在页面内更新到该页,不再重新加载页面',
                        ReplyInPage: '页面内发送评论',
                        ReplyInPageCaption: '发送评论后页面内更新,不再刷新页面',
                        EditInPage: '页面内编辑评论',
                        EditInPageCaption: '页面内弹窗编辑,不再打开新标签页',
                        AutoRefresh: '楼层自动刷新',
                        get AutoRefreshCaption() { return `每隔${ CONST.Internal.ReviewAutoRefreshInterval / 1000 }s自动刷新页面内评论,并高亮显示新的楼层和被修改过的楼层`; },
                        RefreshToLast: '刷新到最后一页',
                        RefreshToLastCaption: '楼层自动刷新时,总是刷新到书评的最后一页,而不是当前所在页码',
                    },
                },
                UserRemark: {
                    RemarkUser: '用户备注',
                    RemarkDisplay: '用户备注: {Remark}',
                    RemarkNotSet: '未设置用户备注',
                    Prompt: {
                        Title: '为用户设置备注',
                        Message: '您要为用户 {Name} 设置的备注为:',
                        Ok: '保存',
                        Cancel: '取消',
                        Saved: '已保存'
                    },
                    Settings: {
                        Label: '用户备注',
                        Enabled: '启用用户备注功能',
                        EnabledCaption: '若不启用,则不会在页面中显示相关UI',
                    }
                },
                UserReview: {
                    CheckUserReviews: '用户书评',
                },
                MetaCopy: {
                    CopyButton: '[复制]',
                    Copied: '已复制',
                },
                Bookcase: {
                    Collector: {
                        FetchingBookcases: '正在调阅书架...',
                        ArrangingBookcases: '正在整理书架...',
                        UpdatingBookcase: '正在更新书架...',
                        SubmitingChange: '正在提交更改...',
                        RefreshBookcase: '刷新书架内容',
                        Refreshed: '书架已刷新',
                        Removed: '已移出书架',
                        ActionFinished: '已{ActionName}',
                        NoBooksSelected: '请先选择要操作的书目!',
                        Dialog: {
                            ConfirmRemove: {
                                Message: '确实要将 {Name} 移出书架么?',
                                Title: '移出书籍',
                                ok: '是的',
                                cancel: '还是算了'
                            },
                        },
                    },
                    Naming: {
                        DefaultName: '第{ClassID}组书架',
                        Rename: '重命名书架',
                        MoveTo: '移到{Name}',
                        Dialog: {
                            PromptNewName: {
                                Message: '请给 {OldName} 取个新名字吧:',
                                Title: '重命名书架',
                                Ok: '保存',
                                Cancel: '取消',
                            }
                        },
                    },
                    AddpageJump: {
                        GotoBookcase: '前往书架',
                    },
                },
                ReadLater: {
                    Add: '添加到稍后再读',
                    Added: '添加成功',
                    AddSuccess: '稍后再读 {Name}',
                    AddDuplicate: '{Name} 已经在稍后再读中了,要不要现在就读一读呢?',
                    Title: '稍后再读(拖动可排序)',
                    EmptyListPlaceholder: '添加到稍后再读的书籍会显示在这里',
                },
                BlockFolding: {
                    Fold: '折叠',
                    UnFold: '展开',
                },
                Downloader: {
                    SideButton: '下载器',
                    Title: '文库下载器',
                    Notes: `<p>本书轻小说文库链接:<a href="{URL}">{URL}</a><br>Epub电子书由<a href="${GM_info.script.homepageURL}">${GM_info.script.name}</a>合成。</p><p>本资源仅供试读,如喜爱本书,请购买正版。</p>`,
                    Options: {
                        Format: {
                            Title: '格式',
                            txt: 'TXT 文档',
                            epub: 'Epub 电子书',
                            image: '仅插图',
                        },
                        Encoding: {
                            Title: '编码',
                            Caption: '仅对txt文档生效',
                            gbk: 'GBK',
                            utf8: 'UTF-8'
                        },
                        Filename: '文件名',
                    },
                    UI: {
                        DownloadButton: '下载',
                        Author: '作者: ',
                        LastUpdate: '最后更新: ',
                        Tags: '作品Tags: ',
                        BookStatus: '状态: ',
                        Intro: '内容简介: ',
                        ContentSelectorTitle: '请选择下载的章节: ',
                        NoContentSelected: '已勾选的下载章节为空',
                        Progress: {
                            Global: '当前步骤 ({CurStep}/{Total}): {Name}',
                            Sub: '当前进度: {CurStep}/{Total}',
                            Ready: '下载器准备就绪',
                            Loading: '正在加载书籍信息...',
                        }
                    },
                    Steps: {
                        txt: {
                            NovelContent: '下载章节内容',
                            EncodeText: '编码文本',
                            GenerateZIP: '合成ZIP文件',
                        },
                        image: {
                            NovelContent: '下载章节内容',
                            DownloadImage: '下载图片',
                            GenerateZIP: '合成ZIP文件',
                        },
                        epub: {
                            NovelContent: '加载章节内容和图片',
                            GenerateEpub: '合成Epub文件',
                        },
                    },
                },
                Autovote: {
                    Add: '添加到自动推书',
                    Added: '添加成功',
                    AddSuccess: '将 {Name} 添加到了每日自动推书中',
                    AddDuplicate: '其实 {Name} 已经在自动推书列表中了',
                    Configure: '自动推书配置',
                    VoteStart: '开始自动推书...',
                    VoteEnd: '自动推书完成',
                    VoteDetail: '详情',
                    UI: {
                        Title: '自动推书配置',
                        Votes: '每日推荐票数',
                        TimeAdded: '添加时间: ',
                        VotedCount: '累计自动推书: ',
                        ConfirmRemove: {
                            Title: '从自动推书中移除书籍',
                            Message: '确实要将 {Name} 从自动推书中移除吗?移除后,以前的推书记录也将一同被删除。',
                            Ok: '确定',
                            Cancel: '还是算了',
                        },
                    },
                    Settings: {
                        Title: '自动推书',
                        Configuration: '自动推书配置',
                        Configure: '编辑',
                        Enabled: '启用自动推书',
                        EnabledCaption: '关闭后将不再每日自动推书、不显示相关UI,但推书配置和记录仍将保留',
                    }
                },
                ReviewCollection: {
                    CollectionTitle: '书评收藏',
                    Add: '收藏书评',
                    Remove: '取消收藏书评',
                    Added: '已添加到书评收藏',
                    Removed: '已取消收藏此书评',
                    Settings: {
                        Title: '书评收藏',
                        Enabled: '启用书评收藏',
                        EnabledCaption: '关闭后,将不再显示相关UI,但收藏的书评仍将保留',
                        ListPosition: '首页收藏列表放置位置',
                        ListPositionCaption: '在哪里显示收藏的书评',
                        ListPositionLeft: '左侧',
                        ListPositionRight: '右侧',
                        OpenLastPage: '打开书评最后一页',
                        OpenLastPageCaption: '从首页的书评收藏列表中打开书评时,直接跳转到书评最后一页',
                    },
                },
                Background: {
                    Settings: {
                        Title: '自定义背景',
                        Enabled: '启用自定义背景',
                        EnabledCaption: '启用后,将改变页面背景,覆盖文库自带白色背景和深色模式的黑色背景',
                        Type: '背景类型',
                        Types: [{
                            label: '本地图片',
                            value: 'local'
                        }, {
                            label: '网络图片',
                            value: 'url'
                        }, {
                            label: '纯色',
                            value: 'color'
                        }],
                        ImageUrl: '网络图片链接',
                        Image: '本地图片',
                        ImageFit: '图片缩放与裁剪',
                        ImageFitOptions: [{
                            label: '放大图片到宽或者高的其中任何一条边能够填满屏幕,剩余不能填满屏幕的部分将用底色填充(底色取决于浏览器),不改变图片宽高比例',
                            brief: '包含在页面内',
                            value: 'contain',
                        }, {
                            label: '放大图片到能完全覆盖整个页面的最小尺寸,溢出屏幕的部分将被裁剪,不改变图片宽高比例',
                            brief: '覆盖整个页面',
                            value: 'cover',
                        }, {
                            label: '缩放图片到完全适合网页页面大小,必要时改变图片的宽高比(允许将图片压扁或拉长)',
                            brief: '缩放到页面尺寸',
                            value: 'fill',
                        }, {
                            label: '保持图片自身原始大小与宽高比,无论是否适合页面',
                            brief: '保持原始尺寸',
                            value: 'none',
                        }],
                        MaskOpacity: '图片遮罩层不透明度',
                        Color: '颜色',
                    },
                },
                OpenLastPage: {
                    OpenLastPageButton: '[打开尾页]',
                },
            }
        },
        /**
         * @returns {typeof CONST.TextAllLang['zh-CN']>}
         */
        get Text() {
            const i18n = Object.keys(CONST.TextAllLang).includes(navigator.language) ? navigator.language : CONST.TextAllLang.DEFAULT;
            return CONST.TextAllLang[i18n];
        },

        // 文库内部所用常量
        Wenku8: {
            /** @typedef {typeof CONST.Wenku8.LanguageCode} LanguageCode */
            LanguageCode: {
                Simplified: 0,
                Traditional: 1
            }
        },

        // 脚本内部配置
        Internal: {
            // 用于各类解锁时,取得DOM等模板所用的未锁定书籍的aid
            UnlockTemplateAID: 1,
            // 最长存储日志页面数量
            DefaultLogMaxPage: 10,
            // 最长存储日志条数
            DefaultLogMaxLength: 30,
            // 最长存储错误数量
            DefaultErrorMaxLength: 10,
            // 板块折叠:消失的板块所对应的折叠记录在连续观察到几次从文档中消失后清除
            RemoveBlockFoldingCount: 10,
            // 自动推书:其他标签页存活检测 最长更新时间间隔
            AutovoteActiveTimeout: 10 * 1000,
            // 书评楼层自动刷新间隔
            ReviewAutoRefreshInterval: 20 * 1000,
            // 默认书评收藏
            BuiltinReviewCollection: [{
                rid: 298520,
                name: '[轻小说文库+] 脚本反馈站'
            }, {
                rid: 228884,
                name: '文库导航姬',
            }, {
                rid: 282295,
                name: '文库导航 / 中转站',
            }],
            // 书评图片重试缩放间隔
            ReviewResizeInterval: 500,
            // 自适应高度编辑器的最大和最小高度
            EditorHeight: {
                Min: 150,
                Max: 500,
            },
        },
    };

    const functions = {
        utils: {
            /** @typedef {Awaited<ReturnType<typeof functions.utils.func>>} utils */
            func() {
                /** @type {typeof window} */
                const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

                // 当日志模块加载完毕时,记录日志
                require('logger', true).then(() => {
                    /** @type {logger} */
                    const logger = require('logger');
                    logger.log(
                        logger.LogLevel.Info,
                        `${GM_info.script.name} v${GM_info.script.version} starting`
                    );
                });

                // 当基础框架功能集加载完毕时,记录日志
                Promise.all(
                    ['utils', 'debugging', 'logger', 'dependencies'].map(id => require(id, true))
                ).then(() => {
                    /** @type {logger} */
                    const logger = require('logger');
                    logger.log(
                        logger.LogLevel.Message,
                        `${GM_info.script.name} v${GM_info.script.version} running`
                    );
                });

                // 当全部可加载功能加载完毕时,记录日志
                $AEL(default_pool, 'all_load', e => {
                    /** @type {logger} */
                    const logger = require('logger');
                    logger.log(
                        logger.LogLevel.Info,
                        `[${GM_info.script.name}] all functions loaded`
                    );
                });

                /**
                 * 获取当前页面的语言:繁体中文/简体中文
                 * @returns {number} 文库语言代码,参考 {@link LanguageCode}
                 */
                function getLanguage() {
                    if ('currentEncoding' in win) {
                        return {
                            1: CONST.Wenku8.LanguageCode.Traditional,
                            2: CONST.Wenku8.LanguageCode.Simplified,
                        }[win.currentEncoding];
                    } else {
                        return {
                            'GBK': CONST.Wenku8.LanguageCode.Simplified,
                            'Big5': CONST.Wenku8.LanguageCode.Traditional,
                        }[document.characterSet];
                    }
                }

                /**
                 * 向输入框的当前光标位置中插入文本
                 * @param {HTMLTextAreaElement | HTMLInputElement} elm - 输入框元素
                 * @param {string} text - 待插入的文本
                 * @param {string} [pangu=false] - 是否保证插入部分和周围文本之间至少有一个空格
                 * @param {string} [select=false] - 是否选中插入部分内容
                 */
                function insertText(elm, text, pangu=false, select=false) {
                    const orig_start = elm.selectionStart;
                    let before_selection = elm.value.slice(0, elm.selectionStart);
                    let after_selection = elm.value.slice(elm.selectionEnd);
                    if (pangu) {
                        // 当前面有内容时,将前面内容的结尾空格替换为1个
                        if (before_selection && !before_selection.endsWith('\n')) {
                            before_selection = before_selection.replace(/ +$/g, '');
                            text = ' ' + text;
                        }
                        // 无论后面是否有内容,均将后面内容的开头空格替换为1个
                        after_selection = after_selection.replace(/^ +/g, '');
                        text = text + ' ';
                    }
                    elm.value = before_selection + text + after_selection;
                    const text_end = orig_start + text.length;
                    if (select) {
                        elm.setSelectionRange(orig_start, text_end, 'forward');
                    } else {
                        elm.setSelectionRange(text_end, text_end, 'none');
                    }
                }

                /**
                 * 新建一个FuncPool加载oFuncs,oFuncs以对象格式书写(而非标准的数组格式)  
                 * 返回 { promise, pool }, promise将会在加载完毕时resolve,pool为加载时创建的新FuncPool
                 * @param {Object} pool_funcs
                 * @param {Record<'GM_getValue' | 'GM_setValue' | 'GM_listValues' | 'GM_deleteValue', function>} [GM_funcs={}]
                 * @returns {{ promise: Promise, pool: InstanceType<typeof FunctionLoader.FuncPool> }}
                 */
                function loadFuncInNewPool(pool_funcs, GM_funcs={}) {
                    /**
                     * @param {InstanceType<typeof FunctionLoader.FuncPool>} pool
                     * @param {Object} oFuncs
                     */
                    async function loadWithErrorHandling(pool, oFuncs) {
                        /** @type {debugging} */
                        const debugging = await require('debugging', true);
                        debugging.catchPoolErrors(pool);
                        // 确保oFuncs一定在下个事件循环及以后加载
                        // 防止pool还没return就同步加载完成了
                        // 导致外部调用方运行时无法获取pool报错
                        return new Promise(resolve => 
                            setTimeout( () => pool.load(oFuncs).then(() => resolve()) )
                        );
                    }

                    const pool = new FunctionLoader.FuncPool({ GM_funcs });
                    const oFuncs = Object.entries(pool_funcs).reduce((arr, [id, oFunc]) => {
                        oFunc.id = id;
                        arr.push(oFunc);
                        return arr;
                    }, []);
                    const promise = loadWithErrorHandling(pool, oFuncs);

                    return { promise, pool };
                }

                /**
                 * 创建存储的默认值层,定义默认值后,读取对应键时若无已设置值则返回默认值
                 * 返回带默认值的 GM_getValue 函数
                 * @param {Record<string, any>} default_values - 存储默认值对象
                 * @param {typeof GM_getValue} orig_get - GM_getValue函数
                 */
                function defaultedGet(default_values, orig_get) {
                    const Empty = Symbol('defaultedGet: no value written');
                    default_values = window.structuredClone(default_values);
                    return GM_getValue;

                    /**
                     * 带默认值层的GM_getValue读存储函数,会在存储中未写入值时
                     * @param {*} key - 需读取的存储的键
                     * @param {*} defaultValue - 本次读取时使用的默认值,本次读取中优先级高于之前定义的默认值对象
                     */
                    function GM_getValue(key, defaultValue = Empty) {
                        // 之前设置的默认值对象中,此键的默认值
                        const global_default = default_values.hasOwnProperty(key) ? default_values[key] : null;
                        // 本次调用中,显式设置的默认值
                        const current_default = defaultValue;
                        // 最终使用的默认值
                        const default_val = current_default !== Empty ? current_default : global_default;
                        // 读取值并返回
                        const val = orig_get(key, default_val);
                        return val;
                    }
                }

                /**
                 * 从诸如"普通会员","禁言會員"这样的文字中确定用户组类型
                 * @param {string} text 
                 * @returns { 'user' | 'admin' | 'banned' | 'limited' } 
                 */
                function getUserType(text) {
                    return ({
                        // 简体,繁体(推荐),繁体(备用)
                        '普通会员': 'user',
                        '普通會員': 'user',
                        '喱通会员': 'user',
                        '系统管理员': 'admin',
                        '系統管理員': 'admin',
                        '系统嗷理员': 'admin',
                        '禁言会员': 'banned',
                        '禁言會員': 'banned',
                        '禁言會員': 'banned',
                        '受限会员': 'limited', 
                        '受限會員': 'limited',
                        '受限會員': 'limited',
                    }) [text];
                }

                /**
                 * 从诸如"新手上路","高級會員"这样的文字中确定用户等级
                 * @param {string} text 
                 * @returns { 'newbie' | 'normal' | 'intermediate' | 'advanced' | 'golden' | 'elder' }
                 */
                function getUserLevel(text) {
                    return ({
                        // 简体,繁体(推荐),繁体(备用)
                        '新手上路': 'newbie',
                        '新手上路': 'newbie',
                        '新手上路': 'newbie',
                        '普通会员': 'normal',
                        '普通會員': 'normal',
                        '普通會員': 'normal',
                        '中级会员': 'intermediate',
                        '中級會員': 'intermediate',
                        '中級會員': 'intermediate',
                        '高级会员': 'advanced',
                        '高級會員': 'advanced',
                        '坨级会员': 'advanced',
                        '金牌会员': 'golden',
                        '金牌會員': 'golden',
                        '金牌會員': 'golden',
                        '本站元老': 'elder',
                        '本站元老': 'elder',
                        '本站元老': 'elder',
                    }) [text];
                }

                /**
                 * 将给定的方法包装为排队执行的版本,返回的新方法将在队列中执行,以限制最大并行执行数并添加执行间隔
                 * @template {function} F
                 * @param {F} func 
                 * @param {Object} [options] 
                 * @param {Object} [options.queue_id] 并行队列id,相同的id将在同一队列内运行;省略时生成随机id
                 * @param {Object} [options.max=5] 最大并行执行数
                 * @param {Object} [options.sleep=0] 每两次执行间的等待间隔时长
                 * @returns {F}
                 */
                function toQueued(func, { queue_id=null, max = 5, sleep = 0 } = {}) {
                    queue_id === null && (queue_id = 'toQueued-' + randstr());
                    queueTask[queue_id] = { max, sleep };

                    return function queued(...args) {
                        return queueTask(() => func(...args), queue_id);
                    };
                }

                /**
                 * 以当前网页的编码将form元素内容或者FormData对象序列化为post表单字符串
                 * @param {HTMLFormElement | FormData} form
                 * @returns {string}
                 */
                function serializeFormData(form) {
                    /** @type {FormData} */
                    const formdata = Object.prototype.toString.call(form) === '[object FormData]' ?
                        form : new FormData(form);
                    return [...formdata.entries()].map(([key, val]) =>
                        `${ encodeURIComponent(key) }=${ encodeURIComponent(val) }`).join('&');

                    /**
                     * 和标准同名方法一致,但是根据当前文档的编码进行
                     * @type {typeof window.encodeURIComponent}
                     */
                    function encodeURIComponent(text) {
                        return Array.from(text).map(char => 
                            /[A-Za-z0-9\-_\.!~\*'\(\)]/.test(char) ?
                                char : $URL.encode(char)
                        ).join('');
                    }
                }

                /**
                 * 在给定字符串头部填0使字符串达到给定长度
                 * @param {string} text 
                 * @param {number} len 
                 * @returns {String}
                 */
                function zfill(text, len) {
                    return '0'.repeat(Math.max(0, len - text.length)) + text;
                }

                /**
                 * Encode text into html text format
                 * @param {string} text
                 * @returns {string}
                 */
				function htmlEncode(text) {
					const span = $CrE('div');
					span.innerText = text;
					return span.innerHTML;
				}

                /**
                 * 随机字符串
                 * @param {number} length - 随机字符串长度 
                 * @param {boolean} cases - 是否包含大写字母
                 * @param {string[]} aviod - 需要排除的字符串,在这里的字符串不会作为随机结果返回;通常用于防止随机出重复字符串
                 * @returns {string}
                 */
				function randstr(length=16, cases=true, aviod=[]) {
					const all = 'abcdefghijklmnopqrstuvwxyz0123456789' + (cases ? 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' : '');
					while (true) {
						let str = '';
						for (let i = 0; i < length; i++) {
							str += all.charAt(randint(0, all.length-1));
						}
						if (!aviod.includes(str)) {return str;};
					}
				}

                /**
                 * 随机整数
                 * @param {number} min - 最小值(包含)
                 * @param {number} max - 最大值(包含)
                 * @returns {number}
                 */
				function randint(min, max) {
					return Math.floor(Math.random() * (max - min + 1)) + min;
				}

                /**
                 * 调用GM_xmlhttpRequest并按照当前页面字符编码解析返回的文本
                 * 传入的detail对象中的onload将会被替换为文本字符解码器,因此自定义的onload将不会被执行
                 * @param {*} detail 
                 * @returns {Promise<string>}
                 */
                function requestText(detail) {
                    const { promise, resolve } = Promise.withResolvers();
                    detail.responseType = 'arraybuffer';
                    detail.onload = response => {
                        const buffer = (response.response);
                        const decoder = new TextDecoder(document.characterSet);
                        const text = decoder.decode(buffer);
                        resolve(text);
                    };
                    GM_xmlhttpRequest(detail);
                    return promise;
                }

                /**
                 * 调用GM_xmlhttpRequest并按照当前页面字符编码解析返回的文本为文档
                 * 传入的detail对象中的onload将会被替换为文本字符解码器,因此自定义的onload将不会被执行
                 * @param {*} detail 
                 * @returns {Promise<Document>}
                 */
                async function requestDocument(detail) {
                    const source = await requestText(detail);
                    const doc = new DOMParser().parseFromString(source, 'text/html');
                    return doc;
                }

                const requestBlob = toQueued(_requestBlob, {
                    max: 5,
                    sleep: 0,
                    queue_id: 'blob_request'
                });

                /**
                 * 获取指定url的文件为blob
                 * @param {string} url
                 * @returns {Promise<Blob>}
                 */
                function _requestBlob(url) {
                    const { promise, resolve } = Promise.withResolvers();
                    GM_xmlhttpRequest({
                        method: 'GET',
                        url,
                        responseType: 'blob',
                        onload(response) {
                            resolve(response.response);
                        }
                    });
                    return promise;
                }

                /**
                 * 获取OPFS中指定模块的目录
                 * 注意:这里并不使用OPFS的全部命名空间,而是将全部脚本所用存储存放到OPFS:WenkuPlus目录中,为日后网站官方开发预留主要命名空间
                 * @param {string} id - 指定模块oFunc.id
                 */
                async function getModuleDir(id) {
                    const root = await navigator.storage.getDirectory();
                    const script_root = await root.getDirectoryHandle('WenkuPlus', { create: true });
                    const dir = await script_root.getDirectoryHandle(id, { create: true });
                    return dir;
                }

                /**
                 * @param {HTMLElement} elm
                 * @param {string} content
                 */
                function setTip(elm, content) {
                    elm.setAttribute('tiptitle', content);
                    $AEL(elm, 'mouseover', e => win.tipshow(elm.getAttribute('tiptitle')));
                    $AEL(elm, 'mouseout', e => win.tiphide('tiptitle'));
                }

                /**
                 * Async task progress manager \
                 * when awaiting async tasks, replace `await task` with `await manager.progress(task)` \
                 * suppoerts sub-tasks, just `manager.sub(sub_steps, sub_callback)`
                 */
                class ProgressManager extends EventTarget {
                    /** @type {*} */
                    info;
                    /** @type {number} */
                    steps;
                    /** @type {number} */
                    finished;
                    /** @type {'none' | 'sub' | 'self'} */
                    error;
                    /** @type {ProgressManager[]} */
                    #children;
                    /** @type {ProgressManager} */
                    #parent;

                    /**
                     * This callback is called each time a promise resolves
                     * @callback progressCallback
                     * @param {number} resolved_count
                     * @param {number} total_count
                     * @param {ProgressManager} manager
                     */

                    /**
                     * @param {number} [steps=0] - total steps count of the task
                     * @param {progressCallback} [callback] - callback each time progress updates
                     * @param {*} [info] - attach any data about this manager if need
                     */
                    constructor(steps=0, info=undefined) {
                        super();

                        this.steps = steps;
                        this.info = info;
                        this.finished = 0;
                        this.error = 'none';

                        this.#children = [];
                        this.#broadcast('progress');
                    }

                    add() { this.steps++; }

                    /**
                     * @template {Promise | null} task
                     * @param {task} [promise] - task to await, null is acceptable if no task to await
                     * @param {number} [finished] - set this.finished to this value, adds 1 to this.finished if omitted
                     * @param {boolean} [accept_reject=true] - whether to treat rejected promise as resolved; if true, callback will get error object in arguments; if not, progress function itself rejects
                     * @returns {Promise<Awaited<task>>}
                     */
                    async progress(promise, finished, accept_reject = true) {
                        let val;
                        try {
                            val = await Promise.resolve(promise);
                        } catch(err) {
                            this.newError('self', false);
                            if (!accept_reject) {
                                throw err;
                            }
                        }
                        try {
                            this.finished = (typeof finished === 'number' && finished >= 0) ? finished : this.finished + 1;
                            this.#broadcast('progress');
                            //this.finished === this.steps && this.#parent && this.#parent.progress();
                        } finally {
                            return val;
                        }
                    }

                    /**
                     * New error occured in manager's scope, update error status
                     * @param {'none' | 'sub' | 'self'} [error='self']
                     * @param {boolean} [callCallback=true]
                     */
                    newError(error = 'self', callCallback = true) {
                        const error_level = ['none', 'sub', 'self'];
                        if (error_level.indexOf(error) <= error_level.indexOf(this.error)) { return; }

                        this.error = error;
                        this.#parent && this.#parent.newError('sub');
                        callCallback && this.#broadcast('error');
                    }

                    /**
                     * Creates a new ProgressManager as a sub-progress of this
                     * @param {number} [steps=0] - total steps count of the task
                     * @param {*} [info] - attach any data about the sub-manager if need
                     */
                    sub(steps, info) {
                        const manager = new ProgressManager(steps ?? 0, info);
                        manager.#parent = this;
                        this.#children.push(manager);
                        this.#broadcast('sub');
                        return manager;
                    }

                    /**
                     * reset this to an empty manager
                     */
                    reset() {
                        this.steps = 0;
                        this.finished = 0;
                        this.#parent = null;
                        this.#children = [];
                        this.#broadcast('reset');
                    }

                    #broadcast(evt_name) {
                        //this.callback(this.finished, this.steps, this);
                        this.dispatchEvent(new CustomEvent(evt_name, {
                            detail: {
                                type: evt_name,
                                manager: this
                            }
                        }));
                    }

                    get children() {
                        return [...this.#children];
                    }

                    get parent() {
                        return this.#parent;
                    }
                }

                return {
                    // 窗口
                    window: win,
                    // 文库相关
                    getLanguage, getUserType, getUserLevel,
                    // 文库页面功能,
                    setTip,
                    // 功能相关
                    insertText, loadFuncInNewPool, defaultedGet, requestText, requestDocument, requestBlob, getModuleDir,
                    // 管理器
                    ProgressManager,
                    // 算法相关
                    toQueued, serializeFormData, zfill, htmlEncode, randstr, randint,
                };
            }
        },
        debugging: {
            desc: 'script error handler and debugging tool',
            dependencies: 'logger',
            params: ['GM_setValue', 'GM_getValue'],
            /** @typedef {Awaited<ReturnType<typeof functions.debugging.func>>} debugging */
            async func(GM_setValue, GM_getValue) {
                /**
                 * @typedef {Object} debugging_storage
                 * @property {ErrorObject[]} errors - 错误存档
                 * @property {number} max_save - 最大错误存档长度
                 * @property {number} script_debug - 脚本是否处于调试状态
                 */
                /** @type {logger} */
                const logger = require('logger');

                // Automatically record default funcpool load errors
                catchPoolErrors(default_pool);

                // 调试模式接口
                GM_getValue('script_debug', false) && enableScriptDebugging();

                // Menu commands
                // Delay 1s to put menu item into last place in menus list
                setTimeout(() => {
                    GM_registerMenuCommand(CONST.Text.ExportDebugInfo, exportDebugInfo);
                    toggleScriptDebug('script_debug', false);

                    /**
                     * 
                     * @param {boolean} toggle - 是否实际改变脚本调试状态,如果为false,则仅更新/创建菜单项 
                     * @param {string | number} [menu_id] - 需要更新的现有菜单项的id,不提供则新建菜单项
                     * @returns 
                     */
                    function toggleScriptDebug(menu_id, toggle=true) {
                        const script_debug = toggle === GM_getValue('script_debug', false);
                        let label;
                        if (script_debug) {
                            // 已处于调试模式,关闭调试模式,提供开启按钮
                            toggle && disableScriptDebugging();
                            label = CONST.Text.EnableScriptDebugging;
                        } else {
                            // 未处于调试模式,开启调试模式,提供关闭按钮
                            toggle && enableScriptDebugging();
                            label = CONST.Text.DisableScriptDebugging;
                        }
                        const options = {};
                        GM_registerMenuCommand(label, () => toggleScriptDebug(menu_id, true), { id: menu_id });
                    }
                }, 1000);

                /**
                 * @typedef {Object} ErrorDetail
                 * @property {string} [key] - use key to avoid saving same error multiple times
                 * @property {string} type 
                 * @property {Error} error 
                 * @property {*} info 
                 */
                /**
                 * @typedef {Object} ErrorObject
                 * @property {string} [key]
                 * @property {string} type
                 * @property {*} info
                 * @property {string} message
                 * @property {string | undefined} stack
                 * @property {string} url
                 * @property {boolean} iframe
                 * @property {number} timestamp
                 */
                /**
                 * wrap error details into error object
                 * @param {ErrorDetail} detail
                 * @returns {ErrorObject}
                 */
                function wrapErrorData({type, error, info, key}) {
                    const data = {
                        type, info,
                        message: error.message,
                        stack: error.stack,
                        url: location.href,
                        iframe: window.top !== window,
                        timestamp: Date.now()
                    };
                    key && (data.key = key);
                    return data;
                }

                /**
                 * Save an error into storage
                 * @param {ErrorDetail} detail
                 * @returns {ErrorObject}
                 */
                function saveError({type, error, info, key}) {
                    const data = wrapErrorData({type, error, info, key});
                    const errors = GM_getValue('errors', []);
                    if (key && errors.some(error => error.key === key)) { return; }
                    errors.push(data);
                    const max_save = GM_getValue('max_save', CONST.Internal.DefaultErrorMaxLength);
                    while (errors.length > max_save) { errors.shift(); }
                    GM_setValue('errors', errors);
                    return data;
                }

                /** @typedef {InstanceType<typeof FunctionLoader.FuncPool>} FuncPool */
                /**
                 * Automatically catch and save all errors from FuncPool loaded oFuncs
                 * @param {FuncPool} pool 
                 */
                function catchPoolErrors(pool) {
                    pool.catch_errors = true;
                    pool.addEventListener('error', e => {
                        const { error, oFunc } = e.detail;
                        dealLoadError(error, oFunc);
                    });
                    pool.errors.forEach(({error, oFunc}) => dealLoadError(error, oFunc));

                    function dealLoadError(error, oFunc) {
                        saveError({
                            type: 'oFunc',
                            error,
                            info: { id: oFunc.id },
                            //key: `oFunc-${oFunc.id}`
                        });
                        if (GM_getValue('script_debug', false)) {
                            throw error;
                        } else {
                            logger.error(logger.LogLevel.Error, error);
                        }
                    };
                }

                /**
                 * @callback ErrorHandler
                 * @param {Error} err - the error object
                 * @param {function} func - function tried to run
                 * @param {any} thisArg - thisArg passed to the function
                 * @param {any[]} args - thisArg passed to the function
                 * @returns {boolean} whether to save this error
                 */
                /**
                 * Call given function with error handling
                 * @template {function} F
                 * @param {F} func 
                 * @param {any} [thisArg]
                 * @param {any[]} [args] 
                 * @param {ErrorHandler} [handler] - callback when error occurs
                 * @returns {ReturnType<F>}
                 */
                function callWithErrorHandling(func, thisArg=null, args=[], handler=null) {
                    try {
                        return func.apply(thisArg, args);
                    } catch (err) {
                        const save = typeof handler === 'function' ? handler(err, func, thisArg, args) : true;
                        save && saveError({
                            type: 'func-error',
                            error: err,
                            info: { func/*, thisArg, args*/ } // thisArg and args may contain circular structure
                        });
                        if (GM_getValue('script_debug', false)) {
                            throw err;
                        } else {
                            logger.error(logger.LogLevel.Error, err);
                        }
                    }
                }

                /**
                 * Export an error to user as a json file
                 * returns error object
                 * @param {ErrorDetail} detail
                 * @returns {ErrorObject}
                 */
                function exportError({type, error, info, key}) {
                    const data = wrapErrorData({type, error, info, key});
                    download_object(data, `${GM_info.script.name} Error.json`);
                    return data;
                }

                /**
                 * Export all saved errors to user as a json file
                 */
                function exportAllErrors() {
                    const errors = GM_getValue('errors', []);
                    download_object(errors, `${GM_info.script.name} All Errors.json`);
                }

                function exportDebugInfo() {
                    const errors = GM_getValue('errors', []);
                    const logs = logger.pages;
                    const debug_info = {
                        errors, logs,
                        ua: navigator.userAgent,
                        version: GM_info.script.version,
                        manager: GM_info.scriptHandler,
                        manager_version: GM_info.version,
                        timestamp: Date.now(),
                    };
                    download_object(debug_info, `${GM_info.script.name} Debug Info.json`);
                }

                /**
                 * download any jsonable data as file
                 * @param {*} data - any jsonable data
                 * @param {string} filename 
                 * @param {string} mimetype 
                 */
                function download_object(data, filename, mimetype='application/json') {
                    const json = JSON.stringify(data);
                    const url = URL.createObjectURL(new Blob([json], { type: mimetype }));
                    dl_browser(url, filename);
                    setTimeout(() => URL.revokeObjectURL(url));
                }

                function enableScriptDebugging() {
                    // Do not depend on utils (or any other dependencies) while debugging
                    GM_setValue('script_debug', true);
                    const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
                    win.w8p = {
                        // 脚本实现的接口
                        require, default_pool,
                        // 脚本@require的接口
                        $URL, confetti,
                    };
                    logger.log(logger.LogLevel.Message, `[${GM_info.script.name}]\nScript debugging enabled.\nDebugging interface injected as %cwindow.w8p%c.`, 'color: #6666CC;', '');
                }

                function disableScriptDebugging() {
                    GM_setValue('script_debug', false);
                    const win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
                    delete win.w8p;
                    logger.log(logger.LogLevel.Message, `[${GM_info.script.name}] Script debugging disabled.`);
                }

                return {
                    wrapErrorData, saveError, catchPoolErrors, callWithErrorHandling, exportError, exportAllErrors, exportDebugInfo, enableScriptDebugging,
                    /** @type {ErrorObject[]} */
                    get errors() { return GM_getValue('errors', []); },
                    /** @type {number} */
                    get max_save() { return GM_getValue('max_save', 10); },
                    set max_save(val) { GM_setValue('max_save', val); },
                    /** @type {boolean} */
                    get script_debug() { return GM_getValue('script_debug', false); },
                    set script_debug(val) { GM_setValue('script_debug', val); }
                };
            }
        },
        logger: {
            dependencies: 'utils',
            params: ['GM_setValue', 'GM_getValue'],
            /** @typedef {Awaited<ReturnType<typeof functions.logger.func>>} logger */
            func(GM_setValue, GM_getValue) {
                /**
                 * @typedef {Object} logger_storage
                 * @property {LogPage[]} pages
                 * @property {number} [loglevel] - 日志输出级别
                 * @property {number} [max_pages] - 最多存储页面数量
                 * @property {number} [max_logs] - 每个页面最多存储日志条数
                 */

                /** @type {utils} */
                const utils = require('utils');

                const csl = Object.assign({}, console);
                const LogLevel = {
                    // 仅作调试用途
                    Debug: 0,
                    // 详细运行日志
                    Info: 1,
                    // 运行日志中可能需要关注的部分
                    Warn: 2,
                    // 运行过程中的主要(简略)日志内容
                    Message: 2,
                    // 报错日志
                    Error: 3,
                };

                /**
                 * @typedef {Object} LogData
                 * @property {number} level - 日志级别,仅用于筛选是否在控制台输出,不会改变输出内容和格式
                 * @property {keyof typeof csl} funcname - 使用的log函数名,如'log','error'等
                 * @property {any} content
                 * @property {number} timestamp
                 * @property {string} url
                 * @property {boolean} iframe
                 */
                /**
                 * 代表一个页面上的全部日志
                 * @typedef {Object} LogPage
                 * @property {number} id - 页面id,用 performance.timeOrigin 表示
                 * @property {LogData[]} logs
                 * @property {string} url
                 * @property {number | null} parent - 若页面在iframe中,为父页面的id;若不在,则为null
                 */
                /**
                 * wrap content into standard log data format
                 * @param {number} level - 日志级别,仅用于筛选是否在控制台输出,不会改变输出内容和格式
                 * @param {keyof typeof csl} funcname - 使用的log函数名,如'log','error'等
                 * @param {*} content
                 * @returns {LogData} 
                 */
                function wrapLog(level, funcname, content) {
                    return {
                        level, funcname, content,
                        timestamp: Date.now(),
                        url: location.href,
                        iframe: utils.window.top !== utils.window
                    };
                }

                /**
                 * 获取当前页面的日志对象id
                 * @returns {number}
                 */
                function getCurPageID() {
                    return utils.window.performance.timeOrigin;
                }

                /**
                 * 获取当前页面的日志对象
                 * @returns {LogPage}
                 */
                function getCurPage() {
                    const id = getCurPageID();
                    return GM_getValue('pages').find(page => page.id === id);
                }

                /**
                 * 获取当前日志输出级别
                 * @returns {number}
                 */
                function getLoglevel() {
                    return GM_getValue('loglevel', LogLevel.Message);
                }

                /**
                 * 设置日志输出级别
                 * @param {number} level 
                 */
                function setLoglevel(level) {
                    GM_setValue('loglevel', level);
                }

                /** @typedef {number | keyof typeof LogLevel} LogLevelArg */
                /**
                 * 输出、记录日志,和console.log基本相同
                 * 新增参数:第一个参数level日志级别,第二个参数使用的log函数
                 * @param {LogLevelArg} level - 日志级别,仅用于筛选是否在控制台输出,不会改变输出内容和格式;可以是数字或者其名称(不区分大小写);参考 {@link LogLevel}
                 * @param {keyof typeof csl} funcname - 使用的log函数名,如'log','error'等
                 * @param {Parameters<typeof console.log>} content - 日志内容
                 * @returns {LogData} 当前页面的日志对象
                 */
                function _log(level, funcname, ...content) {
                    if (typeof level === 'string') {
                        const standard_levelname = level.at(0).toUpperCase() + level.slice(1).toLowerCase();
                        if (LogLevel.hasOwnProperty(standard_levelname)) {
                            level = LogLevel[standard_levelname] ?? level;
                        } else {
                            Err(`日志级别应为数字或LogLevel中声明的字符串关键字,而不是 ${ JSON.stringify(level) }`, TypeError);
                        }
                    }

                    // 根据level输出到控制台
                    level >= getLoglevel() && csl[funcname](...content);

                    // 获取页面日志对象
                    const pages = GM_getValue('pages', []);
                    /** @type {LogPage} */
                    const page = pages.find(page => page.id === getCurPageID()) ?? {
                        id: performance.timeOrigin,
                        logs: [],
                        parent: utils.window.parent !== utils.window ? utils.window.parent.performance.timeOrigin : null,
                        url: location.href,
                    };
                    const logs = page.logs;

                    // 写入页面日志对象,并删除超限旧数据
                    logs.push(wrapLog(level, funcname, content));
                    logs.splice(0, logs.length - GM_getValue('max_logs', CONST.Internal.DefaultLogMaxLength));
                    !pages.includes(page) && pages.push(page);
                    pages.splice(0, pages.length - GM_getValue('max_pages', CONST.Internal.DefaultLogMaxPage));

                    // 保存
                    GM_setValue('pages', pages);
                    return logs;
                }

                /**
                 * @param {LogLevelArg} level 
                 * @param  {...any} content 
                 */
                function log(level, ...content) {
                    _log(level, 'log', ...content);
                }

                /**
                 * @param {LogLevelArg} level 
                 * @param  {...any} content 
                 */
                function error(level, ...content) {
                    _log(level, 'error', ...content);
                }

                /**
                 * @param {LogLevelArg} level 
                 * @param  {...any} content 
                 */
                function warn(level, ...content) {
                    _log(level, 'warn', ...content);
                }

                return {
                    // 日志输出等级
                    get loglevel() { return getLoglevel(); },
                    set loglevel(val) { setLoglevel(val); },

                    // 只读日志对象
                    get pages() { return GM_getValue('pages'); },
                    get logs() { return getCurPage(); },

                    // 日志等级表
                    LogLevel,

                    // 记录日志功能函数
                    log, error, warn,
                };
            }
        },
        dependencies: {
            desc: 'load dependencies like vue into the page',
            detectDom: ['head', 'body'],
            async func() {
                const StandbySuffix = '-bak';
                const deps = [{
                    name: 'vue-js',
                    type: 'script',
                }, {
                    name: 'quasar-icon',
                    type: 'style'
                }, {
                    name: 'quasar-css',
                    type: 'style'
                }, {
                    name: 'quasar-js',
                    type: 'script'
                }];

                await Promise.all(deps.map(dep => {
                    return new Promise((resolve, reject) => {
                        const resource_text = GM_getResourceText(dep.name) || GM_getResourceText(dep.name + StandbySuffix);
                        switch (dep.type) {
                            case 'script': {
                                // Once load, dispatch load event on messager
                                const evt_name = `load:${dep.name};${Date.now()}`;
                                const rand = Math.random().toString();
                                const messager = new EventTarget();
                                const load_code = [
                                    '\n;',
                                    `window[${escJsStr(rand)}].dispatchEvent(new Event(${escJsStr(evt_name)}));`,
                                    `delete window[${escJsStr(rand)}];\n`
                                ].join('\n');
                                unsafeWindow[rand] = messager;
                                $AEL(messager, evt_name, resolve);
                                GM_addElement(document.head, 'script', {
                                    textContent: `/* ${dep.name} */\n` + resource_text + load_code,
                                });
                                break;
                            }
                            case 'style': {
                                GM_addElement(document.head, 'style', {
                                    textContent: `/* ${dep.name} */\n` + resource_text,
                                });
                                resolve();
                                break;
                            }
                        }
                    });
                }));

                // 创建一个Vue app并调用Quasar以进行初始化,以使用Quasar插件(Quasar.Dialog, Quasar.Loading等等)
                const app = Vue.createApp({});
                app.use(Quasar);

                // configurations
                Quasar.setCssVar('primary', '#6f9ff1');
                //Quasar.setCssVar('secondary', '#12b5a5');
                Quasar.setCssVar('negative', '#e63c4f');
                require('darkmode', true).then(
                    /** @param {darkmode} darkmode */
                    darkmode => setTimeout(() => Quasar.Dark.set(darkmode.actual_enabled))
                );
                addStyle(`
                    /* 自动对应深色和浅色模式的背景颜色和文字颜色 */
                    .body--light .text-lightdark {
                        color: black;
                    }
                    .body--light .bg-lightdark {
                        background: #fff;
                    }
                    .body--dark .text-lightdark {
                        color: #fff;
                    }
                    .body--dark .bg-lightdark {
                        background: var(--q-dark);
                    }
                    .body--light .bg-active {
                        background: #EDEDED;
                    }
                    .body--dark .bg-active {
                        background: #2A2A2A;
                    }
                `);
                Quasar.Notify.registerType('info', {
                    color: 'lightdark',
                    textColor: 'lightdark',
                    icon: 'info',
                    iconColor: 'primary',
                    position: 'top-right',
                    badgeColor: 'primary',
                    badgeTextColor: 'lightdark',
                });
                Quasar.Notify.registerType('success', {
                    color: 'lightdark',
                    textColor: 'lightdark',
                    icon: 'done',
                    iconColor: 'primary',
                    position: 'top-right',
                    badgeColor: 'primary',
                    badgeTextColor: 'lightdark',
                });
                Quasar.Notify.registerType('warning', {
                    color: 'lightdark',
                    textColor: 'warning',
                    icon: 'info',
                    iconColor: 'warning',
                    position: 'top-right',
                    badgeColor: 'warning',
                    badgeTextColor: 'lightdark',
                });
                Quasar.Notify.registerType('error', {
                    color: 'lightdark',
                    textColor: 'negative',
                    icon: 'close',
                    iconColor: 'negative',
                    position: 'top-right',
                    badgeColor: 'negative',
                    badgeTextColor: 'lightdark',
                });
                Quasar.LoadingBar.setDefaults({
                    hijackFilter(url) {
                        return false;
                    }
                });

                // some fixes
                addStyle(`
                    *:where([class*="q-"], [class*="q-"]:not(body) *) {
                        font-family: Roboto,-apple-system,Helvetica Neue,Helvetica,Arial,sans-serif;
                    }
                    *:not([class*="q-"], [class*="q-"]:not(body) *) {
                        box-sizing: content-box;
                    }
                    *:where([class*="q-"]:not(body), [class*="q-"]:not(body) *), :after, :before {
                        box-sizing: border-box;
                    }
                    p:where(:not([class*="q-"])) {
                        margin: unset;
                    }
                    [class*="q-"]:not(body) .block:not(.plus-preserve-border) {
                        border: none;
                    }
                    [class*="q-"]:not(body) .block {
                        margin-bottom: 0;
                    }
                `);
                const loadStyle = () => addStyle(`
                    body {
                        ${
                            $('link[href="/configs/article/page.css"]') ?
                            'font-family: 宋体,新细明体,Verdana,Arial,sans-serif;' :
                            'font: 12px/120% 宋体,Verdana,Arial,sans-serif;'
                        }
                        line-height: unset;
                    }
                `);
                document.readyState === 'loading' ? $AEL(document, 'DOMContentLoaded', e => loadStyle()) : loadStyle();
            }
        },
        api: {
            dependencies: ['utils', 'debugging'],
            /** @typedef {Awaited<ReturnType<typeof functions.api.func>>} api */
            func() {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {debugging} */
                const debugging = require('debugging');

                /**
                 * 根据API返回的数字代码获取错误信息
                 * @param {number} errcode 
                 */
                function getErrorInfo(errcode) {
                    return ({
                        0: '请求发生错误',
                        1: '成功(登陆、添加、删除、发帖)',
                        2: '用户名错误',
                        3: '密码错误',
                        4: '请先登录(不可用)',
                        5: '已经在书架',
                        6: '书架已满',
                        7: '小说不在书架',
                        8: '回复帖子主题不存在',
                        9: '签到失败',
                        10: '推荐失败',
                        11: '帖子发送失败',
                        22: 'refer page 0'
                    }) [errcode] ?? `未知错误 ${errcode}`;
                }

                /**
                 * encode request data param for wenku8 api
                 * @param {string} str 
                 * @returns {string}
                 */
                function encode(str) {
                    return '&appver=1.13&request=' + btoa(str) + '&timetoken=' + (new Date().getTime());
                }

                /**
                 * @param {Object} detail
                 * @param {string} detail.url 
                 * @returns {Promise<string>}
                 */
                async function _request({ url }) {
                    const { promise, resolve, reject } = Promise.withResolvers();
                    GM_xmlhttpRequest({
                        method: 'POST',
                        url: 'http://app.wenku8.com/android.php',
                        headers: {
                            'Content-Type': 'application/x-www-form-urlencoded',
                            'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 7.1.2; unknown Build/NZH54D)'
                        },
                        data: encode(url),
                        onload(response) {
                            if (response.status !== 200) {
                                const err = new Error('Network error while fetching api');
                                debugging.saveError({
                                    type: 'api',
                                    error: err,
                                    info: { url }
                                });
                                reject(response);
                            }
                            resolve(response.responseText);
                        },
                        onerror(err) {
                            reject(err);
                        }
                    });
                    return promise;
                }

                const request = utils.toQueued(_request, {
                    max: 5,
                    sleep: 0,
                    queue_id: 'api_request'
                });

                /**
                 * 请求api并将返回字符串解析为XML文档
                 * 如果返回字符串无法解析为XML文档,则返回原始字符串
                 * @param  {Parameters<typeof request>} args 
                 * @returns {Promise<ReturnType<typeof parseXML> | string>}
                 */
                async function requestXML(...args) {
                    const xml_source = await request(...args);
                    try {
                        return parseXML(xml_source);
                    } catch (err) {
                        return xml_source;
                    }
                }

                /**
                 * 将传入的字符串按照XML解析为XMLDocument,如果格式错误不能解析则显式报错
                 * @param {string} xml_source 
                 * @returns {XMLDocument}
                 */
                function parseXML(xml_source) {
                    const parser = new DOMParser();
                    const xml = parser.parseFromString(xml_source, 'text/xml');
                    Assert(!xml.querySelector('parsererror'), 'parse error', Error);
                    return xml;
                }

                /**
                 * 获取书籍简要信息
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @param {number} detail.lang - 文库语言代码,请使用 {@link LanguageCode}
                 * @returns {Promise<XMLDocument>}
                 */
                async function getNovelShortInfo({ aid, lang }) {
                    return requestXML({
                        url: `action=book&do=info&aid=${aid}&t=${lang}`
                    });
                }

                /**
                 * 获取书籍信息(升级版)
                 * 实测也就多了个tags数据
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @param {number} detail.lang - 文库语言代码,请使用 {@link LanguageCode}
                 * @returns {Promise<XMLDocument>}
                 */
                async function getNovelInfo({ aid, lang }) {
                    return requestXML({
                        url: `action=book&do=bookinfo&aid=${aid}&t=${lang}`
                    });
                }

                /**
                 * 获取书籍完整元信息
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @param {number} detail.lang - 文库语言代码,请使用 {@link LanguageCode}
                 * @returns {Promise<XMLDocument>}
                 */
                async function getNovelFullMeta({ aid, lang }) {
                    return requestXML({
                        url: `action=book&do=meta&aid=${aid}&t=${lang}`
                    });
                }

                /**
                 * 获取书籍完整简介
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @param {number} detail.lang - 文库语言代码,请使用 {@link LanguageCode}
                 * @returns {Promise<string>}
                 */
                async function getNovelFullIntro({ aid, lang }) {
                    return request({
                        url: `action=book&do=intro&aid=${aid}&t=${lang}`
                    });
                }

                /**
                 * 获取书籍封面图片
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @returns {Promise<string>}
                 */
                async function getNovelCover({ aid }) {
                    return request({
                        url: `action=book&do=cover&aid=${aid}`
                    });
                }

                /**
                 * 获取书籍目录
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @param {number} detail.lang - 文库语言代码,请使用 {@link LanguageCode}
                 * @returns {Promise<XMLDocument>}
                 */
                async function getNovelIndex({ aid, lang }) {
                    return requestXML({
                        url: `action=book&do=list&aid=${aid}&t=${lang}`
                    });
                }

                /**
                 * 获取某一章节内容
                 * @param {Object} detail 
                 * @param {number | string} detail.aid - 文库书籍ID
                 * @param {number | string} detail.cid - 文库章节ID
                 * @param {number} detail.lang - 文库语言代码,请使用 {@link LanguageCode}
                 * @returns {Promise<string>}
                 */
                async function getNovelContent({ aid, cid, lang }) {
                    return request({
                        url: `action=book&do=text&aid=${aid}&cid=${cid}&t=${lang}`
                    });
                }

                /**
                 * 获取用户信息
                 * @returns {Promise<XMLDocument>}
                 */
                async function getUserInfo() {
                    return requestXML({
                        url: 'action=userinfo'
                    });
                }

                /**
                 * 用户登录(不可用),可选通过用户名或邮箱登录(不可用)
                 * 也许需要注意:纯http请求+明文密码或许是安全性的地狱
                 * @param {string} username - username or email 
                 * @param {string} password 
                 * @param {boolean} [useEmail=false] 
                 */
                async function login(username, password, useEmail = false) {
                    return request({
                        url: `action=${useEmail ? 'loginemail' : 'login'}&username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}`
                    });
                }

                /**
                 * 退出登录(不可用)
                 */
                async function logout() {
                    return request({
                        url: 'action=logout'
                    });
                }

                return {
                    getErrorInfo, encode, request, requestXML,
                    getNovelShortInfo, getNovelInfo, getNovelFullMeta, getNovelFullIntro, getNovelCover, getNovelIndex, getNovelContent, getUserInfo, login, logout,
                };
            }
        },
        sidepanel: {
            desc: '工具栏按钮',
            dependencies: ['dependencies', 'debugging', 'utils'],
            detectDom: 'body',
            /** @typedef {Awaited<ReturnType<typeof functions.sidepanel.func>>} sidepanel */
            func() {
                /** @type {debugging} */
                const debugging = require('debugging');
                /** @type {utils} */
                const utils = require('utils');

                let instance;

                /**
                 * @callback ButtonCallback
                 * @param {PointerEvent} e
                 */
                /**
                 * 按钮类型,不同类型按钮会通过不同外观给予用户不同视觉提示
                 * @typedef {'universal' | 'functional'} ButtonType
                 */
                /**
                 * 按钮数据
                 * @typedef {Object} Button
                 * @property {string} id - 按钮id,需全局唯一
                 * @property {string} label
                 * @property {string} icon
                 * @property {ButtonType} [type='functional']
                 * @property {ButtonCallback} callback - 按钮点击回调,带点击事件
                 * @property {number} index - button的位置,按钮排序顺序:上 <== -1 -2 -3 ... 3 2 1 <== 下
                 */

                const container = $CrE('div');
                container.innerHTML = `
                    <div class="plus-sidepanel q-mt-md">
                        <q-fab
                            square
                            external-label
                            label="${ CONST.Text.SidePanel.PanelShowHide }"
                            label-position="left"
                            vertical-actions-align="center"
                            color="primary"
                            icon="keyboard_arrow_up"
                            direction="up"
                            padding="0.75em"
                            label-style="font-size: 1em; line-height: 1.715em;"
                            v-model="expanded"
                        >
                            <q-fab-action v-for="button of buttons"
                                external-label
                                square padding="0.75em"
                                :color="ButtonColors[button.type]"
                                label-position="left"
                                @click="onClick.call(this, $event, button.callback)"
                                :icon="button.icon"
                                :label="button.label"
                                label-style="font-size: 1em; line-height: 1.715em;"
                            ></q-fab-action>
                        </q-fab>
                    </div>
                `;
                document.body.append(container);

                addStyle(`
                    .plus-sidepanel {
                        position: fixed;
                        right: 2em;
                        bottom: 2em;
                    }
                `);

                const app = Vue.createApp({
                    data() {
                        return {
                            /** @type {Button[]} */
                            buttons: [],
                            expanded: false,
                        };
                    },
                    computed: {
                        ButtonColors() {
                            return {
                                'universal': 'primary',
                                'functional': 'secondary',
                            };
                        }
                    },
                    methods: {
                        /**
                         * 按钮被点击:
                         * 1. 阻止侧边栏自动折叠
                         * 2. 带错误处理地执行按钮回调
                         * @param {PointerEvent} e 
                         * @param {ButtonCallback} callback 
                         */
                        onClick(e, callback) {
                            this.expanded = true;
                            debugging.callWithErrorHandling(callback, this, [e]);
                        },
                    },
                    mounted() {
                        // Vue作用域外使用instance引用this
                        // 本作用域依然属于Vue作用域内,按照原则使用that
                        const that = instance = this;

                        // 点击侧边栏以外的文档任意位置,隐藏侧边栏
                        $AEL(document, 'click', e => {
                            if (!container.contains(e.target)) {
                                that.expanded = false;
                            }
                        });
                    }
                });
                app.use(Quasar);
                app.mount(container);

                /**
                 * 注册(不可用)一个新按钮到侧边栏
                 * 每次有新按钮注册(不可用)或已有按钮移除都会重新排序所有按钮,保证顺序符合index升序
                 * @param {Button} button 
                 */
                function registerButton(button) {
                    // 检查id是否全局唯一
                    Assert(
                        !hasButton(button.id),
                        `duplicate button id ${escJsStr(button.id)}`
                    );
                    
                    // 先克隆button对象,防止后续外部代码修改产生影响
                    button = Object.assign({}, button);

                    // 补充可选属性默认值
                    !button.type && (button.type = 'functional');

                    // 添加到UI中
                    instance.buttons.push(button);

                    // 重新排序
                    instance.buttons.sort((btn1, btn2) => {
                        // 上 <== -1 -2 -3 ... 3 2 1 <== 下
                        const [i1, i2] = [btn1.index, btn2.index];
                        if (i1 * i2 > 0) {
                            // [1, 2, 3, ...] | [..., -3, -2, -1]
                            return btn1.index - btn2.index;
                        } else {
                            // positive, negative
                            return i1 < 0 ? 1 : -1;
                        }
                    });
                }

                /**
                 * 从侧边栏移除一个按钮
                 * @param {string} id - 按钮id
                 * @returns {Button} 被移除的按钮
                 */
                function removeButton(id) {
                    // 检查按钮是否存在
                    Assert(
                        hasButton(id),
                        `No button found with id ${escJsStr(id)}`
                    );

                    // 移除按钮
                    const index = instance.buttons.findIndex(btn => btn.id === id);
                    return index >= 0 ? instance.buttons.splice(index, 1) : null;
                }

                /**
                 * 更新已注册(不可用)按钮的属性
                 * @param {string} id - 按钮id
                 * @param {Partial<Button>} props - 需要修改的按钮属性-值
                 */
                function updateButton(id, props) {
                    // 检查按钮是否存在
                    Assert(
                        hasButton(id),
                        `No button found with id ${escJsStr(id)}`
                    );

                    // 更新按钮
                    const button = instance.buttons.find(btn => btn.id === id);
                    Object.assign(button, props);
                }

                /**
                 * 检查指定id对应的按钮是否存在
                 * @param {string} id 
                 * @returns 
                 */
                function hasButton(id) {
                    return instance.buttons.some(btn => btn.id === id);
                }

                // 注册(不可用)一些通用按钮
                registerButton({
                    id: 'JumpToTop',
                    label: CONST.Text.SidePanel.GotoTop,
                    icon: 'keyboard_arrow_up',
                    type: 'universal',
                    index: -1,
                    callback() {
                        const elms = [document.body.parentElement, $('#content'), $('#contentmain')];

                        for (const elm of elms) {
                            elm && elm.scrollTo && elm.scrollTo({
                                left: elm.scrollLeft,
                                top: 0,
                                behavior: 'smooth'
                            });
                        }
                    }
                });
                registerButton({
                    id: 'JumpToBottom',
                    label: CONST.Text.SidePanel.GotoBottom,
                    icon: 'keyboard_arrow_down',
                    type: 'universal',
                    index: -2,
                    callback() {
                        const elms = [document.body.parentElement, $('#content'), $('#contentmain')];

                        for (const elm of elms) {
                            elm && elm.scrollTo && elm.scrollTo({
                                left: elm.scrollLeft,
                                top: elm.scrollHeight,
                                behavior: 'smooth'
                            });
                        }
                    }
                });
                registerButton({
                    id: 'RefreshPage',
                    label: CONST.Text.SidePanel.Refresh,
                    icon: 'refresh',
                    type: 'universal',
                    index: -3,
                    callback() {
                        const location = utils.window.top.location;
                        if (location.href.includes('#')) {
                            const url = new URL(location.href);
                            url.searchParams.set('_t', Date.now().toString());
                            location.replace(url);
                        } else {
                            location.replace(location.href);
                        }
                    }
                });

                return {
                    registerButton, removeButton, updateButton, hasButton,
                };
            }
        },
        settings: {
            desc: '分组展示的设置界面(仅界面UI)',
            dependencies: ['dependencies', 'debugging'],
            params: ['GM_setValue', 'GM_getValue'],
            /** @typedef {Awaited<ReturnType<typeof functions.settings.func>>} settings */
            async func(GM_setValue, GM_getValue) {
                /** @type {debugging} */
                const debugging = require('debugging');

                /**
                 * 代表一个设置组,同一设置组内的设置将会显示在同一板块/标签页中
                 * @typedef {Object} SettingsGroup
                 * @property {SettingItem[]} items - 组内全部设置项
                 * @property {string} label - 组名称,用于在UI中展示
                 * @property {string} id - 组id标识,全局唯一
                 */
                /**
                 * 代表一条设置项
                 * @typedef {Object} SettingItem
                 * @property {string} label - 设置项名称
                 * @property {string} type - 设置项类型
                 * @property {string | null} [caption] - 设置项副标题,可以省略;为假值时不渲染副标题元素
                 * @property {string} key - 用作settings 读/写对象的prop名称,也用作v-model值
                 * @property {string} [help] - 在用户编辑此项设置时,显示的帮助文档
                 * @property {boolean | 'page'} [reload] - 修改设置后是否需要重载页面才能生效,false: 实时生效,true: 需要重载,'page': 其他页面需要重载;默认为false
                 * @property {{label: string, value: string}[]} [options] - select类型设置的options
                 * @property {{min: number, max: number, step: number}} [range] - 滑块类型的最小/最大值、步长
                 * @property {function} [callback] - button类型设置项的按钮回调;以及其他任何类型的设置值在当前页面的UI中被改变的回调
                 * @property {string} [button_label] - button类型设置项的按钮文本
                 * @property {string} [button_icon] - button类型设置项的按钮图标
                 * @property {getter} get - 需要显示设置内容到UI中时,实际执行读取设置操作的函数
                 * @property {setter} set - 用户在UI中更改设置时,实际执行保存设置操作的函数
                 */
                /**
                 * 用户在UI中更改设置时,实际执行保存设置操作的函数
                 * @callback setter
                 * @param {any} val
                 */
                /**
                 * 需要显示设置内容到UI中时,实际执行读取设置操作的函数
                 * @callback getter
                 * @returns {any}
                 */

                // 创建UI
                const Settings = CONST.Text.Settings;
                const container = $CrE('div');
                container.innerHTML = `
                    <q-dialog v-model="visible" full-width full-height class="plus-settings">
                        <q-layout container view="hHh Lpr fFf">
                            <q-header bordered>
                                <q-toolbar>
                                    <q-btn flat round icon="menu" style="background: transparent;" @click="$refs.drawer.toggle()"></q-btn>
                                    <q-toolbar-title>${ Settings.DialogTitle }</q-toolbar-title>
                                    <q-btn flat round icon="close" style="background: transparent;" @click="visible = false"></q-btn>
                                </q-toolbar>

                                <q-tabs
                                    align="left"
                                    v-model="header_tab"
                                >
                                    <q-tab
                                        name="settings"
                                        label="${ Settings.Tabs.ModuleSettings }"
                                    ></q-tab>
                                    <q-tab
                                        name="about"
                                        label="${ Settings.Tabs.About }"
                                    ></q-tab>
                                </q-tabs>
                            </q-header>

                            <q-drawer
                                show-if-above
                                bordered
                                side="left"
                                :breakpoint="drawer_breakpoint"
                                ref="drawer"
                            >
                                <!-- 根据header tab值确定drawer内容 -->
                                <q-tab-panels v-model="header_tab">
                                    <q-tab-panel name="settings" class="q-pa-none">
                                        <q-tabs
                                            v-model="tab"
                                            indicator-color="primary"
                                            active-bg-color="active"
                                            vertical
                                        >
                                            <q-tab v-for="group of groups"
                                                no-caps
                                                :name="group.id"
                                                :label="group.label"
                                            ></q-tab>
                                        </q-tabs>
                                    </q-tab-panel>
                                    <q-tab-panel name="about" class="q-pa-none">
                                        <q-tabs
                                            v-model="about_tab"
                                            indicator-color="primary"
                                            active-bg-color="active"
                                            vertical
                                        >
                                            <q-tab
                                                no-caps
                                                name="about"
                                                label="${ Settings.Tabs.AboutTab }"
                                            ></q-tab>
                                            <q-tab
                                                no-caps
                                                name="faq"
                                                label="${ Settings.Tabs.FAQ }"
                                            ></q-tab>
                                        </q-tabs>
                                    </q-tab-panel>
                                </q-tab-panels>
                            </q-drawer>

                            <q-page-container>
                                <q-page>
                                    <q-card square class="settings-container q-pa-md">
                                        <q-tab-panels v-model="header_tab">

                                            <!-- "设置"选项卡:设置项列表 -->
                                            <q-tab-panel name="settings" class="q-pa-none">
                                                <q-list v-if="header_tab === 'settings'">
                                                    <q-item v-if="current_group" v-for="item of current_group.items" tag="label">
                                                        <q-item-section>
                                                            <q-item-label>{{ item.label }}</q-item-label>
                                                            <q-item-label caption v-if="item.caption">{{ item.caption }}</q-item-label>
                                                            <q-item-label caption v-if="item.reload === true && modified[item.key]" class="text-warning">${ CONST.Text.Settings.NeedsReload }</q-item-label>
                                                            <q-item-label caption v-if="item.reload === 'page' && modified[item.key]" class="text-warning">${ CONST.Text.Settings.OtherPageNeedsReload }</q-item-label>
                                                        </q-item-section>
                                                        <q-item-section avatar>
                                                            <!-- 布尔值类型: 开关 -->
                                                            <q-toggle v-if="item.type === 'boolean'"
                                                                color="primary"
                                                                v-model="settings[item.key]"
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></q-toggle>

                                                            <!-- 字符串类型: 输入框 -->
                                                            <q-input v-else-if="item.type === 'string'"
                                                                v-model="settings[item.key]"
                                                                @focus="tooltips[item.key] = true"
                                                                @blur="tooltips[item.key] = false"
                                                                @keydown="e => e.stopPropagation()"
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></q-input>

                                                            <!-- 数字范围类型:滑块 -->
                                                            <q-slider v-else-if="item.type === 'range'"
                                                                :max="item.range.max"
                                                                :min="item.range.min"
                                                                :step="item.range.step"
                                                                style="width: 10em;"
                                                                v-model="settings[item.key]"
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></q-slider>

                                                            <!-- select类型: 选择器 -->
                                                            <q-select v-else-if="item.type === 'select'"
                                                                :options="item.options"
                                                                v-model="settings[item.key]" emit-value map-options
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></q-select>

                                                            <!-- choose类型: 单选(更复杂的选择器) -->
                                                            <p-choose v-else-if="item.type === 'choose'"
                                                                :options="item.options"
                                                                v-model="settings[item.key]"
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></p-choose>

                                                            <!-- 颜色类型: 颜色选择器 -->
                                                            <p-color v-else-if="item.type === 'color'"
                                                                v-model="settings[item.key]"
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></p-color>
                                                            
                                                            <!-- 本地图片类型: 本地图片选择器 -->
                                                            <p-image-select v-else-if="item.type === 'image'"
                                                                v-model="settings[item.key]"
                                                                @update:model-value="val => onSettingUpdate(item, val)"
                                                            ></p-image-select>

                                                            <!-- 按钮类型: 按钮 -->
                                                            <q-btn v-else-if="item.type === 'button'"
                                                                :label="item.button_label"
                                                                :icon="item.button_icon"
                                                                @click="item.callback"
                                                                flat
                                                            ></q-btn>

                                                            <!-- 浮动提示 -->
                                                            <q-tooltip v-if="item.help"
                                                                v-model="tooltips[item.key]"
                                                                :no-parent-event="item.type === 'string'"
                                                                v-html="item.help"
                                                                style="font-size: 1em;"
                                                            ></q-tooltip>
                                                        </q-item-section>
                                                    </q-item>
                                                </q-list>
                                            </q-tab-panel>

                                            <!-- "关于"选项卡 -->
                                            <q-tab-panel name="about" class="q-pa-none text-body1">
                                                <q-tab-panels v-model="about_tab">
                                                    <!-- 关于 -->
                                                    <q-tab-panel name="about" class="q-pa-none">
                                                        <div class="text-h5 q-mb-md">${ GM_info.script.name }</div>
                                                        <div class="text-subtitle1 q-my-sm">${ GM_info.script.description }</div>
                                                        <div class="q-my-sm">${ Settings.About.Version }</div>
                                                        <div class="q-my-sm">${ Settings.About.Author }</div>
                                                        <div class="q-my-sm">${ Settings.About.Homepage }</div>
                                                        <div class="q-my-sm">
                                                            ${ Settings.About.TechnicalNote }
                                                            <span class="text-weight-bold" style="cursor: pointer;" @click="cool">Cool!</span>
                                                        </div>
                                                    </q-tab-panel>
                                                    
                                                    <!-- 常见问题 -->
                                                    <q-tab-panel name="faq" class="q-pa-none">
                                                        <q-expansion-item
                                                            v-for="faq of FAQ"
                                                            :label="faq.Q"
                                                            class="text-h6"
                                                        >
                                                            <div class="text-body1 q-pa-md">{{ faq.A }}</div>
                                                        </q-expansion-item>
                                                    </q-tab-panel>
                                                </q-tab-panels>
                                            </q-tab-panel>

                                        </q-tab-panels>
                                    </q-card>
                                </q-page>
                            </q-page-container>
                        </q-layout>
                    </q-dialog>
                `;

                let instance;
                const app = Vue.createApp({
                    data() {
                        return {
                            /**
                             * 存储设置项信息
                             * @type {SettingsGroup[]}
                             */
                            groups: [],
                            tab: '',
                            header_tab: 'settings',
                            about_tab: 'about',
                            visible: false,
                            /**
                             * 存储全部设置内容的变量
                             * @type {Record<string, Record<string, any>>}
                             */
                            all_settings: {},
                            /**
                             * 记录设置项自从设置界面创建起,是否被修改过的变量
                             * @type {Record<string, Record<string, boolean>>}
                             */
                            all_modified: {},
                            FAQ: Settings.About.FAQ,
                        };
                    },
                    computed: {
                        /** @type {SettingsGroup} */
                        current_group() {
                            return this.groups.find(g => g.id === this.tab);
                        },
                        /**
                         * 读写当前UI上的active tab对应的group的设置
                         * @type {Record<string, any>}
                         */
                        settings() {
                            return this.all_settings[this.tab];
                        },
                        modified() {
                            return this.all_modified[this.tab];
                        },
                        /**
                         * 当前UI上的active tab对应的group的key - help文档对照表对象
                         * @type {Record<string, string>}
                         */
                        tooltips() {
                            return this.current_group.items.reduce((tips, item) => {
                                tips[item.key] = item.help;
                                return tips;
                            }, {});
                        },
                        drawer_breakpoint() {
                            return debugging.script_debug ? 880 : 1023;
                        },
                    },
                    watch: {
                        // 监听设置组变化
                        groups: {
                            async handler(val, old_val) {
                                // 当从没有设置组到有一个设置组加入时,自动将此设置组设为active tag
                                if (val.length && !this.tab) {
                                    this.tab = val[0].id;
                                }

                                // 自动将新加入的设置组加入到this.all_settings和this.all_modified中
                                for (const group of val) {
                                    // 无论是否已有此组都强制更新此组,因为组内设置项可能变化
                                    const setting = {};
                                    await Promise.all(group.items.map(async item => {
                                        item.get && (setting[item.key] = await Promise.resolve(item.get()));
                                        return setting;
                                    }));
                                    this.all_settings[group.id] = setting;
                                    this.all_modified[group.id] = group.items.reduce((modified, item) => {
                                        modified[item.key] = false;
                                        return modified;
                                    }, {});
                                }
                            },
                            deep: true,
                        },
                    },
                    methods: {
                        /**
                         * @param {SettingItem} item
                         * @param {any} val
                         */
                        async onSettingUpdate(item, val) {
                            // 回调外部setter,保存设置
                            await Promise.resolve(item.set(val));

                            // 如果有callback,回调callback
                            if (item.callback) {
                                await Promise.resolve(item.callback());
                            }

                            // 记录此项已被修改过
                            this.modified[item.key] = true;
                        },
                        
                        /**
                         * Make some confetti. Congratulations!
                         */
                        cool() {
                            let count = 200;
                            let defaults = {
                                origin: { y: 0.7 },
                                zIndex: 8000,
                            };

                            function fire(particleRatio, opts) {
                                confetti({
                                    ...defaults,
                                    ...opts,
                                    particleCount: Math.floor(count * particleRatio),
                                });
                            }

                            fire(0.25, {
                                spread: 26,
                                startVelocity: 55,
                            });
                            fire(0.2, {
                                spread: 60,
                            });
                            fire(0.35, {
                                spread: 100,
                                decay: 0.91,
                                scalar: 0.8,
                            });
                            fire(0.1, {
                                spread: 120,
                                startVelocity: 25,
                                decay: 0.92,
                                scalar: 1.2,
                            });
                            fire(0.1, {
                                spread: 120,
                                startVelocity: 45,
                            });
                        },
                    },
                    mounted() {
                        instance = this;
                    },
                });

                document.body.append(container);
                app.use(Quasar);

                // 注册(不可用)自定义设置项表单组件
                // 本地图片选择器
                app.component('p-image-select', {
                    name: 'PImageSelect',
                    props: ['modelValue'],
                    emits: ['update:modelValue'],
                    template: `
                        <q-img v-if="file"
                            :src="img_src"
                            style="width: 10em;"
                        ></q-img>
                        <q-btn v-show="!file"
                            label="${ Settings.Component.SelectImage }"
                            @click="selectImage"
                            flat
                        ></q-btn>
                    `,
                    computed: {
                        // v-model
                        file: {
                            get() {
                                return this.modelValue;
                            },
                            set(file) {
                                this.$emit('update:modelValue', file);
                            }
                        },

                        // 图片src
                        img_src(_, old_src) {
                            old_src && URL.revokeObjectURL(old_src);
                            return URL.createObjectURL(this.modelValue);
                        },
                    },
                    methods: {
                        /**
                         * 用户选择图片
                         */
                        selectImage() {
                            const that = this;
                            $$CrE({
                                tagName: 'input',
                                props: {
                                    type: 'file',
                                },
                                listeners: [['change', async e => {
                                    /** @type {HTMLInputElement} */
                                    const input = e.target;
                                    const file = input.files[0];
                                    that.file = file;
                                }]]
                            }).click();
                        },
                    }
                });
                // 颜色选择器
                app.component('p-color', {
                    name: 'PColor',
                    props: ['modelValue'],
                    emits: ['update:modelValue'],
                    template: `
                        <q-input
                            v-model="color"
                            :rules="['anyColor']"
                        >
                            <template v-slot:append>
                                <q-icon name="colorize" class="cursor-pointer">
                                    <q-popup-proxy cover transition-show="scale" transition-hide="scale">
                                        <q-color v-model="color"></q-color>
                                    </q-popup-proxy>
                                </q-icon>
                            </template>
                        </q-input>
                    `,
                    computed: {
                        color: {
                            get() {
                                return this.modelValue;
                            },
                            set(color) {
                                this.$emit('update:modelValue', color);
                            },
                        },
                    },
                });
                // 列表单选类型
                app.component('p-choose', {
                    name: 'PChoose',
                    props: ['modelValue', 'options'],
                    emits: ['update:modelValue'],
                    template: `
                        <q-btn :label="brief" icon-right="keyboard_arrow_down" flat>
                            <q-popup-proxy>
                                <q-list>
                                    <q-item v-for="option of options" tag="label">
                                        <q-radio
                                            v-model="value"
                                            :val="option.value"
                                            :label="option.label"
                                        ></q-radio>
                                    </q-item>
                                </q-list>
                            </q-popup-proxy>
                        </q-btn>
                    `,
                    computed: {
                        value: {
                            get() {
                                return this.modelValue;
                            },
                            set(val) {
                                this.$emit('update:modelValue', val);
                            },
                        },
                        brief() {
                            return this.options.find(o => o.value === this.value)?.brief ?? Settings.Component.PleaseChoose;
                        }
                    },
                });

                // 挂载Vue
                app.mount(container);

                // 设置界面样式
                addStyle(`
                    .plus-settings .settings-container {
                        position: absolute;
                        width: 100%;
                        height: 100%;
                    }
                `);

                // 注册(不可用)侧边栏设置按钮
                require('sidepanel', true).then(
                    /** @param {sidepanel} sidepanel */
                    sidepanel => sidepanel.registerButton({
                        id: 'settings.show',
                        label: CONST.Text.Settings.DialogTitle,
                        icon: 'settings',
                        type: 'universal',
                        index: -4,
                        callback() { instance.visible = true; }
                    })
                );

                /**
                 * 注册(不可用)新的设置组
                 * @param {SettingsGroup} group - 设置组对象
                 */
                function registerGroup({ id, label, items = [] }) {
                    /** @type {SettingsGroup[]} */
                    const groups = instance.groups;
                    Assert(groups.every(g => g.id !== id), `duplicate id ${escJsStr(id)}`, TypeError);
                    groups.push({ id, label, items });
                }

                /**
                 * 注册(不可用)新的设置项
                 * @param {string} id - 设置组id
                 * @param {SettingItem | SettingItem[]} items 
                 */
                function registerSettings(id, items) {
                    items = Array.isArray(items) ? items : [items];

                    /** @type {SettingsGroup[]} */
                    const groups = instance.groups;
                    const group = groups.find(g => g.id === id);
                    Assert(group, `Settings group with id ${escJsStr(id)} not exist, call registerGroup first.`, TypeError);
                    group.items.push(...items);
                }
                /**
                 * 主动更新设置项的值到设置UI中
                 * @param {string} group_id - 设置组id
                 * @param {string} item_key - 设置项key
                 * @param {any} val - 设置项的新值 
                 */
                function update(group_id, item_key, val) {
                    instance.all_settings[group_id][item_key] = val;
                }

                return {
                    registerGroup, registerSettings,
                    update,

                    /** 用于导出JSDoc类型,无实际作用 */
                    _types: {
                        /** @type {SettingItem} */
                        SettingItem: {},
                        /** @type {SettingsGroup} */
                        SettingsGroup: {},
                    },
                };
            }
        },
        configs: {
            desc: '模块配置管理器,对settings和脚本存储空间的高级封装;分模块管理配置存储与设置界面,跨页面实例同步配置、功能与设置界面;负责模块的 设置界面 - 设置存储 - 模块功能 间的统一调度',
            dependencies: ['settings'],
            /** @typedef {Awaited<ReturnType<typeof functions.configs.func>>} configs */
            async func() {
                /** @type {settings} */
                const settings = require('settings');

                /** @typedef {typeof settings._types.SettingItem} SettingItem */
                /** @typedef {typeof settings._types.SettingsGroup} SettingsGroup */

                /**
                 * 模块监听器函数
                 * @callback update_callback
                 * @param {string} key - 设置项key
                 * @param {any} old_val - 设置项旧值
                 * @param {any} new_val - 设置项新值
                 * @param {boolean} remote - 表示本次更改是否来源于另一页面的脚本实例
                 */
                /**
                 * 代表模块监听器的对象
                 * @typedef {{ id: symbol, callback: update_callback }} config_listener
                 */
                
                /**
                 * 代表一个模块的配置
                 * @typedef {Object} Config
                 * @property {string} id - 全局唯一模块id
                 * @property {typeof GM_addValueChangeListener || null} GM_addValueChangeListener - 用于监听设置项内容变化的GM函数
                 * @property {SettingItem[]} items - 注册(不可用)到settings界面中的设置项数组
                 * @property {string} label - 显示在settings界面中的模块名称
                 * @property {Record<string, config_listener[]>} listeners - 监听模块设置内容变化的全部监听器
                 */

                /** @type {Record<string, Config>} */
                const configs = {};

                /**
                 * 注册(不可用)一个新模块
                 * 为模块提供以下功能:  
                 * - 注册(不可用)设置项到settings界面中
                 * - 在跨页面跨实例的配置存储更新中:
                 *   - 提供更新回调接口,以供模块将更改应用于实际功能
                 *   - 自动将新配置值同步到settings界面中
                 * @param {string} id - 全局唯一模块id
                 * @param {Object} options
                 * @param {typeof GM_addValueChangeListener} [options.GM_addValueChangeListener] - 用于监听设置项内容变化的GM函数
                 * @param {SettingItem | SettingItem[]} [options.items=[]] - 注册(不可用)到settings界面中的设置项数组
                 * @param {string} options.label - 显示在settings界面中的模块名称
                 * @param {Record<string, update_callback[]> | update_callback} [options.listeners={}] - 监听设置值变化的监听器, 可以为一个key-listener格式的对象用于分别监听多个设置项,也可以为一个listener函数用于监听全部模块设置项变化
                 */
                function registerConfig(id, {
                    GM_addValueChangeListener = null,
                    items = [],
                    label,
                    listeners = {},
                }) {
                    // 记录此模块
                    const config = configs[id] = {
                        id,
                        items,
                        label,
                        listeners: {},
                        GM_addValueChangeListener,
                    };

                    // 注册(不可用)设置项
                    items = Array.isArray(items) ? items : [items];
                    settings.registerGroup({ id, label });
                    registerSettings(id, items);

                    // 注册(不可用)监听器
                    registerUpdateCallback(id, listeners);
                }

                /**
                 * 注册(不可用)设置项:
                 * - 注册(不可用)到settings界面中
                 * - 为每个设置项自动监听变化:
                 *   - 自动同步到设置界面中
                 *   - 执行回调
                 * @param {string} id - 全局唯一模块id
                 * @param {SettingItem | SettingItem[]} items - 需注册(不可用)的设置项
                 * @param {typeof GM_addValueChangeListener} [GM_addValueChangeListener] - 本次注册(不可用)的设置项,监听其值变化时所使用的GM函数,如不提供则使用模块注册(不可用)时提供的GM函数
                 */
                function registerSettings(id, items=[], GM_addValueChangeListener=null) {
                    items = Array.isArray(items) ? items : [items];
                    const config = configs[id];

                    // 注册(不可用)设置UI
                    settings.registerSettings(id, items);

                    // 用于监听设置项变化的GM函数
                    GM_addValueChangeListener = 
                        GM_addValueChangeListener ??
                        config.GM_addValueChangeListener ?? null;
                    // 此次调用和此前注册(不可用)中,至少要提供一个GM_addValueChangeListener,否则无法监听设置项内容变化
                    Assert(GM_addValueChangeListener, 'configs.registerSettings: GM_addValueChangeListener not provided when adding value change listeners');
                    
                    // 监听每个设置项内容变化
                    items.forEach(item => {
                        // 创建此设置项的监听器数组
                        config.listeners[item.key] = [];

                        // 监听设置项内容变化
                        GM_addValueChangeListener(
                            item.key, (key, old_val, new_val, remote) => {
                                // 同步到设置UI
                                settings.update(id, key, new_val);
                                // 模块回调
                                configs[id].listeners[key].forEach(cb => cb.callback(key, old_val, new_val, remote));
                            }
                        );
                    });
                }

                /**
                 * 注册(不可用)设置内容更新回调
                 * @param {string} id - 监听目标模块id
                 * @param {Record<string, update_callback> | update_callback} listener - 回调函数,可以为一个key-listener格式的对象用于分别监听多个设置项,也可以为一个listener函数用于监听全部模块设置项变化
                 * @returns {() => void} 用于取消回调的方法,调用后不再监听本次注册(不可用)的所有相关设置项内容更新
                 */
                function registerUpdateCallback(id, callback) {
                    const config = configs[id];
                    if (typeof callback === 'function') {
                        const unregisters = Reflect.ownKeys(config.listeners).map(key => register(key, callback));
                        return () => unregisters.forEach(unregister => unregister());
                    } else {
                        const unregisters = Object.entries(callback).map(([key, callback]) => register(key, callback));
                        return () => unregisters.forEach(unregister => unregister());
                    }

                    /**
                     * 对模块内的一项设置注册(不可用)内容更新回调
                     * @param {string} key 
                     * @param {update_callback} callback 
                     * @returns {() => void} 用于取消回调的方法,调用后不再监听内容更新
                     */
                    function register(key, callback) {
                        const callback_id = Symbol('Configs.UpdateCallbackId');
                        config.listeners[key].push({
                            id: callback_id, callback
                        });
                        return () => config.listeners[key].splice(
                            config.listeners[key].findIndex(cb => cb.id === callback_id),
                            1
                        )
                    }
                }

                return {
                    registerConfig, registerSettings, registerUpdateCallback,
                };
            },
        },
        _styling: {
            desc: '文库网页样式管理器',
            disabled: true,
            detectDom: ['head', 'body'],
            dependencies: ['utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                // 带默认值的GM_getValue
                GM_getValue = utils.defaultedGet({
                    enabled: false,
                    theme: 'darkmode',
                    /**
                     * 存储用户自定义主题
                     * @type {Record<string, string>}
                     */
                    themes: {},
                }, GM_getValue);

                /** @typedef {typeof FunctionLoader._types.checker} checker */
                /** @typedef {{ checkers: [checker | checker[]], content: string }} Style */
                /**
                 * 将主题色应用到页面的CSS
                 * @type {Record<string, Style>}
                 */
                const Styles = {
                    block: {
                        content: `
                            /* 标题、内容和脚注 */
                            .plus-styled .blocktitle {
                                border-color: var(--plus-background-title);
                            }

                            .plus-styled :is(#left, #right, #centers, *) .blocktitle>:is(.txt, .txtr) {
                                background-color: var(--plus-background-3);
                                line-height: 27px;
                                padding-top: 0;
                            }

                            .plus-styled :is(#left, #right, *) .blockcontent {
                                background-color: var(--plus-background-1)
                            }

                            .plus-styled :is(#left, #right, *) .blocknote {
                                background-color: var(--plus-background-2);
                            }

                            /* 特定类型内容 */
                            .plus-styled :is(#left, #right, #centers, *) :is(.blocktitle, .blocktitle *, .ultop li) {
                                color: var(--plus-text-title)
                            }

                            /* 边框 */
                            .plus-styled .block {
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled :is(.blockcontent, .blocknote) {
                                border-color: var(--plus-primary);
                            }

                            .plus-styled .block :is(.ultop li, .ultops li) {
                                border-bottom: 1px dashed var(--plus-primary);
                            }
                        `,
                    },
                    book: {
                        checkers: [{
                            type: 'regpath',
                            value: /\/book\/\d+\.htm/
                        }, {
                            type: 'regpath',
                            value: /\/modules\/article\/articleinfo\.php/
                        }],
                        content: `
                            /* 需要补充基层颜色的各区域 */
                            .plus-styled :is(:is(#left, #right, #centers) .blockcontent, .blockcontent, .odd, .even) {
                                background-color: var(--plus-background-1);
                                color: var(--plus-text-1);
                            }

                            /* 表头 */
                            .plus-styled table.grid:not(form table) tr:first-of-type > td:nth-of-type(2n+1) {
                                background-color: var(--plus-background-2) !important;
                            }

                            /* 表行 */
                            .plus-styled table.grid td {
                                background-color: var(--plus-background-1) !important;
                            }

                            /* 《文学少女》吐槽吧,不吐不快! */
                            .plus-styled table.grid:not(form table, #content .main > table:first-of-type) tr:first-of-type > td:first-of-type {
                                color: var(--plus-text-title);
                            }

                            .plus-styled fieldset {
                                border: 2px solid var(--plus-primary);
                            }

                            .plus-styled :is(table.grid, table.grid td, table.grid caption, .gridtop) {
                                border: 1px solid var(--plus-primary);
                            }
                        `,
                    },
                    bookindex: {
                        checkers: [{
                            type: 'regpath',
                            value: /^\/novel\/\d+\/\d+\/index\.html?$/
                        }, {
                            type: 'path',
                            value: '/modules/article/reader.php'
                        }],
                        content: `
                            .plus-styled :is(.css, .vcss, .ccss) {
                                background-color: var(--plus-background-1);
                                color: var(--plus-text-1);
                            }

                            .plus-styled #headlink {
                                border-bottom: 1px solid var(--plus-primary);
                                border-top: 1px solid var(--plus-primary);
                            }

                            .plus-styled :is(.css, .vcss, .ccss) {
                                border: 1px solid var(--plus-primary);
                                border-collapse: collapse;
                            }
                        `,
                    },
                    common: {
                        content: `
                            /* 通用页面样式 */
                            body.plus-styled:not(#stonger-than-quasar) {
                                background: var(--plus-page-bg);
                                color: var(--plus-text);
                            }
                        `,
                    },
                    dialog: {
                        content: `
                            .plus-styled #dialog {
                                color: var(--plus-text-1);
                                background-color: var(--plus-background-1);
                                border: 5px solid var(--plus-primary);
                            }

                            .plus-styled #dialog a[onclick="closeDialog()"] {
                                border: 1px solid var(--plus-primary) !important;
                                outline: thin solid var(--plus-primary) !important;
                            }
                        `,
                    },
                    element: {
                        content: `
                            .plus-styled :is(.even, .odd) {
                                background-color: var(--plus-background-1);
                            }

                            .plus-styled table.grid td {
                                background-color: var(--plus-background-1) !important;
                            }

                            .plus-styled :is(input:not([type]:not([type="text"], [type="number"], [type="file"], [type="password"])), textarea, .plus_list_item, button):not([class*="q-"]:not(body) *) {
                                background-color: var(--plus-background-2);
                                color: var(--plus-text-input);
                            }

                            .plus-styled :is(.button, input[type="button"]) {
                                color: var(--plus-text-2);
                                background-color: var(--plus-background-2);
                            }

                            .plus-styled select {
                                color: var(--plus-text-2);
                                background-color: var(--plus-background-2);
                            }

                            .plus-styled :is(.hottext, a.hottext) {
                                color: var(--plus-text-hot);
                            }

                            .plus-styled :is(.button, select, textarea, input:not(.plus_list_item>input, .UBB_ColorList input), .plus_list_item):not(:disabled, [class*="q-"]:not(body) *) {
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled :is(input, textarea, button):disabled {
                                border: 1px solid var(--plus-border-disabled);
                            }

                            .plus-styled a {
                                color: var(--plus-text-link);
                            }

                            .plus-styled a:hover {
                                color: var(--plus-text-link-hover);
                            }

                            .plus-styled a:is(.ultop li a, .poptext, a.poptext, .ultops li a) {
                                color: var(--plus-text-hot);
                            }

                            .plus-styled :is(table.grid caption, .gridtop, table.grid th, .head) {
                                border: 1px solid var(--plus-primary);
                                background: var(--plus-background-title);
                                color: var(--plus-text-title);
                            }

                            .plus-styled :is(table.grid, table.grid td) {
                                border: 1px solid var(--plus-primary);
                            }

                            /* 未发现用处
                            .plus-styled input[type="checkbox"]::after {
                                background-color: #333333;
                            }
                            */

                            /* 滚动条样式 */
                            :is(.plus-styled, .plus-styled *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement) {
                                scrollbar-color: var(--plus-background-2) var(--plus-background-1);
                            }

                            :is(.plus-styled, .plus-styled *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement):hover {
                                scrollbar-color: var(--plus-background-3) var(--plus-background-1);
                            }

                            :is(.plus-styled, .plus-styled *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar {
                                background-color: var(--plus-background-1);
                            }

                            :is(.plus-styled, .plus-styled *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-corner {
                                background-color: var(--plus-background-1);
                            }

                            :is(.plus-styled, .plus-styled *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-thumb,
                            .plus-styled *:not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-button {
                                background-color: var(--plus-background-2);
                            }

                            :is(.plus-styled, .plus-styled *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-thumb:hover,
                            .plus-styled *:not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-button:hover {
                                background-color: var(--plus-background-3);
                            }
                        `,
                    },
                    frmreview: {
                        content: `
                            .plus-styled form[name="frmreview"] caption {
                                background: var(--plus-background-title);
                                color: var(--plus-text-title);
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled .UBB_FontSizeList li {
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled .UBB_ColorList :is(table, table td) {
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled .UBB_ColorList {
                                background-color: var(--plus-background-1);
                            }
                        `,
                    },
                    headfoot: {
                        content: `
                            .plus-styled :is(.main.m_top, .nav, .navinner, .navlist, .nav li, :is(#left, #right, #centers, *) :is(.blocktitle, .blocktitle *)) {
                                background: var(--plus-background-header);
                            }

                            .plus-styled :is(.nav a.current, .nav a:hover, .nav a:active) {
                                background: var(--plus-background-header-active);
                            }

                            .plus-styled .m_foot {
                                border-top: 1px dashed var(--plus-primary);
                                border-bottom: 1px dashed var(--plus-primary);
                            }
                        `,
                    },
                    indexpage: {
                        checkers: [{
                            type: 'path',
                            value: '/index.php'
                        }, {
                            type: 'path',
                            value: '/'
                        }],
                        content: `
                            .plus-styled :is(:is(#left, #right, #centers) .blockcontent, .blockcontent, .odd, .even) {
                                background-color: var(--plus-text-1);
                                color: var(--plus-background-1);
                            }

                            .plus-styled :is(#left, #right, #centers, *) :is(.blocktitle, .blocktitle *) a {
                                color: var(--plus-text-link);
                            }

                            a[href^="http://tieba.baidu.com"] {
                                color: var(--plus-text-link-highlight) !important;
                            }
                        `,
                    },
                    mousetip: {
                        content: `
                            .plus-styled #tips {
                                background-color: var(--plus-background-title);
                                color: var(--plus-text-title); /* #f0f7ff */
                                border: 1px solid var(--plus-primary);
                            }
                        `,
                    },
                    novel: {
                        checkers: {
                            type: 'func',
                            value: () => {
                                return location.pathname.startsWith('/novel/') && location.pathname.split('/').pop() !== 'index.htm';
                            }
                        },
                        content: `
                            .plus-styled a {
                                color: var(--plus-text-link-highlight);
                            }

                            .plus-styled #content {
                                color: var(--plus-text-1) !important;
                            }
                        `
                    },
                    reviewshow: {
                        checkers: {
                            type: 'regurl',
                            value: /^https?:\/\/www\.wenku8\.(net|cc)\/modules\/article\/reviewshow\.php\/?/
                        },
                        content: `
                            .plus-styled table.grid td {
                                background-color: var(--plus-background-1);
                            }

                            .plus-styled :is(#content table.grid hr, #content>table:nth-of-type(2) th, #pagelink) {
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled :is(.jieqiQuote, .jieqiCode, .jieqiNote) {
                                background-color: var(--plus-background-2);
                                color: var(--plus-text-2);
                                border: 1px solid var(--plus-primary);
                            }

                            .plus-styled :is(.pagelink, .pagelink a:hover) {
                                background-color: var(--plus-background-title);
                                color: var(--plus-text-link-hover);
                            }

                            .plus-styled .pagelink strong {
                                background-color: var(--plus-background-highlight);
                            }

                            .plus-styled .pagelink em {
                                border-right: 1px solid var(--plus-primary);
                            }

                            .plus-styled .pagelink kbd {
                                border-left: 1px solid var(--plus-primary);
                            }

                            .plus-styled .pagelink {
                                border: 1px solid var(--plus-primary);
                            }
                        `,
                    },
                };

                /**
                 * 定义主题色的CSS(内置主题)
                 * @type {Record<string, string>}
                 */
                const BuiltinThemes = {
                    darkmode: `
                        /* 深色模式 */
                        body.plus-styled.plus-darkmode {
                            /* 主要颜色 */
                            --plus-primary: #0d548b;
                            /* 页面通用文字色和背景色 从底层到高层颜色逐渐加深或变浅 */
                            --plus-text-1: #C8C8C8;
                            --plus-background-1: #222222;
                            --plus-text-2: #C8C8C8;
                            --plus-background-2: #282828;
                            --plus-text-3: #ffffff;
                            --plus-background-3: #383838;
                            /* 特定用途/位置的颜色 */
                            --plus-text-title: #6f9ff1;
                            --plus-text-input: #DDDDDD;
                            --plus-background-title: #333333;
                            --plus-background-highlight: #444444;
                            --plus-text-hot: #f36d55;
                            --plus-text-link: #AAAAAA;
                            --plus-text-link-hover: #4a8dff;
                            --plus-text-link-highlight: #4a8dff;
                            --plus-border-disabled: #444444;
                            --plus-background-header: #333333;
                            --plus-background-header-active: #444444;
                        }
                    `,
                };

                const Settings = CONST.Text.Styling.Settings;
                configs.registerConfig('styling', {
                    GM_addValueChangeListener,
                    items: [{
                        type: 'boolean',
                        label: Settings.Enabled,
                        caption: Settings.EnabledCaption,
                        key: 'enabled',
                        get() { return GM_getValue('enabled'); },
                        set(val) { GM_setValue('enabled', val); },
                    }],
                    label: Settings.Title,
                    listeners: {
                        enabled(key, old_val, new_val, remote) {
                            new_val ? install() : uninstall();
                        },
                        themes(key, old_val, new_val, remote) {
                            // 比对新旧主题,将改动应用到页面
                            const old_ids = Object.keys(old_val);
                            const new_ids = Object.keys(new_val);

                            // 删除消失的主题
                            old_ids.filter(id => !new_ids.includes(id)).forEach(
                                id => $(`plus-theme-${ id }`)?.remove());
                            // 添加新增的主题
                            new_ids.filter(id => !old_ids.includes(id)).forEach(
                                id => addStyle(new_val[id], `plus-theme-${ id }`));
                            // 更新改变的主题
                            new_ids.filter(id => 
                                old_ids.includes(id) && old_val[id] !== new_val[id]
                            ).forEach(
                                id => addStyle(new_val[id], `plus-theme-${ id }`));
                        },
                    }
                });

                GM_getValue('enabled') && install();

                /** 安装本模块功能到页面 */
                function install() {
                    // 根据页面添加对应控制性css
                    Object.entries(Styles).forEach(([id, style]) => {
                        if (!style.checkers || FunctionLoader.testCheckers(style.checkers)) {
                            addStyle(style.content, `plus-styling-${ id }`);
                        }
                    });
                    // 添加主题包到页面
                    const themes = Object.assign({}, BuiltinThemes, GM_getValue('themes'));
                    Object.entries(themes).forEach(([id, css]) => addStyle(css, `plus-theme-${ id }`));

                    // body添加 plus-styled 类名
                    document.body.classList.add('plus-styled');
                }

                /** 从页面卸载本模块功能 */
                function uninstall() {
                    // 移除所有控制性css
                    Array.from($All('style[id^="plus-styling-"]')).forEach(s => s.remove());
                    // 移除所有主题包
                    Array.from($All('style[id^="plus-theme-"]')).forEach(s => s.remove());
                    // 移除 plus-styled 类名
                    document.body.classList.remove('plus-styled');
                }

                /**
                 * 安装一个新主题
                 * 这里只需要安装到存储,其他部分代码检测到存储变化会自动安装到页面的
                 * @param {string} id - 主题id,应全局唯一,如和已有主题id重复,则会更新该id对应主题的内容 
                 * @param {string} css - 主题的css样式代码
                 */
                function installTheme(id, css) {
                    const themes = GM_getValue('themes');
                    themes[id] = css;
                    GM_setValue('themes', themes);
                }

                /**
                 * 卸载一个主题
                 * @param {string} id 主题的id
                 */
                function uninstallTheme(id) {
                    const themes = GM_getValue('themes');
                    delete themes[id];
                    GM_setValue('themes', themes);
                }
            }
        },
        unlocker: {
            desc: '各类网页端内容解锁',
            dependencies: ['api', 'utils', 'debugging'],
            async func() {
                /** @type {api} */
                const api = require('api');
                /** @type {utils} */
                const utils = require('utils');
                /** @type {debugging} */
                const debugging = require('debugging');

                const pool = new FunctionLoader.FuncPool();
                debugging.catchPoolErrors(pool);
                await pool.load([
                    {
                        id: 'read',
                        desc: '在线阅读',
                        checkers: {
                            type: 'func',
                            value() {
                                const is_reader_page = (
                                    location.pathname.startsWith('/novel/')
                                    || location.pathname.match(/\/modules\/article\/reader.php/)
                                ) && unsafeWindow.chapter_id !== '0';
                                const need_unlock = $('#contentmain>:first-child')?.innerText.trim() === 'null';
                                return is_reader_page && need_unlock;
                            }
                        },
                        detectDom: '#footlink',
                        async func() {
                            Quasar.Loading.show({ message: CONST.Text.Unlocker.FetchingContent });
                            const content = await api.getNovelContent({
                                aid: utils.window.article_id,
                                cid: utils.window.chapter_id,
                                lang: utils.getLanguage()
                            });
                            const html = content
                            .replaceAll(/[\r\n]+/g, '<br>')
                            .replaceAll(' ', '&nbsp;')
                            .replaceAll(
                                /<!--image-->([^<]+?)<!--image-->/g,
                                `<div class="divimage"><a href="$1" target="_blank"><img src="$1" border="0" class="imagecontent"></a></div>`
                            );
                            [...$('#content').childNodes].forEach(elm => elm.remove());
                            $('#content').insertAdjacentHTML('afterbegin', html);
                            Quasar.Loading.hide();
                        }
                    },
                    {
                        id: 'download',
                        desc: '下载',
                        checkers: [{
                            type: 'regpath',
                            value: /\/book\/\d+\.htm/
                        }, {
                            type: 'path',
                            value: '/modules/article/articleinfo.php'
                        }, {
                            type: 'path',
                            value: '/modules/article/packshow.php'
                        }],
                        async func() {
                            const pool = new FunctionLoader.FuncPool();
                            debugging.catchPoolErrors(pool);
                            await pool.load([
                                {
                                    id: 'bookinfo',
                                    desc: '书籍介绍页',
                                    checkers: [{
                                        type: 'regpath',
                                        value: /\/book\/\d+\.htm/
                                    }, {
                                        type: 'startpath',
                                        value: '/modules/article/articleinfo.php'
                                    }],
                                    detectDom: '.main.m_foot',
                                    async func() {
                                        // 检查是否需要解锁
                                        if ($('#content>div:first-child fieldset>legend>b')) { return; }

                                        // 需要解锁,创建下载页面入口
                                        const aid = new URLSearchParams(location.search).get('id') ?? location.href.match(/book\/(\d+)\.htm/)[1];
                                        const bookinfo = await api.getNovelShortInfo({
                                            aid, lang: utils.getLanguage()
                                        });
                                        const title = bookinfo.querySelector('[name="Title"]').firstChild.nodeValue;
                                        const div = $$CrE({
                                            tagName: 'div',
                                            attrs: {
                                                style: 'margin:0px auto;overflow:hidden;'
                                            }
                                        });
                                        div.innerHTML = `
                                            <fieldset style="width:820px;height:35px;margin:0px auto;padding:0px;">
                                            <legend><b>《${title}》小说TXT、UMD、JAR电子书下载</b></legend>
                                                <div style="width:210px; float:left; text-align:center;"><a href="https://${ location.host }/modules/article/packshow.php?id=${aid}&amp;type=txt">TXT简繁分卷</a></div>
                                                <div style="width:210px; float:left; text-align:center;"><a href="https://${ location.host }/modules/article/packshow.php?id=${aid}&amp;type=txtfull">TXT简繁全本</a></div>
                                                <div style="width:210px; float:left; text-align:center;"><a href="https://${ location.host }/modules/article/packshow.php?id=${aid}&amp;type=umd">UMD全本下载</a></div>
                                                <div style="width:190px; float:left; text-align:center;"><a href="https://${ location.host }/modules/article/packshow.php?id=${aid}&amp;type=jar">JAR全本下载</a></div>
                                            </fieldset>
                                        `;
                                        $('#content>div:first-child').insertAdjacentElement('beforeend', div);
                                    }
                                },
                                {
                                    id: 'download',
                                    desc: '下载页',
                                    checkers: {
                                        type: 'startpath',
                                        value: '/modules/article/packshow.php'
                                    },
                                    async func() {
                                        /*
                                        页面加载思路:
                                        1. 在锁定的页面引入iframe,导航至文学少女的对应packshow页面
                                        2. iframe内运行的脚本实例负责将此页面修改为对应书籍的页面

                                        因此需要加载两个不同oFunc:
                                        1. 检测到锁定页面内容,引入iframe
                                        2. 检测到外部为锁定页面的文学少女iframe,修改页面内容
                                        */
                                        const pool = new FunctionLoader.FuncPool();
                                        debugging.catchPoolErrors(pool);
                                        await pool.load([
                                            {
                                                id: 'outer',
                                                desc: '外部锁定页面',
                                                checkers: {
                                                    type: 'switch',
                                                    value: isLockedPage(utils.window)
                                                },
                                                detectDom: '.blocknote, .main.m_foot',
                                                func() {
                                                    Quasar.Loading.show({ message: CONST.Text.Unlocker.ConstructingPage });
                                                    const search = new URLSearchParams(location.search);
                                                    const url = new URL(location.href);
                                                    search.set('id', CONST.Internal.UnlockTemplateAID.toString());
                                                    url.search = search.toString();
                                                    const iframe = $$CrE({
                                                        tagName: 'iframe',
                                                        props: {
                                                            src: url.href
                                                        },
                                                        styles: {
                                                            position: 'fixed',
                                                            top: '0',
                                                            left: '0',
                                                            width: '100vw',
                                                            height: '100vh',
                                                            border: '0',
                                                            padding: '0',
                                                            margin: '0',
                                                            background: 'white',
                                                            zIndex: '-1',
                                                            opacity: '0.001',
                                                        },
                                                        listeners: [['load', e => {
                                                            Quasar.Loading.hide();
                                                            iframe.style.zIndex = '1';
                                                            iframe.style.opacity = '1';
                                                            document.body.style.overflow = 'hidden';
                                                        }]]
                                                    });
                                                    document.body.append(iframe);
                                                }
                                            },
                                            {
                                                id: 'inner',
                                                desc: '内部《文学少女》页面',
                                                checkers: {
                                                    type: 'func',
                                                    value() {
                                                        const in_iframe = utils.window.top !== utils.window;
                                                        const id_corrent = new URLSearchParams(location.search).get('id') === CONST.Internal.UnlockTemplateAID.toString();
                                                        const outer_locked = isLockedPage(utils.window.top);
                                                        return in_iframe && id_corrent && outer_locked;
                                                    },
                                                },
                                                async func() {
                                                    Quasar.Loading.show({ message: CONST.Text.Unlocker.FetchingDownloadInfo });

                                                    // 获取书籍信息
                                                    const aid = new URLSearchParams(utils.window.top.location.search).get('id');
                                                    const lang = utils.getLanguage();
                                                    const [templateinfo, bookinfo, bookindex] = await Promise.all([
                                                        api.getNovelFullMeta({ aid: CONST.Internal.UnlockTemplateAID, lang }),
                                                        api.getNovelFullMeta({ aid, lang }),
                                                        api.getNovelIndex({ aid, lang })
                                                    ]);
                                                    const template_title = templateinfo.querySelector('[name="Title"]').firstChild.nodeValue;
                                                    const book_title = $(bookinfo, '[name="Title"]').firstChild.nodeValue;
                                                    const book_update = $(bookinfo, '[name="LastUpdate"]').getAttribute('value');
                                                    const book_length = $(bookinfo, '[name="BookLength"]').getAttribute('value');

                                                    // 处理页面内导航
                                                    $AEL(document, 'click', function(event) {
                                                        const anchor = event.target.closest('a');
                                                        if (anchor && anchor.href && !anchor.target) {
                                                            if (!event.ctrlKey && !event.metaKey && !event.shiftKey) {
                                                                event.preventDefault();
                                                                window.top.location.href = anchor.href;
                                                            }
                                                        }
                                                    }, true);

                                                    // 页面标题
                                                    utils.window.top.document.title = document.title.replaceAll(template_title, book_title);

                                                    // 所有指向《文学少女》的链接改为指向目标书籍
                                                    detectDom({
                                                        selector: 'a',
                                                        callback(a) {
                                                            const template_pathname = `/book/${CONST.Internal.UnlockTemplateAID}.htm`;
                                                            if (a.pathname === template_pathname) {
                                                                a.pathname = `/book/${aid}.htm`;
                                                            }
                                                        }
                                                    });

                                                    // 下载表格表头标题书名改为目标书籍书名
                                                    (await detectDom('#content>table>caption>a')).innerText = book_title;

                                                    // 重建下载列表
                                                    [...$All('#content>table tr:not(:first-of-type)')].forEach(tr => tr.remove());
                                                    const tbody = $('#content>table>tbody');
                                                    const type = new URLSearchParams(location.search).get('type');
                                                    const list_builders = {
                                                        async txt() {
                                                            for (const volume of $All(bookindex, 'volume')) {
                                                                const volume_title = volume.firstChild.nodeValue;
                                                                const vid = volume.getAttribute('vid');
                                                                const tr = $CrE('tr');
                                                                tr.innerHTML = `
                                                                    <td class="odd">${ volume_title }</td>
                                                                    <td class="even" align="center">
                                                                        <a href="https://dl.wenku8.com/packtxt.php?aid=${ aid }&amp;vid=${ vid }&amp;charset=gbk" target="_blank">简体(G)</a> 
                                                                        <a href="https://dl.wenku8.com/packtxt.php?aid=${ aid }&amp;vid=${ vid }&amp;charset=utf-8" target="_blank">简体(U)</a> 
                                                                        <a href="https://dl.wenku8.com/packtxt.php?aid=${ aid }&amp;vid=${ vid }&amp;charset=big5" target="_blank">繁体(U)</a>
                                                                    </td>
                                                                    <td class="even" align="center">
                                                                        <a href="https://dl.wenku8.com/packtxt.php?aid=${ aid }&amp;vid=${ vid }&amp;aname=${ $URL.encode(book_title) }&amp;vname=${ $URL.encode(volume_title) }&amp;charset=gbk" target="_blank">简体(G)</a> 
                                                                        <a href="https://dl.wenku8.com/packtxt.php?aid=${ aid }&amp;vid=${ vid }&amp;aname=${ $URL.encode(book_title) }&amp;vname=${ $URL.encode(volume_title) }&amp;charset=utf-8" target="_blank">简体(U)</a> 
                                                                        <a href="https://dl.wenku8.com/packtxt.php?aid=${ aid }&amp;vid=${ vid }&amp;aname=${ $URL.encode(book_title) }&amp;vname=${ $URL.encode(volume_title) }&amp;charset=big5" target="_blank">繁体(U)</a>
                                                                    </td>
                                                                `;
                                                                tbody.append(tr);
                                                            }
                                                        },
                                                        async txtfull() {
                                                            const tr = $CrE('tr');
                                                            tr.innerHTML = `
                                                                <td class="odd" align="center">${ book_update }</td>
                                                                <td class="even" align="center">${ Math.round(book_length * 2 / 1024) }K(G版) / ${ Math.round(book_length * 3 / 1024) }K(U版)</td>
                                                                
                                                                <td class="even" align="center">
                                                                    简体(G)(<a href="https://dl.wenku8.com/down.php?type=txt&amp;node=1&amp;id=${ aid }" target="_blank">载点一</a> 
                                                                    <a href="https://dl.wenku8.com/down.php?type=txt&amp;node=2&amp;id=${ aid }" target="_blank">载点二</a>)

                                                                    简体(U)(<a href="https://dl.wenku8.com/down.php?type=utf8&amp;node=1&amp;id=${ aid }" target="_blank">载点一</a> 
                                                                    <a href="https://dl.wenku8.com/down.php?type=utf8&amp;node=2&amp;id=${ aid }" target="_blank">载点二</a>)

                                                                    繁体(U)(<a href="https://dl.wenku8.com/down.php?type=big5&amp;node=1&amp;id=${ aid }" target="_blank">载点一</a> 
                                                                    <a href="https://dl.wenku8.com/down.php?type=big5&amp;node=2&amp;id=${ aid }" target="_blank">载点二</a>)
                                                                </td>
                                                            `;
                                                            tbody.append(tr);
                                                        },
                                                        async umd() {
                                                            const tr = $CrE('tr');
                                                            tr.innerHTML = `
                                                                <td class="odd" align="center">全本</td>
                                                                    <td class="even" align="center">未知</td>
                                                                    <td class="odd" align="center">${ book_update }</td>
                                                                    <td class="odd">${ $(bookindex, 'volume:first-of-type').firstChild.nodeValue } - ${ $(bookindex, 'volume:last-of-type').firstChild.nodeValue }</td>
                                                                    <td class="even" align="center"><a href="https://dl.wenku8.com/down.php?type=umd&amp;id=${ aid }&amp;vsize=0&amp;vid=1" target="_blank">下载UMD</a>
                                                                </td>
                                                            `;
                                                            tbody.append(tr);
                                                        },
                                                        async jar() {
                                                            const tr = $CrE('tr');
                                                            tr.innerHTML = `
                                                                <td class="odd" align="center">全本</td>
                                                                <td class="even" align="center">未知</td>
                                                                <td class="odd" align="center">${ book_update }</td>
                                                                <td class="odd">${ $(bookindex, 'volume:first-of-type').firstChild.nodeValue } - ${ $(bookindex, 'volume:last-of-type').firstChild.nodeValue }</td>
                                                                <td class="even" align="center"><a href="https://dl.wenku8.com/down.php?type=jar&amp;id=${ aid }&amp;vsize=0&amp;vid=1" target="_blank">下载JAR</a> <a href="https://dl.wenku8.com/down.php?type=jad&amp;id=${ aid }&amp;vsize=0&amp;vid=1" target="_blank">下载JAD</a></td>
                                                            `;
                                                            tbody.append(tr);
                                                        },
                                                    };
                                                    await list_builders[type]();

                                                    Quasar.Loading.hide();
                                                }
                                            }
                                        ]);

                                        /**
                                         * 判断给定页面是否为锁定的下载页面
                                         * @param {Window} win 
                                         * @returns {boolean}
                                         */
                                        function isLockedPage(win) {
                                            const path_correct = win.location.pathname.startsWith('/modules/article/packshow.php');
                                            const messages = [
                                                '错误原因:对不起,该文章不存在!',
                                                '錯誤原因︰對不起,該文章不存在!'
                                            ]
                                            const content_correct = messages.some(message => win.document.body.innerText.includes(message));
                                            return path_correct && content_correct;
                                        }
                                    }
                                }
                            ]);
                        }
                    }
                ]);
            }
        },
        darkmode: {
            desc: '深色模式',
            css: [
                // Common
                {
                    id: 'common',
                    checker: {
                        type: 'switch',
                        value: true,
                    },
                    css: 'body.plus-darkmode:not(#stonger-than-quasar) {background-color: #222222;color: #C8C8C8;}'
                },

                // Mouse tip
                {
                    id: 'mousetip',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\//
                    },
                    css: '.plus-darkmode #tips {background-color: #333333;color: #f0f7ff;border: 1px solid #0d548b;}'
                },

                // .block
                {
                    id: 'block',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\//
                    },
                    css: '.plus-darkmode :is(#left,#right,*) .blockcontent{background-color:#222222}.plus-darkmode :is(#left,#right,*) .blocknote{background-color:#282828}.plus-darkmode :is(#left,#right,#centers,*) :is(.blocktitle,.blocktitle *,.ultop li){color:#6f9ff1}.plus-darkmode :is(#left,#right,#centers,*) .blocktitle>:is(.txt,.txtr){background-color:#383838;line-height:27px;padding-top:0}.plus-darkmode .block{border:1px solid #0d548b}.plus-darkmode .blocktitle{border-color:#333333}.plus-darkmode :is(.blockcontent,.blocknote){border-color:#0d548b}.plus-darkmode .block :is(.ultop li,.ultops li){border-bottom:1px dashed #0d548b}'
                },

                // header and footer
                {
                    id: 'headfoot',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\//
                    },
                    css: '.plus-darkmode :is(.main.m_top, .nav, .navinner, .navlist, .nav li, :is(#left, #right, #centers, *) :is(.blocktitle, .blocktitle *)) {background: #333333;}.plus-darkmode :is(.nav a.current, .nav a:hover, .nav a:active) {background: #444444;}.plus-darkmode .m_foot {border-top: 1px dashed #0d548b;border-bottom: 1px dashed #0d548b;}'
                },

                // elements (input textarea .button scrollbar, etc)
                {
                    id: 'element',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\//
                    },
                    css: '.plus-darkmode :is(.even, .odd) {background-color: #222222;}.plus-darkmode table.grid td {background-color: #222222 !important;}.plus-darkmode :is(input:not([type]:not([type="text"], [type="number"], [type="file"], [type="password"])), textarea, .plus_list_item, button):not([class*="q-"]:not(body) *) {background-color: #333333;color: #DDDDDD;}.plus-darkmode :is(.button, input[type="button"]) {color: #C8C8C8;background-color: #333333;}.plus-darkmode select {color: #AAAAAA;background-color: #333333;}.plus-darkmode :is(.hottext, a.hottext) {color: #f36d55;}.plus-darkmode :is(.button, select, textarea, input:not(.plus_list_item>input, .UBB_ColorList input), .plus_list_item):not(:disabled, [class*="q-"]:not(body) *) {border: 1px solid #0d548b;}.plus-darkmode :is(input, textarea, button):disabled {border: 2px solid #444444;}.plus-darkmode a {color: #AAAAAA;}.plus-darkmode a:hover {color: #4a8dff;}.plus-darkmode a:is(.ultop li a, .poptext, a.poptext, .ultops li a) {color: #f36d55;}.plus-darkmode :is(table.grid caption, .gridtop, table.grid th, .head) {border: 1px solid #0d548b;background: #333333;color: #6f9ff1;}.plus-darkmode :is(table.grid, table.grid td) {border: 1px solid #0d548b;}.plus-darkmode input[type="checkbox"]::after {background-color: #333333;}.plus-darkmode :is(.pagelink, .pagelink a:hover) {background-color: #333333;color: #6f9ff1;}.plus-darkmode .pagelink strong {background-color: #444444;}.plus-darkmode .pagelink em {border-right: 1px solid #0d548b;}.plus-darkmode .pagelink kbd {border-left: 1px solid #0d548b;}.plus-darkmode .pagelink {border: 1px solid #0d548b;}:is(.plus-darkmode, .plus-darkmode *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement) {scrollbar-color: #444444 #333333;}:is(.plus-darkmode, .plus-darkmode *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement):hover {scrollbar-color: #484848 #333333;}:is(.plus-darkmode, .plus-darkmode *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar {background-color: #333333;}:is(.plus-darkmode, .plus-darkmode *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-corner {background-color: #333333;}:is(.plus-darkmode, .plus-darkmode *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-thumb, .plus-darkmode *:not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-button {background-color: #444444;}:is(.plus-darkmode, .plus-darkmode *):not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-thumb:hover, .plus-darkmode *:not(#NotAElement>#NotAElement>#NotAElement>#NotAElement)::-webkit-scrollbar-button:hover {background-color: #484848;}'
                },

                // dialog
                {
                    id: 'dialog',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\//
                    },
                    css: '.plus-darkmode #dialog {color: #C8C8C8;background-color: #222222;border: 5px solid #0d548b;}.plus-darkmode #dialog a[onclick="closeDialog()"] {border: 1px solid #0d548b !important;outline: thin solid #0d548b !important;}'
                },

                // replyarea
                {
                    id: 'replyarea',
                    checker: [
                        // Page: reviews list
                        '/modules/article/reviews.php',

                        // Page: review
                        '/modules/article/reviewshow.php',

                        // Page: review edit
                        '/modules/article/reviewedit.php',

                        // Page: book
                        '/book/',
                        '/modules/article/articleinfo.php',
                    ].map(p => ({
                        type: 'startpath',
                        value: p
                    })),
                    css: '.plus-darkmode form[name="frmreview"] caption {background: #333333;color: #6f9ff1;border: 1px solid #0d548b;}'
                },

                // index page
                {
                    id: 'index',
                    checker: [{
                        type: 'path',
                        value: '/index.php'
                    }, {
                        type: 'path',
                        value: '/'
                    }],
                    css: '.plus-darkmode :is(:is(#left, #right, #centers) .blockcontent, .blockcontent, .odd, .even) {background-color: #222222;color: #C8C8C8;}.plus-darkmode :is(#left, #right, #centers, *) :is(.blocktitle, .blocktitle *) a {color: #AAAAAA;}a[href^="http://tieba.baidu.com"] {color: #4a8dff !important;}',
                },

                // login page
                {
                    id: 'login',
                    checker: {
                        type: 'path',
                        value: '/login.php'
                    },
                    css: ''
                },

                // Book
                {
                    id: 'book',
                    checker: [{
                        type: 'regpath',
                        value: /\/book\/\d+\.htm/
                    }, {
                        type: 'regpath',
                        value: /\/modules\/article\/articleinfo\.php/
                    }],
                    css: '.plus-darkmode :is(:is(#left, #right, #centers) .blockcontent, .blockcontent, .odd, .even) {background-color: #222222;color: #C8C8C8;}.plus-darkmode table.grid td {background-color: #222222 !important;}.plus-darkmode table.grid:not(form table) tr:first-of-type>td:nth-of-type(2n+1) {background-color: #333333 !important;}.plus-darkmode table.grid:not(form table, #content .main>table:first-of-type) tr:first-of-type>td:first-of-type {color: #6f9ff1;}.plus-darkmode fieldset {border: 2px solid #0d548b;}.plus-darkmode :is(table.grid, table.grid td, table.grid caption, .gridtop) {border: 1px solid #0d548b;}'
                },

                // Book index
                {
                    id: 'bookindex',
                    checker: [{
                        type: 'regpath',
                        value: /^\/novel\/\d+\/\d+\/index\.html?$/
                    }, {
                        type: 'path',
                        value: '/modules/article/reader.php'
                    }],
                    css: '.plus-darkmode :is(.css, .vcss, .ccss) {background-color: #222222;color: #C8C8C8;}.plus-darkmode #headlink {border-bottom: 1px solid #0d548b;border-top: 1px solid #0d548b;}.plus-darkmode :is(.css, .vcss, .ccss) {border: 1px solid #0d548b;border-collapse: collapse;}'
                },

                // Novel
                {
                    id: 'novel',
                    checker: {
                        type: 'func',
                        value: () => {
                            return location.pathname.startsWith('/novel/') && location.pathname.split('/').pop() !== 'index.htm';
                        }
                    },
                    css: '.plus-darkmode a {color: #4a8dff;} .plus-darkmode #content {color: rgb(200, 200, 200) !important;}'
                },

                // Reviewshow
                {
                    id: 'reviewshow',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\/modules\/article\/reviewshow\.php\/?/
                    },
                    css: '.plus-darkmode table.grid td {background-color: #222222;}.plus-darkmode :is(#content table.grid hr, #content>table:nth-of-type(2) th, #pagelink) {border: 1px solid #0d548b;}.plus-darkmode :is(.jieqiQuote, .jieqiCode, .jieqiNote) {background-color: #282828;color: #6f9ff1;border: 1px solid #0d548b;}'
                },

                // frmreview
                {
                    id: 'frmreview',
                    checker: [
                        // Page: reviews list
                        '/modules/article/reviews.php',

                        // Page: review
                        '/modules/article/reviewshow.php',

                        // Page: review edit
                        '/modules/article/reviewedit.php',

                        // Page: book
                        '/book/',
                        '/modules/article/articleinfo.php',
                    ].map(p => ({
                        type: 'startpath',
                        value: p
                    })),
                    css: '.plus-darkmode .UBB_FontSizeList li {border: 1px solid #0d548b;}.plus-darkmode .UBB_ColorList :is(table, table td) {border: 1px solid #0d548b;}.plus-darkmode .UBB_ColorList {background-color: #222222;}'
                },

                /* Template
                {
                    id: '',
                    checker: {
                        type: 'regurl',
                        value: /^https?:\/\/www\.wenku8\.(net|cc)\//
                    },
                    css: ''
                },
                */
            ],
            dependencies: ['dependencies', 'utils', 'debugging', 'configs'],
            params: ['oFunc', 'GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            /** @typedef {Awaited<ReturnType<typeof functions.darkmode.func>>} darkmode */
            async func(oFunc, GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {configs} */
                const configs = require('configs');
                /** @type {utils} */
                const utils = require('utils');
                /** @type {debugging} */
                const debugging = require('debugging');

                // 带默认值的GM_getValue
                GM_getValue = utils.defaultedGet({
                    enabled: false,
                    follow_system: false,
                    sidebutton: true,
                }, GM_getValue);

                // 设置项、配置存储管理 与 基于设置项的功能切换
                configs.registerConfig('darkmode', {
                    GM_addValueChangeListener,
                    label: CONST.Text.Darkmode.Settings.Label,
                    items: [{
                        type: 'boolean',
                        label: CONST.Text.Darkmode.Settings.Enbaled,
                        caption: CONST.Text.Darkmode.Settings.EnabledCaption,
                        key: 'enabled',
                        get() { return isEnabled(); },
                        set(val) { setEnabled(val); },
                    }, {
                        type: 'boolean',
                        label: CONST.Text.Darkmode.Settings.FollowSystem,
                        caption: CONST.Text.Darkmode.Settings.FollowSystemCaption,
                        key: 'follow_system',
                        get() { return isFollow(); },
                        set(val) { setFollow(val); }
                    }, {
                        type: 'boolean',
                        label: CONST.Text.Darkmode.Settings.SideButton,
                        caption: CONST.Text.Darkmode.Settings.SideButtonCaption,
                        key: 'sidebutton',
                        get() { return GM_getValue('sidebutton'); },
                        set(val) { return GM_setValue('sidebutton', val); },
                    }],
                    listeners: {
                        enabled: applyDarkmode,
                        follow_system: applyDarkmode,
                        sidebutton(key, old_val, val, remote) {
                            updateSideButton(val);
                        }
                    },
                });

                // 应用合适的样式表到页面
                oFunc.css.forEach(css => {
                    FunctionLoader.testCheckers(css.checker) && addStyle(css.css, `darkmode-${css.id}`);
                });

                // 深色模式切换回调列表
                /** @type {((enabled: boolean) => any)[]} */
                const listeners = [];

                // 根据配置切换深色模式
                applyDarkmode();

                // 每当系统深色模式切换时,重新根据配置切换深色模式
                const darkmode_mediaquery = window.matchMedia('(prefers-color-scheme: dark)');
                $AEL(darkmode_mediaquery, 'change', e => applyDarkmode());

                // 侧边栏添加深色模式开关
                require('sidepanel', true).then(() => updateSideButton());


                /**
                 * 检查深色模式是否开启
                 * @returns {boolean}
                 */
                function isEnabled() {
                    return GM_getValue('enabled');
                }

                /**
                 * 设置深色模式开启状态,并应用到页面
                 * @param {boolean} enabled - 深色模式是否开启
                 */
                function setEnabled(enabled) {
                    GM_setValue('enabled', enabled);
                }

                /**
                 * 检查深色模式是否跟随系统
                 * @returns {boolean}
                 */
                function isFollow() {
                    return GM_getValue('follow_system');
                }

                /**
                 * 设置深色模式跟随系统,并应用到页面
                 * @param {boolean} follow - 深色模式是否跟随系统
                 */
                function setFollow(follow) {
                    GM_setValue('follow_system', follow);
                }

                /**
                 * 根据设置综合计算是否应用深色模式,并应用更改到页面;当实际发生更改时,回调listeners
                 */
                function applyDarkmode() {
                    const enabled = isActualDark();
                    const cur_enabled = document.body.classList.contains('plus-darkmode');

                    if (cur_enabled !== enabled) {
                        document.body.classList[enabled ? 'add' : 'remove']('plus-darkmode');
                        require('dependencies', true).then(() => Quasar.Dark.set(enabled));
                        listeners.forEach(listener => debugging.callWithErrorHandling(listener, null, [enabled]));
                    }
                }

                /**
                 * 获取各种设置综合效果下的**实际深色模式启用状态**
                 * @returns {boolean}
                 */
                function isActualDark() {
                    return isFollow() ? getSystemDarkmode() : isEnabled();
                }

                /**
                 * 检测系统深色模式是否开启
                 * @returns {boolean}
                 */
                function getSystemDarkmode() {
                    return window.matchMedia('(prefers-color-scheme: dark)').matches;
                }

                /**
                 * 切换是否展示深色模式开关按钮
                 * @param {boolean} [show_button] - 是否展示开关按钮,不提供时使用存储的配置
                 */
                async function updateSideButton(show_button) {
                    /** @type {sidepanel} */
                    const sidepanel = await require('sidepanel', true);
                    show_button = show_button ?? GM_getValue('sidebutton');

                    if (show_button) {
                        sidepanel.hasButton('darkmode.toggle') || sidepanel.registerButton({
                            id: 'darkmode.toggle',
                            icon: isEnabled() ? 'light_mode' : 'dark_mode',
                            label: isEnabled() ? CONST.Text.Darkmode.Switch2Light : CONST.Text.Darkmode.Switch2Dark,
                            index: 1,
                            callback() {
                                const enabled = !isEnabled();
                                sidepanel.updateButton('darkmode.toggle', {
                                    icon: enabled ? 'light_mode' : 'dark_mode',
                                    label: enabled ? CONST.Text.Darkmode.Switch2Light : CONST.Text.Darkmode.Switch2Dark
                                });
                                setEnabled(enabled);

                                if (isFollow()) {
                                    Quasar.Notify.create({
                                        type: 'warning',
                                        message: CONST.Text.Darkmode.FollowEnabledTip,
                                        caption: CONST.Text.Darkmode.FollowEnabledTipCaption,
                                        group: 'darkmode.darkmode-tip',
                                    });
                                }
                            }
                        });
                    } else {
                        sidepanel.hasButton('darkmode.toggle') && sidepanel.removeButton('darkmode.toggle');
                    }
                }

                /**
                 * 注册(不可用)当页面实际深色/浅色模式进行切换时的回调
                 * @param {(enabled: boolean) => any} callback - 页面实际深色/浅色模式切换的回调,参数为深色模式是否开启
                 */
                function onToggle(callback) {
                    listeners.push(callback);
                }

                /**
                 * 根据url,筛选出属于此页面的css样式列表
                 * @param {string} url - 页面url
                 * @returns {string[]} - 全部样式css的数组
                 */
                function getPageCSS(url) {
                    return oFunc.css.filter(css => FunctionLoader.testCheckers(css.checker)).map(css => css.css);
                }

                /**
                 * 获取指定的css样式字符串
                 * @param {string} id - css样式的id
                 */
                function getCSS(id) {
                    return oFunc.css.find(css => css.id === id).css;
                }

                /**
                 * 将指定的css作为<style>元素添加到指定的父元素中
                 * @param {string} id - css样式的id
                 * @param {HTMLElement} parent - 父元素
                 */
                function applyCSS(id, parent) {
                    addStyle(parent, getCSS(id));
                }

                return {
                    get enabled() { return isEnabled(); },
                    set enabled(val) { setEnabled(val); },
                    get follow_system() { return isFollow(); },
                    set follow_system(val) { setFollow(val); },
                    get actual_enabled() { return isActualDark(); },

                    onToggle, getPageCSS, getCSS, applyCSS,
                };
            }
        },
        review: {
            desc: '书评页面增强',
            checkers: {
                type: 'path',
                value: '/modules/article/reviewshow.php'
            },
            dependencies: ['dependencies', 'debugging', 'utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            /** @typedef {Awaited<ReturnType<typeof functions.review.func>>} review */
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                // 如果是发评论返回的提示页面,不继续运行
                if ($All('.block').length === 1) { return; }

                // 注册(不可用)设置组
                configs.registerConfig('review', {
                    GM_addValueChangeListener,
                    label: CONST.Text.Review.Settings.Label
                });

                /*
                通信信使,通过CustomEvent传递消息,目前有以下事件:
                - update  
                  代表当前页面内容被更新,有楼层被更新,或有新楼层加入页面
                  - floors
                    被更新或者新增的楼层实例
                */
                const messager = new EventTarget();

                /**
                 * 书评页面每条评论称为一个楼层,即Floor
                 * Floor类型表示一个楼层,一条评论
                 * @typedef {Object} Floor
                 * @property {FloorElement} element
                 * @property {FloorData} data
                 */
                /**
                 * {@link Floor} 类型中的页面元素
                 * @typedef {Object} FloorElement
                 * @property {HTMLTableElement} root - table根元素
                 * @property {HTMLTableCellElement} userarea - 左侧用户区
                 * @property {HTMLImageElement} avatar - 用户头像图片元素
                 * @property {HTMLAnchorElement} userlink - 用户名链接
                 * @property {FloorUserLine[]} userlines - 用户区域中的用户信息行结合
                 * @property {FloorButton[]} userbuttons - 用户相关操作按钮集合
                 * @property {HTMLTableCellElement} contentarea - 右侧内容区
                 * @property {HTMLElement} title - 标题strong元素
                 * @property {FloorButton[]} floorbuttons - 楼层相关操作按钮集合
                 * @property {HTMLDivElement} metaarea - 楼层相关操作按钮,以及楼层时间所在容器
                 * @property {HTMLDivElement} content - 内容正文区
                 */
                /**
                 * {@link FloorElement} 类型中的操作按钮
                 * @typedef {Object} FloorButton
                 * @property {string} id - 按钮ID,全局唯一
                 * @property {boolean} wenku - 是否为文库自带按钮
                 * @property {number} index - 按钮排序位置,文库自带按钮均为负数,新添加按钮均为正数,升序排列
                 * @property {HTMLElement} element - 按钮DOM元素
                 */
                /**
                 * {@link FloorElement} 类型中的用户信息行
                 * @typedef {Object} FloorUserLine
                 * @property {string} id - 行ID,全局唯一
                 * @property {boolean} wenku - 是否为文库自带行
                 * @property {HTMLElement | Text} element - 行DOM节点
                 */
                /**
                 * {@link Floor} 类型中的楼层数据
                 * @typedef {Object} FloorData
                 * @property {FloorUser} user - 层主用户数据
                 * @property {string} title - 楼层标题
                 * @property {string} content - 楼层内容
                 * @property {number} time - 楼层时间戳
                 * @property {string} url - 楼层链接
                 * @property {number} rid - 书评id
                 * @property {number} yid - 楼层id
                 * @property {number} number - 楼层编号
                 * @property {boolean} highlight - 是否对楼层应用了高亮效果
                 */
                /**
                 * {@link FloorData} 类型中的用户数据
                 * @typedef {Object} FloorUser
                 * @property {string} avatar - 用户头像src
                 * @property {string} name - 用户名
                 * @property {number} id - 用户数字id
                 * @property {FloorUserType} type - 用户类型
                 * @property {FloorUserLevel} level - 用户等级
                 * @property {number} jointime - 加入日期时间戳
                 * @property {number} experience - 经验
                 * @property {number} credit - 积分
                 */
                /**
                 * 用户类型
                 * @typedef {'admin' | 'user' | 'banned' | 'limited'} FloorUserType
                 */
                /**
                 * 用户等级
                 * @typedef {'newbie' | 'normal' | 'intermediate' | 'advanced' | 'golden' | 'elder'} FloorUserLevel
                 */

                const pool_funcs = {
                    FloorManager: {
                        desc: '楼层内容解析器',
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.FloorManager.func>>} FloorManager */
                        async func() {
                            const pool_funcs = {
                                parser: {
                                    desc: '楼层内容解析器',
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.parser.func>>} parser */
                                    func() {
                                        /**
                                         * 从给定文档中解析所有Floor
                                         * @param {Document} [doc=document] - 需解析的Document文档,默认为window.document,也可以是任何其他文档,如从xhr请求获取的文档
                                         * @returns {Floor[]}
                                         */
                                        function parseAll(doc = document) {
                                            return [...$All(doc, '#content > table.grid')].filter(table => $(table, 'img.avatar')).map(table => parse(table));
                                        }

                                        /**
                                         * 将楼层DOM结构解析为标准楼层对象
                                         * 仅可解析未经修改的Wenku自带DOM
                                         * @param {HTMLTableElement} table - 楼层的table根元素 
                                         * @returns {Floor}
                                         */
                                        function parse(table) {
                                            const element = parseElement(table);
                                            const data = parseData(element);
                                            return { element, data };
                                        }

                                        /**
                                         * 从楼层DOM结构解析标准楼层元素对象
                                         * 仅可解析未经修改的Wenku自带DOM
                                         * @param {HTMLTableElement} table - 楼层的table根元素
                                         * @returns {FloorElement}
                                         */
                                        function parseElement(table) {
                                            const userarea = $(table, 'td:first-of-type');
                                            const contentarea = $(table, 'td:last-of-type');
                                            const avatar = $(userarea, 'img.avatar');
                                            const userlink = $(userarea, 'strong>a');
                                            const userlines = getUserLines();
                                            const userbuttons = [{
                                                id: 'message',
                                                wenku: true,
                                                index: -2,
                                                element: $(userarea, 'a[onclick^="openDialog(\'/newmessage.php?"]')
                                            }, {
                                                id: 'detail',
                                                wenku: true,
                                                index: -1,
                                                element: $(userarea, `a[href^="https://${ location.host }/userpage.php?"]:not(strong > a)`),
                                            }];
                                            const title = $(table, 'td:last-of-type > div:nth-of-type(1) > strong');
                                            const floorbuttons = getFloorButtons();
                                            const metaarea = $(table, 'td:last-of-type > div:nth-of-type(2)');
                                            const content = $(table, 'td:last-of-type > div:nth-of-type(3)');
                                            return {
                                                root: table,
                                                userarea, contentarea, avatar,
                                                userlink, userbuttons, userlines,
                                                title, floorbuttons, metaarea,
                                                content,
                                            }

                                            function getUserLines() {
                                                // 获取userlink后的第index个textnode的方法,index从1开始
                                                const getTextNode = index => {
                                                    let elm = userlink.parentElement;
                                                    for (let i = 0; i < index; i++) {
                                                        elm = elm.nextElementSibling;
                                                    }
                                                    return elm.nextSibling;
                                                }
                                                const getText = index => getTextNode(index).nodeValue.trim();

                                                /** @type {FloorUserLine[]} */
                                                const lines = [{
                                                    id: 'type',
                                                    wenku: true,
                                                    element: getTextNode(1)
                                                }, {
                                                    id: 'level',
                                                    wenku: true,
                                                    element: getTextNode(2)
                                                }, {
                                                    id: 'jointime',
                                                    wenku: true,
                                                    element: getTextNode(3)
                                                }, {
                                                    id: 'experience',
                                                    wenku: true,
                                                    element: getTextNode(4)
                                                }, {
                                                    id: 'credit',
                                                    wenku: true,
                                                    element: getTextNode(5)
                                                }];

                                                return lines;
                                            }

                                            function getFloorButtons() {
                                                const floorbuttons = [{
                                                    id: 'link',
                                                    wenku: true,
                                                    element: $(table, 'td:last-of-type > div:nth-of-type(2) > a[href^="#yid"]'),
                                                }];
                                                const edit = $(table, 'td:last-of-type > div:nth-of-type(2) > a[href*="/modules/article/reviewedit.php?yid="]');
                                                edit && floorbuttons.push({
                                                    id: 'edit',
                                                    wenku: true,
                                                    index: -1,
                                                    element: edit,
                                                });
                                                return floorbuttons;
                                            }
                                        }

                                        /**
                                         * 从楼层元素对象解析楼层数据
                                         * 仅可解析未经修改的Wenku自带DOM
                                         * @param {FloorElement} element - 楼层元素对象
                                         * @returns {FloorData}
                                         */
                                        function parseData(element) {
                                            const getLineText = line_id => element.userlines.find(l => l.id === line_id).element.nodeValue.trim();
                                            /** @type {FloorUser} */
                                            const user = {
                                                avatar: element.avatar.src,
                                                name: element.userlink.innerText,
                                                id: new URL(element.userlink.href).searchParams.get('uid'),
                                                type: utils.getUserType(getLineText('type')),
                                                level: utils.getUserLevel(getLineText('level')),
                                                jointime: new Date(getLineText('jointime').match(/\d+-\d+-\d+/)[0]).getTime(),
                                                experience: parseInt(getLineText('experience').match(/\d+/)[0], 10),
                                                credit: parseInt(getLineText('credit').match(/\d+/)[0], 10),
                                            };
                                            const title = element.title.innerText;
                                            const content = parseContent(element.content);
                                            const link_elm = element.floorbuttons.find(b => b.id === 'link').element;
                                            const last_floor_button = element.floorbuttons[element.floorbuttons.length-1].element;
                                            const time = new Date(last_floor_button.previousSibling.nodeValue.match(/\d+-\d+-\d+ +\d+:\d+:\d+/)[0]).getTime();
                                            const url = link_elm.href;
                                            const rid = parseInt(new URLSearchParams(link_elm.search).get('rid'), 10);
                                            const yid = parseInt(link_elm.hash.match(/\d+/)[0], 10);
                                            const number = parseInt(link_elm.innerText.match(/\d+/)[0], 10);
                                            const highlight = false;
                                            return { user, title, content, time, url, rid, yid, number, highlight };
                                        }

                                        // Get floor content by BBCode format (content only, no title)
                                        // Argv: <div> content element
                                        /**
                                         * 从正文内容div的DOM结构中解析bbcode源代码
                                         * @param {HTMLDivElement} content_elm 
                                         * @param {boolean} [use_img_tag=false] 
                                         * @returns {string}
                                         */
                                        function parseContent(content_elm, use_img_tag=false) {
                                            const subNodes = content_elm.childNodes;
                                            let content = '';

                                            for (const node of subNodes) {
                                                const type = node.nodeName;
                                                switch (type) {
                                                    case '#text':
                                                        // Prevent 'Quote:' repeat
                                                        content += node.data.replace(/^\s*Quote:\s*$/, ' ');
                                                        break;
                                                    case 'IMG':
                                                        // wenku8 has forbidden [img] tag for secure reason (preventing CSRF)
                                                        //content += '[img]S[/img]'.replace('S', node.src);
                                                        content += use_img_tag ? '[img]S[/img]'.replace('S', node.src) : ' S '.replace('S', node.src);
                                                        break;
                                                    case 'A':
                                                        content += '[url=U]T[/url]'.replace('U', node.getAttribute('href')).replace('T', parseContent(node));
                                                        break;
                                                    case 'BR':
                                                        // no need to add \n, because \n will be preserved in #text nodes
                                                        //content += '\n';
                                                        break;
                                                    case 'DIV':
                                                        if (node.classList.contains('jieqiQuote')) {
                                                            content += getTagedSubcontent('quote', node);
                                                        } else if (node.classList.contains('jieqiCode')) {
                                                            content += getTagedSubcontent('code', node);
                                                        } else if (node.classList.contains('divimage')) {
                                                            content += parseContent(node, use_img_tag);
                                                        } else {
                                                            content += parseContent(node, use_img_tag);
                                                        }
                                                        break;
                                                    case 'CODE': content += parseContent(node, use_img_tag); break; // Just ignore
                                                    case 'PRE':  content += parseContent(node, use_img_tag); break; // Just ignore
                                                    case 'SPAN': content += getFontedSubcontent(node); break; // Size and color
                                                    case 'P':    content += getFontedSubcontent(node); break; // Text Align
                                                    case 'B':    content += getTagedSubcontent('b', node); break;
                                                    case 'I':    content += getTagedSubcontent('i', node); break;
                                                    case 'U':    content += getTagedSubcontent('u', node); break;
                                                    case 'DEL':  content += getTagedSubcontent('d', node); break;
                                                    default:     content += parseContent(node, use_img_tag); break;
                                                }
                                            }
                                            return content;

                                            function getTagedSubcontent(tag, node) {
                                                const subContent = parseContent(node, use_img_tag);
                                                return '[{T}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{S}', subContent);
                                            }

                                            function getFontedSubcontent(node) {
                                                let tag, value;

                                                let strSize = node.style.fontSize.match(/\d+/);
                                                let strColor = node.style.color;
                                                let strAlign = node.align;
                                                strSize = strSize ? strSize[0] : null;
                                                strColor = strColor ? rgbToHex.apply(null, strColor.match(/\d+/g)) : null;

                                                tag = tag || (strSize  ? 'size'  : null);
                                                tag = tag || (strColor ? 'color' : null);
                                                tag = tag || (strAlign ? 'align' : null);
                                                value = value || strSize || null;
                                                value = value || strColor || null;
                                                value = value || strAlign || null;

                                                const subContent = parseContent(node, use_img_tag);
                                                if (tag && value) {
                                                    return '[{T}={V}]{S}[/{T}]'.replaceAll('{T}', tag).replaceAll('{V}', value).replaceAll('{S}', subContent);
                                                } else {
                                                    return subContent;
                                                }

                                                function rgbToHex(r, g, b) {return ((r << 16) | (g << 8) | b).toString(16).padStart('0', 6);}
                                            }
                                        }

                                        /**
                                         * 根据id获取一个楼层操作按钮
                                         * @param {Floor} floor 
                                         * @param {string} id 
                                         * @returns {FloorButton | null}
                                         */
                                        function getFloorButton(floor, id) {
                                            return floor.element.floorbuttons.find(b => b.id === id);
                                        }

                                        /**
                                         * 根据id获取一个用户操作按钮
                                         * @param {Floor} floor 
                                         * @param {string} id 
                                         * @returns {FloorButton | null}
                                         */
                                        function getUserButton(floor, id) {
                                            return floor.element.userbuttons.find(b => b.id === id);
                                        }

                                        /**
                                         * 根据id获取一个用户信息行
                                         * @param {Floor} floor 
                                         * @param {string} id 
                                         * @returns {FloorUserLine | null}
                                         */
                                        function getUserLine(floor, id) {
                                            return floor.element.userlines.find(l => l.id === id);
                                        }

                                        return {
                                            parse, parseAll, parseContent,
                                            getFloorButton, getUserButton, getUserLine,
                                        }
                                    }
                                },
                                transformer: {
                                    desc: '楼层内容修改器',
                                    dependencies: 'parser',
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.transformer.func>>} transformer */
                                    func() {
                                        /** @type {parser} */
                                        const parser = pool.require('parser');

                                        /**
                                         * 在楼层右上角按钮处新增一个按钮
                                         * @param {Floor} floor 
                                         * @param {Object} options
                                         * @param {string} options.id 
                                         * @param {string} options.label 
                                         * @param {number} options.index - 按钮排序位置,仅在非文库自带按钮间排序,文库按钮均在非文库按钮之前
                                         * @param {function} [options.callback] - 按钮点击回调,和element二选一
                                         * @param {function} [options.element] - 按钮元素,和callback二选一
                                         * @returns {FloorButton}
                                         */
                                        function addFloorButton(floor, { id, label, index, callback=null, element=null }) {
                                            const floorbuttons = floor.element.floorbuttons;

                                            // 创建按钮元素
                                            const elm = element ?? $$CrE({
                                                tagName: 'span',
                                                props: {
                                                    innerText: label
                                                },
                                                listeners: [['click', e => callback()]]
                                            });
                                            elm.style.color = 'var(--q-primary)';
                                            elm.style.cursor = 'pointer';

                                            // 记录当前页面上最右侧(第一个)按钮以及其右侧#text
                                            const first_button = floorbuttons[0];
                                            const first_button_sibling = first_button.element.nextSibling;

                                            // 添加按钮数据并按照index排序
                                            const button = {
                                                id,
                                                wenku: false,
                                                element: elm
                                            };
                                            floorbuttons.push(button);
                                            floorbuttons.sort((b1, b2) => b1.index - b2.index);

                                            // 将所有按钮按照新顺序重新添加到页面
                                            floorbuttons.forEach(btn => {
                                                // 依次移除所有按钮
                                                if (btn.element.closest('body') === document.body) {
                                                    // 当按钮不是先前的最右侧按钮时,把右边的" | "也移除掉
                                                    btn !== first_button && btn.element.nextSibling.remove();
                                                    btn.element.remove();
                                                }
                                            });
                                            floorbuttons.forEach((btn, i) => {
                                                if (i === 0) {
                                                    // 第一个按钮添加到原先最右侧按钮右边的#text左侧
                                                    first_button_sibling.before(btn.element);
                                                } else {
                                                    // 后续按钮依次添加到上一个按钮之前(左侧)
                                                    const last_floor_button = floorbuttons[i-1];
                                                    last_floor_button.element.before(btn.element);
                                                }
                                                // 当不是第一个(最右侧)按钮时,右侧添加" | "
                                                i > 0 && btn.element.after(' | ');
                                            });

                                            return button;
                                        }

                                        /**
                                         * 在楼层左侧用户区下方新增一个按钮
                                         * @param {Floor} floor 
                                         * @param {Object} options
                                         * @param {string} options.id 
                                         * @param {string} [options.label] - 按钮文字,和element二选一
                                         * @param {number} options.index - 按钮排序位置,仅在非文库自带按钮间排序,文库按钮均在非文库按钮之前
                                         * @param {function} [options.callback] - 按钮点击回调,和element二选一
                                         * @param {function} [options.element] - 按钮元素,和callback二选一
                                         * @returns {FloorButton}
                                         */
                                        function addUserButton(floor, { id, label = null, index, callback=null, element=null }) {
                                            // 创建/装饰按钮元素
                                            /** @type {HTMLDivElement} */
                                            const container = floor.element.avatar.parentElement;
                                            const elm = element ?? $$CrE({
                                                tagName: 'span',
                                                props: { innerText: label },
                                                listeners: [['click', e => callback()]]
                                            });
                                            elm.style.color = 'var(--q-primary)';
                                            elm.style.cursor = 'pointer';

                                            // 添加按钮数据,按照index重新排序
                                            const button = {
                                                id,
                                                wenku: false,
                                                index,
                                                element: elm
                                            };
                                            floor.element.userbuttons.push(button);
                                            floor.element.userbuttons.sort((b1, b2) => b1.index - b2.index);

                                            // 将所有按钮按照新顺序重新添加到页面
                                            const userbuttons = floor.element.userbuttons;
                                            userbuttons.forEach(btn => {
                                                if (btn.element.closest('body') === document.body) {
                                                    const prev = btn.element.previousSibling;
                                                    ['#text', 'BR'].includes(prev.nodeName) && prev.remove();
                                                    btn.element.remove();
                                                }
                                            });
                                            userbuttons.forEach((btn, i) => {
                                                const number = i + 1;
                                                if (number % 2 === 1) {
                                                    // 每行第一个
                                                    i !== 0 && container.append($CrE('br'));
                                                    container.append(btn.element);
                                                } else {
                                                    // 每行第二个
                                                    container.append(' | ');
                                                    container.append(btn.element);
                                                }
                                            });
                                            
                                            return button;
                                        }

                                        /**
                                         * 添加一行内容到指定楼层的左侧用户区域
                                         * @param {Floor} floor - 添加到的楼层
                                         * @param {Object} options
                                         * @param {string} options.id - 全局唯一,信息行id
                                         * @param {Node | string} options.line - 添加的内容,字符串将转换为文本节点添加
                                         * @param {string} options.base - 一个现有信息行的id,和 position 配合使用,添加到该行的前面或者后面
                                         * @param {'before' | 'after'} options.position - 添加的位置,前面还是后面
                                         */
                                        function addUserLine(floor, { id, line, base, position }) {
                                            // 将字符串line转换为TextNode
                                            if (typeof line === 'string') {
                                                line = document.createTextNode(line);
                                            }
                                            // 插入到指定行的指定位置
                                            const base_line = parser.getUserLine(floor, base);
                                            switch (position) {
                                                case 'before': {
                                                    base_line.element.before(line);
                                                    base_line.element.before($CrE('br'));
                                                    break;
                                                }
                                                case 'after': {
                                                    base_line.element.after(line);
                                                    base_line.element.after($CrE('br'));
                                                    break;
                                                }
                                            }
                                            // 添加到楼层行数据中
                                            /** @type {FloorUserLine} */
                                            const userline = {
                                                id,
                                                wenku: false,
                                                element: line,
                                            };
                                            let index = floor.element.userlines.indexOf(base_line);
                                            position === 'after' && index++;
                                            floor.element.userlines.splice(index, 0, userline);
                                        }

                                        /**
                                         * 更新指定楼层一个已有用户信息行的内容
                                         * @param {Floor} floor - 更新的楼层
                                         * @param {string} id - 信息行id
                                         * @param {Node | string} line - 新的信息行内容,字符串将转换为文本节点
                                         */
                                        function updateLine(floor, id, line) {
                                            // 将字符串line转换为TextNode
                                            if (typeof line === 'string') {
                                                line = document.createTextNode(line);
                                            }
                                            const userline = parser.getUserLine(floor, id);
                                            const previous_node = userline.element.previousSibling;
                                            previous_node.after(line);
                                            userline.element.remove();
                                            userline.element = line;
                                        }

                                        addStyle(`
                                            .plus-highlight {
                                                box-shadow: 0 0 10px 1px #75b1df;
                                            }
                                            .plus-darkmode .plus-highlight {
                                                box-shadow: 0 0 10px 1px #0d688b;
                                            }
                                        `, 'plus-review-transformer')

                                        /**
                                         * 对楼层应用高亮效果
                                         * @param {Floor} floor
                                         */
                                        function applyHighlight(floor) {
                                            floor.data.highlight = true;
                                            floor.element.root.classList.add('plus-highlight');
                                            $AEL(floor.element.root, 'click', e => clearHighlight(floor), { once: true });
                                        }

                                        /**
                                         * 对楼层清除高亮效果
                                         * @param {Floor} floor
                                         */
                                        function clearHighlight(floor) {
                                            floor.data.highlight = false;
                                            floor.element.root.classList.remove('plus-highlight');
                                        }

                                        return {
                                            addFloorButton, addUserButton, addUserLine, updateLine,
                                            applyHighlight, clearHighlight,
                                        }
                                    }
                                },
                                updater: {
                                    desc: '从服务器获取实时评论页面,更新页面内容',
                                    dependencies: ['parser', 'transformer'],
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.updater.func>>} updater */
                                    async func() {
                                        /** @type {parser} */
                                        const parser = pool.require('parser');
                                        /** @type {transformer} */
                                        const transformer = pool.require('transformer');
                                        
                                        /**
                                         * 更新页面时用的提示UI
                                         * @satisfies {Record<string, { start: () => void, end: (updated: number) => void }>}
                                         */
                                        const UI = {
                                            loading: {
                                                start() {
                                                    Quasar.Loading.show({ message: CONST.Text.Review.FloorManager.UpdatingFloors });
                                                },
                                                end() {
                                                    Quasar.Loading.hide();
                                                }
                                            },
                                            notify: {
                                                start() {
                                                    Quasar.Notify.create({
                                                        type: 'info',
                                                        message: CONST.Text.Review.FloorManager.UpdatingFloors,
                                                        group: 'review.update_floor'
                                                    });
                                                },
                                                end(updated) {
                                                    Quasar.Notify.create({
                                                        type: 'success',
                                                        message: CONST.Text.Review.FloorManager.FloorUpdated,
                                                        caption: replaceText(
                                                            CONST.Text.Review.FloorManager.FloorUpdatedCaption,
                                                            { '{Updated}': updated },
                                                        ),
                                                        group: 'review.update_floor'
                                                    });
                                                }
                                            }
                                        };

                                        /**
                                         * 获取一个评论页面,并解析
                                         * @param {number} rid 
                                         * @param {number | 'last'} [page] 
                                         * @returns {Promise<{ floors: Floor[], pagelink: HTMLDivElement }>}
                                         */
                                        async function fetch(rid, page) {
                                            const doc = await utils.requestDocument({
                                                method: 'GET',
                                                url: `/modules/article/reviewshow.php?rid=${rid}&page=${page ?? 1}`,
                                            });
                                            const floors = parser.parseAll(doc);
                                            const pagelink = $(doc, '#pagelink');
                                            return { floors, pagelink };
                                        }

                                        /**
                                         * 从文库服务器获取当前书评页面的最新版本,并更新到页面中,同时也更新floors全局实例
                                         * 注意:只有在楼层标题或内容有所改变时,才会更新对应楼层
                                         * @param {keyof typeof UI} [ui='notify'] - 采用什么UI提示用户页面楼层正在更新;默认"notify"
                                         * @param {number | 'last'} [page] - 需要加载(更新到)的页面页码,默认为当前页码;默认当前页码;注意:这里即使填写了"last",最终url也会显示对应的数字格式的页码,而不是"page=last"
                                         * @param {boolean} [highlight=true] - 是否高亮发生了更改的楼层;默认为true
                                         * @param {'push' | 'replace' | 'none'} [state='replace'] - 在页码改变时,是添加新浏览历史、修改现有浏览状态还是不改变浏览历史和状态;默认"replace"(修改现有);注意:当页码没有改变时,无论填写什么,都既不会添加新浏览记录,又不会改变现有浏览记录
                                         */
                                        async function update(ui = 'notify', page=null, highlight=true, state='replace') {
                                            UI[ui].start();

                                            // 获取最新的页面楼层
                                            const search = new URLSearchParams(location.search);
                                            const rid = parseInt(search.get('rid'), 10);
                                            const cur_page = parseInt($('#pagelink > strong').innerText.trim(), 10);
                                            page = page ?? cur_page;
                                            const { floors: new_floors, pagelink: new_pagelink } = await fetch(rid, page);

                                            // 旧楼层列表比新楼层列表长时,去除旧楼层尾部多出来的楼层
                                            if (floors.length > new_floors.length) {
                                                for (let i = new_floors.length; i < floors.length; i++) {
                                                    floors[i].element.root.remove();
                                                }
                                                floors.splice(new_floors.length, floors.length - new_floors.length);
                                            }

                                            // 和页面现有楼层比对,对有内容更新的楼层进行更新
                                            const updated_floors = [];
                                            new_floors.forEach((new_floor, i) => {
                                                const old_floor = floors[i];

                                                // 跳过无内容更新的楼层
                                                if (
                                                    old_floor &&
                                                    old_floor.data.number === new_floor.data.number &&
                                                    old_floor.data.content === new_floor.data.content &&
                                                    old_floor.data.title === new_floor.data.title
                                                ) { return; }
                                                
                                                // 更新楼层
                                                if (old_floor) {
                                                    old_floor.element.root.before(new_floor.element.root);
                                                    old_floor.element.root.remove();
                                                } else {
                                                    // 新增楼层
                                                    floors[floors.length-1].element.root.after(new_floor.element.root);
                                                }

                                                // 对新楼层应用高亮效果
                                                highlight && transformer.applyHighlight(new_floor);

                                                // 更新楼层实例数据
                                                old_floor ?
                                                    floors.splice(floors.indexOf(old_floor), 1, new_floor) :
                                                    floors.push(new_floor);

                                                // 记录新楼层
                                                updated_floors.push(new_floor);
                                            });

                                            // 同时更新一下页脚翻页指示器
                                            const old_pagelink = $('#pagelink');
                                            old_pagelink.before(new_pagelink);
                                            old_pagelink.remove();

                                            // 如果页码有改变,添加/改变浏览历史
                                            const num_page = page === 'last' ? parseInt($('#pagelink > strong').innerText.trim(), 10) : page;
                                            num_page !== cur_page && ({
                                                'push': history.pushState.bind(history),
                                                'replace': history.replaceState.bind(history),
                                                'none': () => {},
                                            })[state](null, '', `/modules/article/reviewshow.php?rid=${rid}&page=${num_page}`);

                                            // 广播楼层更新事件
                                            messager.dispatchEvent(new CustomEvent('update', {
                                                detail: {
                                                    floors: updated_floors,
                                                }
                                            }));

                                            UI[ui].end(updated_floors.length);
                                        }

                                        return { fetch, update, };
                                    }
                                },
                            };
                            const { promise, pool } = utils.loadFuncInNewPool(pool_funcs);
                            await promise;

                            /** @type {parser} */
                            const parser = pool.require('parser');
                            const floors = parser.parseAll();

                            /**
                             * 将传入的方法应用于全部的Floor,包括一开始就在页面上的和后来通过更新等方式添加到页面上的
                             * @param {(floor: Floor) => any} func
                             */
                            function applyToAllFloors(func) {
                                floors.forEach(floor => func(floor));
                                $AEL(messager, 'update', e => 
                                    e.detail.floors.forEach(floor => func(floor))
                                );
                            }

                            return {
                                /** 全局唯一 floors 数据实例,一切涉及楼层的操作都应围绕此数据实例进行 */
                                floors,

                                /** @type {parser} */
                                parser: pool.require('parser'),
                                /** @type {transformer} */
                                transformer: pool.require('transformer'),
                                /** @type {updater} */
                                updater: pool.require('updater'),

                                applyToAllFloors,
                            }
                        }
                    },
                    citing: {
                        desc: '楼层引用功能',
                        dependencies: 'FloorManager',
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.citing.func>>} citing */
                        func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            GM_getValue = utils.defaultedGet({
                                no_content: false,
                                pangu: true,
                                select: false,
                            }, GM_getValue);

                            configs.registerSettings('review', [{
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.Pangu,
                                caption: CONST.Text.Review.Settings.PanguCaption,
                                key: 'pangu',
                                get() { return GM_getValue('pangu'); },
                                set(val) { return GM_setValue('pangu', val); },
                            }, {
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.NoContent,
                                caption: CONST.Text.Review.Settings.NoContentCaption,
                                key: 'no_content',
                                get() { return GM_getValue('no_content'); },
                                set(val) { return GM_setValue('no_content', val); },
                            }, {
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.Select,
                                caption: CONST.Text.Review.Settings.SelectCaption,
                                key: 'select',
                                get() { return GM_getValue('select'); },
                                set(val) { return GM_setValue('select', val); },
                            }], GM_addValueChangeListener);

                            /** @type {FloorManager} */
                            const FloorManager = pool.require('FloorManager');

                            // 为每个楼层添加引用按钮
                            FloorManager.applyToAllFloors(addCiteButton);

                            /**
                             * 为给定楼层添加引用按钮
                             * @param {Floor} floor 
                             */
                            function addCiteButton(floor) {
                                FloorManager.transformer.addFloorButton(floor, {
                                    id: 'cite',
                                    label: CONST.Text.Review.Cite.Cite,
                                    index: 1,
                                    callback: () => cite(floor)
                                });
                            }

                            /**
                             * 引用某个楼层到回帖输入框中
                             * @param {Floor} floor - 引用的楼层
                             * @param {boolean} [no_content] - 是否仅引用楼号,省略则使用储存的配置
                             * @param {boolean} [pangu] - 是否保证和周围文字之间有且仅有一个空格,省略则使用储存的配置
                             * @param {boolean} [select] - 是否选中引用部分文字,省略则使用储存的配置
                             */
                            function cite(floor, no_content=null, pangu=null, select=null) {
                                no_content = no_content ?? GM_getValue('no_content');
                                pangu = pangu ?? GM_getValue('pangu');
                                select = select ?? GM_getValue('select');

                                /** @type {HTMLTextAreaElement | null} */
                                const textarea = $('#pcontent');
                                if (!textarea) { return; }

                                // 插入引用内容
                                const bbcode = no_content ?
                                    `[url=${floor.data.url}]#${floor.data.number}[/url]` :
                                    `[url=${floor.data.url}]#${floor.data.number}[/url] [quote]${floor.data.content}[/quote]\n`;
                                utils.insertText(textarea, bbcode, pangu, select);
                                
                                // 自动聚焦到输入框的同时平滑滚动到输入框位置
                                // .focus会自动跳转到元素位置,因此需要先复位到.focus前再开始平滑滚动
                                // 虽然有 preventScroll 选项,但是这个选项在安卓上似乎不可用
                                const [orig_x, orig_y] = [window.scrollX, window.scrollY];
                                textarea.focus({ preventScroll: true });
                                window.scroll(orig_x, orig_y);
                                textarea.scrollIntoView({ behavior: 'smooth' });
                            }

                            return {
                                /** @type {boolean} */
                                get pangu() { return GM_getValue('pangu'); },
                                set pangu(val) { return GM_setValue('pangu', val); },
                                /** @type {boolean} */
                                get no_content() { return GM_getValue('no_content'); },
                                set no_content(val) { return GM_setValue('no_content', val); },
                                /** @type {boolean} */
                                get select() { return GM_getValue('select'); },
                                set select(val) { return GM_setValue('select', val); }
                            };
                        }
                    },
                    floorjump: {
                        desc: '点击页面内楼层链接,直接跳转到页面位置,而不是重新加载页面到该位置',
                        dependencies: 'FloorManager',
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            GM_getValue = utils.defaultedGet({
                                jump: true
                            }, GM_getValue);

                            /** @type {FloorManager} */
                            const FloorManager = pool.require('FloorManager');
                            const floors = FloorManager.floors;

                            // 拦截<a>点击事件,根据设置决定是否跳转
                            const content = $('#content');
                            $AEL(content, 'click', e => {
                                // 检查是否开启跳转功能
                                if (!GM_getValue('jump')) { return; }

                                // 按下Ctrl/Meta/Shift键代表用户显式指定在新标签页/窗口打开,不执行跳转
                                if (e.ctrlKey || e.shiftKey || e.metaKey) { return; }

                                // 检查是否点击到了一个指向楼层的链接
                                /** @type {null | HTMLAnchorElement} */
                                const a = e.target.closest('a[href*="#yid"]');
                                if (
                                    !a ||
                                    a.pathname !== '/modules/article/reviewshow.php' ||
                                    !/^#yid\d+$/.test(a.hash)
                                ) { return; }

                                // 检查链接是否在某楼层正文内
                                if (floors.every(floor => !floor.element.content.contains(a))) { return; }

                                // 检查目标楼层是否在页面内
                                /** @type {Floor} */
                                const floor = floors.find(floor => 
                                    // 防止 rid 参数不存在,因此选择将floor的rid转换为字符串进行比较,而不是将a的rid参数转换为数字
                                    floor.data.rid.toString() === new URLSearchParams(a.search).get('rid') &&
                                    // 前面判断过a.hash符合yid\d+的格式,可以直接match取值
                                    floor.data.yid.toString() === a.hash.match(/\d+/)[0]
                                );
                                if (!floor) { return; }

                                // 检查通过,跳转
                                e.preventDefault();
                                floor.element.root.scrollIntoView({ behavior: 'smooth' });
                            });

                            // 注册(不可用)设置项
                            configs.registerSettings('review', [{
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.FloorJump,
                                caption: CONST.Text.Review.Settings.FloorJumpCaption,
                                key: 'floorjump',
                                get() { return GM_getValue('jump'); },
                                set(val) { return GM_setValue('jump', val); },
                            }], GM_addValueChangeListener);
                        }
                    },
                    pagejump: {
                        desc: '点击右下角页码切换,直接页面内更新,而不是重建加载页面到该页码',
                        dependencies: ['FloorManager'],
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            /** @type {FloorManager} */
                            const FloorManager = pool.require('FloorManager');

                            GM_getValue = utils.defaultedGet({
                                jump: true,
                            }, GM_getValue);

                            // 拦截#pagelink > a点击事件,根据设置决定是否页面内更新
                            // 因为 #pagelink 会随着页面内更新而更新改变,所以要把事件监听器添加到父元素上
                            const pagelink_parent = $('#pagelink').parentElement;
                            $AEL(pagelink_parent, 'click', e => {
                                // 检查是否开启跳转功能
                                if (!GM_getValue('jump')) { return; }

                                // 按下Ctrl/Meta/Shift键代表用户显式指定在新标签页/窗口打开,不执行跳转
                                if (e.ctrlKey || e.shiftKey || e.metaKey) { return; }

                                // 检查是否点击到了一个指向新页码的链接
                                /** @type {null | HTMLAnchorElement} */
                                const a = e.target.closest('a[href^="/modules/article/reviewshow.php"]');
                                if (
                                    !a ||
                                    a.pathname !== '/modules/article/reviewshow.php'
                                ) { return; }

                                // 页面内更新
                                e.preventDefault();
                                const search = new URLSearchParams(a.search);
                                const page = parseInt(search.get('page'), 10);
                                FloorManager.updater.update('loading', page, false, 'push');
                            });

                            // 当点击浏览器后退按钮时,更新页面
                            $AEL(window, 'popstate', e => {
                                const search = new URLSearchParams(location.search);
                                const page = search.get('page');
                                GM_getValue('jump') ?
                                    FloorManager.updater.update('loading', page, false, 'none') :
                                    location.reload();
                            });

                            // 注册(不可用)设置项
                            configs.registerSettings('review', [{
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.PageJump,
                                caption: CONST.Text.Review.Settings.PageJumpCaption,
                                key: 'pagejump',
                                get() { return GM_getValue('jump'); },
                                set(val) { return GM_setValue('jump', val); },
                            }], GM_addValueChangeListener);
                        }
                    },
                    replyinpage: {
                        desc: '页面内免刷新发评论',
                        detectDom: '.main.m_foot',
                        dependencies: ['FloorManager'],
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.replyinpage.func>>} replyinpage */
                        async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            /** @type {FloorManager} */
                            const FloorManager = pool.require('FloorManager');

                            GM_getValue = utils.defaultedGet({
                                enabled: true,
                            }, GM_getValue);

                            configs.registerSettings('review', [{
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.ReplyInPage,
                                caption: CONST.Text.Review.Settings.ReplyInPageCaption,
                                key: 'replyinpage',
                                get() { return GM_getValue('enabled'); },
                                set(val) { return GM_setValue('enabled', val); },
                            }], GM_addValueChangeListener);

                            const form = $('form[name="frmreview"]');
                            form && hookSubmit(form);

                            /**
                             * 将评论编辑器的表单提交改为ajax请求,并在请求完成后更新页面楼层
                             * @param {HTMLFormElement} form 
                             * @param {(form: HTMLFormElement) => any} onSend - 评论发送完成回调
                             * @param {boolean} to_last - 发送完毕更新页面楼层是否更新到最后一页,如果为否则更新到当前页
                             */
                            function hookSubmit(form, onSend, to_last=true) {
                                let submit_ongoing = false;
                                $AEL(form, 'submit', async e => {
                                    if (!GM_getValue('enabled')) { return; }
                                    if (submit_ongoing) { return; }

                                    // 拦截默认行为
                                    e.preventDefault();
                                    const ReplyInPage = CONST.Text.Review.ReplyInPage;

                                    // 不允许发送空数据
                                    const formdata = new FormData(form);
                                    if (!formdata.get('pcontent').length) {
                                        Quasar.Notify.create({
                                            type: 'error',
                                            message: ReplyInPage.NoEmptyContent,
                                            caption: ReplyInPage.NoEmptyContentCaption,
                                        });
                                        return;
                                    }

                                    // 发送评论
                                    submit_ongoing = true;
                                    Quasar.Loading.show({ message: ReplyInPage.SendingReply });
                                    const data = utils.serializeFormData(formdata);
                                    const doc = await utils.requestDocument({
                                        method: 'POST',
                                        url: form.getAttribute('action'),
                                        data,
                                        headers: {
                                            'content-type': 'application/x-www-form-urlencoded'
                                        }
                                    });
                                    Quasar.Loading.hide();
                                    submit_ongoing = false;

                                    // 发送完成提示
                                    const is_block = !!$(doc, '.block');
                                    Quasar.Notify.create({
                                        type: 'success',
                                        message: ReplyInPage.ReplySent,
                                        caption: is_block ? $(doc, '.blocktitle').innerText : undefined,
                                        actions: is_block ? [{
                                            label: ReplyInPage.SentStatusDetails,
                                            async handler() {
                                                // 使用文库返回的block作为详情弹窗内容
                                                const block = $(doc, '.block').cloneNode(true);
                                                block.classList.add('plus-preserve-border');
                                                // 移除脚注
                                                $(block, '.blocknote')?.remove();
                                                // 点击任意<a>链接时,什么都不做(拦截默认行为与事件处理器)
                                                [...$All(block, 'a')].forEach(a => 
                                                    $AEL(a, 'click', e =>
                                                        e.ctrlKey || e.metaKey || e.shiftKey || destroyEvent(e),
                                                        { capture: true }
                                                    )
                                                );
                                                // 点击返回时,关闭弹窗并重新聚焦到编辑器
                                                [...$All(block, 'a[href="javascript:history.back(1)"]')].forEach(a =>
                                                    $AEL(a, 'click', e => {
                                                        dialog.hide();
                                                        setTimeout(() => $(form, '#pcontent').focus());
                                                    }, { capture: true })
                                                );
                                                // 点击关闭此窗口时,关闭弹窗
                                                [...$All(block, 'a[href="javascript:window.close()"]')].forEach(a =>
                                                    $AEL(a, 'click', e => {
                                                        dialog.hide();
                                                    }, { capture: true })
                                                );
                                                
                                                // Quasar Dialog 展示详情
                                                const dialog = Quasar.Dialog.create({
                                                    message: '<div id="plus-reply-detail"></div>',
                                                    html: true,
                                                    ok: ReplyInPage.DetailsOk,
                                                });
                                                (await detectDom('#plus-reply-detail')).append(block);
                                            }
                                        }] : [],
                                        group: 'review.replyinpage.reply-sent',
                                    });

                                    // 回调
                                    onSend && onSend(form);

                                    // 更新页面楼层
                                    const page = to_last ? 'last' : new URLSearchParams(location.search).get('page') ?? 1;
                                    await FloorManager.updater.update('loading', page, true, 'push');
                                });
                            }

                            return { hookSubmit, };
                        },
                    },
                    editinpage: {
                        desc: '编辑楼层功能页面内完成',
                        dependencies: ['FloorManager', 'replyinpage'],
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.editinpage.func>>} editinpage */
                        async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            /** @type {FloorManager} */
                            const FloorManager = pool.require('FloorManager');
                            /** @type {replyinpage} */
                            const replyinpage = pool.require('replyinpage');
                            /** @type {darkmode} */
                            const darkmode = await require('darkmode', true);
                            /** @type {ubbeditor} */
                            const ubbeditor = await require('ubbeditor', true);

                            GM_getValue = utils.defaultedGet({
                                enabled: true,
                            }, GM_getValue);

                            configs.registerSettings('review', [{
                                type: 'boolean',
                                label: CONST.Text.Review.Settings.EditInPage,
                                caption: CONST.Text.Review.Settings.EditInPageCaption,
                                key: 'editinpage',
                                get() { return GM_getValue('enabled'); },
                                set(val) { return GM_setValue('enabled', val); },
                            }], GM_addValueChangeListener);

                            FloorManager.applyToAllFloors(hookEdit);

                            /**
                             * 将楼层的编辑按钮(如果有)点击后改为在页面内编辑,而不是打开一个新页面
                             * @param {Floor} floor 
                             */
                            function hookEdit(floor) {
                                const edit = FloorManager.parser.getFloorButton(floor, 'edit');
                                const yid = floor.data.yid;
                                if (!edit) { return; }

                                $AEL(edit.element, 'click', async e => {
                                    if (!GM_getValue('enabled')) { return; }

                                    // 按下Ctrl/Meta/Shift时,为用户显式指定在新标签页/新窗口打开,不拦截
                                    if (e.ctrlKey || e.metaKey || e.shiftKey) { return; }

                                    // 阻止打开新页面
                                    e.preventDefault();

                                    // 获取编辑框部分html
                                    const url = `/modules/article/reviewedit.php?yid=${yid}&ajax_gets=jieqi_contents`;
                                    const doc = await utils.requestDocument({
                                        method: 'GET', url,
                                    });
                                    const editor = $(doc, 'form[name="frmreview"]').cloneNode(true);
                                    [...$All(editor, 'script')].forEach(s => s.remove());
                                    const editor_html = editor.outerHTML;

                                    // 获取页面资源
                                    const [editor_js, common_js] = await Promise.all([
                                        utils.requestText({
                                            method: 'GET',
                                            url: '/scripts/ubbeditor_gbk.js'
                                        }),
                                        utils.requestText({
                                            method: 'GET',
                                            url: '/scripts/common.js'
                                        }),
                                    ]);
                                    // 合成整体html
                                    /*
                                    const body_html = [
                                        // 文档编码
                                        `<meta charset="${ document.characterSet }">`,
                                        // 文库自带CSS
                                        '<link rel="stylesheet" href="/themes/wenku8/style.css">',
                                        // 深色模式CSS
                                        darkmode.getPageCSS(url).map(css => `<style>${css}</style>`).join('\n'),
                                        // UBBEditor所用loadJS依赖
                                        '<script src="/scripts/common.js"></script>',
                                        // 编辑器和表单
                                        editor_html,
                                    ].join('\n');
                                    */
                                    const body_html = [
                                        // 文库自带CSS
                                        '<link rel="stylesheet" href="/themes/wenku8/style.css">',
                                        // 深色模式CSS
                                        darkmode.getPageCSS(url).map(css => `<style>${css}</style>`).join('\n'),
                                        // JS依赖
                                        `<script>${ common_js }</script>`,
                                        // 编辑器和表单
                                        editor_html,
                                        // UBBEditor
                                        `<script>${ editor_js };\nUBBEditor.Create("pcontent");</script>`,
                                    ].join('\n');
                                    // 深色模式
                                    const body_class = darkmode.actual_enabled ? 'plus-darkmode' : '';
                                    const html = `
                                        <body
                                            class="${body_class}"
                                            style="overflow: hidden;"
                                        >
                                            ${body_html}
                                        </body>
                                    `.replaceAll(/[\r\n][\r\n \t]*/g, '\n');
                                    darkmode.onToggle(enabled => {
                                        iframe.contentDocument?.body.classList[enabled ? 'add' : 'remove']('plus-darkmode')
                                    });

                                    // 包装到iframe中
                                    /** @type {HTMLIFrameElement} */
                                    const iframe = $$CrE({
                                        tagName: 'iframe',
                                        props: {
                                            srcdoc: html,
                                        },
                                        styles: {
                                            border: 'none',
                                        },
                                        listeners: [[
                                            'load', e => {
                                                const doc = iframe.contentDocument;

                                                // 调整宽高
                                                function resize() {
                                                    iframe.width = doc.body.scrollWidth;
                                                    iframe.height = doc.body.scrollHeight;
                                                }
                                                resize();
                                                const observer = new ResizeObserver(entries => resize());
                                                observer.observe(iframe.contentDocument.body);
                                                // 这里无法在onDismiss中unobserve,因为onDismiss时iframe的body已不存在
                                                //dialog.onDismiss(() => observer.unobserve(iframe.contentDocument.body));

                                                // 编辑器修复与增强
                                                const form = $(doc, 'form[name="frmreview"]');
                                                replyinpage.hookSubmit(form, () => dialog.hide(), false);
                                                ubbeditor.enhance(form);
                                            },
                                        ]]
                                    });

                                    // 在 Quasar Dialog 中展示
                                    const dialog = Quasar.Dialog.create({
                                        message: `<div id="plus-edit-dialog"></div>`,
                                        html: true,
                                        ok: false,
                                        cancel: false,
                                        style: {
                                            width: 'fit-content',
                                            height: 'fit-content',
                                            maxWidth: 'none',
                                        },
                                    });
                                    (await detectDom('#plus-edit-dialog')).append(iframe);
                                });
                            }
                        },
                    },
                    autorefresh: {
                        desc: '自动刷新楼层',
                        dependencies: ['FloorManager'],
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            /** @type {FloorManager} */
                            const FloorManager = pool.require('FloorManager');

                            GM_getValue = utils.defaultedGet({
                                enabled: false,
                                refresh_last: true,
                            }, GM_getValue);

                            const Settings = CONST.Text.Review.Settings;
                            configs.registerSettings('review', [{
                                type: 'boolean',
                                label: Settings.AutoRefresh,
                                caption: Settings.AutoRefreshCaption,
                                key: 'autorefresh',
                                get() { return GM_getValue('enabled'); },
                                set(val) { GM_setValue('enabled', val)},
                            }, {
                                type: 'boolean',
                                label: Settings.RefreshToLast,
                                caption: Settings.RefreshToLastCaption,
                                key: 'refresh_last',
                                get() { return GM_getValue('refresh_last'); },
                                set(val) { GM_setValue('refresh_last', val); },
                            }], GM_addValueChangeListener);

                            setInterval(
                                () => GM_getValue('enabled') && document.visibilityState === 'visible' &&
                                    (GM_getValue('refresh_last') ? FloorManager.updater.update('notify', 'last', true, 'replace') : FloorManager.updater.update('notify', null, true, 'replace')),
                                CONST.Internal.ReviewAutoRefreshInterval,
                            );
                        },
                    },
                    beautifier: {
                        desc: '页面样式修复增强',
                        detecoDom: 'head',
                        async func() {
                            // 回复内引用、代码文字最大宽度限制
                            addStyle(`
                                pre {
                                    white-space: pre-wrap;       /* 保留格式但允许自动换行 */
                                    word-break: break-word;      /* 即使没有空格也能断句 */
                                    overflow-wrap: break-word;   /* 兼容性增强 */
                                }
                            `);

                            // 回复内图片最大宽度限制
                            detectDom({
                                selector: '.divimage > img',
                                /** @param {HTMLImageElement} img */
                                callback(img) {
                                    const tryResize = () => img.naturalWidth ? resize() : setTimeout(() => tryResize(), CONST.Internal.ReviewResizeInterval);
                                    tryResize();

                                    function resize() {
                                        img.style.width = `min(100%, ${img.naturalWidth}px)`;
                                    }
                                }
                            });
                        }
                    },
                };
                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, { GM_setValue, GM_getValue, GM_addValueChangeListener });
                await promise;

                return {
                    /** @type {FloorManager} */
                    FloorManager: pool.require('FloorManager'),
                    /** @type {citing} */
                    citing: pool.require('citing'),
                    messager,

                    _types: {
                        /** @type {Floor} */
                        Floor: {},
                    }
                }
            }
        },
        ubbeditor: {
            desc: '编辑器修复与增强',
            dependencies: ['utils'],
            /** @typedef {Awaited<ReturnType<typeof functions.ubbeditor.func>>} ubbeditor */
            async func() {
                /** @type {utils} */
                const utils = require('utils');

                // 自动将修复与增强功能应用于已知的页面内自带UBBEditor实例
                const pages = [{
                    checkers: {
                        type: 'path',
                        value: '/modules/article/reviewshow.php'
                    },
                    selector: 'form[name="frmreview"]'
                }, {
                    checkers: [{
                        type: 'regpath',
                        value: /\/book\/\d+\.htm/
                    }, {
                        type: 'startpath',
                        value: '/modules/article/articleinfo.php'
                    }],
                    selector: 'form[name="frmreview"]'
                }, {
                    checkers: {
                        type: 'path',
                        value: '/modules/article/reviews.php'
                    },
                    selector: 'form[name="frmreview"]'
                }, {
                    checkers: {
                        type: 'path',
                        value: '/modules/article/reviewedit.php'
                    },
                    selector: 'form[name="frmreview"]'
                }];
                pages.forEach(page => FunctionLoader.testCheckers(page.checkers) &&
                    detectDom(page.selector).then(form => enhance(form)));

                /**
                 * 将修复与增强功能应用于UBBEditor实例
                 * @param {HTMLFormElement} form - 存放UBBEditor的form,通常是[name="frmreview"],无需等待其中的UBBEditor加载初始化完毕
                 * @returns {Promise<void>}
                 */
                async function enhance(form) {
                    await Promise.all([
                        // 样式表式修复
                        detectDom(form.ownerDocument, 'head').then(head => addStyle(head, `
                            textarea[name="pcontent"] {
                                padding: 2px;
                                width: 90%;
                            }
                        `, 'plus-ubbeditor-enhance')),

                        // 重写插入图片
                        detectDom(form, '#menuItemInsertImage').then(
                            /** @param {HTMLInputElement} input */
                            input => $AEL(input, 'click', async e => {
                                e.stopImmediatePropagation();

                                const InsertImage = CONST.Text.Review.UBBEditor.InsertImage;
                                let url = await prompt({
                                    message: InsertImage.InputUrl + '<br>' + InsertImage.UrlFormatTip,
                                    title: InsertImage.Title,
                                    html: true,
                                    ok: InsertImage.Ok,
                                    cancel: InsertImage.Cancel,
                                    isValid(url) { return isValidImageUrl(url.trim()); },
                                });
                                if (url === null) { return; }

                                url = url.trim();
                                const textarea = $('#pcontent');
                                utils.insertText(textarea, url, true);
                                textarea.focus();
                            }, { capture: true })
                        ),

                        // 重写插入链接
                        detectDom(form, '#menuItemInsertUrl').then(
                            /** @param {HTMLInputElement} input */
                            input => $AEL(input, 'click', async e => {
                                e.stopImmediatePropagation();

                                const InsertUrl = CONST.Text.Review.UBBEditor.InsertUrl;
                                let url = await prompt({
                                    message: InsertUrl.InputUrl + '<br>' + InsertUrl.UrlFormatTip,
                                    title: InsertUrl.Title,
                                    html: true,
                                    ok: InsertUrl.Ok,
                                    cancel: InsertUrl.Cancel,
                                    isValid(url) { return isValidUrl(url.trim()); },
                                });
                                if (url === null) { return; }

                                url = url.trim();
                                const textarea = $('#pcontent');
                                utils.insertText(textarea, `[url=${url}]${url}[/url]`);
                                textarea.focus();
                            }, { capture: true })
                        ),

                        // Ctrl/Meta + Enter键发表书评
                        detectDom(form, '#pcontent').then(pcontent => {
                            $AEL(pcontent, 'keydown', e => {
                                const os = GM_info.platform?.os ?? GM_info.userAgentData.platform;
                                const is_mac = ['darwin', 'osx', 'mac'].some(str => os.includes(str));
                                if ((is_mac ? e.metaKey : e.ctrlKey) && e.code === 'Enter') {
                                    $(form, 'input[type="submit"][name="Submit"]')?.click();
                                }
                            });
                        }),

                        // 自适应高度
                        detectDom(form, '#pcontent').then(
                            /** @param {HTMLTextAreaElement} pcontent */
                            pcontent => $AEL(pcontent, 'input', e => {
                                const cur_height = parseInt(getComputedStyle(pcontent).height.match(/\d+/)[0], 10);
                                // 跟deepseek学的:先设为auto以便正确计算pcontent.scrollHeight
                                pcontent.style.height = 'auto';
                                // 根据当前输入框内部滚动高度和预设的上下限确定输入框新高度
                                let target_height = Math.min(
                                    CONST.Internal.EditorHeight.Max,
                                    Math.max(
                                        CONST.Internal.EditorHeight.Min,
                                        pcontent.scrollHeight
                                    )
                                );
                                // 仅自动增高,不自动缩小
                                target_height = cur_height < target_height ? target_height : cur_height;
                                // 设置高度
                                pcontent.style.height = `${target_height}px`;
                            })
                        ),
                    ]);
                }

                /**
                 * 检查给定链接是否为符合文库书评语法格式的图片链接
                 * @param {string} url 
                 * @returns {boolean}
                 */
                function isValidImageUrl(url) {
                    const prefix_valid = url.startsWith('http://') || url.startsWith('https://');
                    const suffix_valid = /\.(jpe?g|a?png|gif|webp)$/.test(url);
                    const url_valid = prefix_valid && suffix_valid;
                    return url_valid;
                }

                /**
                 * 检查给定链接是否为符合文库书评语法格式的链接
                 * @param {string} url 
                 * @returns {boolean}
                 */
                function isValidUrl(url) {
                    const prefix_valid = url.startsWith('http://') || url.startsWith('https://');
                    const url_valid = prefix_valid;
                    return url_valid;
                }

                /**
                 * Quasar Dialog 实现的prompt
                 * @param {Object} options
                 * @param {string} options.message - 提示文本
                 * @param {string} [options.title] - 输入框标题
                 * @param {boolean} [options.html=false] - 提示文本是否为html(不安全)
                 * @param {string} [options.ok] - 确认按钮文本
                 * @param {string} [options.cancel] - 取消按钮文本
                 * @param {string} [options.model=''] - 输入框初始值
                 * @param {(val: string) => boolean} options.isValid - 验证输入数据是否合法的方法
                 * @returns {Promise<string | null>}
                 */
                function prompt({ message, title, html, ok, cancel, model, isValid }) {
                    const { promise, resolve } = Promise.withResolvers();

                    const options = {
                        message,
                        ok: {
                            color: 'primary',
                        },
                        cancel: {
                            color: 'secondary',
                        },
                        prompt: {
                            model: model ?? '',
                            isValid,
                        },
                    };
                    title && (options.title = title);
                    html && (options.html = html);
                    ok && (options.ok.label = ok);
                    cancel && (options.cancel.label = cancel);

                    Quasar.Dialog.create(options).onOk(text => resolve(text)).onCancel(() => resolve(null));

                    return promise;
                }

                return { enhance };
            }
        },
        userpage: {
            desc: '用户信息页相关功能,目前就一个DOM解析器',
            checkers: {
                type: 'path',
                value: '/userpage.php'
            },
            dependencies: ['utils'],
            /** @typedef {Awaited<ReturnType<typeof functions.userpage.func>>} userpage */
            async func() {
                /** @type {utils} */
                const utils = require('utils');

                // 注:这里的对象并非完整,按需开发即可
                /**
                 * 标准页面对象,由页面解析器生成
                 * @typedef {Object} UserPage
                 * @property {UserElement} element
                 * @property {UserData} data
                 */
                /**
                 * {@link UserPage} 类型中的DOM元素
                 * @typedef {Object} UserElement
                 * @property {HTMLDivElement} info - 会员信息block
                 * @property {HTMLAnchorElement} avatar - 头像Img
                 * @property {HTMLElement} name - 昵称strong
                 * @property {UserLine[]} userlines - 会员信息板块信息行集合
                 * @property {UserButton[]} userbuttons - 会员信息板块操作按钮集合
                 * @property {HTMLUListElement} linecontainer - 会员信息板块信息行的父元素容器
                 * @property {HTMLUListElement} buttoncontainer - 会员信息板块操作按钮的父元素容器
                 */
                /**
                 * {@link UserPage} 类型中的数据
                 * @typedef {Object} UserData
                 * @property {User} user
                 */
                /**
                 * {@link UserElement} 类型中的一行信息行
                 * @typedef {Object} UserLine
                 * @property {string} id - 信息行id,全局唯一
                 * @property {boolean} wenku - 是否为文库页面自带行
                 * @property {HTMLLIElement} element - 对应的DOM节点
                 */
                /**
                 * {@link UserElement} 类型中的一个操作按钮
                 * @typedef {Object} UserButton
                 * @property {string} id - 按钮id,全局唯一
                 * @property {boolean} wenku - 是否为文库页面自带按钮
                 * @property {number} index - 按钮排序位置,升序排列,文库自带均为负数,新增按钮均为正数
                 * @property {HTMLElement} element - 对应的DOM节点,应为li内部的按钮元素而非li节点
                 */
                /**
                 * {@link UserData} 类型中的用户数据
                 * @typedef {Object} User
                 * @property {number} id
                 * @property {string} name
                 */

                const pool_funcs = {
                    PageManager: {
                        desc: '管理页面对象实例及其解析与修改',
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.PageManager.func>>} PageManager */
                        async func() {
                            const pool_funcs = {
                                parser: {
                                    desc: 'DOM解析器',
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.parser.func>>} parser */
                                    func() {
                                        /**
                                         * 将Document解析为标准用户页对象
                                         * 仅可解析未被修改的原始文库页面
                                         * @param {Document} [doc=document] - 被解析的文档,省略则默认为当前页面文档
                                         * @returns {UserPage}
                                         */
                                        function parse(doc = document) {
                                            const element = parseElement(doc);
                                            const data = parseData(element);
                                            return { element, data }
                                        }

                                        /**
                                         * 将Document解析为标准用户页对象的元素部分
                                         * 仅可解析未被修改的原始文库页面
                                         * @param {Document} [doc=document] - 被解析的文档,省略则默认为当前页面文档
                                         * @returns {UserElement}
                                         */
                                        function parseElement(doc = document) {
                                            const info = $(doc, '#left > .block:first-child');
                                            const avatar = $(info, '.blockcontent .avatars');
                                            const name = $(info, '.blockcontent .ulrow > li:nth-child(2)');
                                            const linecontainer = $(info, '.blockcontent .ulrow');
                                            const buttoncontainer = $(info, '.blockcontent > div > ul:nth-of-type(2)');
                                            const userlines = getUserLines();
                                            const userbuttons = getUserButtons();
                                            return { info, avatar, name, userlines, userbuttons, linecontainer, buttoncontainer }

                                            function getUserLines() {
                                                return [...$All(info, '.blockcontent .ulrow > li')]
                                                    .filter(li => !li.children.length)
                                                    .map(
                                                        /**
                                                         * @param {HTMLLIElement} li 
                                                         * @param {number} i
                                                         * @returns {UserLine}
                                                         */
                                                        (li, i) => ({
                                                            id: ['type', 'level'][i],
                                                            wenku: true,
                                                            element: li
                                                        })
                                                    );
                                            }

                                            function getUserButtons() {
                                                return [...$All(info, '.blockcontent > div > :nth-child(2) > li > a')]
                                                    .map(
                                                        /**
                                                         * @param {HTMLAnchorElement} a 
                                                         * @param {number} i 
                                                         * @returns {UserButton}
                                                         */
                                                        (a, i, anchors) => ({
                                                            id: ['message', 'friend', 'detail'][i],
                                                            wenku: true,
                                                            index: i - anchors.length,
                                                            element: a
                                                        })
                                                    )
                                            }
                                        }

                                        /**
                                         * 从标准用户页对象元素部分解析数据
                                         * 仅可解析未被修改的原始文库页面
                                         * @param {UserElement} element - 被解析的文档,省略则默认为当前页面文档
                                         * @returns {UserData}
                                         */
                                        function parseData(element) {
                                            /** @type {User} */
                                            const user = {
                                                id: parseInt(
                                                    new URLSearchParams(
                                                        element.userbuttons
                                                            .find(b => b.id === 'detail')
                                                            .element.search
                                                    ).get('id'), 10
                                                ),
                                                name: element.name.innerText.trim()
                                            };
                                            return { user };
                                        }

                                        /**
                                         * 根据id获取指定信息行
                                         * @param {UserPage} page 
                                         * @param {string} id 
                                         */
                                        function getUserLine(page, id) {
                                            return page.element.userlines.find(l => l.id === id);
                                        }

                                        /**
                                         * 根据id获取指定操作按钮
                                         * @param {UserPage} page 
                                         * @param {string} id 
                                         */
                                        function getUserButton(page, id) {
                                            return page.element.userbuttons.find(b => b.id === id);
                                        }

                                        return {
                                            parse,
                                            getUserLine, getUserButton,
                                        }
                                    }
                                },
                                transformer: {
                                    desc: '页面修改器',
                                    dependencies: 'parser',
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.transformer.func>>} transformer */
                                    func() {
                                        /** @type {parser} */
                                        const parser = pool.require('parser');
                                        
                                        /**
                                         * 在楼层左侧用户区下方新增一个按钮
                                         * @param {UserPage} page 
                                         * @param {Object} options
                                         * @param {string} options.id 
                                         * @param {string} [options.label] - 按钮文字,和element二选一
                                         * @param {string} [options.index] - 按钮的排序位置
                                         * @param {function} [options.callback] - 按钮点击回调,和element二选一
                                         * @param {HTMLElement} [options.element] - 按钮元素,和callback二选一
                                         * @returns {FloorButton}
                                         */
                                        function addUserButton(page, { id, label = null, index, callback = null, element = null }) {
                                            // 创建按钮元素
                                            /** @type {HTMLDivElement} */
                                            const container = $$CrE({
                                                tagName: 'li',
                                                styles: {
                                                    cssText: 'width:49%;float:left;'
                                                }
                                            });
                                            const elm = element ?? $$CrE({
                                                tagName: 'span',
                                                props: { innerText: label },
                                                listeners: [['click', e => callback()]]
                                            });
                                            elm.style.color = 'var(--q-primary)';
                                            elm.style.cursor = 'pointer';
                                            container.append(elm);

                                            // 添加按钮数据
                                            const button = {
                                                id,
                                                wenku: false,
                                                index,
                                                element: elm
                                            };
                                            const userbuttons = page.element.userbuttons;
                                            userbuttons.push(button);

                                            // 按照index排序
                                            userbuttons.sort((b1, b2) => b1.index - b2.index);

                                            // 按照排好的顺序重新添加到页面
                                            const parent = page.element.buttoncontainer;
                                            userbuttons.forEach(btn => parent.append(btn.element.parentElement));

                                            return button;
                                        }

                                        /**
                                         * 添加一行内容到会员信息的信息行中
                                         * @param {UserPage} page - 用户页对象
                                         * @param {Object} options
                                         * @param {string} options.id - 全局唯一,信息行id
                                         * @param {Node | string} options.line - 添加的内容,字符串将转换为文本节点添加
                                         * @param {string} options.base - 一个现有信息行的id,和 position 配合使用,添加到该行的前面或者后面
                                         * @param {'before' | 'after'} options.position - 添加的位置,前面还是后面
                                         */
                                        function addUserLine(page, { id, line, base, position }) {
                                            // 将字符串line转换为TextNode
                                            if (typeof line === 'string') {
                                                line = document.createTextNode(line);
                                            }
                                            // 使用li包装
                                            const li = $CrE('li');
                                            li.append(line);
                                            // 插入到指定行的指定位置
                                            const base_line = parser.getUserLine(page, base);
                                            switch (position) {
                                                case 'before': {
                                                    base_line.element.before(li);
                                                    break;
                                                }
                                                case 'after': {
                                                    base_line.element.after(li);
                                                    break;
                                                }
                                            }
                                            // 添加到楼层行数据中
                                            /** @type {UserLine} */
                                            const userline = {
                                                id,
                                                wenku: false,
                                                element: li,
                                            };
                                            let index = page.element.userlines.indexOf(base_line);
                                            position === 'after' && index++;
                                            page.element.userlines.splice(index, 0, userline);
                                        }

                                        /**
                                         * 更新一个已有用户信息行的内容
                                         * @param {UserPage} page - 更新的楼层
                                         * @param {string} id - 信息行id
                                         * @param {Node | string} line - 新的信息行内容,字符串将转换为文本节点
                                         */
                                        function updateLine(page, id, line) {
                                            // 将字符串line转换为TextNode
                                            if (typeof line === 'string') {
                                                line = document.createTextNode(line);
                                            }
                                            // 用li包装
                                            const li = $CrE('li');
                                            li.append(line);
                                            // 更新
                                            const userline = parser.getUserLine(page, id);
                                            const previous_node = userline.element.previousSibling;
                                            previous_node.after(li);
                                            userline.element.remove();
                                            userline.element = li;
                                        }

                                        return {
                                            addUserButton, addUserLine, updateLine
                                        };
                                    }
                                }
                            };
                            const { promise, pool } = utils.loadFuncInNewPool(pool_funcs);
                            await promise;

                            /** @type {parser} */
                            const parser = pool.require('parser');
                            /** 当前页面的唯一页面对象实例,所有对页面的访问和修改都应围绕此实例进行 */
                            const page = parser.parse();

                            return {
                                page,
                                /** @type {parser} */
                                parser: pool.require('parser'),
                                /** @type {transformer} */
                                transformer: pool.require('transformer'),
                            }
                        }
                    },
                };
                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs);
                await promise;

                return {
                    /** @type {PageManager} */
                    PageManager: pool.require('PageManager'),
                }
            }
        },
        userremark: {
            desc: '对用户进行备注的功能',
            checkers: [{
                // 书评
                type: 'path',
                value: '/modules/article/reviewshow.php'
            }, {
                // 用户主页
                type: 'path',
                value: '/userpage.php'
            }],
            dependencies: ['debugging', 'utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                // 如果是发评论返回的提示页面,不继续运行
                if ($All('.block').length === 1) { return; }

                GM_getValue = utils.defaultedGet({
                    /** @type {Record<string, string>} 字符串用户id - 用户备注 */
                    remarks: {},
                    enabled: true,
                }, GM_getValue);

                /**
                 * 模块通讯信使,承担以下通讯任务:
                 * - remarks更新消息
                 */
                const messager = new EventTarget();

                // 注册(不可用)设置组
                configs.registerConfig('remarks', {
                    GM_addValueChangeListener,
                    label: CONST.Text.UserRemark.Settings.Label,
                    items: [{
                        type: 'boolean',
                        label: CONST.Text.UserRemark.Settings.Enabled,
                        caption: CONST.Text.UserRemark.Settings.EnabledCaption,
                        key: 'enabled',
                        reload: true,
                        get() { return GM_getValue('enabled'); },
                        set(val) { return GM_setValue('enabled', val); },
                    }],
                });

                // 实际功能函数,只有启用备注功能时才运行
                const pool_funcs = {
                    review: {
                        checkers: {
                            type: 'path',
                            value: '/modules/article/reviewshow.php'
                        },
                        async func() {
                            /** @type {review} */
                            const review = await require('review', true);
                            const FloorManager = review.FloorManager;
                            const floors = FloorManager.floors;

                            // 显示用户备注
                            floors.forEach(floor => {
                                addRemarkButton(floor);
                                displayRemark(floor);
                            });
                            $AEL(review.messager, 'update', e => {
                                e.detail.floors.forEach(floor => {
                                    addRemarkButton(floor);
                                    displayRemark(floor);
                                });
                            });

                            // 随用户备注更新显示
                            $AEL(messager, 'change', e => {
                                /** @type { {id: number, remark: string} } */
                                const { id, remark } = e.detail;
                                floors.filter(floor => floor.data.user.id === id).forEach(floor => {
                                    review.FloorManager.transformer.updateLine(
                                        floor,
                                        'remark',
                                        getRemarkText(floor.data.user.id)
                                    );
                                });
                            });

                            /** @typedef {typeof review._types.Floor} Floor */
                            /**
                             * 为评论楼层添加用户备注按钮
                             * @param {Floor} floor 
                             */
                            function addRemarkButton(floor) {
                                review.FloorManager.transformer.addUserButton(floor, {
                                    id: 'remark',
                                    label: CONST.Text.UserRemark.RemarkUser,
                                    index: 1,
                                    callback() {
                                        promptRemark({
                                            id: floor.data.user.id,
                                            name: floor.data.user.name
                                        });
                                    }
                                });
                            }

                            /**
                             * 为评论楼层的用户展示备注
                             * @param {Floor} floor 
                             */
                            function displayRemark(floor) {
                                review.FloorManager.transformer.addUserLine(floor, {
                                    id: 'remark',
                                    line: getRemarkText(floor.data.user.id),
                                    base: 'type',
                                    position: 'before',
                                });
                            }
                        }
                    },
                    userpage: {
                        checkers: {
                            type: 'path',
                            value: '/userpage.php'
                        },
                        async func() {
                            /** @type {userpage} */
                            const userpage = await require('userpage', true);
                            const page = userpage.PageManager.page;

                            // 设置备注按钮
                            userpage.PageManager.transformer.addUserButton(
                                page, {
                                    id: 'remark',
                                    label: CONST.Text.UserRemark.RemarkUser,
                                    index: 1,
                                    callback() {
                                        promptRemark({
                                            id: page.data.user.id,
                                            name: page.data.user.name
                                        });
                                    }
                                }
                            );

                            // 显示备注
                            userpage.PageManager.transformer.addUserLine(
                                page, {
                                    id: 'remark',
                                    line: getRemarkText(page.data.user.id),
                                    base: 'level',
                                    position: 'after',
                                }
                            );

                            // 随用户备注更新显示
                            $AEL(messager, 'change', e => {
                                /** @type { {id: number, remark: string} } */
                                const { id, remark } = e.detail;
                                userpage.PageManager.transformer.updateLine(
                                    page,
                                    'remark',
                                    getRemarkText(page.data.user.id)
                                );
                            });
                        }
                    }
                };
                if (GM_getValue('enabled')) {
                    const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, { GM_setValue, GM_getValue });
                    await promise;
                }

                /**
                 * 弹窗提示用户对指定用户设置备注
                 * @param {Object} user - 用户信息
                 * @param {number} user.id - 用户id
                 * @param {string} [user.name] - 用户名
                 */
                function promptRemark({ id, name = null }) {
                    Quasar.Dialog.create({
                        title: CONST.Text.UserRemark.Prompt.Title,
                        message: replaceText(
                            CONST.Text.UserRemark.Prompt.Message,
                            { '{Name}': name ?? id.toString() }
                        ),
                        prompt: {
                            model: getRemark(id) ?? name ?? id,
                            type: 'text',
                            color: 'primary',
                        },
                        ok: {
                            label: CONST.Text.UserRemark.Prompt.Ok,
                            color: 'primary',
                        },
                        cancel: {
                            label: CONST.Text.UserRemark.Prompt.Cancel,
                            color: 'secondary',
                        },
                    }).onOk(remark => {
                        setRemark(id, remark);
                        Quasar.Notify.create({
                            type: 'success',
                            message: CONST.Text.UserRemark.Prompt.Saved,
                            caption: remark,
                            group: 'remark.remark-saved',
                        });
                    });
                }

                /**
                 * 获取对用户的备注
                 * @param {number} id - 用户id
                 */
                function getRemark(id) {
                    const str_id = id.toString();
                    const remarks = GM_getValue('remarks');
                    return remarks.hasOwnProperty(str_id) ? remarks[str_id] : null;
                }

                /**
                 * 设置用户的备注
                 * @param {number} id - 用户id
                 * @param {string} remark - 备注内容
                 */
                function setRemark(id, remark) {
                    const str_id = id.toString();
                    const remarks = GM_getValue('remarks');
                    if (remark) {
                        remarks[str_id] = remark;
                    } else {
                        delete remarks[str_id];
                    }
                    GM_setValue('remarks', remarks);
                    messager.dispatchEvent(new CustomEvent('change', {
                        detail: { id, remark }
                    }));
                }

                /**
                 * 获取用户备注在UI中显示的文本
                 * 形如: "用户备注: 备注内容" / "未设置用户备注"
                 */
                function getRemarkText(id) {
                    const remark = getRemark(id);
                    return remark ? replaceText(
                        CONST.Text.UserRemark.RemarkDisplay,
                        { '{Remark}': remark }
                    ) : CONST.Text.UserRemark.RemarkNotSet;
                }

                return {
                    get remarks() { return GM_getValue('remarks') },
                    getRemark, setRemark,
                }
            }
        },
        userreview: {
            desc: '查看用户书评',
            checkers: [{
                // 书评
                type: 'path',
                value: '/modules/article/reviewshow.php'
            }, {
                // 用户主页
                type: 'path',
                value: '/userpage.php'
            }],
            dependencies: ['debugging', 'utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                GM_getValue = utils.defaultedGet({
                    enabled: true,
                }, GM_getValue);

                // 如果是发评论返回的提示页面,不继续运行
                if ($All('.block').length === 1) { return; }

                // 实际功能函数,只有启用备注功能时才运行
                const pool_funcs = {
                    review: {
                        checkers: {
                            type: 'path',
                            value: '/modules/article/reviewshow.php'
                        },
                        async func() {
                            /** @type {review} */
                            const review = await require('review', true);
                            const FloorManager = review.FloorManager;
                            const floors = FloorManager.floors;

                            // 显示用户备注
                            floors.forEach(floor => {
                                addReviewButton(floor);
                            });
                            $AEL(review.messager, 'update', e => {
                                e.detail.floors.forEach(floor => {
                                    addReviewButton(floor);
                                });
                            });

                            /** @typedef {typeof review._types.Floor} Floor */
                            /**
                             * 
                             * @param {Floor} floor 
                             */
                            function addReviewButton(floor) {
                                review.FloorManager.transformer.addUserButton(floor, {
                                    id: 'user_review',
                                    label: CONST.Text.UserReview.CheckUserReviews,
                                    index: 2,
                                    element: $$CrE({
                                        tagName: 'a',
                                        attrs: {
                                            href: `https://${ location.host }/modules/article/reviewslist.php?keyword=${ floor.data.user.id }`,
                                            target: '_blank',
                                        },
                                        props: {
                                            innerText: CONST.Text.UserReview.CheckUserReviews,
                                        },
                                    }),
                                });
                            }
                        }
                    },
                    userpage: {
                        checkers: {
                            type: 'path',
                            value: '/userpage.php'
                        },
                        async func() {
                            /** @type {userpage} */
                            const userpage = await require('userpage', true);
                            const page = userpage.PageManager.page;

                            // 设置备注按钮
                            const uid = parseInt(new URLSearchParams(location.search).get('uid'), 10);
                            userpage.PageManager.transformer.addUserButton(
                                page, {
                                    id: 'remark',
                                    index: 1,
                                    element: $$CrE({
                                        tagName: 'a',
                                        attrs: {
                                            href: `https://${ location.host }/modules/article/reviewslist.php?keyword=${ uid }`,
                                            target: '_blank',
                                        },
                                        props: {
                                            innerText: CONST.Text.UserReview.CheckUserReviews,
                                        },
                                    }),
                                }
                            );
                        }
                    },
                };
                if (GM_getValue('enabled')) {
                    const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, { GM_setValue, GM_getValue });
                    await promise;
                }
            }
        },
        metacopy: {
            desc: '在小说信息页提供复制小说元标签的功能',
            checkers: [{
                type: 'regpath',
                value: /\/book\/\d+\.htm/
            }, {
                type: 'path',
                value: '/modules/article/articleinfo.php'
            }],
            detectDom: '.main.m_foot',
            func() {
                const tds = [...$All('#content > div:first-child > table:first-child > tbody > tr:last-child > td')];
                tds.forEach(td => addCopyButton(td));

                /**
                 * @param {HTMLTableCellElement} td 
                 */
                function addCopyButton(td) {
                    const [key, val] = td.innerText.trim().split(':');
                    td.insertAdjacentElement('beforeend', $$CrE({
                        tagName: 'span',
                        props: {
                            innerText: CONST.Text.MetaCopy.CopyButton,
                        },
                        styles: {
                            color: 'var(--q-primary)',
                            cursor: 'pointer',
                            paddingLeft: '0.5em',
                        },
                        listeners: [['click', e => {
                            GM_setClipboard(val, 'text/plain');
                            Quasar.Notify.create({
                                type: 'success',
                                message: CONST.Text.MetaCopy.Copied,
                                caption: val,
                                group: 'metacopy.copied',
                            });
                        }]]
                    }));
                }
            },
        },
        bookcase: {
            desc: '书架相关功能',
            checkers: [{
                type: 'path',
                value: '/modules/article/bookcase.php'
            }, {
                type: 'path',
                value: '/modules/article/addbookcase.php'
            }],
            dependencies: ['utils'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            /** @typedef {Awaited<ReturnType<typeof functions.darkmode.func>>} darkmode */
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                
                /**
                 * 通信信使,通过派发CustomEvent传递消息,目前有以下事件:
                 * - switch  
                 *   当用户切换页面上展示的书架时派发,如从默认书架切换至第一组书架
                 *   - classid {number}
                 *   - old_form {HTMLFormElement} - 切换前显示的form元素
                 *   - new_form {HTMLFormElement} - 切换后显示的form元素
                 * - update  
                 *   当书架刷新完成时派发,可以是用户主动刷新书架/执行某些书架修改后自动刷新等
                 *   - classid {number}
                 *   - old_form {HTMLFormElement} - 数据更新前旧的form元素
                 *   - new_form {HTMLFormElement} - 数据更新后新的form元素
                 * - rename  
                 *   当用户重命名书架时派发
                 *   - classid {number}
                 *   - old_name {string}
                 *   - new_name {string}
                 */
                const messager = new EventTarget();
                
                const pool_funcs = {
                    collector: {
                        desc: '多书架整合',
                        checkers: {
                            type: 'path',
                            value: '/modules/article/bookcase.php'
                        },
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.collector.func>>} collector */
                        async func() {
                            // 获取所有书架页面
                            Quasar.Loading.show({ message: CONST.Text.Bookcase.Collector.FetchingBookcases });
                            let page_classid = parseInt(new URLSearchParams(location.search).get('classid') ?? '0', 10);
                            const forms = await Promise.all([0, 1, 2, 3, 4, 5].map(async classid => {
                                return classid === page_classid ?
                                    await detectDom('#checkform') :
                                    await fetchBookcase(classid);
                            }));

                            // 切换书架功能
                            Quasar.Loading.show({ message: CONST.Text.Bookcase.Collector.ArrangingBookcases });
                            /** @type {(classid: number) => void} */
                            const switchBookcase = classid => {
                                // 切换form
                                const cur_form = $('#checkform');
                                const form = forms[classid];
                                cur_form.after(form);
                                cur_form.remove();

                                // 切换classid并更新到url
                                page_classid = classid;
                                const new_url = new URL(location.href);
                                new_url.searchParams.set('classid', classid.toString());
                                history.replaceState(null, '', new_url.href);

                                // 广播切换事件
                                messager.dispatchEvent(new CustomEvent('switch', {
                                    detail: {
                                        old_form: cur_form,
                                        new_form: form,
                                        classid,
                                    }
                                }));
                            };
                            /** @type {(form: HTMLFormElement, classid: number) => void} */
                            const connectSwitcher = (form, classid) => $AEL($(form, 'select[name="classlist"]'), 'change', e => {
                                e.stopImmediatePropagation();
                                const select = e.target;
                                const new_classid = parseInt(select.value, 10);
                                select.value = classid.toString();
                                switchBookcase(new_classid);
                            }, { capture: true });
                            applyToAllForms(connectSwitcher);

                            // 页面内更新书架功能
                            /** @type {([classid]: number, [new_form]: HTMLFormElement) => Promise<void>} */
                            const updateBookcase = async (classid=null, new_form=null) => {
                                Quasar.Loading.show({ message: CONST.Text.Bookcase.Collector.UpdatingBookcase });

                                // 如果不提供classid,则更新所有书架
                                if (classid === null) {
                                    await Promise.all(forms.map(async (form, classid) => updateBookcase(classid)));
                                    return;
                                }

                                // 获取新书架
                                const form = forms[classid];
                                new_form = new_form ?? await fetchBookcase(classid);
                                forms[classid] = new_form;
                                if (document.body.contains(form)) {
                                    form.after(new_form);
                                    form.remove();
                                }

                                // 广播更新事件
                                messager.dispatchEvent(new CustomEvent('update', {
                                    detail: {
                                        old_form: form,
                                        new_form: new_form,
                                        classid,
                                    }
                                }));

                                Quasar.Loading.hide();
                            };
                            const convertActionsInpage = (form, classid) => {
                                // 表单提交改为ajax提交
                                $AEL(form, 'submit', async e => {
                                    const form = e.target;

                                    // 记录当前操作的名称
                                    const action_select = $(form, '#newclassid');
                                    const action_val = action_select.value;
                                    const action_name = [...$All(action_select, 'option')]
                                        .find(option => option.value === action_val).innerText;

                                    // 提交时,阻止默认表单提交
                                    e.preventDefault();

                                    // 接管文库页面自带的submit钩子
                                    e.stopImmediatePropagation();
                                    const orig_checker = form.onsubmit;
                                    if (!await checkSubmit()) { return; }

                                    // ajax提交表单
                                    Quasar.Loading.show({ message: CONST.Text.Bookcase.Collector.SubmitingChange });
                                    const formdata = new FormData(form);
                                    const doc = await utils.requestDocument({
                                        method: 'POST',
                                        url: `/modules/article/bookcase.php?classid=${page_classid}&ajax_gets=jieqi_contents`,
                                        data: utils.serializeFormData(formdata),
                                        headers: {
                                            'content-type': 'application/x-www-form-urlencoded',
                                            'referrer': location.href,
                                        },
                                    });
                                    const new_form = $(doc, '#checkform');
                                    Quasar.Loading.hide();

                                    // 更新书架
                                    await Promise.all([
                                        // 更新当前书架
                                        updateBookcase(classid, new_form),
                                        // 如果有,更新相关书架
                                        formdata.get('newclassid') ? updateBookcase(parseInt(formdata.get('newclassid'))) : Promise.resolve()
                                    ]);

                                    // 提示完成
                                    Quasar.Notify.create({
                                        type: 'success',
                                        message: replaceText(
                                            CONST.Text.Bookcase.Collector.ActionFinished,
                                            { '{ActionName}': action_name }
                                        ),
                                        group: 'bookcase.moved'
                                    });
                                }, { capture: true });

                                // 移除书籍按钮改为ajax提交
                                [...$All(form, 'tbody > tr > td:last-child > a')].forEach(a => $AEL(a, 'click', async e => {
                                    e.preventDefault();
                                    const bid = parseInt(new URLSearchParams(a.closest('tr').children[1].querySelector('a').search).get('bid'), 10);
                                    const bookname = a.closest('tr').children[1].querySelector('a').innerText.trim();
                                    
                                    if (!await confirmRemove(bookname)) { return; }
                                    const doc = await utils.requestDocument({
                                        method: 'GET',
                                        url: `/modules/article/bookcase.php?classid=${classid}&ajax_gets=jieqi_contents&ajax_request=${Date.now()}&delid=${bid}`,
                                    });
                                    const new_form = $(doc, '#checkform');
                                    updateBookcase(classid, new_form);
                                    Quasar.Notify.create({
                                        type: 'success',
                                        message: CONST.Text.Bookcase.Collector.Removed,
                                        caption: bookname,
                                        group: 'bookcase.book-removed',
                                    });
                                }));

                                /**
                                 * 功能和文库自身的window.check_confirm一模一样,用于表单提交前检查和操作确认,但是用quasar提示框重写的
                                 * @returns {Promise<boolean>}
                                 */
                                async function checkSubmit() {
                                    const form = $('#checkform');

                                    // 检查是否未选中任何书籍
                                    /** @type {string[]} 被选择的书名 */
                                    const checked_books = [...$All(form, 'input[name="checkid[]"]')]
                                        .filter(check => check.checked)
                                        .map(check => check.closest('tr').children[1].querySelector('a').innerText.trim());
                                    if (!checked_books.length) {
                                        Quasar.Notify.create({
                                            type: 'error',
                                            message: CONST.Text.Bookcase.Collector.NoBooksSelected
                                        });
                                        return false;
                                    }

                                    // 如果正在移除书籍,先进行确认
                                    // 这里的 == 非全等号写法是在和文库自带函数代码保持一致,实际上value值应为'-1'
                                    if ($(form, '#newclassid').value == -1) {
                                        const book_names = checked_books.join('、');
                                        return await confirmRemove(book_names);
                                    } else {
                                        return true;
                                    }
                                }
                            };
                            applyToAllForms(convertActionsInpage);

                            // 侧边栏按钮
                            require('sidepanel', true).then(
                                /** @param {sidepanel} sidepanel */
                                sidepanel => sidepanel.registerButton({
                                    id: 'bookcase.refresh',
                                    icon: 'sync',
                                    label: CONST.Text.Bookcase.Collector.RefreshBookcase,
                                    index: 2,
                                    async callback() {
                                        await updateBookcase();
                                        Quasar.Notify.create({
                                            type: 'success',
                                            message: CONST.Text.Bookcase.Collector.Refreshed,
                                            group: 'bookcase.bookcase-refreshed',
                                        });
                                    },
                                })
                            );

                            Quasar.Loading.hide();

                            /**
                             * 询问用户是否要将某一书籍移出书架
                             * @param {string} bookname
                             * @returns {Promise<boolean>} 
                             */
                            function confirmRemove(bookname) {
                                const { promise, resolve } = Promise.withResolvers();
                                const ConfirmRemove = CONST.Text.Bookcase.Collector.Dialog.ConfirmRemove;
                                Quasar.Dialog.create({
                                    message: replaceText(
                                        ConfirmRemove.Message,
                                        { '{Name}': bookname }
                                    ),
                                    title: ConfirmRemove.Title,
                                    ok: {
                                        label: ConfirmRemove.ok,
                                        color: 'primary',
                                    },
                                    cancel: {
                                        label: ConfirmRemove.cancel,
                                        color: 'secondary',
                                    },
                                }).onOk(() => resolve(true)).onCancel(() => resolve(false));

                                return promise;
                            }

                            /**
                             * 网络请求获取指定书架form元素
                             * @param {number} classid
                             * @returns {Promise<HTMLFormElement>} 
                             */
                            async function fetchBookcase(classid) {
                                const doc = await utils.requestDocument({
                                    method: 'GET',
                                    url: `/modules/article/bookcase.php?classid=${classid}&ajax_gets=jieqi_contents&ajax_request=${Date.now()}`,
                                });
                                return $(doc, '#checkform');
                            }

                            /**
                             * 将提供的方法对所有书架form元素执行一次,包括现有的form、未来更新创建的新form等全部form
                             * @param {(form: HTMLFormElement, classid: number) => any} func 
                             */
                            function applyToAllForms(func) {
                                forms.forEach((form, classid) => func(form, classid));
                                $AEL(messager, 'update', e => func(e.detail.new_form, e.detail.classid));
                            }
                            
                            return {
                                // 数据
                                forms,
                                get classid() { return page_classid; },
                                set classid(classid) { page_classid = classid; },
                                // 功能
                                switchBookcase, updateBookcase,
                                // 底层-适合内部使用
                                connectSwitcher, convertActionsInpage,
                                // 底层-适合外部使用
                                fetchBookcase, applyToAllForms,
                            }
                        }
                    },
                    naming: {
                        desc: '书架自命名',
                        dependencies: 'collector',
                        params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
                        checkers: {
                            type: 'path',
                            value: '/modules/article/bookcase.php'
                        },
                        async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                            /** @type {collector} */
                            const collector = pool.require('collector');

                            const default_names = [
                                '默认书架',
                                '第1组书架',
                                '第2组书架',
                                '第3组书架',
                                '第4组书架',
                                '第5组书架',
                            ];
                            GM_getValue = utils.defaultedGet({
                                names: default_names,
                            }, GM_getValue);

                            // 当储存的names名称数据变化时,派发rename事件
                            GM_addValueChangeListener('names', (key, old_val, new_val, remote) => {
                                for (let classid = 0; classid < 6; classid++) {
                                    const [old_name, new_name] = [
                                        old_val?.[classid] ?? default_names[classid],
                                        new_val[classid]
                                    ];
                                    old_name !== new_name &&
                                        messager.dispatchEvent(new CustomEvent('rename', {
                                            detail: {
                                                old_name,
                                                new_name,
                                                classid,
                                            }
                                        }));
                                }
                            });

                            // 重命名按钮
                            collector.applyToAllForms((form, classid) => {
                                const select = $(form, 'select[name="classlist"]');
                                const button = $$CrE({
                                    tagName: 'span',
                                    props: {
                                        innerText: CONST.Text.Bookcase.Naming.Rename,
                                    },
                                    styles: {
                                        border: '1px solid',
                                        padding: '3px',
                                        cursor: 'pointer',
                                        marginLeft: '0.5em',
                                    },
                                    listeners: [['click', async e => {
                                        const name = await promptNewName(classid);
                                        name !== null && saveName(classid, name);
                                    }]]
                                });
                                const icon = $$CrE({
                                    tagName: 'i',
                                    classes: 'material-icons',
                                    props: { innerText: 'drive_file_rename_outline' },
                                    styles: {
                                        verticalAlign: 'text-bottom',
                                    }
                                });
                                button.insertAdjacentElement('afterbegin', icon);
                                select.after(button);
                            });

                            // 对每个书架应用用户设定的名称
                            collector.applyToAllForms((form, classid) => {
                                const names = GM_getValue('names');
                                [...$All(form, 'select[name="classlist"] > option')].forEach((option, op_classid) => {
                                    option.innerText = names[op_classid];
                                });
                                [...$All(form, '#newclassid > option')].forEach(option => {
                                    const op_classid = parseInt(option.value, 10);
                                    if (op_classid >= 0) {
                                        option.innerText = replaceText(
                                            CONST.Text.Bookcase.Naming.MoveTo,
                                            { '{Name}': names[op_classid] }
                                        );
                                    }
                                });
                            })

                            // 重命名发生时修改GUI中的名称
                            $AEL(messager, 'rename', e => {
                                collector.forms.forEach((form, classid) => {
                                    const switch_option = $(form, `select[name="classlist"] > option[value="${e.detail.classid}"]`);
                                    switch_option.innerText = e.detail.new_name;
                                    const move_option = $(form, `#newclassid > option[value="${e.detail.classid}"]`);
                                    move_option.innerText = replaceText(
                                        CONST.Text.Bookcase.Naming.MoveTo,
                                        { '{Name}': e.detail.new_name }
                                    );
                                });
                            });

                            /**
                             * 向用户弹窗输入新的书架名字
                             * @param {number} classid 
                             * @returns {Promise<string | null>} 新名字,或者null(当用户点击取消时)
                             */
                            function promptNewName(classid) {
                                const { promise, resolve } = Promise.withResolvers();
                                const Naming = CONST.Text.Bookcase.Naming;
                                const PromptNewName = Naming.Dialog.PromptNewName;
                                const old_name = GM_getValue('names')[classid] ?? replaceText(
                                    Naming.DefaultName,
                                    { '{ClassID}': classid.toString() }
                                );
                                Quasar.Dialog.create({
                                    message: replaceText(
                                        PromptNewName.Message,
                                        { '{OldName}': old_name }
                                    ),
                                    title: PromptNewName.Title,
                                    prompt: {
                                        model: old_name,
                                        type: 'text',
                                        color: 'primary',
                                    },
                                    ok: {
                                        label: PromptNewName.Ok,
                                        color: 'primary',
                                    },
                                    cancel: {
                                        label: PromptNewName.Cancel,
                                        color: 'secondary'
                                    }
                                }).onOk(new_name => resolve(new_name)).onCancel(() => resolve(null));
                                return promise;
                            }

                            function saveName(classid, name) {
                                // 保存名称
                                const names = GM_getValue('names');
                                names[classid] = name;
                                GM_setValue('names', names);
                            }
                        }
                    },
                    addpagejump: {
                        desc: '在“成功加入书架!”页面添加跳转到书架的按钮',
                        checkers: {
                            type: 'path',
                            value: '/modules/article/addbookcase.php',
                        },
                        detectDom: '.blocknote',
                        func() {
                            const close_btn = $('a[href="javascript:window.close()"]');
                            const container = close_btn.parentElement;
                            container.insertAdjacentText('afterbegin', ' ');
                            container.insertAdjacentText('afterbegin', ']');
                            container.insertAdjacentElement('afterbegin', $$CrE({
                                tagName: 'a',
                                attrs: {
                                    href: `/modules/article/bookcase.php`,
                                },
                                props: {
                                    innerText: CONST.Text.Bookcase.AddpageJump.GotoBookcase
                                },
                            }));
                            container.insertAdjacentText('afterbegin', '[');
                        }
                    }
                };
                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                    GM_setValue, GM_getValue, GM_addValueChangeListener
                });
                await promise;
            },
        },
        readlater: {
            desc: '稍后再读',
            dependencies: ['utils', 'debugging'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            /** @typedef {Awaited<ReturnType<typeof functions.darkmode.func>>} darkmode */
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {debugging} */
                const debugging = require('debugging');

                GM_getValue = utils.defaultedGet({
                    /** @type {Book[]} */
                    list: [],
                }, GM_getValue);

                /**
                 * @typedef {Object} Book
                 * @property {number} aid
                 * @property {string} name
                 * @property {string} cover
                 */

                const pool_funcs = {
                    core: {
                        // 这里不用让FunctionLoader包装子存储,直接将list存储在readlater的全局作用域中即可
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.core.func>>} core */
                        func() {
                            // 内容更改监听器
                            /** @type {((val: Book[]) => any)[]} */
                            const listeners = [];

                            GM_addValueChangeListener('list', (key, old_val, new_val, remote) => {
                                listeners.forEach(l => debugging.callWithErrorHandling(l, null, [new_val]));
                            });

                            /**
                             * 将书籍添加到稍后再读列表
                             * @param {Book} book
                             * @returns {boolean} 添加成功,还是已经在稍后再读中
                             */
                            function add(book) {
                                /** @type {Book[]} */
                                const list = GM_getValue('list');
                                if (list.some(b => b.aid === book.aid)) { return false; }

                                list.push(book);
                                GM_setValue('list', list);
                                return true;
                            }

                            /**
                             * 从稍后再读中移除一本书
                             * @param {number} aid
                             * @returns {Book | null} 如果移除成功,返回这本书;如果指定书不存在,返回null
                             */
                            function remove(aid) {
                                /** @type {Book[]} */
                                const list = GM_getValue('list');
                                const index = list.findIndex(b => b.aid === aid);
                                if (index < 0) { return null; }

                                const book = list.splice(index, 1)[0];
                                GM_setValue('list', list);
                                return book;
                            }

                            /**
                             * 添加稍后列表值改变监听器
                             * @param {(val: Book[]) => any} listener 
                             */
                            function onChange(listener) {
                                listeners.push(listener);
                            }

                            return {
                                /** @type {Book[]} */
                                get list() { return GM_getValue('list'); },
                                set list(val) { return GM_setValue('list', val); },
                                add, remove, onChange,
                            };
                        }
                    },
                    bookpage: {
                        desc: '书籍信息页添加稍后再读按钮',
                        checkers: [{
                            type: 'regpath',
                            value: /\/book\/\d+\.htm/
                        }, {
                            type: 'path',
                            value: '/modules/article/articleinfo.php'
                        }],
                        dependencies: 'core',
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.bookpage.func>>} bookpage */
                        async func() {
                            /** @type {sidepanel} */
                            const sidepanel = await require('sidepanel', true);
                            /** @type {core} */
                            const core = pool.require('core');

                            sidepanel.registerButton({
                                id: 'readlater.add',
                                icon: 'watch_later',
                                label: CONST.Text.ReadLater.Add,
                                index: 3,
                                callback() {
                                    const aid = parseInt(new URLSearchParams(location.search).get('id')
                                        ?? location.pathname.match(/\/book\/(\d+)\.htm/)[1], 10);
                                    const name = $('#content > div:first-child > table:first-child > tbody > tr:first-child > td > table span > b').innerText.trim();
                                    const cover = $('#content > div:first-child > table:nth-of-type(2) img').src;
                                    const success = core.add({ aid, name, cover });

                                    const ReadLater = CONST.Text.ReadLater;
                                    Quasar.Notify.create({
                                        type: 'success',
                                        message: ReadLater.Added,
                                        caption: replaceText(
                                            success ? ReadLater.AddSuccess : ReadLater.AddDuplicate,
                                            { '{Name}': name }
                                        ),
                                        icon: success ? 'done' : 'question_mark',
                                        group: 'readlater.added'
                                    });
                                }
                            });
                        }
                    },
                    indexpage: {
                        desc: '主页展示稍后再读',
                        checkers: [{
                            type: 'path',
                            value: '/index.php'
                        }, {
                            type: 'path',
                            value: '/'
                        }],
                        detectDom: '.main.m_foot',
                        dependencies: ['core'],
                        async func() {
                            /** @type {core} */
                            const core = pool.require('core');

                            // 创建稍后再读列表
                            const container = $$CrE({
                                tagName: 'div',
                                classes: 'main'
                            });
                            container.innerHTML = `
                                <div class="block">
                                    <div class="blocktitle">${ CONST.Text.ReadLater.Title }</div>
                                    <div class="blockcontent">
                                        <div style="height:155px;">
                                        </div>
                                    </div>
                                </div>
                            `;
                            $('.main.m_foot').previousElementSibling.previousElementSibling.before(container);
                            const books_container = $(container, '.blockcontent > div');

                            // 创建Sortable
                            const sortable = new Sortable(books_container, {
                                onUpdate(e) {
                                    const aidlist = sortable.toArray();
                                    core.list = aidlist.map(aid => core.list.find(book => book.aid === parseInt(aid, 10)));
                                },
                            });

                            // 创建列表内容
                            refreshList();

                            // 当列表更改时,重建列表
                            core.onChange(list => refreshList(list));

                            /**
                             * 清空稍后再读列表并重建
                             * @param {Book[]} [list]
                             */
                            function refreshList(list) {
                                list = list ?? core.list;

                                // 首先清空已有内容
                                [...books_container.children].forEach(elm => elm.remove());

                                // 重建
                                if (list.length) {
                                    // 如果稍后再读不为空,则为前十本书创建元素
                                    // 之所以是前十本,是因为文库的这个列表只有展示十本的空间
                                    list.filter((b, i) => i < 10).forEach(book => {
                                        const book_container = $$CrE({
                                            tagName: 'div',
                                            attrs: {
                                                style: 'float: left;text-align:center;width: 95px; height:155px;overflow:hidden;',
                                                'data-id': book.aid.toString(),
                                            },
                                            styles: { position: 'relative' },
                                            classes: 'plus-readlater-book',
                                        });
                                        book_container.innerHTML = `
                                            <a href="/book/${ book.aid }.htm" target="_blank">
                                            <img src="${ book.cover }" border="0" width="90" height="127"></a>
                                            <br>
                                            <a href="/book/${ book.aid }.htm" target="_blank">${ book.name }</a>
                                        `;
                                        book_container.append($$CrE({
                                            tagName: 'div',
                                            props: {
                                                innerHTML: `<i class="material-icons">close</i>`,
                                            },
                                            classes: ['plus-remove-readlater'],
                                            listeners: [[ 'click', e => core.remove(book.aid) ]]
                                        }));
                                        addStyle(`
                                            .plus-remove-readlater {
                                                position: absolute;
                                                right: 0;
                                                top: 0;
                                                font-size: 1.5em;
                                                color: #0d548b;
                                                border: 1px dashed #0d548b;
                                                padding: 0.1em;
                                                cursor: pointer;
                                                background: rgba(255, 255, 255, 0.5);
                                                display: none;
                                            }
                                            :is(body.mobile, .plus-readlater-book:hover) .plus-remove-readlater {
                                                display: block;
                                            }
                                            .plus-remove-readlater:hover {
                                                background: rgba(255, 255, 255, 0.8);
                                            }
                                        `, 'readlater-style');
                                        utils.setTip($(book_container, 'a:first-child'), book.name);
                                        books_container.append(book_container);
                                    });
                                } else {
                                    // 如果稍后再读为空,展示提示
                                    books_container.append($$CrE({
                                        tagName: 'div',
                                        props: {
                                            innerText: CONST.Text.ReadLater.EmptyListPlaceholder
                                        },
                                        classes: 'text-grey-7',
                                        styles: {
                                            width: '100%',
                                            height: '100%',
                                            display: 'flex',
                                            alignItems: 'center',
                                            justifyContent: 'center',
                                            fontSize: '1.5em',
                                        }
                                    }));
                                }
                            }
                        }
                    }
                };

                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                    GM_setValue, GM_getValue, GM_addValueChangeListener
                });
                await promise;

                return {
                    /** @type {core} */
                    core: pool.require('core'),

                    /** 用于导出JSDoc类型,无实际作用 */
                    _types: {
                        /** @type {Book} */
                        Book: {},
                    }
                };
            },
        },
        blockfolding: {
            desc: '主页板块折叠',
            checkers: [{
                type: 'path',
                value: '/'
            }, {
                type: 'path',
                value: '/index.php'
            }],
            dependencies: ['utils'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');

                /**
                 * 记录了折叠状态但不再出现在文档中的板块的记录
                 * @typedef {{title: string, count: number}} DisappearRecord
                 */
                GM_getValue = utils.defaultedGet({
                    /** @type {string[]} 折叠板块的标题列表 */
                    folds: [],
                    /** @type {DisappearRecord[]} 在folds中但在页面中未出现的板块列表 */
                    unused: [],
                }, GM_getValue);

                // 应用折叠到文档
                detectDom({
                    selector: '.block',
                    /** @param {HTMLDivElement} block */
                    callback(block) {
                        block.matches('[class*="q-"]:not(body) *') || initBlock(block);
                    }
                });

                // 当存储改变时,同步改变文档折叠状态
                GM_addValueChangeListener('folds', (key, old_val, new_val, remote) => {
                    [...$All('.block')].forEach(block => applyFoldStatus(block));
                });

                // 清理已消失的板块的折叠状态
                $AEL(window, 'load', e => {
                    const folds = GM_getValue('folds');
                    const titles = [...$All('.block')].filter(block => !block.matches('[class*="q-"]:not(body) *')).map(block => getTitle(block));
                    let modified = false;

                    // 记录在folds中、但最终未出现在文档中的板块,记录到unused中
                    // 当在unused中记录次数达到一定值时,将其从folds和unused中移除
                    folds.filter(t => !titles.includes(t)).forEach(title => {
                        /** @type {DisappearRecord[]} */
                        const unused = GM_getValue('unused');
                        const record = unused.find(r => r.title === title) ?? {
                            title, count: 0
                        };
                        record.count++;

                        if (record.count >= CONST.Internal.RemoveBlockFoldingCount) {
                            // 达到清除标准,从unused和folds中移除此板块和记录
                            modified = true;
                            folds.splice(folds.indexOf(record.title), 1);
                            unused.includes(record) && unused.splice(unused.indexOf(record), 1);
                        } else {
                            // 未达到清除标准,仅修改记录次数
                            !unused.includes(record) && unused.push(record);
                        }
                        GM_setValue('unused', unused);
                    });

                    // 在unused中有记录,但本次观察到出现的板块,清除在unused中的记录
                    /** @type {DisappearRecord[]} */
                    const unused = GM_getValue('unused');
                    unused.filter(r => titles.includes(r.title)).forEach(record => 
                        unused.splice(unused.indexOf(record), 1));

                    modified && GM_setValue('folds', folds);
                });
                // 样式
                addStyle(`
                    .plus-folded .blockcontent {
                        display: none;
                    }
                    .blocktitle .foldbtn {
                        display: inline;
                    }
                    .plus-folded .blocktitle .foldbtn {
                        display: none;
                    }
                    .blocktitle .unfoldbtn {
                        display: none;
                    }
                    .plus-folded .blocktitle .unfoldbtn {
                        display: inline;
                    }
                    .foldbtn-group {
                        float: right;
                        height: 100%;
                        display: flex;
                        flex-direction: row;
                        align-items: center;
                        cursor: pointer;
                        margin-right: 10px;
                        width: 0;
                        position: relative;
                        overflow: visible;
                    }
                    .foldbtn-group * {
                        position: absolute;
                        right: 0;
                        text-align: right;
                        white-space: nowrap;
                    }
                `);

                /**
                 * 初始化指定板块,添加折叠/展开按钮,一次性应用存储的折叠/展开状态
                 * @param {HTMLDivElement} block 
                 */
                function initBlock(block) {
                    // 添加折叠/展开按钮
                    const button = $$CrE({
                        tagName: 'span',
                        classes: 'foldbtn-group'
                    });
                    button.append(
                        $$CrE({
                            tagName: 'span',
                            props: {
                                innerText: CONST.Text.BlockFolding.Fold,
                            },
                            classes: 'foldbtn',
                            listeners: [['click', e => setFold(block, true)]]
                        }),
                        $$CrE({
                            tagName: 'span',
                            props: {
                                innerText: CONST.Text.BlockFolding.UnFold,
                            },
                            classes: 'unfoldbtn',
                            listeners: [['click', e => setFold(block, false)]]
                        }),
                    );
                    $(block, '.blocktitle').append(button);

                    // 应用存储的折叠/展开状态
                    applyFoldStatus(block);
                }

                /**
                 * 将存储的折叠状态应用到指定的板块DOM中
                 * @param {HTMLDivElement} block 
                 */
                function applyFoldStatus(block) {
                    const title = getTitle(block);
                    const folded = GM_getValue('folds').includes(title);
                    folded ? fold(block) : unfold(block);
                }

                /**
                 * 将一个板块DOM置于折叠状态
                 * @param {HTMLDivElement} block 
                 */
                function fold(block) {
                    block.classList.add('plus-folded');
                }

                /**
                 * 将一个板块DOM置于展开(非折叠)状态
                 * @param {HTMLDivElement} block 
                 */
                function unfold(block) {
                    block.classList.remove('plus-folded');
                }

                /**
                 * 设置一个板块的折叠/展开状态到存储
                 * @param {HTMLDivElement} block 
                 * @param {boolean} fold 
                 */
                function setFold(block, fold) {
                    const title = getTitle(block);
                    const folds = GM_getValue('folds');
                    fold ?
                        (folds.includes(title) || folds.push(title)) :
                        (folds.includes(title) && folds.splice(folds.indexOf(title), 1));
                    GM_setValue('folds', folds);
                }

                function getTitle(block) {
                    const blocktitle = $(block, '.blocktitle').cloneNode(true);
                    $(blocktitle, '.foldbtn-group')?.remove();
                    return blocktitle.innerText.trim();
                }
            },
        },
        announcements: {
            desc: '在首页等位置插入脚本公告信息等',
            checkers: [{
                type: 'path',
                value: '/'
            },{
                type: 'path',
                value: '/index.php'
            }],
            detectDom: '.main.m_foot',
            async func() {
                const block = $('#centers > .block:first-child');
                const blockcontent = $(block, '.blockcontent');
                blockcontent.append(
                    $CrE('br'),
                    $$CrE({
                        tagName: 'span',
                        props: {
                            innerText: CONST.Text.Announcements.Running,
                        },
                        styles: {
                            color: '#6f9ff1'
                        },
                    }
                ));
            }
        },
        downloader: {
            desc: '多功能下载器',
            dependencies: ['utils', 'api'],
            checkers: [{
                type: 'regpath',
                value: /\/book\/\d+\.htm/
            }, {
                type: 'path',
                value: '/modules/article/articleinfo.php'
            }, {
                type: 'regpath',
                value: /\/novel\/\d+\/\d+\/index.html?/
            }, {
                type: 'path',
                value: '/modules/article/reader.php'
            }],
            /** @typedef {Awaited<ReturnType<typeof functions.downloader.func>>} downloader */
            async func() {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {api} */
                const api = require('api');

                const pool_funcs = {
                    core: {
                        desc: '下载器核心:下载器界面、功能',
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.core.func>>} core */
                        async func() {
                            const Options = CONST.Text.Downloader.Options;
                            const DownloadOptions = {
                                format: {
                                    type: 'select',
                                    label: Options.Format.Title,
                                    options: [{
                                        label: Options.Format.txt,
                                        value: 'txt',
                                    }, {
                                        label: Options.Format.epub,
                                        value: 'epub',
                                    }, {
                                        label: Options.Format.image,
                                        value: 'image',
                                    }],
                                    default: 'epub',
                                },
                                encoding: {
                                    type: 'select',
                                    label: Options.Encoding.Title,
                                    caption: Options.Encoding.Caption,
                                    options: [{
                                        label: Options.Encoding.gbk,
                                        value: 'gbk',
                                    }, {
                                        label: Options.Encoding.utf8,
                                        value: 'utf-8',
                                    }],
                                    default: 'utf-8'
                                },
                            };

                            /**
                             * @typedef {Object} NovelInfo
                             * @property {string} intro
                             * @property {NovelMeta} meta
                             * @property {NovelVolume[]} volumes 
                             * @property {string} cover
                             */
                            /**
                             * @typedef {Object} NovelMeta
                             * @property {{value: string, aid: number}} Title
                             * @property {string} Author
                             * @property {number} DayHitsCount
                             * @property {number} TotalHitsCount
                             * @property {number} PushCount
                             * @property {number} FavCount
                             * @property {{value: string, sid: number}} PressId
                             * @property {string} BookStatus
                             * @property {number} BookLength
                             * @property {string} LastUpdate
                             * @property {string} Tags
                             * @property {{value: string, cid: number}} LatestSection
                             */
                            /**
                             * @typedef {Object} NovelVolume
                             * @property {string} name
                             * @property {number} vid
                             * @property {NovelChapter[]} chapters
                             */
                            /**
                             * @typedef {Object} NovelChapter
                             * @property {string} name
                             * @property {number} cid
                             */
                            /**
                             * @callback DownloadCallback
                             * @param {Object} detail
                             * @param {number} detail.aid
                             * @param {NovelInfo} detail.info
                             * @param {Record<string, any>} detail.options
                             * @param {number[]} detail.chapters
                             * @returns {any}
                             */

                            const pool_funcs = {
                                gui: {
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.gui.func>>} gui */
                                    async func() {
                                        const container = $CrE('div');
                                        const UI = CONST.Text.Downloader.UI;
                                        container.innerHTML = `
                                            <q-dialog v-model="visible" full-width full-height class="plus-downloader">
                                                <q-layout container view="hHh lpR fFf">

                                                    <q-header bordered class="bg-primary text-white" height-hint="98">
                                                        <q-toolbar>
                                                            <q-toolbar-title>
                                                                <q-icon name="book" class="q-px-sm"></q-icon>
                                                                ${ CONST.Text.Downloader.Title }
                                                            </q-toolbar-title>
                                                            <q-btn icon="close" v-close-popup flat></q-btn>
                                                        </q-toolbar>
                                                    </q-header>

                                                    <q-page-container>
                                                        <q-page>
                                                            <q-card square class="downloader-container q-pa-md">
                                                                <!-- 小屏幕上下拆分,大屏幕左右拆分 -->
                                                                <div :class="{ row: horizontal }" class="text-body2 scroll" style="height: 100%;">
                                                                    <!-- 书籍信息和下载选项 -->
                                                                    <div class="col">
                                                                        <!-- 上半部分 书籍信息 -->
                                                                        <div class="row">
                                                                            <!-- 左侧封面图 -->
                                                                            <div class="col-3 q-pa-md">
                                                                                <q-skeleton v-if="loading" type="rect" width="100%" height="15em"></q-skeleton>
                                                                                <q-img v-else :src="info.cover"></q-img>
                                                                            </div>
                                                                            <!-- 右侧书籍信息 -->
                                                                            <div class="col-9 q-pa-md">
                                                                                <div v-if="loading">
                                                                                    <q-skeleton type="rect" class="text-h5 q-mb-md" width="10em" height="1.2em"></q-skeleton>
                                                                                    <q-skeleton type="rect" width="100%" height="12em"></q-skeleton>
                                                                                </div>
                                                                                <div v-else>
                                                                                    <div class="text-h5 q-mb-md">{{ info.meta.Title.value }}</div>
                                                                                    <div class="q-my-sm"><span class="text-weight-bold">${ UI.Author }</span>{{ info.meta.Author }}</div>
                                                                                    <div class="q-my-sm"><span class="text-weight-bold">${ UI.BookStatus }</span>{{ info.meta.BookStatus }}</div>
                                                                                    <div class="q-my-sm"><span class="text-weight-bold">${ UI.LastUpdate }</span>{{ info.meta.LastUpdate }}</div>
                                                                                    <div class="q-my-sm"><span class="text-weight-bold">${ UI.Tags }</span>{{ info.meta.Tags }}</div>
                                                                                    <div class="q-my-sm"><span class="text-weight-bold">${ UI.Intro }</span>{{ info.intro }}</div>
                                                                                </div>
                                                                            </div>
                                                                        </div>
                                                                        <!-- 下半部分 下载选项 -->
                                                                        <div>
                                                                            <q-list>
                                                                                <q-item tag="label" v-for="(option, key) in options" class="row">
                                                                                    <q-item-section class="col-9">
                                                                                        <q-item-label>{{ option.label }}</q-item-label>
                                                                                        <q-item-label caption v-if="option.caption">{{ option.caption }}</q-item-label>
                                                                                    </q-item-section>
                                                                                    <q-item-section side class="col-3">
                                                                                        <!-- 根据不同option类型创建不同的表单元素 -->
                                                                                        <div v-if="option.type === 'boolean'">
                                                                                            <q-toggle
                                                                                                color="primary"
                                                                                                v-model="option_vals[key]"
                                                                                            >
                                                                                        </div>
                                                                                        <div v-if="option.type === 'select'" style="width: 100%;">
                                                                                            <q-select
                                                                                                :options="option.options"
                                                                                                v-model="option_vals[key]"
                                                                                            ></q-select>
                                                                                        </div>
                                                                                        <div v-if="option.type === 'string'" style="width: 100%;">
                                                                                            <q-input
                                                                                                v-model="option_vals[key]"
                                                                                            ></q-input>
                                                                                        </div>
                                                                                    </q-item-section>
                                                                                </q-item>
                                                                            </q-list>
                                                                        </div>
                                                                    </div>

                                                                    <!-- 下载内容范围选择器 -->
                                                                    <div class="col q-pa-md" :class="{ scroll: horizontal }" :style="{ height: horizontal ? '100%' : '' }">
                                                                        <div class="text-h5 q-pb-md">${ UI.ContentSelectorTitle }</div>
                                                                        <q-skeleton v-if="loading" type="rect" width="100%" height="70%"></q-skeleton>
                                                                        <q-tree v-else
                                                                            :nodes="tree"
                                                                            node-key="id"
                                                                            tick-strategy="leaf"
                                                                            v-model:ticked="ticked"
                                                                        ></q-tree>
                                                                    </div>
                                                                </div>

                                                            </q-card>
                                                        </q-page>
                                                    </q-page-container>

                                                    <q-footer bordered class="bg-dark">
                                                        <q-toolbar>
                                                            <q-toolbar-title>
                                                                <!-- 下载进度显示 -->
                                                                <span class="text-body2">
                                                                    <span v-if="downloading">
                                                                        <span class="q-px-sm">${ replaceText(
                                                                            UI.Progress.Global,
                                                                            {
                                                                                '{Total}': '{{ progress.total }}',
                                                                                '{CurStep}': '{{ Math.min(progress.total, progress.finished + 1) }}',
                                                                                '{Name}': '{{ sub_progress.name ?? "" }}',
                                                                            }
                                                                        ) }</span>
                                                                        <span class="q-px-sm">${ replaceText(
                                                                            UI.Progress.Sub,
                                                                            {
                                                                                '{Total}': '{{ sub_progress.total }}',
                                                                                '{CurStep}': '{{ Math.min(sub_progress.total, sub_progress.finished + 1) }}',
                                                                            }
                                                                        ) }</span>
                                                                    </span>
                                                                    <span v-else>{{ loading ? "${ UI.Progress.Loading }" : "${ UI.Progress.Ready }" }}</span>
                                                                </span>
                                                            </q-toolbar-title>
                                                            <q-skeleton v-if="loading" type="QBtn"></q-skeleton>
                                                            <q-btn v-else icon="download" :loading="downloading" :percentage="download_percentage" label="${ UI.DownloadButton }" @click="submit" flat></q-btn>
                                                        </q-toolbar>
                                                    </q-footer>

                                                </q-layout>
                                            </q-dialog>
                                        `;
                                        document.body.append(container);

                                        addStyle(`
                                            .plus-downloader .downloader-container {
                                                position: absolute;
                                                width: 100%;
                                                height: 100%;
                                            }
                                        `);

                                        let instance;
                                        const app = Vue.createApp({
                                            data() {
                                                return {
                                                    visible: false,
                                                    aid: 0,
                                                    // 正在加载状态(加载时显示占位UI)
                                                    loading: false,
                                                    // 正在下载状态(下载时显示下载状态UI)
                                                    downloading: false,
                                                    // 是否已获取到完整api信息
                                                    api_loaded: false,
                                                    // 存储api原始信息
                                                    api: {
                                                        full_intro: null,
                                                        full_meta: null,
                                                        novel_index: null,
                                                    },
                                                    // 下载选项
                                                    options: {},
                                                    // 选项数据
                                                    option_vals: {},
                                                    // 用户选择下载的内容
                                                    ticked: [],
                                                    // 下载按钮回调
                                                    /** @type {DownloadCallback} */
                                                    callback: (...args) => console.log(args),
                                                    // 下载进度管理器
                                                    download_manager: null,
                                                    // 下载进度
                                                    progress: {
                                                        finished: 0,
                                                        total: 0,
                                                    },
                                                    // 次级下载进度管理器
                                                    sub_manager: null,
                                                    // 次级下载进度
                                                    sub_progress: {
                                                        finished: 0,
                                                        total: 0,
                                                        name: null,
                                                    }
                                                }
                                            },
                                            computed: {
                                                /**
                                                 * 是否为大屏幕,大屏幕横向布局,小屏幕纵向布局
                                                 * @type {boolean}
                                                 */
                                                horizontal() {
                                                    return Quasar.Screen.gt.sm;
                                                },

                                                /**
                                                 * 从api原始信息解析为纯信息数据对象
                                                 * @type {NovelInfo}
                                                 */
                                                info() {
                                                    const { full_intro, full_meta, novel_index, cover } = this.api;

                                                    /** @type {NovelInfo} */
                                                    const info = {};
                                                    info.intro = full_intro;
                                                    info.meta = [...$All(full_meta, 'data')].reduce((meta, data) => {
                                                        const attrs = {};

                                                        // 获取主要值
                                                        const name = data.getAttribute('name');
                                                        const value = data.getAttribute('value') ?? data.firstChild.nodeValue;

                                                        // 获取次要值
                                                        const cloned_data = data.cloneNode(true);
                                                        cloned_data.removeAttribute('name');
                                                        cloned_data.removeAttribute('value');
                                                        const attr_names = cloned_data.getAttributeNames();

                                                        // 根据次要值是否存在决定如何合并到总meta数据对象中
                                                        if (attr_names.length) {
                                                            // 次要值存在:主要值作为"value"属性值,次要值作为其他属性,整体attr对象作为一个属性合并到meta数据对象中
                                                            attrs.value = value;
                                                            for (let attr_name of attr_names) {
                                                                let attr_val = data.getAttribute(attr_name);
                                                                attr_val = /^\d+$/.test(attr_val) ? parseInt(attr_val, 10) : attr_val;
                                                                attrs[attr_name] = attr_val;
                                                            }
                                                            return Object.assign(meta, { [name]: attrs });
                                                        } else {
                                                            // 次要值不存在,只有主要值:name: 主要值 直接作为一个属性合并到meta数据对象中
                                                            attrs[name] = value;
                                                            return Object.assign(meta, attrs);
                                                        }
                                                    }, {});
                                                    info.volumes = [...$All(novel_index, 'volume')].map(volume => {
                                                        return {
                                                            name: volume.firstChild.nodeValue,
                                                            vid: parseInt(volume.getAttribute('vid'), 10),
                                                            chapters: [...$All(volume, 'chapter')].map(chapter => {
                                                                return {
                                                                    name: chapter.firstChild.nodeValue,
                                                                    cid: parseInt(chapter.getAttribute('cid'), 10),
                                                                };
                                                            }),
                                                        };
                                                    });
                                                    info.cover = `http://img.wenku8.com/image/${ Math.floor(this.aid / 1000) }/${ this.aid }/${ this.aid }s.jpg`;
                                                    return info;
                                                },

                                                tree() {
                                                    // 注意:QTree的节点id要求全局唯一(而不仅仅是同层级唯一),这里直接使用了
                                                    // vid和cid作为QTree的id,是因为已知vid、cid是全局唯一的。若vid、cid并非
                                                    // 全局唯一,就需要自行创建适用于QTree的id并做好与章节、分卷之间的映射
                                                    return this.api_loaded ? this.info.volumes.map(
                                                        volume => ({
                                                            id: volume.vid,
                                                            label: volume.name,
                                                            children: volume.chapters.map(chapter => ({
                                                                id: chapter.cid,
                                                                label: chapter.name,
                                                            }))
                                                        })
                                                    ) : [];
                                                },

                                                download_percentage() {
                                                    return (this.progress.finished / this.progress.total) * 100;
                                                },
                                            },
                                            watch: {
                                                // 当options改变时,重置option_vals为各option.default
                                                options: {
                                                    handler(val, old_val) {
                                                        this.option_vals = Object.entries(Vue.toRaw(val)).reduce(
                                                            (vals, [key, option]) =>
                                                                Object.assign(vals, { [key]: option.options.find(o => o.value === option.default) }),
                                                            {}
                                                        );
                                                    },
                                                    deep: true,
                                                },

                                                // 自动绑定下载管理器进度与当前app下载进度
                                                download_manager: {
                                                    handler(new_manager, old_manager) {
                                                        if (!new_manager) { return; }
                                                        const that = this;

                                                        // 同步大进度
                                                        const progress = this.progress;
                                                        const sync = manager => {
                                                            progress.finished = manager.finished;
                                                            progress.total = manager.steps;
                                                        };
                                                        $AEL(new_manager, 'progress', e => sync(new_manager));

                                                        // 防止下载器在首次更新进度时还没有添加进度同步监听器,这里手动同步一次
                                                        this.download_manager && sync(this.download_manager)

                                                        // 同步小进度
                                                        const sub_progress = this.sub_progress;
                                                        const linkSubManager = sub_manager => {
                                                            that.sub_manager = sub_manager;
                                                            sub_progress.name = sub_manager.info;
                                                            $AEL(sub_manager, 'progress', e => {
                                                                sub_progress.finished = sub_manager.finished;
                                                                sub_progress.total = sub_manager.steps;
                                                            });
                                                        };
                                                        $AEL(new_manager, 'sub', e => {
                                                            const sub_manager = new_manager.children[new_manager.children.length-1];
                                                            linkSubManager(sub_manager);
                                                        });

                                                        // 防止下载器在首次生成子进度管理器的时候还没有添加小进度同步监听器,这里手动同步一次
                                                        if (new_manager.children.length) {
                                                            const sub_manager = new_manager.children[new_manager.children.length-1];
                                                            linkSubManager(sub_manager);
                                                        }

                                                        // 有关大小进度:实际下载实现中,所有下载器均应按照以下标准:
                                                        // - 整体下载进度分N步,称为 大步骤、大进度
                                                        // - 每个大进度内部分M步,称为 小步骤、小进度
                                                        // - 只有当一个大步骤内部的全部小步骤都完成时,这个大步骤才会完成,此时大进度++,刚刚完成的这个大步骤内部的小进度应为100%
                                                        // - 大进度和小进度分别用一个ProgressManager和它的一个sub manager表示和管理
                                                        // 因此,全局只有一个大进度对应的ProgressManager,统一时刻只有一个活跃的sub manager
                                                        // 故不用担心上一大步骤的下属sub manager突然更新并对sub_progress写入脏数据,因为所有之前大步骤的sub_manager都应时100%进度且不再活跃
                                                    },
                                                    immediate: true,
                                                },

                                                // 当章节列表更新时,自动选中全部章节
                                                tree: {
                                                    handler(new_tree, old_tree) {
                                                        if (!this.api_loaded) { return; }
                                                        for (const volume of new_tree) {
                                                            for (const chapter of volume.children) {
                                                                this.ticked.push(chapter.id);
                                                            }
                                                        }
                                                    },
                                                    immediate: true,
                                                }
                                            },
                                            methods: {
                                                /**
                                                 * 从文库服务器获取有关当前书籍的全部下载器所需信息,填充到this.api中
                                                 * 获取时将UI置为加载中状态
                                                 */
                                                async request() {
                                                    this.loading = true;
                                                    const [aid, lang] = [this.aid, utils.getLanguage()];
                                                    [
                                                        this.api.full_intro,
                                                        this.api.full_meta,
                                                        this.api.novel_index,
                                                    ] = await Promise.all([
                                                        api.getNovelFullIntro({ aid, lang }),
                                                        api.getNovelFullMeta({ aid, lang }),
                                                        api.getNovelIndex({ aid, lang }),
                                                    ]);
                                                    this.loading = false;
                                                    this.api_loaded = true;
                                                },

                                                resetProgress() {
                                                    this.progress = {
                                                        finished: 0,
                                                        total: 0,
                                                    };
                                                    this.sub_progress = {
                                                        finished: 0,
                                                        total: 0,
                                                        name: null,
                                                    };
                                                    this.download_manager = null;
                                                    this.sub_manager = null;
                                                },

                                                async submit() {
                                                    const aid = this.aid;
                                                    const info = structuredClone(Vue.toRaw(this.info));
                                                    const chapters = Array.from(Vue.toRaw(this.ticked));
                                                    const options = Object.entries(Vue.toRaw(this.option_vals))
                                                        .reduce((options, [key, val]) => 
                                                            Object.assign(options, { [key]: val.value }), {});
                                                    const callback = this.callback ?? function() {};

                                                    if (chapters.length) {
                                                        this.downloading = true;
                                                        this.resetProgress();
                                                        await Promise.resolve(callback({ aid, info, options, chapters }));
                                                        this.downloading = false;
                                                    } else {
                                                        Quasar.Notify.create({
                                                            type: 'error',
                                                            message: CONST.Text.Downloader.UI.NoContentSelected,
                                                            group: 'downloader.core.gui.no-chapters-selected',
                                                        });
                                                    }
                                                }
                                            },
                                            mounted() {
                                                instance = this;
                                            },
                                        });
                                        app.use(Quasar);
                                        app.mount(container);

                                        /**
                                         * 根据提供的书籍aid,初始化并展示下载器gui
                                         * @param {number} aid 
                                         * @param {DownloadCallback} [callback]
                                         */
                                        async function show(aid, callback) {
                                            instance.aid = aid;
                                            callback && (instance.callback = callback);
                                            instance.options = DownloadOptions;
                                            instance.request();
                                            instance.visible = true;
                                        }

                                        /**
                                         * 隐藏下载器gui
                                         */
                                        function hide() {
                                            instance.visible = false;
                                        }

                                        return {
                                            get download_progress() { return instance.download_manager; },
                                            set download_progress(manager) { instance.download_manager = manager; },
                                            show, hide,
                                        };
                                    }
                                },
                                downloader: {
                                    /** @typedef {Awaited<ReturnType<typeof pool_funcs.downloader.func>>} downloader */
                                    async func() {
                                        // 每种下载格式独立实现一个子功能函数,提供download接口
                                        /**
                                         * 标准下载接口
                                         * @callback DownloadFunction
                                         * @param {Object} options
                                         * @param {number} options.aid - 书籍id
                                         * @param {NovelInfo} options.info - 书籍信息
                                         * @param {number[]} options.chapters - 需要下载的章节列表
                                         * @param {string} [options.encoding='utf-8'] - 使用的编码(如果支持)
                                         * @returns {{ blob_promise: Promise<Blob>, manager: InstanceType<typeof utils.ProgressManager>, filename: string }}
                                         */
                                        const pool_funcs = {
                                            txt: {
                                                /** @typedef {Awaited<ReturnType<typeof pool_funcs.txt.func>>} txt */
                                                func() {
                                                    /**
                                                     * 下载为txt文件
                                                     * @type {DownloadFunction}
                                                     */
                                                    async function download({ aid, info, chapters, encoding='utf-8' }) {
                                                        // 进度管理器
                                                        const manager = new utils.ProgressManager(3);

                                                        // 下载txt主流程
                                                        const blob_promise = new Promise(async (resolve, reject) => {
                                                            // 下载章节内容
                                                            const manager_content = manager.sub(chapters.length, CONST.Text.Downloader.Steps.txt.NovelContent);
                                                            const lang = utils.getLanguage();
                                                            const contents = await manager.progress(Promise.all(chapters.map(async cid =>
                                                                await manager_content.progress(api.getNovelContent({
                                                                    aid, cid, lang, 
                                                                }))
                                                            )));

                                                            // 编码
                                                            const manager_encode = manager.sub(chapters.length, CONST.Text.Downloader.Steps.txt.EncodeText);
                                                            const SupportedEncodings = ['gbk', 'big5'];
                                                            const blobs = contents.map(content => {
                                                                const buffer = SupportedEncodings.includes(encoding) ?
                                                                    $URL[encoding].encodeBuffer(content) :
                                                                    new TextEncoder().encode(content);
                                                                const blob = new Blob([buffer], { type: 'text/plain' });
                                                                manager_encode.progress();
                                                                return blob;
                                                            });
                                                            manager.progress();

                                                            // 合成zip文件
                                                            const manager_zip = manager.sub(100, CONST.Text.Downloader.Steps.txt.GenerateZIP);
                                                            const zip = new JSZip();
                                                            blobs.forEach((blob, i) => {
                                                                const cid = chapters[i];
                                                                const volume = info.volumes.find(v => v.chapters.some(c => c.cid === cid));
                                                                const chapter = volume.chapters.find(c => c.cid === cid);
                                                                const folder = zip.folder(`${ volume.vid } - ${volume.name}`);
                                                                folder.file(`${ chapter.cid } - ${ chapter.name }.txt`, blob);
                                                            });
                                                            const blob = await manager.progress(zip.generateAsync(
                                                                { type: 'blob' },
                                                                metadata => manager_zip.progress(null, Math.round(metadata.percent))
                                                            ));

                                                            resolve(blob);
                                                        });

                                                        return {
                                                            blob_promise,
                                                            manager,
                                                            filename: `${aid} - ${info.meta.Title.value}.zip`,
                                                        }
                                                    }

                                                    return { download };
                                                }
                                            },
                                            image: {
                                                /** @typedef {Awaited<ReturnType<typeof pool_funcs.image.func>>} image */
                                                func() {
                                                    /**
                                                     * 下载全部插图
                                                     * @type {DownloadFunction}
                                                     */
                                                    async function download({ aid, info, chapters, encoding='utf-8' }) {
                                                        const manager = new utils.ProgressManager(3);

                                                        // 获取与合成图片zip文件主流程
                                                        const blob_promise = new Promise(async (resolve, reject) => {
                                                            // 获取全部章节,解析插图
                                                            /**
                                                             * @typedef {Object} ImageChapter
                                                             * @property {string[]} urls
                                                             * @property {number} cid
                                                             * @property {string} title
                                                             */
                                                            const manager_content = manager.sub(chapters.length, CONST.Text.Downloader.Steps.image.NovelContent);
                                                            const lang = utils.getLanguage();
                                                            const image_chapters = await manager.progress(Promise.all(chapters.map(async cid => {
                                                                const content = await api.getNovelContent({ aid, cid, lang });
                                                                const matches = content.matchAll(/<!--image-->([^<]+?)<!--image-->/g);
                                                                const urls = [...matches].map(([full, url]) => url);
                                                                const volume = info.volumes.find(volume => volume.chapters.some(chapter => chapter.cid === cid));
                                                                const chapter = volume.chapters.find(chapter => chapter.cid === cid);
                                                                const title = chapter.name;

                                                                /** @type {ImageChapter} */
                                                                const image_chapter = { cid, title, urls };
                                                                manager_content.progress();
                                                                return image_chapter;
                                                            })));

                                                            // 获取全部插图并打包为ZIP
                                                            const manager_image = manager.sub(image_chapters.length, CONST.Text.Downloader.Steps.image.DownloadImage);
                                                            const zip = new JSZip();
                                                            await manager.progress(Promise.all(image_chapters.map(async image_chapter => {
                                                                // 没有图片的章节就不创建文件夹了
                                                                if (!image_chapter.urls.length) { return; }

                                                                // 为章节创建文件夹
                                                                const foldername = `${image_chapter.cid} - ${image_chapter.title}`;
                                                                const folder = zip.folder(foldername);

                                                                // 添加图片到文件夹中
                                                                const num_len = image_chapter.urls.length.toString().length;
                                                                await Promise.all(image_chapter.urls.map(async (url, i) => {
                                                                    const path = new URL(url).pathname;
                                                                    const ext = path.includes('.') ? path.slice(path.lastIndexOf('.') + 1) : 'jpg';
                                                                    const filename = `${ utils.zfill(`${i+1}`, num_len) }.${ ext }`;
                                                                    const blob = await utils.requestBlob(url);
                                                                    folder.file(filename, blob);
                                                                }));

                                                                manager_image.progress();
                                                            })));

                                                            // 生成blob文件
                                                            const manager_blob = manager.sub(100, CONST.Text.Downloader.Steps.image.GenerateZIP);
                                                            const blob = await manager.progress(zip.generateAsync(
                                                                { type: 'blob' },
                                                                metadata => manager_blob.progress(null, Math.round(metadata.percent))
                                                            ));
                                                            resolve(blob);
                                                        });

                                                        return {
                                                            blob_promise,
                                                            manager,
                                                            filename: `${aid} - ${info.meta.Title.value}.zip`,
                                                        }
                                                    }

                                                    return { download };
                                                }
                                            },
                                            epub: {
                                                /** @typedef {Awaited<ReturnType<typeof pool_funcs.epub.func>>} epub */
                                                func() {
                                                    /**
                                                     * @type {DownloadFunction}
                                                     */
                                                    function download({ aid, info, chapters, encoding='utf-8' }) {
                                                        const manager = new utils.ProgressManager(2);

                                                        const blob_promise = new Promise(async (resolve, reject) => {
                                                            // jEpub 实例
                                                            const epub = new jEpub();
                                                            epub.init({
                                                                i18n: 'en',
                                                                title: info.meta.Title.value,
                                                                author: info.meta.Author,
                                                                publisher: info.meta.PressId.value,
                                                                description: info.intro,
                                                                tags: info.meta.Tags.split(/\s+/g)
                                                            });
                                                            epub.date(new Date(info.meta.LastUpdate));
                                                            epub.notes(replaceText(
                                                                CONST.Text.Downloader.Notes, {
                                                                    '{URL}': `https://${location.host}/book/${aid}.htm`,
                                                                }
                                                            ));

                                                            /**
                                                             * 用于记录分卷层级信息的Map
                                                             * 内容为每一分卷所对应的全部章节在epub中的page的index数组
                                                             * @type {Map<NovelVolume, number[]>}
                                                             */
                                                            const volume_map = new Map();

                                                            // 并发进行所有需要网络请求的工作
                                                            const manager_fetch = manager.sub(chapters.length + 1, CONST.Text.Downloader.Steps.epub.NovelContent);
                                                            await manager.progress(Promise.all([
                                                                // 加载封面
                                                                (async function() {
                                                                    const blob = await utils.requestBlob(info.cover);
                                                                    epub.cover(blob);
                                                                    manager_fetch.progress();
                                                                }) (),

                                                                // 加载章节内容
                                                                (async function() {
                                                                    // 先获取、整理章节内容
                                                                    const epub_chapters = await Promise.all(chapters.map(async (cid, i) => {
                                                                        // 获取章节内容
                                                                        const lang = utils.getLanguage();
                                                                        const content = await api.getNovelContent({ aid, cid, lang });
                                                                        let html_content = content;

                                                                        // 处理章节图片
                                                                        const matches = [...html_content.matchAll(/<!--image-->([^<]+?)<!--image-->/g)];
                                                                        const len = matches.length.toString().length;
                                                                        const chapter_index = utils.zfill(`${i + 1}`, chapters.length.toString().length);
                                                                        await Promise.all(matches.map(async ([full, url], i) => {
                                                                            const image_index = utils.zfill(`${i+1}`, len);
                                                                            const image_id = `ChapterImage-${ chapter_index }-${ image_index }`;
                                                                            html_content = html_content.replace(full, `<%= image[${ escJsStr(image_id) }] %>`);
                                                                            epub.image(await utils.requestBlob(url), image_id);
                                                                        }));

                                                                        // 整理文本内容
                                                                        html_content = html_content.split(/[\r\n]+/g).map(line => `<p>${line}</p>`).join('\n');

                                                                        // 整理返回epub信息
                                                                        const volume = info.volumes.find(v => v.chapters.some(c => c.cid === cid));
                                                                        const chapter = volume.chapters.find(c => c.cid === cid);
                                                                        manager_fetch.progress();
                                                                        return {
                                                                            volume, chapter,
                                                                            title: chapter.name,
                                                                            content: html_content,
                                                                        };
                                                                    }));

                                                                    // 最后再按顺序统一添加到epub
                                                                    // 同时记录分卷层级信息
                                                                    epub_chapters.forEach((epub_chapter, index) => {
                                                                        // 添加章节到epub
                                                                        epub.add(epub_chapter.title, epub_chapter.content);

                                                                        // 记录分卷层级信息
                                                                        const volume = epub_chapter.volume;
                                                                        volume_map.has(volume) || volume_map.set(volume, []);
                                                                        volume_map.get(volume).push(index);
                                                                    });
                                                                }) (),
                                                            ]));

                                                            // Hook epub的zip文件添加过程,以修改toc文件内部目录层级
                                                            const zip = epub._Zip;
                                                            const add_file = zip.file.bind(zip);
                                                            zip.file = function(path, content) {
                                                                switch (path) {
                                                                    case 'toc.ncx':
                                                                        return ncx();
                                                                    case 'OEBPS/table-of-contents.html':
                                                                        return html();
                                                                    default:
                                                                        return add_file(...arguments);
                                                                }

                                                                function ncx() {
                                                                    // 解析为xml
                                                                    const xml = new DOMParser().parseFromString(content, 'application/xml');

                                                                    // 按照分卷重构目录结构
                                                                    volume_map.entries().forEach(([volume, indexes], volume_index) => {
                                                                        // 创建分卷层级的<navPoint>
                                                                        const first_page_src = $(xml, `#page-${indexes[0]} > content`).getAttribute('src');
                                                                        const volume_nav = xml.createElement('navPoint');
                                                                        volume_nav.id = `volume-${volume_index}`;
                                                                        volume_nav.innerHTML = `
                                                                            <navLabel>
                                                                                <text>${ utils.htmlEncode(volume.name) }</text>
                                                                            </navLabel>
                                                                            <content src=${ escJsStr(first_page_src) }></content>
                                                                        `;
                                                                        $(xml, 'navMap').append(volume_nav);

                                                                        // 将该分卷所属所有章节的<navPoint>移动到分卷<navPoint>内
                                                                        indexes.forEach(index => volume_nav.append($(xml, `#page-${index}`)));
                                                                    });

                                                                    // 重新生成playOrder
                                                                    let playOrder = 0;
                                                                    const order_map = new Map();
                                                                    for (const nav of $All(xml, 'navPoint')) {
                                                                        const src = $(nav, 'content').getAttribute('src');
                                                                        order_map.has(src) || order_map.set(src, ++playOrder);
                                                                        nav.setAttribute('playOrder', (order_map.get(src)).toString());
                                                                    }

                                                                    // 序列化为xml代码
                                                                    let new_xml_code = new XMLSerializer().serializeToString(xml);
                                                                    // xml序列化会自动添加namespace信息,即xmlns="...",不符合epub规范,需要删掉
                                                                    new_xml_code = new_xml_code.replaceAll(/navPoint xmlns="[^"]*"/g, 'navPoint');
                                                                    // 添加到zip中
                                                                    return add_file(path, new_xml_code);
                                                                }

                                                                function html() {
                                                                    // 解析为html文档
                                                                    const doc = new DOMParser().parseFromString(content, 'text/html');

                                                                    // 按照分卷重构目录结构
                                                                    volume_map.entries().forEach(([volume, indexes], volume_index) => {
                                                                        const li = $$CrE({
                                                                            tagName: 'li',
                                                                            classes: 'chaptertype-1',
                                                                            props: { innerHTML: volume.name },
                                                                        });
                                                                        const ul = $CrE('ul');
                                                                        li.append(ul);
                                                                        $(doc, '#toc > ul').append(li);
                                                                        
                                                                        indexes.forEach(index => {
                                                                            const a = $(doc, `a[href="page-${index}.html"]`);
                                                                            const li = a.parentElement;
                                                                            li.classList.remove('chaptertype-1');
                                                                            ul.append(li);
                                                                        });
                                                                    });
                                                                    
                                                                    // 序列化为html代码
                                                                    const new_html_code = new XMLSerializer().serializeToString(doc);
                                                                    // 添加到zip中
                                                                    return add_file(path, new_html_code);
                                                                }
                                                            }

                                                            // 为epub生成blob
                                                            const manager_blob = manager.sub(100, CONST.Text.Downloader.Steps.epub.GenerateEpub);
                                                            const blob = await manager.progress(epub.generate(
                                                                'blob',
                                                                metadata => manager_blob.progress(null, Math.round(metadata.percent))
                                                            ));
                                                            resolve(blob);
                                                        });

                                                        return {
                                                            blob_promise,
                                                            manager,
                                                            filename: `${aid} - ${info.meta.Title.value}.epub`,
                                                        }
                                                    }

                                                    return { download };
                                                }
                                            },
                                        };

                                        const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                                            GM_setValue, GM_getValue, GM_addValueChangeListener
                                        });
                                        await promise;

                                        /** @type {txt} */
                                        const txt = pool.require('txt');
                                        /** @type {image} */
                                        const image = pool.require('image');
                                        /** @type {epub} */
                                        const epub = pool.require('epub');

                                        return { txt, image, epub, };
                                    }
                                },
                            };

                            const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                                GM_setValue, GM_getValue, GM_addValueChangeListener
                            });
                            await promise;

                            /** @type {gui} */
                            const gui = pool.require('gui');
                            /** @type {downloader} */
                            const downloader = pool.require('downloader');

                            /**
                             * 为指定书籍展示下载器
                             * @param {number} aid 
                             */
                            function show(aid) {
                                gui.show(aid, async ({ aid, info, chapters, options }) => {
                                    if (downloader[options.format]) {
                                        const { blob_promise, manager, filename } = await downloader[options.format].download({
                                            aid,
                                            info,
                                            chapters,
                                            encoding: options.encoding,
                                        });
                                        gui.download_progress = manager;
                                        const blob = await blob_promise;
                                        const url = URL.createObjectURL(blob);
                                        dl_browser(url, filename);
                                        setTimeout(() => URL.revokeObjectURL(url));
                                    } else {
                                        console.log(aid, info, chapters, options);
                                    }
                                });
                            }

                            return {
                                gui, downloader,
                                show,
                            };
                        }
                    },
                };
                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                    GM_setValue, GM_getValue, GM_addValueChangeListener
                });
                await promise;

                /** @type {core} */
                const core = pool.require('core');

                require('sidepanel', true).then(
                    /** @param {sidepanel} sidepanel */
                    sidepanel => sidepanel.registerButton({
                        id: 'downloader.show',
                        label: CONST.Text.Downloader.SideButton,
                        icon: 'download',
                        index: 2,
                        async callback() {
                            const aid = parseInt(
                                new URLSearchParams(location.search).get('aid') ??
                                new URLSearchParams(location.search).get('id') ??
                                location.href.match(/book\/(\d+)\.htm/)?.[1] ??
                                location.href.match(/novel\/\d+\/(\d+)\//)?.[1],
                            10);
                            core.show(aid);
                        }
                    })
                );
            }
        },
        autovote: {
            desc: '每日自动推书',
            dependencies: ['utils', 'debugging', 'logger', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {debugging} */
                const debugging = require('debugging');
                /** @type {logger} */
                const logger = require('logger');
                /** @type {configs} */
                const configs = require('configs');

                /**
                 * @typedef {Object} Book
                 * @property {number} aid
                 * @property {string} name
                 * @property {string} cover - 封面url
                 * @property {number} votes - 每日推书票数
                 * @property {number} time_added - 添加到自动推书列表的时间
                 * @property {number} voted - 累计自动推书票数
                 */
                /**
                 * @typedef {Object} VoteRecord
                 * @property {number} last_voted - 上一次执行自动推书的时间
                 * @property {Record<string, number>} vote_status - 上一次执行自动推书时的推书进度
                 */
                GM_getValue = utils.defaultedGet({
                    /** @type {Book[]} */
                    list: [],
                    /** @type {VoteRecord} */
                    record: {
                        last_voted: 0,
                        vote_status: [],
                    },
                    /** @type {boolean} */
                    enabled: true,
                }, GM_getValue);

                const Settings = CONST.Text.Autovote.Settings;
                configs.registerConfig('autovote', {
                    GM_addValueChangeListener,
                    items: [{
                        type: 'boolean',
                        label: Settings.Enabled,
                        caption: Settings.EnabledCaption,
                        key: 'enabled',
                        reload: true,
                        get() { return GM_getValue('enabled'); },
                        set(val) { GM_setValue('enabled', val); },
                    }, {
                        type: 'button',
                        label: Settings.Configuration,
                        button_icon: 'edit_note',
                        button_label: Settings.Configure,
                        async callback() {
                            /** @type {gui} */
                            const gui = await pool.require('gui', true);
                            gui.show();
                        },
                    }],
                    label: Settings.Title,
                })
                
                const pool_funcs = {
                    core: {
                        desc: '实现推书列表的增删改查',
                        // 这里不用让FunctionLoader包装子存储,直接将list存储在autovote的全局作用域中即可
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.core.func>>} core */
                        func() {
                            // 内容更改监听器
                            /** @type {((val: Book[]) => any)[]} */
                            const listeners = [];

                            GM_addValueChangeListener('list', (key, old_val, new_val, remote) => {
                                // 防抖,比对确认确实存在数据差异再回调
                                // 时间复杂度:对于m本书、每本书n个属性,大致为 O(m) * O(n)
                                const variable_same = old_val === new_val;
                                const both_array = Array.isArray(old_val) === Array.isArray(new_val);
                                const array_same = both_array && old_val.length === new_val.length && old_val.every(
                                    /** @param {Book} book */
                                    (book, i) => {
                                        const old_book = book;
                                        const new_book = new_val[i];
                                        const old_keys = Object.keys(old_book);
                                        const new_keys = Object.keys(new_book);
                                        if (old_keys.length !== new_keys.length) { return false; }
                                        if (old_keys.some((k, j) => k != new_keys[j])) { return false; }
                                        if (old_keys.some(k => old_book[k] !== new_book[k])) { return false; }
                                    }
                                )
                                if (variable_same || array_same) { return; }
                                listeners.forEach(l => debugging.callWithErrorHandling(l, null, [new_val]));
                            });

                            /**
                             * 添加一本书到自动推书
                             * @param {Book} book 
                             * @returns {boolean} 成功添加 / 已经在推书列表中
                             */
                            function add(book) {
                                if (has(book.aid)) { return false; }
                                const books = list();
                                books.push(book);
                                GM_setValue('list', books);
                                return true;
                            }

                            /**
                             * 直接设置整个books数组
                             * @overload
                             * @param {Book[]} books
                             * @returns {void}
                             */
                            /**
                             * 设置某一已在推书列表中的书籍的推书票数
                             * @overload
                             * @param {number} aid 
                             * @param {number} votes 
                             * @returns {boolean}
                             */
                            function set(...args) {
                                // 直接设置整个books数组
                                if (args.length === 1) {
                                    const books = args[0];
                                    GM_setValue('list', books);
                                    return;
                                }
                                
                                // 设置某一已在推书列表中的书籍的推书票数
                                if (args.length === 2) {
                                    const [aid, votes] = args;

                                    if (!has(aid)) { return false; }
                                    const books = list();
                                    books.find(b => b.aid === aid).votes = votes;
                                    GM_setValue('list', books);
                                    return true;
                                }

                                throw new TypeError('autovote.core.set: arguments\' length invalid');
                            }

                            /**
                             * 检查某一本书是否在推书列表中
                             * @param {number} aid 
                             * @returns {boolean}
                             */
                            function has(aid) {
                                const books = list();
                                return books.some(book => book.aid === aid);
                            }

                            /**
                             * 获取全部
                             * @returns {Book[]}
                             */
                            function list() {
                                return GM_getValue('list');
                            }

                            /**
                             * 添加稍后列表值改变监听器
                             * @param {(val: Book[]) => any} listener 
                             */
                            function onChange(listener) {
                                listeners.push(listener);
                            }

                            return { add, set, has, list, onChange };
                        }
                    },
                    bookpage: {
                        desc: '在书籍信息页侧边栏添加自动推书按钮',
                        checkers: [{
                            type: 'regpath',
                            value: /\/book\/\d+\.htm/
                        }, {
                            type: 'path',
                            value: '/modules/article/articleinfo.php'
                        }],
                        dependencies: ['core'],
                        async func() {
                            /** @type {core} */
                            const core = pool.require('core');
                            /** @type {sidepanel} */
                            const sidepanel = await require('sidepanel', true);
                            const aid = parseInt(new URLSearchParams(location.search).get('id')
                                ?? location.pathname.match(/\/book\/(\d+)\.htm/)[1], 10);
                            const name = $('#content > div:first-child > table:first-child > tbody > tr:first-child > td > table span > b').innerText.trim();
                            const cover = $('#content > div:first-child > table:nth-of-type(2) img').src;

                            GM_getValue('enabled') && sidepanel.registerButton({
                                id: 'autovote.add',
                                label: CONST.Text.Autovote.Add,
                                icon: 'playlist_add',
                                index: 4,
                                callback() {
                                    const Autovote = CONST.Text.Autovote;
                                    const time_added = Date.now();
                                    const success = core.add({ aid, name, cover, votes: 1, time_added, voted: 0 });
                                    Quasar.Notify.create({
                                        type: 'success',
                                        message: Autovote.Added,
                                        caption: replaceText(
                                            success ? Autovote.AddSuccess : Autovote.AddDuplicate,
                                            { '{Name}': name }
                                        ),
                                        icon: success ? 'done' : 'lightbulb',
                                        group: 'autovote.added',
                                    });
                                }
                            })
                        }
                    },
                    gui: {
                        desc: '在书架、书籍信息页和设置界面中展示的自动推书配置界面',
                        dependencies: ['core'],
                        detectDom: 'body',
                        /** @typedef {Awaited<ReturnType<typeof pool_funcs.gui.func>>} gui */
                        async func() {
                            /** @type {core} */
                            const core = pool.require('core');

                            const container = $CrE('div');
                            const UI = CONST.Text.Autovote.UI;
                            container.innerHTML = `
                                <q-dialog v-model="visible" full-width full-height class="plus-autovote">
                                    <q-layout container view="hHh lpR fFf">

                                        <q-header bordered class="bg-primary text-white">
                                            <q-toolbar>
                                                <q-toolbar-title>
                                                    <q-avatar icon="collections_bookmark"></q-avatar>
                                                    ${ UI.Title }
                                                </q-toolbar-title>
                                                <q-btn icon="close" flat v-close-popup></q-btn>
                                            </q-toolbar>
                                        </q-header>

                                        <q-page-container>
                                            <q-page class="bg-lightdark q-pa-md">
                                                <q-list>
                                                    <q-item v-for="(book, i) of books">
                                                        <q-item-section avatar>
                                                            <a :href="book_urls[book.aid]" style="width: 100%;" target="_blank">
                                                                <q-img :src="book.cover"></q-img>
                                                            </a>
                                                        </q-item-section>
                                                        <q-item-section>
                                                            <q-item-label class="text-body1">
                                                                <a :href="book_urls[book.aid]" target="_blank">{{ book.name }}</a>
                                                            </q-item-label>
                                                            <q-item-label caption>
                                                                ${ UI.TimeAdded }{{ new Date(book.time_added).toLocaleDateString() }}
                                                            </q-item-label>
                                                            <q-item-label caption>
                                                                ${ UI.VotedCount }{{ book.voted }}
                                                            </q-item-label>
                                                        </q-item-section>
                                                        <q-item-section avatar>
                                                            <q-input v-model.number="book.votes" label="${ UI.Votes }"></q-input>
                                                        </q-item-section>
                                                        <q-item-section avatar>
                                                            <q-btn icon="delete_outline" @click="remove(book.aid)" flat></q-btn>
                                                        </q-item-section>
                                                    </q-item>
                                                </q-list>
                                            </q-page>
                                        </q-page-container>

                                    </q-layout>
                                </q-dialog>
                            `;
                            document.body.append(container);

                            let instance;
                            const app = Vue.createApp({
                                data() {
                                    return {
                                        visible: false,
                                        books: core.list(),
                                    };
                                },
                                computed: {
                                    /**
                                     * 根据书籍aid自动合成的书籍信息页链接
                                     * @type {Record<number | string, string>}
                                     */
                                    book_urls() {
                                        return this.books.reduce((urls, book) => 
                                            Object.assign(urls, { [book.aid]: `/book/${ book.aid }.htm`}), {});
                                    },
                                },
                                methods: {
                                    /**
                                     * 删除一个自动推书项(即一本书)
                                     * @param {number} aid 
                                     */
                                    remove(aid) {
                                        const book = this.books.find(b => b.aid === aid);
                                        Quasar.Dialog.create({
                                            title: UI.ConfirmRemove.Title,
                                            message: replaceText(
                                                UI.ConfirmRemove.Message,
                                                { '{Name}': book.name }
                                            ),
                                            ok: {
                                                label: UI.ConfirmRemove.Ok,
                                                color: 'primary',
                                            },
                                            cancel: {
                                                label: UI.ConfirmRemove.Cancel,
                                                color: 'secondary',
                                            },
                                        }).onOk(() => this.books.splice(this.books.findIndex(book => book.aid === aid), 1))
                                    }
                                },
                                watch: {
                                    // 自动保存配置更改到存储空间
                                    books: {
                                        handler(new_val, old_val) {
                                            core.set(new_val);
                                        },
                                        deep: true
                                    },
                                },
                                mounted() {
                                    instance = this;

                                    // 自动根据存储的推书配置更新UI
                                    core.onChange(books => this.books = books);
                                }
                            });
                            app.use(Quasar);
                            app.mount(container);

                            function show() {
                                instance.visible = true;
                            }

                            function hide() {
                                instance.visible = false;
                            }

                            if (FunctionLoader.testCheckers([{
                                type: 'regpath',
                                value: /\/book\/\d+\.htm/
                            }, {
                                type: 'path',
                                value: '/modules/article/articleinfo.php'
                            }, {
                                type: 'path',
                                value: '/modules/article/bookcase.php'
                            }]) && GM_getValue('enabled')) {
                                require('sidepanel', true).then(
                                    /** @param {sidepanel} sidepanel */
                                    sidepanel => {
                                        sidepanel.registerButton({
                                            id: 'autovote.show',
                                            icon: 'edit_note',
                                            label: CONST.Text.Autovote.Configure,
                                            index: 4,
                                            callback: show,
                                        });
                                    }
                                );
                            }

                            return { show, hide, }
                        },
                    },
                    vote: {
                        desc: '每天执行一次推书任务',
                        dependencies: ['core'],
                        // 这里不用让FunctionLoader包装子存储,直接将推书记录存储在autovote的全局作用域中即可
                        async func() {
                            /** @type {core} */
                            const core = pool.require('core');
                            
                            const record = getRecord();
                            const books = core.list();

                            // 如果没有开启自动推书,停止运行
                            if (!GM_getValue('enabled')) {
                                logger.log('Info', 'Autovote: autovote not enabled');
                                return;
                            }

                            // 如果今日已经完成了自动推书,停止运行
                            const today_voted = new Date(record.last_voted).toDateString() === new Date().toDateString();
                            const vote_completed = books.every(book => record.vote_status[book.aid] >= book.votes);
                            if (today_voted && vote_completed) {
                                logger.log('Info', 'Autovote: today voted');
                                return;
                            }

                            // 如果有其他页面内的脚本实例正在执行推书任务,当前实例就不重复执行
                            const autovote_active = Date.now() - record.last_voted <= CONST.Internal.AutovoteActiveTimeout;
                            if (autovote_active) {
                                logger.log('Info', 'Autovote: voting active in another page');
                                return;
                            }

                            const voteBook = utils.toQueued(_voteBook, {
                                max: 5,
                                sleep: 0,
                                queue_id: 'votebook'
                            });

                            // 执行自动推书
                            logger.log('Info', 'Autovote: start voting');
                            Quasar.Notify.create({
                                type: 'info',
                                message: CONST.Text.Autovote.VoteStart,
                                group: 'autovote.vote',
                            });

                            const divs = await doAutovote();

                            Quasar.Notify.create({
                                type: 'success',
                                message: CONST.Text.Autovote.VoteEnd,
                                /*actions: [{
                                    label: CONST.Text.Autovote.VoteDetail,
                                    handler() {
                                        Quasar.Dialog.create({
                                            //
                                        });
                                    }
                                }],*/
                                group: 'autovote.vote',
                            });

                            /**
                             * 根据今日推书状态,为未推完部分执行自动推书
                             * @returns {Promise<Record<string, HTMLDivElement[]>>} { [书籍字符串aid]: (推书结果文档中的block)[] }
                             */
                            async function doAutovote() {
                                const record = getRecord();
                                const books = core.list();

                                // 筛选出今日未推完的书,并计算剩余推书票数
                                /** @type {Record<string, number>} 未推完的书及其剩余推书票数 */
                                const task = books.reduce((task, book) => {
                                    const str_aid = book.aid.toString();
                                    // 上次自动推书是不是今天
                                    const today_voted = new Date(record.last_voted).toDateString() === new Date().toDateString();
                                    // 这本书每天应该推的总票数
                                    const total = books.find(b => b.aid === book.aid).votes;
                                    // 这本书今日还应推的票数
                                    const rest = today_voted ? Math.max(0, total - (record.vote_status[str_aid] ?? 0)) : total;

                                    rest > 0 && (task[str_aid] = rest);
                                    return task;
                                }, {});

                                // 推书
                                const result = {};
                                await Promise.all(Object.entries(task).map(async ([str_aid, votes]) => {
                                    const aid = parseInt(str_aid, 10);
                                    const divs = await Promise.all(Array.from('a'.repeat(votes)).map((_, i) => voteBook(aid)));
                                    result[str_aid] = divs;
                                }, {}));

                                // 更新最后推书完成时间,确保哪怕没有任何书要推也每天仅执行一次
                                const new_record = getRecord();
                                new_record.last_voted = Date.now();
                                GM_setValue('record', new_record);

                                return result;
                            }

                            /**
                             * 执行推书一次(投一票),并记到推书记录中
                             * @param {number} aid 
                             * @returns {Promise<HTMLDivElement>} 返回的页面中的.block元素
                             */
                            async function _voteBook(aid) {
                                // 推书
                                const str_aid = aid.toString();
                                const doc = await utils.requestDocument({
                                    method: 'GET',
                                    url: `/modules/article/uservote.php?id=${str_aid}`,
                                });
                                const block = $(doc, '.block');
                                block || logger.log('Warn', 'Autovote: .block not found in vote page', doc);

                                // 记录
                                const record = getRecord();
                                const books = core.list();

                                // 如果上次自动推书不是今天,就先清除推书记录
                                const today_voted = new Date(record.last_voted).toDateString() === new Date().toDateString();
                                today_voted || (record.vote_status = {});

                                // 推书记录中为当前书籍已推书计数加一
                                record.vote_status[str_aid] = (record.vote_status[str_aid] ?? 0) + 1;

                                // 自动推书配置中累计推书次数加一
                                books.find(b => b.aid === aid).voted++;

                                // 更新推书记录中的时间
                                record.last_voted = Date.now();

                                // 保存
                                GM_setValue('record', record);
                                core.set(books);

                                return block;
                            }

                            /**
                             * 获取自动推书记录
                             * @returns {VoteRecord}
                             */
                            function getRecord() {
                                return GM_getValue('record');
                            }
                        }
                    },
                };

                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                    GM_setValue, GM_getValue, GM_addValueChangeListener
                });
                await promise;
            },
        },
        reviewcollection: {
            desc: '书评收藏',
            dependencies: ['utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            async func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                /**
                 * @typedef {Object} Review
                 * @property {number} rid
                 * @property {string} name
                 */
                GM_getValue = utils.defaultedGet({
                    /** @type {Review[]} */
                    reviews: CONST.Internal.BuiltinReviewCollection,
                    /** @type {boolean} */
                    enabled: true,
                    /** @type {'left' | 'right'} */
                    list_position: 'left',
                    /** @type {boolean} */
                    open_lastpage: false,
                }, GM_getValue);

                const Settings = CONST.Text.ReviewCollection.Settings;
                configs.registerConfig('reviewcollection', {
                    GM_addValueChangeListener,
                    items: [{
                        type: 'boolean',
                        label: Settings.Enabled,
                        caption: Settings.EnabledCaption,
                        key: 'enabled',
                        get() { return GM_getValue('enabled'); },
                        set(val) { GM_setValue('enabled', val); },
                    }, {
                        type: 'select',
                        options: [{
                            label: Settings.ListPositionLeft,
                            value: 'left',
                        }, {
                            label: Settings.ListPositionRight,
                            value: 'right',
                        }],
                        label: Settings.ListPosition,
                        caption: Settings.ListPositionCaption,
                        key: 'list_position',
                        get() { return GM_getValue('list_position'); },
                        set(val) { GM_setValue('list_position', val); },
                    }, {
                        type: 'boolean',
                        label: Settings.OpenLastPage,
                        caption: Settings.OpenLastPageCaption,
                        key: 'open_lastpage',
                        get() { return GM_getValue('open_lastpage'); },
                        set(val) { GM_setValue('open_lastpage', val); },
                    }],
                    label: Settings.Title,
                });

                const pool_funcs = {
                    /*/
                    gui: {
                        desc: '收藏书评管理界面',
                        async func() {
                            const container = $CrE('div');
                            container.innerHTML = `
                                <q-dialog v-model="visible">
                                    
                                </q-dialog>
                            `;
                        },
                    },
                    */
                    indexpage: {
                        desc: '在首页展示收藏的书评列表',
                        checkers: [{
                            type: 'path',
                            value: '/'
                        }, {
                            type: 'path',
                            value: '/index.php'
                        }],
                        async func() {
                            // 页面内列表
                            makeList();
                            configs.registerUpdateCallback('reviewcollection', (key, old_val, new_val, remote) => {
                                switch (key) {
                                    case 'enabled':
                                        new_val ? makeList() : $('#plus-review-collection')?.remove();
                                        break;
                                    case 'list_position':
                                    case 'open_lastpage':
                                        makeList();
                                        break;
                                }
                            });
                            GM_addValueChangeListener('reviews', () => makeList());

                            addStyle(`
                                .ultop {
                                    overflow-x: hidden;
                                }
                            `);

                            /**
                             * 创建书评列表展示框并添加到DOM,如DOM已有展示框就替换掉旧的
                             */
                            function makeList() {
                                // 如果没有启用就不创建
                                if (!GM_getValue('enabled')) { return; }

                                /** @type {Review[]} */
                                const reviews = GM_getValue('reviews');

                                // 制作列表
                                const block = $$CrE({
                                    tagName: 'div',
                                    classes: 'block',
                                    props: {
                                        innerHTML: `
                                            <div class="blocktitle">
                                                <span class="txt">${ CONST.Text.ReviewCollection.CollectionTitle }</span>
                                                <span class="txtr"></span>
                                            </div>
                                            <div class="blockcontent">
                                                <ul class="ultop"></ul>
                                            </div>
                                        `,
                                    },
                                    attrs: {
                                        id: 'plus-review-collection',
                                    },
                                });
                                const ul = $(block, '.ultop');

                                reviews.forEach(review => {
                                    const url = `https://${ location.host }/modules/article/reviewshow.php?rid=${ review.rid }&page=${ GM_getValue('open_lastpage') ? 'last' : '1' }`;
                                    const li = $CrE('li');
                                    const a = $$CrE({
                                        tagName: 'a',
                                        attrs: {
                                            href: url,
                                            target: '_blank',
                                        },
                                        props: {
                                            innerText: review.name,
                                        },
                                    });
                                    utils.setTip(a, review.name);
                                    li.append(a);
                                    ul.append(li);
                                });

                                // 添加到页面
                                $('#plus-review-collection')?.remove();
                                const parent = $(({
                                    left: '#left',
                                    right: '#right',
                                }) [GM_getValue('list_position')]);
                                parent.append(block);
                            }
                        },
                    },
                    reviewpage: {
                        desc: '在书评页面添加收藏按钮',
                        checkers: {
                            type: 'path',
                            value: '/modules/article/reviewshow.php',
                        },
                        async func() {
                            /** @type {sidepanel} */
                            const sidepanel = await require('sidepanel', true);

                            toggleSideButton();
                            configs.registerUpdateCallback('reviewcollection', {
                                enabled(key, old_val, new_val, remote) {
                                    toggleSideButton();
                                }
                            });

                            /**
                             * 根据enabled,注册(不可用)或移除侧边栏收藏按钮
                             * @param {boolean} [enabled] 
                             */
                            function toggleSideButton(enabled=null) {
                                enabled === null && (enabled = GM_getValue('enabled'));

                                const ButtonID = 'reviewcollection.toggle';
                                const ReviewCollection = CONST.Text.ReviewCollection;
                                const rid = parseInt(new URLSearchParams(location.search).get('rid'), 10);
                                let in_collection = GM_getValue('reviews').some(r => r.rid === rid);

                                if (enabled) {
                                    sidepanel.registerButton({
                                        id: ButtonID,
                                        label: in_collection ? ReviewCollection.Remove : ReviewCollection.Add,
                                        icon: in_collection ? 'bookmark' : 'bookmark_border',
                                        index: 2,
                                        callback() {
                                            in_collection = !in_collection;

                                            // 修改书评收藏
                                            /** @type {Review[]} */
                                            const reviews = GM_getValue('reviews');
                                            const rid = parseInt(new URLSearchParams(location.search).get('rid'), 10);
                                            let name = $('#content > table.grid th > strong').innerText.trim();
                                            name.includes(':') && (name = name.split(':')[1]);
                                            if (in_collection) {
                                                // 需要添加书评收藏
                                                // 因为是内存存储收藏状态而非实时检测配置存储,因此需要防止重复添加
                                                reviews.every(r => r.rid !== rid) &&
                                                    reviews.push({ rid, name });
                                            } else {
                                                // 需要移除书评收藏
                                                // 因为是内存存储收藏状态而非实时检测配置存储,因此需要确保目前确实是已收藏状态
                                                const index = reviews.findIndex(r => r.rid === rid);
                                                index >= 0 && reviews.splice(index, 1);
                                            }
                                            GM_setValue('reviews', reviews);

                                            // 提示
                                            Quasar.Notify.create({
                                                type: 'success',
                                                message: in_collection ? ReviewCollection.Added : ReviewCollection.Removed,
                                                group: 'reviewcollection.toggle'
                                            });

                                            // 更新按钮
                                            sidepanel.updateButton(ButtonID, {
                                                label: in_collection ? ReviewCollection.Remove : ReviewCollection.Add,
                                                icon: in_collection ? 'bookmark' : 'bookmark_border',
                                            });
                                        }
                                    });
                                } else {
                                    sidepanel.hasButton(ButtonID) && sidepanel.removeButton(ButtonID);
                                }
                            }
                        }
                    },
                };

                const { promise, pool } = utils.loadFuncInNewPool(pool_funcs, {
                    GM_setValue, GM_getValue, GM_addValueChangeListener
                });
                await promise;
            },
        },
        background: {
            desc: '自定义页面背景',
            detectDom: 'body',
            dependencies: ['utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                /**
                 * @typedef {'local' | 'url' | 'color'} BGType
                 */
                GM_getValue = utils.defaultedGet({
                    /** @type {boolean} */
                    enabled: false,
                    /** @type {BGType} */
                    type: 'color',
                    /** @type {string} */
                    image_url: '',
                    /** @type {'contain' | 'cover' | 'fill' | 'none' | 'scale-down'} */
                    image_fit: 'fill',
                    /** @type {number} */
                    mask_opacity: 0.5,
                    /** @type {string} */
                    color: 'rgb(255, 255, 255)',
                }, GM_getValue);

                // 创建背景
                /**
                 * 背景管理器
                 * @typedef {{ install: function, update: function, uninstall: function }} BackgroundManager
                 */
                /**
                 * 当前已经应用背景的管理器
                 * @type {BackgroundManager | null}
                 */
                let cur_bg = null;
                /**
                 * 已实现的全部背景管理器
                 * @satisfies {Record<string, BackgroundManager>}
                 */
                const BG = {
                    image: {
                        /**
                         * @param {string} url 
                         * @param {number} mask_opacity 
                         */
                        install(url, mask_opacity, image_fit) {
                            const img = $$CrE({
                                tagName: 'img',
                                attrs: {
                                    src: url,
                                    id: 'plus-background-img',
                                },
                                styles: {
                                    position: 'fixed',
                                    left: '0',
                                    top: '0',
                                    width: '100vw',
                                    height: '100vh',
                                    zIndex: '-2',
                                    display: url ? 'block' : 'none',
                                    objectFit: image_fit,
                                },
                            });
                            const mask_container = $$CrE({
                                tagName: 'div',
                                classes: ['plus-main'],
                                attrs: {
                                    id: 'plus-background-mask-container'
                                },
                                styles: {
                                    position: 'relative',
                                    height: '0'
                                }
                            });
                            const mask = $$CrE({
                                tagName: 'div',
                                attrs:{
                                    id: 'plus-background-mask',
                                },
                                styles: {
                                    position: 'absolute',
                                    bottom: '0',
                                    left: '0',
                                    width: '960px',
                                    height: '10000vh',
                                    zIndex: '-1',
                                    opacity: `${mask_opacity}`,
                                }
                            });
                            mask_container.append(mask);
                            document.body.append(img, mask_container);

                            addStyle(`
                                /* 网页自带背景调成透明 */
                                body:is(.plus-darkmode, :not(.plus-darkmode)):not(#StrongerThanDarkmode) {
                                    background-color: transparent;
                                }
                                :is(body.plus-darkmode, body:not(.plus-darkmode)) :is(table.grid td, .blockcontent, .even, .odd) {
                                    background: transparent !important;
                                }
                                .plus-main{
                                    width: 960px;
                                    clear: both;
                                    text-align: center;
                                    margin-left: auto;
                                    margin-right: auto;
                                    margin-top:3px;
                                }
                                #plus-background-mask {
                                    background: white;
                                }
                                .plus-darkmode #plus-background-mask {
                                    background: black;
                                }
                            `, 'plus-background-style');
                        },
                        update(url, mask_opacity, image_fit) {
                            $('#plus-background-img').src = url;
                            $('#plus-background-img').style.objectFit = image_fit;
                            $('#plus-background-mask').style.opacity = `${mask_opacity}`;
                        },
                        uninstall() {
                            $('#plus-background-img')?.remove();
                            $('#plus-background-mask')?.remove();
                            $('#plus-background-style')?.remove();
                        },
                    },
                    color: {
                        /**
                         * @param {string} color 
                         */
                        install(color) {
                            document.body.append($$CrE({
                                tagName: 'div',
                                attrs: {
                                    id: 'plus-background-block',
                                },
                                styles: {
                                    position: 'fixed',
                                    left: '0',
                                    top: '0',
                                    width: '100vw',
                                    height: '100vh',
                                    backgroundColor: color,
                                    zIndex: '-1',
                                },
                            }));
                            addStyle(`
                                /* 网页自带背景调成透明 */
                                body:is(.plus-darkmode, :not(.plus-darkmode)):not(#StrongerThanDarkmode) {
                                    background-color: transparent;
                                }
                                :is(body.plus-darkmode, body:not(.plus-darkmode)) :is(table.grid td, .blockcontent, .even, .odd) {
                                    background: transparent !important;
                                }
                            `, 'plus-background-style');
                        },
                        update(color) {
                            $('#plus-background-block').style.background = color;
                        },
                        uninstall() {
                            $('#plus-background-block')?.remove();
                            $('#plus-background-style')?.remove();
                        }
                    }
                };
                applyBackground();

                // 注册(不可用)设置,设置切换时实时应用
                const Settings = CONST.Text.Background.Settings;
                configs.registerConfig('background', {
                    GM_addValueChangeListener,
                    items: [{
                        type: 'boolean',
                        label: Settings.Enabled,
                        caption: Settings.EnabledCaption,
                        key: 'enabled',
                        get() { return GM_getValue('enabled'); },
                        set(val) { GM_setValue('enabled', val); },
                    }, {
                        type: 'select',
                        label: Settings.Type,
                        options: Settings.Types,
                        key: 'type',
                        get() { return GM_getValue('type'); },
                        set(val) { GM_setValue('type', val); },
                    }, {
                        type: 'string',
                        label: Settings.ImageUrl,
                        key: 'image_url',
                        get() { return GM_getValue('image_url'); },
                        set(val) { GM_setValue('image_url', val); },
                    }, {
                        type: 'image',
                        label: Settings.Image,
                        key: 'image',
                        callback: applyBackground,
                        reload: 'page',
                        async get() {
                            // 从 OPFS:%Module%//background/image 中取出blob
                            const root = await utils.getModuleDir('background');
                            let has_image = false;
                            for await (const key of root.keys()) {
                                if (key === 'image') {
                                    has_image = true;
                                    break;
                                }
                            }
                            if (has_image) {
                                const image = await root.getFileHandle('image', { create: true });
                                const file = await image.getFile();
                                return file;
                            } else {
                                return null;
                            }
                        },
                        /**
                         * @param {File} file 
                         */
                        async set(file) {
                            // 写入到 OPFS:%Module%//background/image
                            const root = await utils.getModuleDir('background');
                            const image = await root.getFileHandle('image', { create: true });
                            const writable = await image.createWritable({ keepExistingData: false, mode: 'exclusive' });
                            const buffer = await file.arrayBuffer();
                            await writable.write(buffer);
                            await writable.close();
                        },
                    }, {
                        type: 'range',
                        label: Settings.MaskOpacity,
                        range: {
                            max: 1,
                            min: 0,
                            step: 0.05,
                        },
                        key: 'mask_opacity',
                        get() { return GM_getValue('mask_opacity'); },
                        set(val) { GM_setValue('mask_opacity', val); },
                    }, {
                        type: 'color',
                        label: Settings.Color,
                        key: 'color',
                        get() { return GM_getValue('color'); },
                        set(val) { GM_setValue('color', val); },
                    }, {
                        type: 'choose',
                        label: Settings.ImageFit,
                        options: Settings.ImageFitOptions,
                        key: 'image_fit',
                        get() { return GM_getValue('image_fit'); },
                        set(val) { GM_setValue('image_fit', val); },
                    }],
                    label: Settings.Title,
                    listeners: applyBackground,
                });

                /**
                 * 根据设置应用背景
                 */
                async function applyBackground() {
                    // 如果未启用背景功能,卸载现有背景并退出
                    if (!GM_getValue('enabled')) {
                        cur_bg !== null && cur_bg.uninstall();
                        cur_bg = null;
                        return;
                    }

                    // 目前应使用的背景类型及对应的背景管理器
                    /** @type {BGType} */
                    const type = GM_getValue('type');
                    const new_bg = ({
                        'url': BG.image,
                        'local': BG.image,
                        'color': BG.color,
                    }) [type];

                    // 传递给背景管理器的参数
                    /** @type {any[]} */
                    let args = [];
                    switch (type) {
                        case 'url':
                            args = [
                                GM_getValue('image_url'),
                                GM_getValue('mask_opacity'),
                                GM_getValue('image_fit'),
                            ];
                            break;
                        case 'local': {
                            const root = await utils.getModuleDir('background');
                            const image = await root.getFileHandle('image', { create: true });
                            const file = await image.getFile();
                            const url = URL.createObjectURL(file);
                            args = [
                                url,
                                GM_getValue('mask_opacity'),
                                GM_getValue('image_fit'),
                            ];
                            break;
                        }
                        case 'color':
                            args = [GM_getValue('color')];
                            break;
                    }
                    
                    // 如果背景类型不变,调用更新方法,否则卸载当前背景,安装新背景
                    if (cur_bg === new_bg) {
                        new_bg.update.apply(null, args);
                    } else {
                        cur_bg && cur_bg.uninstall();
                        new_bg.install.apply(null, args);
                    }
                    
                    // 更新当前背景管理器
                    cur_bg = new_bg;
                }
            },
        },
        openlastpage: {
            desc: '书评打开尾页',
            checkers: [
                // 书籍信息页
                {
                    type: 'regpath',
                    value: /\/book\/\d+\.htm/
                },
                {
                    type: 'path',
                    value: '/modules/article/articleinfo.php'
                },
                
                // 书评列表页
                {
                    type: 'path',
                    value: '/modules/article/reviews.php'
                },
                {
                    type: 'path',
                    value: '/modules/article/reviewslist.php'
                },
            ],
            async func() {
                detectDom({
                    selector: 'a[href*="/modules/article/reviewshow.php"]',
                    /**
                     * @param {HTMLAnchorElement} a 
                     */
                    callback(a) {
                        if (a.pathname !== '/modules/article/reviewshow.php') { return; }
                        a.before($$CrE({
                            tagName: 'span',
                            props: {
                                innerText: CONST.Text.OpenLastPage.OpenLastPageButton,
                            },
                            styles: {
                                color: 'var(--q-primary)',
                                cursor: 'pointer',
                                paddingRight: '0.3em',
                            },
                            listeners: [['click', e => {
                                const str_rid = new URLSearchParams(a.search).get('rid');
                                window.open(`/modules/article/reviewshow.php?rid=${ str_rid }&page=last`);
                            }]],
                        }));
                    }
                })
            },
        },
        styling: {
            desc: '样式管理器',
            disabled: true,
            detectDom: 'head',
            dependencies: ['utils', 'configs'],
            params: ['GM_setValue', 'GM_getValue', 'GM_addValueChangeListener'],
            func(GM_setValue, GM_getValue, GM_addValueChangeListener) {
                /** @type {utils} */
                const utils = require('utils');
                /** @type {configs} */
                const configs = require('configs');

                // 控制性样式表,用于对文库自带样式表进行一一对应地覆盖
                // 格式:Record<文库自带样式表相对路径, 样式表内容>
                const ControllingStyleSheets = {
                    '/themes/wenku8/style.css': ``,
                    '/configs/article/page.css': ``,
                };

                GM_getValue = utils.defaultedGet({
                }, GM_getValue);

                install();

                /**
                 * 安装所有控制性样式表到页面
                 */
                function install() {
                    Array.from($All('link[rel="stylesheet"][href]')).forEach(link => {
                        const href = link.href;
                        ControllingStyleSheets.hasOwnProperty(href) &&
                            addStyle(ControllingStyleSheets[href], `plus-styling-${href}`);
                    });
                }
            }
        },
    };
    const oFuncs = Object.entries(functions).reduce((arr, [id, oFunc]) => {
        oFunc.id = id;
        arr.push(oFunc);
        return arr;
    }, []);
    default_pool.catch_errors = true;
    loadFuncs(oFuncs);
}) ();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址