unlock lpsg videos

Unlock lpsg video access(in thread and in gallery).It also includes other useful features I like, such as hiding plain text posts to skip boring bickering, automatically displaying attached photos, etc. Download and try it if you're interested.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         unlock lpsg videos
// @namespace    MBing
// @version      8.7
// @description  Unlock lpsg video access(in thread and in gallery).It also includes other useful features I like, such as hiding plain text posts to skip boring bickering, automatically displaying attached photos, etc. Download and try it if you're interested.
// @author       MBing
// @connect      cdn-videos.lpsg.com
// @match        https://www.lpsg.com/threads*
// @match        https://www.lpsg.com/gallery*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=lpsg.com
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';

    // ==========================================
    // 第一部分:配置和状态管理
    // ==========================================

    /**
     * CONFIG 对象:集中存放所有固定的数值配置
     * 这样以后要改数字,只需要改这里一处
     */
    //const VIDEO_EXTENSIONS = [ "m4a", "ogv" ];
    const VIDEO_EXTENSIONS = [ "mp4", "mov", "m4v", "webm", "mpeg", "mpg", "m4a", "ogv" ];
    const CONFIG = {

        DEFAULT_VOLUME: 0.5,           // 默认视频音量 50%
        AUTO_SWITCH_INTERVAL: 2500,     // 视频格式自动切换间隔(毫秒)
        AUTO_SWITCH_INTERVAL_SLOW: 5000,// 视频多的时候用更长的间隔
        VIDEO_THRESHOLD: 5,             // 超过这个数量的视频算"多"
        SCROLL_OFFSET: 800,             // 距离底部多少像素时停止滚动
        NEXT_PAGE_DELAY: 1500,          // 自动跳转到下一页前的等待时间
        AUTO_START_DELAY: 2000,         // 页面加载后自动开始滚动的延迟
        VIDEO_WIDTH: '800px',           // 视频显示宽度
        VIDEO_MAX_HEIGHT: '750px'       // 视频最大高度
    };

    /**
     * state 对象:存放运行时会变化的值
     * 用对象包起来是为了让函数内部能修改这些值
     */
    const state = {
        initDone:false,//脚本初始化是否结束
        shouldContinue: true,           // 视频格式自动切换是否继续
        volume: CONFIG.DEFAULT_VOLUME,  // 当前音量
        autoSwitchInterval: CONFIG.AUTO_SWITCH_INTERVAL  // 当前切换间隔
    };

    // 从 localStorage 读取之前保存的音量设置
    // 如果用户之前改过,就用用户的;没改过就用默认的 0.05
    const savedVolume = localStorage.getItem('default-volume-key');
    if (savedVolume !== null) {
        state.volume = Number(savedVolume)/100;
    }

    // ==========================================
    // 第二部分:通用工具函数(各种小帮手)
    // ==========================================

    /**
     * 从 localStorage 读取布尔值
     * @param {string} key - 存储的键名
     * @returns {boolean} - true 或 false
     *
     * 为什么用 'true' 字符串判断?
     * 因为 localStorage 只能存字符串,存 true 会变成 "true"
     */
    function getStorageBool(key) {
        return localStorage.getItem(key) === 'true';
    }

    /**
     * 保存布尔值到 localStorage
     * @param {string} key - 存储的键名
     * @param {boolean} value - 要保存的值
     */
    function setStorageBool(key, value) {
        localStorage.setItem(key, value.toString());
    }

    /**
      * 修改URL的文件扩展名
      * @param {string} url - 原始URL
      * @param {string} newExt - 新扩展名(不含点)
      * @returns {string} - 修改后的URL
      */
    function setUrlFileExtension(url, newExt) {
        // 移除旧的扩展名,添加新的
        // 使用全局 VIDEO_EXTENSIONS 构建正则,添加图片格式
        const extPattern = VIDEO_EXTENSIONS.join('|');
        // 动态构建正则表达式,匹配所有已知扩展名
        return url.replace(new RegExp(`\\.(${extPattern})$`, 'i'), `.${newExt}`);
    }

    /**
      * 用HEAD请求探测视频URL是否存在
      * @param {string} url - 要探测的URL
      * @returns {Promise<boolean>} - 是否存在
      */
    function probeVideoUrl(url) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "HEAD",
                url: url,
                headers: { "Accept": "video/*" },
                onload: (response) => {
                    const isSuccess = response.status >= 200 && response.status < 300;
                    if (isSuccess) {
                        console.log(`链接测试成功${url}`);
                    }
                    resolve(isSuccess);
                },
                onerror: () => resolve(false),
                ontimeout: () => resolve(false)
            });
        });
    }

    /**
     * 异步查找有效的视频URL
      * 先尝试从图片URL构造,逐个格式探测
      * @param {string} previewImgSrc - 预览图片的src
      * @returns {Promise<string|null>} - 有效的视频URL或null
      */
    async function findVideoUrl(previewImgSrc) {
        // 检查URL是否符合预期格式
        if (!previewImgSrc.includes("/attachments/posters/") &&
            !previewImgSrc.includes("/lsvideo/thumbnails/")) {
            console.warn(`预览图片链接不符合规则: ${previewImgSrc}`);
            return null;
        }

        // 构造基础视频URL(替换路径)
        let videoBaseUrl = previewImgSrc
        .replace('/attachments/posters', '/video')
        .replace('/lsvideo/thumbnails', '/lsvideo/videos')
        .replace('.jpg', '.mp4'); // 先给个默认后缀,后面会替换

        // 逐个格式探测
        for (const ext of VIDEO_EXTENSIONS) {
            const potentialVideoUrl = setUrlFileExtension(videoBaseUrl, ext);
            if (await probeVideoUrl(potentialVideoUrl)) {
                return potentialVideoUrl;
            }
        }

        console.error(`找不到对应有效的视频链接: ${previewImgSrc}`);
        return null;
    }

    // ==========================================
    // 第三部分:UI 创建工具(核心封装)
    // ==========================================

    /**
 * 【核心函数】创建通用按钮(无 localStorage)
 * 用于创建各种功能按钮,风格与 createStorageCheckbox 保持一致
 *
 * @param {Object} options - 配置选项
 * @param {string} options.id - 按钮的 ID
 * @param {string} options.text - 按钮显示的文字
 * @param {Function} options.onClick - 点击回调函数
 * @param {string} [options.tip=''] - 悬浮提示文字
 * @param {HTMLElement} [options.container] - 放到哪个容器里(默认是左上角的控制面板)
 * @returns {HTMLButtonElement} - 创建好的按钮元素
 */
    function createActionButton(options) {
        const {
            id,
            text,
            html,      // 【新增】可选,直接传 HTML
            onClick,
            tip = '',
            container = document.getElementById('custom-control-div')
        } = options;

        // -------- 第 1 步:创建 DOM --------

        // 创建包装器 div,与其他控件保持一致的布局
        const wrapper = document.createElement('div');
        wrapper.style.marginLeft = '5px';
        wrapper.style.marginTop = '2px';  // 与其他元素间距一致
        wrapper.style.marginBottom = '7px';  // 底部也加一点,平衡视觉

        // 创建按钮元素
        const button = document.createElement('button');
        button.id = id;
        if (html) {
            button.innerHTML = html;
        } else {
            button.textContent = text;
        }

        // 【修改】按钮样式:更透明背景、fit-content 宽度、左对齐、小字体
        button.style.cssText = `
        display: inline-flex;
        align-items: center;
        gap: 6px;
        background: rgba(255,255,255,0.1);
        color: white;
        border: 1px solid rgba(255,255,255,0.3);
        padding: 1px 5px;
        border-radius: 3px;
        cursor: pointer;
        font-size: 14px;
        font-weight: bold;
        width: fit-content;
        transition: all 0.2s;
    `;


        // 添加提示
        if (tip) {
            button.title = tip;
        }

        // 组装并添加到页面
        wrapper.appendChild(button);
        container.appendChild(wrapper);

        // -------- 第 2 步:绑定事件 --------

        // 悬停效果
        button.addEventListener('mouseenter', () => {
            button.style.background = 'rgba(255,255,255,0.2)';
        });
        button.addEventListener('mouseleave', () => {
            button.style.background = 'rgba(255,255,255,0.1)';
        });

        // 点击事件
        button.addEventListener('click', (e) => {
            if (typeof onClick === 'function') {
                onClick(e, button);
            }
        });

        return button;
    }

    /**
     * 【核心函数】创建带 localStorage 状态的复选框
     *
     * 这个函数替代了原来 10 多个重复的创建函数
     * 统一处理:创建 DOM → 读取状态 → 事件监听 → 保存状态
     *
     * @param {Object} options - 配置选项
     * @param {string} options.id - 复选框的 ID(也是 localStorage 的 key)
     * @param {string} options.labelText - 标签显示的文字
     * @param {boolean} [options.defaultValue=false] - 默认是否勾选(第一次用时的状态)
     * @param {Function} [options.onChange] - 勾选状态变化时的回调函数(可选)
     * @param {HTMLElement} [options.container] - 放到哪个容器里(默认是左上角的控制面板)
     * @param {HTMLElement} [options.insertBefore] - 插入到哪个元素前面(可选,用于调整顺序)
     * @returns {HTMLInputElement} - 创建好的复选框元素
     */
    function createStorageCheckbox(options) {
        // 从 options 对象里解构出各个参数
        // 带 = 号的是默认值,调用时没传就用这个
        const {
            id,                    // 复选框 ID,也是 localStorage 的 key
            labelText,             // 标签文字
            defaultValue = false,  // 默认不勾选
            onChange,              // 变化回调(可选)
            container = document.getElementById('custom-control-div'),  // 默认容器
            insertBefore = null,    // 默认不指定插入位置,就往后追加
            tip = ''  // 提示文字,默认空
        } = options;

        // -------- 第 1 步:创建 DOM 元素 --------

        // 创建一个 div 作为包装器,把复选框和标签包在一起
        const wrapper = document.createElement('div');

        // 创建复选框 input 元素
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';     // 类型是复选框
        checkbox.id = id;               // 设置 ID,用于 label 关联和后续查找
        checkbox.style.marginLeft = '5px';  // 【保留原始样式】左边距 5px

        // 创建标签 label 元素
        const label = document.createElement('label');
        label.htmlFor = id;             // 关联到对应的复选框(点击标签也能 toggle)
        label.textContent = labelText;  // 显示的文字

        //如果有提示文字,添加到 label
        if (tip) {
            label.title = tip;  // 浏览器默认悬浮提示
            checkbox.title = tip; // checkbox 也加上
        }

        // 把复选框和标签装进 wrapper
        wrapper.appendChild(checkbox);
        wrapper.appendChild(label);

        // 决定插入到哪里
        if (insertBefore && insertBefore.parentNode) {
            // 如果指定了 insertBefore,就插在它前面
            insertBefore.parentNode.insertBefore(wrapper, insertBefore);
        } else {
            // 否则追加到 container 末尾
            container.appendChild(wrapper);
        }

        // -------- 第 2 步:初始化状态 --------

        // 从 localStorage 读取之前保存的状态
        const savedState = localStorage.getItem(id);
        const initialState = savedState !== null ? savedState === 'true' : defaultValue;

        // 设置复选框状态
        checkbox.checked = initialState;

        // 如果没有保存过,写入默认值
        if (savedState === null) {
            localStorage.setItem(id, defaultValue.toString());
        }

        // 【新增】初始化时执行一次 onChange
        if (typeof onChange === 'function') {
            onChange(initialState, checkbox);
        }

        // -------- 第 3 步:绑定事件 --------

        // 当用户点击复选框时触发
        checkbox.addEventListener('change', () => {
            // 【关键】读取复选框当前的实际状态(true/false)
            const currentState = checkbox.checked;

            // 把这个状态保存到 localStorage(转成字符串存)
            localStorage.setItem(id, currentState.toString());

            // 如果调用时传了 onChange 回调函数,就执行它
            // 把当前状态和复选框元素都传过去,方便外部使用
            if (typeof onChange === 'function') {
                onChange(currentState, checkbox);
            }
        });

        // 返回创建好的复选框元素,外部可以用变量接收后操作
        return checkbox;
    }

    /**
 * 【核心函数】创建带 localStorage 的文本输入框
 *
 * 用于:音量设置、滚动速度、图片最大宽度等需要输入数字的场景
 *
 * @param {Object} options - 配置选项
 * @param {string} options.id - 输入框 ID,也是 localStorage 的 key
 * @param {string} options.labelText - 标签文字
 * @param {string} options.defaultValue - 默认值(字符串)
 * @param {Function} options.validator - 验证函数,接收输入值,返回 true/false
 * @param {Function} [options.onValid] - 验证通过后的回调(可选)
 * @returns {HTMLInputElement} - 创建好的输入框元素
 */
    function createStorageInput(options) {
        const {
            id,
            labelText,
            defaultValue,
            validator,      // 验证函数:判断输入是否合法
            onValid,         // 验证通过后的回调(可选)
            tip = ''//悬浮提示
        } = options;

        // -------- 第 1 步:创建 DOM --------

        const wrapper = document.createElement('div');
        // 添加 flex 布局,让 checkbox 和 label-input 在同一行
        wrapper.style.display = 'flex';
        wrapper.style.alignItems = 'center';

        // 【新增】创建控制复选框
        const checkboxId = `checkbox-${id}`;
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.id = checkboxId;
        checkbox.style.marginLeft = '5px';

        // 创建标签
        const label = document.createElement('label');
        label.textContent = labelText;
        //label.style.marginLeft = '5px';  // 【保留原始样式】

        // 创建输入框
        const input = document.createElement('input');
        input.id = id;
        input.type = 'text';            // 文本输入
        input.style.width = '60px';
        input.style.textOverflow = 'ellipsis';   // 超出显示省略号...
        input.style.marginLeft = '5px';
        input.style.background = 'transparent';      // 透明背景
        input.style.border = 'none';                 // 无边框
        input.style.borderBottom = '1px solid #fff'; // 底部白线
        input.style.outline = 'none';                // 无焦点边框
        input.style.color = '#4f4e4e';               // 深灰色文字
        input.style.fontWeight = 'bold';
        input.style.textAlign = 'center';            // 文字居中

        // 添加提示
        if (tip) {
            checkbox.title = tip;
            label.title = tip;
            input.title = tip;
        }

        // 组装并添加到页面
        wrapper.appendChild(checkbox);  // 【新增】先加 checkbox
        wrapper.appendChild(label);
        wrapper.appendChild(input);
        document.getElementById('custom-control-div').appendChild(wrapper);

        // -------- 第 2 步:绑定事件 --------

        // 【新增】复选框 change 事件
        checkbox.addEventListener('change', () => {
            // 保存复选框状态到 localStorage
            localStorage.setItem(checkboxId, checkbox.checked.toString());

            // 触发 input 的 change 事件
            input.dispatchEvent(new Event('change', {
                bubbles: true,
                cancelable: true
            }));
        });

        // 当输入框内容变化且失去焦点时触发(比 input 事件更适合)
        input.addEventListener('change', () => {
            // 去掉首尾空格
            const value = input.value.trim();

            // 用传入的验证函数检查输入是否合法
            if (validator(value)) {
                // 合法:保存到 localStorage
                localStorage.setItem(id, value);
                // 执行验证通过后的回调(如果有)
                if (typeof onValid === 'function') {
                    onValid(value, input);
                }
            } else {
                // 不合法:恢复默认值
                localStorage.setItem(id, defaultValue);
                input.value = defaultValue;
            }
        });

        // -------- 第 3 步:初始化 --------

        // 【新增】初始化复选框状态
        const savedCheckboxState = localStorage.getItem(checkboxId);
        checkbox.checked = savedCheckboxState !== null ? savedCheckboxState === 'true' : false;
        // 如果没有保存过,写入默认 false(未勾选)
        if (savedCheckboxState === null) {
            localStorage.setItem(checkboxId, 'false');
        };

        // 从 localStorage 读取值,没有就用默认值
        input.value = localStorage.getItem(id) || defaultValue;
        // 主动触发一次 change 事件,让验证和 onValid 执行
        // 这样页面加载时就能应用保存的设置
        input.dispatchEvent(new Event('change', {
            bubbles: true,      // 事件冒泡
            cancelable: true    // 可以取消
        }));

        return input;
    }

    /**
 * 创建黄色标题栏标签
 * 用于分组显示:Video Setting、User Info Area 等
 * @param {string} text - 显示的文字
 * @param {string} [color='#e8d68e'] - 标签颜色(可选,默认黄色)
 */
    function createSectionLabel(text, color = '#e8d68e') {
        const wrapper = document.createElement('div');

        const label = document.createElement('label');
        label.textContent = text;
        label.style.color = color;  // 使用传入的颜色或默认值

        wrapper.appendChild(label);
        document.getElementById('custom-control-div').appendChild(wrapper);
    }

    // ==========================================
    // 第四部分:具体功能实现(调用上面的工具函数)
    // ==========================================


    /**
 * 创建 TTS 优化按钮
 */
    function createTTSOptimizeButton() {
        createActionButton({
            id: 'tts-optimize-btn',
            html: `
<svg width="14" height="14" viewBox="2 2 20 20" fill="none" stroke="currentColor" stroke-width="2.5">
    <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
</svg>
    <span>Optimize for TTS</span>
`,
            tip: 'Replace words that TTS struggles with (eh, uh, lol, etc.)',
            onClick: () => {
                const result = optimizeForTTS();
                if (result.totalReplacements === 0) {
                    alert('No TTS-unfriendly words found to replace.');
                } else {
                    // 显示结果弹窗
                    showTTSResultModal(result);
                }
            }
        });
    }

    /**
     * 创建左上角的控制面板
     * 点击图标按钮切换显示/隐藏,附带关闭按钮
     */
    function createControlDiv() {
        // 外层容器:保留你原本的样式
        const foldableDiv = document.createElement('div');
        foldableDiv.id = 'foldable-div';
        foldableDiv.style.cssText = `
        position: fixed;
        top: 5px;
        left: 5px;
        max-width: 300px;
        z-index: 999;
        padding: 8px;
        border: 1px solid #ccc;
        border-radius: 5px;
        font-size: 14px;
        font-weight: bold;
        background-color: #6ba65e;
        color: white;
    `;

        // 【新增】按钮容器:放图标按钮和关闭按钮
        const btnContainer = document.createElement('div');
        btnContainer.style.cssText = `
        display: flex;
        align-items: center;
        justify-content: space-between;
    `;

        // 【新增】图标按钮(替代原来的文字提示)
        const toggleBtn = document.createElement('button');
        toggleBtn.id = 'control-toggle-btn';
        toggleBtn.title = 'Extra Enhancements';
        toggleBtn.style.cssText = `
        background: transparent;
        border: none;
        color: white;
        cursor: pointer;
        padding: 0;
        display: flex;
        align-items: center;
    `;

        // SVG 工具图标
        toggleBtn.innerHTML = `
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <path d="M3 12h18M3 6h18M3 18h18"/>
        </svg>
    `;

        // 【新增】关闭按钮(展开时显示)
        const closeBtn = document.createElement('button');
        closeBtn.id = 'control-close-btn';
        closeBtn.style.cssText = `
        background: transparent;
        border: none;
        color: white;
        cursor: pointer;
        padding: 0;
        display: none;  /* 默认隐藏 */
        align-items: center;
        margin-left: 8px;
    `;

        // SVG 关闭图标(X)
        closeBtn.innerHTML = `
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
            <line x1="18" y1="6" x2="6" y2="18"></line>
            <line x1="6" y1="6" x2="18" y2="18"></line>
        </svg>
    `;

        // 组装按钮容器
        btnContainer.appendChild(toggleBtn);
        btnContainer.appendChild(closeBtn);
        foldableDiv.appendChild(btnContainer);

        // 内部容器:保留你原本的样式,默认隐藏
        const controlDiv = document.createElement('div');
        controlDiv.id = 'custom-control-div';
        controlDiv.style.display = 'none';
        controlDiv.style.marginTop = '8px';  // 和按钮间距
        foldableDiv.appendChild(controlDiv);

        // 添加到页面
        document.body.appendChild(foldableDiv);

        // 【修改】点击图标按钮切换显示
        let isExpanded = false;

        toggleBtn.addEventListener('click', () => {
            isExpanded = !isExpanded;

            if (isExpanded) {
                controlDiv.style.display = 'block';
                closeBtn.style.display = 'flex';  // 显示关闭按钮
            } else {
                controlDiv.style.display = 'none';
                closeBtn.style.display = 'none';  // 隐藏关闭按钮
            }
        });

        // 【新增】点击关闭按钮直接关闭
        closeBtn.addEventListener('click', () => {
            isExpanded = false;
            controlDiv.style.display = 'none';
            closeBtn.style.display = 'none';
        });

        // 点击页面其他位置收起菜单
        document.addEventListener('click', (e) => {
            // 如果菜单未展开,不处理
            if (!isExpanded) return;

            // 如果点击的是菜单内部(foldableDiv 或其子元素),不处理
            if (foldableDiv.contains(e.target)) return;

            // 否则收起菜单
            isExpanded = false;
            controlDiv.style.display = 'none';
            closeBtn.style.display = 'none';
        });

    }



    /**
     * 创建音量输入框
     * 范围 0-1,比如 0.1 表示 10% 音量
     */
    function createVolumeInput() {
        const inputId = 'default-volume-key';  // 外部变量
        createStorageInput({
            id: inputId,           // 【统一】ID 即 storageKey
            labelText: 'Default Volume %:',
            defaultValue: '10',
            // 验证:必须是数字,且在 0-100 之间
            validator: (v) => !isNaN(v) && Number(v) >= 0 && Number(v) <= 100 && v!=="",
            // 验证通过后:更新全局音量状态
            onValid: (v) => {
                if(getStorageBool(`checkbox-${inputId}`)){
                    state.volume = Number(v)/100;
                }else{
                    state.volume=CONFIG.DEFAULT_VOLUME;
                }
            },
            tip: 'The default volume of all videos on this page, enter a number between 0 and 100.'
        });
    }

    /**
     * 创建滚动速度输入框
     * 必须是正数
     */
    function createScrollSpeedInput() {
        createStorageInput({
            id: 'scroll-speed',
            labelText: 'Scroll Speed:',
            defaultValue: '5',
            validator: (v) => !isNaN(v) && Number(v) > 0,
            tip: 'Control the automatic scrolling speed of the page.'
        });
    }

    /**
     * 创建图片最大宽度输入框
     * 空字符串表示不限制,或者输入正数
     */
    function createMaxWidthInput() {
        const inputId = 'max-img-width';  // 外部变量

        createStorageInput({
            id: inputId,
            labelText: 'Pic Max Width:',
            defaultValue: '',
            tip: 'Control the maximum width of images on the page.',
            // 验证:空字符串 或 正数
            validator: (v) => v === '' || (!isNaN(v) && Number(v) > 0),
            // 验证通过后:应用到所有图片
            onValid: (v) => {
                const images = document.querySelectorAll(
                    '.message-cell.message-cell--main img:not(.smilie)'
                );
                images.forEach(img => {
                    // 如果有值就设置 maxWidth,否则清空
                    img.style.maxWidth = v&&getStorageBool(`checkbox-${inputId}`) ? `${v}px` : '';
                    //img.style.width = v ? `100%` : '';
                });
            }
        });
    }

    /**
     * 创建"自动滚动"复选框
     * 控制页面加载后是否自动开始滚动
     */
    function createAutoscrollCheckbox() {
        createStorageCheckbox({
            id: 'autoscroll-key',
            labelText: 'Auto Scroll On Load',
            defaultValue: false,
            tip: 'The page will automatically scroll down after it finishes loading.'
        });
    }

    /**
     * 创建"滚动到底自动跳转下一页"复选框
     */
    function createAutoscrollJumpCheckbox() {
        createStorageCheckbox({
            id: 'autoscroll-jump-key',
            labelText: 'Auto Jump To Next Page',
            defaultValue: false,
            tip: 'Automatically switch to the next page when scrolling to the bottom of the page.'
        });
    }

    /**
     * 创建"向后翻页"复选框
     * 勾选后自动跳转会变成向前翻(看历史内容)
     */
    function createAutoJumpBackwardCheckbox() {
        createStorageCheckbox({
            id: 'auto-jump-backward',
            labelText: 'Jump To Prev Page',
            defaultValue: false,
            tip: 'Control whether the page navigates to the previous or next page.'
        });
    }

    /**
     * 创建"自动跳过纯文字页面"复选框
     * 如果页面没有图片/视频,自动跳转到下一页
     */
    function createAutoJumpTextOnlyCheckbox() {
        const checkbox = createStorageCheckbox({
            id: 'auto-jump-text-only-page',
            labelText: 'Autoskip Textonly Pages',
            defaultValue: false,
            tip: 'Automatically redirect to the next page when the page contains only text.'
        });

        // 【保留原始样式】设置容器 ID 和左边距
        checkbox.parentElement.id = 'auto-jump-text-only-page-container';
        checkbox.parentElement.style.marginLeft = '10px';
        checkbox.parentElement.style.display = getStorageBool( 'hide-text-only-post') ? '' : 'none';
    }

    /**
 * 创建"显示自动滚动按钮"复选框
 * 用于控制右下角悬浮滚动按钮的显示/隐藏
 */
    function createShowScrollButtonCheckbox() {
        createStorageCheckbox({
            id: 'show-scroll-button',
            labelText: 'Show Scroll Button',
            defaultValue: false,
            tip: 'Display the button for automatic scroll down.',
            onChange: (checked) => {
                const btn = document.getElementById('auto-scroll-btn');
                if (!btn) return;

                // 控制按钮显示/隐藏
                btn.style.display = checked ? 'flex' : 'none';
            }
        });
    }

    /**
     * 创建"隐藏纯文字帖子"复选框
     * 把没有图片/视频的帖子隐藏掉,只看有料的
     */
    function createHideTextOnlyCheckbox() {
        createStorageCheckbox({
            id: 'hide-text-only-post',
            labelText: 'Hide Text-Only Post',
            defaultValue: false,
            tip: 'Hide posts containing only text.',
            // 变化时:勾选就隐藏,取消就显示
            onChange: (checked) => {
                if (checked) {
                    // 【互斥】取消用户筛选
                    const filterCheckbox = document.getElementById('checkbox-filter-by-user');
                    if (filterCheckbox && filterCheckbox.checked) {
                        filterCheckbox.click();  // 模拟点击取消勾选
                    }
                    hideTextOnlyPosts();
                } else {
                    if(state.initDone){//在初始化的时候,如果没有勾选,不需要执行showall
                        showAllPosts();
                    }
                }
            },
        });
    }

    /**
     * 创建"强制在新窗口打开"复选框
     * 在gallery浏览页面把链接改成强制新窗口打开
     */
    function createForceNewWindowCheckbox() {
        createStorageCheckbox({
            id: 'force-new-window',
            labelText: 'Force Open In New Window',
            defaultValue: false,
            tip: 'Force links in media page to open in new window.',
            onChange: (checked) => {
                if (checked) {
                    state.initDone && rewriteGalleryLinks();
                } else {
                    state.initDone && location.reload();
                }
            },
        });
    }

    /**
     * 创建"隐藏用户信息"复选框
     * 隐藏头像、等级等,页面更清爽
     */
    function createHideUserInfoCheckbox() {
        createStorageCheckbox({
            id: 'hide-user-info',
            labelText: 'Hide Extra User Info',
            defaultValue: false,
            tip: 'Hide user details.',
            onChange: (checked) => {
                // 找到所有用户信息区域,显示或隐藏
                document.querySelectorAll('.message-userExtras').forEach(el => {
                    el.style.display = checked ? 'none' : '';
                });
            }
        });
    }

    /**
     * 创建"每行一张图片"复选框
     * 勾选后图片独占一行,不并排显示
     */
    function createOnePicPerLineCheckbox() {
        // 两种显示模式
        const DISPLAY = {
            ONE: 'block',        // 独占一行
            MULTI: 'inline-block' // 并排显示
        };

        // 需要修改的图片选择器
        const IMG_SELECTORS = '.bbImageWrapper, .inserted-img, .bbImage, .bbMediaWrapper';

        createStorageCheckbox({
            id: 'one-pic-per-line',
            labelText: 'One Pic Per Line',
            defaultValue: false,
            tip: 'Only one photo can be displayed per line.',
            onChange: (checked) => {
                const display = checked ? DISPLAY.ONE : DISPLAY.MULTI;
                document.querySelectorAll(IMG_SELECTORS).forEach(el => {
                    el.style.display = display;
                });
            }
        });
    }

    /**
     * 创建"只看某用户帖子"输入框
     * 输入用户名,只显示该用户的帖子,空则显示全部
     */
    function createFilterByUserInput() {
        const inputId = 'filter-by-user';  // 外部变量

        createStorageInput({
            id: inputId,
            labelText: 'Filter Post By User:',
            defaultValue: '',
            tip: 'Enter username to show only their posts. Leave empty to show all.',
            // 验证:任意字符串都合法(包括空字符串)
            validator: (v) => true,
            // 验证通过后:应用过滤
            onValid: (v) => {
                // 【互斥】如果勾选了筛选,取消隐藏纯文字
                const userFilterCheckbox = document.getElementById(`checkbox-${inputId}`);
                const hideTextCheckbox = document.getElementById('hide-text-only-post');
                if (hideTextCheckbox && hideTextCheckbox.checked && userFilterCheckbox && userFilterCheckbox.checked) {
                    hideTextCheckbox.click();  // 模拟点击取消勾选
                }

                const username = userFilterCheckbox?.checked ? v.trim() : "";
                //初始化结束后,无论有没勾选都调用,初始化没结束,只有勾选才调用(因为如果调用,就会覆盖掉hide-text-only的操作)
                if(state.initDone||(!state.initDone && userFilterCheckbox?.checked)){
                    filterPostsByUser(username);
                }
            }
        });
    }

    /**
     * 创建"放大附件图片"复选框
     * 把附件里的小图变成大图直接显示
     */
    function createEnlargeAttachmentCheckbox() {
        createStorageCheckbox({
            id: 'enlarge-attachment-pics',
            labelText: 'Enlarge Attachment Pics',
            defaultValue: false,
            tip: 'Enlarge the images in the attachment.',
            onChange: (checked) => {
                if (checked) {
                    displayBigPreview();
                } else {
                    hideBigPreview();
                }
            }
        });
    }

    /**
     * 创建"视频网页内全屏"复选框
     * 播放时视频占满网页可视区,暂停/结束时恢复
     */
    function createVideoAutoFullscreenCheckbox() {
        createStorageCheckbox({
            id: 'video-auto-fullscreen',
            labelText: 'Auto Fullscreen On Play',
            defaultValue: false,
            tip: 'Automatically full screen when playing videos',
            onChange: (checked) => {
                bindVideoFullscreenEvents(checked);
            }
        });
    }

    /**
 * 创建文本提取按钮
 * 【修改】使用新的 createActionButton 函数创建
 * 放在 User Related 分组下
 */
    function createTextExtractButton() {
        createActionButton({
            id: 'text-extract-btn',
            html: `
    <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="flex-shrink: 0;">
        <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
        <polyline points="14 2 14 8 20 8"/>
        <line x1="16" y1="13" x2="8" y2="13"/>
        <line x1="16" y1="17" x2="8" y2="17"/>
    </svg>
    <span>Extract Text Posts</span>
`,
            tip: 'Extract text content from posts. Respects user filter if enabled.',
            onClick: () => {
                const text = extractPostsText();
                if (text) {
                    showExtractOptions(text);
                }
            }
        });
    }

    // ==========================================
    // 第五部分:具体业务逻辑函数
    // ==========================================
    // ==========================================
    // 新增:首次使用引导弹窗
    // ==========================================

    /**
 * 显示一次性欢迎弹窗
 * 使用 localStorage 记录是否已显示
 */
    function showFirstTimeWelcome() {
        const WELCOME_KEY = 'lpsg-script-welcome-shown';

        // 检查是否已显示过
        if (localStorage.getItem(WELCOME_KEY) === 'true') {
            return;  // 已显示过,直接返回
        }

        // 创建弹窗
        const modal = document.createElement('div');
        modal.id = 'first-time-welcome';
        modal.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: #6ba65e;
        color: white;
        padding: 24px;
        border-radius: 8px;
        box-shadow: 0 8px 32px rgba(0,0,0,0.4);
        z-index: 100001;
        max-width: 500px;
        width: 85%;
        font-family: inherit;
        text-align: center;
    `;

        modal.innerHTML = `
        <div style="font-size: 32px; margin-bottom: 12px;">👋</div>
        <h3 style="margin: 0 0 12px 0; font-size: 18px;">Hi! This user script dose more than just unlocking video access</h3>
        <p style="margin: 0 0 20px 0; font-size: 14px; line-height: 1.5; opacity: 0.95;">
            Click the <b>top-left button</b> to explore extra features:<br>
            better pic display, auto-scroll, hide useless posts, and more.<br>
            (This message will only be displayed once.)
        </p>
        <button id="welcome-got-it" style="
            background: white;
            color: #6ba65e;
            border: none;
            padding: 10px 24px;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
            font-size: 14px;
        ">Got it!</button>
    `;

        // 遮罩层
        const overlay = document.createElement('div');
        overlay.style.cssText = `
        position: fixed;
        top: 0; left: 0; width: 100%; height: 100%;
        background: rgba(0,0,0,0.5);
        z-index: 100000;
    `;

        // 关闭并记录
        const closeWelcome = () => {
            modal.remove();
            overlay.remove();
            localStorage.setItem(WELCOME_KEY, 'true');
        };

        modal.querySelector('#welcome-got-it').addEventListener('click', closeWelcome);
        overlay.addEventListener('click', closeWelcome);

        document.body.appendChild(overlay);
        document.body.appendChild(modal);
    }

    /**
 * 绑定或解绑视频全屏事件(事件委托版本)
 * 监听 document 上的 play/ended 事件,自动处理动态插入的视频
 */
    function bindVideoFullscreenEvents(enable) {
        // 移除旧的委托监听(防止重复绑定)
        document.removeEventListener('play', handleDelegatedPlay, true);
        document.removeEventListener('ended', handleDelegatedEnded, true);

        if (enable) {
            // 使用捕获阶段监听,确保在视频自己的监听器之前执行
            document.addEventListener('play', handleDelegatedPlay, true);
            document.addEventListener('ended', handleDelegatedEnded, true);
        }
    }

    /**
 * 委托的 play 事件处理器
 * 检查事件源是否是视频元素,且是我们替换的视频
 */
    function handleDelegatedPlay(e) {
        // 确保是 video 元素,且开启了自动全屏功能
        if (e.target.tagName !== 'VIDEO') return;

        // 检查全局开关(从 localStorage 实时读取)
        if (!getStorageBool('video-auto-fullscreen')) return;

        // 调用原有的全屏处理逻辑
        handleVideoPlay(e);
    }

    /**
 * 委托的 ended 事件处理器
 */
    function handleDelegatedEnded(e) {
        if (e.target.tagName !== 'VIDEO') return;

        // 检查是否是全屏状态的视频
        if (e.target.classList.contains('video-page-fullscreen')) {
            handleVideoEnded(e);
        }
    }

    /**
     * 进入网页内全屏(带独立退出按钮)
     */
    function handleVideoPlay(e) {
        const video = e.target;
        video.dataset.ready = 'true';//开始播放就把video改成ready状态
        if (video.classList.contains('video-page-fullscreen')) return;

        // 保存原始样式
        video._originalStyle = {
            position: video.style.position,
            top: video.style.top,
            left: video.style.left,
            width: video.style.width,
            height: video.style.height,
            zIndex: video.style.zIndex,
            maxWidth: video.style.maxWidth,
            maxHeight: video.style.maxHeight
        };

        // 【新增】创建退出按钮
        const exitBtn = document.createElement('button');
        exitBtn.textContent = '✕';
        exitBtn.id = 'video-exit-fullscreen-btn';
        exitBtn.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        z-index: 1000000;
        padding: 8px 16px;
        background: rgba(0,0,0,0.7);
        color: white;
        border: 1px solid white;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
    `;

        exitBtn.onclick = () => {
            video.pause();
            exitVideoFullscreen(video);
        };

        document.body.appendChild(exitBtn);

        // 应用全屏样式(原来的代码)
        video.classList.add('video-page-fullscreen');
        video.style.cssText = `
        position: fixed !important;
        top: 0 !important;
        left: 0 !important;
        width: 100vw !important;
        height: 100vh !important;
        z-index: 999999 !important;
        max-width: none !important;
        max-height: none !important;
        object-fit: contain !important;
        background: black !important;
    `;

        // 【删除】原来的 showFullscreenTip 调用
    }


    /**
     * 播放结束退出全屏
     */
    function handleVideoEnded(e) {
        const video = e.target;
        if (video.classList.contains('video-page-fullscreen')) {
            exitVideoFullscreen(video);
        }
    }


    /**
     * 退出网页内全屏
     */
    function exitVideoFullscreen(video) {
        if (!video._originalStyle) return;

        // 【新增】移除退出按钮
        const exitBtn = document.getElementById('video-exit-fullscreen-btn');
        if (exitBtn) exitBtn.remove();

        video.classList.remove('video-page-fullscreen');

        // 【修改】恢复原始样式(简化写法)
        Object.assign(video.style, video._originalStyle);
        delete video._originalStyle;

        // 【删除】原来的 hideFullscreenTip 调用
    }

    /**
     * 显示附件大图
     * 在附件区域直接插入大图,不用点击预览
     */
    function displayBigPreview() {
        // 先检查是否已经有插入过的大图了
        const existing = document.querySelectorAll('img.inserted-img');

        if (existing.length > 0) {
            // 有的话,显示出来就行
            existing.forEach(img => img.style.display = '');
        } else {
            // 没有的话,需要创建大图插入
            document.querySelectorAll('section.message-attachments').forEach(section => {
                section.querySelectorAll('a.file-preview.js-lbImage').forEach(link => {
                    const img = document.createElement('img');
                    img.src = link.href;        // 用原图链接
                    img.alt = 'Inserted Image';

                    // 如果设置了最大宽度,应用上去
                    const maxWidth = localStorage.getItem('max-img-width');
                    if (maxWidth) {
                        img.style.maxWidth = `${maxWidth}px`;
                    }

                    img.style.height = 'auto';
                    img.style.margin = '5px';
                    if (getStorageBool('one-pic-per-line')) img.style.display = 'block';
                    img.classList.add('inserted-img');  // 标记类名,方便后续操作
                    section.appendChild(img);
                });
            });
        }

        // 隐藏原来的附件列表(因为已经直接显示大图了)
        document.querySelectorAll('ul.attachmentList').forEach(list => {
            // 检查是否有非图片附件(比如 zip、pdf)
            // 如果有,不能隐藏列表,否则下载不了
            const hasNonImage = Array.from(list.querySelectorAll('a')).some(a =>
                                                                            a.className === 'file-preview'
                                                                           );

            if (!hasNonImage) {
                list.style.display = 'none';
            }
        });
    }

    /**
     * 隐藏附件大图,恢复原始列表
     */
    function hideBigPreview() {
        // 隐藏所有插入的大图
        document.querySelectorAll('img.inserted-img').forEach(img => {
            img.style.display = 'none';
        });

        // 显示原始附件列表
        document.querySelectorAll('ul.attachmentList').forEach(list => {
            list.style.display = '';
        });
    }
    /**
 * 根据用户名过滤帖子显示
 * @param {string} username - 要显示的用户名,空字符串表示显示全部
 */
    function filterPostsByUser(username) {
        const articles = document.querySelectorAll(
            'article.message.message--post.js-post.js-inlineModContainer'
        );

        articles.forEach(article => {
            // 查找用户名元素
            const nameElement = article.querySelector('.message-name .username ');

            if (!nameElement) {
                // 找不到用户名,默认显示
                article.style.display = '';
                return;
            }

            const postUsername = nameElement.textContent.trim();

            if (username === '') {
                // 空字符串,显示全部
                article.style.display = '';
            } else {
                // 非空,只匹配对应用户(不区分大小写)
                const match = postUsername.toLowerCase() === username.toLowerCase();
                if(match){
                    article.style.display = '';
                }else{
                    article.style.display = 'none';
                }
            }
        });

        console.log(username === '' ? '显示全部帖子' : `只显示用户 "${username}" 的帖子`);
    }

    /**
     * 隐藏纯文字帖子(没有图片、视频、链接的帖子)
     */
    function hideTextOnlyPosts() {
        // 需要显示/隐藏的元素 ID 列表
        const containerIds = [
            'auto-jump-text-only-page-container'
        ];
        containerIds.forEach(id => {
            const el = document.getElementById(id);
            if (el) el.style.display = '';
        });


        // 获取所有帖子
        const articles = document.querySelectorAll(
            'article.message.message--post.js-post.js-inlineModContainer'
        );

        let textPostCount = 0;      // 纯文字帖子数
        let nonTextPostCount = 0;   // 有图片/视频的帖子数

        articles.forEach(article => {

            // 找到帖子内容区
            const originalContent = article.querySelector('.message-content.js-messageContent');
            if (!originalContent) return;

            // 克隆一份内容,用于检查(避免修改原 DOM)
            const content = originalContent.cloneNode(true);

            // 去掉引用和签名,这些不算内容
            content.querySelectorAll('blockquote, aside.message-signature').forEach(el => el.remove());

            // 检查视频元素
            const hasVideo = content.querySelector('video') !== null ;
            const hasVideoProcessing = content.querySelector('.thap-processing-placeholder') !== null;
            const hasIframe = content.querySelector('iframe') !== null ;

            //检查链接元素(排除用户名链接)
            const hasLink = Array.from(content.querySelectorAll('a')).some(a =>
                !a.classList.contains('username')
            );

            // 检查是否有非表情图片(表情不算内容)
            const hasValidImage = Array.from(content.querySelectorAll('img')).some(img =>
                                                                                   !img.className.includes('smilie')
                                                                                  ) ;

            // 判断:没有视频、没有处理中的视频、没有有效图片、没有 iframe、没有链接
            if (!hasVideo && !hasVideoProcessing && !hasValidImage && !hasIframe && !hasLink) {
                textPostCount++;
                article.style.display = 'none';// 隐藏纯文字帖子
            } else {
                article.style.display = '';//显示帖子
                nonTextPostCount++;
            }
        });

        // 如果当前页面全是纯文字,且勾选了"自动跳过",就跳转到下一页
        if (nonTextPostCount === 0 && getStorageBool('auto-jump-text-only-page')) {
            setTimeout(clickNextPage, CONFIG.NEXT_PAGE_DELAY);
        }

        console.log(`隐藏了${textPostCount}篇纯文字帖子`);
    }

    /**
     * 显示所有帖子(取消隐藏)
     */
    function showAllPosts() {
        // 需要显示/隐藏的元素 ID 列表
        const containerIds = [
            'auto-jump-text-only-page-container'
        ];
        containerIds.forEach(id => {
            const el = document.getElementById(id);
            if (el) el.style.display = 'none';
        });

        // 显示所有帖子
        document.querySelectorAll(
            'article.message.message--post.js-post.js-inlineModContainer'
        ).forEach(article => {
            article.style.display = '';
        });
    }

    /**
     * 点击下一页按钮(或上一页)
     * 根据"向后翻页"复选框决定方向
     */
    function clickNextPage() {

        // 检查是否向后翻页
        const isBackward = getStorageBool( 'auto-jump-backward');

        // 根据方向选择对应的选择器
        const selector = isBackward
        ? 'a.pageNav-jump.pageNav-jump--prev'   // 上一页
        : 'a.pageNav-jump.pageNav-jump--next';  // 下一页

        const nextLink = document.querySelector(selector);

        if (nextLink) {
            console.log('自动点击下一页按钮');
            nextLink.click();
        } else {
            console.warn('未找到下一页按钮');
        }
    }

    // ==========================================
    // 新增:TTS 优化功能模块
    // ==========================================

    /**
 * TTS 替换词典
 * 键:原文本(支持正则)
 * 值:替换后的 TTS 友好文本
 */
    const TTS_REPLACEMENTS = {
        // 语气词(不区分大小写)
        '\\beh\\b': 'er',           // eh → er (更自然的停顿)
        '\\buh\\b': 'uhh',          // uh → uhh
        '\\bah\\b': 'ahh',          // ah → ahh
        '\\boh\\b': 'ohh',          // oh → ohh
        '\\bmm\\b': 'hmm',          // mm → hmm
        '\\bmmm\\b': 'hmm',         // mmm → hmm
        '\\bhuh\\b': 'huhh',        // huh → huhh
        '\\bheh\\b': 'heh',         // heh → hey (轻笑)
        '\\bha\\b': 'hah',          // ha → hah (笑声)

        // 重复字母(如 sooooo → so)
        '\\b(so+)\\b': 'so',
        '\\b(no+)\\b': 'no',
        '\\b(yes+)\\b': 'yes',
        '\\b(yeah+)\\b': 'yeah',

        // 网络缩写(可选,按需启用)
        '\\blol\\b': 'laughing out loud',
        '\\blmao\\b': 'laughing my ass off',
        '\\brofl\\b': 'rolling on the floor laughing',
        '\\bbtw\\b': 'by the way',
        '\\bimo\\b': 'in my opinion',
        '\\bimho\\b': 'in my humble opinion',
        '\\btbh\\b': 'to be honest',
        '\\btbf\\b': 'to be fair',
        '\\bidk\\b': 'I do not know',
        '\\bikr\\b': 'I know right',
        '\\bnvm\\b': 'never mind',
        '\\bftw\\b': 'for the win',
        '\\bffs\\b': 'for fucks sake',
        '\\bsmh\\b': 'shaking my head',
        '\\btho\\b': 'though',
        '\\bcuz\\b': 'because',
        '\\bcos\\b': 'because',

        // 拟声词优化
        '\\bshh+\\b': 'shush',
        '\\bwoah\\b': 'whoa',
        '\\bwooo+\\b': 'woo',
        '\\byay\\b': 'yay',
        '\\bwhew\\b': 'few',
        '\\bphew\\b': 'few',
    };

    /**
 * 执行 TTS 优化替换
 * 遍历所有帖子正文,替换 TTS 不友好词汇
 */
    function optimizeForTTS() {
        const articles = document.querySelectorAll(
            'article.message.message--post.js-post.js-inlineModContainer'
        );

        let modifiedCount = 0;
        let totalReplacements = 0;

        articles.forEach(article => {
            // 跳过隐藏的帖子
            if (article.style.display === 'none') return;

            const bodyElement = article.querySelector('.message-body.js-selectToQuote');
            if (!bodyElement) return;

            // 获取所有文本节点(排除代码块、引用等)
            const walker = document.createTreeWalker(
                bodyElement,
                NodeFilter.SHOW_TEXT,
                {
                    acceptNode: (node) => {
                        // 跳过代码块、引用内的文本
                        const parent = node.parentElement;
                        if (parent.closest('code, pre, blockquote, .bbCodeBlock')) {
                            return NodeFilter.FILTER_REJECT;
                        }
                        return NodeFilter.FILTER_ACCEPT;
                    }
                }
            );

            const textNodes = [];
            let node;
            while (node = walker.nextNode()) {
                textNodes.push(node);
            }

            // 执行替换
            let nodeModified = false;
            textNodes.forEach(textNode => {
                let originalText = textNode.textContent;
                let modifiedText = originalText;
                let nodeReplacements = 0;

                Object.entries(TTS_REPLACEMENTS).forEach(([pattern, replacement]) => {
                    const regex = new RegExp(pattern, 'gi');
                    const matches = modifiedText.match(regex);
                    if (matches) {
                        nodeReplacements += matches.length;
                        modifiedText = modifiedText.replace(regex, replacement);
                    }
                });

                if (modifiedText !== originalText) {
                    textNode.textContent = modifiedText;
                    nodeModified = true;
                    totalReplacements += nodeReplacements;
                }
            });

            if (nodeModified) {
                modifiedCount++;
            }
        });

        console.log(`TTS 优化完成:修改了 ${modifiedCount} 个帖子,共 ${totalReplacements} 处替换`);
        return { modifiedCount, totalReplacements };
    }


    /**
 * 恢复原始文本(通过重新加载页面或保存原始文本)
 * 简单方案:提示用户刷新页面
 */
    function restoreOriginalText() {
        location.reload();
    }


    /**
 * 显示 TTS 优化结果弹窗
 */
    function showTTSResultModal(result) {
        let existingModal = document.getElementById('tts-result-modal');
        if (existingModal) existingModal.remove();

        const modal = document.createElement('div');
        modal.id = 'tts-result-modal';
        modal.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: #6ba65e;
        color: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 4px 20px rgba(0,0,0,0.3);
        z-index: 100000;
        max-width: 400px;
        width: 90%;
        font-family: inherit;
    `;

        modal.innerHTML = `
        <h3 style="margin: 0 0 15px 0; font-size: 16px;">TTS Optimization Done</h3>
        <p style="margin: 0 0 10px 0; font-size: 14px;">
            Modified ${result.modifiedCount} posts<br>
            Total replacements: ${result.totalReplacements}
        </p>
        <p style="margin: 0 0 15px 0; font-size: 12px; opacity: 0.9;">
            Replaced: eh→er, uh→uhh, lol→laughing out loud, etc.
        </p>
        <div style="display: flex; gap: 10px; justify-content: center;">
            <button id="tts-undo-btn" style="background: rgba(255,255,255,0.2); color: white; border: 1px solid white; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-weight: bold; flex: 1;">
                Undo
            </button>
            <button id="tts-close-btn" style="background: white; color: #6ba65e; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-weight: bold; flex: 1;">
                Done
            </button>
        </div>
    `;

        const overlay = document.createElement('div');
        overlay.style.cssText = `
        position: fixed;
        top: 0; left: 0; width: 100%; height: 100%;
        background: rgba(0,0,0,0.5);
        z-index: 99999;
    `;

        const closeModal = () => {
            modal.remove();
            overlay.remove();
        };

        overlay.addEventListener('click', closeModal);
        modal.querySelector('#tts-close-btn').addEventListener('click', closeModal);
        modal.querySelector('#tts-undo-btn').addEventListener('click', restoreOriginalText);

        document.body.appendChild(overlay);
        document.body.appendChild(modal);
    }


    // ==========================================
    // 新增:文本提取功能模块
    // ==========================================

    /**
 * 提取帖子文本内容
 * 根据当前筛选状态(checkbox-filter-by-user)决定是否只提取特定用户
 */
    function extractPostsText() {
        // 检查是否启用了用户筛选
        const isFilterEnabled = getStorageBool('checkbox-filter-by-user');
        const targetUsername = isFilterEnabled ?
              (localStorage.getItem('filter-by-user') || '').trim().toLowerCase() : '';

        const articles = document.querySelectorAll(
            'article.message.message--post.js-post.js-inlineModContainer'
        );

        let extractedTexts = [];
        let includedCount = 0;
        let skippedCount = 0;

        articles.forEach((article, index) => {
            // 如果帖子被隐藏了(通过 hide-text-only-post 或其他方式),跳过
            if (article.style.display === 'none') {
                return;
            }

            // 检查用户名筛选
            const nameElement = article.querySelector('.message-name .username ');
            const postUsername = nameElement ? nameElement.textContent.trim() : '';

            // 如果启用了筛选且用户名不匹配,跳过
            //但其实这段不需要,因为如果开启了用户名筛选,post就会被隐藏,开头就被跳过了,所以这段注释掉
            /**
            if (isFilterEnabled && targetUsername &&
                postUsername.toLowerCase() !== targetUsername) {
                skippedCount++;
                return;
            }
            */

            // 提取正文内容
            const bodyElement = article.querySelector('.message-body.js-selectToQuote');
            if (!bodyElement) return;

            // 克隆元素以避免修改原 DOM
            const clone = bodyElement.cloneNode(true);

            // 移除引用块、签名等不需要的内容(参考原有的 hideTextOnlyPosts 逻辑)
            clone.querySelectorAll('blockquote, aside.message-signature, .bbMediaWrapper, .video-easter-egg-poster, img, .inserted-img').forEach(el => el.remove());

            // 获取纯文本
            let text = clone.textContent || '';

            // 保留换行和段落结构,只清理行内多余空格
            text = text
                .replace(/[ \t]+\n/g, '\n')      // 行尾空格清理
                .replace(/\n[ \t]+/g, '\n')      // 行首空格清理.replace(/[^\S\n]+/g, ' ')  // 非换行的空白合并为单个空格
                .replace(/\n{2,}/g, '\n')  // 超过1个的连续换行合并为1个
                .trim();

            if (text.length > 0) {
                // 【修改】修复时间戳获取:使用 data-date-string 和 data-time-string
                const timeElement = article.querySelector('.message-attribution-main time.u-dt');
                let timestamp = '';
                if (timeElement) {
                    const dateStr = timeElement.getAttribute('data-date-string') || '';
                    const timeStr = timeElement.getAttribute('data-time-string') || '';
                    if (dateStr && timeStr) {
                        timestamp = `${dateStr} ${timeStr}`;
                    } else {
                        // 降级:使用 textContent
                        timestamp = timeElement.textContent.trim();
                    }
                }

                extractedTexts.push({
                    index: index + 1,
                    username: postUsername,
                    text: text,
                    timestamp: timestamp
                });
                includedCount++;
            }
        });

        console.log(`提取完成:包含 ${includedCount} 条,跳过 ${skippedCount} 条`);

        if (extractedTexts.length === 0) {
            alert('No text content found to extract.');
            return null;
        }

        return formatExtractedText(extractedTexts);
    }

    /**
 * 格式化提取的文本
 */
    function formatExtractedText(posts) {
        const header = `Extracted from: ${window.location.href}\n` +
              `Date: ${new Date().toLocaleString()}\n` +
              `Total posts: ${posts.length}\n` +
              `${'='.repeat(50)}\n\n`;

        const body = posts.map((post, idx) => {
            return `[Post #${post.index}] ${post.username}${post.timestamp ? ' - ' + post.timestamp : ''}\n\n${post.text}\n`;
        }).join('\n\n');

        return header + body;
    }

    /**
 * 下载文本为文件
 */
    function downloadTextFile(content, filename) {
        const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = filename;
        a.style.display = 'none';
        document.body.appendChild(a);
        a.click();

        // 清理
        setTimeout(() => {
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
        }, 100);
    }

    /**
 * 复制到剪贴板
 */
    async function copyToClipboard(text) {
        try {
            await navigator.clipboard.writeText(text);
            return true;
        } catch (err) {
            console.error('Clipboard API failed:', err);
            // 降级方案
            const textarea = document.createElement('textarea');
            textarea.value = text;
            textarea.style.position = 'fixed';
            textarea.style.opacity = '0';
            document.body.appendChild(textarea);
            textarea.select();

            try {
                document.execCommand('copy');
                document.body.removeChild(textarea);
                return true;
            } catch (e) {
                document.body.removeChild(textarea);
                return false;
            }
        }
    }

    /**
 * 显示提取选项弹窗(使用现有的绿色主题风格)
 * 【修改】增加弹窗宽度,确保按钮文字显示完整
 */
    function showExtractOptions(text) {
        // 检查是否已存在弹窗
        let existingModal = document.getElementById('text-extract-modal');
        if (existingModal) {
            existingModal.remove();
        }

        // 创建弹窗
        const modal = document.createElement('div');
        modal.id = 'text-extract-modal';
        // 【修改】增加最大宽度到 500px,确保按钮文字不换行
        modal.style.cssText = `
        position: fixed;
        top: 50%;
        left: 50%;
        transform: translate(-50%, -50%);
        background: #6ba65e;
        color: white;
        padding: 20px;
        border-radius: 8px;
        box-shadow: 0 4px 20px rgba(0,0,0,0.3);
        z-index: 100000;
        max-width: 500px;
        width: 90%;
        font-family: inherit;
    `;

        // 内容预览(截断)
        const previewText = text.length > 200 ? text.substring(0, 200) + '...' : text;
        const postCount = (text.match(/\[Post #/g) || []).length;

        modal.innerHTML = `
        <h3 style="margin: 0 0 15px 0; font-size: 16px;">Text Extracted</h3>
        <p style="margin: 0 0 10px 0; font-size: 13px; opacity: 0.9;">
            ${postCount} posts extracted (${text.length} characters)
        </p>
        <div style="background: rgba(255,255,255,0.1); padding: 10px; border-radius: 4px; margin-bottom: 15px; max-height: 150px; overflow-y: auto; font-size: 12px; font-family: monospace; white-space: pre-wrap; word-break: break-word;">
            ${previewText.replace(/</g, '&lt;').replace(/>/g, '&gt;')}
        </div>
        <div style="display: flex; gap: 10px; justify-content: center;">
            <button id="extract-download-btn" style="background: white; color: #6ba65e; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-weight: bold; flex: 1; white-space: nowrap;">
                Download
            </button>
            <button id="extract-copy-btn" style="background: rgba(255,255,255,0.2); color: white; border: 1px solid white; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-weight: bold; flex: 1; white-space: nowrap;">
                Copy
            </button>
            <button id="extract-close-btn" style="background: transparent; color: white; border: 1px solid rgba(255,255,255,0.5); padding: 8px 16px; border-radius: 4px; cursor: pointer; flex: 0.5; white-space: nowrap;">
                ✕
            </button>
        </div>
    `;

        // 遮罩层
        const overlay = document.createElement('div');
        overlay.id = 'text-extract-overlay';
        overlay.style.cssText = `
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: rgba(0,0,0,0.5);
        z-index: 99999;
    `;

        // 关闭函数
        const closeModal = () => {
            modal.remove();
            overlay.remove();
        };

        // 事件绑定
        overlay.addEventListener('click', closeModal);
        modal.querySelector('#extract-close-btn').addEventListener('click', closeModal);

        modal.querySelector('#extract-download-btn').addEventListener('click', () => {
            // 【修改开始】获取帖子标题和页码
            let filename;

            // 获取标题
            const titleElement = document.querySelector('h1.p-title-value');
            const title = titleElement ? titleElement.textContent.trim() : '';

            // 获取当前页码
            const pageElement = document.querySelector('li.pageNav-page--current a');
            const pageNum = pageElement ? pageElement.textContent.trim() : '';

            // 如果能获取到标题和页码,使用它们生成文件名
            if (title && pageNum) {
                // 页码格式化为3位数 (001, 002, ...)
                const pageStr = pageNum.padStart(3, '0');
                // 清理非法文件名字符: \ / : * ? " < > |
                const safeTitle = title.replace(/[\\/:*?"<>|]/g, '_');
                filename = `${safeTitle}_p${pageStr}.txt`;
            } else {
                //  fallback:使用默认格式 + 时间精确到秒
                const now = new Date();
                const timeStr = now.toISOString()
                .replace(/[-:T]/g, '')     // 去掉 - : T
                .slice(0, 15);              // 取到秒: YYYYMMDDHHmmss
                filename = `lpsg_posts_${timeStr}.txt`;
            }
            downloadTextFile(text, filename);
            closeModal();
        });

        modal.querySelector('#extract-copy-btn').addEventListener('click', async () => {
            const success = await copyToClipboard(text);
            if (success) {
                const btn = modal.querySelector('#extract-copy-btn');
                const originalText = btn.textContent;
                btn.textContent = '✓ Copied!';
                btn.style.background = 'rgba(255,255,255,0.4)';
                setTimeout(() => {
                    btn.textContent = originalText;
                    btn.style.background = 'rgba(255,255,255,0.2)';
                }, 1500);
            } else {
                alert('Failed to copy to clipboard');
            }
        });

        document.body.appendChild(overlay);
        document.body.appendChild(modal);
    }



    // ==========================================
    // 第六部分:视频处理模块
    // ==========================================

    /**
 * 替换!!!Thread里!!!!!无法播放的视频
 * 把网站的预览图替换成直接可播放的视频播放器
 * 【修改】使用异步HEAD请求预探测有效URL,替代定时轮询切换
 * 【新增】分两阶段处理:先同步显示所有"测试链接中",再逐个异步探测
 * 【优化】并发探测视频URL,哪个先完成就先更新UI,无需等待全部完成
 */
    async function replaceThreadUnplayableVideos() {
        // 找到所有视频预览图(这些是需要替换的)
        const posters = document.getElementsByClassName('video-easter-egg-poster');

        // 如果没有需要替换的,说明是官方视频,直接放大处理
        if (posters.length === 0) {
            enlargeAndMuteVideo();
            return;
        }

        // ==========================================
        // 第一阶段:同步收集信息并显示所有"测试链接中"状态
        // 正序遍历(i=0开始),用户看到的加载顺序更自然
        // ==========================================
        const videoTasks = [];

        for (let i = 0; i < posters.length; i++) {
            const poster = posters[i];
            const img = poster.children[0];
            if (!img) continue;

            const imageUrl = img.src;

            // 创建容器和UI(先显示加载状态)
            const newDiv = document.createElement('div');
            newDiv.className = 'newVideoDiv';
            newDiv.id = `video-container-${i}`;

            // 显示"测试链接中"状态
            newDiv.innerHTML = `
            <div id="video-loading-${i}" style="
                padding: 5px;
                background: #f0f0f0;
                border: 2px dashed #6ba65e;
                border-radius: 8px;
                text-align: center;
                color: #333;
                font-weight: bold;
                margin: 5px;
            ">
                <div style="margin-bottom: 0px;">🎬 Testing video link...</div>
            </div>
        `;

            // 插入到预览图后面
            const parent = poster.parentElement?.parentElement;
            if (!parent) continue;

            parent.appendChild(newDiv);

            // 保存原始poster的包装器,方便后续立即删除
            const originalWrapper = poster.parentElement;

            videoTasks.push({
                index: i,
                imageUrl: imageUrl,
                parent: parent,
                newDiv: newDiv,
                originalWrapper: originalWrapper
            });
        }

        // ==========================================
        // 第二阶段:并发探测所有视频URL,哪个先完成就先更新UI
        // ==========================================

        // 为每个任务创建一个Promise,探测完成后立即更新UI
        const updatePromises = videoTasks.map(task =>
                                              findVideoUrl(task.imageUrl).then(validVideoUrl => {
            // 探测完成,立即更新这个视频的UI
            updateVideoUI(task, validVideoUrl);
        }).catch(err => {
            console.error(`视频 #${task.index} 探测失败:`, err);
            // 出错时按无有效URL处理
            updateVideoUI(task, null);
        })
                                             );

        // 等待所有更新完成(但每个已经各自更新了)
        await Promise.allSettled(updatePromises);

        // 处理官方视频
        enlargeAndMuteVideo();

        // ==========================================
        // 辅助函数:更新单个视频的UI
        // ==========================================
        function updateVideoUI(task, validVideoUrl) {
            const { index, imageUrl, parent, newDiv, originalWrapper } = task;
            const loadingDiv = document.getElementById(`video-loading-${index}`);

            if (validVideoUrl) {
                // 探测成功:创建视频播放器
                // 从URL中提取当前格式
                const extPattern = VIDEO_EXTENSIONS.join('|');
                const currentFormat = validVideoUrl.match(new RegExp(`\\.(${extPattern})$`, 'i'))?.[1] || 'mp4';

                // 移除加载提示
                if (loadingDiv) loadingDiv.remove();

                // 创建视频HTML
                const videoHtml = `
                <video data-video-id="${index}"
                    onloadstart="this.volume=${state.volume}"
                    style="width:${CONFIG.VIDEO_WIDTH};max-width:${CONFIG.VIDEO_WIDTH}; max-height:${CONFIG.VIDEO_MAX_HEIGHT};"
                    controls
                    data-xf-init="video-init"
                    data-poster="${imageUrl}"
                    poster="${imageUrl}">
                    <source data-src="${validVideoUrl}" src="${validVideoUrl}">
                    <div class="bbMediaWrapper-fallback">Your browser is not able to display this video.</div>
                </video>
            `;

                newDiv.innerHTML = videoHtml;

            } else {
                // 探测失败:显示错误信息,但仍提供手动切换
                if (loadingDiv) {
                    loadingDiv.innerHTML = `
                    <div style="color: #c5455c; margin-bottom: 0px;">
                        ⚠️ Video link not found
                    </div>
                    <div style="font-size: 12px; color: #666; margin-bottom: 0px;">
                        Auto-detection failed. Please try manual format switching.
                    </div>
                `;
                    loadingDiv.style.borderColor = '#c5455c';
                    loadingDiv.style.background = '#fff5f5';
                }

                // 即使探测失败,也创建一个默认视频元素(用mp4),让用户可以手动切换
                const defaultVideoUrl = imageUrl
                .replace('/attachments/posters', '/video')
                .replace('/lsvideo/thumbnails', '/lsvideo/videos')
                .replace('.jpg', '.mp4');

                const videoHtml = `
                <video data-video-id="${index}"
                    onloadstart="this.volume=${state.volume}"
                    style="width:${CONFIG.VIDEO_WIDTH};max-width:${CONFIG.VIDEO_WIDTH}; max-height:${CONFIG.VIDEO_MAX_HEIGHT}; "
                    controls
                    data-xf-init="video-init"
                    data-poster="${imageUrl}"
                    poster="${imageUrl}">
                    <source data-src="${defaultVideoUrl}" src="${defaultVideoUrl}">
                    <div class="bbMediaWrapper-fallback">Your browser is not able to display this video.</div>
                </video>
            `;

                const videoDiv = document.createElement('div');
                videoDiv.innerHTML = videoHtml;
                newDiv.insertBefore(videoDiv, loadingDiv);

                // 添加格式切换选择器(手动模式)
                const selectorWrapper = document.createElement('span');
                selectorWrapper.style.marginLeft = '5px';
                selectorWrapper.appendChild(document.createTextNode('Format: '));

                const selector = createFormatSelector(index, 'mp4', true);
                selectorWrapper.appendChild(selector);
                parent.appendChild(selectorWrapper);
            }

            // 立即删除原始poster元素
            if (originalWrapper && originalWrapper.parentElement) {
                originalWrapper.parentElement.removeChild(originalWrapper);
            }
        }
    }






    /**
     * 替换!!!Gallery里!!!!!无法播放的视频
     * 把网站的预览图替换成直接可播放的视频播放器
     */
    function replaceGalleryUnplayableVideos() {

        /**
         * 读取Gallery页面中的视频地址
         * @returns {string|null} - contentUrl 或 null
         */
        function getVideoUrl() {
            // 找到 JSON-LD 脚本
            const script = document.querySelector('script[type="application/ld+json"]');
            if (!script) return null;

            try {
                // 解析 JSON
                const data = JSON.parse(script.textContent);

                // 直接读取 mainEntity.contentUrl
                return data.mainEntity?.contentUrl || null;

            } catch (e) {
                console.warn('JSON-LD 解析失败:', e);
                return null;
            }
        }

        // 找到所有视频预览图(这些是需要替换的)
        const posters = document.getElementsByClassName('video-easter-egg-poster');

        // 如果没有需要替换的,说明是官方视频,直接放大处理
        if (posters.length === 0) {
            enlargeAndMuteVideo();
            return;
        }

        // 倒序遍历:从最后一个视频开始处理
        // 这样先处理后面的,不会影响到前面元素的索引
        for (let i = posters.length - 1; i >= 0; i--) {
            const poster = posters[i];
            const img = poster.children[0];
            if (!img) {
                continue;
            }

            const imageUrl = img.src;

            // 构造视频 URL:替换路径和扩展名
            const videoUrl = getVideoUrl();
            if (!videoUrl) {
                console.log('找不到视频url');
                continue;
            }

            // 创建视频播放器 HTML
            // ✅ 给 video 添加 data-video-id 属性,用于精确对应
            const videoHtml = `
                <video data-video-id="${i}"
                    onloadstart="this.volume=${state.volume}"
                    style="width:100%; max-width:1070px; max-height:${CONFIG.VIDEO_MAX_HEIGHT};"
                    controls
                    data-xf-init="video-init"
                    data-poster="${imageUrl}"
                    poster="${imageUrl}">
                    <source data-src="${videoUrl}" src="${videoUrl}">
                    <div class="bbMediaWrapper-fallback">Your browser is not able to display this video.</div>
                </video>
            `;

            // 创建容器插入视频
            const newDiv = document.createElement('div');
            newDiv.className = 'newVideoDiv';
            newDiv.innerHTML = videoHtml;

            // 插入到预览图后面
            const parent = poster.parentElement?.parentElement;
            if (parent) {
                parent.appendChild(newDiv);
            }
        }

        // 清理原始元素
        ['video-easter-egg-poster'].forEach(className => {
            Array.from(document.getElementsByClassName(className)).reverse().forEach(el => {
                const wrapper = el.parentElement;
                if (wrapper) {
                    wrapper.parentElement?.removeChild(wrapper);
                }
            });
        });

    }

    /**
 * 创建视频格式选择器(替代原来的3个按钮)
 * 【修改】支持传入默认选中的格式,以及手动模式(探测失败时使用)
 * @param {number} videoId - 视频索引,用于与 video 元素精确对应
 * @param {string} [defaultFormat='mp4'] - 默认选中的格式
 * @param {boolean} [isManualMode=false] - 是否为手动模式(探测失败)
 * @returns {HTMLSelectElement} - 下拉选择元素
 */
    function createFormatSelector(videoId, defaultFormat = 'mp4', isManualMode = false) {
        // 创建下拉选择器
        const select = document.createElement('select');

        // 给 select 添加 data-video-id,与 video 对应
        select.dataset.videoId = videoId;
        select.id = `format-select-${videoId}`;

        select.style.cssText = `
        margin-left: 5px;
        padding: 2px 5px;
        border: 1px solid #ccc;
        border-radius: 3px;
        background: white;
        cursor: pointer;
        font-size: 12px;
    `;

        // 添加选项
        const formats = VIDEO_EXTENSIONS.map(ext => ({
            value: ext,
            label: ext.toUpperCase()
        }));

        formats.forEach(fmt => {
            const option = document.createElement('option');
            option.value = fmt.value;
            option.textContent = fmt.label;
            select.appendChild(option);
        });

        // 设置默认值
        select.value = defaultFormat;

        // 如果是手动模式,添加提示样式
        if (isManualMode) {
            select.style.borderColor = '#c5455c';
            select.style.background = '#fff5f5';
            select.title = 'Auto-detection failed, please select format manually';
        }

        // 切换事件
        select.addEventListener('change', () => {
            const newFormat = select.value;

            // 通过 data-video-id 精确查找对应的 video
            const video = document.querySelector(`video[data-video-id="${videoId}"]`);
            if (!video) {
                console.error(`找不到 video[data-video-id="${videoId}"]`);
                return;
            }

            // 显示视频(如果是手动模式且之前隐藏了)
            video.style.display = '';

            //移除提示
            const loadingDiv = document.getElementById(`video-loading-${videoId}`);
            if (loadingDiv) loadingDiv.remove();

            // 直接修改 source 属性,不碰 innerHTML,避免销毁事件监听器
            const sources = video.querySelectorAll('source');
            sources.forEach(source => {
                ['src', 'data-src'].forEach(attr => {
                    const url = source.getAttribute(attr);
                    if (url) {
                        // 只替换最后的后缀,使用全局 VIDEO_EXTENSIONS 构建正则
                        const extPattern = VIDEO_EXTENSIONS.join('|');
                        const newUrl = url.replace(new RegExp(`\\.(${extPattern})$`, 'i'), `.${newFormat}`);
                        source.setAttribute(attr, newUrl);
                    }
                });
            });

            // 重新加载视频
            video.load();

            // 移除手动模式的警告样式(如果用户成功切换了)
            select.style.borderColor = '#ccc';
            select.style.background = 'white';

            console.log(`视频 #${videoId} 切换到格式: ${newFormat}`);
        });

        return select;
    }


    /**
     * 放大官方视频并静音
     * 官方视频不需要替换,但默认太小,需要放大
     */
    function enlargeAndMuteVideo() {
        const videos = document.querySelectorAll('video');
        let count = 0;

        videos.forEach(video => {
            const parent = video.parentElement;
            const fullUrl = window.location.href;

            // 处理替换的视频在(newVideoDiv 里),只处理thread里的,变回随窗口变化
            if ((!parent || parent.className === 'newVideoDiv') && fullUrl.includes('www.lpsg.com/threads')) {
                video.style.width = "100%";
                return;
            }

            //开始处理官方视频
            count++;

            // 设置样式
            video.style.width = "100%";
            video.style.maxWidth = CONFIG.VIDEO_WIDTH;
            video.style.maxHeight = CONFIG.VIDEO_MAX_HEIGHT;
            video.volume = state.volume;  // 应用默认音量

            // 把视频提升到父元素同级,并删除父元素(去掉包裹层)
            parent.before(video);
            parent.remove();
        });

        // 同时放大视频容器的宽度
        document.querySelectorAll('div.bbMediaWrapper.bbMediaWrapper--inline').forEach(el => {
            el.style.width = CONFIG.VIDEO_WIDTH;
        });

        console.log(`放大了${count}个视频`);
    }

    /**
     * itemList-item 容器内所有 a链接全部重写
     * 强制在新窗口打开
     */
    function rewriteGalleryLinks() {
        if (!getStorageBool('force-new-window')) return;
        document.querySelectorAll('.itemList-item.js-inlineModContainer a[data-fancybox]').forEach(link => {
            // 只保留必要的属性和内容
            const href = link.href;
            const text = link.textContent;
            const img = link.querySelector('img');  // 如果有缩略图

            // 清空并重建
            link.removeAttribute('data-type');
            link.removeAttribute('data-fancybox');
            link.removeAttribute('data-src');
            link.removeAttribute('data-lb-type-override');
            link.removeAttribute('data-lb-sidebar');
            link.removeAttribute('data-lb-caption-desc');
            link.removeAttribute('data-lb-caption-href');
            link.removeAttribute('data-caption');
            link.removeAttribute('class');  // 移除 js-lbImage 等类

            // 设置新窗口打开
            link.target = '_blank';
            link.className = 'gallery-link';  // 可选:加自定义类

            console.log('重写链接:', href);
        });
    }

    // ==========================================
    // 第七部分:自动滚动按钮模块
    // ==========================================

    /**
     * 创建右下角的悬浮自动滚动按钮
     * 点击开始/停止滚动,滚到底可自动跳转下一页
     */
    function createAutoScrollButton() {
        // 按钮配置
        const BTN_CONFIG = {
            scrollSpeed: 5,           // 默认滚动速度
            buttonSize: 50,           // 按钮大小(像素)
            buttonRight: 30,          // 距离右边距
            buttonBottom: '50%',      // 垂直位置:50% 表示居中
            buttonOffset: 0,          // 垂直偏移修正
            primaryColor: '#6ba65e',  // 主色:靛蓝
            hoverColor: '#77b969',    // 悬停色
            activeColor: '#538d47',   // 激活色(滚动中)
            zIndex: 99999            // 层级,确保在最上面
        };

        // 状态
        let isScrolling = false;      // 是否正在滚动
        let animationId = null;       // 动画帧 ID,用于取消
        let button = null;            // 按钮元素引用

        /**
         * 检查元素是否在视口内(可见)
         */
        function isElementInViewport(el) {
            if (!el || el.offsetParent === null) return false;

            const rect = el.getBoundingClientRect();
            const windowHeight = window.innerHeight || document.documentElement.clientHeight;
            const windowWidth = window.innerWidth || document.documentElement.clientWidth;

            // 检查元素是否在视口范围内(至少部分可见)
            return (
                rect.top < windowHeight &&
                rect.bottom > 0 &&
                rect.left < windowWidth &&
                rect.right > 0 &&
                rect.width > 0 &&
                rect.height > 0
            );
        }

        /**
         * 检查是否有视频进入视口
         */
        function isVideoInViewport() {
            const videos = document.querySelectorAll('video');
            for (let video of videos) {
                if (isElementInViewport(video)) {
                    return true;
                }
            }
            return false;
        }

        /**
         * 创建按钮 DOM
         */
        function createButton() {
            const btn = document.createElement('div');
            btn.id = 'auto-scroll-btn';

            // 计算垂直位置
            const offset = typeof BTN_CONFIG.buttonBottom === 'string' &&
                  BTN_CONFIG.buttonBottom.includes('%')
            ? `calc(${BTN_CONFIG.buttonBottom} + ${BTN_CONFIG.buttonOffset}px)`
                : `${BTN_CONFIG.buttonBottom + BTN_CONFIG.buttonOffset}px`;

            // 设置样式:玻璃拟态效果
            btn.style.cssText = `
                position: fixed;
                right: ${BTN_CONFIG.buttonRight}px;
                top: ${offset};
                transform: translateY(-50%);
                width: ${BTN_CONFIG.buttonSize}px;
                height: ${BTN_CONFIG.buttonSize}px;
                background: rgba(107, 166, 94,0.9);
                backdrop-filter: blur(10px);
                -webkit-backdrop-filter: blur(10px);
                border: 1px solid rgba(255, 255, 255, 0.2);
                border-radius: 50%;
                cursor: pointer;
                z-index: ${BTN_CONFIG.zIndex};
                display: flex;
                align-items: center;
                justify-content: center;
                box-shadow: 0 8px 32px rgba(99, 102, 241, 0.3),
                            0 2px 8px rgba(0, 0, 0, 0.1),
                            inset 0 1px 0 rgba(255, 255, 255, 0.2);
                transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
                opacity: 0;
                scale: 0.8;
            `;

            // 内部图标:默认是向下箭头
            btn.innerHTML = `
				<svg id="scroll-icon" width="24" height="24" viewBox="0 0 24 24"
					style="transition: all 0.3s ease;">
					<path d="M12 5v14M5 12l7 7 7-7"
						  fill="none"
						  stroke="white"
						  stroke-width="2.5"
						  stroke-linecap="round"
						  stroke-linejoin="round"/>
				</svg>
			`;

            // 鼠标悬停效果(未滚动时)
            btn.addEventListener('mouseenter', () => {
                if (isScrolling) return;
                btn.style.background = BTN_CONFIG.hoverColor;
                btn.style.transform = 'translateY(-50%) scale(1.1)';
            });

            btn.addEventListener('mouseleave', () => {
                if (isScrolling) return;
                btn.style.background = BTN_CONFIG.primaryColor;
                btn.style.transform = 'translateY(-50%) scale(1)';
            });

            // 点击切换滚动状态
            btn.addEventListener('click', toggleScroll);

            // 页面加载时:如果之前设置为隐藏,立即应用
            if (!getStorageBool('show-scroll-button')) {
                btn.style.display = 'none';
            }

            return btn;
        }

        /**
         * 开始自动滚动
         */
        function startScroll() {
            if (isScrolling) return;  // 已经在滚动了,忽略

            isScrolling = true;

            // 从 localStorage 读取速度设置
            const speed = getStorageBool(`checkbox-scroll-speed`)?(Number(localStorage.getItem('scroll-speed')) || BTN_CONFIG.scrollSpeed):BTN_CONFIG.scrollSpeed;

            // 更新按钮样式为激活状态
            button.style.background = BTN_CONFIG.activeColor;
            button.style.transform = 'translateY(-50%) scale(0.95)';

            // 图标变成停止方块
            button.querySelector('#scroll-icon').innerHTML =
                '<rect x="6" y="6" width="12" height="12" rx="2" fill="white" stroke="none"/>';

            /**
             * 滚动步进函数
             * 使用 requestAnimationFrame 实现平滑滚动
             */
            function step() {
                if (!isScrolling) return;  // 被停止了

                // 获取当前滚动位置和最大高度
                const current = window.pageYOffset || document.documentElement.scrollTop;
                const max = document.documentElement.scrollHeight - window.innerHeight;

                // 距离底部 CONFIG.SCROLL_OFFSET 像素时停止
                if (current >= max - CONFIG.SCROLL_OFFSET) {
                    stopScroll();

                    // 如果勾选了"自动跳转下一页",就跳转
                    if (getStorageBool('autoscroll-jump-key')) {
                        setTimeout(clickNextPage, CONFIG.NEXT_PAGE_DELAY);
                    }
                    return;
                }

                // 【新增】检测视频逻辑:首次检测到视频后,再滚动几次再停止
                if (isVideoInViewport()) {
                    // 使用静态变量记录还需要继续滚动的次数
                    if (typeof step.continueScrollCount === 'undefined') {
                        step.continueScrollCount = 90; // 再滚动一些让视频进入界面
                        //console.log('检测到视频进入视口,继续滚动3次后停止');
                    }

                    if (step.continueScrollCount > 0) {
                        step.continueScrollCount--;
                        window.scrollBy(0, speed);
                        animationId = requestAnimationFrame(step);
                        return;
                    } else {
                        // 继续滚动次数用完,真正停止
                        console.log('视频已进入视窗中心,停止滚动');
                        stopScroll();
                        // 重置计数器,为下次做准备
                        step.continueScrollCount = undefined;
                        return;
                    }
                } else {
                    // 视频不在视口内,重置计数器
                    step.continueScrollCount = undefined;
                }

                // 正常滚动一小段距离
                window.scrollBy(0, speed);

                // 继续下一帧
                animationId = requestAnimationFrame(step);
            }

            // 开始动画循环
            animationId = requestAnimationFrame(step);
        }

        /**
         * 停止自动滚动
         */
        function stopScroll() {
            if (!isScrolling) return;  // 已经停止了,忽略

            isScrolling = false;
            cancelAnimationFrame(animationId);  // 取消动画帧

            // 恢复按钮样式
            button.style.background = BTN_CONFIG.primaryColor;
            button.style.transform = 'translateY(-50%) scale(1)';

            // 图标恢复向下箭头
            button.querySelector('#scroll-icon').innerHTML = `
				<path d="M12 5v14M5 12l7 7 7-7"
					  fill="none"
					  stroke="white"
					  stroke-width="2.5"
					  stroke-linecap="round"
					  stroke-linejoin="round"/>
			`;
        }

        /**
         * 切换滚动状态(开始/停止)
         */
        function toggleScroll() {
            isScrolling ? stopScroll() : startScroll();
        }

        /**
         * 初始化按钮
         */
        function init() {
            // 防止重复初始化
            if (document.getElementById('auto-scroll-btn')) return;

            // 添加 CSS 动画关键帧
            const style = document.createElement('style');
            style.textContent = `
                @keyframes pulse {
                    0%, 100% { box-shadow: 0 8px 32px rgba(99, 102, 241, 0.4), 0 0 0 0 rgba(99, 102, 241, 0.4); }
                    50% { box-shadow: 0 8px 32px rgba(99, 102, 241, 0.4), 0 0 0 20px rgba(99, 102, 241, 0); }
                }
            `;
            document.head.appendChild(style);

            // 创建并添加按钮
            button = createButton();
            document.body.appendChild(button);

            // 入场动画:延迟显示
            setTimeout(() => {
                button.style.opacity = '1';
                button.style.scale = '1';
            }, 100);

            // 监听滚轮事件:用户手动滚动时停止自动滚动
            window.addEventListener('wheel', () => {
                if (isScrolling) stopScroll();
            }, { passive: true });

            // 如果勾选了"自动滚动",延迟后开始滚动
            if (getStorageBool('autoscroll-key')) {
                setTimeout(startScroll, CONFIG.AUTO_START_DELAY);
            }
        }

        // 页面加载完成后初始化
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', init);
        } else {
            init();
        }
    }

    // ==========================================
    // 第八部分:总初始化
    // ==========================================

    /**
     * 初始化所有功能
     * 按顺序创建控制面板和各种选项
     */
    function initThread() {
        //创建控制面板容器(左上角折叠面板)
        createControlDiv();

        //处理视频替换(核心功能)
        replaceThreadUnplayableVideos();

        //跳过设置组
        createSectionLabel('Skip Useless Stuff');
        createHideTextOnlyCheckbox();
        createAutoJumpTextOnlyCheckbox();

        //页面控制组
        createSectionLabel('Page Jump Setting');
        createAutoJumpBackwardCheckbox();

        //视频设置组
        createSectionLabel('Video Setting');
        createVolumeInput();
        createVideoAutoFullscreenCheckbox();

        //图片显示组
        createSectionLabel('Better Pic Display');
        createEnlargeAttachmentCheckbox();
        createOnePicPerLineCheckbox();
        createMaxWidthInput();

        //用户信息组
        createSectionLabel('User Related');
        createHideUserInfoCheckbox();
        createFilterByUserInput();

        //自动滚动组
        createSectionLabel('Auto Scroll');
        createShowScrollButtonCheckbox();
        createAutoscrollCheckbox();
        createAutoscrollJumpCheckbox();
        createScrollSpeedInput();

        //文本处理组
        createSectionLabel('Erotic Stories');
        createTextExtractButton();  // 添加文本提取按钮
        //createTTSOptimizeButton();  // 【新增】TTS 优化按钮

        //创建悬浮滚动按钮
        createAutoScrollButton();
    }


    /**
     * 初始化所有功能
     * 按顺序创建控制面板和各种选项
     */
    function initGallery() {
        // 1. 创建控制面板容器(左上角折叠面板)
        createControlDiv();

        // 2. 处理视频替换(核心功能)
        replaceGalleryUnplayableVideos();
        rewriteGalleryLinks();//在浏览页把所有链接强制为在新窗口打开

        // 3. 视频设置组
        createSectionLabel('Script only works on new window!!!','#c5455c');
        createSectionLabel('Video Setting');
        createForceNewWindowCheckbox();
        createVolumeInput();
        createVideoAutoFullscreenCheckbox();

    }

    const fullUrl = window.location.href;

    // 判断执行不同代码
    if (fullUrl.includes('www.lpsg.com/threads')) {
        // thread代码
        initThread();
    } else if (fullUrl.includes('www.lpsg.com/gallery')) {
        // gallery代码
        initGallery();
    }
    state.initDone=true;
    showFirstTimeWelcome();
})();