百合会论坛阅读增强

为百合会论坛提供漫画/小说的沉浸式阅读体验,支持多种阅读模式、暗色模式、Material Design风格

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         百合会论坛阅读增强
// @namespace    http://tampermonkey.net/
// @version      2.1.0
// @description  为百合会论坛提供漫画/小说的沉浸式阅读体验,支持多种阅读模式、暗色模式、Material Design风格
// @author       bluelightgit
// @match        https://bbs.yamibo.com/thread-*
// @match        https://bbs.yamibo.com/forum.php?mod=viewthread*
// @icon         https://bbs.yamibo.com/favicon.ico
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @connect      bbs.yamibo.com
// @run-at       document-end
// @license      MIT
// ==/UserScript==
(function() {
    'use strict';

        function normalizeSeriesTitle(rawTitle) {
            if (!rawTitle) return '';
            let title = rawTitle;
            title = title.replace(/[((]\s*\d+\s*p\s*[))]\s*$/i, '');
            title = title.replace(/第[\d一二三四五六七八九十百千万〇零兩两1234567890\.]+[话話章节節回卷篇]/gi, ' ');
            title = title.replace(/\d+(?:\.\d+)?\s*[话話章节節回卷篇]/gi, ' ');
            title = title.replace(/\d+(?:\.\d+)?\s*[上下前后前後左右中篇部卷期全完]+(?:\s*[++&和及與并並,,/]\s*[上下前后前後左右中篇部卷期全完]+)+/gi, ' ');
            title = title.replace(/[((][上下前后前後中全完]+(?:\s*[,,++&和及與并並/]\s*[上下前后前後中全完]+)*[))]/g, ' ');
            title = title.replace(/\d+(?:\.\d+)?\s*[上下前后前後左右中篇部卷期全完]+/gi, ' ');
            title = title.replace(/(?:\s+|[-‐‑‒–—―-~~·•_、::])?\d+(?:\.\d+)*(?:\s*[上下前后前後左右中篇部卷期話话节節全完])?\s*$/g, ' ');
            title = title.replace(/[--—–~~\u2013\u2014\s]+$/g, ' ');
            title = title.replace(/[\[\]【】()()]/g, ' ');
            title = title.replace(/\s+/g, ' ').trim();
            if (!title) {
                return rawTitle.trim();
            }
            return title;
        }

        function buildSeriesKey(title) {
            const normalized = normalizeSeriesTitle(title || '');
            const base = normalized || (title || '').trim();
            return base.toLowerCase();
        }

        function normalizeSearchResultsPerPageValue(rawValue) {
            const numeric = Number(rawValue);
            if (!Number.isFinite(numeric)) {
                return CONFIG.SEARCH_RESULTS_PER_PAGE_DEFAULT;
            }
            const clamped = Math.min(
                CONFIG.SEARCH_RESULTS_PER_PAGE_MAX,
                Math.max(CONFIG.SEARCH_RESULTS_PER_PAGE_MIN, Math.floor(numeric))
            );
            return clamped;
        }

        // =========================
        const CONFIG = {
            GOLDEN_RATIO: 0.618,
            DEFAULT_MAIN_WIDTH_RATIO: 0.7,
            SEARCH_RESULTS_PER_PAGE_DEFAULT: 60,
            SEARCH_RESULTS_PER_PAGE_MIN: 20,
            SEARCH_RESULTS_PER_PAGE_MAX: 120,
            MAX_SEARCH_PAGES: 10,
            IMAGE_SIZE_THRESHOLD: 100 * 1024,
            PRELOAD_COUNT: 3,
            SEARCH_RETRY_DELAY: 10000,
            STORAGE_KEY: 'yamibo_reader_data',
            AUTO_OPEN_KEY: 'yamibo_reader_auto_open',
            // 阅读模式
            VIEW_MODES: {
                SCROLL_DOWN: 'scroll-down',
                SCROLL_LEFT: 'scroll-left',
                SCROLL_RIGHT: 'scroll-right',
                FLIP_LEFT_SINGLE: 'flip-left-single',
                FLIP_LEFT_DOUBLE: 'flip-left-double',
                FLIP_RIGHT_SINGLE: 'flip-right-single',
                FLIP_RIGHT_DOUBLE: 'flip-right-double'
            }
        };

        // =========================
        // SVG 图标库
        // =========================
        const ICONS = {
            book: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21 5c-1.11-.35-2.33-.5-3.5-.5-1.95 0-4.05.4-5.5 1.5-1.45-1.1-3.55-1.5-5.5-1.5S2.45 4.9 1 6v14.65c0 .25.25.5.5.5.1 0 .15-.05.25-.05C3.1 20.45 5.05 20 6.5 20c1.95 0 4.05.4 5.5 1.5 1.35-.85 3.8-1.5 5.5-1.5 1.65 0 3.35.3 4.75 1.05.1.05.15.05.25.05.25 0 .5-.25.5-.5V6c-.6-.45-1.25-.75-2-1zm0 13.5c-1.1-.35-2.3-.5-3.5-.5-1.7 0-4.15.65-5.5 1.5V8c1.35-.85 3.8-1.5 5.5-1.5 1.2 0 2.4.15 3.5.5v11.5z"/></svg>',
            bookmark: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>',
            bookmarkFilled: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>',
            settings: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.8,11.69,4.8,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"/></svg>',
            close: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>',
            search: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/></svg>',
            arrowLeft: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M15.41 7.41L14 6l-6 6 6 6 1.41-1.41L10.83 12z"/></svg>',
            arrowRight: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>',
            chevronsLeft: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M17 7.41L15.59 6l-6 6 6 6L17 16.59 12.41 12z"/><path d="M11 7.41L9.59 6l-6 6 6 6L11 16.59 6.41 12z"/></svg>',
            chevronsRight: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M7 6L5.59 7.41 10.17 12l-4.58 4.59L7 18l6-6z"/><path d="M15 6l-1.41 1.41L18.17 12l-4.58 4.59L15 18l6-6z"/></svg>',
            darkMode: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 2c-1.05 0-2.05.16-3 .46 4.06 1.27 7 5.06 7 9.54 0 4.48-2.94 8.27-7 9.54.95.3 1.95.46 3 .46 5.52 0 10-4.48 10-10S14.52 2 9 2z"/></svg>',
            lightMode: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 7c-2.76 0-5 2.24-5 5s2.24 5 5 5 5-2.24 5-5-2.24-5-5-5zM2 13h2c.55 0 1-.45 1-1s-.45-1-1-1H2c-.55 0-1 .45-1 1s.45 1 1 1zm18 0h2c.55 0 1-.45 1-1s-.45-1-1-1h-2c-.55 0-1 .45-1 1s.45 1 1 1zM11 2v2c0 .55.45 1 1 1s1-.45 1-1V2c0-.55-.45-1-1-1s-1 .45-1 1zm0 18v2c0 .55.45 1 1 1s1-.45 1-1v-2c0-.55-.45-1-1-1s-1 .45-1 1zM5.99 4.58c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0s.39-1.03 0-1.41L5.99 4.58zm12.37 12.37c-.39-.39-1.03-.39-1.41 0-.39.39-.39 1.03 0 1.41l1.06 1.06c.39.39 1.03.39 1.41 0 .39-.39.39-1.03 0-1.41l-1.06-1.06zm1.06-10.96c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06zM7.05 18.36c.39-.39.39-1.03 0-1.41-.39-.39-1.03-.39-1.41 0l-1.06 1.06c-.39.39-.39 1.03 0 1.41s1.03.39 1.41 0l1.06-1.06z"/></svg>',
            viewMode: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z"/></svg>',
            play: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>',
            delete: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/></svg>'
        };

        const ALL_VIEW_MODES = new Set(Object.values(CONFIG.VIEW_MODES));
        const LEGACY_VIEW_MODE_MAP = {
            'scroll-ttb': CONFIG.VIEW_MODES.SCROLL_DOWN,
            'scroll-ltr': CONFIG.VIEW_MODES.SCROLL_RIGHT,
            'scroll-rtl': CONFIG.VIEW_MODES.SCROLL_LEFT,
            'page-single': CONFIG.VIEW_MODES.FLIP_RIGHT_SINGLE,
            'page-double': CONFIG.VIEW_MODES.FLIP_RIGHT_DOUBLE
        };

        // =========================
        // 图片缓存管理
        // =========================
        class ImageCache {
            constructor() {
                this.cache = new Map(); // url -> objectUrl
            }

            async load(url) {
                if (this.cache.has(url)) {
                    return this.cache.get(url);
                }

                try {
                    const response = await fetch(url);
                    const blob = await response.blob();
                    const objectUrl = URL.createObjectURL(blob);
                    this.cache.set(url, objectUrl);
                    return objectUrl;
                } catch (e) {
                    console.error('Failed to load image:', url, e);
                    return url;
                }
            }

            has(url) {
                return this.cache.has(url);
            }

            get(url) {
                return this.cache.get(url);
            }

            clear() {
                for (const objectUrl of this.cache.values()) {
                    URL.revokeObjectURL(objectUrl);
                }
                this.cache.clear();
            }
        }

    // =========================
    // 数据存储管理
    // =========================
    class DataStore {
        constructor() {
            this.data = this.load();
            this.ensureStructure();
        }

        load() {
            const stored = GM_getValue(CONFIG.STORAGE_KEY, '{}');
            try {
                const data = JSON.parse(stored);
                return data;
            } catch (e) {
                console.error('Failed to parse storage data:', e);
                return {
                    favorites: {},
                    readingProgress: {},
                    settings: {
                        darkMode: false,
                        viewMode: CONFIG.VIEW_MODES.SCROLL_DOWN,
                        floatingButtonPosition: null,
                        searchResultsPerPage: CONFIG.SEARCH_RESULTS_PER_PAGE_DEFAULT,
                        sidebarCollapsed: false
                    }
                };
            }
        }

        save() {
            GM_setValue(CONFIG.STORAGE_KEY, JSON.stringify(this.data));
        }

        ensureStructure() {
            let settingsUpdated = false;
            if (!this.data.settings) {
                this.data.settings = {
                    darkMode: false,
                    viewMode: CONFIG.VIEW_MODES.SCROLL_DOWN,
                    floatingButtonPosition: null,
                    searchResultsPerPage: CONFIG.SEARCH_RESULTS_PER_PAGE_DEFAULT,
                    sidebarCollapsed: false
                };
                settingsUpdated = true;
            }
            if (!this.data.favorites || typeof this.data.favorites !== 'object') {
                this.data.favorites = {};
            }
            if (!this.data.readingProgress || typeof this.data.readingProgress !== 'object') {
                this.data.readingProgress = {};
            }
            if (!this.data.seriesNameOverrides || typeof this.data.seriesNameOverrides !== 'object') {
                this.data.seriesNameOverrides = {};
            }
            const pos = this.data.settings.floatingButtonPosition;
            if (pos && (typeof pos !== 'object' || pos.left === undefined || pos.top === undefined ||
                !Number.isFinite(Number(pos.left)) || !Number.isFinite(Number(pos.top)))) {
                this.data.settings.floatingButtonPosition = null;
                settingsUpdated = true;
            }
            if (!Object.prototype.hasOwnProperty.call(this.data.settings, 'floatingButtonPosition')) {
                this.data.settings.floatingButtonPosition = null;
                settingsUpdated = true;
            }
            if (!Object.prototype.hasOwnProperty.call(this.data.settings, 'searchResultsPerPage')) {
                this.data.settings.searchResultsPerPage = CONFIG.SEARCH_RESULTS_PER_PAGE_DEFAULT;
                settingsUpdated = true;
            } else {
                const normalizedPerPage = normalizeSearchResultsPerPageValue(this.data.settings.searchResultsPerPage);
                if (normalizedPerPage !== this.data.settings.searchResultsPerPage) {
                    this.data.settings.searchResultsPerPage = normalizedPerPage;
                    settingsUpdated = true;
                }
            }
            if (!Object.prototype.hasOwnProperty.call(this.data.settings, 'sidebarCollapsed')) {
                this.data.settings.sidebarCollapsed = false;
                settingsUpdated = true;
            } else if (typeof this.data.settings.sidebarCollapsed !== 'boolean') {
                this.data.settings.sidebarCollapsed = !!this.data.settings.sidebarCollapsed;
                settingsUpdated = true;
            }
            this.migrateLegacyFavorites();

            let favoritesUpdated = false;
            if (this.data.favorites && typeof this.data.favorites === 'object') {
                Object.values(this.data.favorites).forEach(series => {
                    if (!series || typeof series !== 'object') {
                        return;
                    }
                    if (!Object.prototype.hasOwnProperty.call(series, 'directoryCount')) {
                        const storedChapters = series.chapters && typeof series.chapters === 'object'
                            ? Object.keys(series.chapters).length
                            : 0;
                        series.directoryCount = storedChapters;
                        favoritesUpdated = true;
                    } else {
                        const numericCount = Number(series.directoryCount);
                        const normalized = Number.isFinite(numericCount) && numericCount >= 0
                            ? Math.floor(numericCount)
                            : 0;
                        if (series.directoryCount !== normalized) {
                            series.directoryCount = normalized;
                            favoritesUpdated = true;
                        }
                    }
                });
            }

            if (favoritesUpdated || settingsUpdated) {
                this.save();
            }
        }

        migrateLegacyFavorites() {
            const favorites = this.data.favorites;
            if (!favorites) return;
            const legacyEntries = Object.values(favorites).filter(fav => fav && !fav.seriesKey);
            if (legacyEntries.length === 0) {
                return;
            }

            const migrated = Object.values(favorites).reduce((acc, fav) => {
                if (fav && fav.seriesKey) {
                    acc[fav.seriesKey] = fav;
                }
                return acc;
            }, {});
            legacyEntries.forEach(fav => {
                const title = fav.title || '';
                const seriesTitle = normalizeSeriesTitle(title) || title || '未命名合集';
                const seriesKey = buildSeriesKey(title || fav.threadId || String(Date.now()));
                const timestamp = fav.lastVisited || fav.addedAt || Date.now();
                if (!migrated[seriesKey]) {
                    migrated[seriesKey] = {
                        seriesKey,
                        seriesTitle,
                        author: fav.author || '',
                        chapters: {},
                        latestThreadId: '',
                        latestTitle: '',
                        latestUrl: '',
                        latestFloor: 0,
                        latestTotalFloors: 0,
                        directoryCount: 0,
                        addedAt: fav.addedAt || timestamp,
                        lastVisited: timestamp
                    };
                }

                if (fav.threadId) {
                    migrated[seriesKey].chapters[fav.threadId] = {
                        threadId: fav.threadId,
                        title: fav.title || '',
                        url: fav.url || '',
                        currentFloor: fav.currentFloor || 0,
                        totalFloors: fav.totalFloors || 0,
                        lastVisited: timestamp
                    };

                    const latest = migrated[seriesKey];
                    const latestRef = latest.latestThreadId ? latest.chapters[latest.latestThreadId] : null;
                    if (!latestRef || (latestRef.lastVisited || 0) <= timestamp) {
                        latest.latestThreadId = fav.threadId;
                        latest.latestTitle = fav.title || '';
                        latest.latestUrl = fav.url || '';
                        latest.latestFloor = fav.currentFloor || 0;
                        latest.latestTotalFloors = fav.totalFloors || 0;
                        latest.lastVisited = timestamp;
                    }

                    const chapterTotal = latest.chapters ? Object.keys(latest.chapters).length : 0;
                    latest.directoryCount = chapterTotal;
                }
            });

            this.data.favorites = migrated;
            this.save();
        }

        addOrUpdateFavorite(seriesKey, payload) {
            if (!seriesKey) return;
            if (!this.data.favorites) this.data.favorites = {};

            const now = Date.now();
            if (!this.data.favorites[seriesKey]) {
                const seriesTitle = payload?.seriesTitle || normalizeSeriesTitle(payload?.chapterTitle || '') || seriesKey;
                this.data.favorites[seriesKey] = {
                    seriesKey,
                    seriesTitle,
                    author: payload?.author || '',
                    chapters: {},
                    latestThreadId: '',
                    latestTitle: '',
                    latestUrl: '',
                    latestFloor: 0,
                    latestTotalFloors: 0,
                    directoryCount: Number.isFinite(payload?.directoryCount) && payload.directoryCount >= 0
                        ? Math.floor(payload.directoryCount)
                        : 0,
                    addedAt: now,
                    lastVisited: now
                };
            }

            const series = this.data.favorites[seriesKey];
            if (!Object.prototype.hasOwnProperty.call(series, 'directoryCount') ||
                !Number.isFinite(Number(series.directoryCount)) || series.directoryCount < 0) {
                series.directoryCount = 0;
            }

            if (Number.isFinite(payload?.directoryCount) && payload.directoryCount >= 0) {
                const normalizedPayloadCount = Math.floor(payload.directoryCount);
                if (series.directoryCount < normalizedPayloadCount) {
                    series.directoryCount = normalizedPayloadCount;
                }
            }

            this.updateFavoriteChapter(seriesKey, payload, now, false);
            this.save();
        }

        updateFavoriteChapter(seriesKey, payload, timestamp = Date.now(), autoSave = true) {
            if (!seriesKey || !payload || !payload.threadId) return;
            const series = this.data.favorites && this.data.favorites[seriesKey];
            if (!series) return;

            if (payload.seriesTitle) {
                series.seriesTitle = payload.seriesTitle;
            }
            if (payload.author) {
                series.author = payload.author;
            }

            if (!series.chapters) {
                series.chapters = {};
            }

            const chapter = {
                threadId: payload.threadId,
                title: payload.chapterTitle || payload.title || '',
                url: payload.url || '',
                currentFloor: payload.currentFloor || 0,
                totalFloors: payload.totalFloors || 0,
                lastVisited: timestamp
            };

            series.chapters[payload.threadId] = chapter;
            series.lastVisited = timestamp;

            const latest = series.latestThreadId ? series.chapters[series.latestThreadId] : null;
            if (!latest || (latest.lastVisited || 0) <= timestamp || series.latestThreadId === payload.threadId) {
                series.latestThreadId = payload.threadId;
                series.latestTitle = chapter.title;
                series.latestUrl = chapter.url;
                series.latestFloor = chapter.currentFloor;
                series.latestTotalFloors = chapter.totalFloors;
            }

            const totalChapters = series.chapters ? Object.keys(series.chapters).length : 0;
            const existingDirectoryCount = Number(series.directoryCount);
            if (!Number.isFinite(existingDirectoryCount) || existingDirectoryCount < totalChapters) {
                series.directoryCount = totalChapters;
            }

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

        removeFavorite(seriesKey) {
            if (this.data.favorites && this.data.favorites[seriesKey]) {
                delete this.data.favorites[seriesKey];
                this.save();
            }
        }

        isSeriesFavorited(seriesKey) {
            return !!(this.data.favorites && this.data.favorites[seriesKey]);
        }

        updateSeriesDirectoryCount(seriesKey, count) {
            if (!seriesKey) {
                return;
            }
            const series = this.data.favorites && this.data.favorites[seriesKey];
            if (!series) {
                return;
            }
            const numericCount = Number(count);
            if (!Number.isFinite(numericCount) || numericCount < 0) {
                return;
            }
            const normalized = Math.floor(numericCount);
            if (Number(series.directoryCount) === normalized) {
                return;
            }
            series.directoryCount = normalized;
            this.save();
        }

        getSeriesDirectoryCount(seriesKey) {
            const series = this.data.favorites && this.data.favorites[seriesKey];
            if (!series) {
                return 0;
            }
            const numericCount = Number(series.directoryCount);
            return Number.isFinite(numericCount) && numericCount >= 0 ? Math.floor(numericCount) : 0;
        }

        getFavorite(seriesKey) {
            return this.data.favorites && this.data.favorites[seriesKey];
        }

        getSeriesNameOverride(baseKey) {
            if (!baseKey) {
                return null;
            }
            const overrides = this.data.seriesNameOverrides;
            if (!overrides || typeof overrides !== 'object') {
                return null;
            }
            const value = overrides[baseKey];
            return typeof value === 'string' && value.trim() ? value.trim() : null;
        }

        setSeriesNameOverride(baseKey, name) {
            if (!baseKey) {
                return;
            }
            const trimmed = typeof name === 'string' ? name.trim() : '';
            if (!trimmed) {
                return;
            }
            if (!this.data.seriesNameOverrides || typeof this.data.seriesNameOverrides !== 'object') {
                this.data.seriesNameOverrides = {};
            }
            if (this.data.seriesNameOverrides[baseKey] === trimmed) {
                return;
            }
            this.data.seriesNameOverrides[baseKey] = trimmed;
            this.save();
        }

        renameFavoriteSeries(oldKey, newKey, newTitle) {
            if (!oldKey || !newKey || oldKey === newKey) {
                return;
            }
            if (!this.data.favorites || typeof this.data.favorites !== 'object') {
                return;
            }
            const source = this.data.favorites[oldKey];
            if (!source) {
                return;
            }

            let target = this.data.favorites[newKey];
            if (target && target !== source) {
                // 合并章节
                if (!target.chapters || typeof target.chapters !== 'object') {
                    target.chapters = {};
                }
                if (source.chapters && typeof source.chapters === 'object') {
                    Object.keys(source.chapters).forEach(threadId => {
                        if (!target.chapters[threadId]) {
                            target.chapters[threadId] = source.chapters[threadId];
                        }
                    });
                }

                // 更新元数据
                const timestamps = [target.lastVisited, source.lastVisited].filter(Boolean);
                target.lastVisited = timestamps.length ? Math.max(...timestamps) : Date.now();
                target.addedAt = Math.min(target.addedAt || Date.now(), source.addedAt || Date.now());
                if (!target.author && source.author) {
                    target.author = source.author;
                }
                const sourceLastVisited = source.lastVisited || 0;
                const targetLastVisited = target.lastVisited || 0;
                if ((!target.latestThreadId && source.latestThreadId) || sourceLastVisited >= targetLastVisited) {
                    target.latestThreadId = source.latestThreadId;
                    target.latestTitle = source.latestTitle;
                    target.latestUrl = source.latestUrl;
                    target.latestFloor = source.latestFloor;
                    target.latestTotalFloors = source.latestTotalFloors;
                }
                const sourceChapterCount = source.chapters ? Object.keys(source.chapters).length : 0;
                const targetChapterCount = target.chapters ? Object.keys(target.chapters).length : 0;
                const sourceDirectoryCount = Number(source.directoryCount);
                const targetDirectoryCount = Number(target.directoryCount);
                const normalizedSourceDirectoryCount = Number.isFinite(sourceDirectoryCount) && sourceDirectoryCount >= 0
                    ? Math.floor(sourceDirectoryCount)
                    : sourceChapterCount;
                const normalizedTargetDirectoryCount = Number.isFinite(targetDirectoryCount) && targetDirectoryCount >= 0
                    ? Math.floor(targetDirectoryCount)
                    : targetChapterCount;
                target.directoryCount = Math.max(normalizedTargetDirectoryCount, normalizedSourceDirectoryCount, targetChapterCount);
            } else {
                this.data.favorites[newKey] = source;
                target = this.data.favorites[newKey];
            }

            delete this.data.favorites[oldKey];

            if (target) {
                target.seriesKey = newKey;
                if (newTitle) {
                    target.seriesTitle = newTitle;
                }
            }

            this.save();
        }

        getAllFavorites() {
            if (!this.data.favorites) return [];
            return Object.values(this.data.favorites);
        }

        setProgress(threadId, floor) {
            if (!this.data.readingProgress) this.data.readingProgress = {};
            this.data.readingProgress[threadId] = {
                floor,
                timestamp: Date.now()
            };
            this.save();
        }

        getProgress(threadId) {
            return this.data.readingProgress && this.data.readingProgress[threadId];
        }

        setSetting(key, value) {
            if (!this.data.settings) this.data.settings = {};
            this.data.settings[key] = value;
            this.save();
        }

        getSetting(key, defaultValue) {
            return this.data.settings && this.data.settings[key] !== undefined
                ? this.data.settings[key]
                : defaultValue;
        }

        getFloatingButtonPosition() {
            const raw = this.getSetting('floatingButtonPosition', null);
            if (!raw || typeof raw !== 'object') {
                return null;
            }
            const left = Number(raw.left);
            const top = Number(raw.top);
            if (!Number.isFinite(left) || !Number.isFinite(top)) {
                return null;
            }
            return { left, top };
        }

        setFloatingButtonPosition(left, top) {
            if (!Number.isFinite(left) || !Number.isFinite(top)) {
                return;
            }
            const payload = {
                left: Math.round(left),
                top: Math.round(top)
            };
            this.setSetting('floatingButtonPosition', payload);
        }

        exportData(pretty = true) {
            const payload = {
                favorites: this.data.favorites || {},
                settings: this.data.settings || {},
                readingProgress: this.data.readingProgress || {},
                seriesNameOverrides: this.data.seriesNameOverrides || {}
            };
            return JSON.stringify(payload, pretty ? 2 : 0);
        }

        importData(jsonInput) {
            if (!jsonInput) {
                throw new Error('数据为空');
            }

            let parsed = jsonInput;
            if (typeof jsonInput === 'string') {
                try {
                    parsed = JSON.parse(jsonInput);
                } catch (e) {
                    throw new Error('JSON 解析失败');
                }
            }

            if (!parsed || typeof parsed !== 'object') {
                throw new Error('数据格式不正确');
            }

            const nextData = {
                favorites: typeof parsed.favorites === 'object' && parsed.favorites !== null ? parsed.favorites : {},
                settings: typeof parsed.settings === 'object' && parsed.settings !== null ? parsed.settings : {},
                readingProgress: typeof parsed.readingProgress === 'object' && parsed.readingProgress !== null ? parsed.readingProgress : {},
                seriesNameOverrides: typeof parsed.seriesNameOverrides === 'object' && parsed.seriesNameOverrides !== null ? parsed.seriesNameOverrides : {}
            };

            this.data = {
                ...this.data,
                ...nextData
            };
            this.ensureStructure();
            this.save();
        }
    }

    // =========================
    // 内容解析器
    // =========================
    class ContentParser {
        constructor() {
            this.threadId = this.getThreadId();
            this.threadTitle = this.getThreadTitle();
            this.authorUid = this.getAuthorUid();
            this.authorName = this.getAuthorName();
            this.seriesTitle = normalizeSeriesTitle(this.threadTitle);
            this.seriesKey = buildSeriesKey(this.threadTitle);
        }

        getThreadId() {
            const href = window.location.href;
            let match = href.match(/thread-(\d+)-/);
            if (match) {
                return match[1];
            }

            try {
                const url = new URL(href);
                const tidParam = url.searchParams.get('tid');
                if (tidParam) {
                    return tidParam;
                }
            } catch (e) {
                // ignore URL parsing errors and fallback to regex below
            }

            match = href.match(/[?&]tid=(\d+)/);
            return match ? match[1] : null;
        }

        getThreadTitle() {
            const titleElement = document.querySelector('#thread_subject');
            return titleElement ? titleElement.textContent.trim() : '';
        }

        getAuthorUid() {
            const firstPost = document.querySelector('#postlist > div[id^="post_"]');
            if (firstPost) {
                const authorLink = firstPost.querySelector('.favatar .authi a');
                if (authorLink) {
                    const href = authorLink.getAttribute('href');
                    let match = href.match(/uid=(\d+)/);
                    if (!match) {
                        match = href.match(/uid-(\d+)/);
                    }
                    return match ? match[1] : null;
                }
            }
            return null;
        }

        getAuthorName() {
            const firstPost = document.querySelector('#postlist > div[id^="post_"]');
            if (firstPost) {
                const authorLink = firstPost.querySelector('.favatar .authi a');
                if (authorLink) {
                    return authorLink.textContent.trim();
                }
            }
            return '';
        }

        // 获取楼主的所有帖子
        getAuthorPosts() {
            const posts = [];
            const postElements = document.querySelectorAll('#postlist > div[id^="post_"]');

            postElements.forEach((postEl) => {
                const authorLink = postEl.querySelector('.favatar .authi a');
                if (authorLink) {
                    const href = authorLink.getAttribute('href');
                    let match = href.match(/uid=(\d+)/);
                    if (!match) {
                        match = href.match(/uid-(\d+)/);
                    }

                    if (match && match[1] === this.authorUid) {
                        const postId = postEl.id.replace('post_', '');
                        const floorNum = this.getFloorNumber(postEl);
                        const content = postEl.querySelector('.t_f, .pcb');
                        const images = content ? Array.from(content.querySelectorAll('img.zoom, img[id^="aimg_"]')) : [];

                        // 统计图片总数(包括未加载的)
                        const imageUrls = images.map(img => {
                            return img.getAttribute('file') ||
                                   img.getAttribute('zoomfile') ||
                                   img.getAttribute('src') ||
                                   img.getAttribute('data-original') ||
                                   '';
                        }).filter(url => url && !url.includes('static/image'));

                        posts.push({
                            postId,
                            floor: floorNum,
                            element: postEl,
                            content: content,
                            images: imageUrls,
                            imageCount: imageUrls.length
                        });
                    }
                }
            });

            return posts;
        }

        getFloorNumber(postEl) {
            const floorElement = postEl.querySelector('.pi strong a em');
            if (floorElement) {
                const text = floorElement.textContent;
                const match = text.match(/(\d+)/);
                return match ? parseInt(match[1]) : 1;
            }

            const postnumLink = postEl.querySelector('[id^="postnum"]');
            if (postnumLink) {
                const em = postnumLink.querySelector('em');
                if (em) {
                    const match = em.textContent.match(/(\d+)/);
                    return match ? parseInt(match[1]) : 1;
                }
            }

            return 1;
        }

        extractSeriesName() {
            return normalizeSeriesTitle(this.threadTitle);
        }
    }

    // =========================
    // 阅读模式界面
    // =========================
    class ReaderUI {
        constructor(parser, dataStore) {
            this.parser = parser;
            this.dataStore = dataStore;
            this.isReaderMode = false;
            this.currentFloor = 0;
            this.currentImageIndex = 0;
            this.posts = [];
            this.allImages = [];
            this.directory = [];
            this.searchRetryTimer = null;
            this.readerContainer = null;
            const storedViewMode = dataStore.getSetting('viewMode', CONFIG.VIEW_MODES.SCROLL_DOWN);
            this.viewMode = LEGACY_VIEW_MODE_MAP[storedViewMode] || storedViewMode;
            if (!ALL_VIEW_MODES.has(this.viewMode)) {
                this.viewMode = CONFIG.VIEW_MODES.SCROLL_DOWN;
                this.dataStore.setSetting('viewMode', this.viewMode);
            } else if (this.viewMode !== storedViewMode) {
                this.dataStore.setSetting('viewMode', this.viewMode);
            }
            this.darkMode = dataStore.getSetting('darkMode', false);
            this.imageCache = new ImageCache(); // 图片缓存
            this.scrollHandler = null;
            this.scrollUpdateScheduled = false;
            this.currentScrollImageIndex = 0;
            this.lastFlipDirection = 'next';
            this.baseSeriesKey = buildSeriesKey(this.parser.threadTitle);
            const defaultSeriesName = this.parser.seriesTitle || normalizeSeriesTitle(this.parser.threadTitle) || this.parser.threadTitle || '未命名合集';
            const storedSeriesName = this.dataStore.getSeriesNameOverride(this.baseSeriesKey);
            this.currentSeriesName = (storedSeriesName || defaultSeriesName).trim() || defaultSeriesName;
            this.seriesTitle = this.currentSeriesName;
            this.seriesKey = buildSeriesKey(this.currentSeriesName);
            if (this.seriesKey !== this.baseSeriesKey && this.dataStore.getFavorite && this.dataStore.getFavorite(this.baseSeriesKey)) {
                this.dataStore.renameFavoriteSeries(this.baseSeriesKey, this.seriesKey, this.currentSeriesName);
            }
            const storedMainWidth = parseFloat(this.dataStore.getSetting('mainWidthRatio', CONFIG.DEFAULT_MAIN_WIDTH_RATIO));
            this.mainWidthRatio = Number.isFinite(storedMainWidth) ? storedMainWidth : CONFIG.DEFAULT_MAIN_WIDTH_RATIO;
            this.mainWidthRatio = Math.min(Math.max(this.mainWidthRatio, 0.5), 0.9);
            this.sidebarCollapsed = !!this.dataStore.getSetting('sidebarCollapsed', false);

            this.currentDirectoryCount = null;
            this.createFloatingButton();
            this.autoOpenIfRequested();
        }

        createFloatingButton() {
            const button = document.createElement('div');
            button.id = 'yamibo-reader-btn';
            button.innerHTML = ICONS.book;
            button.title = '开启阅读模式';

            this.makeDraggable(button);
            document.body.appendChild(button);
            this.floatingBtn = button;
            this.applyFloatingButtonPosition();
            this.handleWindowResize = () => this.updateFloatingButtonDockState();
            window.addEventListener('resize', this.handleWindowResize, { passive: true });
            button.addEventListener('mouseenter', () => this.handleFloatingButtonHover(true));
            button.addEventListener('mouseleave', () => this.handleFloatingButtonHover(false));
        }

        applyFloatingButtonPosition() {
            if (!this.floatingBtn) {
                return;
            }
            const saved = this.dataStore.getFloatingButtonPosition();
            const element = this.floatingBtn;
            if (saved) {
                element.style.left = `${saved.left}px`;
                element.style.top = `${saved.top}px`;
                element.style.right = 'auto';
                element.style.bottom = 'auto';
                element.style.transform = 'none';
            } else {
                element.style.left = 'auto';
                element.style.bottom = 'auto';
                element.style.right = '20px';
                element.style.top = '50%';
                element.style.transform = 'translateY(-50%)';
            }
            this.updateFloatingButtonDockState();
        }

        updateFloatingButtonDockState() {
            if (!this.floatingBtn) {
                return;
            }
            if (this.floatingBtn.dataset.dockExpanded === '1') {
                return;
            }
            const rect = this.floatingBtn.getBoundingClientRect();
            const threshold = 12;
            const nearLeft = rect.left <= threshold;
            const nearRight = window.innerWidth - rect.right <= threshold;
            const isLeftOnly = nearLeft && !nearRight;
            const isRightOnly = nearRight && !nearLeft;
            let dockState = '';
            if (isLeftOnly) {
                this.floatingBtn.classList.add('edge-left');
                this.floatingBtn.classList.remove('edge-right');
                dockState = 'left';
            } else if (isRightOnly) {
                this.floatingBtn.classList.add('edge-right');
                this.floatingBtn.classList.remove('edge-left');
                dockState = 'right';
            } else {
                this.floatingBtn.classList.remove('edge-left', 'edge-right');
            }
            this.applyFloatingButtonDockOffset(dockState);
        }

        applyFloatingButtonDockOffset(direction, options = {}) {
            const btn = this.floatingBtn;
            if (!btn) {
                return;
            }
            const { preserveState = false, force = false } = options;
            const previous = btn.dataset.dockState || '';
            if (!direction && preserveState && previous) {
                // fall through to restoration without clearing state
            } else if (previous === direction && !force) {
                return;
            }

            const hasStoredLeft = Object.prototype.hasOwnProperty.call(btn.dataset, 'restoreLeft');
            const hasStoredRight = Object.prototype.hasOwnProperty.call(btn.dataset, 'restoreRight');

            if (!direction) {
                if (hasStoredLeft) {
                    btn.style.left = btn.dataset.restoreLeft;
                }
                if (hasStoredRight) {
                    btn.style.right = btn.dataset.restoreRight;
                }
                if (!preserveState) {
                    delete btn.dataset.restoreLeft;
                    delete btn.dataset.restoreRight;
                    btn.dataset.dockState = '';
                }
                return;
            }

            if (!hasStoredLeft) {
                btn.dataset.restoreLeft = btn.style.left || '';
            }
            if (!hasStoredRight) {
                btn.dataset.restoreRight = btn.style.right || '';
            }

            const halfWidth = Math.round(btn.offsetWidth / 2);
            if (direction === 'left') {
                btn.style.left = `${-halfWidth}px`;
                btn.style.right = 'auto';
            } else if (direction === 'right') {
                btn.style.right = `${-halfWidth}px`;
                btn.style.left = 'auto';
            }

            btn.dataset.dockState = direction;
        }

        handleFloatingButtonHover(isHovering) {
            const btn = this.floatingBtn;
            if (!btn) {
                return;
            }
            const dockState = btn.dataset.dockState || '';
            if (!dockState) {
                return;
            }

            if (isHovering) {
                if (btn.dataset.dockExpanded === '1') {
                    return;
                }
                this.applyFloatingButtonDockOffset('', { preserveState: true });
                btn.classList.add('edge-expanded');
                btn.dataset.dockExpanded = '1';
            } else {
                if (btn.dataset.dockExpanded !== '1') {
                    return;
                }
                btn.classList.remove('edge-expanded');
                delete btn.dataset.dockExpanded;
                this.applyFloatingButtonDockOffset(dockState, { force: true });
            }
        }

        makeDraggable(element) {
            const DRAG_DELAY = 300;
            let isDragging = false;
            let dragTimeoutId = null;
            let pointerDownTime = 0;
            let pointerId = null;
            let startX = 0;
            let startY = 0;
            let startLeft = 0;
            let startTop = 0;

            const clearDragTimer = () => {
                if (dragTimeoutId !== null) {
                    clearTimeout(dragTimeoutId);
                    dragTimeoutId = null;
                }
            };

            const beginDragging = () => {
                if (isDragging) return;
                isDragging = true;
                element.style.transition = 'none';
                element.style.transform = 'none';
                element.style.left = `${startLeft}px`;
                element.style.top = `${startTop}px`;
                element.style.right = 'auto';
                element.style.cursor = 'move';
                document.body.classList.add('reader-btn-dragging');
                element.classList.remove('edge-left', 'edge-right');
                if (element.dataset.dockExpanded === '1') {
                    this.handleFloatingButtonHover(false);
                }
            };

            const endDragging = () => {
                if (!isDragging) return;
                isDragging = false;
                const rect = element.getBoundingClientRect();
                this.dataStore.setFloatingButtonPosition(rect.left, rect.top);
                this.applyFloatingButtonPosition();
                element.style.transition = '';
                element.style.cursor = '';
                document.body.classList.remove('reader-btn-dragging');
                this.updateFloatingButtonDockState();
            };

            element.addEventListener('pointerdown', (e) => {
                if (typeof e.button === 'number' && e.button !== 0) {
                    return;
                }

                pointerDownTime = performance.now();
                pointerId = e.pointerId;
                startX = e.clientX;
                startY = e.clientY;
                const rect = element.getBoundingClientRect();
                startLeft = rect.left;
                startTop = rect.top;

                clearDragTimer();
                dragTimeoutId = window.setTimeout(() => {
                    if (pointerId !== null) {
                        beginDragging();
                    }
                }, DRAG_DELAY);

                element.setPointerCapture?.(pointerId);
                e.preventDefault();
            });

            element.addEventListener('pointermove', (e) => {
                if (!isDragging) {
                    return;
                }

                const deltaX = e.clientX - startX;
                const deltaY = e.clientY - startY;
                let newLeft = startLeft + deltaX;
                let newTop = startTop + deltaY;

                newLeft = Math.max(0, Math.min(window.innerWidth - element.offsetWidth, newLeft));
                newTop = Math.max(0, Math.min(window.innerHeight - element.offsetHeight, newTop));

                element.style.left = `${newLeft}px`;
                element.style.top = `${newTop}px`;
                element.style.right = 'auto';
            });

            const handlePointerEnd = (e) => {
                if (pointerId !== null && element.releasePointerCapture) {
                    try {
                        element.releasePointerCapture(pointerId);
                    } catch (err) {
                        /* ignore */
                    }
                }

                clearDragTimer();

                if (isDragging) {
                    endDragging();
                } else if (pointerDownTime > 0) {
                    const elapsed = performance.now() - pointerDownTime;
                    if (elapsed < DRAG_DELAY) {
                        this.toggleReaderMode();
                    }
                }

                pointerDownTime = 0;
                pointerId = null;
            };

            element.addEventListener('pointerup', handlePointerEnd);
            element.addEventListener('pointercancel', handlePointerEnd);
        }

        toggleReaderMode() {
            if (!this.isReaderMode) {
                this.enterReaderMode();
            } else {
                this.exitReaderMode();
            }
        }

        enterReaderMode() {
            this.isReaderMode = true;
            this.posts = this.parser.getAuthorPosts();

            if (this.posts.length === 0) {
                alert('未找到楼主的帖子内容');
                return;
            }

            // 收集所有图片
            this.allImages = [];
            this.posts.forEach(post => {
                post.images.forEach(img => {
                    this.allImages.push({
                        url: img,
                        floor: post.floor,
                        loaded: false
                    });
                });
            });

            document.body.classList.add('yamibo-reader-active');
            if (this.darkMode) {
                document.body.classList.add('dark-mode');
            }

            this.createReaderContainer();

            const progress = this.dataStore.getProgress(this.parser.threadId);
            if (progress) {
                this.currentFloor = progress.floor;
            }

            this.renderContent();
            this.loadDirectory();
        }

        exitReaderMode() {
            this.isReaderMode = false;
            document.body.classList.remove('yamibo-reader-active', 'dark-mode', 'reader-resizing');
            this.teardownScrollInteractions();

            const container = document.getElementById('yamibo-reader-container');
            if (container) {
                container.classList.remove('resizing');
                container.remove();
            }
            this.readerContainer = null;
        }

        createReaderContainer() {
            const currentPreload = this.dataStore.getSetting('preloadCount', CONFIG.PRELOAD_COUNT);
            const cachedCount = this.imageCache.cache.size;
            const container = document.createElement('div');
            container.id = 'yamibo-reader-container';
            container.innerHTML = `
                <div class="reader-main" id="reader-main">
                    <div class="reader-main-inner">
                        <div class="reader-toolbar">
                            <div class="toolbar-left">
                                <button id="view-mode-btn" class="icon-btn" title="切换阅读模式">
                                    ${ICONS.viewMode}
                                </button>
                                <button id="dark-mode-btn" class="icon-btn" title="切换暗色模式">
                                    ${this.darkMode ? ICONS.lightMode : ICONS.darkMode}
                                </button>
                            </div>
                            <div class="toolbar-center">
                                <div class="reader-controls">
                                    <button id="prev-floor" class="nav-btn" title="上一页">
                                        ${ICONS.arrowLeft}
                                    </button>
                                    <span id="floor-indicator">1 / ${this.posts.length}</span>
                                    <button id="next-floor" class="nav-btn" title="下一页">
                                        ${ICONS.arrowRight}
                                    </button>
                                </div>
                            </div>
                            <div class="toolbar-right">
                                <button id="close-reader-top" class="icon-btn" title="关闭阅读模式">
                                    ${ICONS.close}
                                </button>
                                <button id="toggle-sidebar-btn" class="icon-btn" title="收起右侧菜单" aria-pressed="false">
                                    ${ICONS.chevronsRight}
                                </button>
                            </div>
                        </div>
                        <div class="reader-content-wrapper">
                            <div class="reader-content" id="reader-content" data-view-mode="${this.viewMode}">
                                <div class="content-loading">正在加载图片...</div>
                            </div>
                        </div>
                    </div>
                </div>
                <div class="reader-resizer" id="reader-resizer"></div>
                <div class="reader-sidebar">
                        <div class="sidebar-top">
                            <div class="sidebar-tabs">
                            <button class="tab-btn active" data-tab="directory">目录</button>
                            <button class="tab-btn" data-tab="comments">评论</button>
                            <button class="tab-btn" data-tab="favorites">收藏</button>
                        </div>
                    </div>
                    <div class="sidebar-content">
                        <div class="tab-panel active" id="directory-panel">
                            <div class="directory-search">
                                <input type="text" placeholder="搜索系列..." id="series-search">
                                <button id="search-btn" class="icon-btn">${ICONS.search}</button>
                                <button id="favorite-btn" class="icon-btn" title="收藏本系列">
                                    ${this.dataStore.isSeriesFavorited(this.seriesKey) ? ICONS.bookmarkFilled : ICONS.bookmark}
                                </button>
                            </div>
                            <div class="directory-list" id="directory-list">
                                <div class="loading">正在加载目录...</div>
                            </div>
                        </div>
                        <div class="tab-panel" id="comments-panel">
                            <div class="comments-list" id="comments-list">
                                <div class="loading">加载评论中...</div>
                            </div>
                        </div>
                        <div class="tab-panel" id="favorites-panel">
                            <div class="favorites-list" id="favorites-list">
                                <div class="empty-message">暂无收藏</div>
                            </div>
                        </div>
                    </div>
                </div>
                <div id="view-mode-menu" class="popup-menu" style="display: none;">
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.SCROLL_DOWN}">
                        <span>滑动-下</span>
                    </div>
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.SCROLL_LEFT}">
                        <span>滑动-左</span>
                    </div>
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.SCROLL_RIGHT}">
                        <span>滑动-右</span>
                    </div>
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.FLIP_LEFT_SINGLE}">
                        <span>翻页-左-单页</span>
                    </div>
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.FLIP_LEFT_DOUBLE}">
                        <span>翻页-左-双页</span>
                    </div>
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.FLIP_RIGHT_SINGLE}">
                        <span>翻页-右-单页</span>
                    </div>
                    <div class="menu-item" data-mode="${CONFIG.VIEW_MODES.FLIP_RIGHT_DOUBLE}">
                        <span>翻页-右-双页</span>
                    </div>
                    <div class="menu-divider"></div>
                    <div class="menu-settings">
                        <div class="menu-section-title">阅读设置</div>
                        <label class="menu-range-label" for="menu-preload-slider">
                            预加载图片
                            <span id="menu-preload-value">${currentPreload}</span>
                        </label>
                        <input type="range" id="menu-preload-slider" min="0" max="10" step="1" value="${currentPreload}">
                        <label class="menu-input-label" for="menu-search-per-page">每页搜索数量</label>
                        <input
                            type="number"
                            id="menu-search-per-page"
                            class="menu-number-input"
                            min="${CONFIG.SEARCH_RESULTS_PER_PAGE_MIN}"
                            max="${CONFIG.SEARCH_RESULTS_PER_PAGE_MAX}"
                            step="10"
                            value="${this.getSearchResultsPerPage()}">
                        <div class="menu-hint">少于该数量时停止翻页</div>
                        <div class="menu-cache-info">已缓存: <span id="menu-cache-count">${cachedCount}</span> 张</div>
                        <button id="menu-clear-cache" class="menu-action-btn">清除图片缓存</button>
                        <button id="menu-data-transfer" class="menu-action-btn">导入/导出</button>
                    </div>
                </div>
            `;

            document.body.appendChild(container);
            this.readerContainer = container;
            const searchInput = document.getElementById('series-search');
            if (searchInput) {
                searchInput.value = this.currentSeriesName;
            }
            this.applyLayoutSizing();
            this.bindReaderEvents();
            this.setupResizer();
            this.applySidebarState();
        }

        bindReaderEvents() {
            document.getElementById('prev-floor').addEventListener('click', () => this.prevFloor());
            document.getElementById('next-floor').addEventListener('click', () => this.nextFloor());

            // 点击内容区域左右两侧翻页
            const contentDiv = document.getElementById('reader-content');
            const applyCursorZone = (zone) => {
                if (!contentDiv) {
                    return;
                }
                contentDiv.classList.toggle('reader-content-hover', zone !== 'center');
                contentDiv.classList.toggle('cursor-left', zone === 'left');
                contentDiv.classList.toggle('cursor-right', zone === 'right');
            };

            contentDiv.addEventListener('click', (e) => {
                const rect = contentDiv.getBoundingClientRect();
                const clickX = e.clientX - rect.left;
                const width = rect.width;
                const isScrollLeft = this.viewMode === CONFIG.VIEW_MODES.SCROLL_LEFT;
                // 左侧 30% 区域
                if (clickX < width * 0.3) {
                    if (isScrollLeft) {
                        this.nextFloor();
                    } else {
                        this.prevFloor();
                    }
                }
                // 右侧 30% 区域
                else if (clickX > width * 0.7) {
                    if (isScrollLeft) {
                        this.prevFloor();
                    } else {
                        this.nextFloor();
                    }
                }
            });

            const updateHoverCursor = (event) => {
                const rect = contentDiv.getBoundingClientRect();
                const hoverX = event.clientX - rect.left;
                const width = rect.width;
                if (hoverX < width * 0.3) {
                    applyCursorZone('left');
                } else if (hoverX > width * 0.7) {
                    applyCursorZone('right');
                } else {
                    applyCursorZone('center');
                }
            };

            contentDiv.addEventListener('mousemove', updateHoverCursor);
            contentDiv.addEventListener('mouseleave', () => {
                applyCursorZone('center');
            });

            document.addEventListener('keydown', (e) => {
                if (!this.isReaderMode) return;

                if (e.key === 'ArrowLeft') {
                    this.prevFloor();
                } else if (e.key === 'ArrowRight') {
                    this.nextFloor();
                } else if (e.key === 'Escape') {
                    this.exitReaderMode();
                }
            });

            document.querySelectorAll('.tab-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const tab = e.target.closest('.tab-btn').dataset.tab;
                    this.switchTab(tab);
                });
            });

            document.getElementById('favorite-btn').addEventListener('click', () => this.toggleFavorite());
            document.getElementById('close-reader-top').addEventListener('click', () => this.exitReaderMode());
            const toggleSidebarBtn = document.getElementById('toggle-sidebar-btn');
            if (toggleSidebarBtn) {
                toggleSidebarBtn.addEventListener('click', () => this.toggleSidebar());
            }
            document.getElementById('search-btn').addEventListener('click', () => this.searchDirectory());
            const searchInputEl = document.getElementById('series-search');
            if (searchInputEl) {
                searchInputEl.addEventListener('keydown', (event) => {
                    if (event.key === 'Enter') {
                        event.preventDefault();
                        this.searchDirectory();
                    }
                });
                searchInputEl.addEventListener('blur', () => {
                    const value = searchInputEl.value.trim();
                    if (!value) {
                        searchInputEl.value = this.currentSeriesName;
                        return;
                    }
                    this.updateSeriesName(value, { persistOverride: true });
                });
            }

            // 暗色模式切换
            document.getElementById('dark-mode-btn').addEventListener('click', () => this.toggleDarkMode());

            // 阅读模式切换
            document.getElementById('view-mode-btn').addEventListener('click', (e) => {
                const menu = document.getElementById('view-mode-menu');
                const rect = e.target.getBoundingClientRect();
                menu.style.top = `${rect.bottom + 5}px`;
                menu.style.left = `${rect.left}px`;

                this.syncMenuSettingControls();

                menu.style.display = menu.style.display === 'none' || menu.style.display === '' ? 'block' : 'none';
            });

            document.querySelectorAll('#view-mode-menu .menu-item').forEach(item => {
                item.addEventListener('click', (e) => {
                    const mode = e.target.closest('.menu-item').dataset.mode;
                    this.changeViewMode(mode);
                    document.getElementById('view-mode-menu').style.display = 'none';
                });
            });

            const dataTransferBtn = document.getElementById('menu-data-transfer');
            if (dataTransferBtn) {
                dataTransferBtn.addEventListener('click', (event) => {
                    event.preventDefault();
                    event.stopPropagation();
                    const menu = document.getElementById('view-mode-menu');
                    if (menu) {
                        menu.style.display = 'none';
                    }
                    this.showDataTransferDialog();
                });
            }

            // 点击其他地方关闭菜单
            document.addEventListener('click', (e) => {
                const menu = document.getElementById('view-mode-menu');
                const btn = document.getElementById('view-mode-btn');
                if (menu && !menu.contains(e.target) && !btn.contains(e.target)) {
                    menu.style.display = 'none';
                }

                const preloadSlider = document.getElementById('menu-preload-slider');
                const preloadValue = document.getElementById('menu-preload-value');
                const cacheCountSpan = document.getElementById('menu-cache-count');
                const clearCacheBtn = document.getElementById('menu-clear-cache');
                const perPageInput = document.getElementById('menu-search-per-page');

                if (preloadSlider && preloadValue && !preloadSlider.dataset.bound) {
                    preloadSlider.dataset.bound = 'true';
                    preloadSlider.addEventListener('input', (event) => {
                        const value = Number(event.target.value);
                        preloadValue.textContent = value;
                        this.dataStore.setSetting('preloadCount', value);
                    });
                }

                if (perPageInput && !perPageInput.dataset.bound) {
                    perPageInput.dataset.bound = 'true';
                    perPageInput.addEventListener('change', (event) => {
                        const normalized = normalizeSearchResultsPerPageValue(event.target.value);
                        event.target.value = normalized;
                        this.dataStore.setSetting('searchResultsPerPage', normalized);
                    });
                }

                if (clearCacheBtn && cacheCountSpan && !clearCacheBtn.dataset.bound) {
                    clearCacheBtn.dataset.bound = 'true';
                    clearCacheBtn.addEventListener('click', () => {
                        this.imageCache.clear();
                        cacheCountSpan.textContent = '0';
                    });
                }
            });

            this.updateFavoriteButton();
        }

        applyLayoutSizing() {
            const container = document.getElementById('yamibo-reader-container');
            if (!container) {
                return;
            }

            const ratio = Math.min(Math.max(this.mainWidthRatio, 0.5), 0.9);
            this.mainWidthRatio = ratio;
            const sidebarRatio = 1 - ratio;
            const mainWidthPercent = (ratio * 100).toFixed(3) + '%';
            const sidebarWidthPercent = (sidebarRatio * 100).toFixed(3) + '%';
            const contentScale = (ratio / CONFIG.DEFAULT_MAIN_WIDTH_RATIO).toFixed(3);

            container.style.setProperty('--main-width', mainWidthPercent);
            container.style.setProperty('--sidebar-width', sidebarWidthPercent);
            container.style.setProperty('--content-scale', contentScale);
        }

        setupResizer() {
            const resizer = document.getElementById('reader-resizer');
            const container = document.getElementById('yamibo-reader-container');
            if (!resizer || !container) {
                return;
            }

            const handlePointerMove = (event) => {
                const rect = container.getBoundingClientRect();
                if (rect.width <= 0) {
                    return;
                }
                let ratio = (event.clientX - rect.left) / rect.width;
                ratio = Math.min(0.9, Math.max(0.5, ratio));
                this.mainWidthRatio = ratio;
                this.applyLayoutSizing();
            };

            const handlePointerUp = (event) => {
                if (event?.pointerId !== undefined) {
                    resizer.releasePointerCapture?.(event.pointerId);
                }
                window.removeEventListener('pointermove', handlePointerMove);
                window.removeEventListener('pointerup', handlePointerUp);
                window.removeEventListener('pointercancel', handlePointerUp);
                container.classList.remove('resizing');
                document.body.classList.remove('reader-resizing');
                const persistedRatio = Number(this.mainWidthRatio.toFixed(3));
                this.mainWidthRatio = persistedRatio;
                this.dataStore.setSetting('mainWidthRatio', persistedRatio);
            };

            resizer.addEventListener('pointerdown', (event) => {
                event.preventDefault();
                if (event.pointerId !== undefined) {
                    resizer.setPointerCapture?.(event.pointerId);
                }
                container.classList.add('resizing');
                document.body.classList.add('reader-resizing');
                window.addEventListener('pointermove', handlePointerMove);
                window.addEventListener('pointerup', handlePointerUp);
                window.addEventListener('pointercancel', handlePointerUp);
            });
        }

        toggleSidebar() {
            this.sidebarCollapsed = !this.sidebarCollapsed;
            this.dataStore.setSetting('sidebarCollapsed', this.sidebarCollapsed);
            this.applySidebarState();
        }

        applySidebarState() {
            const container = this.readerContainer || document.getElementById('yamibo-reader-container');
            if (!container) {
                return;
            }
            container.classList.toggle('sidebar-collapsed', this.sidebarCollapsed);
            const toggleBtn = document.getElementById('toggle-sidebar-btn');
            if (toggleBtn) {
                toggleBtn.innerHTML = this.sidebarCollapsed ? ICONS.chevronsLeft : ICONS.chevronsRight;
                toggleBtn.title = this.sidebarCollapsed ? '展开右侧菜单' : '收起右侧菜单';
                toggleBtn.setAttribute('aria-pressed', String(this.sidebarCollapsed));
            }
        }

        isScrollMode() {
            return this.viewMode === CONFIG.VIEW_MODES.SCROLL_DOWN ||
                this.viewMode === CONFIG.VIEW_MODES.SCROLL_LEFT ||
                this.viewMode === CONFIG.VIEW_MODES.SCROLL_RIGHT;
        }

        isHorizontalScrollMode() {
            return this.viewMode === CONFIG.VIEW_MODES.SCROLL_LEFT ||
                this.viewMode === CONFIG.VIEW_MODES.SCROLL_RIGHT;
        }

        isFlipMode() {
            return this.viewMode === CONFIG.VIEW_MODES.FLIP_LEFT_SINGLE ||
                this.viewMode === CONFIG.VIEW_MODES.FLIP_LEFT_DOUBLE ||
                this.viewMode === CONFIG.VIEW_MODES.FLIP_RIGHT_SINGLE ||
                this.viewMode === CONFIG.VIEW_MODES.FLIP_RIGHT_DOUBLE;
        }

        isFlipLeftDirection() {
            return this.viewMode === CONFIG.VIEW_MODES.FLIP_LEFT_SINGLE ||
                this.viewMode === CONFIG.VIEW_MODES.FLIP_LEFT_DOUBLE;
        }

        isFlipDouble() {
            return this.viewMode === CONFIG.VIEW_MODES.FLIP_LEFT_DOUBLE ||
                this.viewMode === CONFIG.VIEW_MODES.FLIP_RIGHT_DOUBLE;
        }

        scrollByPage(direction) {
            if (!this.isHorizontalScrollMode()) {
                return false;
            }

            const contentDiv = document.getElementById('reader-content');
            if (!contentDiv) {
                return false;
            }

            const containers = Array.from(contentDiv.querySelectorAll('.image-container'));
            if (containers.length === 0) {
                return false;
            }

            this.updateScrollCurrentPage(contentDiv);
            let targetIndex = this.currentScrollImageIndex + direction;
            targetIndex = Math.max(0, Math.min(containers.length - 1, targetIndex));
            if (targetIndex === this.currentScrollImageIndex) {
                return false;
            }

            const target = containers[targetIndex];
            if (target) {
                target.scrollIntoView({
                    behavior: 'smooth',
                    block: 'nearest',
                    inline: 'center'
                });
                this.currentScrollImageIndex = targetIndex;
                this.currentImageIndex = targetIndex;
                this.updateFloorIndicator();
            }

            return true;
        }

        navigateFlip(direction) {
            if (direction === 0 || !this.isFlipMode()) {
                return;
            }

            const post = this.posts[this.currentFloor];
            if (!post) {
                return;
            }

            this.lastFlipDirection = direction > 0 ? 'next' : 'prev';
            const step = this.isFlipDouble() ? 2 : 1;

            if (direction > 0) {
                if (post.images.length > 0 && this.currentImageIndex + step < post.images.length) {
                    this.currentImageIndex += step;
                    this.currentScrollImageIndex = this.currentImageIndex;
                    this.renderContent();
                } else if (this.currentFloor < this.posts.length - 1) {
                    this.currentFloor++;
                    this.currentImageIndex = 0;
                    this.currentScrollImageIndex = 0;
                    this.renderContent();
                }
            } else {
                if (this.currentImageIndex > 0) {
                    this.currentImageIndex = Math.max(0, this.currentImageIndex - step);
                    this.currentScrollImageIndex = this.currentImageIndex;
                    this.renderContent();
                } else if (this.currentFloor > 0) {
                    this.currentFloor--;
                    const prevPost = this.posts[this.currentFloor];
                    if (prevPost && prevPost.images.length > 0) {
                        const remainder = prevPost.images.length % step;
                        if (remainder === 0) {
                            this.currentImageIndex = Math.max(0, prevPost.images.length - step);
                        } else {
                            this.currentImageIndex = Math.max(0, prevPost.images.length - remainder);
                        }
                    } else {
                        this.currentImageIndex = 0;
                    }
                    this.currentScrollImageIndex = this.currentImageIndex;
                    this.renderContent();
                }
            }
        }

        toggleDarkMode() {
            this.darkMode = !this.darkMode;
            this.dataStore.setSetting('darkMode', this.darkMode);
            document.body.classList.toggle('dark-mode', this.darkMode);

            const btn = document.getElementById('dark-mode-btn');
            btn.innerHTML = this.darkMode ? ICONS.lightMode : ICONS.darkMode;
        }

        changeViewMode(mode) {
            this.viewMode = mode;
            this.dataStore.setSetting('viewMode', mode);
            this.lastFlipDirection = 'next';

            // 重置图片索引
            this.currentImageIndex = 0;

            const contentDiv = document.getElementById('reader-content');
            contentDiv.setAttribute('data-view-mode', mode);

            this.renderContent();
        }

        async renderContent() {
            const post = this.posts[this.currentFloor];
            if (!post) return;

            const contentDiv = document.getElementById('reader-content');
            if (contentDiv) {
                contentDiv.setAttribute('data-view-mode', this.viewMode);
            }

            if (this.isFlipMode()) {
                this.teardownScrollInteractions();
                this.renderPageMode(contentDiv, post, this.isFlipDouble());
            } else {
                this.renderScrollMode(contentDiv, post);
            }

            this.preloadImages();
            this.updateFloorIndicator();
            this.dataStore.setProgress(this.parser.threadId, this.currentFloor);

            // 如果已收藏,更新阅读进度
            if (this.dataStore.isSeriesFavorited(this.seriesKey)) {
                this.dataStore.updateFavoriteChapter(this.seriesKey, {
                    seriesTitle: this.seriesTitle,
                    author: this.parser.authorName || '未知作者',
                    threadId: this.parser.threadId,
                    chapterTitle: this.parser.threadTitle,
                    url: window.location.href,
                    currentFloor: this.currentFloor + 1,
                    totalFloors: this.posts.length
                });
            }
        }

        updateFloorIndicator() {
            const post = this.posts[this.currentFloor];
            const indicator = document.getElementById('floor-indicator');

            if (this.isFlipMode()) {
                // 翻页模式显示图片索引
                if (post && post.images.length > 0) {
                    const step = this.isFlipDouble() ? 2 : 1;
                    const startIndex = this.currentImageIndex;
                    const endIndex = Math.min(startIndex + step, post.images.length);
                    indicator.textContent = `第${this.currentFloor + 1}楼 ${startIndex + 1}-${endIndex}/${post.images.length}图`;
                } else {
                    indicator.textContent = `${this.currentFloor + 1} / ${this.posts.length}`;
                }
            } else {
                // 滚动模式显示当前可视图片索引
                if (post && post.images.length > 0) {
                    const current = Math.min(this.currentScrollImageIndex + 1, post.images.length);
                    indicator.textContent = `第${this.currentFloor + 1}楼 ${current}/${post.images.length}图`;
                } else {
                    indicator.textContent = `${this.currentFloor + 1} / ${this.posts.length}`;
                }
            }
        }

        async renderScrollMode(contentDiv, post) {
            // 不清空内容,只更新图片
            const existingImages = contentDiv.querySelectorAll('.image-container');
            const existingCount = existingImages.length;

            // 如果已经有相同楼层的图片,不重新渲染
            if (existingCount === post.images.length &&
                contentDiv.dataset.floor === post.floor.toString()) {
                return;
            }

            contentDiv.innerHTML = '';
            contentDiv.dataset.floor = post.floor;
            this.currentScrollImageIndex = 0;
            this.currentImageIndex = 0;

            if (post.images.length > 0) {
                for (let index = 0; index < post.images.length; index++) {
                    const imgSrc = post.images[index];
                    const imgContainer = document.createElement('div');
                    imgContainer.className = 'image-container';

                    // 检查缓存
                    if (this.imageCache.has(imgSrc)) {
                        const cachedUrl = this.imageCache.get(imgSrc);
                        const img = document.createElement('img');
                        img.src = cachedUrl;
                        img.alt = `第 ${post.floor} 楼 - 图片 ${index + 1}`;
                        imgContainer.appendChild(img);
                    } else {
                        // 显示加载状态
                        const loader = document.createElement('div');
                        loader.className = 'image-loader';
                        loader.textContent = `加载中 ${index + 1}/${post.images.length}...`;
                        imgContainer.appendChild(loader);

                        // 加载并缓存图片
                        this.loadAndCacheImage(imgSrc, imgContainer, loader, post.floor, index + 1, post.images.length);
                    }

                    contentDiv.appendChild(imgContainer);
                }
            } else if (post.content) {
                const textContent = post.content.cloneNode(true);
                textContent.querySelectorAll('.pstatus, .psign, .pattm').forEach(el => el.remove());
                contentDiv.appendChild(textContent);
            }

            this.setupScrollInteractions(contentDiv, post);
        }

        setupScrollInteractions(contentDiv, post) {
            this.teardownScrollInteractions();

            if (!post || !Array.isArray(post.images) || post.images.length === 0) {
                this.updateFloorIndicator();
                return;
            }

            const containers = contentDiv.querySelectorAll('.image-container');
            containers.forEach((container, index) => {
                container.dataset.imageIndex = index;
            });

            this.scrollHandler = () => {
                if (this.scrollUpdateScheduled) return;
                this.scrollUpdateScheduled = true;
                requestAnimationFrame(() => {
                    this.scrollUpdateScheduled = false;
                    this.updateScrollCurrentPage(contentDiv);
                });
            };

            contentDiv.addEventListener('scroll', this.scrollHandler, { passive: true });
            this.updateScrollCurrentPage(contentDiv);
        }

        teardownScrollInteractions() {
            const contentDiv = document.getElementById('reader-content');
            if (contentDiv && this.scrollHandler) {
                contentDiv.removeEventListener('scroll', this.scrollHandler);
            }
            this.scrollHandler = null;
            this.scrollUpdateScheduled = false;
        }

        updateScrollCurrentPage(contentDiv) {
            const containers = Array.from(contentDiv.querySelectorAll('.image-container'));
            if (containers.length === 0) {
                this.currentScrollImageIndex = 0;
                this.currentImageIndex = 0;
                this.updateFloorIndicator();
                return;
            }

            const rect = contentDiv.getBoundingClientRect();
            const referenceX = rect.left + rect.width / 2;
            const referenceY = rect.top + rect.height / 2;

            let closestIndex = 0;
            let minDistance = Infinity;

            containers.forEach((container, index) => {
                const containerRect = container.getBoundingClientRect();
                const centerX = containerRect.left + containerRect.width / 2;
                const centerY = containerRect.top + containerRect.height / 2;
                const distance = this.viewMode === CONFIG.VIEW_MODES.SCROLL_DOWN
                    ? Math.abs(centerY - referenceY)
                    : Math.abs(centerX - referenceX);

                if (distance < minDistance) {
                    minDistance = distance;
                    closestIndex = index;
                }
            });

            this.currentScrollImageIndex = closestIndex;
            this.currentImageIndex = closestIndex;
            this.updateFloorIndicator();
        }

        autoOpenIfRequested() {
            const raw = GM_getValue(CONFIG.AUTO_OPEN_KEY, '');
            if (!raw) {
                return;
            }

            let data = null;
            try {
                data = JSON.parse(raw);
            } catch (e) {
                console.warn('[阅读模式] 自动开启配置解析失败:', e);
            }

            GM_setValue(CONFIG.AUTO_OPEN_KEY, '');

            if (!data || !data.enabled) {
                return;
            }

            if (data.target && data.target !== this.parser.threadId) {
                return;
            }

            if (data.timestamp && Date.now() - data.timestamp > 60000) {
                return;
            }

            if (data.seriesName && typeof data.seriesName === 'string') {
                this.updateSeriesName(data.seriesName, { persistOverride: true });
            }

            setTimeout(() => {
                if (!this.isReaderMode) {
                    this.enterReaderMode();
                }
            }, 100);
        }

        async renderPageMode(contentDiv, post, isDouble) {
            // 翻页模式需要维护当前图片索引
            if (!Number.isInteger(this.currentImageIndex) || this.currentImageIndex < 0) {
                this.currentImageIndex = 0;
            }
            this.currentScrollImageIndex = this.currentImageIndex;

            // 清空内容
            contentDiv.innerHTML = '';
            contentDiv.dataset.floor = post.floor;

            if (post.images.length > 0) {
                const imagesToShow = isDouble ? 2 : 1;
                const startIndex = this.currentImageIndex;
                const endIndex = Math.min(startIndex + imagesToShow, post.images.length);

                for (let i = startIndex; i < endIndex; i++) {
                    const imgSrc = post.images[i];
                    const imgContainer = document.createElement('div');
                    imgContainer.className = 'image-container';

                    // 检查缓存
                    if (this.imageCache.has(imgSrc)) {
                        const cachedUrl = this.imageCache.get(imgSrc);
                        const img = document.createElement('img');
                        img.src = cachedUrl;
                        img.alt = `第 ${post.floor} 楼 - 图片 ${i + 1}`;
                        imgContainer.appendChild(img);
                    } else {
                        // 显示加载状态
                        const loader = document.createElement('div');
                        loader.className = 'image-loader';
                        loader.textContent = `加载中 ${i + 1}/${post.images.length}...`;
                        imgContainer.appendChild(loader);

                        // 加载并缓存图片
                        this.loadAndCacheImage(imgSrc, imgContainer, loader, post.floor, i + 1, post.images.length);
                    }

                    contentDiv.appendChild(imgContainer);
                    this.applyFlipAnimation(imgContainer);
                }
            } else if (post.content) {
                const textContent = post.content.cloneNode(true);
                textContent.querySelectorAll('.pstatus, .psign, .pattm').forEach(el => el.remove());
                contentDiv.appendChild(textContent);
            }
        }

        applyFlipAnimation(container) {
            if (!container || !this.isFlipMode()) {
                return;
            }
            const direction = this.lastFlipDirection === 'prev' ? 'prev' : 'next';
            const baseClass = direction === 'prev' ? 'flip-slide-enter-prev' : 'flip-slide-enter-next';
            container.classList.add('flip-slide-enter', baseClass);
            const cleanup = () => {
                container.classList.remove('flip-slide-enter', 'flip-slide-enter-next', 'flip-slide-enter-prev');
                container.removeEventListener('animationend', cleanup);
            };
            container.addEventListener('animationend', cleanup);
        }

        async loadAndCacheImage(imgSrc, container, loader, floor, index, total) {
            try {
                const cachedUrl = await this.imageCache.load(imgSrc);
                const img = document.createElement('img');
                img.src = cachedUrl;
                img.alt = `第 ${floor} 楼 - 图片 ${index}`;

                img.onload = () => {
                    if (loader && loader.parentNode) {
                        loader.remove();
                    }
                    container.appendChild(img);
                };

                img.onerror = () => {
                    if (loader) {
                        loader.textContent = '加载失败';
                        loader.classList.add('error');
                    }
                };
            } catch (e) {
                console.error('Failed to load image:', imgSrc, e);
                if (loader) {
                    loader.textContent = '加载失败';
                    loader.classList.add('error');
                }
            }
        }

        preloadImages() {
            // 获取预加载数量配置(按图片计数)
            const preloadCount = this.dataStore.getSetting('preloadCount', CONFIG.PRELOAD_COUNT);

            if (preloadCount === 0) {
                return;
            }

            let scheduledCount = 0;
            const totalPosts = this.posts.length;
            // 从当前楼层的下一张图片开始,逐张预加载
            for (let floorIndex = this.currentFloor; floorIndex < totalPosts && scheduledCount < preloadCount; floorIndex++) {
                const post = this.posts[floorIndex];
                if (!post || !Array.isArray(post.images) || post.images.length === 0) {
                    continue;
                }

                const startImageIndex = floorIndex === this.currentFloor ? (this.currentImageIndex || 0) + 1 : 0;
                if (startImageIndex >= post.images.length) {
                    continue;
                }

                for (let imgIndex = startImageIndex; imgIndex < post.images.length && scheduledCount < preloadCount; imgIndex++) {
                    const imgSrc = post.images[imgIndex];
                    if (!imgSrc) {
                        continue;
                    }

                    if (this.imageCache.has(imgSrc)) {
                        continue;
                    }

                    this.imageCache.load(imgSrc).then(() => {
                    }).catch(e => {
                        console.error(`[预加载] ✗ 失败: 第${floorIndex + 1}楼-第${imgIndex + 1}张`, e);
                    });

                    scheduledCount++;
                }
            }
        }

        prevFloor() {
            if (this.isHorizontalScrollMode() && this.scrollByPage(-1)) {
                return;
            }

            if (this.isFlipMode()) {
                const stepDirection = this.isFlipLeftDirection() ? 1 : -1;
                this.navigateFlip(stepDirection);
                return;
            }

            if (this.currentFloor > 0) {
                this.currentFloor--;
                this.currentImageIndex = 0;
                this.currentScrollImageIndex = 0;
                this.renderContent();
            }
        }

        nextFloor() {
            if (this.isHorizontalScrollMode() && this.scrollByPage(1)) {
                return;
            }

            if (this.isFlipMode()) {
                const stepDirection = this.isFlipLeftDirection() ? -1 : 1;
                this.navigateFlip(stepDirection);
                return;
            }

            if (this.currentFloor < this.posts.length - 1) {
                this.currentFloor++;
                this.currentImageIndex = 0;
                this.currentScrollImageIndex = 0;
                this.renderContent();
            }
        }

        goToFloor(targetFloor, targetImageIndex = 0) {
            if (!this.isReaderMode) {
                this.enterReaderMode();
            }

            if (!Array.isArray(this.posts) || this.posts.length === 0) {
                return;
            }

            const clampedFloor = Math.max(0, Math.min(this.posts.length - 1, targetFloor));
            const clampedImageIndex = Math.max(0, targetImageIndex);

            this.currentFloor = clampedFloor;
            this.currentImageIndex = clampedImageIndex;
            this.currentScrollImageIndex = clampedImageIndex;
            this.lastFlipDirection = 'next';

            this.renderContent();

            const contentDiv = document.getElementById('reader-content');
            if (contentDiv) {
                contentDiv.scrollTop = 0;
                contentDiv.scrollLeft = 0;
            }
        }

        switchTab(tabName) {
            document.querySelectorAll('.tab-btn').forEach(btn => {
                btn.classList.toggle('active', btn.dataset.tab === tabName);
            });

            document.querySelectorAll('.tab-panel').forEach(panel => {
                panel.classList.toggle('active', panel.id === `${tabName}-panel`);
            });

            if (tabName === 'comments') {
                this.loadComments();
            } else if (tabName === 'favorites') {
                this.loadFavorites();
            } else if (tabName === 'directory') {
                this.scrollDirectoryToCurrent();
            }
        }

        updateSeriesName(rawName, options = {}) {
            const { persistOverride = true } = options;
            const trimmed = typeof rawName === 'string' ? rawName.trim() : '';
            if (!trimmed) {
                return false;
            }

            const previousKey = this.seriesKey;
            const previousName = this.currentSeriesName;

            this.currentSeriesName = trimmed;
            this.seriesTitle = trimmed;
            const newKey = buildSeriesKey(trimmed) || previousKey;
            this.seriesKey = newKey;

            if (persistOverride) {
                this.dataStore.setSeriesNameOverride(this.baseSeriesKey, trimmed);
            }

            if (previousKey && previousKey !== newKey) {
                this.dataStore.renameFavoriteSeries(previousKey, newKey, trimmed);
            } else if (previousKey === newKey && previousName !== trimmed) {
                const existingFavorite = this.dataStore.getFavorite(newKey);
                if (existingFavorite) {
                    existingFavorite.seriesTitle = trimmed;
                    this.dataStore.save();
                }
            }

            const searchInput = document.getElementById('series-search');
            if (searchInput && searchInput.value.trim() !== trimmed) {
                searchInput.value = trimmed;
            }

            this.updateFavoriteButton();
            this.updateFavoriteDirectoryCountDisplay();

            const favPanel = document.getElementById('favorites-panel');
            if (favPanel && favPanel.classList.contains('active')) {
                this.loadFavorites();
            }

            return previousKey !== newKey || previousName !== trimmed;
        }

        toggleFavorite() {
            const isFavorited = this.dataStore.isSeriesFavorited(this.seriesKey);

            if (isFavorited) {
                this.dataStore.removeFavorite(this.seriesKey);
            } else {
                this.dataStore.addOrUpdateFavorite(this.seriesKey, {
                    seriesTitle: this.seriesTitle,
                    author: this.parser.authorName || '未知作者',
                    threadId: this.parser.threadId,
                    chapterTitle: this.parser.threadTitle,
                    url: window.location.href,
                    currentFloor: this.currentFloor + 1,
                    totalFloors: this.posts.length,
                    directoryCount: typeof this.currentDirectoryCount === 'number' && this.currentDirectoryCount >= 0
                        ? Math.floor(this.currentDirectoryCount)
                        : this.dataStore.getSeriesDirectoryCount(this.seriesKey)
                });
            }

            this.updateFavoriteButton();
            // 刷新收藏列表(如果正在显示)
            const favPanel = document.getElementById('favorites-panel');
            if (favPanel && favPanel.classList.contains('active')) {
                this.loadFavorites();
            }
        }

        updateFavoriteButton() {
            const btn = document.getElementById('favorite-btn');
            if (btn) {
                const isFavorited = this.dataStore.isSeriesFavorited(this.seriesKey);
                btn.innerHTML = isFavorited ? ICONS.bookmarkFilled : ICONS.bookmark;
                btn.classList.toggle('favorited', isFavorited);
                btn.title = isFavorited ? '取消收藏' : '收藏';
            }
        }

        showDataTransferDialog() {
            const existingOverlay = document.getElementById('data-transfer-overlay');
            if (existingOverlay) {
                existingOverlay.remove();
            }

            const overlay = document.createElement('div');
            overlay.id = 'data-transfer-overlay';
            overlay.className = 'data-transfer-overlay';
            overlay.innerHTML = `
                <div class="data-transfer-dialog">
                    <div class="data-transfer-header">
                        <h3>数据导入 / 导出</h3>
                        <button type="button" class="data-transfer-close" title="关闭">${ICONS.close}</button>
                    </div>
                    <div class="data-transfer-body">
                        <textarea id="data-transfer-textarea" spellcheck="false"></textarea>
                        <div class="data-transfer-desc">包含收藏、阅读设置与阅读进度。打开时会展示当前保存的数据。</div>
                    </div>
                    <div class="data-transfer-footer">
                        <button type="button" class="data-transfer-btn primary" id="data-transfer-save">保存</button>
                        <button type="button" class="data-transfer-btn secondary" id="data-transfer-cancel">关闭</button>
                    </div>
                </div>
            `;

            document.body.appendChild(overlay);

            const dialog = overlay.querySelector('.data-transfer-dialog');
            const textarea = overlay.querySelector('#data-transfer-textarea');
            const saveBtn = overlay.querySelector('#data-transfer-save');
            const cancelBtn = overlay.querySelector('#data-transfer-cancel');
            const closeBtn = overlay.querySelector('.data-transfer-close');

            let handleKeydown = null;
            const closeDialog = () => {
                if (handleKeydown) {
                    document.removeEventListener('keydown', handleKeydown);
                }
                if (overlay && overlay.parentNode) {
                    overlay.remove();
                }
            };

            handleKeydown = (event) => {
                if (event.key === 'Escape') {
                    closeDialog();
                }
            };
            document.addEventListener('keydown', handleKeydown);

            if (textarea) {
                textarea.value = this.dataStore.exportData(true);
                textarea.focus();
                textarea.select();
            }

            if (saveBtn && textarea) {
                saveBtn.addEventListener('click', () => {
                    const raw = textarea.value.trim();
                    if (!raw) {
                        alert('请先填写 JSON 数据');
                        textarea.focus();
                        return;
                    }
                    try {
                        this.dataStore.importData(raw);
                        this.applySettingsFromStore();
                        textarea.value = this.dataStore.exportData(true);
                        alert('数据已保存!');
                    } catch (err) {
                        console.error('导入失败', err);
                        alert(`导入失败: ${err.message || err}`);
                    }
                });
            }

            const registerClose = (element) => {
                if (!element) {
                    return;
                }
                element.addEventListener('click', () => {
                    closeDialog();
                });
            };

            registerClose(cancelBtn);
            registerClose(closeBtn);

            overlay.addEventListener('click', (event) => {
                if (event.target === overlay) {
                    closeDialog();
                }
            });

            if (dialog) {
                dialog.addEventListener('click', (event) => {
                    event.stopPropagation();
                });
            }
        }

        applySettingsFromStore() {
            const storedPreload = parseInt(this.dataStore.getSetting('preloadCount', CONFIG.PRELOAD_COUNT), 10);
            if (Number.isFinite(storedPreload)) {
                CONFIG.PRELOAD_COUNT = storedPreload;
            }

            this.darkMode = this.dataStore.getSetting('darkMode', false);
            document.body.classList.toggle('dark-mode', this.darkMode);
            const darkModeBtn = document.getElementById('dark-mode-btn');
            if (darkModeBtn) {
                darkModeBtn.innerHTML = this.darkMode ? ICONS.lightMode : ICONS.darkMode;
            }

            let importedViewMode = this.dataStore.getSetting('viewMode', CONFIG.VIEW_MODES.SCROLL_DOWN);
            importedViewMode = LEGACY_VIEW_MODE_MAP[importedViewMode] || importedViewMode;
            if (!ALL_VIEW_MODES.has(importedViewMode)) {
                importedViewMode = CONFIG.VIEW_MODES.SCROLL_DOWN;
            }
            this.viewMode = importedViewMode;

            const ratioSetting = Number(this.dataStore.getSetting('mainWidthRatio', this.mainWidthRatio));
            if (Number.isFinite(ratioSetting)) {
                this.mainWidthRatio = Math.min(Math.max(ratioSetting, 0.5), 0.9);
                this.applyLayoutSizing();
            }

            this.applyFloatingButtonPosition();
            this.updateFavoriteButton();
            this.syncMenuSettingControls();
            this.sidebarCollapsed = !!this.dataStore.getSetting('sidebarCollapsed', this.sidebarCollapsed);
            this.applySidebarState();

            if (this.isReaderMode) {
                const contentDiv = document.getElementById('reader-content');
                if (contentDiv) {
                    contentDiv.setAttribute('data-view-mode', this.viewMode);
                }
                this.renderContent();
                this.scrollDirectoryToCurrent();
                const favPanel = document.getElementById('favorites-panel');
                if (favPanel && favPanel.classList.contains('active')) {
                    this.loadFavorites();
                }
            }
        }

        syncMenuSettingControls() {
            const slider = document.getElementById('menu-preload-slider');
            const sliderValue = document.getElementById('menu-preload-value');
            const cacheCountSpan = document.getElementById('menu-cache-count');
            const perPageInput = document.getElementById('menu-search-per-page');

            const currentPreload = this.dataStore.getSetting('preloadCount', CONFIG.PRELOAD_COUNT);
            if (slider) {
                slider.value = currentPreload;
            }
            if (sliderValue) {
                sliderValue.textContent = currentPreload;
            }
            if (cacheCountSpan) {
                cacheCountSpan.textContent = this.imageCache.cache.size;
            }
            if (perPageInput) {
                perPageInput.value = this.getSearchResultsPerPage();
            }
        }

        getViewModeName(mode) {
            const names = {
                [CONFIG.VIEW_MODES.SCROLL_DOWN]: '滑动-下',
                [CONFIG.VIEW_MODES.SCROLL_LEFT]: '滑动-左',
                [CONFIG.VIEW_MODES.SCROLL_RIGHT]: '滑动-右',
                [CONFIG.VIEW_MODES.FLIP_LEFT_SINGLE]: '翻页-左-单页',
                [CONFIG.VIEW_MODES.FLIP_LEFT_DOUBLE]: '翻页-左-双页',
                [CONFIG.VIEW_MODES.FLIP_RIGHT_SINGLE]: '翻页-右-单页',
                [CONFIG.VIEW_MODES.FLIP_RIGHT_DOUBLE]: '翻页-右-双页'
            };
            return names[mode] || mode;
        }

        extractThreadIdFromUrl(url) {
            if (!url) return null;
            let match = url.match(/thread-(\d+)-/);
            if (match) {
                return match[1];
            }
            match = url.match(/[?&]tid=(\d+)/);
            return match ? match[1] : null;
        }

        handleDirectoryNavigation(event, anchor) {
            event.preventDefault();
            const url = anchor.href;
            const targetThreadId = anchor.dataset.threadId || this.extractThreadIdFromUrl(url);

            if (!url) {
                return;
            }

            if (!targetThreadId) {
                window.location.href = url;
                return;
            }

            if (targetThreadId === this.parser.threadId) {
                return;
            }

            this.scheduleAutoOpen(targetThreadId);
            window.location.href = url;
        }

        scheduleAutoOpen(targetThreadId) {
            const payload = {
                enabled: true,
                target: targetThreadId,
                timestamp: Date.now(),
                seriesName: this.currentSeriesName
            };
            GM_setValue(CONFIG.AUTO_OPEN_KEY, JSON.stringify(payload));
        }

        loadDirectory() {
            this.searchDirectory();
        }

        getSearchResultsPerPage() {
            const storedValue = this.dataStore.getSetting('searchResultsPerPage', CONFIG.SEARCH_RESULTS_PER_PAGE_DEFAULT);
            const normalized = normalizeSearchResultsPerPageValue(storedValue);
            if (normalized !== storedValue) {
                this.dataStore.setSetting('searchResultsPerPage', normalized);
            }
            return normalized;
        }

        searchDirectory() {
            console.log('[搜索] ===== 开始搜索合集 =====');
            const searchInput = document.getElementById('series-search');
            const query = searchInput ? searchInput.value.trim() : '';
            console.log('[搜索] 搜索关键词:', query);

            if (!query) {
                console.log('[搜索] 错误: 搜索关键词为空');
                this.showDirectoryError('请输入搜索关键词');
                return;
            }

            this.updateSeriesName(query, { persistOverride: true });

            const perPageSetting = this.getSearchResultsPerPage();
            this.setDirectoryLoadingMessage(`搜索中... (每页${perPageSetting}条)`);

            // Discuz 搜索需要先GET获取formhash
            console.log('[搜索] 第一步: 获取搜索页面formhash');
            const searchPageUrl = 'https://bbs.yamibo.com/search.php?mod=forum';

            GM_xmlhttpRequest({
                method: 'GET',
                url: searchPageUrl,
                onload: (response) => {
                    console.log('[搜索] 获取搜索页面成功,状态码:', response.status);

                    // 提取formhash
                    const doc = new DOMParser().parseFromString(response.responseText, 'text/html');
                    const formhashInput = doc.querySelector('input[name="formhash"]');
                    const formhash = formhashInput ? formhashInput.value : '';
                    console.log('[搜索] 提取到formhash:', formhash);

                    // 直接使用GET方式搜索(更可靠)
                    const finalSearchUrl = `https://bbs.yamibo.com/search.php?mod=forum&srchtxt=${encodeURIComponent(query)}&formhash=${formhash}&searchsubmit=yes`;
                    console.log('[搜索] 第二步: 执行搜索请求');
                    console.log('[搜索] 请求URL:', finalSearchUrl);

                    GM_xmlhttpRequest({
                        method: 'GET',
                        url: finalSearchUrl,
                        onload: (searchResponse) => {
                            console.log('[搜索] 搜索请求完成! 状态码:', searchResponse.status);
                            console.log('[搜索] 最终URL:', searchResponse.finalUrl);
                            console.log('[搜索] 响应内容长度:', searchResponse.responseText.length);

                            // 检查是否是错误页面
                            if (searchResponse.responseText.includes('System Error') ||
                                searchResponse.responseText.includes('搜索无结果')) {
                                console.log('[搜索] 错误: 搜索返回错误页面或无结果');
                                this.showDirectoryError('搜索失败,请稍后重试');
                                return;
                            }

                            this.handleSearchResponse(searchResponse, perPageSetting);
                        },
                        onerror: (error) => {
                            console.error('[搜索] 搜索请求失败:', error);
                            this.showDirectoryError('搜索失败,10秒后自动重试');
                            this.scheduleRetry();
                        }
                    });
                },
                onerror: (error) => {
                    console.error('[搜索] 获取搜索页面失败:', error);
                    this.showDirectoryError('搜索失败,10秒后自动重试');
                    this.scheduleRetry();
                }
            });
        }

        handleSearchResponse(searchResponse, perPageSetting) {
            const aggregatedEntries = [];
            const seenKeys = new Set();
            const normalizedUrl = this.buildSearchPageUrl(searchResponse?.finalUrl, 1, perPageSetting);

            if (normalizedUrl) {
                this.fetchFirstPageAndContinue({
                    url: normalizedUrl,
                    perPage: perPageSetting,
                    aggregatedEntries,
                    seenKeys
                });
                return;
            }

            console.log('[搜索] 未能构造分页URL,直接使用初始响应渲染');
            this.processSearchPageHtml(searchResponse.responseText, normalizedUrl, perPageSetting, aggregatedEntries, seenKeys);
        }

        fetchFirstPageAndContinue({ url, perPage, aggregatedEntries, seenKeys }) {
            this.setDirectoryLoadingMessage('搜索中... (第1页)');
            GM_xmlhttpRequest({
                method: 'GET',
                url,
                onload: (resp) => {
                    console.log('[搜索] 第 1 页重新获取成功,状态码:', resp.status);
                    this.processSearchPageHtml(resp.responseText, url, perPage, aggregatedEntries, seenKeys);
                },
                onerror: (error) => {
                    console.error('[搜索] 重新获取第 1 页失败:', error);
                    this.showDirectoryError('搜索失败,请稍后重试');
                }
            });
        }

        processSearchPageHtml(html, baseUrl, perPage, aggregatedEntries, seenKeys) {
            const firstPageEntries = this.extractSearchEntriesFromHtml(html);
            const firstPageCount = this.appendDirectoryEntries(firstPageEntries, aggregatedEntries, seenKeys);
            console.log(`[搜索] 第 1 页解析完成,条目数: ${firstPageCount}`);

            if (!baseUrl) {
                console.log('[搜索] 未能构造有效的分页URL,直接渲染当前结果');
                this.renderDirectoryEntries(aggregatedEntries);
                return;
            }

            if (firstPageCount < perPage) {
                this.renderDirectoryEntries(aggregatedEntries);
                return;
            }

            this.fetchAdditionalSearchPages({
                baseUrl,
                nextPage: 2,
                perPage,
                aggregatedEntries,
                seenKeys
            });
        }

        fetchAdditionalSearchPages({ baseUrl, nextPage, perPage, aggregatedEntries, seenKeys }) {
            if (nextPage > CONFIG.MAX_SEARCH_PAGES) {
                console.log(`[搜索] 已达到最大翻页数量 ${CONFIG.MAX_SEARCH_PAGES},停止继续请求`);
                this.renderDirectoryEntries(aggregatedEntries);
                return;
            }

            const nextUrl = this.buildSearchPageUrl(baseUrl, nextPage, perPage);
            if (!nextUrl) {
                console.log('[搜索] 无法构造下一页URL,停止继续请求');
                this.renderDirectoryEntries(aggregatedEntries);
                return;
            }

            this.setDirectoryLoadingMessage(`搜索中... (第${nextPage}页)`);

            GM_xmlhttpRequest({
                method: 'GET',
                url: nextUrl,
                onload: (resp) => {
                    console.log(`[搜索] 第 ${nextPage} 页请求完成,状态码:`, resp.status);
                    const pageEntries = this.extractSearchEntriesFromHtml(resp.responseText);
                    const pageCount = this.appendDirectoryEntries(pageEntries, aggregatedEntries, seenKeys);
                    if (pageCount < perPage) {
                        this.renderDirectoryEntries(aggregatedEntries);
                    } else {
                        this.fetchAdditionalSearchPages({
                            baseUrl,
                            nextPage: nextPage + 1,
                            perPage,
                            aggregatedEntries,
                            seenKeys
                        });
                    }
                },
                onerror: (error) => {
                    console.error(`[搜索] 第 ${nextPage} 页请求失败:`, error);
                    this.renderDirectoryEntries(aggregatedEntries);
                }
            });
        }

        buildSearchPageUrl(baseUrl, pageNumber, perPage) {
            if (!baseUrl) {
                return '';
            }
            let urlObj;
            try {
                urlObj = new URL(baseUrl);
            } catch (err) {
                try {
                    urlObj = new URL(baseUrl, 'https://bbs.yamibo.com/');
                } catch (error) {
                    console.error('[搜索] 无法解析搜索URL:', baseUrl, error);
                    return '';
                }
            }
            if (pageNumber !== undefined && pageNumber !== null) {
                urlObj.searchParams.set('page', String(pageNumber));
            }
            if (perPage) {
                urlObj.searchParams.set('perpage', String(perPage));
            }
            return urlObj.toString();
        }


        appendDirectoryEntries(entries, aggregatedEntries, seenKeys) {
            if (!Array.isArray(entries) || entries.length === 0) {
                return 0;
            }
            entries.forEach(entry => {
                const key = entry.threadId ? `tid:${entry.threadId}` : (entry.url ? `url:${entry.url}` : '');
                if (key && seenKeys.has(key)) {
                    return;
                }
                if (key) {
                    seenKeys.add(key);
                }
                aggregatedEntries.push(entry);
            });
            return entries.length;
        }

        extractSearchEntriesFromHtml(html) {
            console.log('[解析] ===== 开始解析搜索结果 =====');
            const entries = [];
            if (!html) {
                return entries;
            }

            const parser = new DOMParser();
            const doc = parser.parseFromString(html, 'text/html');

            const selectors = [
                'tbody[id^="normalthread_"]',
                '#threadlist tbody',
                'table.tbopt tbody[id^="normalthread_"]',
                '.tl tbody',
                '.pbw .xl.xl2.o.cl li',
                '.pbw .xl.xl2 li',
                '.threadlist li',
                '#threadlist li',
                'li.pbw'
            ];

            let results = [];
            let usedSelector = '';
            for (const selector of selectors) {
                results = doc.querySelectorAll(selector);
                if (results.length > 0) {
                    usedSelector = selector;
                    console.log(`[解析] 使用选择器 "${selector}" 找到 ${results.length} 个结果`);
                    break;
                }
            }

            if (results.length === 0) {
                results = doc.querySelectorAll('a.s.xst, a.xst');
                usedSelector = 'a.s.xst, a.xst';
                console.log(`[解析] 使用兜底选择器 "${usedSelector}" 找到 ${results.length} 个链接`);
            }

            const linkSelector = 'a.s.xst, a.xst, a[href*="thread-"], a[href*="viewthread"]';

            results.forEach((item, index) => {
                let link = null;
                if (item.tagName === 'TBODY' || item.tagName === 'LI') {
                    link = item.querySelector(linkSelector);
                } else if (item.tagName === 'A') {
                    link = item;
                }

                if (!link) {
                    return;
                }

                const title = link.textContent.trim();
                const href = link.getAttribute('href');
                const isThreadLink = !!href && (/thread-\d+-/.test(href) || href.includes('mod=viewthread'));

                if (!href || !title || !isThreadLink) {
                    console.log(`[解析] 跳过第 ${index + 1} 个结果: href=${href}, title=${title}`);
                    return;
                }

                const sanitizedHref = href.replace(/^\/+/, '');
                const fullUrl = href.startsWith('http') ? href : `https://bbs.yamibo.com/${sanitizedHref}`;
                const threadId = this.extractThreadIdFromUrl(fullUrl);

                entries.push({
                    title,
                    url: fullUrl,
                    threadId
                });
            });

            console.log(`[解析] 原始页面共解析到 ${entries.length} 个项目`);
            return entries;
        }

        renderDirectoryEntries(entries) {
            const listDiv = document.getElementById('directory-list');
            if (!listDiv) {
                return;
            }

            if (!entries || entries.length === 0) {
                console.log('[解析] 未找到任何有效搜索结果');
                this.showDirectoryError('未找到相关内容');
                return;
            }

            listDiv.innerHTML = '';
            let foundCount = 0;

            entries.forEach(entry => {
                const itemDiv = document.createElement('div');
                itemDiv.className = 'directory-item';
                if (entry.threadId && entry.threadId === this.parser.threadId) {
                    itemDiv.classList.add('current');
                }

                const anchor = document.createElement('a');
                anchor.href = entry.url;
                anchor.textContent = entry.title;
                if (entry.threadId) {
                    anchor.dataset.threadId = entry.threadId;
                }
                anchor.addEventListener('click', (event) => this.handleDirectoryNavigation(event, anchor));
                itemDiv.appendChild(anchor);
                listDiv.appendChild(itemDiv);
                foundCount++;
            });

            console.log(`[解析] 解析完成,共找到 ${foundCount} 个有效结果`);
            this.currentDirectoryCount = foundCount;
            this.updateFavoriteDirectoryCountDisplay();
            if (this.dataStore.isSeriesFavorited(this.seriesKey)) {
                this.dataStore.updateSeriesDirectoryCount(this.seriesKey, foundCount);
            }
            requestAnimationFrame(() => this.scrollDirectoryToCurrent());
        }

        setDirectoryLoadingMessage(text) {
            const listDiv = document.getElementById('directory-list');
            if (listDiv) {
                listDiv.innerHTML = `<div class="loading">${text}</div>`;
            }
            this.currentDirectoryCount = null;
            this.updateFavoriteDirectoryCountDisplay();
        }

        scrollDirectoryToCurrent() {
            const listDiv = document.getElementById('directory-list');
            if (!listDiv) {
                return;
            }
            const currentItem = listDiv.querySelector('.directory-item.current');
            if (!currentItem) {
                return;
            }
            const targetScroll = currentItem.offsetTop - Math.max(0, (listDiv.clientHeight - currentItem.offsetHeight) / 2);
            listDiv.scrollTo({ top: Math.max(0, targetScroll), behavior: 'smooth' });
        }

        showDirectoryError(message) {
            const listDiv = document.getElementById('directory-list');
            listDiv.innerHTML = `<div class="error">${message}</div>`;
            this.currentDirectoryCount = null;
            this.updateFavoriteDirectoryCountDisplay();
        }

        scheduleRetry() {
            if (this.searchRetryTimer) {
                clearTimeout(this.searchRetryTimer);
            }

            this.searchRetryTimer = setTimeout(() => {
                this.searchDirectory();
            }, CONFIG.SEARCH_RETRY_DELAY);
        }

        loadComments() {
            const commentsDiv = document.getElementById('comments-list');
            const allPosts = document.querySelectorAll('#postlist > div[id^="post_"]');
            const comments = [];

            allPosts.forEach((postEl) => {
                const authorLink = postEl.querySelector('.favatar .authi a');
                if (authorLink) {
                    const href = authorLink.getAttribute('href');
                    let match = href.match(/uid=(\d+)/);
                    if (!match) {
                        match = href.match(/uid-(\d+)/);
                    }

                    if (match && match[1] !== this.parser.authorUid) {
                        const author = authorLink.textContent.trim();
                        const content = postEl.querySelector('.t_f, .pcb');
                        const floorNum = this.parser.getFloorNumber(postEl);
                        const timeEl = postEl.querySelector('.pti .authi em');

                        comments.push({
                            floor: floorNum,
                            author,
                            content: content ? content.innerHTML : '',
                            time: timeEl ? timeEl.textContent : ''
                        });
                    }
                }
            });

            if (comments.length === 0) {
                commentsDiv.innerHTML = '<div class="no-comments">暂无评论</div>';
                return;
            }

            commentsDiv.innerHTML = '';
            comments.forEach(comment => {
                const commentDiv = document.createElement('div');
                commentDiv.className = 'comment-item';
                commentDiv.innerHTML = `
                    <div class="comment-header">
                        <span class="comment-author">${comment.author}</span>
                        <span class="comment-floor">#${comment.floor}</span>
                        <span class="comment-time">${comment.time}</span>
                    </div>
                    <div class="comment-content">${comment.content}</div>
                `;
                commentsDiv.appendChild(commentDiv);
            });
        }

        updateFavoriteDirectoryCountDisplay() {
            const selector = `.favorite-count[data-series-key="${this.seriesKey}"]`;
            const stored = this.dataStore.getSeriesDirectoryCount
                ? this.dataStore.getSeriesDirectoryCount(this.seriesKey)
                : 0;
            const normalizedStored = Number.isFinite(stored) && stored >= 0 ? Math.floor(stored) : 0;
            const hasCurrent = typeof this.currentDirectoryCount === 'number' && this.currentDirectoryCount >= 0;
            const normalizedCurrent = hasCurrent ? Math.floor(this.currentDirectoryCount) : null;
            const displayCount = normalizedCurrent !== null ? normalizedCurrent : normalizedStored;
            document.querySelectorAll(selector).forEach(span => {
                span.textContent = `收录章节: ${displayCount} 篇`;
            });
        }

        loadFavorites() {
            const favoritesDiv = document.getElementById('favorites-list');
            const favorites = this.dataStore.getAllFavorites();

            favoritesDiv.innerHTML = '';

            if (favorites.length === 0) {
                favoritesDiv.innerHTML = '<div class="empty-message">暂无收藏</div>';
                return;
            }

            // 按最后访问时间排序
            favorites.sort((a, b) => (b.lastVisited || 0) - (a.lastVisited || 0));

            favorites.forEach(fav => {
                const favItem = document.createElement('div');
                favItem.className = 'favorite-item';

                const isCurrentSeries = fav.seriesKey === this.seriesKey;
                if (isCurrentSeries) {
                    favItem.classList.add('current');
                }

                const seriesTitle = fav.seriesTitle || '未命名合集';
                const latestTitle = fav.latestTitle || '未记录章节';
                const latestUrl = fav.latestUrl || '';
                const latestThreadId = fav.latestThreadId || '';
                const latestFloor = fav.latestFloor || 0;
                const latestTotalFloors = fav.latestTotalFloors || 0;
                const storedChapterCount = fav.chapters ? Object.keys(fav.chapters).length : 0;
                const storedDirectoryCountRaw = Number(fav.directoryCount);
                const normalizedStoredDirectoryCount = Number.isFinite(storedDirectoryCountRaw) && storedDirectoryCountRaw >= 0
                    ? Math.floor(storedDirectoryCountRaw)
                    : Math.max(0, storedChapterCount);
                const hasCurrentDirectoryCount = fav.seriesKey === this.seriesKey && typeof this.currentDirectoryCount === 'number' && this.currentDirectoryCount >= 0;
                const currentSeriesDirectoryCount = hasCurrentDirectoryCount ? Math.floor(this.currentDirectoryCount) : null;
                const directoryCount = hasCurrentDirectoryCount
                    ? currentSeriesDirectoryCount
                    : normalizedStoredDirectoryCount;

                let timeStr = '未访问';
                if (fav.lastVisited) {
                    const date = new Date(fav.lastVisited);
                    const now = new Date();
                    const diffDays = Math.floor((now - date) / (1000 * 60 * 60 * 24));

                    if (diffDays === 0) {
                        timeStr = '今天';
                    } else if (diffDays === 1) {
                        timeStr = '昨天';
                    } else if (diffDays < 7) {
                        timeStr = `${diffDays}天前`;
                    } else {
                        timeStr = date.toLocaleDateString();
                    }
                }

                let progressText = '未开始';
                if (latestFloor > 0 && latestTotalFloors > 0) {
                    progressText = `第${latestFloor}/${latestTotalFloors}楼`;
                } else if (latestFloor > 0) {
                    progressText = `第${latestFloor}楼`;
                }

                const authorText = fav.author ? `<span class="favorite-author">作者: ${fav.author}</span>` : '';
                const displayDirectoryCount = Number.isFinite(directoryCount) && directoryCount >= 0 ? directoryCount : 0;
                const chapterCountText = `<span class="favorite-count" data-series-key="${fav.seriesKey}">收录章节: ${displayDirectoryCount} 篇</span>`;
                const continueDisabled = !latestUrl;
                const continueTitle = continueDisabled ? '暂无可继续的章节' : '继续阅读';
                const seriesTitleHtml = continueDisabled
                    ? `<span class="favorite-title">${seriesTitle}</span>`
                    : `<a href="${latestUrl}" target="_blank" class="favorite-title">${seriesTitle}</a>`;
                const latestLink = latestUrl
                    ? `<a href="${latestUrl}" target="_blank">${latestTitle}</a>`
                    : `<span class="favorite-latest-text">${latestTitle}</span>`;

                favItem.innerHTML = `
                    <div class="favorite-header">
                        <div class="favorite-title-line">
                            ${seriesTitleHtml}
                            ${authorText}
                        </div>
                        ${chapterCountText}
                    </div>
                    <div class="favorite-meta">
                        <div class="favorite-latest">上次阅读: ${latestLink}</div>
                        <div class="favorite-progress">进度: ${progressText}</div>
                        <div class="favorite-time">最近阅读: ${timeStr}</div>
                    </div>
                    <div class="favorite-actions">
                        <button class="favorite-action-btn" data-action="continue" data-series-key="${fav.seriesKey}" data-thread-id="${latestThreadId}" data-floor="${latestFloor}" data-url="${latestUrl}" title="${continueTitle}" ${continueDisabled ? 'disabled' : ''}>
                            ${ICONS.play || '▶'}
                        </button>
                        <button class="favorite-action-btn" data-action="remove" data-series-key="${fav.seriesKey}" title="取消收藏">
                            ${ICONS.delete || '🗑'}
                        </button>
                    </div>
                `;

                favoritesDiv.appendChild(favItem);
            });

            favoritesDiv.querySelectorAll('.favorite-action-btn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();

                    const action = btn.dataset.action;
                    const seriesKey = btn.dataset.seriesKey;
                    const threadId = btn.dataset.threadId;
                    const floor = parseInt(btn.dataset.floor || '0', 10) || 0;
                    const targetUrl = btn.dataset.url;

                    if (action === 'continue') {
                        const fav = favorites.find(f => f.seriesKey === seriesKey);
                        if (fav) {
                            if (threadId && threadId === this.parser.threadId) {
                                if (floor > 0) {
                                    this.goToFloor(Math.max(0, floor - 1));
                                }
                            } else if (targetUrl) {
                                window.open(targetUrl, '_blank');
                            }
                        }
                    } else if (action === 'remove') {
                        if (confirm('确定要取消收藏吗?')) {
                            this.dataStore.removeFavorite(seriesKey);
                            this.loadFavorites();

                            if (seriesKey === this.seriesKey) {
                                this.updateFavoriteButton();
                            }
                        }
                    }
                });
            });

            this.updateFavoriteDirectoryCountDisplay();
        }
    }

    // =========================
    // Material Design 样式
    // =========================
    GM_addStyle(`
        :root {
            --primary-color: #4caf50;
            --primary-dark: #388e3c;
            --primary-light: #81c784;
            --accent-color: #66bb6a;
            --background: #fafafa;
            --surface: #ffffff;
            --error: #f44336;
            --text-primary: rgba(0,0,0,0.87);
            --text-secondary: rgba(0,0,0,0.6);
            --divider: rgba(0,0,0,0.12);
            --shadow-1: 0 2px 4px rgba(0,0,0,0.1);
            --shadow-2: 0 4px 8px rgba(0,0,0,0.12);
            --shadow-3: 0 8px 16px rgba(0,0,0,0.15);
        }

        body.dark-mode {
            --primary-color: #66bb6a;
            --primary-dark: #4caf50;
            --primary-light: #81c784;
            --accent-color: #81c784;
            --background: #121212;
            --surface: #1e1e1e;
            --error: #cf6679;
            --text-primary: rgba(255,255,255,0.87);
            --text-secondary: rgba(255,255,255,0.6);
            --divider: rgba(255,255,255,0.12);
        }

        #yamibo-reader-btn {
            position: fixed;
            right: 20px;
            top: 50%;
            transform: translateY(-50%);
            width: 56px;
            height: 56px;
            background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
            color: white;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            box-shadow: var(--shadow-3);
            z-index: 99999;
            transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
            touch-action: none;
        }

        #yamibo-reader-btn svg {
            width: 28px;
            height: 28px;
        }

        #yamibo-reader-btn:hover {
            transform: translateY(-50%) scale(1.1);
            box-shadow: 0 12px 24px rgba(76,175,80,0.3);
        }

        #yamibo-reader-btn.edge-left,
        #yamibo-reader-btn.edge-right {
            opacity: 0.4;
        }

        #yamibo-reader-btn.edge-left {
            clip-path: inset(0 0 0 50%);
        }

        #yamibo-reader-btn.edge-right {
            clip-path: inset(0 50% 0 0);
        }

        #yamibo-reader-btn.edge-left.edge-expanded,
        #yamibo-reader-btn.edge-right.edge-expanded {
            clip-path: none;
            opacity: 1;
        }

        body.yamibo-reader-active > *:not(#yamibo-reader-container):not(#yamibo-reader-btn):not(#data-transfer-overlay) {
            display: none !important;
        }

        #yamibo-reader-container {
            position: fixed;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            background: var(--background);
            color: var(--text-primary);
            z-index: 99998;
            display: flex;
            --main-width: 70%;
            --sidebar-width: 30%;
            --content-scale: 1.133;
            --toolbar-height: 44px;
        }

        .reader-main {
            flex: 0 0 var(--main-width);
            width: var(--main-width);
            height: 100%;
            display: flex;
            flex-direction: column;
            background: var(--surface);
            overflow: hidden;
        }

        .reader-main-inner {
            width: 100%;
            height: 100%;
            display: flex;
            flex-direction: column;
        }

        .reader-content-wrapper {
            flex: 1;
            display: flex;
            position: relative;
            overflow: hidden;
            min-width: 0;
            min-height: 0;
        }

        .reader-content-wrapper .reader-content {
            flex: 1;
            width: calc(100% / var(--content-scale));
            height: calc(100% / var(--content-scale));
            transform: scale(var(--content-scale));
            transform-origin: left top;
            min-width: 0;
            min-height: 0;
        }

        .reader-resizer {
            flex: 0 0 6px;
            width: 6px;
            background: var(--divider);
            cursor: col-resize;
            position: relative;
            z-index: 12;
            transition: background 0.2s ease;
            touch-action: none;
        }

        .reader-resizer::after {
            content: '';
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 2px;
            height: 40px;
            background: var(--text-secondary);
            border-radius: 1px;
            opacity: 0.6;
        }

        #yamibo-reader-container.resizing .reader-resizer,
        .reader-resizer:hover {
            background: var(--primary-light);
        }

        body.reader-resizing {
            user-select: none;
            cursor: col-resize;
        }

        body.reader-btn-dragging {
            cursor: move !important;
        }

        .reader-toolbar {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 6px 10px;
            background: var(--surface);
            border-bottom: 1px solid var(--divider);
            box-shadow: var(--shadow-1);
            min-height: var(--toolbar-height);
        }

        .toolbar-left, .toolbar-right {
            display: flex;
            gap: 6px;
            align-items: center;
        }

        .toolbar-center {
            flex: 1;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
        }

        .icon-btn {
            width: 32px;
            height: 32px;
            border: none;
            background: var(--surface);
            color: var(--text-primary);
            border-radius: 50%;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: background 0.2s, box-shadow 0.2s;
            box-shadow: var(--shadow-1);
        }

        .icon-btn:hover {
            background: var(--divider);
            box-shadow: var(--shadow-2);
        }

        .icon-btn svg {
            width: 18px;
            height: 18px;
        }

        .icon-btn.favorited {
            color: var(--primary-color);
        }

        .reader-content {
            flex: none;
            overflow-y: auto;
            overflow-x: hidden;
            padding: 20px;
            display: flex;
            gap: 20px;
        }

        .reader-content[data-view-mode="scroll-down"] {
            flex-direction: column;
            align-items: center;
        }

        .reader-content[data-view-mode="scroll-right"] {
            flex-direction: row;
            align-items: flex-start;
            overflow-x: auto;
            overflow-y: hidden;
            scroll-snap-type: x mandatory;
            scroll-behavior: smooth;
        }

        .reader-content.reader-content-hover {
            cursor: pointer;
        }

        .reader-content.cursor-left {
            cursor: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc2NCcgaGVpZ2h0PSc2NCcgdmlld0JveD0nMCAwIDI0IDI0JyBmaWxsPSdub25lJz48cGF0aCBmaWxsPScjNGNhZjUwJyBmaWxsLW9wYWNpdHk9JzAuOCcgZD0nTTE1LjQxIDcuNDFMMTQgNmwtNiA2IDYgNiAxLjQxLTEuNDFMMTAuODMgMTJ6Jy8+PC9zdmc+') 16 32, pointer;
        }

        .reader-content.cursor-right {
            cursor: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0naHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmcnIHdpZHRoPSc2NCcgaGVpZ2h0PSc2NCcgdmlld0JveD0nMCAwIDI0IDI0JyBmaWxsPSdub25lJz48cGF0aCBmaWxsPScjNGNhZjUwJyBmaWxsLW9wYWNpdHk9JzAuOCcgZD0nTTEwIDZMOC41OSA3LjQxIDEzLjE3IDEybC00LjU4IDQuNTlMMTAgMThsNi02eicvPjwvc3ZnPg==') 48 32, pointer;
        }

        .image-container.flip-slide-enter {
            will-change: opacity, filter;
            animation-duration: 0.32s;
            animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
            animation-fill-mode: both;
        }

        .image-container.flip-slide-enter-next {
            animation-name: flip-slide-next;
        }

        .image-container.flip-slide-enter-prev {
            animation-name: flip-slide-prev;
        }

        @keyframes flip-slide-next {
            0% {
                opacity: 0;
                filter: brightness(0.75);
            }
            60% {
                opacity: 1;
                filter: brightness(0.92);
            }
            100% {
                opacity: 1;
                filter: brightness(1);
            }
        }

        @keyframes flip-slide-prev {
            0% {
                opacity: 0;
                filter: brightness(0.75);
            }
            60% {
                opacity: 1;
                filter: brightness(0.92);
            }
            100% {
                opacity: 1;
                filter: brightness(1);
            }
        }

        @media (prefers-reduced-motion: reduce) {
            .image-container.flip-slide-enter {
                animation: none !important;
            }
        }

        .reader-content[data-view-mode="scroll-left"] {
            flex-direction: row-reverse;
            align-items: flex-start;
            overflow-x: auto;
            overflow-y: hidden;
            scroll-snap-type: x mandatory;
            scroll-behavior: smooth;
        }

        .reader-content[data-view-mode="flip-left-single"],
        .reader-content[data-view-mode="flip-right-single"] {
            flex-direction: column;
            align-items: center;
            justify-content: center;
            padding: 12px 0;
        }

        .reader-content[data-view-mode="flip-left-double"],
        .reader-content[data-view-mode="flip-right-double"] {
            flex-direction: row;
            align-items: center;
            justify-content: center;
            padding: 12px 16px;
            gap: 12px;
        }

        .reader-content[data-view-mode="flip-left-double"] {
            flex-direction: row-reverse;
        }

        .image-container {
            position: relative;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .reader-content[data-view-mode="scroll-down"] .image-container,
        .reader-content[data-view-mode="flip-left-single"] .image-container,
        .reader-content[data-view-mode="flip-right-single"] .image-container {
            width: 100%;
        }

        .reader-content[data-view-mode="scroll-right"] .image-container,
        .reader-content[data-view-mode="scroll-left"] .image-container {
            flex-shrink: 0;
            height: calc(100vh - 120px);
            scroll-snap-align: center;
            margin: 0 6px;
        }

        .reader-content[data-view-mode="flip-left-double"] .image-container,
        .reader-content[data-view-mode="flip-right-double"] .image-container {
            flex: 1;
            max-width: 49%;
            height: calc(100vh - 120px);
        }

        .image-container img {
            max-width: 100%;
            height: auto;
            box-shadow: var(--shadow-2);
            border-radius: 8px;
        }

        .reader-content[data-view-mode="flip-left-single"] .image-container img,
        .reader-content[data-view-mode="flip-right-single"] .image-container img {
            max-height: calc(100vh - 120px);
            width: auto;
        }

        .reader-content[data-view-mode="flip-left-double"] .image-container img,
        .reader-content[data-view-mode="flip-right-double"] .image-container img {
            max-height: 100%;
            width: auto;
            object-fit: contain;
        }

        .reader-content[data-view-mode="scroll-right"] .image-container img,
        .reader-content[data-view-mode="scroll-left"] .image-container img {
            height: 100%;
            width: auto;
            max-width: none;
            object-fit: contain;
        }

        .image-loader {
            padding: 40px;
            text-align: center;
            color: var(--text-secondary);
            font-size: 14px;
        }

        .image-loader.error {
            color: var(--error);
        }

        .reader-controls {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 12px;
        }

        .nav-btn {
            width: 32px;
            height: 32px;
            border: none;
            background: var(--primary-color);
            color: white;
            border-radius: 50%;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s;
            box-shadow: var(--shadow-1);
        }

        .nav-btn:hover {
            background: var(--primary-dark);
            box-shadow: var(--shadow-2);
        }

        .nav-btn:active {
            transform: scale(0.95);
        }

        .nav-btn svg {
            width: 18px;
            height: 18px;
        }

        #floor-indicator {
            font-size: 14px;
            color: var(--text-primary);
            font-weight: 500;
            min-width: 120px;
            text-align: center;
        }

        .reader-sidebar {
            flex: 0 0 var(--sidebar-width);
            width: var(--sidebar-width);
            height: 100%;
            display: flex;
            flex-direction: column;
            background: var(--surface);
            border-left: 1px solid var(--divider);
            position: relative;
            z-index: 10;
            transition: transform 0.3s ease, opacity 0.3s ease;
            transform: translateX(0);
        }

        .sidebar-top {
            padding: 6px 10px;
            border-bottom: 1px solid var(--divider);
            background: var(--surface);
            position: relative;
            z-index: 11;
            display: flex;
            align-items: center;
            justify-content: center;
            min-height: var(--toolbar-height);
            box-shadow: var(--shadow-1);
        }

        .sidebar-tabs {
            display: flex;
            gap: 6px;
            margin: 0;
            width: 100%;
            justify-content: center;
            align-items: center;
            height: 100%;
        }

        .tab-btn {
            flex: 1;
            padding: 6px 10px;
            border: none;
            background: var(--surface);
            color: var(--text-secondary);
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.2s;
            font-weight: 500;
            font-size: 14px;
            box-shadow: var(--shadow-1);
            height: 100%;
            display: flex;
            align-items: center;
            justify-content: center;
        }

        .tab-btn:hover {
            background: var(--divider);
            box-shadow: var(--shadow-2);
        }

        .tab-btn.active {
            background: var(--primary-color);
            color: white;
            box-shadow: var(--shadow-2);
        }

        .sidebar-content {
            flex: 1;
            overflow: hidden;
            position: relative;
        }

        .tab-panel {
            display: none;
            height: 100%;
            flex-direction: column;
        }

        .tab-panel.active {
            display: flex;
        }

        #yamibo-reader-container.sidebar-collapsed .reader-main {
            flex: 1 1 auto;
            width: 100%;
        }

        #yamibo-reader-container.sidebar-collapsed .reader-sidebar {
            flex: 0 0 0;
            width: 0;
            opacity: 0;
            pointer-events: none;
            transform: translateX(32px);
            border-left: none;
        }

        #yamibo-reader-container.sidebar-collapsed .reader-resizer {
            opacity: 0;
            pointer-events: none;
        }

        .directory-search {
            padding: 16px;
            border-bottom: 1px solid var(--divider);
            display: flex;
            gap: 8px;
        }

        .directory-search input {
            flex: 1;
            padding: 10px 16px;
            border: 1px solid var(--divider);
            border-radius: 8px;
            font-size: 14px;
            background: var(--surface);
            color: var(--text-primary);
            transition: border-color 0.2s;
        }

        .directory-search .icon-btn {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            box-shadow: none;
        }

        .directory-search .icon-btn:hover {
            box-shadow: none;
        }

        .directory-search input:focus {
            outline: none;
            border-color: var(--primary-color);
        }

        .directory-list {
            flex: 1;
            overflow-y: auto;
            padding: 12px;
        }

        .directory-item {
            padding: 12px 16px;
            margin-bottom: 8px;
            background: var(--background);
            border-radius: 8px;
            transition: all 0.2s;
            border-left: 3px solid transparent;
        }

        .directory-item:hover {
            background: var(--divider);
            transform: translateX(4px);
        }

        .directory-item.current {
            background: rgba(76,175,80,0.1);
            border-left-color: var(--primary-color);
        }

        .directory-item a {
            color: var(--text-primary);
            text-decoration: none;
            display: block;
            font-size: 14px;
        }

        .directory-item.current a {
            color: var(--primary-color);
            font-weight: 500;
        }

        .comments-list {
            flex: 1;
            overflow-y: auto;
            padding: 16px;
        }

        .comment-item {
            margin-bottom: 16px;
            padding: 16px;
            background: var(--background);
            border-radius: 8px;
            box-shadow: var(--shadow-1);
        }

        .comment-header {
            display: flex;
            align-items: center;
            gap: 12px;
            margin-bottom: 12px;
            font-size: 13px;
        }

        .comment-author {
            font-weight: 600;
            color: var(--primary-color);
        }

        .comment-floor {
            color: var(--text-secondary);
            font-size: 12px;
        }

        .comment-time {
            margin-left: auto;
            color: var(--text-secondary);
            font-size: 12px;
        }

        .comment-content {
            font-size: 14px;
            line-height: 1.6;
            color: var(--text-primary);
        }

        /* 收藏列表样式 */
        .favorites-list {
            flex: 1;
            overflow-y: auto;
            padding: 16px;
        }

        .favorite-item {
            margin-bottom: 12px;
            padding: 16px;
            background: var(--background);
            border-radius: 8px;
            box-shadow: var(--shadow-1);
            transition: all 0.2s ease;
            border-left: 3px solid transparent;
        }

        .favorite-item:hover {
            box-shadow: var(--shadow-2);
            transform: translateX(2px);
        }

        .favorite-item.current {
            border-left-color: var(--primary-color);
            background: var(--surface);
        }

        .favorite-header {
            margin-bottom: 8px;
            display: flex;
            flex-direction: column;
            gap: 4px;
        }

        .favorite-title-line {
            display: flex;
            align-items: center;
            gap: 8px;
        }

        .favorite-title {
            font-size: 14px;
            font-weight: 600;
            color: var(--text-primary);
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }

        .favorite-author,
        .favorite-count {
            font-size: 12px;
            color: var(--text-secondary);
        }

        .favorite-meta {
            display: flex;
            flex-direction: column;
            align-items: flex-start;
            gap: 4px;
            font-size: 12px;
            color: var(--text-secondary);
            margin-bottom: 8px;
        }

        .favorite-latest a {
            color: var(--primary-color);
            text-decoration: none;
        }

        .favorite-latest a:hover {
            text-decoration: underline;
        }

        .favorite-latest-text {
            color: var(--text-secondary);
        }

        .favorite-progress {
            font-weight: 500;
            color: var(--primary-color);
        }

        .favorite-time {
            font-size: 11px;
        }

        .favorite-actions {
            display: flex;
            gap: 8px;
            justify-content: flex-end;
        }

        .favorite-action-btn {
            padding: 6px 12px;
            background: var(--surface);
            border: 1px solid var(--divider);
            border-radius: 4px;
            cursor: pointer;
            transition: all 0.2s ease;
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 12px;
        }

        .favorite-action-btn:hover {
            background: var(--primary-light);
            border-color: var(--primary-color);
            transform: scale(1.05);
        }

        .favorite-action-btn[disabled],
        .favorite-action-btn[disabled]:hover {
            opacity: 0.6;
            cursor: not-allowed;
            background: var(--surface);
            border-color: var(--divider);
            transform: none;
        }

        .favorite-action-btn svg {
            width: 14px;
            height: 14px;
        }

        .empty-message {
            padding: 60px 20px;
            text-align: center;
            color: var(--text-secondary);
            font-size: 14px;
        }

        .loading, .error, .no-comments, .content-loading {
            padding: 60px 20px;
            text-align: center;
            color: var(--text-secondary);
            font-size: 14px;
        }

        .error {
            color: var(--error);
        }

        .popup-menu {
            position: fixed;
            background: var(--surface);
            border-radius: 8px;
            box-shadow: var(--shadow-3);
            z-index: 100000;
            min-width: 200px;
            overflow: hidden;
        }

        .menu-item {
            padding: 12px 16px;
            cursor: pointer;
            transition: background 0.2s;
            font-size: 14px;
            color: var(--text-primary);
        }

        .menu-item:hover {
            background: var(--divider);
        }

        .menu-divider {
            height: 1px;
            background: var(--divider);
            margin: 4px 0;
        }

        .menu-settings {
            padding: 12px 16px 16px;
            display: flex;
            flex-direction: column;
            gap: 10px;
        }

        .menu-section-title {
            font-size: 12px;
            font-weight: 600;
            color: var(--text-secondary);
            letter-spacing: 0.8px;
            text-transform: uppercase;
        }

        .menu-range-label {
            display: flex;
            justify-content: space-between;
            align-items: center;
            font-size: 13px;
            color: var(--text-primary);
        }

        .menu-range-label span {
            font-weight: 600;
            color: var(--primary-color);
        }

        .menu-input-label {
            font-size: 13px;
            color: var(--text-primary);
        }

        .menu-number-input {
            width: 100%;
            padding: 6px 10px;
            border-radius: 6px;
            border: 1px solid var(--divider);
            background: var(--surface);
            color: var(--text-primary);
            font-size: 13px;
            transition: border-color 0.2s ease, box-shadow 0.2s ease;
            box-sizing: border-box;
        }

        .menu-number-input:focus {
            outline: none;
            border-color: var(--primary-color);
            box-shadow: 0 0 0 2px rgba(76,175,80,0.15);
        }

        .menu-hint {
            font-size: 12px;
            color: var(--text-secondary);
        }

        #menu-preload-slider {
            width: 100%;
            height: 6px;
            background: var(--divider);
            border-radius: 3px;
            -webkit-appearance: none;
            appearance: none;
            outline: none;
        }

        #menu-preload-slider::-webkit-slider-thumb {
            -webkit-appearance: none;
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: var(--primary-color);
            cursor: pointer;
            box-shadow: var(--shadow-1);
        }

        #menu-preload-slider::-moz-range-thumb {
            width: 16px;
            height: 16px;
            border-radius: 50%;
            background: var(--primary-color);
            cursor: pointer;
            border: none;
            box-shadow: var(--shadow-1);
        }

        .menu-cache-info {
            font-size: 12px;
            color: var(--text-secondary);
        }

        .menu-action-btn {
            align-self: flex-end;
            padding: 8px 12px;
            background: var(--surface);
            border: 1px solid var(--divider);
            border-radius: 6px;
            font-size: 12px;
            cursor: pointer;
            transition: all 0.2s ease;
            color: var(--text-primary);
            box-shadow: var(--shadow-1);
        }

        .menu-action-btn:hover {
            background: var(--primary-light);
            border-color: var(--primary-color);
            color: var(--text-primary);
            box-shadow: var(--shadow-2);
        }

        #data-transfer-overlay.data-transfer-overlay {
            position: fixed;
            inset: 0;
            background: rgba(0, 0, 0, 0.45);
            display: flex;
            align-items: center;
            justify-content: center;
            z-index: 100002;
            padding: 24px;
        }

        .data-transfer-dialog {
            background: var(--surface);
            color: var(--text-primary);
            box-shadow: var(--shadow-3);
            border-radius: 12px;
            width: min(520px, 100%);
            max-width: 90vw;
            display: flex;
            flex-direction: column;
            gap: 16px;
            padding: 20px 24px;
        }

        .data-transfer-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
        }

        .data-transfer-header h3 {
            margin: 0;
            font-size: 18px;
            font-weight: 600;
            color: var(--text-primary);
        }

        .data-transfer-close {
            border: none;
            background: transparent;
            color: var(--text-primary);
            width: 32px;
            height: 32px;
            border-radius: 50%;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            transition: background 0.2s ease, color 0.2s ease;
        }

        .data-transfer-close:hover {
            background: var(--divider);
            color: var(--primary-color);
        }

        .data-transfer-body {
            display: flex;
            flex-direction: column;
            gap: 8px;
        }

        #data-transfer-textarea {
            width: 100%;
            height: 220px;
            border: 1px solid var(--divider);
            border-radius: 8px;
            padding: 12px;
            resize: vertical;
            background: var(--background);
            color: var(--text-primary);
            font-family: Consolas, 'Courier New', monospace;
            font-size: 13px;
            line-height: 1.5;
            box-shadow: inset 0 1px 2px rgba(0,0,0,0.08);
            box-sizing: border-box;
        }

        #data-transfer-textarea:focus {
            outline: none;
            border-color: var(--primary-color);
            box-shadow: 0 0 0 2px rgba(76,175,80,0.2);
        }

        .data-transfer-desc {
            font-size: 12px;
            color: var(--text-secondary);
        }

        .data-transfer-footer {
            display: flex;
            justify-content: flex-end;
            gap: 10px;
        }

        .data-transfer-btn {
            min-width: 88px;
            padding: 8px 14px;
            border-radius: 8px;
            font-size: 13px;
            cursor: pointer;
            transition: all 0.2s ease;
        }

        .data-transfer-btn.primary {
            border: none;
            background: var(--primary-color);
            color: #ffffff;
            box-shadow: var(--shadow-1);
        }

        .data-transfer-btn.primary:hover {
            background: var(--primary-dark);
            box-shadow: var(--shadow-2);
        }

        .data-transfer-btn.secondary {
            border: 1px solid var(--divider);
            background: var(--surface);
            color: var(--text-primary);
        }

        .data-transfer-btn.secondary:hover {
            border-color: var(--primary-color);
            color: var(--primary-color);
        }

        ::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }

        ::-webkit-scrollbar-track {
            background: var(--background);
        }

        ::-webkit-scrollbar-thumb {
            background: var(--divider);
            border-radius: 4px;
        }

        ::-webkit-scrollbar-thumb:hover {
            background: var(--text-secondary);
        }

        @media (max-width: 1024px) {
            .reader-main {
                width: 100%;
            }
            .reader-sidebar {
                display: none;
            }
        }
    `);

    // =========================
    // 初始化
    // =========================
    function init() {
        const parser = new ContentParser();
        if (!parser.threadId) {
            return;
        }

        const dataStore = new DataStore();
        new ReaderUI(parser, dataStore);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }

})();