Greasy Fork is available in English.

哔哩哔哩(bilibili.com)调整

一、1.自动签到;2.首页新增推荐视频历史记录(仅记录前6个推荐位中的非广告内容),以防误点刷新错过想看的视频。二、动态页调整:默认显示"投稿视频"内容,可自行设置URL以免未来URL发生变化。三、播放页调整:1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置播放器默认模式;3.可设置是否自动选择最高画质;4.新增快速返回播放器漂浮按钮;5.新增点击评论区时间锚点可快速返回播放器;6.网页全屏模式解锁(网页全屏模式下可滚动查看评论,并在播放器控制栏新增快速跳转至评论区按钮);7.将视频简介内容优化后插入评论区或直接替换原简介区内容(替换原简介中固定格式的静态内容为跳转链接);8.视频播放过程中跳转指定时间节点至目标时间节点(可用来跳过片头片尾及中间广告等);9.新增点击视频合集、下方推荐视频、结尾推荐视频卡片快速返回播放器;

// ==UserScript==
// @name              哔哩哔哩(bilibili.com)调整
// @namespace         哔哩哔哩(bilibili.com)调整
// @copyright         QIAN
// @license           GPL-3.0 License
// @version           0.1.37
// @description       一、1.自动签到;2.首页新增推荐视频历史记录(仅记录前6个推荐位中的非广告内容),以防误点刷新错过想看的视频。二、动态页调整:默认显示"投稿视频"内容,可自行设置URL以免未来URL发生变化。三、播放页调整:1.自动定位到播放器(进入播放页,可自动定位到播放器,可设置偏移量及是否在点击主播放器时定位);2.可设置播放器默认模式;3.可设置是否自动选择最高画质;4.新增快速返回播放器漂浮按钮;5.新增点击评论区时间锚点可快速返回播放器;6.网页全屏模式解锁(网页全屏模式下可滚动查看评论,并在播放器控制栏新增快速跳转至评论区按钮);7.将视频简介内容优化后插入评论区或直接替换原简介区内容(替换原简介中固定格式的静态内容为跳转链接);8.视频播放过程中跳转指定时间节点至目标时间节点(可用来跳过片头片尾及中间广告等);9.新增点击视频合集、下方推荐视频、结尾推荐视频卡片快速返回播放器;
// @author            QIAN
// @match             *://www.bilibili.com
// @match             *://www.bilibili.com/video/*
// @match             *://www.bilibili.com/bangumi/play/*
// @match             *://www.bilibili.com/list/*
// @match             *://t.bilibili.com/*
// @require           https://cdn.jsdelivr.net/npm/md5@2.3.0/dist/md5.min.js
// @require           https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js
// @require           https://cdn.jsdelivr.net/npm/axios@1.6.5/dist/axios.min.js
// @require           https://scriptcat.org/lib/513/2.0.0/ElementGetter.js#sha256=KbLWud5OMbbXZHRoU/GLVgvIgeosObRYkDEbE/YanRU=
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_addStyle
// @grant             GM_registerMenuCommand
// @grant             window.onurlchange
// @supportURL        https://github.com/QIUZAIYOU/Bilibili-Adjustment
// @homepageURL       https://github.com/QIUZAIYOU/Bilibili-Adjustment
// @icon              https://www.bilibili.com/favicon.ico?v=1
// ==/UserScript==
// #sha256=KbLWud5OMbbXZHRoU/GLVgvIgeosObRYkDEbE/YanRU=
(function () {
  'use strict';
  let vars = {
    theMainFunctionRunningCount: 0,
    thePrepFunctionRunningCount: 0,
    autoSelectScreenModeRunningCount: 0,
    autoCancelMuteRunningCount: 0,
    webfullUnlockRunningCount: 0,
    autoSelectVideoHighestQualityRunningCount: 0,
    insertGoToCommentButtonCount: 0,
    insertSetSkipTimeNodesButtonCount: 0,
    insertSetSkipTimeNodesSwitchButtonCount: 0,
    setIndexRecordRecommendVideoHistoryArrayCount: 0,
    functionExecutionsCount: 0,
    checkScreenModeSwitchSuccessDepths: 0,
    autoLocationToPlayerRetryDepths: 0,
  }
  let arrays = {
    screenModes: ['wide', 'web'],
    intervalIds: [],
    skipNodesRecords: [],
    indexRecommendVideoHistory: [],
    videoCategoriesActiveClass: ['adjustment_button', 'primary', 'plain'],
  }
  let objects = {
    videoCategories: {
      douga: { name: "动画", tids: [1, 24, 25, 47, 210, 86, 253, 27] },
      anime: { name: "番剧", tids: [13, 51, 152, 32, 33] },
      guochuang: { name: "国创", tids: [167, 153, 168, 169, 170, 195] },
      music: { name: "音乐", tids: [3, 28, 31, 30, 59, 193, 29, 130, 243, 244] },
      dance: { name: "舞蹈", tids: [129, 20, 154, 156, 198, 199, 200, 255] },
      game: { name: "游戏", tids: [4, 17, 171, 172, 65, 173, 121, 136, 19] },
      knowledge: { name: "知识", tids: [36, 201, 124, 228, 207, 208, 209, 229, 122] },
      tech: { name: "科技", tids: [188, 95, 230, 231, 232, 233] },
      sports: { name: "运动", tids: [234, 235, 249, 164, 236, 237, 238] },
      car: { name: "汽车", tids: [223, 245, 246, 247, 248, 240, 227, 176, 258] },
      life: { name: "生活", tids: [160, 138, 250, 251, 239, 161, 162, 21, 254] },
      food: { name: "美食", tids: [211, 76, 212, 213, 214, 215] },
      animal: { name: "动物圈", tids: [217, 218, 219, 220, 221, 222, 75] },
      kichiku: { name: "鬼畜", tids: [119, 22, 26, 126, 216, 127] },
      fashion: { name: "时尚", tids: [155, 157, 252, 158, 159] },
      information: { name: "资讯", tids: [202, 203, 204, 205, 206] },
      ent: { name: "娱乐", tids: [5, 71, 241, 242, 137] },
      cinephile: { name: "影视", tids: [181, 182, 183, 85, 184] },
      documentary: { name: "纪录片", tids: [177, 37, 178, 179, 180] },
      movie: { name: "电影", tids: [23, 147, 145, 146, 83] },
      tv: { name: "电视剧", tids: [11, 185, 187] }
    }
  }
  const selectors = {
    app: '#app',
    header: '#biliMainHeader',
    player: '#bilibili-player',
    playerWrap: '#playerWrap',
    playerWebscreen: '#bilibili-player.mode-webscreen',
    playerContainer: '#bilibili-player .bpx-player-container',
    playerController: '#bilibili-player .bpx-player-ctrl-btn',
    playerControllerBottomRight: '.bpx-player-control-bottom-right',
    playerTooltipArea: '.bpx-player-tooltip-area',
    playerTooltipTitle: '.bpx-player-tooltip-title',
    playerDanmuSetting: '.bpx-player-dm-setting',
    playerEndingRelateVideo: '.bpx-player-ending-related-item',
    volumeButton: '.bpx-player-ctrl-volume-icon',
    mutedButton: '.bpx-player-ctrl-muted-icon',
    video: '#bilibili-player video',
    videoWrap: '#bilibili-player .bpx-player-video-wrap',
    videoBwp: 'bwp-video',
    videoTitleArea: '#viewbox_report',
    videoFloatNav: '.fixed-sidenav-storage',
    videoComment: '#comment',
    videoCommentReplyList: '#comment .reply-list',
    videoRootReplyContainer: '#comment .root-reply-container',
    videoTime: '.video-time,.video-seek',
    videoDescription: '#v_desc',
    videoDescriptionInfo: '#v_desc .basic-desc-info',
    videoDescriptionText: '#v_desc .desc-info-text',
    videoNextPlayAndRecommendLink: '.video-page-card-small .card-box',
    videoSectionsEpisodeLink: '.video-sections-content-list .video-episode-card',
    videoMultiPageLink: '#multi_page ul li',
    bangumiApp: '#__next',
    bangumiComment: '#comment_module',
    bangumiFloatNav: '#__next div[class*="navTools_floatNavExp"] div[class*="navTools_navMenu"]',
    bangumiMainContainer: '.main-container',
    bangumiSectionsEpisodeLink: '#__next div[class*="numberList_wrapper"] div[class*="numberListItem_number_list_item"] ',
    qualitySwitchButtons: '.bpx-player-ctrl-quality-menu-item',
    screenModeWideEnterButton: '.bpx-player-ctrl-wide-enter',
    screenModeWideLeaveButton: '.bpx-player-ctrl-wide-leave',
    screenModeWebEnterButton: '.bpx-player-ctrl-web-enter',
    screenModeWebLeaveButton: '.bpx-player-ctrl-web-leave',
    screenModeFullControlButton: '.bpx-player-ctrl-full',
    danmukuBox: '#danmukuBox',
    danmuShowHideTip: 'div[aria-label="弹幕显示隐藏"]',
    membersContainer: '.members-info-container',
    membersUpAvatarFace: '.membersinfo-upcard:first-child picture img',
    upAvatarFace: '.up-info-container .up-avatar-wrap .bili-avatar .bili-avatar-face',
    upAvatarDecoration: '.up-info-container .up-avatar-wrap .bili-avatar .bili-avatar-pendent-dom .bili-avatar-img',
    upAvatarIcon: '.up-info-container .up-avatar-wrap .bili-avatar .bili-avatar-icon',
    setSkipTimeNodesPopover: '#setSkipTimeNodesPopover',
    setSkipTimeNodesPopoverToggleButton: '#setSkipTimeNodesPopoverToggleButton',
    setSkipTimeNodesPopoverHeaderExtra: '#setSkipTimeNodesPopover .header .extra',
    setSkipTimeNodesPopoverTips: '#setSkipTimeNodesPopover .tips',
    setSkipTimeNodesPopoverTipsDetail: '#setSkipTimeNodesPopover .tips .detail',
    setSkipTimeNodesPopoverTipsContents: '#setSkipTimeNodesPopover .tips .contents',
    setSkipTimeNodesPopoverRecords: '#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .records',
    setSkipTimeNodesPopoverClouds: '#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .clouds',
    setSkipTimeNodesPopoverResult: '#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .result',
    setSkipTimeNodesInput: '#setSkipTimeNodesInput',
    skipTimeNodesRecordsArray: '#skipTimeNodesRecordsArray',
    skipTimeNodesCloudsArray: '#skipTimeNodesCloudsArray',
    clearRecordsButton: '#clearRecordsButton',
    saveRecordsButton: '#saveRecordsButton',
    uploadSkipTimeNodesButton: '#uploadSkipTimeNodesButton',
    syncSkipTimeNodesButton: '#syncSkipTimeNodesButton',
    indexApp: '#i_cecream',
    indexRecommendVideoSix: '.recommended-container_floor-aside .feed-card:nth-child(-n+7)',
    indexRecommendVideoRollButtonWrapper: '.recommended-container_floor-aside .feed-roll-btn',
    indexRecommendVideoHistoryPopoverTitle: '#indexRecommendVideoHistoryPopoverTitle',
    indexRecommendVideoRollButton: '.recommended-container_floor-aside .feed-roll-btn button.roll-btn',
    indexRecommendVideoHistoryOpenButton: '#indexRecommendVideoHistoryOpenButton',
    indexRecommendVideoHistoryPopover: '#indexRecommendVideoHistoryPopover',
    indexRecommendVideoHistoryCategory: '#indexRecommendVideoHistoryCategory',
    indexRecommendVideoHistoryCategoryButtons: '#indexRecommendVideoHistoryCategory li',
    indexRecommendVideoHistoryCategoryButtonsExceptAll: '#indexRecommendVideoHistoryCategory li:not(.all)',
    indexRecommendVideoHistoryCategoryButtonAll: '#indexRecommendVideoHistoryCategory li.all',
    indexRecommendVideoHistoryList: '#indexRecommendVideoHistoryList',
    clearRecommendVideoHistoryButton: '#clearRecommendVideoHistoryButton',
    dynamicSettingPopover: '#dynamicSettingPopover',
    dynamicSettingSaveButton: '#dynamicSettingSaveButton',
    dynamicSettingPopoverTips: '#dynamicSettingPopoverTips',
    dynamicHeaderContainer: '#bili-header-container',
    videoSettingPopover: '#videoSettingPopover',
    videoSettingSaveButton: '#videoSettingSaveButton',
    notChargeHighLevelCover: '.not-charge-high-level-cover',
    AutoSkipSwitchInput: '#Auto-Skip-Switch',
    WebVideoLinkInput: '#Web-Video-Link',
    IsVip: '#Is-Vip',
    AutoLocate: '#Auto-Locate',
    AutoLocateVideo: '#Auto-Locate-Video',
    AutoLocateBangumi: '#Auto-Locate-Bangumi',
    TopOffset: '#Top-Offset',
    ClickPlayerAutoLocation: '#Click-Player-Auto-Location',
    AutoQuality: '#Auto-Quality',
    Quality4K: '#Quality-4K',
    Quality8K: '#Quality-8K',
    Checkbox4K: '.adjustment_checkbox.fourK',
    Checkbox8K: '.adjustment_checkbox.eightK',
    FourKAndEightK: '.fourK,.eightK',
    SelectScreenMode: 'input[name="Screen-Mode"]',
    WebfullUnlock: '#Webfull-Unlock',
    AutoReload: '#Auto-Reload',
    AutoSkip: '#Auto-Skip',
    InsertVideoDescriptionToComment: '#Insert-Video-Description-To-Comment',
    PauseVideo: '#PauseVideo',
    ContinuePlay: '#ContinuePlay',
  }
  const vals = {
    is_vip: () => { return utils.getValue('is_vip') },
    player_type: () => { return utils.getValue('player_type') },
    offset_top: () => { return Math.trunc(utils.getValue('offset_top')) },
    auto_locate: () => { return utils.getValue('auto_locate') },
    get_offset_method: () => { return utils.getValue('get_offset_method') },
    auto_locate_video: () => { return utils.getValue('auto_locate_video') },
    auto_locate_bangumi: () => { return utils.getValue('auto_locate_bangumi') },
    click_player_auto_locate: () => { return utils.getValue('click_player_auto_locate') },
    video_player_offset_top: () => { return Math.trunc(utils.getValue('video_player_offset_top')) },
    bangumi_player_offset_top: () => { return Math.trunc(utils.getValue('bangumi_player_offset_top')) },
    current_screen_mode: () => { return utils.getValue('current_screen_mode') },
    selected_screen_mode: () => { return utils.getValue('selected_screen_mode') },
    auto_select_video_highest_quality: () => { return utils.getValue('auto_select_video_highest_quality') },
    contain_quality_4k: () => { return utils.getValue('contain_quality_4k') },
    contain_quality_8k: () => { return utils.getValue('contain_quality_8k') },
    webfull_unlock: () => { return utils.getValue('webfull_unlock') },
    auto_reload: () => { return utils.getValue('auto_reload') },
    auto_skip: () => { return utils.getValue('auto_skip') },
    insert_video_description_to_comment: () => { return utils.getValue('insert_video_description_to_comment') },
    web_video_link: () => { return utils.getValue('web_video_link') },
    signIn_date: () => { return utils.getValue('signIn_date') },
    dev_checkScreenModeSwitchSuccess_method: () => { return utils.getValue('dev_checkScreenModeSwitchSuccess_method') },
    pause_video: () => { return utils.getValue('pause_video') },
    continue_play: () => { return utils.getValue('continue_play') },
  }
  const styles = {
    BilibiliAdjustment: '.adjustment_popover{position:fixed;top:50%;left:50%;box-sizing:border-box;margin:0;padding:20px;width:400px;max-height:70vh;border:none;border-radius:6px;font-size:1em;transform:translate(-50%,-50%);overscroll-behavior:contain}.adjustment_popover::backdrop{backdrop-filter:blur(3px)}.adjustment_popoverTitle{margin-bottom:15px;padding-bottom:20px;border-bottom:1px solid #dcdfe6;text-align:center;font-weight:700;font-size:22px}.adjustment_buttonGroup{display:flex;margin-top:10px;align-items:center;justify-content:end;gap:10px}.adjustment_button{display:inline-block;box-sizing:border-box;margin:0;padding:10px 20px;outline:0;border:1px solid #dcdfe6;border-radius:4px;background:#fff;color:#606266;text-align:center;white-space:nowrap;font-weight:500;font-size:14px;line-height:1;cursor:pointer;transition:.1s;-webkit-appearance:none;-moz-user-select:none;-webkit-user-select:none;-ms-user-select:none}.adjustment_button.plain:disabled,.adjustment_button.plain:disabled:active,.adjustment_button.plain:disabled:focus,.adjustment_button.plain:disabled:hover,.adjustment_button:disabled,.adjustment_button:disabled:active,.adjustment_button:disabled:focus,.adjustment_button:disabled:hover{border-color:#ebeef5;background-color:#fff;background-image:none;color:#c0c4cc;cursor:not-allowed}.adjustment_button.primary{border-color:#409eff;background-color:#409eff;color:#fff}.adjustment_button.success{border-color:#67c23a;background-color:#67c23a;color:#fff}.adjustment_button.info{border-color:#909399;background-color:#909399;color:#fff}.adjustment_button.warning{border-color:#e6a23c;background-color:#e6a23c;color:#fff}.adjustment_button.danger{border-color:#f56c6c;background-color:#f56c6c;color:#fff}.adjustment_button.primary:focus,.adjustment_button.primary:hover{border-color:#66b1ff;background:#66b1ff;color:#fff}.adjustment_button.success:focus,.adjustment_button.success:hover{border-color:#85ce61;background:#85ce61;color:#fff}.adjustment_button.info:focus,.adjustment_button.info:hover{border-color:#a6a9ad;background:#a6a9ad;color:#fff}.adjustment_button.warning:focus,.adjustment_button.warning:hover{border-color:#ebb563;background:#ebb563;color:#fff}.adjustment_button.danger:focus,.adjustment_button.danger:hover{border-color:#f78989;background:#f78989;color:#fff}.adjustment_button.primary.plain{border-color:#b3d8ff;background:#ecf5ff;color:#409eff}.adjustment_button.success.plain{border-color:#c2e7b0;background:#f0f9eb;color:#67c23a}.adjustment_button.info.plain{border-color:#a6a9ad;background:#a6a9ad;color:#fff}.adjustment_button.warning.plain{border-color:#f5dab1;background:#fdf6ec;color:#e6a23c}.adjustment_button.danger.plain{border-color:#fbc4c4;background:#fef0f0;color:#f56c6c}.adjustment_button.primary.plain:focus,.adjustment_button.primary.plain:hover{border-color:#409eff;background:#409eff;color:#fff}.adjustment_button.success.plain:focus,.adjustment_button.success.plain:hover{border-color:#67c23a;background-color:#67c23a;color:#fff}.adjustment_button.info.plain:focus,.adjustment_button.info.plain:hover{border-color:#909399;background-color:#909399;color:#fff}.adjustment_button.warning.plain:focus,.adjustment_button.warning.plain:hover{border-color:#e6a23c;background-color:#e6a23c;color:#fff}.adjustment_button.danger.plain:focus,.adjustment_button.danger.plain:hover{border-color:#f56c6c;background-color:#f56c6c;color:#fff}.adjustment_button.primary:disabled,.adjustment_button.primary:disabled:active,.adjustment_button.primary:disabled:focus,.adjustment_button.primary:disabled:hover{border-color:#a0cfff;background-color:#a0cfff;color:#fff}.adjustment_button.success:disabled,.adjustment_button.success:disabled:active,.adjustment_button.success:disabled:focus,.adjustment_button.success:disabled:hover{border-color:#b3e19d;background-color:#b3e19d;color:#fff}.adjustment_button.info:disabled,.adjustment_button.info:disabled:active,.adjustment_button.info:disabled:focus,.adjustment_button.info:disabled:hover{border-color:#c8c9cc;background-color:#c8c9cc;color:#fff}.adjustment_button.warning:disabled,.adjustment_button.warning:disabled:active,.adjustment_button.warning:disabled:focus,.adjustment_button.warning:disabled:hover{border-color:#f3d19e;background-color:#f3d19e;color:#fff}.adjustment_button.danger:disabled,.adjustment_button.danger:disabled:active,.adjustment_button.danger:disabled:focus,.adjustment_button.danger:disabled:hover{border-color:#fab6b6;background-color:#fab6b6;color:#fff}.adjustment_button.primary.plain:disabled,.adjustment_button.primary.plain:disabled:active,.adjustment_button.primary.plain:disabled:focus,.adjustment_button.primary.plain:disabled:hover{border-color:#d9ecff;background-color:#ecf5ff;color:#8cc5ff}.adjustment_button.success.plain:disabled,.adjustment_button.success.plain:disabled:active,.adjustment_button.success.plain:disabled:focus,.adjustment_button.success.plain:disabled:hover{border-color:#e1f3d8;background-color:#f0f9eb;color:#a4da89}.adjustment_button.info.plain:disabled,.adjustment_button.info.plain:disabled:active,.adjustment_button.info.plain:disabled:focus,.adjustment_button.info.plain:disabled:hover{border-color:#e9e9eb;background-color:#f4f4f5;color:#bcbec2}.adjustment_button.warning.plain:disabled,.adjustment_button.warning.plain:disabled:active,.adjustment_button.warning.plain:disabled:focus,.adjustment_button.warning.plain:disabled:hover{border-color:#faecd8;background-color:#fdf6ec;color:#f0c78a}.adjustment_button.danger.plain:disabled,.adjustment_button.danger.plain:disabled:active,.adjustment_button.danger.plain:disabled:focus,.adjustment_button.danger.plain:disabled:hover{border-color:#fde2e2;background-color:#fef0f0;color:#f9a7a7}.adjustment_tips{display:inline-block;box-sizing:border-box;padding:3px 5px;height:fit-content;border:1px solid #d9ecff;border-radius:4px;background-color:#ecf5ff;color:#409eff;font-size:14px;line-height:1.5}.adjustment_tips.info{border-color:#e9e9eb;background-color:#f4f4f5;color:#909399}.adjustment_tips.success{border-color:#e1f3d8;background-color:#f0f9eb;color:#67c23a}.adjustment_tips.warning{border-color:#faecd8;background-color:#fdf6ec;color:#e6a23c}.adjustment_tips.danger{border-color:#fde2e2;background-color:#fef0f0;color:#f56c6c}.adjustment_form,.adjustment_form_item{display:flex;flex-direction:column}.adjustment_form{gap:5px}.adjustment_form_item{gap:5px}.adjustment_checkbox,.adjustment_form_item_content{display:flex;align-items:center;justify-content:space-between}.adjustment_form_item label{font-size:18px}.adjustment_checkboxGroup{display:flex;align-items:center;justify-content:flex-start;gap:10px}.adjustment_checkbox{font-size:16px;gap:3px}.adjustment_input{display:inline-flex;padding:1px 11px;outline:0;border:1px solid #dcdfe6;border-radius:6px;background:#f5f5f5;line-height:32px;cursor:text;flex-grow:1;align-items:center;justify-content:center}',
    IndexAdjustment: '#indexRecommendVideoHistoryOpenButton{margin-top:10px}#indexRecommendVideoHistoryPopover{width:600px}#indexRecommendVideoHistoryPopover #indexRecommendVideoHistoryPopoverTitle{display:flex;box-sizing:border-box;padding-bottom:15px;border-bottom:1px solid #dcdfe6;font-weight:700;font-size:22px;align-items:center;justify-content:space-between}#indexRecommendVideoHistoryPopover ul{display:flex;flex-direction:column;align-items:center;justify-content:space-between}ul#indexRecommendVideoHistoryCategory{display:grid;margin:10px 0 0;padding-bottom:10px;border-bottom:1px solid #dcdfe6!important;gap:5px;grid-template-columns:repeat(8,1fr);align-items:center;justify-content:center}ul#indexRecommendVideoHistoryCategory li{padding:3px 0!important;border:1px solid;border-radius:6px;justify-content:center}#indexRecommendVideoHistoryPopover ul li{display:flex;align-items:center;padding:7px 0;width:100%;border-color:#dcdfe6!important;border-style:solid;line-height:24px;border-bottom-width:1px}#indexRecommendVideoHistoryPopover #indexRecommendVideoHistoryList li span{display:flex;width:32px;height:24px;margin-right:5px;border-radius:4px; overflow:hidden}#indexRecommendVideoHistoryPopover #indexRecommendVideoHistoryList li span img{width:100%;height:24px;object-fit:inherit}#indexRecommendVideoHistoryPopover #indexRecommendVideoHistoryList li a{color:#333!important}#indexRecommendVideoHistoryPopover #indexRecommendVideoHistoryList li:hover a{color:#00a1d6!important}#clearRecommendVideoHistoryButton{position:sticky;display:flex;padding:10px;width:80px;border-radius:6px;background:#00a1d6;color:#fff;font-size:15px;line-height:16px;cursor:pointer;align-items:center;justify-content:center}',
    VideoPageAdjustment: '.back-to-top-wrap .locate{visibility:hidden}.back-to-top-wrap:has(.visible) .locate{visibility:visible}.bpx-player-container[data-screen=full] #goToComments{opacity:.6;cursor:not-allowed;pointer-events:none}#comment-description .user-name{display:flex;padding:0 5px;height:22px;border:1px solid;border-radius:4px;align-items:center;justify-content:center}.bpx-player-ctrl-skip{border:none!important;background:0 0!important}.bpx-player-container[data-screen=full] #setSkipTimeNodesPopoverToggleButton,.bpx-player-container[data-screen=web] #setSkipTimeNodesPopoverToggleButton{height:32px!important;line-height:32px!important}#setSkipTimeNodesPopover{top:50%!important;left:50%!important;box-sizing:border-box!important;padding:15px!important;max-width:456px!important;border:0!important;border-radius:6px!important;font-size:14px!important;transform:translate(-50%,-50%)!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper{display:flex!important;flex-direction:column!important;gap:7px!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper button{display:flex!important;width:100%;height:34px!important;border-style:solid!important;border-width:1px!important;border-radius:6px!important;text-align:center!important;line-height:34px!important;cursor:pointer;align-items:center!important;justify-content:center!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper button:disabled{cursor:not-allowed}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .header{display:flex!important;font-weight:700!important;align-items:center!important;justify-content:space-between!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .header .title{font-weight:700!important;font-size:16px!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .header .extra{font-size:12px!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .header .extra,#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .result{padding:2px 5px!important;border:1px solid #d9ecff!important;border-radius:6px!important;background-color:#ecf5ff!important;color:#409eff!important;font-weight:400!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .success{display:flex!important;padding:2px 5px!important;border-color:#e1f3d8!important;background-color:#f0f9eb!important;color:#67c23a!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .danger{display:flex!important;padding:2px 5px!important;border-color:#fde2e2!important;background-color:#fef0f0!important;color:#f56c6c!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .handles{display:flex!important;align-items:center!important;justify-content:space-between!important;gap:7px!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .tips{position:relative!important;overflow:hidden;box-sizing:border-box!important;padding:7px!important;border-color:#e9e9eb!important;border-radius:6px!important;background-color:#f4f4f5!important;color:#909399!important;font-size:13px!important;transition:height .3s!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .tips.open{height:134px!important;line-height:20px!important;}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .tips.close{height:34px!important;line-height:22px!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .tips .detail{position:absolute!important;top:9px!important;right:7px!important;display:flex!important;cursor:pointer!important;transition:transform .3s!important}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .tips .detail.open{transform:rotate(0)}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .tips .detail.close{transform:rotate(180deg)}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .records{display:none;flex-direction:column!important;gap:7px}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .records .recordsButtonsGroup{display:flex!important;align-items:center!important;justify-content:space-between!important;gap:7px!important}#clearRecordsButton{border-color:#d3d4d6!important;background:#f4f4f5!important;color:#909399!important}#clearRecordsButton:disabled{border-color:#e9e9eb!important;background-color:#f4f4f5!important;color:#bcbec2!important}#saveRecordsButton{border-color:#c2e7b0!important;background:#f0f9eb!important;color:#67c23a!important}#saveRecordsButton:disabled{border-color:#e1f3d8!important;background-color:#f0f9eb!important;color:#a4da89!important}#setSkipTimeNodesInput{box-sizing:border-box!important;padding:5px!important;width:calc(100% - 39px)!important;height:34px!important;border:1px solid #cecece!important;border-radius:6px!important;line-height:34px!important}#uploadSkipTimeNodesButton,#syncSkipTimeNodesButton{width:52px!important;height:34px!important;border:none!important;background:#00a1d6!important;color:#fff!important}#uploadSkipTimeNodesButton:hover{background:#00b5e5!important}#skipTimeNodesRecordsArray{display:flex!important;padding:2px 5px!important;border-radius:6px!important}#bilibili-player .bpx-player-video-wrap{position:relative}#bilibili-player .bpx-player-video-wrap:before{position:absolute;display:block;background:var(--video-cover) top left no-repeat;background-size:cover;content:"";inset:0}#setSkipTimeNodesPopover .setSkipTimeNodesWrapper .records{display:none}',
    BodyHidden: 'body{overflow:hidden!important}',
    ResetPlayerLayout: 'body{padding-top:0;position:auto}#playerWrap{display:block}#bilibili-player{height:auto;position:relative}.bpx-player-mini-warp{display:none}',
    UnlockWebscreen: 'body.webscreen-fix{padding-top:BODYHEIGHT;position:unset}#bilibili-player.mode-webscreen{height:BODYHEIGHT;position:absolute}#playerWrap{display:none}#danmukuBox{margin-top:0}',
    FreezeHeaderAndVideoTitle: '#biliMainHeader{height:64px!important}#viewbox_report{height:108px!important;padding-top:22px!important}.members-info-container{height:86px!important;overflow:hidden!important;padding-top:11px!important}.membersinfo-wide .header{display:none!important}',
    DynamicSetting: '#dynamicSettingPopoverTitle{margin-bottom:15px;text-align:center;font-weight:700;font-size:21px}#dynamicSettingPopover #dynamicSettingPopoverTips{margin-top:5px}',
    VideoSetting: '#videoSettingPopover{width:550px;max-height:90vh}#Top-Offset{flex-grow:.5}',
    UnlockEpisodeSelector: '.bpx-player-control-bottom-right .bpx-player-ctrl-btn.bpx-player-ctrl-eplist{visibility:visible!important;width:36px!important}'
  }
  const regexps = {
    // 如果使用全局检索符(g),则在多次使用 RegExp.prototype.test() 时会导致脚本执行失败,
    // 因为在全局检索符下(g), RegExp.prototype.test() 在匹配成功后会设置下一次匹配的起始索引 lastindex
    // 但是当前页面的 URL 为固定字符串,在上一次匹配成功后设置的 lastindex 后没有其他字符串,所以会匹配失败
    // 例如:使用 /asifadeaway/g.test('https://www.asifadeaway.com/post/Watched.html') 检查是否含有字符串'asifadeaway',此时返回 true 并将 lastindex 设为 23
    // 后续再执行一次同样的检查则会返回 false 并将 lastindex 设为 0,因为继上次检查匹配成功后再次检查会从索引位置 23 开始,而此位置往后并没有字符串'asifadeaway'
    // 以下的正则表达式都包含了整个 URL 字符串,所以匹配成功一次之后 lastindex 会被设置为 URL 字符串的长度,再次执行后必定返回 false
    // 所以会产生匹配成功之后再次匹配就会失败的奇怪现象,就是因为 lastindex 的值在上一次匹配成功后被设为了字符串的长度
    // 因此不使用全局检索符(g)
    video: /.*:\/\/www\.bilibili\.com\/(video|bangumi\/play|list)\/.*/i,
    dynamic: /.*:\/\/t\.bilibili\.com\/.*/i,
  }
  const utils = {
    /**
     * 初始化所有数据
     * - #region 初始化所有数据
     */
    initValue() {
      const value = [{
        name: 'is_vip',
        value: true,
      }, {
        name: 'player_type',
        value: 'video',
      }, {
        name: 'offset_top',
        value: 5,
      }, {
        name: 'video_player_offset_top',
        value: 168,
      }, {
        name: 'bangumi_player_offset_top',
        value: 104,
      }, {
        name: 'auto_locate',
        value: true,
      }, {
        name: 'get_offset_method',
        value: 'function',
      }, {
        name: 'auto_locate_video',
        value: true,
      }, {
        name: 'auto_locate_bangumi',
        value: true,
      }, {
        name: 'click_player_auto_locate',
        value: true,
      }, {
        name: 'current_screen_mode',
        value: 'normal',
      }, {
        name: 'selected_screen_mode',
        value: 'wide',
      }, {
        name: 'auto_select_video_highest_quality',
        value: true,
      }, {
        name: 'contain_quality_4k',
        value: false,
      }, {
        name: 'contain_quality_8k',
        value: false,
      }, {
        name: 'webfull_unlock',
        value: false,
      }, {
        name: 'auto_reload',
        value: false,
      }, {
        name: 'auto_skip',
        value: false,
      }, {
        name: 'insert_video_description_to_comment',
        value: true
      }, {
        name: 'web_video_link',
        value: 'https://t.bilibili.com/?tab=video'
      }, {
        name: 'signIn_date',
        value: ''
      }, {
        name: 'dev_checkScreenModeSwitchSuccess_method',
        value: 'interval'
      }, {
        name: 'pause_video',
        value: false
      }, {
        name: 'continue_play',
        value: false
      },
      ]
      value.forEach(v => {
        if (utils.getValue(v.name) === undefined) {
          utils.setValue(v.name, v.value)
        }
      })
    },
    // #endregion 初始化所有数据
    /**
     * 获取自定义数据
     * - #region 获取自定义数据
     * @param {String} 数据名称
     * @returns 数据数值
     */
    getValue(name) {
      return GM_getValue(name)
    },
    // #endregion 获取自定义数据
    /**
     * 写入自定义数据
     * - #region 写入自定义数据
     * @param {String} 数据名称
     * @param {*} 数据数值
     */
    setValue(name, value) {
      GM_setValue(name, value)
    },
    // #endregion 写入自定义数据
    /**
     * 休眠
     * - #region 休眠
     * @param {Number} 时长
     * @returns
     */
    sleep(times) {
      return new Promise(resolve => setTimeout(resolve, times))
    },
    // #endregion 休眠
    /**
     * 通过名称获取指定cookie的值
     * - #region 通过名称获取指定cookie的值
     * @param {String} name cookie中某一项的名称
     * @returns 
     */
    getCookieByName(name) {
      const value = `; ${document.cookie}`;
      const parts = value.split(`; ${name}=`);
      if (parts.length === 2) return parts.pop().split(';').shift();
      return null;
    },
    // #endregion 通过名称获取指定cookie的值
    /**
     * 判断数组长度是否为偶数
     * - #region 判断数组长度是否为偶数
     */
    isArrayLengthEven(arr) {
      return arr.length % 2 === 0
    },
    // #endregion 判断数组长度是否为偶数
    /**
     * 向文档插入自定义样式
     * - #region 向文档插入自定义样式
     * @param {String} id 样式表id
     * @param {String} css 样式内容
     */
    insertStyleToDocument(id, css) {
      const styleElement = GM_addStyle(css)
      styleElement.id = id
    },
    // #endregion 向文档插入自定义样式
    /**
     * 自定义日志打印
     * - #region 自定义日志打印
     * - info->信息;warn->警告
     * - error->错误;debug->调试
     */
    logger: {
      info(content) {
        console.info('%c播放页调整', 'color:white;background:#006aff;padding:2px;border-radius:2px', content);
      },
      warn(content) {
        console.warn('%c播放页调整', 'color:white;background:#ff6d00;padding:2px;border-radius:2px', content);
      },
      error(content) {
        console.error('%c播放页调整', 'color:white;background:#f33;padding:2px;border-radius:2px', content);
      },
      debug(content) {
        console.info('%c播放页调整(调试)', 'color:white;background:#cc00ff;padding:2px;border-radius:2px', content);
      },
    },
    // #endregion 自定义日志打印
    /**
     * 检查当前文档是否被激活
     * - #region 检查当前文档是否被激活
     */
    checkDocumentIsHidden() {
      const visibilityChangeEventNames = ['visibilitychange', 'mozvisibilitychange', 'webkitvisibilitychange', 'msvisibilitychange']
      const documentHiddenPropertyName = visibilityChangeEventNames.find(name => name in document) || 'onfocusin' in document || 'onpageshow' in window ? 'hidden' : null
      if (documentHiddenPropertyName !== null) {
        const isHidden = () => document[documentHiddenPropertyName]
        const onChange = () => isHidden()
        // 添加各种事件监听器
        visibilityChangeEventNames.forEach(eventName => document.addEventListener(eventName, onChange))
        window.addEventListener('focus', onChange)
        window.addEventListener('blur', onChange)
        window.addEventListener('pageshow', onChange)
        window.addEventListener('pagehide', onChange)
        document.onfocusin = document.onfocusout = onChange
        return isHidden()
      }
      // 如果无法判断是否隐藏,则返回undefined
      return undefined
    },
    // #endregion 检查当前文档是否被激活
    /**
     * 刷新当前页面
     * - #region 刷新当前页面
     */
    reloadCurrentTab(...args) {
      if (args && args[0] === true) {
        location.reload()
      } else if (vals.auto_reload()) location.reload()
    },
    // #endregion 刷新当前页面
    /**
     * 滚动文档至目标位置
     * - #region 滚动文档至目标位置
     * @param {Number} 滚动距离
     */
    documentScrollTo(offset) {
      document.documentElement.scrollTop = offset
    },
    // #endregion 滚动文档至目标位置
    /**
     * 获取指定 meta 标签的属性值
     * - #region 获取指定meta标签的属性值
     * @param {*} attribute 属性名称
     * @returns 属性值
     */
    async getMetaContent(attribute) {
      const meta = await utils.getElementAndCheckExistence(`meta[${attribute}]`)
      if (meta) {
        return meta.getAttribute('content')
      } else {
        return null
      }
    },
    // #endregion 获取指定meta标签的属性值
    /**
     * 获取 Body 元素高度
     * - #region 获取Body元素高度
     * @returns Body 元素高度
     */
    getBodyHeight() {
      const bodyHeight = document.body.clientHeight || 0
      const docHeight = document.documentElement.clientHeight || 0
      return bodyHeight < docHeight ? bodyHeight : docHeight
    },
    // #endregion 获取Body元素高度
    /**
     * 确保页面销毁时清除所有定时器
     * - #region 确保页面销毁时清除所有定时器
     */
    clearAllTimersWhenCloseTab() {
      window.addEventListener('beforeunload', () => {
        for (let id of arrays.intervalIds) {
          clearInterval(id)
        }
        arrays.intervalIds = []
      })
    },
    // #endregion 确保页面销毁时清除所有定时器
    /**
     * 获取目标元素至文档距离
     * - #region 获取目标元素至文档距离
     * @param {String} 目标元素
     * @returns 顶部和左侧距离
     */
    getElementOffsetToDocument(element) {
      let rect, win
      if (!element.getClientRects().length) {
        return {
          top: 0,
          left: 0
        }
      }
      rect = element.getBoundingClientRect()
      win = element.ownerDocument.defaultView
      return {
        top: rect.top + win.pageYOffset,
        left: rect.left + win.pageXOffset
      }
    },
    // #endregion 获取目标元素至文档距离
    /**
     * 创建并插入元素至目标元素
     * - #region 创建并插入元素至目标元素
     * @param {String} Html 字符串
     * @param {Element} 目标元素
     * @param {String} 插入方法(before/after/prepend/append)
     * @returns 被创建的元素
     */
    createElementAndInsert(HtmlString, target, method) {
      const element = elmGetter.create(HtmlString, target)
      target[method](element)
      return element
    },
    // #endregion 创建并插入元素至目标元素
    /**
     * 判断函数是否为异步函数
     * - #region 判断函数是否为异步函数
     * - 不使用 targetFunction() instanceof Promise 方法
     * - 因为这会导致 targetFunction 函数在此处执行一遍,从而增加 vars 里相关的计数变量
     * - 当之后真正执行时会因为相关计数变量值不等于 1 导致在 executeFunctionsSequentially 函数里获取不到返回值
     */
    isAsyncFunction(targetFunction) {
      return targetFunction.constructor.name === 'AsyncFunction'
    },
    // #endregion 判断函数是否为异步函数
    /**
     * 按顺序依次执行函数数组中的函数
     * - #region 按顺序依次执行函数数组中的函数
     * @param {Array} functionsArray 待执行的函数数组
     * - 当函数为异步函数时,只有当前一个函数执行完毕时才会继续执行下一个函数
     * - 当函数为同步函数时,则只会执行相应函数
     */
    executeFunctionsSequentially(functionsArray) {
      if (functionsArray.length > 0) {
        // console.log(functionsArray.length)
        const currentFunction = functionsArray.shift()
        if (utils.isAsyncFunction(currentFunction)) {
          currentFunction().then(result => {
            // console.log(currentFunction.name, result)
            if (result) {
              const { message, callback } = result
              if (message) utils.logger.info(message)
              if (callback && Array.isArray(callback)) utils.executeFunctionsSequentially(callback)
            }
            // else utils.logger.debug(currentFunction.name)
            utils.executeFunctionsSequentially(functionsArray)
          }).catch(error => {
            utils.logger.error(error)
            utils.reloadCurrentTab()
          })
        } else {
          // console.log(currentFunction.name, result)
          const result = currentFunction()
          if (result) {
            const { message } = result
            if (message) utils.logger.info(message)
          }
        }
      }
    },
    // #endregion 按顺序依次执行函数数组中的函数
    /**
     * 检查元素数组中元素是否存在
     * - #region 检查元素数组中元素是否存在
     * @param {Array} elementsArray 元素数组
     */
    checkElementExistence(elementsArray) {
      if (Array.isArray(elementsArray)) {
        return elementsArray.map(element => Boolean(element))
      } else {
        return [Boolean(elementsArray)]
      }
    },
    // #endregion 检查元素数组中元素是否存在
    /**
     * 获取元素并检查元素是否存在
     * - #region 获取元素并检查元素是否存在
     * @param {String | String[]} selectors 元素选择器
     * @param {Number} delay 超时时间
     * @param {Boolean} debug debug 开关
     * @returns 获取的元素
     */
    async getElementAndCheckExistence(selectors, ...args) {
      let delay = 7000, debug = false
      if (args.length === 1) {
        const type = typeof args[0]
        if (type === 'number') delay = args[0]
        if (type === 'boolean') debug = args[0]
      }
      if (args.length === 2) {
        delay = args[0]
        debug = args[1]
      }
      const result = await elmGetter.get(selectors, delay)
      if (debug) utils.logger.debug(utils.checkElementExistence(result))
      return result
    },
    // #endregion 获取元素并检查元素是否存在
    /**
     * 为元素添加监听器并执行相应函数
     * - #region 为元素添加监听器并执行相应函数
     */
    async addEventListenerToElement() {
      if (window.location.href === 'https://www.bilibili.com/') {
        const [$indexRecommendVideoRollButton, $clearRecommendVideoHistoryButton] = await utils.getElementAndCheckExistence([selectors.indexRecommendVideoRollButton, selectors.clearRecommendVideoHistoryButton])
        $indexRecommendVideoRollButton.addEventListener('click', () => {
          const functionsArray = [
            modules.setIndexRecordRecommendVideoHistory,
            modules.getIndexRecordRecommendVideoHistory,
            modules.generatorVideoCategories
          ]
          setTimeout(() => {
            vars.setIndexRecordRecommendVideoHistoryArrayCount = 0
            utils.executeFunctionsSequentially(functionsArray)
          }, 1000)
        })
        $clearRecommendVideoHistoryButton.addEventListener('click', () => {
          modules.clearRecommendVideoHistory()
        })
      }
      if (regexps.video.test(window.location.href)) {
        if (window.onurlchange === null) {
          window.addEventListener('urlchange', () => {
            modules.locationToPlayer()
            modules.insertVideoDescriptionToComment()
            // utils.logger.debug('URL改变了!')
          })
        } else {
          modules.clickRelatedVideoAutoLocation()
        }
        window.addEventListener("popstate", () => {
          modules.autoLocationAndInsertVideoDescriptionToComment()
        })
        const [$playerContainer, $AutoSkipSwitchInput] = await utils.getElementAndCheckExistence([selectors.playerContainer, selectors.AutoSkipSwitchInput])
        $playerContainer.addEventListener('fullscreenchange', (event) => {
          let isFullscreen = document.fullscreenElement === event.target
          if (!isFullscreen) modules.locationToPlayer()
        })
        document.addEventListener('keydown', (event) => {
          if (event.key === 'j') {
            $AutoSkipSwitchInput.click()
          }
        })
        if (vals.auto_skip()) {
          const [$video, $setSkipTimeNodesPopoverToggleButton, $setSkipTimeNodesPopoverRecords, $skipTimeNodesRecordsArray, $saveRecordsButton] = await utils.getElementAndCheckExistence([selectors.video, selectors.setSkipTimeNodesPopoverToggleButton, selectors.setSkipTimeNodesPopoverRecords, selectors.skipTimeNodesRecordsArray, selectors.saveRecordsButton])
          document.addEventListener('keydown', (event) => {
            if (event.key === 'k') {
              const currentTime = Math.ceil($video.currentTime)
              arrays.skipNodesRecords.push(currentTime)
              arrays.skipNodesRecords = Array.from(new Set(arrays.skipNodesRecords))
              if (arrays.skipNodesRecords.length > 0) {
                $setSkipTimeNodesPopoverRecords.style.display = 'flex'
                $skipTimeNodesRecordsArray.innerText = `打点数据:${JSON.stringify(arrays.skipNodesRecords)}`
                if (utils.isArrayLengthEven(arrays.skipNodesRecords)) {
                  $skipTimeNodesRecordsArray.classList.remove('danger')
                  $skipTimeNodesRecordsArray.classList.add('success')
                  $saveRecordsButton.removeAttribute('disabled')
                } else {
                  $skipTimeNodesRecordsArray.classList.remove('success')
                  $skipTimeNodesRecordsArray.classList.add('danger')
                  $saveRecordsButton.setAttribute('disabled', true)
                }
              }
            }
            if (event.key === 'g') {
              $setSkipTimeNodesPopoverToggleButton.click()
            }
          })
        }
      }
    }
    // #endregion 为元素添加监听器并执行相应函数
  }
  const biliApis = {
    /**
     * 获取解密WBI鉴权后的参数
     * - #region 获取解密WBI鉴权后的参数
     * @param {Object} originalParams 
     * @returns 
     */
    async getQueryWithWbi(originalParams) {
      const mixinKeyEncTab = [
        46, 47, 18, 2, 53, 8, 23, 32, 15, 50, 10, 31, 58, 3, 45, 35, 27, 43, 5, 49,
        33, 9, 42, 19, 29, 28, 14, 39, 12, 38, 41, 13, 37, 48, 7, 16, 24, 55, 40,
        61, 26, 17, 0, 1, 60, 51, 30, 4, 22, 25, 54, 21, 56, 59, 6, 63, 57, 62, 11,
        36, 20, 34, 44, 52
      ]

      // 对 imgKey 和 subKey 进行字符顺序打乱编码
      const getMixinKey = (orig) => mixinKeyEncTab.map(n => orig[n]).join('').slice(0, 32)

      // 为请求参数进行 wbi 签名
      const encWbi = (params, img_key, sub_key) => {
        const mixin_key = getMixinKey(img_key + sub_key),
          curr_time = Math.round(Date.now() / 1000),
          chr_filter = /[!'()*]/g

        Object.assign(params, { wts: curr_time }) // 添加 wts 字段
        // 按照 key 重排参数
        const query = Object.keys(params).sort().map(key => {
          // 过滤 value 中的 "!'()*" 字符
          const value = params[key].toString().replace(chr_filter, '')
          return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
        }).join('&')

        const wbi_sign = MD5(query + mixin_key) // 计算 w_rid

        return query + '&w_rid=' + wbi_sign
      }

      // 获取最新的 img_key 和 sub_key
      const getWbiKeys = async () => {
        const url = 'https://api.bilibili.com/x/web-interface/nav'
        const res = await axios.get(url, { withCredentials: true })
        const { data: { wbi_img: { img_url, sub_url } } } = res.data

        return {
          img_key: img_url.slice(
            img_url.lastIndexOf('/') + 1,
            img_url.lastIndexOf('.')
          ),
          sub_key: sub_url.slice(
            sub_url.lastIndexOf('/') + 1,
            sub_url.lastIndexOf('.')
          )
        }
      }
      const main = async () => {
        const web_keys = await getWbiKeys()
        const params = originalParams,
          img_key = web_keys.img_key,
          sub_key = web_keys.sub_key
        const query = encWbi(params, img_key, sub_key)
        return query
      }
      return main()
    },
    // #endregion 获取解密WBI鉴权后的参数
    /**
     * 获取视频基本信息
     * - #region 获取视频基本信息
     * @param {String} videoId 视频ID(video BVID)
     * @returns videoInfo
     */
    async getVideoInformation(videoId) {
      const url = `https://api.bilibili.com/x/web-interface/view?bvid=${videoId}`
      const { data, data: { code } } = await axios.get(url, { withCredentials: true })
      if (code === 0) return data
      else if (code === -400) utils.logger.info("获取视频基本信息丨请求错误")
      else if (code === -403) utils.logger.info("获取视频基本信息丨权限不足")
      else if (code === -404) utils.logger.info("获取视频基本信息丨无视频")
      else if (code === 62002) utils.logger.info("获取视频基本信息丨稿件不可见")
      else if (code === 62004) utils.logger.info("获取视频基本信息丨稿件审核中")
      else if (code === 'ERR_BAD_REQUEST') utils.logger.info("获取视频基本信息丨请求失败")
      else utils.logger.warn("获取视频基本信息丨请求错误")
    },
    // #endregion 获取视频基本信息
    /**
     * 获取用户基本信息
     * - #region 获取用户基本信息
     * @param {String} userId 用户ID
     * @returns userInfo
     */
    async getUserInformation(userId) {
      const url = `https://api.bilibili.com/x/web-interface/card?mid=${userId}`
      const { data, data: { code } } = await axios.get(url, { withCredentials: true })
      if (code === 0) return data
      else if (code === -400) utils.logger.info("获取用户基本信息丨请求错误")
      else if (code === -403) utils.logger.info("获取用户基本信息丨权限不足")
      else if (code === -404) utils.logger.info("获取用户基本信息丨用户不存在")
      else if (code === 'ERR_BAD_REQUEST') utils.logger.info("获取用户基本信息丨请求失败")
      else utils.logger.warn("获取用户基本信息丨请求失败")
    },
    // #endregion 获取用户基本信息
    /**
     * 判断用户是否是大会员
     * - #region 判断用户是否是大会员
     * - 签到后执行,若已签到则不执行,避免触发多次请求
     */
    async isVip() {
      const userId = utils.getCookieByName('DedeUserID')
      const { data: { card: { vip: { status } } } } = await biliApis.getUserInformation(userId)
      if (status) utils.setValue('is_vip', true)
      else utils.setValue('is_vip', false)
    },
    // #endregion 判断用户是否是大会员
    /**
     * 自动签到
     * - #region 自动签到
     */
    async autoSignIn() {
      const now = new Date()
      const signInDate = `${now.getFullYear()}-${(now.getMonth() + 1)}-${now.getDate()}`
      if (!vals.signIn_date() || vals.signIn_date() !== signInDate) {
        const url = `https://api.live.bilibili.com/sign/doSign`
        const { data: { code } } = await axios.get(url, { withCredentials: true })
        if (code === 0) {
          utils.logger.info("自动签到丨签到成功")
          utils.setValue('signIn_date', signInDate)
          await biliApis.isVip()
        } else if (code === 1011040) {
          utils.logger.info("自动签到丨今日已签")
          utils.setValue('signIn_date', signInDate)
        } else {
          utils.logger.warn("自动签到丨签到异常")
          utils.setValue('signIn_date', '')
        }
      } else {
        utils.logger.info("自动签到丨今日已签")
      }
    },
    // #endregion 自动签到
    /**
     * 获取用户投稿视频列表
     * - #region 获取用户投稿视频列表
     */
    async getUserVideoList(userId) {
      const wib = await biliApis.getQueryWithWbi({ mid: userId })
      const url = `https://api.bilibili.com/x/space/wbi/arc/search?${wib}`
      const { data, data: { code } } = await axios.get(url, { withCredentials: true })
      if (code === 0) return data
      else if (code === -400) {
        utils.logger.info("获取用户投稿视频列表丨权限不足")
      } else if (code === -412) {
        utils.logger.info("获取用户投稿视频列表丨请求被拦截")
      } else {
        utils.logger.warn("获取用户投稿视频列表丨请求失败")
      }
    },
    // #endregion 获取用户投稿视频列表
  }
  const modules = {
    //** ----------------------- 通用功能 ----------------------- **//
    // #region 通用功能
    /**
     * 获取视频类型(video/bangumi)
     * - #region 获取视频类型
     * 如果都没匹配上则弹窗报错
     * @returns 当前视频类型
     */
    async getCurrentPlayerType(url = window.location.href) {
      let playerType
      const setCurrentPlayerType = () => {
        playerType = (url.startsWith('https://www.bilibili.com/video') || url.startsWith('https://www.bilibili.com/list/')) ? 'video' : url.startsWith('https://www.bilibili.com/bangumi') ? 'bangumi' : false
        if (!playerType) {
          utils.logger.debug('视频类型丨未匹配')
          alert('未匹配到当前视频类型,请反馈当前地址栏链接。')
        }
        utils.setValue('player_type', playerType)
      }
      window.addEventListener('focus', () => {
        setCurrentPlayerType()
      })
      // utils.logger.debug(`${playerType} ${vals.player_type()}`)
      setCurrentPlayerType()
      if (vals.player_type() === playerType) return { message: `视频类型丨${playerType}` }
      else modules.getCurrentPlayerType()
    },
    // #endregion 获取视频类型
    /**
     * 判断用户是否登录
     * - #region 判断用户是否登录
     */
    isLogin() {
      return Boolean(document.cookie.replace(/(?:(?:^|.*;\s*)bili_jct\s*=\s*([^;]*).*$)|^.*$/, '$1') || window.UserStatus.userInfo.isLogin || null)
    },
    // #endregion 判断用户是否登录
    /**
     * 获取视频ID/video BVID/bangumi EPID
     * - #region 获取视频ID
     */
    getCurrentVideoID(url = window.location.href) {
      return url.startsWith('https://www.bilibili.com/video') ? url.split('/')[4] : url.startsWith('https://www.bilibili.com/bangumi') ? url.split('/')[5].split('?')[0] : 'error'
    },
    // #endregion 获取视频ID
    /**
     * 获取Vue版本号
     * - #region 获取Vue版本号
     */
    async getVueScopeId(selector) {
      const element = await utils.getElementAndCheckExistence(selector)
      // utils.logger.debug(element)
      return new Promise((resolve, reject) => {
        let attrsArray = Array.from(element.attributes)
        let vueScopeAttrs = attrsArray.filter(attr => attr.name.startsWith('data-v-'))
        vueScopeAttrs.filter(attr => {
          // 使用字符串分割来提取 'data-v-' 后面的部分  
          const vueScopeId = attr.name.split('data-v-')[1];
          resolve(vueScopeId)
          // utils.logger.debug(vueScopeId) 
        })
      })
    },
    // #endregion 获取Vue版本号
    // #endregion 通用功能
    //** ----------------------- 视频播放页相关功能 ----------------------- **//
    // #region 视频播放页相关功能
    /**
     * 检查视频元素是否存在
     * - #region 检查视频元素是否存在
     * - 若存在返回成功消息
     * - 若不存在则抛出异常
     */
    async checkVideoExistence() {
      const [$videoWrap, $video] = await utils.getElementAndCheckExistence([selectors.videoWrap, selectors.video])
      if ($video) return { message: '播放器|已找到', callback: [modules.setVideoCover.bind(null, $videoWrap, $video)] }
      else throw new Error('播放器|未找到')
    },
    // #endregion 检查视频元素是否存在
    /**
     * 检查视频是否可以播放
     * - #region 检查视频是否可以播放
     */
    async checkVideoCanPlayThrough() {
      return new Promise((resolve, reject) => {
        let attempts = 100
        let message
        const timer = setInterval(() => {
          const $video = document.querySelector(selectors.video)
          const videoReadyState = $video.readyState
          if (videoReadyState === 4) {
            message = '视频资源|可以播放'
            resolve({ message })
            clearInterval(timer)
          } else if (attempts <= 0) {
            message = '视频资源|加载失败'
            reject({ message })
            clearInterval(timer)
          }
          attempts--
        }, 100)
        arrays.intervalIds.push(timer)
      })
    },
    // #endregion 检查视频是否可以播放
    /**
     * 监听屏幕模式变化(normal/wide/web/full)
     * - #region 监听屏幕模式变化
     */
    async observerPlayerDataScreenChanges() {
      const $playerContainer = await utils.getElementAndCheckExistence(selectors.playerContainer)
      const observer = new MutationObserver(() => {
        const playerDataScreen = $playerContainer.getAttribute('data-screen')
        utils.setValue('current_screen_mode', playerDataScreen)
      })
      observer.observe($playerContainer, {
        attributes: true,
        attributeFilter: ['data-screen'],
      })
    },
    // #endregion 监听屏幕模式变化
    /**
     * 获取当前屏幕模式
     * - #region 获取当前屏幕模式
     * @param {Number} 延时
     * @returns
     */
    async getCurrentScreenMode() {
      return new Promise((resolve) => {
        let attempts = 100
        const timer = setInterval(() => {
          const $playerContainer = document.querySelector(selectors.playerContainer)
          const playerDataScreen = $playerContainer?.getAttribute('data-screen')
          if (playerDataScreen) {
            resolve(playerDataScreen)
            clearInterval(timer)
          } else if (attempts <= 0) {
            clearInterval(timer)
            throw new Error(`获取当前屏幕模式|失败:已达到最大重试次数`)
          }
          attempts--
        }, 100)
        arrays.intervalIds.push(timer)
      })
    },
    // #endregion 获取当前屏幕模式
    /**
     * 视频未开始播放时显示视频封面
     * - #region 视频未开始播放时显示视频封面
     * - 应用于舞蹈类视频
     * - 视频播放时移除封面
     */
    async setVideoCover($videoWrap, $video) {
      if (vals.player_type() === 'bangumi') return
      const targetTids = Array.from(new Set().add([...objects.videoCategories.dance.tids, ...objects.videoCategories.fashion.tids])).flat()
      const { data: { pic, tid } } = await biliApis.getVideoInformation(modules.getCurrentVideoID(window.location.href))
      if (targetTids.includes(tid) && pic) {
        $videoWrap.style.setProperty('--video-cover', `url(${pic.replace(/^http:/i, 'https:')})`) // 设置视频封面的CSS变量
        $video.addEventListener('play', () => {
          $videoWrap.style.setProperty('--video-cover', '')
        })
        // $video.addEventListener('pause', function () {
        //   $videoWrap.style.setProperty('--video-cover', `url(${pic})`)
        // })
      }
    },
    // #endregion 视频未开始播放时显示视频封面
    /**
     * 判断当前视频是否未充电
     * - #region 判断当前视频是否未充电
     */
    checkVideoNoCharge() {
      return document.querySelector(selectors.notChargeHighLevelCover)
    },
    // #endregion 判断当前视频是否未充电
    // #region 自动选择播放器默认模式
    /**
     * 执行自动切换屏幕模式
     * - #region 执行自动切换屏幕模式
     * - 功能未开启,不执行切换函数,直接返回成功
     * - 功能开启,但当前屏幕已为宽屏或网页全屏,则直接返回成功
     * - 功能开启,执行切换函数
     */
    async autoSelectScreenMode() {
      if (modules.checkVideoNoCharge()) return
      if (++vars.autoSelectScreenModeRunningCount !== 1) return
      if (vals.selected_screen_mode() === 'close') return { message: '屏幕模式|功能已关闭' }
      const currentScreenMode = await modules.getCurrentScreenMode()
      if (arrays.screenModes.includes(currentScreenMode)) return { message: `屏幕模式|当前已是 ${currentScreenMode.toUpperCase()} 模式` }
      if (arrays.screenModes.includes(vals.selected_screen_mode())) {
        const result = await modules.checkScreenModeSwitchSuccess(vals.selected_screen_mode())
        if (result) return { message: `屏幕模式|${vals.selected_screen_mode().toUpperCase()}|切换成功` }
        else throw new Error(`屏幕模式|${vals.selected_screen_mode().toUpperCase()}|切换失败:已达到最大重试次数`)
      }
    },
    // #endregion 执行自动切换屏幕模式
    /**
     * 检查屏幕模式是否切换成功
     * - #region 检查屏幕模式是否切换成功
     * @param {*} expectScreenMode 期望的屏幕模式
     * - 未成功自动重试
     * - 定时器方式超过 10 次失败,1s 执行一次
     * - 递归方式超过 10 次返回失败
     */
    async checkScreenModeSwitchSuccess(expectScreenMode) {
      const enterBtnMap = {
        wide: async () => await utils.getElementAndCheckExistence(selectors.screenModeWideEnterButton),
        web: async () => await utils.getElementAndCheckExistence(selectors.screenModeWebEnterButton),
      }
      // 定时器方式检查
      if (vals.dev_checkScreenModeSwitchSuccess_method() === 'interval') {
        if (enterBtnMap[expectScreenMode]) {
          return new Promise((resolve) => {
            let attempts = 10
            const timer = setInterval(async () => {
              const enterBtn = await enterBtnMap[expectScreenMode]()
              enterBtn.click()
              const currentScreenMode = await modules.getCurrentScreenMode()
              const equal = expectScreenMode === currentScreenMode
              const success = vals.player_type() === 'video' ? expectScreenMode === 'wide' ? equal && +getComputedStyle(document.querySelector(selectors.danmukuBox))['margin-top'].slice(0, -2) > 0 : equal : equal
              if (success) {
                clearInterval(timer)
                resolve(success)
              } else if (attempts <= 0) {
                clearInterval(timer)
                resolve(false)
                utils.logger.warn(`屏幕模式切换失败,继续尝试丨当前:${currentScreenMode},期望:${expectScreenMode}`)
              }
              attempts--
            }, 1000)
            arrays.intervalIds.push(timer)
          })
        }
      }
      if (vals.dev_checkScreenModeSwitchSuccess_method() === 'recursive') {
        // 递归方式检查
        if (enterBtnMap[expectScreenMode]) {
          const enterBtn = await enterBtnMap[expectScreenMode]()
          enterBtn.click()
          const currentScreenMode = await modules.getCurrentScreenMode()
          const equal = expectScreenMode === currentScreenMode
          const success = vals.player_type() === 'video' ? expectScreenMode === 'wide' ? equal && +getComputedStyle(document.querySelector(selectors.danmukuBox))['margin-top'].slice(0, -2) > 0 : equal : equal
          // utils.logger.debug(`${vals.player_type()} ${expectScreenMode} ${currentScreenMode} ${equal} ${success}`)
          if (success) return success
          else {
            if (++vars.checkScreenModeSwitchSuccessDepths === 10) return false
            // utils.logger.warn(`屏幕模式切换失败,继续尝试丨当前:${currentScreenMode},期望:${expectScreenMode}`)
            await utils.sleep(300)
            return modules.checkScreenModeSwitchSuccess(expectScreenMode)
          }
        }
      }
    },
    // #endregion 检查屏幕模式是否切换成功
    // #endregion 自动选择播放器默认模式
    // #region 自动定位至播放器
    /**
     * 设置位置数据并滚动至播放器
     * - #region 设置位置数据并滚动至播放器
     * @returns 
     */
    async setLocationDataAndScrollToPlayer() {
      const getOffsetMethod = vals.get_offset_method()
      let playerOffsetTop
      if (getOffsetMethod === 'elements') {
        const $header = await utils.getElementAndCheckExistence(selectors.header)
        const $placeholderElement = await utils.getElementAndCheckExistence(selectors.videoTitleArea) || await utils.getElementAndCheckExistence(selectors.bangumiMainContainer)
        const headerHeight = $header.getBoundingClientRect().height
        const placeholderElementHeight = $placeholderElement.getBoundingClientRect().height
        playerOffsetTop = vals.player_type() === 'video' ? headerHeight + placeholderElementHeight : headerHeight + +getComputedStyle($placeholderElement)['margin-top'].slice(0, -2)
      }
      if (getOffsetMethod === 'function') {
        const $player = await utils.getElementAndCheckExistence(selectors.player)
        playerOffsetTop = utils.getElementOffsetToDocument($player).top
      }
      // utils.logger.debug(playerOffsetTop)
      vals.player_type() === 'video' ? utils.setValue('video_player_offset_top', playerOffsetTop) : utils.setValue('bangumi_player_offset_top', playerOffsetTop)
      await modules.getCurrentScreenMode() === 'wide' ? utils.documentScrollTo(playerOffsetTop - vals.offset_top()) : utils.documentScrollTo(0)
      return
      // utils.logger.debug('定位至播放器!')
    },
    // #endregion 设置位置数据并滚动至播放器
    /**
     * 自动定位至播放器并检查是否成功
     * - #region 自动定位至播放器并检查是否成功
     */
    async autoLocationToPlayer() {
      const unlockbody = () => {
        document.getElementById('BodyHiddenStyle')?.remove()
      }
      const onAutoLocate = vals.auto_locate() && ((!vals.auto_locate_video() && !vals.auto_locate_bangumi()) || (vals.auto_locate_video() && vals.player_type() === 'video') || (vals.auto_locate_bangumi() && vals.player_type() === 'bangumi'))
      if (!onAutoLocate || vals.selected_screen_mode() === 'web') return { callback: [unlockbody] }
      await modules.setLocationDataAndScrollToPlayer()
      const playerOffsetTop = vals.player_type() === 'video' ? vals.video_player_offset_top() : vals.bangumi_player_offset_top()
      const result = await modules.checkAutoLocationSuccess(playerOffsetTop - vals.offset_top())
      if (result) return { message: '自动定位|成功', callback: [unlockbody] }
      else throw new Error(`自动定位|失败:已达到最大重试次数`)
    },
    // #endregion 自动定位至播放器并检查是否成功
    /**
     * 递归检查屏自动定位是否成功
     * - #region 递归检查屏自动定位是否成功
     * @param {*} expectOffset 期望文档滚动偏移量
     * - 未定位成功自动重试,递归超过 10 次则返回失败
     * - 基础数据:
     * - videoOffsetTop:播放器相对文档顶部距离,大小不随页面滚动变化
     * - videoClientTop:播放器相对浏览器视口顶部距离,大小随页面滚动变化
     * - targetOffset:用户期望的播放器相对浏览器视口顶部距离,由用户自定义
     * - 文档滚动距离:videoOffsetTop - targetOffset
     */
    async checkAutoLocationSuccess(expectOffset) {
      const $video = await utils.getElementAndCheckExistence(selectors.video)
      utils.documentScrollTo(expectOffset)
      await utils.sleep(300)
      const videoClientTop = Math.trunc($video.getBoundingClientRect().top)
      const playerOffsetTop = vals.player_type() === 'video' ? vals.video_player_offset_top() : vals.bangumi_player_offset_top()
      // 成功条件:实际偏移量与用户设置偏移量相等/期望文档滚动偏移量与当前文档滚动偏移量相等/实际偏移量与用户设置偏移量误差小于5
      const success = (videoClientTop === vals.offset_top()) || ((playerOffsetTop - vals.offset_top()) - Math.trunc(window.scrollY) === 0) || (Math.abs(videoClientTop - vals.offset_top()) < 5)
      if (success) return success
      else {
        if (++vars.autoLocationToPlayerRetryDepths === 10) return false
        // utils.logger.debug(`${videoOffsetTop} ${videoClientTop} ${vals.offset_top()} ${Math.abs((videoOffsetTop - vals.offset_top()) - Math.trunc(window.scrollY))}`)
        utils.logger.warn(`
                    自动定位失败,继续尝试
                    -----------------
                    期望文档滚动偏移量:${playerOffsetTop - vals.offset_top()}
                    当前文档滚动偏移量:${Math.trunc(window.scrollY)}
                    文档滚动偏移量误差:${(playerOffsetTop - vals.offset_top()) - Math.trunc(window.scrollY)}
                    播放器顶部偏移量:${videoClientTop}
                    设置偏移量:${vals.offset_top()}`)
        utils.documentScrollTo(0)
        await utils.sleep(300)
        return modules.checkAutoLocationSuccess(expectOffset)
      }
    },
    // #endregion 递归检查屏自动定位是否成功
    /**
     * 文档滚动至播放器(使用已有数据)
     * - #region 文档滚动至播放器(使用已有数据)
     */
    async locationToPlayer() {
      const playerOffsetTop = vals.player_type() === 'video' ? vals.video_player_offset_top() : vals.bangumi_player_offset_top()
      const scrollOffset = await modules.getCurrentScreenMode() !== 'web' ? playerOffsetTop - vals.offset_top() : 0
      utils.documentScrollTo(scrollOffset)
    },
    // #endregion 文档滚动至播放器(使用已有数据)
    /**
     * 点击播放器自动定位
     * - #region 点击播放器自动定位
     */
    async clickPlayerAutoLocation() {
      if (vals.click_player_auto_locate()) {
        const $video = await utils.getElementAndCheckExistence(selectors.video)
        $video.addEventListener('click', async () => {
          const currentScreenMode = await modules.getCurrentScreenMode()
          if (['full', 'mini'].includes(currentScreenMode)) return
          await modules.locationToPlayer()
        })
      }
    },
    // #endregion 点击播放器自动定位
    /**
     * 点击时间锚点自动返回播放器
     * - #region 点击时间锚点自动返回播放器
     */
    async clickVideoTimeAutoLocation() {
      await utils.sleep(100)
      const $video = await utils.getElementAndCheckExistence('video')
      const $clickTarget = vals.player_type() === 'video' ? await utils.getElementAndCheckExistence(selectors.videoComment) : await utils.getElementAndCheckExistence(selectors.bangumiComment)
      await elmGetter.each(selectors.videoTime, $clickTarget, async (target) => {
        target.addEventListener('click', async (event) => {
          event.stopPropagation()
          await modules.locationToPlayer()
          // const targetTime = vals.player_type() === 'video' ? target.dataset.videoTime : target.dataset.time
          const targetTime = target.dataset.videoTime
          if (targetTime > $video.duration) alert('当前时间点大于视频总时长,将跳到视频结尾!')
          $video.currentTime = targetTime
          $video.play()
        })
      })
    },
    // #endregion 点击时间锚点自动返回播放器
    // #endregion 自动定位至播放器
    /**
     * 自动关闭静音
     * - #region 自动关闭静音
     */
    async autoCancelMute() {
      if (++vars.autoCancelMuteRunningCount !== 1) return
      const [$mutedButton, $volumeButton] = await utils.getElementAndCheckExistence([selectors.mutedButton, selectors.volumeButton])
      // const mutedButtonDisplay = getComputedStyle(mutedButton)['display']
      // const volumeButtonDisplay = getComputedStyle(volumeButton)['display']
      const mutedButtonDisplay = $mutedButton.style.display
      const volumeButtonDisplay = $volumeButton.style.display
      if (mutedButtonDisplay === 'block' || volumeButtonDisplay === 'none') {
        $mutedButton.click()
        // utils.logger.info('静音丨已关闭')
        return {
          message: '静音丨已关闭'
        }
      }

    },
    // #endregion 自动关闭静音
    /**
     * 自动选择最高画质
     * - #region 自动选择最高画质
     * - 质量代码:
     * - 127->8K 超高清;120->4K 超清;116->1080P 60帧;
     * - 80->1080P 高清;64->720P 高清;32->480P 清晰;
     * - 16->360P 流畅;0->自动
     */
    async autoSelectVideoHighestQuality() {
      if (modules.checkVideoNoCharge()) return
      if (++vars.autoSelectVideoHighestQualityRunningCount !== 1) return
      let message
      const qualitySwitchButtonsMap = new Map()
      if (!vals.auto_select_video_highest_quality()) return
      await elmGetter.each(selectors.qualitySwitchButtons, document.body, button => {
        qualitySwitchButtonsMap.set(button.dataset.value, button)
      })
      const qualitySwitchButtonsArray = [...qualitySwitchButtonsMap]
      const select4K = () => {
        qualitySwitchButtonsMap.get('120').click()
        message = '最高画质|VIP|4K|切换成功'
      }
      const select8K = () => {
        qualitySwitchButtonsMap.get('127').click()
        message = '最高画质|VIP|4K|切换成功'
      }
      const selectNo4K8K = () => {
        qualitySwitchButtonsArray.filter(quality => {
          return +quality[0] < 120
        })[0][1].click()
        message = '最高画质|VIP|不包含4K及8K|切换成功'
      }
      if (vals.is_vip()) {
        if (!vals.contain_quality_4k() && !vals.contain_quality_8k()) {
          selectNo4K8K()
        }
        if (vals.contain_quality_4k() && !vals.contain_quality_8k()) {
          if (qualitySwitchButtonsMap.get('120')) {
            select4K()
          } else {
            selectNo4K8K()
          }
        }
        if (!vals.contain_quality_4k() && vals.contain_quality_8k()) {
          if (qualitySwitchButtonsMap.get('127')) {
            select8K()
          } else {
            selectNo4K8K()
          }
        }
        if ((vals.contain_quality_4k() && vals.contain_quality_8k())) {
          if (qualitySwitchButtonsMap.get('127')) {
            select8K()
          } else if (qualitySwitchButtonsMap.get('120')) {
            select4K()
          } else {
            selectNo4K8K()
          }
        }
      } else {
        qualitySwitchButtonsArray.filter(button => {
          return button[1].children.length < 2
        })[0][1].click()
        message = '最高画质|非VIP|切换成功'
      }
      // utils.logger.info(message)
      return { message }

    },
    // #endregion 自动选择最高画质
    /**
     * 插入漂浮功能按钮
     * - #region 插入漂浮功能按钮
     * - 快速返回至播放器
     */
    async insertFloatSideNavToolsButton() {
      const $floatNav = vals.player_type() === 'video' ? await utils.getElementAndCheckExistence(selectors.videoFloatNav) : await utils.getElementAndCheckExistence(selectors.bangumiFloatNav)
      const dataV = $floatNav.lastChild.attributes[1].name
      let $locateButton
      if (vals.player_type() === 'video') {
        const locateButtonHtml = '<div class="fixed-sidenav-storage-item locate" title="定位至播放器"><svg t="1643419779790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" width="200" height="200" style="width: 50%;height: 100%;fill: currentColor;"><path d="M512 352c-88.008 0-160.002 72-160.002 160 0 88.008 71.994 160 160.002 160 88.01 0 159.998-71.992 159.998-160 0-88-71.988-160-159.998-160z m381.876 117.334c-19.21-177.062-162.148-320-339.21-339.198V64h-85.332v66.134c-177.062 19.198-320 162.136-339.208 339.198H64v85.334h66.124c19.208 177.062 162.144 320 339.208 339.208V960h85.332v-66.124c177.062-19.208 320-162.146 339.21-339.208H960v-85.334h-66.124zM512 810.666c-164.274 0-298.668-134.396-298.668-298.666 0-164.272 134.394-298.666 298.668-298.666 164.27 0 298.664 134.396 298.664 298.666S676.27 810.666 512 810.666z" p-id="1776"></path></svg>定位</div>'.replace('title="定位至播放器"', `title="定位至播放器" ${dataV}`)
        $locateButton = utils.createElementAndInsert(locateButtonHtml, $floatNav.lastChild, 'prepend')
      }
      if (vals.player_type() === 'bangumi') {
        const floatNavMenuItemClass = $floatNav.lastChild.lastChild.getAttribute('class')
        const locateButtonHtml = `<div class="${floatNavMenuItemClass} locate" style="height:40px;padding:0" title="定位至播放器">\n<svg t="1643419779790" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1775" width="200" height="200" style="width: 50%;height: 100%;fill: currentColor;"><path d="M512 352c-88.008 0-160.002 72-160.002 160 0 88.008 71.994 160 160.002 160 88.01 0 159.998-71.992 159.998-160 0-88-71.988-160-159.998-160z m381.876 117.334c-19.21-177.062-162.148-320-339.21-339.198V64h-85.332v66.134c-177.062 19.198-320 162.136-339.208 339.198H64v85.334h66.124c19.208 177.062 162.144 320 339.208 339.208V960h85.332v-66.124c177.062-19.208 320-162.146 339.21-339.208H960v-85.334h-66.124zM512 810.666c-164.274 0-298.668-134.396-298.668-298.666 0-164.272 134.394-298.666 298.668-298.666 164.27 0 298.664 134.396 298.664 298.666S676.27 810.666 512 810.666z" p-id="1776"></path></svg></div>`
        $locateButton = utils.createElementAndInsert(locateButtonHtml, $floatNav.lastChild, 'before')
      }
      $locateButton.addEventListener('click', async () => {
        await modules.locationToPlayer()
      })
    },
    // #endregion 插入漂浮功能按钮
    // #region 网页全屏模式解锁
    /**
     * 执行网页全屏模式解锁
     * - #region 执行网页全屏模式解锁
     */
    async webfullScreenModeUnlock() {
      if (!vals.webfull_unlock() || !vals.selected_screen_mode() === 'web' || ++vars.webfullUnlockRunningCount !== 1) return
      if (vals.player_type() === 'bangumi') return
      const [$app, $playerWrap, $player, $playerWebscreen, $wideEnterButton, $wideLeaveButton, $webEnterButton, $webLeaveButton, $fullControlButton] = await utils.getElementAndCheckExistence([selectors.app, selectors.playerWrap, selectors.player, selectors.playerWebscreen, selectors.screenModeWideEnterButton, selectors.screenModeWideLeaveButton, selectors.screenModeWebEnterButton, selectors.screenModeWebLeaveButton, selectors.screenModeFullControlButton])
      const resetPlayerLayout = async () => {
        if (document.getElementById('UnlockWebscreenStyle')) document.getElementById('UnlockWebscreenStyle').remove()
        if (!document.getElementById('ResetPlayerLayoutStyle')) utils.insertStyleToDocument('ResetPlayerLayoutStyle', styles.ResetPlayerLayout)
        $playerWrap.append($player)
        utils.setValue('current_screen_mode', 'wide')
        await utils.sleep(300)
        await modules.locationToPlayer()
      }
      const bodyHeight = utils.getBodyHeight()
      utils.insertStyleToDocument('UnlockWebscreenStyle', styles.UnlockWebscreen.replace(/BODYHEIGHT/gi, `${bodyHeight}px`))
      $app.prepend($playerWebscreen)
      $webLeaveButton.addEventListener('click', async () => {
        await utils.sleep(100)
        await resetPlayerLayout()
      })
      $webEnterButton.addEventListener('click', async () => {
        if (!document.getElementById('UnlockWebscreenStyle')) utils.insertStyleToDocument('UnlockWebscreenStyle', styles.UnlockWebscreen.replace(/BODYHEIGHT/gi, `${bodyHeight}px`))
        $app.prepend($playerWebscreen)
        await modules.locationToPlayer()
      })
      $wideEnterButton.addEventListener('click', async () => {
        await utils.sleep(100)
        await resetPlayerLayout()
      })
      $wideLeaveButton.addEventListener('click', async () => {
        await utils.sleep(100)
        await resetPlayerLayout()
      })
      $fullControlButton.addEventListener('click', async () => {
        await utils.sleep(100)
        await resetPlayerLayout()
      })
      return {
        message: '网页全屏解锁|成功',
        callback: [modules.insertGoToCommentButton]
      }

    },
    // #endregion 执行网页全屏模式解锁
    /**
     * 网页全屏模式解锁后插入跳转评论按钮
     * - #region 网页全屏模式解锁后插入跳转评论按钮
     */
    async insertGoToCommentButton() {
      if (vals.player_type() !== 'video' || !vals.webfull_unlock() || ++vars.insertGoToCommentButtonCount !== 1) return
      const [$comment, $playerControllerBottomRight] = await utils.getElementAndCheckExistence([selectors.videoComment, selectors.playerControllerBottomRight])
      const goToCommentBtnHtml = '<div class="bpx-player-ctrl-btn bpx-player-ctrl-comment" role="button" aria-label="前往评论" tabindex="0"><div id="goToComments" class="bpx-player-ctrl-btn-icon"><span class="bpx-common-svg-icon"><svg class="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" width="88" height="88" preserveAspectRatio="xMidYMid meet" style="width: 100%; height: 100%; transform: translate3d(0px, 0px, 0px);"><path d="M512 85.333c235.637 0 426.667 191.03 426.667 426.667S747.637 938.667 512 938.667a424.779 424.779 0 0 1-219.125-60.502A2786.56 2786.56 0 0 0 272.82 866.4l-104.405 28.48c-23.893 6.507-45.803-15.413-39.285-39.296l28.437-104.288c-11.008-18.688-18.219-31.221-21.803-37.91A424.885 424.885 0 0 1 85.333 512c0-235.637 191.03-426.667 426.667-426.667zm-102.219 549.76a32 32 0 1 0-40.917 49.216A223.179 223.179 0 0 0 512 736c52.97 0 103.19-18.485 143.104-51.67a32 32 0 1 0-40.907-49.215A159.19 159.19 0 0 1 512 672a159.19 159.19 0 0 1-102.219-36.907z" fill="#currentColor"/></svg></span></div></div>'
      const $goToCommentButton = utils.createElementAndInsert(goToCommentBtnHtml, $playerControllerBottomRight, 'append')
      $goToCommentButton.addEventListener('click', (event) => {
        event.stopPropagation()
        utils.documentScrollTo(utils.getElementOffsetToDocument($comment).top - 10)
        // utils.logger.info('到达评论区')
      })

    },
    // #endregion 网页全屏模式解锁后插入跳转评论按钮
    // #endregion 网页全屏模式解锁
    /**
     * 将视频简介内容插入评论区或直接替换原简介区内容
     * - #region 视频简介优化
     * - 视频简介存在且内容过长,则将视频简介内容插入评论区,否则直接替换原简介区内容
     * - 若视频简介中包含型如 "00:00:00" 的时间内容,则将其转换为可点击的时间锚点元素
     * - 若视频简介中包含 URL 链接,则将其转换为跳转链接
     * - 若视频简介中包含视频 BV 号或专栏 cv 号,则将其转换为跳转链接
     */

    async insertVideoDescriptionToComment() {
      if (!vals.insert_video_description_to_comment() || vals.player_type() === 'bangumi') return
      const $commentDescription = document.getElementById('comment-description')
      if ($commentDescription) $commentDescription.remove()
      const [$videoDescription, $videoDescriptionInfo, $videoCommentReplyList] = await utils.getElementAndCheckExistence([selectors.videoDescription, selectors.videoDescriptionInfo, selectors.videoCommentReplyList])
      const getTotalSecondsFromTimeString = (timeString) => {
        if (timeString.length === 5) timeString = '00:' + timeString
        const [hours, minutes, seconds] = timeString.split(':').map(Number)
        const totalSeconds = hours * 3600 + minutes * 60 + seconds
        return totalSeconds
      }
      const nbspToBlankRegexp = /&nbsp;/g
      const timeStringRegexp = /(\d\d:\d\d(:\d\d)*)/g
      const urlRegexp = /(?<!((href|url)="))(http|https|ftp):\/\/[\w-]+(\.[\w\-]+)*([\w\-\.\,\@\?\^\=\%\&\:\/\~\+\#;]*[\w\-\@?\^\=\%\&\/~\+#;])?/g
      const plaintVideoIdRegexp = /(?<!(>|\/))(BV1([a-km-zA-HJ-NP-Z1-9]){9})(?!(<\/))/g
      const plaintReadIdRegexp = /(?<!(>|\/))(cv([0-9]){7})(?!(<\/a))/g
      const blankRegexp = /^\s*[\r\n]/gm
      // 匹配一种特殊空白符(%09)
      const specialBlankRegexp = /%09(%09)*/g
      if ($videoDescription.childElementCount > 1 && $videoDescriptionInfo.childElementCount > 0) {
        let $upAvatarFace, $upAvatarIcon, upAvatarFaceLink
        const $membersContainer = document.querySelector(selectors.membersContainer)
        if ($membersContainer) {
          const $membersUpAvatarFace = await utils.getElementAndCheckExistence(selectors.membersUpAvatarFace)
          upAvatarFaceLink = $membersUpAvatarFace.getAttribute('src')
        } else {
          [$upAvatarFace, $upAvatarIcon] = await utils.getElementAndCheckExistence([selectors.upAvatarFace, selectors.upAvatarIcon])
          upAvatarFaceLink = $upAvatarFace.dataset.src.replace('@96w_96h_1c_1s_!web-avatar', '@160w_160h_1c_1s_!web-avatar-comment')
        }
        // 先将内容编码后替换特殊空白符(%09)为普通空格(%20)后再解码供后续使用
        const resetVideoDescriptionInfoHtml = decodeURIComponent(encodeURIComponent($videoDescriptionInfo.innerHTML).replace(specialBlankRegexp, '%20'))
        // 先将 % 编码为 %25 防止后续执行 decodeURIComponent() 报错,因为 % 为非法字符
        const videoDescriptionInfoHtml = resetVideoDescriptionInfoHtml.replaceAll('%', '%25').replace(nbspToBlankRegexp, ' ').replace(timeStringRegexp, (match) => {
          return `<a class="jump-link video-time" data-video-part="-1" data-video-time="${getTotalSecondsFromTimeString(match)}">${match}</a>`
        }).replace(urlRegexp, (match) => {
          return `<a href="${match}" target="_blank">${match}</a>`
        }).replace(plaintVideoIdRegexp, (match) => {
          return `<a href="https://www.bilibili.com/video/${match}" target="_blank">${match}</a>`
        }).replace(plaintReadIdRegexp, (match) => {
          return `<a href="https://www.bilibili.com/read/${match}" target="_blank">${match}</a>`
        }).replace(blankRegexp, '')
        const upAvatarDecorationLink = document.querySelector(selectors.upAvatarDecoration) ? document.querySelector(selectors.upAvatarDecoration).dataset.src.replace('@144w_144h_!web-avatar', '@240w_240h_!web-avatar-comment') : ''
        const vueScopeId = await modules.getVueScopeId(selectors.videoRootReplyContainer)
        // utils.logger.debug(vueScopeId)
        const videoDescriptionReplyTemplate = `
          <div data-v-${vueScopeId}="" data-v-bad1995c="" id="comment-description" class="reply-item">
              <div data-v-${vueScopeId}="" class="root-reply-container">
                  <div data-v-${vueScopeId}="" class="root-reply-avatar" >
                      <div data-v-${vueScopeId}="" class="avatar">
                          <div class="bili-avatar" style="width:48px;height:48px">
                              <img class="bili-avatar-img bili-avatar-face bili-avatar-img-radius" data-src="${upAvatarFaceLink}" src="${upAvatarFaceLink}">
                              <div class="bili-avatar-pendent-dom">
                                  <img class="bili-avatar-img" data-src="${upAvatarDecorationLink}" alt="" src="${upAvatarDecorationLink}">
                              </div>
                              <span class="${$upAvatarIcon?.classList}"></span>
                          </div>
                      </div>
                  </div>
                  <div data-v-${vueScopeId}="" class="content-warp">
                      <div data-v-${vueScopeId}="" class="user-info">
                          <div data-v-${vueScopeId}="" class="user-name" style="color:#00a1d6!important">视频简介丨播放页调整</div>
                      </div>
                      <div data-v-${vueScopeId}="" class="root-reply">
                          <span data-v-${vueScopeId}="" class="reply-content-container root-reply">
                              <span class="reply-content">${decodeURIComponent(videoDescriptionInfoHtml)}</span>
                          </span>
                      </div>
                  </div>
              </div>
              <div data-v-${vueScopeId}="" class="bottom-line"></div>
          </div>`
        utils.createElementAndInsert(videoDescriptionReplyTemplate, $videoCommentReplyList, 'prepend')
        document.querySelector('#comment-description:not(:first-child)')?.remove()
      } else {
        $videoDescriptionInfo.innerHTML = $videoDescriptionInfo.innerHTML.replace(nbspToBlankRegexp, ' ').replace(timeStringRegexp, (match) => {
          return `<a class="jump-link video-time" data-video-part="-1" data-video-time="${getTotalSecondsFromTimeString(match)}">${match}</a>`
        }).replace(urlRegexp, (match) => {
          return `<a href="${match}" target="_blank">${match}</a>`
        }).replace(plaintVideoIdRegexp, (match) => {
          return `<a href="https://www.bilibili.com/video/${match}" target="_blank">${match}</a>`
        }).replace(plaintReadIdRegexp, (match) => {
          return `<a href="https://www.bilibili.com/read/${match}" target="_blank">${match}</a>`
        }).replace(blankRegexp, '')
      }
    },
    // #endregion 视频简介优化
    // #region 自动跳过时间节点
    /**
     * 设置当前视频自动跳过信息
     * - #region 设置当前视频自动跳过信息(本地)
     * - indexedDB
     * - 数据存在浏览器本地
     */
    async setVideoSkipTimeNodesByIndexedDB(videoSkipTimeNodesArray, videoID = modules.getCurrentVideoID()) {
      if (videoID !== 'error') {
        const videoSkipTimeNodesList = localforage.createInstance({
          name: 'videoSkipTimeNodesList',
        })
        const result = videoSkipTimeNodesList.setItem(videoID, videoSkipTimeNodesArray).then(() => {
          // logger.info(`自动跳过丨节点储存丨${value}丨成功丨本地`)
          return {
            code: 200,
            message: `节点上传丨本地:成功`
          }
        }).catch(error => {
          // logger.error(error)
          return {
            code: 0,
            message: error
          }
        })
        return result
      } else {
        utils.logger.error('videoID丨获取失败')
      }
    },
    // #endregion 设置当前视频自动跳过信息(本地)
    /**
     * 获取当前视频自动跳过信息
     * - #region 获取当前视频自动跳过信息(本地)
     * - indexedDB
     * - 数据存在浏览器本地
     */
    async getVideoSkipTimeNodesByIndexedDB(videoID = modules.getCurrentVideoID()) {
      const videoSkipTimeNodesList = localforage.createInstance({
        name: 'videoSkipTimeNodesList',
      })
      if (videoID !== 'error') {
        try {
          const value = await videoSkipTimeNodesList.getItem(videoID)
          return value
        } catch (error) {
          utils.logger.error(error)
        }
      } else {
        utils.logger.error('videoID丨获取失败')
      }
    },
    // #endregion 获取当前视频自动跳过信息(本地)
    /**
     * 设置当前视频自动跳过信息
     * - #region 设置当前视频自动跳过信息(云端)
     * - Axios
     * - 数据存在云数据库
     */
    async setVideoSkipTimeNodesByAxios(timeNodesArray, videoID = modules.getCurrentVideoID()) {
      const videoAuthor = decodeURIComponent(await utils.getMetaContent('name="author"'))
      let videoTitle, videoUrl
      if (vals.player_type() === 'video') {
        videoTitle = decodeURIComponent(document.title.replace('_哔哩哔哩_bilibili', ''))
        videoUrl = decodeURIComponent(await utils.getMetaContent('itemprop="url"'))
      }
      if (vals.player_type() === 'bangumi') {
        videoTitle = document.title.replace(/-*高清.*哩/gi, '')
        videoUrl = decodeURIComponent(await utils.getMetaContent('property="og:url"'))
      }
      if (videoID !== 'error') {
        const timeNodesArraySafe = decodeURIComponent(timeNodesArray)
        const url = `https://hn216.api.yesapi.cn/?s=SVIP.Swxqian_MyApi.AUpdateSkipTimeNodes&return_data=0&videoID=${videoID}&timeNodesArray=${timeNodesArraySafe}&videoTitle=${videoTitle}&videoAuthor=${videoAuthor}&videoUrl=${videoUrl}&app_key=A11B09901609FA722CFDFEB981EC31DB&sign=6BAEA5FDE94074B8C3ADF35789AE8B18&yesapi_allow_origin=1`
        const result = axios.post(url).then(response => {
          // utils.logger.debug(response)
          const responseData = response.data
          const { msg, ret, data } = responseData
          const { err_msg } = data
          if (Object.keys(data).length === 0) {
            return {
              code: ret,
              message: `云端:失败:${msg}`
            }
          } else {
            return {
              code: ret,
              message: err_msg
            }
          }
        }).catch(error => {
          return {
            message: error
          }
        })
        return result
      } else {
        utils.logger.error('videoID丨获取失败')
      }
    },
    // #endregion 设置当前视频自动跳过信息(云端)
    /**
     * 获取当前视频自动跳过信息
     * - #region 获取当前视频自动跳过信息(云端)
     * - Axios
     * - 数据存在云数据库
     */
    async getVideoSkipTimeNodesByAxios(videoID = modules.getCurrentVideoID()) {
      if (videoID !== 'error') {
        const url = `https://hn216.api.yesapi.cn/?s=SVIP.Swxqian_MyApi.AGetSkipTimeNodes&return_data=0&videoID=${videoID}&app_key=A11B09901609FA722CFDFEB981EC31DB&sign=574181B06EBD07D9252199563CD7D9D3&yesapi_allow_origin=1`
        const result = axios.post(url).then(response => {
          const skipNodesInfo = response.data.data
          const success = skipNodesInfo.success
          const timeNodesArray = skipNodesInfo.info?.timeNodesArray
          if (success && timeNodesArray !== '') {
            // utils.logger.info(skipNodesInfo.info.timeNodesArray)
            return JSON.parse(timeNodesArray)
          } else {
            return false
          }
        }).catch(error => {
          utils.logger.error(error)
        })
        return result
      } else {
        utils.logger.error('videoID丨获取失败')
      }
    },
    // #endregion 获取当前视频自动跳过信息(云端)
    /**
     * 自动跳过视频已设置设置时间节点
     * - #region 自动跳过视频已设置设置时间节点
     */
    async autoSkipTimeNodes() {
      if (!vals.auto_skip()) return
      const videoID = modules.getCurrentVideoID()
      const [$video, $setSkipTimeNodesInput] = await utils.getElementAndCheckExistence([selectors.video, selectors.setSkipTimeNodesInput])
      const skipTo = (seconds) => {
        $video.currentTime = seconds
        if ($video.paused) {
          $video.play()
        }
      }
      const findTargetTimeNode = (num, arr) => {
        for (let i = 0; i < arr[0].length; i++) {
          if (arr[0][i] === num) {
            return arr[1][i];
          }
        }
        return null;
      }
      // [[10,30],[20,40]] → [[10,20],[30,40]]
      const convertArraySaveToReadable = (arr) => {
        if (typeof arr === 'string') arr = JSON.parse(arr)
        const readableArr = arr[0].map((col, i) => arr.map(row => row[i]))
        return JSON.stringify(readableArr).slice(1, -1)
      }
      if (videoID !== 'error') {
        let videoSkipTimeNodesArray
        const videoSkipTimeNodesArrayIndexedDB = await modules.getVideoSkipTimeNodesByIndexedDB(videoID)
        if (videoSkipTimeNodesArrayIndexedDB) {
          videoSkipTimeNodesArray = videoSkipTimeNodesArrayIndexedDB
        } else {
          const videoSkipTimeNodesArrayAxios = await modules.getVideoSkipTimeNodesByAxios(videoID)
          if (videoSkipTimeNodesArrayAxios) {
            videoSkipTimeNodesArray = videoSkipTimeNodesArrayAxios
            await modules.setVideoSkipTimeNodesByIndexedDB(videoSkipTimeNodesArray, videoID)
          } else {
            utils.logger.info('自动跳过丨节点信息不存在')
            return
          }
        }
        utils.logger.info(`自动跳过丨已获取节点信息丨${JSON.stringify(videoSkipTimeNodesArray)}`)
        $setSkipTimeNodesInput.value = convertArraySaveToReadable(videoSkipTimeNodesArray)
        $video.addEventListener('timeupdate', function () {
          const currentTime = Math.ceil($video.currentTime)
          const targetTimeNode = findTargetTimeNode(currentTime, videoSkipTimeNodesArray)
          if (vals.auto_skip() && targetTimeNode) skipTo(targetTimeNode)
        })
      }
    },
    // #endregion 自动跳过视频已设置设置时间节点
    /**
     * 插入设置跳过时间节点按钮
     * - #region 插入设置跳过时间节点按钮
     */
    async insertSetSkipTimeNodesButton() {
      const videoID = modules.getCurrentVideoID()
      if (++vars.insertSetSkipTimeNodesButtonCount !== 1 || !vals.auto_skip()) return
      const [$video, $playerContainer, $playerControllerBottomRight, $playerTooltipArea] = await utils.getElementAndCheckExistence([selectors.video, selectors.playerContainer, selectors.playerControllerBottomRight, selectors.playerTooltipArea])
      const validateInputValue = (inputValue) => {
        const regex = /^\[\d+,\d+\](,\[\d+,\d+\])*?$/g;
        const numbers = inputValue.match(/\[(\d+),(\d+)\]/g)?.flatMap(match => match.slice(1, -1).split(',')).map(Number) || [];
        const hasDuplicates = new Set(numbers).size !== numbers.length
        if (inputValue === '' || !regex.test(inputValue) || hasDuplicates) {
          return false
        }
        const isAscending = numbers.every((num, i) => i === 0 || num >= numbers[i - 1])
        return isAscending
      }
      // [[10,20],[30,40]] → [[10,30],[20,40]]
      const convertArrayReadableToSave = (arr) => {
        return arr[0].map((col, i) => arr.map(row => row[i]))
      }
      // [10,20,30,40] → [[10,30],[20,40]]
      // const convertArrayRecordToSave = (arr) => {
      //     return arr.reduce((acc, num, i) => {
      //         i % 2 === 0 ? acc[0].push(num) : acc[1].push(num);
      //         return acc;
      //     }, [[], []]);
      // }
      // [10,20,30,40] → [[10,20],[30,40]]
      const convertArrayRecordToReadable = (arr) => {
        return arr.reduce((acc, _, i) => {
          if (i % 2 === 0) {
            acc.push(arr.slice(i, i + 2));
          }
          return acc;
        }, []);
      }
      const setSkipTimeNodesPopoverToggleButtonHtml = `
          <button id="${selectors.setSkipTimeNodesPopoverToggleButton.slice(1)}" popovertarget="${selectors.setSkipTimeNodesPopover.slice(1)}" class="bpx-player-ctrl-btn bpx-player-ctrl-skip" role="button" aria-label="插入时间节点" tabindex="0">
              <div class="bpx-player-ctrl-btn-icon">
                  <span class="bpx-common-svg-icon">
                      <svg xmlns="http://www.w3.org/2000/svg" width="88" height="88" class="icon" viewBox="0 0 1024 1024">
                          <path fill="#fff" d="M672 896a21.333 21.333 0 0 1 21.333 21.333v21.334A21.333 21.333 0 0 1 672 960H352a21.333 21.333 0 0 1-21.333-21.333v-21.334A21.333 21.333 0 0 1 352 896h320zM512 64a362.667 362.667 0 0 1 181.333 676.821v69.846A21.333 21.333 0 0 1 672 832H352a21.333 21.333 0 0 1-21.333-21.333V740.82A362.667 362.667 0 0 1 512 64zm24.107 259.243a21.333 21.333 0 0 0-29.398 6.826l-1.792 3.499a21.333 21.333 0 0 0-1.45 7.765l-.043 62.806-129.45-80.896a21.333 21.333 0 0 0-32.64 18.09v179.03a21.333 21.333 0 0 0 21.333 21.333l3.968-.384a21.333 21.333 0 0 0 7.338-2.859l129.451-80.981.043 62.89a21.333 21.333 0 0 0 32.64 18.091l143.232-89.514a21.333 21.333 0 0 0 0-36.182z" />
                      </svg>
                  </span>
              </div>
          </button>`
      const setSkipTimeNodesPopoverHtml = `
          <div id="${selectors.setSkipTimeNodesPopover.slice(1)}" popover>
              <div class="setSkipTimeNodesWrapper">
                  <div class="header">
                      <span class="title">上传时间节点(${videoID})</span>
                      <span class="extra"></span>
                  </div>
                  <div class="tips close">
                      <span class="detail open">
                          <svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="18" height="18">
                              <path d="M512 926.476C283.087 926.476 97.524 740.913 97.524 512S283.087 97.524 512 97.524 926.476 283.087 926.476 512 740.913 926.476 512 926.476zm0-73.143c188.514 0 341.333-152.82 341.333-341.333S700.513 170.667 512 170.667 170.667 323.487 170.667 512 323.487 853.333 512 853.333zm-6.095-192.097L283.526 438.857l51.712-51.712 170.667 170.667L676.57 387.145l51.712 51.712-222.378 222.379z" fill="#909399"></path>
                          </svg>
                      </span>
                      <div class="contents">
                          视频播放到相应时间点时将触发跳转至设定时间点
                          <br>
                          格式:[触发时间点,目标时间点]
                          <br>
                          条件:触发时间点始终小于目标时间点且任意两数不相等
                          <br>
                          例:[10,20] 表示视频播放至第 10 秒时跳转至第 20 秒
                          <br>
                          若有多组节点请使用英文逗号 ',' 隔开
                          <br>
                          例:[10,20],[30,40],[50,60]
                      </div>
                  </div>
                  <span style="display:flex;color:#f56c6c">🈲请勿随意上传无意义时间点,否则将严重影响其他用户观看体验!</span>
                  <div class="records">
                      <span id="${selectors.skipTimeNodesRecordsArray.slice(1)}"></span>
                      <div class="recordsButtonsGroup">
                          <button id="${selectors.clearRecordsButton.slice(1)}">清除数据</button>
                          <button id="${selectors.saveRecordsButton.slice(1)}">保存数据</button>
                      </div>
                  </div>
                  <div class="adjustment_tips clouds">
                    <div>已有记录:<span id="${selectors.skipTimeNodesCloudsArray.slice(1)}"></span></div>
                  </div>
                  <div class="handles">
                      <input id="${selectors.setSkipTimeNodesInput.slice(1)}" class="adjustment_input" value="">
                      <button id="${selectors.uploadSkipTimeNodesButton.slice(1)}">上传</button>
                      <button id="${selectors.syncSkipTimeNodesButton.slice(1)}">同步</button>
                  </div>
                  <div class="result" style="display:none"></div>
              </div>
          </div>`
      const setSkipTimeNodesButtonTipHtml = `
          <div id="setSkipTimeNodesButtonTip" class="bpx-player-tooltip-item" style="visibility: hidden; opacity: 0; transform: translate(0px, 0px);">
              <div class="bpx-player-tooltip-title">上传节点</div>
          </div>`
      const $setSkipTimeNodesPopoverToggleButton = utils.createElementAndInsert(setSkipTimeNodesPopoverToggleButtonHtml, $playerControllerBottomRight, 'append')
      const $setSkipTimeNodesPopover = utils.createElementAndInsert(setSkipTimeNodesPopoverHtml, $playerContainer, 'append')
      const $setSkipTimeNodesButtonTip = utils.createElementAndInsert(setSkipTimeNodesButtonTipHtml, $playerTooltipArea, 'append')
      $setSkipTimeNodesPopoverToggleButton.addEventListener('mouseover', function () {
        const { top, left } = utils.getElementOffsetToDocument(this)
        // utils.logger.debug(`${top} ${left} ${window.scrollY} ${top - window.scrollY}`)
        $setSkipTimeNodesButtonTip.style.top = `${top - window.scrollY - (this.clientHeight * 2) - 5}px`
        $setSkipTimeNodesButtonTip.style.left = `${left - ($setSkipTimeNodesButtonTip.clientWidth / 2) + (this.clientWidth / 2)}px`
        $setSkipTimeNodesButtonTip.style.opacity = 1
        $setSkipTimeNodesButtonTip.style.visibility = 'visible'
        $setSkipTimeNodesButtonTip.style.transition = 'opacity .3s'
      })
      $setSkipTimeNodesPopoverToggleButton.addEventListener('mouseout', () => {
        $setSkipTimeNodesButtonTip.style.opacity = 0
        $setSkipTimeNodesButtonTip.style.visibility = 'hidden'
      })
      const [$setSkipTimeNodesPopoverHeaderExtra, $setSkipTimeNodesPopoverTips, $setSkipTimeNodesPopoverTipsDetail, $setSkipTimeNodesPopoverRecords, $setSkipTimeNodesInput, $skipTimeNodesRecordsArray, $setSkipTimeNodesPopoverResult, $clearRecordsButton, $saveRecordsButton, $uploadSkipTimeNodesButton, $syncSkipTimeNodesButton, $setSkipTimeNodesPopoverClouds, $skipTimeNodesCloudsArray] = await utils.getElementAndCheckExistence([selectors.setSkipTimeNodesPopoverHeaderExtra, selectors.setSkipTimeNodesPopoverTips, selectors.setSkipTimeNodesPopoverTipsDetail, selectors.setSkipTimeNodesPopoverRecords, selectors.setSkipTimeNodesInput, selectors.skipTimeNodesRecordsArray, selectors.setSkipTimeNodesPopoverResult, selectors.clearRecordsButton, selectors.saveRecordsButton, selectors.uploadSkipTimeNodesButton, selectors.syncSkipTimeNodesButton, selectors.setSkipTimeNodesPopoverClouds, selectors.skipTimeNodesCloudsArray])
      const cloudsArray = await modules.getVideoSkipTimeNodesByAxios(videoID)
      if (cloudsArray) {
        if (typeof cloudsArray === 'string') cloudsArray = JSON.parse(cloudsArray)
        $setSkipTimeNodesPopoverClouds.style.display = 'block'
        $skipTimeNodesCloudsArray.innerText = JSON.stringify(convertArrayReadableToSave(cloudsArray)).slice(1, -1)
      } else {
        $setSkipTimeNodesPopoverClouds.style.display = 'none'
      }
      $setSkipTimeNodesPopoverTipsDetail.addEventListener('click', function (event) {
        event.stopPropagation()
        const detailClassList = [...this.classList]
        if (detailClassList.includes('open')) {
          this.classList.replace('open', 'close')
          $setSkipTimeNodesPopoverTips.classList.replace('close', 'open')
        }
        if (detailClassList.includes('close')) {
          this.classList.replace('close', 'open')
          $setSkipTimeNodesPopoverTips.classList.replace('open', 'close')
        }
      })
      $setSkipTimeNodesPopoverToggleButton.addEventListener('click', () => {
        const currentTime = Math.ceil($video.currentTime)
        $setSkipTimeNodesPopoverHeaderExtra.innerText = `${currentTime} / ${$video.duration}`
      })
      $setSkipTimeNodesPopover.addEventListener('toggle', (event) => {
        if (event.newState === 'open') {
          $video.pause()
        }
        if (event.newState === 'closed') {
          $video.play()
        }
      })
      $clearRecordsButton.addEventListener('click', () => {
        arrays.skipNodesRecords = []
        $skipTimeNodesRecordsArray.className = ''
        $skipTimeNodesRecordsArray.innerText = ''
        $setSkipTimeNodesPopoverRecords.style.display = 'none'
        $setSkipTimeNodesInput.value = ''
      })
      $saveRecordsButton.addEventListener('click', () => {
        $setSkipTimeNodesInput.value = JSON.stringify(convertArrayRecordToReadable(JSON.parse($skipTimeNodesRecordsArray.innerText.replace('打点数据:', '')))).slice(1, -1)
      })
      const resetResultContent = (delay = 3000) => {
        const resetResultContentTimeout = setTimeout(() => {
          $setSkipTimeNodesPopoverResult.innerText = ''
          $setSkipTimeNodesPopoverResult.className = 'result'
          clearTimeout(resetResultContentTimeout)
        }, delay)
        arrays.intervalIds.push(resetResultContentTimeout)
      }
      $uploadSkipTimeNodesButton.addEventListener('click', async () => {
        const inputValue = $setSkipTimeNodesInput.value
        if (!validateInputValue(inputValue)) {
          $setSkipTimeNodesPopoverResult.classList.remove('success')
          $setSkipTimeNodesPopoverResult.classList.add('danger')
          $setSkipTimeNodesPopoverResult.innerText = '请按格式条件输入正确内容!'
          resetResultContent()
        } else {
          const timeNodesArray = convertArrayReadableToSave(JSON.parse(`[${inputValue}]`))
          const result_indexedDB = await modules.setVideoSkipTimeNodesByIndexedDB(timeNodesArray, videoID)
          const result_axios = await modules.setVideoSkipTimeNodesByAxios(JSON.stringify(timeNodesArray), videoID)
          // logger.debug(`${JSON.stringify(result_indexedDB)}丨${JSON.stringify(result_axios)}`)
          if ((result_indexedDB.code && result_axios.code) === 200) {
            $setSkipTimeNodesInput.value = ''
            $setSkipTimeNodesPopoverResult.classList.remove('danger')
            $setSkipTimeNodesPopoverResult.classList.add('success')
            $setSkipTimeNodesPopoverResult.innerText = `${result_indexedDB.message}丨${result_axios.message}`
          } else {
            $setSkipTimeNodesPopoverResult.classList.remove('success')
            $setSkipTimeNodesPopoverResult.classList.add('danger')
            $setSkipTimeNodesPopoverResult.innerText = `${result_indexedDB.message}丨${result_axios.message}`
          }
          resetResultContent()
        }
      })
      $syncSkipTimeNodesButton.addEventListener('click', async () => {
        const cloudsArray = await modules.getVideoSkipTimeNodesByAxios(videoID)
        if (cloudsArray) {
          if (typeof cloudsArray === 'string') cloudsArray = JSON.parse(cloudsArray)
          modules.setVideoSkipTimeNodesByIndexedDB(cloudsArray, videoID)
          const readableArr = JSON.stringify(convertArrayReadableToSave(cloudsArray)).slice(1, -1)
          $skipTimeNodesCloudsArray.innerText = readableArr
          $setSkipTimeNodesInput.value = readableArr
        }
      })

    },
    // #endregion 插入设置跳过时间节点按钮
    /**
     * 插入跳过时间节点功能开关
     * - #region 插入跳过时间节点功能开关
     */
    async insertSkipTimeNodesSwitchButton() {
      if (++vars.insertSetSkipTimeNodesSwitchButtonCount !== 1) return
      const skipTimeNodesSwitchButtonHtml = `
          <div id="autoSkipSwitchButton" class="bpx-player-dm-switch bui bui-danmaku-switch" aria-label="跳过开启关闭">
            <div class="bui-area">
                <input id="${selectors.AutoSkipSwitchInput.slice(1)}" class="bui-danmaku-switch-input" type="checkbox" ${vals.auto_skip() ? 'checked' : ''}>
                <label class="bui-danmaku-switch-label">
                <span class="bui-danmaku-switch-on">
                    <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24">
                    <path fill-rule="evenodd" d="M12 4.83h-1.53L8.76 2.27a1 1 0 1 0-1.67 1.12l1 1.5L5.92 5a4 4 0 0 0-3.83 3.4 30.92 30.92 0 0 0-.24 4.18 31.81 31.81 0 0 0 .35 5.12A4 4 0 0 0 6 21.06l.91.05c1.2.06 1.8.09 3.6.09a1 1 0 0 0 1-1 1 1 0 0 0-1-1c-1.76 0-2.34 0-3.5-.09l-.91-.05a2 2 0 0 1-1.91-1.71 29.75 29.75 0 0 1-.33-4.8 28 28 0 0 1 .23-3.9A2 2 0 0 1 6 6.93c2.45-.08 4.47-.13 6.06-.13s3.62 0 6.07.13A2 2 0 0 1 20 8.75c.08.52.12 2 .14 3.06v.88a1 1 0 1 0 2-.06v-.86c0-1.12-.08-2.66-.16-3.27A4 4 0 0 0 18.19 5l-2.53-.08 1.05-1.46a1 1 0 0 0-1.64-1.18l-1.86 2.55H12z" />
                    <path fill="#00aeec" fill-rule="evenodd" d="M22.85 14.63a1 1 0 0 0-1.42.07l-5.09 5.7-2.21-2.27L14 18a1 1 0 0 0-1.32 1.49l3 3 .1.09a1 1 0 0 0 1.36-.12L22.93 16l.08-.1a1 1 0 0 0-.16-1.27z" />
                    <path d="M7.58 8.23h3.12v3.54h-.9v1.62h1v.67a7.14 7.14 0 0 0 1.84-1.41v-1l-.72.36a17 17 0 0 0-1-2.17l.83-.41a18.26 18.26 0 0 1 .9 2.12V7.82h1v5a9 9 0 0 1-.47 3.05 5.26 5.26 0 0 1-1.4 2.13l-.78-.7a5 5 0 0 0 1.56-3.4 7.46 7.46 0 0 1-1.29 1.1l-.5-.83v.09h-1V16c.37-.13.7-.25 1-.37v.94a29.54 29.54 0 0 1-3.39 1.19l-.29-.93.42-.11v-3.9h.84v3.64l.55-.18v-4.51H7.58zm2.22 2.68V9.09H8.48v1.82zm6.53-1.81l.86.42a10 10 0 0 1-1.25 2.32l-.71-.5v.92a11.11 11.11 0 0 1 2 1.62l-.59.9a11.39 11.39 0 0 0-1.39-1.44v3.17c0 .21.1.32.29.32h.35a.36.36 0 0 0 .35-.22 4.31 4.31 0 0 0 .18-1.47l.9.28a4.27 4.27 0 0 1-.4 2 1.1 1.1 0 0 1-.83.3h-.84c-.66 0-1-.34-1-1v-8.9h1v3.33a9.28 9.28 0 0 0 1.08-2.05z" />
                    </svg>
                </span>
                <span class="bui-danmaku-switch-off">
                    <svg xmlns="http://www.w3.org/2000/svg" data-pointer="none" viewBox="0 0 24 24">
                    <path fill-rule="evenodd" d="M8.09 4.89l-1-1.5a1 1 0 1 1 1.68-1.12l1.7 2.57h2.74l1.86-2.59a1 1 0 0 1 1.64 1.18l-1.05 1.45 2.53.12a4 4 0 0 1 3.74 3.51c.08.61.13 2.15.16 3.27v.86a1 1 0 0 1-2 .07v-.89c0-1.1-.06-2.54-.14-3.06a2 2 0 0 0-1.85-1.82c-2-.07-4-.12-6.07-.13-1.59 0-3.62 0-6.06.13a2 2 0 0 0-1.92 1.74 28 28 0 0 0-.23 3.91 29.71 29.71 0 0 0 .33 4.79 2 2 0 0 0 1.91 1.71c1.8.1 3.61.14 5.41.14a1 1 0 0 1 1 1 1 1 0 0 1-1 1c-1.84 0-3.67-.05-5.51-.15A4 4 0 0 1 2.2 17.7a31.81 31.81 0 0 1-.35-5.12 30.92 30.92 0 0 1 .24-4.18A4 4 0 0 1 5.92 5l2.16-.07zm10 17.17a4 4 0 1 0-4-4 4 4 0 0 0 3.97 4zm0-1.5a2.5 2.5 0 0 1-2.5-2.5 2.61 2.61 0 0 1 .28-1.16l3.33 3.4a2.55 2.55 0 0 1-1.14.26zm2.5-2.5a2.38 2.38 0 0 1-.29 1.16l-3.3-3.4a2.5 2.5 0 0 1 3.61 2.24z" />
                    <path fill="none" d="M8.28 9.08H9.6v1.83H8.28zM13.42 15.08v-.85h-.11c0 .29-.09.58-.15.85z" />
                    <path d="M13.31 14.23h-1a7.52 7.52 0 0 1-.18.85h1.05c.04-.27.09-.56.13-.85zM13.4 9.6v-.24l-.54.24h.54zM13.4 9V7.82h-1V8l.33-.11A8.32 8.32 0 0 1 13.4 9zM12.41 9.4v.2h.11a2 2 0 0 0-.11-.2zM11.59 9.6l-.08-.18-.84.41c.18.32.36.67.53 1V9.6z" />
                    <path d="M11.2 13.64a7 7 0 0 1-.64.41v-.67h-1v-1.61h.9V8.22H7.37v3.55h1.32v4.5l-.55.18v-3.64h-.83v3.87l-.42.11.29.94a32.83 32.83 0 0 0 3.38-1.19v-.95c-.27.12-.59.24-1 .38v-1.69h1v-.08l.51.82a6.91 6.91 0 0 0 .94-.79h-.81zm-2.92-2.73V9.08H9.6v1.83zM15 8.2v-.38h-1V9.6h.34c.28-.46.5-.93.66-1.4zM10.78 17.3l.8.69a5.19 5.19 0 0 0 1.24-1.84h-1.15a4.22 4.22 0 0 1-.89 1.15zM16.81 9.89c.06-.13.12-.24.18-.38l-.86-.42c-.07.18-.16.34-.24.51h.92z" />
                    <path d="M15 13.84v-.5c.1.08.21.2.32.3a4.33 4.33 0 0 1 .92-.44 11.62 11.62 0 0 0-1.24-.95v-.91l.7.49a9.47 9.47 0 0 0 1.08-1.94V9.6h-.92a8.86 8.86 0 0 1-.86 1.55v-3c-.19.47-.41.94-.65 1.4H14v5.17a5.13 5.13 0 0 1 1-.88zM13.4 12.83V9.6h-.54l.54-.24V9a8.32 8.32 0 0 0-.66-1.11l-.33.11v1.4a2 2 0 0 1 .11.2h-.11v2a18.76 18.76 0 0 0-.82-2h-.39v1.27a12.22 12.22 0 0 1 .48 1.13l.73-.37v1a7.31 7.31 0 0 1-1.21 1v.59h.8c.11-.11.23-.21.34-.33 0 .12 0 .22-.06.33h1a12.21 12.21 0 0 0 .12-1.39zM13.16 15.08h-1.05a4.9 4.9 0 0 1-.44 1.07h1.15c0-.09.07-.17.11-.27.07-.25.16-.52.23-.8z" />
                    </svg>
                </span>
                </label>
            </div>
          </div>`
      const skipTimeNodesSwitchButtonTipHtml = `
          <div id="autoSkipTips" class="bpx-player-tooltip-item" style="visibility: hidden; opacity: 0; transform: translate(0px, 0px);">
              <div class="bpx-player-tooltip-title">关闭自动跳过(j)</div>
          </div>`
      const [playerDanmuSetting, playerTooltipArea] = await utils.getElementAndCheckExistence([selectors.playerDanmuSetting, selectors.playerTooltipArea])
      const $skipTimeNodesSwitchButton = utils.createElementAndInsert(skipTimeNodesSwitchButtonHtml, playerDanmuSetting, 'after')
      const $autoSkipTips = utils.createElementAndInsert(skipTimeNodesSwitchButtonTipHtml, playerTooltipArea, 'append')
      const $AutoSkipSwitchInput = await utils.getElementAndCheckExistence(selectors.AutoSkipSwitchInput)
      $AutoSkipSwitchInput.addEventListener('change', async event => {
        const $AutoSkipInput = await utils.getElementAndCheckExistence(selectors.AutoSkip)
        utils.setValue('auto_skip', event.target.checked)
        $AutoSkipInput.checked = event.target.checked
        $autoSkipTips.querySelector(selectors.playerTooltipTitle).innerText = event.target.checked ? '关闭自动跳过(j)' : '开启自动跳过(j)'
      })
      $skipTimeNodesSwitchButton.addEventListener('mouseover', async function () {
        const { top, left } = utils.getElementOffsetToDocument(this)
        $autoSkipTips.style.top = `${top - window.scrollY - (this.clientHeight) - 12}px`
        $autoSkipTips.style.left = `${left - ($autoSkipTips.clientWidth / 2) + (this.clientWidth / 2)}px`
        $autoSkipTips.style.opacity = 1
        $autoSkipTips.style.visibility = 'visible'
        $autoSkipTips.style.transition = 'opacity .3s'
      })
      $skipTimeNodesSwitchButton.addEventListener('mouseout', function () {
        $autoSkipTips.style.opacity = 0
        $autoSkipTips.style.visibility = 'hidden'
      })

    },
    // #endregion 插入跳过时间节点功能开关
    // #endregion 自动跳过时间节点
    /**
     * 自动返回播放器并更新评论区简介
     * - #region 自动返回播放器并更新评论区简介
     */
    async autoLocationAndInsertVideoDescriptionToComment() {
      modules.locationToPlayer()
      await utils.sleep(1500)
      modules.insertVideoDescriptionToComment()
    },
    // #endregion 自动返回播放器并更新评论区简介
    /**
     * 点击相关视频自动返回播放器并更新评论区简介
     * - #region 点击相关视频自动返回播放器并更新评论区简介
     * - 合集中的其他视频
     * - 推荐列表中的视频
     */
    async clickRelatedVideoAutoLocation() {
      if (vals.player_type() === 'video') {
        // 视频合集
        await elmGetter.each(selectors.videoSectionsEpisodeLink, (link) => {
          link.addEventListener('click', () => {
            modules.autoLocationAndInsertVideoDescriptionToComment()
          })
        })
        // 视频选集
        await elmGetter.each(selectors.videoMultiPageLink, (link) => {
          link.addEventListener('click', () => {
            modules.autoLocationAndInsertVideoDescriptionToComment()
          })
        })
        // 接下来播放及推荐视频
        await elmGetter.each(selectors.videoNextPlayAndRecommendLink, (link) => {
          link.addEventListener('click', () => {
            modules.autoLocationAndInsertVideoDescriptionToComment()
          })
        })
        // 视频结尾推荐视频
        await elmGetter.each(selectors.playerEndingRelateVideo, (link) => {
          link.addEventListener('click', () => {
            modules.autoLocationAndInsertVideoDescriptionToComment()
          })
        })
      }
      if (vals.player_type() === 'bangumi') {
        // 番剧剧集
        await elmGetter.each(selectors.bangumiSectionsEpisodeLink, (link) => {
          link.addEventListener('click', async () => {
            await utils.sleep(100)
            modules.locationToPlayer()
          })
        })
      }
    },
    // #endregion 点击相关视频自动返回播放器并更新评论区简介
    /**
    * 解锁合集/选集视频集数选择按钮
    * - #region 解锁合集/选集视频集数选择按钮
    */
    async unlockEpisodeSelector() {
      const videoInfo = await biliApis.getVideoInformation(modules.getCurrentVideoID(window.location.href))
      const { pages = false, ugc_season = false } = videoInfo.data
      if (ugc_season || pages.length > 1) {
        if (!document.getElementById('UnlockEpisodeSelectorStyle')) utils.insertStyleToDocument('UnlockEpisodeSelectorStyle', styles.UnlockEpisodeSelector)
      } else if (document.getElementById('UnlockEpisodeSelectorStyle')) document.getElementById('UnlockEpisodeSelectorStyle').remove()
    },
    // #endregion 解锁合集/选集视频集数选择按钮
    /**
    * 离开当前页面暂停视频
    * - #region 离开当前页面暂停视频
    */
    async pauseVideoWhenLeavingCurrentPage() {
      const $video = await utils.getElementAndCheckExistence(selectors.video)
      let playFlag = false
      const timer = setInterval(async () => {
        const documentHidden = utils.checkDocumentIsHidden()
        if (documentHidden) {
          $video.pause()
          playFlag = true
        }
        else if (vals.continue_play() && playFlag) {
          $video.play()
          playFlag = false
        }
      }, 100)
      arrays.intervalIds.push(timer)
    },
    // #endregion 离开当前页面暂停视频
    // #endregion 视频播放页相关功能
    //** ----------------------- 动态页相关功能 ----------------------- **//
    // #region 动态页相关功能
    /**
     * 默认显示投稿视频
     * - #region 默认显示投稿视频
     */
    changeCurrentUrlToVideoSubmissions() {
      const web_video_link = vals.web_video_link()
      const url = window.location.href
      const indexLink = 'https://t.bilibili.com/pages/nav/index'
      const newIndexLinkRegexp = /(https:\/\/t.bilibili.com\/pages\/nav\/index_new).*/i
      const indexVoteLinkRegexp = /https:\/\/t.bilibili.com\/vote\/h5\/index\/#\/result\?vote_id=.*/i
      const webVoteLinkRegexp = /t.bilibili.com\/h5\/dynamic\/vote#\/result\?vote_id=.*/i
      const indexLotteryLinkRegexp = /https:\/\/t.bilibili.com\/lottery\/h5\/index\/.*/i
      const webLotteryLinkRegexp = /https:\/\/t.bilibili.com\/lottery\/.*/i
      const moreDynamicLinkRegexp = /https:\/\/t.bilibili.com\/[0-9]+\?tab=[0-9]+/i
      const dynamicDetailLinkRegexp = /https:\/\/t.bilibili.com\/[0-9]+/i
      const dynamicTopicDetailLinkRegexp = /https:\/\/t.bilibili.com\/topic\/[0-9]+/i
      if (url == indexLink || newIndexLinkRegexp.test(url) || indexVoteLinkRegexp.test(url) || webVoteLinkRegexp.test(url) || indexLotteryLinkRegexp.test(url) || webLotteryLinkRegexp.test(url) || moreDynamicLinkRegexp.test(url) || dynamicDetailLinkRegexp.test(url) || dynamicTopicDetailLinkRegexp.test(url)) {
        //不影响BiliBili首页导航栏动态悬浮窗、动态页里投票及互动抽奖页等内容显示
        return false
      }
      if (url !== web_video_link) {
        window.location.href = web_video_link
      } else {
        return { message: '动态页|已切换至投稿视频' }
      }
    },
    // #endregion 默认显示投稿视频
    // #endregion 动态页相关功能
    //** ----------------------- 首页相关功能 ----------------------- **//
    // #region 首页相关功能
    // #region 记录首页推荐视频历史
    /**
     * 将推荐视频写入本地
     * - #region 将推荐视频写入本地
     */
    async setIndexRecordRecommendVideoHistory() {
      if (++vars.setIndexRecordRecommendVideoHistoryArrayCount !== 1) return
      const indexRecommendVideoHistory = localforage.createInstance({
        name: 'indexRecommendVideoHistory',
      })
      await elmGetter.each(selectors.indexRecommendVideoSix, document.body, async video => {
        const url = video.querySelector('a').href
        const title = video.querySelector('h3').title
        if (window.location.host.includes('bilibili.com') && !url.includes('cm.bilibili.com')) {
          const { data: { tid, pic } } = await biliApis.getVideoInformation(modules.getCurrentVideoID(url))
          indexRecommendVideoHistory.setItem(title, [tid, url, pic])
        }
      })

    },
    // #endregion 将推荐视频写入本地
    /**
     * 将本地推荐数据转为数组
     * - #region 将本地推荐数据转为数组
     * @returns 推荐记录数组
     */
    async getIndexRecordRecommendVideoHistoryArray() {
      arrays.indexRecommendVideoHistory = []
      const indexRecommendVideoHistory = localforage.createInstance({
        name: 'indexRecommendVideoHistory',
      })
      await indexRecommendVideoHistory.iterate((value, key) => {
        arrays.indexRecommendVideoHistory.push({ key, value })
      })
      if (!arrays.indexRecommendVideoHistory.length || arrays.indexRecommendVideoHistory.length !== await indexRecommendVideoHistory.length()) {
        return await modules.getIndexRecordRecommendVideoHistoryArray()
      }
      else return arrays.indexRecommendVideoHistory
    },
    // #endregion 将本地推荐数据转为数组
    /**
     * 插入推荐历史记录按钮
     * - #region 插入推荐历史记录按钮
     */
    async insertIndexRecommendVideoHistoryOpenButton() {
      if (document.getElementById(selectors.indexRecommendVideoHistoryOpenButton)) document.getElementById(selectors.indexRecommendVideoHistoryOpenButton).remove()
      if (document.getElementById(selectors.indexRecommendVideoHistoryPopover)) document.getElementById(selectors.indexRecommendVideoHistoryPopover).remove()
      const $indexRecommendVideoRollButtonWrapper = await utils.getElementAndCheckExistence(selectors.indexRecommendVideoRollButtonWrapper)
      const indexRecommendVideoHistoryOpenButtonHtml = `
        <button id="${selectors.indexRecommendVideoHistoryOpenButton.slice(1)}" popovertarget="${selectors.indexRecommendVideoHistoryPopover.slice(1)}" class="primary-btn roll-btn">
            <span>历史记录</span>
        </button>`
      const indexRecommendVideoHistoryPopoverHtml = `
        <div id="${selectors.indexRecommendVideoHistoryPopover.slice(1)}"  class="adjustment_popover" popover>
            <div id="${selectors.indexRecommendVideoHistoryPopoverTitle.slice(1)}">
                <span>首页视频推荐历史记录</span>
                <div id="${selectors.clearRecommendVideoHistoryButton.slice(1)}">清空记录</div>
            </div>
            <ul id="${selectors.indexRecommendVideoHistoryCategory.slice(1)}">
              <li class='all adjustment_button primary plain'>全部</li>
            </ul>
            <ul id="${selectors.indexRecommendVideoHistoryList.slice(1)}"></ul>
        </ul>`
      utils.createElementAndInsert(indexRecommendVideoHistoryOpenButtonHtml, $indexRecommendVideoRollButtonWrapper, 'append')
      const $indexRecommendVideoHistoryPopover = utils.createElementAndInsert(indexRecommendVideoHistoryPopoverHtml, document.body, 'append')
      $indexRecommendVideoHistoryPopover.addEventListener('toggle', async (event) => {
        const [$indexApp, $indexRecommendVideoHistoryPopoverTitle] = await utils.getElementAndCheckExistence([selectors.indexApp, selectors.indexRecommendVideoHistoryPopoverTitle])
        if (event.newState === 'open') {
          $indexApp.style.pointerEvents = 'none'
          $indexRecommendVideoHistoryPopoverTitle.querySelector('span').append(`(${$indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList).childElementCount})`)
        }
        if (event.newState === 'closed') {
          $indexApp.style.pointerEvents = 'auto'
          $indexRecommendVideoHistoryPopoverTitle.querySelector('span').innerText = '首页视频推荐历史记录'
        }
      })
    },
    // #endregion 插入推荐历史记录按钮
    /**
     * 获取推荐历史记录
     * - #region 获取推荐历史记录
     */
    async getIndexRecordRecommendVideoHistory() {
      const getIndexRecordRecommendVideoHistoryArray = await modules.getIndexRecordRecommendVideoHistoryArray()
      const $indexRecommendVideoHistoryPopover = await utils.getElementAndCheckExistence(selectors.indexRecommendVideoHistoryPopover)
      $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList).innerHTML = ''
      for (const record of getIndexRecordRecommendVideoHistoryArray) {
        utils.createElementAndInsert(`<li><span><img src="${record.value[2]}"></span><a href="${record.value[1]}" target="_blank">${record.key}</a></li>`, $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList), 'append')
      }
    },
    // #endregion 获取推荐历史记录
    /**
     * 生成推荐视频分区信息
     * - #region 生成推荐视频分区信息
     */
    async generatorVideoCategories() {
      const setCategoryButtonActiveClass = (element) => {
        element.parentElement.querySelectorAll(selectors.indexRecommendVideoHistoryCategoryButtons).forEach(element => { element.classList.remove(...arrays.videoCategoriesActiveClass) })
        element.classList.add(...arrays.videoCategoriesActiveClass)
      }
      const getIndexRecordRecommendVideoHistoryArray = await modules.getIndexRecordRecommendVideoHistoryArray()
      const $indexRecommendVideoHistoryPopover = await utils.getElementAndCheckExistence(selectors.indexRecommendVideoHistoryPopover)
      $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryCategory).innerHTML = "<li class='all adjustment_button primary plain'>全部</li>"
      let categoryHasVideoSet = new Set()
      // arrays.videoCategories.filter(category => {
      //   getIndexRecordRecommendVideoHistoryArray.filter(record => {
      //     if (category.tids.includes(record.value[0])) {
      //       categoryHasVideoSet.add(category)
      //     }
      //   })
      // })
      for (const [key, value] of Object.entries(objects.videoCategories)) {
        getIndexRecordRecommendVideoHistoryArray.filter(record => {
          if (value.tids.includes(record.value[0])) {
            categoryHasVideoSet.add(objects.videoCategories[key])
          }
        })
      }
      for (const category of Array.from(categoryHasVideoSet)) {
        utils.createElementAndInsert(`<li data-tids="[${category.tids}]">${category.name}</li>`, $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryCategory), 'append')
      }
      await elmGetter.each(selectors.indexRecommendVideoHistoryCategoryButtonsExceptAll, $indexRecommendVideoHistoryPopover, category => {
        category.addEventListener('click', async function () {
          setCategoryButtonActiveClass(this)
          $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList).innerHTML = ''
          const categoryIds = this.dataset.tids
          for (const record of getIndexRecordRecommendVideoHistoryArray) {
            if (categoryIds.includes(record.value[0])) {
              utils.createElementAndInsert(`<li><span><img src="${record.value[2]}"></span><a href="${record.value[1]}" target="_blank">${record.key}</a></li>`, $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList), 'append')
            }
          }
        })
      })
      $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryCategoryButtonAll).addEventListener('click', async function () {
        setCategoryButtonActiveClass(this)
        $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList).innerHTML = ''
        for (const record of getIndexRecordRecommendVideoHistoryArray) {
          utils.createElementAndInsert(`<li><span><img src="${record.value[2]}"></span><a href="${record.value[1]}" target="_blank">${record.key}</a></li>`, $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList), 'append')
        }
      })
    },
    // #endregion 生成推荐视频分区信息
    /**
     * 清除推荐历史记录
     * - #region 清除推荐历史记录
     */
    async clearRecommendVideoHistory() {
      const indexRecommendVideoHistory = localforage.createInstance({
        name: 'indexRecommendVideoHistory',
      })
      indexRecommendVideoHistory.clear()
      arrays.indexRecommendVideoHistory = []
      const $indexRecommendVideoHistoryPopover = await utils.getElementAndCheckExistence(selectors.indexRecommendVideoHistoryPopover)
      $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryList).innerHTML = ''
      $indexRecommendVideoHistoryPopover.querySelector(selectors.indexRecommendVideoHistoryCategory).innerHTML = "<li class='all adjustment_button primary plain'>全部</li>"
      $indexRecommendVideoHistoryPopover.hidePopover()
    },
    // #endregion 清除推荐历史记录
    // #endregion 记录首页推荐视频历史
    // #endregion 首页相关功能
    //** ----------------------- 脚本最终执行函数 ----------------------- **//
    // #region 脚本最终执行函数
    /**
     *
     * 注册脚本设置选项
     * - #region 注册脚本设置选项
     */
    async registerMenuCommand() {
      if (regexps.dynamic.test(window.location.href)) {
        const dynamicSettingPopoverHtml = `
          <div id="${selectors.dynamicSettingPopover.slice(1)}" class="adjustment_popover" popover>
              <div class="adjustment_popoverTitle">哔哩哔哩动态页设置</div>
              <label class="bilibili-adjustment-setting-label" style="padding-top:0!important;display: grid;grid-gap: 10px">
                  「投稿视频」链接:
                  <input id="${selectors.WebVideoLinkInput.slice(1)}" class="adjustment_input" value="${utils.getValue('web_video_link')}">
              </label>
              <div id="${selectors.dynamicSettingPopoverTips.slice(1)}" class="adjustment_tips info">点击「投稿视频」选项后,填入当前浏览器地址栏链接,即可自动跳转至该链接</div>
              <div class="adjustment_buttonGroup">
                <button id="${selectors.dynamicSettingSaveButton.slice(1)}" class="adjustment_button primary">保存</button>
              </div>
          </div>`
        if (document.getElementById(selectors.dynamicSettingPopover)) document.getElementById(selectors.dynamicSettingPopover).remove()
        const $dynamicSettingPopover = utils.createElementAndInsert(dynamicSettingPopoverHtml, document.body, 'append')
        GM_registerMenuCommand('设置', () => {
          $dynamicSettingPopover.showPopover()
        })
        const [$app, $dynamicHeaderContainer, $WebVideoLinkInput, $dynamicSettingSaveButton] = await utils.getElementAndCheckExistence([selectors.app, selectors.dynamicHeaderContainer, selectors.WebVideoLinkInput, selectors.dynamicSettingSaveButton])
        $WebVideoLinkInput.addEventListener('input', event => {
          utils.setValue('web_video_link', event.target.value.trim())
        })
        $dynamicSettingPopover.addEventListener('toggle', event => {
          if (event.newState === 'open') {
            $app.style.pointerEvents = 'none'
            $dynamicHeaderContainer.style.pointerEvents = 'none'
          }
          if (event.newState === 'closed') {
            $app.style.pointerEvents = 'auto'
            $dynamicHeaderContainer.style.pointerEvents = 'auto'
          }
        })
        $dynamicSettingSaveButton.addEventListener('click', () => {
          $dynamicSettingPopover.hidePopover()
        })
      }
      if (regexps.video.test(window.location.href)) {
        const $player = await utils.getElementAndCheckExistence(selectors.player, 10)
        const playerOffsetTop = Math.trunc(utils.getElementOffsetToDocument($player).top)
        const videoSettingPopoverHtml = `
          <div id="${selectors.videoSettingPopover.slice(1)}" class="adjustment_popover" popover>
            <div class="adjustment_popoverTitle">哔哩哔哩播放页设置</div>
            <div class="adjustment_form">
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>是否为大会员</label>
                  <input type="checkbox" id="${selectors.IsVip.slice(1)}" ${vals.is_vip() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <span class="adjustment_tips info"> -> 请如实勾选,否则影响自动选择清晰度</span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>自动定位至播放器</label>
                  <input type="checkbox" id="${selectors.AutoLocate.slice(1)}" ${vals.auto_locate() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <div class="adjustment_checkboxGroup">
                  <div class="adjustment_checkbox video">
                    <span>普通视频(video)</span>
                    <input type="checkbox" id="${selectors.AutoLocateVideo.slice(1)}" ${vals.auto_locate_video() ? 'checked' : ''} class="adjustment_checkbox">
                  </div>
                  <div class="adjustment_checkbox bangumi">
                    <span>其他视频(bangumi)</span>
                    <input type="checkbox" id="${selectors.AutoLocateBangumi.slice(1)}" ${vals.auto_locate_bangumi() ? 'checked' : ''} class="adjustment_checkbox">
                  </div>
                </div>
                <span class="adjustment_tips info">
                  -> 只有勾选自动定位至播放器,才会执行自动定位的功能;勾选自动定位至播放器后,video 和 bangumi
                  两者全选或全不选,默认在这两种类型视频播放页都执行;否则勾选哪种类型,就只在这种类型的播放页才执行。
                </span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>播放器顶部偏移(px)</label>
                  <input id="${selectors.TopOffset.slice(1)}" class="adjustment_input" value="${vals.offset_top()}">
                </div>
                <span class="adjustment_tips info">
                  -> 播放器距离浏览器窗口默认距离为 ${playerOffsetTop};请填写小于 ${playerOffsetTop} 的正整数或 0;当值为 0 时,播放器上沿将紧贴浏览器窗口上沿、值为 ${playerOffsetTop} 时,将保持B站默认。
                </span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>点击播放器时定位</label>
                  <input type="checkbox" id="${selectors.ClickPlayerAutoLocation.slice(1)}" ${vals.click_player_auto_locate() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
              </div>
              <div class="adjustment_form_item screen-mode">
                <div class="adjustment_form_item_content">
                  <label>播放器默认模式</label>
                  <div class="adjustment_checkboxGroup">
                    <div class="adjustment_checkbox">
                      <input type="radio" name="Screen-Mode" value="close" ${vals.selected_screen_mode() === 'close' ? 'checked' : ''}>
                      <span>关闭</span>
                    </div>
                    <div class="adjustment_checkbox">
                      <input type="radio" name="Screen-Mode" value="wide" ${vals.selected_screen_mode() === 'wide' ? 'checked' : ''}>
                      <span>宽屏</span>
                    </div>
                    <div class="adjustment_checkbox">
                      <input type="radio" name="Screen-Mode" value="web" ${vals.selected_screen_mode() === 'web' ? 'checked' : ''}>
                      <span>网页全屏</span>
                    </div>
                  </div>
                </div>
                <span class="adjustment_tips info"> -> 若遇到不能自动选择播放器模式可尝试点击重置</span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>网页全屏模式解锁</label>
                  <input type="checkbox" id="${selectors.WebfullUnlock.slice(1)}" ${vals.webfull_unlock() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <span class="adjustment_tips info">
                  -> 勾选后网页全屏模式下可以滑动滚动条查看下方评论等内容(番剧播放页不支持)
                  <br>->新增迷你播放器显示,不过比较简陋,只支持暂停/播放操作,有条件的建议还是直接使用浏览器自带的小窗播放功能。</span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>自动选择最高画质</label>
                  <input type="checkbox" id="${selectors.AutoQuality.slice(1)}" ${vals.auto_select_video_highest_quality() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <div class="adjustment_checkboxGroup">
                  <div class="adjustment_checkbox fourK" style="display:${vals.is_vip() ? 'flex' : 'none'}">
                    <span>是否包含4K画质</span>
                    <input type="checkbox" id="${selectors.Quality4K.slice(1)}" ${vals.contain_quality_4k() ? 'checked' : ''} class="adjustment_checkbox">
                  </div>
                  <div class="adjustment_checkbox eightK" style="display:${vals.is_vip() ? 'flex' : 'none'}">
                    <span>是否包含8K画质</span>
                    <input type="checkbox" id="${selectors.Quality8K.slice(1)}" ${vals.contain_quality_8k() ? 'checked' : ''} class="adjustment_checkbox">
                  </div>
                </div>
                <span class="adjustment_tips info"> -> 网络条件好时可以启用此项,勾哪项选哪项,都勾选8k,否则选择4k及8k外最高画质。</span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                    <label>优化视频简介并插入评论区</label>
                    <input type="checkbox" id="${selectors.InsertVideoDescriptionToComment.slice(1)}"
                        ${vals.insert_video_description_to_comment() ? 'checked' : ''}
                        class="adjustment_checkbox">
                </div>
                <span class="adjustment_tips info"> -> 将视频简介内容优化后插入评论区或直接替换原简介区内容(替换原简介中固定格式的静态内容为跳转链接)。</span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>自动跳过时间节点</label>
                  <input type="checkbox" id="${selectors.AutoSkip.slice(1)}" ${vals.auto_skip() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <span class="adjustment_tips info"> -> 自动跳过视频已设置设置时间节点,视频播放到相应时间点时将触发跳转至设定时间点。</span>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>离开页面自动暂停视频</label>
                  <input type="checkbox" id="${selectors.PauseVideo.slice(1)}" ${vals.pause_video() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <div class="adjustment_checkboxGroup">
                  <div class="adjustment_checkbox continuePlay" style="display:${vals.is_vip() ? 'flex' : 'none'}">
                    <span>返回页面恢复播放</span>
                    <input type="checkbox" id="${selectors.ContinuePlay.slice(1)}" ${vals.continue_play() ? 'checked' : ''} class="adjustment_checkbox">
                  </div>
                </div>
              </div>
              <div class="adjustment_form_item">
                <div class="adjustment_form_item_content">
                  <label>自动刷新</label>
                  <input type="checkbox" id="${selectors.AutoReload.slice(1)}" ${vals.auto_reload() ? 'checked' : ''} class="adjustment_checkbox">
                </div>
                <span class="adjustment_tips info"> ->
                  (不建议开启)若脚本执行失败是否自动刷新页面重试,开启后可能会对使用体验起到一定改善作用,但若是因为B站页面改版导致脚本失效,则会陷入页面无限刷新的情况,此时则必须在页面加载时看准时机关闭此项才能恢复正常,请自行选择是否开启。</span>
              </div>
            </div>
            <div class="adjustment_buttonGroup">
              <button id="${selectors.videoSettingSaveButton.slice(1)}" class="adjustment_button primary">保存</button>
            </div>
          </div>`
        if (document.getElementById(selectors.videoSettingPopover)) document.getElementById(selectors.videoSettingPopover).remove()
        const $videoSettingPopover = utils.createElementAndInsert(videoSettingPopoverHtml, document.body, 'append')
        GM_registerMenuCommand('设置', () => {
          $videoSettingPopover.showPopover()
        })
        const $app = vals.player_type() === 'video' ? await utils.getElementAndCheckExistence(selectors.app) : await utils.getElementAndCheckExistence(selectors.bangumiApp)
        const [$IsVip, $AutoLocate, $AutoLocateVideo, $AutoLocateBangumi, $TopOffset, $ClickPlayerAutoLocation, $AutoQuality, $Quality4K, $Quality8K, $Checkbox4K, $Checkbox8K, $WebfullUnlock, $AutoReload, $videoSettingSaveButton, $AutoSkip, $InsertVideoDescriptionToComment, $PauseVideo, $ContinuePlay] = await utils.getElementAndCheckExistence([selectors.IsVip, selectors.AutoLocate, selectors.AutoLocateVideo, selectors.AutoLocateBangumi, selectors.TopOffset, selectors.ClickPlayerAutoLocation, selectors.AutoQuality, selectors.Quality4K, selectors.Quality8K, selectors.Checkbox4K, selectors.Checkbox8K, selectors.WebfullUnlock, selectors.AutoReload, selectors.videoSettingSaveButton, selectors.AutoSkip, selectors.InsertVideoDescriptionToComment, selectors.PauseVideo, selectors.ContinuePlay])
        $videoSettingPopover.addEventListener('toggle', event => {
          if (event.newState === 'open') {
            // document.querySelector('*:not(#videoSettingPopover *)').style.pointerEvents = 'none'
            $app.style.pointerEvents = 'none'
          }
          if (event.newState === 'closed') {
            $app.style.pointerEvents = 'auto'
          }
        })
        $IsVip.addEventListener('change', async event => {
          utils.setValue('is_vip', event.target.checked)
          $Checkbox4K.style.display = event.target.checked ? 'flex' : 'none'
          $Checkbox8K.style.display = event.target.checked ? 'flex' : 'none'
        })
        $AutoLocate.addEventListener('change', event => {
          utils.setValue('auto_locate', event.target.checked)
        })
        $AutoLocateVideo.addEventListener('change', event => {
          utils.setValue('auto_locate_video', event.target.checked)
        })
        $AutoLocateBangumi.addEventListener('change', event => {
          utils.setValue('auto_locate_bangumi', event.target.checked)
        })
        $TopOffset.addEventListener('change', event => {
          utils.setValue('offset_top', +event.target.value)
        })
        $ClickPlayerAutoLocation.addEventListener('change', event => {
          utils.setValue('click_player_auto_locate', event.target.checked)
        })
        $AutoQuality.addEventListener('change', event => {
          utils.setValue('auto_select_video_highest_quality', event.target.checked)
        })
        $Quality4K.addEventListener('change', event => {
          utils.setValue('contain_quality_4k', event.target.checked)
        })
        $Quality8K.addEventListener('change', event => {
          utils.setValue('contain_quality_8k', event.target.checked)
        })
        $WebfullUnlock.addEventListener('change', event => {
          utils.setValue('webfull_unlock', event.target.checked)
        })
        $InsertVideoDescriptionToComment.addEventListener('change', event => {
          utils.setValue('insert_video_description_to_comment', event.target.checked)
        })
        $AutoSkip.addEventListener('change', event => {
          utils.setValue('auto_skip', event.target.checked)
        })
        $PauseVideo.addEventListener('change', event => {
          utils.setValue('pause_video', event.target.checked)
        })
        $ContinuePlay.addEventListener('change', event => {
          utils.setValue('continue_play', event.target.checked)
        })
        $AutoReload.addEventListener('change', event => {
          utils.setValue('auto_reload', event.target.checked)
        })
        await elmGetter.each(selectors.SelectScreenMode, $videoSettingPopover, radioInput => {
          radioInput.addEventListener('click', function () {
            utils.setValue('selected_screen_mode', this.value)
          })
        })
        $videoSettingSaveButton.addEventListener('click', () => {
          $videoSettingPopover.hidePopover()
          utils.reloadCurrentTab(true)
        })
      }
    },
    // #endregion 注册脚本设置选项
    /**
     * 前期准备函数
     * - #region 前期准备函数
     * 提前执行其他脚本功能所依赖的其他函数
     */
    thePrepFunction() {
      if (++vars.thePrepFunctionRunningCount !== 1) return
      utils.initValue()
      utils.clearAllTimersWhenCloseTab()
      modules.registerMenuCommand()
      utils.insertStyleToDocument('BilibiliAdjustmentStyle', styles.BilibiliAdjustment)
      biliApis.autoSignIn()
      if (window.location.href === 'https://www.bilibili.com/') {
        utils.insertStyleToDocument('IndexAdjustmentStyle', styles.IndexAdjustment)
      }
      if (regexps.video.test(window.location.href)) {
        utils.insertStyleToDocument('BodyHiddenStyle', styles.BodyHidden)
        utils.insertStyleToDocument('VideoPageAdjustmentStyle', styles.VideoPageAdjustment)
        utils.insertStyleToDocument('FreezeHeaderAndVideoTitleStyle', styles.FreezeHeaderAndVideoTitle)
        utils.insertStyleToDocument('VideoSettingStyle', styles.VideoSetting)
        modules.observerPlayerDataScreenChanges()
      }
      if (regexps.dynamic.test(window.location.href)) {
        utils.insertStyleToDocument('DynamicSettingStyle', styles.DynamicSetting)
      }

    },
    // #endregion 前期准备函数
    /**
     * 执行主函数
     * - #region 执行主函数
     */
    async theMainFunction() {
      if (++vars.theMainFunctionRunningCount !== 1) return
      if (modules.isLogin()) {
        modules.thePrepFunction()
        const timer = setInterval(async () => {
          const documentHidden = utils.checkDocumentIsHidden()
          if (!documentHidden) {
            clearInterval(timer)
            utils.logger.info('当前标签|已激活|开始应用配置')
            let functionsArray = []
            if (regexps.video.test(window.location.href) || regexps.dynamic.test(window.location.href)) {
              if (regexps.video.test(window.location.href)) {
                functionsArray = [
                  modules.getCurrentPlayerType,
                  modules.checkVideoExistence,
                  modules.checkVideoCanPlayThrough,
                  modules.autoSelectScreenMode,
                  modules.webfullScreenModeUnlock,
                  modules.autoLocationToPlayer,
                  modules.autoCancelMute,
                  modules.autoSelectVideoHighestQuality,
                  modules.clickPlayerAutoLocation,
                  modules.insertFloatSideNavToolsButton,
                  modules.clickVideoTimeAutoLocation,
                  modules.insertVideoDescriptionToComment,
                  modules.insertSetSkipTimeNodesButton,
                  modules.insertSkipTimeNodesSwitchButton,
                  modules.autoSkipTimeNodes,
                  modules.unlockEpisodeSelector
                ]
              }
              if (regexps.dynamic.test(window.location.href)) {
                functionsArray = [
                  modules.changeCurrentUrlToVideoSubmissions
                ]
              }
              if (vals.pause_video()) functionsArray.push(modules.pauseVideoWhenLeavingCurrentPage)
            }
            if (window.location.href === 'https://www.bilibili.com/') {
              functionsArray = [
                modules.insertIndexRecommendVideoHistoryOpenButton,
                modules.setIndexRecordRecommendVideoHistory,
                modules.getIndexRecordRecommendVideoHistory,
                modules.generatorVideoCategories
              ]
            }
            utils.addEventListenerToElement()
            utils.executeFunctionsSequentially(functionsArray)
          } else {
            utils.logger.info('当前标签|未激活|等待激活')
          }
        }, 100)
        arrays.intervalIds.push(timer)
      }
      else utils.logger.warn('请登录|本脚本只能在登录状态下使用')

    }
    // #endregion 执行主函数
    // #endregion 脚本最终执行函数
  }
  modules.theMainFunction()
})();