☰

VoiceLinks

Makes RJ codes more useful.(8-bit RJCode supported.)

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램을 먼저 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        VoiceLinks
// @name:en     VoiceLinks
// @namespace   Sanya
// @description Makes RJ codes more useful.(8-bit RJCode supported.)
// @description:en Makes RJ codes more useful.(8-bit RJCode supported.)
// @match       *://*/*
// @match       file:///*
// @version     5.0.4
// @connect     dlsite.com
// @connect     media.ci-en.jp
// @connect     img.dlsite.jp
// @connect     *
// @grant       GM_setClipboard
// @grant       GM_openInTab
// @grant       GM_registerMenuCommand
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_addElement
// @grant       GM.xmlHttpRequest
// @grant       GM_xmlhttpRequest
// @run-at      document-start
// @require     https://cdnjs.cloudflare.com/ajax/libs/js-sha256/0.11.0/sha256.min.js
// @homepage    https://sleazyfork.org/zh-CN/scripts/456775-voicelinks
// ==/UserScript==

(function () {
    'use strict';

    const IS_PREVIEW = false;

    //region 持久化设置项

    //长度可变(由用户增删)的列表类设置项,读取时不进行长度校验
    const VARIABLE_LENGTH_SETTINGS = new Set(["_s_search_warehouses"]);

    let settings = {
        //语言设置
        _s_lang: "zh_CN",
        _s_popup_lang: "zh_CN",

        //常规设置
        _s_parse_url: true,
        _s_parse_url_in_dl: false,
        _s_show_translated_title_in_dl: true,
        _s_copy_as_filename_btn: true,
        _s_show_compatibility_warning: true,
        _s_url_insert_mode: "before_rj",
        _s_url_insert_text: "🔗",
        _s_sfw_mode: false,
        _s_sfw_blur_level: "medium",
        _s_sfw_remove_when_hover: true,
        _s_sfw_blur_transition: true,

        //信息显示设置
        _s_category_preset: "voice",
        _s_voice__info_display_order: [
            "voice__dl_count",
            "voice__circle_name",
            "voice__translator_name",
            "voice__release_date",
            "voice__update_date",
            "voice__age_rating",
            "voice__scenario",
            "voice__illustration",
            "voice__voice_actor",
            "voice__music",
            "voice__genre",
            "voice__file_size"
        ],
        _s_voice__dl_count: false,
        _s_voice__circle_name: true,
        _s_voice__translator_name: true,
        _s_voice__release_date: true,
        _s_voice__update_date: true,
        _s_voice__age_rating: true,
        _s_voice__scenario: false,
        _s_voice__illustration: false,
        _s_voice__voice_actor: true,
        _s_voice__music: true,
        _s_voice__genre: true,
        _s_voice__file_size: true,
        _s_game__info_display_order: [
            "game__dl_count",
            "game__circle_name",
            "game__translator_name",
            "game__release_date",
            "game__update_date",
            "game__age_rating",
            "game__scenario",
            "game__illustration",
            "game__voice_actor",
            "game__music",
            "game__genre",
            "game__file_size"
        ],
        _s_game__dl_count: false,
        _s_game__circle_name: true,
        _s_game__translator_name: true,
        _s_game__release_date: true,
        _s_game__update_date: true,
        _s_game__age_rating: true,
        _s_game__scenario: true,
        _s_game__illustration: true,
        _s_game__voice_actor: true,
        _s_game__music: true,
        _s_game__genre: true,
        _s_game__file_size: true,
        _s_manga__info_display_order:[
            "manga__dl_count",
            "manga__circle_name",
            "manga__translator_name",
            "manga__release_date",
            "manga__update_date",
            "manga__age_rating",
            "manga__scenario",
            "manga__illustration",
            "manga__voice_actor",
            "manga__music",
            "manga__genre",
            "manga__file_size"
        ],
        _s_manga__dl_count: false,
        _s_manga__circle_name: true,
        _s_manga__translator_name: true,
        _s_manga__release_date: true,
        _s_manga__update_date: true,
        _s_manga__age_rating: true,
        _s_manga__scenario: true,
        _s_manga__illustration: true,
        _s_manga__voice_actor: true,  //音声漫画
        _s_manga__music: true,
        _s_manga__genre: true,
        _s_manga__file_size: true,
        _s_video__info_display_order: [
            "video__dl_count",
            "video__circle_name",
            "video__translator_name",
            "video__release_date",
            "video__update_date",
            "video__age_rating",
            "video__scenario",
            "video__illustration",
            "video__voice_actor",
            "video__music",
            "video__genre",
            "video__file_size"
        ],
        _s_video__dl_count: false,
        _s_video__circle_name: true,
        _s_video__translator_name: true,
        _s_video__release_date: true,
        _s_video__update_date: true,
        _s_video__age_rating: true,
        _s_video__scenario: true,
        _s_video__illustration: true,
        _s_video__voice_actor: true,
        _s_video__music: true,
        _s_video__genre: true,
        _s_video__file_size: true,
        _s_novel__info_display_order: [
            "novel__dl_count",
            "novel__circle_name",
            "novel__translator_name",
            "novel__release_date",
            "novel__update_date",
            "novel__age_rating",
            "novel__scenario",
            "novel__illustration",
            "novel__voice_actor",
            "novel__music",
            "novel__genre",
            "novel__file_size"
        ],
        _s_novel__dl_count: false,
        _s_novel__circle_name: true,
        _s_novel__translator_name: true,
        _s_novel__release_date: true,
        _s_novel__update_date: true,
        _s_novel__age_rating: true,
        _s_novel__scenario: true,
        _s_novel__illustration: true,
        _s_novel__voice_actor: false,
        _s_novel__music: false,
        _s_novel__genre: true,
        _s_novel__file_size: true,
        _s_other__info_display_order: [
            "other__dl_count",
            "other__circle_name",
            "other__translator_name",
            "other__release_date",
            "other__update_date",
            "other__age_rating",
            "other__scenario",
            "other__illustration",
            "other__voice_actor",
            "other__music",
            "other__genre",
            "other__file_size"
        ],
        _s_other__dl_count: false,
        _s_other__circle_name: true,
        _s_other__translator_name: true,
        _s_other__release_date: true,
        _s_other__update_date: true,
        _s_other__age_rating: true,
        _s_other__scenario: true,
        _s_other__illustration: true,
        _s_other__voice_actor: true,
        _s_other__music: true,
        _s_other__genre: true,
        _s_other__file_size: true,

        //标签显示设置
        _s_tag_main_switch: true,
        _s_tag_display_order: [
            "tag_no_longer_available",
            "tag_rate",
            "tag_work_type",
            "tag_translatable",
            "tag_not_translatable",
            "tag_translated",
            "tag_bonus_work",
            "tag_has_bonus",
            "tag_language_support",
            "tag_file_format",
            "tag_ai",
        ],
        _s_tag_rate: true,
        _s_tag_work_type: true,
        _s_tag_translatable: true,
        _s_tag_not_translatable: true,
        _s_tag_translated: true,
        _s_tag_language_support: true,
        _s_tag_bonus_work: true,
        _s_tag_has_bonus: true,
        _s_tag_file_format: false,
        _s_tag_no_longer_available: true,
        _s_tag_ai: true,

        _s_show_rate_count: false,

        _s_tag_translation_request: true,
        _s_tag_translation_request_display_order: [
            "tag_translation_request_simplified_chinese",
            "tag_translation_request_traditional_chinese",
            "tag_translation_request_english",
            "tag_translation_request_korean",
            "tag_translation_request_spanish",
            "tag_translation_request_german",
            "tag_translation_request_french",
            "tag_translation_request_indonesian",
            "tag_translation_request_italian",
            "tag_translation_request_portuguese",
            "tag_translation_request_swedish",
            "tag_translation_request_thai",
            "tag_translation_request_vietnamese",
        ],
        _s_tag_translation_request_english: false,
        _s_tag_translation_request_simplified_chinese: true,
        _s_tag_translation_request_traditional_chinese: true,
        _s_tag_translation_request_korean: false,
        _s_tag_translation_request_spanish: false,
        _s_tag_translation_request_german: false,
        _s_tag_translation_request_french: false,
        _s_tag_translation_request_indonesian: false,
        _s_tag_translation_request_italian: false,
        _s_tag_translation_request_portuguese: false,
        _s_tag_translation_request_swedish: false,
        _s_tag_translation_request_thai: false,
        _s_tag_translation_request_vietnamese: false,

        //搜索相关
        _s_base_search: false,
        _s_search_warehouses: [], //仓库列表,最多MAX_SEARCH_WAREHOUSES条,顺序决定搜索结果标签的顺序
        _s_cue_languages: "CHI_HANS CHI_HANT",
        _ss_search_profiles: [],
        _ss_cue_lang: ["CHI_HANS", "CHI_HANT"],  //限制关联搜索的语言范围,每多一种语言意味着多一次查询请求
        _s_full_linkage: true,  //开启后,会从DLSite上获取作品的所有关联情况。关闭则会尽力在不进行额外请求的情况下,找到部分作品关联。
        _s_search_linkage: true,  //开启后,会到仓库中查找所有关联作品,可能会产生多次网络请求(如果你的仓库搜索自带该功能,开启此项也不会严重影响速度)。

        backup: function() {
            let backup = {};
            for (let key in this) {
                if(!key.startsWith("_s_")) continue;
                //如果类型为列表,则需要将其拷贝出来
                if(this[key] && Array.isArray(this[key])){
                    backup[key] = [...this[key]];
                }else{
                    backup[key] = this[key];
                }
            }
            this.default_backup = backup;
        },

        //备份默认值
        default_backup: {},

        //暂存已修改值,不更新到设置
        temp_edited: {},

        load: function(){
            let need_reorder = false;
            for(let key in this){
                if(!key.startsWith("_s_")) continue;
                let val = GM_getValue(key.substring(3), this[key]);
                if(typeof val !== typeof this[key]){
                    val = this[key];
                }
                //长度可变的列表(如仓库列表)不能因为长度与默认值不同而被重置
                if(Array.isArray(val) && val.length !== this[key].length && !VARIABLE_LENGTH_SETTINGS.has(key)){
                    val = this[key];
                    need_reorder = true;
                }
                this[key] = val !== undefined ? val : this[key];
            }

            if(need_reorder) {
                window.addEventListener("load", () => {
                    window.alert("VoiceLinks: " + localize(localizationMap.need_reorder));
                });
                this.save();
            }
        },

        save: function () {
            //将暂存修改应用至Settings
            for (let key in this.temp_edited) {
                if(!key.startsWith("_s_")) continue;
                if(this[key] === undefined || this.temp_edited[key] === undefined) continue;
                this[key] = this.temp_edited[key];
                this.temp_edited[key] = undefined;
            }

            //将修改保存至GM
            for(let key in this){
                if(!key.startsWith("_s_")) continue;
                GM_setValue(key.substring(3), this[key]);
            }
        },

        //保存临时修改
        saveTemp: function (key, value){
            if(!key.startsWith("_s_")) key = "_s_" + key;
            this.temp_edited[key] = value;
        },

        clearTemp: function (){
            this.temp_edited = {};
        },

        reset: function () {
            if(!this.default_backup) return;
            for(let key in this.default_backup){
                if(!key.startsWith("_s_")) continue;
                GM_setValue(key.substring(3), this.default_backup[key]);
            }
        },

        hasEdited: function (key) {
            if(!key.startsWith("_s_")) key = "_s_" + key;
            if(this[key] === undefined) return false;
            return this[key] !== this.default_backup[key];
        },

        getDefaultValue: function (key) {
            if(!key.startsWith("_s_")) key = "_s_" + key;
            return this.default_backup[key];
        }
    }
    settings.backup();
    settings.load();
    //endregion

    //region 本地化选项
    const localizationMap = {
        notice_update: {
            zh_CN: "VoiceLinks公告更新,可能包含重要的新功能说明,是否跳转至说明页面?\n\n注意:如果你需要仓库检查功能,请查看说明页面。",
            zh_TW: "VoiceLinks公告更新,可能包含重要的新功能説明,是否跳轉至説明頁面?\n\n注意:如果你需要仓库检查功能,请查看说明页面。",
            en_US: "VoiceLinks Notice Update, may contain important new features, do you want to jump to the notice page?\n\nNote: If you need repo check (kikoeru), please check notice page."
        },

        title_settings: {
            zh_CN: "VoiceLinks 设置",
            zh_TW: "VoiceLinks 設定",
            en_US: "VoiceLinks Settings"
        },

        title_language_settings: {
            zh_CN: "语言设置",
            zh_TW: "語言設定",
            en_US: "Language Settings"
        },

        display_language: {
            zh_CN: "显示语言",
            zh_TW: "顯示語言",
            en_US: "Language"
        },

        popup_language: {
            zh_CN: "弹窗语言",
            zh_TW: "彈窗語言",
            en_US: "Popup Language"
        },

        popup_language_tooltip: {
            zh_CN: "仅修改标题和标签显示语言,信息本身的语言以DLSite网页设置的语言为准。",
            zh_TW: "只修改標題和標籤顯示語言,資訊本身的語言以DLSite網頁設定的語言為準。",
            en_US: "Only modify the title and tag display language, the language of the information itself is determined by the language of the DLSite page settings."
        },

        title_general_settings: {
            zh_CN: "常规",
            zh_TW: "常規",
            en_US: "General"
        },

        parse_url: {
            zh_CN: "解析URL",
            zh_TW: "解析URL",
            en_US: "Parse URL"
        },

        parse_url_tooltip: {
            zh_CN: "鼠标悬停导指向DLSite作品页面的URL时,同样显示作品信息",
            zh_TW: "鼠標懸停導向DLSite作品頁面的URL時,同樣顯示作品資訊",
            en_US: "Show work info when hovering over DLSite work URL"
        },

        parse_url_in_dl: {
            zh_CN: "在DLSite上解析URL",
            zh_TW: "在DLSite上解析URL",
            en_US: "Parse URL in DLSite"
        },

        parse_url_in_dl_tooltip: {
            zh_CN: "URL较多可能影响正常阅读",
            zh_TW: "URL較多可能影響正常閱讀",
            en_US: "URL is more likely to affect normal reading"
        },

        show_translated_title_in_dl: {
            zh_CN: "在DLSite显示对应语言的翻译标题",
            zh_TW: "在DLSite顯示對應語言的翻譯標題",
            en_US: "Show translated title in DLSite"
        },

        show_translated_title_in_dl_tooltip: {
            zh_CN: "作品信息页面的标题将会被修改为与翻译语言对应的标题,避免简中看繁中作品标题为日文的问题",
            zh_TW: "作品資訊頁面的標題將會被修改為與翻譯語言對應的標題,避免繁中看簡中作品標題為日文的問題",
            en_US: "The title of the work info page will be modified to match the corresponding translation language, to avoid viewing the title as Japanese when viewing a work in non-English language."
        },

        copy_as_filename_btn: {
            zh_CN: "“复制为有效文件名”按钮",
            zh_TW: "“複製為有效檔案名”按鈕",
            en_US: '"Copy as filename" button'
        },

        copy_as_filename_btn_tooltip: {
            zh_CN: "鼠标悬停至DLSite作品标题部分将会出现该按钮,点击即可将标题复制为有效文件名,有效文件名指的是会将标题中的非法部分用相似的符号代替",
            zh_TW: "鼠標懸停至DLSite作品標題部分將會出現按鈕,點擊即可將標題複製為有效檔案名,有效檔案名指的是會將標題中的非法部分用相似的符號代替",
            en_US: "Show button when hovering over DLSite work title. Clicking it will copy the title to a valid filename, which will replace the illegal part of the title with similar symbols."
        },

        show_compatibility_warning: {
            zh_CN: "显示兼容性警告",
            zh_TW: "顯示兼容性警告",
            en_US: "Show compatibility warning"
        },

        show_compatibility_warning_tooltip: {
            zh_CN: "如果脚本中,修改DLSite页面元素的功能覆盖了其它脚本的修改,则会触发该弹窗警告",
            zh_TW: "如果腳本中,修改DLSite頁面元素的功能覆蓋了其它腳本的修改,则會觸發該彈窗警告",
            en_US: "If the script modifies the functionality of DLSite elements that are covered by other scripts, the warning will be triggered"
        },

        url_insert_mode: {
            zh_CN: "导向文本的插入方式",
            zh_TW: "導向文本的插入方式",
            en_US: "Type of the insertion"
        },

        url_insert_mode_tooltip: {
            zh_CN: "如果某段链接中的RJ号被解析成功,为了保证原链接不被完全覆盖,会根据需要,在URL的文本前/后插入特定导向文本",
            zh_TW: "如果某段連結中的RJ號被解析成功,為了保證原連結不被完全覆蓋,會根據需要,在URL的文本前/後插入特定導向文本",
            en_US: "If the RJ number in a link is parsed successfully, it is necessary to insert a specific text in the URL before/after the link when the link is almost completely covered by the script"
        },

        url_insert_mode_none: {
            zh_CN: "不插入",
            zh_TW: "不插入",
            en_US: "None"
        },

        url_insert_mode_prefix: {
            zh_CN: "前缀插入代替原链接",
            zh_TW: "前綴插入代替原連結",
            en_US: "Insert before the link as original link."
        },

        url_insert_mode_before_rj: {
            zh_CN: "插入到RJ号前代替RJ链接",
            zh_TW: "插入到RJ號前代替RJ連結",
            en_US: "Insert before the RJ link as the RJ link."
        },

        url_insert_text: {
            zh_CN: "导向文本",
            zh_TW: "導向文本",
            en_US: "Text to insert"
        },

        sfw_mode: {
            zh_CN: "SFW 模式",
            zh_TW: "SFW 模式",
            en_US: "SFW Mode"
        },

        sfw_mode_tooltip: {
            zh_CN: "启用后,所有作品封面均会模糊处理(固定窗口时将鼠标移动到图片上可临时去除模糊)",
            zh_TW: "啟用後,所有作品封面均會模糊處理(固定視窗時將滑鼠移動到圖片上可暫時去除模糊)",
            en_US: "Turn on to blur the cover of all works (temporarily remove the blur by moving the mouse over the image when the window is fixed)."
        },

        sfw_blur_level: {
            zh_CN: "模糊程度",
            zh_TW: "模糊程度",
            en_US: "Blur level"
        },

        sfw_remove_when_hover: {
            zh_CN: "鼠标移到图片上时移除模糊",
            zh_TW: "滑鼠移到圖片上時移除模糊",
            en_US: "Remove the blur when the mouse moves over the image"
        },

        sfw_blur_transition: {
            zh_CN: "模糊动画(卡顿请关闭)",
            zh_TW: "模糊動畫(卡頓請關閉)",
            en_US: "Blur animation"
        },

        low: {
            zh_CN: "低 - 仅模糊细节",
            zh_TW: "低 - 僅模糊細節",
            en_US: "Low - Only blur details"
        },

        medium: {
            zh_CN: "中 - 依稀可见",
            zh_TW: "中 - 依稀可見",
            en_US: "Medium - Hard to see"
        },

        high: {
            zh_CN: "高 - 完全无法辨认",
            zh_TW: "高 - 完全無法辨識",
            en_US: "High - Unrecognizable"
        },

        title_info_settings: {
            zh_CN: "信息显示",
            zh_TW: "信息顯示",
            en_US: "Info Display"
        },

        category_preset: {
            zh_CN: "类别预设",
            zh_TW: "類別預設",
            en_US: "Category Preset"
        },

        category_preset_tooltip: {
            zh_CN: "使不同类别的作品根据需要显示不同的信息<br/><br/>注意:即使勾选了显示,若作品中不存在该信息则也会隐藏。",
            zh_TW: "使不同類別的作品根據需要顯示不同的信息<br/><br/>注意:即使勾選了顯示,若作品中不存在該信息則也會隱藏。",
            en_US: "Show the information of different categories of works. <br/><br/>Note: even if checked, the information of a work that does not exist will be hidden."
        },

        rate: {
            zh_CN: "评分",
            zh_TW: "評分",
            en_US: "Rate"
        },

        rate_tooltip: {
            zh_CN: "星数★ (评分人数 (设置开启))",
            zh_TW: "星數★ (評分人數 (設置開啟))",
            en_US: "Star★ (number of ratings (enable in settings))"
        },

        dl_count: {
            zh_CN: "销量",
            zh_TW: "銷量",
            en_US: "Sales"
        },

        circle_name: {
            zh_CN: "社团名",
            zh_TW: "社團名",
            en_US: "Circle Name"
        },

        translator_name: {
            zh_CN: "翻译者",
            zh_TW: "翻譯者",
            en_US: "Translator"
        },

        release_date: {
            zh_CN: "发售日",
            zh_TW: "發售日",
            en_US: "Release Date"
        },

        update_date: {
            zh_CN: "更新日",
            zh_TW: "更新日",
            en_US: "Update Date"
        },

        age_rating: {
            zh_CN: "年龄指定",
            zh_TW: "年齡指定",
            en_US: "Age Rating"
        },

        scenario: {
            zh_CN: "剧情",
            zh_TW: "劇情",
            en_US: "Scenario"
        },

        illustration: {
            zh_CN: "插画",
            zh_TW: "插圖",
            en_US: "Illustration"
        },

        voice_actor: {
            zh_CN: "声优",
            zh_TW: "聲優",
            en_US: "Voice Actor"
        },

        music: {
            zh_CN: "音乐",
            zh_TW: "音樂",
            en_US: "Music"
        },

        genre: {
            zh_CN: "分类",
            zh_TW: "分類",
            en_US: "Genre"
        },

        file_size: {
            zh_CN: "文件容量",
            zh_TW: "檔案容量",
            en_US: "File Size"
        },

        title_tag_settings: {
            zh_CN: "标签显示",
            zh_TW: "標籤顯示",
            en_US: "Tag Display"
        },

        tag_main_switch: {
            zh_CN: "标签总开关",
            zh_TW: "標籤總開關",
            en_US: "Tag Main Switch"
        },

        tag_main_switch_tooltip: {
            zh_CN: "关闭则所有标签均不显示",
            zh_TW: "關閉則所有標籤都不顯示",
            en_US: "If turned off, all tags will not be displayed"
        },

        tag_work_type: {
            zh_CN: "作品类型",
            zh_TW: "作品類型",
            en_US: "Work Type"
        },

        work_type_game: {
            zh_CN: "游戏",
            zh_TW: "遊戲",
            en_US: "Game"
        },

        work_type_comic: {
            zh_CN: "漫画",
            zh_TW: "漫畫",
            en_US: "Manga"
        },

        work_type_illustration: {
            zh_CN: "CG・插画",
            zh_TW: "CG・插畫",
            en_US: "CG + Illustrations"
        },

        work_type_novel: {
            zh_CN: "小说",
            zh_TW: "小說",
            en_US: "Novel"
        },

        work_type_video: {
            zh_CN: "视频",
            zh_TW: "影片",
            en_US: "Video"
        },

        work_type_voice: {
            zh_CN: "音声・ASMR",
            zh_TW: "聲音作品・ASMR",
            en_US: "Voice / ASMR"
        },

        work_type_music: {
            zh_CN: "音乐",
            zh_TW: "音樂",
            en_US: "Music"
        },

        work_type_tool: {
            zh_CN: "工具/装饰",
            zh_TW: "工具/配件",
            en_US: "Tools / Accessories"
        },

        work_type_voice_comic: {
            zh_CN: "音声漫画",
            zh_TW: "有聲漫畫",
            en_US: "Voiced Comics"
        },

        work_type_other: {
            zh_CN: "其他",
            zh_TW: "其他",
            en_US: "Miscellaneous"
        },

        tag_translatable: {
            zh_CN: "可翻译",
            zh_TW: "可翻譯",
            en_US: "Translatable"
        },

        tag_translatable_tooltip: {
            zh_CN: "大家一起来翻译 授权作品",
            zh_TW: "大家一起翻譯 授权作品",
            en_US: "Translators Unite translation permitted work"
        },

        tag_not_translatable: {
            zh_CN: "不可翻译",
            zh_TW: "不可翻譯",
            en_US: "Not Translatable"
        },

        tag_not_translatable_tooltip: {
            zh_CN: "未授权 大家一起来翻译",
            zh_TW: "未授權 大家一起來翻譯",
            en_US: "Not Translators Unite translation permitted work"
        },

        tag_translated: {
            zh_CN: "翻译作品",
            zh_TW: "翻譯作品",
            en_US: "Translated"
        },

        tag_translated_tooltip: {
            zh_CN: "当前作品为 大家一起来翻译 作品",
            zh_TW: "當前作品為 大家一起來翻譯 作品",
            en_US: "Current work is Translators Unite translation work"
        },

        tag_language_support: {
            zh_CN: "语言支持",
            zh_TW: "語言支援",
            en_US: "Language Support"
        },

        language_japanese: {
            zh_CN: "日文",
            zh_TW: "日文",
            en_US: "Japanese"
        },

        language_english: {
            zh_CN: "英文",
            zh_TW: "英文",
            en_US: "English"
        },

        language_korean: {
            zh_CN: "韩语",
            zh_TW: "韓語",
            en_US: "Korean"
        },

        language_simplified_chinese: {
            zh_CN: "简体中文",
            zh_TW: "簡體中文",
            en_US: "Simplified Chinese"
        },

        language_traditional_chinese: {
            zh_CN: "繁体中文",
            zh_TW: "繁體中文",
            en_US: "Traditional Chinese"
        },

        language_german: {
            zh_CN: "德语",
            zh_TW: "德語",
            en_US: "German"
        },

        language_french: {
            zh_CN: "法语",
            zh_TW: "法語",
            en_US: "French"
        },

        language_russian: {
            zh_CN: "俄语",
            zh_TW: "俄語",
            en_US: "Russian"
        },

        language_spanish: {
            zh_CN: "西班牙语",
            zh_TW: "西班牙語",
            en_US: "Spanish"
        },

        language_indonesian: {
            zh_CN: "印尼文",
            zh_TW: "印尼文",
            en_US: "Indonesian"
        },

        language_italian: {
            zh_CN: "意大利语",
            zh_TW: "義大利語",
            en_US: "Italian"
        },

        language_arabic: {
            zh_CN: "阿拉伯语",
            zh_TW: "阿拉伯語",
            en_US: "Arabic"
        },

        language_portuguese: {
            zh_CN: "葡萄牙语",
            zh_TW: "葡萄牙語",
            en_US: "Portuguese"
        },

        language_finnish: {
            zh_CN: "芬兰语",
            zh_TW: "芬蘭語",
            en_US: "Finnish"
        },

        language_polish: {
            zh_CN: "波兰语",
            zh_TW: "波蘭語",
            en_US: "Polish"
        },

        language_swedish: {
            zh_CN: "瑞典文",
            zh_TW: "瑞典文",
            en_US: "Swedish"
        },

        language_thai: {
            zh_CN: "泰语",
            zh_TW: "泰語",
            en_US: "Thai"
        },

        language_vietnamese: {
            zh_CN: "越南语",
            zh_TW: "越南語",
            en_US: "Vietnamese"
        },

        language_japanese_abbr: {
            zh_CN: "日",
            zh_TW: "日",
            en_US: "JP"
        },

        language_english_abbr: {
            zh_CN: "英",
            zh_TW: "英",
            en_US: "EN"
        },

        language_korean_abbr: {
            zh_CN: "韩",
            zh_TW: "韩",
            en_US: "KO"
        },

        language_simplified_chinese_abbr: {
            zh_CN: "简中",
            zh_TW: "簡中",
            en_US: "ZH"
        },

        language_traditional_chinese_abbr: {
            zh_CN: "繁中",
            zh_TW: "繁中",
            en_US: "TW"
        },

        language_german_abbr: {
            zh_CN: "德",
            zh_TW: "德",
            en_US: "DE"
        },

        language_french_abbr: {
            zh_CN: "法",
            zh_TW: "法",
            en_US: "FR"
        },

        language_spanish_abbr: {
            zh_CN: "西",
            zh_TW: "西",
            en_US: "ES"
        },

        language_indonesian_abbr: {
            zh_CN: "印",
            zh_TW: "印",
            en_US: "ID"
        },

        language_italian_abbr: {
            zh_CN: "意",
            zh_TW: "意",
            en_US: "IT"
        },

        language_portuguese_abbr: {
            zh_CN: "葡",
            zh_TW: "葡",
            en_US: "PT"
        },

        language_swedish_abbr: {
            zh_CN: "瑞典",
            zh_TW: "瑞典",
            en_US: "SV"
        },

        language_thai_abbr: {
            zh_CN: "泰",
            zh_TW: "泰",
            en_US: "TH"
        },

        language_vietnamese_abbr: {
            zh_CN: "越",
            zh_TW: "越",
            en_US: "VN"
        },

        show_rate_count: {
            zh_CN: "显示评分人数",
            zh_TW: "顯示評分人數",
            en_US: "Show Rate Count"
        },

        tag_translation_request: {
            zh_CN: "翻译申请情况",
            zh_TW: "翻譯申請情况",
            en_US: "Translation Request"
        },

        tag_translation_request_tooltip: {
            zh_CN: "当前作品目前的翻译申请情况,格式为:语言简写 申请数-发售数",
            zh_TW: "當前作品目前的翻譯申請情況,格式為:语言簡稱 申請數-發售數",
            en_US: "Current work's translation request. Format: Language_Abbr Number_of_Requests - Number_of_Sales"
        },

        tag_bonus_work: {
            zh_CN: "特典",
            zh_TW: "特典",
            en_US: "Bonus"
        },

        tag_bonus_work_tooltip: {
            zh_CN: "当前作品是某部作品的特典",
            zh_TW: "當前作品是某部作品的特典",
            en_US: "Current work is a bonus work"
        },

        tag_has_bonus: {
            zh_CN: "有特典",
            zh_TW: "有特典",
            en_US: "Has Bonus"
        },

        tag_has_bonus_tooltip: {
            zh_CN: "当前作品目前附赠特典,若特典已下架则不会显示该标签",
            zh_TW: "當前作品目前附赠特典,若特典已下架則不會顯示該標籤",
            en_US: "Current work has bonus. If bonus is not available, the tag will not be displayed."
        },

        tag_file_format: {
            zh_CN: "文件格式",
            zh_TW: "檔案形式",
            en_US: "File Format"
        },

        tag_file_format_tooltip: {
            zh_CN: "WAV、EXE、MP3等",
            zh_TW: "WAV、EXE、MP3等",
            en_US: "WAV, EXE, MP3, etc."
        },

        tag_no_longer_available: {
            zh_CN: "已下架",
            zh_TW: "已下架",
            en_US: "Unavailable"
        },

        tag_announce: {
            zh_CN: "预告",
            zh_TW: "預告",
            en_US: "Announce"
        },

        tag_ai: {
            zh_CN: "AI & 部分AI",
            zh_TW: "AI & 部分AI",
            en_US: "AI & Partial AI"
        },

        tag_aig: {
            zh_CN: "AI生成",
            zh_TW: "AI生成",
            en_US: "AI Gen",
        },

        tag_aip: {
            zh_CN: "AI部分使用",
            zh_TW: "AI部分使用",
            en_US: "AI Partial",
        },

        tag_ai_tooltip: {
            zh_CN: "全部或部分使用AI的作品",
            zh_TW: "全部或部分使用AI的作品",
            en_US: "Full or partial use of AI",
        },

        button_save: {
            zh_CN: "保存设置",
            zh_TW: "保存設置",
            en_US: "Save",
        },

        button_cancel: {
            zh_CN: "取消设置",
            zh_TW: "取消設置",
            en_US: "Cancel",
        },

        button_reset: {
            zh_CN: "重置设置",
            zh_TW: "重置設置",
            en_US: "Reset",
        },

        need_reorder: {
            zh_CN: "检测到设置更新,可能添加了新的信息位,请重新设置对应设置项的排列",
            zh_TW: "檢查到設置更新,可能添加了新的信息位,请重新設置對應設置項的排列",
            en_US: "There is a new setting item added. Please reorder the corresponding setting item",
        },

        save_complete: {
            zh_CN: "设置已保存,部分设置需要刷新对应页面以生效",
            zh_TW: "設置已保存,部分設置需要刷新對應頁面以生效",
            en_US: "Settings saved, some settings need to refresh the corresponding page to take effect",
        },

        save_failed: {
            zh_CN: "设置保存失败",
            zh_TW: "設置保存失敗",
            en_US: "Settings save failed",
        },

        reset_confirm: {
            zh_CN: "确定要将设置重置到最初始的状态吗?(重置后,需要再点击保存才会生效)",
            zh_TW: "確定要將設置重置到最初始的狀態嗎?(重置後,需要再點擊保存才會生效)",
            en_US: "Are you sure you want to reset the settings to the initial state? (After resetting, you need to click Save to take effect)",
        },

        reset_complete: {
            zh_CN: "设置已重置",
            zh_TW: "設置已重置",
            en_US: "Settings reset",
        },

        reset_failed: {
            zh_CN: "设置重置失败",
            zh_TW: "設置重置失敗",
            en_US: "Settings reset failed",
        },

        reset_order: {
            zh_CN: "重置顺序",
            zh_TW: "重置順序",
            en_US: "Reset Order",
        },

        reset_order_confirm: {
            zh_CN: "确定要将元素顺序重置到最初始的状态吗?",
            zh_TW: "確定要將元素順序重置到最初始的狀態嗎?",
            en_US: "Are you sure you want to reset the element order to the initial state?",
        },

        reset_order_and_setting: {
            zh_CN: "重置元素顺序和各自的设置值",
            zh_TW: "重置元素順序和各自的設置值",
            en_US: "Reset element order and their settings",
        },

        hint_pin: {
            zh_CN: "按住{pin_key}以固定弹框,固定时可复制信息",
            zh_TW: "按住{pin_key}以固定彈窗,固定時可複製資訊",
            en_US: "Hold {pin_key} to pin the popup, info can be copied.",
        },

        hint_unpin: {
            zh_CN: "抬起{pin_key}以关闭弹框 & 查看其它作品RJ信息",
            zh_TW: "抬起{pin_key}以關閉彈窗 & 查看其它作品RJ信息",
            en_US: "Release {pin_key} to close the popup & view other works.",
        },

        hint_copy: {
            zh_CN: "左键单击以复制信息",
            zh_TW: "左鍵單擊以複製資訊",
            en_US: "Left click to copy info.",
        },

        hint_copy_all: {
            zh_CN: "左键单击以复制内部所有信息",
            zh_TW: "左鍵單擊以複製內部所有資訊",
            en_US: "Left click to copy all contained info.",
        },

        hint_copy_work_title: {
            zh_CN: "单击复制标题,Alt+单击复制为有效文件名",
            zh_TW: "單擊複製標題,Alt+單擊複製為有效檔名",
            en_US: "Click to copy title, Alt+click to copy as valid filename.",
        },

        search_result_this: {
            zh_CN: "本作",
            zh_TW: "本作",
            en_US: "This"
        },

        search_result_this_lang: {
            zh_CN: "该语言",
            zh_TW: "该语言",
            en_US: "Lang"
        },

        search_result_orig: {
            zh_CN: "原版",
            zh_TW: "原版",
            en_US: "Orig"
        },

        search_result_translation: {
            zh_CN: "翻译",
            zh_TW: "翻译",
            en_US: "🌐"
        },

        //仓检:设置分区
        //说明:文本中的{max}、{count}、{name}、{fields}会在使用时由localizeFormat替换为对应内容
        search_settings_title: {
            zh_CN: "仓库检查",
            zh_TW: "倉庫檢查",
            en_US: "Repository Check",
        },

        search_enable: {
            zh_CN: "开启仓检",
            zh_TW: "開啟倉檢",
            en_US: "Enable Repository Check",
        },

        search_enable_tooltip: {
            zh_CN: "开启后,会检查特定仓库内的作品及其相关版本是否存在,并以标签的形式展示每个仓库的搜索结果。<br/>" +
                "<b>内置kikoeru框架的预设(非asmr.one),也可通过自定义预设适配其它框架的仓库</b><br/><br/>" +
                "<b><u>注意:该功能会向第三方仓库API发送查询信息,若仓库不属于你,请在确认仓库所有者允许的情况下使用该功能!</u></b>",
            zh_TW: "開啟後,會檢查特定倉庫內的作品及其相關版本是否存在,並以標籤的形式展示每個倉庫的搜尋結果。<br/>" +
                "<b>內建 kikoeru 框架的預設(非 asmr.one),也可透過自訂預設適配其他框架的倉庫</b><br/><br/>" +
                "<b><u>注意:該功能會向第三方倉庫 API 發送查詢資訊,若倉庫不屬於你,請在確認倉庫所有者允許的情況下使用該功能!</u></b>",
            en_US: "Once enabled, the script checks whether the work and its related versions exist in the specified repositories, and shows each repository's search result as a label.<br/>" +
                "<b>A preset for the kikoeru framework is built in (asmr.one is not supported), and repositories on other frameworks can be adapted with the custom preset</b><br/><br/>" +
                "<b><u>Note: this feature sends queries to third-party repository APIs. If a repository is not yours, please make sure its owner allows this before using the feature!</u></b>",
        },

        search_manage_warehouses: {
            zh_CN: "管理仓库(已添加 {count}/{max})",
            zh_TW: "管理倉庫(已新增 {count}/{max})",
            en_US: "Manage repositories ({count}/{max} added)",
        },

        search_warehouse_list: {
            zh_CN: "仓库列表",
            zh_TW: "倉庫列表",
            en_US: "Repository list",
        },

        search_manage_warehouses_tooltip: {
            zh_CN: "添加、编辑、启用/停用、排序或删除仓库。<br/>" +
                "最多可添加{max}个仓库,列表顺序即为弹框中搜索结果标签的顺序。<br/>" +
                "账号密码、登录状态与查询缓存均按仓库独立保存;停用或删除仓库时会清空该仓库的缓存。",
            zh_TW: "新增、編輯、啟用/停用、排序或刪除倉庫。<br/>" +
                "最多可新增 {max} 個倉庫,列表順序即為彈出視窗中搜尋結果標籤的順序。<br/>" +
                "帳號密碼、登入狀態與查詢快取均按倉庫獨立儲存;停用或刪除倉庫時會清空該倉庫的快取。",
            en_US: "Add, edit, enable/disable, reorder, or delete repositories.<br/>" +
                "Up to {max} repositories can be added, and their order is the order of the search result labels in the pop-up.<br/>" +
                "Credentials, login state, and query cache are stored per repository; disabling or deleting a repository clears its cache.",
        },

        search_full_linkage: {
            zh_CN: "完整关联",
            zh_TW: "完整關聯",
            en_US: "Full linkage",
        },

        search_full_linkage_tooltip: {
            zh_CN: "查询仓库前,会先寻找与该作品相关的其它作品(如原版、翻译版等)<br/>" +
                "会产生多次网络请求,但能使得仓库搜索结果更完整(需要同时开启连锁查询)",
            zh_TW: "查詢倉庫前,會先尋找與該作品相關的其他作品(如原版、翻譯版等)<br/>" +
                "會產生多次網路請求,但能讓倉庫搜尋結果更完整(需要同時開啟連鎖查詢)",
            en_US: "Before querying the repositories, the script looks for other works related to this one (such as the original or translated versions).<br/>" +
                "This causes several network requests, but makes the repository search results more complete (requires chained search to be enabled as well).",
        },

        search_cue_languages: {
            zh_CN: "语言列表",
            zh_TW: "語言列表",
            en_US: "Language list",
        },

        search_cue_languages_tooltip: {
            zh_CN: "用空格分隔的语言代码,代表你想要搜索的所有语言关联。<br/>" +
                "语言个数会影响搜索效率和发往DLSite的请求数量。<br/><br/>" +
                "<b>例子:CHI_HANS CHI_HANT ENG</b><br/>" +
                "上例表示需要搜索简体中文、繁体中文、英文语言的翻译作品关联,其它语言的关联将不会搜寻。",
            zh_TW: "以空格分隔的語言代碼,代表你想要搜尋的所有語言關聯。<br/>" +
                "語言數量會影響搜尋效率與發往 DLSite 的請求數量。<br/><br/>" +
                "<b>範例:CHI_HANS CHI_HANT ENG</b><br/>" +
                "上例表示需要搜尋簡體中文、繁體中文、英文語言的翻譯作品關聯,其他語言的關聯將不會搜尋。",
            en_US: "Space-separated language codes, representing all language relations you want to search.<br/>" +
                "The number of languages affects search speed and the number of requests sent to DLSite.<br/><br/>" +
                "<b>Example: CHI_HANS CHI_HANT ENG</b><br/>" +
                "The example above searches for translation relations in Simplified Chinese, Traditional Chinese, and English; relations in other languages are not searched.",
        },

        search_linkage: {
            zh_CN: "连锁查询",
            zh_TW: "連鎖查詢",
            en_US: "Chained search",
        },

        search_linkage_tooltip: {
            zh_CN: "搜索仓库时,对所有关联的RJ号进行搜索,可能产生多次请求。<br/>" +
                "如果你的仓库包含搜索其它语言版本的功能,开启此项也不会影响速度。",
            zh_TW: "搜尋倉庫時,對所有關聯的 RJ 號進行搜尋,可能產生多次請求。<br/>" +
                "如果你的倉庫本身包含搜尋其他語言版本的功能,開啟此項也不會影響速度。",
            en_US: "Search every related RJ code in the repositories, which may cause several requests.<br/>" +
                "If your repository can already search for other language versions, enabling this option will not slow things down.",
        },

        //仓检:仓库管理弹窗
        search_manager_title: {
            zh_CN: "仓库管理",
            zh_TW: "倉庫管理",
            en_US: "Repository Management",
        },

        search_manager_add: {
            zh_CN: "添加仓库",
            zh_TW: "新增倉庫",
            en_US: "Add repository",
        },

        search_manager_add_limit: {
            zh_CN: "最多只能添加{max}个仓库",
            zh_TW: "最多只能新增 {max} 個倉庫",
            en_US: "You can add at most {max} repositories",
        },

        search_manager_hint: {
            zh_CN: "最多可添加{max}个仓库,列表顺序即为弹框中搜索结果标签的顺序。" +
                "关闭左侧开关可以暂停该仓库的仓检(停用时会清空该仓库的缓存)。",
            zh_TW: "最多可新增 {max} 個倉庫,列表順序即為彈出視窗中搜尋結果標籤的順序。" +
                "關閉左側開關可以暫停該倉庫的倉檢(停用時會清空該倉庫的快取)。",
            en_US: "Up to {max} repositories can be added, and their order is the order of the search result labels in the pop-up. " +
                "Turn off the switch on the left to pause the check for that repository (disabling it clears the repository's cache).",
        },

        search_manager_empty: {
            zh_CN: "还没有添加任何仓库,点击右上角「添加仓库」开始配置。",
            zh_TW: "還沒有新增任何倉庫,點擊右上角「新增倉庫」開始設定。",
            en_US: "No repositories have been added yet. Click \"Add repository\" in the top-right corner to get started.",
        },

        search_manager_close: {
            zh_CN: "关闭",
            zh_TW: "關閉",
            en_US: "Close",
        },

        search_manager_toggle_tooltip: {
            zh_CN: "是否启用该仓库的仓检",
            zh_TW: "是否啟用該倉庫的倉檢",
            en_US: "Whether this repository takes part in Repository Check",
        },

        search_manager_disabled: {
            zh_CN: "|已停用",
            zh_TW: "|已停用",
            en_US: " | Disabled",
        },

        search_manager_edit: {
            zh_CN: "编辑",
            zh_TW: "編輯",
            en_US: "Edit",
        },

        search_manager_move_up: {
            zh_CN: "上移",
            zh_TW: "上移",
            en_US: "Move up",
        },

        search_manager_move_down: {
            zh_CN: "下移",
            zh_TW: "下移",
            en_US: "Move down",
        },

        search_manager_delete: {
            zh_CN: "删除",
            zh_TW: "刪除",
            en_US: "Delete",
        },

        search_manager_delete_confirm: {
            zh_CN: "确定要删除仓库「{name}」吗?\n该仓库的账号密码、登录状态与查询结果缓存都会被删除。",
            zh_TW: "確定要刪除倉庫「{name}」嗎?\n該倉庫的帳號密碼、登入狀態與查詢結果快取都會被刪除。",
            en_US: "Are you sure you want to delete the repository \"{name}\"?\nIts credentials, login state, and query result cache will all be deleted.",
        },

        search_manager_editor_add_title: {
            zh_CN: "添加仓库",
            zh_TW: "新增倉庫",
            en_US: "Add repository",
        },

        search_manager_editor_edit_title: {
            zh_CN: "编辑仓库",
            zh_TW: "編輯倉庫",
            en_US: "Edit repository",
        },

        search_manager_preset_field: {
            zh_CN: "预设类型",
            zh_TW: "預設類型",
            en_US: "Preset type",
        },

        search_manager_name_field: {
            zh_CN: "仓库名称",
            zh_TW: "倉庫名稱",
            en_US: "Repository name",
        },

        search_manager_name_tooltip: {
            zh_CN: "显示在搜索结果标签前的名称,多个仓库可以重名。",
            zh_TW: "顯示在搜尋結果標籤前的名稱,多個倉庫可以重名。",
            en_US: "The name shown in front of the search result label. Multiple repositories may share the same name.",
        },

        search_manager_footer_add: {
            zh_CN: "添加",
            zh_TW: "新增",
            en_US: "Add",
        },

        search_manager_footer_save: {
            zh_CN: "保存",
            zh_TW: "儲存",
            en_US: "Save",
        },

        search_manager_cancel: {
            zh_CN: "取消",
            zh_TW: "取消",
            en_US: "Cancel",
        },

        search_manager_required_alert: {
            zh_CN: "以下内容不能为空:\n{fields}",
            zh_TW: "以下內容不能為空:\n{fields}",
            en_US: "The following fields cannot be empty:\n{fields}",
        },

        //仓检:仓库预设与预设字段
        search_preset_kikoeru_tooltip: {
            zh_CN: "内置预设:适配kikoeru仓库(音声仓库),自带账号密码登录与 /api/search 查询。",
            zh_TW: "內建預設:適配 kikoeru 倉庫(音聲倉庫),自帶帳號密碼登入與 /api/search 查詢。",
            en_US: "Built-in preset: for kikoeru repositories (doujin audio libraries). Comes with username/password login and /api/search queries.",
        },

        search_preset_custom: {
            zh_CN: "自定义",
            zh_TW: "自訂",
            en_US: "Custom",
        },

        search_preset_custom_tooltip: {
            zh_CN: "自定义预设:通过数据描述适配其它框架的仓库,URL模板中的%s会被替换为搜索关键词(RJ号)。",
            zh_TW: "自訂預設:透過資料描述適配其他框架的倉庫,URL 模板中的 %s 會被替換為搜尋關鍵字(RJ 號)。",
            en_US: "Custom preset: adapts repositories on other frameworks through a data description. The %s in URL templates is replaced with the search keyword (the RJ code).",
        },

        search_field_repo_url: {
            zh_CN: "仓库地址",
            zh_TW: "倉庫地址",
            en_US: "Repository URL",
        },

        search_field_repo_url_tooltip: {
            zh_CN: "仓库的根地址,即访问仓库的链接<br/><br/><b>例如:https://example.com</b>",
            zh_TW: "倉庫的根地址,即存取倉庫的連結<br/><br/><b>例如:https://example.com</b>",
            en_US: "The root URL of the repository, i.e. the link you normally use to access it.<br/><br/><b>Example: https://example.com</b>",
        },

        search_field_api_url: {
            zh_CN: "API地址",
            zh_TW: "API 位址",
            en_US: "API URL",
        },

        search_field_api_url_tooltip: {
            zh_CN: "仓库API的地址(baseURL)<br/>" +
                "<b>仅当仓库前端与后端API分离时(如API使用了独立的域名或端口)才需要单独填写;留空则使用上方仓库地址</b><br/><br/>" +
                "<b>例如:https://example.com</b>",
            zh_TW: "倉庫 API 的位址(baseURL)<br/>" +
                "<b>僅當倉庫前端與後端 API 分離時(如 API 使用了獨立的網域或連接埠)才需要單獨填寫;留空則使用上方倉庫地址</b><br/><br/>" +
                "<b>例如:https://example.com</b>",
            en_US: "The address of the repository API (baseURL).<br/>" +
                "<b>Only needed when the repository frontend and backend API are hosted separately (for example when the API uses its own domain or port); leave it empty to use the repository URL above</b><br/><br/>" +
                "<b>Example: https://example.com</b>",
        },

        search_field_auth: {
            zh_CN: "认证方式",
            zh_TW: "認證方式",
            en_US: "Authentication",
        },

        search_auth_login: {
            zh_CN: "账号密码登录",
            zh_TW: "帳號密碼登入",
            en_US: "Username/Password Login",
        },

        search_auth_headers: {
            zh_CN: "固定请求头",
            zh_TW: "固定請求標頭",
            en_US: "Fixed Request Headers",
        },

        search_auth_none: {
            zh_CN: "无认证",
            zh_TW: "無認證",
            en_US: "No Authentication",
        },

        search_field_auth_tooltip_kikoeru: {
            zh_CN: "账号密码登录:自动调用仓库登录接口获取token并缓存,token失效时自动重新登录。<br/>" +
                "固定请求头:仅使用下方自定义请求头中的内容进行验证(如手动粘贴的token)。<br/>" +
                "无认证:不进行任何验证,仅适用于无需登录即可查询的仓库。",
            zh_TW: "帳號密碼登入:自動呼叫倉庫登入 API 取得 token 並快取,token 失效時自動重新登入。<br/>" +
                "固定請求標頭:僅使用下方自訂請求標頭中的內容進行驗證(如手動貼上的 token)。<br/>" +
                "無認證:不進行任何驗證,僅適用於無需登入即可查詢的倉庫。",
            en_US: "Username/Password Login: automatically calls the repository's login API, caches the token, and logs in again once the token expires.<br/>" +
                "Fixed Request Headers: authenticates using only the custom request headers below (for example a token copied by hand).<br/>" +
                "No Authentication: no authentication at all; only suitable for repositories that can be queried without logging in.",
        },

        search_field_auth_tooltip_custom: {
            zh_CN: "无认证:不进行任何验证。<br/>" +
                "固定请求头:仅使用下方自定义请求头中的内容进行验证。<br/>" +
                "账号密码登录:按下方配置调用登录接口获取token并缓存,token失效时自动重新登录。<br/>" +
                "不同仓库的登录方式差异较大,若下方配置无法满足,请反馈具体的接口格式。",
            zh_TW: "無認證:不進行任何驗證。<br/>" +
                "固定請求標頭:僅使用下方自訂請求標頭中的內容進行驗證。<br/>" +
                "帳號密碼登入:依下方設定呼叫登入 API 取得 token 並快取,token 失效時自動重新登入。<br/>" +
                "不同倉庫的登入方式差異較大,若下方設定無法滿足,請回報具體的介面格式。",
            en_US: "No Authentication: no authentication at all.<br/>" +
                "Fixed Request Headers: authenticates using only the custom request headers below.<br/>" +
                "Username/Password Login: calls the login API according to the settings below, caches the token, and logs in again once the token expires.<br/>" +
                "Login methods differ a lot between repositories; if the settings below cannot cover yours, please report the actual API format.",
        },

        search_field_username: {
            zh_CN: "仓库用户名",
            zh_TW: "倉庫使用者名稱",
            en_US: "Repository username",
        },

        search_field_username_tooltip: {
            zh_CN: "用于自动登录仓库的用户名。",
            zh_TW: "用於自動登入倉庫的使用者名稱。",
            en_US: "The username used to log in to the repository automatically.",
        },

        search_field_password: {
            zh_CN: "仓库密码",
            zh_TW: "倉庫密碼",
            en_US: "Repository password",
        },

        search_field_password_tooltip: {
            zh_CN: "用于自动登录仓库的密码(不会自动去除首尾空格)。<br/>" +
                "脚本会将账号密码本地保存且仅用于登录仓库,不会将其发送给除仓库以外的第三方。",
            zh_TW: "用於自動登入倉庫的密碼(不會自動去除首尾空格)。<br/>" +
                "腳本會將帳號密碼儲存在本機,且僅用於登入倉庫,不會將其傳送給倉庫以外的第三方。",
            en_US: "The password used to log in to the repository automatically (leading and trailing spaces are not trimmed).<br/>" +
                "The script stores your credentials locally and uses them only to log in to the repository; they are never sent to any third party other than the repository.",
        },

        search_field_headers: {
            zh_CN: "自定义请求头",
            zh_TW: "自訂請求標頭",
            en_US: "Custom request headers",
        },

        search_field_headers_tooltip_kikoeru: {
            zh_CN: "每行一个,格式为key:value,用换行符分隔。<br/>" +
                "本项内容会随仓检请求一起发送,用于应对仓库API需要特定请求头才能访问的情况(一般留空即可)。<br/>" +
                "认证方式为账号密码登录时,本项中的Authorization头会被token覆盖(登录请求中也会将该头过滤)。<br/><br/>" +
                "<b>例如:<br/>Authorization: Bearer xxxxxxxxxx</b>",
            zh_TW: "每行一個,格式為 key:value,以換行符分隔。<br/>" +
                "本項內容會隨倉檢請求一起傳送,用於應對倉庫 API 需要特定請求標頭才能存取的情況(一般留空即可)。<br/>" +
                "認證方式為帳號密碼登入時,本項中的 Authorization 標頭會被 token 覆蓋(登入請求中也會將該標頭過濾)。<br/><br/>" +
                "<b>例如:<br/>Authorization: Bearer xxxxxxxxxx</b>",
            en_US: "One entry per line in the form key:value.<br/>" +
                "These headers are sent along with Repository Check requests, for repository APIs that require specific headers to be accessible (usually left empty).<br/>" +
                "When the authentication method is username/password login, the Authorization header here is overridden by the token (and filtered out of login requests).<br/><br/>" +
                "<b>Example:<br/>Authorization: Bearer xxxxxxxxxx</b>",
        },

        search_field_headers_tooltip_custom: {
            zh_CN: "每行一个,格式为key:value,用换行符分隔。<br/>" +
                "本项内容会随仓检请求一起发送;登录接口同样会携带本项内容(但会过滤掉其中的Authorization)。<br/><br/>" +
                "<b>例如:<br/>X-API-Key: xxxxxxxxxx</b>",
            zh_TW: "每行一個,格式為 key:value,以換行符分隔。<br/>" +
                "本項內容會隨倉檢請求一起傳送;登入 API 同樣會攜帶本項內容(但會過濾掉其中的 Authorization)。<br/><br/>" +
                "<b>例如:<br/>X-API-Key: xxxxxxxxxx</b>",
            en_US: "One entry per line in the form key:value.<br/>" +
                "These headers are sent along with Repository Check requests, and the login API receives them as well (with Authorization filtered out).<br/><br/>" +
                "<b>Example:<br/>X-API-Key: xxxxxxxxxx</b>",
        },

        search_field_search_url: {
            zh_CN: "搜索URL模板",
            zh_TW: "搜尋 URL 模板",
            en_US: "Search URL template",
        },

        search_field_search_url_tooltip: {
            zh_CN: "仓库搜索接口地址,其中的%s会被替换为搜索关键词(RJ号)。<br/>" +
                "<b>注意:仅支持%s这一个变量,需要其它信息(如分页、时间戳等)的仓库暂不支持。</b><br/><br/>" +
                "<b>例如:https://www.xxx.com/api/search?keyword=%s</b>",
            zh_TW: "倉庫搜尋 API 位址,其中的 %s 會被替換為搜尋關鍵字(RJ 號)。<br/>" +
                "<b>注意:僅支援 %s 這一個變數,需要其他資訊(如分頁、時間戳等)的倉庫暫不支援。</b><br/><br/>" +
                "<b>例如:https://www.xxx.com/api/search?keyword=%s</b>",
            en_US: "The repository's search API address, where %s is replaced with the search keyword (the RJ code).<br/>" +
                "<b>Note: only the single %s variable is supported; repositories that need other information (paging, timestamps, etc.) are not supported yet.</b><br/><br/>" +
                "<b>Example: https://www.xxx.com/api/search?keyword=%s</b>",
        },

        search_field_show_url: {
            zh_CN: "结果展示URL模板",
            zh_TW: "結果展示 URL 模板",
            en_US: "Result page URL template",
        },

        search_field_show_url_tooltip: {
            zh_CN: "点击搜索结果标签时打开的地址,其中的%s会被替换为RJ号。<br/>" +
                "留空则点击标签不打开任何页面。<br/><br/>" +
                "<b>例如:https://www.xxx.com/works?keyword=%s</b>",
            zh_TW: "點擊搜尋結果標籤時開啟的位址,其中的 %s 會被替換為 RJ 號。<br/>" +
                "留空則點擊標籤不會開啟任何頁面。<br/><br/>" +
                "<b>例如:https://www.xxx.com/works?keyword=%s</b>",
            en_US: "The address opened when a search result label is clicked, where %s is replaced with the RJ code.<br/>" +
                "If left empty, clicking the label opens nothing.<br/><br/>" +
                "<b>Example: https://www.xxx.com/works?keyword=%s</b>",
        },

        search_field_method: {
            zh_CN: "请求方法",
            zh_TW: "請求方法",
            en_US: "Request method",
        },

        search_field_method_tooltip: {
            zh_CN: "查询仓库时使用的HTTP方法。",
            zh_TW: "查詢倉庫時使用的 HTTP 方法。",
            en_US: "The HTTP method used when querying the repository.",
        },

        search_field_body_template: {
            zh_CN: "请求体模板",
            zh_TW: "請求主體模板",
            en_US: "Request body template",
        },

        search_field_body_template_tooltip: {
            zh_CN: "使用POST时发送的请求体内容,其中的%s会被替换为搜索关键词(RJ号)。<br/>" +
                "留空则发送空请求体。<br/><br/>" +
                "<b>例如:{\"keyword\":\"%s\"}</b>",
            zh_TW: "使用 POST 時傳送的請求主體內容,其中的 %s 會被替換為搜尋關鍵字(RJ 號)。<br/>" +
                "留空則傳送空的主體。<br/><br/>" +
                "<b>例如:{\"keyword\":\"%s\"}</b>",
            en_US: "The request body sent when POST is used, where %s is replaced with the search keyword (the RJ code).<br/>" +
                "Leave it empty to send an empty body.<br/><br/>" +
                "<b>Example: {\"keyword\":\"%s\"}</b>",
        },

        search_field_content_type: {
            zh_CN: "请求体类型",
            zh_TW: "請求主體類型",
            en_US: "Request body type",
        },

        search_field_content_type_tooltip: {
            zh_CN: "使用POST时请求体的内容类型。",
            zh_TW: "使用 POST 時請求主體的內容類型。",
            en_US: "The content type of the request body when POST is used.",
        },

        search_option_form: {
            zh_CN: "表单(x-www-form-urlencoded)",
            zh_TW: "表單(x-www-form-urlencoded)",
            en_US: "Form (x-www-form-urlencoded)",
        },

        search_field_login_username: {
            zh_CN: "用户名",
            zh_TW: "使用者名稱",
            en_US: "Username",
        },

        search_field_login_username_tooltip: {
            zh_CN: "登录时使用的用户名。",
            zh_TW: "登入時使用的使用者名稱。",
            en_US: "The username used when logging in.",
        },

        search_field_login_password: {
            zh_CN: "密码",
            zh_TW: "密碼",
            en_US: "Password",
        },

        search_field_login_password_tooltip: {
            zh_CN: "登录时使用的密码(不会自动去除首尾空格)。",
            zh_TW: "登入時使用的密碼(不會自動去除首尾空格)。",
            en_US: "The password used when logging in (leading and trailing spaces are not trimmed).",
        },

        search_field_login_url: {
            zh_CN: "登录URL",
            zh_TW: "登入 URL",
            en_US: "Login URL",
        },

        search_field_login_url_tooltip: {
            zh_CN: "登录接口地址。<br/><br/><b>例如:https://www.xxx.com/api/auth/me</b>",
            zh_TW: "登入 API 位址。<br/><br/><b>例如:https://www.xxx.com/api/auth/me</b>",
            en_US: "The address of the login API.<br/><br/><b>Example: https://www.xxx.com/api/auth/me</b>",
        },

        search_field_login_body: {
            zh_CN: "登录请求体",
            zh_TW: "登入請求主體",
            en_US: "Login request body",
        },

        search_field_login_body_tooltip: {
            zh_CN: "登录请求体模板,%username%与%password%会分别替换为用户名与密码。<br/><br/>" +
                "<b>例如:{\"name\":\"%username%\",\"password\":\"%password%\"}</b>",
            zh_TW: "登入請求主體模板,%username% 與 %password% 會分別替換為使用者名稱與密碼。<br/><br/>" +
                "<b>例如:{\"name\":\"%username%\",\"password\":\"%password%\"}</b>",
            en_US: "The login request body template; %username% and %password% are replaced with the username and password respectively.<br/><br/>" +
                "<b>Example: {\"name\":\"%username%\",\"password\":\"%password%\"}</b>",
        },

        search_field_login_content_type: {
            zh_CN: "登录请求体类型",
            zh_TW: "登入請求主體類型",
            en_US: "Login request body type",
        },

        search_field_login_content_type_tooltip: {
            zh_CN: "登录请求体的内容类型。",
            zh_TW: "登入請求主體的內容類型。",
            en_US: "The content type of the login request body.",
        },

        search_field_token_path: {
            zh_CN: "token取值路径",
            zh_TW: "token 取值路徑",
            en_US: "Token path",
        },

        search_field_token_path_tooltip: {
            zh_CN: "登录响应中token所在的路径,用点号分隔。<br/><br/><b>例如:token 或 data.token</b>",
            zh_TW: "登入回應中 token 所在的路徑,以點號分隔。<br/><br/><b>例如:token 或 data.token</b>",
            en_US: "The path of the token in the login response, separated by dots.<br/><br/><b>Example: token or data.token</b>",
        },

        search_field_token_header: {
            zh_CN: "token请求头",
            zh_TW: "token 請求標頭",
            en_US: "Token header",
        },

        search_field_token_header_tooltip: {
            zh_CN: "携带token的请求头,{token}会被替换为token。<br/><br/>" +
                "<b>例如:Authorization: Bearer {token}</b>",
            zh_TW: "攜帶 token 的請求標頭,{token} 會被替換為 token。<br/><br/>" +
                "<b>例如:Authorization: Bearer {token}</b>",
            en_US: "The request header that carries the token; {token} is replaced with the token.<br/><br/>" +
                "<b>Example: Authorization: Bearer {token}</b>",
        },

        search_field_list_path: {
            zh_CN: "作品列表路径",
            zh_TW: "作品列表路徑",
            en_US: "Work list path",
        },

        search_field_list_path_tooltip: {
            zh_CN: "响应JSON中作品列表所在的路径,用点号分隔。<br/><br/><b>例如:works 或 data.list</b>",
            zh_TW: "回應 JSON 中作品列表所在的路徑,以點號分隔。<br/><br/><b>例如:works 或 data.list</b>",
            en_US: "The path of the work list in the JSON response, separated by dots.<br/><br/><b>Example: works or data.list</b>",
        },

        search_field_id_field: {
            zh_CN: "作品标识字段",
            zh_TW: "作品識別欄位",
            en_US: "Work ID field",
        },

        search_field_id_field_tooltip: {
            zh_CN: "列表中代表作品的字段名。<br/><br/><b>例如:id 或 workno</b>",
            zh_TW: "列表中代表作品的欄位名稱。<br/><br/><b>例如:id 或 workno</b>",
            en_US: "The name of the field that identifies a work in the list.<br/><br/><b>Example: id or workno</b>",
        },

        search_field_id_rule: {
            zh_CN: "作品标识转换",
            zh_TW: "作品識別轉換",
            en_US: "Work ID conversion",
        },

        search_field_id_rule_tooltip: {
            zh_CN: "自动判断:返回值是完整作品编号时按编号匹配,是纯数字时按数字匹配。<br/>" +
                "原样作为作品编号:返回值必须是完整的作品编号(如RJ01081353)。<br/>" +
                "按数字匹配:忽略前缀,只按数字部分匹配,适合只返回数字ID的仓库。",
            zh_TW: "自動判斷:回傳值是完整作品編號時按編號比對,是純數字時按數字比對。<br/>" +
                "原樣作為作品編號:回傳值必須是完整的作品編號(如 RJ01081353)。<br/>" +
                "按數字比對:忽略前綴,只按數字部分比對,適合只回傳數字 ID 的倉庫。",
            en_US: "Auto: match by code when the value is a complete work code, and by number when it is a plain number.<br/>" +
                "Use as Work Code: the value must be a complete work code (for example RJ01081353).<br/>" +
                "Match by Number: ignore the prefix and match by the numeric part only, which suits repositories that return numeric IDs only.",
        },

        search_option_id_auto: {
            zh_CN: "自动判断",
            zh_TW: "自動判斷",
            en_US: "Auto",
        },

        search_option_id_raw: {
            zh_CN: "原样作为作品编号",
            zh_TW: "原樣作為作品編號",
            en_US: "Use as Work Code",
        },

        search_option_id_numeric: {
            zh_CN: "按数字匹配",
            zh_TW: "按數字比對",
            en_US: "Match by Number",
        },



        get: function (key, langKey = "_s_lang") {
            return typeof key === "string" ? localizationMap[key][settings[langKey]] : key[settings[langKey]];
        }
    }

    function localize(key) {
        return localizationMap.get(key);
    }

    function localizePopup(key) {
        return localizationMap.get(key, "_s_popup_lang");
    }

    /**
     * 取得本地化文本,并将其中的{xxx}占位符替换为对应的内容
     * @param key {string}
     * @param values {{}} 占位符名称与对应内容,如 {count: 2}
     * @returns {string}
     */
    function localizeFormat(key, values = {}) {
        let text = localize(key);
        for(const name of Object.keys(values)) {
            text = text.replaceAll(`{${name}}`, `${values[name]}`);
        }
        return text;
    }
    //endregion

    //region 常量
    const RJ_REGEX = new RegExp("(R[JE][0-9]{8})|(R[JE][0-9]{6})|([VB]J[0-9]{8})|([VB]J[0-9]{6})", "gi");
    //仅作品编号部分复用RJ_REGEX,"dlsite页面链接"这个定位条件保持不变
    const URL_REGEX = new RegExp(`dlsite.com/.*/product_id/(${RJ_REGEX.source})`, "g");
    const VOICELINK_CLASS = 'voicelink-' + Math.random().toString(36).slice(2);
    const VOICELINK_IGNORED_CLASS = `${VOICELINK_CLASS}_ignored`;
    const RJCODE_ATTRIBUTE = 'rjcode';
    //仓检相关
    const MAX_SEARCH_WAREHOUSES = 5;            //最多可添加的仓库数量
    const SEARCH_REQUEST_LIMIT = 3;             //全局同时进行的请求数量上限
    const SEARCH_CACHE_TTL = 3 * 60 * 1000;     //单个仓库的查询结果缓存有效期(毫秒)
    const SEARCH_CACHE_SIZE = 256;              //单个仓库的查询结果缓存条数
    const LANG_MAP = {
        JPN: localizePopup(localizationMap.language_japanese),
        ENG: localizePopup(localizationMap.language_english),
        CHI_HANS: localizePopup(localizationMap.language_simplified_chinese),
        CHI_HANT: localizePopup(localizationMap.language_traditional_chinese),
        KO_KR: localizePopup(localizationMap.language_korean),
        SPA: localizePopup(localizationMap.language_spanish),
        FRE: localizePopup(localizationMap.language_french),
        RUS: localizePopup(localizationMap.language_russian),
        THA: localizePopup(localizationMap.language_thai),
        GER: localizePopup(localizationMap.language_german),
        FIN: localizePopup(localizationMap.language_finnish),
        POR: localizePopup(localizationMap.language_portuguese),
        VIE: localizePopup(localizationMap.language_vietnamese),
        ITA: localizePopup(localizationMap.language_italian),
        ARA: localizePopup(localizationMap.language_arabic),
        POL: localizePopup(localizationMap.language_polish),
    };
    const LANG_MAP_ABBR = {
        JPN: localizePopup(localizationMap.language_japanese_abbr),
        ENG: localizePopup(localizationMap.language_english_abbr),
        CHI_HANS: localizePopup(localizationMap.language_simplified_chinese_abbr),
        CHI_HANT: localizePopup(localizationMap.language_traditional_chinese_abbr),
        KO_KR: localizePopup(localizationMap.language_korean_abbr),
        SPA: localizePopup(localizationMap.language_spanish_abbr),
        FRE: localizePopup(localizationMap.language_french_abbr),
        THA: localizePopup(localizationMap.language_thai_abbr),
        GER: localizePopup(localizationMap.language_german_abbr),
        POR: localizePopup(localizationMap.language_portuguese_abbr),
        VIE: localizePopup(localizationMap.language_vietnamese_abbr),
        ITA: localizePopup(localizationMap.language_italian_abbr),
    };
    const POPUP_CSS = `
    .${VOICELINK_CLASS}_voicepopup {
        min-width: 630px !important;
        z-index: 2147483646 !important;
        max-width: 80% !important;
        position: fixed !important;
        line-height: normal !important;  /*原1.4em !important;*/
        font-size:1.1em!important;
        margin-bottom: 10px !important;
        box-shadow: 0 0 .125em 0 rgba(0,0,0,.5) !important;
        border-radius: 0.5em !important;
        background-color:#8080C0 !important;
        color:#F6F6F6 !important;
        text-align: left !important;
        padding: 10px !important;
        pointer-events: none !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup[pin][mouse-in] *[copy-text] {
        text-decoration: underline !important;
        cursor: pointer !important;
    }
    .${VOICELINK_CLASS}_voicepopup[pin][mouse-in] *[copy-text]:active {
        opacity: 0.5 !important;
    }
    
    #${VOICELINK_CLASS}_info-container {
        font-size: 1em !important;
    }
    #${VOICELINK_CLASS}_info-container > div {
        margin-bottom: 3px !important;
        font-size: 1em !important;
    }
    #${VOICELINK_CLASS}_info-container > div > a {
        display: inline;
    }
    #${VOICELINK_CLASS}_info-container > div > .info-title {
        margin-right: 5px !important;
        display: inline-block;
    }
    #${VOICELINK_CLASS}_info-container > div > .info-title::after {
        content: ":" !important;
        text-decoration: none !important;
        display: inline-block !important;
    }
    #${VOICELINK_CLASS}_info-container .${VOICELINK_CLASS}_tags {
        margin-top: 12px !important;
        margin-bottom: 0 !important;
        font-size: 0.909091em !important;
    }
    
    .${VOICELINK_CLASS}_loader {
        display: flex !important;
        justify-content: center !important;
        align-items: center !important;
        position: absolute !important;
        top: 50% !important;
        left: 50% !important;
        transform: translate(-50%, -50%) !important;
        width: 100% !important;
        height: 100% !important;
        min-width: 300px !important;
        min-height: 30px !important;
        z-index: -1 !important;
    }
    .${VOICELINK_CLASS}_dot {
        width: 20px !important;
        height: 20px !important;
        margin: 0 8px !important;
        background-color: #fbfbfb !important;
        border-radius: 50% !important;
        animation: ${VOICELINK_CLASS}_scale 1s infinite !important;
    }
    .${VOICELINK_CLASS}_dot:nth-child(1) {
        animation-delay: 0s !important;
    }
    .${VOICELINK_CLASS}_dot:nth-child(2) {
        animation-delay: 0.2s !important;
    }
    .${VOICELINK_CLASS}_dot:nth-child(3) {
        animation-delay: 0.4s !important;
    }
    @keyframes ${VOICELINK_CLASS}_scale {
      0%, 100% {
          transform: scale(1);
      }
      50% {
          transform: scale(1.5);
      }
    }
    
    .${VOICELINK_CLASS}_voicepopup-maniax{
        background-color:#8080C0 !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup-girls{
        background-color:#B33761 !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_left_panel{
        display: flex !important;
        flex-direction: column !important;
        justify-content: space-between !important;
        margin: 0 16px 0 0 !important;
        width: 310px !important;
        flex-shrink: 0 !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_img_container{
        width: 100% !important;
        padding: 3px !important;
        position: relative;
    }

    .${VOICELINK_CLASS}_img_container img {
        width: 100% !important;
        height: auto !important;
    }
    
    #${VOICELINK_CLASS}_hint {
        font-size: 0.8em !important;
        opacity: 0.5 !important;
        max-width: 300px !important;
        margin-top: 5px !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup a {
        text-decoration: none !important;
        color: pink !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_age-18{
        color: hsl(300deg 76% 77%) !important;
    }
    
    .${VOICELINK_CLASS}_voicepopup .${VOICELINK_CLASS}_age-all{
        color: hsl(157deg 82% 52%) !important;
    }

    .${VOICELINK_CLASS}_voice-title {
        font-size: 1.363636em !important;   /*原1.4em*/
        font-weight: bold !important;
        text-align: center !important;
        margin: 5px 10px 0 0 !important;
        display: block !important;
    }

    .${VOICELINK_CLASS}_rjcode {
        text-align: center !important;
        margin: 5px 0 !important;
        font-size: 1.2012987em !important;  /*原1.2em !important;*/
        font-style: italic !important;
        opacity: 0.3 !important;
    }

    .${VOICELINK_CLASS}_error {
        height: 210px !important;
        line-height: 210px !important;
        text-align: center !important;
    }

    .${VOICELINK_CLASS}_discord-dark {
        background-color: #36393f !important;
        color: #dcddde !important;
        font-size: 0.9375rem !important;
    }
    
    .${VOICELINK_CLASS}_work_title:hover #${VOICELINK_CLASS}_copy_btn {
        opacity: 1 !important;
    }
    
    #${VOICELINK_CLASS}_copy_btn {
        background: transparent !important;
        border-color: transparent !important;
        cursor: pointer !important;
        transition: all 0.3s !important;
        opacity: 0 !important;
        font-size: 0.75em !important;
        user-select: none !important;
        position: absolute !important;
    }
    
    #${VOICELINK_CLASS}_copy_btn:hover {
        scale: 1.2 !important;
    }
    
    #${VOICELINK_CLASS}_copy_btn:active {
        scale: 1.1 !important;
    }
    
  `
    const SETTINGS_CSS = `
        #${VOICELINK_CLASS}_settings-container {
            font-family: Arial, sans-serif !important;
            background-color: #f4f4f9 !important;
            margin: auto !important;
            padding: 20px 30px !important;
            line-height: unset !important;
            
            position: fixed !important;
            overflow-y: auto !important;
            overflow-x: hidden !important;
            top: 20px !important;
            bottom: 20px !important;
            left: 50% !important;
            transform: translateX(-50%) !important;
            box-sizing: border-box !important;
            max-width: 800px !important;
            width: 100% !important;
            height: calc(100% - 40px) !important;
            z-index: 2147483647 !important;
            border-radius: 20px !important;
            box-shadow: darkgray 0px 0px 17px 2px !important;
            
            /*scrollbar-width: none;*/
            /*-ms-overflow-style: none;*/
        }
        #${VOICELINK_CLASS}_settings-container::-webkit-scrollbar {
            width: 5px !important;
            height: 5px !important;
        }
        #${VOICELINK_CLASS}_settings-container::-webkit-scrollbar-track {
            background-color: #f4f4f9 !important;
            border-radius: 5px !important;
        }
        #${VOICELINK_CLASS}_settings-container::-webkit-scrollbar-thumb {
            background-color: #888 !important;
            border-radius: 5px !important; 
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_container {
            max-width: 800px !important;
            margin: auto !important;
            background: #fff !important;
            padding: 20px !important;
            border-radius: 10px !important;
            box-shadow: 0 0 10px rgba(0, 0, 0, 0.1) !important;
        }
        #${VOICELINK_CLASS}_settings-container h1 {
            display: block !important;
            text-align: center !important;
            color: #333 !important;
            font-size: 32px !important;
            margin: 21.44px 0 !important;
            font-weight: bold !important;
            line-height: normal !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_section-container {
            margin: 20px 0 !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_section-container h2 {
            display: block !important;
            color: #007bff !important;
            font-size: 24px !important;
            margin: 22px 0 14px 0 !important;
            font-weight: bold !important;
            line-height: normal !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting {
            /*display: flex;*/
            /*align-items: center;*/
            /*justify-content: space-between;*/
            margin: 10px 0 !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting .${VOICELINK_CLASS}_row-title {
            margin: 0 0 0 10px !important;
            color: #555 !important;
            font-size: 18px !important;
            font-weight: normal !important;
            /*flex-grow: 1;*/
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="text"],
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="password"],
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="number"],
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="email"],
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting select {
            width: 100% !important;
            padding: 10px !important;
            border: 1px solid #ddd !important;
            border-radius: 5px !important;
            background: #fafafa !important;
            box-sizing: border-box !important;
            color: #666666FF !important;
            font-size: 13.3333px !important;
            height: unset !important;
            max-height: unset !important;
            max-width: unset !important;
            /*margin-bottom: 10px;*/
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="checkbox"] {
            display: none !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_toggle-container {
            display: flex !important;
            flex-direction: row !important;
            align-items: center !important;
            justify-content: flex-end !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting .${VOICELINK_CLASS}_toggle {
            display: inline-block !important;
            margin: 0 !important;
            width: 60px !important;
            height: 30px !important;
            padding: 0 !important;
            background: #ccc !important;
            border-radius: 15px !important;
            position: relative !important;
            cursor: pointer !important;
            transition: background 0.3s !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_toggle:before {
            content: "" !important;
            display: block !important;
            width: 24px !important;
            height: 24px !important;
            background: #fff !important;
            border-radius: 50% !important;
            position: absolute !important;
            top: 3px !important;
            left: 3px !important;
            transition: transform 0.3s !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="checkbox"]:checked + label {
            background: #007bff !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_setting input[type="checkbox"]:checked + label:before {
            transform: translateX(30px) !important;
        }
        #${VOICELINK_CLASS}_button-close{
            position: absolute !important;
            top: 20px !important;
            right: 20px !important;
            font-size: 24px !important;
            cursor: pointer !important;
            background: rgba(0, 0, 0, 0.05) !important;
            border: none !important;
            width: 42px !important;
            height: 42px !important;
            border-radius: 50% !important;
        }
        #${VOICELINK_CLASS}_button-save,
        #${VOICELINK_CLASS}_button-cancel,
        #${VOICELINK_CLASS}_button-reset{
            display: block !important;
            width: 100% !important;
            padding: 10px !important;
            border: none !important;
            border-radius: 5px !important;
            background: #007bff !important;
            color: #fff !important;
            font-size: 16px !important;
            cursor: pointer !important;
            margin-top: 10px !important;

            transition: background 0.3s, filter 0.3s !important;
        }
        #${VOICELINK_CLASS}_button-reset{
            background: #999 !important;
        }
        #${VOICELINK_CLASS}_button-save:hover,
        #${VOICELINK_CLASS}_button-cancel:hover,
        #${VOICELINK_CLASS}_button-reset:hover{
            filter: brightness(1.3) !important;
        }
        #${VOICELINK_CLASS}_button-save:active,
        #${VOICELINK_CLASS}_button-cancel:active,
        #${VOICELINK_CLASS}_button-reset:active{
            filter: brightness(0.9) !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tooltip {
            position: relative !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tooltip .${VOICELINK_CLASS}_tooltip-text {
            visibility: hidden !important;
            min-width: 200px !important;
            max-width: 100% !important;
            background-color: #555 !important;
            color: #fff !important;
            font-size: 14px !important;
            text-align: center !important;
            border-radius: 5px !important;
            padding: 8px 10px !important;
            position: absolute !important;
            z-index: 1 !important;
            bottom: 125% !important;
            left: 0 !important;
            /*margin-left: -100px;*/
            opacity: 0 !important;
            filter: brightness(1.0) !important;
            transition: opacity 0.3s !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tooltip:hover .${VOICELINK_CLASS}_tooltip-text {
            visibility: visible !important;
            opacity: 1 !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_sortable {
            cursor: move !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_sortable span{
            cursor: default !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_dragging{
            background-color: #1e82ff38 !important;
            user-select: none !important;
            transition: background-color 0.3s !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_sortable .${VOICELINK_CLASS}_setting {
            cursor: move !important;
        }
        #${VOICELINK_CLASS}_settings-container table {
            width: 100% !important;
            margin-bottom: 20px !important;
            border-collapse: collapse !important;
            font-size: unset !important;
        }
        #${VOICELINK_CLASS}_settings-container table,
        #${VOICELINK_CLASS}_settings-container th,
        #${VOICELINK_CLASS}_settings-container td {
            border: 0 solid #ddd !important;
        }
        #${VOICELINK_CLASS}_settings-container th,
        #${VOICELINK_CLASS}_settings-container td {
            border-bottom: 1px dashed rgba(221, 221, 221, 0.64) !important;
            /*border-top: 1px solid #ddd;*/
            padding: 8px 10px !important;
            text-align: left !important;
            vertical-align: middle !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_hidden{
            display: none !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_input-cell{
            text-align: right !important;
            padding-right: 20px !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_indent-1 > td {
            padding: 8px 24px !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_indent-1 .${VOICELINK_CLASS}_input-cell {
            padding: 8px 20px !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tags {
            font-size: 14px;
        }
        .${VOICELINK_CLASS}_tags {
            display: flex !important;
            flex-wrap: wrap !important;
            justify-content: left !important;
            align-items: stretch !important;
        }
        .${VOICELINK_CLASS}_tags > label,
        .${VOICELINK_CLASS}_tags > span {
            border-radius: 5px !important;
            font-size: 1em !important;
            margin-right: 8px !important;
            margin-bottom: 8px !important;
            padding: 5px 8px !important;
            
            display: flex !important;
            justify-content: center !important;
            align-items: center !important;

            transition: color 0.3s, background-color 0.3s !important;
        }
        .${VOICELINK_CLASS}_tags > label.${VOICELINK_CLASS}_tag_tight,
        .${VOICELINK_CLASS}_tags > span.${VOICELINK_CLASS}_tag_tight{
            padding: 2px 7px !important;
        }
        .${VOICELINK_CLASS}_tags > label.${VOICELINK_CLASS}_tag_small,
        .${VOICELINK_CLASS}_tags > span.${VOICELINK_CLASS}_tag_small{
            padding: 2px 7px !important;
            font-size: 0.857143em !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_tag-off{
            background-color: #ffffff !important;
            color: #aaaaaa !important;
        }

        .${VOICELINK_CLASS}_tag-purple{
            background-color: #EED9F2 !important;
            color: #7B1FA2 !important;
        }

        .${VOICELINK_CLASS}_tag-blue{
            background-color: #d9eefc !important;
            color: #4285F4 !important;
        }

        .${VOICELINK_CLASS}_tag-red{
            background-color: #ffd6da !important;
            color: #EA4335 !important;
        }

        .${VOICELINK_CLASS}_tag-yellow{
            background-color: #FFF8E1 !important;
            color: #F57F17 !important;
        }

        .${VOICELINK_CLASS}_tag-green{
            background-color: #dcf5e4 !important;
            color: #34A853 !important;
        }

        .${VOICELINK_CLASS}_tag-teal{
            background-color: #d8eced !important;
            color: #0097A7 !important;
        }

        .${VOICELINK_CLASS}_tag-gray{
            background-color: #E0E0E0 !important;
            color: #424242 !important;
        }

        .${VOICELINK_CLASS}_tag-pink{
            background-color: #ffd9e7 !important;
            color: #f032a7 !important;
        }

        .${VOICELINK_CLASS}_tag-orange{
            background-color: #ffebcc !important;
            color: #f04000 !important;
        }

        .${VOICELINK_CLASS}_tag-darkblue{
            background-color: #d2e7fa !important;
            color: #0D47A1 !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_reset-btn-small {
            position: relative !important;
            display: inline-block !important;
            width: 16px !important;
            height: 16px !important;
            margin-right: 4px !important;
            padding: 0 !important;
            color: transparent !important;
            background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAA6xJREFUeF7tmlFy2jAQhn+ZHIScJOEixYQ++BYJt/BDSU0vUnKSuvcoVkeq3RpXsndlScYAMzwwyLL20+rXarUCN/4RN24/7gDuHnDjBO5LYEoHSF+ytHjPiynHMJkHrLfZVwApgOKwzzdTQZgEQMv4xu7JIEQHYDB+UghRAfQYDyGwKr7kx9hLIRqASzRewY4C4FKNjwKgz3gAZesLIfGhBhVzawzqAQPG9y13DUZIHFDhWBS5+h3kEwxA+jl7lhLfPYxaGX8UJ+xCgAgHIM2WVYK1EHjzAEF1UUqJ4tt7vvPUn+4mGADVeeofggYhTlj58oagAAgQSiGwwS+UeMASFZZS4KkOkXs1wpc3BAdAgmCYUeU9coHXPhhS4m3skogCwBVC85xcQB2cnk0uMRZCNAAECMfDPl+ZjBzQEr2MXMPoqAB6IJSHff44pO6fXrJXy67iLIzRARggkIxv4KgkihR6SXQ/rH6ahycB0IbgImI2TxASG24YPRmAIXcf+t8Cge0FswVQb5Mq1F62YXG9YLYA9DIy6wHLC+YN4E+w9F+MwMkuzRqA8gKTFnCCo9kDqLXgR0c0rUFVV1xnD0AZtN5mSgzPQuXDPifZRmo0tCVN/b8JgDjhkXJkvhYAzS3T37mgCuFVADAJoVcASmiQdI6jgZOVnGVligeoARHJA0wJTs5WwzHGpa3RA4jngqsAYEq/exXBsXuty6xyngkOoN5rZWdQrJibYxC37XqbqUDo7FDkPQ4w7rUT3eh2Aa23WXdy4B3A2JibO6vU9pYTIbnggiSCTQZHLuAcc1MN4rYbEwWqd5EB2GJuasDBNYzS3pYfpLo/G4CPBATFMGobk/hx4xOWB1i2Q3BfSjWwr50tMcqZfbYHaC2wpKGExI6bkXUFYbt6p4a/7feyPKAlhqarqlE3NFQYNi9Ut8aUy5Xue9gANAR78YPzDQ0FQF/RhasYOwFQg+27pvJ1dd2G0ms88eBjguwMYACCFsakwoGSlembfUKRBTn/5x0AYXDOZS2EvpU9o4x32gW6FIkD/Vf1laC0XWW3+lIJTmM9QPN+X1vvqCXQhtGjCTYPb5e+nZ3khgTRVfC8LwGTNwyVtQwZN/C/KpfbjNWVUXEAxYB6r7aWtVD66LQJFmN4WwImo+qokVL1ZXpcC2gC/AwZYQYF0Fils8oPWFYVnoTQmZv2t9ECLZRSokwSfLjW/HC9KwoA7qBitr8DiEn7Et9194BLnJWYY7p5D/gNXP0HX03p5E0AAAAASUVORK5CYII=") !important;
            background-position: center !important;
            background-size: contain !important;
            background-color: transparent !important;
            border-radius: 3px !important;
            border: none !important;
            opacity: 0.5 !important;
        }
        #${VOICELINK_CLASS}_settings-container button.${VOICELINK_CLASS}_reset-btn-small:hover {
            opacity: 1 !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-flat {
            background-color: transparent !important;
            border: none !important;
            color: #aaa !important;
            cursor: pointer !important;
            border-radius: 5px !important;
            padding: 5px 5px !important;
            margin-bottom: 6px !important;
            margin-right: 6px !important;

            display: inline-flex !important;
            align-items: center !important;
            justify-content: center !important;

            transition: background-color 0.3s !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-flat:hover {
            background-color: rgba(0, 0, 0, 0.1) !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-flat span{
            display: inline-block !important;
        }

        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-primary {
            display: inline-block !important;
            padding: 8px 14px !important;
            border: none !important;
            border-radius: 5px !important;
            background: #007bff !important;
            color: #fff !important;
            font-size: 14px !important;
            cursor: pointer !important;

            transition: filter 0.3s !important;
        }
        #${VOICELINK_CLASS}_settings-container .${VOICELINK_CLASS}_button-primary:hover {
            filter: brightness(1.15) !important;
        }

        /*仓库管理弹窗*/
        #${VOICELINK_CLASS}_warehouse-container {
            font-family: Arial, sans-serif !important;
            background-color: #f4f4f9 !important;
            color: #333 !important;
            font-size: 14px !important;
            line-height: 1.5 !important;
            text-align: left !important;

            position: fixed !important;
            overflow-y: auto !important;
            overflow-x: hidden !important;
            top: 40px !important;
            bottom: 40px !important;
            left: 50% !important;
            transform: translateX(-50%) !important;
            box-sizing: border-box !important;
            max-width: 640px !important;
            width: 100% !important;
            padding: 16px 24px !important;
            z-index: 2147483647 !important;
            border-radius: 20px !important;
            box-shadow: darkgray 0px 0px 17px 2px !important;
        }
        #${VOICELINK_CLASS}_warehouse-container h1 {
            display: block !important;
            font-size: 24px !important;
            font-weight: bold !important;
            margin: 8px 0 12px 0 !important;
            color: #333 !important;
            line-height: normal !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-header {
            display: flex !important;
            align-items: center !important;
            justify-content: space-between !important;
            gap: 10px !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-hint {
            color: #666 !important;
            font-size: 13px !important;
            margin: 4px 0 12px 0 !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-list {
            border-top: 1px solid #ddd !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-row {
            display: flex !important;
            align-items: center !important;
            gap: 6px !important;
            padding: 8px 4px !important;
            border-bottom: 1px solid #eee !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-info {
            flex: 1 1 auto !important;
            min-width: 0 !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-name {
            font-weight: bold !important;
            overflow: hidden !important;
            text-overflow: ellipsis !important;
            white-space: nowrap !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-preset {
            color: #888 !important;
            font-size: 12px !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-btn {
            display: inline-block !important;
            padding: 4px 8px !important;
            border: none !important;
            border-radius: 4px !important;
            background: #007bff !important;
            color: #fff !important;
            font-size: 13px !important;
            cursor: pointer !important;
            white-space: nowrap !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-btn:hover {
            filter: brightness(1.15) !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-btn:disabled {
            background: #bbb !important;
            cursor: not-allowed !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-btn-danger {
            background: #d9534f !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-footer {
            display: flex !important;
            justify-content: flex-end !important;
            gap: 8px !important;
            margin-top: 16px !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field {
            margin-bottom: 12px !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field-title {
            display: block !important;
            font-weight: bold !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field-tooltip {
            color: #777 !important;
            font-size: 12px !important;
            margin-bottom: 4px !important;
            white-space: normal !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field-input input,
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field-input select,
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field-input textarea {
            width: 100% !important;
            box-sizing: border-box !important;
            padding: 6px 8px !important;
            border: 1px solid #ccc !important;
            border-radius: 4px !important;
            background: #fff !important;
            color: #333 !important;
            font-size: 14px !important;
            font-family: inherit !important;
            line-height: normal !important;
            margin: 0 !important;
        }
        #${VOICELINK_CLASS}_warehouse-container .${VOICELINK_CLASS}_warehouse-field-input textarea {
            min-height: 70px !important;
            resize: vertical !important;
        }
    `
    //endregion

    //region Utilities

    /**
     * Work promise cache
     * @type {{info:{}, api:{}, api2: {}, circle: {}}}
     */
    const work_promise = {};

    function getAdditionalPopupClasses() {
        const hostname = document.location.hostname;
        switch (hostname) {
            case "boards.4chan.org": return "post reply";
            case "discordapp.com": return `${VOICELINK_CLASS}_discord-dark`;
            default: return null;
        }
    }

    function getOS() {
        const userAgent = navigator.userAgent;
        if (userAgent.indexOf("Windows NT 10.0") !== -1) return "Windows 10";
        if (userAgent.indexOf("Windows NT 6.2") !== -1) return "Windows 8";
        if (userAgent.indexOf("Windows NT 6.1") !== -1) return "Windows 7";
        if (userAgent.indexOf("Windows NT 6.0") !== -1) return "Windows Vista";
        if (userAgent.indexOf("Windows NT 5.1") !== -1) return "Windows XP";
        if (userAgent.indexOf("Windows NT 5.0") !== -1) return "Windows 2000";
        if (userAgent.indexOf("Mac") !== -1) return "Mac";
        if (userAgent.indexOf("X11") !== -1) return "UNIX";
        if (userAgent.indexOf("Linux") !== -1) return "Linux";
        return "Other";
    }

    function getVoiceLinkTarget(target){
        while (target && !target.classList.contains(VOICELINK_CLASS)){
            target = target.parentElement;
        }
        return target;
    }

    function isInDLSite(){
        return document.location.hostname.endsWith("dlsite.com");
    }

    /**
     * Convert to valid file name.
     * @param {String} original
     */
    function convertToValidFileName(original){
        const charMapRegs = {
            "\\/": "/",
            "\\\\": "\",
            "\\:": ":",
            "\\*": "*",
            "\\?": "?",
            "\"": """,
            "\\<": "<",
            "\\>": ">",
            "\\|": "|"
        }

        let fileName = original;
        for (let key in charMapRegs){
            fileName = fileName.replaceAll(new RegExp(key, "g"), charMapRegs[key]);
        }
        return fileName;
    }

    function getXmlHttpRequest() {
        return (typeof GM !== "undefined" && GM !== null ? GM.xmlHttpRequest : GM_xmlhttpRequest);
    }

    /**
     * 全局请求队列:限制同时进行的请求数量,避免多仓库查询时并发过高。
     * 高优先级请求(如弹框自身的信息请求)会优先于低优先级请求(如仓库批量查询)执行
     */
    const RequestQueue = {
        _limit: SEARCH_REQUEST_LIMIT,
        _active: 0,
        _waiting: [],
        _waitingHigh: [],

        get limit() {
            return this._limit;
        },

        set limit(value) {
            if(typeof value === "number" && value > 0) this._limit = Math.floor(value);
        },

        /**
         * 将请求排队执行
         * @param task {function} 返回Promise的任务
         * @param priority {string} "high"(默认)或"low"
         * @returns {Promise<*>}
         */
        enqueue(task, priority = "high") {
            return new Promise((resolve, reject) => {
                const item = {task: task, resolve: resolve, reject: reject};
                if(priority === "low") {
                    this._waiting.push(item);
                }else{
                    this._waitingHigh.push(item);
                }
                this.pump();
            });
        },

        pump() {
            while(this._active < this._limit && (this._waitingHigh.length > 0 || this._waiting.length > 0)) {
                const item = this._waitingHigh.length > 0 ? this._waitingHigh.shift() : this._waiting.shift();
                this._active++;
                (async () => {
                    try{
                        item.resolve(await item.task());
                    }catch (e) {
                        item.reject(e);
                    }finally {
                        this._active--;
                        this.pump();
                    }
                })();
            }
        }
    };

    /**
     * 发送GET请求
     * @param url {string}
     * @param anonymous {boolean} 是否匿名请求(不携带cookie)
     * @param cacheAge {number} 缓存时间(秒)
     * @param customHeaders {{}} 额外的请求头
     * @param priority {string} 请求队列优先级,"high"或"low"
     */
    function getHttpAsync (url, anonymous = false, cacheAge = 0, customHeaders = {}, priority = "high"){
        let headers = {...customHeaders};
        headers["Accept"] = "text/xml";
        headers["User-Agent"] = "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:67.0)";
        headers["Cache-Control"] = cacheAge <= 0 ? "no-cache" : "max-age=" + cacheAge;
        return RequestQueue.enqueue(() => new Promise((resolve, reject) => {
            getXmlHttpRequest()({
                method: "GET",
                url,
                headers: headers,
                onload: resolve,
                onerror: resp => {
                    reject(resp);
                },
                anonymous: anonymous
            });
        }), priority);
    }

    /**
     * 发送POST请求
     * @param url {string}
     * @param body {string} 请求体内容
     * @param contentType {string} 请求体的内容类型
     * @param customHeaders {{}} 额外的请求头
     * @param priority {string} 请求队列优先级,"high"或"low"
     */
    function postAsync (url, body, contentType = "application/json", customHeaders = {}, priority = "high"){
        let headers = {...customHeaders};
        headers["Content-Type"] = contentType;
        headers["Accept"] = contentType.includes("json") ? "application/json" : "*/*";
        return RequestQueue.enqueue(() => new Promise((resolve, reject) => {
            getXmlHttpRequest()({
                method: "POST",
                url,
                data: body,
                headers: headers,
                onload: resolve,
                onerror: resp => {
                    reject(resp);
                },
                anonymous: false
            });
        }), priority);
    }

    /**
     * 发送JSON格式的POST请求
     * @param url {string}
     * @param data {{}} 请求体内容,会被序列化为JSON字符串
     * @param customHeaders {{}} 额外的请求头
     * @param priority {string} 请求队列优先级,"high"或"low"
     */
    function postJsonAsync (url, data, customHeaders = {}, priority = "high"){
        return postAsync(url, JSON.stringify(data), "application/json", customHeaders, priority);
    }

    /**
     * 判断响应是否为验证失效(需要重新登录)
     * @param resp {{}}
     * @returns {boolean}
     */
    function isAuthFailure(resp) {
        if (!resp) return false;
        return resp.status === 401 || resp.status === 403;
    }

    /**
     * 从JWT中读取过期时间戳(毫秒),无法读取时返回0(表示有效期未知)
     * @param token {string}
     * @returns {number}
     */
    function parseJwtExpireTime(token) {
        try {
            const payload = token.split(".")[1];
            if (!payload) return 0;

            let base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
            while (base64.length % 4 !== 0) base64 += "=";

            const data = JSON.parse(new TextDecoder().decode(
                Uint8Array.from(atob(base64), c => c.charCodeAt(0))));

            //提前60秒视为过期,避免在临界状态下使用即将失效的token
            return typeof data.exp === "number" ? data.exp * 1000 - 60 * 1000 : 0;
        } catch (e) {
            return 0;
        }
    }

    /**
     * 判断文本本身是否为一个完整的作品编号。
     * 编号格式统一由RJ_REGEX判断,不在此处重复维护格式规则
     * @param text {string}
     * @returns {boolean}
     */
    function isRjCode(text) {
        if(typeof text !== "string" || text === "") return false;
        //使用String.match以避免与其它地方的RJ_REGEX.exec共用lastIndex
        const matches = text.match(RJ_REGEX);
        return matches !== null && matches.length === 1 && matches[0] === text;
    }

    /**
     * 取出作品编号或数字ID中的数字部分,用于把只有数字的标识关联到作品编号上
     * @param value {string|number}
     * @returns {number|null} 无法取出时返回null
     */
    function getWorkNumber(value) {
        if(value === undefined || value === null) return null;

        let digits = "";
        for(const char of `${value}`.trim()) {
            if(char >= "0" && char <= "9") digits += char;
        }
        if(digits === "") return null;

        const number = Number(digits);
        return Number.isFinite(number) ? number : null;
    }

    /**
     * 在关联作品表中查找仓库返回的作品标识对应的作品编号。
     * rule为"raw"时标识本身必须是完整的作品编号;为"numeric"时按数字匹配;
     * 为"auto"时优先按完整编号匹配,标识为数字时按数字匹配
     * @param linkages {{}} 关联作品表(作品编号 → 作品信息)
     * @param raw {*} 仓库返回的作品标识
     * @param rule {string} auto / raw / numeric
     * @returns {string} 关联表中的作品编号,找不到时返回空字符串
     */
    function findLinkedWorkno(linkages, raw, rule) {
        if(raw === undefined || raw === null) return "";
        const text = `${raw}`.trim().toUpperCase();

        //原样模式:标识本身必须是完整的作品编号,且存在于关联表中
        if(rule === "raw") {
            return isRjCode(text) && linkages[text] !== undefined ? text : "";
        }
        //自动模式:优先按完整编号匹配
        if(rule !== "numeric" && isRjCode(text)) {
            if(linkages[text] !== undefined) return text;
            return "";
        }

        //数字模式:按数字部分与关联表中的作品编号匹配
        const number = getWorkNumber(text);
        if(number === null) return "";

        for(const workno of Object.keys(linkages)) {
            if(getWorkNumber(workno) === number) return workno;
        }
        return "";
    }

    /**
     * 将设置中的自定义请求头文本解析为请求头对象
     * @param text {string} 每行一个key:value
     * @returns {{}}
     */
    function parseHeaderText(text) {
        let headers = {};
        if(typeof text !== "string") return headers;
        for(const line of text.split("\n")) {
            const pos = line.indexOf(":");
            //没有冒号的行不是合法的请求头,直接跳过
            if(pos < 0) continue;
            const key = line.substring(0, pos).trim();
            if(key === "") continue;
            headers[key] = line.substring(pos + 1).trim();
        }
        return headers;
    }

    /**
     * 按点分路径从对象中取值,如"data.list"
     * @param data {{}}
     * @param path {string}
     * @returns {*} 找不到时返回undefined
     */
    function getByPath(data, path) {
        if(!path) return undefined;
        let current = data;
        for(const key of `${path}`.split(".")) {
            if(key === "") continue;
            if(current === undefined || current === null) return undefined;
            current = current[key];
        }
        return current;
    }

    /**
     * 将文本转化成 'SHA-256|原文本长度' 格式的哈希字符串
     * @param text {string} 待转换的文本
     * @returns {Promise<string>}
     */
    async function hash(text) {
        return sha256(text);
    }

    /**
     * 通过blob加载图片,防止被广告拦截插件误杀
     * @param img 需要加载图片的img元素
     * @param url 图片网址
     * @returns {Promise<void>}
     */
    async function loadImageSafe(img, url) {
        try {
            const response = await GM.xmlHttpRequest({
                method: "GET",
                url: url,
                responseType: "blob" // 声明返回 Blob 数据
            });

            if (response.status === 200) {
                // 生成本地 blob: 协议临时链接
                const blobUrl = URL.createObjectURL(response.response);
                img.src = blobUrl;

                // 渲染完成后释放内存
                img.onload = () => URL.revokeObjectURL(blobUrl);
            } else {
                throw new Error(`图片获取失败,状态码: ${response.status}`);
            }
        } catch (err) {
            console.error("GM.xmlHttpRequest 请求异常:", err);
        }
    }

    //endregion

    //region DLSite页面覆盖

    function setUserSelectTitle(){
        // Make title selectable
        const hostname = document.location.hostname;
        if(!hostname.endsWith("dlsite.com")){
            return;
        }
        const rjList = document.URL.match(RJ_REGEX)
        const rj = rjList[rjList.length - 1]

        const title = document.getElementById("work_name");
        if(!title){
            return;
        }
        let titleStr = title.innerText;
        let titleHtml = title.innerHTML;

        const button = document.createElement("button");
        button.id = `${VOICELINK_CLASS}_copy_btn`;
        button.innerText = "📃";
        button.addEventListener("mouseenter", function(){
            button.innerText = "📃 复制为有效文件名";
        });
        button.addEventListener("mouseleave", function(){
            button.innerText = "📃";
        });
        button.addEventListener("click", function(){
            const fileName = convertToValidFileName(titleStr);
            // const promise = navigator.clipboard.writeText(fileName);
            const promise = GM_setClipboard(fileName, "text");
            promise?.then(() => {
                button.innerText = "✔ 复制成功";
            });
            promise?.catch(e => {
                window.prompt("复制失败,请手动复制", fileName);
                button.innerText = "📃";
            });
        });

        title.style.setProperty("user-select", "text", "important");  //userSelect = "text !important";
        title.classList.add(`${VOICELINK_CLASS}_work_title`);

        if(settings._s_show_translated_title_in_dl){
            //将Title替换成大家翻对应的语言翻译版本
            WorkPromise.getTranslationInfo(rj).then(info => {
                if(info.is_original) {
                    return null;
                }
                else{
                    return WorkPromise.getWorkTitle(rj);
                }
            }).then(t => {
                if(!t){
                    if(settings._s_copy_as_filename_btn) title.appendChild(button);
                    return;
                }
                compatibilityCheck(title, titleHtml);
                titleStr = t
                title.innerText = t
                if(settings._s_copy_as_filename_btn) title.appendChild(button);
            })
        }else{
            if(settings._s_copy_as_filename_btn) title.appendChild(button);
        }
    }

    function compatibilityCheck(titleElement, titleHtml){
        if(!settings._s_show_compatibility_warning) return;

        if(titleElement.innerHTML.trim() === titleHtml.trim()){
            return;
        }

        //其它脚本修改了标题内部,进行警告
        window.alert("警告:\n" +
            "VoiceLinks检测到DL作品标题元素发生变化,该变化可能是脚本与其它插件冲突导致的。\n" +
            "可以关闭本脚本中的 “在DLSite显示对应语言的翻译标题” 设置项,以尝试解决冲突。(也可根据情况酌情关闭 “在DL作品标题旁添加复制为文件名按钮” 选项)\n\n" +
            "本脚本的设置方法:点击Tampermonkey等扩展程序的按钮,在弹出的脚本列表中找到当前脚本,点击下方的Settings按钮即可打开设置页面。\n\n" +
            "注意:如果不想看到该警告,可以同时关闭“显示兼容性警告”设置项。")
    }

    //endregion

    //region 解析器

    //站点在极短时间内反复重写同一段文本时的抖动保护参数
    const RUN_REBUILD_LIMIT = 12;       //统计窗口内允许重建的次数
    const RUN_REBUILD_WINDOW = 500;     //统计窗口长度(毫秒)
    const RUN_REBUILD_PAUSE = 3000;     //触发保护后暂停解析该节点的时间(毫秒)

    const Parser = {
        /***
         * 已解析片段的注册表。
         * key:站点原本持有的文本节点(拆分后作为前缀节点保留在文档中)
         * value:{ expected: 脚本写入该节点的文本, nodes: 本次拆分注入的全部节点 }
         * 站点就地改写文本时据此回滚过期的链接节点,并按新内容重新解析。
         ***/
        _runs: new WeakMap(),

        /***
         * 抖动保护状态:key 为文本节点,value 为允许再次解析的时间戳
         ***/
        _runPauses: new WeakMap(),

        /***
         * 抖动保护计数:key 为文本节点,value 为 { count, windowStart }
         ***/
        _runRebuilds: new WeakMap(),

        walkNodes: function (elem) {
            function acceptNode(node) {
                if (
                    node.nodeName === "SCRIPT" ||
                    (node.parentElement && node.parentElement?.nodeName === "SCRIPT")
                ) {
                    return NodeFilter.FILTER_REJECT;
                }

                if (node.parentElement?.isContentEditable) {
                    return NodeFilter.FILTER_SKIP;
                }

                if (settings._s_parse_url && node.nodeName === "A") {
                    if (
                        !settings._s_parse_url_in_dl &&
                        document.location.hostname.endsWith("dlsite.com")
                    ) {
                        return NodeFilter.FILTER_SKIP;
                    }

                    let href = node.href;
                    if (
                        href.match(URL_REGEX) &&
                        !node.classList.contains(VOICELINK_IGNORED_CLASS)
                    ) {
                        return NodeFilter.FILTER_ACCEPT;
                    }
                }

                if (node.nodeName !== "#text") {
                    return NodeFilter.FILTER_SKIP;
                }

                if (
                    node.parentElement?.classList.contains(VOICELINK_IGNORED_CLASS) ||
                    node.parentElement?.hasAttribute(RJCODE_ATTRIBUTE)
                ) {
                    return NodeFilter.FILTER_SKIP;
                }

                if (node.parentElement?.classList.contains(VOICELINK_CLASS)) {
                    return NodeFilter.FILTER_ACCEPT;
                }

                if (node.nodeValue?.match(RJ_REGEX)) {
                    return NodeFilter.FILTER_ACCEPT;
                }

                return NodeFilter.FILTER_SKIP;
            }

            function processNode(node) {
                if (node.parentElement?.nodeName === "TEXTAREA") {
                    return;
                }

                if (node.parentElement?.classList.contains(VOICELINK_CLASS)) {
                    Parser.rebindEvents(node.parentElement);
                } else if (node.nodeName === "A") {
                    Parser.linkifyURL(node);
                } else {
                    Parser.linkify(node);
                }
            }

            // ① 先手动检查 root
            const rootResult = acceptNode(elem);

            if (rootResult === NodeFilter.FILTER_REJECT) {
                // root 被拒绝,连子树也不遍历
                return;
            }

            if (rootResult === NodeFilter.FILTER_ACCEPT) {
                processNode(elem);
            }

            // ② 再遍历 root 的后代
            const walker = document.createTreeWalker(
                elem,
                NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT,
                {
                    acceptNode
                }
            );

            while (walker.nextNode()) {
                processNode(walker.currentNode);
            }
        },

        wrapPlaceholder: function (content) {
            let e;
            e = document.createElement("span");
            e.classList = VOICELINK_CLASS;
            e.innerText = content;
            e.classList.add(VOICELINK_IGNORED_CLASS);
            e.setAttribute(RJCODE_ATTRIBUTE, "");
            return e;
        },

        wrapRJCode: function (rjCode) {
            let e;
            e = document.createElement("a");
            e.classList = VOICELINK_CLASS;
            e.href = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode.toUpperCase()}.html`
            e.innerText = rjCode;
            e.target = "_blank";
            e.rel = "noreferrer";
            e.classList.add(VOICELINK_IGNORED_CLASS);
            e.style.setProperty("display", "inline", "important");  //display = "inline !important";

            e.setAttribute(RJCODE_ATTRIBUTE, rjCode.toUpperCase());
            e.setAttribute("voicelink-linkified", "true");
            e.addEventListener("mouseover", Popup.over);
            e.addEventListener("mouseout", Popup.out);
            e.addEventListener("mousemove", Popup.move);
            e.addEventListener("keydown", Popup.keydown);
            //e.addEventListener("keyup", Popup.keyup);
            return e;
        },

        calculateCoverage: function(text){
            const matches = text.match(RJ_REGEX);
            if (!matches) return 0;
            //覆盖大小 = 所有匹配项的长度总和
            const coverSize = matches.reduce((total, current) => total + current.length, 0);
            return (coverSize / text.length) * 100;
        },

        /***
         * 处理直链
         * @param {Node} node
         ***/
        linkifyURL: function(node) {
            const e = node;
            const href = e.href;
            const rjs = href.match(RJ_REGEX);
            const rj = rjs[rjs.length - 1];
            if(!rj) return;

            // alert(`解析链接:${e.nodeValue}`)

            e.classList.add(VOICELINK_CLASS);
            e.setAttribute(RJCODE_ATTRIBUTE, rj.toUpperCase());
            e.addEventListener("mouseover", Popup.over);
            e.addEventListener("mouseout", Popup.out);
            e.addEventListener("mousemove", Popup.move);
            e.addEventListener("keydown", Popup.keydown);
            //e.addEventListener("keyup", Popup.keyup);
        },

        linkify: function (textNode) {
            //节点已脱离文档(例如站点在同一批次里先插入又移除)时无法确定插入位置,直接跳过
            if (!textNode.parentElement) return;

            //该节点当前已有注入的链接节点时不再重复处理(站点改写后由重建流程负责)
            const run = this._runs.get(textNode);
            if (run && run.nodes.length > 0) return;

            //抖动保护期间不再解析该节点,避免脚本与站点互相触发
            if (this.isRunPaused(textNode)) return;

            const nodeOriginalText = textNode.nodeValue;
            const matches = [];

            let insert = settings._s_url_insert_mode;
            let tagA = textNode.parentElement.closest("a");
            let tagB = textNode.parentElement.closest("button");
            let tag = tagA ? tagA : tagB;
            if((!tagA && !tagB) || insert.trim() !== "none" && this.calculateCoverage(tag.innerText) < 71){
                insert = "none";
            }

            let match;
            while (match = RJ_REGEX.exec(nodeOriginalText)) {
                matches.push({
                    index: match.index,
                    value: match[0],
                });
            }
            if(matches.length === 0) return;

            // alert(`解析文本:${textNode.nodeValue}`)

            // Keep text in text node until first RJ code
            textNode.nodeValue = nodeOriginalText.substring(0, matches[0].index);
            if(insert.startsWith("prefix")){
                //加前缀
                textNode.nodeValue = `${settings._s_url_insert_text}${textNode.nodeValue}`
            }

            // Insert rest of text while linkifying RJ codes
            //记录本次拆分注入的全部节点,站点就地改写文本时据此回滚
            const injectedNodes = [];
            const insertInjected = function (node, referenceNode) {
                textNode.parentNode.insertBefore(node, referenceNode);
                injectedNodes.push(node);
            };

            let prevNode = null;
            for (let i = 0; i < matches.length; ++i) {
                // Insert linkified RJ code
                let code = matches[i].value
                let rjLinkNode = Parser.wrapRJCode(code);
                //保证后续游走时忽略当前节点
                if(insert.startsWith("before_rj")){
                    //用导向文本替代RJ号链接,RJ号保留到后面的文本里不变
                    rjLinkNode.innerText = settings._s_url_insert_text;
                    insertInjected(
                        rjLinkNode,
                        prevNode ? prevNode.nextSibling : textNode.nextSibling,
                    );
                    prevNode = rjLinkNode;
                    rjLinkNode = Parser.wrapPlaceholder(code);
                }
                insertInjected(
                    rjLinkNode,
                    prevNode ? prevNode.nextSibling : textNode.nextSibling,
                );

                // Insert text after if there is any
                //找到当前RJ和下一个RJ之间的字符串
                let nextRJ = undefined;
                if (i < matches.length - 1) {
                    nextRJ = matches[i + 1].index;
                }
                let substring = nodeOriginalText.substring(matches[i].index + matches[i].value.length, nextRJ);

                if (substring) {
                    const subtextNode = document.createTextNode(substring);
                    insertInjected(
                        subtextNode,
                        rjLinkNode.nextElementSibling,
                    );
                    prevNode = subtextNode;
                }
                else {
                    prevNode = rjLinkNode;
                }
            }

            //注册本次拆分结果,站点就地改写文本时按新内容回滚重建
            this.registerRun(textNode, injectedNodes);
        },

        /***
         * 注册一次拆分结果,站点就地改写文本时据此回滚重建。
         * nodes 为空表示该文本节点仍在脚本托管下,但当前没有注入的节点。
         * @param {Text} textNode 站点原本持有的文本节点
         * @param {Node[]} injectedNodes 本次拆分注入的全部节点
         ***/
        registerRun: function (textNode, injectedNodes) {
            this._runs.set(textNode, {
                expected: textNode.nodeValue,
                nodes: injectedNodes,
            });
        },

        /***
         * 判断一条文本变更是否让已注册的片段过期。
         * oldValue 与脚本写入的内容一致,说明这次改写来自站点而不是脚本自身;
         * 改写前后内容相同时无需处理(否则 prefix 模式下空前缀的空写会造成误判)。
         * @param {MutationRecord} mutation
         * @returns {boolean}
         ***/
        isRunInvalidated: function (mutation) {
            const run = this._runs.get(mutation.target);
            return !!run &&
                mutation.oldValue === run.expected &&
                mutation.target.nodeValue !== mutation.oldValue;
        },

        /***
         * 回滚一次拆分:移除脚本注入的节点,只留下站点写入的文本
         * @param {Text} textNode
         * @returns {boolean} 是否存在已注册的片段
         ***/
        rollbackRun: function (textNode) {
            const run = this._runs.get(textNode);
            if (!run) return false;

            for (const node of run.nodes) {
                if (node.parentNode) {
                    node.parentNode.removeChild(node);
                }
            }
            run.nodes.length = 0;
            //保留注册项:该节点仍由脚本托管,站点再次改写时需要重新解析
            run.expected = textNode.nodeValue;
            return true;
        },

        /***
         * 站点就地改写了已解析的文本节点:先回滚过期的链接,再按新内容重新解析
         * @param {Text} textNode
         ***/
        rebuildRun: function (textNode) {
            this.rollbackRun(textNode);

            if (this.checkRunThrottle(textNode)) return;

            this.linkify(textNode);
        },

        /***
         * 抖动保护:统计同一节点的重建频率,过于频繁时暂停一段时间再自动恢复,
         * 避免脚本注入节点与站点重新渲染互相触发导致死循环。
         * 注意:调用会更新统计窗口,每次重建只应调用一次。
         * @param {Text} textNode
         * @returns {boolean} 当前是否处于暂停状态
         ***/
        checkRunThrottle: function (textNode) {
            const now = Date.now();
            if (this.isRunPaused(textNode)) return true;

            const record = this._runRebuilds.get(textNode);
            if (!record || now - record.windowStart > RUN_REBUILD_WINDOW) {
                this._runRebuilds.set(textNode, { count: 1, windowStart: now });
                return false;
            }

            ++record.count;
            if (record.count > RUN_REBUILD_LIMIT) {
                this._runRebuilds.delete(textNode);
                this._runPauses.set(textNode, now + RUN_REBUILD_PAUSE);
                console.warn("VoiceLinks: 检测到文本被高频重写,已暂停解析该节点一段时间。");
                return true;
            }

            return false;
        },

        /***
         * 该节点当前是否处于抖动保护暂停期
         * @param {Text} textNode
         * @returns {boolean}
         ***/
        isRunPaused: function (textNode) {
            return (this._runPauses.get(textNode) || 0) > Date.now();
        },

        rebindEvents: function (elem) {
            if (elem.nodeName === "A") {
                elem.addEventListener("mouseover", Popup.over);
                elem.addEventListener("mouseout", Popup.out);
                elem.addEventListener("mousemove", Popup.move);
                elem.addEventListener("keydown", Popup.keydown);
                //elem.addEventListener("keyup", Popup.keyup);
            }
            else {
                const voicelinks = elem.querySelectorAll("." + VOICELINK_CLASS);
                for (let i = 0, j = voicelinks.length; i < j; i++) {
                    const voicelink = voicelinks[i];
                    voicelink.addEventListener("mouseover", Popup.over);
                    voicelink.addEventListener("mouseout", Popup.out);
                    voicelink.addEventListener("mousemove", Popup.move);
                    voicelink.addEventListener("keydown", Popup.keydown);
                    //voicelink.addEventListener("keyup", Popup.keyup);
                }
            }
        },

    }

    const DateParser = {
        parseDateStr: function(dateStr, lang){
            dateStr = dateStr.trim().replace(/ /g, "");
            lang = lang.trim().toLowerCase().replace(/_/g, "-");
            let nums = this.parseNumbers(dateStr);
            if(!nums || nums.length < 3 && lang !== "en-us" || nums.length < 2 && lang === "en-us"){
                //数字不够,无法解析
                return null;
            }

            let parsers = [
                this.parseAsiaDateStr,
                this.parseEnglishDateStr,
                this.parseEuropeanDateStr,
                this.parseSpanishDateStr
            ]
            let date = null;
            for (let i = 0; i < parsers.length; i++){
                date = parsers[i](dateStr, nums, lang);
                if(date){
                    break;
                }
            }

            return date;
        },
        parseNumbers: function (dateStr){
            let nums = dateStr.match(/\d+/g);
            if(!nums) return null;

            for (let i = 0; i < nums.length; i++) {
                nums[i] = Number(nums[i]);
            }
            return nums;
        },
        parseAsiaDateStr: function(dateStr, nums, lang){
            //2024年10月05日
            //2024년 10월 05일(已去除空格)
            if (!dateStr.match(/\d{4}年\d{1,2}月\d{1,2}日/)
                && !dateStr.match(/\d{4}년\d{1,2}월\d{1,2}일/)) {
                return null;
            }
            return new Date(nums[0], nums[1] - 1, nums[2]);
        },
        parseEnglishDateStr: function(dateStr, nums, lang){
            //Oct/05/2024
            if(!dateStr.match(/[a-zA-Z]{3}\/\d{1,2}\/\d{4}/)){
                return null;
            }
            const monthMap = {
                "Jan": 0, "Feb": 1, "Mar": 2,
                "Apr": 3, "May": 4, "Jun": 5,
                "Jul": 6, "Aug": 7, "Sep": 8,
                "Oct": 9, "Nov": 10, "Dec": 11
            }
            let monthStr = dateStr.substring(0, dateStr.indexOf("/")).toLowerCase();
            monthStr = monthStr[0].toUpperCase() + monthStr.substring(1);
            return new Date(nums[1], monthMap[monthStr], nums[0])
        },
        parseSpanishDateStr: function (dateStr, nums, lang) {
            //10/05/2024
            if(lang !== "es-es" || !dateStr.match(/\d{1,2}\/\d{1,2}\/\d{4}/)){
                return null;
            }
            return new Date(nums[2], nums[0] - 1, nums[1]);
        },
        parseEuropeanDateStr: function (dateStr, nums, lang) {
            //05/10/2024
            if(lang === "es-es" || !dateStr.match(/\d{1,2}\/\d{1,2}\/\d{4}/)){
                return null;
            }
            return new Date(nums[2], nums[1] - 1, nums[0]);
        },
        /***
         获得带倒计时的文本HTML
         @param date {Date}
         ***/
        getCountDownDateElement: function(date){
            if(!date) return "";

            const today = new Date();
            today.setHours(0);
            today.setMinutes(0);
            today.setSeconds(0);
            today.setMilliseconds(0);
            date.setHours(0);
            date.setMinutes(0);
            date.setSeconds(0);
            date.setMilliseconds(0);

            if(date.getTime() < today.getTime()) return "";
            let days = (date.getTime() - today.getTime()) / (1000 * 60 * 60 * 24);
            let element = document.createElement("span");
            element.innerText = `(Coming in ${days} day${(days > 1 ? "s" : "")})`;
            element.style.setProperty("color", "#ffeb3b", "important");
            element.style.setProperty("font-style", "italic", "important");
            return element;
            //return `<span style="color:#ffeb3b !important; font-size: 16px !important; font-style: italic !important; margin-left: 16px !important"></span>`
        },
    }

    //endregion

    //region 搜索支持

    //获取token后无条件信任的时间(毫秒),避免因时间偏差等原因反复重新登录
    const TOKEN_TRUST_INTERVAL = 60 * 1000;
    //登录失败后的冷却时间(毫秒),避免登录接口被反复请求
    const LOGIN_RETRY_COOLDOWN = 60 * 1000;

    /**
     * 将用户填写的地址规范化为baseurl。
     * 既可以填写纯baseurl(如https://www.xxx.com),也可以粘贴完整地址
     * (如https://www.xxx.com/api/search?keyword=%s、https://www.xxx.com/works?keyword=%s),
     * 后者会被截取为对应的baseurl
     * @param url {string}
     * @returns {string}
     */
    function normalizeBaseUrl(url) {
        let base = `${url === undefined || url === null ? "" : url}`.trim();
        if(base === "") return "";

        //截取粘贴完整地址时残留的接口路径
        const apiPos = base.lastIndexOf("/api/");
        if(apiPos >= 0) base = base.substring(0, apiPos);
        //"/works"仅在作为独立路径段时截取,避免误伤"https://www.xxx.com/workshop"这类地址
        const worksPos = base.search(/\/works(?=$|[?#/])/);
        if(worksPos >= 0) base = base.substring(0, worksPos);

        //去掉查询串、锚点与结尾多余的"/"
        const cutPos = base.search(/[?#]/);
        if(cutPos >= 0) base = base.substring(0, cutPos);
        base = base.replace(/\/+$/, "");

        //以API目录结尾的地址(如https://www.xxx.com/api)同样视作根地址
        if(base.endsWith("/api")) base = base.substring(0, base.length - "/api".length);

        return base;
    }

    //账号密码登录中使用的占位符
    const LOGIN_USERNAME_PLACEHOLDER = "%username%";
    const LOGIN_PASSWORD_PLACEHOLDER = "%password%";
    const LOGIN_TOKEN_PLACEHOLDER = "{token}";
    //请求体可选的两种内容类型
    const CONTENT_TYPE_JSON = "application/json";
    const CONTENT_TYPE_FORM = "application/x-www-form-urlencoded";
    //作品标识转换规则的可用值
    const ID_RULES = ["auto", "raw", "numeric"];

    /**
     * 仓库预设:定义不同类型仓库的配置项、请求方式、响应解析与认证方式。
     * fields中的字段会被仓库管理界面用于动态生成编辑表单
     */
    const SEARCH_PRESETS = {
        kikoeru: {
            id: "kikoeru",
            title: "kikoeru",
            //预设与字段的名称、说明使用getter,切换界面语言后无需重新加载页面
            get tooltip() { return localize(localizationMap.search_preset_kikoeru_tooltip); },
            defaults: {
                showBase: "",
                searchBase: "",
                authType: "login",
                username: "",
                password: "",
                headers: ""
            },
            fields: [
                {
                    key: "showBase", type: "input", required: true,
                    get title() { return localize(localizationMap.search_field_repo_url); },
                    get tooltip() { return localize(localizationMap.search_field_repo_url_tooltip); }
                },
                {
                    key: "searchBase", type: "input", required: false,
                    get title() { return localize(localizationMap.search_field_api_url); },
                    get tooltip() { return localize(localizationMap.search_field_api_url_tooltip); }
                },
                {
                    key: "authType", type: "dropdown", required: true,
                    options: [
                        {value: "login", get title() { return localize(localizationMap.search_auth_login); }},
                        {value: "headers", get title() { return localize(localizationMap.search_auth_headers); }},
                        {value: "none", get title() { return localize(localizationMap.search_auth_none); }}
                    ],
                    get title() { return localize(localizationMap.search_field_auth); },
                    get tooltip() { return localize(localizationMap.search_field_auth_tooltip_kikoeru); }
                },
                {
                    key: "username", type: "input", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_username); },
                    get tooltip() { return localize(localizationMap.search_field_username_tooltip); }
                },
                {
                    key: "password", type: "password", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_password); },
                    get tooltip() { return localize(localizationMap.search_field_password_tooltip); }
                },
                {
                    key: "headers", type: "textarea", required: false,
                    get title() { return localize(localizationMap.search_field_headers); },
                    get tooltip() { return localize(localizationMap.search_field_headers_tooltip_kikoeru); }
                }
            ],

            getAuthType(profile) {
                const authType = profile.config.authType;
                return authType === "login" || authType === "headers" || authType === "none" ? authType : "login";
            },

            resolve(profile) {
                const config = profile.config;
                //地址可能被填写为完整url,此处统一截取为baseurl
                let apiBase = normalizeBaseUrl(config.searchBase);
                let frontBase = normalizeBaseUrl(config.showBase);
                //任意一个留空则使用另一个
                if(apiBase === "") apiBase = frontBase;
                if(frontBase === "") frontBase = apiBase;

                profile.apiBaseUrl = apiBase;
                profile.showBaseUrl = frontBase;
                //http://192.168.196.226:8088/api/search?page=1&sort=desc&order=release&nsfw=0&lyric=&seed=26&isAdvance=0&keyword=%s
                profile.searchUrlTemplate = apiBase === "" ? "" :
                    `${apiBase}/api/search?page=1&sort=desc&order=release&nsfw=0&lyric=&seed=26&isAdvance=0&keyword=%s`;
                //http://192.168.196.226:8088/works?keyword=%s
                profile.showUrlTemplate = frontBase === "" ? "" : `${frontBase}/works?keyword=%s`;
            },

            getLoginConfig(profile) {
                if(!profile.apiBaseUrl) return null;
                return {
                    url: `${profile.apiBaseUrl}/api/auth/me`,
                    contentType: CONTENT_TYPE_JSON,
                    //直接序列化,避免用户名或密码中的引号破坏JSON
                    buildBody: () => JSON.stringify({name: profile.username, password: profile.password}),
                    tokenPath: "token",
                    tokenHeader: `Authorization: Bearer ${LOGIN_TOKEN_PLACEHOLDER}`
                };
            },

            async search(profile, rjCode, linkages) {
                const url = `${profile.searchUrlTemplate}`.replaceAll("%s", rjCode);
                //未配置仓库地址时无法查询
                if(url === "") return null;

                const resp = await profile.authedRequest(url, {method: "GET", cacheAge: 180});
                if(!resp || !(resp.readyState === 4 && resp.status === 200)) return null;

                let data;
                try{
                    data = JSON.parse(resp.responseText);
                }catch (e) {
                    throw new Error("仓库响应不是合法的JSON");
                }
                if(!Array.isArray(data.works)) {
                    throw new Error("仓库响应中不包含works列表");
                }
                if(data.works.length <= 0) return [];

                let result = [];
                for(const work of data.works) {
                    //kikoeru的id是作品编号中的数字部分,按数字匹配关联作品
                    const link = linkages[findLinkedWorkno(linkages, work.id, "numeric")];
                    if(!link) continue;

                    result.push(new SearchWorkInfo(link.workno, link.type, link.lang));
                }

                return result;
            }
        },

        custom: {
            id: "custom",
            get title() { return localize(localizationMap.search_preset_custom); },
            get tooltip() { return localize(localizationMap.search_preset_custom_tooltip); },
            defaults: {
                searchUrlTemplate: "",
                showUrlTemplate: "",
                method: "GET",
                bodyTemplate: "",
                contentType: CONTENT_TYPE_JSON,
                authType: "none",
                username: "",
                password: "",
                loginUrlTemplate: "",
                loginBodyTemplate: `{"name":"${LOGIN_USERNAME_PLACEHOLDER}","password":"${LOGIN_PASSWORD_PLACEHOLDER}"}`,
                loginContentType: CONTENT_TYPE_JSON,
                tokenPath: "token",
                tokenHeader: `Authorization: Bearer ${LOGIN_TOKEN_PLACEHOLDER}`,
                headers: "",
                listPath: "works",
                idField: "id",
                idRule: "auto"
            },
            fields: [
                {
                    key: "searchUrlTemplate", type: "input", required: true,
                    get title() { return localize(localizationMap.search_field_search_url); },
                    get tooltip() { return localize(localizationMap.search_field_search_url_tooltip); }
                },
                {
                    key: "showUrlTemplate", type: "input", required: false,
                    get title() { return localize(localizationMap.search_field_show_url); },
                    get tooltip() { return localize(localizationMap.search_field_show_url_tooltip); }
                },
                {
                    key: "method", type: "dropdown", required: true,
                    options: [{value: "GET", title: "GET"}, {value: "POST", title: "POST"}],
                    get title() { return localize(localizationMap.search_field_method); },
                    get tooltip() { return localize(localizationMap.search_field_method_tooltip); }
                },
                {
                    key: "bodyTemplate", type: "textarea", required: false,
                    binding: {key: "method", value: "POST"},
                    get title() { return localize(localizationMap.search_field_body_template); },
                    get tooltip() { return localize(localizationMap.search_field_body_template_tooltip); }
                },
                {
                    key: "contentType", type: "dropdown", required: true,
                    binding: {key: "method", value: "POST"},
                    options: [
                        {value: CONTENT_TYPE_JSON, title: "JSON"},
                        {value: CONTENT_TYPE_FORM, get title() { return localize(localizationMap.search_option_form); }}
                    ],
                    get title() { return localize(localizationMap.search_field_content_type); },
                    get tooltip() { return localize(localizationMap.search_field_content_type_tooltip); }
                },
                {
                    key: "authType", type: "dropdown", required: true,
                    options: [
                        {value: "none", get title() { return localize(localizationMap.search_auth_none); }},
                        {value: "headers", get title() { return localize(localizationMap.search_auth_headers); }},
                        {value: "login", get title() { return localize(localizationMap.search_auth_login); }}
                    ],
                    get title() { return localize(localizationMap.search_field_auth); },
                    get tooltip() { return localize(localizationMap.search_field_auth_tooltip_custom); }
                },
                {
                    key: "username", type: "input", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_login_username); },
                    get tooltip() { return localize(localizationMap.search_field_login_username_tooltip); }
                },
                {
                    key: "password", type: "password", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_login_password); },
                    get tooltip() { return localize(localizationMap.search_field_login_password_tooltip); }
                },
                {
                    key: "loginUrlTemplate", type: "input", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_login_url); },
                    get tooltip() { return localize(localizationMap.search_field_login_url_tooltip); }
                },
                {
                    key: "loginBodyTemplate", type: "textarea", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_login_body); },
                    get tooltip() { return localize(localizationMap.search_field_login_body_tooltip); }
                },
                {
                    key: "loginContentType", type: "dropdown", required: true,
                    binding: {key: "authType", value: "login"},
                    options: [
                        {value: CONTENT_TYPE_JSON, title: "JSON"},
                        {value: CONTENT_TYPE_FORM, get title() { return localize(localizationMap.search_option_form); }}
                    ],
                    get title() { return localize(localizationMap.search_field_login_content_type); },
                    get tooltip() { return localize(localizationMap.search_field_login_content_type_tooltip); }
                },
                {
                    key: "tokenPath", type: "input", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_token_path); },
                    get tooltip() { return localize(localizationMap.search_field_token_path_tooltip); }
                },
                {
                    key: "tokenHeader", type: "input", required: true,
                    binding: {key: "authType", value: "login"},
                    get title() { return localize(localizationMap.search_field_token_header); },
                    get tooltip() { return localize(localizationMap.search_field_token_header_tooltip); }
                },
                {
                    key: "headers", type: "textarea", required: false,
                    get title() { return localize(localizationMap.search_field_headers); },
                    get tooltip() { return localize(localizationMap.search_field_headers_tooltip_custom); }
                },
                {
                    key: "listPath", type: "input", required: true,
                    get title() { return localize(localizationMap.search_field_list_path); },
                    get tooltip() { return localize(localizationMap.search_field_list_path_tooltip); }
                },
                {
                    key: "idField", type: "input", required: true,
                    get title() { return localize(localizationMap.search_field_id_field); },
                    get tooltip() { return localize(localizationMap.search_field_id_field_tooltip); }
                },
                {
                    key: "idRule", type: "dropdown", required: true,
                    options: [
                        {value: "auto", get title() { return localize(localizationMap.search_option_id_auto); }},
                        {value: "raw", get title() { return localize(localizationMap.search_option_id_raw); }},
                        {value: "numeric", get title() { return localize(localizationMap.search_option_id_numeric); }}
                    ],
                    get title() { return localize(localizationMap.search_field_id_rule); },
                    get tooltip() { return localize(localizationMap.search_field_id_rule_tooltip); }
                }
            ],

            getAuthType(profile) {
                const authType = profile.config.authType;
                return authType === "login" || authType === "headers" || authType === "none" ? authType : "none";
            },

            resolve(profile) {
                const config = profile.config;
                profile.searchUrlTemplate = `${config.searchUrlTemplate ?? ""}`.trim();
                profile.showUrlTemplate = `${config.showUrlTemplate ?? ""}`.trim();
            },

            getLoginConfig(profile) {
                const config = profile.config;
                const url = `${config.loginUrlTemplate ?? ""}`.trim();
                if(url === "") return null;
                return {
                    url: url,
                    contentType: config.loginContentType === CONTENT_TYPE_FORM ? CONTENT_TYPE_FORM : CONTENT_TYPE_JSON,
                    bodyTemplate: `${config.loginBodyTemplate ?? ""}`,
                    tokenPath: `${config.tokenPath ?? ""}`.trim(),
                    tokenHeader: `${config.tokenHeader ?? ""}`.trim()
                };
            },

            async search(profile, rjCode, linkages) {
                const config = profile.config;
                const url = `${profile.searchUrlTemplate}`.replaceAll("%s", rjCode);
                //未配置搜索URL时无法查询
                if(url === "") return null;

                const method = `${config.method}`.toUpperCase() === "POST" ? "POST" : "GET";
                const contentType = config.contentType === CONTENT_TYPE_FORM ? CONTENT_TYPE_FORM : CONTENT_TYPE_JSON;
                const body = method === "POST" ? `${config.bodyTemplate ?? ""}`.replaceAll("%s", rjCode) : undefined;

                const resp = await profile.authedRequest(url, {method: method, body: body, contentType: contentType, cacheAge: 180});
                if(!resp || !(resp.readyState === 4 && resp.status === 200)) return null;

                let data;
                try{
                    data = JSON.parse(resp.responseText);
                }catch (e) {
                    throw new Error("仓库响应不是合法的JSON(自定义预设目前仅支持JSON响应)");
                }

                let list = getByPath(data, `${config.listPath ?? ""}`.trim());
                if(!Array.isArray(list) && list !== null && typeof list === "object") {
                    list = Object.values(list);
                }
                if(!Array.isArray(list)) {
                    throw new Error(`无法按路径「${config.listPath}」找到作品列表`);
                }

                let result = [];
                for(const item of list) {
                    const link = linkages[findLinkedWorkno(linkages,
                        getByPath(item, `${config.idField ?? ""}`.trim()), config.idRule)];
                    if(!link) continue;

                    result.push(new SearchWorkInfo(link.workno, link.type, link.lang));
                }

                return result;
            }
        }
    };

    /**
     * 生成仓库id(用于区分各仓库的缓存与token)
     * @returns {string}
     */
    function createWarehouseId() {
        const exists = Array.isArray(settings._s_search_warehouses) ? settings._s_search_warehouses : [];
        let id;
        do{
            id = `w${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
        }while(exists.some(warehouse => warehouse && warehouse.id === id));
        return id;
    }

    /**
     * 创建一个仓库配置
     * @param presetId {string} 预设类型
     * @param options {{id, name, enabled, config}}
     * @returns {{}}
     */
    function createWarehouse(presetId, options = {}) {
        const preset = SEARCH_PRESETS[presetId] ? SEARCH_PRESETS[presetId] : SEARCH_PRESETS.kikoeru;
        return {
            id: options.id === undefined ? createWarehouseId() : options.id,
            name: `${options.name === undefined ? preset.title : options.name}`.trim() || preset.title,
            preset: preset.id,
            enabled: options.enabled !== false,
            config: {...preset.defaults, ...(options.config === undefined ? {} : options.config)}
        };
    }

    /**
     * 规范化仓库配置,补齐缺失字段,避免设置数据损坏导致报错
     * @param warehouse {{}}
     * @returns {{}|null} 无法识别时返回null
     */
    function normalizeWarehouse(warehouse) {
        if(!warehouse || typeof warehouse !== "object") return null;
        const preset = SEARCH_PRESETS[warehouse.preset];
        if(!preset) return null;

        return {
            id: typeof warehouse.id === "string" && warehouse.id !== "" ? warehouse.id : createWarehouseId(),
            name: `${warehouse.name === undefined ? preset.title : warehouse.name}`.trim() || preset.title,
            preset: preset.id,
            enabled: warehouse.enabled !== false,
            config: {...preset.defaults, ...(warehouse.config && typeof warehouse.config === "object" ? warehouse.config : {})}
        };
    }

    /**
     * 读取设置中的仓库列表(已规范化,最多MAX_SEARCH_WAREHOUSES条)
     * @returns {[{}]}
     */
    function getWarehouses() {
        if(!Array.isArray(settings._s_search_warehouses)) return [];
        return settings._s_search_warehouses
            .map(warehouse => normalizeWarehouse(warehouse))
            .filter(warehouse => warehouse !== null)
            .slice(0, MAX_SEARCH_WAREHOUSES);
    }

    /**
     * 保存仓库列表
     * @param warehouses {[{}]}
     */
    function setWarehouses(warehouses) {
        settings._s_search_warehouses = warehouses.slice(0, MAX_SEARCH_WAREHOUSES);
        settings.save();
    }

    /**
     * 将旧版本的单一仓库设置迁移为仓库列表中的第一个仓库。
     * 迁移后删除旧设置项,避免用户清空仓库列表后又被旧值还原
     */
    function migrateSearchWarehouses() {
        if(!Array.isArray(settings._s_search_warehouses)) settings._s_search_warehouses = [];
        if(settings._s_search_warehouses.length > 0) return;

        //上一版的设置项:仓库地址、API地址、账号密码、自定义请求头
        const showBase = normalizeBaseUrl(GM_getValue("show_url_base", ""));
        const searchBase = normalizeBaseUrl(GM_getValue("search_url_base", ""));
        const username = `${GM_getValue("search_username", "")}`;
        const password = `${GM_getValue("search_password", "")}`;
        const headers = `${GM_getValue("search_url_headers", "")}`;
        //更早版本的设置项:完整的查询url与展示url模板
        const legacySearchUrl = normalizeBaseUrl(GM_getValue("search_url_pattern", ""));
        const legacyShowUrl = normalizeBaseUrl(GM_getValue("show_url_pattern", ""));

        const apiBase = searchBase !== "" ? searchBase : legacySearchUrl;
        const frontBase = showBase !== "" ? showBase : legacyShowUrl;
        const configured = apiBase !== "" || frontBase !== "" || headers !== "" || username !== "" || password !== "";
        if(!configured) return;

        //账号密码齐全时使用账号密码登录,否则回退为固定请求头
        const authType = username !== "" && password !== "" ? "login" : (headers !== "" ? "headers" : "none");
        settings._s_search_warehouses = [createWarehouse("kikoeru", {
            name: "kikoeru",
            config: {
                showBase: frontBase,
                searchBase: apiBase,
                authType: authType,
                username: username,
                password: password,
                headers: headers
            }
        })];

        for(const key of ["show_url_base", "search_url_base", "search_username", "search_password",
            "search_url_headers", "search_url_pattern", "show_url_pattern"]) {
            GM_deleteValue(key);
        }
        //旧版本的查询结果缓存池不再使用,一并清除
        try{
            DataCacheStorage.dropStorage("search-results");
        }catch (e) {
            console.error(e);
        }
    }

    /**
     * 根据设置中的仓库列表构建运行时使用的仓库配置(仅包含已启用且配置完整的仓库)
     * @returns {[SearchProfile]}
     */
    function buildSearchProfiles() {
        const profiles = [];
        for(const warehouse of getWarehouses()) {
            if(!warehouse.enabled) continue;

            const profile = new SearchProfile(warehouse);
            //无法生成搜索url的仓库(如未填写地址)不参与仓检
            if(profile.searchUrlTemplate === "") continue;

            profiles.push(profile);
        }
        return profiles;
    }

    /**
     * 重建运行时使用的仓库配置(仓库列表发生变化后调用)
     * @returns {[SearchProfile]}
     */
    function rebuildSearchProfiles() {
        settings._ss_search_profiles = buildSearchProfiles();
        return settings._ss_search_profiles;
    }

    /**
     * 判断指定仓库当前是否仍处于启用状态。
     * 用于在仓库被停用或删除后,中止它尚未完成的连锁查询
     * @param searchProfile {SearchProfile}
     * @returns {boolean}
     */
    function isSearchProfileActive(searchProfile) {
        return settings._ss_search_profiles.some(profile => profile.id === searchProfile.id);
    }

    class SearchProfile {
        //当前仓库的token缓存
        #token = null;
        //token的过期时间戳(毫秒),0表示有效期未知
        #tokenExpire = 0;
        //获取token的时间戳(毫秒)
        #tokenFetchedAt = 0;
        //是否已尝试从持久化存储中读取过token
        #tokenLoaded = false;
        //进行中的登录请求,用于避免并发重复登录
        #loginPromise = null;
        //最近一次登录失败的时间戳(毫秒),0表示最近没有登录失败
        #loginFailedAt = 0;

        /**
         * @param warehouse {{}} 仓库配置(设置中仓库列表的一项)
         */
        constructor(warehouse) {
            this.warehouse = warehouse;
            this.id = warehouse.id;
            this.name = warehouse.name;
            this.presetId = warehouse.preset;
            this.preset = SEARCH_PRESETS[warehouse.preset];
            this.enabled = warehouse.enabled === true;
            this.config = warehouse.config;

            //认证信息(仅账号密码登录的预设需要)
            this.username = `${this.config.username === undefined || this.config.username === null ? "" : this.config.username}`.trim();
            this.password = `${this.config.password === undefined || this.config.password === null ? "" : this.config.password}`;
            this.customHeaders = parseHeaderText(this.config.headers);

            //由预设生成实际使用的查询url、展示url等信息
            this.searchUrlTemplate = "";
            this.showUrlTemplate = "";
            if(this.preset) this.preset.resolve(this);
        }

        /**
         * 认证方式:none(无认证)/ headers(固定请求头)/ login(账号密码登录)
         * @returns {string}
         */
        get authType() {
            return this.preset ? this.preset.getAuthType(this) : "none";
        }

        /**
         * 是否配置了可用于登录的账号密码(认证方式为登录,且用户名与密码均已填写)
         * @returns {boolean}
         */
        get canLogin() {
            return this.authType === "login" && this.username !== "" && this.password.trim() !== "";
        }

        /**
         * 登录接口地址(未配置登录方式时返回空字符串)
         * @returns {string}
         */
        get loginUrl() {
            const loginConfig = this.preset ? this.preset.getLoginConfig(this) : null;
            return loginConfig ? loginConfig.url : "";
        }

        /**
         * 该仓库配置的指纹,用于区分查询结果缓存(配置变化后自动失效)
         * @returns {Promise<string>}
         */
        async getHash() {
            return await hash(`${this.presetId}|${JSON.stringify(this.config)}`)
        }

        /**
         * 使用该仓库的预设查询指定RJ号
         * @param rjCode {string}
         * @param linkages {{}} 关联作品表
         * @returns {Promise<[SearchWorkInfo]|null|undefined>}
         */
        async search(rjCode, linkages) {
            if(!this.preset) throw new Error(`未找到仓库预设:${this.presetId}`);
            return await this.preset.search(this, rjCode, linkages);
        }

        /**
         * 清除该仓库的查询结果缓存与登录状态(禁用或删除仓库时使用)
         */
        async clearCache() {
            WorkPromise.clearSearchResultCache(this);
            await this.clearToken();
        }

        /**
         * 当前缓存的token是否可用(有效期未知时视为可用,实际失效由请求重试流程处理)
         * @returns {boolean}
         */
        #isTokenValid() {
            if(!this.#token) return false;
            //刚刚获取到的token直接信任,避免因时间偏差导致反复重新登录
            if(Date.now() - this.#tokenFetchedAt < TOKEN_TRUST_INTERVAL) return true;
            if(this.#tokenExpire <= 0) return true;
            return Date.now() < this.#tokenExpire;
        }

        /**
         * token缓存的存储key:每个仓库独立缓存,删除仓库时一并删除
         * @returns {string}
         */
        #getTokenCacheKey() {
            return `search_token_${this.id}`;
        }

        /**
         * 从持久化存储中读取此前缓存的token
         * @returns {Promise<void>}
         */
        async #loadToken() {
            this.#tokenLoaded = true;
            try {
                const cached = await GM_getValue(this.#getTokenCacheKey(), null);
                if(cached && typeof cached.token === "string") {
                    this.#token = cached.token;
                    this.#tokenExpire = typeof cached.expire === "number" ? cached.expire : 0;
                    this.#tokenFetchedAt = typeof cached.fetchedAt === "number" ? cached.fetchedAt : 0;
                }
            }catch (e) {
                console.error(e);
            }
        }

        /**
         * 缓存token(同时写入内存与持久化存储)
         * @param token {string}
         * @returns {Promise<void>}
         */
        async #saveToken(token) {
            this.#token = token;
            this.#tokenExpire = parseJwtExpireTime(token);
            this.#tokenFetchedAt = Date.now();
            this.#tokenLoaded = true;
            try {
                GM_setValue(this.#getTokenCacheKey(), {
                    token: this.#token,
                    expire: this.#tokenExpire,
                    fetchedAt: this.#tokenFetchedAt
                });
            }catch (e) {
                console.error(e);
            }
        }

        /**
         * 清除当前缓存的token
         * @returns {Promise<void>}
         */
        async clearToken() {
            this.#token = null;
            this.#tokenExpire = 0;
            this.#tokenFetchedAt = 0;
            this.#tokenLoaded = true;
            try {
                GM_deleteValue(this.#getTokenCacheKey());
            }catch (e) {
                console.error(e);
            }
        }

        /**
         * 使用账号密码调用预设定义的登录接口获取token
         * @returns {Promise<string>} 登录得到的token
         */
        async login() {
            if(!this.canLogin) {
                throw new Error("未配置仓库账号密码,无法登录");
            }

            const loginConfig = this.preset ? this.preset.getLoginConfig(this) : null;
            if(!loginConfig || !loginConfig.url) {
                throw new Error("未配置登录接口地址,无法登录");
            }

            //登录请求体:内置预设直接序列化,自定义预设按模板替换用户名与密码
            const body = typeof loginConfig.buildBody === "function" ? loginConfig.buildBody() :
                `${loginConfig.bodyTemplate === undefined ? "" : loginConfig.bodyTemplate}`
                    .replaceAll(LOGIN_USERNAME_PLACEHOLDER, this.username)
                    .replaceAll(LOGIN_PASSWORD_PLACEHOLDER, this.password);

            //登录接口同样可能依赖自定义请求头才能访问,故一并携带;
            //但需要过滤其中的Authorization,避免可能已失效的旧token干扰登录接口
            const resp = await postAsync(loginConfig.url, body,
                loginConfig.contentType === CONTENT_TYPE_FORM ? CONTENT_TYPE_FORM : CONTENT_TYPE_JSON,
                this.#getCustomHeadersWithoutAuth());
            if(!(resp.readyState === 4 && resp.status === 200)) {
                throw new Error(`仓库「${this.name}」登录失败:${resp.status} ${resp.statusText}`);
            }

            let data;
            try{
                data = JSON.parse(resp.responseText);
            }catch (e) {
                throw new Error("仓库登录响应不是合法的JSON");
            }
            const token = getByPath(data, loginConfig.tokenPath);
            if(typeof token !== "string" || token === "") {
                throw new Error(`仓库登录响应中不包含token(取值路径:${loginConfig.tokenPath})`);
            }

            await this.#saveToken(token);
            return token;
        }

        /**
         * 获取可用的token,必要时使用账号密码登录
         * @param forceRefresh {boolean} 是否强制重新登录(用于token被判定为失效时)
         * @returns {Promise<string>} token,未配置账号密码时返回null
         */
        async ensureToken(forceRefresh = false) {
            if(!this.canLogin) return null;

            if(!forceRefresh) {
                if(!this.#tokenLoaded) {
                    await this.#loadToken();
                }
                if(this.#isTokenValid()) return this.#token;
            }

            //登录失败后短时间内不再请求登录接口,避免登录接口被反复请求
            if(this.#loginFailedAt > 0 && Date.now() - this.#loginFailedAt < LOGIN_RETRY_COOLDOWN) {
                throw new Error("仓库登录失败过于频繁,已暂时跳过登录");
            }

            //并发的搜索请求共用同一个登录请求
            if(this.#loginPromise) return this.#loginPromise;

            this.#loginPromise = (async () => {
                try{
                    const token = await this.login();
                    this.#loginFailedAt = 0;
                    return token;
                }catch (e) {
                    this.#loginFailedAt = Date.now();
                    throw e;
                }finally {
                    this.#loginPromise = null;
                }
            })();

            return this.#loginPromise;
        }

        /**
         * 取出自定义请求头中除Authorization以外的部分。
         * 自定义请求头的作用不只是提供Authorization(部分仓库API需要特定请求头才可访问),
         * 因此除Authorization外的内容会始终随请求一起发送
         * @returns {{}}
         */
        #getCustomHeadersWithoutAuth() {
            let headers = {...this.customHeaders};
            for(const key of Object.keys(headers)) {
                //Authorization由账号密码登录得到的token或回退流程负责
                if(key.toLowerCase() === "authorization") delete headers[key];
            }
            return headers;
        }

        /**
         * 生成携带指定token的请求头:自定义请求头(去除Authorization) + 预设定义的token请求头
         * @param token {string}
         * @returns {{}}
         */
        #buildTokenHeaders(token) {
            let headers = this.#getCustomHeadersWithoutAuth();
            const loginConfig = this.preset ? this.preset.getLoginConfig(this) : null;
            const headerText = loginConfig && loginConfig.tokenHeader ? loginConfig.tokenHeader : `Authorization: Bearer ${LOGIN_TOKEN_PLACEHOLDER}`;
            const tokenHeaders = parseHeaderText(headerText.replaceAll(LOGIN_TOKEN_PLACEHOLDER, token));

            for(const key of Object.keys(tokenHeaders)) {
                //避免与自定义请求头中的同名请求头冲突
                for(const existKey of Object.keys(headers)) {
                    if(existKey.toLowerCase() === key.toLowerCase()) delete headers[existKey];
                }
                headers[key] = tokenHeaders[key];
            }
            return headers;
        }

        /**
         * 携带身份验证信息请求仓库接口。
         * 认证方式为账号密码登录时优先使用登录得到的token;若仓库返回验证失效(401/403),
         * 则重新登录并携带新token重试一次。未配置登录、登录失败或重试仍未通过时,
         * 回退为仅使用自定义请求头。
         * @param url {string}
         * @param options {{method, cacheAge, body, contentType}}
         * @returns {Promise<{}>}
         */
        async authedRequest(url, options = {}) {
            const method = `${options.method === undefined ? "GET" : options.method}`.toUpperCase();
            const request = (headers) => {
                if(method === "POST") {
                    return postAsync(url, options.body === undefined ? "" : options.body,
                        options.contentType === CONTENT_TYPE_FORM ? CONTENT_TYPE_FORM : CONTENT_TYPE_JSON,
                        headers, "low");
                }
                return getHttpAsync(url, false, options.cacheAge === undefined ? 180 : options.cacheAge, headers, "low");
            };

            let resp = null;

            if(this.canLogin) {
                let token = null;
                try{
                    token = await this.ensureToken();
                }catch (e) {
                    console.error("仓库登录失败:", e);
                }

                if(token) {
                    resp = await request(this.#buildTokenHeaders(token));

                    if(isAuthFailure(resp)) {
                        //验证失效:丢弃缓存的token,重新登录后再试一次
                        await this.clearToken();
                        token = null;
                        try{
                            token = await this.ensureToken(true);
                        }catch (e) {
                            console.error("仓库重新登录失败:", e);
                        }
                        if(token) {
                            resp = await request(this.#buildTokenHeaders(token));
                        }
                    }
                }
            }

            //自定义请求头作为回退手段。
            //尚未发起过请求时(无认证或不进行登录)必须发出请求;
            //已经用token请求过且被判定失效时,仅在存在自定义请求头时才额外重试一次
            if(resp === null || (isAuthFailure(resp) && Object.keys(this.customHeaders).length > 0)) {
                resp = await request(this.customHeaders);
            }

            return resp;
        }

        /**
         * 携带身份验证信息发起GET请求
         * @param url {string}
         * @param cacheAge {number} 缓存时间(秒)
         * @returns {Promise<{}>}
         */
        async authedGet(url, cacheAge = 180) {
            return await this.authedRequest(url, {method: "GET", cacheAge: cacheAge});
        }
    }

    class SearchWorkInfo {
        constructor(workno, type, lang) {
            this.workno = workno;
            this.type = type;
            this.lang = lang;
        }
    }

    class SearchResult {

        /*
        一共有以下几种情况:
        1. 有/无本作
        2. 有原版
        3. 有翻译版(未知子版本)
        4. 有翻译版(其它子版本)
        5. 有翻译版(其它语言)

        各种组合方案预览(极端情况):
        对原版来说:
        ---
        已存:本作、翻译(简中-2/繁中-1/英-1)
        未存本作|已存:翻译(简中-2/繁中-1/英-1)
        ---
        ✔ 本作 翻译 (简中-2/繁中-1/英-1)
        ✔ 本作 翻译(简中/繁中/英...

        ✘本作|✔ 翻译 (简中-2/繁中-1/英-1)
        ✘本作|✔ 翻译(简中/繁中/英...  [只有用父作品当rj时会显示这些翻译结果]

        对翻译版父RJ来说:
        ---
        已存:此语言、原版、翻译 (简中-2/繁中-1/英-1)
        未存此语言|已存:原版、翻译 (繁中-1/英-1)
        ---
        ✔ 此语言 原版 翻译(简中-2/繁中-1/英-1)     ✔ Lang, Orig, 🌐(SC-2/TC-1/EN-1)
        ✔ 此语言(简中-2) 原版...

        ✘此语言|✔ 原版 翻译(繁中-1/英-1)
        ✘此语言|✔ 原版...

        对子RJ来说:
        ---
        已存:本作、原版、翻译 (简中-2/繁中-1/英-1)
        未存本作|已存:原版、翻译 (简中-1/繁中-1/英-1)
        ---
        ✔ 本作 原版 翻译(简中-2/繁中-1/英-1)     ✔ This, Orig, 🌐(SC-2/TC-1/EN-1)
        ✔ 本作 原版...

        ✘本作|✔ 原版 翻译(简中-1/繁中-1/英-1)       ✘This|✔ This, 🌐(SC-1/TC-1/EN-1)
        ✘本作|✔ 原版...
         */

        #analyzeInfo = null;

        /**
         * @param searchProfileName {string}
         * @param searchWorkInfo {SearchWorkInfo}
         * @param resultList {[SearchWorkInfo]}
         */
        constructor(searchProfileName, searchWorkInfo, resultList) {
            this.profileName = searchProfileName;
            this.workInfo = searchWorkInfo;

            /**
             * @type {{String: SearchWorkInfo}}
             */
            this.result = {};

            for (let res of resultList) {
                this.result[res.workno] = res;
            }
        }

        analyze() {
            if (this.#analyzeInfo) {
                return this.#analyzeInfo;
            }
            let work = this.workInfo;
            let result = this.result;
            let info = {
                hasCurrent: false,
                hasOriginal: false,
                hasTranslation: false,
                langList: {},
            }

            for (const res of Object.values(result)) {
                if (res.workno === work.workno) {
                    info.hasCurrent = true;
                }

                if(work.type === "parent" && res.type === "child" && work.lang === res.lang){
                    info.hasCurrent = true;
                }

                if(work.type !== "original" && res.type === "original") {
                    info.hasOriginal = true;
                }

                if (res.type !== "original") {
                    info.hasTranslation = true;

                    let ll = info.langList;
                    ll[res.lang] = ll[res.lang] ? ll[res.lang] : 0;
                    ll[res.lang] += res.type === "child" ? 1 : 11;
                }
            }

            this.#analyzeInfo = info;
            return info;
        }

        getStatusText() {
            let etc = !settings._s_full_linkage || !settings._s_search_linkage;
            let work = this.workInfo;
            let info = this.analyze();
            let text = info.hasCurrent ?
                `✔ ${work.type === "parent" ? localizePopup(localizationMap.search_result_this_lang) : localizePopup(localizationMap.search_result_this)}` :
                `✘ ${work.type === "parent" ? localizePopup(localizationMap.search_result_this_lang) : localizePopup(localizationMap.search_result_this)}`;
            if (info.hasOriginal || info.hasTranslation){
                text += info.hasCurrent ? ` ` : `|✔ `;
            }
            if (info.hasOriginal){
                text += `${localizePopup(localizationMap.search_result_orig)}${info.hasTranslation ? " " : ""}`;
            }
            if (info.hasTranslation){
                text += `${localizePopup(localizationMap.search_result_translation)}`
            } else {
                return `${text}${etc ? "..." : ""}`;
            }

            let str = "";
            for (let lang of settings._ss_cue_lang) {
                let langStr = LANG_MAP_ABBR[lang];
                let langCot = info.langList[lang];
                if(!langCot || langCot % 10 === 0) continue;

                str += `${langStr}x${langCot % 10}${langCot > 11 ? "?" : ""}/`
            }
            if(!str.endsWith("/")){
                //代表语言的括号内是空的,说明有该作品但是语言不在cue lang中,此时在里面加"..."代表还有未知语言
                //TODO 以后改成按已有语言展示而不是按cue lang展示
                etc = true;
            }else{
                str = str.substring(0, str.length - 1);
            }

            return `${text}(${str}${etc ? "..." : ""})`;
        }
    }

    //endregion

    //region 缓存系统

    class DataCache {
        _data;
        _timeAdd;
        _timeUpdate;
        _timeAccess;
        _timeExpire = 0;
        constructor(data, timeExp = 0) {
            this._data = data;
            this._timeAdd = Date.now();
            this._timeUpdate = undefined;
            this._timeAccess = undefined;
            this._timeExpire = timeExp;
        }

        get data() {
            this._timeAccess = Date.now();
            return this._data;
        }
        get timeAdd() { return this._timeAdd; }
        get timeUpdate() { return this._timeUpdate; }
        get timeAccess() { return this._timeAccess; }
        get timeExpire() { return this._timeExpire; }
        get hasExpired() { return this._timeExpire > 0 && this._timeExpire < Date.now()}  //0代表无过期时间

        set timeExpire(value) {
            if(typeof value !== "number" || value < 0) return;
            this._timeExpire = value;
        }

        update(data, expTime = -1) {
            this._data = data;
            this._timeUpdate = Date.now();
            this.timeExpire = expTime;
        }
    }

    class DataCacheStorage {
        static #activeStorages = {}
        _name;
        _maxSize;
        _autoSave;
        _dropExpired;
        _dataMap;

        get #head() { return this._dataMap["-head"]; }
        get #tail() { return this._dataMap["-tail"]; }

        get name() { return this._name; }
        get maxSize() { return this._maxSize; }
        get autoSave() { return this._autoSave; }
        get dropExpired() { return this._dropExpired; }

        /**
         * @param value {string}
         */
        set name(value) {
            if(typeof value !== "string") throw new Error("Invalid Storage Name");
            this._name = value;
        }

        /**
         * @param value {number}
         */
        set maxSize(value) {
            if(typeof value !== "number" || value <= 0) return;
            this._maxSize = value;
        }

        /**
         * @param value {boolean}
         */
        set autoSave(value) {
            if(typeof value !== "boolean") return;
            this._autoSave = value;
        }

        /**
         * @param value {boolean}
         */
        set dropExpired(value) {
            if(typeof value !== "boolean") return;
            this._dropExpired = value;
        }

        /**
         * 不要在外部调用该构造器,请使用CacheStorage.open()
         */
        constructor(name, maxSize = 128, dropExpired = false, autoSave = true) {
            this.name = name;
            this._dataMap = {
                "-head": {next: "-tail", prev: null},
                "-tail": {next: null, prev: "-head"}
            };
            this.maxSize = maxSize;
            this.dropExpired = dropExpired;
            this.autoSave = autoSave;
        }

        static fromObject(obj, name) {
            if(!obj._name) return new DataCacheStorage(name);
            let storage = Object.assign(new DataCacheStorage(name), obj);
            for (const keyX in storage._dataMap) {
                let node = storage._dataMap[keyX];
                let cache = node.cache;
                if(cache) {
                    cache = Object.assign(new DataCache(null, -1), cache);
                    node.cache = cache;
                }
            }
            return storage;
        }

        /**
         * 打开指定的缓存库,若库不存在则会新建。可通过额外参数指定或覆盖缓存库设置
         * @param storageName {string} 库名称
         * @param maxSize {number} 最大缓存记录条数
         * @param dropExpired {boolean} 是否删除过期记录
         * @param autoSave {boolean} 是否自动保存
         * @param replaceProp {boolean} 如果库已存在,是否使用设置的值替换原有设置
         * @returns {DataCacheStorage} 名称对应的存储库
         */
        static open(storageName, maxSize = undefined, dropExpired = undefined, autoSave = undefined, replaceProp = false) {
            if (!(storageName in this.#activeStorages)) {
                this.#activeStorages[storageName] = DataCacheStorage.fromObject(GM_getValue(`cache_${storageName}`, new DataCacheStorage(storageName, maxSize, dropExpired, autoSave)), storageName);
            }
            let storage = this.#activeStorages[storageName];

            if(replaceProp){
                storage.maxSize = maxSize;
                storage.dropExpired = dropExpired;
                storage.autoSave = autoSave;
            }

            if(storage.dropExpired) {
                storage.dropExpiredCache();
            }

            return this.#activeStorages[storageName];
        }

        static dropStorage(storageName) {
            if(storageName in this.#activeStorages){
                delete this.#activeStorages[storageName];
            }
            GM_deleteValue(`cache_${storageName}`);
        }

        /**
         * 保存缓存库至持久化存储
         */
        save() {
            GM_setValue(`cache_${this.name}`, this);
        }

        #disconnectNode(node) {
            if(node.next) this._dataMap[node.next].prev = node.prev;
            if(node.prev) this._dataMap[node.prev].next = node.next;

            node.next = null;
            node.prev = null;
        }

        #moveNodeNextTo(key, node, prevKey) {
            let keyX = "_" + key;
            let prevKeyX = prevKey ? "_" + prevKey : "-head";
            this.#disconnectNode(node);

            node.prev = prevKeyX;
            node.next = this._dataMap[prevKeyX].next;
            this._dataMap[prevKeyX].next = keyX;
            this._dataMap[node.next].prev = keyX;
        }

        #moveNodeBefore(key, node, nextKey) {
            let keyX = "_" + key;
            let nextKeyX = nextKey ? "_" + nextKey : "-tail";
            this.#disconnectNode(node);

            node.next = nextKeyX;
            node.prev = this._dataMap[nextKeyX].prev;
            this._dataMap[nextKeyX].prev = keyX;
            this._dataMap[node.prev].next = keyX;
        }

        #sizeLimitCheck() {
            let overflow = Object.keys(this._dataMap).length - 2 - this.maxSize;
            for (let i = 0; i < overflow; i++) {
                if(this.#head.next === "-tail") break;
                this.drop(this.#head.next);
            }
        }

        #isExpired(key) {
            let keyX = "_" + key;
            if(!(keyX in this._dataMap)) return true;
            let cache = this._dataMap[keyX].cache;
            let expired = cache.hasExpired;

            if (expired && this.dropExpired) this.drop(key);
            return expired;
        }

        /**
         * 提交或新增对缓存记录的更改
         * @param key {string}
         * @param data {*}
         * @param expTime {number} 过期时间(毫秒数),为-1则不修改
         */
        commit(key, data, expTime = -1) {
            let keyX = "_" + key;
            let node = this._dataMap[keyX];
            if (node) {
                node.cache.update(data, expTime);
            } else {
                node = {
                    cache: new DataCache(data, expTime),
                    next: null,
                    prev: null
                };
                this._dataMap[keyX] = node;
                this.#sizeLimitCheck();
            }
            this.#moveNodeBefore(key, node, null);

            if(this.autoSave) this.save();
        }

        /**
         * 删除指定key对应的缓存记录
         * @param key {string}
         */
        drop(key) {
            let keyX = "_" + key;
            if (!(keyX in this._dataMap)) return;
            this.#disconnectNode(this._dataMap[keyX])
            delete this._dataMap[keyX];

            if(this.autoSave) this.save();
        }

        /**
         * 删除所有过期的缓存记录
         */
        dropExpiredCache() {
            for (let keyX in this._dataMap){
                if(keyX.startsWith("-")) continue;
                let node = this._dataMap[keyX];
                if(!node.cache.hasExpired) continue;

                this.drop(keyX.substring(1));
            }
        }

        /**
         * 获取指定缓存记录的数据
         * @param key {string}
         * @returns {*}
         */
        get(key) {
            let keyX = "_" + key;
            let value;
            if(keyX in this._dataMap && !this.#isExpired(key)) {
                this.#moveNodeBefore(key, this._dataMap[keyX], null);
                value = this._dataMap[keyX].cache.data;
            }

            if(this.autoSave) this.save();
            return value;
        }

        /**
         * 获取key对应的缓存对象(包含添加、修改、访问时间信息),注意该方法不会触发自动保存。
         * @param key {string}
         * @param keepExpired {boolean} 是否返回已过期的缓存记录?(如果缓存已删除,如设置了dropExpired为true,则无法获取到对象)
         * @returns {DataCache}
         */
        getCache(key, keepExpired) {
            let keyX = "_" + key;
            let value;
            if(keyX in this._dataMap && (keepExpired || !this.#isExpired(key))) {
                this.#moveNodeBefore(key, this._dataMap[keyX], null);
                value = this._dataMap[keyX].cache;
            }

            //if(this.autoSave) this.save();
            return value;
        }
    }

    //endregion

    //region 弹框生成 & 更新

    const Popup = {
        popupElement: {
            popup: null,
            not_found: null,
            left_panel: null,
            img: {container: null},
            right_panel: null,
            title: null,
            rj_code: null,
            info_container: null,
            loader: null,
            flag: null,
            tags: null,
            dl_count: null,
            circle_name: null,
            debug: null,
            translator_name: null,
            release_date: null,
            update_date: null,
            age_rating: null,
            scenario: null,
            illustration: null,
            voice_actor: null,
            music: null,
            genre: null,
            file_size: null,

            _state: {
                workno: undefined,
                mouseX: 0,
                mouseY: 0
            }
        },

        get currentWorkno() {
            return Popup.popupElement._state.workno;
        },

        isCurrentWork(workno) {
            return Popup.popupElement._state.workno === workno;
        },

        setCurrentWork(workno) {
            Popup.popupElement._state.workno = workno;
        },

        makePopup: function (display) {
            const popup = document.createElement("div");
            const ele = Popup.popupElement;
            ele.popup = popup;

            popup.className = `${VOICELINK_CLASS}_voicepopup ${VOICELINK_CLASS}_voicepopup-maniax ` + (getAdditionalPopupClasses() || '');
            popup.id = `${VOICELINK_CLASS}-voice-popup`;  // + rjCode;
            popup.style.setProperty("display", display === false ? "none" : "flex", "important");  //display = display === false ? "none" : "flex";
            document.body.appendChild(popup);

            popup.addEventListener("mouseenter", () => {
                popup.setAttribute("mouse-in", "");
            });
            popup.addEventListener("mouseleave", () => {
                popup.removeAttribute("mouse-in");
            })

            const notFoundElement = document.createElement("div");
            ele.not_found = notFoundElement;
            //占满整个popup
            //"display: none; width: 100%; height: 100%";
            notFoundElement.style.setProperty("display", "none", "important");
            notFoundElement.style.setProperty("width", "100%", "important");
            notFoundElement.style.setProperty("height", "100%", "important");
            notFoundElement.innerText = "Work Not Found.";
            popup.appendChild(notFoundElement);

            const leftPanel = document.createElement("div");
            leftPanel.classList.add(`${VOICELINK_CLASS}_left_panel`);
            popup.appendChild(leftPanel);
            ele.left_panel = leftPanel;

            const imgContainer = document.createElement("div")
            imgContainer.classList.add(`${VOICELINK_CLASS}_img_container`);
            ele.img.container = imgContainer;
            leftPanel.appendChild(imgContainer);

            //左下角提示状态栏
            ele.hint = document.createElement("div");
            leftPanel.appendChild(ele.hint);
            ele.hint.id = `${VOICELINK_CLASS}_hint`;

            const rightPanel = document.createElement("div");
            ele.right_panel = rightPanel;

            const titleElement = Popup.createCopyTag("div", "", false, localizePopup(localizationMap.hint_copy_work_title));
            ele.title = titleElement;
            titleElement.classList.add(`${VOICELINK_CLASS}_voice-title`);
            rightPanel.appendChild(titleElement);

            const rjCodeElement = document.createElement("div");
            ele.rj_code = rjCodeElement;
            rjCodeElement.classList.add(`${VOICELINK_CLASS}_rjcode`);
            rightPanel.appendChild(rjCodeElement);

            const infoContainer = document.createElement("div");
            ele.info_container = infoContainer;
            infoContainer.id = `${VOICELINK_CLASS}_info-container`;
            infoContainer.style.setProperty("position", "relative", "important");  //position = "relative !important";
            infoContainer.style.setProperty("min-height", "70px", "important");  //minHeight = "70px !important";
            rightPanel.appendChild(infoContainer);

            const loader = document.createElement("div");
            loader.className = `${VOICELINK_CLASS}_loader`;
            loader.innerHTML = Csp.createHTML(`
            <div class="${VOICELINK_CLASS}_dot"></div>
            <div class="${VOICELINK_CLASS}_dot"></div>
            <div class="${VOICELINK_CLASS}_dot"></div>
            `);
            ele.loader = loader;
            infoContainer.appendChild(loader);

            ele.tags = document.createElement("div");
            infoContainer.appendChild(ele.tags);

            ele.dl_count = document.createElement("div");
            ele.circle_name = document.createElement("div");
            ele.debug = document.createElement("div");
            ele.translator_name = document.createElement("div");
            ele.release_date = document.createElement("div");
            ele.update_date = document.createElement("div");
            ele.age_rating = document.createElement("div");
            ele.scenario = document.createElement("div");
            ele.illustration = document.createElement("div");
            ele.voice_actor = document.createElement("div");
            ele.music = document.createElement("div");
            ele.genre = document.createElement("div");
            ele.file_size = document.createElement("div");

            rightPanel.style.setProperty("padding-bottom", "3px", "important");  //paddingBottom = "3px !important";
            rightPanel.style.setProperty("flex-grow", "1", "important");  //flexGrow = "1 !important";
            popup.appendChild(rightPanel);
            popup.insertBefore(leftPanel, popup.childNodes[0]);
        },

        updatePopup: async function(e, rjCode, isParent=false) {
            const ele = Popup.popupElement;
            const popup = ele.popup;
            popup.className = `${VOICELINK_CLASS}_voicepopup ${VOICELINK_CLASS}_voicepopup-maniax ` + (getAdditionalPopupClasses() || '');
            // popup.id = "voice-" + rjCode;
            popup.style.setProperty("display", "flex", "important");  //= "display: flex";
            popup.setAttribute(RJCODE_ATTRIBUTE, rjCode);
            Popup.setCurrentWork(rjCode);

            //------检查作品存在情况------
            let workFound = true;
            Popup.setFoundState(true);
            WorkPromise.getFound(rjCode).then(async found => {
                if(!Popup.isCurrentWork(rjCode)) return;

                if(found){
                    //找到则直接返回交给下一级处理
                    return {found: true, parentRJ: rjCode};
                }

                //没找到则尝试找到父作品的RJ号,填补子作品信息的缺失
                let parentRJ = await WorkPromise.getParentRJ(rjCode);
                if(parentRJ === rjCode || !parentRJ) {
                    return {found: false, parentRJ: rjCode};
                }
                found = await WorkPromise.getFound(parentRJ);
                return {found: found, parentRJ: parentRJ};

            }).then(async (state) => {
                if(!Popup.isCurrentWork(rjCode)) return;

                const found = state.found;
                const rj = state.parentRJ;
                if(found && rj !== rjCode){
                    //如果找到了父作品的信息但子作品找不到,就重新update
                    await Popup.updatePopup(e, rj, true);
                    return;
                }

                ele.not_found.style.setProperty("display", found ? "none" : "block", "important");  //display = found ? "none" : "block";
                Popup.setFoundState(found);
                workFound = found;
            });

            //------检查是否为女性向------
            WorkPromise.getGirls(rjCode).then(isGirls => {
                if(!Popup.isCurrentWork(rjCode)) return;
                if(isGirls) popup.className += (` ${VOICELINK_CLASS}_voicepopup-girls`)
            }).catch(e => {});

            //------获取作品封面------
            const imgContainer = ele.img.container;

            //NSFW模糊等级
            const blur_map = {
                low: "6px",
                medium: "12px",
                high: "24px"
            };

            //先对Container内的所有img进行隐藏
            for (let i = 0; i < imgContainer.childNodes.length; ++i) {
                imgContainer.childNodes[i].style.setProperty("display", "none", "important");  //display = "none !important";
            }

            //NOTE: 注意这里可能因为快速的多次获取导致同时加载两个图片,可使用占位符来预先占用图片位置
            new Promise((resolve, reject) => {
                let img = ele.img[rjCode];
                if(img) resolve(img);
                else throw Error("首次加载图片");
            }).catch(_ => {
                //首次加载图片,对图片添加占位
                ele.img[rjCode] = 1;
                return WorkPromise.getImgLink(rjCode);
            }).then(async link => {
                if(typeof link !== "string"){
                    //图片已经加载过,传递的是img,不通过当前then
                    return link;  //实际上是img
                }

                if(!Popup.isCurrentWork(rjCode)) {
                    //清除占位
                    ele.img[rjCode] = null;
                    return null;
                }
                let img;
                try{
                    img = GM_addElement("img", {
                        src: '',
                    });
                    if(!img) { // noinspection ExceptionCaughtLocallyJS
                        throw new Error("API调用生成失败");
                    }
                }catch (e) {
                    img = document.createElement("img");
                    img.src = '';
                }

                imgContainer.appendChild(img);
                console.warn("添加封面!")
                ele.img[rjCode] = img;

                //开启动画
                if(settings._s_sfw_blur_transition){
                    img.style.setProperty("transition", "all 0.3s", "important");
                }

                //鼠标移动上去解除模糊
                img.addEventListener("mouseenter", e => {
                    if(!settings._s_sfw_remove_when_hover){
                        return;
                    }
                    img.style.setProperty("filter", "inherit", "important");
                });
                img.addEventListener("mouseleave", e => {
                    if(settings._s_sfw_mode){
                        img.style.setProperty("filter", `blur(${blur_map[settings._s_sfw_blur_level]})`, "important");
                    }else{
                        img.style.setProperty("filter", "inherit", "important");
                    }
                });

                loadImageSafe(img, link);

                return img;
            }).then(img => {
                if(!(img instanceof HTMLElement)) return;
                img.style.setProperty("display", "block", "important");  //display = "block"

                //设置NSFW模糊
                if(settings._s_sfw_mode){
                    img.style.setProperty("filter", `blur(${blur_map[settings._s_sfw_blur_level]})`, "important");
                }else{
                    img.style.setProperty("filter", "inherit", "important");
                }
            }).catch(e => {
                //清理并在下次重试
                if(ele.img[rjCode] instanceof HTMLElement) img.remove();
                ele.img[rjCode] = null;
                console.error(e)
            });

            //------设置hint可见------
            ele.hint.style.setProperty("display", "block", "important");

            //------设置标题------
            const titleElement = ele.title;
            titleElement.innerText = "Loading...";
            titleElement.setHint(localizePopup(localizationMap.hint_copy_work_title));
            titleElement.setCopyText(null);
            titleElement.setSecondaryCopyText(null);
            WorkPromise.getWorkTitle(rjCode).then(title => {
                if(!Popup.isCurrentWork(rjCode)) return;
                titleElement.innerText = title;
                titleElement.setCopyText(title);
                titleElement.setSecondaryCopyText(convertToValidFileName(title));
            }).catch(_ => {
                if(!Popup.isCurrentWork(rjCode)) return;
                titleElement.innerHTML = Csp.createHTML("");
            })

            //------设置RJ号------
            const rjCodeElement = ele.rj_code;
            let ie = Popup.createCopyTag("span", rjCode);
            ie.innerText = rjCode;
            ie.classList.add(VOICELINK_IGNORED_CLASS);
            ie.style.setProperty("font-weight", "bold", "important");
            ie.style.setProperty("text-decoration", "underline", "important");
            rjCodeElement.innerHTML = Csp.createHTML(`[ ${isParent ? " ↑ " : ""}`);
            rjCodeElement.appendChild(ie)
            rjCodeElement.appendChild(document.createTextNode(" ]"))

            WorkPromise.getRJChain(rjCode).then(chain => {
                if(!Popup.isCurrentWork(rjCode)) return;
                rjCodeElement.innerText = "[ ";
                //构造chain
                for (let i = 0; i < chain.length; i++) {
                    const rj = chain[i];
                    let e = Popup.createCopyTag("span", rj);
                    e.innerText = rj;
                    e.classList.add(VOICELINK_IGNORED_CLASS);
                    if(i === 0) {
                        //第一个元素,也就是当前RJ号
                        e.style.setProperty("font-weight", "bold", "important");
                        e.style.setProperty("text-decoration", "underline", "important");
                    }else{
                        rjCodeElement.appendChild(document.createTextNode(" → "));
                    }
                    rjCodeElement.appendChild(e);
                }
                rjCodeElement.appendChild(document.createTextNode(" ]"));
            });

            //清除原有信息并展示加载界面
            for(let child of [...this.popupElement.info_container.children]){
                if(child === this.popupElement.loader) continue;
                child.remove();
            }
            ele.loader.style.setProperty("display", "flex", "important");  //display = "flex !important";
            WorkPromise.getWorkCategory(rjCode).then(category => {
                if(!Popup.isCurrentWork(rjCode)) return;
                this.set_info_container(rjCode, category);
            }).catch(e => {
                if (!Popup.isCurrentWork(rjCode)) return;
                //默认other
                this.set_info_container(rjCode, "other");
            });

            Popup.move(e);
        },

        setFoundState(found){
            const ele = Popup.popupElement;

            ele.not_found.style.setProperty("display", found ? "none" : "block", "important");
            //ele.img.container.style.setProperty("display", found && !Popup.hideImg ? "block" : "none", "important");
            ele.left_panel.style.setProperty("display", found ? "flex" : "none", "important");
            ele.right_panel.style.setProperty("display", found ? "block" : "none", "important");
            ele.title.style.setProperty("display", found ? "block" : "none", "important");
            ele.rj_code.style.setProperty("display", found ? "block" : "none", "important");
            ele.info_container.style.setProperty("display", found ? "block" : "none", "important");
            ele.hint.style.setProperty("display", found ? "block" : "none", "important");
            /*ele.dl_count.style.setProperty("display", found ? "block" : "none", "important");
            ele.circle_name.style.setProperty("display", found ? "block" : "none", "important");
            ele.debug.style.setProperty("display", found ? "block" : "none", "important");
            ele.translator_name.style.setProperty("display", found ? "block" : "none", "important");
            ele.release_date.style.setProperty("display", found ? "block" : "none", "important");
            ele.update_date.style.setProperty("display", found ? "block" : "none", "important");
            ele.age_rating.style.setProperty("display", found ? "block" : "none", "important");
            ele.voice_actor.style.setProperty("display", found ? "block" : "none", "important");
            ele.music.style.setProperty("display", found ? "block" : "none", "important");
            ele.genre.style.setProperty("display", found ? "block" : "none", "important");
            ele.file_size.style.setProperty("display", found ? "block" : "none", "important");*/
        },

        /**
         * 创建可复制标签
         * @param tag {string|HTMLElement} 标签名或标签对象(如果为标签对象,则会将对象原地转化成可复制标签)
         * @param copyText {string} 需要复制的文本
         * @param isTitle {boolean} 是否为标题元素(使用特殊class)
         * @param hint {string} 提示栏显示的提示文本
         * @returns {HTMLElement} 创建/转换后的标签
         */
        createCopyTag: function (tag, copyText, isTitle = false, hint = isTitle ? localizePopup(localizationMap.hint_copy_all) : localizePopup(localizationMap.hint_copy)) {
            tag = (typeof tag === "string") ? document.createElement(tag) : tag;
            if(isTitle) tag.classList.add("info-title");

            //添加自定义方法
            tag.getCopyText = () => tag.getAttribute("copy-text");
            tag.setCopyText = text => {
                if(!text){
                    tag.removeAttribute("copy-text");
                    return;
                }
                tag.setAttribute("copy-text", text);
            };

            tag.getSecondaryCopyText = () => tag.getAttribute("sec-copy-text");
            tag.setSecondaryCopyText = text => {
                if(!text){
                    tag.removeAttribute("sec-copy-text");
                    return;
                }
                tag.setAttribute("sec-copy-text", text);
            };

            tag.getHint = () => tag.getAttribute("hint");
            tag.setHint = hint => {
                tag.setAttribute("hint", hint);
            };

            tag.setCopyText(copyText);
            tag.setHint(hint);
            tag.addEventListener("click", e => {
                const attr = e.altKey ? "sec-copy-text" : "copy-text";
                if(!tag.hasAttribute(attr)) return;
                GM_setClipboard(tag.getAttribute(attr), "text")?.finally();
                // navigator.clipboard.writeText(tag.getAttribute(attr)).finally();
            });
            tag.addEventListener("mouseenter", e => {
                let hint = tag.getHint();
                Popup.popupElement.hint.innerText = hint ? hint : Popup.popupElement.hint.innerText;
            })
            return tag;
        },

        /**
         * 显示某行的信息
         * @param rjCode {string} 信息对应的RJ号
         * @param id {string} 信息对应的ID
         * @param rowElement {HTMLElement} 行对应的Element元素
         * @param title {string} 行信息标题
         * @param contentProvider {Promise<any|Array|HTMLElement>} 无参内容Provider,返回字符串/字符串列表等用于生成文本
         * @param suffixProvider {Promise<HTMLElement>} 无参后缀Provider,返回一个Element用于放在Content后面
         * @param contentSeperator {string, HTMLElement}  Content如果是列表,则该内容为作为分隔符
         * @param contentSeperatorText {string} 如果采用HTMLElement的分隔符,则在复制的时候需要有一个文本表示
         */
        set_info_row: function (rjCode, id, rowElement, title, contentProvider, suffixProvider, contentSeperator = " ", contentSeperatorText = undefined ){
            const settingId = `_s_${id}`;
            //如果设置了不展示信息,或信息没在设置中定义,则不显示
            if(!settings[settingId]) return;

            const ele = Popup.popupElement;
            const popup = ele.popup;
            const titleElement = Popup.createCopyTag("span", "", true);
            titleElement.innerText = title;
            const contentElement = Popup.createCopyTag("span", null);
            contentElement.innerText = "Loading...";

            rowElement.innerHTML = Csp.createHTML("");
            rowElement.appendChild(titleElement);
            rowElement.appendChild(contentElement);

            contentProvider.then(contents => {
                if(!Popup.isCurrentWork(rjCode)) return;
                if(!Array.isArray(contents)){
                    //单个结果转化成列表
                    contents = [contents];
                }

                //处理结果列表
                contentElement.setCopyText(null);
                contentElement.innerText = "";
                //以指定的分隔符文本为准,不指定分隔符文本再使用分隔符内的文本
                let sepText = contentSeperatorText ? contentSeperatorText : contentSeperator.toString();
                let sep;
                if(typeof contentSeperator === "string"){
                    sep = document.createElement("span");
                    sep.innerText = contentSeperator;
                }else{
                    sep = contentSeperator;
                }

                let contentsText = [];
                for (let i = 0; i < contents.length; i++) {
                    let c = contents[i];
                    if(i > 0) {
                        //如果Seperator是Element,则直接复制一个出来添加,否则创建一个span然后把内容转换成文本放进去。
                        contentElement.appendChild(sep.cloneNode(true));
                    }

                    let element;
                    if(c instanceof HTMLElement){
                        element = c;
                    }else{
                        element = Popup.createCopyTag("a", c);
                        element.innerText = c;
                    }

                    //将复制文本加入复制列表
                    const copyText = element.getAttribute("copy-text");
                    if(copyText) contentsText.push(copyText);

                    contentElement.appendChild(element);
                }

                //为标题添加复制文本
                titleElement.setCopyText(contentsText.join(sepText));
            }).catch(e => {
                if(!Popup.isCurrentWork(rjCode)) return;
                rowElement.innerHTML = Csp.createHTML("");
                //console.error(e);
            }).finally(() => {
                Popup.adjustPopup(ele._state.mouseX, ele._state.mouseY, true);
            });

            if(suffixProvider){
                suffixProvider.then((element) => {
                    if(!Popup.isCurrentWork(rjCode)) return;
                    rowElement.appendChild(element);
                }).catch(_ => {});
            }

            return rowElement;
        },
        set_dl_count: function (rjCode, category){
            const id = `${category}__dl_count`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.dl_count, localizePopup(localizationMap.dl_count),
                WorkPromise.getDLCount(rjCode));
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_circle_name: function (rjCode, category){
            const id = `${category}__circle_name`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.circle_name, localizePopup(localizationMap.circle_name),
                WorkPromise.getCircle(rjCode), null);
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_translator_name: function (rjCode, category){
            const id = `${category}__translator_name`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.translator_name,
                localizePopup(localizationMap.translator_name), WorkPromise.getTranslatorName(rjCode), null);
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_release_date: function (rjCode, category){
            const id = `${category}__release_date`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.release_date, localizePopup(localizationMap.release_date),
                WorkPromise.getReleaseDate(rjCode).then(async (date) => {
                    const [dateStr, isAnnounce] = date;
                    const e = Popup.createCopyTag("a", dateStr);
                    e.innerText = dateStr;
                    if(isAnnounce) e.style.setProperty("color", "gold", "important");
                    return e;
                }), WorkPromise.getReleaseCountDownElement(rjCode).then(element => {
                    element.style.setProperty("margin-left", "16px", "important");
                    return element;
                }));

            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_update_date: function (rjCode, category){
            const id = `${category}__update_date`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.update_date, localizePopup(localizationMap.update_date),
                WorkPromise.getUpdateDate(rjCode));
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_age_rating: function (rjCode, category){
            const id = `${category}__age_rating`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.age_rating, localizePopup(localizationMap.age_rating),
                WorkPromise.getAgeRating(rjCode).then(rating => {
                    let ratingClass = `${VOICELINK_CLASS}_age-all`;
                    if(rating.includes("18")){
                        ratingClass = `${VOICELINK_CLASS}_age-18`;
                    }
                    let e = Popup.createCopyTag("a", rating);
                    e.innerText = rating;
                    e.classList.add(ratingClass);
                    return e;
                }));
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_scenario: function (rjCode, category){
            const id = `${category}__scenario`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.scenario, localizePopup(localizationMap.scenario),
                WorkPromise.getScenario(rjCode), null, " / ");
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_illustration: function (rjCode, category){
            const id = `${category}__illustration`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.illustration, localizePopup(localizationMap.illustration),
                WorkPromise.getIllustrator(rjCode), null, " / ");
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_voice_actor: function (rjCode, category){
            const id = `${category}__voice_actor`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.voice_actor, localizePopup(localizationMap.voice_actor),
                WorkPromise.getCV(rjCode), null, " / ");
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_music: function (rjCode, category) {
            const id = `${category}__music`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.music, localizePopup(localizationMap.music),
                WorkPromise.getMusic(rjCode), null, " / ");
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_genre: function (rjCode, category){
            const id = `${category}__genre`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.genre, localizePopup(localizationMap.genre),
                WorkPromise.getTags(rjCode), null, "\u3000");
            if(element) Popup.popupElement.info_container.appendChild(element);
        },
        set_file_size: function (rjCode, category){
            const id = `${category}__file_size`;
            const element = Popup.set_info_row(rjCode, id, Popup.popupElement.file_size, localizePopup(localizationMap.file_size),
                WorkPromise.getFileSize(rjCode));
            if(element) Popup.popupElement.info_container.appendChild(element);
        },

        get_tag: function (text, tagClass) {
            if(!tagClass.startsWith(`${VOICELINK_CLASS}_`)){
                tagClass = `${VOICELINK_CLASS}_${tagClass}`
            }
            const tag = document.createElement("span");
            tag.classList.add(`${VOICELINK_CLASS}_tag_tight`);
            tag.classList.add(tagClass);
            tag.innerText = text;
            return tag;
        },
        get_tag_rate: async function (rjCode) {
            let rate = await WorkPromise.getRateAvg(rjCode);
            let cot = await WorkPromise.getRateCount(rjCode);
            return Popup.get_tag(`${rate.toFixed(2)}★` + (settings._s_show_rate_count ? ` (${cot})` : ""), "tag-yellow");
        },
        get_tag_no_longer_available: async function (rjCode) {
            let sale = await WorkPromise.getSale(rjCode);
            if(sale) return;
            return Popup.get_tag(localizePopup(localizationMap.tag_no_longer_available),
                "tag-gray");
        },
        get_tag_work_type: async function (rjCode) {
            let type = await WorkPromise.getWorkTypeText(rjCode);
            let tagClass = "tag-gray";
            switch (type) {
                case localizePopup(localizationMap.work_type_game):
                    tagClass = "tag-purple";
                    break;
                case localizePopup(localizationMap.work_type_comic):
                    tagClass = "tag-green";
                    break;
                case localizePopup(localizationMap.work_type_illustration):
                    tagClass = "tag-teal";
                    break;
                case localizePopup(localizationMap.work_type_novel):
                    tagClass = "tag-gray";
                    break;
                case localizePopup(localizationMap.work_type_video):
                    tagClass = "tag-darkblue";
                    break;
                case localizePopup(localizationMap.work_type_voice):
                    tagClass = "tag-orange";
                    break;
                case localizePopup(localizationMap.work_type_music):
                    tagClass = "tag-yellow";
                    break;
                case localizePopup(localizationMap.work_type_tool):
                    tagClass = "tag-gray";
                    break;
                case localizePopup(localizationMap.work_type_voice_comic):
                    tagClass = "tag-blue";
                    break;
                case localizePopup(localizationMap.work_type_other):
                    tagClass = "tag-gray";
                    break;
                default:
                    tagClass = "tag-gray";
                    break;
            }
            return Popup.get_tag(type, tagClass);
        },
        get_tag_translatable: async function (rjCode) {
            let able = await WorkPromise.getTranslatable(rjCode);
            if(!able) return;
            return Popup.get_tag(localizePopup(localizationMap.tag_translatable),
                "tag-green");
        },
        get_tag_not_translatable: async function (rjCode) {
            let able = await WorkPromise.getTranslatable(rjCode);
            let translated = await WorkPromise.getTranslated(rjCode);
            if(able || translated) return;
            return Popup.get_tag(localizePopup(localizationMap.tag_not_translatable),
                "tag-red");
        },
        get_tag_translated: async function (rjCode) {
            let translated = await WorkPromise.getTranslated(rjCode);
            if(!translated) return;
            return Popup.get_tag(localizePopup(localizationMap.tag_translated), "tag-teal");
        },
        get_tag_bonus_work: async function (rjCode) {
            let bonus = await WorkPromise.getBonus(rjCode);
            if(!bonus) return;
            return Popup.get_tag(localizePopup(localizationMap.tag_bonus_work),
                "tag-yellow");
        },
        get_tag_has_bonus: async function (rjCode) {
            let has = await WorkPromise.getHasBonus(rjCode);
            if(!has) return;
            return Popup.get_tag(localizePopup(localizationMap.tag_has_bonus),
                "tag-orange");
        },
        get_tag_language_support: async function (rjCode) {
            const lang = await WorkPromise.getLanguages(rjCode);
            if(!lang || lang.length <= 0){
                return;
            }
            let txt = "";
            lang.forEach(l => {
                txt += ` | ${l}`;
            });
            txt = txt.substring(3);
            return Popup.get_tag(txt, "tag-pink");
        },
        get_tag_file_format: async function (rjCode) {
            const format = await WorkPromise.getFileFormats(rjCode);
            if(!format || format.length <= 0){
                return;
            }
            let txt = "";
            format.forEach(f => {
                txt += ` | ${f}`;
            });
            txt = txt.substring(3);
            return Popup.get_tag(txt, "tag-darkblue");
        },
        get_tag_ai: async function (rjCode) {
            const ai = await WorkPromise.getAIUsedText(rjCode);
            if(!ai) return;
            return Popup.get_tag(ai, "tag-purple");
        },
        get_translatable_tag: async function (rjCode, tag_id) {
            if(settings[`_s_${tag_id}`] !== true) return;

            if(tag_id.startsWith("tag_")) tag_id = tag_id.substring(4);
            const t = await WorkPromise.getWorkPromise(rjCode).translatable;
            const stat = t[tag_id];

            const hasRequest = stat.request > 0;
            const hasSale = stat.sale > 0;
            const displayCount = stat.agree || hasRequest || hasSale;
            const lang = tag_id.substring("translation_request_".length);
            const tag = Popup.get_tag(`${localizePopup(localizationMap[`language_${lang}_abbr`])}${stat.agree ? "" : (stat.agree === false ? " ✘" : " ?")} ${displayCount ? ` ${stat.request}-${stat.sale}` : ""}`,
                hasSale ? "tag-green" : (hasRequest ? "tag-orange" : "tag-gray"));
            tag.classList.add(`${VOICELINK_CLASS}_tag_small`);
            return tag;
        },
        get_tag_base_search: async function(rjCode, searchProfile) {
            const textPrefix = `${searchProfile.name}: `;
            const tag = Popup.get_tag(`${textPrefix}⏳`, "tag-gray");
            let urlList = [];
            const updateUrlList = function (result) {
                urlList = [];

                if(!result) {
                    urlList.push(searchProfile.showUrlTemplate?.replaceAll("%s", rjCode));
                    return;
                }

                let findCurrent = false;
                for (const workno of Object.keys(result.result)) {
                    let url = searchProfile.showUrlTemplate?.replaceAll("%s", workno);
                    urlList.push(url);

                    findCurrent = findCurrent || (workno === rjCode);
                }

                if(!findCurrent) {
                    let url = searchProfile.showUrlTemplate?.replaceAll("%s", rjCode);
                    urlList.unshift(url);
                }

                urlList.reverse();
            }

            const onClick = function (e) {
                for (const url of urlList) {
                    //未配置仓库地址等原因导致url为空时跳过
                    if(!url) continue;
                    GM_openInTab(url);
                }
            }

            const updateTag = (result, done, error = false, errText = "error") => {
                if(error) {
                    updateUrlList(null);

                    tag.classList.remove("tag-gray", "tag-orange", "tag-green", "tag-blue");
                    tag.classList.add("tag-gray");
                    tag.innerText = `${textPrefix}❌${errText}`;
                    return;
                }

                updateUrlList(result);

                let info = result.analyze();
                let tagClass = `${VOICELINK_CLASS}_tag-gray`;
                tag.innerText = `${textPrefix}${result.getStatusText()}${done ? "" : "⏳"}`;
                tag.classList.remove(`${VOICELINK_CLASS}_tag-gray`,
                    `${VOICELINK_CLASS}_tag-orange`, `${VOICELINK_CLASS}_tag-green`, `${VOICELINK_CLASS}_tag-blue`);
                if(info.hasCurrent) {
                    tagClass = `${VOICELINK_CLASS}_tag-green`;
                } else if (info.hasTranslation) {
                    tagClass = `${VOICELINK_CLASS}_tag-blue`;
                } else if (info.hasOriginal) {
                    tagClass = `${VOICELINK_CLASS}_tag-orange`;
                }
                tag.classList.add(tagClass);

                return Popup.isCurrentWork(rjCode);
            }

            WorkPromise.getSearchResult(rjCode, searchProfile, result => {
                return updateTag(result, false);
            }).then(result => {
                return updateTag(result, true);
            }).catch(e => {
                console.error(e);
                return updateTag(null, false, true);
            });

            tag.classList.add(`${VOICELINK_CLASS}_tag_small`);
            tag.addEventListener("click", onClick);
            tag.style.cursor = "pointer";
            return tag;
        },

        get_tag_container: function (rjCode, tag_list) {
            const container = document.createElement("div");
            container.classList.add(`${VOICELINK_CLASS}_tags`);
            for (const tag_id of tag_list) {
                if(settings[`_s_${tag_id}`] !== true) continue;

                let shadowTag = document.createElement("span");
                shadowTag.style.setProperty("display", "none", "important");  //display = "none !important";
                shadowTag.setAttribute("data-id", tag_id);
                container.appendChild(shadowTag);

                let tag_get = this[`get_${tag_id}`];
                tag_get(rjCode).then(tag => {
                    if(tag){
                        container.insertBefore(tag, shadowTag);
                        shadowTag.remove();
                    }
                });
            }
            return container;
        },
        get_translatable_tag_container: function (rjCode, tag_list) {
            const container = document.createElement("div");
            container.classList.add(`${VOICELINK_CLASS}_tags`);
            container.style.setProperty("margin-top", "0", "important");  //marginTop = "0 !important";
            for (const tag_id of tag_list) {
                let shadowTag = document.createElement("span");
                shadowTag.style.setProperty("display", "none", "important");  //display = "none !important";
                shadowTag.setAttribute("data-id", tag_id);
                container.appendChild(shadowTag);

                Popup.get_translatable_tag(rjCode, tag_id).then(tag => {
                    if(tag){
                        container.insertBefore(tag, shadowTag);
                        shadowTag.remove();
                    }
                }).catch(e => {});
            }
            return container;
        },
        get_search_tag_container: function (rjCode) {
            const container = document.createElement("div");
            container.classList.add(`${VOICELINK_CLASS}_tags`);
            container.style.setProperty("margin-top", "0", "important");  //marginTop = "0 !important";

            //每个启用的仓库各生成一个标签,顺序与仓库列表顺序一致
            for(const searchProfile of settings._ss_search_profiles) {
                const shadowTag = document.createElement("span");
                shadowTag.style.setProperty("display", "none", "important");
                container.appendChild(shadowTag);

                Popup.get_tag_base_search(rjCode, searchProfile).then(tag => {
                    if(tag){
                        container.insertBefore(tag, shadowTag);
                    }
                    shadowTag.remove();
                }).catch(e => {
                    shadowTag.remove();
                });
            }
            return container;
        },

        //整合顺序
        set_info_container: function (rjCode, category) {
            //清除上次的信息
            for(let child of [...this.popupElement.info_container.children]){
                if(child === this.popupElement.loader) {
                    child.style.setProperty("display", "none", "important");  //display = "none !important";
                    continue;
                }
                child.remove();
            }

            //TAG部分
            const infoContainer = this.popupElement.info_container;

            let tagContainer = null;
            if(settings._s_tag_main_switch === true){
                const container = this.get_tag_container(rjCode,
                    settings[`_s_tag_display_order`]);
                tagContainer = container;
                infoContainer.appendChild(container);
            }

            //翻译申请情况
            const shadowContainer = document.createElement("div");
            shadowContainer.style.setProperty("display", "none", "important");  //display = "none !important";
            infoContainer.appendChild(shadowContainer);
            WorkPromise.getTranslatable(rjCode).then(able => {
                if(!Popup.isCurrentWork(rjCode)) return;
                if(able && settings._s_tag_translation_request === true){
                    const translatableContainer = this.get_translatable_tag_container(rjCode,
                        settings._s_tag_translation_request_display_order);
                    infoContainer.insertBefore(translatableContainer, shadowContainer);
                    shadowContainer.remove();
                }
            }).catch(e => {});

            //仓库搜索情况
            if(settings._s_base_search && settings._ss_search_profiles.length > 0){
                const searchContainer = Popup.get_search_tag_container(rjCode);
                infoContainer.appendChild(searchContainer);
            }

            //信息部分
            const order = settings[`_s_${category}__info_display_order`];
            order.forEach(id => {
                try{
                    id = id.substring(id.indexOf("__") + 2);
                    this["set_" + id](rjCode, category);
                }catch (e) {
                    console.error(e);
                }
            });

            const debugElement = document.createElement("div");
            this.popupElement.info_container.appendChild(debugElement);
            WorkPromise.getDebug(rjCode).then(t => {
                debugElement.innerHTML = Csp.createHTML(t);
            });
        },

        //调整弹框位置
        adjustPopup: function (mouseX, mouseY, force = false){
            // console.log("定位修正")

            //定位修正
            const popup = Popup.popupElement.popup;
            const ele = Popup.popupElement;
            if(!Popup.pinRJ || force){
                if (popup.offsetWidth + mouseX + 10 < window.innerWidth - 10) {
                    popup.style.setProperty("left", (mouseX + 10) + "px", "important");
                }
                else {
                    popup.style.setProperty("left", (window.innerWidth - popup.offsetWidth - 10) + "px", "important");
                }
            }

            let rect = popup.getBoundingClientRect();
            if(!Popup.pinRJ || force || rect.top < 0 || rect.bottom > window.innerHeight){
                if (mouseY > window.innerHeight / 2) {
                    let top = Math.max(mouseY - popup.offsetHeight - 8, 0);
                    popup.style.setProperty("top", top + "px", "important");
                }
                else {
                    let top = Math.min(mouseY + 20, window.innerHeight - popup.offsetHeight);
                    popup.style.setProperty("top", top + "px", "important");
                }
            }

            //大小修正
            let currentFontSize = popup.computedStyleMap().get("font-size").toString();
            currentFontSize = parseFloat(currentFontSize.substring(0, Math.max(currentFontSize.indexOf("px"), 1)));
            const sizeLevel = [15, 14.5, 14, 13.5, 13, 12.5, 12];
            let size = sizeLevel[sizeLevel.length - 1];
            if(popup.offsetHeight > window.innerHeight){
                //计算popup的高度与window高度的比值,找到离它最相近且更大的当前字体大小和sizeLevel的比值
                for (const s of sizeLevel) {
                    if(popup.offsetHeight / window.innerHeight < currentFontSize / s){
                        size = s;
                        break;
                    }
                }
                popup.style.setProperty("font-size", size + "px", "important");
            }

            //封面图位置修正
            ele.img.container.style.top = `${Math.max(0, -popup.offsetTop)}px`;
        },

        pinRJ: undefined,
        setPinState: function (rjCode, pin, close = true){
            const ele = Popup.popupElement;
            const popup = ele.popup;
            if(!pin){
                //关闭弹框
                popup.style.setProperty("pointer-events", "none", "important");
                Popup.pinRJ = undefined;
                popup.removeAttribute("pin");

                if(close) popup.style.setProperty("display", "none", "important");

                //取消注册自动关闭监听
                document.removeEventListener("keyup", Popup.keyup);
                document.removeEventListener("mousemove", Popup.domMove);
                return
            }

            popup.style.setProperty("pointer-events", "auto", "important");
            Popup.pinRJ = rjCode;
            popup.setAttribute("pin", "");

            //添加监听器
            document.addEventListener("keyup", Popup.keyup);
            document.addEventListener("mousemove", Popup.domMove);
        },
        hasPinned: function (){
            return Popup.popupElement.popup.hasAttribute("pin");
        },
        /**
         * @param e {KeyboardEvent}
         */
        isHoldPinKey: function(e){
            if(getOS() === "Mac"){
                return e.metaKey;
            }
            return e.ctrlKey;
        },
        /**
         * @param e {KeyboardEvent}
         */
        isPinKeyDown: function (e) {
            if(getOS() === "Mac"){
                return e.key === "Meta";
            }
            return e.key === "Control";
        },

        /**
         * 监听网页内的鼠标移动事件,来保证弹框正常移除
         * @param e {MouseEvent}
         */
        domMove: function (e) {
            if(!Popup.hasPinned() || Popup.isHoldPinKey(e)){
                return;
            }
            Popup.setPinState(null, false);
        },
        /**
         * 鼠标移动到链接上触发
         * @param e {MouseEvent}
         */
        over: async function (e) {
            const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
            if(!target || !target.classList.contains(VOICELINK_CLASS)) return;

            const rjCode = target.getAttribute(RJCODE_ATTRIBUTE);
            if(rjCode === null) return;

            //记录鼠标位置
            let ele = Popup.popupElement;
            ele._state.mouseX = e.clientX;
            ele._state.mouseY = e.clientY;

            const pinKey = getOS() === "Mac" ? "Command" : "CTRL";

            //如果用户固定了弹框,则提示用户必须ctrl关闭弹框才能解析
            if(Popup.isHoldPinKey(e) && Popup.pinRJ){
                ele.hint.innerText = localizePopup(localizationMap.hint_unpin).replace(/{pin_key}/g, pinKey);
                return;
            }else{
                //没有固定弹框的话清理pinRJ,因为有时候pinRJ没办法被keyup清理(如keyup未触发)
                Popup.pinRJ = undefined
                ele.hint.innerText = localizePopup(localizationMap.hint_pin).replace(/{pin_key}/g, pinKey);
            }

            //修正链接
            if(target.hasAttribute("voicelink-linkified")){
                WorkPromise.getWorkPromise(rjCode).info.then(info => {
                    if(info.is_announce === true){
                        target.href = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
                    }
                });
            }

            let popup = document.querySelector(`div#${VOICELINK_CLASS}-voice-popup`);  // + rjCode);
            if (popup) {
                popup.style.setProperty("display", "flex", "important");  //display = "flex !important";
                //先将字体大小变回原样
                popup.style.setProperty("font-size", "15.4px", "important");
            }
            else {
                Popup.makePopup();
                popup = ele.popup;
            }
            await Popup.updatePopup(e, rjCode);

            //如果按住了CTRL,则将popup可被点击,否则设置穿透
            //并设置Copy显示情况
            if(Popup.isHoldPinKey(e)){
                Popup.setPinState(rjCode, true)
                ele.hint.innerText = localizePopup(localizationMap.hint_unpin).replace(/{pin_key}/g, pinKey);
            }else{
                Popup.setPinState(rjCode, false, false)
            }

            //设置焦点至链接上
            target.focus();
            target.style.setProperty("outline", "none", "important");

        },

        /**
         * 鼠标离开时触发
         * @param e {MouseEvent}
         */
        out: function (e) {
            //如果固定则禁止关闭
            if(Popup.isHoldPinKey(e)) {
                return
            }

            const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
            if(!target || !target.classList.contains(VOICELINK_CLASS)) return;

            const rjCode = target.getAttribute(RJCODE_ATTRIBUTE);
            if(rjCode === null) return;

            //取消固定并关闭
            Popup.setPinState(rjCode, false)

            //取消focus
            target.blur();
            target.style.setProperty("outline", null);
        },

        /**
         * 鼠标移动时触发
         * @param e {MouseEvent}
         */
        move: function (e) {
            const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
            if(!target || !target.classList.contains(VOICELINK_CLASS)) return;

            const popup = document.querySelector(`div#${VOICELINK_CLASS}-voice-popup`);  // + rjCode);
            if(!popup) return;

            const rjCode = e.target.getAttribute(RJCODE_ATTRIBUTE);
            if(rjCode === null) return;

            let ele = Popup.popupElement;
            ele._state.mouseX = e.clientX;
            ele._state.mouseY = e.clientY;

            //焦点不在浏览器上的时候无法触发keydown,因此需要用move来辅助激活pin
            if(Popup.isHoldPinKey(e) && !Popup.pinRJ){
                //按下pin键但没激活pin模式的时候手动激活
                Popup.setPinState(rjCode, true);
            }

            //如果弹框已固定且固定的并非当前所选链接RJ号,则不进行定位修正
            if(Popup.pinRJ && rjCode !== Popup.pinRJ){
                return;
            }

            Popup.adjustPopup(e.clientX, e.clientY);

        },

        /**
         * 按键按下时触发
         * @param e {KeyboardEvent}
         */
        keydown: function (e) {
            const target = isInDLSite() ? e.target : getVoiceLinkTarget(e.target);
            if(!target || !target.classList.contains(VOICELINK_CLASS)) return;

            const rjCode = target.getAttribute(RJCODE_ATTRIBUTE);
            if(rjCode === null) return;

            let popup = Popup.popupElement.popup;
            if(popup.style.display !== "none" && Popup.isPinKeyDown(e)){
                //按住CTRL以固定显示弹框
                Popup.setPinState(rjCode, true);
            }
        },

        /**
         * 按键抬起时触发
         * @param e {KeyboardEvent}
         */
        keyup: function (e) {
            let popup = Popup.popupElement.popup;
            if(popup && Popup.isPinKeyDown(e)){
                Popup.setPinState(null, false);
            }
        }
    }

    //endregion

    //region 作品信息爬取

    const WorkPromise = {

        checkNotNull: function (obj){
            if(obj === null || obj === undefined) throw new Error();
            return obj;
        },

        getWorkPromise: function (rjCode){
            if(work_promise[rjCode]){
                return work_promise[rjCode];
            }
            work_promise[rjCode] = DLsite.getWorkRequestPromise(rjCode);
            return work_promise[rjCode];
        },

        getFound: async function(rjCode){
            try{
                const data = await WorkPromise.getWorkPromise(rjCode).api2;
                if(data && data.product_id !== undefined) return true;

                //否则再次检查api1
                const api = await WorkPromise.getWorkPromise(rjCode).api;
                return api && api.is_sale !== undefined;
            }catch (e){
                //说明是网络问题,删除缓存并返回true
                delete work_promise[rjCode];
                return true;
            }
        },

        getTranslationInfo: async function(rjCode){
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api2;
            if(data.translation_info) return data.translation_info;

            data = await p.api;
            return data.translation_info ? data.translation_info : {};
        },

        getRJChain: async function(rjCode) {
            //RJxxx → RJxxx → RJxxx,这样从子级指向父级
            const trans = await WorkPromise.getTranslationInfo(rjCode);
            let chain = [rjCode];
            if(trans.is_child){
                chain.push(trans.parent_workno, trans.original_workno);
            }else if(trans.is_parent){
                chain.push(trans.original_workno);
            }
            return chain;
        },

        getParentRJ: async function(rjCode){
            try{
                const p = WorkPromise.getWorkPromise(rjCode);
                let trans = await WorkPromise.getTranslationInfo(rjCode);
                if(trans.is_original || trans.is_parent) return rjCode;
                if(trans.parent_workno) return trans.parent_workno;

                let data = await p.info;
                return data.parentWork;
            }catch (e){
                return null;
            }
        },

        getGirls: async function(rjCode){
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api2;
            if(data.sex_category && data.sex_category === 2) return true;
            if(data.site_id === "girls") return true;

            //否则再次检查api1
            data = await WorkPromise.getWorkPromise(rjCode).api;
            WorkPromise.checkNotNull(data.is_girls)
            return data.is_girls;
        },

        getAnnounce: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const info = await p.info;
            return info.is_announce;
        },

        getSale: async function(rjCode, checkAnnounce = true){
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            if(!checkAnnounce){
                return data.is_sale;
            }
            return data.is_sale || await WorkPromise.getAnnounce(rjCode);
        },

        getDLCount: async function (rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            WorkPromise.checkNotNull(data.dl_count);
            return data.dl_count;
        },

        getRateAvg: async function (rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            if(data.rate_average_2dp) return data.rate_average_2dp;

            //还可以累加api2的结果获得
            data = await p.api2;
            this.checkNotNull(data.rate_count_detail);
            let sum = 0;
            let count = 0;
            for (const key in data.rate_count_detail) {
                let rate = parseInt(key);
                let cot = parseInt(data.rate_count_detail[key]);
                count += cot
                sum += rate * cot;
            }
            return sum / count;
        },

        getRateCount: async function (rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            if(data.rate_count) return data.rate_count;

            //还可以累加api2的结果获得
            data = await p.api2;
            this.checkNotNull(data.rate_count_detail);
            let count = 0;
            for (const key in data.rate_count_detail) {
                count += parseInt(data.rate_count_detail[key]);
            }
            return count;
        },

        getWishlistCount: async function (rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            this.checkNotNull(data.wishlist_count);
            return data.wishlist_count;
        },

        getPriceText: async function (rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            //TODO: 价格以后再加,还要考虑汇率和添加设置项
        },

        getBonus: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            return !data.is_sale && data.is_free && data.is_oly && data.wishlist_count === 0;
            // return data.is_bonus;
        },

        getHasBonus: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            let data = await p.api;
            return data.bonuses && data.bonuses.length > 0;
        },

        getTranslatable: async function(rjCode) {
            const trans = await WorkPromise.getTranslationInfo(rjCode);
            return trans.is_translation_agree === true;
        },

        getTranslated: async function(rjCode) {
            const trans = await WorkPromise.getTranslationInfo(rjCode);
            return trans.is_parent === true || trans.is_child === true;
        },

        getLanguages: async function(rjCode){
            //返回字符串数组,根据popup设置的语言返回支持的语言列表
            const p = WorkPromise.getWorkPromise(rjCode);
            let api = await p.api2;
            api = api.options ? api : await p.api;
            const options = api.options?.split("#");
            const result = [];
            for (const key in LANG_MAP) {
                const lang = LANG_MAP[key];
                if(options?.includes(key)) result.push(lang);
            }
            return result;
        },

        getFileFormats: async function(rjCode){
            //返回字符串数组,返回文件格式列表
            const result = [];
            const p = WorkPromise.getWorkPromise(rjCode);
            let api = await p.api2;
            if(api.file_type === "EXE"){
                result.push("EXE");
            }else if(api.file_type_string){
                result.push(api.file_type_string);
            }
            if(api.file_type_special) result.push(api.file_type_special);

            if(!api.options) api = await p.api;
            if(api.options && api.options.includes("WPD")){
                result.push("PDF");
            }
            if(api.options && api.options.includes("WAP")){
                result.push("APK");
            }

            return result;
        },

        getAIUsedText: async function(rjCode) {
            //返回是否使用或部分使用AI,根据popup语言返回字符串。
            const p = WorkPromise.getWorkPromise(rjCode);
            let api = await p.api2;
            api = api.options ? api : await p.api;
            const options = api.options ? api.options : "";
            if(options.includes("AIG")){
                return localizePopup(localizationMap.tag_aig);
            }else if(options.includes("AIP")){
                return localizePopup(localizationMap.tag_aip);
            }
            return null;
        },

        getDebug: async function(rjCode){
            return "";
            const work = WorkPromise.getWorkPromise(rjCode);
            const api2 = await work.api2;
            const api = await work.api;
            const info = await work.info;
            const circle = work.circle;

            return `is_ana_api2: ${api2.is_ana}<br/>
                    is_ana_api: ${api.is_ana}`;
        },

        getWorkCategory: async function(rjCode){
            const type = await WorkPromise.getWorkType(rjCode);
            /* voice: 音声
             * game: 游戏
             * manga: 漫画/插画/音声漫画
             * video: 视频
             * novel: 小说
             * other: 其它
            */
            switch (type) {
                case 0:
                    return "voice";
                case 1:
                    return "game";
                case 2 || 3 || 8:
                    return "manga";
                case 5:
                    return "video";
                case 4:
                    return "novel";
                default:
                    return "other";
            }
        },

        getWorkTypeText: async function(rjCode) {
            const mapping = [
                localizePopup(localizationMap.work_type_voice),
                localizePopup(localizationMap.work_type_game),
                localizePopup(localizationMap.work_type_comic),
                localizePopup(localizationMap.work_type_illustration),
                localizePopup(localizationMap.work_type_novel),
                localizePopup(localizationMap.work_type_video),
                localizePopup(localizationMap.work_type_music),
                localizePopup(localizationMap.work_type_tool),
                localizePopup(localizationMap.work_type_voice_comic),
                localizePopup(localizationMap.work_type_other),
            ];
            return mapping[await WorkPromise.getWorkType(rjCode)];
        },

        getWorkType: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const api2 = await p.api2;
            let workType = api2.work_type;
            if(!workType) workType = (await p.api).work_type;

            switch (workType) {
                case "SOU":
                    return 0;
                case (["ACN", "QIZ", "ADV", "RPG", "TBL", "DNV", "SLN", "TYP", "STG", "PZL", "ETC"]
                    .includes(workType) ? workType : "ERR"):
                    return 1;
                case (["MNG", "SCM", "WBT"]
                    .includes(workType) ? workType : "ERR"):
                    return 2;
                case "ICG":
                    return 3;
                case (["NRE", "KSV"].includes(workType) ? workType : "ERR"):
                    return 4;
                case "MOV":
                    return 5;
                case "MUS":
                    return 6;
                case (["TOL", "IMT", "AMT"]
                    .includes(workType) ? workType : "ERR"):
                    return 7;
                case "VCM":
                    return 8;
                case "ET3":
                    return 9;
                default:
                    throw new Error("无法获取作品类型/未知作品类型:" + workType);
            }
        },

        getImgLink: async function(rjCode){
            let link = undefined;
            const p = WorkPromise.getWorkPromise(rjCode);

            try {
                let data = await p.api2;
                if (data.image_main && data.image_main.url) link = "https:" + data.image_main.url;
            } catch (e) {}

            if(link && !link.includes("no_img_main.gif")){
                return link;
            }

            try{
                const info = await p.info;
                WorkPromise.checkNotNull(info.img);
                return info.img;
            }catch (e) {
            }

            try{
                const apiData = await WorkPromise.getWorkPromise(rjCode).api;
                if(apiData.work_image) return "https:" + apiData.work_image;
            }catch (e){}

            throw new Error("无法获取图片链接");
        },

        getWorkTitle: async function(rjCode){
            return await WorkPromise.getWorkPromise(rjCode).translated_title;
        },

        getAgeRating: async function(rjCode){
            let p = WorkPromise.getWorkPromise(rjCode);
            let api = await p.api2;
            if(!api.age_category) api = await p.api;
            switch (api.age_category){
                case 1:
                    return "All";
                case 2:
                    return "R15";
                case 3:
                    return "R18";
            }

            const info = await p.info;
            WorkPromise.checkNotNull(info.rating);
            return info.rating;
        },

        getCircle: async function(rjCode, findOriginal = true){
            let trans = await WorkPromise.getTranslationInfo(rjCode);
            if(!trans.is_original && findOriginal){
                //使用原作RJ号开始寻找,如果找不到翻译信息就没办法了
                rjCode = trans.original_workno ? trans.original_workno : rjCode;
            }

            let work = WorkPromise.getWorkPromise(rjCode);
            let api2 = await work.api2;
            if(api2.maker_name) return api2.maker_name;

            /**
             * 接下来有两种搜索方式:
             * 1. api1 + circle接口
             * 2. info搜索
             * 前者成功率更高(下架后还能获取到api1,社团没解散就能获得社团信息),两个加载速度不确定谁快谁慢,所以把1放在前面
             */

            const circleInfo = await work.circle;
            if(circleInfo && circleInfo.name) return circleInfo.name;

            let info = await work.info;
            if(info.circle) return info.circle.trim();

            throw new Error("无法获取社团信息");
        },

        getTranslatorName: async function(rjCode){
            let trans = await WorkPromise.getTranslationInfo(rjCode);
            if(!trans.is_child) throw new Error("非翻译作品RJ号");
            return await WorkPromise.getCircle(rjCode, false);
        },

        getReleaseDate: async function(rjCode){
            const p = WorkPromise.getWorkPromise(rjCode);
            const info = await p.info;
            if(info && !info.is_announce && info.date) return [info.date.trim(), false];
            if(info && info.is_announce && info.dateAnnounce) return [info.dateAnnounce.trim(), true];

            //从api中查找发售时间
            let api = await p.api2;
            api = api.regist_date ? api : await p.api;
            WorkPromise.checkNotNull(api.regist_date)

            return [api.regist_date, api.is_announce];
        },

        getReleaseCountDownElement: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const info = await p.info;
            if(info && info.is_announce && info.dateAnnounce) {
                return DateParser.getCountDownDateElement(DateParser.parseDateStr(info.dateAnnounce, info.lang));
            }
            return null;
        },

        getUpdateDate: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const info = await p.info;
            if(info["update"]) return info["update"].trim();

            throw new Error();
        },

        getScenario: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const api2 = await p.api2;
            if(api2.creaters && api2.creaters.scenario_by && api2.creaters.scenario_by.length > 0){
                return api2.creaters.scenario_by.map(v => v.name);
            }

            //无法获取api2则直接通过html获取
            const info = await WorkPromise.getWorkPromise(rjCode).info;
            WorkPromise.checkNotNull(info.scenario);
            return info.scenario;
        },

        getIllustrator: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const api2 = await p.api2;
            if(api2.creaters && api2.creaters.illust_by && api2.creaters.illust_by.length > 0){
                return api2.creaters.illust_by.map(v => v.name);
            }

            //无法获取api2则直接通过html获取
            const info = await WorkPromise.getWorkPromise(rjCode).info;
            WorkPromise.checkNotNull(info.illustration);
            return info.illustration;
        },

        getCV: async function(rjCode){
            const p = WorkPromise.getWorkPromise(rjCode);
            const api2 = await p.api2;
            if(api2.creaters && api2.creaters.voice_by && api2.creaters.voice_by.length > 0){
                return api2.creaters.voice_by.map(v => v.name);
            }

            //无法获取api2则直接通过html获取
            const info = await WorkPromise.getWorkPromise(rjCode).info;
            WorkPromise.checkNotNull(info.cv);
            return info.cv;
        },

        getMusic: async function(rjCode) {
            const p = WorkPromise.getWorkPromise(rjCode);
            const api2 = await p.api2;
            if(api2.creaters && api2.creaters.music_by && api2.creaters.music_by.length > 0){
                return api2.creaters.music_by.map(v => v.name);
            }

            //无法获取api2则直接通过html获取
            const info = await WorkPromise.getWorkPromise(rjCode).info;
            WorkPromise.checkNotNull(info.music);
            return info.music;
        },

        getTags: async function(rjCode) {
            //注意该方法返回字符串数组而不是纯字符串
            const p = WorkPromise.getWorkPromise(rjCode);
            const api2 = await p.api2;
            if(api2.genres && api2.genres.length > 0){
                return api2.genres.map(genre => genre.name);
            }

            //无法获取api2时通过html获取
            const info = await p.info;
            WorkPromise.checkNotNull(info.tags);
            return info.tags;
        },

        getFileSizeStr: function(byteCount = 0){
            const units = ["B", "KB", "MB", "GB", "TB"];
            let unit = "B";
            for (let i = 1; byteCount >= 1024; i++){
                byteCount /= 1024;
                unit = units[i];
            }
            return `${Math.round(byteCount * 100) / 100}${unit}`;
        },

        getFileSize: async function(rjCode) {
            const trans = await WorkPromise.getTranslationInfo(rjCode);
            if(trans.is_parent){
                //翻译版本的父级没有内容信息,自然无法显示文件大小,所以需要获得原作品的大小信息
                //Child和Original都有各自的大小信息,正常获取计算即可
                rjCode = trans.original_workno ? trans.original_workno : rjCode;
            }

            const p = WorkPromise.getWorkPromise(rjCode);
            let api2 = await p.api2;
            if(api2.contents_file_size && api2.contents_file_size > 0){
                return WorkPromise.getFileSizeStr(api2.contents_file_size);
            }

            //通过html获取
            let info = trans.is_child && trans.original_workno ? await WorkPromise.getWorkPromise(trans.original_workno).info : await p.info;
            if(info.filesize) return info.filesize;

            throw new Error("无法获取文件大小信息");
        },

        mergeLinkage: function(l1, l2) {
            let linkage = {}
            for (const work of Object.values(l1)) {
                if(!work.workno) continue;
                linkage[work.workno] = work;
            }
            for (const work of Object.values(l2)) {
                if(!work.workno) continue;
                linkage[work.workno] = work;
            }
            return linkage;
        },

        cacheLinkage: function(originalWorkno, linkage) {
            //缓存与rjCode相关的关联作品信息,任意一个关联作品RJ均能找到此关联信息

            function getExpireTime() {
                //UTC+9第二天的0点
                const now = new Date();
                const nowMs = now.getTime();
                const utc9Ms = nowMs + now.getTimezoneOffset() * 60000 + 9 * 3600 * 1000;
                const localeOffset = utc9Ms - nowMs;
                const dayMs = 24 * 3600 * 1000;
                const nextDayUtc9 = utc9Ms - (utc9Ms % dayMs) + dayMs;
                return nextDayUtc9 - localeOffset;
            }

            let maxLinkMapSize = 128;
            let linkCache = DataCacheStorage.open(
                "work-linkages", maxLinkMapSize, true, true, true);

            //存入Linkage
            let langs = settings._ss_cue_lang.join();
            let data = linkCache.get(originalWorkno);
            if(Array.isArray(data)){
                //已存在部分Linkage则合并它们
                data = WorkPromise.mergeLinkage(data, linkage);
            } else {
                data = linkage;
            }
            linkCache.commit(`${originalWorkno}|${langs}`, data, getExpireTime());
        },

        getLinkageFromCache: function(originalWorkno) {
            const hashKey = `${originalWorkno}|${settings._ss_cue_lang.join()}`
            let storage = DataCacheStorage.open("work-linkages", 128, true, true, true);
            return storage.get(hashKey);
        },

        getLinkedWorks: async function(rjCode) {
            try {
                let trans = await WorkPromise.getTranslationInfo(rjCode);
                let p = await WorkPromise.getWorkPromise(rjCode);
                let api = await p.api2;
                let result = {};
                if(trans.is_original){
                    result[rjCode] = {workno: rjCode, type: "original", lang: "JPN"};
                    let languageEditions = api.language_editions;
                    if(!Array.isArray(languageEditions)) languageEditions = Object.values(languageEditions);
                    for (let edition of languageEditions) {
                        result[edition.workno] = {workno: edition.workno, type: "parent", lang: edition.lang};
                    }
                }else if(trans.is_parent) {
                    //parent作品可以获取当前语言下所有的作品关联,但无法获取其它语言作品关联,作品数更新时也无法注意到
                    result[trans.original_workno] = {workno: trans.original_workno, type: "original", lang: "JPN"};
                    result[rjCode] = {workno: rjCode, type: "parent", lang: trans.lang};
                    for (let workno of trans.child_worknos) {
                        result[workno] = {workno: workno, type: "child", lang: trans.lang}
                    }
                }else if(trans.is_child){
                    result[trans.original_workno] = {workno: trans.original_workno, type: "original", lang: "JPN"};
                    result[trans.parent_workno] = {workno: trans.parent_workno, type: "parent", lang: trans.lang};
                    result[rjCode] = {workno: rjCode, type: "child", lang: trans.lang};
                }

                return result;
            } catch (e) {
                console.error(e);
                return {};
            }

        },

        /**
         * 查找指定作品的所有语言版本作品关联
         * @param rjCode 查找关联的RJ号
         * @param useCache 是否使用缓存中的记录
         * @param saveCache 是否记录/更新至缓存
         */
        getLinkedWorksFull: async function(rjCode, useCache = true, saveCache = true) {
            let trans = await WorkPromise.getTranslationInfo(rjCode);
            if(trans.is_original === undefined || trans.is_original === null) return {};
            if(!trans.is_original) {
                //TODO 将结果和待搜索RJ号的本地关联搜索结果merge一下再返回(不存缓存,否则会破坏缓存内语言和cue_lang的对应性
                //TODO 上面那个不算,改成临时添加cue lang,然后方法改成用参数接收cue lang而不是读设置项,缓存那边也要改成参数传递
                let result = await WorkPromise.getLinkedWorksFull(trans.original_workno, useCache, saveCache);
                result = WorkPromise.mergeLinkage(result, await WorkPromise.getLinkedWorks(rjCode));
                return result;
            }

            //先尝试从缓存获取
            let cache = WorkPromise.getLinkageFromCache(rjCode)
            if(cache) {
                return cache;
            }

            let p = await WorkPromise.getWorkPromise(rjCode);
            let api = await p.api2;
            let result = {};

            result[rjCode] = {workno: rjCode, type: "original", lang: "JPN"};
            let languageEditions = api.language_editions;
            if(!Array.isArray(languageEditions)) languageEditions = Object.values(languageEditions);
            for (let edition of languageEditions) {
                if (!settings._ss_cue_lang.includes(edition.lang)) continue;
                //是需要的查询语言,进行Link递归查询
                result = WorkPromise.mergeLinkage(result, await WorkPromise.getLinkedWorks(edition.workno));
            }

            if (saveCache) WorkPromise.cacheLinkage(rjCode, result);
            return result;
        },

        //仓库搜索

        /**
         * 获取指定仓库的查询结果缓存池。
         * 每个仓库独立一个缓存池,便于扩容以及在被禁用/删除时整体清理
         * @param searchProfile {SearchProfile}
         * @returns {DataCacheStorage}
         */
        getSearchResultStorage(searchProfile) {
            return DataCacheStorage.open(`search-results-${searchProfile.id}`, SEARCH_CACHE_SIZE, true, true, true);
        },

        cacheSearchResult(searchProfile, rjCode, searchProfileHash, fullSearch, data) {
            const hashKey = `${rjCode}|${searchProfileHash}|${fullSearch}`;
            WorkPromise.getSearchResultStorage(searchProfile).commit(hashKey, data, Date.now() + SEARCH_CACHE_TTL);
        },

        getSearchResultFromCache(searchProfile, rjCode, searchProfileHash, fullSearch) {
            const hashKey = `${rjCode}|${searchProfileHash}|${fullSearch}`;
            return WorkPromise.getSearchResultStorage(searchProfile).get(hashKey);
        },

        /**
         * 清除指定仓库的查询结果缓存池
         * @param searchProfile {SearchProfile}
         */
        clearSearchResultCache(searchProfile) {
            DataCacheStorage.dropStorage(`search-results-${searchProfile.id}`);
        },

        /**
         * 获取指定仓库的搜索结果
         * @param rjCode {string}
         * @param searchProfile {SearchProfile}
         * @param progressCallback {function (result: SearchResult): boolean} 搜索的阶段性成果,如果返回false则停止搜索
         */
        getSearchResult: async function(rjCode, searchProfile, progressCallback) {
            let linkages = settings._s_full_linkage ?
                await WorkPromise.getLinkedWorksFull(rjCode) : await WorkPromise.getLinkedWorks(rjCode);
            let searchSet = new Set(Object.keys(linkages));
            let work = linkages[rjCode];
            work = new SearchWorkInfo(work.workno, work.type, work.lang);

            //检查缓存是否存在,存在则直接使用缓存结果
            let cache = WorkPromise.getSearchResultFromCache(searchProfile, rjCode, await searchProfile.getHash(), settings._s_search_linkage)
            if(Array.isArray(cache)) return new SearchResult(searchProfile.name, work, cache);

            //查询失败(如验证失效、网络异常等)时抛出错误,避免与"仓库中没有该作品"混淆
            const search = async (target) => {
                let res = await searchProfile.search(target, linkages);
                if(!Array.isArray(res)) {
                    throw new Error(`仓库「${searchProfile.name}」查询失败`);
                }
                return res;
            };

            let result = {};
            let res = await search(rjCode);
            res.forEach(v => result[v.workno] = v);

            if(!settings._s_search_linkage) {
                //直接保存并返回
                WorkPromise.cacheSearchResult(searchProfile, rjCode, await searchProfile.getHash(), false, res)
                return new SearchResult(searchProfile.name, work, res);
            }

            //---进行完整搜索---

            searchSet.delete(rjCode);
            for (const work of res) {
                searchSet.delete(work.workno);
            }

            while(searchSet.size > 0) {
                //提交当前搜索到的部分结果(最后一次的完整结果通过返回值提交,故这段放在while开头
                if(!progressCallback(new SearchResult(searchProfile.name, work, res))) {
                    //非完整结果不保存
                    return new SearchResult(searchProfile.name, work, res);
                }

                //仓库在查询过程中被停用或删除时,不再发起后续请求
                if(!isSearchProfileActive(searchProfile)) {
                    //非完整结果不保存
                    return new SearchResult(searchProfile.name, work, res);
                }

                let target = searchSet.values().next().value;
                res = await search(target);
                res.forEach(v => result[v.workno] = v);

                searchSet.delete(target);
                for(const work of res) {
                    searchSet.delete(work.workno);
                }

                //res也需要合并先前搜索到的部分结果
                res = Object.values(result);
            }

            //保存结果并返回
            WorkPromise.cacheSearchResult(searchProfile, rjCode, await searchProfile.getHash(), true, res)
            return new SearchResult(searchProfile.name, work, res);
        },
    }

    const DLsite = {
        parseWorkDOM: function (dom, rj) {
            // workInfo: {
            //     rj: any;
            //     img: string;
            //     title: any;
            //     circle: any;
            //     date: any;
            //     rating: any;
            //     tags: any[];
            //     cv: any;
            //     filesize: any;
            //     dateAnnounce: any;
            // }
            const workInfo = {};
            workInfo.rj = rj;

            let metaList = dom.getElementsByTagName("meta")
            for (let i = 0; i < metaList.length; i++){
                let meta = metaList[i];
                if(meta.getAttribute("property") === 'og:image'){
                    workInfo.img = meta.content;
                    break;
                }
            }

            workInfo.lang = dom.querySelector("html").getAttribute("lang");
            workInfo.title = dom.getElementById("work_name").innerText;
            workInfo.circle = dom.querySelector("span.maker_name").innerText;
            workInfo.circleId = dom.querySelector("#work_maker a").href;
            workInfo.circleId = workInfo.circleId.substring(workInfo.circleId.lastIndexOf("/") + 1, workInfo.circleId.lastIndexOf(".")).trim();

            const table_outline = dom.querySelector("table#work_outline");
            for (let i = 0, ii = table_outline.rows.length; i < ii; i++) {
                const row = table_outline.rows[i];
                const row_header = row.cells[0].innerText.trim();
                const row_data = row.cells[1];
                const lambda = text => row_header === text;
                switch (true) {
                    case (["販売日", "发售日", "販賣日", "Release date", "판매일", "Lanzamiento", "Veröffentlicht",
                        "Date de sortie", "Tanggal rilis", "Data di rilascio", "Lançamento", "Utgivningsdatum",
                        "วันที่ขาย", "Ngày phát hành"].some(lambda)):
                        workInfo.date = row_data.innerText.trim();
                        break;
                    case (["更新情報", "更新信息", "更新資訊", "Update information", "갱신 정보", "Actualizar información",
                        "Aktualisierungen", "Mise à jour des informations", "Perbarui informasi", "Aggiorna informazioni",
                        "Atualizar informações", "Uppdatera information", "ข้อมูลอัปเดต", "Thông tin cập nhật"].some(lambda)):
                        workInfo.update = row_data.firstChild.data.trim();
                        break;
                    case (["年齢指定", "年龄指定", "年齡指定", "Age", "연령 지정", "Edad", "Altersfreigabe", "Âge", "Batas usia",
                        "Età", "Idade", "Ålder", "การกำหนดอายุ", "Độ tuổi chỉ định"].some(lambda)):
                        workInfo.rating = row_data.innerText.trim();
                        break;
                    case (["ジャンル", "分类", "分類", "Genre", "장르", "Género", "Genre", "Genre", "Genre", "Genere", "Gênero",
                        "Genre", "ประเภท", "Thể loại"].some(lambda)):
                        const tag_nodes = row_data.querySelectorAll("a");
                        workInfo.tags = [...tag_nodes].map(a => { return a.innerText.trim() });
                        break;
                    case (["シナリオ", "Scenario", "剧情", "劇本", "시나리오", "Guión", "Szenario", "Scénario", "Skenario",
                        "Scenario", "Cenário", "Scenario", "บทละคร", "Kịch bản"].some(lambda)):
                        workInfo.scenario = row_data.innerText.trim();
                        break;
                    case (["イラスト", "Illustration", "插画", "插畫", "일러스트", "Ilustración", "AbbilDung", "Illustration",
                        "Ilustrasi", "Illustrazione", "Ilustração", "Illustration", "ภาพประกอบ", "Tranh minh họa"].some(lambda)):
                        workInfo.illustration = row_data.innerText.trim();
                        break;
                    case (["声優", "声优", "聲優", "Voice Actor", "성우", "Doblador", "Synchronsprecher", "Doubleur",
                        "Pengisi suara", "Doppiatore/Doppiatrice", "Ator de voz", "Röstskådespelare", "นักพากย์",
                        "Diễn viên lồng tiếng"].some(lambda)):
                        workInfo.cv = row_data.innerText.trim();
                        break;
                    case (["音楽", "Music", "音乐", "音樂", "음악", "Música", "Musik", "Musique", "Musik", "Musica.",
                        "Música", "musik", "ดนตรี", "Âm nhạc"].some(lambda)):
                        workInfo.music = row_data.innerText.trim();
                        break;
                    case (["ファイル容量", "文件容量", "檔案容量", "File size", "파일 용량", "Tamaño del Archivo", "Dateigröße",
                        "Taille du fichier", "Ukuran file", "Dimensione del file", "Tamanho do arquivo", "Filstorlek",
                        "ขนาดไฟล์", "Dung lượng tệp"].some(lambda)):
                        workInfo.filesize = row_data.innerText.trim();
                        break;
                    default:
                        break;
                }
            }

            //获取发售预告时间
            const work_date_ana = dom.querySelector("strong.work_date_ana");
            if (work_date_ana) {
                workInfo.dateAnnounce = work_date_ana.innerText;
                //workInfo.img = "https://img.dlsite.jp/modpub/images2/ana/doujin/" + rj_group + "/" + rj + "_ana_img_main.jpg"
            }

            return workInfo;
        },

        // Get language code for DLSite API
        getLangCode: function (lang) {
            if(!lang) return "ja-JP";

            switch (lang.toUpperCase()) {
                case "JPN":
                    return "ja-JP";
                case "ENG":
                    return "en-US";
                case "KO_KR":
                    return "ko-KR";
                case "CHI_HANS":
                    return "zh-CN";
                case "CHI_HANT":
                    return "zh-TW";
                default:
                    return "ja-JP"
            }
        },

        parseApiData: function (rjCode, data){
            if(!data) data = {};
            let apiData = data;
            apiData.is_bonus = !data.is_sale && data.is_free && data.is_oly && data.wishlist_count === false;
            apiData.is_girls = (data.options && data.options.indexOf("OTM") >= 0) || (data.site_id === "girls");

            if(data.regist_date){
                let reg_date = data.regist_date.replace(/-/g, '/');
                let releaseDate = new Date(reg_date);
                apiData.regist_timestamp = releaseDate.getTime();
                apiData.regist_date = `${releaseDate.getFullYear()} / ${releaseDate.getMonth() + 1} / ${releaseDate.getDate()}`;
                if(apiData.regist_timestamp > Date.now()){
                    apiData.is_announce = true;
                }
            }
            return apiData;
        },

        parseApi2Data: function (rjCode, data) {
            const translation_info = data.translation_info ? data.translation_info : {};
            data.lang = DLsite.getLangCode(translation_info.lang);

            if(data.regist_date){
                let reg_date = data.regist_date.replace(/-/g, '/');
                let releaseDate = new Date(reg_date);
                data.regist_timestamp = releaseDate.getTime();
                data.regist_date = `${releaseDate.getFullYear()} / ${releaseDate.getMonth() + 1} / ${releaseDate.getDate()}`;
                if(data.regist_timestamp > Date.now()){
                    data.is_announce = true;
                }
            }

            return data;
        },

        getAnnouncePromise: async function (rjCode, parentRJ) {
            const url = `https://www.dlsite.com/maniax/announce/=/product_id/${rjCode}.html`;
            let resp = await getHttpAsync(url);
            if (resp.readyState === 4 && resp.status === 200) {
                const dom = new DOMParser().parseFromString(Csp.createHTML(resp.responseText), "text/html");
                const workInfo = DLsite.parseWorkDOM(dom, rjCode);
                workInfo.parentWork = parentRJ === rjCode ? null : parentRJ;
                workInfo.is_announce = true;
                return workInfo;
            }
            else if (resp.readyState === 4 && resp.status === 404) {
                return {
                    parentWork: parentRJ === rjCode ? null : parentRJ,
                    is_announce: false
                };
            }

        },

        getHtmlPromise: async function (rjCode) {
            const url = `https://www.dlsite.com/maniax/work/=/product_id/${rjCode}.html`;
            let resp = await getHttpAsync(url);
            if (resp.readyState === 4 && resp.status === 200) {
                const dom = new DOMParser().parseFromString(Csp.createHTML(resp.responseText), "text/html");
                const workInfo = DLsite.parseWorkDOM(dom, rjCode);
                workInfo.parentWork = DLsite.getParentWorkRjCode(resp.finalUrl);
                workInfo.parentWork = workInfo.parentWork === rjCode ? null : workInfo.parentWork;
                workInfo.is_announce = false;
                return workInfo;
            }
            else if (resp.readyState === 4 && resp.status === 404) {
                return await DLsite.getAnnouncePromise(rjCode, DLsite.getParentWorkRjCode(resp.finalUrl));
            }
        },

        getApi2Promise: async function (rjCode, locale = undefined) {
            let url = `https://www.dlsite.com/maniax/api/=/product.json?workno=${rjCode}` + (locale ? `&locale=${locale}` : "");
            let resp = await getHttpAsync(url);
            let data;
            if (resp.readyState === 4 && resp.status === 200) {
                data = JSON.parse(resp.responseText);
                data = data ? data[0] : {};
                data = data ? data : {}
            }
            else if (resp.readyState === 4 && resp.status === 404) {
                return {};
            }
            else {
                throw new Error(`无法通过API2获取${rjCode}的信息:${resp.status} ${resp.statusText}`);
            }

            return DLsite.parseApi2Data(rjCode, data);
        },

        getApiPromise: async function (rjCode, locale = undefined) {
            //获取对应语言下的实际信息
            let url = `https://www.dlsite.com/maniax/product/info/ajax?product_id=${rjCode}&cdn_cache_min=1` + (locale ? `&locale=${locale}` : "");
            let resp = await getHttpAsync(url);
            let data;
            if (resp.readyState === 4 && resp.status === 200) {
                data = JSON.parse(resp.responseText);
                data = data ? data[rjCode] : {};
                data = data ? data : {};
            }
            else if(resp.readyState === 4 && resp.status === 404){
                return {};
            }
            else {
                throw new Error(`无法通过API获取${rjCode}的信息:${resp.status} ${resp.statusText}`);
            }

            const translation_info = data.translation_info ? data.translation_info : {};
            data.lang = DLsite.getLangCode(translation_info.lang);

            return DLsite.parseApiData(rjCode, data);
        },

        getCirclePromise: async function (rjCode, apiPromise){
            let apiData = await apiPromise;
            if(!apiData.maker_id) return null;
            const maker_id = apiData.maker_id;

            let url;
            let resp;
            let data;
            try {
                url = `https://media.ci-en.jp/dlsite/lookup/${maker_id}.json`;
                resp = await getHttpAsync(url);
                data = undefined;
                if (resp.readyState === 4 && resp.status === 200) {
                    data = JSON.parse(resp.responseText);
                    data = data ? data[0] : {};
                    data = data ? data : {};
                    data.maker_id = maker_id;
                }
            }catch (e){}

            if(!data || !data.name){
                //未获取到社团名称则使用html解析获取
                url = `https://www.dlsite.com/maniax/circle/profile/=/maker_id/${maker_id}.html`;
                resp = await getHttpAsync(url);
                data = data ? data : {};
                if(resp.readyState === 4 && resp.status === 200){
                    let doc = new DOMParser().parseFromString(Csp.createHTML(resp.responseText), "text/html");
                    let name = doc.querySelector("strong.prof_maker_name");
                    name = name ? name.innerText : null;
                    data.name = name;
                }
            }

            return data;
        },

        getTranslatablePromise: async function (rjCode, site = "maniax") {
            rjCode = rjCode.toUpperCase();
            const result = {
                translation_request_english: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_simplified_chinese:{
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_traditional_chinese:{
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_korean: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_spanish: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_german: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_french: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_indonesian: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_italian: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_portuguese: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_swedish: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_thai: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
                translation_request_vietnamese: {
                    agree: undefined,
                    request: undefined,
                    sale: undefined
                },
            };
            const data = await DLsite.getTranslatableApiPromise(rjCode, site);
            if(!data.translationStatusForTranslator){
                return result;
            }

            const map = {
                translation_request_english: "ENG",
                translation_request_simplified_chinese: "CHI_HANS",
                translation_request_traditional_chinese: "CHI_HANT",
                translation_request_korean: "KO_KR",
                translation_request_spanish: "SPA",
                translation_request_german: "GER",
                translation_request_french: "FRE",
                translation_request_indonesian: "IND",
                translation_request_italian: "ITA",
                translation_request_portuguese: "POR",
                translation_request_swedish: "SWE",
                translation_request_thai: "THA",
                translation_request_vietnamese: "VIE",
            };
            for (let key in map) {
                let lang = map[key];
                let status = data.translationStatusForTranslator[lang];
                if(!status){
                    //状况未知
                    continue;
                }
                result[key].agree = status.available;
                result[key].request = status.count;
                result[key].sale = status.on_sale_count;
            }

            return result;
        },

        getTranslatableApiPromise: async function (rjCode, site = "maniax") {
            //新的可用api,用于搜索作品翻译情况,但也可以获得其它信息。
            rjCode = rjCode.toUpperCase();
            let url = `https://www.dlsite.com/${site}/api/=/translatableProducts.json?keyword=${rjCode}`;    //可以使用locale参数指定语言,但这里不需要
            let resp = await getHttpAsync(url, true);
            let data;
            if (resp.readyState === 4 && resp.status === 200) {
                data = JSON.parse(resp.responseText);
            }
            else {
                throw new Error(`无法通过API获取${rjCode}的翻译信息:${resp.status} ${resp.statusText}`);
            }

            //从结果中找到对应RJ号,由于关键字是RJ号的话结果一般都在第一页,所以就放弃翻页寻找了
            if(data.meta && data.meta.code !== 200){
                throw new Error(`无法通过API查询${rjCode}的翻译信息:${data.meta.code} - ${data.meta.errorType} - ${data.meta.errorMessage}`);
            }
            if(!data.data || !Array.isArray(data.data.products)){
                throw new Error(`无法通过API查询${rjCode}的翻译信息:未预料到的响应格式。`);
            }

            for (const work of data.data.products) {
                if(work.id === rjCode){
                    return work;
                }
            }

            //未找到则返回空对象
            return {};

        },

        getWorkRequestPromise: function (rjCode) {
            return {
                _info: undefined,
                _api: undefined,
                _api2: undefined,
                _circle: undefined,
                _translatable: undefined,
                _translated_title: undefined,
                get info(){
                    return this._info ? this._info : this._info = DLsite.getHtmlPromise(rjCode);
                },
                get api() {
                    return this._api ? this._api : this._api = DLsite.getApiPromise(rjCode);
                },
                get api2() {
                    return this._api2 ? this._api2 : this._api2 = DLsite.getApi2Promise(rjCode);
                },
                get circle(){
                    return this._circle ? this._circle : this._circle = DLsite.getCirclePromise(rjCode, this.api);
                },
                get translatable() {
                    async function getter(t){
                        let api = await t.api2;
                        if(!api.site_id) api = await t.api;

                        return t._translatable ? t._translatable : t._translatable = DLsite.getTranslatablePromise(rjCode,
                            api.site_id ? api.site_id : "maniax");
                    }
                    return getter(this);
                },
                get translated_title(){
                    async function getter(t){
                        if(t._translated_title) return t._translated_title;

                        let api = await t.api2;
                        if(api.translation_info){
                            //api2有效
                            if(!api.translation_info.is_original) {
                                //通过再次查询获得翻译标题
                                api = await DLsite.getApi2Promise(rjCode, api.lang);
                            }
                            t._translated_title = api.work_name;
                            return t._translated_title;
                        }

                        //api2无效,通过api查询
                        api = await t.api;
                        if(!api.translation_info){
                            //api无效则无法获取标题(网页获取希望渺茫)
                            t._translated_title = null;
                            return null;
                        }

                        if(!api.translation_info.is_original) {
                            //非原作则再次查询
                            api = await DLsite.getApiPromise(rjCode, api.lang);
                        }
                        t._translated_title = api.work_name;
                        return t._translated_title;
                    }

                    return getter(this);
                }
            }
        },

        getParentWorkRjCode: function (redirectUrl){
            //仅作品编号部分复用RJ_REGEX,"紧跟在product_id/之后"这个定位条件保持不变
            const reg = new RegExp(`(?<=product_id/)(${RJ_REGEX.source})`, "i");
            return redirectUrl.match(reg)[0];
        }
    }

    //endregion

    //region 设置页面构造

    /**
     * 设置页面模板对象
     */
    function getSettingsUi() {
        return {
            //这一层是设置界面最顶层,编辑大标题信息
            title: localize(localizationMap.title_settings),
            items: [
                {
                    //这一层是设置界面的大分类
                    title: localize(localizationMap.title_language_settings),
                    items: [
                        {
                            //这一层是设置项列表集合(使用表格呈现设置项)"
                            items: [
                                {
                                    //这一层是设置项
                                    type: "dropdown",
                                    title: localize(localizationMap.display_language),
                                    id: "lang",
                                    ignore_reset: true,
                                    options: [
                                        {
                                            title: "简体中文",
                                            value: "zh_CN"
                                        },
                                        {
                                            title: "繁體中文",
                                            value: "zh_TW"
                                        },
                                        {
                                            title: "English",
                                            value: "en_US"
                                        }
                                    ]
                                },
                                {
                                    //这一层是设置项
                                    type: "dropdown",
                                    title: localize(localizationMap.popup_language),
                                    id: "popup_lang",
                                    ignore_reset: true,
                                    tooltip: localize(localizationMap.popup_language_tooltip),
                                    options: [
                                        {
                                            title: "简体中文",
                                            value: "zh_CN"
                                        },
                                        {
                                            title: "繁體中文",
                                            value: "zh_TW"
                                        },
                                        {
                                            title: "English",
                                            value: "en_US"
                                        }
                                    ]
                                }
                            ]
                        }
                    ]
                },

                {
                    //这一层是设置界面的大分类
                    title: localize(localizationMap.title_general_settings),
                    items: [
                        {
                            //这一层是设置项列表集合(使用表格呈现设置项)
                            items: [
                                {
                                    //解析URL
                                    type: "checkbox",
                                    title: localize(localizationMap.parse_url),
                                    id: "parse_url",
                                    tooltip: localize(localizationMap.parse_url_tooltip)
                                },
                                {
                                    //DL上解析URL
                                    binding: {
                                        target: "parse_url",
                                        value: true
                                    },

                                    type: "checkbox",
                                    title: localize(localizationMap.parse_url_in_dl),
                                    id: "parse_url_in_dl",
                                    indent: 1,  //设置项缩进
                                    tooltip: localize(localizationMap.parse_url_in_dl_tooltip)
                                },
                                {
                                    //DL显示翻译标题
                                    type: "checkbox",
                                    title: localize(localizationMap.show_translated_title_in_dl),
                                    id: "show_translated_title_in_dl",
                                    tooltip: localize(localizationMap.show_translated_title_in_dl_tooltip)
                                },
                                {
                                    //“复制为有效文件名”按钮
                                    type: "checkbox",
                                    title: localize(localizationMap.copy_as_filename_btn),
                                    id: "copy_as_filename_btn",
                                    tooltip: localize(localizationMap.copy_as_filename_btn_tooltip)
                                },
                                {
                                    //**显示兼容性警告**
                                    type: "checkbox",
                                    title: `<strong>**${localize(localizationMap.show_compatibility_warning)}**</strong>`,
                                    id: "show_compatibility_warning",
                                    tooltip: localize(localizationMap.show_compatibility_warning_tooltip)
                                },
                                {
                                    //导向文本插入方式
                                    type: "dropdown",
                                    title: localize(localizationMap.url_insert_mode),
                                    id: "url_insert_mode",
                                    tooltip: localize(localizationMap.url_insert_mode_tooltip),
                                    options: [
                                        {
                                            title: localize(localizationMap.url_insert_mode_none),
                                            value: "none"
                                        },
                                        {
                                            title: localize(localizationMap.url_insert_mode_prefix),
                                            value: "prefix"
                                        },
                                        {
                                            title: localize(localizationMap.url_insert_mode_before_rj),
                                            value: "before_rj"
                                        }
                                    ]
                                },
                                {
                                    //导向文本
                                    type: "input",
                                    title: localize(localizationMap.url_insert_text),
                                    id: "url_insert_text",
                                    indent: 1
                                },

                                {
                                    //NSFW模式
                                    type: "checkbox",
                                    title: localize(localizationMap.sfw_mode),
                                    id: "sfw_mode",
                                    tooltip: localize(localizationMap.sfw_mode_tooltip)
                                },
                                {
                                    //模糊程度
                                    binding: {
                                        target: "sfw_mode",
                                        value: true
                                    },

                                    type: "dropdown",
                                    title: localize(localizationMap.sfw_blur_level),
                                    id: "sfw_blur_level",
                                    indent: 1,
                                    options: [
                                        {
                                            title: localize(localizationMap.low),
                                            value: "low"
                                        },
                                        {
                                            title: localize(localizationMap.medium),
                                            value: "medium"
                                        },
                                        {
                                            title: localize(localizationMap.high),
                                            value: "high"
                                        }
                                    ]
                                },
                                {
                                    //鼠标移至图片上方移除模糊
                                    binding: {
                                        target: "sfw_mode",
                                        value: true
                                    },

                                    type: "checkbox",
                                    title: localize(localizationMap.sfw_remove_when_hover),
                                    id: "sfw_remove_when_hover",
                                    indent: 1,
                                },
                                {
                                    //是否开启模糊动画
                                    binding: {
                                        target: "sfw_mode",
                                        value: true
                                    },

                                    type: "checkbox",
                                    title: localize(localizationMap.sfw_blur_transition),
                                    id: "sfw_blur_transition",
                                    indent: 1,
                                }
                            ]
                        }
                    ]
                },
                {
                    //分类:信息显示
                    title: localize(localizationMap.title_info_settings),
                    items: [
                        {
                            //预设表格
                            items: [
                                {
                                    type: "dropdown",
                                    title: localize(localizationMap.category_preset),
                                    id: "category_preset",
                                    tooltip: localize(localizationMap.category_preset_tooltip),
                                    ignore_reset: true,  //不显示重置按钮
                                    options: [
                                        {
                                            title: localize(localizationMap.work_type_voice),
                                            value: "voice"
                                        },
                                        {
                                            title: localize(localizationMap.work_type_game),
                                            value: "game"
                                        },
                                        {
                                            title: `${localize(localizationMap.work_type_comic)} / ${localize(localizationMap.work_type_illustration)} / ${localize(localizationMap.work_type_voice_comic)}`,
                                            value: "manga"
                                        },
                                        {
                                            title: localize(localizationMap.work_type_video),
                                            value: "video"
                                        },
                                        {
                                            title: localize(localizationMap.work_type_novel),
                                            value: "novel"
                                        },
                                        {
                                            title: localize(localizationMap.work_type_other),
                                            value: "other"
                                        }
                                    ]
                                }
                            ]
                        },
                        {
                            //音声Preset对应的表格,注意使用Binding来决定表格是否显示
                            binding: {
                                target: "category_preset",
                                value: "voice"
                            },
                            sortable: true,  //为true则代表该表格内的行可以排序
                            sort_id: "voice__info_display_order",  //若排序则一定要指定id,该id将会存储在设置项中,作为列表记录每个元素的顺序
                            items: [
                                {
                                    //销量
                                    type: "checkbox",
                                    title: localize(localizationMap.dl_count),
                                    id: "voice__dl_count",  //注意这里不同的preset要改成不同的值
                                },
                                {
                                    //社团名
                                    type: "checkbox",
                                    title: localize(localizationMap.circle_name),
                                    id: "voice__circle_name",  //注意这里不同的preset要改成不同的值
                                },
                                {
                                    //翻译者
                                    type: "checkbox",
                                    title: localize(localizationMap.translator_name),
                                    id: "voice__translator_name",
                                },
                                {
                                    //发售日
                                    type: "checkbox",
                                    title: localize(localizationMap.release_date),
                                    id: "voice__release_date",
                                },
                                {
                                    //更新日
                                    type: "checkbox",
                                    title: localize(localizationMap.update_date),
                                    id: "voice__update_date",
                                },
                                {
                                    //年龄指定
                                    type: "checkbox",
                                    title: localize(localizationMap.age_rating),
                                    id: "voice__age_rating",
                                },
                                {
                                    //剧情作者
                                    type: "checkbox",
                                    title: localize(localizationMap.scenario),
                                    id: "voice__scenario",
                                },
                                {
                                    //插画作者
                                    type: "checkbox",
                                    title: localize(localizationMap.illustration),
                                    id: "voice__illustration",
                                },
                                {
                                    //配音者
                                    type: "checkbox",
                                    title: localize(localizationMap.voice_actor),
                                    id: "voice__voice_actor",
                                },
                                {
                                    //音乐作者
                                    type: "checkbox",
                                    title: localize(localizationMap.music),
                                    id: "voice__music",
                                },
                                {
                                    //作品标签/分类
                                    type: "checkbox",
                                    title: localize(localizationMap.genre),
                                    id: "voice__genre",
                                },
                                {
                                    //文件大小
                                    type: "checkbox",
                                    title: localize(localizationMap.file_size),
                                    id: "voice__file_size",
                                }
                            ]
                        },
                        {
                            //游戏Preset对应的表格
                            binding: {
                                target: "category_preset",
                                value: "game"
                            },
                            sortable: true,  //为true则代表该表格内的行可以排序
                            sort_id: "game__info_display_order",
                            items: [
                                {
                                    //销量
                                    type: "checkbox",
                                    title: localize(localizationMap.dl_count),
                                    id: "game__dl_count",
                                },
                                {
                                    //社团名
                                    type: "checkbox",
                                    title: localize(localizationMap.circle_name),
                                    id: "game__circle_name",
                                },
                                {
                                    //翻译者
                                    type: "checkbox",
                                    title: localize(localizationMap.translator_name),
                                    id: "game__translator_name",
                                },
                                {
                                    //发售日
                                    type: "checkbox",
                                    title: localize(localizationMap.release_date),
                                    id: "game__release_date",
                                },
                                {
                                    //更新日
                                    type: "checkbox",
                                    title: localize(localizationMap.update_date),
                                    id: "game__update_date",
                                },
                                {
                                    //年龄指定
                                    type: "checkbox",
                                    title: localize(localizationMap.age_rating),
                                    id: "game__age_rating",
                                },
                                {
                                    //剧情作者
                                    type: "checkbox",
                                    title: localize(localizationMap.scenario),
                                    id: "game__scenario",
                                },
                                {
                                    //插画作者
                                    type: "checkbox",
                                    title: localize(localizationMap.illustration),
                                    id: "game__illustration",
                                },
                                {
                                    //配音者
                                    type: "checkbox",
                                    title: localize(localizationMap.voice_actor),
                                    id: "game__voice_actor",
                                },
                                {
                                    //音乐作者
                                    type: "checkbox",
                                    title: localize(localizationMap.music),
                                    id: "game__music",
                                },
                                {
                                    //作品标签/分类
                                    type: "checkbox",
                                    title: localize(localizationMap.genre),
                                    id: "game__genre",
                                },
                                {
                                    //文件大小
                                    type: "checkbox",
                                    title: localize(localizationMap.file_size),
                                    id: "game__file_size",
                                }
                            ]
                        },
                        {
                            //漫画对应preset
                            binding: {
                                target: "category_preset",
                                value: "manga"
                            },
                            sortable: true,  //为true则代表该表格内的行可以排序
                            sort_id: "manga__info_display_order",
                            items: [
                                {
                                    //销量
                                    type: "checkbox",
                                    title: localize(localizationMap.dl_count),
                                    id: "manga__dl_count",
                                },
                                {
                                    //社团名
                                    type: "checkbox",
                                    title: localize(localizationMap.circle_name),
                                    id: "manga__circle_name",
                                },
                                {
                                    //翻译者
                                    type: "checkbox",
                                    title: localize(localizationMap.translator_name),
                                    id: "manga__translator_name",
                                },
                                {
                                    //发售日
                                    type: "checkbox",
                                    title: localize(localizationMap.release_date),
                                    id: "manga__release_date",
                                },
                                {
                                    //更新日
                                    type: "checkbox",
                                    title: localize(localizationMap.update_date),
                                    id: "manga__update_date",
                                },
                                {
                                    //年龄指定
                                    type: "checkbox",
                                    title: localize(localizationMap.age_rating),
                                    id: "manga__age_rating",
                                },
                                {
                                    //剧情作者
                                    type: "checkbox",
                                    title: localize(localizationMap.scenario),
                                    id: "manga__scenario",
                                },
                                {
                                    //插画作者
                                    type: "checkbox",
                                    title: localize(localizationMap.illustration),
                                    id: "manga__illustration",
                                },
                                {
                                    //配音者
                                    type: "checkbox",
                                    title: localize(localizationMap.voice_actor),
                                    id: "manga__voice_actor",
                                    tooltip: localize(localizationMap.work_type_voice_comic)
                                },
                                {
                                    //音乐作者
                                    type: "checkbox",
                                    title: localize(localizationMap.music),
                                    id: "manga__music",
                                },
                                {
                                    //作品标签/分类
                                    type: "checkbox",
                                    title: localize(localizationMap.genre),
                                    id: "manga__genre",
                                },
                                {
                                    //文件大小
                                    type: "checkbox",
                                    title: localize(localizationMap.file_size),
                                    id: "manga__file_size",
                                }
                            ]
                        },
                        {
                            //视频对应preset
                            binding: {
                                target: "category_preset",
                                value: "video"
                            },
                            sortable: true,  //为true则代表该表格内的行可以排序
                            sort_id: "video__info_display_order",
                            items: [
                                {
                                    //销量
                                    type: "checkbox",
                                    title: localize(localizationMap.dl_count),
                                    id: "video__dl_count",
                                },
                                {
                                    //社团名
                                    type: "checkbox",
                                    title: localize(localizationMap.circle_name),
                                    id: "video__circle_name",
                                },
                                {
                                    //翻译者
                                    type: "checkbox",
                                    title: localize(localizationMap.translator_name),
                                    id: "video__translator_name",
                                },
                                {
                                    //发售日
                                    type: "checkbox",
                                    title: localize(localizationMap.release_date),
                                    id: "video__release_date",
                                },
                                {
                                    //更新日
                                    type: "checkbox",
                                    title: localize(localizationMap.update_date),
                                    id: "video__update_date",
                                },
                                {
                                    //年龄指定
                                    type: "checkbox",
                                    title: localize(localizationMap.age_rating),
                                    id: "video__age_rating",
                                },
                                {
                                    //剧情作者
                                    type: "checkbox",
                                    title: localize(localizationMap.scenario),
                                    id: "video__scenario",
                                },
                                {
                                    //插画作者
                                    type: "checkbox",
                                    title: localize(localizationMap.illustration),
                                    id: "video__illustration",
                                },
                                {
                                    //配音者
                                    type: "checkbox",
                                    title: localize(localizationMap.voice_actor),
                                    id: "video__voice_actor",
                                },
                                {
                                    //音乐作者
                                    type: "checkbox",
                                    title: localize(localizationMap.music),
                                    id: "video__music",
                                },
                                {
                                    //作品标签/分类
                                    type: "checkbox",
                                    title: localize(localizationMap.genre),
                                    id: "video__genre",
                                },
                                {
                                    //文件大小
                                    type: "checkbox",
                                    title: localize(localizationMap.file_size),
                                    id: "video__file_size",
                                }
                            ]
                        },
                        {
                            //小说对应Preset
                            binding: {
                                target: "category_preset",
                                value: "novel"
                            },
                            sortable: true,  //为true则代表该表格内的行可以排序
                            sort_id: "novel__info_display_order",
                            items: [
                                {
                                    //销量
                                    type: "checkbox",
                                    title: localize(localizationMap.dl_count),
                                    id: "novel__dl_count",
                                },
                                {
                                    //社团名
                                    type: "checkbox",
                                    title: localize(localizationMap.circle_name),
                                    id: "novel__circle_name",
                                },
                                {
                                    //翻译者
                                    type: "checkbox",
                                    title: localize(localizationMap.translator_name),
                                    id: "novel__translator_name",
                                },
                                {
                                    //发售日
                                    type: "checkbox",
                                    title: localize(localizationMap.release_date),
                                    id: "novel__release_date",
                                },
                                {
                                    //更新日
                                    type: "checkbox",
                                    title: localize(localizationMap.update_date),
                                    id: "novel__update_date",
                                },
                                {
                                    //年龄指定
                                    type: "checkbox",
                                    title: localize(localizationMap.age_rating),
                                    id: "novel__age_rating",
                                },
                                {
                                    //剧情作者
                                    type: "checkbox",
                                    title: localize(localizationMap.scenario),
                                    id: "novel__scenario",
                                },
                                {
                                    //插画作者
                                    type: "checkbox",
                                    title: localize(localizationMap.illustration),
                                    id: "novel__illustration",
                                },
                                {
                                    //配音者
                                    type: "checkbox",
                                    title: localize(localizationMap.voice_actor),
                                    id: "novel__voice_actor",
                                },
                                {
                                    //音乐作者
                                    type: "checkbox",
                                    title: localize(localizationMap.music),
                                    id: "novel__music",
                                },
                                {
                                    //作品标签/分类
                                    type: "checkbox",
                                    title: localize(localizationMap.genre),
                                    id: "novel__genre",
                                },
                                {
                                    //文件大小
                                    type: "checkbox",
                                    title: localize(localizationMap.file_size),
                                    id: "novel__file_size",
                                }
                            ]
                        },
                        {
                            //其他对应Preset
                            binding: {
                                target: "category_preset",
                                value: "other"
                            },
                            sortable: true,  //为true则代表该表格内的行可以排序
                            sort_id: "other__info_display_order",
                            items: [
                                {
                                    //销量
                                    type: "checkbox",
                                    title: localize(localizationMap.dl_count),
                                    id: "other__dl_count",
                                },
                                {
                                    //社团名
                                    type: "checkbox",
                                    title: localize(localizationMap.circle_name),
                                    id: "other__circle_name",
                                },
                                {
                                    //翻译者
                                    type: "checkbox",
                                    title: localize(localizationMap.translator_name),
                                    id: "other__translator_name",
                                },
                                {
                                    //发售日
                                    type: "checkbox",
                                    title: localize(localizationMap.release_date),
                                    id: "other__release_date",
                                },
                                {
                                    //更新日
                                    type: "checkbox",
                                    title: localize(localizationMap.update_date),
                                    id: "other__update_date",
                                },
                                {
                                    //年龄指定
                                    type: "checkbox",
                                    title: localize(localizationMap.age_rating),
                                    id: "other__age_rating",
                                },
                                {
                                    //剧情作者
                                    type: "checkbox",
                                    title: localize(localizationMap.scenario),
                                    id: "other__scenario",
                                },
                                {
                                    //插画作者
                                    type: "checkbox",
                                    title: localize(localizationMap.illustration),
                                    id: "other__illustration",
                                },
                                {
                                    //配音者
                                    type: "checkbox",
                                    title: localize(localizationMap.voice_actor),
                                    id: "other__voice_actor",
                                },
                                {
                                    //音乐作者
                                    type: "checkbox",
                                    title: localize(localizationMap.music),
                                    id: "other__music",
                                },
                                {
                                    //作品标签/分类
                                    type: "checkbox",
                                    title: localize(localizationMap.genre),
                                    id: "other__genre",
                                },
                                {
                                    //文件大小
                                    type: "checkbox",
                                    title: localize(localizationMap.file_size),
                                    id: "other__file_size",
                                }
                            ]
                        }
                    ]
                },
                {
                    //分类:标签显示
                    title: localize(localizationMap.title_tag_settings),
                    items: [
                        {
                            //总开关表格
                            items: [
                                {
                                    type: "checkbox",
                                    title: localize(localizationMap.tag_main_switch),
                                    tooltip: localize(localizationMap.tag_main_switch_tooltip),
                                    id: "tag_main_switch"
                                },
                                {
                                    //显示评分人数
                                    type: "checkbox",
                                    title: localize(localizationMap.show_rate_count),
                                    id: "show_rate_count",
                                    indent: 1
                                }
                            ]
                        },
                        {
                            //标签开关表格
                            binding: {
                                target: "tag_main_switch",
                                value: true
                            },
                            items: [
                                {
                                    //所有的标签开关集合
                                    type: "tag_switch",
                                    //标签之间可以排序
                                    sortable: true,
                                    sort_id: "tag_display_order",
                                    items: [
                                        {
                                            //销量
                                            title: localize(localizationMap.rate),
                                            id: "tag_rate",
                                            class: "tag-yellow",
                                            tooltip: localize(localizationMap.rate_tooltip)
                                        },
                                        {
                                            //作品类型
                                            title: localize(localizationMap.tag_work_type),
                                            id: "tag_work_type",
                                            class: "tag-darkblue",
                                            tooltip: `
<div class="${VOICELINK_CLASS}_tags">
    <span class="${VOICELINK_CLASS}_tag-purple">${localize(localizationMap.work_type_game)}</span>
    <span class="${VOICELINK_CLASS}_tag-green">${localize(localizationMap.work_type_comic)}</span>
    <span class="${VOICELINK_CLASS}_tag-teal">${localize(localizationMap.work_type_illustration)}</span>
    <span class="${VOICELINK_CLASS}_tag-gray">${localize(localizationMap.work_type_novel)}</span>
    <span class="${VOICELINK_CLASS}_tag-darkblue">${localize(localizationMap.work_type_video)}</span>
    <span class="${VOICELINK_CLASS}_tag-orange">${localize(localizationMap.work_type_voice)}</span>
    <span class="${VOICELINK_CLASS}_tag-yellow">${localize(localizationMap.work_type_music)}</span>
    <span class="${VOICELINK_CLASS}_tag-gray">${localize(localizationMap.work_type_tool)}</span>
    <span class="${VOICELINK_CLASS}_tag-blue">${localize(localizationMap.work_type_voice_comic)}</span>
    <span class="${VOICELINK_CLASS}_tag-gray">${localize(localizationMap.work_type_other)}</span>
</div>`,
                                        },
                                        {
                                            //可翻译
                                            title: localize(localizationMap.tag_translatable),
                                            id: "tag_translatable",
                                            class: "tag-green",
                                            tooltip: localize(localizationMap.tag_translatable_tooltip)
                                        },
                                        {
                                            //不可翻译
                                            title: localize(localizationMap.tag_not_translatable),
                                            id: "tag_not_translatable",
                                            class: "tag-red",
                                            tooltip: localize(localizationMap.tag_not_translatable_tooltip)
                                        },
                                        {
                                            //翻译作品
                                            title: localize(localizationMap.tag_translated),
                                            id: "tag_translated",
                                            class: "tag-teal",
                                            tooltip: localize(localizationMap.tag_translated_tooltip)
                                        },
                                        {
                                            //支持语言
                                            title: localize(localizationMap.tag_language_support),
                                            id: "tag_language_support",
                                            class: "tag-pink",
                                            tooltip: `
<div class="${VOICELINK_CLASS}_tags">
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_japanese)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_simplified_chinese)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_traditional_chinese)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_english)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_korean)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_german)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_french)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_indonesian)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_italian)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_portuguese)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_swedish)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_thai)}</span>
<span class="${VOICELINK_CLASS}_tag-pink">${localize(localizationMap.language_vietnamese)}</span>
</div>
                  `
                                        },
                                        {
                                            //特典作品
                                            title: localize(localizationMap.tag_bonus_work),
                                            id: "tag_bonus_work",
                                            class: "tag-yellow",
                                            tooltip: localize(localizationMap.tag_bonus_work_tooltip)
                                        },
                                        {
                                            //含特典
                                            title: localize(localizationMap.tag_has_bonus),
                                            id: "tag_has_bonus",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.tag_has_bonus_tooltip)
                                        },
                                        {
                                            //文件格式
                                            title: localize(localizationMap.tag_file_format),
                                            id: "tag_file_format",
                                            class: "tag-darkblue",
                                            tooltip: localize(localizationMap.tag_file_format_tooltip)
                                        },
                                        {
                                            //已下架
                                            title: localize(localizationMap.tag_no_longer_available),
                                            id: "tag_no_longer_available",
                                            class: "tag-gray",
                                        },
                                        {
                                            //AI & 部分AI
                                            title: localize(localizationMap.tag_ai),
                                            id: "tag_ai",
                                            class: "tag-purple",
                                            tooltip: localize(localizationMap.tag_ai_tooltip)
                                        }
                                    ]
                                },
                            ]
                        },
                        {
                            //翻译情况显示表格
                            items: [
                                {
                                    //翻译情况显示开关
                                    type: "checkbox",
                                    title: localize(localizationMap.tag_translation_request),
                                    id: "tag_translation_request",
                                    tooltip: localize(localizationMap.tag_translation_request_tooltip)
                                },
                            ]
                        },
                        {
                            //翻译情况标签显示表格
                            binding: {
                                target: "tag_translation_request",
                                value: true
                            },
                            items: [
                                {
                                    //各种翻译情况显示
                                    type: "tag_switch",
                                    sortable: true,
                                    sort_id: "tag_translation_request_display_order",
                                    items: [
                                        {
                                            //英语
                                            title: `${localize(localizationMap.language_english_abbr)} 1-1`,
                                            id: "tag_translation_request_english",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_english)
                                        },
                                        {
                                            //简体中文
                                            title: `${localize(localizationMap.language_simplified_chinese_abbr)} 1-1`,
                                            id: "tag_translation_request_simplified_chinese",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_simplified_chinese)
                                        },
                                        {
                                            //繁体中文
                                            title: `${localize(localizationMap.language_traditional_chinese_abbr)} 1-1`,
                                            id: "tag_translation_request_traditional_chinese",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_traditional_chinese)
                                        },
                                        {
                                            //韩语
                                            title: `${localize(localizationMap.language_korean_abbr)} 1-1`,
                                            id: "tag_translation_request_korean",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_korean)
                                        },
                                        {
                                            //西班牙语
                                            title: `${localize(localizationMap.language_spanish_abbr)} 1-1`,
                                            id: "tag_translation_request_spanish",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_spanish)
                                        },
                                        {
                                            //德语
                                            title: `${localize(localizationMap.language_german_abbr)} 1-1`,
                                            id: "tag_translation_request_german",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_german)
                                        },
                                        {
                                            //法语
                                            title: `${localize(localizationMap.language_french_abbr)} 1-1`,
                                            id: "tag_translation_request_french",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_french)
                                        },
                                        {
                                            //印尼语
                                            title: `${localize(localizationMap.language_indonesian_abbr)} 1-1`,
                                            id: "tag_translation_request_indonesian",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_indonesian)
                                        },
                                        {
                                            //意大利语
                                            title: `${localize(localizationMap.language_italian_abbr)} 1-1`,
                                            id: "tag_translation_request_italian",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_italian)
                                        },
                                        {
                                            //葡萄牙语
                                            title: `${localize(localizationMap.language_portuguese_abbr)} 1-1`,
                                            id: "tag_translation_request_portuguese",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_portuguese)
                                        },
                                        {
                                            //瑞典语
                                            title: `${localize(localizationMap.language_swedish_abbr)} 1-1`,
                                            id: "tag_translation_request_swedish",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_swedish)
                                        },
                                        {
                                            //泰语
                                            title: `${localize(localizationMap.language_thai_abbr)} 1-1`,
                                            id: "tag_translation_request_thai",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_thai)
                                        },
                                        {
                                            //越南语
                                            title: `${localize(localizationMap.language_vietnamese_abbr)} 1-1`,
                                            id: "tag_translation_request_vietnamese",
                                            class: "tag-orange",
                                            tooltip: localize(localizationMap.language_vietnamese)
                                        }
                                    ]
                                }
                            ]
                        },
                    ]
                },
                {
                    //分类:仓库检查
                    title: `${localize(localizationMap.search_settings_title)} ( <u><a href="https://github.com/IceFoxy062/VoiceLinks-Extend/blob/dev/docs/major_updates/v5.0.x/v5.0.x-${settings._s_lang}.md">?</a></u> )`,
                    items: [
                        {
                            items: [
                                {
                                    //仓检开关
                                    type: "checkbox",
                                    title: localize(localizationMap.search_enable),
                                    id: "base_search",
                                    tooltip: localize(localizationMap.search_enable_tooltip)
                                },
                                {
                                    //仓库管理
                                    binding: {
                                        target: "base_search",
                                        value: true
                                    },

                                    type: "button",
                                    title: localize(localizationMap.search_warehouse_list),
                                    button_title: localizeFormat(localizationMap.search_manage_warehouses, {
                                        count: getWarehouses().length,
                                        max: MAX_SEARCH_WAREHOUSES
                                    }),
                                    tooltip: localizeFormat(localizationMap.search_manage_warehouses_tooltip, {
                                        max: MAX_SEARCH_WAREHOUSES
                                    }),
                                    onclick: () => WarehouseManager.showPopup()
                                },
                                {
                                    //是否查找完整关联
                                    binding: {
                                        target: "base_search",
                                        value: true
                                    },

                                    type: "checkbox",
                                    title: localize(localizationMap.search_full_linkage),
                                    id: "full_linkage",
                                    tooltip: localize(localizationMap.search_full_linkage_tooltip)
                                },
                                {
                                    //搜索语言列表
                                    binding: {
                                        target: "base_search",
                                        value: true
                                    },

                                    type: "input",
                                    title: localize(localizationMap.search_cue_languages),
                                    id: "cue_languages",
                                    tooltip: localize(localizationMap.search_cue_languages_tooltip)
                                },
                                {
                                    //是否进行连锁查询
                                    binding: {
                                        target: "base_search",
                                        value: true
                                    },

                                    type: "checkbox",
                                    title: localize(localizationMap.search_linkage),
                                    id: "search_linkage",
                                    tooltip: localize(localizationMap.search_linkage_tooltip)
                                }
                            ]
                        }
                    ]
                }
            ]
        };
    }

    class SettingPageBuilder {
        constructor(structure, settings) {
            this.structure = structure;
            this.settings = settings;
            this.container = null;
        }

        getClass(name) {
            if(!VOICELINK_CLASS || VOICELINK_CLASS === "") return name;
            return `${VOICELINK_CLASS}_${name}`;
        }

        build(useTemp = false) {
            const f = this.structure;
            let tempSettings = this.settings;

            //若未要求则清空设置暂存
            if(useTemp){
                tempSettings = {};
                for(let key in this.settings.temp_edited){
                    if(!key.startsWith("_s_")) continue;
                    tempSettings[key] = this.settings.temp_edited[key];
                }
            }else{
                this.settings.clearTemp();
            }

            //创建container
            const container = document.createElement("div");
            container.className = this.getClass("container");
            container.id = this.getClass("settings-container");
            this.container = container;

            //创建关闭按钮
            const closeButton = document.createElement("button");
            closeButton.id = this.getClass("button-close");
            closeButton.innerText = "X";
            closeButton.onclick = () => {
                container.remove();
            };
            container.appendChild(closeButton);

            //创建标题
            const title = document.createElement("h1");
            title.innerText = f.title;
            container.appendChild(title);

            //遍历构建Section
            for(const section of f.items){
                container.appendChild(this.buildSection(section));
            }

            //添加保存、重置按钮
            const buttonContainer = document.createElement("div");
            buttonContainer.className = this.getClass("button-container");
            const saveButton = document.createElement("button");
            saveButton.id = this.getClass("button-save");
            saveButton.innerText = localize(localizationMap.button_save);
            saveButton.onclick = () => {
                try{
                    this.settings.save();
                    window.alert(localize(localizationMap.save_complete))
                    container.remove();
                }catch (e){
                    window.alert(e);
                }
            };
            const resetButton = document.createElement("button");
            resetButton.id = this.getClass("button-reset");
            resetButton.innerText = localize(localizationMap.button_reset);
            resetButton.onclick = () => {
                if(!window.confirm(localize(localizationMap.reset_confirm))){
                    return;
                }
                try{
                    // this.settings.reset();
                    window.alert(localize(localizationMap.reset_complete))
                }catch (e) {
                    window.alert(e);
                }
                this.refreshSettings(this.settings.default_backup);
            };
            buttonContainer.appendChild(saveButton);
            buttonContainer.appendChild(resetButton);
            container.appendChild(buttonContainer);

            this.initSortableElement();
            this.refreshSettings(tempSettings);
            return container;
        };

        buildSection(section){
            //创建容器
            const container = document.createElement("div");
            container.className = this.getClass("section-container");

            //创建标题
            const title = document.createElement("h2");
            title.innerHTML = Csp.createHTML(section.title);
            container.appendChild(title);

            //遍历构建Table
            for(const item of section.items){
                container.appendChild(this.buildTable(item, container));
            }

            return container;
        };

        buildTable(table, section){
            //创建table
            const tableElement = document.createElement("table");

            //创建可能存在的preset绑定
            this.createBinding(table, tableElement, section);

            //遍历构建Row(先把row缓存在列表里,经过排序后构建)
            let rowList = [];
            const nodesCache = document.createElement("div");
            for(const row of table.items){
                let rowElement = this.buildRow(row, nodesCache);
                if(table.sortable){
                    this.setSortable(rowElement);
                }
                rowList.push(rowElement);

                //存入缓存用于绑定
                nodesCache.appendChild(rowElement);
            }

            //重置按钮行
            let resetRow;
            if(table.sortable){
                tableElement.setAttribute("data-sort-id", table.sort_id);
                this.sortSortable(rowList, table.sort_id);

                //若可排序则还要添加重置按钮
                if(table.ignore_reset !== true){
                    resetRow = document.createElement("tr");
                    const resetCell = document.createElement("td");
                    resetCell.classList.add(this.getClass("input-cell"))
                    resetCell.colSpan = 2;
                    resetCell.style.setProperty("text-align", "right", "important");  //textAlign = "right !important";
                    const resetButton = document.createElement("button");
                    resetButton.classList.add(this.getClass("button-flat"));
                    resetButton.title = localize(localizationMap.reset_order);
                    resetButton.style.setProperty("margin-right", "0", "important");  //marginRight = "0 !important";
                    resetButton.style.setProperty("margin-bottom", "0", "important");  //marginBottom = "0 !important";
                    const icon = document.createElement("span");
                    icon.classList.add(this.getClass("reset-btn-small"));
                    resetButton.appendChild(icon);
                    const title = document.createElement("span");
                    title.innerText = localize(localizationMap.reset_order);
                    resetButton.appendChild(title);
                    resetButton.onclick = () => {
                        if(!window.confirm(localize(localizationMap.reset_order_confirm))){
                            return;
                        }
                        this.reorderSortable(table.sort_id, this.settings.getDefaultValue(table.sort_id));
                        // tableElement.insertBefore(resetRow, tableElement.firstChild);
                    };
                    resetCell.appendChild(resetButton);
                    resetRow.appendChild(resetCell);
                    // tableElement.appendChild(resetRow);
                }
            }

            rowList.forEach((row) => {
                tableElement.appendChild(row);
            });
            if(resetRow){
                tableElement.appendChild(resetRow);
            }

            return tableElement;
        };

        buildRow(row, table){
            //创建row
            let rowElement = document.createElement("tr");

            //根据类型创建内容
            switch (row.type) {
                case "checkbox":
                    rowElement = this.createToggleRow(row);
                    break;
                case "dropdown":
                    rowElement = this.createDropdownRow(row);
                    break;
                case "input":
                    rowElement = this.createInputRow(row);
                    break;
                case "password":
                    rowElement = this.createInputRow(row);
                    break;
                case "button":
                    rowElement = this.createButtonRow(row);
                    break;
                case "textarea":
                    rowElement = this.createTextareaRow(row);
                    break;
                case "tag_switch":
                    rowElement = this.createTagSwitchRow(row);
                    break;
                default:
                    console.error(`Unknown row type: ${row.type}`);
                    break;
            }

            //设置Binding
            if(row.binding){
                this.createBinding(row, rowElement, table)
            }

            rowElement.classList.add(this.getClass("setting"));
            if(row.id){
                rowElement.setAttribute("data-id", row.id);
            }
            return rowElement;
        };

        createEmptyRow(row) {
            const rowElement = document.createElement("tr");
            if(row.indent) {
                rowElement.classList.add(this.getClass(`indent-${row.indent}`));
            }
            return rowElement;
        };

        createTitleCell(row, titleTag = "label") {
            const titleCell = document.createElement("td");
            titleCell.className = this.getClass("tooltip");

            let titleElement;
            if(titleTag === "label") {
                titleElement = document.createElement("label");
                titleElement.setAttribute("for", this.getClass(row.id));
            } else {
                titleElement = document.createElement(titleTag);
            }
            titleElement.className = this.getClass("row-title") + " " + this.getClass("ignore-drag");
            titleElement.innerHTML = Csp.createHTML(row.title);
            titleCell.appendChild(titleElement);

            if(row.tooltip) {
                const tooltip = document.createElement("span");
                tooltip.className = this.getClass("tooltip-text");
                tooltip.innerHTML = Csp.createHTML(row.tooltip);
                titleCell.appendChild(tooltip);
            }

            return [titleCell, titleElement];
        };

        createResetButtonSmall(row, resetFunction) {
            const settingId = `_s_${row.id}`;
            const defaultValue = this.settings.getDefaultValue(settingId);
            const resetButton = document.createElement("button");
            resetButton.className = this.getClass("reset-btn-small") + " " + this.getClass("ignore-drag");
            resetButton.title = localize(localizationMap.button_reset);
            resetButton.onclick = () => resetFunction(defaultValue);

            return [resetButton, defaultValue];
        }

        createToggleRow(row){
            //创建Row
            const rowElement = this.createEmptyRow(row);
            //创建设置项标题
            const [titleCell, titleElement] = this.createTitleCell(row, "span");

            //创建开关和重置按钮
            const settingId = `_s_${row.id}`;
            const inputCell = document.createElement("td");
            inputCell.classList.add(this.getClass("input-cell"));
            const inputContainer = document.createElement("div");
            inputContainer.classList.add(this.getClass("toggle-container"));
            inputCell.appendChild(inputContainer);

            //创建开关
            const input = document.createElement("input");
            input.type = "checkbox";
            input.id = this.getClass(row.id);
            input.name = input.id;
            // inputCell.appendChild(input);
            inputContainer.appendChild(input);

            const label = document.createElement("label");
            label.classList.add(this.getClass("toggle"));
            label.setAttribute("for", input.id);
            const hidden = document.createElement("span");
            hidden.classList.add(this.getClass("hidden"));
            hidden.innerHTML = Csp.createHTML(row.title);
            label.appendChild(hidden);
            // inputCell.appendChild(label);
            inputContainer.appendChild(label);

            //创建重置按钮
            if(row.ignore_reset !== true){
                const [resetBtn, defaultValue] = this.createResetButtonSmall(row, defaultValue => {
                    input.checked = defaultValue === true;
                    input.dispatchEvent(new Event("change"));
                });

                inputContainer.insertBefore(resetBtn, inputContainer.firstChild);
                input.addEventListener("change", () => {
                    //决定是否显示重置按钮
                    resetBtn.style.setProperty("display", input.checked === defaultValue ? "none" : "inline-block", "important");  //display = input.checked === defaultValue ? "none" : "inline-block";
                });
            }

            //监听器都创建好了再设置初始值
            input.addEventListener("change", () => {
                //更新到暂存设置
                this.settings.saveTemp(settingId, input.checked);
            })
            input.checked = this.settings[settingId] === true;
            input.dispatchEvent(new Event("change"));

            rowElement.appendChild(titleCell);
            rowElement.appendChild(inputCell);
            return rowElement;
        };

        createDropdownRow(row){
            const rowElement = this.createEmptyRow(row);
            const [titleCell, titleElement] = this.createTitleCell(row, "label");

            const inputCell = document.createElement("td");
            inputCell.classList.add(this.getClass("input-cell"));
            const select = document.createElement("select");
            select.className = this.getClass("ignore-drag");
            select.id = this.getClass(row.id);
            select.name = select.id;
            for(const item of row.options){
                const option = document.createElement("option");
                option.value = item.value;
                option.innerText = item.title;
                select.appendChild(option);
            }
            inputCell.appendChild(select);

            //创建重置按钮
            const settingId = `_s_${row.id}`;
            if(row.ignore_reset !== true){
                const [resetBtn, defaultValue] = this.createResetButtonSmall(row, defaultValue => {
                    select.value = defaultValue;
                    select.dispatchEvent(new Event("change"));
                });

                inputCell.insertBefore(resetBtn, inputCell.firstChild);
                select.addEventListener("change", () => {
                    //决定是否显示重置按钮
                    resetBtn.style.setProperty("display", select.value === defaultValue ? "none" : "inline-block", "important");  //display = select.value === defaultValue ? "none" : "inline-block";
                });
            }

            //监听器都创建好了再设置初始值
            select.addEventListener("change", () => {
                //更新到暂存设置
                this.settings.saveTemp(settingId, select.value);
            })
            select.value = this.settings[settingId];
            select.dispatchEvent(new Event("change"));

            rowElement.appendChild(titleCell);
            rowElement.appendChild(inputCell);
            return rowElement;
        };

        createInputRow(row){
            const rowElement = this.createEmptyRow(row);
            const [titleCell, titleElement] = this.createTitleCell(row, "label");

            const inputCell = document.createElement("td");
            inputCell.classList.add(this.getClass("input-cell"));
            const input = document.createElement("input");
            input.type = row.type === "password" ? "password" : "text";
            input.id = this.getClass(row.id);
            input.name = input.id;
            inputCell.appendChild(input);

            //创建重置按钮
            const settingId = `_s_${row.id}`;
            if(row.ignore_reset !== true){
                const [resetBtn, defaultValue] = this.createResetButtonSmall(row, defaultValue => {
                    input.value = defaultValue;
                    input.dispatchEvent(new Event("change"));
                });

                inputCell.insertBefore(resetBtn, inputCell.firstChild);
                input.addEventListener("change", () => {
                    //决定是否显示重置按钮
                    resetBtn.style.setProperty("display", input.value === defaultValue ? "none" : "inline-block", "important");  //display = input.value === defaultValue ? "none" : "inline-block";
                });
            }

            //监听器都创建好了再设置初始值
            input.addEventListener("change", () => {
                //更新到暂存设置
                this.settings.saveTemp(settingId, input.value);
            })
            input.value = this.settings[settingId];
            input.dispatchEvent(new Event("change"));

            rowElement.appendChild(titleCell);
            rowElement.appendChild(inputCell);
            return rowElement;
        };

        createButtonRow(row){
            const rowElement = this.createEmptyRow(row);
            const [titleCell] = this.createTitleCell(row, "span");

            const inputCell = document.createElement("td");
            inputCell.classList.add(this.getClass("input-cell"));

            const button = document.createElement("button");
            button.className = this.getClass("button-primary");
            button.innerText = row.button_title === undefined ? row.title : row.button_title;
            button.onclick = () => {
                if(typeof row.onclick === "function") row.onclick();
            };
            inputCell.appendChild(button);

            rowElement.appendChild(titleCell);
            rowElement.appendChild(inputCell);
            return rowElement;
        };

        createTextareaRow(row){
            const rowElement = this.createEmptyRow(row);
            const [titleCell, titleElement] = this.createTitleCell(row, "label");

            const inputCell = document.createElement("td");
            inputCell.classList.add(this.getClass("input-cell"));
            const input = document.createElement("textarea");
            input.id = this.getClass(row.id);
            input.name = input.id;
            input.style.minWidth = "100%";
            input.style.maxWidth = "100%";
            input.style.height = "100px";
            input.style.resize = "vertical"
            inputCell.appendChild(input);

            //创建重置按钮
            const settingId = `_s_${row.id}`;
            if(row.ignore_reset !== true){
                const [resetBtn, defaultValue] = this.createResetButtonSmall(row, defaultValue => {
                    input.value = defaultValue;
                    input.dispatchEvent(new Event("change"));
                });

                inputCell.insertBefore(resetBtn, inputCell.firstChild);
                input.addEventListener("change", () => {
                    //决定是否显示重置按钮
                    resetBtn.style.setProperty("display", input.value === defaultValue ? "none" : "inline-block", "important");  //display = input.value === defaultValue ? "none" : "inline-block";
                });
            }

            //监听器都创建好了再设置初始值
            input.addEventListener("change", () => {
                //更新到暂存设置
                this.settings.saveTemp(settingId, input.value);
            })
            input.value = this.settings[settingId];
            input.dispatchEvent(new Event("change"));

            rowElement.appendChild(titleCell);
            rowElement.appendChild(inputCell);
            return rowElement;
        };

        createTagSwitchRow(row){
            const rowElement = this.createEmptyRow(row);

            const tagCell = document.createElement("td");
            tagCell.colSpan = 2;
            //用内部容器再次包裹标签,保证重置按钮的位置
            const tagContainer = document.createElement("div");
            tagContainer.className = this.getClass("tags");
            tagCell.appendChild(tagContainer);

            const tagList = [];
            for(const tag of row.items){
                const tagSpan = document.createElement("label");
                tagSpan.classList.add(this.getClass(tag["class"]));
                tagSpan.innerText = tag.title;
                tagSpan.setAttribute("for", this.getClass(tag.id));
                tagSpan.setAttribute("data-id", tag.id);
                this.setSortable(tagSpan);

                //添加switch
                const settingId = `_s_${tag.id}`;
                const switchInput = document.createElement("input");
                switchInput.style.setProperty("display", "none", "important");  //display = "none !important";
                switchInput.type = "checkbox";
                switchInput.id = this.getClass(tag.id);
                switchInput.name = switchInput.id;
                switchInput.addEventListener("change", (event) => {
                    if(event.target.checked) {
                        tagSpan.classList.remove(this.getClass("tag-off"));
                    }else if(!tagSpan.classList.contains(this.getClass("tag-off"))) {
                        tagSpan.classList.add(this.getClass("tag-off"));
                    }

                    //更新到暂存设置
                    this.settings.saveTemp(settingId, switchInput.checked);
                })
                switchInput.checked = this.settings[`_s_${tag.id}`] === true;
                switchInput.dispatchEvent(new Event("change"));

                tagSpan.classList.toggle(this.getClass("tag-off"), !switchInput.checked);
                tagSpan.appendChild(switchInput);

                if(tag.tooltip) {
                    tagSpan.classList.add(this.getClass("tooltip"));
                    const tooltip = document.createElement("span");
                    tooltip.className = this.getClass("tooltip-text");
                    tooltip.innerHTML = Csp.createHTML(tag.tooltip);
                    tagSpan.appendChild(tooltip);
                }

                tagList.push(tagSpan);
            }

            if(row.sortable){
                tagContainer.setAttribute("data-sort-id", row.sort_id);
                this.sortSortable(tagList, row.sort_id);

                //添加排列重置按钮
                if(row.ignore_reset !== true){
                    const resetOrderButton = document.createElement("button");
                    resetOrderButton.classList.add(this.getClass("button-flat"));
                    resetOrderButton.title = localize(localizationMap.button_reset);
                    resetOrderButton.onclick = () => {
                        if(!window.confirm(localize(localizationMap.reset_order_confirm))){
                            return;
                        }
                        this.reorderSortable(row.sort_id, this.settings.getDefaultValue(row.sort_id));
                    };

                    const icon = document.createElement("span");
                    icon.classList.add(this.getClass("reset-btn-small"));
                    resetOrderButton.appendChild(icon);

                    const title = document.createElement("span");
                    title.innerText = localize(localizationMap.reset_order);
                    resetOrderButton.appendChild(title);

                    tagCell.appendChild(resetOrderButton);
                    // tagCell.insertBefore(resetOrderButton, tagCell.firstChild);
                }
            }

            //添加设置重置按钮
            if(row.ignore_reset !== true){
                const resetSettingsButton = document.createElement("button");
                resetSettingsButton.classList.add(this.getClass("button-flat"));
                resetSettingsButton.title = localize(localizationMap.button_reset);
                resetSettingsButton.onclick = () => {
                    if(!window.confirm(localize(localizationMap.reset_confirm))){
                        return;
                    }
                    for (const item of row.items) {
                        let tag = tagContainer.querySelector("#" + this.getClass(item.id));
                        if(!tag) continue;
                        tag.checked = this.settings.getDefaultValue(item.id);
                        tag.dispatchEvent(new Event("change"));
                    }
                };

                const icon = document.createElement("span");
                icon.classList.add(this.getClass("reset-btn-small"));
                resetSettingsButton.appendChild(icon);

                const title = document.createElement("span");
                title.innerText = localize(localizationMap.button_reset);
                resetSettingsButton.appendChild(title);

                tagCell.appendChild(resetSettingsButton);
                // tagCell.insertBefore(resetOrderButton, tagCell.firstChild);
            }


            tagList.forEach((tag) => {
                tagContainer.appendChild(tag);
            });
            rowElement.appendChild(tagCell);
            return rowElement;
        };

        createBinding(data, element, section){
            if(!data.binding || !this.container) return;

            const binding = data.binding;
            const target = section.querySelector("#" + this.getClass(binding.target));
            if(!target) return;

            let showState = "unset";
            switch (element.nodeName){
                case "TABLE":
                    showState = "table";
                    break;
                case "TR":
                    showState = "table-row";
                    break;
            }

            target.addEventListener("change", (event) => {
                const value = event.target.value;
                const checked = event.target.checked;
                element.style.setProperty("display", value === binding.value || checked === binding.value ? showState : "none", "important");  //display = value === binding.value || checked === binding.value ? showState : "none";
            });

            element.style.setProperty("display", target.value === binding.value || target.checked === binding.value ? showState : "none", "important");  //display = target.value === binding.value || target.checked === binding.value ? showState : "none";
        };

        sortSortable(sortList, sortId, orderList){
            orderList = orderList ? orderList : this.settings[`_s_${sortId}`];
            if(!orderList) return;

            for (let i = 0; i < orderList.length; ++i) {
                for (let j = 0; j < sortList.length; ++j){
                    if(orderList[i] === sortList[j].getAttribute("data-id")) {
                        let tmp = sortList[i];
                        sortList[i] = sortList[j];
                        sortList[j] = tmp;
                        break;
                    }
                }
            }
        };

        setSortable(ele){
            ele.classList.add(this.getClass("sortable"));
        };

        refreshSettings(settings) {
            //刷新设置项值的展示情况(保持input值和设置中的实际值一致)
            //清空设置暂存
            this.settings.clearTemp();
            settings = settings ? settings : this.settings;

            //刷新设置项
            for(const key in settings){
                if(!key.startsWith("_s_")) continue;
                const targetId = this.getClass(key.substring(3));

                if(settings[key] && Array.isArray(settings[key])){
                    //可能是排序对象,先搜索排序目标,搜不到再按默认值处理
                    if(this.reorderSortable(key.substring(3), settings[key])) continue;
                }

                //非排序对象
                const target = this.container.querySelector("#" + targetId);
                if(!target) continue;

                if(target.tagName === "INPUT") {
                    if(target.type === "checkbox") {
                        target.checked = settings[key] === true;
                    }else{
                        target.value = settings[key];
                    }
                }else if(target.tagName === "SELECT") {
                    target.value = settings[key];
                }

                if(key === "_s_lang") continue;
                target.dispatchEvent(new Event("change"));
            }
        };

        reorderSortable(sort_id, orderList) {
            const target = this.container.querySelector(`*[data-sort-id="${sort_id}"]`);
            if(target) {
                let list = [...target.children];
                this.sortSortable(list, sort_id, orderList);

                for(let i = 0; i < list.length; ++i) {
                    target.removeChild(list[i]);
                }

                for(let i = 0; i < list.length; ++i) {
                    target.appendChild(list[i]);
                }

                this.settings.saveTemp(sort_id, list.map(ele => ele.getAttribute("data-id")).filter(val => val && val !== ""));
                return true;
            }
            return false;
        }

        initSortableElement() {
            //初始化排序组件
            const sortableGroups = this.container.querySelectorAll(`.${this.getClass('sortable')}`);
            sortableGroups.forEach(group => {
                new Sortable(group, group.parentElement.getAttribute("data-sort-id"), this.settings);
            });
        };

    }

    class Sortable {
        constructor(element, sort_id, settings, options) {
            this.element = element;
            this.options = options;
            this.sortId = sort_id;
            this.settings = settings;
            this.dragging = null;

            if(!sort_id) {
                throw new Error("sort_id is required");
            }

            this.init();
        }

        getClass(name) {
            if(!VOICELINK_CLASS || VOICELINK_CLASS === "") return name;
            return `${VOICELINK_CLASS}_${name}`;
        }

        init() {
            this.element.addEventListener('mousedown', this.onMouseDown.bind(this));
            document.addEventListener('mousemove', this.onMouseMove.bind(this));
            document.addEventListener('mouseup', this.onMouseUp.bind(this));
        }

        onMouseDown(event) {
            if(event.target.nodeName === "INPUT" || event.target.classList.contains(this.getClass("toggle")) || event.target.classList.contains(this.getClass("ignore-drag"))) return;
            if (event.target.closest(`.${this.getClass('sortable')}`)) {
                this.dragging = event.target.closest(`.${this.getClass('sortable')}`);
                this.dragging.classList.add(this.getClass('dragging'));
            }
        }

        onMouseMove(event) {
            if (this.dragging) {
                event.preventDefault();

                try{
                    let sibling = document.elementFromPoint(event.clientX, event.clientY).closest(`.${this.getClass('sortable')}`);
                    if (sibling && sibling !== this.dragging) {
                        this.element.parentNode.insertBefore(this.dragging, sibling.nextSibling === this.dragging ? sibling : sibling.nextSibling);
                        //暂存当前顺序
                        const order = [...this.element.parentNode.children].map(item => item.getAttribute("data-id")).filter(item => item || item === "");
                        this.settings.saveTemp(this.sortId, order);
                    }
                }catch (e) {
                    console.error(e)
                }

            }
        }

        onMouseUp(event) {
            if (this.dragging) {
                this.dragging.classList.remove(this.getClass('dragging'));
                this.dragging = null;
            }
        }
    }

    /**
     * 仓库管理弹窗:添加、编辑、启用/停用、排序、删除仓库
     * 所有修改会立即保存并生效(无需刷新页面)
     */
    const WarehouseManager = {
        //当前编辑状态 {warehouse: {}, isNew: boolean}
        editing: null,
        //编辑表单中的输入控件 {字段key: 元素}
        formInputs: {},
        //编辑表单容器
        formElement: null,

        getClass(name) {
            return `${VOICELINK_CLASS}_warehouse-${name}`;
        },

        getContainer() {
            return document.getElementById(`${VOICELINK_CLASS}_warehouse-container`);
        },

        /**
         * 打开仓库管理界面
         */
        showPopup() {
            WarehouseManager.close();
            const container = document.createElement("div");
            container.id = `${VOICELINK_CLASS}_warehouse-container`;
            document.body.appendChild(container);
            WarehouseManager.renderList(container);
        },

        close() {
            const container = WarehouseManager.getContainer();
            if(container) container.remove();
            WarehouseManager.editing = null;
            WarehouseManager.formInputs = {};
            WarehouseManager.formElement = null;
        },

        /**
         * 渲染仓库列表
         * @param container {Element}
         */
        renderList(container) {
            container.dataset.view = "list";
            container.innerHTML = "";
            WarehouseManager.editing = null;
            WarehouseManager.formInputs = {};
            WarehouseManager.formElement = null;

            const body = document.createElement("div");
            container.appendChild(body);

            const warehouses = getWarehouses();

            //标题栏
            const header = document.createElement("div");
            header.className = WarehouseManager.getClass("header");
            const title = document.createElement("h1");
            title.innerText = localize(localizationMap.search_manager_title);
            header.appendChild(title);

            const addButton = document.createElement("button");
            addButton.className = WarehouseManager.getClass("btn");
            addButton.innerText = localize(localizationMap.search_manager_add);
            addButton.disabled = warehouses.length >= MAX_SEARCH_WAREHOUSES;
            if(addButton.disabled) {
                addButton.title = localizeFormat(localizationMap.search_manager_add_limit, {max: MAX_SEARCH_WAREHOUSES});
            }
            addButton.onclick = () => WarehouseManager.openEditor(null);
            header.appendChild(addButton);
            body.appendChild(header);

            const hint = document.createElement("p");
            hint.className = WarehouseManager.getClass("hint");
            hint.innerText = localizeFormat(localizationMap.search_manager_hint, {max: MAX_SEARCH_WAREHOUSES});
            body.appendChild(hint);

            //仓库列表
            const list = document.createElement("div");
            list.className = WarehouseManager.getClass("list");
            body.appendChild(list);

            if(warehouses.length === 0) {
                const empty = document.createElement("p");
                empty.className = WarehouseManager.getClass("hint");
                empty.innerText = localize(localizationMap.search_manager_empty);
                list.appendChild(empty);
            }else{
                warehouses.forEach((warehouse, index) => {
                    list.appendChild(WarehouseManager.createRow(warehouse, index, warehouses.length));
                });
            }

            //底部按钮
            const footer = document.createElement("div");
            footer.className = WarehouseManager.getClass("footer");
            footer.appendChild(WarehouseManager.createButton(localize(localizationMap.search_manager_close),
                () => WarehouseManager.close()));
            body.appendChild(footer);
        },

        /**
         * 创建列表中的一行仓库
         */
        createRow(warehouse, index, total) {
            const row = document.createElement("div");
            row.className = WarehouseManager.getClass("row");

            //启用开关
            const toggle = document.createElement("input");
            toggle.type = "checkbox";
            toggle.checked = warehouse.enabled;
            toggle.title = localize(localizationMap.search_manager_toggle_tooltip);
            toggle.onchange = () => WarehouseManager.toggleWarehouse(warehouse.id, toggle.checked);
            row.appendChild(toggle);

            //名称与预设
            const info = document.createElement("div");
            info.className = WarehouseManager.getClass("info");
            const name = document.createElement("div");
            name.className = WarehouseManager.getClass("name");
            name.innerText = warehouse.name;
            name.title = warehouse.name;
            const presetText = document.createElement("div");
            presetText.className = WarehouseManager.getClass("preset");
            const preset = SEARCH_PRESETS[warehouse.preset];
            presetText.innerText = `${preset ? preset.title : warehouse.preset}` +
                `${warehouse.enabled ? "" : localize(localizationMap.search_manager_disabled)}`;
            info.appendChild(name);
            info.appendChild(presetText);
            row.appendChild(info);

            //操作按钮
            row.appendChild(WarehouseManager.createButton(localize(localizationMap.search_manager_edit),
                () => WarehouseManager.openEditor(warehouse.id)));
            row.appendChild(WarehouseManager.createButton(localize(localizationMap.search_manager_move_up),
                () => WarehouseManager.moveWarehouse(index, -1), index <= 0));
            row.appendChild(WarehouseManager.createButton(localize(localizationMap.search_manager_move_down),
                () => WarehouseManager.moveWarehouse(index, 1), index >= total - 1));
            row.appendChild(WarehouseManager.createButton(localize(localizationMap.search_manager_delete),
                () => WarehouseManager.deleteWarehouse(warehouse.id), false, true));

            return row;
        },

        createButton(text, onclick, disabled = false, danger = false) {
            const button = document.createElement("button");
            button.className = WarehouseManager.getClass("btn") + (danger ? ` ${WarehouseManager.getClass("btn-danger")}` : "");
            button.innerText = text;
            button.disabled = disabled === true;
            button.onclick = onclick;
            return button;
        },

        /**
         * 保存仓库列表的修改,并重建运行时配置、刷新界面
         * @param warehouses {[{}]}
         */
        applyWarehouses(warehouses) {
            setWarehouses(warehouses);
            rebuildSearchProfiles();
            const container = WarehouseManager.getContainer();
            if(container) WarehouseManager.renderList(container);
        },

        /**
         * 清除指定仓库的查询缓存与登录状态
         * @param warehouse {{}}
         */
        clearWarehouseCache(warehouse) {
            try{
                const profile = new SearchProfile(warehouse);
                profile.clearCache().catch(e => console.error(e));
            }catch (e) {
                console.error(e);
            }
        },

        /**
         * 启用/停用仓库(停用时清空该仓库的缓存)
         */
        toggleWarehouse(id, enabled) {
            const warehouses = getWarehouses();
            const warehouse = warehouses.find(item => item.id === id);
            if(!warehouse) return;
            warehouse.enabled = enabled;

            if(!enabled) {
                WarehouseManager.clearWarehouseCache(warehouse);
            }
            WarehouseManager.applyWarehouses(warehouses);
        },

        /**
         * 调整仓库顺序
         */
        moveWarehouse(index, delta) {
            const warehouses = getWarehouses();
            const target = index + delta;
            if(target < 0 || target >= warehouses.length) return;

            const tmp = warehouses[index];
            warehouses[index] = warehouses[target];
            warehouses[target] = tmp;
            WarehouseManager.applyWarehouses(warehouses);
        },

        /**
         * 删除仓库(同时删除其账号密码、登录状态与查询缓存)
         */
        deleteWarehouse(id) {
            const warehouses = getWarehouses();
            const warehouse = warehouses.find(item => item.id === id);
            if(!warehouse) return;

            if(!window.confirm(localizeFormat(localizationMap.search_manager_delete_confirm, {name: warehouse.name}))) {
                return;
            }

            WarehouseManager.clearWarehouseCache(warehouse);
            WarehouseManager.applyWarehouses(warehouses.filter(item => item.id !== id));
        },

        /**
         * 打开编辑界面
         * @param id {string|null} 为null时表示添加新仓库
         */
        openEditor(id) {
            const container = WarehouseManager.getContainer();
            if(!container) return;

            let warehouse = null;
            let isNew = false;
            if(id === null) {
                warehouse = createWarehouse("kikoeru");
                isNew = true;
            }else{
                warehouse = getWarehouses().find(item => item.id === id);
            }
            if(!warehouse) return;

            WarehouseManager.editing = {warehouse: warehouse, isNew: isNew};
            WarehouseManager.renderEditor(container);
        },

        /**
         * 渲染编辑界面(字段内容由预设决定)
         * @param container {Element}
         */
        renderEditor(container) {
            container.dataset.view = "editor";
            container.innerHTML = "";
            WarehouseManager.formInputs = {};

            const editing = WarehouseManager.editing;
            const warehouse = editing.warehouse;
            const preset = SEARCH_PRESETS[warehouse.preset];
            if(!preset) return;

            const body = document.createElement("div");
            container.appendChild(body);

            const title = document.createElement("h1");
            title.innerText = editing.isNew ?
                localize(localizationMap.search_manager_editor_add_title) :
                localize(localizationMap.search_manager_editor_edit_title);
            body.appendChild(title);

            const form = document.createElement("div");
            WarehouseManager.formElement = form;
            body.appendChild(form);

            //预设类型
            const presetField = WarehouseManager.createField(form, {
                key: "", title: localize(localizationMap.search_manager_preset_field), required: true,
                tooltip: preset.tooltip
            });
            const presetSelect = document.createElement("select");
            for(const presetId of Object.keys(SEARCH_PRESETS)) {
                const option = document.createElement("option");
                option.value = presetId;
                option.innerText = SEARCH_PRESETS[presetId].title;
                presetSelect.appendChild(option);
            }
            presetSelect.value = warehouse.preset;
            presetSelect.onchange = () => WarehouseManager.changePreset(presetSelect.value);
            presetField.input.appendChild(presetSelect);

            //仓库名称
            const nameField = WarehouseManager.createField(form, {
                key: "name", title: localize(localizationMap.search_manager_name_field), required: true,
                tooltip: localize(localizationMap.search_manager_name_tooltip)
            });
            const nameInput = document.createElement("input");
            nameInput.type = "text";
            nameInput.value = warehouse.name;
            nameField.input.appendChild(nameInput);
            WarehouseManager.formInputs.name = nameInput;

            //预设字段
            for(const field of preset.fields) {
                const fieldElement = WarehouseManager.createField(form, field);
                const input = field.type === "textarea" ? document.createElement("textarea") :
                    field.type === "dropdown" ? document.createElement("select") : document.createElement("input");
                if(input.nodeName === "INPUT") input.type = field.type === "password" ? "password" : "text";

                const value = warehouse.config[field.key] === undefined ? "" : `${warehouse.config[field.key]}`;
                if(field.type === "dropdown") {
                    for(const option of field.options) {
                        const optionElement = document.createElement("option");
                        optionElement.value = option.value;
                        optionElement.innerText = option.title;
                        input.appendChild(optionElement);
                    }
                    input.value = value;
                }else{
                    input.value = value;
                }
                input.addEventListener("change", () => WarehouseManager.applyFieldBindings());
                fieldElement.input.appendChild(input);
                WarehouseManager.formInputs[field.key] = input;
            }

            //根据当前内容应用字段显隐
            WarehouseManager.applyFieldBindings();

            //底部按钮
            const footer = document.createElement("div");
            footer.className = WarehouseManager.getClass("footer");
            footer.appendChild(WarehouseManager.createButton(
                localize(editing.isNew ? localizationMap.search_manager_footer_add : localizationMap.search_manager_footer_save),
                () => WarehouseManager.saveEditor()));
            footer.appendChild(WarehouseManager.createButton(localize(localizationMap.search_manager_cancel),
                () => WarehouseManager.backToList()));
            body.appendChild(footer);
        },

        /**
         * 创建表单字段
         * @param form {Element}
         * @param field {{key, title, tooltip, required, binding}}
         * @returns {{wrapper: Element, input: Element}}
         */
        createField(form, field) {
            const wrapper = document.createElement("div");
            wrapper.className = WarehouseManager.getClass("field");
            wrapper.dataset.fieldKey = field.key === undefined ? "" : field.key;
            if(field.binding) {
                wrapper.dataset.bindingKey = field.binding.key;
                wrapper.dataset.bindingValue = field.binding.value;
            }

            const title = document.createElement("div");
            title.className = WarehouseManager.getClass("field-title");
            title.innerText = `${field.title}${field.required === true ? " *" : ""}`;
            wrapper.appendChild(title);

            if(field.tooltip) {
                const tooltip = document.createElement("div");
                tooltip.className = WarehouseManager.getClass("field-tooltip");
                tooltip.innerHTML = Csp.createHTML(field.tooltip);
                wrapper.appendChild(tooltip);
            }

            const input = document.createElement("div");
            input.className = WarehouseManager.getClass("field-input");
            wrapper.appendChild(input);

            form.appendChild(wrapper);
            return {wrapper: wrapper, input: input};
        },

        /**
         * 根据当前表单内容更新字段的显示与隐藏(与设置页面的binding逻辑一致)
         */
        applyFieldBindings() {
            const form = WarehouseManager.formElement;
            if(!form) return;

            for(const wrapper of form.querySelectorAll(`.${WarehouseManager.getClass("field")}`)) {
                const bindingKey = wrapper.dataset.bindingKey;
                if(!bindingKey) continue;

                const controller = WarehouseManager.formInputs[bindingKey];
                const visible = controller !== undefined && `${controller.value}` === wrapper.dataset.bindingValue;
                if(visible) {
                    wrapper.style.removeProperty("display");
                }else{
                    wrapper.style.setProperty("display", "none", "important");
                }
            }
        },

        /**
         * 读取当前表单内容
         * @returns {{}}
         */
        getFormValues() {
            const values = {};
            for(const key of Object.keys(WarehouseManager.formInputs)) {
                values[key] = WarehouseManager.formInputs[key].value;
            }
            return values;
        },

        /**
         * 切换预设类型(仅保留含义相同的请求头与账号密码,其余字段使用新预设的默认值)
         * @param presetId {string}
         */
        changePreset(presetId) {
            const editing = WarehouseManager.editing;
            const container = WarehouseManager.getContainer();
            const preset = SEARCH_PRESETS[presetId];
            if(!editing || !container || !preset) return;

            const current = editing.warehouse;
            const values = WarehouseManager.getFormValues();
            const config = {...preset.defaults};
            for(const key of ["headers", "username", "password"]) {
                if(values[key] !== undefined && Object.prototype.hasOwnProperty.call(config, key)) {
                    config[key] = values[key];
                }
            }

            editing.warehouse = {
                id: current.id,
                name: `${values.name === undefined ? "" : values.name}`.trim() || preset.title,
                preset: presetId,
                enabled: current.enabled,
                config: config
            };
            WarehouseManager.renderEditor(container);
        },

        /**
         * 保存编辑结果
         */
        saveEditor() {
            const editing = WarehouseManager.editing;
            const container = WarehouseManager.getContainer();
            if(!editing || !container) return;

            const warehouse = editing.warehouse;
            const preset = SEARCH_PRESETS[warehouse.preset];
            if(!preset) return;

            const values = WarehouseManager.getFormValues();
            const form = WarehouseManager.formElement;

            //校验:名称与当前可见的必填字段不能为空
            const missing = [];
            if(`${values.name === undefined ? "" : values.name}`.trim() === "") {
                missing.push(localize(localizationMap.search_manager_name_field));
            }
            for(const field of preset.fields) {
                if(field.required !== true) continue;
                const wrapper = form.querySelector(`[data-field-key="${field.key}"]`);
                if(wrapper && wrapper.style.display === "none") continue;
                if(`${values[field.key] === undefined ? "" : values[field.key]}`.trim() === "") missing.push(field.title);
            }
            if(missing.length > 0) {
                window.alert(localizeFormat(localizationMap.search_manager_required_alert, {fields: missing.join("\n")}));
                return;
            }

            const updated = {
                id: warehouse.id,
                name: `${values.name}`.trim(),
                preset: warehouse.preset,
                enabled: warehouse.enabled === true,
                config: {...preset.defaults}
            };
            for(const field of preset.fields) {
                const value = values[field.key];
                updated.config[field.key] = value === undefined ? preset.defaults[field.key] : value;
            }

            const warehouses = getWarehouses();
            if(editing.isNew) {
                if(warehouses.length >= MAX_SEARCH_WAREHOUSES) {
                    window.alert(localizeFormat(localizationMap.search_manager_add_limit, {max: MAX_SEARCH_WAREHOUSES}));
                    return;
                }
                warehouses.push(updated);
            }else{
                const index = warehouses.findIndex(item => item.id === updated.id);
                if(index < 0) return;
                warehouses[index] = updated;
            }

            setWarehouses(warehouses);
            //配置变化后清除该仓库的登录状态与查询缓存
            if(!editing.isNew) WarehouseManager.clearWarehouseCache(updated);
            rebuildSearchProfiles();
            WarehouseManager.renderList(container);
        },

        /**
         * 返回仓库列表
         */
        backToList() {
            const container = WarehouseManager.getContainer();
            if(container) WarehouseManager.renderList(container);
        }
    };

    const SettingsPopup = {
        showPopup(useTemp = false) {
            let uiBuilder = new SettingPageBuilder(getSettingsUi(), settings);
            let ui = uiBuilder.build(useTemp);
            const displayLangElement = ui.querySelector(`#${VOICELINK_CLASS}_lang`);
            displayLangElement.addEventListener("change", e => {
                settings._s_lang = displayLangElement.value;
                GM_setValue("lang", settings._s_lang);
                ui.remove();
                SettingsPopup.showPopup(true);
            });
            document.body.appendChild(ui);
        }
    };

    //endregion

    //region 初始化加载

    let isInit = false;
    let observing = false;
    function init () {
        if(!isInit) {
            const style = document.createElement("style");
            style.innerHTML = Csp.createHTML(POPUP_CSS + SETTINGS_CSS);
            document.head.appendChild(style);
            // SettingsPopup.getPopup()
            GM_registerMenuCommand("Settings - 设置", () => SettingsPopup.showPopup())
            GM_registerMenuCommand("Notice - 公告", () => showUpdateNotice(true));
            GM_registerMenuCommand("Home / Update - 主页 / 更新", () =>
                GM_openInTab(IS_PREVIEW ? "https://sleazyfork.org/zh-CN/scripts/521128-voicelinks-preview" : "https://sleazyfork.org/zh-CN/scripts/456775-voicelinks", {active: true}));

            document.addEventListener("securitypolicyviolation", function (e) {
                if (e.blockedURI.includes("img.dlsite.jp")) {
                    const img = document.querySelector(`img[src="${e.blockedURI}"]`);
                    img.remove();

                    const imgContainer = Popup.popupElement.img.container;
                    if(imgContainer){
                        imgContainer.style.setProperty("display", "none", "important");  //display = "none !important";
                        Popup.hideImg = true;
                    }
                }
            });

            isInit = true;
        }

        //兼容旧版本的单一仓库设置,并重建运行时使用的仓库配置
        migrateSearchWarehouses();
        settings._ss_cue_lang = settings._s_cue_languages.split(" ").map(val => val.trim());
        rebuildSearchProfiles();
        settings.save();

        setTimeout(() => {
            if(!document.body || observing){
                return;
            }

            Parser.walkNodes(document.body);
            if(!document.getElementById(`${VOICELINK_CLASS}-voice-popup`)) Popup.makePopup(false);

            const observer = new MutationObserver(function (m) {
                //同一批次里被反复改写的节点只需要重建一次
                const rebuildTargets = new Set();

                for (let i = 0; i < m.length; ++i) {
                    if (m[i].type === "characterData") {
                        //文本被就地改写:已解析片段的链接节点可能已经过期,需要回滚重建
                        if (Parser.isRunInvalidated(m[i])) {
                            rebuildTargets.add(m[i].target);
                        }
                        continue;
                    }

                    let addedNodes = m[i].addedNodes;

                    for (let j = 0; j < addedNodes.length; ++j) {
                        Parser.walkNodes(addedNodes[j]);
                    }
                }

                for (const textNode of rebuildTargets) {
                    Parser.rebuildRun(textNode);
                }
            });

            observer.observe(document.body, {
                childList: true,
                subtree: true,
                characterData: true,
                characterDataOldValue: true,
            })
            setUserSelectTitle();

            //显示重要通知
            showUpdateNotice();

            observing = true;
        }, 100);
    }

    document.addEventListener("DOMContentLoaded", init);

    //endregion

    //region 公告显示

    function showUpdateNotice(force = false) {
        const firstTimeToken = 108;
        if(GM_getValue("first_token", undefined) === firstTimeToken && !force){
            return;
        }
        GM_setValue("first_token", firstTimeToken);

        if(!force && window.confirm(localize(localizationMap.notice_update)) === false) {
            return;
        }

        GM_openInTab(
            IS_PREVIEW ? `https://github.com/IceFoxy062/VoiceLinks-Extend/blob/dev/docs/major_updates/v5.0.x/v5.0.x-${settings._s_lang}.md`
                : `https://github.com/IceFoxy062/VoiceLinks-Extend/blob/dev/docs/major_updates/v5.0.x/v5.0.x-${settings._s_lang}.md`,
            {active: true});
    }

    //endregion

    //region CSP绕过

    //Deal with Trusted Types
    let Csp = {
        createHTML: (str) => str
    };
    if(window.isSecureContext === true && window.trustedTypes){
        Csp = window.trustedTypes.createPolicy(
            window.trustedTypes.defaultPolicy ? "VoiceLinkTrustedTypes" : "VoiceLinkTrustedTypes",
            Csp);
    }

    //endregion

    if(!isInit && document.readyState === "complete"){
        init();
    }
})();